Merge branch 'develop' into translate-name
commit
2d20e790b6
@ -0,0 +1,23 @@
|
|||||||
|
preserve-tombs
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. dfhack-tool::
|
||||||
|
: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
|
||||||
|
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 currently tracked tomb assignments
|
||||||
|
``preserve-tombs now``
|
||||||
|
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.
|
@ -1 +1 @@
|
|||||||
Subproject commit aeab463a0d35ac9ff896db840735cabfa12df712
|
Subproject commit a598bc6770199e9b965e00d0eade3f8400c4be9e
|
@ -0,0 +1,514 @@
|
|||||||
|
local _ENV = mkmodule('plugins.sort.info')
|
||||||
|
|
||||||
|
local gui = require('gui')
|
||||||
|
local sortoverlay = require('plugins.sort.sortoverlay')
|
||||||
|
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
|
||||||
|
local tasks = info.jobs
|
||||||
|
local work_details = info.labor.work_details
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
--
|
||||||
|
-- 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 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 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 get_trainer_search_key(unit)
|
||||||
|
if not unit then return end
|
||||||
|
return ('%s %s'):format(dfhack.TranslateName(unit.name), dfhack.units.getProfessionName(unit))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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(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
|
||||||
|
sortoverlay.single_vector_search(
|
||||||
|
{get_search_key_fn=get_unit_search_key},
|
||||||
|
vec, data, text, incremental)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
vec:insert('#', elem)
|
||||||
|
end
|
||||||
|
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
|
||||||
|
--
|
||||||
|
|
||||||
|
InfoOverlay = defclass(InfoOverlay, sortoverlay.SortOverlay)
|
||||||
|
InfoOverlay.ATTRS{
|
||||||
|
default_pos={x=64, y=8},
|
||||||
|
viewscreens='dwarfmode/Info',
|
||||||
|
frame={w=40, h=4},
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoOverlay:init()
|
||||||
|
self:addviews{
|
||||||
|
widgets.BannerPanel{
|
||||||
|
view_id='panel',
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
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(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,
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
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 = is_tabs_in_two_rows()
|
||||||
|
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 = 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
|
||||||
|
|
||||||
|
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
|
||||||
|
frame.l, frame.t = l, t
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function InfoOverlay:onRenderBody(dc)
|
||||||
|
InfoOverlay.super.onRenderBody(self, dc)
|
||||||
|
if self:updateFrames() then
|
||||||
|
self:updateLayout()
|
||||||
|
end
|
||||||
|
if self.refresh_search then
|
||||||
|
self.refresh_search = nil
|
||||||
|
self:do_search(self.subviews.search.text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InfoOverlay:onInput(keys)
|
||||||
|
if keys._MOUSE_L and self:get_key() == 'WORK_DETAILS' then
|
||||||
|
self.refresh_search = true
|
||||||
|
end
|
||||||
|
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
|
||||||
|
--
|
||||||
|
|
||||||
|
InterrogationOverlay = defclass(InterrogationOverlay, sortoverlay.SortOverlay)
|
||||||
|
InterrogationOverlay.ATTRS{
|
||||||
|
default_pos={x=47, y=10},
|
||||||
|
viewscreens='dwarfmode/Info/JUSTICE',
|
||||||
|
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,
|
||||||
|
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) self:do_search(text) end,
|
||||||
|
},
|
||||||
|
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() self:do_search(self.subviews.search.text, true) end,
|
||||||
|
},
|
||||||
|
widgets.CycleHotkeyLabel{
|
||||||
|
view_id='subset',
|
||||||
|
frame={l=0, t=2, w=28},
|
||||||
|
key='CUSTOM_SHIFT_F',
|
||||||
|
label='Show:',
|
||||||
|
options={
|
||||||
|
{label='All', value='all', pen=COLOR_GREEN},
|
||||||
|
{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_BLUE},
|
||||||
|
{label='Deceased or missing', value='deceased', pen=COLOR_MAGENTA},
|
||||||
|
{label='Others', value='others', pen=COLOR_GRAY},
|
||||||
|
},
|
||||||
|
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{
|
||||||
|
df.profession.THIEF,
|
||||||
|
df.profession.MASTER_THIEF,
|
||||||
|
df.profession.CRIMINAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function is_risky(unit)
|
||||||
|
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, flag)
|
||||||
|
if justice.interrogating then
|
||||||
|
local include_interviewed = self.subviews.include_interviewed:getOptionValue()
|
||||||
|
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)
|
||||||
|
elseif dfhack.units.isAnimal(unit) then
|
||||||
|
return subset == 'animals'
|
||||||
|
elseif dfhack.units.isCitizen(unit) then
|
||||||
|
return subset == 'citizens'
|
||||||
|
elseif unit.flags2.roaming_wilderness_population_source then
|
||||||
|
return subset == 'others'
|
||||||
|
end
|
||||||
|
return subset == 'residents'
|
||||||
|
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
|
||||||
|
|
||||||
|
return _ENV
|
@ -0,0 +1,89 @@
|
|||||||
|
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 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 ('%s %s'):format(dfhack.TranslateName(entity.name, true), stringify_spheres(spheres))
|
||||||
|
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
|
@ -0,0 +1,179 @@
|
|||||||
|
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
|
||||||
|
-- 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()
|
||||||
|
self.state = {}
|
||||||
|
self.handlers = {}
|
||||||
|
-- subclasses expected to provide an EditField widget with view_id='search'
|
||||||
|
end
|
||||||
|
|
||||||
|
function SortOverlay:register_handler(key, vec, search_fn, cleanup_fn)
|
||||||
|
self.handlers[key] = {
|
||||||
|
vec=vec,
|
||||||
|
search_fn=search_fn,
|
||||||
|
cleanup_fn=cleanup_fn
|
||||||
|
}
|
||||||
|
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
|
||||||
|
local cleanup_fn = safe_index(self.handlers, key, 'cleanup_fn')
|
||||||
|
if cleanup_fn then
|
||||||
|
cleanup_fn(data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
self:do_search(self.subviews.search.text, true)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
vec:resize(data.saved_visible_size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
vec:erase(idx)
|
||||||
|
flags_vec:erase(idx)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return _ENV
|
@ -0,0 +1,101 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
-- ----------------------
|
||||||
|
-- UnitSelectorOverlay
|
||||||
|
--
|
||||||
|
|
||||||
|
UnitSelectorOverlay = defclass(UnitSelectorOverlay, sortoverlay.SortOverlay)
|
||||||
|
UnitSelectorOverlay.ATTRS{
|
||||||
|
default_pos={x=62, y=6},
|
||||||
|
viewscreens='dwarfmode/UnitSelector',
|
||||||
|
frame={w=31, h=1},
|
||||||
|
handled_screens=DEFAULT_NIL,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 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]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function UnitSelectorOverlay:get_key()
|
||||||
|
local key = df.unit_selector_context_type[unit_selector.context]
|
||||||
|
if self.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
|
||||||
|
|
||||||
|
-- ----------------------
|
||||||
|
-- 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
|
@ -0,0 +1,88 @@
|
|||||||
|
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)
|
||||||
|
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()
|
||||||
|
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
|
@ -0,0 +1,287 @@
|
|||||||
|
#include "Debug.h"
|
||||||
|
#include "PluginManager.h"
|
||||||
|
#include "MiscUtils.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "modules/Units.h"
|
||||||
|
#include "modules/Buildings.h"
|
||||||
|
#include "modules/Persistence.h"
|
||||||
|
#include "modules/EventManager.h"
|
||||||
|
#include "modules/World.h"
|
||||||
|
#include "modules/Translation.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;
|
||||||
|
|
||||||
|
|
||||||
|
// <BOILERPLATE>
|
||||||
|
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 constexpr int32_t cycle_freq = 100;
|
||||||
|
|
||||||
|
enum ConfigValues {
|
||||||
|
CONFIG_IS_ENABLED = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::unordered_map<int32_t, int32_t> tomb_assignments;
|
||||||
|
|
||||||
|
namespace DFHack {
|
||||||
|
DBG_DECLARE(preservetombs, config, DebugCategory::LINFO);
|
||||||
|
DBG_DECLARE(preservetombs, cycle, DebugCategory::LINFO);
|
||||||
|
DBG_DECLARE(preservetombs, event, 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);
|
||||||
|
static command_result do_command(color_ostream& out, std::vector<std::string>& params);
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
|
||||||
|
commands.push_back(PluginCommand(
|
||||||
|
plugin_name,
|
||||||
|
"Preserve tomb assignments when assigned units die.",
|
||||||
|
do_command));
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static command_result do_command(color_ostream& out, std::vector<std::string>& params) {
|
||||||
|
if (!Core::getInstance().isWorldLoaded()) {
|
||||||
|
out.printerr("Cannot use %s without a loaded world.\n", plugin_name);
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
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("tracked tomb assignments:\n");
|
||||||
|
std::for_each(tomb_assignments.begin(), tomb_assignments.end(), [&out](const auto& p){
|
||||||
|
auto& [unit_id, building_id] = p;
|
||||||
|
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] == "now") {
|
||||||
|
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");
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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 {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// PluginManager handles unregistering our handler from EventManager,
|
||||||
|
// so we don't have to do that here
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
is_enabled = get_config_bool(config, CONFIG_IS_ENABLED);
|
||||||
|
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) {
|
||||||
|
tomb_assignments.clear();
|
||||||
|
if (is_enabled) {
|
||||||
|
DEBUG(config,out).print("world unloaded; disabling %s\n",
|
||||||
|
plugin_name);
|
||||||
|
is_enabled = false;
|
||||||
|
}
|
||||||
|
EventManager::unregisterAll(plugin_self);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// </BOILERPLATE>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 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<std::intptr_t>(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)) {
|
||||||
|
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 back to their tomb\n", unit_id);
|
||||||
|
tomb_assignments.erase(it);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Update tomb assignments
|
||||||
|
//
|
||||||
|
//
|
||||||
|
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) {
|
||||||
|
|
||||||
|
auto* tomb = virtual_cast<df::building_civzonest>(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);
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto tomb = virtual_cast<df::building_civzonest>(world->buildings.other.ZONE_TOMB[tomb_idx]);
|
||||||
|
if (!tomb || !tomb->flags.bits.exists) {
|
||||||
|
DEBUG(cycle, out).print("%s tomb missing: %d - removing\n", plugin_name, building_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tomb->assigned_unit_id != unit_id) {
|
||||||
|
DEBUG(cycle, out).print("%s unit %d unassigned from tomb %d - removing\n", plugin_name, 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) {
|
||||||
|
|
||||||
|
df::unit* unit = df::unit::find(unit_id);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
df::building_civzonest* tomb = virtual_cast<df::building_civzonest>(world->buildings.other.ZONE_TOMB[tomb_idx]);
|
||||||
|
if (!tomb || tomb->assigned_unit) return false;
|
||||||
|
|
||||||
|
Buildings::setOwner(tomb, unit);
|
||||||
|
return true;
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit 28bcd6e313ea6f87ffd805c8cf40360da5f21509
|
Subproject commit 6166bb73dc9ae19a51780ecf026d92f2fffd277f
|
Loading…
Reference in New Issue