Skip to content

Commit b36c739

Browse files
authored
Merge pull request #1475 from chdoc/fix-immortal-cravings
[immortal-cravings] prioritize high-value meals
2 parents 61e0e18 + 80a82d4 commit b36c739

File tree

2 files changed

+97
-29
lines changed

2 files changed

+97
-29
lines changed

changelog.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Template for new versions:
3232

3333
## Fixes
3434

35+
- `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach
36+
3537
## Misc Improvements
3638

3739
## Removed

immortal-cravings.lua

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@
33

44
local idle = reqscript('idle-crafting')
55
local repeatutil = require("repeat-util")
6+
67
--- utility functions
78

9+
local verbose = false
10+
---conditional printing of debug messages
11+
---@param message string
12+
local function debug(message)
13+
if verbose then
14+
print(message)
15+
end
16+
end
17+
818
---3D city metric
919
---@param p1 df.coord
1020
---@param p2 df.coord
@@ -13,26 +23,40 @@ function distance(p1, p2)
1323
return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z)
1424
end
1525

26+
---maybe a candidate for utils.lua?
27+
---find best available item in an item vector (according to some metric)
28+
---@generic T : df.item
29+
---@param item_vector T[]
30+
---@param metric fun(item: T): number?
31+
---@return T?
32+
function findBest(item_vector, metric, smallest)
33+
local best = nil
34+
local mbest = nil
35+
for _, item in ipairs(item_vector) do
36+
mitem = metric(item)
37+
if mitem and (not best or (smallest and mitem < mbest or mitem > mbest)) then
38+
best = item
39+
mbest = mitem
40+
end
41+
end
42+
return best
43+
end
44+
1645
---find closest accessible item in an item vector
1746
---@generic T : df.item
1847
---@param pos df.coord
1948
---@param item_vector T[]
2049
---@param is_good? fun(item: T): boolean
2150
---@return T?
2251
local function findClosest(pos, item_vector, is_good)
23-
local closest = nil
24-
local dclosest = -1
25-
for _,item in ipairs(item_vector) do
26-
if not item.flags.in_job and (not is_good or is_good(item)) then
52+
local function metric(item)
53+
if not is_good or is_good(item) then
2754
local pitem = xyz2pos(dfhack.items.getPosition(item))
28-
local ditem = distance(pos, pitem)
29-
if dfhack.maps.canWalkBetween(pos, pitem) and (not closest or ditem < dclosest) then
30-
closest = item
31-
dclosest = ditem
32-
end
55+
return dfhack.maps.canWalkBetween(pos, pitem) and distance(pos, pitem) or nil
3356
end
57+
return nil
3458
end
35-
return closest
59+
return findBest(item_vector, metric, true)
3660
end
3761

3862
---find a drink
@@ -41,24 +65,32 @@ end
4165
local function get_closest_drink(pos)
4266
local is_good = function (drink)
4367
local container = dfhack.items.getContainer(drink)
44-
return container and container:isFoodStorage()
68+
return not drink.flags.in_job and container and container:isFoodStorage()
4569
end
4670
return findClosest(pos, df.global.world.items.other.DRINK, is_good)
4771
end
4872

49-
---find some prepared meal
73+
---find available meal with highest per-portion value
5074
---@return df.item_foodst?
51-
local function get_closest_meal(pos)
75+
local function get_best_meal(pos)
76+
5277
---@param meal df.item_foodst
53-
local function is_good(meal)
54-
if meal.flags.rotten then
55-
return false
78+
local function portion_value(meal)
79+
local accessible = dfhack.maps.canWalkBetween(pos,xyz2pos(dfhack.items.getPosition(meal)))
80+
if meal.flags.in_job or meal.flags.rotten or not accessible then
81+
return nil
5682
else
83+
-- check that meal is either on the ground or in food storage (and not in a backpack)
5784
local container = dfhack.items.getContainer(meal)
58-
return not container or container:isFoodStorage()
85+
if not container or container:isFoodStorage() then
86+
return dfhack.items.getValue(meal) / meal.stack_size
87+
else
88+
return nil
89+
end
5990
end
6091
end
61-
return findClosest(pos, df.global.world.items.other.FOOD, is_good)
92+
93+
return findBest(df.global.world.items.other.FOOD, portion_value)
6294
end
6395

6496
---create a Drink job for the given unit
@@ -86,11 +118,22 @@ end
86118
---create Eat job for the given unit
87119
---@param unit df.unit
88120
local function goEat(unit)
89-
local meal = get_closest_meal(unit.pos)
90-
if not meal then
121+
local meal_stack = get_best_meal(unit.pos)
122+
if not meal_stack then
91123
-- print('no accessible meals found')
92124
return
93125
end
126+
127+
---@type df.item|df.item_foodst
128+
local meal
129+
if meal_stack.stack_size > 1 then
130+
meal = meal_stack:splitStack(1, true)
131+
meal:categorize(true)
132+
else
133+
meal = meal_stack
134+
end
135+
dfhack.items.setOwner(meal, unit)
136+
94137
local job = idle.make_job()
95138
job.job_type = df.job_type.Eat
96139
job.flags.special = true
@@ -105,6 +148,25 @@ local function goEat(unit)
105148
print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name))
106149
end
107150

151+
---unit is ready to take jobs (will interrupt social activities)
152+
---@param unit df.unit
153+
---@return boolean
154+
function unitIsAvailable(unit)
155+
if unit.job.current_job then
156+
return false
157+
elseif #unit.individual_drills > 0 then
158+
return false
159+
elseif unit.flags1.caged or unit.flags1.chained then
160+
return false
161+
elseif unit.military.squad_id ~= -1 then
162+
local squad = df.squad.find(unit.military.squad_id)
163+
-- this lookup should never fail
164+
---@diagnostic disable-next-line: need-check-nil
165+
return #squad.orders == 0 and squad.activity == -1
166+
end
167+
return true
168+
end
169+
108170
--- script logic
109171

110172
local GLOBAL_KEY = 'immortal-cravings'
@@ -137,7 +199,7 @@ local threshold = -9000
137199

138200
---unit loop: check for idle watched units and create eat/drink jobs for them
139201
local function unit_loop()
140-
-- print(('immortal-cravings: running unit loop (%d watched units)'):format(#watched))
202+
debug(('immortal-cravings: running unit loop (%d watched units)'):format(#watched))
141203
---@type integer[]
142204
local kept = {}
143205
for _, unit_id in ipairs(watched) do
@@ -148,7 +210,8 @@ local function unit_loop()
148210
then
149211
goto next_unit
150212
end
151-
if not idle.unitIsAvailable(unit) then
213+
if not unitIsAvailable(unit) then
214+
debug("immortal-cravings: skipping busy"..dfhack.units.getReadableName(unit))
152215
table.insert(kept, unit.id)
153216
else
154217
-- unit is available for jobs; satisfy one of its needs
@@ -166,7 +229,7 @@ local function unit_loop()
166229
end
167230
watched = kept
168231
if #watched == 0 then
169-
-- print('immortal-cravings: no more watched units, cancelling unit loop')
232+
debug('immortal-cravings: no more watched units, cancelling unit loop')
170233
repeatutil.cancel(GLOBAL_KEY .. '-unit')
171234
end
172235
end
@@ -178,18 +241,21 @@ end
178241

179242
---main loop: look for citizens with personality needs for food/drink but w/o physiological need
180243
local function main_loop()
181-
-- print('immortal-cravings watching:')
244+
debug('immortal-cravings watching:')
182245
watched = {}
183-
for _, unit in ipairs(dfhack.units.getCitizens()) do
184-
if not is_active_caste_flag(unit, 'NO_DRINK') and not is_active_caste_flag(unit, 'NO_EAT') then
246+
for _, unit in ipairs(dfhack.units.getCitizens(false, false)) do
247+
if
248+
not (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) or
249+
unit.counters2.stomach_content > 0
250+
then
185251
goto next_unit
186252
end
187253
for _, need in ipairs(unit.status.current_soul.personality.needs) do
188-
if need.id == DrinkAlcohol and need.focus_level < threshold or
189-
need.id == EatGoodMeal and need.focus_level < threshold
254+
if need.id == DrinkAlcohol and need.focus_level < threshold or
255+
need.id == EatGoodMeal and need.focus_level < threshold
190256
then
191257
table.insert(watched, unit.id)
192-
-- print(' '..dfhack.df2console(dfhack.units.getReadableName(unit)))
258+
debug(' '..dfhack.df2console(dfhack.units.getReadableName(unit)))
193259
goto next_unit
194260
end
195261
end

0 commit comments

Comments
 (0)