From 673287d0a4bbf15940e29dcd3d53a4102e6013a1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Oct 2023 18:40:47 -0700 Subject: [PATCH] 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