From 7ae9e4d2dfcd9dbd808e3e3fce443fcca63ad12b Mon Sep 17 00:00:00 2001 From: Milo Christiansen Date: Tue, 22 Nov 2016 14:29:33 -0500 Subject: [PATCH 1/6] Add "change-build-menu" and "if-entity". change-build-menu provides a simple API for adding or removing buildings from DF's build sidebar. if-entity runs a command if the current entity matches the given ID. --- modtools/change-build-menu.lua | 560 +++++++++++++++++++++++++++++++++ modtools/if-entity.lua | 52 +++ 2 files changed, 612 insertions(+) create mode 100644 modtools/change-build-menu.lua create mode 100644 modtools/if-entity.lua diff --git a/modtools/change-build-menu.lua b/modtools/change-build-menu.lua new file mode 100644 index 0000000000..2114f72f25 --- /dev/null +++ b/modtools/change-build-menu.lua @@ -0,0 +1,560 @@ +-- Edit the build mode sidebar menus. +-- +-- Based on my script for the Rubble addon "Libs/Change Build List". + +--[[ +Copyright 2016 Milo Christiansen + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim +that you wrote the original software. If you use this software in a product, an +acknowledgment in the product documentation would be appreciated but is not +required. + +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. +]] + +--@ module = true +--@ enable = true + +-- From here until I say otherwise the APIs are generic building manipulation functions from "Libs/Buildings" + +local utils = require "utils" + +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) + +-- GetWShopID returns a workshop or furnace's string ID based on it's numeric ID triplet. +-- This string ID *should* match what is expected by eventful for hardcoded buildings. +function GetWShopID(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 + +-- GetWShopIDs returns a workshop or furnace's ID numbers as a table. +-- The passed in ID should be the building's string identifier, it makes +-- no difference if it is a custom building or a hardcoded one. +-- The return table is structured like so: `{type, subtype, custom}` +function GetWShopType(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 + +-- OK, Now to "Libs/Change Build List" proper... + +--[[ + -- Examples: + + -- Remove the carpenters workshop. + ChangeBuilding("CARPENTERS", "WORKSHOPS", false) + + -- Make it impossible to build walls (not recommended!). + 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 category_name_to_id = { + ["MAIN_PAGE"] = 0, + ["SIEGE_ENGINES"] = 1, + ["TRAPS"] = 2, + ["WORKSHOPS"] = 3, + ["FURNACES"] = 4, + ["CONSTRUCTIONS"] = 5, + ["MACHINES"] = 6, + ["CONSTRUCTIONS_TRACK"] = 7, +} + +--[[ + { + 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 = stuffToChange or {} + +-- 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 = GetWShopType(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 entsrc = df.historical_entity.find(df.global.ui.civ_id) + if entsrc == nil then + return false + end + local entity = entsrc.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 = GetWShopType(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 = GetWShopType(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 + +-- Return early if module mode. Note that the ticker will not start in module mode! +-- To start the ticker you need to enable the script first ("enable change-build-menu"). +if moduleMode then return end + +function usage() + dfhack.print [==[ +Change the build sidebar menus. + +This script provides a flexible and comprehensive system for adding and removing +items from the build sidebar menus. You can add or remove workshops/furnaces by +text ID, or you can add/remove ANY building via a numeric building ID triplet. + +Changes made with this script do not survive a save/load. You will need to redo +your changes each time the world loads. + +Just to be clear: You CANNOT use this script AT ALL if there is no world +loaded! + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +Command Usage: + + change-build-menu start|enable + enable change-build-menu + Start the ticker. This needs to be done before any changes will take + effect. Note that you can make changes before or after starting the + ticker, both options should work equally well. + + change-build-menu stop|disable + disable change-build-menu + Stop the ticker. Does not clear stored changes. The ticker will + automatically stop when the current world is unloaded. + + change-build-menu add [] + Add the workshop or furnace with the ID to . + is an optional DF hotkey ID. + + may be one of: + MAIN_PAGE + SIEGE_ENGINES + TRAPS + WORKSHOPS + FURNACES + CONSTRUCTIONS + MACHINES + CONSTRUCTIONS_TRACK + + Valid values for hardcoded buildings are as follows: + CARPENTERS + FARMERS + MASONS + CRAFTSDWARFS + JEWELERS + METALSMITHSFORGE + MAGMAFORGE + BOWYERS + MECHANICS + SIEGE + BUTCHERS + LEATHERWORKS + TANNERS + CLOTHIERS + FISHERY + STILL + LOOM + QUERN + KENNELS + ASHERY + KITCHEN + DYERS + TOOL + MILLSTONE + WOOD_FURNACE + SMELTER + GLASS_FURNACE + MAGMA_SMELTER + MAGMA_GLASS_FURNACE + MAGMA_KILN + KILN + + change-build-menu remove + Remove the workshop or furnace with the ID from . + + and may have the same values as for the "add" option. + + change-build-menu revert + Revert an earlier remove or add operation. It is NOT safe to "remove" + and "add"ed building or vice versa, use this option to reverse any + changes you no longer want/need. + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +Module Usage: + + To use this script as a module put the following somewhere in your own script: + + local buildmenu = reqscript "change-build-menu" + + Then you can call the functions documented here like so: + + -- Example: Remove the carpenters workshop. + buildmenu.ChangeBuilding("CARPENTERS", "WORKSHOPS", false) + + Note that to allow any of your changes to take effect you need to start the + ticker. See the "Command Usage" section. + +Global Functions: + + function GetWShopID(btype, bsubtype, bcustom) + GetWShopID returns a workshop or furnace's string ID based on it's + numeric ID triplet. This string ID *should* match what is expected + by eventful for hardcoded buildings. + + function GetWShopType(id) + GetWShopIDs returns a workshop or furnace's ID numbers as a table. + The passed in ID should be the building's string identifier, it makes + no difference if it is a custom building or a hardcoded one. + The return table is structured like so: `{type, subtype, custom}` + + function IsEntityPermitted(id) + IsEntityPermitted 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. You do not need to + specify an entity, the current fortress race is used. + + ChangeBuilding(id, category, [add, [key]]) + ChangeBuildingAdv(typ, subtyp, custom, category, [add, [key]]) + These two functions apply changes to the build sidebar menus. If "add" + is true then the building is added to the specified category, else it + is removed. When adding you may specify "key", a string DF hotkey ID. + + The first version of this function takes a workshop or furnace ID as a + string, the second takes a numeric ID triplet (which can specify any + building, not just workshops or furnaces). + + function RevertBuildingChanges(id, category) + function RevertBuildingChangesAdv(typ, subtyp, custom, category) + These two functions revert changes made by "ChangeBuilding" and + "ChangeBuildingAdv". Like those two functions there are two versions, + a simple one that takes a string ID and one that takes a numeric ID + triplet. +]==] +end + +if not dfhack.isWorldLoaded() then + dfhack.color(RED) + print("change-build-menu: No World Loaded!\n") + dfhack.color(-1) + usage() + return +end + +args = {...} +if dfhack_flags and dfhack_flags.enable then + table.insert(args, dfhack_flags.enable_state and 'enable' or 'disable') +end + +tickerOn = tickerOn or false +tickerStart = false + +if #args >= 1 then + if args[1] == 'start' or args[1] == 'enable' then + if not tickerOn then tickerStart = true end + elseif args[1] == 'stop' or args[1] == 'disable' then + tickerOn = false + elseif args[1] == 'add' then + ChangeBuilding(args[2], args[3], true, args[4]) + return + elseif args[1] == 'remove' then + ChangeBuilding(args[2], args[3], false) + return + elseif args[1] == 'revert' then + RevertBuildingChanges(args[2], args[3]) + return + else + usage() + return + end +else + usage() + return +end + +-- These two store the values we *think* are in effect, they are used to detect changes. +sidebarLastCat = sidebarLastCat or -1 +sidebarIsBuild = sidebarIsBuild or false + +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 + +-- The logic for the ticker is much more complicated here than the Rubble version. +-- The Rubble version can lean on the script loader, this has to do all the work itself. +-- Greatly complicating things is that Rubble will only evaluate a module once, whereas a +-- command script may be evaluated many times. + +dfhack.onStateChange.ChangeBuildList = function(event) + if event == SC_WORLD_LOADED and not tickerOn and tickerStart then + -- Set ticker flags... + tickerOn = true + tickerStart = false + + -- Then start the ticker itself. + checkSidebar() + end + if event == SC_WORLD_UNLOADED and tickerOn then + -- This will kill the ticker next frame. + tickerOn = false + + -- The Rubble version doesn't need to worry about this stuff, but we do here... + sidebarLastCat = -1 + sidebarIsBuild = false + stuffToChange = nil + end +end +dfhack.onStateChange.ChangeBuildList(SC_WORLD_LOADED) diff --git a/modtools/if-entity.lua b/modtools/if-entity.lua new file mode 100644 index 0000000000..12bf7bed62 --- /dev/null +++ b/modtools/if-entity.lua @@ -0,0 +1,52 @@ +-- Run a command if the current entity matches a given ID. + +--[[ +Consider this public domain (CC0). + - Milo Christiansen +]] + +local usage = [=[ +Run a command if the current entity matches a given ID. + +To use this script effectively it needs to be called from "raw/onload.init". +Calling this from the main dfhack.init file will do nothing, as no world has +been loaded yet. + +Arguments: + -id + Specify the entity ID to match + -cmd [ commandStrs ] + Specify the command to be run if the current entity matches the entity + given via -id +All arguments are required. + +Example: + -- Print a message if you load an elf fort, but not a dwarf, human, etc + -- fort. + if-entity -id "FOREST" -cmd [ lua "print('Dirty hippies.')" ] +]=] + +local utils = require 'utils' + +validArgs = validArgs or utils.invert({ + 'help', + 'id', + 'cmd', +}) + +local args = utils.processArgs({...}, validArgs) + +if not args.id or not args.cmd or args.help then + dfhack.print(usage) + return +end + +local entsrc = df.historical_entity.find(df.global.ui.civ_id) +if entsrc == nil then + dfhack.printerr("Could not find current entity. No world loaded?") + return +end + +if entsrc.entity_raw.code == args.id then + dfhack.run_command(table.unpack(args.cmd)) +end From 30e22887311bdaddceca87606b703757a3fffda8 Mon Sep 17 00:00:00 2001 From: Milo Christiansen Date: Tue, 22 Nov 2016 15:00:47 -0500 Subject: [PATCH 2/6] Fix tabs vs spaces --- modtools/change-build-menu.lua | 610 ++++++++++++++++----------------- modtools/if-entity.lua | 18 +- 2 files changed, 314 insertions(+), 314 deletions(-) diff --git a/modtools/change-build-menu.lua b/modtools/change-build-menu.lua index 2114f72f25..e1a42b8efe 100644 --- a/modtools/change-build-menu.lua +++ b/modtools/change-build-menu.lua @@ -32,60 +32,60 @@ misrepresented as being the original software. local utils = require "utils" 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", + [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", + [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) -- GetWShopID returns a workshop or furnace's string ID based on it's numeric ID triplet. -- This string ID *should* match what is expected by eventful for hardcoded buildings. function GetWShopID(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 + 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 -- GetWShopIDs returns a workshop or furnace's ID numbers as a table. @@ -93,190 +93,190 @@ end -- no difference if it is a custom building or a hardcoded one. -- The return table is structured like so: `{type, subtype, custom}` function GetWShopType(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 + 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 -- OK, Now to "Libs/Change Build List" proper... --[[ - -- Examples: - - -- Remove the carpenters workshop. - ChangeBuilding("CARPENTERS", "WORKSHOPS", false) - - -- Make it impossible to build walls (not recommended!). - 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") + -- Examples: + + -- Remove the carpenters workshop. + ChangeBuilding("CARPENTERS", "WORKSHOPS", false) + + -- Make it impossible to build walls (not recommended!). + 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 category_name_to_id = { - ["MAIN_PAGE"] = 0, - ["SIEGE_ENGINES"] = 1, - ["TRAPS"] = 2, - ["WORKSHOPS"] = 3, - ["FURNACES"] = 4, - ["CONSTRUCTIONS"] = 5, - ["MACHINES"] = 6, - ["CONSTRUCTIONS_TRACK"] = 7, + ["MAIN_PAGE"] = 0, + ["SIEGE_ENGINES"] = 1, + ["TRAPS"] = 2, + ["WORKSHOPS"] = 3, + ["FURNACES"] = 4, + ["CONSTRUCTIONS"] = 5, + ["MACHINES"] = 6, + ["CONSTRUCTIONS_TRACK"] = 7, } --[[ - { - 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, - }, - } + { + 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 = stuffToChange or {} -- 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 = GetWShopType(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 entsrc = df.historical_entity.find(df.global.ui.civ_id) - if entsrc == nil then - return false - end - local entity = entsrc.entity_raw - - for _, bid in ipairs(entity.workshops.permitted_building_id) do - if wshop.custom == bid then - return true - end - end - return false + local wshop = GetWShopType(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 entsrc = df.historical_entity.find(df.global.ui.civ_id) + if entsrc == nil then + return false + end + local entity = entsrc.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 = GetWShopType(id) - if wshop == nil then - qerror("RevertBuildingChanges: Invalid workshop ID: "..id) - end - - RevertBuildingChangesAdv(wshop.type, wshop.subtype, wshop.custom, category) + local wshop = GetWShopType(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 + 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 = GetWShopType(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) + 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 = GetWShopType(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, - }, - }) + 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 -- Return early if module mode. Note that the ticker will not start in module mode! @@ -284,7 +284,7 @@ end if moduleMode then return end function usage() - dfhack.print [==[ + dfhack.print [==[ Change the build sidebar menus. This script provides a flexible and comprehensive system for adding and removing @@ -424,11 +424,11 @@ Global Functions: end if not dfhack.isWorldLoaded() then - dfhack.color(RED) - print("change-build-menu: No World Loaded!\n") - dfhack.color(-1) - usage() - return + dfhack.color(RED) + print("change-build-menu: No World Loaded!\n") + dfhack.color(-1) + usage() + return end args = {...} @@ -440,26 +440,26 @@ tickerOn = tickerOn or false tickerStart = false if #args >= 1 then - if args[1] == 'start' or args[1] == 'enable' then - if not tickerOn then tickerStart = true end - elseif args[1] == 'stop' or args[1] == 'disable' then - tickerOn = false - elseif args[1] == 'add' then - ChangeBuilding(args[2], args[3], true, args[4]) - return - elseif args[1] == 'remove' then - ChangeBuilding(args[2], args[3], false) - return - elseif args[1] == 'revert' then - RevertBuildingChanges(args[2], args[3]) - return - else - usage() - return - end + if args[1] == 'start' or args[1] == 'enable' then + if not tickerOn then tickerStart = true end + elseif args[1] == 'stop' or args[1] == 'disable' then + tickerOn = false + elseif args[1] == 'add' then + ChangeBuilding(args[2], args[3], true, args[4]) + return + elseif args[1] == 'remove' then + ChangeBuilding(args[2], args[3], false) + return + elseif args[1] == 'revert' then + RevertBuildingChanges(args[2], args[3]) + return + else + usage() + return + end else - usage() - return + usage() + return end -- These two store the values we *think* are in effect, they are used to detect changes. @@ -467,70 +467,70 @@ sidebarLastCat = sidebarLastCat or -1 sidebarIsBuild = sidebarIsBuild or false 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 + -- 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 -- The logic for the ticker is much more complicated here than the Rubble version. @@ -539,22 +539,22 @@ end -- command script may be evaluated many times. dfhack.onStateChange.ChangeBuildList = function(event) - if event == SC_WORLD_LOADED and not tickerOn and tickerStart then - -- Set ticker flags... - tickerOn = true - tickerStart = false - - -- Then start the ticker itself. - checkSidebar() - end - if event == SC_WORLD_UNLOADED and tickerOn then - -- This will kill the ticker next frame. - tickerOn = false - - -- The Rubble version doesn't need to worry about this stuff, but we do here... - sidebarLastCat = -1 - sidebarIsBuild = false - stuffToChange = nil - end + if event == SC_WORLD_LOADED and not tickerOn and tickerStart then + -- Set ticker flags... + tickerOn = true + tickerStart = false + + -- Then start the ticker itself. + checkSidebar() + end + if event == SC_WORLD_UNLOADED and tickerOn then + -- This will kill the ticker next frame. + tickerOn = false + + -- The Rubble version doesn't need to worry about this stuff, but we do here... + sidebarLastCat = -1 + sidebarIsBuild = false + stuffToChange = nil + end end dfhack.onStateChange.ChangeBuildList(SC_WORLD_LOADED) diff --git a/modtools/if-entity.lua b/modtools/if-entity.lua index 12bf7bed62..27ff3c5caa 100644 --- a/modtools/if-entity.lua +++ b/modtools/if-entity.lua @@ -2,7 +2,7 @@ --[[ Consider this public domain (CC0). - - Milo Christiansen + - Milo Christiansen ]] local usage = [=[ @@ -29,24 +29,24 @@ Example: local utils = require 'utils' validArgs = validArgs or utils.invert({ - 'help', - 'id', - 'cmd', + 'help', + 'id', + 'cmd', }) local args = utils.processArgs({...}, validArgs) if not args.id or not args.cmd or args.help then - dfhack.print(usage) - return + dfhack.print(usage) + return end local entsrc = df.historical_entity.find(df.global.ui.civ_id) if entsrc == nil then - dfhack.printerr("Could not find current entity. No world loaded?") - return + dfhack.printerr("Could not find current entity. No world loaded?") + return end if entsrc.entity_raw.code == args.id then - dfhack.run_command(table.unpack(args.cmd)) + dfhack.run_command(table.unpack(args.cmd)) end From 71ac7b3fb61aa218b28b8a19c8e6e5396e05967a Mon Sep 17 00:00:00 2001 From: Milo Christiansen Date: Tue, 22 Nov 2016 17:21:04 -0500 Subject: [PATCH 3/6] Fix print weirdness. I would say I was drunk, but I don't drink... --- modtools/change-build-menu.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modtools/change-build-menu.lua b/modtools/change-build-menu.lua index e1a42b8efe..072ce1245a 100644 --- a/modtools/change-build-menu.lua +++ b/modtools/change-build-menu.lua @@ -424,9 +424,7 @@ Global Functions: end if not dfhack.isWorldLoaded() then - dfhack.color(RED) - print("change-build-menu: No World Loaded!\n") - dfhack.color(-1) + dfhack.printerr("change-build-menu: No World Loaded!") usage() return end From f40526be3d21dcf3a29ad3c0a384dec470a61672 Mon Sep 17 00:00:00 2001 From: Milo Christiansen Date: Tue, 29 Nov 2016 12:40:16 -0500 Subject: [PATCH 4/6] Fix minor typo --- modtools/change-build-menu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modtools/change-build-menu.lua b/modtools/change-build-menu.lua index 072ce1245a..5a76b012b9 100644 --- a/modtools/change-build-menu.lua +++ b/modtools/change-build-menu.lua @@ -366,7 +366,7 @@ Command Usage: change-build-menu revert Revert an earlier remove or add operation. It is NOT safe to "remove" - and "add"ed building or vice versa, use this option to reverse any + an "add"ed building or vice versa, use this option to reverse any changes you no longer want/need. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * From 264920695794e8bda520f6bee06b36f0d2de2b18 Mon Sep 17 00:00:00 2001 From: Milo Christiansen Date: Fri, 9 Dec 2016 13:08:18 -0500 Subject: [PATCH 5/6] Upload latest version of master files This should fix all formatting nitpicks, as well as adding documentation. --- modtools/change-build-menu.lua | 1121 ++++++++++++++++---------------- modtools/if-entity.lua | 110 ++-- 2 files changed, 621 insertions(+), 610 deletions(-) diff --git a/modtools/change-build-menu.lua b/modtools/change-build-menu.lua index 5a76b012b9..a2a0d9c91a 100644 --- a/modtools/change-build-menu.lua +++ b/modtools/change-build-menu.lua @@ -1,558 +1,563 @@ --- Edit the build mode sidebar menus. --- --- Based on my script for the Rubble addon "Libs/Change Build List". - ---[[ -Copyright 2016 Milo Christiansen - -This software is provided 'as-is', without any express or implied warranty. In -no event will the authors be held liable for any damages arising from the use of -this software. - -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim -that you wrote the original software. If you use this software in a product, an -acknowledgment in the product documentation would be appreciated but is not -required. - -2. Altered source versions must be plainly marked as such, and must not be -misrepresented as being the original software. - -3. This notice may not be removed or altered from any source distribution. -]] - ---@ module = true ---@ enable = true - --- From here until I say otherwise the APIs are generic building manipulation functions from "Libs/Buildings" - -local utils = require "utils" - -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) - --- GetWShopID returns a workshop or furnace's string ID based on it's numeric ID triplet. --- This string ID *should* match what is expected by eventful for hardcoded buildings. -function GetWShopID(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 - --- GetWShopIDs returns a workshop or furnace's ID numbers as a table. --- The passed in ID should be the building's string identifier, it makes --- no difference if it is a custom building or a hardcoded one. --- The return table is structured like so: `{type, subtype, custom}` -function GetWShopType(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 - --- OK, Now to "Libs/Change Build List" proper... - ---[[ - -- Examples: - - -- Remove the carpenters workshop. - ChangeBuilding("CARPENTERS", "WORKSHOPS", false) - - -- Make it impossible to build walls (not recommended!). - 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 category_name_to_id = { - ["MAIN_PAGE"] = 0, - ["SIEGE_ENGINES"] = 1, - ["TRAPS"] = 2, - ["WORKSHOPS"] = 3, - ["FURNACES"] = 4, - ["CONSTRUCTIONS"] = 5, - ["MACHINES"] = 6, - ["CONSTRUCTIONS_TRACK"] = 7, -} - ---[[ - { - 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 = stuffToChange or {} - --- 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 = GetWShopType(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 entsrc = df.historical_entity.find(df.global.ui.civ_id) - if entsrc == nil then - return false - end - local entity = entsrc.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 = GetWShopType(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 = GetWShopType(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 - --- Return early if module mode. Note that the ticker will not start in module mode! --- To start the ticker you need to enable the script first ("enable change-build-menu"). -if moduleMode then return end - -function usage() - dfhack.print [==[ -Change the build sidebar menus. - -This script provides a flexible and comprehensive system for adding and removing -items from the build sidebar menus. You can add or remove workshops/furnaces by -text ID, or you can add/remove ANY building via a numeric building ID triplet. - -Changes made with this script do not survive a save/load. You will need to redo -your changes each time the world loads. - -Just to be clear: You CANNOT use this script AT ALL if there is no world -loaded! - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -Command Usage: - - change-build-menu start|enable - enable change-build-menu - Start the ticker. This needs to be done before any changes will take - effect. Note that you can make changes before or after starting the - ticker, both options should work equally well. - - change-build-menu stop|disable - disable change-build-menu - Stop the ticker. Does not clear stored changes. The ticker will - automatically stop when the current world is unloaded. - - change-build-menu add [] - Add the workshop or furnace with the ID to . - is an optional DF hotkey ID. - - may be one of: - MAIN_PAGE - SIEGE_ENGINES - TRAPS - WORKSHOPS - FURNACES - CONSTRUCTIONS - MACHINES - CONSTRUCTIONS_TRACK - - Valid values for hardcoded buildings are as follows: - CARPENTERS - FARMERS - MASONS - CRAFTSDWARFS - JEWELERS - METALSMITHSFORGE - MAGMAFORGE - BOWYERS - MECHANICS - SIEGE - BUTCHERS - LEATHERWORKS - TANNERS - CLOTHIERS - FISHERY - STILL - LOOM - QUERN - KENNELS - ASHERY - KITCHEN - DYERS - TOOL - MILLSTONE - WOOD_FURNACE - SMELTER - GLASS_FURNACE - MAGMA_SMELTER - MAGMA_GLASS_FURNACE - MAGMA_KILN - KILN - - change-build-menu remove - Remove the workshop or furnace with the ID from . - - and may have the same values as for the "add" option. - - change-build-menu revert - Revert an earlier remove or add operation. It is NOT safe to "remove" - an "add"ed building or vice versa, use this option to reverse any - changes you no longer want/need. - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -Module Usage: - - To use this script as a module put the following somewhere in your own script: - - local buildmenu = reqscript "change-build-menu" - - Then you can call the functions documented here like so: - - -- Example: Remove the carpenters workshop. - buildmenu.ChangeBuilding("CARPENTERS", "WORKSHOPS", false) - - Note that to allow any of your changes to take effect you need to start the - ticker. See the "Command Usage" section. - -Global Functions: - - function GetWShopID(btype, bsubtype, bcustom) - GetWShopID returns a workshop or furnace's string ID based on it's - numeric ID triplet. This string ID *should* match what is expected - by eventful for hardcoded buildings. - - function GetWShopType(id) - GetWShopIDs returns a workshop or furnace's ID numbers as a table. - The passed in ID should be the building's string identifier, it makes - no difference if it is a custom building or a hardcoded one. - The return table is structured like so: `{type, subtype, custom}` - - function IsEntityPermitted(id) - IsEntityPermitted 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. You do not need to - specify an entity, the current fortress race is used. - - ChangeBuilding(id, category, [add, [key]]) - ChangeBuildingAdv(typ, subtyp, custom, category, [add, [key]]) - These two functions apply changes to the build sidebar menus. If "add" - is true then the building is added to the specified category, else it - is removed. When adding you may specify "key", a string DF hotkey ID. - - The first version of this function takes a workshop or furnace ID as a - string, the second takes a numeric ID triplet (which can specify any - building, not just workshops or furnaces). - - function RevertBuildingChanges(id, category) - function RevertBuildingChangesAdv(typ, subtyp, custom, category) - These two functions revert changes made by "ChangeBuilding" and - "ChangeBuildingAdv". Like those two functions there are two versions, - a simple one that takes a string ID and one that takes a numeric ID - triplet. -]==] -end - -if not dfhack.isWorldLoaded() then - dfhack.printerr("change-build-menu: No World Loaded!") - usage() - return -end - -args = {...} -if dfhack_flags and dfhack_flags.enable then - table.insert(args, dfhack_flags.enable_state and 'enable' or 'disable') -end - -tickerOn = tickerOn or false -tickerStart = false - -if #args >= 1 then - if args[1] == 'start' or args[1] == 'enable' then - if not tickerOn then tickerStart = true end - elseif args[1] == 'stop' or args[1] == 'disable' then - tickerOn = false - elseif args[1] == 'add' then - ChangeBuilding(args[2], args[3], true, args[4]) - return - elseif args[1] == 'remove' then - ChangeBuilding(args[2], args[3], false) - return - elseif args[1] == 'revert' then - RevertBuildingChanges(args[2], args[3]) - return - else - usage() - return - end -else - usage() - return -end - --- These two store the values we *think* are in effect, they are used to detect changes. -sidebarLastCat = sidebarLastCat or -1 -sidebarIsBuild = sidebarIsBuild or false - -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 - --- The logic for the ticker is much more complicated here than the Rubble version. --- The Rubble version can lean on the script loader, this has to do all the work itself. --- Greatly complicating things is that Rubble will only evaluate a module once, whereas a --- command script may be evaluated many times. - -dfhack.onStateChange.ChangeBuildList = function(event) - if event == SC_WORLD_LOADED and not tickerOn and tickerStart then - -- Set ticker flags... - tickerOn = true - tickerStart = false - - -- Then start the ticker itself. - checkSidebar() - end - if event == SC_WORLD_UNLOADED and tickerOn then - -- This will kill the ticker next frame. - tickerOn = false - - -- The Rubble version doesn't need to worry about this stuff, but we do here... - sidebarLastCat = -1 - sidebarIsBuild = false - stuffToChange = nil - end -end -dfhack.onStateChange.ChangeBuildList(SC_WORLD_LOADED) +-- Edit the build mode sidebar menus. +-- +-- Based on my script for the Rubble addon "Libs/Change Build List". + +--[[ +Copyright 2016 Milo Christiansen + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim +that you wrote the original software. If you use this software in a product, an +acknowledgment in the product documentation would be appreciated but is not +required. + +2. Altered source versions must be plainly marked as such, and must not be +misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. +]] + +--@ module = true +--@ enable = true + +local usage = [====[ +modtools/change-build-menu +========================== + +Change the build sidebar menus. + +This script provides a flexible and comprehensive system for adding and removing +items from the build sidebar menus. You can add or remove workshops/furnaces by +text ID, or you can add/remove ANY building via a numeric building ID triplet. + +Changes made with this script do not survive a save/load. You will need to redo +your changes each time the world loads. + +Just to be clear: You CANNOT use this script AT ALL if there is no world +loaded! + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +Usage: + +modtools/change-build-menu start|enable +enable modtools/change-build-menu + Start the ticker. This needs to be done before any changes will take + effect. Note that you can make changes before or after starting the + ticker, both options should work equally well. + +modtools/change-build-menu stop|disable +disable modtools/change-build-menu + Stop the ticker. Does not clear stored changes. The ticker will + automatically stop when the current world is unloaded. + +modtools/change-build-menu add [] + Add the workshop or furnace with the ID to . + is an optional DF hotkey ID. + + may be one of: + MAIN_PAGE + SIEGE_ENGINES + TRAPS + WORKSHOPS + FURNACES + CONSTRUCTIONS + MACHINES + CONSTRUCTIONS_TRACK + + Valid values for hardcoded buildings are as follows: + CARPENTERS + FARMERS + MASONS + CRAFTSDWARFS + JEWELERS + METALSMITHSFORGE + MAGMAFORGE + BOWYERS + MECHANICS + SIEGE + BUTCHERS + LEATHERWORKS + TANNERS + CLOTHIERS + FISHERY + STILL + LOOM + QUERN + KENNELS + ASHERY + KITCHEN + DYERS + TOOL + MILLSTONE + WOOD_FURNACE + SMELTER + GLASS_FURNACE + MAGMA_SMELTER + MAGMA_GLASS_FURNACE + MAGMA_KILN + KILN + +modtools/change-build-menu remove + Remove the workshop or furnace with the ID from . + + and may have the same values as for the "add" option. + +modtools/change-build-menu revert + Revert an earlier remove or add operation. It is NOT safe to "remove" + an "add"ed building or vice versa, use this option to reverse any + changes you no longer want/need. + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +Module Usage: + +To use this script as a module put the following somewhere in your own script: + + local buildmenu = reqscript "change-build-menu" + +Then you can call the functions documented here like so: + + -- Example: Remove the carpenters workshop. + buildmenu.ChangeBuilding("CARPENTERS", "WORKSHOPS", false) + + -- Make it impossible to build walls (not recommended!). + local typ, styp = df.building_type.Construction, df.construction_type.Wall + buildmenu.ChangeBuildingAdv(typ, styp, -1, "CONSTRUCTIONS", false) + +Note that to allow any of your changes to take effect you need to start the +ticker. See the "Command Usage" section. + +Global Functions: + +function GetWShopID(btype, bsubtype, bcustom) + GetWShopID returns a workshop's or furnace's string ID based on its + numeric ID triplet. This string ID *should* match what is expected + by eventful for hardcoded buildings. + +function GetWShopType(id) + GetWShopIDs returns a workshop or furnace's ID numbers as a table. + The passed in ID should be the building's string identifier, it makes + no difference if it is a custom building or a hardcoded one. + The return table is structured like so: `{type, subtype, custom}` + +function IsEntityPermitted(id) + IsEntityPermitted 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. You do not need to + specify an entity, the current fortress race is used. + +ChangeBuilding(id, category, [add, [key]]) +ChangeBuildingAdv(typ, subtyp, custom, category, [add, [key]]) + These two functions apply changes to the build sidebar menus. If "add" + is true then the building is added to the specified category, else it + is removed. When adding you may specify "key", a string DF hotkey ID. + + The first version of this function takes a workshop or furnace ID as a + string, the second takes a numeric ID triplet (which can specify any + building, not just workshops or furnaces). + +function RevertBuildingChanges(id, category) +function RevertBuildingChangesAdv(typ, subtyp, custom, category) + These two functions revert changes made by "ChangeBuilding" and + "ChangeBuildingAdv". Like those two functions there are two versions, + a simple one that takes a string ID and one that takes a numeric ID + triplet. +]====] + +-- From here until I say otherwise the APIs are generic building manipulation functions from "Libs/Buildings" + +local utils = require "utils" + +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) + +-- GetWShopID returns a workshop or furnace's string ID based on it's numeric ID triplet. +-- This string ID *should* match what is expected by eventful for hardcoded buildings. +function GetWShopID(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 + +-- GetWShopIDs returns a workshop or furnace's ID numbers as a table. +-- The passed in ID should be the building's string identifier, it makes +-- no difference if it is a custom building or a hardcoded one. +-- The return table is structured like so: `{type, subtype, custom}` +function GetWShopType(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 + +-- OK, Now to "Libs/Change Build List" proper... + +--[[ + -- Examples: + + -- Remove the carpenters workshop. + ChangeBuilding("CARPENTERS", "WORKSHOPS", false) + + -- Make it impossible to build walls (not recommended!). + 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 category_name_to_id = { + ["MAIN_PAGE"] = 0, + ["SIEGE_ENGINES"] = 1, + ["TRAPS"] = 2, + ["WORKSHOPS"] = 3, + ["FURNACES"] = 4, + ["CONSTRUCTIONS"] = 5, + ["MACHINES"] = 6, + ["CONSTRUCTIONS_TRACK"] = 7, +} + +--[[ + { + 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 = stuffToChange or {} + +-- 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 = GetWShopType(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 entsrc = df.historical_entity.find(df.global.ui.civ_id) + if entsrc == nil then + return false + end + local entity = entsrc.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 = GetWShopType(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 = GetWShopType(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 + +-- Return early if module mode. Note that the ticker will not start in module mode! +-- To start the ticker you need to enable the script first ("enable modtools/change-build-menu"). +if moduleMode then return end + +if not dfhack.isWorldLoaded() then + dfhack.printerr("change-build-menu: No World Loaded!") + dfhack.print(usage) + return +end + +args = {...} +if dfhack_flags and dfhack_flags.enable then + table.insert(args, dfhack_flags.enable_state and 'enable' or 'disable') +end + +tickerOn = tickerOn or false +tickerStart = false + +if #args >= 1 then + if args[1] == 'start' or args[1] == 'enable' then + if not tickerOn then tickerStart = true end + elseif args[1] == 'stop' or args[1] == 'disable' then + tickerOn = false + elseif args[1] == 'add' then + ChangeBuilding(args[2], args[3], true, args[4]) + return + elseif args[1] == 'remove' then + ChangeBuilding(args[2], args[3], false) + return + elseif args[1] == 'revert' then + RevertBuildingChanges(args[2], args[3]) + return + else + dfhack.print(usage) + return + end +else + dfhack.print(usage) + return +end + +-- These two store the values we *think* are in effect, they are used to detect changes. +sidebarLastCat = sidebarLastCat or -1 +sidebarIsBuild = sidebarIsBuild or false + +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 + +-- The logic for the ticker is much more complicated here than the Rubble version. +-- The Rubble version can lean on the script loader, this has to do all the work itself. +-- Greatly complicating things is that Rubble will only evaluate a module once, whereas a +-- command script may be evaluated many times. + +dfhack.onStateChange.ChangeBuildList = function(event) + if event == SC_WORLD_LOADED and not tickerOn and tickerStart then + -- Set ticker flags... + tickerOn = true + tickerStart = false + + -- Then start the ticker itself. + checkSidebar() + end + if event == SC_WORLD_UNLOADED and tickerOn then + -- This will kill the ticker next frame. + tickerOn = false + + -- The Rubble version doesn't need to worry about this stuff, but we do here... + sidebarLastCat = -1 + sidebarIsBuild = false + stuffToChange = nil + end +end +dfhack.onStateChange.ChangeBuildList(SC_WORLD_LOADED) diff --git a/modtools/if-entity.lua b/modtools/if-entity.lua index 27ff3c5caa..8c8a9566c4 100644 --- a/modtools/if-entity.lua +++ b/modtools/if-entity.lua @@ -1,52 +1,58 @@ --- Run a command if the current entity matches a given ID. - ---[[ -Consider this public domain (CC0). - - Milo Christiansen -]] - -local usage = [=[ -Run a command if the current entity matches a given ID. - -To use this script effectively it needs to be called from "raw/onload.init". -Calling this from the main dfhack.init file will do nothing, as no world has -been loaded yet. - -Arguments: - -id - Specify the entity ID to match - -cmd [ commandStrs ] - Specify the command to be run if the current entity matches the entity - given via -id -All arguments are required. - -Example: - -- Print a message if you load an elf fort, but not a dwarf, human, etc - -- fort. - if-entity -id "FOREST" -cmd [ lua "print('Dirty hippies.')" ] -]=] - -local utils = require 'utils' - -validArgs = validArgs or utils.invert({ - 'help', - 'id', - 'cmd', -}) - -local args = utils.processArgs({...}, validArgs) - -if not args.id or not args.cmd or args.help then - dfhack.print(usage) - return -end - -local entsrc = df.historical_entity.find(df.global.ui.civ_id) -if entsrc == nil then - dfhack.printerr("Could not find current entity. No world loaded?") - return -end - -if entsrc.entity_raw.code == args.id then - dfhack.run_command(table.unpack(args.cmd)) -end +-- Run a command if the current entity matches a given ID. + +--[[ +Consider this public domain (CC0). + - Milo Christiansen +]] + +local usage = [====[ +modtools/if-entity +================== + +Run a command if the current entity matches a given ID. + +To use this script effectively it needs to be called from "raw/onload.init". +Calling this from the main dfhack.init file will do nothing, as no world has +been loaded yet. + +Usage: + +-id + Specify the entity ID to match +-cmd [ commandStrs ] + Specify the command to be run if the current entity matches the entity + given via -id + +All arguments are required. + +Example: + + # Print a message if you load an elf fort, but not a dwarf, human, etc + # fort. + if-entity -id "FOREST" -cmd [ lua "print('Dirty hippies.')" ] +]====] + +local utils = require 'utils' + +validArgs = validArgs or utils.invert({ + 'help', + 'id', + 'cmd', +}) + +local args = utils.processArgs({...}, validArgs) + +if not args.id or not args.cmd or args.help then + dfhack.print(usage) + return +end + +local entsrc = df.historical_entity.find(df.global.ui.civ_id) +if entsrc == nil then + dfhack.printerr("Could not find current entity. No world loaded?") + return +end + +if entsrc.entity_raw.code == args.id then + dfhack.run_command(table.unpack(args.cmd)) +end From e4b23ca3ca700613c0fc2265a628cde4ea8a00bf Mon Sep 17 00:00:00 2001 From: Milo Christiansen Date: Fri, 9 Dec 2016 13:10:56 -0500 Subject: [PATCH 6/6] Fixed a nit I missed --- modtools/change-build-menu.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modtools/change-build-menu.lua b/modtools/change-build-menu.lua index a2a0d9c91a..8144a13bfe 100644 --- a/modtools/change-build-menu.lua +++ b/modtools/change-build-menu.lua @@ -128,8 +128,8 @@ Then you can call the functions documented here like so: -- Example: Remove the carpenters workshop. buildmenu.ChangeBuilding("CARPENTERS", "WORKSHOPS", false) - -- Make it impossible to build walls (not recommended!). - local typ, styp = df.building_type.Construction, df.construction_type.Wall + -- Make it impossible to build walls (not recommended!). + local typ, styp = df.building_type.Construction, df.construction_type.Wall buildmenu.ChangeBuildingAdv(typ, styp, -1, "CONSTRUCTIONS", false) Note that to allow any of your changes to take effect you need to start the