From 986e64aed06be13182f74dcbce8edd4babb0fb53 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 12:27:09 +0100 Subject: [PATCH 01/76] new plugin preserve-tombs ensures that units that die keep their tomb assignments in death --- plugins/CMakeLists.txt | 1 + plugins/preserve-tombs.cpp | 243 +++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 plugins/preserve-tombs.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index b96606284..392a69dcc 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -144,6 +144,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) #dfhack_plugin(petcapRemover petcapRemover.cpp) #dfhack_plugin(plants plants.cpp) + dfhack_plugin(preserve-tombs preserve-tombs.cpp) dfhack_plugin(probe probe.cpp) dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) #dfhack_plugin(power-meter power-meter.cpp LINK_LIBRARIES lua) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp new file mode 100644 index 000000000..857597d23 --- /dev/null +++ b/plugins/preserve-tombs.cpp @@ -0,0 +1,243 @@ +#include "Debug.h" +#include "PluginManager.h" +#include "MiscUtils.h" + +#include +#include +#include +#include +#include +#include + +#include "modules/Units.h" +#include "modules/Buildings.h" +#include "modules/Persistence.h" +#include "modules/EventManager.h" +#include "modules/World.h" + +#include "df/world.h" +#include "df/unit.h" +#include "df/building.h" +#include "df/building_civzonest.h" + +using namespace DFHack; +using namespace df::enums; + + +// +DFHACK_PLUGIN("preserve-tombs"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + + +static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; + +static int32_t cycle_timestamp; +static int32_t cycle_freq; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_CYCLES = 1 +}; + +static std::unordered_map tomb_assignments; + +namespace DFHack { + DBG_DECLARE(preservetombs, config, DebugCategory::LINFO); +} + + +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); +} + +static bool assign_to_tomb(int32_t unit_id, int32_t building_id); +static void update_tomb_assignments(color_ostream& out); +void onUnitDeath(color_ostream& out, void* ptr); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + return CR_OK; +} + +// event listener +EventManager::EventHandler assign_tomb_handler(onUnitDeath, 0); + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(config,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + EventManager::registerListener(EventManager::EventType::UNIT_DEATH, assign_tomb_handler, plugin_self); + if (enable) + update_tomb_assignments(out); + } else { + EventManager::unregisterAll(plugin_self); + DEBUG(config,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); + + return CR_OK; +} + +DFhackCExport command_result plugin_load_data (color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + set_config_val(config, CONFIG_CYCLES, 25); + } + + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + cycle_freq = get_config_val(config, CONFIG_CYCLES); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= cycle_freq) + update_tomb_assignments(out); + return CR_OK; +} +// + + + + +// On unit death - check if we assigned them a tomb +// +// +void onUnitDeath(color_ostream& out, void* ptr) { + // input is void* that contains the unit id + int32_t unit_id = reinterpret_cast(ptr); + + // check if unit was assigned a tomb in life + auto it = tomb_assignments.find(unit_id); + if (it == tomb_assignments.end()) return; + + // assign that unit to their previously assigned tomb in life + int32_t building_id = it->second; + if (!assign_to_tomb(unit_id, building_id)) return; + + // success, print status update and remove assignment from our memo-list + out.print("Unit %d died - assigning them to tomb %d\n", unit_id, building_id); + tomb_assignments.erase(it); + +} + + +// Update tomb assignments +// +// +static void update_tomb_assignments(color_ostream &out) { + + // check tomb civzones for assigned units + for (auto* bld : world->buildings.other.ZONE_TOMB) { + + auto* tomb = virtual_cast(bld); + if (!tomb || !tomb->flags.bits.exists) continue; + if (!tomb->assigned_unit) continue; + if (Units::isDead(tomb->assigned_unit)) continue; // we only care about living units + + auto it = tomb_assignments.find(tomb->assigned_unit_id); + + if (it == tomb_assignments.end()) { + tomb_assignments.emplace(tomb->assigned_unit_id, tomb->id); + out.print("%s new tomb assignment, unit %d to tomb %d\n", plugin_name, tomb->assigned_unit_id, tomb->id); + } + + else { + if (it->second != tomb->id) { + out.print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", plugin_name, tomb->assigned_unit_id, it->second, tomb->id); + } + it->second = tomb->id; + } + + } + + // now check our civzones for unassignment / deleted zone / + std::erase_if(tomb_assignments, [&](const std::pair& pair){ + const auto &[unit_id, building_id] = pair; + + const size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + if (tomb_idx == -1) { + out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); + return true; + } + const auto tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); + if (!tomb || !tomb->flags.bits.exists) { + out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); + return true; + } + if (tomb->assigned_unit_id != unit_id) { + out.print("%s unassigned unit %d from tomb %d - removing\n", unit_id, building_id); + return true; + } + + return false; + }); + +} + + +// ASSIGN UNIT TO TOMB +// +// +static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { + + size_t unit_idx = Units::findIndexById(unit_id); + if (unit_idx == -1) return false; + + df::unit* unit = world->units.all[unit_idx]; + if (!Units::isDead(unit)) return false; + + size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + if (tomb_idx == -1) return false; + + df::building_civzonest* tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); + if (!tomb || tomb->assigned_unit) return false; // in the game we cannot reassign tombs - more research is required to see if reassignment is safe. + + Buildings::setOwner(tomb, unit); + return true; +} \ No newline at end of file From 2a145d06b622b273f4ff619620581e1dc35fd6b9 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 13:11:13 +0100 Subject: [PATCH 02/76] fixed crash on tomb unassignment (caused by incorrect params passed to formatted string) --- plugins/preserve-tombs.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 857597d23..245ce5949 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -70,6 +70,7 @@ static void update_tomb_assignments(color_ostream& out); void onUnitDeath(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + tomb_assignments.clear(); return CR_OK; } @@ -77,6 +78,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector & pair){ - const auto &[unit_id, building_id] = pair; + for (auto it = tomb_assignments.begin(); it != tomb_assignments.end(); ++it){ + auto &[unit_id, building_id] = *it; const size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) { out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); - return true; + it = tomb_assignments.erase(it); + continue; } const auto tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); if (!tomb || !tomb->flags.bits.exists) { out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); - return true; + it = tomb_assignments.erase(it); + continue; } if (tomb->assigned_unit_id != unit_id) { - out.print("%s unassigned unit %d from tomb %d - removing\n", unit_id, building_id); - return true; + out.print("%s unassigned unit %d from tomb %d - removing\n", plugin_name, unit_id, building_id); + it = tomb_assignments.erase(it); + continue; } - - return false; - }); + } } From b0a15b2e8a5319fb896cd927ee2b63ad00ba3775 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 13:38:52 +0100 Subject: [PATCH 03/76] added command to show status of preservetombs (is enabled or disabled) --- plugins/preserve-tombs.cpp | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 245ce5949..d80ca3b03 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -68,12 +68,27 @@ static void set_config_bool(PersistentDataItem &c, int index, bool value) { static bool assign_to_tomb(int32_t unit_id, int32_t building_id); static void update_tomb_assignments(color_ostream& out); void onUnitDeath(color_ostream& out, void* ptr); +static command_result do_command(color_ostream& out, std::vector& params); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - tomb_assignments.clear(); + commands.push_back(PluginCommand( + plugin_name, + "Preserves tomb assignments to units when they die.", + do_command)); return CR_OK; } +static command_result do_command(color_ostream& out, std::vector& params) { + if (params.size() != 1 || params[0] != "status") { + out.print("%s wrong usage", plugin_name); + return CR_WRONG_USAGE; + } + else { + out.print("%s is currently %s", plugin_name, is_enabled ? "enabled" : "disabled"); + return CR_OK; + } +} + // event listener EventManager::EventHandler assign_tomb_handler(onUnitDeath, 0); @@ -154,7 +169,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { void onUnitDeath(color_ostream& out, void* ptr) { // input is void* that contains the unit id int32_t unit_id = reinterpret_cast(ptr); - + // check if unit was assigned a tomb in life auto it = tomb_assignments.find(unit_id); if (it == tomb_assignments.end()) return; @@ -199,10 +214,10 @@ static void update_tomb_assignments(color_ostream &out) { } - // now check our civzones for unassignment / deleted zone / + // now check our civzones for unassignment / deleted zone / for (auto it = tomb_assignments.begin(); it != tomb_assignments.end(); ++it){ auto &[unit_id, building_id] = *it; - + const size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) { out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); @@ -229,13 +244,13 @@ static void update_tomb_assignments(color_ostream &out) { // // static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { - + size_t unit_idx = Units::findIndexById(unit_id); if (unit_idx == -1) return false; - + df::unit* unit = world->units.all[unit_idx]; if (!Units::isDead(unit)) return false; - + size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) return false; From 0b2989fb1508fe6291ccee57dd4077f59b1f720d Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 14:08:49 +0100 Subject: [PATCH 04/76] preservetombs status now shows list of all tracked tomb assignments --- plugins/preserve-tombs.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index d80ca3b03..e9964d2f1 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -84,7 +84,14 @@ static command_result do_command(color_ostream& out, std::vector& p return CR_WRONG_USAGE; } else { - out.print("%s is currently %s", plugin_name, is_enabled ? "enabled" : "disabled"); + out.print("%s is currently %s\n", plugin_name, is_enabled ? "enabled" : "disabled"); + if (is_enabled) { + out.print("tracked tomb assignments:\n"); + std::for_each(tomb_assignments.begin(), tomb_assignments.end(), [&out](const auto& p){ + auto& [unit_id, building_id] = p; + out.print("unit %d -> building %d\n", unit_id, building_id); + }); + } return CR_OK; } } @@ -138,6 +145,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(config,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); + if (is_enabled) update_tomb_assignments(out); return CR_OK; } @@ -255,7 +263,7 @@ static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { if (tomb_idx == -1) return false; df::building_civzonest* tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); - if (!tomb || tomb->assigned_unit) return false; // in the game we cannot reassign tombs - more research is required to see if reassignment is safe. + if (!tomb || tomb->assigned_unit) return false; Buildings::setOwner(tomb, unit); return true; From df5de8b7ec26b6fbc9bd6a7beb3de0f82707d12a Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 14:21:44 +0100 Subject: [PATCH 05/76] added doc and changelog entry for preserve-tombs plugin --- docs/changelog.txt | 1 + docs/plugins/preserve-tombs.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/plugins/preserve-tombs.rst diff --git a/docs/changelog.txt b/docs/changelog.txt index f819587de..07ed84ebf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,6 +52,7 @@ Template for new versions: # Future ## New Tools +- `preserve-tombs`: tracks tomb assignments to living units and ensures that the tomb stays assigned to them when they die. ## New Features diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst new file mode 100644 index 000000000..7c148e3ee --- /dev/null +++ b/docs/plugins/preserve-tombs.rst @@ -0,0 +1,22 @@ +preserve-tombs +============== + +.. dfhack-tool:: + :summary: Fix tombs being unassigned to units on death + :tags: fort bugfix + +If you find that the tombs you assign to units get unassigned from them when +they die (e.g. your nobles), this tool can help fix that. + +Usage +----- + +:: + + enable preserve-tombs + preserve-tombs status + +This tool runs in the background. You can check the status of the plugin +by running ``preserve-tombs status`` which will show whether the plugin +is enabled and if so, display a list of all tracked tomb assignments +to living units. From 579fe6ee766222e595b17ba1a5534ad42fb9638a Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 14:24:42 +0100 Subject: [PATCH 06/76] fixed sign compare issue linux build --- plugins/preserve-tombs.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index e9964d2f1..8d82146b2 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -226,7 +226,7 @@ static void update_tomb_assignments(color_ostream &out) { for (auto it = tomb_assignments.begin(); it != tomb_assignments.end(); ++it){ auto &[unit_id, building_id] = *it; - const size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + const int tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) { out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); it = tomb_assignments.erase(it); @@ -253,13 +253,13 @@ static void update_tomb_assignments(color_ostream &out) { // static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { - size_t unit_idx = Units::findIndexById(unit_id); + const int unit_idx = Units::findIndexById(unit_id); if (unit_idx == -1) return false; df::unit* unit = world->units.all[unit_idx]; if (!Units::isDead(unit)) return false; - size_t tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); + const int tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) return false; df::building_civzonest* tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); From 028fbc34ade99fc91e43cee3d35655e87c0e736b Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 14:30:01 +0100 Subject: [PATCH 07/76] using std::erase_if instead of iterator loop --- plugins/preserve-tombs.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 8d82146b2..6730dee76 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -222,28 +222,27 @@ static void update_tomb_assignments(color_ostream &out) { } - // now check our civzones for unassignment / deleted zone / - for (auto it = tomb_assignments.begin(); it != tomb_assignments.end(); ++it){ - auto &[unit_id, building_id] = *it; + // now check our civzones for unassignment / deleted zone + std::erase_if(tomb_assignments,[&](const auto& p){ + auto &[unit_id, building_id] = p; const int tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) { out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); - it = tomb_assignments.erase(it); - continue; + return true; } const auto tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); if (!tomb || !tomb->flags.bits.exists) { out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); - it = tomb_assignments.erase(it); - continue; + return true; } if (tomb->assigned_unit_id != unit_id) { out.print("%s unassigned unit %d from tomb %d - removing\n", plugin_name, unit_id, building_id); - it = tomb_assignments.erase(it); - continue; + return true; } - } + + return false; + }); } From 7933e291191e58eba0cdb9e6c2e66c8d25eed2c9 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 29 Sep 2023 14:45:46 +0100 Subject: [PATCH 08/76] newline at eof - preservetombs.cpp --- plugins/preserve-tombs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 6730dee76..0fa9d5b36 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -266,4 +266,4 @@ static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { Buildings::setOwner(tomb, unit); return true; -} \ No newline at end of file +} From 6be9de5e518aec7cd751273782f22653d608623c Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Sat, 30 Sep 2023 12:30:40 +0100 Subject: [PATCH 09/76] preserve-tombs added option to change tick rate, changed default update frequency, changed some print to debug statements, some other adjustments --- docs/plugins/preserve-tombs.rst | 19 ++++---- plugins/preserve-tombs.cpp | 77 +++++++++++++++++++++++---------- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst index 7c148e3ee..e667e64b0 100644 --- a/docs/plugins/preserve-tombs.rst +++ b/docs/plugins/preserve-tombs.rst @@ -11,12 +11,15 @@ they die (e.g. your nobles), this tool can help fix that. Usage ----- -:: +``enable preserve-tombs`` + enable the plugin +``preserve-tombs status`` + check the status of the plugin, and if the plugin is enabled, + lists all tracked tomb assignments +``preserve-tombs update`` + forces an immediate update of the tomb assignments. +``preserve-tombs freq [val]`` + changes the rate at which the plugin rechecks and updates + tomb assignments, in ticks (default is ``100``) - enable preserve-tombs - preserve-tombs status - -This tool runs in the background. You can check the status of the plugin -by running ``preserve-tombs status`` which will show whether the plugin -is enabled and if so, display a list of all tracked tomb assignments -to living units. +This tool runs in the background. \ No newline at end of file diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 0fa9d5b36..f75b7aeb7 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -14,6 +14,7 @@ #include "modules/Persistence.h" #include "modules/EventManager.h" #include "modules/World.h" +#include "modules/Translation.h" #include "df/world.h" #include "df/unit.h" @@ -46,6 +47,8 @@ static std::unordered_map tomb_assignments; namespace DFHack { DBG_DECLARE(preservetombs, config, DebugCategory::LINFO); + DBG_DECLARE(preservetombs, cycle, DebugCategory::LINFO); + DBG_DECLARE(preservetombs, event, DebugCategory::LINFO); } @@ -79,28 +82,50 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector & params) { - if (params.size() != 1 || params[0] != "status") { - out.print("%s wrong usage", plugin_name); + if (params.size() == 0) { + out.print("%s wrong usage\n", plugin_name); return CR_WRONG_USAGE; } - else { + if (params[0] == "status") { out.print("%s is currently %s\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (is_enabled) { + out.print("Update frequency: %d ticks", cycle_freq); out.print("tracked tomb assignments:\n"); std::for_each(tomb_assignments.begin(), tomb_assignments.end(), [&out](const auto& p){ auto& [unit_id, building_id] = p; - out.print("unit %d -> building %d\n", unit_id, building_id); + auto* unit = df::unit::find(unit_id); + std::string name = unit ? Translation::TranslateName(&unit->name) : "UNKNOWN UNIT" ; + out.print("%s (id %d) -> building %d\n", name.c_str(), unit_id, building_id); }); } return CR_OK; } + if (params[0] == "update") { + CoreSuspender suspend; + update_tomb_assignments(out); + out.print("Updated tomb assignments\n"); + return CR_OK; + } + if (params.size() < 2) { + out.print("%s wrong usage\n", plugin_name); + return CR_WRONG_USAGE; + } + if (params[0] == "ticks" || params[0] == "freq" || params[0] == "rate") { + int new_tickrate = std::stoi(params[1]); + if (new_tickrate <= 0) { + out.print("new tickrate (%d) cannot be <= 0\n", new_tickrate); + return CR_WRONG_USAGE; + } + cycle_freq = new_tickrate; + set_config_val(config, CONFIG_CYCLES, cycle_freq); + } + return CR_WRONG_USAGE; } // event listener EventManager::EventHandler assign_tomb_handler(onUnitDeath, 0); DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - tomb_assignments.clear(); if (!Core::getInstance().isWorldLoaded()) { out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); return CR_FAILURE; @@ -111,11 +136,15 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DEBUG(config,out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); - EventManager::registerListener(EventManager::EventType::UNIT_DEATH, assign_tomb_handler, plugin_self); - if (enable) + if (enable) { + EventManager::registerListener(EventManager::EventType::UNIT_DEATH, assign_tomb_handler, plugin_self); update_tomb_assignments(out); + } + else { + tomb_assignments.clear(); + EventManager::unregisterAll(plugin_self); + } } else { - EventManager::unregisterAll(plugin_self); DEBUG(config,out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); @@ -137,7 +166,7 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(config,out).print("no config found in this save; initializing\n"); config = World::AddPersistentData(CONFIG_KEY); set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); - set_config_val(config, CONFIG_CYCLES, 25); + set_config_val(config, CONFIG_CYCLES, 100); } is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); @@ -145,7 +174,6 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(config,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); - if (is_enabled) update_tomb_assignments(out); return CR_OK; } @@ -157,6 +185,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan plugin_name); is_enabled = false; } + EventManager::unregisterAll(plugin_self); } return CR_OK; } @@ -184,10 +213,12 @@ void onUnitDeath(color_ostream& out, void* ptr) { // assign that unit to their previously assigned tomb in life int32_t building_id = it->second; - if (!assign_to_tomb(unit_id, building_id)) return; - + if (!assign_to_tomb(unit_id, building_id)) { + DEBUG(event, out).print("Unit %d died - but failed to assign them to tomb %d\n", unit_id, building_id); + return; + } // success, print status update and remove assignment from our memo-list - out.print("Unit %d died - assigning them to tomb %d\n", unit_id, building_id); + INFO(event, out).print("Unit %d died - assigning them to tomb %d\n", unit_id, building_id); tomb_assignments.erase(it); } @@ -197,7 +228,7 @@ void onUnitDeath(color_ostream& out, void* ptr) { // // static void update_tomb_assignments(color_ostream &out) { - + cycle_timestamp = world->frame_counter; // check tomb civzones for assigned units for (auto* bld : world->buildings.other.ZONE_TOMB) { @@ -210,12 +241,14 @@ static void update_tomb_assignments(color_ostream &out) { if (it == tomb_assignments.end()) { tomb_assignments.emplace(tomb->assigned_unit_id, tomb->id); - out.print("%s new tomb assignment, unit %d to tomb %d\n", plugin_name, tomb->assigned_unit_id, tomb->id); + DEBUG(cycle, out).print("%s new tomb assignment, unit %d to tomb %d\n", + plugin_name, tomb->assigned_unit_id, tomb->id); } else { if (it->second != tomb->id) { - out.print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", plugin_name, tomb->assigned_unit_id, it->second, tomb->id); + DEBUG(cycle, out).print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", + plugin_name, tomb->assigned_unit_id, it->second, tomb->id); } it->second = tomb->id; } @@ -228,16 +261,16 @@ static void update_tomb_assignments(color_ostream &out) { const int tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) { - out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); return true; } const auto tomb = virtual_cast(world->buildings.other.ZONE_TOMB[tomb_idx]); if (!tomb || !tomb->flags.bits.exists) { - out.print("%s tomb missing: %d - removing\n", plugin_name, building_id); + DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id); return true; } if (tomb->assigned_unit_id != unit_id) { - out.print("%s unassigned unit %d from tomb %d - removing\n", plugin_name, unit_id, building_id); + DEBUG(cycle, out).print("%s unassigned unit %d from tomb %d - removing\n", plugin_name, unit_id, building_id); return true; } @@ -252,11 +285,9 @@ static void update_tomb_assignments(color_ostream &out) { // static bool assign_to_tomb(int32_t unit_id, int32_t building_id) { - const int unit_idx = Units::findIndexById(unit_id); - if (unit_idx == -1) return false; + df::unit* unit = df::unit::find(unit_id); - df::unit* unit = world->units.all[unit_idx]; - if (!Units::isDead(unit)) return false; + if (!unit || !Units::isDead(unit)) return false; const int tomb_idx = binsearch_index(world->buildings.other.ZONE_TOMB, building_id); if (tomb_idx == -1) return false; From 7e75fd6ebc10faf5237afa1f9fa2fec7639371b9 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Sat, 30 Sep 2023 12:49:46 +0100 Subject: [PATCH 10/76] eof fix --- docs/plugins/preserve-tombs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst index e667e64b0..846c6c553 100644 --- a/docs/plugins/preserve-tombs.rst +++ b/docs/plugins/preserve-tombs.rst @@ -22,4 +22,4 @@ Usage changes the rate at which the plugin rechecks and updates tomb assignments, in ticks (default is ``100``) -This tool runs in the background. \ No newline at end of file +This tool runs in the background. From e2dcced8ef23861f64daa34e7960af1785c1497d Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Sat, 30 Sep 2023 13:21:04 +0100 Subject: [PATCH 11/76] preserve-tombs command guarded from using update argument when plugin not enabled --- plugins/preserve-tombs.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index f75b7aeb7..9216a5de4 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -101,6 +101,10 @@ static command_result do_command(color_ostream& out, std::vector& p return CR_OK; } if (params[0] == "update") { + if (!is_enabled) { + out.printerr("Cannot update %s when not enabled", plugin_name); + return CR_FAILURE; + } CoreSuspender suspend; update_tomb_assignments(out); out.print("Updated tomb assignments\n"); From 7d3764d3ec1708e6b8d8feef3236914f60463983 Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Sat, 30 Sep 2023 14:42:29 +0100 Subject: [PATCH 12/76] removed option to set tickrate for preserve-tombs, fixed at 100 tick interval --- docs/plugins/preserve-tombs.rst | 12 +++++------- plugins/preserve-tombs.cpp | 27 +++++---------------------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst index 846c6c553..045d15a5a 100644 --- a/docs/plugins/preserve-tombs.rst +++ b/docs/plugins/preserve-tombs.rst @@ -13,13 +13,11 @@ Usage ``enable preserve-tombs`` enable the plugin -``preserve-tombs status`` +``preserve-tombs [status]`` check the status of the plugin, and if the plugin is enabled, - lists all tracked tomb assignments + lists all currently tracked tomb assignments ``preserve-tombs update`` - forces an immediate update of the tomb assignments. -``preserve-tombs freq [val]`` - changes the rate at which the plugin rechecks and updates - tomb assignments, in ticks (default is ``100``) + forces an immediate update of the tomb assignments. This plugin + automatically updates the tomb assignments once every 100 ticks. -This tool runs in the background. +This tool runs in the background. diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 9216a5de4..3e5cea1d2 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -36,11 +36,10 @@ static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; static PersistentDataItem config; static int32_t cycle_timestamp; -static int32_t cycle_freq; +static constexpr int32_t cycle_freq = 100; enum ConfigValues { CONFIG_IS_ENABLED = 0, - CONFIG_CYCLES = 1 }; static std::unordered_map tomb_assignments; @@ -82,14 +81,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector & params) { - if (params.size() == 0) { - out.print("%s wrong usage\n", plugin_name); - return CR_WRONG_USAGE; + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot use %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - if (params[0] == "status") { + if (params.size() == 0 || params[0] == "status") { out.print("%s is currently %s\n", plugin_name, is_enabled ? "enabled" : "disabled"); if (is_enabled) { - out.print("Update frequency: %d ticks", cycle_freq); out.print("tracked tomb assignments:\n"); std::for_each(tomb_assignments.begin(), tomb_assignments.end(), [&out](const auto& p){ auto& [unit_id, building_id] = p; @@ -110,19 +108,6 @@ static command_result do_command(color_ostream& out, std::vector& p out.print("Updated tomb assignments\n"); return CR_OK; } - if (params.size() < 2) { - out.print("%s wrong usage\n", plugin_name); - return CR_WRONG_USAGE; - } - if (params[0] == "ticks" || params[0] == "freq" || params[0] == "rate") { - int new_tickrate = std::stoi(params[1]); - if (new_tickrate <= 0) { - out.print("new tickrate (%d) cannot be <= 0\n", new_tickrate); - return CR_WRONG_USAGE; - } - cycle_freq = new_tickrate; - set_config_val(config, CONFIG_CYCLES, cycle_freq); - } return CR_WRONG_USAGE; } @@ -170,11 +155,9 @@ DFhackCExport command_result plugin_load_data (color_ostream &out) { DEBUG(config,out).print("no config found in this save; initializing\n"); config = World::AddPersistentData(CONFIG_KEY); set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); - set_config_val(config, CONFIG_CYCLES, 100); } is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); - cycle_freq = get_config_val(config, CONFIG_CYCLES); DEBUG(config,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); From a8e09ac8d4e84f667e59f4bb260b8fad96304c9a Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Sat, 30 Sep 2023 14:55:22 +0100 Subject: [PATCH 13/76] removed trailing ws --- docs/plugins/preserve-tombs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst index 045d15a5a..67e7ceff6 100644 --- a/docs/plugins/preserve-tombs.rst +++ b/docs/plugins/preserve-tombs.rst @@ -20,4 +20,4 @@ Usage forces an immediate update of the tomb assignments. This plugin automatically updates the tomb assignments once every 100 ticks. -This tool runs in the background. +This tool runs in the background. From 0956c06341265180804564f3a22536bb4ac988ff Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Sun, 1 Oct 2023 17:33:53 +0100 Subject: [PATCH 14/76] update arg changed to 'now' for consistency --- docs/plugins/preserve-tombs.rst | 2 +- plugins/preserve-tombs.cpp | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst index 67e7ceff6..e1d0ba8ce 100644 --- a/docs/plugins/preserve-tombs.rst +++ b/docs/plugins/preserve-tombs.rst @@ -16,7 +16,7 @@ Usage ``preserve-tombs [status]`` check the status of the plugin, and if the plugin is enabled, lists all currently tracked tomb assignments -``preserve-tombs update`` +``preserve-tombs now`` forces an immediate update of the tomb assignments. This plugin automatically updates the tomb assignments once every 100 ticks. diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 3e5cea1d2..976b6bd22 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -98,7 +98,7 @@ static command_result do_command(color_ostream& out, std::vector& p } return CR_OK; } - if (params[0] == "update") { + if (params[0] == "now") { if (!is_enabled) { out.printerr("Cannot update %s when not enabled", plugin_name); return CR_FAILURE; @@ -232,11 +232,9 @@ static void update_tomb_assignments(color_ostream &out) { plugin_name, tomb->assigned_unit_id, tomb->id); } - else { - if (it->second != tomb->id) { - DEBUG(cycle, out).print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", - plugin_name, tomb->assigned_unit_id, it->second, tomb->id); - } + else if (it->second != tomb->id) { + DEBUG(cycle, out).print("%s tomb assignment to %d changed, (old: %d, new: %d)\n", + plugin_name, tomb->assigned_unit_id, it->second, tomb->id); it->second = tomb->id; } @@ -257,7 +255,7 @@ static void update_tomb_assignments(color_ostream &out) { return true; } if (tomb->assigned_unit_id != unit_id) { - DEBUG(cycle, out).print("%s unassigned unit %d from tomb %d - removing\n", plugin_name, unit_id, building_id); + DEBUG(cycle, out).print("%s unit %d unassigned from tomb %d - removing\n", plugin_name, unit_id, building_id); return true; } From 964c9d38ea7c3df28cfd0a97776544b458d0d875 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 4 Oct 2023 00:31:04 +0000 Subject: [PATCH 15/76] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 28bcd6e31..a8a7ec8bc 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 28bcd6e313ea6f87ffd805c8cf40360da5f21509 +Subproject commit a8a7ec8bc52ae8b63c1ae2a58f2904958734f3c5 From 082a97a0f6345529b0711a69753cce12343bc621 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 4 Oct 2023 01:10:16 -0500 Subject: [PATCH 16/76] remove refrence to unused field `wait_timer` is not really there; what we have labeled as `wait_timer` is actually padding so there is no need to do this --- plugins/strangemood.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/strangemood.cpp b/plugins/strangemood.cpp index b300eb795..88d1df071 100644 --- a/plugins/strangemood.cpp +++ b/plugins/strangemood.cpp @@ -1217,7 +1217,6 @@ command_result df_strangemood (color_ostream &out, vector & parameters) ref->setID(unit->id); job->general_refs.push_back(ref); unit->job.current_job = job; - job->wait_timer = 0; // Generate the artifact's name if (type == mood_type::Fell || type == mood_type::Macabre) From 89be6f56ef60303dbc0b34e22f7d87a128913701 Mon Sep 17 00:00:00 2001 From: Myk Date: Thu, 5 Oct 2023 12:56:46 -0700 Subject: [PATCH 17/76] Apply suggestions from code review --- plugins/preserve-tombs.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/preserve-tombs.cpp b/plugins/preserve-tombs.cpp index 976b6bd22..be560e1ce 100644 --- a/plugins/preserve-tombs.cpp +++ b/plugins/preserve-tombs.cpp @@ -75,7 +75,7 @@ static command_result do_command(color_ostream& out, std::vector& p DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( plugin_name, - "Preserves tomb assignments to units when they die.", + "Preserve tomb assignments when assigned units die.", do_command)); return CR_OK; } @@ -144,6 +144,8 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { DFhackCExport command_result plugin_shutdown (color_ostream &out) { DEBUG(config,out).print("shutting down %s\n", plugin_name); +// PluginManager handles unregistering our handler from EventManager, +// so we don't have to do that here return CR_OK; } @@ -201,11 +203,11 @@ void onUnitDeath(color_ostream& out, void* ptr) { // assign that unit to their previously assigned tomb in life int32_t building_id = it->second; if (!assign_to_tomb(unit_id, building_id)) { - DEBUG(event, out).print("Unit %d died - but failed to assign them to tomb %d\n", unit_id, building_id); + WARN(event, out).print("Unit %d died - but failed to assign them back to their tomb %d\n", unit_id, building_id); return; } // success, print status update and remove assignment from our memo-list - INFO(event, out).print("Unit %d died - assigning them to tomb %d\n", unit_id, building_id); + INFO(event, out).print("Unit %d died - assigning them back to their tomb\n", unit_id); tomb_assignments.erase(it); } From bad0448d345bfbb7a7a3cffd4c682a7673ac485d Mon Sep 17 00:00:00 2001 From: Myk Date: Thu, 5 Oct 2023 12:57:05 -0700 Subject: [PATCH 18/76] Update docs/plugins/preserve-tombs.rst --- docs/plugins/preserve-tombs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/preserve-tombs.rst b/docs/plugins/preserve-tombs.rst index e1d0ba8ce..2f01162c6 100644 --- a/docs/plugins/preserve-tombs.rst +++ b/docs/plugins/preserve-tombs.rst @@ -2,7 +2,7 @@ preserve-tombs ============== .. dfhack-tool:: - :summary: Fix tombs being unassigned to units on death + :summary: Preserve tomb assignments when assigned units die. :tags: fort bugfix If you find that the tombs you assign to units get unassigned from them when From 8a0956bc8316eb9c80edb86a80a9c375e409c973 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Oct 2023 13:30:00 -0700 Subject: [PATCH 19/76] restore mouse state after sending input to parent --- library/lua/gui.lua | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/library/lua/gui.lua b/library/lua/gui.lua index bb29124a5..4ee3eba4a 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -14,9 +14,15 @@ CLEAR_PEN = to_pen{tile=dfhack.internal.getAddress('init') and df.global.init.te TRANSPARENT_PEN = to_pen{tile=0, ch=0} KEEP_LOWER_PEN = to_pen{ch=32, fg=0, bg=0, keep_lower=true} +local function set_and_get_undo(field, is_set) + local prev_value = df.global.enabler[field] + df.global.enabler[field] = is_set and 1 or 0 + return function() df.global.enabler[field] = prev_value end +end + local MOUSE_KEYS = { - _MOUSE_L = function(is_set) df.global.enabler.mouse_lbut = is_set and 1 or 0 end, - _MOUSE_R = function(is_set) df.global.enabler.mouse_rbut = is_set and 1 or 0 end, + _MOUSE_L = curry(set_and_get_undo, 'mouse_lbut'), + _MOUSE_R = curry(set_and_get_undo, 'mouse_rbut'), _MOUSE_M = true, _MOUSE_L_DOWN = true, _MOUSE_R_DOWN = true, @@ -61,12 +67,16 @@ function simulateInput(screen,...) end end end + local undo_fns = {} for mk, fn in pairs(MOUSE_KEYS) do if type(fn) == 'function' then - fn(enabled_mouse_keys[mk]) + table.insert(undo_fns, fn(enabled_mouse_keys[mk])) end end dscreen._doSimulateInput(screen, keys) + for _, undo_fn in ipairs(undo_fns) do + undo_fn() + end end function mkdims_xy(x1,y1,x2,y2) From 194fb49f886958ee0b6b8ac4a9c1186881a33f38 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:22:55 +0000 Subject: [PATCH 20/76] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index a8a7ec8bc..d76ac0b71 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit a8a7ec8bc52ae8b63c1ae2a58f2904958734f3c5 +Subproject commit d76ac0b710f989a8581d6f1a5c4afb70a99fee1a From b68a317c052e555c491a223ed75b51413ba4bc97 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Oct 2023 14:25:14 -0700 Subject: [PATCH 21/76] fix encoding of cleanowned output --- plugins/cleanowned.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index 3fece8bfe..c13f7826c 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -147,14 +147,14 @@ command_result df_cleanowned (color_ostream &out, vector & parameters) out.print( "[%d] %s (wear level %d)", item->id, - description.c_str(), + DF2CONSOLE(description).c_str(), item->getWear() ); df::unit *owner = Items::getOwner(item); if (owner) - out.print(", owner %s", Translation::TranslateName(&owner->name,false).c_str()); + out.print(", owner %s", DF2CONSOLE(Translation::TranslateName(&owner->name,false)).c_str()); if (!dry_run) { From beaba199391d9e6a3a2cdecf3ae11bed045f93b9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Oct 2023 18:23:19 -0700 Subject: [PATCH 22/76] fix dig doc formatting --- docs/plugins/dig.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/plugins/dig.rst b/docs/plugins/dig.rst index 8d5c99536..2055a7572 100644 --- a/docs/plugins/dig.rst +++ b/docs/plugins/dig.rst @@ -147,15 +147,15 @@ Designation options: Other options: -``--zdown``, ``-d`` +``-d``, ``--zdown`` Only designates tiles on the cursor's z-level and below. -``--zup``, ``-u`` +``-u``, ``--zup`` Only designates tiles on the cursor's z-level and above. -``--cur-zlevel``, ``-z`` +``-z``, ``--cur-zlevel`` Only designates tiles on the same z-level as the cursor. -``--hidden``, ``-h`` +``-h``, ``--hidden`` Allows designation of hidden tiles, and picking a hidden tile as the target type. -``--no-auto``, ``-a`` +``-a``, ``--no-auto`` No automatic mining mode designation - useful if you want to avoid dwarves digging where you don't want them. digexp From 25600e45b092c04a177c4f72a60571e3992c0059 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Oct 2023 18:25:24 -0700 Subject: [PATCH 23/76] support melting masterworks in logistics --- docs/changelog.txt | 1 + docs/plugins/logistics.rst | 4 ++ docs/plugins/stockpiles.rst | 8 +++- plugins/logistics.cpp | 29 ++++++++---- plugins/lua/logistics.lua | 17 +++++-- plugins/lua/stockpiles.lua | 89 +++++++++++++++++++++++++++++++++---- 6 files changed, 128 insertions(+), 20 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index ee608652c..1a97e637d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -56,6 +56,7 @@ Template for new versions: - `preserve-tombs`: tracks tomb assignments to living units and ensures that the tomb stays assigned to them when they die. ## New Features +- `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay ## Fixes diff --git a/docs/plugins/logistics.rst b/docs/plugins/logistics.rst index a65571484..d9aeee067 100644 --- a/docs/plugins/logistics.rst +++ b/docs/plugins/logistics.rst @@ -72,3 +72,7 @@ Options Causes the command to act upon stockpiles with the given names or numbers instead of the stockpile that is currently selected in the UI. Note that the numbers are the stockpile numbers, not the building ids. +``-m``, ``--melt-masterworks`` + If specified with a ``logistics add melt`` command, will configure the + stockpile to allow melting of masterworks. By default, masterworks are not + marked for melting, even if they are in an automelt stockpile. diff --git a/docs/plugins/stockpiles.rst b/docs/plugins/stockpiles.rst index 249a27d13..7d4ec050b 100644 --- a/docs/plugins/stockpiles.rst +++ b/docs/plugins/stockpiles.rst @@ -100,7 +100,13 @@ Overlay This plugin provides a panel that appears when you select a stockpile via an `overlay` widget. You can use it to easily toggle `logistics` plugin features -like autotrade, automelt, or autotrain. +like autotrade, automelt, or autotrain. There are also buttons along the top frame for: + +- minimizing the panel (if it is in the way of the vanilla stockpile + configuration widgets) +- showing help for the overlay widget in `gui/launcher` (this page) +- configuring advanced settings for the stockpile, such as whether automelt + will melt masterworks .. _stockpiles-library: diff --git a/plugins/logistics.cpp b/plugins/logistics.cpp index 86f65d351..d4d2f0afe 100644 --- a/plugins/logistics.cpp +++ b/plugins/logistics.cpp @@ -44,6 +44,7 @@ enum StockpileConfigValues { STOCKPILE_CONFIG_TRADE = 2, STOCKPILE_CONFIG_DUMP = 3, STOCKPILE_CONFIG_TRAIN = 4, + STOCKPILE_CONFIG_MELT_MASTERWORKS = 5, }; static int get_config_val(PersistentDataItem& c, int index) { @@ -81,6 +82,7 @@ static PersistentDataItem& ensure_stockpile_config(color_ostream& out, int stock set_config_bool(c, STOCKPILE_CONFIG_TRADE, false); set_config_bool(c, STOCKPILE_CONFIG_DUMP, false); set_config_bool(c, STOCKPILE_CONFIG_TRAIN, false); + set_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS, false); return c; } @@ -259,8 +261,8 @@ public: class MeltStockProcessor : public StockProcessor { public: - MeltStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats &stats) - : StockProcessor("melt", stockpile_number, enabled, stats) { } + MeltStockProcessor(int32_t stockpile_number, bool enabled, ProcessorStats &stats, bool melt_masterworks) + : StockProcessor("melt", stockpile_number, enabled, stats), melt_masterworks(melt_masterworks) { } bool is_designated(color_ostream &out, df::item *item) override { return item->flags.bits.melt; @@ -294,7 +296,9 @@ public: } } - if (item->getQuality() >= df::item_quality::Masterful) + if (!melt_masterworks && item->getQuality() >= df::item_quality::Masterful) + return false; + if (item->flags.bits.artifact) return false; return true; @@ -305,6 +309,9 @@ public: item->flags.bits.melt = 1; return true; } + + private: + const bool melt_masterworks; }; class TradeStockProcessor: public StockProcessor { @@ -519,11 +526,12 @@ static void do_cycle(color_ostream& out, int32_t& melt_count, int32_t& trade_cou int32_t stockpile_number = bld->stockpile_number; bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT); + bool melt_masterworks = get_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS); bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE); bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP); bool train = get_config_bool(c, STOCKPILE_CONFIG_TRAIN); - MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats); + MeltStockProcessor melt_stock_processor(stockpile_number, melt, melt_stats, melt_masterworks); TradeStockProcessor trade_stock_processor(stockpile_number, trade, trade_stats); DumpStockProcessor dump_stock_processor(stockpile_number, dump, dump_stats); TrainStockProcessor train_stock_processor(stockpile_number, train, train_stats); @@ -555,7 +563,7 @@ static int logistics_getStockpileData(lua_State *L) { for (auto bld : df::global::world->buildings.other.STOCKPILE) { int32_t stockpile_number = bld->stockpile_number; - MeltStockProcessor melt_stock_processor(stockpile_number, false, melt_stats); + MeltStockProcessor melt_stock_processor(stockpile_number, false, melt_stats, false); TradeStockProcessor trade_stock_processor(stockpile_number, false, trade_stats); DumpStockProcessor dump_stock_processor(stockpile_number, false, dump_stats); TrainStockProcessor train_stock_processor(stockpile_number, false, train_stats); @@ -581,12 +589,14 @@ static int logistics_getStockpileData(lua_State *L) { PersistentDataItem &c = entry.second; bool melt = get_config_bool(c, STOCKPILE_CONFIG_MELT); + bool melt_masterworks = get_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS); bool trade = get_config_bool(c, STOCKPILE_CONFIG_TRADE); bool dump = get_config_bool(c, STOCKPILE_CONFIG_DUMP); bool train = get_config_bool(c, STOCKPILE_CONFIG_TRAIN); unordered_map config; config.emplace("melt", melt ? "true" : "false"); + config.emplace("melt_masterworks", melt_masterworks ? "true" : "false"); config.emplace("trade", trade ? "true" : "false"); config.emplace("dump", dump ? "true" : "false"); config.emplace("train", train ? "true" : "false"); @@ -633,11 +643,13 @@ static unordered_map get_stockpile_config(int32_t stockpile_number) if (watched_stockpiles.count(stockpile_number)) { PersistentDataItem &c = watched_stockpiles[stockpile_number]; stockpile_config.emplace("melt", get_config_bool(c, STOCKPILE_CONFIG_MELT)); + stockpile_config.emplace("melt_masterworks", get_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS)); stockpile_config.emplace("trade", get_config_bool(c, STOCKPILE_CONFIG_TRADE)); stockpile_config.emplace("dump", get_config_bool(c, STOCKPILE_CONFIG_DUMP)); stockpile_config.emplace("train", get_config_bool(c, STOCKPILE_CONFIG_TRAIN)); } else { stockpile_config.emplace("melt", false); + stockpile_config.emplace("melt_masterworks", false); stockpile_config.emplace("trade", false); stockpile_config.emplace("dump", false); stockpile_config.emplace("train", false); @@ -666,9 +678,9 @@ static int logistics_getStockpileConfigs(lua_State *L) { return 1; } -static void logistics_setStockpileConfig(color_ostream& out, int stockpile_number, bool melt, bool trade, bool dump, bool train) { - DEBUG(status, out).print("entering logistics_setStockpileConfig stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d\n", - stockpile_number, melt, trade, dump, train); +static void logistics_setStockpileConfig(color_ostream& out, int stockpile_number, bool melt, bool trade, bool dump, bool train, bool melt_masterworks) { + DEBUG(status, out).print("entering logistics_setStockpileConfig stockpile_number=%d, melt=%d, trade=%d, dump=%d, train=%d, melt_masterworks=%d\n", + stockpile_number, melt, trade, dump, train, melt_masterworks); if (!find_stockpile(stockpile_number)) { out.printerr("invalid stockpile number: %d\n", stockpile_number); @@ -677,6 +689,7 @@ static void logistics_setStockpileConfig(color_ostream& out, int stockpile_numbe auto &c = ensure_stockpile_config(out, stockpile_number); set_config_bool(c, STOCKPILE_CONFIG_MELT, melt); + set_config_bool(c, STOCKPILE_CONFIG_MELT_MASTERWORKS, melt_masterworks); set_config_bool(c, STOCKPILE_CONFIG_TRADE, trade); set_config_bool(c, STOCKPILE_CONFIG_DUMP, dump); set_config_bool(c, STOCKPILE_CONFIG_TRAIN, train); diff --git a/plugins/lua/logistics.lua b/plugins/lua/logistics.lua index 0231ce593..2f260cc59 100644 --- a/plugins/lua/logistics.lua +++ b/plugins/lua/logistics.lua @@ -29,6 +29,7 @@ function getStockpileData() trade=make_stat('trade', stockpile_number, stats, configs), dump=make_stat('dump', stockpile_number, stats, configs), train=make_stat('train', stockpile_number, stats, configs), + melt_masterworks=configs[stockpile_number] and configs[stockpile_number].melt_masterworks == 'true', }) end table.sort(data, function(a, b) return a.sort_key < b.sort_key end) @@ -41,16 +42,24 @@ local function print_stockpile_data(data) name_len = math.min(40, math.max(name_len, #sp.name)) end + local has_melt_mastworks = false + print('Designated/designatable items in stockpiles:') print() local fmt = '%6s %-' .. name_len .. 's %4s %10s %5s %11s %4s %10s %5s %11s'; print(fmt:format('number', 'name', 'melt', 'melt items', 'trade', 'trade items', 'dump', 'dump items', 'train', 'train items')) local function uline(len) return ('-'):rep(len) end print(fmt:format(uline(6), uline(name_len), uline(4), uline(10), uline(5), uline(11), uline(4), uline(10), uline(5), uline(11))) - local function get_enab(stats) return ('[%s]'):format(stats.enabled and 'x' or ' ') end + local function get_enab(stats, ch) return ('[%s]'):format(stats.enabled and (ch or 'x') or ' ') end local function get_dstat(stats) return ('%d/%d'):format(stats.designated, stats.designated + stats.can_designate) end for _,sp in ipairs(data) do - print(fmt:format(sp.stockpile_number, sp.name, get_enab(sp.melt), get_dstat(sp.melt), get_enab(sp.trade), get_dstat(sp.trade), get_enab(sp.dump), get_dstat(sp.dump), get_enab(sp.train), get_dstat(sp.train))) + has_melt_mastworks = has_melt_mastworks or sp.melt_masterworks + print(fmt:format(sp.stockpile_number, sp.name, get_enab(sp.melt, sp.melt_masterworks and 'X'), get_dstat(sp.melt), + get_enab(sp.trade), get_dstat(sp.trade), get_enab(sp.dump), get_dstat(sp.dump), get_enab(sp.train), get_dstat(sp.train))) + end + if has_melt_mastworks then + print() + print('An "X" in the "melt" column indicates that masterworks in the stockpile will be melted.') end end @@ -101,7 +110,8 @@ local function do_add_stockpile_config(features, opts) features.melt or config.melt == 1, features.trade or config.trade == 1, features.dump or config.dump == 1, - features.train or config.train == 1) + features.train or config.train == 1, + not not opts.melt_masterworks) end end end) @@ -125,6 +135,7 @@ local function process_args(opts, args) return argparse.processArgsGetopt(args, { {'h', 'help', handler=function() opts.help = true end}, + {'m', 'melt-masterworks', handler=function() opts.melt_masterworks = true end}, {'s', 'stockpile', hasArg=true, handler=function(arg) opts.sp = arg end}, }) end diff --git a/plugins/lua/stockpiles.lua b/plugins/lua/stockpiles.lua index 4707c97ad..f25205a9c 100644 --- a/plugins/lua/stockpiles.lua +++ b/plugins/lua/stockpiles.lua @@ -4,6 +4,7 @@ local argparse = require('argparse') local gui = require('gui') local logistics = require('plugins.logistics') local overlay = require('plugins.overlay') +local textures = require('gui.textures') local widgets = require('gui.widgets') local STOCKPILES_DIR = 'dfhack-config/stockpiles' @@ -262,6 +263,45 @@ local function do_export() export_view = export_view and export_view:raise() or StockpilesExportScreen{}:show() end +-------------------- +-- ConfigModal +-------------------- + +ConfigModal = defclass(ConfigModal, gui.ZScreenModal) +ConfigModal.ATTRS{ + focus_path='stockpiles_config', + on_close=DEFAULT_NIL, +} + +function ConfigModal:init() + local sp = dfhack.gui.getSelectedStockpile(true) + local cur_setting = false + if sp then + local config = logistics.logistics_getStockpileConfigs(sp.stockpile_number)[1] + cur_setting = config.melt_masterworks == 1 + end + + self:addviews{ + widgets.Window{ + frame={w=35, h=10}, + frame_title='Advanced logistics settings', + subviews={ + widgets.ToggleHotkeyLabel{ + view_id='melt_masterworks', + frame={l=0, t=0}, + key='CUSTOM_M', + label='Melt masterworks', + initial_option=cur_setting, + }, + }, + }, + } +end + +function ConfigModal:onDismiss() + self.on_close{melt_masterworks=self.subviews.melt_masterworks:getOptionValue()} +end + -------------------- -- MinimizeButton -------------------- @@ -368,9 +408,7 @@ function StockpilesOverlay:init() view_id='main', frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, - visible=function() - return not self.minimized - end, + visible=function() return not self.minimized end, subviews={ -- widgets.HotkeyLabel{ -- frame={t=0, l=0}, @@ -439,14 +477,40 @@ function StockpilesOverlay:init() }, } + local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} + local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} + local help_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} + local configure_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol + self:addviews{ - main_panel, MinimizeButton{ + main_panel, + MinimizeButton{ frame={t=0, r=9}, - get_minimized_fn=function() - return self.minimized - end, + get_minimized_fn=function() return self.minimized end, on_click=self:callback('toggleMinimized'), }, + widgets.Label{ + frame={t=0, r=5, w=3}, + text={ + {tile=button_pen_left}, + {tile=configure_pen_center}, + {tile=button_pen_right}, + }, + on_click=function() ConfigModal{on_close=self:callback('on_custom_config')}:show() end, + }, + widgets.Label{ + frame={t=0, r=1, w=3}, + text={ + {tile=button_pen_left}, + {tile=help_pen_center}, + {tile=button_pen_right}, + }, + on_click=function() dfhack.run_command('gui/launcher', 'stockpiles ') end, + }, } end @@ -475,7 +539,16 @@ function StockpilesOverlay:toggleLogisticsFeature(feature) -- logical xor logistics.logistics_setStockpileConfig(config.stockpile_number, (feature == 'melt') ~= (config.melt == 1), (feature == 'trade') ~= (config.trade == 1), - (feature == 'dump') ~= (config.dump == 1), (feature == 'train') ~= (config.train == 1)) + (feature == 'dump') ~= (config.dump == 1), (feature == 'train') ~= (config.train == 1), + config.melt_masterworks == 1) +end + +function StockpilesOverlay:on_custom_config(custom) + local sp = dfhack.gui.getSelectedStockpile(true) + if not sp then return end + local config = logistics.logistics_getStockpileConfigs(sp.stockpile_number)[1] + logistics.logistics_setStockpileConfig(config.stockpile_number, + config.melt == 1, config.trade == 1, config.dump == 1, config.train == 1, custom.melt_masterworks) end function StockpilesOverlay:toggleMinimized() From 16eea7c8d45582cf2430e5e5131f1983df8e175f Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 7 Oct 2023 06:33:42 +0000 Subject: [PATCH 24/76] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index aeab463a0..e49ebc338 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit aeab463a0d35ac9ff896db840735cabfa12df712 +Subproject commit e49ebc3380fd376d85e8d42accb68b25c4bc969c diff --git a/scripts b/scripts index d76ac0b71..740683792 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit d76ac0b710f989a8581d6f1a5c4afb70a99fee1a +Subproject commit 740683792d2e4bbdbb6d008f1dab1a1c07b9ce40 From a3cd847181b3f02f865b1bc7508f039320b4f449 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 7 Oct 2023 07:21:47 +0000 Subject: [PATCH 25/76] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index e49ebc338..ff278cfe3 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit e49ebc3380fd376d85e8d42accb68b25c4bc969c +Subproject commit ff278cfe3b98f6f6a36c7a4be19884677b753a8d diff --git a/scripts b/scripts index 740683792..728d90271 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 740683792d2e4bbdbb6d008f1dab1a1c07b9ce40 +Subproject commit 728d902712655592ec4385e88fd36077641ccfb1 From c4b31176a96fd1e579c029a7315465c43adc2cc9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 00:46:42 -0700 Subject: [PATCH 26/76] remove outdated info for persistence API --- docs/dev/Lua API.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 49f0b3633..9f9a3e374 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -684,7 +684,7 @@ Persistent configuration storage -------------------------------- This api is intended for storing configuration options in the world itself. -It probably should be restricted to data that is world-dependent. +It is intended for data that is world-dependent. Entries are identified by a string ``key``, but it is also possible to manage multiple entries with the same key; their identity is determined by ``entry_id``. @@ -717,10 +717,8 @@ Every entry has a mutable string ``value``, and an array of 7 mutable ``ints``. otherwise the existing one is simply updated. Returns *entry, did_create_new* -Since the data is hidden in data structures owned by the DF world, -and automatically stored in the save game, these save and retrieval -functions can just copy values in memory without doing any actual I/O. -However, currently every entry has a 180+-byte dead-weight overhead. +The data is kept in memory, so no I/O occurs when getting or saving keys. It is +all written to a json file in the game save directory when the game is saved. It is also possible to associate one bit per map tile with an entry, using these two methods: From ae16b65c890e41297aeea4c7223a5239a4a53193 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 12:10:13 -0700 Subject: [PATCH 27/76] process keys before cursor dragging so a held mouse button doesn't inhibit text input --- library/lua/gui/widgets.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 05d237f35..34b154bf8 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -739,12 +739,6 @@ function EditField:onInput(keys) end end return not not self.key - elseif keys._MOUSE_L_DOWN then - local mouse_x = self:getMousePos() - if mouse_x then - self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) - return true - end elseif keys._STRING then local old = self.text if keys._STRING == 0 then @@ -795,6 +789,12 @@ function EditField:onInput(keys) elseif keys.CUSTOM_CTRL_V then self:insert(dfhack.internal.getClipboardTextCp437()) return true + elseif keys._MOUSE_L_DOWN then + local mouse_x = self:getMousePos() + if mouse_x then + self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) + return true + end end -- if we're modal, then unconditionally eat all the input From 37ad0c3e6f29b4a5a1d16ab7e0a8d39f03852c28 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 12:10:52 -0700 Subject: [PATCH 28/76] add more focus string details for info panels --- library/modules/Gui.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 58763262f..a53876d8a 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -240,7 +240,14 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) switch(game->main_interface.info.current_mode) { case df::enums::info_interface_mode_type::CREATURES: - newFocusString += '/' + enum_item_key(game->main_interface.info.creatures.current_mode); + if (game->main_interface.info.creatures.showing_overall_training) + newFocusString += "/OverallTraining"; + else if (game->main_interface.info.creatures.showing_activity_details) + newFocusString += "/ActivityDetails"; + else if (game->main_interface.info.creatures.adding_trainer) + newFocusString += "/AddingTrainer"; + else + newFocusString += '/' + enum_item_key(game->main_interface.info.creatures.current_mode); break; case df::enums::info_interface_mode_type::BUILDINGS: newFocusString += '/' + enum_item_key(game->main_interface.info.buildings.mode); From ae1d6f98f64210751b9e420fe1e4b0be65bd02f6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 12:11:24 -0700 Subject: [PATCH 29/76] add basic framework for info search widget --- plugins/lua/sort.lua | 14 ++-- plugins/lua/sort/creatures.lua | 139 +++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 plugins/lua/sort/creatures.lua diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 6d8c8a298..1159ce56e 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1,5 +1,6 @@ local _ENV = mkmodule('plugins.sort') +local creatures = require('plugins.sort.creatures') local gui = require('gui') local overlay = require('plugins.overlay') local setbelief = reqscript('modtools/set-belief') @@ -275,29 +276,29 @@ local function get_ranged_skill_effectiveness_rating(unit) return get_rating(ranged_skill_effectiveness(unit), 0, 800000, 72, 52, 31, 11) end -local function make_sort_by_ranged_skill_effectiveness_desc(list) +local function make_sort_by_ranged_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 = ranged_skill_effectiveness(unit1, list) - local rating2 = ranged_skill_effectiveness(unit2, list) + local rating1 = ranged_skill_effectiveness(unit1) + local rating2 = ranged_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_ranged_skill_effectiveness_asc(list) +local function make_sort_by_ranged_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 = ranged_skill_effectiveness(unit1, list) - local rating2 = ranged_skill_effectiveness(unit2, list) + local rating1 = ranged_skill_effectiveness(unit1) + local rating2 = ranged_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 @@ -1261,6 +1262,7 @@ end OVERLAY_WIDGETS = { squad_assignment=SquadAssignmentOverlay, squad_annotation=SquadAnnotationOverlay, + creatures=creatures.InfoOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/creatures.lua b/plugins/lua/sort/creatures.lua new file mode 100644 index 000000000..adf6b3fe9 --- /dev/null +++ b/plugins/lua/sort/creatures.lua @@ -0,0 +1,139 @@ +local _ENV = mkmodule('plugins.sort.creatures') + +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local creatures = df.global.game.main_interface.info.creatures + +-- ---------------------- +-- InfoOverlay +-- + +InfoOverlay = defclass(InfoOverlay, overlay.OverlayWidget) +InfoOverlay.ATTRS{ + default_pos={x=64, y=9}, + default_enabled=true, + viewscreens={ + 'dwarfmode/Info/CREATURES/CITIZEN', + 'dwarfmode/Info/CREATURES/PET', + 'dwarfmode/Info/CREATURES/OverallTraining', + 'dwarfmode/Info/CREATURES/AddingTrainer', + 'dwarfmode/Info/CREATURES/OTHER', + 'dwarfmode/Info/CREATURES/DECEASED', + }, + hotspot=true, + overlay_onupdate_max_freq_seconds=0, + frame={w=40, h=3}, +} + +function InfoOverlay:init() + self.state = {} + + self:addviews{ + widgets.BannerPanel{ + view_id='panel', + frame={l=0, t=0, r=0, h=1}, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=self:callback('text_input'), + }, + }, + }, + } +end + +function InfoOverlay:overlay_onupdate(scr) + if next(self.state) and not dfhack.gui.matchFocusString('dwarfmode/Info', scr) then + -- TODO: add dynamically allocated elements that were not visible at the time of + -- closure back to the list so they can be properly disposed of + self.state = {} + self.subviews.search:setText('') + self.subviews.search:setFocus(false) + self.overlay_onupdate_max_freq_seconds = 60 + end +end + +local function are_tabs_in_two_rows() + local pen = dfhack.screen.readTile(64, 6, false) -- tile is occupied iff tabs are in one row + return pen.ch == 0 +end + +local function resize_overlay(self) + local sw = dfhack.screen.getWindowSize() + local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) + if overlay_width ~= self.frame.w then + self.frame.w = overlay_width + return true + end +end + +function InfoOverlay:updateFrames() + local ret = resize_overlay(self) + local two_rows = are_tabs_in_two_rows() + if (self.two_rows == two_rows) then return ret end + self.two_rows = two_rows + self.subviews.panel.frame.t = two_rows and 2 or 0 + return true +end + +local function get_key() + if creatures.current_mode == df.unit_list_mode_type.PET then + if creatures.showing_overall_training then + return 'PET_OT' + elseif creatures.adding_trainer then + return 'PET_AT' + end + end + return df.unit_list_mode_type[creatures.current_mode] +end + +local function check_context(self) + local key = get_key() + if self.state.prev_key ~= key then + self.state.prev_key = key + local prev_text = ensure_key(self.state, key).prev_text + self.subviews.search:setText(prev_text or '') + end +end + +function InfoOverlay:onRenderBody(dc) + if next(self.state) then + check_context(self) + end + if self:updateFrames() then + self:updateLayout() + end + self.overlay_onupdate_max_freq_seconds = 0 + InfoOverlay.super.onRenderBody(self, dc) +end + +function InfoOverlay:text_input(text) + if not next(self.state) and text == '' then return end + -- the EditField state is guaranteed to be consistent with the current + -- context since when clicking to switch tabs, onRenderBody is always called + -- before this text_input callback, even if a key is pressed before the next + -- graphical frame would otherwise be printed. if this ever becomes untrue, + -- then we can add an on_char handler to the EditField that also calls + -- check_context. + local key = get_key() + local prev_text = ensure_key(self.state, key).prev_text + if text == prev_text then return end + if prev_text and text:startswith(prev_text) then + -- TODO: search + print('performing incremental search; text:', text, 'key:', key) + else + -- TODO: save list if not already saved + -- TODO: else restore list from saved list + -- TODO: if text ~= '' then search + -- TODO: sort according to vanilla sort widget state + print('performing full search; text:', text, 'key:', key) + end + -- TODO: save visible list + self.state[key].prev_text = text +end + +return _ENV From 673287d0a4bbf15940e29dcd3d53a4102e6013a1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 18:40:47 -0700 Subject: [PATCH 30/76] implement search logic --- plugins/lua/sort/creatures.lua | 199 ++++++++++++++++++++++++++++++--- 1 file changed, 183 insertions(+), 16 deletions(-) diff --git a/plugins/lua/sort/creatures.lua b/plugins/lua/sort/creatures.lua index adf6b3fe9..563bd3cca 100644 --- a/plugins/lua/sort/creatures.lua +++ b/plugins/lua/sort/creatures.lua @@ -2,9 +2,168 @@ local _ENV = mkmodule('plugins.sort.creatures') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') +local utils = require('utils') local creatures = df.global.game.main_interface.info.creatures +-- these sort functions attempt to match the vanilla sort behavior, which is not +-- quite the same as the rest of DFHack. For example, in other DFHack sorts, +-- we'd always sort by name descending as a secondary sort. To match vanilla sorting, +-- if the primary sort is ascending, the secondary name sort will also be ascending. +-- +-- also note that vanilla sorts are not stable, so there might still be some jitter +-- if the player clicks one of the vanilla sort widgets after searching +local function sort_by_name_desc(a, b) + return a.sort_name < b.sort_name +end + +local function sort_by_name_asc(a, b) + return a.sort_name > b.sort_name +end + +local function sort_by_prof_desc(a, b) + if a.profession_list_order1 == b.profession_list_order1 then + return sort_by_name_desc(a, b) + end + return a.profession_list_order1 < b.profession_list_order1 +end + +local function sort_by_prof_asc(a, b) + if a.profession_list_order1 == b.profession_list_order1 then + return sort_by_name_asc(a, b) + end + return a.profession_list_order1 > b.profession_list_order1 +end + +local function sort_by_job_name_desc(a, b) + if a.job_sort_name == b.job_sort_name then + return sort_by_name_desc(a, b) + end + return a.job_sort_name < b.job_sort_name +end + +local function sort_by_job_name_asc(a, b) + if a.job_sort_name == b.job_sort_name then + -- use descending tertiary sort for visual stability + return sort_by_name_desc(a, b) + end + return a.job_sort_name > b.job_sort_name +end + +local function sort_by_job_desc(a, b) + if not not a.jb == not not b.jb then + return sort_by_job_name_desc(a, b) + end + return not not a.jb +end + +local function sort_by_job_asc(a, b) + if not not a.jb == not not b.jb then + return sort_by_job_name_asc(a, b) + end + return not not b.jb +end + +local function sort_by_stress_desc(a, b) + if a.stress == b.stress then + return sort_by_name_desc(a, b) + end + return a.stress > b.stress +end + +local function sort_by_stress_asc(a, b) + if a.stress == b.stress then + return sort_by_name_asc(a, b) + end + return a.stress < b.stress +end + +local function get_sort() + if creatures.sorting_cit_job then + return creatures.sorting_cit_job_is_ascending and sort_by_job_asc or sort_by_job_desc + elseif creatures.sorting_cit_stress then + return creatures.sorting_cit_stress_is_ascending and sort_by_stress_asc or sort_by_stress_desc + elseif creatures.sorting_cit_nameprof_doing_prof then + return creatures.sorting_cit_nameprof_is_ascending and sort_by_prof_asc or sort_by_prof_desc + else + return creatures.sorting_cit_nameprof_is_ascending and sort_by_name_asc or sort_by_name_desc + end +end + +local function copy_to_lua_table(vec) + local tab = {} + for k,v in ipairs(vec) do + tab[k+1] = v + end + return tab +end + +local function general_search(vec, get_search_key_fn, get_sort_fn, data, filter, incremental) + if not data.saved_original then + data.saved_original = copy_to_lua_table(vec) + elseif not incremental then + vec:assign(data.saved_original) + end + if filter ~= '' then + local search_tokens = filter:split() + for idx = #vec-1,0,-1 do + local search_key = get_search_key_fn(vec[idx]) + if search_key and not utils.search_text(search_key, search_tokens) then + vec:erase(idx) + end + end + end + data.saved_visible = copy_to_lua_table(vec) + if get_sort_fn then + table.sort(data.saved_visible, get_sort_fn()) + vec:assign(data.saved_visible) + end +end + +-- add dynamically allocated elements that were not visible at the time of +-- closure back to the vector so they can be cleaned up when it is next initialized +local function cri_unitst_cleanup(vec, data) + if not data.saved_visible or not data.saved_original then return end + for _,elem in ipairs(data.saved_original) do + if not utils.linear_index(data.saved_visible, elem) then + vec:insert('#', elem) + end + end +end + +local function make_cri_unitst_handlers(vec) + return { + search_fn=curry(general_search, vec, + function(elem) return elem.sort_name end, + get_sort), + cleanup_fn=curry(cri_unitst_cleanup, vec), + } +end + +local function overall_training_search(data, filter, incremental) + general_search(creatures.atk_index, function(elem) + local raw = df.creature_raw.find(elem) + if not raw then return '' end + return raw.name[1] + end, nil, data, filter, incremental) +end + +local function assign_trainer_search(data, filter, incremental) + general_search(creatures.trainer, function(elem) + if not elem then return nil end + return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.units.getProfessionName(elem)) + end, nil, data, filter, incremental) +end + +local HANDLERS = { + CITIZEN=make_cri_unitst_handlers(creatures.cri_unit.CITIZEN), + PET=make_cri_unitst_handlers(creatures.cri_unit.PET), + OTHER=make_cri_unitst_handlers(creatures.cri_unit.OTHER), + DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED), + PET_OT={search_fn=overall_training_search}, + PET_AT={search_fn=assign_trainer_search}, +} + -- ---------------------- -- InfoOverlay -- @@ -46,10 +205,18 @@ function InfoOverlay:init() } end -function InfoOverlay:overlay_onupdate(scr) - if next(self.state) and not dfhack.gui.matchFocusString('dwarfmode/Info', scr) then - -- TODO: add dynamically allocated elements that were not visible at the time of - -- closure back to the list so they can be properly disposed of +local function cleanup(state) + for k,v in pairs(state) do + local cleanup_fn = safe_index(HANDLERS, k, 'cleanup_fn') + if cleanup_fn then cleanup_fn(v) end + end +end + +function InfoOverlay:overlay_onupdate() + if next(self.state) and + not dfhack.gui.matchFocusString('dwarfmode/Info', dfhack.gui.getDFViewscreen(true)) + then + cleanup(self.state) self.state = {} self.subviews.search:setText('') self.subviews.search:setFocus(false) @@ -121,19 +288,19 @@ function InfoOverlay:text_input(text) -- check_context. local key = get_key() local prev_text = ensure_key(self.state, key).prev_text - if text == prev_text then return end - if prev_text and text:startswith(prev_text) then - -- TODO: search - print('performing incremental search; text:', text, 'key:', key) - else - -- TODO: save list if not already saved - -- TODO: else restore list from saved list - -- TODO: if text ~= '' then search - -- TODO: sort according to vanilla sort widget state - print('performing full search; text:', text, 'key:', key) - end - -- TODO: save visible list + -- some screens reset their contents between context switches; regardless + -- a switch back to the context should results in an incremental search + local incremental = prev_text and text:startswith(prev_text) + HANDLERS[key].search_fn(self.state[key], text, incremental) self.state[key].prev_text = text end +function InfoOverlay:onInput(keys) + if keys._MOUSE_R and self.subviews.search.focus then + self.subviews.search:setFocus(false) + return true + end + return InfoOverlay.super.onInput(self, keys) +end + return _ENV From 504948333035af6be03d98bb532c04fe44e5930b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 18:55:39 -0700 Subject: [PATCH 31/76] refactor text search routine out into utils fn --- docs/changelog.txt | 2 ++ docs/dev/Lua API.rst | 21 +++++++++++++--- library/lua/gui/widgets.lua | 29 +--------------------- library/lua/utils.lua | 26 +++++++++++++++++++ plugins/lua/buildingplan/itemselection.lua | 1 - 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1a97e637d..57b78c087 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,8 +67,10 @@ Template for new versions: ## API ## Lua +- ``utils.search_text``: text search routine (generalized from ``widgets.FilteredList``) ## Removed +- ``FILTER_FULL_TEXT``: moved from ``gui.widgets`` to ``utils``; if your full text search preference is lost, please reset it in `gui/control-panel` # 50.11-r1 diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 9f9a3e374..f5314e4fb 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3337,6 +3337,20 @@ utils Exactly like ``erase_sorted_key``, but if field is specified, takes the key from ``item[field]``. +* ``utils.search_text(text,search_tokens)`` + + Returns true if all the search tokens are found within ``text``. The text and + search tokens are normalized to lower case and special characters (e.g. ``A`` + with a circle on it) are converted to their "basic" forms (e.g. ``a``). + ``search_tokens`` can be a string or a table of strings. If it is a string, + it is split into space-separated tokens before matching. The search tokens + are treated literally, so any special regular expression characters do not + need to be escaped. If ``utils.FILTER_FULL_TEXT`` is ``true``, then the + search tokens can match any part of ``text``. If it is ``false``, then the + matches must happen at the beginning of words within ``text``. You can change + the value of ``utils.FILTER_FULL_TEXT`` in `gui/control-panel` on the + "Preferences" tab. + * ``utils.call_with_string(obj,methodname,...)`` Allocates a temporary string object, calls ``obj:method(tmp,...)``, and @@ -5291,12 +5305,11 @@ FilteredList class ------------------ This widget combines List, EditField and Label into a combo-box like -construction that allows filtering the list by subwords of its items. +construction that allows filtering the list. In addition to passing through all attributes supported by List, it supports: -:case_sensitive: If ``true``, matching is case sensitive. Defaults to ``false``. :edit_pen: If specified, used instead of ``cursor_pen`` for the edit field. :edit_below: If true, the edit field is placed below the list instead of above. :edit_key: If specified, the edit field is disabled until this key is pressed. @@ -5345,9 +5358,9 @@ Filter behavior: By default, the filter matches substrings that start at the beginning of a word (or after any punctuation). You can instead configure filters to match any -substring with a command like:: +substring across the full text with a command like:: - :lua require('gui.widgets').FILTER_FULL_TEXT=true + :lua require('utils').FILTER_FULL_TEXT=true TabBar class ------------ diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 05d237f35..61da7af65 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -2017,12 +2017,9 @@ end -- Filtered List -- ------------------- -FILTER_FULL_TEXT = false - FilteredList = defclass(FilteredList, Widget) FilteredList.ATTRS { - case_sensitive = false, edit_below = false, edit_key = DEFAULT_NIL, edit_ignore_keys = DEFAULT_NIL, @@ -2172,7 +2169,6 @@ function FilteredList:setFilter(filter, pos) pos = nil for i,v in ipairs(self.choices) do - local ok = true local search_key = v.search_key if not search_key then if type(v.text) ~= 'table' then @@ -2187,30 +2183,7 @@ function FilteredList:setFilter(filter, pos) search_key = table.concat(texts, ' ') end end - for _,key in ipairs(tokens) do - key = key:escape_pattern() - if key ~= '' then - search_key = dfhack.toSearchNormalized(search_key) - key = dfhack.toSearchNormalized(key) - if not self.case_sensitive then - search_key = string.lower(search_key) - key = string.lower(key) - end - - -- the separate checks for non-space or non-punctuation allows - -- punctuation itself to be matched if that is useful (e.g. - -- filenames or parameter names) - if not FILTER_FULL_TEXT and not search_key:match('%f[^%p\x00]'..key) - and not search_key:match('%f[^%s\x00]'..key) then - ok = false - break - elseif FILTER_FULL_TEXT and not search_key:find(key) then - ok = false - break - end - end - end - if ok then + if utils.search_text(search_key, tokens) then table.insert(choices, v) cidx[#choices] = i if ipos == i then diff --git a/library/lua/utils.lua b/library/lua/utils.lua index 3883439f1..fb41835da 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -460,6 +460,32 @@ function erase_sorted(vector,item,field,cmp) return erase_sorted_key(vector,key,field,cmp) end +FILTER_FULL_TEXT = false + +function search_text(text, search_tokens) + text = dfhack.toSearchNormalized(text) + if type(search_tokens) ~= 'table' then + search_tokens = search_tokens:split() + end + + for _,search_token in ipairs(search_tokens) do + if search_token == '' then goto continue end + search_token = dfhack.toSearchNormalized(search_token:escape_pattern()) + + -- the separate checks for non-space or non-punctuation allows + -- punctuation itself to be matched if that is useful (e.g. + -- filenames or parameter names) + if not FILTER_FULL_TEXT and not text:match('%f[^%p\x00]'..search_token) + and not text:match('%f[^%s\x00]'..search_token) then + return false + elseif FILTER_FULL_TEXT and not text:find(search_token) then + return false + end + ::continue:: + end + return true +end + -- Calls a method with a string temporary function call_with_string(obj,methodname,...) return dfhack.with_temp_object( diff --git a/plugins/lua/buildingplan/itemselection.lua b/plugins/lua/buildingplan/itemselection.lua index 9dfd0cc69..4b8ee73d8 100644 --- a/plugins/lua/buildingplan/itemselection.lua +++ b/plugins/lua/buildingplan/itemselection.lua @@ -151,7 +151,6 @@ function ItemSelection:init() widgets.FilteredList{ view_id='flist', frame={t=0, b=0}, - case_sensitive=false, choices=choices, icon_width=2, on_submit=self:callback('toggle_group'), From 8184a093d9b0f76a33dbef94c5ec68c00c79f5a4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 19:00:26 -0700 Subject: [PATCH 32/76] support dynamic onupdate frequency adjustments set to 0 for an immediate burst of high frequency calls --- docs/changelog.txt | 1 + docs/dev/overlay-dev-guide.rst | 5 ++++- plugins/lua/overlay.lua | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1a97e637d..ba05e6e87 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates ## Documentation diff --git a/docs/dev/overlay-dev-guide.rst b/docs/dev/overlay-dev-guide.rst index 54e200700..b5b6cf0e3 100644 --- a/docs/dev/overlay-dev-guide.rst +++ b/docs/dev/overlay-dev-guide.rst @@ -135,7 +135,10 @@ The ``overlay.OverlayWidget`` superclass defines the following class attributes: seconds) that your widget can take to react to changes in information and not annoy the player. Set to 0 to be called at the maximum rate. Be aware that running more often than you really need to will impact game FPS, - especially if your widget can run while the game is unpaused. + especially if your widget can run while the game is unpaused. If you change + the value of this attribute dynamically, it may not be noticed until the + previous timeout expires. However, if you need a burst of high-frequency + updates, set it to ``0`` and it will be noticed immediately. Registering a widget with the overlay framework *********************************************** diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 9751561a8..d8b3c4f81 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -433,8 +433,12 @@ end -- reduces the next call by a small random amount to introduce jitter into the -- widget processing timings local function do_update(name, db_entry, now_ms, vs) - if db_entry.next_update_ms > now_ms then return end local w = db_entry.widget + if w.overlay_onupdate_max_freq_seconds ~= 0 and + db_entry.next_update_ms > now_ms + then + return + end db_entry.next_update_ms = get_next_onupdate_timestamp(now_ms, w) if detect_frame_change(w, function() return w:overlay_onupdate(vs) end) then if register_trigger_lock_screen(w:overlay_trigger(), name) then From d0ffd78479e5e6e29d2f08751df7113e0e999e92 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 19:04:19 -0700 Subject: [PATCH 33/76] more focus string details for location selector --- library/modules/Gui.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 58763262f..50a878070 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -556,7 +556,13 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) } if (game->main_interface.location_selector.open) { newFocusString = baseFocus; - newFocusString += "/LocationSelector"; + newFocusString += "/LocationSelector/"; + if (game->main_interface.location_selector.choosing_temple_religious_practice) + newFocusString += "Temple"; + else if (game->main_interface.location_selector.choosing_craft_guild) + newFocusString += "Guildhall"; + else + newFocusString += "Default"; focusStrings.push_back(newFocusString); } if (game->main_interface.location_details.open) { From f8a95667ee81c6471ee31f0e84789a7c13d48d99 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 19:12:53 -0700 Subject: [PATCH 34/76] succumb to american spelling --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 3 +++ library/lua/dfhack.lua | 3 +++ 3 files changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1a97e637d..7693052d9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## API ## Lua +- added ``GRAY`` color aliases for ``GREY`` colors ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 9f9a3e374..781ba6819 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3081,6 +3081,9 @@ environment by the mandatory init file dfhack.lua: COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN, COLOR_LIGHTRED, COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE + ``COLOR_GREY`` and ``COLOR_DARKGREY`` can also be spelled ``COLOR_GRAY`` and + ``COLOR_DARKGRAY``. + * State change event codes, used by ``dfhack.onStateChange`` Available only in the `core context `, as is the event itself: diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 8ea5e9dac..059c008d2 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -38,6 +38,9 @@ COLOR_LIGHTMAGENTA = 13 COLOR_YELLOW = 14 COLOR_WHITE = 15 +COLOR_GRAY = COLOR_GREY +COLOR_DARKGRAY = COLOR_DARKGREY + -- Events if dfhack.is_core_context then From 53c29a05c96513de26168882af209c6f2ada4aa4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 8 Oct 2023 02:19:08 +0000 Subject: [PATCH 35/76] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 728d90271..0759b6b7d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 728d902712655592ec4385e88fd36077641ccfb1 +Subproject commit 0759b6b7dde184c3bf36669f92138748a0e2382b From 2accc5ff569d6445f7dd409f1e4182c2e14162b4 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sun, 8 Oct 2023 02:48:53 +0000 Subject: [PATCH 36/76] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 0759b6b7d..20d54145e 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 0759b6b7dde184c3bf36669f92138748a0e2382b +Subproject commit 20d54145e97fbae58cda391f66e6a32c7ee20330 From 9865eda9848c3543ae5d51728777bed7610adcb6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 20:29:51 -0700 Subject: [PATCH 37/76] add changelog for #3849 --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index a985793a4..feda83f2c 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: ## New Features - `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay +- `sort`: new search widgets for all the "Creatures" tabs on the info panel, e.g. "Citizens", "Pets", etc. This includes the assign trainers and view species training knowledge screens as well ## Fixes From 8886cd7e79c3e347993a6758f1ac1bd833b53aec Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 7 Oct 2023 20:51:11 -0700 Subject: [PATCH 38/76] Allow searching for job --- plugins/lua/sort/creatures.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/lua/sort/creatures.lua b/plugins/lua/sort/creatures.lua index 563bd3cca..eeab12087 100644 --- a/plugins/lua/sort/creatures.lua +++ b/plugins/lua/sort/creatures.lua @@ -134,7 +134,9 @@ end local function make_cri_unitst_handlers(vec) return { search_fn=curry(general_search, vec, - function(elem) return elem.sort_name end, + function(elem) + return ('%s %s'):format(elem.sort_name, elem.job_sort_name) + end, get_sort), cleanup_fn=curry(cri_unitst_cleanup, vec), } From ab386a0ed23e2a1921b6341ad4e116986e1c246b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 22:50:31 -0700 Subject: [PATCH 39/76] add docs for creatures search overlay --- docs/plugins/sort.rst | 8 ++++++++ plugins/lua/sort/creatures.lua | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 31ab2d3fc..8ea9e114c 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -91,3 +91,11 @@ 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/ + +Creatures overlay +----------------- + +The search widget that appears on the "Creatures" info panel sub-tabs (e.g. +"Citizens", "Pets", etc.) can search the lists by name and other shown +attibutes. For example, searching for ``caged`` will show all caged prisoners +on the "Other" tab. diff --git a/plugins/lua/sort/creatures.lua b/plugins/lua/sort/creatures.lua index eeab12087..692855679 100644 --- a/plugins/lua/sort/creatures.lua +++ b/plugins/lua/sort/creatures.lua @@ -145,14 +145,14 @@ end local function overall_training_search(data, filter, incremental) general_search(creatures.atk_index, function(elem) local raw = df.creature_raw.find(elem) - if not raw then return '' end + if not raw then return end return raw.name[1] end, nil, data, filter, incremental) end local function assign_trainer_search(data, filter, incremental) general_search(creatures.trainer, function(elem) - if not elem then return nil end + if not elem then return end return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.units.getProfessionName(elem)) end, nil, data, filter, incremental) end From a063c0cf4193e4ce604770f6b42e36e6f9f03627 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Oct 2023 09:47:40 -0700 Subject: [PATCH 40/76] only recheck orders that have conditions --- docs/changelog.txt | 1 + docs/plugins/orders.rst | 13 +++++++------ plugins/lua/orders.lua | 16 ++++++++-------- plugins/orders.cpp | 12 ++++++++---- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index feda83f2c..3dcf287cb 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## Misc Improvements - `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates +- `orders`: ``recheck`` command now only resets orders that have conditions that can be rechecked ## Documentation diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index acb25fe99..32708b4c8 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -18,12 +18,13 @@ Usage ``orders clear`` Deletes all manager orders in the current embark. ``orders recheck [this]`` - Sets the status to ``Checking`` (from ``Active``) for all work orders. if the - "this" option is passed, only sets the status for the workorder whose condition - details page is open. This makes the manager reevaluate its conditions. - This is especially useful for an order that had its conditions met when it - was started, but the requisite items have since disappeared and the workorder - is now generating job cancellation spam. + Sets the status to ``Checking`` (from ``Active``) for all work orders that + have conditions that can be re-checked. If the "this" option is passed, + only sets the status for the workorder whose condition details page is + open. This makes the manager reevaluate its conditions. This is especially + useful for an order that had its conditions met when it was started, but + the requisite items have since disappeared and the workorder is now + generating job cancellation spam. ``orders sort`` Sorts current manager orders by repeat frequency so repeating orders don't prevent one-time orders from ever being completed. The sorting order is: diff --git a/plugins/lua/orders.lua b/plugins/lua/orders.lua index 102580fab..0eeb327fc 100644 --- a/plugins/lua/orders.lua +++ b/plugins/lua/orders.lua @@ -71,7 +71,7 @@ OrdersOverlay.ATTRS{ default_pos={x=53,y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/WORK_ORDERS/Default', - frame={w=46, h=4}, + frame={w=43, h=4}, } function OrdersOverlay:init() @@ -99,7 +99,7 @@ function OrdersOverlay:init() }, widgets.HotkeyLabel{ frame={t=0, l=15}, - label='recheck', + label='recheck conditions', key='CUSTOM_CTRL_K', auto_width=true, on_activate=do_recheck, @@ -112,7 +112,7 @@ function OrdersOverlay:init() on_activate=do_sort, }, widgets.HotkeyLabel{ - frame={t=0, l=31}, + frame={t=1, l=28}, label='clear', key='CUSTOM_CTRL_C', auto_width=true, @@ -179,10 +179,10 @@ local function set_current_inactive() end end -local function is_current_active() +local function can_recheck() local scrConditions = df.global.game.main_interface.info.work_orders.conditions local order = scrConditions.wq - return order.status.active + return order.status.active and #order.item_conditions > 0 end -- ------------------- @@ -197,7 +197,7 @@ RecheckOverlay.ATTRS{ default_enabled=true, viewscreens=focusString, -- width is the sum of lengths of `[` + `Ctrl+A` + `: ` + button.label + `]` - frame={w=1 + 6 + 2 + 16 + 1, h=3}, + frame={w=1 + 6 + 2 + 19 + 1, h=3}, } local function areTabsInTwoRows() @@ -226,10 +226,10 @@ function RecheckOverlay:init() widgets.TextButton{ view_id = 'button', -- frame={t=0, l=0, r=0, h=1}, -- is set in `updateTextButtonFrame()` - label='request re-check', + label='re-check conditions', key='CUSTOM_CTRL_A', on_activate=set_current_inactive, - enabled=is_current_active, + enabled=can_recheck, }, } diff --git a/plugins/orders.cpp b/plugins/orders.cpp index a84f14172..4b4c0ed4f 100644 --- a/plugins/orders.cpp +++ b/plugins/orders.cpp @@ -1036,11 +1036,15 @@ static command_result orders_sort_command(color_ostream & out) static command_result orders_recheck_command(color_ostream & out) { - for (auto it : world->manager_orders) - { - it->status.bits.active = false; - it->status.bits.validated = false; + size_t count = 0; + for (auto it : world->manager_orders) { + if (it->item_conditions.size() && it->status.bits.active) { + ++count; + it->status.bits.active = false; + it->status.bits.validated = false; + } } + out << "Re-checking conditions for " << count << " manager orders." << std::endl; return CR_OK; } From 420e0d0952af6d46814d21e4eec186e5d17f5d50 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Oct 2023 11:30:57 -0700 Subject: [PATCH 41/76] add search support for info objects tabs --- docs/changelog.txt | 2 +- plugins/lua/sort/creatures.lua | 66 +++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3dcf287cb..fa6db90ad 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: ## New Features - `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay -- `sort`: new search widgets for all the "Creatures" tabs on the info panel, e.g. "Citizens", "Pets", etc. This includes the assign trainers and view species training knowledge screens as well +- `sort`: new search widgets for all the "Creatures" and "Objects" tabs on the info panel ## Fixes diff --git a/plugins/lua/sort/creatures.lua b/plugins/lua/sort/creatures.lua index 692855679..7fc0ca766 100644 --- a/plugins/lua/sort/creatures.lua +++ b/plugins/lua/sort/creatures.lua @@ -4,10 +4,12 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local utils = require('utils') -local creatures = df.global.game.main_interface.info.creatures +local info = df.global.game.main_interface.info +local creatures = info.creatures +local objects = info.artifacts --- these sort functions attempt to match the vanilla sort behavior, which is not --- quite the same as the rest of DFHack. For example, in other DFHack sorts, +-- these sort functions attempt to match the vanilla info panelsort behavior, which +-- is not quite the same as the rest of DFHack. For example, in other DFHack sorts, -- we'd always sort by name descending as a secondary sort. To match vanilla sorting, -- if the primary sort is ascending, the secondary name sort will also be ascending. -- @@ -165,6 +167,16 @@ local HANDLERS = { PET_OT={search_fn=overall_training_search}, PET_AT={search_fn=assign_trainer_search}, } +for idx,name in ipairs(df.artifacts_mode_type) do + if idx < 0 then goto continue end + HANDLERS[name] = { + search_fn=curry(general_search, objects.list[idx], + function(elem) + return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.TranslateName(elem.name, true)) + end, nil) + } + ::continue:: +end -- ---------------------- -- InfoOverlay @@ -172,7 +184,7 @@ local HANDLERS = { InfoOverlay = defclass(InfoOverlay, overlay.OverlayWidget) InfoOverlay.ATTRS{ - default_pos={x=64, y=9}, + default_pos={x=64, y=8}, default_enabled=true, viewscreens={ 'dwarfmode/Info/CREATURES/CITIZEN', @@ -181,10 +193,14 @@ InfoOverlay.ATTRS{ 'dwarfmode/Info/CREATURES/AddingTrainer', 'dwarfmode/Info/CREATURES/OTHER', 'dwarfmode/Info/CREATURES/DECEASED', + 'dwarfmode/Info/ARTIFACTS/ARTIFACTS', + 'dwarfmode/Info/ARTIFACTS/SYMBOLS', + 'dwarfmode/Info/ARTIFACTS/NAMED_OBJECTS', + 'dwarfmode/Info/ARTIFACTS/WRITTEN_CONTENT', }, hotspot=true, overlay_onupdate_max_freq_seconds=0, - frame={w=40, h=3}, + frame={w=40, h=4}, } function InfoOverlay:init() @@ -226,11 +242,6 @@ function InfoOverlay:overlay_onupdate() end end -local function are_tabs_in_two_rows() - local pen = dfhack.screen.readTile(64, 6, false) -- tile is occupied iff tabs are in one row - return pen.ch == 0 -end - local function resize_overlay(self) local sw = dfhack.screen.getWindowSize() local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) @@ -240,24 +251,39 @@ local function resize_overlay(self) end end +local function get_panel_offsets() + local tabs_in_two_rows = dfhack.screen.readTile(64, 6, false).ch == 0 + local is_objects = info.current_mode == df.info_interface_mode_type.ARTIFACTS + local l_offset = (not tabs_in_two_rows and is_objects) and 4 or 0 + local t_offset = 1 + if tabs_in_two_rows then + t_offset = is_objects and 0 or 3 + end + return l_offset, t_offset +end + function InfoOverlay:updateFrames() local ret = resize_overlay(self) - local two_rows = are_tabs_in_two_rows() - if (self.two_rows == two_rows) then return ret end - self.two_rows = two_rows - self.subviews.panel.frame.t = two_rows and 2 or 0 + local l, t = get_panel_offsets() + local frame = self.subviews.panel.frame + if (frame.l == l and frame.t == t) then return ret end + frame.l, frame.t = l, t return true end local function get_key() - if creatures.current_mode == df.unit_list_mode_type.PET then - if creatures.showing_overall_training then - return 'PET_OT' - elseif creatures.adding_trainer then - return 'PET_AT' + if info.current_mode == df.info_interface_mode_type.CREATURES then + if creatures.current_mode == df.unit_list_mode_type.PET then + if creatures.showing_overall_training then + return 'PET_OT' + elseif creatures.adding_trainer then + return 'PET_AT' + end end + return df.unit_list_mode_type[creatures.current_mode] + elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then + return df.artifacts_mode_type[objects.mode] end - return df.unit_list_mode_type[creatures.current_mode] end local function check_context(self) From 0ad61ccf26718caef87b33140159a3d41794a4a2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Oct 2023 13:44:09 -0700 Subject: [PATCH 42/76] rename file to reflect the more general usage --- plugins/lua/sort.lua | 4 ++-- plugins/lua/sort/{creatures.lua => info.lua} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename plugins/lua/sort/{creatures.lua => info.lua} (99%) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 1159ce56e..23ba83feb 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1,6 +1,6 @@ local _ENV = mkmodule('plugins.sort') -local creatures = require('plugins.sort.creatures') +local info = require('plugins.sort.info') local gui = require('gui') local overlay = require('plugins.overlay') local setbelief = reqscript('modtools/set-belief') @@ -1262,7 +1262,7 @@ end OVERLAY_WIDGETS = { squad_assignment=SquadAssignmentOverlay, squad_annotation=SquadAnnotationOverlay, - creatures=creatures.InfoOverlay, + info=info.InfoOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/creatures.lua b/plugins/lua/sort/info.lua similarity index 99% rename from plugins/lua/sort/creatures.lua rename to plugins/lua/sort/info.lua index 7fc0ca766..de6017f4e 100644 --- a/plugins/lua/sort/creatures.lua +++ b/plugins/lua/sort/info.lua @@ -1,4 +1,4 @@ -local _ENV = mkmodule('plugins.sort.creatures') +local _ENV = mkmodule('plugins.sort.info') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') From 34bbf4cce9b7e247cc2001ca8b69874959697c91 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Oct 2023 13:44:45 -0700 Subject: [PATCH 43/76] add more focus strings for justice screens --- library/modules/Gui.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index f315573c3..3e4281c29 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -259,7 +259,12 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) newFocusString += '/' + enum_item_key(game->main_interface.info.artifacts.mode); break; case df::enums::info_interface_mode_type::JUSTICE: - newFocusString += '/' + enum_item_key(game->main_interface.info.justice.current_mode); + if (game->main_interface.info.justice.interrogating) + newFocusString += "/Interrogating"; + else if (game->main_interface.info.justice.convicting) + newFocusString += "/Convicting"; + else + newFocusString += '/' + enum_item_key(game->main_interface.info.justice.current_mode); break; case df::enums::info_interface_mode_type::WORK_ORDERS: if (game->main_interface.info.work_orders.conditions.open) From c1531ae64635e63bd880cb308f1142dcb1aa16cf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Oct 2023 15:49:11 -0700 Subject: [PATCH 44/76] prototype justice overlay --- plugins/lua/sort.lua | 1 + plugins/lua/sort/info.lua | 103 +++++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 23ba83feb..174c7005e 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1263,6 +1263,7 @@ OVERLAY_WIDGETS = { squad_assignment=SquadAssignmentOverlay, squad_annotation=SquadAnnotationOverlay, info=info.InfoOverlay, + interrogation=info.InterrogationOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index de6017f4e..a82b5b842 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -1,5 +1,6 @@ local _ENV = mkmodule('plugins.sort.info') +local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local utils = require('utils') @@ -7,6 +8,7 @@ local utils = require('utils') local info = df.global.game.main_interface.info local creatures = info.creatures local objects = info.artifacts +local justice = info.justice -- these sort functions attempt to match the vanilla info panelsort behavior, which -- is not quite the same as the rest of DFHack. For example, in other DFHack sorts, @@ -159,6 +161,9 @@ local function assign_trainer_search(data, filter, incremental) end, nil, data, filter, incremental) end +local function interrogation_search(data, filter, incremental) +end + local HANDLERS = { CITIZEN=make_cri_unitst_handlers(creatures.cri_unit.CITIZEN), PET=make_cri_unitst_handlers(creatures.cri_unit.PET), @@ -166,6 +171,7 @@ local HANDLERS = { DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED), PET_OT={search_fn=overall_training_search}, PET_AT={search_fn=assign_trainer_search}, + INTERROGATING={search_fn=interrogation_search}, } for idx,name in ipairs(df.artifacts_mode_type) do if idx < 0 then goto continue end @@ -251,8 +257,12 @@ local function resize_overlay(self) end end +local function is_tabs_in_two_rows() + return dfhack.screen.readTile(64, 6, false).ch == 0 +end + local function get_panel_offsets() - local tabs_in_two_rows = dfhack.screen.readTile(64, 6, false).ch == 0 + local tabs_in_two_rows = is_tabs_in_two_rows() local is_objects = info.current_mode == df.info_interface_mode_type.ARTIFACTS local l_offset = (not tabs_in_two_rows and is_objects) and 4 or 0 local t_offset = 1 @@ -331,4 +341,95 @@ function InfoOverlay:onInput(keys) return InfoOverlay.super.onInput(self, keys) end +-- ---------------------- +-- InterrogationOverlay +-- + +InterrogationOverlay = defclass(InterrogationOverlay, overlay.OverlayWidget) +InterrogationOverlay.ATTRS{ + default_pos={x=47, y=10}, + default_enabled=true, + viewscreens={ + 'dwarfmode/Info/JUSTICE/Interrogating', + 'dwarfmode/Info/JUSTICE/Convicting', + }, + frame={w=27, h=9}, +} + +function InterrogationOverlay:init() + self:addviews{ + widgets.Panel{ + view_id='panel', + frame={l=0, t=4, h=5, r=0}, + frame_background=gui.CLEAR_PEN, + frame_style=gui.FRAME_MEDIUM, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=0, t=0, r=0}, + label_text="Search: ", + key='CUSTOM_ALT_S', + }, + widgets.CycleHotkeyLabel{ + view_id='subset', + frame={l=0, t=1, w=24}, + key='CUSTOM_SHIFT_F', + label='Show:', + options={ + {label='All', value='all', pen=COLOR_GREEN}, + {label='Undead visitors', value='undead', pen=COLOR_RED}, + {label='Other visitors', value='visitors', pen=COLOR_LIGHTRED}, + {label='Residents', value='residents', pen=COLOR_YELLOW}, + {label='Citizens', value='citizens', pen=COLOR_CYAN}, + {label='Animals', value='animals', pen=COLOR_MAGENTA}, + {label='Deceased', value='deceased', pen=COLOR_BLUE}, + }, + }, + widgets.ToggleHotkeyLabel{ + view_id='include_interviewed', + frame={l=0, t=2, w=23}, + key='CUSTOM_SHIFT_I', + label='Interviewed:', + options={ + {label='Include', value=true, pen=COLOR_GREEN}, + {label='Exclude', value=false, pen=COLOR_RED}, + }, + initial_option=true, + }, + }, + }, + } +end + +function InterrogationOverlay:render(dc) + local sw = dfhack.screen.getWindowSize() + local info_panel_border = 31 -- from edges of panel to screen edges + local info_panel_width = sw - info_panel_border + local info_panel_center = info_panel_width // 2 + local panel_x_offset = (info_panel_center + 5) - self.frame_rect.x1 + local frame_w = math.min(panel_x_offset + 37, info_panel_width - 56) + local panel_l = panel_x_offset + local panel_t = is_tabs_in_two_rows() and 4 or 0 + + if self.frame.w ~= frame_w or + self.subviews.panel.frame.l ~= panel_l or + self.subviews.panel.frame.t ~= panel_t + then + self.frame.w = frame_w + self.subviews.panel.frame.l = panel_l + self.subviews.panel.frame.t = panel_t + self:updateLayout() + end + + InterrogationOverlay.super.render(self, dc) +end + +function InterrogationOverlay:onInput(keys) + if keys._MOUSE_R and self.subviews.search.focus then + self.subviews.search:setFocus(false) + return true + end + return InfoOverlay.super.onInput(self, keys) +end + return _ENV From 060becec7ccd347ff22f454e403755e9a3019c31 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Oct 2023 16:28:02 -0700 Subject: [PATCH 45/76] implement search for conviction screen --- plugins/lua/sort/info.lua | 139 +++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 56 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index a82b5b842..633006d9a 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -10,6 +10,8 @@ local creatures = info.creatures local objects = info.artifacts local justice = info.justice +local state = {} + -- these sort functions attempt to match the vanilla info panelsort behavior, which -- is not quite the same as the rest of DFHack. For example, in other DFHack sorts, -- we'd always sort by name descending as a secondary sort. To match vanilla sorting, @@ -161,7 +163,15 @@ local function assign_trainer_search(data, filter, incremental) end, nil, data, filter, incremental) end -local function interrogation_search(data, filter, incremental) +local function interrogating_search(data, filter, incremental) +end + +local function convicting_search(data, filter, incremental) + general_search(justice.conviction_list, function(elem) + return ('%s %s'):format( + dfhack.units.getReadableName(elem), + dfhack.units.getProfessionName(elem)) + end, nil, data, filter, incremental) end local HANDLERS = { @@ -171,7 +181,8 @@ local HANDLERS = { DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED), PET_OT={search_fn=overall_training_search}, PET_AT={search_fn=assign_trainer_search}, - INTERROGATING={search_fn=interrogation_search}, + INTERROGATING={search_fn=interrogating_search}, + CONVICTING={search_fn=convicting_search}, } for idx,name in ipairs(df.artifacts_mode_type) do if idx < 0 then goto continue end @@ -184,6 +195,54 @@ for idx,name in ipairs(df.artifacts_mode_type) do ::continue:: end +local function get_key() + if info.current_mode == df.info_interface_mode_type.JUSTICE then + if justice.interrogating then + return 'INTERROGATING' + elseif justice.convicting then + return 'CONVICTING' + end + elseif info.current_mode == df.info_interface_mode_type.CREATURES then + if creatures.current_mode == df.unit_list_mode_type.PET then + if creatures.showing_overall_training then + return 'PET_OT' + elseif creatures.adding_trainer then + return 'PET_AT' + end + end + return df.unit_list_mode_type[creatures.current_mode] + elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then + return df.artifacts_mode_type[objects.mode] + end +end + +local function check_context(self) + local key = get_key() + if state.prev_key ~= key then + state.prev_key = key + local prev_text = key and ensure_key(state, key).prev_text or '' + self.subviews.search:setText(prev_text) + end +end + +local function do_search(self, text) + if not next(state) and text == '' then return end + -- the EditField state is guaranteed to be consistent with the current + -- context since when clicking to switch tabs, onRenderBody is always called + -- before this text_input callback, even if a key is pressed before the next + -- graphical frame would otherwise be printed. if this ever becomes untrue, + -- then we can add an on_char handler to the EditField that also calls + -- check_context. + local key = get_key() + if not key then return end + local prev_text = ensure_key(state, key).prev_text + -- some screens reset their contents between context switches; regardless + -- a switch back to the context should results in an incremental search + local incremental = prev_text and text:startswith(prev_text) + HANDLERS[key].search_fn(state[key], text, incremental) + state[key].prev_text = text +end + -- ---------------------- -- InfoOverlay -- @@ -210,8 +269,6 @@ InfoOverlay.ATTRS{ } function InfoOverlay:init() - self.state = {} - self:addviews{ widgets.BannerPanel{ view_id='panel', @@ -222,14 +279,14 @@ function InfoOverlay:init() frame={l=1, t=0, r=1}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=self:callback('text_input'), + on_change=curry(do_search, self), }, }, }, } end -local function cleanup(state) +local function cleanup() for k,v in pairs(state) do local cleanup_fn = safe_index(HANDLERS, k, 'cleanup_fn') if cleanup_fn then cleanup_fn(v) end @@ -237,11 +294,11 @@ local function cleanup(state) end function InfoOverlay:overlay_onupdate() - if next(self.state) and + if next(state) and not dfhack.gui.matchFocusString('dwarfmode/Info', dfhack.gui.getDFViewscreen(true)) then - cleanup(self.state) - self.state = {} + cleanup() + state = {} self.subviews.search:setText('') self.subviews.search:setFocus(false) self.overlay_onupdate_max_freq_seconds = 60 @@ -281,32 +338,8 @@ function InfoOverlay:updateFrames() return true end -local function get_key() - if info.current_mode == df.info_interface_mode_type.CREATURES then - if creatures.current_mode == df.unit_list_mode_type.PET then - if creatures.showing_overall_training then - return 'PET_OT' - elseif creatures.adding_trainer then - return 'PET_AT' - end - end - return df.unit_list_mode_type[creatures.current_mode] - elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then - return df.artifacts_mode_type[objects.mode] - end -end - -local function check_context(self) - local key = get_key() - if self.state.prev_key ~= key then - self.state.prev_key = key - local prev_text = ensure_key(self.state, key).prev_text - self.subviews.search:setText(prev_text or '') - end -end - function InfoOverlay:onRenderBody(dc) - if next(self.state) then + if next(state) then check_context(self) end if self:updateFrames() then @@ -316,23 +349,6 @@ function InfoOverlay:onRenderBody(dc) InfoOverlay.super.onRenderBody(self, dc) end -function InfoOverlay:text_input(text) - if not next(self.state) and text == '' then return end - -- the EditField state is guaranteed to be consistent with the current - -- context since when clicking to switch tabs, onRenderBody is always called - -- before this text_input callback, even if a key is pressed before the next - -- graphical frame would otherwise be printed. if this ever becomes untrue, - -- then we can add an on_char handler to the EditField that also calls - -- check_context. - local key = get_key() - local prev_text = ensure_key(self.state, key).prev_text - -- some screens reset their contents between context switches; regardless - -- a switch back to the context should results in an incremental search - local incremental = prev_text and text:startswith(prev_text) - HANDLERS[key].search_fn(self.state[key], text, incremental) - self.state[key].prev_text = text -end - function InfoOverlay:onInput(keys) if keys._MOUSE_R and self.subviews.search.focus then self.subviews.search:setFocus(false) @@ -349,13 +365,15 @@ InterrogationOverlay = defclass(InterrogationOverlay, overlay.OverlayWidget) InterrogationOverlay.ATTRS{ default_pos={x=47, y=10}, default_enabled=true, - viewscreens={ - 'dwarfmode/Info/JUSTICE/Interrogating', - 'dwarfmode/Info/JUSTICE/Convicting', - }, + viewscreens='dwarfmode/Info/JUSTICE', frame={w=27, h=9}, } +local function is_interrogate_or_convict() + local key = get_key() + return key == 'INTERROGATING' or key == 'CONVICTING' +end + function InterrogationOverlay:init() self:addviews{ widgets.Panel{ @@ -363,12 +381,14 @@ function InterrogationOverlay:init() frame={l=0, t=4, h=5, r=0}, frame_background=gui.CLEAR_PEN, frame_style=gui.FRAME_MEDIUM, + visible=is_interrogate_or_convict, subviews={ widgets.EditField{ view_id='search', frame={l=0, t=0, r=0}, label_text="Search: ", key='CUSTOM_ALT_S', + on_change=curry(do_search, self), }, widgets.CycleHotkeyLabel{ view_id='subset', @@ -424,12 +444,19 @@ function InterrogationOverlay:render(dc) InterrogationOverlay.super.render(self, dc) end +function InterrogationOverlay:onRenderBody(dc) + if next(state) then + check_context(self) + end + InterrogationOverlay.super.onRenderBody(self, dc) +end + function InterrogationOverlay:onInput(keys) if keys._MOUSE_R and self.subviews.search.focus then self.subviews.search:setFocus(false) return true end - return InfoOverlay.super.onInput(self, keys) + return InterrogationOverlay.super.onInput(self, keys) end return _ENV From 06faeb669b0183b09efa674804726db51fd623a2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 01:10:32 -0700 Subject: [PATCH 46/76] add support for work details and interrogation --- plugins/lua/sort/info.lua | 237 ++++++++++++++++++++++++++++---------- 1 file changed, 173 insertions(+), 64 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 633006d9a..f8833a642 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -7,8 +7,9 @@ local utils = require('utils') local info = df.global.game.main_interface.info local creatures = info.creatures -local objects = info.artifacts local justice = info.justice +local objects = info.artifacts +local work_details = info.labor.work_details local state = {} @@ -104,17 +105,19 @@ local function copy_to_lua_table(vec) return tab end -local function general_search(vec, get_search_key_fn, get_sort_fn, data, filter, incremental) +local function general_search(vec, get_search_key_fn, get_sort_fn, matches_filters_fn, data, filter, incremental) if not data.saved_original then data.saved_original = copy_to_lua_table(vec) elseif not incremental then vec:assign(data.saved_original) end - if filter ~= '' then + if matches_filters_fn ~= DEFAULT_NIL or filter ~= '' then local search_tokens = filter:split() for idx = #vec-1,0,-1 do local search_key = get_search_key_fn(vec[idx]) - if search_key and not utils.search_text(search_key, search_tokens) then + if (search_key and not utils.search_text(search_key, search_tokens)) or + (matches_filters_fn ~= DEFAULT_NIL and not matches_filters_fn(vec[idx])) + then vec:erase(idx) end end @@ -148,30 +151,76 @@ local function make_cri_unitst_handlers(vec) } end -local function overall_training_search(data, filter, incremental) +local function overall_training_search(matches_filters_fn, data, filter, incremental) general_search(creatures.atk_index, function(elem) local raw = df.creature_raw.find(elem) if not raw then return end return raw.name[1] - end, nil, data, filter, incremental) + end, nil, matches_filters_fn, data, filter, incremental) end -local function assign_trainer_search(data, filter, incremental) +local function assign_trainer_search(matches_filters_fn, data, filter, incremental) general_search(creatures.trainer, function(elem) if not elem then return end return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.units.getProfessionName(elem)) - end, nil, data, filter, incremental) + end, nil, matches_filters_fn, data, filter, incremental) end -local function interrogating_search(data, filter, incremental) +local function get_unit_search_key(unit) + return ('%s %s %s'):format( + dfhack.units.getReadableName(unit), -- last name is in english + dfhack.units.getProfessionName(unit), + dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name end -local function convicting_search(data, filter, incremental) - general_search(justice.conviction_list, function(elem) - return ('%s %s'):format( - dfhack.units.getReadableName(elem), - dfhack.units.getProfessionName(elem)) - end, nil, data, filter, incremental) +local function work_details_search(matches_filters_fn, data, filter, incremental) + if work_details.selected_work_detail_index ~= data.selected then + data.saved_original = nil + data.selected = work_details.selected_work_detail_index + end + general_search(work_details.assignable_unit, get_unit_search_key, + nil, matches_filters_fn, data, filter, incremental) +end + +-- independent implementation of search algorithm since we need to +-- keep two vectors in sync +local function interrogating_search(matches_filters_fn, data, filter, incremental) + local vec, flags_vec = justice.interrogation_list, justice.interrogation_list_flag + if not data.saved_original then + data.saved_original = copy_to_lua_table(vec) + data.saved_flags = copy_to_lua_table(flags_vec) + data.saved_idx_map = {} + for idx, unit in ipairs(data.saved_original) do + data.saved_idx_map[unit.id] = idx -- 1-based idx + end + else -- sync flag changes to saved vector + for idx, unit in ipairs(vec) do -- 0-based idx + data.saved_flags[data.saved_idx_map[unit.id]] = flags_vec[idx] + end + end + + if not incremental then + vec:assign(data.saved_original) + flags_vec:assign(data.saved_flags) + end + + if matches_filters_fn or filter ~= '' then + local search_tokens = filter:split() + for idx = #vec-1,0,-1 do + local search_key = get_unit_search_key(vec[idx]) + if (search_key and not utils.search_text(search_key, search_tokens)) or + (matches_filters_fn and not matches_filters_fn(vec[idx], idx)) + then + vec:erase(idx) + flags_vec:erase(idx) + end + end + end +end + +local function convicting_search(matches_filters_fn, data, filter, incremental) + general_search(justice.conviction_list, get_unit_search_key, + nil, matches_filters_fn, data, filter, incremental) end local HANDLERS = { @@ -181,6 +230,7 @@ local HANDLERS = { DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED), PET_OT={search_fn=overall_training_search}, PET_AT={search_fn=assign_trainer_search}, + WORK_DETAILS={search_fn=work_details_search}, INTERROGATING={search_fn=interrogating_search}, CONVICTING={search_fn=convicting_search}, } @@ -213,6 +263,10 @@ local function get_key() return df.unit_list_mode_type[creatures.current_mode] elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then return df.artifacts_mode_type[objects.mode] + elseif info.current_mode == df.info_interface_mode_type.LABOR then + if info.labor.mode == df.labor_mode_type.WORK_DETAILS then + return 'WORK_DETAILS' + end end end @@ -225,8 +279,8 @@ local function check_context(self) end end -local function do_search(self, text) - if not next(state) and text == '' then return end +local function do_search(matches_filters_fn, text, force_full_search) + if not force_full_search and not next(state) and text == '' then return end -- the EditField state is guaranteed to be consistent with the current -- context since when clicking to switch tabs, onRenderBody is always called -- before this text_input callback, even if a key is pressed before the next @@ -238,11 +292,34 @@ local function do_search(self, text) local prev_text = ensure_key(state, key).prev_text -- some screens reset their contents between context switches; regardless -- a switch back to the context should results in an incremental search - local incremental = prev_text and text:startswith(prev_text) - HANDLERS[key].search_fn(state[key], text, incremental) + local incremental = not force_full_search and prev_text and text:startswith(prev_text) + HANDLERS[key].search_fn(matches_filters_fn, state[key], text, incremental) state[key].prev_text = text end +local function on_update(self) + if self.overlay_onupdate_max_freq_seconds == 0 and + not dfhack.gui.matchFocusString('dwarfmode/Info', dfhack.gui.getDFViewscreen(true)) + then + for k,v in pairs(state) do + local cleanup_fn = safe_index(HANDLERS, k, 'cleanup_fn') + if cleanup_fn then cleanup_fn(v) end + end + state = {} + self.subviews.search:setText('') + self.subviews.search:setFocus(false) + self.overlay_onupdate_max_freq_seconds = 60 + end +end + +local function on_input(self, clazz, keys) + if keys._MOUSE_R and self.subviews.search.focus then + self.subviews.search:setFocus(false) + return true + end + return clazz.super.onInput(self, keys) +end + -- ---------------------- -- InfoOverlay -- @@ -262,6 +339,7 @@ InfoOverlay.ATTRS{ 'dwarfmode/Info/ARTIFACTS/SYMBOLS', 'dwarfmode/Info/ARTIFACTS/NAMED_OBJECTS', 'dwarfmode/Info/ARTIFACTS/WRITTEN_CONTENT', + 'dwarfmode/Info/LABOR/WORK_DETAILS', }, hotspot=true, overlay_onupdate_max_freq_seconds=0, @@ -279,30 +357,15 @@ function InfoOverlay:init() frame={l=1, t=0, r=1}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=curry(do_search, self), + on_change=curry(do_search, DEFAULT_NIL), }, }, }, } end -local function cleanup() - for k,v in pairs(state) do - local cleanup_fn = safe_index(HANDLERS, k, 'cleanup_fn') - if cleanup_fn then cleanup_fn(v) end - end -end - function InfoOverlay:overlay_onupdate() - if next(state) and - not dfhack.gui.matchFocusString('dwarfmode/Info', dfhack.gui.getDFViewscreen(true)) - then - cleanup() - state = {} - self.subviews.search:setText('') - self.subviews.search:setFocus(false) - self.overlay_onupdate_max_freq_seconds = 60 - end + on_update(self) end local function resize_overlay(self) @@ -320,11 +383,12 @@ end local function get_panel_offsets() local tabs_in_two_rows = is_tabs_in_two_rows() - local is_objects = info.current_mode == df.info_interface_mode_type.ARTIFACTS - local l_offset = (not tabs_in_two_rows and is_objects) and 4 or 0 + local shift_right = info.current_mode == df.info_interface_mode_type.ARTIFACTS or + info.current_mode == df.info_interface_mode_type.LABOR + local l_offset = (not tabs_in_two_rows and shift_right) and 4 or 0 local t_offset = 1 if tabs_in_two_rows then - t_offset = is_objects and 0 or 3 + t_offset = shift_right and 0 or 3 end return l_offset, t_offset end @@ -345,16 +409,19 @@ function InfoOverlay:onRenderBody(dc) if self:updateFrames() then self:updateLayout() end + if self.refresh_search then + self.refresh_search = nil + do_search(DEFAULT_NIL, self.subviews.search.text) + end self.overlay_onupdate_max_freq_seconds = 0 InfoOverlay.super.onRenderBody(self, dc) end function InfoOverlay:onInput(keys) - if keys._MOUSE_R and self.subviews.search.focus then - self.subviews.search:setFocus(false) - return true + if keys._MOUSE_L and get_key() == 'WORK_DETAILS' then + self.refresh_search = true end - return InfoOverlay.super.onInput(self, keys) + return on_input(self, InfoOverlay, keys) end -- ---------------------- @@ -367,8 +434,14 @@ InterrogationOverlay.ATTRS{ default_enabled=true, viewscreens='dwarfmode/Info/JUSTICE', frame={w=27, h=9}, + hotspot=true, + overlay_onupdate_max_freq_seconds=0, } +function InterrogationOverlay:overlay_onupdate() + on_update(self) +end + local function is_interrogate_or_convict() local key = get_key() return key == 'INTERROGATING' or key == 'CONVICTING' @@ -388,39 +461,75 @@ function InterrogationOverlay:init() frame={l=0, t=0, r=0}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=curry(do_search, self), + on_change=curry(do_search, self:callback('matches_filters')), + }, + widgets.ToggleHotkeyLabel{ + view_id='include_interviewed', + frame={l=0, t=1, w=23}, + key='CUSTOM_SHIFT_I', + label='Interviewed:', + options={ + {label='Include', value=true, pen=COLOR_GREEN}, + {label='Exclude', value=false, pen=COLOR_RED}, + }, + visible=function() return justice.interrogating end, + on_change=function() + do_search(self:callback('matches_filters'), self.subviews.search.text, true) + end, }, widgets.CycleHotkeyLabel{ view_id='subset', - frame={l=0, t=1, w=24}, + frame={l=0, t=2, w=28}, key='CUSTOM_SHIFT_F', label='Show:', options={ {label='All', value='all', pen=COLOR_GREEN}, - {label='Undead visitors', value='undead', pen=COLOR_RED}, + {label='Risky visitors', value='risky', pen=COLOR_RED}, {label='Other visitors', value='visitors', pen=COLOR_LIGHTRED}, {label='Residents', value='residents', pen=COLOR_YELLOW}, {label='Citizens', value='citizens', pen=COLOR_CYAN}, - {label='Animals', value='animals', pen=COLOR_MAGENTA}, - {label='Deceased', value='deceased', pen=COLOR_BLUE}, - }, - }, - widgets.ToggleHotkeyLabel{ - view_id='include_interviewed', - frame={l=0, t=2, w=23}, - key='CUSTOM_SHIFT_I', - label='Interviewed:', - options={ - {label='Include', value=true, pen=COLOR_GREEN}, - {label='Exclude', value=false, pen=COLOR_RED}, + {label='Animals', value='animals', pen=COLOR_BLUE}, + {label='Deceased or missing', value='deceased', pen=COLOR_MAGENTA}, + {label='Others', value='others', pen=COLOR_GRAY}, }, - initial_option=true, + on_change=function() + do_search(self:callback('matches_filters'), self.subviews.search.text, true) + end, }, }, }, } end +local function is_risky(unit) + return false +end + +function InterrogationOverlay:matches_filters(unit, idx) + if justice.interrogating then + local include_interviewed = self.subviews.include_interviewed:getOptionValue() + if not include_interviewed and justice.interrogation_list_flag[idx] == 2 then + return false + end + end + local subset = self.subviews.subset:getOptionValue() + if subset == 'all' then + return true + elseif dfhack.units.isDead(unit) or not dfhack.units.isActive(unit) then + return subset == 'deceased' + elseif dfhack.units.isVisiting(unit) then + local risky = is_risky(unit) + return (subset == 'risky' and risky) or (subset == 'visitors' and not risky) + elseif dfhack.units.isAnimal(unit) then + return subset == 'animals' + elseif dfhack.units.isCitizen(unit) then + return subset == 'citizens' + elseif dfhack.units.isOwnGroup(unit) then + return subset == 'residents' + end + return subset == 'others' +end + function InterrogationOverlay:render(dc) local sw = dfhack.screen.getWindowSize() local info_panel_border = 31 -- from edges of panel to screen edges @@ -447,16 +556,16 @@ end function InterrogationOverlay:onRenderBody(dc) if next(state) then check_context(self) + else + self.subviews.include_interviewed:setOption(true, false) + self.subviews.subset:setOption('all') end + self.overlay_onupdate_max_freq_seconds = 0 InterrogationOverlay.super.onRenderBody(self, dc) end function InterrogationOverlay:onInput(keys) - if keys._MOUSE_R and self.subviews.search.focus then - self.subviews.search:setFocus(false) - return true - end - return InterrogationOverlay.super.onInput(self, keys) + return on_input(self, InterrogationOverlay, keys) end return _ENV From a575727c09a32b9eb7399ee121ee6a23604196c4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 01:44:21 -0700 Subject: [PATCH 47/76] add support for searching jobs (tasks) --- plugins/lua/sort/info.lua | 68 +++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index f8833a642..8afe5e02b 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -9,11 +9,12 @@ local info = df.global.game.main_interface.info local creatures = info.creatures local justice = info.justice local objects = info.artifacts +local tasks = info.jobs local work_details = info.labor.work_details local state = {} --- these sort functions attempt to match the vanilla info panelsort behavior, which +-- these sort functions attempt to match the vanilla info panel sort behavior, which -- is not quite the same as the rest of DFHack. For example, in other DFHack sorts, -- we'd always sort by name descending as a secondary sort. To match vanilla sorting, -- if the primary sort is ascending, the secondary name sort will also be ascending. @@ -140,13 +141,22 @@ local function cri_unitst_cleanup(vec, data) end end -local function make_cri_unitst_handlers(vec) +local function get_unit_search_key(unit) + return ('%s %s %s'):format( + dfhack.units.getReadableName(unit), -- last name is in english + dfhack.units.getProfessionName(unit), + dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name +end + +local function make_cri_unitst_handlers(vec, sort_fn) return { search_fn=curry(general_search, vec, function(elem) - return ('%s %s'):format(elem.sort_name, elem.job_sort_name) + return ('%s %s'):format( + elem.un and get_unit_search_key(elem.un) or '', + elem.job_sort_name) end, - get_sort), + sort_fn), cleanup_fn=curry(cri_unitst_cleanup, vec), } end @@ -166,13 +176,6 @@ local function assign_trainer_search(matches_filters_fn, data, filter, increment end, nil, matches_filters_fn, data, filter, incremental) end -local function get_unit_search_key(unit) - return ('%s %s %s'):format( - dfhack.units.getReadableName(unit), -- last name is in english - dfhack.units.getProfessionName(unit), - dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name -end - local function work_details_search(matches_filters_fn, data, filter, incremental) if work_details.selected_work_detail_index ~= data.selected then data.saved_original = nil @@ -224,12 +227,13 @@ local function convicting_search(matches_filters_fn, data, filter, incremental) end local HANDLERS = { - CITIZEN=make_cri_unitst_handlers(creatures.cri_unit.CITIZEN), - PET=make_cri_unitst_handlers(creatures.cri_unit.PET), - OTHER=make_cri_unitst_handlers(creatures.cri_unit.OTHER), - DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED), + CITIZEN=make_cri_unitst_handlers(creatures.cri_unit.CITIZEN, get_sort), + PET=make_cri_unitst_handlers(creatures.cri_unit.PET, get_sort), + OTHER=make_cri_unitst_handlers(creatures.cri_unit.OTHER, get_sort), + DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED, get_sort), PET_OT={search_fn=overall_training_search}, PET_AT={search_fn=assign_trainer_search}, + JOBS=make_cri_unitst_handlers(tasks.cri_job), WORK_DETAILS={search_fn=work_details_search}, INTERROGATING={search_fn=interrogating_search}, CONVICTING={search_fn=convicting_search}, @@ -261,6 +265,8 @@ local function get_key() end end return df.unit_list_mode_type[creatures.current_mode] + elseif info.current_mode == df.info_interface_mode_type.JOBS then + return 'JOBS' elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then return df.artifacts_mode_type[objects.mode] elseif info.current_mode == df.info_interface_mode_type.LABOR then @@ -320,6 +326,11 @@ local function on_input(self, clazz, keys) return clazz.super.onInput(self, keys) end +local function is_interrogate_or_convict() + local key = get_key() + return key == 'INTERROGATING' or key == 'CONVICTING' +end + -- ---------------------- -- InfoOverlay -- @@ -328,19 +339,7 @@ InfoOverlay = defclass(InfoOverlay, overlay.OverlayWidget) InfoOverlay.ATTRS{ default_pos={x=64, y=8}, default_enabled=true, - viewscreens={ - 'dwarfmode/Info/CREATURES/CITIZEN', - 'dwarfmode/Info/CREATURES/PET', - 'dwarfmode/Info/CREATURES/OverallTraining', - 'dwarfmode/Info/CREATURES/AddingTrainer', - 'dwarfmode/Info/CREATURES/OTHER', - 'dwarfmode/Info/CREATURES/DECEASED', - 'dwarfmode/Info/ARTIFACTS/ARTIFACTS', - 'dwarfmode/Info/ARTIFACTS/SYMBOLS', - 'dwarfmode/Info/ARTIFACTS/NAMED_OBJECTS', - 'dwarfmode/Info/ARTIFACTS/WRITTEN_CONTENT', - 'dwarfmode/Info/LABOR/WORK_DETAILS', - }, + viewscreens='dwarfmode/Info', hotspot=true, overlay_onupdate_max_freq_seconds=0, frame={w=40, h=4}, @@ -351,13 +350,14 @@ function InfoOverlay:init() widgets.BannerPanel{ view_id='panel', frame={l=0, t=0, r=0, h=1}, + visible=function() return get_key() and not is_interrogate_or_convict() end, subviews={ widgets.EditField{ view_id='search', frame={l=1, t=0, r=1}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=curry(do_search, DEFAULT_NIL), + on_change=function(text) do_search(DEFAULT_NIL, text) end, }, }, }, @@ -390,6 +390,9 @@ local function get_panel_offsets() if tabs_in_two_rows then t_offset = shift_right and 0 or 3 end + if info.current_mode == df.info_interface_mode_type.JOBS then + t_offset = t_offset - 1 + end return l_offset, t_offset end @@ -442,11 +445,6 @@ function InterrogationOverlay:overlay_onupdate() on_update(self) end -local function is_interrogate_or_convict() - local key = get_key() - return key == 'INTERROGATING' or key == 'CONVICTING' -end - function InterrogationOverlay:init() self:addviews{ widgets.Panel{ @@ -461,7 +459,7 @@ function InterrogationOverlay:init() frame={l=0, t=0, r=0}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=curry(do_search, self:callback('matches_filters')), + on_change=function(text) do_search(self:callback('matches_filters'), text) end, }, widgets.ToggleHotkeyLabel{ view_id='include_interviewed', From cfae6065b88fdbc562b07aefdc0f26f1dadb14e2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 01:46:34 -0700 Subject: [PATCH 48/76] update changelog --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index fa6db90ad..fbe37deff 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: ## New Features - `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay -- `sort`: new search widgets for all the "Creatures" and "Objects" tabs on the info panel +- `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice" ## Fixes From 9f9d8ff74bde790008d22e20d2b7bec957bd2a45 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 02:25:45 -0700 Subject: [PATCH 49/76] implement risky visitor detection, refine algorithm --- plugins/lua/sort/info.lua | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 8afe5e02b..62c24d228 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -207,12 +207,12 @@ local function interrogating_search(matches_filters_fn, data, filter, incrementa flags_vec:assign(data.saved_flags) end - if matches_filters_fn or filter ~= '' then + if matches_filters_fn ~= DEFAULT_NIL or filter ~= '' then local search_tokens = filter:split() for idx = #vec-1,0,-1 do local search_key = get_unit_search_key(vec[idx]) if (search_key and not utils.search_text(search_key, search_tokens)) or - (matches_filters_fn and not matches_filters_fn(vec[idx], idx)) + (matches_filters_fn ~= DEFAULT_NIL and not matches_filters_fn(vec[idx], idx)) then vec:erase(idx) flags_vec:erase(idx) @@ -276,10 +276,10 @@ local function get_key() end end -local function check_context(self) +local function check_context(self, key_ctx) local key = get_key() - if state.prev_key ~= key then - state.prev_key = key + if state[key_ctx] ~= key then + state[key_ctx] = key local prev_text = key and ensure_key(state, key).prev_text or '' self.subviews.search:setText(prev_text) end @@ -407,7 +407,7 @@ end function InfoOverlay:onRenderBody(dc) if next(state) then - check_context(self) + check_context(self, InfoOverlay) end if self:updateFrames() then self:updateLayout() @@ -499,8 +499,18 @@ function InterrogationOverlay:init() } end +local RISKY_PROFESSIONS = utils.invert{ + df.profession.THIEF, + df.profession.MASTER_THIEF, + df.profession.CRIMINAL, +} + local function is_risky(unit) - return false + if RISKY_PROFESSIONS[unit.profession] or RISKY_PROFESSIONS[unit.profession2] then + return true + end + if dfhack.units.getReadableName(unit):endswith('necromancer') then return true end + return not dfhack.units.isAlive(unit) -- detect intelligent undead end function InterrogationOverlay:matches_filters(unit, idx) @@ -522,10 +532,10 @@ function InterrogationOverlay:matches_filters(unit, idx) return subset == 'animals' elseif dfhack.units.isCitizen(unit) then return subset == 'citizens' - elseif dfhack.units.isOwnGroup(unit) then - return subset == 'residents' + elseif unit.flags2.roaming_wilderness_population_source then + return subset == 'others' end - return subset == 'others' + return subset == 'residents' end function InterrogationOverlay:render(dc) @@ -553,7 +563,7 @@ end function InterrogationOverlay:onRenderBody(dc) if next(state) then - check_context(self) + check_context(self, InterrogationOverlay) else self.subviews.include_interviewed:setOption(true, false) self.subviews.subset:setOption('all') From 98b44ea8f0f7cda6f151d4a6312ec049074f9c43 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 02:32:26 -0700 Subject: [PATCH 50/76] allow right click exit when search is focused but screen isn't a search-enabled screen --- plugins/lua/sort/info.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 62c24d228..861b44484 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -319,7 +319,7 @@ local function on_update(self) end local function on_input(self, clazz, keys) - if keys._MOUSE_R and self.subviews.search.focus then + if keys._MOUSE_R and self.subviews.search.focus and self:get_handled_key() then self.subviews.search:setFocus(false) return true end @@ -350,7 +350,7 @@ function InfoOverlay:init() widgets.BannerPanel{ view_id='panel', frame={l=0, t=0, r=0, h=1}, - visible=function() return get_key() and not is_interrogate_or_convict() end, + visible=self:callback('get_handled_key'), subviews={ widgets.EditField{ view_id='search', @@ -368,6 +368,10 @@ function InfoOverlay:overlay_onupdate() on_update(self) end +function InfoOverlay:get_handled_key() + return not is_interrogate_or_convict() and get_key() or nil +end + local function resize_overlay(self) local sw = dfhack.screen.getWindowSize() local overlay_width = math.min(40, sw-(self.frame_rect.x1 + 30)) @@ -445,6 +449,10 @@ function InterrogationOverlay:overlay_onupdate() on_update(self) end +function InterrogationOverlay:get_handled_key() + return is_interrogate_or_convict() and get_key() or nil +end + function InterrogationOverlay:init() self:addviews{ widgets.Panel{ From bd1381bbf3c75d679003dfd1058951411b563ab2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 02:37:32 -0700 Subject: [PATCH 51/76] clean up whitespace --- plugins/lua/sort/info.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 861b44484..b732e504f 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -1,6 +1,6 @@ local _ENV = mkmodule('plugins.sort.info') -local gui = require('gui') +local gui = require('gui') local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local utils = require('utils') From e7c07a2494bb58272c9c1f2b9b28cf4cdfef1ea0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 03:09:30 -0700 Subject: [PATCH 52/76] update docs for info overlay --- docs/plugins/sort.rst | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 8ea9e114c..70e101b3f 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -92,10 +92,23 @@ https://www.reddit.com/r/dwarffortress/comments/163kczo/enhancing_military_candi "Mental stability" is explained here: https://www.reddit.com/r/dwarffortress/comments/1617s11/enhancing_military_candidate_selection_part_2/ -Creatures overlay ------------------ - -The search widget that appears on the "Creatures" info panel sub-tabs (e.g. -"Citizens", "Pets", etc.) can search the lists by name and other shown -attibutes. For example, searching for ``caged`` will show all caged prisoners -on the "Other" tab. +Info overlay +------------ + +The Info overlay adds search support to many of the fort-wide "Info" panels +(e.g. "Creatures", "Tasks", etc.). When searching for units, you can search by +name (with either English or native language last names), profession, or +special status (like "necromancer"). If there is text in the second column, you +can search for that text as well. This is often a job name or a status, like +"caged". + +In the interrogation and conviction screens under the "Justice" tab, you can +also filter by the classification of the unit. The classification groups are +ordered by how likely a member of that group is to be involved in a plot. The +groups are: All, Risky visitors, Other visitors, Residents, Citizens, Animals, +Deceased, and Others. "Risky" visitors are those who are especially likely to +be involved in plots, such as criminals, necromancers, necromancer experiments, +and intelligent undead. + +On the interrogations screen, you can also filter units by whether they have +already been interrogated. From 3ee059317f5cca16eb915c651418fd4bae8e4b58 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 03:45:53 -0700 Subject: [PATCH 53/76] add help button to squad panel --- docs/changelog.txt | 1 + plugins/lua/sort.lua | 34 +++++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index fbe37deff..56a1237c1 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: ## Misc Improvements - `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates - `orders`: ``recheck`` command now only resets orders that have conditions that can be rechecked +- `sort`: added help button for squad assignment search/filter/sort ## Documentation diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 174c7005e..52bfac7aa 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -4,6 +4,7 @@ local info = require('plugins.sort.info') local gui = require('gui') local overlay = require('plugins.overlay') local setbelief = reqscript('modtools/set-belief') +local textures = require('gui.textures') local utils = require('utils') local widgets = require('gui.widgets') @@ -631,10 +632,6 @@ SquadAssignmentOverlay.ATTRS{ viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', version='2', frame={w=38, h=31}, - frame_style=gui.FRAME_PANEL, - frame_background=gui.CLEAR_PEN, - autoarrange_subviews=true, - autoarrange_gap=1, } -- allow initial spacebar or two successive spacebars to fall through and @@ -661,7 +658,14 @@ function SquadAssignmentOverlay:init() }) end - self:addviews{ + local main_panel = widgets.Panel{ + frame={l=0, r=0, t=0, b=0}, + frame_style=gui.FRAME_PANEL, + frame_background=gui.CLEAR_PEN, + autoarrange_subviews=true, + autoarrange_gap=1, + } + main_panel:addviews{ widgets.EditField{ view_id='search', frame={l=0}, @@ -940,6 +944,26 @@ function SquadAssignmentOverlay:init() on_change=function() self:refresh_list() end, }, } + + local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} + local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} + local help_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} + + self:addviews{ + main_panel, + widgets.Label{ + frame={t=0, r=1, w=3}, + text={ + {tile=button_pen_left}, + {tile=help_pen_center}, + {tile=button_pen_right}, + }, + on_click=function() dfhack.run_command('gui/launcher', 'sort ') end, + }, + } end local function normalize_search_key(search_key) From 61e44324245a026b895121e4295545ad49a37c31 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 17:24:13 -0700 Subject: [PATCH 54/76] label war and hunt trained animals in readable names --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 3 ++- library/modules/Units.cpp | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index fbe37deff..8c99c2b57 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: ## Misc Improvements - `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates - `orders`: ``recheck`` command now only resets orders that have conditions that can be rechecked +- `zone`: animals trained for war or hunting are now labeled as such in animal assignment screens ## Documentation diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 1d8c2beb6..fd56a11ed 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1609,7 +1609,8 @@ Units module * ``dfhack.units.getReadableName(unit)`` Returns a string that includes the language name of the unit (if any), the - race of the unit, and any syndrome-given descriptions (such as "necromancer"). + race of the unit, whether it is trained for war or hunting, and any + syndrome-given descriptions (such as "necromancer"). * ``dfhack.units.getStressCategory(unit)`` diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index ebfd13b5e..83554ef78 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1267,6 +1267,10 @@ static string get_caste_name(df::unit* unit) { string Units::getReadableName(df::unit* unit) { string race_name = isChild(unit) ? getRaceChildName(unit) : get_caste_name(unit); + if (isHunter(unit)) + race_name = "hunter " + race_name; + if (isWar(unit)) + race_name = "war " + race_name; string name = Translation::TranslateName(getVisibleName(unit)); if (name.empty()) { name = race_name; From 640c77dc48f8933eeb06c1efbdd15c07e0a55940 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Oct 2023 17:31:50 -0700 Subject: [PATCH 55/76] dungeon cages/retraints aren't assignable --- docs/changelog.txt | 1 + plugins/lua/zone.lua | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index fbe37deff..59868e902 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: - `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice" ## Fixes +- `zone`: don't show animal assignment link for dungeon cages/restraints ## Misc Improvements - `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index 13182cef1..eb984093b 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -961,9 +961,13 @@ CageChainOverlay.ATTRS{ local function is_valid_building() local bld = dfhack.gui.getSelectedBuilding(true) - return bld and bld:getBuildStage() == bld:getMaxBuildStage() and - (bld:getType() == df.building_type.Cage or - bld:getType() == df.building_type.Chain) + if not bld or bld:getBuildStage() ~= bld:getMaxBuildStage() then return false end + local bt = bld:getType() + if bt ~= df.building_type.Cage and bt ~= df.building_type.Chain then return false end + for _,zone in ipairs(bld.relations) do + if zone.type == df.civzone_type.Dungeon then return false end + end + return true end local function is_cage_selected() From a085e5ef2871d7cd7363682dda45ceab48f8d620 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 02:33:47 -0700 Subject: [PATCH 56/76] add support for location selector that is, guildhalls and temples also start refactoring code for better reuse --- plugins/lua/sort.lua | 2 + plugins/lua/sort/locationselector.lua | 67 ++++++++++ plugins/lua/sort/sortoverlay.lua | 169 ++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 plugins/lua/sort/locationselector.lua create mode 100644 plugins/lua/sort/sortoverlay.lua diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 52bfac7aa..ce9eb1f9e 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -2,6 +2,7 @@ local _ENV = mkmodule('plugins.sort') local info = require('plugins.sort.info') local gui = require('gui') +local locationselector = require('plugins.sort.locationselector') local overlay = require('plugins.overlay') local setbelief = reqscript('modtools/set-belief') local textures = require('gui.textures') @@ -1288,6 +1289,7 @@ OVERLAY_WIDGETS = { squad_annotation=SquadAnnotationOverlay, info=info.InfoOverlay, interrogation=info.InterrogationOverlay, + location_selector=locationselector.LocationSelectorOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/locationselector.lua b/plugins/lua/sort/locationselector.lua new file mode 100644 index 000000000..8092db1e2 --- /dev/null +++ b/plugins/lua/sort/locationselector.lua @@ -0,0 +1,67 @@ +local _ENV = mkmodule('plugins.sort.locationselector') + +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') + +local location_selector = df.global.game.main_interface.location_selector + +-- ---------------------- +-- LocationSelectorOverlay +-- + +LocationSelectorOverlay = defclass(LocationSelectorOverlay, sortoverlay.SortOverlay) +LocationSelectorOverlay.ATTRS{ + default_pos={x=48, y=7}, + viewscreens='dwarfmode/LocationSelector', + frame={w=26, h=1}, +} + +local function get_religion_string(religion_id, religion_type) + if religion_id == -1 then return end + local entity + if religion_type == 0 then + entity = df.historical_figure.find(religion_id) + elseif religion_type == 1 then + entity = df.historical_entity.find(religion_id) + end + if not entity then return end + return dfhack.TranslateName(entity.name, true) +end + +local function get_profession_string(profession) + return df.profession[profession]:gsub('_', ' ') +end + +function LocationSelectorOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, r=0, h=1}, + visible=self:callback('get_key'), + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + self:register_handler('TEMPLE', location_selector.valid_religious_practice_id, + curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_religion_string}, + location_selector.valid_religious_practice)) + self:register_handler('GUILDHALL', location_selector.valid_craft_guild_type, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_profession_string})) +end + +function LocationSelectorOverlay:get_key() + if location_selector.choosing_temple_religious_practice then + return 'TEMPLE' + elseif location_selector.choosing_craft_guild then + return 'GUILDHALL' + end +end + +return _ENV diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua new file mode 100644 index 000000000..96c0f8ecc --- /dev/null +++ b/plugins/lua/sort/sortoverlay.lua @@ -0,0 +1,169 @@ +local _ENV = mkmodule('plugins.sort.sortoverlay') + +local overlay = require('plugins.overlay') +local utils = require('utils') + +local function copy_to_lua_table(vec) + local tab = {} + for k,v in ipairs(vec) do + tab[k+1] = v + end + return tab +end + +-- ---------------------- +-- SortOverlay +-- + +SortOverlay = defclass(SortOverlay, overlay.OverlayWidget) +SortOverlay.ATTRS{ + default_enabled=true, + hotspot=true, + overlay_onupdate_max_freq_seconds=0, + -- subclasses expected to provide default_pos, viewscreens (single string), and frame +} + +function SortOverlay:init() + self.state = {} + self.handlers = {} + -- subclasses expected to provide an EditField widget with view_id='search' +end + +function SortOverlay:register_handler(key, vec, search_fn, restore_filtered_on_cleanup) + self.handlers[key] = { + vec=vec, + search_fn=search_fn, + restore_filtered_on_cleanup=restore_filtered_on_cleanup + } +end + +local function restore_filtered(vec, data) + if not data.saved_visible or not data.saved_original then return end + for _,elem in ipairs(data.saved_original) do + if not utils.linear_index(data.saved_visible, elem) then + vec:insert('#', elem) + end + end +end + +-- handles reset and clean up when the player exits the handled scope +function SortOverlay:overlay_onupdate() + if self.overlay_onupdate_max_freq_seconds == 0 and + not dfhack.gui.matchFocusString(self.viewscreens, dfhack.gui.getDFViewscreen(true)) + then + for key,data in pairs(self.state) do + if safe_index(self.handlers, key, 'restore_filtered_on_cleanup') then + restore_filtered(self.handlers[key].vec, data) + end + end + self.state = {} + self.subviews.search:setText('') + self.subviews.search:setFocus(false) + self.overlay_onupdate_max_freq_seconds = 300 + end +end + +-- returns the current context key for dereferencing the handler +-- subclasses must override +function SortOverlay:get_key() + return nil +end + +-- handles saving/restoring search strings when the player moves between different contexts +function SortOverlay:onRenderBody(dc) + if next(self.state) then + local key = self:get_key() + if self.state.cur_key ~= key then + self.state.cur_key = key + local prev_text = key and ensure_key(self.state, key).prev_text or '' + self.subviews.search:setText(prev_text) + end + end + self.overlay_onupdate_max_freq_seconds = 0 + SortOverlay.super.onRenderBody(self, dc) +end + +function SortOverlay:onInput(keys) + if keys._MOUSE_R and self.subviews.search.focus and self:get_key() then + self.subviews.search:setFocus(false) + return true + end + return SortOverlay.super.onInput(self, keys) +end + +function SortOverlay:do_search(text, force_full_search) + if not force_full_search and not next(self.state) and text == '' then return end + -- the EditField state is guaranteed to be consistent with the current + -- context since when clicking to switch tabs, onRenderBody is always called + -- before this text_input callback, even if a key is pressed before the next + -- graphical frame would otherwise be printed. if this ever becomes untrue, + -- then we can add an on_char handler to the EditField that also checks for + -- context transitions. + local key = self:get_key() + if not key then return end + local prev_text = ensure_key(self.state, key).prev_text + -- some screens reset their contents between context switches; regardless, + -- a switch back to the context should results in an incremental search + local incremental = not force_full_search and prev_text and text:startswith(prev_text) + local handler = self.handlers[key] + handler.search_fn(handler.vec, self.state[key], text, incremental) + self.state[key].prev_text = text +end + +local function filter_vec(fns, flags_vec, vec, text, erase_fn) + if fns.matches_filters_fn or text ~= '' then + local search_tokens = text:split() + for idx = #vec-1,0,-1 do + local flag = flags_vec and flags_vec[idx] or nil + local search_key = fns.get_search_key_fn(vec[idx], flag) + if (search_key and not utils.search_text(search_key, search_tokens)) or + (fns.matches_filters_fn and not fns.matches_filters_fn(vec[idx], flag)) + then + erase_fn(idx) + end + end + end +end + +function single_vector_search(fns, vec, data, text, incremental) + if not data.saved_original then + data.saved_original = copy_to_lua_table(vec) + elseif not incremental then + vec:assign(data.saved_original) + end + filter_vec(fns, nil, vec, text, function(idx) vec:erase(idx) end) + data.saved_visible = copy_to_lua_table(vec) + if fns.get_sort_fn then + table.sort(data.saved_visible, fns.get_sort_fn()) + vec:assign(data.saved_visible) + end +end + +-- doesn't support cleanup since nothing that uses this needs it yet +function flags_vector_search(fns, flags_vec, vec, data, text, incremental) + local get_elem_id_fn = fns.get_elem_id_fn and fns.get_elem_id_fn(elem) or function(elem) return elem end + if not data.saved_original then + data.saved_original = copy_to_lua_table(vec) + data.saved_flags = copy_to_lua_table(flags_vec) + data.saved_idx_map = {} + for idx,elem in ipairs(data.saved_original) do + data.saved_idx_map[get_elem_id_fn(elem)] = idx -- 1-based idx + end + else -- sync flag changes to saved vector + for idx,elem in ipairs(vec) do -- 0-based idx + data.saved_flags[data.saved_idx_map[get_elem_id_fn(elem)]] = flags_vec[idx] + end + end + + if not incremental then + vec:assign(data.saved_original) + flags_vec:assign(data.saved_flags) + end + + filter_vec(fns, flags_vec, vec, text, function(idx) + vec:erase(idx) + flags_vec:erase(idx) + end) +end + +return _ENV From 9acf81d3a0fa2c1493d2d06057bb5dc5f84d0420 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 03:57:03 -0700 Subject: [PATCH 57/76] port info widgets to unified superclass --- plugins/lua/sort/info.lua | 397 ++++++++------------------ plugins/lua/sort/locationselector.lua | 2 +- plugins/lua/sort/sortoverlay.lua | 13 +- 3 files changed, 130 insertions(+), 282 deletions(-) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index b732e504f..7e759bd10 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -1,7 +1,7 @@ local _ENV = mkmodule('plugins.sort.info') local gui = require('gui') -local overlay = require('plugins.overlay') +local sortoverlay = require('plugins.sort.sortoverlay') local widgets = require('gui.widgets') local utils = require('utils') @@ -12,8 +12,6 @@ local objects = info.artifacts local tasks = info.jobs local work_details = info.labor.work_details -local state = {} - -- these sort functions attempt to match the vanilla info panel sort behavior, which -- is not quite the same as the rest of DFHack. For example, in other DFHack sorts, -- we'd always sort by name descending as a secondary sort. To match vanilla sorting, @@ -98,49 +96,6 @@ local function get_sort() end end -local function copy_to_lua_table(vec) - local tab = {} - for k,v in ipairs(vec) do - tab[k+1] = v - end - return tab -end - -local function general_search(vec, get_search_key_fn, get_sort_fn, matches_filters_fn, data, filter, incremental) - if not data.saved_original then - data.saved_original = copy_to_lua_table(vec) - elseif not incremental then - vec:assign(data.saved_original) - end - if matches_filters_fn ~= DEFAULT_NIL or filter ~= '' then - local search_tokens = filter:split() - for idx = #vec-1,0,-1 do - local search_key = get_search_key_fn(vec[idx]) - if (search_key and not utils.search_text(search_key, search_tokens)) or - (matches_filters_fn ~= DEFAULT_NIL and not matches_filters_fn(vec[idx])) - then - vec:erase(idx) - end - end - end - data.saved_visible = copy_to_lua_table(vec) - if get_sort_fn then - table.sort(data.saved_visible, get_sort_fn()) - vec:assign(data.saved_visible) - end -end - --- add dynamically allocated elements that were not visible at the time of --- closure back to the vector so they can be cleaned up when it is next initialized -local function cri_unitst_cleanup(vec, data) - if not data.saved_visible or not data.saved_original then return end - for _,elem in ipairs(data.saved_original) do - if not utils.linear_index(data.saved_visible, elem) then - vec:insert('#', elem) - end - end -end - local function get_unit_search_key(unit) return ('%s %s %s'):format( dfhack.units.getReadableName(unit), -- last name is in english @@ -148,200 +103,46 @@ local function get_unit_search_key(unit) dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name end -local function make_cri_unitst_handlers(vec, sort_fn) - return { - search_fn=curry(general_search, vec, - function(elem) - return ('%s %s'):format( - elem.un and get_unit_search_key(elem.un) or '', - elem.job_sort_name) - end, - sort_fn), - cleanup_fn=curry(cri_unitst_cleanup, vec), - } +local function get_cri_unit_search_key(cri_unit) + return ('%s %s'):format( + cri_unit.un and get_unit_search_key(cri_unit.un) or '', + cri_unit.job_sort_name) +end + +local function get_race_name(raw_id) + local raw = df.creature_raw.find(raw_id) + if not raw then return end + return raw.name[1] end -local function overall_training_search(matches_filters_fn, data, filter, incremental) - general_search(creatures.atk_index, function(elem) - local raw = df.creature_raw.find(elem) - if not raw then return end - return raw.name[1] - end, nil, matches_filters_fn, data, filter, incremental) +local function get_trainer_search_key(unit) + if not unit then return end + return ('%s %s'):format(dfhack.TranslateName(unit.name), dfhack.units.getProfessionName(unit)) end -local function assign_trainer_search(matches_filters_fn, data, filter, incremental) - general_search(creatures.trainer, function(elem) - if not elem then return end - return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.units.getProfessionName(elem)) - end, nil, matches_filters_fn, data, filter, incremental) +-- get name in both dwarvish and English +local function get_artifact_search_key(artifact) + return ('%s %s'):format(dfhack.TranslateName(artifact.name), dfhack.TranslateName(artifact.name, true)) end -local function work_details_search(matches_filters_fn, data, filter, incremental) +local function work_details_search(vec, data, text, incremental) if work_details.selected_work_detail_index ~= data.selected then data.saved_original = nil data.selected = work_details.selected_work_detail_index end - general_search(work_details.assignable_unit, get_unit_search_key, - nil, matches_filters_fn, data, filter, incremental) -end - --- independent implementation of search algorithm since we need to --- keep two vectors in sync -local function interrogating_search(matches_filters_fn, data, filter, incremental) - local vec, flags_vec = justice.interrogation_list, justice.interrogation_list_flag - if not data.saved_original then - data.saved_original = copy_to_lua_table(vec) - data.saved_flags = copy_to_lua_table(flags_vec) - data.saved_idx_map = {} - for idx, unit in ipairs(data.saved_original) do - data.saved_idx_map[unit.id] = idx -- 1-based idx - end - else -- sync flag changes to saved vector - for idx, unit in ipairs(vec) do -- 0-based idx - data.saved_flags[data.saved_idx_map[unit.id]] = flags_vec[idx] - end - end - - if not incremental then - vec:assign(data.saved_original) - flags_vec:assign(data.saved_flags) - end - - if matches_filters_fn ~= DEFAULT_NIL or filter ~= '' then - local search_tokens = filter:split() - for idx = #vec-1,0,-1 do - local search_key = get_unit_search_key(vec[idx]) - if (search_key and not utils.search_text(search_key, search_tokens)) or - (matches_filters_fn ~= DEFAULT_NIL and not matches_filters_fn(vec[idx], idx)) - then - vec:erase(idx) - flags_vec:erase(idx) - end - end - end -end - -local function convicting_search(matches_filters_fn, data, filter, incremental) - general_search(justice.conviction_list, get_unit_search_key, - nil, matches_filters_fn, data, filter, incremental) -end - -local HANDLERS = { - CITIZEN=make_cri_unitst_handlers(creatures.cri_unit.CITIZEN, get_sort), - PET=make_cri_unitst_handlers(creatures.cri_unit.PET, get_sort), - OTHER=make_cri_unitst_handlers(creatures.cri_unit.OTHER, get_sort), - DECEASED=make_cri_unitst_handlers(creatures.cri_unit.DECEASED, get_sort), - PET_OT={search_fn=overall_training_search}, - PET_AT={search_fn=assign_trainer_search}, - JOBS=make_cri_unitst_handlers(tasks.cri_job), - WORK_DETAILS={search_fn=work_details_search}, - INTERROGATING={search_fn=interrogating_search}, - CONVICTING={search_fn=convicting_search}, -} -for idx,name in ipairs(df.artifacts_mode_type) do - if idx < 0 then goto continue end - HANDLERS[name] = { - search_fn=curry(general_search, objects.list[idx], - function(elem) - return ('%s %s'):format(dfhack.TranslateName(elem.name), dfhack.TranslateName(elem.name, true)) - end, nil) - } - ::continue:: -end - -local function get_key() - if info.current_mode == df.info_interface_mode_type.JUSTICE then - if justice.interrogating then - return 'INTERROGATING' - elseif justice.convicting then - return 'CONVICTING' - end - elseif info.current_mode == df.info_interface_mode_type.CREATURES then - if creatures.current_mode == df.unit_list_mode_type.PET then - if creatures.showing_overall_training then - return 'PET_OT' - elseif creatures.adding_trainer then - return 'PET_AT' - end - end - return df.unit_list_mode_type[creatures.current_mode] - elseif info.current_mode == df.info_interface_mode_type.JOBS then - return 'JOBS' - elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then - return df.artifacts_mode_type[objects.mode] - elseif info.current_mode == df.info_interface_mode_type.LABOR then - if info.labor.mode == df.labor_mode_type.WORK_DETAILS then - return 'WORK_DETAILS' - end - end -end - -local function check_context(self, key_ctx) - local key = get_key() - if state[key_ctx] ~= key then - state[key_ctx] = key - local prev_text = key and ensure_key(state, key).prev_text or '' - self.subviews.search:setText(prev_text) - end -end - -local function do_search(matches_filters_fn, text, force_full_search) - if not force_full_search and not next(state) and text == '' then return end - -- the EditField state is guaranteed to be consistent with the current - -- context since when clicking to switch tabs, onRenderBody is always called - -- before this text_input callback, even if a key is pressed before the next - -- graphical frame would otherwise be printed. if this ever becomes untrue, - -- then we can add an on_char handler to the EditField that also calls - -- check_context. - local key = get_key() - if not key then return end - local prev_text = ensure_key(state, key).prev_text - -- some screens reset their contents between context switches; regardless - -- a switch back to the context should results in an incremental search - local incremental = not force_full_search and prev_text and text:startswith(prev_text) - HANDLERS[key].search_fn(matches_filters_fn, state[key], text, incremental) - state[key].prev_text = text -end - -local function on_update(self) - if self.overlay_onupdate_max_freq_seconds == 0 and - not dfhack.gui.matchFocusString('dwarfmode/Info', dfhack.gui.getDFViewscreen(true)) - then - for k,v in pairs(state) do - local cleanup_fn = safe_index(HANDLERS, k, 'cleanup_fn') - if cleanup_fn then cleanup_fn(v) end - end - state = {} - self.subviews.search:setText('') - self.subviews.search:setFocus(false) - self.overlay_onupdate_max_freq_seconds = 60 - end -end - -local function on_input(self, clazz, keys) - if keys._MOUSE_R and self.subviews.search.focus and self:get_handled_key() then - self.subviews.search:setFocus(false) - return true - end - return clazz.super.onInput(self, keys) -end - -local function is_interrogate_or_convict() - local key = get_key() - return key == 'INTERROGATING' or key == 'CONVICTING' + sortoverlay.single_vector_search( + {get_search_key_fn=get_unit_search_key}, + vec, data, text, incremental) end -- ---------------------- -- InfoOverlay -- -InfoOverlay = defclass(InfoOverlay, overlay.OverlayWidget) +InfoOverlay = defclass(InfoOverlay, sortoverlay.SortOverlay) InfoOverlay.ATTRS{ default_pos={x=64, y=8}, - default_enabled=true, viewscreens='dwarfmode/Info', - hotspot=true, - overlay_onupdate_max_freq_seconds=0, frame={w=40, h=4}, } @@ -350,26 +151,71 @@ function InfoOverlay:init() widgets.BannerPanel{ view_id='panel', frame={l=0, t=0, r=0, h=1}, - visible=self:callback('get_handled_key'), + visible=self:callback('get_key'), subviews={ widgets.EditField{ view_id='search', frame={l=1, t=0, r=1}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=function(text) do_search(DEFAULT_NIL, text) end, + on_change=function(text) self:do_search(text) end, }, }, }, } -end -function InfoOverlay:overlay_onupdate() - on_update(self) -end - -function InfoOverlay:get_handled_key() - return not is_interrogate_or_convict() and get_key() or nil + local CRI_UNIT_VECS = { + CITIZEN=creatures.cri_unit.CITIZEN, + PET=creatures.cri_unit.PET, + OTHER=creatures.cri_unit.OTHER, + DECEASED=creatures.cri_unit.DECEASED, + } + for key,vec in pairs(CRI_UNIT_VECS) do + self:register_handler(key, vec, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=get_cri_unit_search_key, + get_sort_fn=get_sort + }), + true) + end + + self:register_handler('JOBS', tasks.cri_job, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_cri_unit_search_key}), + true) + self:register_handler('PET_OT', creatures.atk_index, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_race_name})) + self:register_handler('PET_AT', creatures.trainer, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_trainer_search_key})) + self:register_handler('WORK_DETAILS', work_details.assignable_unit, work_details_search) + + for idx,name in ipairs(df.artifacts_mode_type) do + if idx < 0 then goto continue end + self:register_handler(name, objects.list[idx], + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_artifact_search_key})) + ::continue:: + end +end + +function InfoOverlay:get_key() + if info.current_mode == df.info_interface_mode_type.CREATURES then + if creatures.current_mode == df.unit_list_mode_type.PET then + if creatures.showing_overall_training then + return 'PET_OT' + elseif creatures.adding_trainer then + return 'PET_AT' + end + end + return df.unit_list_mode_type[creatures.current_mode] + elseif info.current_mode == df.info_interface_mode_type.JOBS then + return 'JOBS' + elseif info.current_mode == df.info_interface_mode_type.ARTIFACTS then + return df.artifacts_mode_type[objects.mode] + elseif info.current_mode == df.info_interface_mode_type.LABOR then + if info.labor.mode == df.labor_mode_type.WORK_DETAILS then + return 'WORK_DETAILS' + end + end end local function resize_overlay(self) @@ -410,49 +256,34 @@ function InfoOverlay:updateFrames() end function InfoOverlay:onRenderBody(dc) - if next(state) then - check_context(self, InfoOverlay) - end + InfoOverlay.super.onRenderBody(self, dc) if self:updateFrames() then self:updateLayout() end if self.refresh_search then self.refresh_search = nil - do_search(DEFAULT_NIL, self.subviews.search.text) + self:do_search(self.subviews.search.text) end - self.overlay_onupdate_max_freq_seconds = 0 - InfoOverlay.super.onRenderBody(self, dc) end function InfoOverlay:onInput(keys) - if keys._MOUSE_L and get_key() == 'WORK_DETAILS' then + if keys._MOUSE_L and self:get_key() == 'WORK_DETAILS' then self.refresh_search = true end - return on_input(self, InfoOverlay, keys) + return InfoOverlay.super.onInput(self, keys) end -- ---------------------- -- InterrogationOverlay -- -InterrogationOverlay = defclass(InterrogationOverlay, overlay.OverlayWidget) +InterrogationOverlay = defclass(InterrogationOverlay, sortoverlay.SortOverlay) InterrogationOverlay.ATTRS{ default_pos={x=47, y=10}, - default_enabled=true, viewscreens='dwarfmode/Info/JUSTICE', frame={w=27, h=9}, - hotspot=true, - overlay_onupdate_max_freq_seconds=0, } -function InterrogationOverlay:overlay_onupdate() - on_update(self) -end - -function InterrogationOverlay:get_handled_key() - return is_interrogate_or_convict() and get_key() or nil -end - function InterrogationOverlay:init() self:addviews{ widgets.Panel{ @@ -460,14 +291,14 @@ function InterrogationOverlay:init() frame={l=0, t=4, h=5, r=0}, frame_background=gui.CLEAR_PEN, frame_style=gui.FRAME_MEDIUM, - visible=is_interrogate_or_convict, + visible=self:callback('get_key'), subviews={ widgets.EditField{ view_id='search', frame={l=0, t=0, r=0}, label_text="Search: ", key='CUSTOM_ALT_S', - on_change=function(text) do_search(self:callback('matches_filters'), text) end, + on_change=function(text) self:do_search(text) end, }, widgets.ToggleHotkeyLabel{ view_id='include_interviewed', @@ -479,9 +310,7 @@ function InterrogationOverlay:init() {label='Exclude', value=false, pen=COLOR_RED}, }, visible=function() return justice.interrogating end, - on_change=function() - do_search(self:callback('matches_filters'), self.subviews.search.text, true) - end, + on_change=function() self:do_search(self.subviews.search.text, true) end, }, widgets.CycleHotkeyLabel{ view_id='subset', @@ -498,13 +327,40 @@ function InterrogationOverlay:init() {label='Deceased or missing', value='deceased', pen=COLOR_MAGENTA}, {label='Others', value='others', pen=COLOR_GRAY}, }, - on_change=function() - do_search(self:callback('matches_filters'), self.subviews.search.text, true) - end, + on_change=function() self:do_search(self.subviews.search.text, true) end, }, }, }, } + + self:register_handler('INTERROGATING', justice.interrogation_list, + curry(sortoverlay.flags_vector_search, + { + get_search_key_fn=get_unit_search_key, + get_elem_id_fn=function(unit) return unit.id end, + matches_filters_fn=self:callback('matches_filters'), + }, + justice.interrogation_list_flag)) + self:register_handler('CONVICTING', justice.conviction_list, + curry(sortoverlay.single_vector_search, + { + get_search_key_fn=get_unit_search_key, + matches_filters_fn=self:callback('matches_filters'), + })) +end + +function InterrogationOverlay:reset() + InterrogationOverlay.super.reset(self) + self.subviews.include_interviewed:setOption(true, false) + self.subviews.subset:setOption('all') +end + +function InterrogationOverlay:get_key() + if justice.interrogating then + return 'INTERROGATING' + elseif justice.convicting then + return 'CONVICTING' + end end local RISKY_PROFESSIONS = utils.invert{ @@ -521,18 +377,20 @@ local function is_risky(unit) return not dfhack.units.isAlive(unit) -- detect intelligent undead end -function InterrogationOverlay:matches_filters(unit, idx) +function InterrogationOverlay:matches_filters(unit, flag) if justice.interrogating then local include_interviewed = self.subviews.include_interviewed:getOptionValue() - if not include_interviewed and justice.interrogation_list_flag[idx] == 2 then - return false - end + if not include_interviewed and flag == 2 then return false end end local subset = self.subviews.subset:getOptionValue() if subset == 'all' then return true elseif dfhack.units.isDead(unit) or not dfhack.units.isActive(unit) then return subset == 'deceased' + elseif dfhack.units.isInvader(unit) or dfhack.units.isOpposedToLife(unit) + or unit.flags2.visitor_uninvited or unit.flags4.agitated_wilderness_creature + then + return subset == 'others' elseif dfhack.units.isVisiting(unit) then local risky = is_risky(unit) return (subset == 'risky' and risky) or (subset == 'visitors' and not risky) @@ -569,19 +427,4 @@ function InterrogationOverlay:render(dc) InterrogationOverlay.super.render(self, dc) end -function InterrogationOverlay:onRenderBody(dc) - if next(state) then - check_context(self, InterrogationOverlay) - else - self.subviews.include_interviewed:setOption(true, false) - self.subviews.subset:setOption('all') - end - self.overlay_onupdate_max_freq_seconds = 0 - InterrogationOverlay.super.onRenderBody(self, dc) -end - -function InterrogationOverlay:onInput(keys) - return on_input(self, InterrogationOverlay, keys) -end - return _ENV diff --git a/plugins/lua/sort/locationselector.lua b/plugins/lua/sort/locationselector.lua index 8092db1e2..8a79a9d8f 100644 --- a/plugins/lua/sort/locationselector.lua +++ b/plugins/lua/sort/locationselector.lua @@ -51,7 +51,7 @@ function LocationSelectorOverlay:init() self:register_handler('TEMPLE', location_selector.valid_religious_practice_id, curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_religion_string}, - location_selector.valid_religious_practice)) + location_selector.valid_religious_practice)) self:register_handler('GUILDHALL', location_selector.valid_craft_guild_type, curry(sortoverlay.single_vector_search, {get_search_key_fn=get_profession_string})) end diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index 96c0f8ecc..b3067a8ba 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -56,13 +56,17 @@ function SortOverlay:overlay_onupdate() restore_filtered(self.handlers[key].vec, data) end end - self.state = {} - self.subviews.search:setText('') - self.subviews.search:setFocus(false) + self:reset() self.overlay_onupdate_max_freq_seconds = 300 end end +function SortOverlay:reset() + self.state = {} + self.subviews.search:setText('') + self.subviews.search:setFocus(false) +end + -- returns the current context key for dereferencing the handler -- subclasses must override function SortOverlay:get_key() @@ -77,6 +81,7 @@ function SortOverlay:onRenderBody(dc) self.state.cur_key = key local prev_text = key and ensure_key(self.state, key).prev_text or '' self.subviews.search:setText(prev_text) + self:do_search(self.subviews.search.text, true) end end self.overlay_onupdate_max_freq_seconds = 0 @@ -141,7 +146,7 @@ end -- doesn't support cleanup since nothing that uses this needs it yet function flags_vector_search(fns, flags_vec, vec, data, text, incremental) - local get_elem_id_fn = fns.get_elem_id_fn and fns.get_elem_id_fn(elem) or function(elem) return elem end + local get_elem_id_fn = fns.get_elem_id_fn or function(elem) return elem end if not data.saved_original then data.saved_original = copy_to_lua_table(vec) data.saved_flags = copy_to_lua_table(flags_vec) From f282efd4d4a7aa5911a22f5b1a39eac800ad2979 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 04:06:00 -0700 Subject: [PATCH 58/76] update docs --- docs/changelog.txt | 1 + docs/plugins/sort.rst | 24 ++++++++++++++++-------- plugins/lua/sort/sortoverlay.lua | 2 ++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 8a740a670..589952305 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: ## New Features - `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay - `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice" +- `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate) ## Fixes - `zone`: don't show animal assignment link for dungeon cages/restraints diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 70e101b3f..21a2acacf 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -92,8 +92,8 @@ https://www.reddit.com/r/dwarffortress/comments/163kczo/enhancing_military_candi "Mental stability" is explained here: https://www.reddit.com/r/dwarffortress/comments/1617s11/enhancing_military_candidate_selection_part_2/ -Info overlay ------------- +Info tabs overlay +----------------- The Info overlay adds search support to many of the fort-wide "Info" panels (e.g. "Creatures", "Tasks", etc.). When searching for units, you can search by @@ -102,13 +102,21 @@ special status (like "necromancer"). If there is text in the second column, you can search for that text as well. This is often a job name or a status, like "caged". +Interrogation overlay +--------------------- + In the interrogation and conviction screens under the "Justice" tab, you can -also filter by the classification of the unit. The classification groups are -ordered by how likely a member of that group is to be involved in a plot. The -groups are: All, Risky visitors, Other visitors, Residents, Citizens, Animals, -Deceased, and Others. "Risky" visitors are those who are especially likely to -be involved in plots, such as criminals, necromancers, necromancer experiments, -and intelligent undead. +search for units by name. You can also filter by the classification of the +unit. The classification groups are ordered by how likely a member of that +group is to be involved in a plot. The groups are: All, Risky visitors, Other +visitors, Residents, Citizens, Animals, Deceased, and Others. "Risky" visitors are those who are especially likely to be involved in plots, such as criminals, +necromancers, necromancer experiments, and intelligent undead. On the interrogations screen, you can also filter units by whether they have already been interrogated. + +Location selection overlay +-------------------------- + +When choosing the type of guildhall or temple to dedicate, you can search for +the relevant profession, religion, or deity by name. diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index b3067a8ba..229fd2559 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -21,6 +21,8 @@ SortOverlay.ATTRS{ hotspot=true, overlay_onupdate_max_freq_seconds=0, -- subclasses expected to provide default_pos, viewscreens (single string), and frame + -- viewscreens should be the top-level scope within which the search widget state is maintained + -- once the player leaves that scope, widget state will be reset } function SortOverlay:init() From 09e3ed427a2967682d54cc3a607028402c658ef9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 05:03:59 -0700 Subject: [PATCH 59/76] add search functionality for burrows assignment screen --- docs/changelog.txt | 1 + plugins/lua/sort.lua | 9 +++-- plugins/lua/sort/burrows.lua | 70 ++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 plugins/lua/sort/burrows.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index 589952305..f79cbdd28 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,7 @@ Template for new versions: - `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay - `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice" - `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate) +- `sort`: new search widgets for burrow assignment screen ## Fixes - `zone`: don't show animal assignment link for dungeon cages/restraints diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index ce9eb1f9e..3bc97cb74 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1,8 +1,6 @@ local _ENV = mkmodule('plugins.sort') -local info = require('plugins.sort.info') local gui = require('gui') -local locationselector = require('plugins.sort.locationselector') local overlay = require('plugins.overlay') local setbelief = reqscript('modtools/set-belief') local textures = require('gui.textures') @@ -1287,9 +1285,10 @@ end OVERLAY_WIDGETS = { squad_assignment=SquadAssignmentOverlay, squad_annotation=SquadAnnotationOverlay, - info=info.InfoOverlay, - interrogation=info.InterrogationOverlay, - location_selector=locationselector.LocationSelectorOverlay, + info=require('plugins.sort.info').InfoOverlay, + interrogation=require('plugins.sort.info').InterrogationOverlay, + location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, + burrows=require('plugins.sort.burrows').BurrowOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/burrows.lua b/plugins/lua/sort/burrows.lua new file mode 100644 index 000000000..fe2b8ec8e --- /dev/null +++ b/plugins/lua/sort/burrows.lua @@ -0,0 +1,70 @@ +local _ENV = mkmodule('plugins.sort.burrows') + +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') + +local unit_selector = df.global.game.main_interface.unit_selector + +-- ---------------------- +-- BurrowOverlay +-- + +BurrowOverlay = defclass(BurrowOverlay, sortoverlay.SortOverlay) +BurrowOverlay.ATTRS{ + default_pos={x=62, y=6}, + viewscreens='dwarfmode/UnitSelector/BURROW_ASSIGNMENT', + frame={w=26, h=1}, +} + +local function get_unit_id_search_key(unit_id) + local unit = df.unit.find(unit_id) + if not unit then return end + return ('%s %s %s'):format( + dfhack.units.getReadableName(unit), -- last name is in english + dfhack.units.getProfessionName(unit), + dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name +end + +function BurrowOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, r=0, h=1}, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + self:register_handler('BURROW', unit_selector.unid, + curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_unit_id_search_key}, + unit_selector.selected)) +end + +function BurrowOverlay:get_key() + if unit_selector.context == df.unit_selector_context_type.BURROW_ASSIGNMENT then + return 'BURROW' + end +end + +function BurrowOverlay:onRenderBody(dc) + BurrowOverlay.super.onRenderBody(self, dc) + if self.refresh_search then + self.refresh_search = nil + self:do_search(self.subviews.search.text) + end +end + +function BurrowOverlay:onInput(keys) + if keys._MOUSE_L then + self.refresh_search = true + end + return BurrowOverlay.super.onInput(self, keys) +end + +return _ENV From e41017a26b4eabe3d6fa8014173fc274dc17deb2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 06:03:06 -0700 Subject: [PATCH 60/76] generalize burrow code to other unit assignment screens --- docs/changelog.txt | 2 +- plugins/lua/sort.lua | 2 +- plugins/lua/sort/burrows.lua | 70 ------------------------ plugins/lua/sort/unitselector.lua | 91 +++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 72 deletions(-) delete mode 100644 plugins/lua/sort/burrows.lua create mode 100644 plugins/lua/sort/unitselector.lua diff --git a/docs/changelog.txt b/docs/changelog.txt index f79cbdd28..6b9b1e597 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,7 +59,7 @@ Template for new versions: - `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay - `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice" - `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate) -- `sort`: new search widgets for burrow assignment screen +- `sort`: new search widgets for burrow assignment screen and other unit assignment dialogs ## Fixes - `zone`: don't show animal assignment link for dungeon cages/restraints diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 3bc97cb74..0fc6932c4 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1288,7 +1288,7 @@ OVERLAY_WIDGETS = { info=require('plugins.sort.info').InfoOverlay, interrogation=require('plugins.sort.info').InterrogationOverlay, location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, - burrows=require('plugins.sort.burrows').BurrowOverlay, + unit_selector=require('plugins.sort.unitselector').UnitSelectorOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/burrows.lua b/plugins/lua/sort/burrows.lua deleted file mode 100644 index fe2b8ec8e..000000000 --- a/plugins/lua/sort/burrows.lua +++ /dev/null @@ -1,70 +0,0 @@ -local _ENV = mkmodule('plugins.sort.burrows') - -local sortoverlay = require('plugins.sort.sortoverlay') -local widgets = require('gui.widgets') - -local unit_selector = df.global.game.main_interface.unit_selector - --- ---------------------- --- BurrowOverlay --- - -BurrowOverlay = defclass(BurrowOverlay, sortoverlay.SortOverlay) -BurrowOverlay.ATTRS{ - default_pos={x=62, y=6}, - viewscreens='dwarfmode/UnitSelector/BURROW_ASSIGNMENT', - frame={w=26, h=1}, -} - -local function get_unit_id_search_key(unit_id) - local unit = df.unit.find(unit_id) - if not unit then return end - return ('%s %s %s'):format( - dfhack.units.getReadableName(unit), -- last name is in english - dfhack.units.getProfessionName(unit), - dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name -end - -function BurrowOverlay:init() - self:addviews{ - widgets.BannerPanel{ - frame={l=0, t=0, r=0, h=1}, - subviews={ - widgets.EditField{ - view_id='search', - frame={l=1, t=0, r=1}, - label_text="Search: ", - key='CUSTOM_ALT_S', - on_change=function(text) self:do_search(text) end, - }, - }, - }, - } - - self:register_handler('BURROW', unit_selector.unid, - curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_unit_id_search_key}, - unit_selector.selected)) -end - -function BurrowOverlay:get_key() - if unit_selector.context == df.unit_selector_context_type.BURROW_ASSIGNMENT then - return 'BURROW' - end -end - -function BurrowOverlay:onRenderBody(dc) - BurrowOverlay.super.onRenderBody(self, dc) - if self.refresh_search then - self.refresh_search = nil - self:do_search(self.subviews.search.text) - end -end - -function BurrowOverlay:onInput(keys) - if keys._MOUSE_L then - self.refresh_search = true - end - return BurrowOverlay.super.onInput(self, keys) -end - -return _ENV diff --git a/plugins/lua/sort/unitselector.lua b/plugins/lua/sort/unitselector.lua new file mode 100644 index 000000000..b5423acee --- /dev/null +++ b/plugins/lua/sort/unitselector.lua @@ -0,0 +1,91 @@ +local _ENV = mkmodule('plugins.sort.unitselector') + +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') + +local unit_selector = df.global.game.main_interface.unit_selector + +-- pen, pit, chain, and cage assignment are handled by dedicated screens +-- squad fill position screen has a specialized overlay +-- we *could* add search functionality to vanilla screens for pit and cage, +-- but then we'd have to handle the itemid vector +local HANDLED_SCREENS = { + ZONE_BEDROOM_ASSIGNMENT='already', + ZONE_OFFICE_ASSIGNMENT='already', + ZONE_DINING_HALL_ASSIGNMENT='already', + ZONE_TOMB_ASSIGNMENT='already', + -- this one should technically appear further to the left, but when the screen + -- gets small enough that that matters, the vanilla widgets are unreadable + WORKER_ASSIGNMENT='selected', + OCCUPATION_ASSIGNMENT='selected', + BURROW_ASSIGNMENT='selected', + SQUAD_KILL_ORDER='selected', +} + +-- ---------------------- +-- UnitSelectorOverlay +-- + +UnitSelectorOverlay = defclass(UnitSelectorOverlay, sortoverlay.SortOverlay) +UnitSelectorOverlay.ATTRS{ + default_pos={x=62, y=6}, + viewscreens='dwarfmode/UnitSelector', + frame={w=26, h=1}, +} + +local function get_unit_id_search_key(unit_id) + local unit = df.unit.find(unit_id) + if not unit then return end + return ('%s %s %s'):format( + dfhack.units.getReadableName(unit), -- last name is in english + dfhack.units.getProfessionName(unit), + dfhack.TranslateName(unit.name, false, true)) -- get untranslated last name +end + +function UnitSelectorOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, r=0, h=1}, + visible=self:callback('get_key'), + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + for name,flags_vec in pairs(HANDLED_SCREENS) do + self:register_handler(name, unit_selector.unid, + curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_unit_id_search_key}, + unit_selector[flags_vec])) + end +end + +function UnitSelectorOverlay:get_key() + local key = df.unit_selector_context_type[unit_selector.context] + if HANDLED_SCREENS[key] then + return key + end +end + +function UnitSelectorOverlay:onRenderBody(dc) + UnitSelectorOverlay.super.onRenderBody(self, dc) + if self.refresh_search then + self.refresh_search = nil + self:do_search(self.subviews.search.text) + end +end + +function UnitSelectorOverlay:onInput(keys) + if keys._MOUSE_L then + self.refresh_search = true + end + return UnitSelectorOverlay.super.onInput(self, keys) +end + +return _ENV From 75e7e6462dd99d2c82b43ab3b816cee698f7aa10 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 16:40:20 -0700 Subject: [PATCH 61/76] add focus strings for viewscreen_worldst --- library/modules/Gui.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 3e4281c29..728786d30 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -89,6 +89,7 @@ using namespace DFHack; #include "df/viewscreen_new_regionst.h" #include "df/viewscreen_setupdwarfgamest.h" #include "df/viewscreen_titlest.h" +#include "df/viewscreen_worldst.h" #include "df/world.h" const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size @@ -224,6 +225,11 @@ DEFINE_GET_FOCUS_STRING_HANDLER(legends) focusStrings.push_back(baseFocus + '/' + screen->page[screen->active_page_index]->header); } +DEFINE_GET_FOCUS_STRING_HANDLER(world) +{ + focusStrings.push_back(baseFocus + '/' + enum_item_key(screen->view_mode)); +} + DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) { std::string newFocusString; From 60818e2194f31549c663ec3df98fb8e4d91bd1ee Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 16:41:02 -0700 Subject: [PATCH 62/76] support search on the world artifacts screen --- plugins/lua/sort.lua | 1 + plugins/lua/sort/info.lua | 13 ++++- plugins/lua/sort/sortoverlay.lua | 30 ++++++----- plugins/lua/sort/world.lua | 87 ++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 plugins/lua/sort/world.lua diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 0fc6932c4..2a98ff42c 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1289,6 +1289,7 @@ OVERLAY_WIDGETS = { interrogation=require('plugins.sort.info').InterrogationOverlay, location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, unit_selector=require('plugins.sort.unitselector').UnitSelectorOverlay, + world=require('plugins.sort.world').WorldOverlay, } dfhack.onStateChange[GLOBAL_KEY] = function(sc) diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 7e759bd10..90a4e2348 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -135,6 +135,15 @@ local function work_details_search(vec, data, text, incremental) vec, data, text, incremental) end +local function cleanup_cri_unit(vec, data) + if not data.saved_visible or not data.saved_original then return end + for _,elem in ipairs(data.saved_original) do + if not utils.linear_index(data.saved_visible, elem) then + vec:insert('#', elem) + end + end +end + -- ---------------------- -- InfoOverlay -- @@ -177,12 +186,12 @@ function InfoOverlay:init() get_search_key_fn=get_cri_unit_search_key, get_sort_fn=get_sort }), - true) + curry(cleanup_cri_unit, vec)) end self:register_handler('JOBS', tasks.cri_job, curry(sortoverlay.single_vector_search, {get_search_key_fn=get_cri_unit_search_key}), - true) + curry(cleanup_cri_unit, vec)) self:register_handler('PET_OT', creatures.atk_index, curry(sortoverlay.single_vector_search, {get_search_key_fn=get_race_name})) self:register_handler('PET_AT', creatures.trainer, diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index 229fd2559..0a6d04bfd 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -31,31 +31,23 @@ function SortOverlay:init() -- subclasses expected to provide an EditField widget with view_id='search' end -function SortOverlay:register_handler(key, vec, search_fn, restore_filtered_on_cleanup) +function SortOverlay:register_handler(key, vec, search_fn, cleanup_fn) self.handlers[key] = { vec=vec, search_fn=search_fn, - restore_filtered_on_cleanup=restore_filtered_on_cleanup + cleanup_fn=cleanup_fn } end -local function restore_filtered(vec, data) - if not data.saved_visible or not data.saved_original then return end - for _,elem in ipairs(data.saved_original) do - if not utils.linear_index(data.saved_visible, elem) then - vec:insert('#', elem) - end - end -end - -- handles reset and clean up when the player exits the handled scope function SortOverlay:overlay_onupdate() if self.overlay_onupdate_max_freq_seconds == 0 and not dfhack.gui.matchFocusString(self.viewscreens, dfhack.gui.getDFViewscreen(true)) then for key,data in pairs(self.state) do - if safe_index(self.handlers, key, 'restore_filtered_on_cleanup') then - restore_filtered(self.handlers[key].vec, data) + local cleanup_fn = safe_index(self.handlers, key, 'cleanup_fn') + if cleanup_fn then + cleanup_fn(data) end end self:reset() @@ -133,25 +125,33 @@ local function filter_vec(fns, flags_vec, vec, text, erase_fn) end function single_vector_search(fns, vec, data, text, incremental) + vec = utils.getval(vec) if not data.saved_original then data.saved_original = copy_to_lua_table(vec) + data.saved_original_size = #vec elseif not incremental then vec:assign(data.saved_original) + vec:resize(data.saved_original_size) end filter_vec(fns, nil, vec, text, function(idx) vec:erase(idx) end) data.saved_visible = copy_to_lua_table(vec) if fns.get_sort_fn then table.sort(data.saved_visible, fns.get_sort_fn()) vec:assign(data.saved_visible) + vec:resize(data.saved_visible_size) end end --- doesn't support cleanup since nothing that uses this needs it yet +-- doesn't support sorting since nothing that uses this needs it yet function flags_vector_search(fns, flags_vec, vec, data, text, incremental) local get_elem_id_fn = fns.get_elem_id_fn or function(elem) return elem end + flags_vec, vec = utils.getval(flags_vec), utils.getval(vec) if not data.saved_original then + -- we save the sizes since trailing nils get lost in the lua -> vec assignment data.saved_original = copy_to_lua_table(vec) + data.saved_original_size = #vec data.saved_flags = copy_to_lua_table(flags_vec) + data.saved_flags_size = #flags_vec data.saved_idx_map = {} for idx,elem in ipairs(data.saved_original) do data.saved_idx_map[get_elem_id_fn(elem)] = idx -- 1-based idx @@ -164,7 +164,9 @@ function flags_vector_search(fns, flags_vec, vec, data, text, incremental) if not incremental then vec:assign(data.saved_original) + vec:resize(data.saved_original_size) flags_vec:assign(data.saved_flags) + flags_vec:resize(data.saved_flags_size) end filter_vec(fns, flags_vec, vec, text, function(idx) diff --git a/plugins/lua/sort/world.lua b/plugins/lua/sort/world.lua new file mode 100644 index 000000000..c056840d3 --- /dev/null +++ b/plugins/lua/sort/world.lua @@ -0,0 +1,87 @@ +local _ENV = mkmodule('plugins.sort.world') + +local sortoverlay = require('plugins.sort.sortoverlay') +local widgets = require('gui.widgets') + +-- ---------------------- +-- WorldOverlay +-- + +WorldOverlay = defclass(WorldOverlay, sortoverlay.SortOverlay) +WorldOverlay.ATTRS{ + default_pos={x=-18, y=2}, + viewscreens='world/ARTIFACTS', + frame={w=40, h=1}, +} + +local function get_world_artifact_search_key(artifact, rumor) + local search_key = ('%s %s'):format(dfhack.TranslateName(artifact.name, true), + dfhack.items.getDescription(artifact.item, 0)) + if rumor then + local hf = df.historical_figure.find(rumor.hfid) + if hf then + search_key = ('%s %s %s'):format(search_key, + dfhack.TranslateName(hf.name), + dfhack.TranslateName(hf.name, true)) + end + local ws = df.world_site.find(rumor.stid) + if ws then + search_key = ('%s %s'):format(search_key, + dfhack.TranslateName(ws.name, true)) + end + else + local hf = df.historical_figure.find(artifact.holder_hf) + if hf then + local unit = df.unit.find(hf.unit_id) + if unit then + search_key = ('%s %s'):format(search_key, + dfhack.units.getReadableName(unit)) + end + end + end + return search_key +end + +local function cleanup_artifact_vectors(data) + print('cleanng up') + local vs_world = dfhack.gui.getDFViewscreen(true) + vs_world.artifact:assign(data.saved_original) + vs_world.artifact_arl:assign(data.saved_flags) +end + +function WorldOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={l=0, t=0, r=0, h=1}, + visible=self:callback('get_key'), + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + self:register_handler('ARTIFACTS', + function() return dfhack.gui.getDFViewscreen(true).artifact end, + curry(sortoverlay.flags_vector_search, + { + get_search_key_fn=get_world_artifact_search_key, + get_elem_id_fn=function(artifact_record) return artifact_record.id end, + }, + function() return dfhack.gui.getDFViewscreen(true).artifact_arl end), + cleanup_artifact_vectors) +end + +function WorldOverlay:get_key() + local scr = dfhack.gui.getDFViewscreen(true) + if scr.view_mode == df.world_view_mode_type.ARTIFACTS then + return 'ARTIFACTS' + end +end + +return _ENV From 276e5a35232c5975d701164ffe469e25526a1351 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Oct 2023 16:42:05 -0700 Subject: [PATCH 63/76] update docs --- docs/changelog.txt | 1 + docs/plugins/sort.rst | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6b9b1e597..56523fac2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: - `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice" - `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate) - `sort`: new search widgets for burrow assignment screen and other unit assignment dialogs +- `sort`: new search widgets for artifacts on the world/raid screen ## Fixes - `zone`: don't show animal assignment link for dungeon cages/restraints diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 21a2acacf..fd8d5102e 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -120,3 +120,9 @@ Location selection overlay When choosing the type of guildhall or temple to dedicate, you can search for the relevant profession, religion, or deity by name. + +World overlay +------------- + +Searching is supported for the Artifacts list when viewing the world map (where +you can initiate raids). From 40f9b0484666458a301314bb486604013f50d94b Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 11 Oct 2023 01:14:43 +0000 Subject: [PATCH 64/76] 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 ff278cfe3..a598bc677 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ff278cfe3b98f6f6a36c7a4be19884677b753a8d +Subproject commit a598bc6770199e9b965e00d0eade3f8400c4be9e From 2e2773b6abee7fbf58e4d81ab1dd25ac056587c6 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Thu, 12 Oct 2023 07:13:36 +0000 Subject: [PATCH 65/76] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 20d54145e..4e8ad9d87 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 20d54145e97fbae58cda391f66e6a32c7ee20330 +Subproject commit 4e8ad9d8711c4b5262b3806c4a5e563985fd8728 From f14f55a520ab5e71aa718f2eb4aa4963a687ae39 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 12 Oct 2023 01:21:23 -0700 Subject: [PATCH 66/76] add missing storage of visible vec length --- plugins/lua/sort/sortoverlay.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/lua/sort/sortoverlay.lua b/plugins/lua/sort/sortoverlay.lua index 0a6d04bfd..18b4877b3 100644 --- a/plugins/lua/sort/sortoverlay.lua +++ b/plugins/lua/sort/sortoverlay.lua @@ -135,6 +135,7 @@ function single_vector_search(fns, vec, data, text, incremental) end filter_vec(fns, nil, vec, text, function(idx) vec:erase(idx) end) data.saved_visible = copy_to_lua_table(vec) + data.saved_visible_size = #vec if fns.get_sort_fn then table.sort(data.saved_visible, fns.get_sort_fn()) vec:assign(data.saved_visible) From 99e437e7826368d544e783862ca5ba68612d24bc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 12 Oct 2023 11:01:05 -0700 Subject: [PATCH 67/76] changelog edits --- docs/changelog.txt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 56523fac2..2163305cf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,18 +52,19 @@ Template for new versions: # Future ## New Tools -- `spectate`: automatically follow productive dwarves (returned to availability) -- `preserve-tombs`: tracks tomb assignments to living units and ensures that the tomb stays assigned to them when they die. +- `spectate`: (reinstated) automatically follow dwarves, cycling among interesting ones +- `preserve-tombs`: keep tombs assigned to units when they die ## New Features -- `logistics`: ``automelt`` now optionally supports melting masterworks; feature accessible from `stockpiles` overlay -- `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", the "Work details" subtab under "Labor", and the "Interrogate" and "Convict" screens under "Justice" +- `logistics`: ``automelt`` now optionally supports melting masterworks; click on gear icon on `stockpiles` overlay frame +- `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", and the "Work details" subtab under "Labor" +- `sort`: new search and filter widgets for the "Interrogate" and "Convict" screens under "Justice" - `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate) - `sort`: new search widgets for burrow assignment screen and other unit assignment dialogs - `sort`: new search widgets for artifacts on the world/raid screen ## Fixes -- `zone`: don't show animal assignment link for dungeon cages/restraints +- `zone`: don't show animal assignment link for cages and restraints linked to dungeon zones (which aren't normally assignable) ## Misc Improvements - `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates @@ -77,7 +78,7 @@ Template for new versions: ## Lua - added ``GRAY`` color aliases for ``GREY`` colors -- ``utils.search_text``: text search routine (generalized from ``widgets.FilteredList``) +- ``utils.search_text``: text search routine (generalized from internal ``widgets.FilteredList`` logic) ## Removed - ``FILTER_FULL_TEXT``: moved from ``gui.widgets`` to ``utils``; if your full text search preference is lost, please reset it in `gui/control-panel` From 448dd5fa19f77910d7b9c5043809dd4c9100cc70 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 12 Oct 2023 11:33:22 -0700 Subject: [PATCH 68/76] clean up artifact search code --- plugins/lua/sort/world.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/lua/sort/world.lua b/plugins/lua/sort/world.lua index c056840d3..a4cf6c0e5 100644 --- a/plugins/lua/sort/world.lua +++ b/plugins/lua/sort/world.lua @@ -43,10 +43,11 @@ local function get_world_artifact_search_key(artifact, rumor) end local function cleanup_artifact_vectors(data) - print('cleanng up') local vs_world = dfhack.gui.getDFViewscreen(true) vs_world.artifact:assign(data.saved_original) + vs_world.artifact:resize(data.saved_original_size) vs_world.artifact_arl:assign(data.saved_flags) + vs_world.artifact_arl:resize(data.saved_flags_size) end function WorldOverlay:init() From db08110e56c825a6244330915afd8c2eaab573d5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 12 Oct 2023 11:48:40 -0700 Subject: [PATCH 69/76] bump release version to 50.11-r2rc1 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c09fc6b71..5ec38b28a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,8 +8,8 @@ project(dfhack) # set up versioning. set(DF_VERSION "50.11") -set(DFHACK_RELEASE "r1") -set(DFHACK_PRERELEASE FALSE) +set(DFHACK_RELEASE "r2rc1") +set(DFHACK_PRERELEASE TRUE) set(DFHACK_VERSION "${DF_VERSION}-${DFHACK_RELEASE}") set(DFHACK_ABI_VERSION 1) From ee78f4fbdfd36b282d1b27eaa5a65edf804b8032 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Oct 2023 12:37:11 -0700 Subject: [PATCH 70/76] support searching for spheres on the religion selector --- docs/plugins/sort.rst | 4 +++- plugins/lua/sort/locationselector.lua | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index fd8d5102e..0ffe5118b 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -119,7 +119,9 @@ Location selection overlay -------------------------- When choosing the type of guildhall or temple to dedicate, you can search for -the relevant profession, religion, or deity by name. +the relevant profession, religion, or deity by name. For temples, you can also +search for the "spheres" associated with the deity or religion, such as +"wealth" or "lies". World overlay ------------- diff --git a/plugins/lua/sort/locationselector.lua b/plugins/lua/sort/locationselector.lua index 8a79a9d8f..d91c536ac 100644 --- a/plugins/lua/sort/locationselector.lua +++ b/plugins/lua/sort/locationselector.lua @@ -16,16 +16,38 @@ LocationSelectorOverlay.ATTRS{ frame={w=26, h=1}, } +local function add_spheres(hf, spheres) + if not hf then return end + for _, sphere in ipairs(hf.info.spheres.spheres) do + spheres[sphere] = true + end +end + +local function stringify_spheres(spheres) + local strs = {} + for sphere in pairs(spheres) do + table.insert(strs, df.sphere_type[sphere]) + end + return table.concat(strs, ' ') +end + local function get_religion_string(religion_id, religion_type) if religion_id == -1 then return end local entity + local spheres = {} if religion_type == 0 then entity = df.historical_figure.find(religion_id) + add_spheres(entity, spheres) elseif religion_type == 1 then entity = df.historical_entity.find(religion_id) + if entity then + for _, deity in ipairs(entity.relations.deities) do + add_spheres(df.historical_figure.find(deity), spheres) + end + end end if not entity then return end - return dfhack.TranslateName(entity.name, true) + return ('%s %s'):format(dfhack.TranslateName(entity.name, true), stringify_spheres(spheres)) end local function get_profession_string(profession) From 43b0c3e10dfc44ddcd92b34698b0ca3e52ba8bd3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Oct 2023 14:33:06 -0700 Subject: [PATCH 71/76] add more detailed focus strings for the nobles info screen --- library/modules/Gui.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 728786d30..500676983 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -280,6 +280,14 @@ DEFINE_GET_FOCUS_STRING_HANDLER(dwarfmode) else newFocusString += "/Default"; break; + case df::enums::info_interface_mode_type::ADMINISTRATORS: + if (game->main_interface.info.administrators.choosing_candidate) + newFocusString += "/Candidates"; + else if (game->main_interface.info.administrators.assigning_symbol) + newFocusString += "/Symbols"; + else + newFocusString += "/Default"; + break; default: break; } From d739d9c1ef85ef9f8afcede8e8b2ac9ca08bda70 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Oct 2023 14:33:22 -0700 Subject: [PATCH 72/76] add search support for noble candidates --- docs/changelog.txt | 2 +- docs/plugins/sort.rst | 9 +++++ plugins/lua/sort.lua | 1 + plugins/lua/sort/info.lua | 83 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2163305cf..5d83dc4cf 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -57,7 +57,7 @@ Template for new versions: ## New Features - `logistics`: ``automelt`` now optionally supports melting masterworks; click on gear icon on `stockpiles` overlay frame -- `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", and the "Work details" subtab under "Labor" +- `sort`: new search widgets for Info panel tabs, including all "Creatures" subtabs, all "Objects" subtabs, "Tasks", candidate assignment on the "Noble" subtab, and the "Work details" subtab under "Labor" - `sort`: new search and filter widgets for the "Interrogate" and "Convict" screens under "Justice" - `sort`: new search widgets for location selection screen (when you're choosing what kind of guildhall or temple to dedicate) - `sort`: new search widgets for burrow assignment screen and other unit assignment dialogs diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index 0ffe5118b..4350a4cb0 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -115,6 +115,15 @@ necromancers, necromancer experiments, and intelligent undead. On the interrogations screen, you can also filter units by whether they have already been interrogated. +Candidates overlay +------------------ + +When you select the button to choose a candidate to assign to a noble role on +the nobles screen, you can search for units by name, profession, or any of the +skills in which they have achieved at least "novice" level. For example, when +assigning a broker, you can search for "appraisal" to find candidates that have +at least some appraisal skill. + Location selection overlay -------------------------- diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 2a98ff42c..58974a357 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1286,6 +1286,7 @@ OVERLAY_WIDGETS = { squad_assignment=SquadAssignmentOverlay, squad_annotation=SquadAnnotationOverlay, info=require('plugins.sort.info').InfoOverlay, + candidates=require('plugins.sort.info').CandidatesOverlay, interrogation=require('plugins.sort.info').InterrogationOverlay, location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, unit_selector=require('plugins.sort.unitselector').UnitSelectorOverlay, diff --git a/plugins/lua/sort/info.lua b/plugins/lua/sort/info.lua index 90a4e2348..1f15d643b 100644 --- a/plugins/lua/sort/info.lua +++ b/plugins/lua/sort/info.lua @@ -6,6 +6,7 @@ local widgets = require('gui.widgets') local utils = require('utils') local info = df.global.game.main_interface.info +local administrators = info.administrators local creatures = info.creatures local justice = info.justice local objects = info.artifacts @@ -135,7 +136,7 @@ local function work_details_search(vec, data, text, incremental) vec, data, text, incremental) end -local function cleanup_cri_unit(vec, data) +local function restore_allocated_data(vec, data) if not data.saved_visible or not data.saved_original then return end for _,elem in ipairs(data.saved_original) do if not utils.linear_index(data.saved_visible, elem) then @@ -144,6 +145,26 @@ local function cleanup_cri_unit(vec, data) end end +local function serialize_skills(unit) + if not unit or not unit.status or not unit.status.current_soul then + return '' + end + local skills = {} + for _, skill in ipairs(unit.status.current_soul.skills) do + if skill.rating > 0 then -- ignore dabbling + table.insert(skills, df.job_skill[skill.id]) + end + end + return table.concat(skills, ' ') +end + +local function get_candidate_search_key(cand) + if not cand.un then return end + return ('%s %s'):format( + get_unit_search_key(cand.un), + serialize_skills(cand.un)) +end + -- ---------------------- -- InfoOverlay -- @@ -186,12 +207,12 @@ function InfoOverlay:init() get_search_key_fn=get_cri_unit_search_key, get_sort_fn=get_sort }), - curry(cleanup_cri_unit, vec)) + curry(restore_allocated_data, vec)) end self:register_handler('JOBS', tasks.cri_job, curry(sortoverlay.single_vector_search, {get_search_key_fn=get_cri_unit_search_key}), - curry(cleanup_cri_unit, vec)) + curry(restore_allocated_data, tasks.cri_job)) self:register_handler('PET_OT', creatures.atk_index, curry(sortoverlay.single_vector_search, {get_search_key_fn=get_race_name})) self:register_handler('PET_AT', creatures.trainer, @@ -259,7 +280,7 @@ function InfoOverlay:updateFrames() local ret = resize_overlay(self) local l, t = get_panel_offsets() local frame = self.subviews.panel.frame - if (frame.l == l and frame.t == t) then return ret end + if frame.l == l and frame.t == t then return ret end frame.l, frame.t = l, t return true end @@ -282,6 +303,60 @@ function InfoOverlay:onInput(keys) return InfoOverlay.super.onInput(self, keys) end +-- ---------------------- +-- CandidatesOverlay +-- + +CandidatesOverlay = defclass(CandidatesOverlay, sortoverlay.SortOverlay) +CandidatesOverlay.ATTRS{ + default_pos={x=54, y=8}, + viewscreens='dwarfmode/Info/ADMINISTRATORS/Candidates', + frame={w=27, h=3}, +} + +function CandidatesOverlay:init() + self:addviews{ + widgets.BannerPanel{ + view_id='panel', + frame={l=0, t=0, r=0, h=1}, + subviews={ + widgets.EditField{ + view_id='search', + frame={l=1, t=0, r=1}, + label_text="Search: ", + key='CUSTOM_ALT_S', + on_change=function(text) self:do_search(text) end, + }, + }, + }, + } + + self:register_handler('CANDIDATE', administrators.candidate, + curry(sortoverlay.single_vector_search, {get_search_key_fn=get_candidate_search_key}), + curry(restore_allocated_data, administrators.candidate)) +end + +function CandidatesOverlay:get_key() + if administrators.choosing_candidate then + return 'CANDIDATE' + end +end + +function CandidatesOverlay:updateFrames() + local t = is_tabs_in_two_rows() and 2 or 0 + local frame = self.subviews.panel.frame + if frame.t == t then return end + frame.t = t + return true +end + +function CandidatesOverlay:onRenderBody(dc) + CandidatesOverlay.super.onRenderBody(self, dc) + if self:updateFrames() then + self:updateLayout() + end +end + -- ---------------------- -- InterrogationOverlay -- From 0323055e0d4eb77c8207e412607808450181b2f8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Oct 2023 14:48:44 -0700 Subject: [PATCH 73/76] better panel alignment on small screen sizes for worker assignment screen --- plugins/lua/sort.lua | 1 + plugins/lua/sort/unitselector.lua | 50 ++++++++++++++++++------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index 58974a357..9be0848c5 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -1290,6 +1290,7 @@ OVERLAY_WIDGETS = { interrogation=require('plugins.sort.info').InterrogationOverlay, location_selector=require('plugins.sort.locationselector').LocationSelectorOverlay, unit_selector=require('plugins.sort.unitselector').UnitSelectorOverlay, + worker_assignment=require('plugins.sort.unitselector').WorkerAssignmentOverlay, world=require('plugins.sort.world').WorldOverlay, } diff --git a/plugins/lua/sort/unitselector.lua b/plugins/lua/sort/unitselector.lua index b5423acee..a904d2f29 100644 --- a/plugins/lua/sort/unitselector.lua +++ b/plugins/lua/sort/unitselector.lua @@ -5,23 +5,6 @@ local widgets = require('gui.widgets') local unit_selector = df.global.game.main_interface.unit_selector --- pen, pit, chain, and cage assignment are handled by dedicated screens --- squad fill position screen has a specialized overlay --- we *could* add search functionality to vanilla screens for pit and cage, --- but then we'd have to handle the itemid vector -local HANDLED_SCREENS = { - ZONE_BEDROOM_ASSIGNMENT='already', - ZONE_OFFICE_ASSIGNMENT='already', - ZONE_DINING_HALL_ASSIGNMENT='already', - ZONE_TOMB_ASSIGNMENT='already', - -- this one should technically appear further to the left, but when the screen - -- gets small enough that that matters, the vanilla widgets are unreadable - WORKER_ASSIGNMENT='selected', - OCCUPATION_ASSIGNMENT='selected', - BURROW_ASSIGNMENT='selected', - SQUAD_KILL_ORDER='selected', -} - -- ---------------------- -- UnitSelectorOverlay -- @@ -30,7 +13,8 @@ UnitSelectorOverlay = defclass(UnitSelectorOverlay, sortoverlay.SortOverlay) UnitSelectorOverlay.ATTRS{ default_pos={x=62, y=6}, viewscreens='dwarfmode/UnitSelector', - frame={w=26, h=1}, + frame={w=31, h=1}, + handled_screens=DEFAULT_NIL, } local function get_unit_id_search_key(unit_id) @@ -59,7 +43,21 @@ function UnitSelectorOverlay:init() }, } - for name,flags_vec in pairs(HANDLED_SCREENS) do + -- pen, pit, chain, and cage assignment are handled by dedicated screens + -- squad fill position screen has a specialized overlay + -- we *could* add search functionality to vanilla screens for pit and cage, + -- but then we'd have to handle the itemid vector + self.handled_screens = self.handled_screens or { + ZONE_BEDROOM_ASSIGNMENT='already', + ZONE_OFFICE_ASSIGNMENT='already', + ZONE_DINING_HALL_ASSIGNMENT='already', + ZONE_TOMB_ASSIGNMENT='already', + OCCUPATION_ASSIGNMENT='selected', + BURROW_ASSIGNMENT='selected', + SQUAD_KILL_ORDER='selected', + } + + for name,flags_vec in pairs(self.handled_screens) do self:register_handler(name, unit_selector.unid, curry(sortoverlay.flags_vector_search, {get_search_key_fn=get_unit_id_search_key}, unit_selector[flags_vec])) @@ -68,7 +66,7 @@ end function UnitSelectorOverlay:get_key() local key = df.unit_selector_context_type[unit_selector.context] - if HANDLED_SCREENS[key] then + if self.handled_screens[key] then return key end end @@ -88,4 +86,16 @@ function UnitSelectorOverlay:onInput(keys) return UnitSelectorOverlay.super.onInput(self, keys) end +-- ---------------------- +-- WorkerAssignmentOverlay +-- + +WorkerAssignmentOverlay = defclass(WorkerAssignmentOverlay, UnitSelectorOverlay) +WorkerAssignmentOverlay.ATTRS{ + default_pos={x=6, y=6}, + viewscreens='dwarfmode/UnitSelector', + frame={w=31, h=1}, + handled_screens={WORKER_ASSIGNMENT='selected'}, +} + return _ENV From cd935dcdcbaf34f081a2c348c26119cc603cb136 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Oct 2023 23:30:09 -0700 Subject: [PATCH 74/76] properly initialize job id when linking into world --- docs/changelog.txt | 1 + plugins/lua/dwarfvet.lua | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 5d83dc4cf..017d61645 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -65,6 +65,7 @@ Template for new versions: ## Fixes - `zone`: don't show animal assignment link for cages and restraints linked to dungeon zones (which aren't normally assignable) +- `dwarfvet`: fix invalid job id assigned to ``Rest`` job, which could cause crashes on reload ## Misc Improvements - `overlay`: allow ``overlay_onupdate_max_freq_seconds`` to be dynamically set to 0 for a burst of high-frequency updates diff --git a/plugins/lua/dwarfvet.lua b/plugins/lua/dwarfvet.lua index a976b91c8..2bda976b7 100644 --- a/plugins/lua/dwarfvet.lua +++ b/plugins/lua/dwarfvet.lua @@ -92,13 +92,12 @@ function HospitalZone:assign_spot(unit, unit_pos) local pos = self:find_spot(unit_pos) if not pos then return false end local job = df.new(df.job) - dfhack.job.linkIntoWorld(job) + dfhack.job.linkIntoWorld(job, true) job.pos.x = pos.x job.pos.y = pos.y job.pos.z = pos.z job.flags.special = true job.job_type = df.job_type.Rest - job.wait_timer = 1600 local gref = df.new(df.general_ref_unit_workerst) gref.unit_id = unit.id job.general_refs:insert('#', gref) From cac17bd8fa0735ac8e84ba0f059230159714b4b8 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 14 Oct 2023 07:11:50 +0000 Subject: [PATCH 75/76] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 4e8ad9d87..6166bb73d 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4e8ad9d8711c4b5262b3806c4a5e563985fd8728 +Subproject commit 6166bb73dc9ae19a51780ecf026d92f2fffd277f From 87af3281fb9db399953bf77d5f7efff8822a6095 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Oct 2023 04:22:43 -0700 Subject: [PATCH 76/76] use generic baby/child names when race doesn't have something specific --- docs/changelog.txt | 1 + library/modules/Units.cpp | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 5d83dc4cf..a196691af 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ Template for new versions: - `sort`: new search widgets for artifacts on the world/raid screen ## Fixes +- `zone`: races without specific child or baby names will now get generic child/baby names instead of an empty string - `zone`: don't show animal assignment link for cages and restraints linked to dungeon zones (which aren't normally assignable) ## Misc Improvements diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 83554ef78..a9d361706 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1225,8 +1225,12 @@ string Units::getRaceBabyNameById(int32_t id) if (id >= 0 && (size_t)id < world->raws.creatures.all.size()) { df::creature_raw* raw = world->raws.creatures.all[id]; - if (raw) - return raw->general_baby_name[0]; + if (raw) { + string & baby_name = raw->general_baby_name[0]; + if (!baby_name.empty()) + return baby_name; + return getRaceReadableNameById(id) + " baby"; + } } return ""; } @@ -1242,8 +1246,12 @@ string Units::getRaceChildNameById(int32_t id) if (id >= 0 && (size_t)id < world->raws.creatures.all.size()) { df::creature_raw* raw = world->raws.creatures.all[id]; - if (raw) - return raw->general_child_name[0]; + if (raw) { + string & child_name = raw->general_child_name[0]; + if (!child_name.empty()) + return child_name; + return getRaceReadableNameById(id) + " child"; + } } return ""; } @@ -1266,7 +1274,10 @@ static string get_caste_name(df::unit* unit) { } string Units::getReadableName(df::unit* unit) { - string race_name = isChild(unit) ? getRaceChildName(unit) : get_caste_name(unit); + string race_name = isBaby(unit) ? getRaceBabyName(unit) : + (isChild(unit) ? getRaceChildName(unit) : get_caste_name(unit)); + if (race_name.empty()) + race_name = getRaceReadableName(unit); if (isHunter(unit)) race_name = "hunter " + race_name; if (isWar(unit))