From 337260ab0c201f0328d75914214723feddeb908d Mon Sep 17 00:00:00 2001 From: lethosor Date: Wed, 6 Sep 2023 18:19:31 -0400 Subject: [PATCH 01/18] Fix unchecked lua_tostring calls --- library/LuaApi.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 1e89d7b45..f792cff90 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1368,8 +1368,8 @@ static CommandHistory * ensureCommandHistory(std::string id, static int getCommandHistory(lua_State *state) { - std::string id = lua_tostring(state, 1); - std::string src_file = lua_tostring(state, 2); + std::string id = luaL_checkstring(state, 1); + std::string src_file = luaL_checkstring(state, 2); std::vector entries; ensureCommandHistory(id, src_file)->getEntries(entries); Lua::PushVector(state, entries); @@ -2030,7 +2030,7 @@ static int units_getCitizens(lua_State *L) { } static int units_getUnitsByNobleRole(lua_State *L) { - std::string role_name = lua_tostring(L, -1); + std::string role_name = luaL_checkstring(L, -1); std::vector units; Units::getUnitsByNobleRole(units, role_name); Lua::PushVector(L, units); From f0b0c2093b1b035ca43a85f5fa3179f75c8734dd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 7 Sep 2023 11:30:08 -0700 Subject: [PATCH 02/18] flatten sort functions --- plugins/lua/sort.lua | 126 +++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 64 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index e775f1ddf..3aabec544 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -425,28 +425,26 @@ local function sort_by_ranged_combat_potential_asc(unit_id_1, unit_id_2) return utils.compare(rating1, rating2) end -local SORT_FNS = { - sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc(MELEE_WEAPON_SKILLS), - sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc(MELEE_WEAPON_SKILLS), - sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc(RANGED_WEAPON_SKILLS), - sort_by_any_ranged_asc=make_sort_by_ranged_skill_effectiveness_asc(RANGED_WEAPON_SKILLS), - sort_by_teacher_desc=make_sort_by_skill_desc(df.job_skill.TEACHING), - sort_by_teacher_asc=make_sort_by_skill_asc(df.job_skill.TEACHING), - sort_by_tactics_desc=make_sort_by_skill_desc(df.job_skill.MILITARY_TACTICS), - sort_by_tactics_asc=make_sort_by_skill_asc(df.job_skill.MILITARY_TACTICS), - sort_by_axe_desc=make_sort_by_skill_desc(df.job_skill.AXE), - sort_by_axe_asc=make_sort_by_skill_asc(df.job_skill.AXE), - sort_by_sword_desc=make_sort_by_skill_desc(df.job_skill.SWORD), - sort_by_sword_asc=make_sort_by_skill_asc(df.job_skill.SWORD), - sort_by_mace_desc=make_sort_by_skill_desc(df.job_skill.MACE), - sort_by_mace_asc=make_sort_by_skill_asc(df.job_skill.MACE), - sort_by_hammer_desc=make_sort_by_skill_desc(df.job_skill.HAMMER), - sort_by_hammer_asc=make_sort_by_skill_asc(df.job_skill.HAMMER), - sort_by_spear_desc=make_sort_by_skill_desc(df.job_skill.SPEAR), - sort_by_spear_asc=make_sort_by_skill_asc(df.job_skill.SPEAR), - sort_by_crossbow_desc=make_sort_by_skill_desc(df.job_skill.CROSSBOW), - sort_by_crossbow_asc=make_sort_by_skill_asc(df.job_skill.CROSSBOW), -} +local sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc(MELEE_WEAPON_SKILLS) +local sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc(MELEE_WEAPON_SKILLS) +local sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc(RANGED_WEAPON_SKILLS) +local sort_by_any_ranged_asc=make_sort_by_ranged_skill_effectiveness_asc(RANGED_WEAPON_SKILLS) +local sort_by_teacher_desc=make_sort_by_skill_desc(df.job_skill.TEACHING) +local sort_by_teacher_asc=make_sort_by_skill_asc(df.job_skill.TEACHING) +local sort_by_tactics_desc=make_sort_by_skill_desc(df.job_skill.MILITARY_TACTICS) +local sort_by_tactics_asc=make_sort_by_skill_asc(df.job_skill.MILITARY_TACTICS) +local sort_by_axe_desc=make_sort_by_skill_desc(df.job_skill.AXE) +local sort_by_axe_asc=make_sort_by_skill_asc(df.job_skill.AXE) +local sort_by_sword_desc=make_sort_by_skill_desc(df.job_skill.SWORD) +local sort_by_sword_asc=make_sort_by_skill_asc(df.job_skill.SWORD) +local sort_by_mace_desc=make_sort_by_skill_desc(df.job_skill.MACE) +local sort_by_mace_asc=make_sort_by_skill_asc(df.job_skill.MACE) +local sort_by_hammer_desc=make_sort_by_skill_desc(df.job_skill.HAMMER) +local sort_by_hammer_asc=make_sort_by_skill_asc(df.job_skill.HAMMER) +local sort_by_spear_desc=make_sort_by_skill_desc(df.job_skill.SPEAR) +local sort_by_spear_asc=make_sort_by_skill_asc(df.job_skill.SPEAR) +local sort_by_crossbow_desc=make_sort_by_skill_desc(df.job_skill.CROSSBOW) +local sort_by_crossbow_asc=make_sort_by_skill_asc(df.job_skill.CROSSBOW) -- ---------------------- -- SquadAssignmentOverlay @@ -492,40 +490,40 @@ function SquadAssignmentOverlay:init() label='Sort by:', key='CUSTOM_SHIFT_S', options={ - {label='melee effectiveness'..CH_DN, value=SORT_FNS.sort_by_any_melee_desc, pen=COLOR_GREEN}, - {label='melee effectiveness'..CH_UP, value=SORT_FNS.sort_by_any_melee_asc, pen=COLOR_YELLOW}, - {label='ranged effectiveness'..CH_DN, value=SORT_FNS.sort_by_any_ranged_desc, pen=COLOR_GREEN}, - {label='ranged effectiveness'..CH_UP, value=SORT_FNS.sort_by_any_ranged_asc, pen=COLOR_YELLOW}, + {label='melee effectiveness'..CH_DN, value=sort_by_any_melee_desc, pen=COLOR_GREEN}, + {label='melee effectiveness'..CH_UP, value=sort_by_any_melee_asc, pen=COLOR_YELLOW}, + {label='ranged effectiveness'..CH_DN, value=sort_by_any_ranged_desc, pen=COLOR_GREEN}, + {label='ranged effectiveness'..CH_UP, value=sort_by_any_ranged_asc, pen=COLOR_YELLOW}, {label='name'..CH_DN, value=sort_by_name_desc, pen=COLOR_GREEN}, {label='name'..CH_UP, value=sort_by_name_asc, pen=COLOR_YELLOW}, - {label='teacher skill'..CH_DN, value=SORT_FNS.sort_by_teacher_desc, pen=COLOR_GREEN}, - {label='teacher skill'..CH_UP, value=SORT_FNS.sort_by_teacher_asc, pen=COLOR_YELLOW}, - {label='tactics skill'..CH_DN, value=SORT_FNS.sort_by_tactics_desc, pen=COLOR_GREEN}, - {label='tactics skill'..CH_UP, value=SORT_FNS.sort_by_tactics_asc, pen=COLOR_YELLOW}, + {label='teacher skill'..CH_DN, value=sort_by_teacher_desc, pen=COLOR_GREEN}, + {label='teacher skill'..CH_UP, value=sort_by_teacher_asc, pen=COLOR_YELLOW}, + {label='tactics skill'..CH_DN, value=sort_by_tactics_desc, pen=COLOR_GREEN}, + {label='tactics skill'..CH_UP, value=sort_by_tactics_asc, pen=COLOR_YELLOW}, {label='migrant wave'..CH_DN, value=sort_by_migrant_wave_desc, pen=COLOR_GREEN}, {label='migrant wave'..CH_UP, value=sort_by_migrant_wave_asc, pen=COLOR_YELLOW}, {label='stress level'..CH_DN, value=sort_by_stress_desc, pen=COLOR_GREEN}, {label='stress level'..CH_UP, value=sort_by_stress_asc, pen=COLOR_YELLOW}, {label='mental stability'..CH_DN, value=sort_by_mental_stability_desc, pen=COLOR_GREEN}, {label='mental stability'..CH_UP, value=sort_by_mental_stability_asc, pen=COLOR_YELLOW}, - {label='axe skill'..CH_DN, value=SORT_FNS.sort_by_axe_desc, pen=COLOR_GREEN}, - {label='axe skill'..CH_UP, value=SORT_FNS.sort_by_axe_asc, pen=COLOR_YELLOW}, - {label='sword skill'..CH_DN, value=SORT_FNS.sort_by_sword_desc, pen=COLOR_GREEN}, - {label='sword skill'..CH_UP, value=SORT_FNS.sort_by_sword_asc, pen=COLOR_YELLOW}, - {label='mace skill'..CH_DN, value=SORT_FNS.sort_by_mace_desc, pen=COLOR_GREEN}, - {label='mace skill'..CH_UP, value=SORT_FNS.sort_by_mace_asc, pen=COLOR_YELLOW}, - {label='hammer skill'..CH_DN, value=SORT_FNS.sort_by_hammer_desc, pen=COLOR_GREEN}, - {label='hammer skill'..CH_UP, value=SORT_FNS.sort_by_hammer_asc, pen=COLOR_YELLOW}, - {label='spear skill'..CH_DN, value=SORT_FNS.sort_by_spear_desc, pen=COLOR_GREEN}, - {label='spear skill'..CH_UP, value=SORT_FNS.sort_by_spear_asc, pen=COLOR_YELLOW}, - {label='crossbow skill'..CH_DN, value=SORT_FNS.sort_by_crossbow_desc, pen=COLOR_GREEN}, - {label='crossbow skill'..CH_UP, value=SORT_FNS.sort_by_crossbow_asc, pen=COLOR_YELLOW}, + {label='axe skill'..CH_DN, value=sort_by_axe_desc, pen=COLOR_GREEN}, + {label='axe skill'..CH_UP, value=sort_by_axe_asc, pen=COLOR_YELLOW}, + {label='sword skill'..CH_DN, value=sort_by_sword_desc, pen=COLOR_GREEN}, + {label='sword skill'..CH_UP, value=sort_by_sword_asc, pen=COLOR_YELLOW}, + {label='mace skill'..CH_DN, value=sort_by_mace_desc, pen=COLOR_GREEN}, + {label='mace skill'..CH_UP, value=sort_by_mace_asc, pen=COLOR_YELLOW}, + {label='hammer skill'..CH_DN, value=sort_by_hammer_desc, pen=COLOR_GREEN}, + {label='hammer skill'..CH_UP, value=sort_by_hammer_asc, pen=COLOR_YELLOW}, + {label='spear skill'..CH_DN, value=sort_by_spear_desc, pen=COLOR_GREEN}, + {label='spear skill'..CH_UP, value=sort_by_spear_asc, pen=COLOR_YELLOW}, + {label='crossbow skill'..CH_DN, value=sort_by_crossbow_desc, pen=COLOR_GREEN}, + {label='crossbow skill'..CH_UP, value=sort_by_crossbow_asc, pen=COLOR_YELLOW}, {label='melee potential'..CH_DN, value=sort_by_melee_combat_potential_desc, pen=COLOR_GREEN}, {label='melee potential'..CH_UP, value=sort_by_melee_combat_potential_asc, pen=COLOR_YELLOW}, {label='ranged potential'..CH_DN, value=sort_by_ranged_combat_potential_desc, pen=COLOR_GREEN}, {label='ranged potential'..CH_UP, value=sort_by_ranged_combat_potential_asc, pen=COLOR_YELLOW}, }, - initial_option=SORT_FNS.sort_by_any_melee_desc, + initial_option=sort_by_any_melee_desc, on_change=self:callback('refresh_list', 'sort'), }, widgets.CycleHotkeyLabel{ @@ -533,10 +531,10 @@ function SquadAssignmentOverlay:init() frame={t=2, l=0, w=11}, options={ {label='melee eff.', value=sort_noop}, - {label='melee eff.'..CH_DN, value=SORT_FNS.sort_by_any_melee_desc, pen=COLOR_GREEN}, - {label='melee eff.'..CH_UP, value=SORT_FNS.sort_by_any_melee_asc, pen=COLOR_YELLOW}, + {label='melee eff.'..CH_DN, value=sort_by_any_melee_desc, pen=COLOR_GREEN}, + {label='melee eff.'..CH_UP, value=sort_by_any_melee_asc, pen=COLOR_YELLOW}, }, - initial_option=SORT_FNS.sort_by_any_melee_desc, + initial_option=sort_by_any_melee_desc, option_gap=0, on_change=self:callback('refresh_list', 'sort_any_melee'), }, @@ -545,8 +543,8 @@ function SquadAssignmentOverlay:init() frame={t=2, r=8, w=12}, options={ {label='ranged eff.', value=sort_noop}, - {label='ranged eff.'..CH_DN, value=SORT_FNS.sort_by_any_ranged_desc, pen=COLOR_GREEN}, - {label='ranged eff.'..CH_UP, value=SORT_FNS.sort_by_any_ranged_asc, pen=COLOR_YELLOW}, + {label='ranged eff.'..CH_DN, value=sort_by_any_ranged_desc, pen=COLOR_GREEN}, + {label='ranged eff.'..CH_UP, value=sort_by_any_ranged_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_any_ranged'), @@ -567,8 +565,8 @@ function SquadAssignmentOverlay:init() frame={t=4, l=0, w=8}, options={ {label='teacher', value=sort_noop}, - {label='teacher'..CH_DN, value=SORT_FNS.sort_by_teacher_desc, pen=COLOR_GREEN}, - {label='teacher'..CH_UP, value=SORT_FNS.sort_by_teacher_asc, pen=COLOR_YELLOW}, + {label='teacher'..CH_DN, value=sort_by_teacher_desc, pen=COLOR_GREEN}, + {label='teacher'..CH_UP, value=sort_by_teacher_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_teacher'), @@ -578,8 +576,8 @@ function SquadAssignmentOverlay:init() frame={t=4, l=10, w=8}, options={ {label='tactics', value=sort_noop}, - {label='tactics'..CH_DN, value=SORT_FNS.sort_by_tactics_desc, pen=COLOR_GREEN}, - {label='tactics'..CH_UP, value=SORT_FNS.sort_by_tactics_asc, pen=COLOR_YELLOW}, + {label='tactics'..CH_DN, value=sort_by_tactics_desc, pen=COLOR_GREEN}, + {label='tactics'..CH_UP, value=sort_by_tactics_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_tactics'), @@ -622,8 +620,8 @@ function SquadAssignmentOverlay:init() frame={t=8, l=0, w=4}, options={ {label='axe', value=sort_noop}, - {label='axe'..CH_DN, value=SORT_FNS.sort_by_axe_desc, pen=COLOR_GREEN}, - {label='axe'..CH_UP, value=SORT_FNS.sort_by_axe_asc, pen=COLOR_YELLOW}, + {label='axe'..CH_DN, value=sort_by_axe_desc, pen=COLOR_GREEN}, + {label='axe'..CH_UP, value=sort_by_axe_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_axe'), @@ -633,8 +631,8 @@ function SquadAssignmentOverlay:init() frame={t=8, w=6}, options={ {label='sword', value=sort_noop}, - {label='sword'..CH_DN, value=SORT_FNS.sort_by_sword_desc, pen=COLOR_GREEN}, - {label='sword'..CH_UP, value=SORT_FNS.sort_by_sword_asc, pen=COLOR_YELLOW}, + {label='sword'..CH_DN, value=sort_by_sword_desc, pen=COLOR_GREEN}, + {label='sword'..CH_UP, value=sort_by_sword_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_sword'), @@ -644,8 +642,8 @@ function SquadAssignmentOverlay:init() frame={t=8, r=0, w=5}, options={ {label='mace', value=sort_noop}, - {label='mace'..CH_DN, value=SORT_FNS.sort_by_mace_desc, pen=COLOR_GREEN}, - {label='mace'..CH_UP, value=SORT_FNS.sort_by_mace_asc, pen=COLOR_YELLOW}, + {label='mace'..CH_DN, value=sort_by_mace_desc, pen=COLOR_GREEN}, + {label='mace'..CH_UP, value=sort_by_mace_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_mace'), @@ -655,8 +653,8 @@ function SquadAssignmentOverlay:init() frame={t=10, l=0, w=7}, options={ {label='hammer', value=sort_noop}, - {label='hammer'..CH_DN, value=SORT_FNS.sort_by_hammer_desc, pen=COLOR_GREEN}, - {label='hammer'..CH_UP, value=SORT_FNS.sort_by_hammer_asc, pen=COLOR_YELLOW}, + {label='hammer'..CH_DN, value=sort_by_hammer_desc, pen=COLOR_GREEN}, + {label='hammer'..CH_UP, value=sort_by_hammer_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_hammer'), @@ -666,8 +664,8 @@ function SquadAssignmentOverlay:init() frame={t=10, w=6}, options={ {label='spear', value=sort_noop}, - {label='spear'..CH_DN, value=SORT_FNS.sort_by_spear_desc, pen=COLOR_GREEN}, - {label='spear'..CH_UP, value=SORT_FNS.sort_by_spear_asc, pen=COLOR_YELLOW}, + {label='spear'..CH_DN, value=sort_by_spear_desc, pen=COLOR_GREEN}, + {label='spear'..CH_UP, value=sort_by_spear_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_spear'), @@ -677,8 +675,8 @@ function SquadAssignmentOverlay:init() frame={t=10, r=0, w=9}, options={ {label='crossbow', value=sort_noop}, - {label='crossbow'..CH_DN, value=SORT_FNS.sort_by_crossbow_desc, pen=COLOR_GREEN}, - {label='crossbow'..CH_UP, value=SORT_FNS.sort_by_crossbow_asc, pen=COLOR_YELLOW}, + {label='crossbow'..CH_DN, value=sort_by_crossbow_desc, pen=COLOR_GREEN}, + {label='crossbow'..CH_UP, value=sort_by_crossbow_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_crossbow'), From d0f08dcc0d1cc6c9a9309321c67c80dddec87b8d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 7 Sep 2023 16:14:20 -0700 Subject: [PATCH 03/18] implement rating overlay --- plugins/lua/sort.lua | 319 +++++++++++++++++++++++++++++++------------ 1 file changed, 228 insertions(+), 91 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 3aabec544..4638c8ffe 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -9,31 +9,33 @@ local widgets = require('gui.widgets') local CH_UP = string.char(30) local CH_DN = string.char(31) -local MELEE_WEAPON_SKILLS = { - df.job_skill.AXE, - df.job_skill.SWORD, - df.job_skill.MACE, - df.job_skill.HAMMER, - df.job_skill.SPEAR, -} - -local RANGED_WEAPON_SKILLS = { - df.job_skill.CROSSBOW, -} +local function get_rating(val, max, med, low) + val = math.min(max, val) + local percent = (val * 100) // max + local color = COLOR_GREEN + if percent < (low or 50) then color = COLOR_RED + elseif percent < (med or 75) then color = COLOR_YELLOW + end + return percent, color +end local function sort_noop(a, b) -- this function is used as a marker and never actually gets called error('sort_noop should not be called') end +local function get_name(unit) + return unit and dfhack.toSearchNormalized(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) +end + local function sort_by_name_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local name1 = dfhack.TranslateName(dfhack.units.getVisibleName(unit1)) - local name2 = dfhack.TranslateName(dfhack.units.getVisibleName(unit2)) + local name1 = get_name(unit1) + local name2 = get_name(unit2) return utils.compare_name(name1, name2) end @@ -43,8 +45,8 @@ local function sort_by_name_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local name1 = dfhack.TranslateName(dfhack.units.getVisibleName(unit1)) - local name2 = dfhack.TranslateName(dfhack.units.getVisibleName(unit2)) + local name1 = get_name(unit1) + local name2 = get_name(unit2) return utils.compare_name(name2, name1) end @@ -61,6 +63,11 @@ local function get_active_idx_cache() return active_idx_cache end +local function get_migrant_wave_rating(unit) + -- TODO: return green for most recent wave, red for the first wave, yellow for all others + return 1, nil +end + local function sort_by_migrant_wave_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local cache = get_active_idx_cache() @@ -77,17 +84,29 @@ local function sort_by_migrant_wave_asc(unit_id_1, unit_id_2) return utils.compare(cache[unit_id_1], cache[unit_id_2]) end +local function get_stress(unit) + return unit and + unit.status.current_soul and + unit.status.current_soul.personality.stress +end + +local function get_stress_rating(unit) + return get_rating(-get_stress(unit) + 100000, 200000, 50, 25) +end + local function sort_by_stress_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local happiness1 = unit1.status.current_soul.personality.stress - local happiness2 = unit2.status.current_soul.personality.stress + local happiness1 = get_stress(unit1) + local happiness2 = get_stress(unit2) if happiness1 == happiness2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + if not happiness2 then return -1 end + if not happiness1 then return 1 end return utils.compare(happiness2, happiness1) end @@ -97,22 +116,37 @@ local function sort_by_stress_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local happiness1 = unit1.status.current_soul.personality.stress - local happiness2 = unit2.status.current_soul.personality.stress + local happiness1 = get_stress(unit1) + local happiness2 = get_stress(unit2) if happiness1 == happiness2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + if not happiness2 then return 1 end + if not happiness1 then return -1 end return utils.compare(happiness1, happiness2) end -local function get_skill(unit_id, skill, unit) - unit = unit or df.unit.find(unit_id) +local function get_skill(skill, unit) return unit and unit.status.current_soul and - utils.binsearch(unit.status.current_soul.skills, skill, 'id') + (utils.binsearch(unit.status.current_soul.skills, skill, 'id')) end -local function melee_skill_effectiveness(unit, skill_list) +local function get_skill_rating(skill, unit) + local uskill = get_skill(skill, unit) + if not uskill then return nil end + return get_rating(uskill.rating, 100, 5, 0) +end + +local MELEE_WEAPON_SKILLS = { + df.job_skill.AXE, + df.job_skill.SWORD, + df.job_skill.MACE, + df.job_skill.HAMMER, + df.job_skill.SPEAR, +} + +local function melee_skill_effectiveness(unit) -- Physical attributes local strength = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.STRENGTH) local agility = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.AGILITY) @@ -128,7 +162,7 @@ local function melee_skill_effectiveness(unit, skill_list) -- Skills -- Finding the highest skill local skill_rating = 0 - for _, skill in ipairs(skill_list) do + for _, skill in ipairs(MELEE_WEAPON_SKILLS) do local melee_skill = dfhack.units.getNominalSkill(unit, skill, true) skill_rating = math.max(skill_rating, melee_skill) end @@ -140,36 +174,44 @@ local function melee_skill_effectiveness(unit, skill_list) return rating end -local function make_sort_by_melee_skill_effectiveness_desc(list) +local function get_melee_skill_effectiveness_rating(unit) + return get_rating(melee_skill_effectiveness(unit), 2000000) +end + +local function make_sort_by_melee_skill_effectiveness_desc() return function(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_skill_effectiveness(unit1, list) - local rating2 = melee_skill_effectiveness(unit2, list) + local rating1 = melee_skill_effectiveness(unit1) + local rating2 = melee_skill_effectiveness(unit2) if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end return utils.compare(rating2, rating1) end end -local function make_sort_by_melee_skill_effectiveness_asc(list) +local function make_sort_by_melee_skill_effectiveness_asc() return function(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_skill_effectiveness(unit1, list) - local rating2 = melee_skill_effectiveness(unit2, list) + local rating1 = melee_skill_effectiveness(unit1) + local rating2 = melee_skill_effectiveness(unit2) if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end return utils.compare(rating1, rating2) end end --- FUnction could easily be adapted to different weapon types. -local function ranged_skill_effectiveness(unit, skill_list) +local RANGED_WEAPON_SKILLS = { + df.job_skill.CROSSBOW, +} + +-- Function could easily be adapted to different weapon types. +local function ranged_skill_effectiveness(unit) -- Physical attributes local agility = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.AGILITY) @@ -181,7 +223,7 @@ local function ranged_skill_effectiveness(unit, skill_list) -- Skills -- Finding the highest skill local skill_rating = 0 - for _, skill in ipairs(skill_list) do + for _, skill in ipairs(RANGED_WEAPON_SKILLS) do local ranged_skill = dfhack.units.getNominalSkill(unit, skill, true) skill_rating = math.max(skill_rating, ranged_skill) end @@ -192,6 +234,10 @@ local function ranged_skill_effectiveness(unit, skill_list) return rating end +local function get_ranged_skill_effectiveness_rating(unit) + return get_rating(ranged_skill_effectiveness(unit), 500000) +end + local function make_sort_by_ranged_skill_effectiveness_desc(list) return function(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end @@ -225,8 +271,8 @@ local function make_sort_by_skill_desc(sort_skill) if unit_id_1 == unit_id_2 then return 0 end if unit_id_1 == -1 then return -1 end if unit_id_2 == -1 then return 1 end - local s1 = get_skill(unit_id_1, sort_skill) - local s2 = get_skill(unit_id_2, sort_skill) + local s1 = get_skill(sort_skill, df.unit.find(unit_id_1)) + local s2 = get_skill(sort_skill, df.unit.find(unit_id_2)) if s1 == s2 then return sort_by_name_desc(unit_id_1, unit_id_2) end if not s2 then return -1 end if not s1 then return 1 end @@ -245,8 +291,8 @@ local function make_sort_by_skill_asc(sort_skill) if unit_id_1 == unit_id_2 then return 0 end if unit_id_1 == -1 then return -1 end if unit_id_2 == -1 then return 1 end - local s1 = get_skill(unit_id_1, sort_skill) - local s2 = get_skill(unit_id_2, sort_skill) + local s1 = get_skill(sort_skill, df.unit.find(unit_id_1)) + local s2 = get_skill(sort_skill, df.unit.find(unit_id_2)) if s1 == s2 then return sort_by_name_desc(unit_id_1, unit_id_2) end if not s2 then return 1 end if not s1 then return -1 end @@ -261,7 +307,7 @@ local function make_sort_by_skill_asc(sort_skill) end -- Statistical rating that is higher for dwarves that are mentally stable -local function mental_stability(unit) +local function get_mental_stability(unit) local ALTRUISM = unit.status.current_soul.personality.traits.ALTRUISM local ANXIETY_PROPENSITY = unit.status.current_soul.personality.traits.ANXIETY_PROPENSITY local BRAVERY = unit.status.current_soul.personality.traits.BRAVERY @@ -301,14 +347,18 @@ local function mental_stability(unit) return rating end +local function get_mental_stability_rating(unit) + return get_rating(get_mental_stability(unit), 100, 10, 0) +end + local function sort_by_mental_stability_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = mental_stability(unit1) - local rating2 = mental_stability(unit2) + local rating1 = get_mental_stability(unit1) + local rating2 = get_mental_stability(unit2) if rating1 == rating2 then -- sorting by stress is opposite -- more mental stable dwarves should have less stress @@ -323,8 +373,8 @@ local function sort_by_mental_stability_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = mental_stability(unit1) - local rating2 = mental_stability(unit2) + local rating1 = get_mental_stability(unit1) + local rating2 = get_mental_stability(unit2) if rating1 == rating2 then return sort_by_stress_desc(unit_id_1, unit_id_2) end @@ -334,7 +384,7 @@ end -- Statistical rating that is higher for more potent dwarves in long run melee military training -- Rating considers fighting melee opponents -- Wounds are not considered! -local function melee_combat_potential(unit) +local function get_melee_combat_potential(unit) -- Physical attributes local strength = unit.body.physical_attrs.STRENGTH.max_value local agility = unit.body.physical_attrs.AGILITY.max_value @@ -353,14 +403,18 @@ local function melee_combat_potential(unit) return rating end +local function get_melee_combat_potential_rating(unit) + return get_rating(get_melee_combat_potential(unit), 2000000) +end + local function sort_by_melee_combat_potential_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_combat_potential(unit1) - local rating2 = melee_combat_potential(unit2) + local rating1 = get_melee_combat_potential(unit1) + local rating2 = get_melee_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_desc(unit_id_1, unit_id_2) end @@ -373,8 +427,8 @@ local function sort_by_melee_combat_potential_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_combat_potential(unit1) - local rating2 = melee_combat_potential(unit2) + local rating1 = get_melee_combat_potential(unit1) + local rating2 = get_melee_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_asc(unit_id_1, unit_id_2) end @@ -383,7 +437,7 @@ end -- Statistical rating that is higher for more potent dwarves in long run ranged military training -- Wounds are not considered! -local function ranged_combat_potential(unit) +local function get_ranged_combat_potential(unit) -- Physical attributes local agility = unit.body.physical_attrs.AGILITY.max_value @@ -397,14 +451,18 @@ local function ranged_combat_potential(unit) return rating end +local function get_ranged_combat_potential_rating(unit) + return get_rating(get_ranged_combat_potential(unit), 40000) +end + local function sort_by_ranged_combat_potential_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = ranged_combat_potential(unit1) - local rating2 = ranged_combat_potential(unit2) + local rating1 = get_ranged_combat_potential(unit1) + local rating2 = get_ranged_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_desc(unit_id_1, unit_id_2) end @@ -417,18 +475,18 @@ local function sort_by_ranged_combat_potential_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = ranged_combat_potential(unit1) - local rating2 = ranged_combat_potential(unit2) + local rating1 = get_ranged_combat_potential(unit1) + local rating2 = get_ranged_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_asc(unit_id_1, unit_id_2) end return utils.compare(rating1, rating2) end -local sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc(MELEE_WEAPON_SKILLS) -local sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc(MELEE_WEAPON_SKILLS) -local sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc(RANGED_WEAPON_SKILLS) -local sort_by_any_ranged_asc=make_sort_by_ranged_skill_effectiveness_asc(RANGED_WEAPON_SKILLS) +local sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc() +local sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc() +local sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc() +local sort_by_any_ranged_asc=make_sort_by_ranged_skill_effectiveness_asc() local sort_by_teacher_desc=make_sort_by_skill_desc(df.job_skill.TEACHING) local sort_by_teacher_asc=make_sort_by_skill_asc(df.job_skill.TEACHING) local sort_by_tactics_desc=make_sort_by_skill_desc(df.job_skill.MILITARY_TACTICS) @@ -446,13 +504,38 @@ local sort_by_spear_asc=make_sort_by_skill_asc(df.job_skill.SPEAR) local sort_by_crossbow_desc=make_sort_by_skill_desc(df.job_skill.CROSSBOW) local sort_by_crossbow_asc=make_sort_by_skill_asc(df.job_skill.CROSSBOW) +local SORT_LIBRARY = { + {label='melee effectiveness', desc_fn=sort_by_any_melee_desc, asc_fn=sort_by_any_melee_asc, rating_fn=get_melee_skill_effectiveness_rating}, + {label='ranged effectiveness', desc_fn=sort_by_any_ranged_desc, asc_fn=sort_by_any_ranged_asc, rating_fn=get_ranged_skill_effectiveness_rating}, + {label='name', desc_fn=sort_by_name_desc, asc_fn=sort_by_name_asc}, + {label='teacher skill', desc_fn=sort_by_teacher_desc, asc_fn=sort_by_teacher_asc, rating_fn=curry(get_skill_rating, df.job_skill.TEACHING)}, + {label='tactics skill', desc_fn=sort_by_tactics_desc, asc_fn=sort_by_tactics_asc, rating_fn=curry(get_skill_rating, df.job_skill.MILITARY_TACTICS)}, + {label='migrant wave', desc_fn=sort_by_migrant_wave_desc, asc_fn=sort_by_migrant_wave_asc, rating_fn=get_migrant_wave_rating}, + {label='stress level', desc_fn=sort_by_stress_desc, asc_fn=sort_by_stress_asc, rating_fn=get_stress_rating}, + {label='mental stability', desc_fn=sort_by_mental_stability_desc, asc_fn=sort_by_mental_stability_asc, rating_fn=get_mental_stability_rating}, + {label='axe skill', desc_fn=sort_by_axe_desc, asc_fn=sort_by_axe_asc, rating_fn=curry(get_skill_rating, df.job_skill.AXE)}, + {label='sword skill', desc_fn=sort_by_sword_desc, asc_fn=sort_by_sword_asc, rating_fn=curry(get_skill_rating, df.job_skill.SWORD)}, + {label='mace skill', desc_fn=sort_by_mace_desc, asc_fn=sort_by_mace_asc, rating_fn=curry(get_skill_rating, df.job_skill.MACE)}, + {label='hammer skill', desc_fn=sort_by_hammer_desc, asc_fn=sort_by_hammer_asc, rating_fn=curry(get_skill_rating, df.job_skill.HAMMER)}, + {label='spear skill', desc_fn=sort_by_spear_desc, asc_fn=sort_by_spear_asc, rating_fn=curry(get_skill_rating, df.job_skill.SPEAR)}, + {label='crossbow skill', desc_fn=sort_by_crossbow_desc, asc_fn=sort_by_crossbow_asc, rating_fn=curry(get_skill_rating, df.job_skill.CROSSBOW)}, + {label='melee potential', desc_fn=sort_by_melee_combat_potential_desc, asc_fn=sort_by_melee_combat_potential_asc, rating_fn=get_melee_combat_potential_rating}, + {label='ranged potential', desc_fn=sort_by_ranged_combat_potential_desc, asc_fn=sort_by_ranged_combat_potential_asc, rating_fn=get_ranged_combat_potential_rating}, +} + +local RATING_FNS = {} +for _, opt in ipairs(SORT_LIBRARY) do + RATING_FNS[opt.desc_fn] = opt.rating_fn + RATING_FNS[opt.asc_fn] = opt.rating_fn +end + -- ---------------------- -- SquadAssignmentOverlay -- SquadAssignmentOverlay = defclass(SquadAssignmentOverlay, overlay.OverlayWidget) SquadAssignmentOverlay.ATTRS{ - default_pos={x=23, y=5}, + default_pos={x=18, y=5}, default_enabled=true, viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', frame={w=38, h=25}, @@ -472,6 +555,20 @@ end function SquadAssignmentOverlay:init() self.dirty = true + local sort_options = {} + for _, opt in ipairs(SORT_LIBRARY) do + table.insert(sort_options, { + label=opt.label..CH_DN, + value=opt.desc_fn, + pen=COLOR_GREEN, + }) + table.insert(sort_options, { + label=opt.label..CH_UP, + value=opt.asc_fn, + pen=COLOR_YELLOW, + }) + end + self:addviews{ widgets.EditField{ view_id='search', @@ -489,40 +586,7 @@ function SquadAssignmentOverlay:init() frame={t=0, l=0}, label='Sort by:', key='CUSTOM_SHIFT_S', - options={ - {label='melee effectiveness'..CH_DN, value=sort_by_any_melee_desc, pen=COLOR_GREEN}, - {label='melee effectiveness'..CH_UP, value=sort_by_any_melee_asc, pen=COLOR_YELLOW}, - {label='ranged effectiveness'..CH_DN, value=sort_by_any_ranged_desc, pen=COLOR_GREEN}, - {label='ranged effectiveness'..CH_UP, value=sort_by_any_ranged_asc, pen=COLOR_YELLOW}, - {label='name'..CH_DN, value=sort_by_name_desc, pen=COLOR_GREEN}, - {label='name'..CH_UP, value=sort_by_name_asc, pen=COLOR_YELLOW}, - {label='teacher skill'..CH_DN, value=sort_by_teacher_desc, pen=COLOR_GREEN}, - {label='teacher skill'..CH_UP, value=sort_by_teacher_asc, pen=COLOR_YELLOW}, - {label='tactics skill'..CH_DN, value=sort_by_tactics_desc, pen=COLOR_GREEN}, - {label='tactics skill'..CH_UP, value=sort_by_tactics_asc, pen=COLOR_YELLOW}, - {label='migrant wave'..CH_DN, value=sort_by_migrant_wave_desc, pen=COLOR_GREEN}, - {label='migrant wave'..CH_UP, value=sort_by_migrant_wave_asc, pen=COLOR_YELLOW}, - {label='stress level'..CH_DN, value=sort_by_stress_desc, pen=COLOR_GREEN}, - {label='stress level'..CH_UP, value=sort_by_stress_asc, pen=COLOR_YELLOW}, - {label='mental stability'..CH_DN, value=sort_by_mental_stability_desc, pen=COLOR_GREEN}, - {label='mental stability'..CH_UP, value=sort_by_mental_stability_asc, pen=COLOR_YELLOW}, - {label='axe skill'..CH_DN, value=sort_by_axe_desc, pen=COLOR_GREEN}, - {label='axe skill'..CH_UP, value=sort_by_axe_asc, pen=COLOR_YELLOW}, - {label='sword skill'..CH_DN, value=sort_by_sword_desc, pen=COLOR_GREEN}, - {label='sword skill'..CH_UP, value=sort_by_sword_asc, pen=COLOR_YELLOW}, - {label='mace skill'..CH_DN, value=sort_by_mace_desc, pen=COLOR_GREEN}, - {label='mace skill'..CH_UP, value=sort_by_mace_asc, pen=COLOR_YELLOW}, - {label='hammer skill'..CH_DN, value=sort_by_hammer_desc, pen=COLOR_GREEN}, - {label='hammer skill'..CH_UP, value=sort_by_hammer_asc, pen=COLOR_YELLOW}, - {label='spear skill'..CH_DN, value=sort_by_spear_desc, pen=COLOR_GREEN}, - {label='spear skill'..CH_UP, value=sort_by_spear_asc, pen=COLOR_YELLOW}, - {label='crossbow skill'..CH_DN, value=sort_by_crossbow_desc, pen=COLOR_GREEN}, - {label='crossbow skill'..CH_UP, value=sort_by_crossbow_asc, pen=COLOR_YELLOW}, - {label='melee potential'..CH_DN, value=sort_by_melee_combat_potential_desc, pen=COLOR_GREEN}, - {label='melee potential'..CH_UP, value=sort_by_melee_combat_potential_asc, pen=COLOR_YELLOW}, - {label='ranged potential'..CH_DN, value=sort_by_ranged_combat_potential_desc, pen=COLOR_GREEN}, - {label='ranged potential'..CH_UP, value=sort_by_ranged_combat_potential_asc, pen=COLOR_YELLOW}, - }, + options=sort_options, initial_option=sort_by_any_melee_desc, on_change=self:callback('refresh_list', 'sort'), }, @@ -846,6 +910,29 @@ local function filter_vector(filter, prev_filter) unit_selector.unid:erase(idx) end end + -- fix up scroll position if it would be off the end of the list + if unit_selector.scroll_position + 10 > #unit_selector.unid then + unit_selector.scroll_position = math.max(0, #unit_selector.unid - 10) + end +end + +local rating_annotations = {} + +local function annotate_visible_units(sort_fn) + rating_annotations = {} + rating_fn = RATING_FNS[sort_fn] + local max_idx = math.min(#unit_selector.unid-1, unit_selector.scroll_position+9) + for idx = unit_selector.scroll_position, max_idx do + local annotation_idx = idx - unit_selector.scroll_position + 1 + local unit = df.unit.find(unit_selector.unid[idx]) + rating_annotations[annotation_idx] = nil + if unit and rating_fn then + local val, color = rating_fn(unit) + if val then + rating_annotations[annotation_idx] = {val=val, color=color} + end + end + end end local SORT_WIDGET_NAMES = { @@ -887,6 +974,8 @@ function SquadAssignmentOverlay:refresh_list(sort_widget, sort_fn) filter_vector(filter, self.prev_filter or {}) self.prev_filter = filter utils.sort_vector(unit_selector.unid, nil, sort_fn) + annotate_visible_units(sort_fn) + self.saved_scroll_position = unit_selector.scroll_position end function SquadAssignmentOverlay:onInput(keys) @@ -904,11 +993,59 @@ function SquadAssignmentOverlay:onRenderFrame(dc, frame_rect) if self.dirty then self:refresh_list() self.dirty = false + elseif self.saved_scroll_position ~= unit_selector.scroll_position then + annotate_visible_units(self.subviews.sort:getOptionValue()) + self.saved_scroll_position = unit_selector.scroll_position + end +end + +-- ---------------------- +-- SquadAnnotationOverlay +-- + +SquadAnnotationOverlay = defclass(SquadAnnotationOverlay, overlay.OverlayWidget) +SquadAnnotationOverlay.ATTRS{ + default_pos={x=56, y=5}, + default_enabled=true, + viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', + frame={w=5, h=35}, + frame_style=gui.FRAME_INTERIOR_MEDIUM, + frame_background=gui.CLEAR_PEN, +} + +function get_annotation_text(idx) + local elem = rating_annotations[idx] + if not elem or not tonumber(elem.val) then return ' - ' end + + return tostring(math.tointeger(elem.val)) +end + +function get_annotation_color(idx) + local elem = rating_annotations[idx] + return elem and elem.color or nil +end + +function SquadAnnotationOverlay:init() + for idx = 1, 10 do + self:addviews{ + widgets.Label{ + frame={t=idx*3+1, h=1, w=3}, + text={ + { + text=curry(get_annotation_text, idx), + pen=curry(get_annotation_color, idx), + width=3, + rjustify=true, + }, + }, + }, + } end end OVERLAY_WIDGETS = { squad_assignment=SquadAssignmentOverlay, + squad_annotation=SquadAnnotationOverlay, } --[[ From 0d366740e76e9ae006dff1448b88be78e6880ae9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 7 Sep 2023 18:29:29 -0700 Subject: [PATCH 04/18] move the dimensions readout out from under the heat safety filter --- docs/changelog.txt | 1 + plugins/lua/buildingplan/planneroverlay.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9b0a1d30e..e192fda72 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: ## New Features ## Fixes +- `buildingplan`: make the construction dimensions readout visible again ## Misc Improvements diff --git a/plugins/lua/buildingplan/planneroverlay.lua b/plugins/lua/buildingplan/planneroverlay.lua index 9a47b8fed..ebd8e6e02 100644 --- a/plugins/lua/buildingplan/planneroverlay.lua +++ b/plugins/lua/buildingplan/planneroverlay.lua @@ -478,7 +478,7 @@ function PlannerOverlay:init() end, }, widgets.Label{ - frame={b=2, l=23}, + frame={b=4, l=23}, text_pen=COLOR_DARKGREY, text={ 'Selected area: ', From 5f32042f1a70b69cc893a7323e9afe8a27d51486 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 16:44:59 -0700 Subject: [PATCH 05/18] adjust colorization of ratings move mental stability from a sort to a filter --- plugins/lua/sort.lua | 176 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 146 insertions(+), 30 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 4638c8ffe..96280a79a 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -9,14 +9,15 @@ local widgets = require('gui.widgets') local CH_UP = string.char(30) local CH_DN = string.char(31) -local function get_rating(val, max, med, low) - val = math.min(max, val) - local percent = (val * 100) // max - local color = COLOR_GREEN - if percent < (low or 50) then color = COLOR_RED - elseif percent < (med or 75) then color = COLOR_YELLOW - end - return percent, color +local function get_rating(val, baseline, range, highest, high, med, low) + val = val - (baseline or 0) + range = range or 100 + local percentile = (math.min(range, val) * 100) // range + if percentile < (low or 25) then return percentile, COLOR_RED end + if percentile < (med or 50) then return percentile, COLOR_LIGHTRED end + if percentile < (high or 75) then return percentile, COLOR_YELLOW end + if percentile < (highest or 90) then return percentile, COLOR_GREEN end + return percentile, COLOR_LIGHTGREEN end local function sort_noop(a, b) @@ -91,7 +92,7 @@ local function get_stress(unit) end local function get_stress_rating(unit) - return get_rating(-get_stress(unit) + 100000, 200000, 50, 25) + return get_rating(dfhack.units.getStressCategory(unit), 0, 100, 4, 3, 2, 1) end local function sort_by_stress_desc(unit_id_1, unit_id_2) @@ -135,7 +136,7 @@ end local function get_skill_rating(skill, unit) local uskill = get_skill(skill, unit) if not uskill then return nil end - return get_rating(uskill.rating, 100, 5, 0) + return get_rating(uskill.rating, 0, 100, 10, 5, 1, 0) end local MELEE_WEAPON_SKILLS = { @@ -175,7 +176,7 @@ local function melee_skill_effectiveness(unit) end local function get_melee_skill_effectiveness_rating(unit) - return get_rating(melee_skill_effectiveness(unit), 2000000) + return get_rating(melee_skill_effectiveness(unit), 350000, 2350000, 78, 64, 49, 35) end local function make_sort_by_melee_skill_effectiveness_desc() @@ -235,7 +236,7 @@ local function ranged_skill_effectiveness(unit) end local function get_ranged_skill_effectiveness_rating(unit) - return get_rating(ranged_skill_effectiveness(unit), 500000) + return get_rating(ranged_skill_effectiveness(unit), 0, 500000, 90, 62, 44, 27) end local function make_sort_by_ranged_skill_effectiveness_desc(list) @@ -347,10 +348,6 @@ local function get_mental_stability(unit) return rating end -local function get_mental_stability_rating(unit) - return get_rating(get_mental_stability(unit), 100, 10, 0) -end - local function sort_by_mental_stability_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) @@ -404,7 +401,7 @@ local function get_melee_combat_potential(unit) end local function get_melee_combat_potential_rating(unit) - return get_rating(get_melee_combat_potential(unit), 2000000) + return get_rating(get_melee_combat_potential(unit), 300000, 2600000, 81, 64, 46, 29) end local function sort_by_melee_combat_potential_desc(unit_id_1, unit_id_2) @@ -447,12 +444,12 @@ local function get_ranged_combat_potential(unit) local kinesthetic_sense = unit.status.current_soul.mental_attrs.KINESTHETIC_SENSE.max_value -- ranged combat potential formula - local rating = agility * 5 + kinesthetic_sense * 5 + spatial_sense * 2 + focus * 2 + local rating = agility * 5 + kinesthetic_sense * 2 + spatial_sense * 5 + focus * 2 return rating end local function get_ranged_combat_potential_rating(unit) - return get_rating(get_ranged_combat_potential(unit), 40000) + return get_rating(get_ranged_combat_potential(unit), 0, 70000, 73, 57, 41, 25) end local function sort_by_ranged_combat_potential_desc(unit_id_1, unit_id_2) @@ -483,6 +480,54 @@ local function sort_by_ranged_combat_potential_asc(unit_id_1, unit_id_2) return utils.compare(rating1, rating2) end +local function get_need(unit) + if not unit or not unit.status.current_soul then return end + for _, need in ipairs(unit.status.current_soul.personality.needs) do + if need.id == df.need_type.MartialTraining and need.focus_level < 0 then + return -need.focus_level + end + end +end + +local function get_need_rating(unit) + local focus_level = get_need(unit) + if not focus_level then return end + focus_level = math.min(focus_level, 100000) + return get_rating(100000 - focus_level, 0, 100000, 100, 99, 90, 0) +end + +local function sort_by_need_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_need(unit1) + local rating2 = get_need(unit2) + if rating1 == rating2 then + return sort_by_stress_desc(unit_id_1, unit_id_2) + end + if not rating2 then return -1 end + if not rating1 then return 1 end + return utils.compare(rating2, rating1) +end + +local function sort_by_need_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_need(unit1) + local rating2 = get_need(unit2) + if rating1 == rating2 then + return sort_by_stress_asc(unit_id_1, unit_id_2) + end + if not rating2 then return 1 end + if not rating1 then return -1 end + return utils.compare(rating1, rating2) +end + local sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc() local sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc() local sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc() @@ -512,7 +557,7 @@ local SORT_LIBRARY = { {label='tactics skill', desc_fn=sort_by_tactics_desc, asc_fn=sort_by_tactics_asc, rating_fn=curry(get_skill_rating, df.job_skill.MILITARY_TACTICS)}, {label='migrant wave', desc_fn=sort_by_migrant_wave_desc, asc_fn=sort_by_migrant_wave_asc, rating_fn=get_migrant_wave_rating}, {label='stress level', desc_fn=sort_by_stress_desc, asc_fn=sort_by_stress_asc, rating_fn=get_stress_rating}, - {label='mental stability', desc_fn=sort_by_mental_stability_desc, asc_fn=sort_by_mental_stability_asc, rating_fn=get_mental_stability_rating}, + {label='need for training', desc_fn=sort_by_need_desc, asc_fn=sort_by_need_asc, rating_fn=get_need_rating}, {label='axe skill', desc_fn=sort_by_axe_desc, asc_fn=sort_by_axe_asc, rating_fn=curry(get_skill_rating, df.job_skill.AXE)}, {label='sword skill', desc_fn=sort_by_sword_desc, asc_fn=sort_by_sword_asc, rating_fn=curry(get_skill_rating, df.job_skill.SWORD)}, {label='mace skill', desc_fn=sort_by_mace_desc, asc_fn=sort_by_mace_asc, rating_fn=curry(get_skill_rating, df.job_skill.MACE)}, @@ -538,7 +583,7 @@ SquadAssignmentOverlay.ATTRS{ default_pos={x=18, y=5}, default_enabled=true, viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', - frame={w=38, h=25}, + frame={w=38, h=31}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, autoarrange_subviews=true, @@ -669,15 +714,15 @@ function SquadAssignmentOverlay:init() on_change=self:callback('refresh_list', 'sort_stress'), }, widgets.CycleHotkeyLabel{ - view_id='sort_mental_stability', - frame={t=6, r=0, w=17}, + view_id='sort_need', + frame={t=6, r=0, w=18}, options={ - {label='mental stability', value=sort_noop}, - {label='mental stability'..CH_DN, value=sort_by_mental_stability_desc, pen=COLOR_GREEN}, - {label='mental stability'..CH_UP, value=sort_by_mental_stability_asc, pen=COLOR_YELLOW}, + {label='need for training', value=sort_noop}, + {label='need for training'..CH_DN, value=sort_by_need_desc, pen=COLOR_GREEN}, + {label='need for training'..CH_UP, value=sort_by_need_asc, pen=COLOR_YELLOW}, }, option_gap=0, - on_change=self:callback('refresh_list', 'sort_mental_stability'), + on_change=self:callback('refresh_list', 'sort_need'), }, widgets.CycleHotkeyLabel{ view_id='sort_axe', @@ -808,6 +853,45 @@ function SquadAssignmentOverlay:init() initial_option='include', on_change=function() self:refresh_list() end, }, + widgets.CycleHotkeyLabel{ + view_id='infant', + frame={l=0}, + key='CUSTOM_SHIFT_M', + label='Mothers carrying infants:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='unstable', + frame={l=0}, + key='CUSTOM_SHIFT_U', + label='Easily stressed units:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='maimed', + frame={l=0}, + key='CUSTOM_SHIFT_I', + label='Critically injured:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, } end @@ -846,6 +930,23 @@ local function is_nobility(unit) return false end +local function has_infant(unit) + -- TODO + return false +end + +local function is_unstable(unit) + -- stddev percentiles are 61, 48, 35, 23 + -- let's go with one stddev below the mean (35) as the cutoff + local _, color = get_rating(get_mental_stability(unit), -40, 80, 35, 0, 0, 0) + return color ~= COLOR_LIGHTGREEN +end + +local function is_maimed(unit) + -- TODO + return false +end + local function filter_matches(unit_id, filter) if unit_id == -1 then return true end local unit = df.unit.find(unit_id) @@ -856,6 +957,12 @@ local function filter_matches(unit_id, filter) if filter.officials == 'exclude' and is_elected_or_appointed_official(unit) then return false end if filter.nobles == 'only' and not is_nobility(unit) then return false end if filter.nobles == 'exclude' and is_nobility(unit) then return false end + if filter.infant == 'only' and not has_infant(unit) then return false end + if filter.infant == 'exclude' and has_infant(unit) then return false end + if filter.unstable == 'only' and not is_unstable(unit) then return false end + if filter.unstable == 'exclude' and is_unstable(unit) then return false end + if filter.maimed == 'only' and not is_maimed(unit) then return false end + if filter.maimed == 'exclude' and is_maimed(unit) then return false end if #filter.search == 0 then return true end local search_key = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) return normalize_search_key(search_key):find(dfhack.toSearchNormalized(filter.search)) @@ -865,14 +972,20 @@ local function is_noop_filter(filter) return #filter.search == 0 and filter.military == 'include' and filter.officials == 'include' and - filter.nobles == 'include' + filter.nobles == 'include' and + filter.infant == 'include' and + filter.unstable == 'include' and + filter.maimed == 'include' end local function is_filter_equal(a, b) return a.search == b.search and a.military == b.military and a.officials == b.officials and - a.nobles == b.nobles + a.nobles == b.nobles and + a.infant == b.infant and + a.unstable == b.unstable and + a.maimed == b.maimed end local unit_selector = df.global.game.main_interface.unit_selector @@ -944,7 +1057,7 @@ local SORT_WIDGET_NAMES = { 'sort_tactics', 'sort_migrant_wave', 'sort_stress', - 'sort_mental_stability', + 'sort_need', 'sort_axe', 'sort_sword', 'sort_mace', @@ -970,6 +1083,9 @@ function SquadAssignmentOverlay:refresh_list(sort_widget, sort_fn) military=self.subviews.military:getOptionValue(), officials=self.subviews.officials:getOptionValue(), nobles=self.subviews.nobles:getOptionValue(), + infant=self.subviews.infant:getOptionValue(), + unstable=self.subviews.unstable:getOptionValue(), + maimed=self.subviews.maimed:getOptionValue(), } filter_vector(filter, self.prev_filter or {}) self.prev_filter = filter From 603f1b16c2dece804d80dac2a6ed50224cb2c277 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 18:48:05 -0700 Subject: [PATCH 06/18] use stress face icons for stress and training need --- plugins/lua/sort.lua | 83 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 96280a79a..30205b300 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -492,8 +492,12 @@ end local function get_need_rating(unit) local focus_level = get_need(unit) if not focus_level then return end - focus_level = math.min(focus_level, 100000) - return get_rating(100000 - focus_level, 0, 100000, 100, 99, 90, 0) + -- convert to stress ratings so we can use stress faces as labels + if focus_level > 100000 then return 0 end + if focus_level > 10000 then return 1 end + if focus_level > 1000 then return 2 end + if focus_level > 100 then return 3 end + return 6 end local function sort_by_need_desc(unit_id_1, unit_id_2) @@ -556,8 +560,8 @@ local SORT_LIBRARY = { {label='teacher skill', desc_fn=sort_by_teacher_desc, asc_fn=sort_by_teacher_asc, rating_fn=curry(get_skill_rating, df.job_skill.TEACHING)}, {label='tactics skill', desc_fn=sort_by_tactics_desc, asc_fn=sort_by_tactics_asc, rating_fn=curry(get_skill_rating, df.job_skill.MILITARY_TACTICS)}, {label='migrant wave', desc_fn=sort_by_migrant_wave_desc, asc_fn=sort_by_migrant_wave_asc, rating_fn=get_migrant_wave_rating}, - {label='stress level', desc_fn=sort_by_stress_desc, asc_fn=sort_by_stress_asc, rating_fn=get_stress_rating}, - {label='need for training', desc_fn=sort_by_need_desc, asc_fn=sort_by_need_asc, rating_fn=get_need_rating}, + {label='stress level', desc_fn=sort_by_stress_desc, asc_fn=sort_by_stress_asc, rating_fn=get_stress_rating, use_stress_faces=true}, + {label='need for training', desc_fn=sort_by_need_desc, asc_fn=sort_by_need_asc, rating_fn=get_need_rating, use_stress_faces=true}, {label='axe skill', desc_fn=sort_by_axe_desc, asc_fn=sort_by_axe_asc, rating_fn=curry(get_skill_rating, df.job_skill.AXE)}, {label='sword skill', desc_fn=sort_by_sword_desc, asc_fn=sort_by_sword_asc, rating_fn=curry(get_skill_rating, df.job_skill.SWORD)}, {label='mace skill', desc_fn=sort_by_mace_desc, asc_fn=sort_by_mace_asc, rating_fn=curry(get_skill_rating, df.job_skill.MACE)}, @@ -569,9 +573,14 @@ local SORT_LIBRARY = { } local RATING_FNS = {} +local STRESS_FACE_FNS = {} for _, opt in ipairs(SORT_LIBRARY) do RATING_FNS[opt.desc_fn] = opt.rating_fn RATING_FNS[opt.asc_fn] = opt.rating_fn + if opt.use_stress_faces then + STRESS_FACE_FNS[opt.desc_fn] = true + STRESS_FACE_FNS[opt.asc_fn] = true + end end -- ---------------------- @@ -857,7 +866,7 @@ function SquadAssignmentOverlay:init() view_id='infant', frame={l=0}, key='CUSTOM_SHIFT_M', - label='Mothers carrying infants:', + label='Mothers with infants:', options={ {label='Include', value='include', pen=COLOR_GREEN}, {label='Only', value='only', pen=COLOR_YELLOW}, @@ -869,8 +878,8 @@ function SquadAssignmentOverlay:init() widgets.CycleHotkeyLabel{ view_id='unstable', frame={l=0}, - key='CUSTOM_SHIFT_U', - label='Easily stressed units:', + key='CUSTOM_SHIFT_F', + label='Weak mental fortitude:', options={ {label='Include', value='include', pen=COLOR_GREEN}, {label='Only', value='only', pen=COLOR_YELLOW}, @@ -931,7 +940,11 @@ local function is_nobility(unit) end local function has_infant(unit) - -- TODO + for _, baby in ipairs(df.global.world.units.other.ANY_BABY2) do + if baby.relationship_ids.Mother == unit.id then + return true + end + end return false end @@ -943,8 +956,9 @@ local function is_unstable(unit) end local function is_maimed(unit) - -- TODO - return false + return unit.flags2.vision_missing or + unit.status2.limbs_grasp_count == 0 or + unit.status2.limbs_stand_count == 0 end local function filter_matches(unit_id, filter) @@ -1029,9 +1043,11 @@ local function filter_vector(filter, prev_filter) end end +local use_stress_faces = false local rating_annotations = {} local function annotate_visible_units(sort_fn) + use_stress_faces = STRESS_FACE_FNS[sort_fn] rating_annotations = {} rating_fn = RATING_FNS[sort_fn] local max_idx = math.min(#unit_selector.unid-1, unit_selector.scroll_position+9) @@ -1141,6 +1157,40 @@ function get_annotation_color(idx) return elem and elem.color or nil end +local to_pen = dfhack.pen.parse +local DASH_PEN = to_pen{ch='-', fg=COLOR_WHITE, keep_lower=true} + +local FACE_TILES = {} +for idx=0,6 do + FACE_TILES[idx] = {} + local face_off = (6 - idx) * 2 + for y=0,1 do + for x=0,1 do + local tile = dfhack.screen.findGraphicsTile('INTERFACE_BITS', 32 + face_off + x, 6 + y) + ensure_key(FACE_TILES[idx], y)[x] = tile + end + end +end + +local ASCII_FACE_TILES = {} +for idx,color in ipairs{COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_WHITE, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} do + local face = {} + ensure_key(face, 0)[0] = to_pen{ch=1, fg=color} + ensure_key(face, 0)[1] = to_pen{ch='\\', fg=color} + ensure_key(face, 1)[0] = to_pen{ch='\\', fg=color} + ensure_key(face, 1)[1] = to_pen{ch='/', fg=color} + ASCII_FACE_TILES[idx-1] = face +end + +function get_stress_face_tile(idx, x, y) + local elem = rating_annotations[idx] + if not elem or not elem.val or elem.val < 0 then + return x == 0 and y == 1 and DASH_PEN or gui.CLEAR_PEN + end + local val = math.min(6, elem.val) + return (dfhack.screen.inGraphicsMode() and FACE_TILES or ASCII_FACE_TILES)[val][y][x] +end + function SquadAnnotationOverlay:init() for idx = 1, 10 do self:addviews{ @@ -1154,6 +1204,19 @@ function SquadAnnotationOverlay:init() rjustify=true, }, }, + visible=function() return not use_stress_faces end, + }, + widgets.Label{ + frame={t=idx*3, r=0, h=2, w=2}, + auto_height=false, + text={ + {width=1, tile=curry(get_stress_face_tile, idx, 0, 0)}, + {width=1, tile=curry(get_stress_face_tile, idx, 1, 0)}, + NEWLINE, + {width=1, tile=curry(get_stress_face_tile, idx, 0, 1)}, + {width=1, tile=curry(get_stress_face_tile, idx, 1, 1)}, + }, + visible=function() return use_stress_faces end, }, } end From 1e9e38a0de23620710c97f289bbf8e5b4748b3cd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 20:06:51 -0700 Subject: [PATCH 07/18] implement ratings for arrival order --- plugins/lua/sort.lua | 62 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 30205b300..ad4f78ed6 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -64,12 +64,46 @@ local function get_active_idx_cache() return active_idx_cache end -local function get_migrant_wave_rating(unit) - -- TODO: return green for most recent wave, red for the first wave, yellow for all others - return 1, nil +local function is_original_dwarf(unit) + return df.global.plotinfo.fortress_age == unit.curse.time_on_site // 10 end -local function sort_by_migrant_wave_desc(unit_id_1, unit_id_2) +local WAVE_END_GAP = 10000 + +local function get_most_recent_wave_oldest_active_idx(cache) + local oldest_unit + for idx=#active_units-1,0,-1 do + local unit = active_units[idx] + if not dfhack.units.isCitizen(unit) then goto continue end + if oldest_unit and unit.curse.time_on_site - oldest_unit.curse.time_on_site > WAVE_END_GAP then + return cache[oldest_unit.id] + else + oldest_unit = unit + end + ::continue:: + end +end + +-- return green for most recent wave, red for the first wave, yellow for all others +-- rating is a three digit number that indicates the (potentially approximate) order +local function get_arrival_rating(unit) + local cache = get_active_idx_cache() + local unit_active_idx = cache[unit.id] + if not unit_active_idx then return end + local most_recent_wave_oldest_active_idx = get_most_recent_wave_oldest_active_idx(cache) + if not most_recent_wave_oldest_active_idx then return end + local num_active_units = #active_units + local rating = num_active_units < 1000 and unit_active_idx or ((unit_active_idx * 1000) // #active_units) + if most_recent_wave_oldest_active_idx < unit_active_idx then + return rating, COLOR_LIGHTGREEN + end + if is_original_dwarf(unit) then + return rating, COLOR_RED + end + return rating, COLOR_YELLOW +end + +local function sort_by_arrival_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local cache = get_active_idx_cache() if not cache[unit_id_1] then return -1 end @@ -77,7 +111,7 @@ local function sort_by_migrant_wave_desc(unit_id_1, unit_id_2) return utils.compare(cache[unit_id_2], cache[unit_id_1]) end -local function sort_by_migrant_wave_asc(unit_id_1, unit_id_2) +local function sort_by_arrival_asc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local cache = get_active_idx_cache() if not cache[unit_id_1] then return -1 end @@ -559,7 +593,7 @@ local SORT_LIBRARY = { {label='name', desc_fn=sort_by_name_desc, asc_fn=sort_by_name_asc}, {label='teacher skill', desc_fn=sort_by_teacher_desc, asc_fn=sort_by_teacher_asc, rating_fn=curry(get_skill_rating, df.job_skill.TEACHING)}, {label='tactics skill', desc_fn=sort_by_tactics_desc, asc_fn=sort_by_tactics_asc, rating_fn=curry(get_skill_rating, df.job_skill.MILITARY_TACTICS)}, - {label='migrant wave', desc_fn=sort_by_migrant_wave_desc, asc_fn=sort_by_migrant_wave_asc, rating_fn=get_migrant_wave_rating}, + {label='arrival order', desc_fn=sort_by_arrival_desc, asc_fn=sort_by_arrival_asc, rating_fn=get_arrival_rating}, {label='stress level', desc_fn=sort_by_stress_desc, asc_fn=sort_by_stress_asc, rating_fn=get_stress_rating, use_stress_faces=true}, {label='need for training', desc_fn=sort_by_need_desc, asc_fn=sort_by_need_asc, rating_fn=get_need_rating, use_stress_faces=true}, {label='axe skill', desc_fn=sort_by_axe_desc, asc_fn=sort_by_axe_asc, rating_fn=curry(get_skill_rating, df.job_skill.AXE)}, @@ -701,15 +735,15 @@ function SquadAssignmentOverlay:init() on_change=self:callback('refresh_list', 'sort_tactics'), }, widgets.CycleHotkeyLabel{ - view_id='sort_migrant_wave', - frame={t=4, r=0, w=13}, + view_id='sort_arrival', + frame={t=4, r=0, w=14}, options={ - {label='migrant wave', value=sort_noop}, - {label='migrant wave'..CH_DN, value=sort_by_migrant_wave_desc, pen=COLOR_GREEN}, - {label='migrant wave'..CH_UP, value=sort_by_migrant_wave_asc, pen=COLOR_YELLOW}, + {label='arrival order', value=sort_noop}, + {label='arrival order'..CH_DN, value=sort_by_arrival_desc, pen=COLOR_GREEN}, + {label='arrival order'..CH_UP, value=sort_by_arrival_asc, pen=COLOR_YELLOW}, }, option_gap=0, - on_change=self:callback('refresh_list', 'sort_migrant_wave'), + on_change=self:callback('refresh_list', 'sort_arrival'), }, widgets.CycleHotkeyLabel{ view_id='sort_stress', @@ -957,7 +991,7 @@ end local function is_maimed(unit) return unit.flags2.vision_missing or - unit.status2.limbs_grasp_count == 0 or + unit.status2.limbs_grasp_count < 2 or unit.status2.limbs_stand_count == 0 end @@ -1071,7 +1105,7 @@ local SORT_WIDGET_NAMES = { 'sort_name', 'sort_teacher', 'sort_tactics', - 'sort_migrant_wave', + 'sort_arrival', 'sort_stress', 'sort_need', 'sort_axe', From 9bcb31f1eb6c09df1dc323cf32a01a4f1238a38d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 20:09:55 -0700 Subject: [PATCH 08/18] update changelog --- docs/changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9b0a1d30e..01dc0e277 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,9 @@ Template for new versions: ## Fixes ## Misc Improvements +- `sort`: sort by need for training on squad assignment screen +- `sort`: filter mothers with infants, units with weak mental fortitude, and critically injured units on the squad assignment screen +- `sort`: display a rating relative to the current sort order next to the visible units on the squad assignment screen ## Documentation From 8f1889edf2f9f7d6127d57a3d5ecfd494a542094 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 20:22:44 -0700 Subject: [PATCH 09/18] increment overlay widget version so pos is reset --- plugins/lua/sort.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index ad4f78ed6..67d8c9e0f 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -626,6 +626,7 @@ SquadAssignmentOverlay.ATTRS{ default_pos={x=18, y=5}, default_enabled=true, viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', + version='2', frame={w=38, h=31}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, From a061a418a141b656d19528ab0751418332351259 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 20:25:20 -0700 Subject: [PATCH 10/18] allow reset to defaults by changing version attribute --- docs/dev/overlay-dev-guide.rst | 4 ++++ plugins/lua/overlay.lua | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 4ec226d30..54e200700 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -90,6 +90,10 @@ The ``overlay.OverlayWidget`` superclass defines the following class attributes: This will be filled in with the display name of your widget, in case you have multiple widgets with the same implementation but different configurations. +- ``version`` + You can set this to any string. If the version string of a loaded widget + does not match the saved settings for that widget, then the configuration + for the widget (position, enabled status) will be reset to defaults. - ``default_pos`` (default: ``{x=-2, y=-2}``) Override this attribute with your desired default widget position. See the `overlay` docs for information on what positive and negative numbers diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 68582e8eb..55ca44bc9 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -261,7 +261,11 @@ local function load_widget(name, widget_class) next_update_ms=widget.overlay_onupdate and 0 or math.huge, } if not overlay_config[name] then overlay_config[name] = {} end + if widget.version ~= overlay_config[name].version then + overlay_config[name] = {} + end local config = overlay_config[name] + config.version = widget.version if config.enabled == nil then config.enabled = widget.default_enabled end From b5cf849ba243e3f816d89b86730faf21bf3e168f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 20:25:37 -0700 Subject: [PATCH 11/18] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9b0a1d30e..76ca6e235 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: ## Documentation ## API +- `overlay`: overlay widgets can now declare a ``version`` attribute. changing the version of a widget will reset its settings to defaults. this is useful when changing the overlay layout and old saved positions will no longer be valid. ## Lua From d4b3c1b3ec51e78fe9cc72bb8fc3bb2dccafcb40 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 8 Sep 2023 21:04:42 -0700 Subject: [PATCH 12/18] update docs for sort overlay --- docs/plugins/sort.rst | 111 +++++++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 45 deletions(-) diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index f142534db..72f8c372e 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -14,54 +14,75 @@ Searching and sorting functionality is provided by `overlay` widgets, and widget Squad assignment overlay ------------------------ -The squad assignment screen can be sorted by name, by migrant wave, by stress, -by various military-related skills or by long-term military potential. - -If sorted by "any melee", then the citizen is sorted according to the "melee -skill effectiveness". This rating uses the highest skill they have in axes, short -swords, maces, warhammers or spears along with physical and mental attributes and -general fighting skill. Citizens with higher rating are expected to be more -effective in melee combat with their corresponding weapon. - -If sorted by "any ranged", then the citizen is sorted according to the "ranged -skill effectiveness". This rating uses crossbow and general archery skills -along with mental and physical attributes. Citizens with higher rating are -expected to be more effective in ranged combat. - -If sorted by "leadership", then the citizen is sorted according to the highest -skill they have in leader, teacher, or military tactics. - -If sorting is done by "mental stability" citizens are arranged based on their -mental stability rating. This rating is a measure that takes into account -facets and values of an individual and correlates to better stress values. -It is designed to be higher for more stress-resistant citizens. - -If sorting is done by "melee potential" citizens are arranged based on -their "melee combat potential" rating. This rating is a statistical measure -that takes into account genetic predispositions in physical and mental -attributes, as well as body size. Dwarves (and other humanoid creatures) with -higher rating are expected to be more effective in melee combat if they train -their attributes to their genetic maximum. - -If sorting is done by "ranged potential" citizens are arranged based on their -ranged combat potential rating. This rating is a statistical measure that takes into -account genetic predispositions in physical and mental attributes. Dwarves -(and other humanoid creatures) with higher rating are expected to be more -effective in ranged combat if they train their attributes to the maximum. - -You can search for a dwarf by name by typing in the Search field. You can also -type in the name of any job skill (military-related or not) and dwarves with -any experience in that skill will be shown. For example, to only see citizens -with military tactics skill, type in "tactics". +You can search for a dwarf by name by typing in the Search field. The search +field is always focused, so any lowercase letter you type will appear there. + +The squad assignment screen can be sorted by name, by arrival order, by stress, +by various military-related skills, or by long-term military potential. + +If sorted by "melee effectiveness" (the default), then the citizens are sorted +according to how well they will perform in battle when using the weapon they +have the most skill in. The effectiveness rating also takes into account +physical and mental attributes as well as general fighting (non-weapon) skills. + +The "ranged effectiveness" sort order does a similar sort for expected +effectiveness with a crossbow. This sort also takes into account relevant +physical and mental attributes. + +The "effectiveness" sorts are the ones you should be using if you need the best +squad you can make right now. The numbers to the left of the unit list indicate +exactly how effective that dwarf is expected to be. Light green numbers +indicate the best of the best, while red numbers indicate dwarves that will not +be effective in the military in their current state (though see "melee +potential" and "ranged potential" sorts below for predictions about future +effectiveness). + +The "arrival order" sort shows the order that your dwarves appeared at your +fort. The numbers on the left indicate the relative arrival order, and the +numbers for the most recent migration wave will be colored bright green. +Dwarves that arrived earlier will have numbers in yellow, and your original +dwarves (if any still survive) will have numbers in red. + +The "stress" sort order will bring your most stressed dwarves to the top, ready +for addition to a :wiki:`therapy squad ` to +help improve their mood. + +Similarly, sorting by "need for training" will show you the dwarves that are +feeling the most unfocused because they are having their military training +needs unmet. + +Both "stress" and "need for training" sorts use the dwarf happiness indicators +to show how dire the dwarf's situation is and how much their mood might be +improved if you add them to an appropriate squad. + +If sorting is done by "melee potential", then citizens are arranged based on +genetic predispositions in physical and mental attributes, as well as body +size. Dwarves (and other humanoid creatures) with higher ratings are expected +to be more effective in melee combat if they train their attributes to their +genetic maximum. + +Similarly, the "ranged potential" sort orders citizens by genetic +predispositions in physical and mental attributes that are relevant to ranged +combat. Dwarves (and other humanoid creatures) with higher rating are expected +to be more effective in ranged combat if they train their attributes to the +maximum. + +The squad assignment panel also offers options for filtering which dwarves are +shown. Each filter option can by cycled through "Include", "Only", and +"Exclude" settings. "Include" does no filtering, "Only" shows only units that +match the filter, and "Exclude" shows only units that do *not* match the filter. + +The following filters are provided: + +- Units that are assigned to other squads +- Elected and appointed officials (e.g. mayor, priests, tavern keepers, etc.) +- Nobility (e.g. monarch, barons, counts, etc.) +- Mothers with infants (you may not want mothers using their babies as shields) +- Weak mental fortitude (units that have facets and values that indicate that they will react poorly to the stresses of battle) +- Critically injured (units that have lost their ability to grasp weapons or walk) "Melee skill effectiveness", "ranged skill effectiveness", "melee combat potential" and "ranged combat potential" are explained in detail here: https://www.reddit.com/r/dwarffortress/comments/163kczo/enhancing_military_candidate_selection_part_3/ "Mental stability" is explained here: https://www.reddit.com/r/dwarffortress/comments/1617s11/enhancing_military_candidate_selection_part_2/ - -You can see all the job skill names that you can search for by running:: - - :lua @df.job_skill - -in `gui/launcher`. From 6ec5e0e1a9accbb3b32025da1aef9aa3476c1765 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 9 Sep 2023 07:11:39 +0000 Subject: [PATCH 13/18] Auto-update submodules library/xml: master --- library/xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/xml b/library/xml index 1cb9a9613..e2e369242 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1cb9a961351db5c2988cee3a194eb24eac4abc1a +Subproject commit e2e369242253b556f6eba7e419aa5f833ff8a46e From 9d233e6e3470ea3c9e9cdf65775448bd96496ace Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 9 Sep 2023 07:00:17 -0700 Subject: [PATCH 14/18] unify stress face pens and rewrite arrival sort docs --- docs/plugins/sort.rst | 11 +++++++---- plugins/lua/sort.lua | 15 +++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 72f8c372e..15257e3d2 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -37,11 +37,14 @@ be effective in the military in their current state (though see "melee potential" and "ranged potential" sorts below for predictions about future effectiveness). -The "arrival order" sort shows the order that your dwarves appeared at your -fort. The numbers on the left indicate the relative arrival order, and the -numbers for the most recent migration wave will be colored bright green. +The "arrival order" sorts your citizens according to the most recent time they +entered your map. The numbers on the left indicate the relative arrival order, +and the numbers for the group of dwarves that most recently entered the map +will be at the top and be colored bright green. If you run this sort after you +get a new group of migrants, the migrant wave will be colored bright green. Dwarves that arrived earlier will have numbers in yellow, and your original -dwarves (if any still survive) will have numbers in red. +dwarves (if any still survive and have never left and re-entered the map) will +have numbers in red. The "stress" sort order will bring your most stressed dwarves to the top, ready for addition to a :wiki:`therapy squad ` to diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 67d8c9e0f..5a5cd2613 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -991,7 +991,7 @@ local function is_unstable(unit) end local function is_maimed(unit) - return unit.flags2.vision_missing or + return not unit.flags2.vision_good or unit.status2.limbs_grasp_count < 2 or unit.status2.limbs_stand_count == 0 end @@ -1207,14 +1207,13 @@ for idx=0,6 do end end -local ASCII_FACE_TILES = {} for idx,color in ipairs{COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_WHITE, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} do local face = {} - ensure_key(face, 0)[0] = to_pen{ch=1, fg=color} - ensure_key(face, 0)[1] = to_pen{ch='\\', fg=color} - ensure_key(face, 1)[0] = to_pen{ch='\\', fg=color} - ensure_key(face, 1)[1] = to_pen{ch='/', fg=color} - ASCII_FACE_TILES[idx-1] = face + ensure_key(face, 0)[0] = to_pen{tile=FACE_TILES[idx-1][0][0], ch=1, fg=color} + ensure_key(face, 0)[1] = to_pen{tile=FACE_TILES[idx-1][0][1], ch='\\', fg=color} + ensure_key(face, 1)[0] = to_pen{tile=FACE_TILES[idx-1][1][0], ch='\\', fg=color} + ensure_key(face, 1)[1] = to_pen{tile=FACE_TILES[idx-1][1][1], ch='/', fg=color} + FACE_TILES[idx-1] = face end function get_stress_face_tile(idx, x, y) @@ -1223,7 +1222,7 @@ function get_stress_face_tile(idx, x, y) return x == 0 and y == 1 and DASH_PEN or gui.CLEAR_PEN end local val = math.min(6, elem.val) - return (dfhack.screen.inGraphicsMode() and FACE_TILES or ASCII_FACE_TILES)[val][y][x] + return FACE_TILES[val][y][x] end function SquadAnnotationOverlay:init() From 45e5168a91cf9b7ec283d6c0eda2df30db55c8ea Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 9 Sep 2023 07:20:59 -0700 Subject: [PATCH 15/18] ensure face textures get reloaded on map load --- plugins/lua/sort.lua | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 5a5cd2613..15d9ebabb 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -6,6 +6,8 @@ local setbelief = reqscript('modtools/set-belief') local utils = require('utils') local widgets = require('gui.widgets') +local GLOBAL_KEY = 'sort' + local CH_UP = string.char(30) local CH_DN = string.char(31) @@ -1196,24 +1198,26 @@ local to_pen = dfhack.pen.parse local DASH_PEN = to_pen{ch='-', fg=COLOR_WHITE, keep_lower=true} local FACE_TILES = {} -for idx=0,6 do - FACE_TILES[idx] = {} - local face_off = (6 - idx) * 2 - for y=0,1 do - for x=0,1 do - local tile = dfhack.screen.findGraphicsTile('INTERFACE_BITS', 32 + face_off + x, 6 + y) - ensure_key(FACE_TILES[idx], y)[x] = tile +local function init_face_tiles() + for idx=0,6 do + FACE_TILES[idx] = {} + local face_off = (6 - idx) * 2 + for y=0,1 do + for x=0,1 do + local tile = dfhack.screen.findGraphicsTile('INTERFACE_BITS', 32 + face_off + x, 6 + y) + ensure_key(FACE_TILES[idx], y)[x] = tile + end end end -end -for idx,color in ipairs{COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_WHITE, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} do - local face = {} - ensure_key(face, 0)[0] = to_pen{tile=FACE_TILES[idx-1][0][0], ch=1, fg=color} - ensure_key(face, 0)[1] = to_pen{tile=FACE_TILES[idx-1][0][1], ch='\\', fg=color} - ensure_key(face, 1)[0] = to_pen{tile=FACE_TILES[idx-1][1][0], ch='\\', fg=color} - ensure_key(face, 1)[1] = to_pen{tile=FACE_TILES[idx-1][1][1], ch='/', fg=color} - FACE_TILES[idx-1] = face + for idx,color in ipairs{COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_WHITE, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} do + local face = {} + ensure_key(face, 0)[0] = to_pen{tile=FACE_TILES[idx-1][0][0], ch=1, fg=color} + ensure_key(face, 0)[1] = to_pen{tile=FACE_TILES[idx-1][0][1], ch='\\', fg=color} + ensure_key(face, 1)[0] = to_pen{tile=FACE_TILES[idx-1][1][0], ch='\\', fg=color} + ensure_key(face, 1)[1] = to_pen{tile=FACE_TILES[idx-1][1][1], ch='/', fg=color} + FACE_TILES[idx-1] = face + end end function get_stress_face_tile(idx, x, y) @@ -1261,6 +1265,14 @@ OVERLAY_WIDGETS = { squad_annotation=SquadAnnotationOverlay, } +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + init_face_tiles() +end + --[[ local utils = require('utils') local units = require('plugins.sort.units') From c37d3e66b28ba0d56d419d02b6d3de4b816514f5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 9 Sep 2023 07:48:24 -0700 Subject: [PATCH 16/18] small edit for sort docs --- docs/plugins/sort.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 15257e3d2..31ab2d3fc 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -81,8 +81,10 @@ The following filters are provided: - Elected and appointed officials (e.g. mayor, priests, tavern keepers, etc.) - Nobility (e.g. monarch, barons, counts, etc.) - Mothers with infants (you may not want mothers using their babies as shields) -- Weak mental fortitude (units that have facets and values that indicate that they will react poorly to the stresses of battle) -- Critically injured (units that have lost their ability to grasp weapons or walk) +- Weak mental fortitude (units that have facets and values that indicate that + they will react poorly to the stresses of battle) +- Critically injured (units that have lost their ability to see, grasp weapons, + or walk) "Melee skill effectiveness", "ranged skill effectiveness", "melee combat potential" and "ranged combat potential" are explained in detail here: From e4edc9be4e4233579a904f87f2836f961f821d08 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 9 Sep 2023 07:49:35 -0700 Subject: [PATCH 17/18] fix crash on invalid saved seed id --- docs/changelog.txt | 1 + plugins/seedwatch.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 609ad09cc..432b936b0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## Fixes - `buildingplan`: make the construction dimensions readout visible again +- `seedwatch`: fix a crash when reading data saved by very very old versions of the plugin ## Misc Improvements - `sort`: sort by need for training on squad assignment screen diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index df49efec2..7cbdcbd2a 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -103,6 +103,10 @@ static bool validate_seed_config(color_ostream& out, PersistentDataItem c) { int seed_id = get_config_val(c, SEED_CONFIG_ID); auto plant = binsearch_in_vector(world->raws.plants.all, &df::plant_raw::index, seed_id); + if (!plant) { + WARN(config, out).print("discarded invalid seed id: %d\n", seed_id); + return false; + } bool valid = (!plant->flags.is_set(df::enums::plant_raw_flags::TREE)); if (!valid) { DEBUG(config, out).print("invalid configuration for %s discarded\n", plant->id.c_str()); From ee61c76bc08226d375f7aa5d1cd858765c5df9bc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 9 Sep 2023 10:42:20 -0700 Subject: [PATCH 18/18] use find method instead of binsearch --- plugins/seedwatch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 7cbdcbd2a..f56470c66 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -102,7 +102,7 @@ static void remove_seed_config(color_ostream &out, int id) { static bool validate_seed_config(color_ostream& out, PersistentDataItem c) { int seed_id = get_config_val(c, SEED_CONFIG_ID); - auto plant = binsearch_in_vector(world->raws.plants.all, &df::plant_raw::index, seed_id); + auto plant = df::plant_raw::find(seed_id); if (!plant) { WARN(config, out).print("discarded invalid seed id: %d\n", seed_id); return false;