diff --git a/docs/changelog.txt b/docs/changelog.txt index 8c2bca09b0..eb53d96b9f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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 @@ -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 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 6cd947099b..4951063ed6 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -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)`` diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index f7bd2ca88f..569471d9f0 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -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); @@ -200,10 +200,10 @@ DFHACK_EXPORT df::unit *getUnitByNobleRole(std::string noble); inline auto citizensRange(std::vector &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))); }); } diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 975202d629..d44a8a0c81 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -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; @@ -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) && diff --git a/plugins/lua/dwarfmonitor.lua b/plugins/lua/dwarfmonitor.lua index fd18178a51..40ed1c71df 100644 --- a/plugins/lua/dwarfmonitor.lua +++ b/plugins/lua/dwarfmonitor.lua @@ -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 diff --git a/plugins/lua/sort/stressicons.lua b/plugins/lua/sort/stressicons.lua index 23baf5b104..1971326e6e 100644 --- a/plugins/lua/sort/stressicons.lua +++ b/plugins/lua/sort/stressicons.lua @@ -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, @@ -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() diff --git a/test/plugins/sort.lua b/test/plugins/sort.lua index b38609b5fe..d417d31960 100644 --- a/test/plugins/sort.lua +++ b/test/plugins/sort.lua @@ -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 @@ -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