-
Notifications
You must be signed in to change notification settings - Fork 218
Fix state inconsistency in uniform-unstick #1490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
cdf3ebb
Fix uniform assignment state inconsistency caused by uniform-unstick
fb3f2d1
Refactor item description logging in uniform-unstick
0831e6e
Clean up/improve code readability in uniform-unstick
5b4072e
Merge branch 'master' into uniform-unstick-fix
ab9rf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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 |
---|---|---|
|
@@ -15,11 +15,15 @@ local validArgs = utils.invert({ | |
|
||
-- Functions | ||
|
||
-- @param item df.item | ||
-- @return string | ||
local function item_description(item) | ||
return dfhack.df2console(dfhack.items.getDescription(item, 0, true)) | ||
return "item #" .. item.id .. " '" .. dfhack.df2console(dfhack.items.getDescription(item, 0, true)) .. "'" | ||
end | ||
|
||
local function get_item_pos(item) | ||
-- @param item df.item | ||
-- @return df.coord|nil | ||
local function get_visible_item_pos(item) | ||
local x, y, z = dfhack.items.getPosition(item) | ||
if not x or not y or not z then | ||
return | ||
|
@@ -30,24 +34,30 @@ local function get_item_pos(item) | |
end | ||
end | ||
|
||
local function get_squad_position(unit, unit_name) | ||
-- @param unit df.unit | ||
-- @return df.squad_position|nil | ||
local function get_squad_position(unit) | ||
local squad = df.squad.find(unit.military.squad_id) | ||
if squad then | ||
if squad.entity_id ~= df.global.plotinfo.group_id then | ||
print("WARNING: Unit " .. unit_name .. " is a member of a squad from another site!" .. | ||
" This may be preventing them from doing any useful work." .. | ||
" You can fix this by assigning them to a local squad and then unassigning them.") | ||
print() | ||
return | ||
end | ||
else | ||
if not squad then | ||
return | ||
end | ||
|
||
if squad.entity_id ~= df.global.plotinfo.group_id then | ||
print("WARNING: Unit " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. " is a member of a squad from another site!" .. | ||
" This may be preventing them from doing any useful work." .. | ||
" You can fix this by assigning them to a local squad and then unassigning them.") | ||
print() | ||
return | ||
end | ||
|
||
if #squad.positions > unit.military.squad_position then | ||
return squad.positions[unit.military.squad_position] | ||
end | ||
end | ||
|
||
-- @param unit df.unit | ||
-- @param item df.item | ||
-- @return number[] list of body part ids | ||
local function bodyparts_that_can_wear(unit, item) | ||
local bodyparts = {} | ||
local unitparts = dfhack.units.getCasteRaw(unit).body_info.body_parts | ||
|
@@ -89,47 +99,61 @@ local function bodyparts_that_can_wear(unit, item) | |
return bodyparts | ||
end | ||
|
||
-- returns new value of need_newline | ||
local function print_line(text, need_newline) | ||
if need_newline then | ||
print() | ||
end | ||
print(text) | ||
return false | ||
-- @param unit_name string | ||
-- @param labor_name string | ||
local function print_bad_labor(unit_name, labor_name) | ||
return print("WARNING: Unit " .. unit_name .. " has the " .. labor_name .. | ||
" labor enabled, which conflicts with military uniforms.") | ||
end | ||
|
||
local function print_bad_labor(unit_name, labor_name, need_newline) | ||
return print_line("WARNING: Unit " .. unit_name .. " has the " .. labor_name .. | ||
" labor enabled, which conflicts with military uniforms.", need_newline) | ||
-- @param squad_position df.squad_position | ||
-- @param item_id number | ||
local function remove_item_from_position(squad_position, item_id) | ||
for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do | ||
for _, uniform_spec in ipairs(uniform_slot_specs) do | ||
for idx, assigned_item_id in ipairs(uniform_spec.assigned) do | ||
if assigned_item_id == item_id then | ||
uniform_spec.assigned:erase(idx) | ||
return | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
-- Will figure out which items need to be moved to the floor, returns an item_id:item map | ||
local function process(unit, args, need_newline) | ||
local function process(unit, args) | ||
local silent = args.all -- Don't print details if we're iterating through all dwarves | ||
local unit_name = dfhack.df2console(dfhack.units.getReadableName(unit)) | ||
local printed = false | ||
|
||
if not silent then | ||
need_newline = print_line("Processing unit " .. unit_name, need_newline) | ||
print("Processing unit " .. unit_name) | ||
printed = true | ||
end | ||
|
||
-- The return value | ||
local to_drop = {} -- item id to item object | ||
|
||
-- First get squad position for an early-out for non-military dwarves | ||
local squad_position = get_squad_position(unit, unit_name) | ||
local squad_position = get_squad_position(unit) | ||
if not squad_position then | ||
if not silent then | ||
need_newline = print_line(unit_name .. " does not have a military uniform.", need_newline) | ||
print(unit_name .. " does not have a military uniform.") | ||
print() | ||
end | ||
return | ||
end | ||
|
||
if unit.status.labors.MINE then | ||
need_newline = print_bad_labor(unit_name, "mining", need_newline) | ||
print_bad_labor(unit_name, "mining") | ||
printed = true | ||
elseif unit.status.labors.CUTWOOD then | ||
need_newline = print_bad_labor(unit_name, "woodcutting", need_newline) | ||
print_bad_labor(unit_name, "woodcutting") | ||
printed = true | ||
elseif unit.status.labors.HUNT then | ||
need_newline = print_bad_labor(unit_name, "hunting", need_newline) | ||
print_bad_labor(unit_name, "hunting") | ||
printed = true | ||
end | ||
|
||
-- Find all worn items which may be at issue. | ||
|
@@ -148,12 +172,12 @@ local function process(unit, args, need_newline) | |
end | ||
|
||
-- Now get info about which items have been assigned as part of the uniform | ||
local assigned_items = {} -- assigned item ids mapped to item objects | ||
for _, specs in ipairs(squad_position.equipment.uniform) do | ||
for _, spec in ipairs(specs) do | ||
for _, assigned in ipairs(spec.assigned) do | ||
local uniform_assigned_items = {} -- assigned item ids mapped to item objects | ||
for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do | ||
for _, uniform_spec in ipairs(uniform_slot_specs) do | ||
for _, assigned_item_id in ipairs(uniform_spec.assigned) do | ||
-- Include weapon and shield so we can avoid dropping them, or pull them out of container/inventory later | ||
assigned_items[assigned] = df.item.find(assigned) | ||
uniform_assigned_items[assigned_item_id] = df.item.find(assigned_item_id) | ||
end | ||
end | ||
end | ||
|
@@ -163,36 +187,49 @@ local function process(unit, args, need_newline) | |
|
||
local present_ids = {} -- map of item ID to item object | ||
local missing_ids = {} -- map of item ID to item object | ||
for u_id, item in pairs(assigned_items) do | ||
if not worn_items[u_id] then | ||
for item_id, item in pairs(uniform_assigned_items) do | ||
if not worn_items[item_id] then | ||
if not silent then | ||
need_newline = print_line(unit_name .. " is missing an assigned item, object #" .. u_id .. " '" .. | ||
item_description(item) .. "'", need_newline) | ||
print(unit_name .. " is missing an assigned item, " .. item_description(item)) | ||
printed = true | ||
end | ||
if dfhack.items.getGeneralRef(item, df.general_ref_type.UNIT_HOLDER) then | ||
need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on object #" .. u_id .. " '" .. item_description(item) .. "'", need_newline) | ||
print(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item)) | ||
printed = true | ||
if args.free then | ||
print(" Removing from uniform") | ||
assigned_items[u_id] = nil | ||
for _, specs in ipairs(squad_position.equipment.uniform) do | ||
for _, spec in ipairs(specs) do | ||
for idx, assigned in ipairs(spec.assigned) do | ||
if assigned == u_id then | ||
spec.assigned:erase(idx) | ||
break | ||
end | ||
end | ||
end | ||
end | ||
uniform_assigned_items[item_id] = nil | ||
remove_item_from_position(squad_position, item_id) | ||
end | ||
else | ||
missing_ids[u_id] = item | ||
missing_ids[item_id] = item | ||
if args.free then | ||
to_drop[u_id] = item | ||
to_drop[item_id] = item | ||
end | ||
end | ||
else | ||
present_ids[u_id] = item | ||
present_ids[item_id] = item | ||
end | ||
end | ||
|
||
-- Make the equipment.assigned_items list consistent with what is present in equipment.uniform | ||
for i=#(squad_position.equipment.assigned_items)-1,0,-1 do | ||
local assigned_item_id = squad_position.equipment.assigned_items[i] | ||
-- Quiver, backpack, and flask are assigned in their own locations rather than in equipment.uniform, and thus need their own checks | ||
-- If more separately-assigned items are added in the future, this handling will need to be updated accordingly | ||
if uniform_assigned_items[assigned_item_id] == nil and | ||
assigned_item_id ~= squad_position.equipment.quiver and | ||
assigned_item_id ~= squad_position.equipment.backpack and | ||
assigned_item_id ~= squad_position.equipment.flask | ||
then | ||
local item = df.item.find(assigned_item_id) | ||
if item ~= nil then | ||
print(unit_name .. " has an improperly assigned item, " .. item_description(item) .. "; removing it") | ||
else | ||
print(unit_name .. " has a nonexistent item assigned, item # " .. assigned_item_id .. "; removing it") | ||
end | ||
printed = true | ||
squad_position.equipment.assigned_items:erase(i) | ||
end | ||
end | ||
|
||
|
@@ -202,10 +239,10 @@ local function process(unit, args, need_newline) | |
-- unless --multi is specified, in which we don't care | ||
local covered = {} -- map of body part id to true/nil | ||
if not args.multi then | ||
for id, item in pairs(present_ids) do | ||
for item_id, item in pairs(present_ids) do | ||
-- weapons and shields don't "cover" the bodypart they're assigned to. (Needed to figure out if we're missing gloves.) | ||
if item._type ~= df.item_weaponst and item._type ~= df.item_shieldst then | ||
covered[worn_parts[id]] = true | ||
covered[worn_parts[item_id]] = true | ||
end | ||
end | ||
end | ||
|
@@ -221,19 +258,23 @@ local function process(unit, args, need_newline) | |
end | ||
|
||
-- Drop everything (except uniform pieces) from body parts which should be covered but aren't | ||
for w_id, item in pairs(worn_items) do | ||
if assigned_items[w_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) | ||
if uncovered[worn_parts[w_id]] then | ||
need_newline = print_line(unit_name .. | ||
" potentially has object #" .. | ||
w_id .. " '" .. item_description(item) .. "' blocking a missing uniform item.", need_newline) | ||
for worn_item_id, item in pairs(worn_items) do | ||
if uniform_assigned_items[worn_item_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) | ||
if uncovered[worn_parts[worn_item_id]] then | ||
print(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.") | ||
printed = true | ||
if args.drop then | ||
to_drop[w_id] = item | ||
to_drop[worn_item_id] = item | ||
end | ||
end | ||
end | ||
end | ||
|
||
-- add a spacing line if there was any output | ||
if printed then | ||
print() | ||
end | ||
|
||
return to_drop | ||
end | ||
|
||
|
@@ -242,15 +283,15 @@ local function do_drop(item_list) | |
return | ||
end | ||
|
||
for id, item in pairs(item_list) do | ||
local pos = get_item_pos(item) | ||
for _, item in pairs(item_list) do | ||
local pos = get_visible_item_pos(item) | ||
if not pos then | ||
dfhack.printerr("Could not find drop location for item #" .. id .. " " .. item_description(item)) | ||
dfhack.printerr("Could not find drop location for " .. item_description(item)) | ||
else | ||
if dfhack.items.moveToGround(item, pos) then | ||
print("Dropped item #" .. id .. " '" .. item_description(item) .. "'") | ||
print("Dropped " .. item_description(item)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use the EDIT: no, it makes no difference here. That's gonna be a rant by itself. |
||
else | ||
dfhack.printerr("Could not drop object #" .. id .. " " .. item_description(item)) | ||
dfhack.printerr("Could not drop " .. item_description(item)) | ||
end | ||
end | ||
end | ||
|
@@ -265,10 +306,8 @@ local function main(args) | |
end | ||
|
||
if args.all then | ||
local need_newline = false | ||
for _, unit in ipairs(dfhack.units.getCitizens(true)) do | ||
do_drop(process(unit, args, need_newline)) | ||
need_newline = true | ||
do_drop(process(unit, args)) | ||
end | ||
else | ||
local unit = dfhack.gui.getSelectedUnit() | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider extending
item_description()
to gracefully handle the case of a nonexistent item.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unclear on what you mean here by the "case of a nonexistent item"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The case handled on line 208. Literally
df.item.find()
returnednil
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is no "valid" response to being asked to describe a null item; the correct response is for the function to throw, which is what it currently does
callers are expected to check for nil or be prepared to accept a throw if they do not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a cursory scan through the code and I think
item
will never be null on calls to this function, but I am not at all sure of that. If you're confident that this code will never be called withitem
as null, it's enough to document thatitem
must never be null. Otherwise, add a test for item being null to this code and provide an appropriate alternative. Otherwise, the script will abort with an exception, which will generally be confusing to users (as most people never look at the console and thus won't see the exception).