-
Notifications
You must be signed in to change notification settings - Fork 199
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
first draft of
item
bulk forbid/dump/melt tool
- Loading branch information
Showing
2 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |