Skip to content

Commit f585c74

Browse files
authored
Merge pull request #1490 from Jarkami/uniform-unstick-fix
Fix state inconsistency in uniform-unstick
2 parents 6749ada + 5b4072e commit f585c74

File tree

2 files changed

+110
-70
lines changed

2 files changed

+110
-70
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Template for new versions:
4444
- `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders
4545
- `confirm`: the pause option now pauses individual confirmation types, allowing multiple different confirmations to be paused independently
4646
- `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach
47+
- `uniform-unstick`: no longer causes units to equip multiples of assigned items
4748

4849
## Misc Improvements
4950
- `devel/hello-world`: updated to show off the new Slider widget

uniform-unstick.lua

Lines changed: 109 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ local validArgs = utils.invert({
1515

1616
-- Functions
1717

18+
-- @param item df.item
19+
-- @return string
1820
local function item_description(item)
19-
return dfhack.df2console(dfhack.items.getDescription(item, 0, true))
21+
return "item #" .. item.id .. " '" .. dfhack.df2console(dfhack.items.getDescription(item, 0, true)) .. "'"
2022
end
2123

22-
local function get_item_pos(item)
24+
-- @param item df.item
25+
-- @return df.coord|nil
26+
local function get_visible_item_pos(item)
2327
local x, y, z = dfhack.items.getPosition(item)
2428
if not x or not y or not z then
2529
return
@@ -30,24 +34,30 @@ local function get_item_pos(item)
3034
end
3135
end
3236

33-
local function get_squad_position(unit, unit_name)
37+
-- @param unit df.unit
38+
-- @return df.squad_position|nil
39+
local function get_squad_position(unit)
3440
local squad = df.squad.find(unit.military.squad_id)
35-
if squad then
36-
if squad.entity_id ~= df.global.plotinfo.group_id then
37-
print("WARNING: Unit " .. unit_name .. " is a member of a squad from another site!" ..
38-
" This may be preventing them from doing any useful work." ..
39-
" You can fix this by assigning them to a local squad and then unassigning them.")
40-
print()
41-
return
42-
end
43-
else
41+
if not squad then
42+
return
43+
end
44+
45+
if squad.entity_id ~= df.global.plotinfo.group_id then
46+
print("WARNING: Unit " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. " is a member of a squad from another site!" ..
47+
" This may be preventing them from doing any useful work." ..
48+
" You can fix this by assigning them to a local squad and then unassigning them.")
49+
print()
4450
return
4551
end
52+
4653
if #squad.positions > unit.military.squad_position then
4754
return squad.positions[unit.military.squad_position]
4855
end
4956
end
5057

58+
-- @param unit df.unit
59+
-- @param item df.item
60+
-- @return number[] list of body part ids
5161
local function bodyparts_that_can_wear(unit, item)
5262
local bodyparts = {}
5363
local unitparts = dfhack.units.getCasteRaw(unit).body_info.body_parts
@@ -89,47 +99,61 @@ local function bodyparts_that_can_wear(unit, item)
8999
return bodyparts
90100
end
91101

92-
-- returns new value of need_newline
93-
local function print_line(text, need_newline)
94-
if need_newline then
95-
print()
96-
end
97-
print(text)
98-
return false
102+
-- @param unit_name string
103+
-- @param labor_name string
104+
local function print_bad_labor(unit_name, labor_name)
105+
return print("WARNING: Unit " .. unit_name .. " has the " .. labor_name ..
106+
" labor enabled, which conflicts with military uniforms.")
99107
end
100108

101-
local function print_bad_labor(unit_name, labor_name, need_newline)
102-
return print_line("WARNING: Unit " .. unit_name .. " has the " .. labor_name ..
103-
" labor enabled, which conflicts with military uniforms.", need_newline)
109+
-- @param squad_position df.squad_position
110+
-- @param item_id number
111+
local function remove_item_from_position(squad_position, item_id)
112+
for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do
113+
for _, uniform_spec in ipairs(uniform_slot_specs) do
114+
for idx, assigned_item_id in ipairs(uniform_spec.assigned) do
115+
if assigned_item_id == item_id then
116+
uniform_spec.assigned:erase(idx)
117+
return
118+
end
119+
end
120+
end
121+
end
104122
end
105123

106124
-- Will figure out which items need to be moved to the floor, returns an item_id:item map
107-
local function process(unit, args, need_newline)
125+
local function process(unit, args)
108126
local silent = args.all -- Don't print details if we're iterating through all dwarves
109127
local unit_name = dfhack.df2console(dfhack.units.getReadableName(unit))
128+
local printed = false
110129

111130
if not silent then
112-
need_newline = print_line("Processing unit " .. unit_name, need_newline)
131+
print("Processing unit " .. unit_name)
132+
printed = true
113133
end
114134

115135
-- The return value
116136
local to_drop = {} -- item id to item object
117137

118138
-- First get squad position for an early-out for non-military dwarves
119-
local squad_position = get_squad_position(unit, unit_name)
139+
local squad_position = get_squad_position(unit)
120140
if not squad_position then
121141
if not silent then
122-
need_newline = print_line(unit_name .. " does not have a military uniform.", need_newline)
142+
print(unit_name .. " does not have a military uniform.")
143+
print()
123144
end
124145
return
125146
end
126147

127148
if unit.status.labors.MINE then
128-
need_newline = print_bad_labor(unit_name, "mining", need_newline)
149+
print_bad_labor(unit_name, "mining")
150+
printed = true
129151
elseif unit.status.labors.CUTWOOD then
130-
need_newline = print_bad_labor(unit_name, "woodcutting", need_newline)
152+
print_bad_labor(unit_name, "woodcutting")
153+
printed = true
131154
elseif unit.status.labors.HUNT then
132-
need_newline = print_bad_labor(unit_name, "hunting", need_newline)
155+
print_bad_labor(unit_name, "hunting")
156+
printed = true
133157
end
134158

135159
-- Find all worn items which may be at issue.
@@ -148,12 +172,12 @@ local function process(unit, args, need_newline)
148172
end
149173

150174
-- Now get info about which items have been assigned as part of the uniform
151-
local assigned_items = {} -- assigned item ids mapped to item objects
152-
for _, specs in ipairs(squad_position.equipment.uniform) do
153-
for _, spec in ipairs(specs) do
154-
for _, assigned in ipairs(spec.assigned) do
175+
local uniform_assigned_items = {} -- assigned item ids mapped to item objects
176+
for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do
177+
for _, uniform_spec in ipairs(uniform_slot_specs) do
178+
for _, assigned_item_id in ipairs(uniform_spec.assigned) do
155179
-- Include weapon and shield so we can avoid dropping them, or pull them out of container/inventory later
156-
assigned_items[assigned] = df.item.find(assigned)
180+
uniform_assigned_items[assigned_item_id] = df.item.find(assigned_item_id)
157181
end
158182
end
159183
end
@@ -163,36 +187,49 @@ local function process(unit, args, need_newline)
163187

164188
local present_ids = {} -- map of item ID to item object
165189
local missing_ids = {} -- map of item ID to item object
166-
for u_id, item in pairs(assigned_items) do
167-
if not worn_items[u_id] then
190+
for item_id, item in pairs(uniform_assigned_items) do
191+
if not worn_items[item_id] then
168192
if not silent then
169-
need_newline = print_line(unit_name .. " is missing an assigned item, object #" .. u_id .. " '" ..
170-
item_description(item) .. "'", need_newline)
193+
print(unit_name .. " is missing an assigned item, " .. item_description(item))
194+
printed = true
171195
end
172196
if dfhack.items.getGeneralRef(item, df.general_ref_type.UNIT_HOLDER) then
173-
need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on object #" .. u_id .. " '" .. item_description(item) .. "'", need_newline)
197+
print(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item))
198+
printed = true
174199
if args.free then
175200
print(" Removing from uniform")
176-
assigned_items[u_id] = nil
177-
for _, specs in ipairs(squad_position.equipment.uniform) do
178-
for _, spec in ipairs(specs) do
179-
for idx, assigned in ipairs(spec.assigned) do
180-
if assigned == u_id then
181-
spec.assigned:erase(idx)
182-
break
183-
end
184-
end
185-
end
186-
end
201+
uniform_assigned_items[item_id] = nil
202+
remove_item_from_position(squad_position, item_id)
187203
end
188204
else
189-
missing_ids[u_id] = item
205+
missing_ids[item_id] = item
190206
if args.free then
191-
to_drop[u_id] = item
207+
to_drop[item_id] = item
192208
end
193209
end
194210
else
195-
present_ids[u_id] = item
211+
present_ids[item_id] = item
212+
end
213+
end
214+
215+
-- Make the equipment.assigned_items list consistent with what is present in equipment.uniform
216+
for i=#(squad_position.equipment.assigned_items)-1,0,-1 do
217+
local assigned_item_id = squad_position.equipment.assigned_items[i]
218+
-- Quiver, backpack, and flask are assigned in their own locations rather than in equipment.uniform, and thus need their own checks
219+
-- If more separately-assigned items are added in the future, this handling will need to be updated accordingly
220+
if uniform_assigned_items[assigned_item_id] == nil and
221+
assigned_item_id ~= squad_position.equipment.quiver and
222+
assigned_item_id ~= squad_position.equipment.backpack and
223+
assigned_item_id ~= squad_position.equipment.flask
224+
then
225+
local item = df.item.find(assigned_item_id)
226+
if item ~= nil then
227+
print(unit_name .. " has an improperly assigned item, " .. item_description(item) .. "; removing it")
228+
else
229+
print(unit_name .. " has a nonexistent item assigned, item # " .. assigned_item_id .. "; removing it")
230+
end
231+
printed = true
232+
squad_position.equipment.assigned_items:erase(i)
196233
end
197234
end
198235

@@ -202,10 +239,10 @@ local function process(unit, args, need_newline)
202239
-- unless --multi is specified, in which we don't care
203240
local covered = {} -- map of body part id to true/nil
204241
if not args.multi then
205-
for id, item in pairs(present_ids) do
242+
for item_id, item in pairs(present_ids) do
206243
-- weapons and shields don't "cover" the bodypart they're assigned to. (Needed to figure out if we're missing gloves.)
207244
if item._type ~= df.item_weaponst and item._type ~= df.item_shieldst then
208-
covered[worn_parts[id]] = true
245+
covered[worn_parts[item_id]] = true
209246
end
210247
end
211248
end
@@ -221,19 +258,23 @@ local function process(unit, args, need_newline)
221258
end
222259

223260
-- Drop everything (except uniform pieces) from body parts which should be covered but aren't
224-
for w_id, item in pairs(worn_items) do
225-
if assigned_items[w_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands)
226-
if uncovered[worn_parts[w_id]] then
227-
need_newline = print_line(unit_name ..
228-
" potentially has object #" ..
229-
w_id .. " '" .. item_description(item) .. "' blocking a missing uniform item.", need_newline)
261+
for worn_item_id, item in pairs(worn_items) do
262+
if uniform_assigned_items[worn_item_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands)
263+
if uncovered[worn_parts[worn_item_id]] then
264+
print(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.")
265+
printed = true
230266
if args.drop then
231-
to_drop[w_id] = item
267+
to_drop[worn_item_id] = item
232268
end
233269
end
234270
end
235271
end
236272

273+
-- add a spacing line if there was any output
274+
if printed then
275+
print()
276+
end
277+
237278
return to_drop
238279
end
239280

@@ -242,15 +283,15 @@ local function do_drop(item_list)
242283
return
243284
end
244285

245-
for id, item in pairs(item_list) do
246-
local pos = get_item_pos(item)
286+
for _, item in pairs(item_list) do
287+
local pos = get_visible_item_pos(item)
247288
if not pos then
248-
dfhack.printerr("Could not find drop location for item #" .. id .. " " .. item_description(item))
289+
dfhack.printerr("Could not find drop location for " .. item_description(item))
249290
else
250291
if dfhack.items.moveToGround(item, pos) then
251-
print("Dropped item #" .. id .. " '" .. item_description(item) .. "'")
292+
print("Dropped " .. item_description(item))
252293
else
253-
dfhack.printerr("Could not drop object #" .. id .. " " .. item_description(item))
294+
dfhack.printerr("Could not drop " .. item_description(item))
254295
end
255296
end
256297
end
@@ -265,10 +306,8 @@ local function main(args)
265306
end
266307

267308
if args.all then
268-
local need_newline = false
269309
for _, unit in ipairs(dfhack.units.getCitizens(true)) do
270-
do_drop(process(unit, args, need_newline))
271-
need_newline = true
310+
do_drop(process(unit, args))
272311
end
273312
else
274313
local unit = dfhack.gui.getSelectedUnit()

0 commit comments

Comments
 (0)