Skip to content

Commit

Permalink
first draft of item bulk forbid/dump/melt tool
Browse files Browse the repository at this point in the history
  • Loading branch information
chdoc committed Nov 17, 2023
1 parent 020580f commit c55fd23
Show file tree
Hide file tree
Showing 2 changed files with 352 additions and 0 deletions.
58 changes: 58 additions & 0 deletions docs/item.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
item
====

.. dfhack-tool::
:summary: Perform bulk operations on items based on various properties.
:tags: fort productivity items

Filter items in you fort by various properties (e.g., item type, material,
wear-level, quality, ...), and perform the bulk operations forbid, dump, melt,
and their inverses. By default, the tool does not act on artifacts.

Usage
-----

``item [ help | count | [un]forbid | [un]dump | [un]melt ] <filter option>``

Options
-------

``-h, --help``
Print help.

``-r, --reachable``
Only include items reachable by one of your citizens.

``-u, --unreachable``
Only include items not reachable by one of your citizens.

``-t, --type <string>``
Filter by item type (e.g., BOULDER, CORPSE, ...). Also accepts lower case
spelling (e.g. "corpse")

``-m, --material <string>``
Filter by material the item is made out of (e.g., "iron").

``-d, --description <string>``
Filter by item description (singular form without stack sizes). Example:
"cave spider silk web". Note: This does not work for animal products such as
wool, because their description always includes the stack size.

``-a, --include-artifacts``
Include artifacts in the item list.

``--min-wear <integer>``
Only include items whose wear/damage level is at least ``integer``. Useful
values are 0 (pristine) to 3 (XX).

``--min-wear <integer>``
Only include items whose wear/damage level is at most ``integer``. Useful
values are 0 (pristine) to 3 (XX).

``--min-quality <integer>``
Only include items whose quality level is at least ``integer``. Useful
values are 0 (standard) to 5 (masterwork).

``--min-quality <integer>``
Only include items whose quality level is at most ``integer``. Useful
values are 0 (standard) to 5 (masterwork).
294 changes: 294 additions & 0 deletions item.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
-----------------------------------------------------------
-- helper functions that should probably be moved elsewhere
-----------------------------------------------------------

-- check whether an item is inside a burrow
local function containsItem(burrow,item)
local res = false
local x,y,z = dfhack.items.getPosition(item)
if x then
res = dfhack.burrows.isAssignedTile(burrow, xyz2pos(x,y,z))
end
return res
end

-- check whether the item is reachable by one of the citizens
local function isReachable(item)
local reachable = false
local x, y, z = dfhack.items.getPosition(item)
if x then -- item has a valid position
local citizens = dfhack.units.getCitizens(true)
for _, unit in pairs(citizens) do
if dfhack.maps.canWalkBetween(unit.pos, xyz2pos(x, y, z)) then
reachable = true
break
end
end
end
return reachable
end

-- fast reachability test for items that requires precomputed walkability groups for
-- all citizens. Returns false for items w/o valid position (e.g., items in inventories).
--- @param item item
--- @param wgroups table<integer,boolean>
--- @return boolean
local function fastReachable(item,wgroups)
local x, y, z = dfhack.items.getPosition(item)
if x then -- item has a valid position
local igroup = dfhack.maps.getWalkableGroup(xyz2pos(x, y, z))
if wgroups[igroup] then
return true
else
return false
end
else
return false
end
end

local function citizenWalkabilityGroups()
local cgroups = {}
for _, unit in pairs(dfhack.units.getCitizens(true)) do
local wgroup = dfhack.maps.getWalkableGroup(unit.pos)
cgroups[wgroup] = true
end
cgroups[0] = false -- exclude unwarkable tiles
return cgroups
end

-- see: https://discord.com/channels/793331351645323264/807443277132595211/1173979892036141137
--- @param item item
local function isMeltable(item)
local type = item:getType()
local matinfo = dfhack.matinfo.decode(item)
return
matinfo:matches{metal=true} and
df.item_type[type] ~= 'CORPSE' and
df.item_type[type] ~= 'CORPSEPIECE' and
df.item_type[type] ~= 'REMAINS' and
df.item_type[type] ~= 'FISH' and
df.item_type[type] ~= 'FISH_RAW' and
df.item_type[type] ~= 'VERMIN' and
df.item_type[type] ~= 'PET' and
df.item_type[type] ~= 'FOOD' and
df.item_type[type] ~= 'EGG'
end

local function addCondition(tab, cond)
if cond then
table.insert(tab, cond)
end
end
-----------------------------------------------------------------------
-- external API: helpers to assemble filters and `item.act` to execute.
-----------------------------------------------------------------------
item = {}

--- @param tab table<any,fun(item:item):boolean>
function item.condition_burrow(tab, burrow, outside)
if outside then
addCondition(
tab,
function (item) return not containsItem(burrow,item) end
)
else
addCondition(
tab,
function (item) return containsItem(burrow,item) end
)
end
end

function item.condition_type(tab, type)
addCondition(
tab,
function(item)
return df.item_type[item:getType()] == string.upper(type)
end
)
end

function item.condition_reachable(tab)
local cgroups = citizenWalkabilityGroups()
addCondition(tab, function (item) return fastReachable(item,cgroups) end)
end

function item.condition_unreachable(tab)
local cgroups = citizenWalkabilityGroups()
addCondition(tab, function (item) return not fastReachable(item,cgroups) end)
end

-- uses the singular form without stack size (i.e., prickle berry)
-- does not work for corpse pieces like "wool"
function item.condition_description(tab, desc)
addCondition(
tab,
function(item) return dfhack.items.getDescription(item, 1) == desc end
)
end

function item.condition_material(tab, material)
addCondition(
tab,
function(item) return dfhack.matinfo.decode(item):toString() == material end
)
end

function item.condition_wear(tab, level, upper)
if upper then
addCondition(tab, function(item) return item.wear <= level end)
else
addCondition(tab, function(item) return item.wear >= level end)
end
end

function item.condition_quality(tab, levelst, upper)
local level = argparse.nonnegativeInt(levelst, 'wear')
if upper then
addCondition(tab, function(item) return item.quality <= level end)
else
addCondition(tab, function(item) return item.quality >= level end)
end
end

function item.act(action,conditons,options)
local count = 0
local changed = 0
local invisible = 0
for _, item in pairs(df.global.world.items.other.IN_PLAY) do
-- never act on items used for constructions/building materials and carried by hostiles
-- also skip artifacts, unless explicitly told to include them
if item.flags.construction or
item.flags.in_building or
item.flags.hostile or
(item.flags.artifact and not options.artifact)
then
goto skipitem
end

-- check conditions provided via options
for _, condition in pairs(conditons) do
if not condition(item) then goto skipitem end
end

-- only try to melt things that are actually meltable
-- TOTHINK: treated as implicit filter, should we count the item?
if action == 'melt' and not isMeltable(item) then
goto skipitem
end

-- item matches the filters
count = count + 1

-- skip items that are in unrevealed parts of the map
local x,y,z = dfhack.items.getPosition(item)
if x and not dfhack.maps.isTileVisible(x,y,z) then
invisible = invisible+1
goto skipitem
end

-- carry out the action
if action == 'forbid' and not item.flags.forbid then
item.flags.forbid = true
changed = changed + 1
elseif action == 'unforbid' and item.flags.forbid then
item.flags.forbid = false
changed = changed + 1
elseif action == 'dump' and not item.flags.dump then
item.flags.dump = true
changed = changed + 1
elseif action == 'undump' and item.flags.dump then
item.flags.dump = false
changed = changed + 1
elseif action == 'melt' and not item.flags.melt then
item.flags.melt = true
changed = changed + 1
elseif action == 'unmelt' and item.flags.melt then
item.flags.melt = false
changed = changed + 1
end
:: skipitem ::
end
print(count, 'items matched the filter options')
print(invisible, 'invisible items were skipped')
print(changed, 'items were acted upon')
end

-----------------------------------------------------------------------
-- script action: check for arguments and main action and run item.act
-----------------------------------------------------------------------
local argparse = require('argparse')

local options, args = {
help = false,
artifact = false
}, { ... }

local conditions = {}

local positionals = argparse.processArgsGetopt(args, {
{ 'h', 'help', handler = function() options.help = true end },
{ 'a', 'include-artifacts', handler = function() options.artifact = true end },
{ 'i', 'inside', hasArg = true,
handler = function (name)
local burrow = dfhack.burrows.findByName(name)
if burrow then item.condition_burrow(conditions, burrow, false)
else qerror('burrow '..name..' not found') end
end
},
{ 'o', 'outside', hasArg = true,
handler = function (name)
local burrow = dfhack.burrows.findByName(name)
if burrow then item.condition_burrow(conditions, burrow, true)
else qerror('burrow '..name..' not found') end
end
},
{ 'r', 'reachable',
handler = function () item.condition_reachable(conditions) end },
{ 'u', 'unreachable',
handler = function () item.condition_unreachable(conditions) end },
{ 't', 'type', hasArg = true,
handler = function (type) item.condition_type(conditions, type) end },
{ 'd', 'description', hasArg = true,
handler = function (desc) item.condition_description(conditions, desc) end },
{ 'm', 'material', hasArg = true,
handler = function (material) item.condition_material(conditions, material) end },
{ nil, 'min-wear', hasArg = true,
handler = function(levelst)
local level = argparse.nonnegativeInt(levelst, 'min-wear')
item.condition_wear(conditions, level, false) end },
{ nil, 'max-wear', hasArg = true,
handler = function(levelst)
local level = argparse.nonnegativeInt(levelst, 'max-wear')
item.condition_wear(conditions, level, true) end },
{ nil, 'min-quality', hasArg = true,
handler = function(levelst)
local level = argparse.nonnegativeInt(levelst, 'min-quality')
item.condition_quality(conditions, level, false) end },
{ nil, 'max-quality', hasArg = true,
handler = function(levelst)
local level = argparse.nonnegativeInt(levelst, 'max-quality')
item.condition_quality(conditions, level, true) end }
})

local action = nil

if options.help or positionals[1] == 'help' then
-- print(dfhack.script_help())
print("HELP!")
return
elseif positionals[1] == 'forbid' then action = 'forbid'
elseif positionals[1] == 'unforbid' then action = 'unforbid'
elseif positionals[1] == 'dump' then action = 'dump'
elseif positionals[1] == 'undump' then action = 'undump'
elseif positionals[1] == 'melt' then action = 'melt'
elseif positionals[1] == 'unmelt' then action = 'unmelt'
elseif positionals[1] == 'count' then action = 'count'
end

if not action then
qerror('main action not recognized')
else
item.act(action,conditions,options)
end

0 comments on commit c55fd23

Please sign in to comment.