diff --git a/LUA_API.rst b/LUA_API.rst
index bb1ef803e..c42785278 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -971,6 +971,80 @@ Low-level building creation functions;
More high-level functions are implemented in lua and can be loaded by
``require('dfhack.buildings')``. See ``hack/lua/dfhack/buildings.lua``.
+Among them are:
+
+* ``dfhack.buildings.getFiltersByType(argtable,type,subtype,custom)``
+
+ Returns a sequence of lua structures, describing input item filters
+ suitable for the specified building type, or *nil* if unknown or invalid.
+ The returned sequence is suitable for use as the ``job_items`` argument
+ of ``constructWithFilters``.
+ Uses tables defined in ``buildings.lua``.
+
+ Argtable members ``material`` (the default name), ``bucket``, ``barrel``,
+ ``chain``, ``mechanism``, ``screw``, ``pipe``, ``anvil``, ``weapon`` are used to
+ augment the basic attributes with more detailed information if the
+ building has input items with the matching name (see the tables for naming details).
+ Note that it is impossible to *override* any properties this way, only supply those that
+ are not mentioned otherwise; one exception is that flags2.non_economic
+ is automatically cleared if an explicit material is specified.
+
+* ``dfhack.buildings.constructBuilding{...}``
+
+ Creates a building in one call, using options contained
+ in the argument table. Returns the building, or *nil, error*.
+
+ **NOTE:** Despite the name, unless the building is abstract,
+ the function creates it in an 'unconstructed' stage, with
+ a queued in-game job that will actually construct it. I.e.
+ the function replicates programmatically what can be done
+ through the construct building menu in the game ui, except
+ that it does less environment constraint checking.
+
+ The following options can be used:
+
+ - ``pos = coordinates``, or ``x = ..., y = ..., z = ...``
+
+ Mandatory. Specifies the left upper corner of the building.
+
+ - ``type = df.building_type.FOO, subtype = ..., custom = ...``
+
+ Mandatory. Specifies the type of the building. Obviously, subtype
+ and custom are only expected if the type requires them.
+
+ - ``fields = { ... }``
+
+ Initializes fields of the building object after creation with ``df.assign``.
+
+ - ``width = ..., height = ..., direction = ...``
+
+ Sets size and orientation of the building. If it is
+ fixed-size, specified dimensions are ignored.
+
+ - ``full_rectangle = true``
+
+ For buildings like stockpiles or farm plots that can normally
+ accomodate individual tile exclusion, forces an error if any
+ tiles within the specified width*height are obstructed.
+
+ - ``items = { item, item ... }``, or ``filters = { {...}, {...}... }``
+
+ Specifies explicit items or item filters to use in construction.
+ It is the job of the user to ensure they are correct for the building type.
+
+ - ``abstract = true``
+
+ Specifies that the building is abstract and does not require construction.
+ Required for stockpiles and civzones; an error otherwise.
+
+ - ``material = {...}, mechanism = {...}, ...``
+
+ If none of ``items``, ``filter``, or ``abstract`` is used,
+ the function uses ``getFiltersByType`` to compute the input
+ item filters, and passes the argument table through. If no filters
+ can be determined this way, ``constructBuilding`` throws an error.
+
+
Constructions module
--------------------
diff --git a/Lua API.html b/Lua API.html
index 3e023ebd3..ed2e9f6a9 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -1163,6 +1163,69 @@ Returns true if the building was destroyed and deallocated immediately.
diff --git a/library/lua/dfhack/buildings.lua b/library/lua/dfhack/buildings.lua
index eb0a6a5b5..ad82da89a 100644
--- a/library/lua/dfhack/buildings.lua
+++ b/library/lua/dfhack/buildings.lua
@@ -4,7 +4,32 @@ local buildings = dfhack.buildings
local utils = require 'utils'
---[[ Building input material tables. ]]
+-- Uninteresting values for filter attributes when reading them from DF memory.
+-- Differs from the actual defaults of the job_item constructor in allow_artifact.
+
+buildings.input_filter_defaults = {
+ item_type = -1,
+ item_subtype = -1,
+ mat_type = -1,
+ mat_index = -1,
+ flags1 = {},
+ -- Instead of noting those that allow artifacts, mark those that forbid them.
+ -- Leaves actually enabling artifacts to the discretion of the API user,
+ -- which is the right thing because unlike the game UI these filters are
+ -- used in a way that does not give the user a chance to choose manually.
+ flags2 = { allow_artifact = true },
+ flags3 = {},
+ flags4 = 0,
+ flags5 = 0,
+ reaction_class = '',
+ has_material_reaction_product = '',
+ metal_ore = -1,
+ min_dimension = -1,
+ has_tool_use = -1,
+ quantity = 1
+}
+
+--[[ Building input material table. ]]
local building_inputs = {
[df.building_type.Chair] = { { item_type=df.item_type.CHAIR, vector_id=df.job_item_vector_id.CHAIR } },
@@ -12,7 +37,6 @@ local building_inputs = {
[df.building_type.Table] = { { item_type=df.item_type.TABLE, vector_id=df.job_item_vector_id.TABLE } },
[df.building_type.Coffin] = { { item_type=df.item_type.COFFIN, vector_id=df.job_item_vector_id.COFFIN } },
[df.building_type.FarmPlot] = { },
- [df.building_type.Furnace] = { { flags2={ building_material=true, fire_safe=true, non_economic=true } } },
[df.building_type.TradeDepot] = { { flags2={ building_material=true, non_economic=true }, quantity=3 } },
[df.building_type.Door] = { { item_type=df.item_type.DOOR, vector_id=df.job_item_vector_id.DOOR } },
[df.building_type.Floodgate] = {
@@ -40,7 +64,6 @@ local building_inputs = {
vector_id=df.job_item_vector_id.ARMORSTAND
}
},
- [df.building_type.Workshop] = { { flags2={ building_material=true, non_economic=true } } },
[df.building_type.Cabinet] = {
{ item_type=df.item_type.CABINET, vector_id=df.job_item_vector_id.CABINET }
},
@@ -89,7 +112,7 @@ local building_inputs = {
[df.building_type.ArcheryTarget] = { { flags2={ building_material=true, non_economic=true } } },
[df.building_type.Chain] = { { item_type=df.item_type.CHAIN, vector_id=df.job_item_vector_id.CHAIN } },
[df.building_type.Cage] = { { item_type=df.item_type.CAGE, vector_id=df.job_item_vector_id.CAGE } },
- [df.building_type.Weapon] = { { vector_id=df.job_item_vector_id.ANY_SPIKE } },
+ [df.building_type.Weapon] = { { name='weapon', vector_id=df.job_item_vector_id.ANY_SPIKE } },
[df.building_type.ScrewPump] = {
{
item_type=df.item_type.BLOCKS,
@@ -158,6 +181,8 @@ local building_inputs = {
[df.building_type.Hive] = { { has_tool_use=df.tool_uses.HIVE, item_type=df.item_type.TOOL } }
}
+--[[ Furnace building input material table. ]]
+
local furnace_inputs = {
[df.furnace_type.WoodFurnace] = { { flags2={ building_material=true, fire_safe=true, non_economic=true } } },
[df.furnace_type.Smelter] = { { flags2={ building_material=true, fire_safe=true, non_economic=true } } },
@@ -168,6 +193,8 @@ local furnace_inputs = {
[df.furnace_type.MagmaKiln] = { { flags2={ building_material=true, magma_safe=true, non_economic=true } } }
}
+--[[ Workshop building input material table. ]]
+
local workshop_inputs = {
[df.workshop_type.Carpenters] = { { flags2={ building_material=true, non_economic=true } } },
[df.workshop_type.Farmers] = { { flags2={ building_material=true, non_economic=true } } },
@@ -250,6 +277,8 @@ local workshop_inputs = {
}
}
+--[[ Trap building input material table. ]]
+
local trap_inputs = {
[df.trap_type.StoneFallTrap] = {
{
@@ -264,7 +293,10 @@ local trap_inputs = {
item_type=df.item_type.TRAPPARTS,
vector_id=df.job_item_vector_id.TRAPPARTS
},
- { vector_id=df.job_item_vector_id.ANY_WEAPON }
+ {
+ name='weapon',
+ vector_id=df.job_item_vector_id.ANY_WEAPON
+ }
},
[df.trap_type.Lever] = {
{
@@ -289,11 +321,28 @@ local trap_inputs = {
}
}
+--[[ Functions for lookup in tables. ]]
+
+local function get_custom_inputs(custom)
+ local defn = df.building_def.find(custom)
+ if defn ~= nil then
+ return utils.clone_with_default(defn.build_items, buildings.input_filter_defaults)
+ end
+end
+
local function get_inputs_by_type(type,subtype,custom)
if type == df.building_type.Workshop then
- return workshop_inputs[subtype]
+ if subtype == df.workshop_type.Custom then
+ return get_custom_inputs(custom)
+ else
+ return workshop_inputs[subtype]
+ end
elseif type == df.building_type.Furnace then
- return furnace_inputs[subtype]
+ if subtype == df.furnace_type.Custom then
+ return get_custom_inputs(custom)
+ else
+ return furnace_inputs[subtype]
+ end
elseif type == df.building_type.Trap then
return trap_inputs[subtype]
else
@@ -357,7 +406,7 @@ end
-- Materials:
items = { item, item ... },
-- OR
- filter = { { ... }, { ... }... }
+ filters = { { ... }, { ... }... }
-- OR
abstract = true
-- OR
diff --git a/library/lua/utils.lua b/library/lua/utils.lua
index 96ab2b190..e67801f4f 100644
--- a/library/lua/utils.lua
+++ b/library/lua/utils.lua
@@ -221,6 +221,68 @@ function clone(obj,deep)
end
end
+local function get_default(default,key,base)
+ if type(default) == 'table' then
+ local dv = default[key]
+ if dv == nil then
+ dv = default._default
+ end
+ if dv == nil then
+ dv = base
+ end
+ return dv
+ else
+ return default
+ end
+end
+
+-- Copy the object as lua data structures, skipping values matching defaults.
+function clone_with_default(obj,default,force)
+ local rv = nil
+ local function setrv(k,v)
+ if v ~= nil then
+ if rv == nil then
+ rv = {}
+ end
+ rv[k] = v
+ end
+ end
+ if default == nil then
+ return nil
+ elseif type(obj) == 'table' then
+ for k,v in pairs(obj) do
+ setrv(k, clone_with_default(v, get_default(default,k)))
+ end
+ elseif df.isvalid(obj) == 'ref' then
+ local kind = obj._kind
+ if kind == 'primitive' then
+ return clone_with_default(obj.value,default,force)
+ elseif kind == 'bitfield' then
+ for k,v in pairs(obj) do
+ setrv(k, clone_with_default(v, get_default(default,k,false)))
+ end
+ elseif kind == 'container' then
+ for k,v in ipairs(obj) do
+ setrv(k+1, clone_with_default(v, default, true))
+ end
+ else -- struct
+ for k,v in pairs(obj) do
+ setrv(k, clone_with_default(v, get_default(default,k)))
+ end
+ end
+ elseif obj == default and not force then
+ return nil
+ elseif obj == nil then
+ return NULL
+ else
+ return obj
+ end
+ if force and rv == nil then
+ rv = {}
+ end
+ return rv
+end
+
-- Sort a vector or lua table
function sort_vector(vector,field,cmp)
local fcmp = compare_field(field,cmp)
diff --git a/scripts/devel/list-filters.lua b/scripts/devel/list-filters.lua
index 69fb57164..87b9f24b1 100644
--- a/scripts/devel/list-filters.lua
+++ b/scripts/devel/list-filters.lua
@@ -1,43 +1,29 @@
-- List input items for the building currently being built.
+--
+-- This is where the filters in lua/dfhack/buildings.lua come from.
local dumper = require 'dumper'
+local utils = require 'utils'
+local buildings = require 'dfhack.buildings'
-local function copy_flags(tgt,src,name)
- local val = src[name]
- if val.whole == 0 then
- return
- end
- local out = {}
- tgt[name] = out
- for k,v in pairs(val) do
- if v then
- out[k] = v
- end
- end
-end
-
-local function copy_value(tgt,src,name,defval)
- if src[name] ~= defval then
- tgt[name] = src[name]
- end
-end
-local function copy_enum(tgt,src,name,defval,ename,enum)
- if src[name] ~= defval then
- tgt[name] = ename..'.'..enum[src[name]]
+local function name_enum(tgt,name,ename,enum)
+ if tgt[name] ~= nil then
+ tgt[name] = ename..'.'..enum[tgt[name]]
end
end
local lookup = {}
+local items = df.global.world.items
for i=df.job_item_vector_id._first_item,df.job_item_vector_id._last_item do
local id = df.job_item_vector_id.attrs[i].other
local ptr
if id == df.items_other_id.ANY then
- ptr = df.global.world.items.all
+ ptr = items.all
elseif id == df.items_other_id.BAD then
- ptr = df.global.world.items.bad
+ ptr = items.bad
else
- ptr = df.global.world.items.other[id]
+ ptr = items.other[id]
end
if ptr then
local _,addr = df.sizeof(ptr)
@@ -46,23 +32,12 @@ for i=df.job_item_vector_id._first_item,df.job_item_vector_id._last_item do
end
local function clone_filter(src,quantity)
- local tgt = {}
- src.flags2.allow_artifact = false
+ local tgt = utils.clone_with_default(src, buildings.input_filter_defaults, true)
if quantity ~= 1 then
tgt.quantity = quantity
end
- copy_enum(tgt, src, 'item_type', -1, 'df.item_type', df.item_type)
- copy_value(tgt, src, 'item_subtype', -1)
- copy_value(tgt, src, 'mat_type', -1)
- copy_value(tgt, src, 'mat_index', -1)
- copy_flags(tgt, src, 'flags1')
- copy_flags(tgt, src, 'flags2')
- copy_flags(tgt, src, 'flags3')
- copy_value(tgt, src, 'reaction_class', '')
- copy_value(tgt, src, 'has_material_reaction_product', '')
- copy_value(tgt, src, 'metal_ore', -1)
- copy_value(tgt, src, 'min_dimension', -1)
- copy_enum(tgt, src, 'has_tool_use', -1, 'df.tool_uses', df.tool_uses)
+ name_enum(tgt, 'item_type', 'df.item_type', df.item_type)
+ name_enum(tgt, 'has_tool_use', 'df.tool_uses', df.tool_uses)
local ptr = src.item_vector
if ptr and ptr ~= df.global.world.items.other[0] then
local _,addr = df.sizeof(ptr)
@@ -78,8 +53,9 @@ local function dump(name)
end
local fmt = dumper.DataDumper(out,name,false,1,4)
- local out = string.gsub(fmt, '"', '')
- print(out)
+ fmt = string.gsub(fmt, '"(df%.[^"]+)"','%1')
+ fmt = string.gsub(fmt, '%s+$', '')
+ print(fmt)
end
local itype = df.global.ui_build_selector.building_type