From a67e1e30c51ed1c577cb7f247b7bbca384744537 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 29 Dec 2024 16:29:57 -0800 Subject: [PATCH 01/13] prep for initial version of gui/rename --- docs/gui/rename.rst | 78 +++++++++++--- gui/rename.lua | 258 +++++++++++++++++++++++++------------------- 2 files changed, 212 insertions(+), 124 deletions(-) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index 5688354bb7..d72255d0ac 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -2,28 +2,74 @@ gui/rename ========== .. dfhack-tool:: - :summary: Give buildings and units new names, optionally with special chars. - :tags: unavailable + :summary: Modify the name of anything that is nameable. + :tags: adventure fort productivity animals items units -Once you select a target on the game map, this tool allows you to rename it. It -is more powerful than the in-game rename functionality since it allows you to -use special characters (like diamond symbols), and it also allows you to rename -enemies and overwrite animal species strings. - -This tool supports renaming units, zones, stockpiles, workshops, furnaces, -traps, and siege engines. +Once you select a target (by clicking on the game map, by passing a commandline +parameter, or by using the provided selection widget) this tool allows you +change its language name, generate a new random name, or rename it with your +preferred component words. It provides an interface similar to the in-game +naming panel that you can use to customize your fortress name at embark. That +is, it allows you to choose words from an in-game language to assemble a name, +just like the default names that the game generates. You will be able to assign +units new given and last names. You can also use this tool to set freeform +"nicknames" for targets that support it. Usage ----- +:: + + gui/rename [] + +Examples +-------- + ``gui/rename`` - Renames the selected building, zone, or unit. -``gui/rename unit-profession`` - Set the unit profession or the animal species string. + Load the selected artifact, location, or unit for renaming. If nothing is + selected, you can select a target from a list. +``gui/rename -u 123 --no-target-selector`` + Load the unit with id ``123`` for renaming and remove the widget that + allows selecting a different target. +``gui/rename --location 2 --site 456`` + Load the location with "abstract building" ID ``2`` attached to the site + with id ``456`` for renaming. + +Options +------- + +``-a``, ``--artifact `` + Rename the artifact with the given item ID. +``-e``, ``--entity `` + Rename the historical entity (e.g. site government, world religion, etc) + with the given ID. +``-f``, ``--histfig `` + Rename the historical figure with the given ID. +``-l``, ``--location `` + Rename the location (e.g. tavern, hospital, guildhall, temple) with the + given ID. If this option is used, ``--site`` can be specified to indicate + locations attached to a specific site. If ``--site`` is not specified, the + location will be loaded from the current site. +``-q``, ``--squad `` + Rename the squad with the given ID. +``-s``, ``--site `` + Rename the site with the given ID. +``-u``, ``--unit `` + Rename the unit with the given ID. Renaming a unit also renames the + associated historical figure. +``-w``, ``--world`` + Rename the current world. +``--no-target-selector`` + Do not allow the player to switch naming targets. An option that sets the + initial target is required when using this option. -Screenshots ------------ +Overlays +-------- -.. image:: /docs/images/rename-bld.png +This tool supports the following overlays: -.. image:: /docs/images/rename-prof.png +``gui/rename.embark`` + Adds widgets to the embark preparation screen for renaming the starting + dwarves. +``gui/rename.world`` + Adds a widget to the world generation screen for renaming the world. diff --git a/gui/rename.lua b/gui/rename.lua index 509b61ba0d..1a05cbe87b 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1,127 +1,169 @@ --- Rename various objects via gui. ---[====[ +local argparse = require('argparse') +local gui = require('gui') +local utils = require('utils') +local widgets = require('gui.widgets') + +-- +-- Rename +-- + +Rename = defclass(Rename, widgets.Window) +Rename.ATTRS { + frame_title='Rename', + frame={w=87, h=30}, + resizable=true, + resize_mid={w=50, h=20}, +} + +function Rename:init(info) + self.target = info.target + self.sync_targets = info.sync_targets or {} -gui/rename -========== -Backed by `rename`, this script allows entering the desired name -via a simple dialog in the game ui. - -* ``gui/rename [building]`` in :kbd:`q` mode changes the name of a building. - - .. image:: /docs/images/rename-bld.png - - The selected building must be one of stockpile, workshop, furnace, trap, or siege engine. - It is also possible to rename zones from the :kbd:`i` menu. - -* ``gui/rename [unit]`` with a unit selected changes the nickname. - - Unlike the built-in interface, this works even on enemies and animals. + self:addviews{ + widgets.Label{ + text={ + self.target and dfhack.TranslateName(self.target) or 'No target', NEWLINE, + self.target and dfhack.TranslateName(self.target, true), + }, + auto_width=true, + }, + } +end -* ``gui/rename unit-profession`` changes the selected unit's custom profession name. +-- +-- RenameScreen +-- - .. image:: /docs/images/rename-prof.png +RenameScreen = defclass(RenameScreen, gui.ZScreen) +RenameScreen.ATTRS { + focus_path='rename', +} - Likewise, this can be applied to any unit, and when used on animals it overrides - their species string. +function RenameScreen:init(info) + self:addviews{ + Rename{ + target=info.target, + sync_targets=info.sync_targets, + show_selector=info.show_selector, + } + } +end -The ``building`` or ``unit`` options are automatically assumed when in relevant UI state. +function RenameScreen:onDismiss() + view = nil +end -]====] -local gui = require 'gui' -local dlg = require 'gui.dialogs' -local widgets = require 'gui.widgets' -local plugin = require 'plugins.rename' +-- +-- CLI +-- -local mode = ... -local focus = dfhack.gui.getCurFocus() +if not dfhack.isWorldLoaded() then + qerror('This script requires a world to be loaded') +end -RenameDialog = defclass(RenameDialog, dlg.InputBox) -function RenameDialog:init(info) - self:addviews{ - widgets.Label{ - view_id = 'controls', - text = { - {key = 'CUSTOM_ALT_C', text = ': Clear, ', - on_activate = function() - self.subviews.edit.text = '' - end}, - {key = 'CUSTOM_ALT_S', text = ': Special chars', - on_activate = curry(dfhack.run_script, 'gui/cp437-table')}, - }, - frame = {b = 0, l = 0, r = 0, w = 70}, - } - } - -- calculate text_width once - self.subviews.controls:getTextWidth() +local function get_artifact_target(item) + if not item or not item.flags.artifact then return end + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local rec = df.artifact_record.find(gref.artifact_id) + if not rec then return end + return rec.name end -function RenameDialog:getWantedFrameSize() - local x, y = self.super.getWantedFrameSize(self) - x = math.max(x, self.subviews.controls.text_width) - return x, y + 2 +local function get_unit_target(unit, sync_targets) + if not unit then return end + local hf = df.historical_figure.find(unit.hist_figure_id) + if hf then table.insert(sync_targets, hf.name) end + return unit.name end -function showRenameDialog(title, text, input, on_input) - RenameDialog{ - frame_title = title, - text = text, - text_pen = COLOR_GREEN, - input = input, - on_input = on_input, - }:show() +local function get_location_target(site, loc_id) + if not site or loc_id < 0 then return end + local loc = utils.binsearch(site.buildings, loc_id, 'id') + if not loc then return end + return loc.name end -local function verify_mode(expected) - if mode ~= nil and mode ~= expected then - qerror('Invalid UI state for mode '..mode) +local function get_target(opts) + local target, sync_targets = nil, {} + if opts.histfig_id then + local hf = df.historical_figure.find(opts.histfig_id) + if not hf then qerror('Historical figure not found') end + target = hf.name + local unit = df.unit.find(hf.unit_id) + if unit then table.insert(sync_targets, unit.name) end + elseif opts.item_id then + target = get_artifact_target(df.item.find(opts.item_id)) + if not target then qerror('Artifact not found') end + elseif opts.location_id then + local site = opts.site_id and df.world_site.find(opts.site_id) or dfhack.world.getCurrentSite() + if not site then qerror('Site not found') end + target = get_location_target(site, opts.location_id) + if not target then qerror('Location not found') end + elseif opts.site_id then + local site = df.world_site.find(opts.site_id) + if not site then qerror('Site not found') end + target = site.name + elseif opts.squad_id then + local squad = df.squad.find(opts.squad_id) + if not squad then qerror('Squad not found') end + target = squad.name + elseif opts.unit_id then + target = get_unit_target(df.unit.find(opts.unit_id), sync_targets) + if not target then qerror('Unit not found') end + elseif opts.world then + target = df.global.world.world_data.name end + return target, sync_targets end -local unit = dfhack.gui.getSelectedUnit(true) -local building = dfhack.gui.getSelectedBuilding(true) - -if building and (not unit or mode == 'building') then - verify_mode('building') - - if plugin.canRenameBuilding(building) then - showRenameDialog( - 'Rename Building', - 'Enter a new name for the building:', - building.name, - curry(plugin.renameBuilding, building) - ) - else - dlg.showMessage( - 'Rename Building', - 'Cannot rename this type of building.', COLOR_LIGHTRED - ) - end -elseif unit then - if mode == 'unit-profession' then - showRenameDialog( - 'Rename Unit', - 'Enter a new profession for the unit:', - unit.custom_profession, - function(newval) - unit.custom_profession = newval - end - ) - else - verify_mode('unit') - - local vname = dfhack.units.getVisibleName(unit) - local vnick = '' - if vname and vname.has_name then - vnick = vname.nickname - end - - showRenameDialog( - 'Rename Unit', - 'Enter a new nickname for the unit:', - vnick, - curry(dfhack.units.setNickname, unit) - ) +local opts = { + help=false, + entity_id=nil, + histfig_id=nil, + item_id=nil, + location_id=nil, + site_id=nil, + squad_id=nil, + unit_id=nil, + world=false, + show_selector=true, +} +local positionals = argparse.processArgsGetopt({...}, { + { 'a', 'artifact', handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, + { 'e', 'entity', handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, + { 'f', 'histfig', handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, + { 'h', 'help', handler = function() opts.help = true end }, + { 'l', 'location', handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, + { 'q', 'squad', handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, + { 's', 'site', handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, + { 'u', 'unit', handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, + { 'w', 'world', handler=function() opts.world = true end }, + { '', 'no-target-selector', handler=function() opts.show_selector = false end }, +}) + +if opts.help or positionals[1] == 'help' then + print(dfhack.script_help()) + return +end + +local target, sync_targets = get_target(opts) + +if not target then + local unit = dfhack.gui.getSelectedUnit(true) + local item = dfhack.gui.getSelectedItem(true) + local zone = dfhack.gui.getSelectedCivZone(true) + if unit then + target = get_unit_target(unit, sync_targets) + elseif item then + target = get_artifact_target(item) + elseif zone then + target = get_location_target(df.world_site.find(zone.site_id), zone.location_id) end -elseif mode then - verify_mode(nil) end + +view = view and view:raise() or RenameScreen{ + target=target, + sync_targets=sync_targets, + show_selector=opts.show_selector +}:show() From 0654d7ebd0d32687662906b22592033afe195d28 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 31 Dec 2024 18:42:28 -0800 Subject: [PATCH 02/13] in progress --- gui/rename.lua | 274 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 261 insertions(+), 13 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 1a05cbe87b..2d57cfa999 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -3,6 +3,18 @@ local gui = require('gui') local utils = require('utils') local widgets = require('gui.widgets') +local CH_UP = string.char(30) +local CH_DN = string.char(31) +local ENGLISH_COL_WIDTH = 16 +local NATIVE_COL_WIDTH = 16 + +-- +-- target selection +-- + +local function select_new_target() +end + -- -- Rename -- @@ -12,22 +24,242 @@ Rename.ATTRS { frame_title='Rename', frame={w=87, h=30}, resizable=true, - resize_mid={w=50, h=20}, + resize_min={w=77, h=30}, } +local function get_language_options() + local options, max_width = {}, 5 + for idx, lang in ipairs(df.language_translation.get_vector()) do + max_width = math.max(max_width, #lang.name) + table.insert(options, {label=dfhack.capitalizeStringWords(dfhack.lowerCp437(lang.name)), value=idx, pen=COLOR_CYAN}) + end + return options, max_width +end + +local function pad_text(text, width) + return (' '):rep((width - #text)//2) .. text +end + +local function sort_by_english_desc(a, b) +end + +local function sort_by_english_asc(a, b) +end + +local function sort_by_native_desc(a, b) +end + +local function sort_by_native_asc(a, b) +end + +local function sort_by_part_of_speech_desc(a, b) +end + +local function sort_by_part_of_speech_asc(a, b) +end + function Rename:init(info) self.target = info.target self.sync_targets = info.sync_targets or {} + self.cache = {} + + local language_options, max_lang_name_width = get_language_options() self:addviews{ - widgets.Label{ - text={ - self.target and dfhack.TranslateName(self.target) or 'No target', NEWLINE, - self.target and dfhack.TranslateName(self.target, true), + widgets.Panel{frame={t=0, h=7}, -- header + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + key='CUSTOM_CTRL_N', + label='Select new target', + on_activate=function() + local target, sync_targets = select_new_target() + if target then + self.target, self.sync_targets = target, sync_targets + self.subviews.language:setOption(self.target.language) + end + end, + visible=info.show_selector, + }, + widgets.HotkeyLabel{ + frame={t=0, r=0}, + label='Generate random name', + key='CUSTOM_CTRL_G', + on_activate=function() end, + auto_width=true, + }, + widgets.Label{ + frame={t=2}, + text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.TranslateName(self.target), self.frame_body.width) end}}, + }, + widgets.Label{ + frame={t=3}, + text={{pen=COLOR_LIGHTCYAN, text=function() return pad_text(('"%s"'):format(dfhack.TranslateName(self.target, true)), self.frame_body.width) end}}, + }, + widgets.CycleHotkeyLabel{ + view_id='language', + frame={t=5, l=0, w=max_lang_name_width + 18}, + key='CUSTOM_CTRL_T', + label='Language:', + options=language_options, + initial_option=self.target and self.target.language or 0, + on_change=function(val) + self.target.language = val + for _, sync_target in ipairs(self.sync_targets) do + sync_target.language = val + end + end, + }, + widgets.Label{ + frame={t=6, l=7}, + text={'Name type: ', {pen=COLOR_CYAN, text=function() return df.language_name_type[self.target.type] end}}, + }, + }, + }, + widgets.Panel{frame={t=8}, -- body + subviews={ + widgets.Panel{frame={t=0, h=1}, -- toolbar + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={t=0, l=0, w=32}, + label='Sort by:', + key='CUSTOM_CTRL_O', + options={ + {label='English'..CH_DN, value=sort_by_english_desc}, + {label='English'..CH_UP, value=sort_by_english_asc}, + {label='native'..CH_DN, value=sort_by_native_desc}, + {label='native'..CH_UP, value=sort_by_native_asc}, + {label='part of speech'..CH_DN, value=sort_by_part_of_speech_desc}, + {label='part of speech'..CH_UP, value=sort_by_part_of_speech_asc}, + }, + initial_option=sort_by_english_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={t=0, l=35}, + label_text='Search: ', + ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + }, + }, + }, + widgets.Panel{frame={t=2, l=0, w=30}, -- component selector + subviews={ + widgets.List{frame={t=0, l=0, b=2}, + view_id='component_list', + on_select=function(idx, choice) print('component choice', idx) printall_recurse(choice) end, + choices=self:get_component_choices(), + row_height=2, + scroll_keys={ + SECONDSCROLL_UP = -1, + SECONDSCROLL_DOWN = 1, + }, + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SECONDSCROLL_UP', + label='Prev component', + on_activate=function() self.subviews.component_list:moveCursor(-1) end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='SECONDSCROLL_DOWN', + label='Next component', + on_activate=function() self.subviews.component_list:moveCursor(1) end, + }, + }, + }, + widgets.Panel{frame={t=2, l=30}, -- words table + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_english', + frame={t=0, l=0, w=8}, + options={ + {label='English', value=DEFAULT_NIL}, + {label='English'..CH_DN, value=sort_by_english_desc}, + {label='English'..CH_UP, value=sort_by_english_asc}, + }, + initial_option=sort_by_english_desc, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_english'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_native', + frame={t=0, l=ENGLISH_COL_WIDTH+2, w=7}, + options={ + {label='native', value=DEFAULT_NIL}, + {label='native'..CH_DN, value=sort_by_native_desc}, + {label='native'..CH_UP, value=sort_by_native_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_native'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_part_of_speech', + frame={t=0, l=ENGLISH_COL_WIDTH+2+NATIVE_COL_WIDTH+2, w=15}, + options={ + {label='part of speech', value=DEFAULT_NIL}, + {label='part_of_speech'..CH_DN, value=sort_by_part_of_speech_desc}, + {label='part_of_speech'..CH_UP, value=sort_by_part_of_speech_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_part_of_speech'), + }, + widgets.FilteredList{ + view_id='list', + frame={t=2, l=0, b=0, r=0}, + on_submit=function() end, + }, + }, + }, }, - auto_width=true, }, } + + -- replace the FilteredList's built-in EditField with our own + self.subviews.list.list.frame.t = 0 + self.subviews.list.edit.visible = false + self.subviews.list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + + self:refresh_list() +end + +function Rename:get_component_choices() + local choices = {} + for val, comp in ipairs(df.language_name_component) do + local text = { + {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE + {text=function() + local word = self.target.words[val] + if word < 0 then return end + return ('word: %s'):format(df.global.world.raws.language.words[word].forms.Noun) + end} + } + table.insert(choices, {text=text, data={val=val}}) + end + return choices +end + +function Rename:get_word_choices() + --if self.cache[] + local translations = df.language_translation.get_vector() + local choices = {} + for idx, word in ipairs(world.raws.language.words) do + table.insert(choices, { + text={ + {text=function() return word.forms.Noun end, width=ENGLISH_COL_WIDTH}, + {gap=2, text=function() return translations[self.subviews.language:getOptionValue()].words[idx].value end, width=NATIVE_COL_WIDTH}, + {text=df.language_part_of_speech[word.part_of_speech], width=15}, + }, + search_key=function() end, + }) + end + return choices +end + +function Rename:refresh_list() end -- @@ -72,9 +304,28 @@ end local function get_unit_target(unit, sync_targets) if not unit then return end + local target = dfhack.units.getVisibleName(unit) local hf = df.historical_figure.find(unit.hist_figure_id) - if hf then table.insert(sync_targets, hf.name) end - return unit.name + if hf then + local hf_name = dfhack.units.getVisibleName(hf) + if hf_name ~= target then + table.insert(sync_targets, hf_name) + end + end + return target +end + +local function get_hf_target(hf, sync_targets) + if not hf then return end + local target = dfhack.units.getVisibleName(hf) + local unit = df.unit.find(hf.unit_id) + if unit then + local unit_name = dfhack.units.getVisibleName(unit) + if unit_name ~= target then + table.insert(sync_targets, unit_name) + end + end + return target end local function get_location_target(site, loc_id) @@ -87,11 +338,8 @@ end local function get_target(opts) local target, sync_targets = nil, {} if opts.histfig_id then - local hf = df.historical_figure.find(opts.histfig_id) - if not hf then qerror('Historical figure not found') end - target = hf.name - local unit = df.unit.find(hf.unit_id) - if unit then table.insert(sync_targets, unit.name) end + target = get_hf_target(df.historical_figure.find(opts.histfig_id), sync_targets) + if not target then qerror('Historical figure not found') end elseif opts.item_id then target = get_artifact_target(df.item.find(opts.item_id)) if not target then qerror('Artifact not found') end From a27c018bb292dbcc5382518aaed9f99bb2306f52 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Jan 2025 19:27:25 -0800 Subject: [PATCH 03/13] implement word loading and editing --- gui/rename.lua | 319 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 269 insertions(+), 50 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 2d57cfa999..58ac9d0efc 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -8,11 +8,54 @@ local CH_DN = string.char(31) local ENGLISH_COL_WIDTH = 16 local NATIVE_COL_WIDTH = 16 +local part_of_speech_to_display = { + [df.part_of_speech.Noun] = 'Singular Noun', + [df.part_of_speech.NounPlural] = 'Plural Noun', + [df.part_of_speech.Adjective] = 'Adjective', + [df.part_of_speech.Prefix] = 'Prefix', + [df.part_of_speech.Verb] = 'Present (1st)', + [df.part_of_speech.Verb3rdPerson] = 'Present (3rd)', + [df.part_of_speech.VerbPast] = 'Preterite', + [df.part_of_speech.VerbPassive] = 'Past Participle', + [df.part_of_speech.VerbGerund] = 'Present Participle', +} + +local langauge_name_type_to_category = { + [df.language_name_type.Figure] = {df.language_name_category.Unit}, + [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, + [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, + [df.language_name_type.Squad] = {df.language_name_category.Battle}, + [df.language_name_type.Site] = {df.language_name_category.Keep}, + [df.language_name_type.World] = {df.language_name_category.Region}, + [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, + [df.language_name_type.Temple] = {df.language_name_category.Temple}, + [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, + [df.language_name_type.Library] = {df.language_name_category.Library}, + [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, + [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, +} + +local language_name_component_to_word_table_index = { + [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, + [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, + [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, + [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, + [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, + +} + +local language = df.global.world.raws.language +local translations = df.language_translation.get_vector() + -- -- target selection -- local function select_new_target() + local target, sync_targets = nil, {} + return target, sync_targets end -- @@ -22,14 +65,14 @@ end Rename = defclass(Rename, widgets.Window) Rename.ATTRS { frame_title='Rename', - frame={w=87, h=30}, + frame={w=88, h=31}, resizable=true, - resize_min={w=77, h=30}, + resize_min={w=70, h=31}, } local function get_language_options() local options, max_width = {}, 5 - for idx, lang in ipairs(df.language_translation.get_vector()) do + for idx, lang in ipairs(translations) do max_width = math.max(max_width, #lang.name) table.insert(options, {label=dfhack.capitalizeStringWords(dfhack.lowerCp437(lang.name)), value=idx, pen=COLOR_CYAN}) end @@ -41,21 +84,69 @@ local function pad_text(text, width) end local function sort_by_english_desc(a, b) + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native < b_native + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_english_asc(a, b) + if a.data.english ~= b.data.english then + return a.data.english > b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native < b_native + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_native_desc(a, b) + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native < b_native + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_native_asc(a, b) + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native > b_native + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_part_of_speech_desc(a, b) + if a.data.part_of_speech ~= b.data.part_of_speech then + return a.data.part_of_speech < b.data.part_of_speech + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + return a_native < b_native end local function sort_by_part_of_speech_asc(a, b) + if a.data.part_of_speech ~= b.data.part_of_speech then + return a.data.part_of_speech > b.data.part_of_speech + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + return a_native < b_native end function Rename:init(info) @@ -85,7 +176,7 @@ function Rename:init(info) frame={t=0, r=0}, label='Generate random name', key='CUSTOM_CTRL_G', - on_activate=function() end, + on_activate=self:callback('generate_random_name'), auto_width=true, }, widgets.Label{ @@ -146,15 +237,20 @@ function Rename:init(info) }, widgets.Panel{frame={t=2, l=0, w=30}, -- component selector subviews={ - widgets.List{frame={t=0, l=0, b=2}, + widgets.List{ + frame={t=0, l=0, b=2, w=ENGLISH_COL_WIDTH+2}, view_id='component_list', - on_select=function(idx, choice) print('component choice', idx) printall_recurse(choice) end, + on_select=function() if self.subviews.component_list then self:refresh_list() end end, choices=self:get_component_choices(), row_height=2, - scroll_keys={ - SECONDSCROLL_UP = -1, - SECONDSCROLL_DOWN = 1, - }, + scroll_keys={}, + }, + widgets.List{ + frame={t=0, l=ENGLISH_COL_WIDTH+4, b=3}, + on_submit=function(_, choice) choice.data.fn() end, + choices=self:get_component_action_choices(), + cursor_pen=COLOR_CYAN, + scroll_keys={}, }, widgets.HotkeyLabel{ frame={b=1, l=0}, @@ -207,9 +303,9 @@ function Rename:init(info) on_change=self:callback('refresh_list', 'sort_part_of_speech'), }, widgets.FilteredList{ - view_id='list', + view_id='words_list', frame={t=2, l=0, b=0, r=0}, - on_submit=function() end, + on_submit=self:callback('set_component_word'), }, }, }, @@ -218,10 +314,10 @@ function Rename:init(info) } -- replace the FilteredList's built-in EditField with our own - self.subviews.list.list.frame.t = 0 - self.subviews.list.edit.visible = false - self.subviews.list.edit = self.subviews.search - self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + self.subviews.words_list.list.frame.t = 0 + self.subviews.words_list.edit.visible = false + self.subviews.words_list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.words_list:callback('onFilterChange') self:refresh_list() end @@ -230,36 +326,161 @@ function Rename:get_component_choices() local choices = {} for val, comp in ipairs(df.language_name_component) do local text = { - {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE - {text=function() + {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE, + {gap=2, pen=COLOR_YELLOW, text=function() local word = self.target.words[val] if word < 0 then return end - return ('word: %s'):format(df.global.world.raws.language.words[word].forms.Noun) - end} + return ('%s'):format(language.words[word].forms[self.target.parts_of_speech[val]]) + end}, } table.insert(choices, {text=text, data={val=val}}) end return choices end -function Rename:get_word_choices() - --if self.cache[] - local translations = df.language_translation.get_vector() +function Rename:get_component_action_choices() local choices = {} - for idx, word in ipairs(world.raws.language.words) do - table.insert(choices, { - text={ - {text=function() return word.forms.Noun end, width=ENGLISH_COL_WIDTH}, - {gap=2, text=function() return translations[self.subviews.language:getOptionValue()].words[idx].value end, width=NATIVE_COL_WIDTH}, - {text=df.language_part_of_speech[word.part_of_speech], width=15}, - }, - search_key=function() end, - }) + for val, comp in ipairs(df.language_name_component) do + local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} + local randomize_fn = self:callback('randomize_component_word', comp) + table.insert(choices, {text=randomize_text, data={fn=randomize_fn}}) + local clear_text = { + {text=function() return self.target.words[val] >= 0 and '[' or '' end, pen=COLOR_RED}, + {text=function() return self.target.words[val] >= 0 and 'Clear' or '' end }, + {text=function() return self.target.words[val] >= 0 and ']' or '' end, pen=COLOR_RED} + } + local clear_fn = self:callback('clear_component_word', comp) + table.insert(choices, {text=clear_text, data={fn=clear_fn}}) end return choices end -function Rename:refresh_list() +function Rename:clear_component_word(comp) + self.target.words[comp] = -1 + for _, sync_target in ipairs(self.sync_targets) do + sync_target.words[comp] = -1 + end +end + +function Rename:set_component_word(_, choice) + local _, comp_choice = self.subviews.component_list:getSelected() + self.target.words[comp_choice.data.val] = choice.data.idx + self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + for _, sync_target in ipairs(self.sync_targets) do + sync_target.words[comp_choice.data.val] = choice.data.idx + sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + end +end + +function Rename:randomize_component_word(comp) + local categories = langauge_name_type_to_category[self.target.type] + local category = categories[math.random(#categories)] + local word_table = language.word_table[0][category] + local words = word_table.words[comp] + local idx = math.random(#words)-1 + self.target.words[comp] = words[idx] + self.target.parts_of_speech[comp] = word_table.parts[comp][idx] + for _, sync_target in ipairs(self.sync_targets) do + sync_target.words[comp] = words[idx] + sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] + end +end + +function Rename:generate_random_name() + print('TODO: generate_random_name') +end + +function Rename:add_word_choice(choices, comp, idx, word, part_of_speech) + local english = word.forms[part_of_speech] + if #english == 0 then return end + local function get_native() + return translations[self.subviews.language:getOptionValue()].words[idx].value + end + local part = part_of_speech_to_display[part_of_speech] + local function get_pen() + if idx == self.target.words[comp] and part_of_speech == self.target.parts_of_speech[comp] then + return COLOR_YELLOW + end + end + table.insert(choices, { + text={ + {text=english, width=ENGLISH_COL_WIDTH, pen=get_pen}, + {gap=2, text=get_native, width=NATIVE_COL_WIDTH, pen=get_pen}, + {gap=2, text=part, width=15, pen=get_pen}, + }, + search_key=function() return ('%s %s %s'):format(english, get_native(), part) end, + data={idx=idx, english=english, native_fn=get_native, part_of_speech=part_of_speech}, + }) +end + +function Rename:get_word_choices(comp) + if self.cache[comp] then + return self.cache[comp] + end + + local choices = {} + for idx, word in ipairs(language.words) do + local flags = word.flags + if comp == df.language_name_component.FrontCompound then + if flags.front_compound_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.front_compound_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.front_compound_adj then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) end + if flags.front_compound_prefix then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Prefix) end + if flags.standard_verb then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Verb) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbPassive) + end + elseif comp == df.language_name_component.RearCompound then + if flags.rear_compound_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.rear_compound_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.rear_compound_adj then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) end + if flags.standard_verb then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Verb) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Verb3rdPerson) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbPast) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbPassive) + end + elseif comp == df.language_name_component.FirstAdjective or comp == df.language_name_component.SecondAdjective then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) + elseif comp == df.language_name_component.HyphenCompound then + if flags.the_compound_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.the_compound_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.the_compound_adj then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) end + if flags.the_compound_prefix then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Prefix) end + elseif comp == df.language_name_component.TheX then + if flags.the_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.the_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + elseif comp == df.language_name_component.OfX then + if flags.of_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.of_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.standard_verb then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbGerund) + end + end + end + + self.cache[comp] = choices + return choices +end + +function Rename:refresh_list(sort_widget) + sort_widget = sort_widget or 'sort' + sort_fn = self.subviews.sort:getOptionValue() + if sort_fn == DEFAULT_NIL then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs{'sort', 'sort_english', 'sort_native', 'sort_part_of_speech'} do + self.subviews[widget_name]:setOption(sort_fn) + end + local list = self.subviews.words_list + local saved_filter = list:getFilter() + list:setFilter('') + local _, comp_choice = self.subviews.component_list:getSelected() + local choices = self:get_word_choices(comp_choice.data.val) + table.sort(choices, self.subviews.sort:getOptionValue()) + list:setChoices(choices) + list:setFilter(saved_filter) end -- @@ -302,30 +523,23 @@ local function get_artifact_target(item) return rec.name end -local function get_unit_target(unit, sync_targets) - if not unit then return end - local target = dfhack.units.getVisibleName(unit) - local hf = df.historical_figure.find(unit.hist_figure_id) - if hf then - local hf_name = dfhack.units.getVisibleName(hf) - if hf_name ~= target then - table.insert(sync_targets, hf_name) - end - end - return target -end - -local function get_hf_target(hf, sync_targets) +local function get_hf_target(hf) if not hf then return end local target = dfhack.units.getVisibleName(hf) local unit = df.unit.find(hf.unit_id) + local sync_targets = {} if unit then local unit_name = dfhack.units.getVisibleName(unit) if unit_name ~= target then table.insert(sync_targets, unit_name) end end - return target + return target, sync_targets +end + +local function get_unit_target(unit) + if not unit then return end + return get_hf_target(df.historical_figure.find(unit.hist_figure_id)) end local function get_location_target(site, loc_id) @@ -338,7 +552,7 @@ end local function get_target(opts) local target, sync_targets = nil, {} if opts.histfig_id then - target = get_hf_target(df.historical_figure.find(opts.histfig_id), sync_targets) + target, sync_targets = get_hf_target(df.historical_figure.find(opts.histfig_id)) if not target then qerror('Historical figure not found') end elseif opts.item_id then target = get_artifact_target(df.item.find(opts.item_id)) @@ -357,7 +571,7 @@ local function get_target(opts) if not squad then qerror('Squad not found') end target = squad.name elseif opts.unit_id then - target = get_unit_target(df.unit.find(opts.unit_id), sync_targets) + target, sync_targets = get_unit_target(df.unit.find(opts.unit_id)) if not target then qerror('Unit not found') end elseif opts.world then target = df.global.world.world_data.name @@ -407,6 +621,11 @@ if not target then target = get_artifact_target(item) elseif zone then target = get_location_target(df.world_site.find(zone.site_id), zone.location_id) + else + target, sync_targets = select_new_target() + end + if not target then + qerror('No target selected') end end From 444f56b2ab4c5aacd6df23809d672abd78ef3497 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Jan 2025 20:50:22 -0800 Subject: [PATCH 04/13] reorder a bit, support units with no hf --- gui/rename.lua | 83 ++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 58ac9d0efc..79d5a39d70 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -8,44 +8,6 @@ local CH_DN = string.char(31) local ENGLISH_COL_WIDTH = 16 local NATIVE_COL_WIDTH = 16 -local part_of_speech_to_display = { - [df.part_of_speech.Noun] = 'Singular Noun', - [df.part_of_speech.NounPlural] = 'Plural Noun', - [df.part_of_speech.Adjective] = 'Adjective', - [df.part_of_speech.Prefix] = 'Prefix', - [df.part_of_speech.Verb] = 'Present (1st)', - [df.part_of_speech.Verb3rdPerson] = 'Present (3rd)', - [df.part_of_speech.VerbPast] = 'Preterite', - [df.part_of_speech.VerbPassive] = 'Past Participle', - [df.part_of_speech.VerbGerund] = 'Present Participle', -} - -local langauge_name_type_to_category = { - [df.language_name_type.Figure] = {df.language_name_category.Unit}, - [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, - [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, - [df.language_name_type.Squad] = {df.language_name_category.Battle}, - [df.language_name_type.Site] = {df.language_name_category.Keep}, - [df.language_name_type.World] = {df.language_name_category.Region}, - [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, - [df.language_name_type.Temple] = {df.language_name_category.Temple}, - [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, - [df.language_name_type.Library] = {df.language_name_category.Library}, - [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, - [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, -} - -local language_name_component_to_word_table_index = { - [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, - [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, - [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, - [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, - [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, - -} - local language = df.global.world.raws.language local translations = df.language_translation.get_vector() @@ -372,6 +334,32 @@ function Rename:set_component_word(_, choice) end end +local langauge_name_type_to_category = { + [df.language_name_type.Figure] = {df.language_name_category.Unit}, + [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, + [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, + [df.language_name_type.Squad] = {df.language_name_category.Battle}, + [df.language_name_type.Site] = {df.language_name_category.Keep}, + [df.language_name_type.World] = {df.language_name_category.Region}, + [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, + [df.language_name_type.Temple] = {df.language_name_category.Temple}, + [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, + [df.language_name_type.Library] = {df.language_name_category.Library}, + [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, + [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, +} + +local language_name_component_to_word_table_index = { + [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, + [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, + [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, + [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, + [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, + +} + function Rename:randomize_component_word(comp) local categories = langauge_name_type_to_category[self.target.type] local category = categories[math.random(#categories)] @@ -390,6 +378,18 @@ function Rename:generate_random_name() print('TODO: generate_random_name') end +local part_of_speech_to_display = { + [df.part_of_speech.Noun] = 'Singular Noun', + [df.part_of_speech.NounPlural] = 'Plural Noun', + [df.part_of_speech.Adjective] = 'Adjective', + [df.part_of_speech.Prefix] = 'Prefix', + [df.part_of_speech.Verb] = 'Present (1st)', + [df.part_of_speech.Verb3rdPerson] = 'Present (3rd)', + [df.part_of_speech.VerbPast] = 'Preterite', + [df.part_of_speech.VerbPassive] = 'Past Participle', + [df.part_of_speech.VerbGerund] = 'Present Participle', +} + function Rename:add_word_choice(choices, comp, idx, word, part_of_speech) local english = word.forms[part_of_speech] if #english == 0 then return end @@ -539,7 +539,12 @@ end local function get_unit_target(unit) if not unit then return end - return get_hf_target(df.historical_figure.find(unit.hist_figure_id)) + local hf = df.historical_figure.find(unit.hist_figure_id) + if hf then + return get_hf_target(hf) + end + -- unit with no hf + return dfhack.units.getVisibleName(unit), {} end local function get_location_target(site, loc_id) From 3bb6321166d46967b4b61d8c67f72351ae56abe3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Jan 2025 22:16:37 -0800 Subject: [PATCH 05/13] target loading, per-component keyboard hotkeys --- gui/rename.lua | 322 +++++++++++++++++++++++++++++++++--------------- gui/sitemap.lua | 9 +- 2 files changed, 232 insertions(+), 99 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 79d5a39d70..d5e0850784 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1,5 +1,7 @@ local argparse = require('argparse') +local dlg = require('gui.dialogs') local gui = require('gui') +local sitemap = reqscript('gui/sitemap') local utils = require('utils') local widgets = require('gui.widgets') @@ -15,11 +17,143 @@ local translations = df.language_translation.get_vector() -- target selection -- -local function select_new_target() - local target, sync_targets = nil, {} +local function get_artifact_target(item) + if not item or not item.flags.artifact then return end + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local rec = df.artifact_record.find(gref.artifact_id) + if not rec then return end + return rec.name +end + +local function get_hf_target(hf) + if not hf then return end + local target = dfhack.units.getVisibleName(hf) + local unit = df.unit.find(hf.unit_id) + local sync_targets = {} + if unit then + local unit_name = dfhack.units.getVisibleName(unit) + if unit_name ~= target then + table.insert(sync_targets, unit_name) + end + end return target, sync_targets end +local function get_unit_target(unit) + if not unit then return end + local hf = df.historical_figure.find(unit.hist_figure_id) + if hf then + return get_hf_target(hf) + end + -- unit with no hf + return dfhack.units.getVisibleName(unit), {} +end + +local function get_location_target(site, loc_id) + if not site or loc_id < 0 then return end + local loc = utils.binsearch(site.buildings, loc_id, 'id') + if not loc then return end + return loc.name +end + +local function select_artifact(cb) + local choices = {} + for _, item in ipairs(df.global.world.items.other.ANY_ARTIFACT) do + if item.flags.garbage_collect then goto continue end + local target = get_artifact_target(item) + if not target then goto continue end + table.insert(choices, { + text=dfhack.items.getReadableDescription(item), + data={target=target}, + }) + ::continue:: + end + dlg.showListPrompt('Rename', 'Select an artifact to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) +end + +local function select_location(site, cb) + local choices = {} + for _,loc in ipairs(site.buildings) do + local desc, pen = sitemap.get_location_desc(loc) + table.insert(choices, { + text={ + dfhack.TranslateName(loc.name, true), + ' (', + {text=desc, pen=pen}, + ')', + }, + data={target=loc.name}, + }) + end + dlg.showListPrompt('Rename', 'Select a location to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) +end + +local function select_site(site, cb) + cb(site.name) +end + +local function select_squad(fort, cb) + local choices = {} + for _,squad_id in ipairs(fort.squads) do + local squad = df.squad.find(squad_id) + if squad then + table.insert(choices, { + text=dfhack.military.getSquadName(squad.id), + data={target=squad.name}, + }) + end + end + dlg.showListPrompt('Rename', 'Select a squad to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) +end + +local function select_unit(cb) + local choices = {} + for _,unit in ipairs(df.global.world.units.active) do + local target, sync_targets = get_unit_target(unit) + if target then + table.insert(choices, { + text=dfhack.units.getReadableName(unit), + data={target=target, sync_targets=sync_targets}, + }) + end + end + dlg.showListPrompt('Rename', 'Select a unit to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target, choice.data.sync_targets) end, + nil, nil, true) +end + +local function select_world(cb) + cb(df.global.world.world_data.name) +end + +local function select_new_target(cb) + local choices = {} + if #df.global.world.items.other.ANY_ARTIFACT > 0 then + table.insert(choices, {text='An artifact', data={fn=select_artifact}}) + end + local site = dfhack.world.getCurrentSite() + if site then + if #site.buildings > 0 then + table.insert(choices, {text='A location', data={fn=curry(select_location, site)}}) + end + table.insert(choices, {text='This fortress', data={fn=curry(select_site, site)}}) + local fort = df.historical_entity.find(df.global.plotinfo.group_id) + if fort and #fort.squads > 0 then + table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) + end + end + if #df.global.world.units.active > 0 then + table.insert(choices, {text='A unit', data={fn=select_unit}}) + end + table.insert(choices, {text='This world', data={fn=select_world}}) + dlg.showListPrompt('Rename', 'What would you like to rename?', COLOR_WHITE, + choices, function(_, choice) choice.data.fn(cb) end) +end + -- -- Rename -- @@ -27,9 +161,9 @@ end Rename = defclass(Rename, widgets.Window) Rename.ATTRS { frame_title='Rename', - frame={w=88, h=31}, + frame={w=89, h=33}, resizable=true, - resize_min={w=70, h=31}, + resize_min={w=61}, } local function get_language_options() @@ -125,21 +259,22 @@ function Rename:init(info) frame={t=0, l=0}, key='CUSTOM_CTRL_N', label='Select new target', + auto_width=true, on_activate=function() - local target, sync_targets = select_new_target() - if target then - self.target, self.sync_targets = target, sync_targets + select_new_target(function(target, sync_targets) + if not target then return end + self.target, self.sync_targets = target, sync_targets or {} self.subviews.language:setOption(self.target.language) - end + end) end, visible=info.show_selector, }, widgets.HotkeyLabel{ frame={t=0, r=0}, - label='Generate random name', key='CUSTOM_CTRL_G', - on_activate=self:callback('generate_random_name'), + label='Generate random name', auto_width=true, + on_activate=self:callback('generate_random_name'), }, widgets.Label{ frame={t=2}, @@ -215,20 +350,38 @@ function Rename:init(info) scroll_keys={}, }, widgets.HotkeyLabel{ - frame={b=1, l=0}, + frame={b=3, l=0}, key='SECONDSCROLL_UP', label='Prev component', on_activate=function() self.subviews.component_list:moveCursor(-1) end, }, widgets.HotkeyLabel{ - frame={b=0, l=0}, + frame={b=2, l=0}, key='SECONDSCROLL_DOWN', label='Next component', on_activate=function() self.subviews.component_list:moveCursor(1) end, }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='CUSTOM_CTRL_D', + label='Randomize component', + on_activate=function() + local _, comp_choice = self.subviews.component_list:getSelected() + self:randomize_component_word(comp_choice.data.val) + end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='CUSTOM_CTRL_H', + label='Clear component', + on_activate=function() + local _, comp_choice = self.subviews.component_list:getSelected() + self:clear_component_word(comp_choice.data.val) + end, + }, }, }, - widgets.Panel{frame={t=2, l=30}, -- words table + widgets.Panel{frame={t=2, l=31}, -- words table subviews={ widgets.CycleHotkeyLabel{ view_id='sort_english', @@ -463,9 +616,9 @@ function Rename:get_word_choices(comp) return choices end -function Rename:refresh_list(sort_widget) +function Rename:refresh_list(sort_widget, sort_fn) sort_widget = sort_widget or 'sort' - sort_fn = self.subviews.sort:getOptionValue() + sort_fn = sort_fn or self.subviews.sort:getOptionValue() if sort_fn == DEFAULT_NIL then self.subviews[sort_widget]:cycle() return @@ -496,7 +649,7 @@ function RenameScreen:init(info) self:addviews{ Rename{ target=info.target, - sync_targets=info.sync_targets, + sync_targets=info.sync_targets or {}, show_selector=info.show_selector, } } @@ -514,46 +667,6 @@ if not dfhack.isWorldLoaded() then qerror('This script requires a world to be loaded') end -local function get_artifact_target(item) - if not item or not item.flags.artifact then return end - local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) - if not gref then return end - local rec = df.artifact_record.find(gref.artifact_id) - if not rec then return end - return rec.name -end - -local function get_hf_target(hf) - if not hf then return end - local target = dfhack.units.getVisibleName(hf) - local unit = df.unit.find(hf.unit_id) - local sync_targets = {} - if unit then - local unit_name = dfhack.units.getVisibleName(unit) - if unit_name ~= target then - table.insert(sync_targets, unit_name) - end - end - return target, sync_targets -end - -local function get_unit_target(unit) - if not unit then return end - local hf = df.historical_figure.find(unit.hist_figure_id) - if hf then - return get_hf_target(hf) - end - -- unit with no hf - return dfhack.units.getVisibleName(unit), {} -end - -local function get_location_target(site, loc_id) - if not site or loc_id < 0 then return end - local loc = utils.binsearch(site.buildings, loc_id, 'id') - if not loc then return end - return loc.name -end - local function get_target(opts) local target, sync_targets = nil, {} if opts.histfig_id then @@ -584,58 +697,71 @@ local function get_target(opts) return target, sync_targets end -local opts = { - help=false, - entity_id=nil, - histfig_id=nil, - item_id=nil, - location_id=nil, - site_id=nil, - squad_id=nil, - unit_id=nil, - world=false, - show_selector=true, -} -local positionals = argparse.processArgsGetopt({...}, { - { 'a', 'artifact', handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, - { 'e', 'entity', handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, - { 'f', 'histfig', handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, - { 'h', 'help', handler = function() opts.help = true end }, - { 'l', 'location', handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, - { 'q', 'squad', handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, - { 's', 'site', handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, - { 'u', 'unit', handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, - { 'w', 'world', handler=function() opts.world = true end }, - { '', 'no-target-selector', handler=function() opts.show_selector = false end }, -}) - -if opts.help or positionals[1] == 'help' then - print(dfhack.script_help()) - return -end - -local target, sync_targets = get_target(opts) - -if not target then +local function main(args) + local opts = { + help=false, + entity_id=nil, + histfig_id=nil, + item_id=nil, + location_id=nil, + site_id=nil, + squad_id=nil, + unit_id=nil, + world=false, + show_selector=true, + } + local positionals = argparse.processArgsGetopt(args, { + { 'a', 'artifact', handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, + { 'e', 'entity', handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, + { 'f', 'histfig', handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, + { 'h', 'help', handler = function() opts.help = true end }, + { 'l', 'location', handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, + { 'q', 'squad', handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, + { 's', 'site', handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, + { 'u', 'unit', handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, + { 'w', 'world', handler=function() opts.world = true end }, + { '', 'no-target-selector', handler=function() opts.show_selector = false end }, + }) + + if opts.help or positionals[1] == 'help' then + print(dfhack.script_help()) + return + end + + local function launch(target, sync_targets) + view = view and view:raise() or RenameScreen{ + target=target, + sync_targets=sync_targets, + show_selector=opts.show_selector, + }:show() + end + + local target, sync_targets = get_target(opts) + if target then + launch(target, sync_targets) + return + end + local unit = dfhack.gui.getSelectedUnit(true) local item = dfhack.gui.getSelectedItem(true) local zone = dfhack.gui.getSelectedCivZone(true) if unit then - target = get_unit_target(unit, sync_targets) + target, sync_targets = get_unit_target(unit) elseif item then target = get_artifact_target(item) elseif zone then target = get_location_target(df.world_site.find(zone.site_id), zone.location_id) - else - target, sync_targets = select_new_target() end - if not target then + if target then + launch(target, sync_targets) + return + end + + if not opts.show_selector then qerror('No target selected') end + + select_new_target(launch) end -view = view and view:raise() or RenameScreen{ - target=target, - sync_targets=sync_targets, - show_selector=opts.show_selector -}:show() +main{...} diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 4440cdd730..ec7ffff7d1 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -1,3 +1,5 @@ +--@ module = true + local gui = require('gui') local utils = require('utils') local widgets = require('gui.widgets') @@ -18,7 +20,8 @@ local function to_title_case(str) return dfhack.capitalizeStringWords(dfhack.lowerCp437(str:gsub('_', ' '))) end -local function get_location_desc(loc) +-- also called by gui/rename +function get_location_desc(loc) if df.abstract_building_hospitalst:is_instance(loc) then return 'Hospital', COLOR_WHITE elseif df.abstract_building_inn_tavernst:is_instance(loc) then @@ -309,6 +312,10 @@ function SitemapScreen:onDismiss() view = nil end +if dfhack_flags.module then + return +end + if not dfhack.isMapLoaded() then qerror('This script requires a map to be loaded') end From 6de7797c82a83a72ff0ebf99a18a9cee98382942 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 00:55:30 -0800 Subject: [PATCH 06/13] allow editing of first name for units --- gui/rename.lua | 185 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 138 insertions(+), 47 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index d5e0850784..ea1bcc01a8 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1,3 +1,5 @@ +--@module = true + local argparse = require('argparse') local dlg = require('gui.dialogs') local gui = require('gui') @@ -161,7 +163,7 @@ end Rename = defclass(Rename, widgets.Window) Rename.ATTRS { frame_title='Rename', - frame={w=89, h=33}, + frame={w=89, h=43}, resizable=true, resize_min={w=61}, } @@ -291,12 +293,7 @@ function Rename:init(info) label='Language:', options=language_options, initial_option=self.target and self.target.language or 0, - on_change=function(val) - self.target.language = val - for _, sync_target in ipairs(self.sync_targets) do - sync_target.language = val - end - end, + on_change=self:callback('set_language'), }, widgets.Label{ frame={t=6, l=7}, @@ -304,46 +301,29 @@ function Rename:init(info) }, }, }, + widgets.Divider{frame={t=8, l=29, w=1}, + frame_style=gui.FRAME_THIN, + frame_style_t=false, + frame_style_b=false, + }, widgets.Panel{frame={t=8}, -- body subviews={ - widgets.Panel{frame={t=0, h=1}, -- toolbar + widgets.Panel{frame={t=0, l=0, w=30}, -- component selector subviews={ - widgets.CycleHotkeyLabel{ - view_id='sort', - frame={t=0, l=0, w=32}, - label='Sort by:', - key='CUSTOM_CTRL_O', - options={ - {label='English'..CH_DN, value=sort_by_english_desc}, - {label='English'..CH_UP, value=sort_by_english_asc}, - {label='native'..CH_DN, value=sort_by_native_desc}, - {label='native'..CH_UP, value=sort_by_native_asc}, - {label='part of speech'..CH_DN, value=sort_by_part_of_speech_desc}, - {label='part of speech'..CH_UP, value=sort_by_part_of_speech_asc}, - }, - initial_option=sort_by_english_desc, - on_change=self:callback('refresh_list', 'sort'), - }, - widgets.EditField{ - view_id='search', - frame={t=0, l=35}, - label_text='Search: ', - ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + widgets.Label{ + frame={t=0, l=0}, + text='Name components:', }, - }, - }, - widgets.Panel{frame={t=2, l=0, w=30}, -- component selector - subviews={ widgets.List{ - frame={t=0, l=0, b=2, w=ENGLISH_COL_WIDTH+2}, + frame={t=2, l=0, b=4, w=ENGLISH_COL_WIDTH+2}, view_id='component_list', - on_select=function() if self.subviews.component_list then self:refresh_list() end end, + on_select=self:callback('refresh_list'), choices=self:get_component_choices(), - row_height=2, + row_height=3, scroll_keys={}, }, widgets.List{ - frame={t=0, l=ENGLISH_COL_WIDTH+4, b=3}, + frame={t=2, l=ENGLISH_COL_WIDTH+4, b=4}, on_submit=function(_, choice) choice.data.fn() end, choices=self:get_component_action_choices(), cursor_pen=COLOR_CYAN, @@ -353,13 +333,23 @@ function Rename:init(info) frame={b=3, l=0}, key='SECONDSCROLL_UP', label='Prev component', - on_activate=function() self.subviews.component_list:moveCursor(-1) end, + on_activate=function() + local clist = self.subviews.component_list + local move = self.target.type ~= df.language_name_type.Figure and + clist:getSelected() == 2 and #clist:getChoices()-2 or -1 + self.subviews.component_list:moveCursor(move) + end, }, widgets.HotkeyLabel{ frame={b=2, l=0}, key='SECONDSCROLL_DOWN', label='Next component', - on_activate=function() self.subviews.component_list:moveCursor(1) end, + on_activate=function() + local clist = self.subviews.component_list + local move = self.target.type ~= df.language_name_type.Figure and + clist:getSelected() == #clist:getChoices() and -#clist:getChoices()+2 or 1 + self.subviews.component_list:moveCursor(move) + end, }, widgets.HotkeyLabel{ frame={b=1, l=0}, @@ -367,7 +357,11 @@ function Rename:init(info) label='Randomize component', on_activate=function() local _, comp_choice = self.subviews.component_list:getSelected() - self:randomize_component_word(comp_choice.data.val) + if comp_choice.data.is_first_name then + self:randomize_first_name() + else + self:randomize_component_word(comp_choice.data.val) + end end, }, widgets.HotkeyLabel{ @@ -378,14 +372,41 @@ function Rename:init(info) local _, comp_choice = self.subviews.component_list:getSelected() self:clear_component_word(comp_choice.data.val) end, + enabled=function() + local _, comp_choice = self.subviews.component_list:getSelected() + if comp_choice.data.is_first_name then return false end + return self.target.words[comp_choice.data.val] >= 0 + end, }, }, }, - widgets.Panel{frame={t=2, l=31}, -- words table + widgets.Panel{frame={t=0, l=31}, -- words table subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={t=0, l=0, w=19}, + label='Change sort', + key='CUSTOM_CTRL_O', + options={ + {label='', value=sort_by_english_desc}, + {label='', value=sort_by_english_asc}, + {label='', value=sort_by_native_desc}, + {label='', value=sort_by_native_asc}, + {label='', value=sort_by_part_of_speech_desc}, + {label='', value=sort_by_part_of_speech_asc}, + }, + initial_option=sort_by_english_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={t=0, l=22}, + label_text='Search: ', + ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + }, widgets.CycleHotkeyLabel{ view_id='sort_english', - frame={t=0, l=0, w=8}, + frame={t=2, l=0, w=8}, options={ {label='English', value=DEFAULT_NIL}, {label='English'..CH_DN, value=sort_by_english_desc}, @@ -397,7 +418,7 @@ function Rename:init(info) }, widgets.CycleHotkeyLabel{ view_id='sort_native', - frame={t=0, l=ENGLISH_COL_WIDTH+2, w=7}, + frame={t=2, l=ENGLISH_COL_WIDTH+2, w=7}, options={ {label='native', value=DEFAULT_NIL}, {label='native'..CH_DN, value=sort_by_native_desc}, @@ -408,7 +429,7 @@ function Rename:init(info) }, widgets.CycleHotkeyLabel{ view_id='sort_part_of_speech', - frame={t=0, l=ENGLISH_COL_WIDTH+2+NATIVE_COL_WIDTH+2, w=15}, + frame={t=2, l=ENGLISH_COL_WIDTH+2+NATIVE_COL_WIDTH+2, w=15}, options={ {label='part of speech', value=DEFAULT_NIL}, {label='part_of_speech'..CH_DN, value=sort_by_part_of_speech_desc}, @@ -419,7 +440,7 @@ function Rename:init(info) }, widgets.FilteredList{ view_id='words_list', - frame={t=2, l=0, b=0, r=0}, + frame={t=4, l=0, b=0, r=0}, on_submit=self:callback('set_component_word'), }, }, @@ -439,6 +460,14 @@ end function Rename:get_component_choices() local choices = {} + table.insert(choices, { + text={ + {text='First Name', + pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, + NEWLINE, + {gap=2, pen=COLOR_YELLOW, text=function() return self.target.first_name end} + }, + data={val=df.language_name_component.TheX, is_first_name=true}}) for val, comp in ipairs(df.language_name_component) do local text = { {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE, @@ -455,8 +484,19 @@ end function Rename:get_component_action_choices() local choices = {} + table.insert(choices, { + text={ + {text='[', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end}, + {text='Random', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, + {text=']', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end} + }, + data={fn=self:callback('randomize_first_name')}, + }) + table.insert(choices, {text='', data={fn=function() end}}) -- shouldn't be able to clear a first name, only overwrite + table.insert(choices, {text='', data={fn=function() end}}) + + local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} for val, comp in ipairs(df.language_name_component) do - local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} local randomize_fn = self:callback('randomize_component_word', comp) table.insert(choices, {text=randomize_text, data={fn=randomize_fn}}) local clear_text = { @@ -466,6 +506,7 @@ function Rename:get_component_action_choices() } local clear_fn = self:callback('clear_component_word', comp) table.insert(choices, {text=clear_text, data={fn=clear_fn}}) + table.insert(choices, {text='', data={fn=function() end}}) end return choices end @@ -477,8 +518,19 @@ function Rename:clear_component_word(comp) end end +function Rename:set_first_name(choice) + self.target.first_name = translations[self.subviews.language:getOptionValue()].words[choice.data.idx].value + for _, sync_target in ipairs(self.sync_targets) do + sync_target.first_name = self.target.first_name + end +end + function Rename:set_component_word(_, choice) local _, comp_choice = self.subviews.component_list:getSelected() + if comp_choice.data.is_first_name then + self:set_first_name(choice) + return + end self.target.words[comp_choice.data.val] = choice.data.idx self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech for _, sync_target in ipairs(self.sync_targets) do @@ -487,6 +539,17 @@ function Rename:set_component_word(_, choice) end end +function Rename:set_language(val, prev_val) + self.target.language = val + -- translate current first name into target language + local idx = utils.linear_index(translations[prev_val].words, self.target.first_name, 'value') + if idx then self.target.first_name = translations[val].words[idx].value end + for _, sync_target in ipairs(self.sync_targets) do + sync_target.language = val + sync_target.first_name = self.target.first_name + end +end + local langauge_name_type_to_category = { [df.language_name_type.Figure] = {df.language_name_category.Unit}, [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, @@ -513,6 +576,12 @@ local language_name_component_to_word_table_index = { } +function Rename:randomize_first_name() + if self.target.type ~= df.language_name_type.Figure then return end + local choices = self:get_word_choices(df.language_name_component.TheX) + self:set_first_name(choices[math.random(#choices)]) +end + function Rename:randomize_component_word(comp) local categories = langauge_name_type_to_category[self.target.type] local category = categories[math.random(#categories)] @@ -550,7 +619,12 @@ function Rename:add_word_choice(choices, comp, idx, word, part_of_speech) return translations[self.subviews.language:getOptionValue()].words[idx].value end local part = part_of_speech_to_display[part_of_speech] + local clist = self.subviews.component_list local function get_pen() + local _, comp_choice = clist:getSelected() + if comp_choice.data.is_first_name then + return get_native() == self.target.first_name and COLOR_YELLOW or nil + end if idx == self.target.words[comp] and part_of_speech == self.target.parts_of_speech[comp] then return COLOR_YELLOW end @@ -617,6 +691,13 @@ function Rename:get_word_choices(comp) end function Rename:refresh_list(sort_widget, sort_fn) + local clist = self.subviews.component_list + if not clist then return end + if self.target.type ~= df.language_name_type.Figure and clist:getSelected() == 1 then + clist:setSelected(self.prev_selected_component or 2) + end + self.prev_selected_component = clist:getSelected() + sort_widget = sort_widget or 'sort' sort_fn = sort_fn or self.subviews.sort:getOptionValue() if sort_fn == DEFAULT_NIL then @@ -629,7 +710,7 @@ function Rename:refresh_list(sort_widget, sort_fn) local list = self.subviews.words_list local saved_filter = list:getFilter() list:setFilter('') - local _, comp_choice = self.subviews.component_list:getSelected() + local _, comp_choice = clist:getSelected() local choices = self:get_word_choices(comp_choice.data.val) table.sort(choices, self.subviews.sort:getOptionValue()) list:setChoices(choices) @@ -659,10 +740,20 @@ function RenameScreen:onDismiss() view = nil end +-- +-- Overlays +-- + +OVERLAY_WIDGETS = {} + -- -- CLI -- +if dfhack_flags.module then + return +end + if not dfhack.isWorldLoaded() then qerror('This script requires a world to be loaded') end From 7b7ae031cddcafa8225de62311894d49bf802d2e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 01:33:26 -0800 Subject: [PATCH 07/13] implement savegame world renaming --- gui/rename.lua | 95 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index ea1bcc01a8..b18e0a13cc 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -3,6 +3,7 @@ local argparse = require('argparse') local dlg = require('gui.dialogs') local gui = require('gui') +local overlay = require('plugins.overlay') local sitemap = reqscript('gui/sitemap') local utils = require('utils') local widgets = require('gui.widgets') @@ -59,6 +60,17 @@ local function get_location_target(site, loc_id) return loc.name end +local function get_world_target() + local target = df.global.world.world_data.name + local sync_targets = { + function() + df.global.world.cur_savegame.world_header.world_name = + ('%s, "%s"'):format(dfhack.TranslateName(target), dfhack.TranslateName(target, true)) + end + } + return target, sync_targets +end + local function select_artifact(cb) local choices = {} for _, item in ipairs(df.global.world.items.other.ANY_ARTIFACT) do @@ -114,14 +126,16 @@ end local function select_unit(cb) local choices = {} - for _,unit in ipairs(df.global.world.units.active) do + -- scan through units.all instead of units.active so we can choose starting dwarves on embark prep screen + for _,unit in ipairs(df.global.world.units.all) do + if not dfhack.units.isActive(unit) then goto continue end local target, sync_targets = get_unit_target(unit) - if target then - table.insert(choices, { - text=dfhack.units.getReadableName(unit), - data={target=target, sync_targets=sync_targets}, - }) - end + if not target then goto continue end + table.insert(choices, { + text=dfhack.units.getReadableName(unit), + data={target=target, sync_targets=sync_targets}, + }) + ::continue:: end dlg.showListPrompt('Rename', 'Select a unit to rename:', COLOR_WHITE, choices, function(_, choice) cb(choice.data.target, choice.data.sync_targets) end, @@ -129,7 +143,8 @@ local function select_unit(cb) end local function select_world(cb) - cb(df.global.world.world_data.name) + local target, sync_targets = get_world_target() + cb(target, sync_targets) end local function select_new_target(cb) @@ -148,7 +163,7 @@ local function select_new_target(cb) table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) end end - if #df.global.world.units.active > 0 then + if #df.global.world.units.all > 0 then table.insert(choices, {text='A unit', data={fn=select_unit}}) end table.insert(choices, {text='This world', data={fn=select_world}}) @@ -514,14 +529,22 @@ end function Rename:clear_component_word(comp) self.target.words[comp] = -1 for _, sync_target in ipairs(self.sync_targets) do - sync_target.words[comp] = -1 + if type(sync_target) == 'function' then + sync_target() + else + sync_target.words[comp] = -1 + end end end function Rename:set_first_name(choice) self.target.first_name = translations[self.subviews.language:getOptionValue()].words[choice.data.idx].value for _, sync_target in ipairs(self.sync_targets) do - sync_target.first_name = self.target.first_name + if type(sync_target) == 'function' then + sync_target() + else + sync_target.first_name = self.target.first_name + end end end @@ -534,8 +557,12 @@ function Rename:set_component_word(_, choice) self.target.words[comp_choice.data.val] = choice.data.idx self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech for _, sync_target in ipairs(self.sync_targets) do - sync_target.words[comp_choice.data.val] = choice.data.idx - sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + if type(sync_target) == 'function' then + sync_target() + else + sync_target.words[comp_choice.data.val] = choice.data.idx + sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + end end end @@ -545,8 +572,12 @@ function Rename:set_language(val, prev_val) local idx = utils.linear_index(translations[prev_val].words, self.target.first_name, 'value') if idx then self.target.first_name = translations[val].words[idx].value end for _, sync_target in ipairs(self.sync_targets) do - sync_target.language = val - sync_target.first_name = self.target.first_name + if type(sync_target) == 'function' then + sync_target() + else + sync_target.language = val + sync_target.first_name = self.target.first_name + end end end @@ -591,8 +622,12 @@ function Rename:randomize_component_word(comp) self.target.words[comp] = words[idx] self.target.parts_of_speech[comp] = word_table.parts[comp][idx] for _, sync_target in ipairs(self.sync_targets) do - sync_target.words[comp] = words[idx] - sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] + if type(sync_target) == 'function' then + sync_target() + else + sync_target.words[comp] = words[idx] + sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] + end end end @@ -744,7 +779,29 @@ end -- Overlays -- -OVERLAY_WIDGETS = {} +WorldRenameOverlay = defclass(WorldRenameOverlay, overlay.OverlayWidget) +WorldRenameOverlay.ATTRS { + desc='Adds a button for renaming newly generated worlds.', + default_pos={x=57, y=3}, + default_enabled=true, + viewscreens='new_region', + frame={w=22, h=1}, +} + +function WorldRenameOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='Rename world', + key='CUSTOM_CTRL_T', + on_activate=function() dfhack.run_script('gui/rename', '--world', '--no-target-selector') end, + }, + } +end + +OVERLAY_WIDGETS = { + world_rename=WorldRenameOverlay, +} -- -- CLI @@ -783,7 +840,7 @@ local function get_target(opts) target, sync_targets = get_unit_target(df.unit.find(opts.unit_id)) if not target then qerror('Unit not found') end elseif opts.world then - target = df.global.world.world_data.name + target, sync_targets = get_world_target() end return target, sync_targets end From 6aa777643e13d4868e223f875868f7626ae616e4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 02:01:28 -0800 Subject: [PATCH 08/13] only show overlay once world is loaded --- gui/rename.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/rename.lua b/gui/rename.lua index b18e0a13cc..ae5820aebb 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -786,6 +786,7 @@ WorldRenameOverlay.ATTRS { default_enabled=true, viewscreens='new_region', frame={w=22, h=1}, + visible=function() return dfhack.isWorldLoaded() end, } function WorldRenameOverlay:init() From 192c252efd8169d9ff3302593e4bc157b066b11b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:12:57 -0800 Subject: [PATCH 09/13] use word tables for randomization --- gui/rename.lua | 90 ++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index ae5820aebb..a6d492ba35 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -511,13 +511,13 @@ function Rename:get_component_action_choices() table.insert(choices, {text='', data={fn=function() end}}) local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} - for val, comp in ipairs(df.language_name_component) do + for comp in ipairs(df.language_name_component) do local randomize_fn = self:callback('randomize_component_word', comp) table.insert(choices, {text=randomize_text, data={fn=randomize_fn}}) local clear_text = { - {text=function() return self.target.words[val] >= 0 and '[' or '' end, pen=COLOR_RED}, - {text=function() return self.target.words[val] >= 0 and 'Clear' or '' end }, - {text=function() return self.target.words[val] >= 0 and ']' or '' end, pen=COLOR_RED} + {text=function() return self.target.words[comp] >= 0 and '[' or '' end, pen=COLOR_RED}, + {text=function() return self.target.words[comp] >= 0 and 'Clear' or '' end }, + {text=function() return self.target.words[comp] >= 0 and ']' or '' end, pen=COLOR_RED} } local clear_fn = self:callback('clear_component_word', comp) table.insert(choices, {text=clear_text, data={fn=clear_fn}}) @@ -537,8 +537,9 @@ function Rename:clear_component_word(comp) end end -function Rename:set_first_name(choice) - self.target.first_name = translations[self.subviews.language:getOptionValue()].words[choice.data.idx].value +function Rename:set_first_name(word_idx) + self.target.first_name = translations[self.subviews.language:getOptionValue()].words[word_idx].value + self.target.has_name = true -- support giving names to previously unnamed units for _, sync_target in ipairs(self.sync_targets) do if type(sync_target) == 'function' then sync_target() @@ -548,24 +549,28 @@ function Rename:set_first_name(choice) end end -function Rename:set_component_word(_, choice) - local _, comp_choice = self.subviews.component_list:getSelected() - if comp_choice.data.is_first_name then - self:set_first_name(choice) - return - end - self.target.words[comp_choice.data.val] = choice.data.idx - self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech +function Rename:set_component_word_by_data(component, word_idx, part_of_speech) + self.target.words[component] = word_idx + self.target.parts_of_speech[component] = part_of_speech for _, sync_target in ipairs(self.sync_targets) do if type(sync_target) == 'function' then sync_target() else - sync_target.words[comp_choice.data.val] = choice.data.idx - sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + sync_target.words[component] = word_idx + sync_target.parts_of_speech[component] = part_of_speech end end end +function Rename:set_component_word(_, choice) + local _, comp_choice = self.subviews.component_list:getSelected() + if comp_choice.data.is_first_name then + self:set_first_name(choice.data.idx) + return + end + self:set_component_word_by_data(comp_choice.data.val, choice.data.idx, choice.data.part_of_speech) +end + function Rename:set_language(val, prev_val) self.target.language = val -- translate current first name into target language @@ -581,7 +586,7 @@ function Rename:set_language(val, prev_val) end end -local langauge_name_type_to_category = { +local language_name_type_to_category = { [df.language_name_type.Figure] = {df.language_name_category.Unit}, [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, @@ -590,7 +595,7 @@ local langauge_name_type_to_category = { [df.language_name_type.World] = {df.language_name_category.Region}, [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, [df.language_name_type.Temple] = {df.language_name_category.Temple}, - [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, + [df.language_name_type.FoodStore] = {df.language_name_category.MeadHall}, [df.language_name_type.Library] = {df.language_name_category.Library}, [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, @@ -599,40 +604,45 @@ local langauge_name_type_to_category = { local language_name_component_to_word_table_index = { [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, - [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, - [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, - + [df.language_name_component.HyphenCompound] = df.language_word_table_index.FrontCompound, + [df.language_name_component.TheX] = df.language_word_table_index.TheX, + [df.language_name_component.OfX] = df.language_word_table_index.OfX, } +local function get_random_word(category, word_table_index) + local word_table = language.word_table[0][category] + local words = word_table.words[word_table_index] + local idx = #words > 0 and math.random(#words)-1 or -1 + local word = idx >= 0 and words[idx] or -1 + local part_of_speech = idx >= 0 and word_table.parts[word_table_index][idx] or df.part_of_speech.Noun + return word, part_of_speech +end + function Rename:randomize_first_name() if self.target.type ~= df.language_name_type.Figure then return end - local choices = self:get_word_choices(df.language_name_component.TheX) - self:set_first_name(choices[math.random(#choices)]) + local word_idx = get_random_word(df.language_name_category.Unit, df.language_word_table_index.FirstName) + self:set_first_name(word_idx) end function Rename:randomize_component_word(comp) - local categories = langauge_name_type_to_category[self.target.type] - local category = categories[math.random(#categories)] - local word_table = language.word_table[0][category] - local words = word_table.words[comp] - local idx = math.random(#words)-1 - self.target.words[comp] = words[idx] - self.target.parts_of_speech[comp] = word_table.parts[comp][idx] - for _, sync_target in ipairs(self.sync_targets) do - if type(sync_target) == 'function' then - sync_target() - else - sync_target.words[comp] = words[idx] - sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] - end - end + local categories = language_name_type_to_category[self.target.type] + local category = categories and categories[math.random(#categories)] or df.language_name_category.MeadHall + local word_idx, part_of_speech = get_random_word(category, language_name_component_to_word_table_index[comp]) + self:set_component_word_by_data(comp, word_idx, part_of_speech) end function Rename:generate_random_name() - print('TODO: generate_random_name') + print('TODO: call dfhack.GenerateName API once it exists') + -- dfhack.GenerateName(self.target) + -- for _, sync_target in ipairs(self.sync_targets) do + -- if type(sync_target) == 'function' then + -- sync_target() + -- else + -- df.assign(sync_target, self.target) + -- end + -- end end local part_of_speech_to_display = { From b342b43c86190f7d141d1fcd50f03dcd7a6ee8dd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:23:09 -0800 Subject: [PATCH 10/13] delegate word table lookups to upcoming library API --- gui/rename.lua | 59 ++++++++++---------------------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index a6d492ba35..7859811443 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -286,13 +286,13 @@ function Rename:init(info) end, visible=info.show_selector, }, - widgets.HotkeyLabel{ - frame={t=0, r=0}, - key='CUSTOM_CTRL_G', - label='Generate random name', - auto_width=true, - on_activate=self:callback('generate_random_name'), - }, + -- widgets.HotkeyLabel{ + -- frame={t=0, r=0}, + -- key='CUSTOM_CTRL_G', + -- label='Generate random name', + -- auto_width=true, + -- on_activate=self:callback('generate_random_name'), + -- }, widgets.Label{ frame={t=2}, text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.TranslateName(self.target), self.frame_body.width) end}}, @@ -586,51 +586,16 @@ function Rename:set_language(val, prev_val) end end -local language_name_type_to_category = { - [df.language_name_type.Figure] = {df.language_name_category.Unit}, - [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, - [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, - [df.language_name_type.Squad] = {df.language_name_category.Battle}, - [df.language_name_type.Site] = {df.language_name_category.Keep}, - [df.language_name_type.World] = {df.language_name_category.Region}, - [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, - [df.language_name_type.Temple] = {df.language_name_category.Temple}, - [df.language_name_type.FoodStore] = {df.language_name_category.MeadHall}, - [df.language_name_type.Library] = {df.language_name_category.Library}, - [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, - [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, -} - -local language_name_component_to_word_table_index = { - [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, - [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, - [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.HyphenCompound] = df.language_word_table_index.FrontCompound, - [df.language_name_component.TheX] = df.language_word_table_index.TheX, - [df.language_name_component.OfX] = df.language_word_table_index.OfX, -} - -local function get_random_word(category, word_table_index) - local word_table = language.word_table[0][category] - local words = word_table.words[word_table_index] - local idx = #words > 0 and math.random(#words)-1 or -1 - local word = idx >= 0 and words[idx] or -1 - local part_of_speech = idx >= 0 and word_table.parts[word_table_index][idx] or df.part_of_speech.Noun - return word, part_of_speech -end - function Rename:randomize_first_name() if self.target.type ~= df.language_name_type.Figure then return end - local word_idx = get_random_word(df.language_name_category.Unit, df.language_word_table_index.FirstName) - self:set_first_name(word_idx) + local choices = self:get_word_choices(df.language_name_component.TheX) + self:set_first_name(choices[math.random(#choices)].data.idx) end function Rename:randomize_component_word(comp) - local categories = language_name_type_to_category[self.target.type] - local category = categories and categories[math.random(#categories)] or df.language_name_category.MeadHall - local word_idx, part_of_speech = get_random_word(category, language_name_component_to_word_table_index[comp]) - self:set_component_word_by_data(comp, word_idx, part_of_speech) + local choices = self:get_word_choices(df.language_name_component.TheX) + local choice = choices[math.random(#choices)] + self:set_component_word_by_data(comp, choice.data.idx, choice.data.part_of_speech) end function Rename:generate_random_name() From 08bffc8aec26116a453273d03b1eec7e2cdbd6fa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:39:25 -0800 Subject: [PATCH 11/13] update docs and clean up code --- docs/gui/rename.rst | 39 ++++++++++++++++++++++++++------------- gui/rename.lua | 12 +++++++++--- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index d72255d0ac..c643186455 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -2,18 +2,22 @@ gui/rename ========== .. dfhack-tool:: - :summary: Modify the name of anything that is nameable. + :summary: Edit in-game language-based names. :tags: adventure fort productivity animals items units -Once you select a target (by clicking on the game map, by passing a commandline -parameter, or by using the provided selection widget) this tool allows you -change its language name, generate a new random name, or rename it with your -preferred component words. It provides an interface similar to the in-game -naming panel that you can use to customize your fortress name at embark. That -is, it allows you to choose words from an in-game language to assemble a name, -just like the default names that the game generates. You will be able to assign -units new given and last names. You can also use this tool to set freeform -"nicknames" for targets that support it. +Once you select a target (by clicking on something on the game map, by passing +a commandline parameter, or by using the selection dialog) this tool allows you +change the language of the name, generate a new random name, or replace +components of the name with your preferred words. + +`gui/rename` provides an interface similar to the in-game naming panel that you +can use to customize your fortress name at embark. That is, it allows you to +choose words from an in-game language to assemble a name, just like the default +names that the game generates. You will be able to assign units new given and +last names, or even rename the world itself. + +You can run `gui/rename` while on the "prepare carefully" embark screen to +rename your starting dwarves. Usage ----- @@ -22,6 +26,16 @@ Usage gui/rename [] +The selection dialog will appear if no options are provided. You can +interactively choose one of the following to rename: + +- An artifact on the current map +- A location (e.g. tavern, hospital, guildhall, temple) on the current map +- The current fortress (or adventurer site) +- A squad belonging to the current fortress +- A unit on the current map +- The world + Examples -------- @@ -38,6 +52,8 @@ Examples Options ------- +Targets specified via these options do not need to be on the local map. + ``-a``, ``--artifact `` Rename the artifact with the given item ID. ``-e``, ``--entity `` @@ -68,8 +84,5 @@ Overlays This tool supports the following overlays: -``gui/rename.embark`` - Adds widgets to the embark preparation screen for renaming the starting - dwarves. ``gui/rename.world`` Adds a widget to the world generation screen for renaming the world. diff --git a/gui/rename.lua b/gui/rename.lua index 7859811443..5908856eff 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -267,6 +267,10 @@ function Rename:init(info) self.sync_targets = info.sync_targets or {} self.cache = {} + if self.target.type == df.language_name_type.NONE then + self.target.type = df.language_name_type.Figure + end + local language_options, max_lang_name_width = get_language_options() self:addviews{ @@ -538,8 +542,10 @@ function Rename:clear_component_word(comp) end function Rename:set_first_name(word_idx) + -- support giving names to previously unnamed units + self.target.has_name = true + self.target.first_name = translations[self.subviews.language:getOptionValue()].words[word_idx].value - self.target.has_name = true -- support giving names to previously unnamed units for _, sync_target in ipairs(self.sync_targets) do if type(sync_target) == 'function' then sync_target() @@ -751,7 +757,7 @@ function RenameScreen:onDismiss() end -- --- Overlays +-- WorldRenameOverlay -- WorldRenameOverlay = defclass(WorldRenameOverlay, overlay.OverlayWidget) @@ -776,7 +782,7 @@ function WorldRenameOverlay:init() end OVERLAY_WIDGETS = { - world_rename=WorldRenameOverlay, + world=WorldRenameOverlay, } -- From e6c1be6f12db57d5d136db63e16ccc32ae3f92de Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:41:47 -0800 Subject: [PATCH 12/13] update changelog --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 5b8aa1c065..c93c9ed114 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,10 +28,12 @@ Template for new versions: ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map +- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (e.g. units, governments, fortresses, or the world) ## New Features - `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) - `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting +- `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved ## Fixes - `fix/dry-buckets`: don't empty buckets for wells that are actively in use From 02ef7ffa598a7653ec9f588f33b93f9c24e5ea22 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 16:06:38 -0800 Subject: [PATCH 13/13] more docs --- docs/gui/rename.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index c643186455..bf58025a1f 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -19,6 +19,15 @@ last names, or even rename the world itself. You can run `gui/rename` while on the "prepare carefully" embark screen to rename your starting dwarves. +Start typing to search for a word. You can search in English, in the selected +native language, or by the part of speech. Click on a word to assign it to the +selected name component slot. You can also clear or randomize each individual +name component slot. + +When giving a name to a unit that didn't previously have a name, you must +assign a word to the First Name slot. Otherwise, the game will not display the +name for the unit. + Usage -----