Bay 12 Games Forum

Please login or register.

Login with username, password and session length
Advanced search  
Pages: [1] 2 3

Author Topic: Disabling Hard coded buildings: An actual working method!  (Read 9772 times)

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Disabling Hard coded buildings: An actual working method!
« on: May 16, 2015, 11:58:40 am »

I was messing around with adding hardcoded jobs (for example "collect sand") to user workshops, and I got to thinking, "if I can modify this menu, what other menus can I change?"

The result is the following Lua module.

This module allows you to add or remove buildings from the hardcoded buildings menu (what you get when you press "b").

To use simple place the following in a file named "libs_dfhack_change_build_list.mod.lua" in "raw/dfhack", install the Rubble script loader, then put `local build_list = rubble.require "libs_dfhack_change_build_list"` at the top of your scripts and call the functions from there.

The script has a few examples in comments, enjoy!

Code: [Select]
local _ENV = rubble.mkmodule("libs_dfhack_change_build_list")

--[[
-- Examples:

-- Remove the carpenters workshop.
ChangeBuilding("CARPENTERS", "WORKSHOPS", false)

-- Make it impossible to build walls.
ChangeBuildingAdv(df.building_type.Construction, df.construction_type.Wall, -1, "CONSTRUCTIONS", false)

-- Add the mechanic's workshops to the machines category.
ChangeBuilding("MECHANICS", "MACHINES", true, "CUSTOM_E")
]]

local utils = require "utils"

local category_id_to_name = {
[0] = "MAIN_PAGE",
[1] = "SIEGE_ENGINES",
[2] = "TRAPS",
[3] = "WORKSHOPS",
[4] = "FURNACES",
[5] = "CONSTRUCTIONS",
[6] = "MACHINES",
[7] = "CONSTRUCTIONS_TRACK",
}
local category_name_to_id = utils.invert(category_id_to_name)

local wshop_type_to_id = {
[df.workshop_type.Carpenters] = "CARPENTERS",
[df.workshop_type.Farmers] = "FARMERS",
[df.workshop_type.Masons] = "MASONS",
[df.workshop_type.Craftsdwarfs] = "CRAFTSDWARFS",
[df.workshop_type.Jewelers] = "JEWELERS",
[df.workshop_type.MetalsmithsForge] = "METALSMITHSFORGE",
[df.workshop_type.MagmaForge] = "MAGMAFORGE",
[df.workshop_type.Bowyers] = "BOWYERS",
[df.workshop_type.Mechanics] = "MECHANICS",
[df.workshop_type.Siege] = "SIEGE",
[df.workshop_type.Butchers] = "BUTCHERS",
[df.workshop_type.Leatherworks] = "LEATHERWORKS",
[df.workshop_type.Tanners] = "TANNERS",
[df.workshop_type.Clothiers] = "CLOTHIERS",
[df.workshop_type.Fishery] = "FISHERY",
[df.workshop_type.Still] = "STILL",
[df.workshop_type.Loom] = "LOOM",
[df.workshop_type.Quern] = "QUERN",
[df.workshop_type.Kennels] = "KENNELS",
[df.workshop_type.Ashery] = "ASHERY",
[df.workshop_type.Kitchen] = "KITCHEN",
[df.workshop_type.Dyers] = "DYERS",
[df.workshop_type.Tool] = "TOOL",
[df.workshop_type.Millstone] = "MILLSTONE",
}
local wshop_id_to_type = utils.invert(wshop_type_to_id)

local furnace_type_to_id = {
[df.furnace_type.WoodFurnace] = "WOOD_FURNACE",
[df.furnace_type.Smelter] = "SMELTER",
[df.furnace_type.GlassFurnace] = "GLASS_FURNACE",
[df.furnace_type.MagmaSmelter] = "MAGMA_SMELTER",
[df.furnace_type.MagmaGlassFurnace] = "MAGMA_GLASS_FURNACE",
[df.furnace_type.MagmaKiln] = "MAGMA_KILN",
[df.furnace_type.Kiln] = "KILN",
}
local furnace_id_to_type = utils.invert(furnace_type_to_id)

-- Not used.
local function getWShopName(btype, bsubtype, bcustom)
if btype == df.building_type.Workshop then
if wshop_type_to_id[bsubtype] ~= nil then
return wshop_type_to_id[bsubtype]
else
return df.building_def_workshopst.find(bcustom).code
end
elseif btype == df.building_type.Furnace then
if furnace_type_to_id[bsubtype] ~= nil then
return furnace_type_to_id[bsubtype]
else
return df.building_def_furnacest.find(bcustom).code
end
end
end

local function getWShopIDs(id)
if wshop_id_to_type[id] ~= nil then
-- Hardcoded workshop
return {
type = df.building_type.Workshop,
subtype = wshop_id_to_type[id],
custom = -1,
}
elseif furnace_id_to_type[id] ~= nil then
-- Hardcoded furnace
return {
type = df.building_type.Furnace,
subtype = furnace_id_to_type[id],
custom = -1,
}
else
-- User defined workshop or furnace.
for i, def in ipairs(df.global.world.raws.buildings.all) do
if def.code == id then
local typ = df.building_type.Furnace
local styp = df.furnace_type.Custom
if getmetatable(def) == "building_def_workshopst" then
typ = df.building_type.Workshop
styp = df.workshop_type.Custom
end

return {
type = typ,
subtype = styp,
custom = i,
}
end
end
end
return nil
end

--[[
{
category = 0, -- The menu category id (from category_name_to_id)
add = true, -- Are we adding a workshop or removing one?
id = {
-- The building IDs.
type = 0,
subtype = 0,
custom = 0,
},
}
]]
stuffToChange = {}

-- Returns true if DF would normally allow you to build a workshop or furnace.
-- Use this if you want to change a building, but only if it is permitted in the current entity.
function IsEntityPermitted(id)
local wshop = getWShopIDs(id)

-- It's hard coded, so yes, of course it's permitted, why did you even ask?
if wshop.custom == -1 then
return true
end

local entity = df.historical_entity.find(df.global.ui.civ_id).entity_raw

for _, bid in ipairs(entity.workshops.permitted_building_id) do
if wshop.custom == bid then
return true
end
end
return false
end

function RevertBuildingChanges(id, category)
local wshop = getWShopIDs(id)
if wshop == nil then
qerror("RevertBuildingChanges: Invalid workshop ID: "..id)
end

RevertBuildingChangesAdv(wshop.type, wshop.subtype, wshop.custom, category)
end

function RevertBuildingChangesAdv(typ, subtyp, custom, category)
local cat
if tonumber(category) ~= nil then
cat = tonumber(category)
else
cat = category_name_to_id[category]
if cat == nil then
qerror("ChangeBuilding: Invalid category ID: "..category)
end
end

for i = #stuffToChange, 1, -1 do
local change = stuffToChange[i]
if change.category == cat then
if typ == change.id.type and subtyp == change.id.subtype and custom == change.id.custom then
table.remove(stuffToChange, i)
end
end
end
end

function ChangeBuilding(id, category, add, key)
local cat
if tonumber(category) ~= nil then
cat = tonumber(category)
else
cat = category_name_to_id[category]
if cat == nil then
qerror("ChangeBuilding: Invalid category ID: "..category)
end
end

local wshop = getWShopIDs(id)
if wshop == nil then
qerror("ChangeBuilding: Invalid workshop ID: "..id)
end

if tonumber(key) == nil then
key = df.interface_key[key]
end
if key == nil then
key = 0
end

ChangeBuildingAdv(wshop.type, wshop.subtype, wshop.custom, category, add, key)
end

function ChangeBuildingAdv(typ, subtyp, custom, category, add, key)
local cat
if tonumber(category) ~= nil then
cat = tonumber(category)
else
cat = category_name_to_id[category]
if cat == nil then
qerror("ChangeBuilding: Invalid category ID: "..category)
end
end

if tonumber(key) == nil then
key = df.interface_key[key]
end
if key == nil then
key = 0
end

table.insert(stuffToChange, {
category = cat,
add = add,
key = key,
id = {
type = typ,
subtype = subtyp,
custom = custom,
},
})
end

-- These two store the values we *think* are in effect, they are used to detect changes.
sidebarLastCat = -1
sidebarIsBuild = false
tickerOn = true

local function checkSidebar()
-- Needs to be "frames" so it ticks over while paused.
if tickerOn then
dfhack.timeout(1, "frames", checkSidebar)
end

local sidebar = df.global.ui_sidebar_menus.building

if not sidebarIsBuild and df.global.ui.main.mode ~= df.ui_sidebar_mode.Build then
-- Not in build mode.
return
elseif sidebarIsBuild and df.global.ui.main.mode ~= df.ui_sidebar_mode.Build then
-- Just exited build mode
sidebarIsBuild = false
sidebarLastCat = -1
return
elseif sidebarIsBuild and sidebar.category_id == sidebarLastCat then
-- In build mode, but category has not changed since last frame.
return
end
-- Either we just entered build mode or the category has changed.
sidebarIsBuild = true
sidebarLastCat = sidebar.category_id

-- Changes made here do not persist, they need to be made every time the side bar is shown.
-- Will just deleting stuff cause a memory leak? (probably, but how can it be avoided?)

local stufftoremove = {}
local stufftoadd = {}
for i, btn in ipairs(sidebar.choices_all) do
if getmetatable(btn) == "interface_button_construction_building_selectorst" then
for _, change in ipairs(stuffToChange) do
if not change.add and sidebar.category_id == change.category and
btn.building_type == change.id.type and btn.building_subtype == change.id.subtype and
btn.custom_type == change.id.custom then
table.insert(stufftoremove, i)
end
end
end
end
for _, change in ipairs(stuffToChange) do
if sidebar.category_id == change.category and change.add then
table.insert(stufftoadd, change)
end
end

-- Do the actual adding and removing.
-- We need to iterate the list backwards to keep from invalidating the stored indexes.
for x = #stufftoremove, 1, -1 do
-- AFAIK item indexes always match (except for one extra item at the end of "choices_all").
local i = stufftoremove[x]
sidebar.choices_visible:erase(i)
sidebar.choices_all:erase(i)
end
for _, change in ipairs(stufftoadd) do
local button = df.interface_button_construction_building_selectorst:new()
button.hotkey_id = change.key
button.building_type = change.id.type
button.building_subtype = change.id.subtype
button.custom_type = change.id.custom

local last = #sidebar.choices_visible
sidebar.choices_visible:insert(last, button)
sidebar.choices_all:insert(last, button)
end
end
checkSidebar()

function onUnload()
-- This will kill the ticker in the frame after this returns.
-- No need to clear the change table, as it (and all the other parts of this script) will go to
-- the land of the garbage collector shortly after this returns (I love garbage collection).
tickerOn = false
end

return _ENV
« Last Edit: July 22, 2015, 09:51:56 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

Deon

  • Bay Watcher
  • 💀 💀 💀 💀 💀
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #1 on: May 16, 2015, 02:01:15 pm »

THat's amazing. That's exactly what we need for total conversions. Just one small question: do you mean we need to put this lua script in a separate script and then call it from dfhack.init? I ask about:
Quote
To use simple place the following in a file named "change_build_list.lua" in "hack/lua", then put `local build_list = require "change_build_list"` at the top of your scripts and call the functions from there.
Logged
▬(ஜ۩۞۩ஜ)▬
✫ DF Wanderer ✫ - the adventure mode crafting and tweaks
✫ Cartographer's Lounge ✫ - a custom worldgen repository

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #2 on: May 16, 2015, 02:13:40 pm »

The script provided is a library, it provides functions to other scripts via the built-in Lua function "require". To use it you place the provided script in the "hack/lua" directory as instructed, then put something like the following in a different script and call that script from onload.init or whatever.

Code: [Select]
local build_list = require "change_build_list"

build_list.ChangeBuilding("CARPENTERS", "WORKSHOPS", false)

Note that the provided script originally used the Rubble pseudo module system, wich provides some things that are unsupported by the default Lua modules (such as reload support). As-is the script will probably have issues when you save a fort, then load a different one. Should be an easy fix, let's see...

Edit: Damn, I can't find the code I need, basically it should have a block that triggers when the fort is unloaded and sets `stuffToChange = {}` so that one world's settings won't clobber another world's stuff.

Edit2: Found it. Reload support for the non-Rubble version is now working (or at least it should be).
« Last Edit: May 16, 2015, 02:23:55 pm 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

Deon

  • Bay Watcher
  • 💀 💀 💀 💀 💀
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #3 on: May 16, 2015, 03:12:57 pm »

Thanks, I will test it today and hopefully return full of happiness.
Logged
▬(ஜ۩۞۩ஜ)▬
✫ DF Wanderer ✫ - the adventure mode crafting and tweaks
✫ Cartographer's Lounge ✫ - a custom worldgen repository

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #4 on: May 16, 2015, 03:27:51 pm »

Be careful what you disable! You could end up with lots of failed moods unless you turn them off :)

BTW: In case you missed it, this allows adding user workshops to other (non-workshop or furnace) categories. For example my powered workshop Rubble addon adds the input an output "workshops" to the machines list.
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

Meph

  • Bay Watcher
    • View Profile
    • worldbicyclist
Re: Disabling Hard coded buildings: An actual working method!
« Reply #5 on: May 17, 2015, 04:10:05 am »

Be careful what you disable! You could end up with lots of failed moods unless you turn them off :)

BTW: In case you missed it, this allows adding user workshops to other (non-workshop or furnace) categories. For example my powered workshop Rubble addon adds the input an output "workshops" to the machines list.
Neat :)

If warmists dragon-fire engines ever get properly released, we can add them to the siege-engine then. :D
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 :::

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #6 on: May 29, 2015, 10:17:34 am »

The script I had posted was lacking a few bug fixes that the Rubble version had (I forgot to sync the copies) so it didn't work.

I fixed all known bugs now, so if it still won't work let me know. (sorry)
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

Slogo

  • Bay Watcher
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #7 on: June 02, 2015, 01:16:15 pm »

Can this be used to disable things like bridge, levers and/or traps as well? If so this is pretty huge in terms of creating a 'balanced' vanilla experience.

I guess a little more specifically, could I say disable specifically cage traps?

Could this same technique be applied to other things like disabling the ability to lock doors?
« Last Edit: June 02, 2015, 01:37:58 pm by Slogo »
Logged

Enemy post

  • Bay Watcher
  • Modder/GM
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #8 on: June 02, 2015, 09:01:45 pm »


Great job, I look forward to seeing what modders do with this. Thanks.
« Last Edit: June 02, 2015, 09:05:59 pm by Enemy post »
Logged
My mods and forum games.
Enemy post has claimed the title of Dragonsong the Harmonic of Melodious Exaltion!

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #9 on: June 03, 2015, 03:16:29 pm »

Can this be used to disable things like bridge, levers and/or traps as well? If so this is pretty huge in terms of creating a 'balanced' vanilla experience.

I guess a little more specifically, could I say disable specifically cage traps?

Yes!

Could this same technique be applied to other things like disabling the ability to lock doors?

I don't know, but probably. It would require different code, but the method would be similar, either replace a vanilla viewscreen with a custom one or edit the vanilla one when it is shown. Better to ask the author of the track stop tweak plugin (the one that allows you to edit the dump direction and friction of a track stop after it has been built).
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

Dirst

  • Bay Watcher
  • [EASILY_DISTRA
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #10 on: June 03, 2015, 03:25:13 pm »

Hey milo, impressive stuff as usual.  Is it possible to add a top-level menu choice by messing with the initialization code?  I have a bunch of custom workshops that I'll be stuffing under the temples menu in the next version, but would love to be able to group them now.
Logged
Just got back, updating:
(0.42 & 0.43) The Earth Strikes Back! v2.15 - Pay attention...  It's a mine!  It's-a not yours!
(0.42 & 0.43) Appearance Tweaks v1.03 - Tease those hippies about their pointy ears.
(0.42 & 0.43) Accessibility Utility v1.04 - Console tools to navigate the map

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #11 on: June 03, 2015, 03:32:52 pm »

I don't know, certainly not without editing the code. It is probably not possible. I don't know of any way to inject a new menu, but I haven't looked either.
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

Roses

  • Bay Watcher
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #12 on: June 03, 2015, 10:44:08 pm »

So just because I was curious, I went through to check out all the categories, here is the list I found

Siege Engines
Workshops
Furnaces
Traps/Levers
Machine Components
Wall/Floor/Stairs/Track

Out of curiousity, can you add custom buildings directly to the uilding list? Like how kennels are now?
Logged

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Disabling Hard coded buildings: An actual working method!
« Reply #13 on: June 04, 2015, 09:04:08 am »

Yes!

Just use category ID "MAIN_PAGE". See the script for a list of category IDs.
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: Disabling Hard coded buildings: An actual working method!
« Reply #14 on: June 20, 2015, 03:20:14 pm »

I have made some improvements to the Rubble version of this, so if you want the latest (with bugs fixed) version you will have to look there. I really need to make an updated non-Rubble version...
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
Pages: [1] 2 3