Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Template for new versions:
- Fixed out-of-bounds color table access in ``Screen`` module when Lua pens use ``COLOR_RESET``
- ``Units::teleport``: update unit occupancy on the full 3x3 footprint of EQUIPMENT units (e.g. wagons) instead of only their center tile
- ``Units::teleport``: only clear tile unit occupancy flags when no other unit of the same kind remains on the tile
- ``Units::isCitizen`` and ``Units::isResident`` now exclude dead units even when ``include_insane`` is set; the flag previously skipped the sanity check that also happened to exclude the dead
- `3dveins`: fix crash when a tile's biome cannot be resolved
- `3dveins`: fix failure on embarks that cross midmap tiles
- `aquifer`: make ``--skip-top`` and top-relative ``--levels`` take effect for the ``drain``, ``convert``, and ``add`` actions instead of being silently ignored
Expand All @@ -93,6 +94,8 @@ Template for new versions:
- `export-world-map`: fix crash exporting site data for a site whose owner has an undefined race
- `gui/quickfort`: show smoothing designations immediately after applying a blueprint
- `overlay`: overlay hotkeys no longer steal typed characters while the game is capturing text input, e.g. when renaming a building
- `preserve-rooms`: no longer assigns zones to dead units who still hold a noble position
- `sort`: the clickable stress icon citizen lists no longer include dead or missing units (issue #5975)
- `sort`: fix the Places/Workshops and task searches missing workshops by their current display names (e.g. "Stoneworker's Workshop", "Screw Press")
- `stocks`: overlay now resets scroll position when collapsing categories so the item list is no longer left blank and unscrollable
- `strangemood`: updated to match behavior of current DF version
Expand Down
8 changes: 5 additions & 3 deletions docs/dev/Lua API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1565,12 +1565,14 @@ Units module
same checks the game uses to decide game-over by extinction,
with an additional sanity check. You can identify citizens,
regardless of their sanity, by passing ``true`` as the optional
second parameter.
second parameter. Dead units are never considered citizens, even
when ``include_insane`` is set (though animate intelligent
undead citizens still count).

* ``dfhack.units.isResident(unit[,include_insane])``

The unit is a resident of the fortress. Same ``include_insane`` semantics as
``isCitizen``.
The unit is a non-dead resident of the fortress. Same
``include_insane`` semantics as ``isCitizen``.

* ``dfhack.units.isFortControlled(unit)``

Expand Down
10 changes: 5 additions & 5 deletions library/include/modules/Units.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ DFHACK_EXPORT bool isActive(df::unit *unit);
DFHACK_EXPORT bool isVisible(df::unit *unit);
// Unit is a non-dead (optionally sane) citizen of fort.
DFHACK_EXPORT bool isCitizen(df::unit *unit, bool include_insane = false);
// Long-term resident, not the hostile type.
// Long-term (non-dead) resident, not the hostile type.
DFHACK_EXPORT bool isResident(df::unit *unit, bool include_insane = false);
// Similar to isCitizen, but includes tame animals. Will reveal ambushers for the fort, etc.
DFHACK_EXPORT bool isFortControlled(df::unit *unit);
Expand Down Expand Up @@ -200,10 +200,10 @@ DFHACK_EXPORT df::unit *getUnitByNobleRole(std::string noble);

inline auto citizensRange(std::vector<df::unit *> &vec, bool exclude_residents = false, bool include_insane = false) {
return vec | std::views::filter([=](df::unit *unit) {
if (isDead(unit) || !isActive(unit))
return false;
return isCitizen(unit, include_insane) ||
(!exclude_residents && isResident(unit, include_insane));
// isCitizen and isResident both reject dead units
return isActive(unit) &&
(isCitizen(unit, include_insane) ||
(!exclude_residents && isResident(unit, include_insane)));
});
}

Expand Down
7 changes: 5 additions & 2 deletions library/modules/Units.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ bool Units::isCitizen(df::unit *unit, bool include_insane) {
// Copied from the conditions used to decide game over,
// except that the game appears to let melancholy/raving
// dwarves count as citizens.
if (unit->flags1.whole & exclude_flags1 ||
// include_insane relaxes only the sanity check; the dead are
// never citizens (intelligent undead are animate, not dead)
if (isDead(unit) ||
unit->flags1.whole & exclude_flags1 ||
unit->flags2.whole & exclude_flags2 ||
(!include_insane && !isSane(unit)))
return false;
Expand All @@ -157,7 +160,7 @@ bool Units::isCitizen(df::unit *unit, bool include_insane) {

bool Units::isResident(df::unit *unit, bool include_insane) {
CHECK_NULL_POINTER(unit);
if (!include_insane && !isSane(unit))
if (isDead(unit) || (!include_insane && !isSane(unit)))
return false;

return isOwnCiv(unit) &&
Expand Down
4 changes: 1 addition & 3 deletions plugins/lua/dwarfmonitor.lua
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,10 @@ end

function MiseryWidget:overlay_onupdate()
local counts, num_colors = {}, #self.colors
for _,unit in ipairs(df.global.world.units.active) do
if not dfhack.units.isCitizen(unit, true) then goto continue end
for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do
local stress_category = math.min(num_colors,
dfhack.units.getStressCategory(unit)+1)
counts[stress_category] = (counts[stress_category] or 0) + 1
::continue::
end

local width = 2 + num_colors - 1 -- 'H:' plus the slashes
Expand Down
17 changes: 12 additions & 5 deletions plugins/lua/sort/stressicons.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ local STRESS_CATEGORY_NAMES = {
'Ecstatic',
}

-- single-unit equivalent of the dfhack.units.getCitizens(true, true) filter
local function is_listable(unit)
return dfhack.units.isCitizen(unit, true) and dfhack.units.isActive(unit)
end

local function show_stress_units(category)
local choices = {}
for _,unit in ipairs(df.global.world.units.active) do
if dfhack.units.isCitizen(unit, true) and
dfhack.units.getStressCategory(unit) == category then
-- living, on-map citizens only (excluding residents), matching the game's
-- own stress icon counts
for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do
if dfhack.units.getStressCategory(unit) == category then
table.insert(choices, {
text=dfhack.units.getReadableName(unit),
unit_id=unit.id,
Expand All @@ -31,9 +37,10 @@ local function show_stress_units(category)
with_filter=true,
choices=choices,
on_select=function(_, choice)
-- re-resolve in case the unit died while the list was open
-- re-resolve in case the unit died or left the map while the list
-- was open; df.unit.find still resolves dead units
local unit = df.unit.find(choice.unit_id)
if not unit then return end
if not unit or not is_listable(unit) then return end
dfhack.gui.revealInDwarfmodeMap(unit.pos.x, unit.pos.y, unit.pos.z, true, true)
end,
}:show()
Expand Down
13 changes: 5 additions & 8 deletions test/plugins/sort.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ end

local function get_citizen_counts()
local counts = {}
for _,unit in ipairs(df.global.world.units.active) do
if dfhack.units.isCitizen(unit, true) then
local cat = dfhack.units.getStressCategory(unit)
counts[cat] = (counts[cat] or 0) + 1
end
for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do
local cat = dfhack.units.getStressCategory(unit)
counts[cat] = (counts[cat] or 0) + 1
end
return counts
end
Expand Down Expand Up @@ -153,9 +151,8 @@ function test.selecting_unit_centers_map()
expect.ne(nil, called_with)
-- the revealed position must be a real citizen's position in that bucket
local matched = false
for _,unit in ipairs(df.global.world.units.active) do
if dfhack.units.isCitizen(unit, true) and
dfhack.units.getStressCategory(unit) == cat and
for _,unit in ipairs(dfhack.units.getCitizens(true, true)) do
if dfhack.units.getStressCategory(unit) == cat and
unit.pos.x == called_with.x and unit.pos.y == called_with.y and
unit.pos.z == called_with.z then
matched = true
Expand Down
Loading