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 diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index 5688354bb7..bf58025a1f 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -2,28 +2,96 @@ gui/rename ========== .. dfhack-tool:: - :summary: Give buildings and units new names, optionally with special chars. - :tags: unavailable + :summary: Edit in-game language-based names. + :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. +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. -This tool supports renaming units, zones, stockpiles, workshops, furnaces, -traps, and siege engines. +`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. + +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 ----- +:: + + 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 +-------- + ``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 +------- + +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 `` + 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.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..5908856eff 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1,127 +1,897 @@ --- Rename various objects via gui. ---[====[ +--@module = true -gui/rename -========== -Backed by `rename`, this script allows entering the desired name -via a simple dialog in the game ui. +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') -* ``gui/rename [building]`` in :kbd:`q` mode changes the name of a building. +local CH_UP = string.char(30) +local CH_DN = string.char(31) +local ENGLISH_COL_WIDTH = 16 +local NATIVE_COL_WIDTH = 16 - .. image:: /docs/images/rename-bld.png +local language = df.global.world.raws.language +local translations = df.language_translation.get_vector() - 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. +-- +-- target selection +-- -* ``gui/rename [unit]`` with a unit selected changes the nickname. +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_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 + 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 - Unlike the built-in interface, this works even on enemies and animals. +local function select_site(site, cb) + cb(site.name) +end -* ``gui/rename unit-profession`` changes the selected unit's custom profession name. +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 - .. image:: /docs/images/rename-prof.png +local function select_unit(cb) + local choices = {} + -- 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 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, + nil, nil, true) +end - Likewise, this can be applied to any unit, and when used on animals it overrides - their species string. +local function select_world(cb) + local target, sync_targets = get_world_target() + cb(target, sync_targets) +end -The ``building`` or ``unit`` options are automatically assumed when in relevant UI state. +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.all > 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 +-- -]====] -local gui = require 'gui' -local dlg = require 'gui.dialogs' -local widgets = require 'gui.widgets' -local plugin = require 'plugins.rename' +Rename = defclass(Rename, widgets.Window) +Rename.ATTRS { + frame_title='Rename', + frame={w=89, h=43}, + resizable=true, + resize_min={w=61}, +} + +local function get_language_options() + local options, max_width = {}, 5 + 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 + return options, max_width +end -local mode = ... -local focus = dfhack.gui.getCurFocus() +local function pad_text(text, width) + return (' '):rep((width - #text)//2) .. text +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) + self.target = info.target + 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() -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')}, + widgets.Panel{frame={t=0, h=7}, -- header + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + key='CUSTOM_CTRL_N', + label='Select new target', + auto_width=true, + on_activate=function() + 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, + 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.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=self:callback('set_language'), + }, + widgets.Label{ + frame={t=6, l=7}, + text={'Name type: ', {pen=COLOR_CYAN, text=function() return df.language_name_type[self.target.type] end}}, + }, }, - frame = {b = 0, l = 0, r = 0, w = 70}, - } + }, + 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, l=0, w=30}, -- component selector + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Name components:', + }, + widgets.List{ + frame={t=2, l=0, b=4, w=ENGLISH_COL_WIDTH+2}, + view_id='component_list', + on_select=self:callback('refresh_list'), + choices=self:get_component_choices(), + row_height=3, + scroll_keys={}, + }, + widgets.List{ + 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, + scroll_keys={}, + }, + widgets.HotkeyLabel{ + frame={b=3, l=0}, + key='SECONDSCROLL_UP', + label='Prev component', + 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() + 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}, + key='CUSTOM_CTRL_D', + label='Randomize component', + on_activate=function() + local _, comp_choice = self.subviews.component_list:getSelected() + 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{ + 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, + 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=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=2, 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=2, 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=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}, + {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='words_list', + frame={t=4, l=0, b=0, r=0}, + on_submit=self:callback('set_component_word'), + }, + }, + }, + }, + }, } - -- calculate text_width once - self.subviews.controls:getTextWidth() -end - -function RenameDialog:getWantedFrameSize() - local x, y = self.super.getWantedFrameSize(self) - x = math.max(x, self.subviews.controls.text_width) - return x, y + 2 -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() -end - -local function verify_mode(expected) - if mode ~= nil and mode ~= expected then - qerror('Invalid UI state for mode '..mode) - end -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 + + -- replace the FilteredList's built-in EditField with our own + 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 + +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, + {gap=2, pen=COLOR_YELLOW, text=function() + local word = self.target.words[val] + if word < 0 then return 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_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 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[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}}) + table.insert(choices, {text='', data={fn=function() end}}) + end + return choices +end + +function Rename:clear_component_word(comp) + self.target.words[comp] = -1 + for _, sync_target in ipairs(self.sync_targets) do + if type(sync_target) == 'function' then + sync_target() + else + sync_target.words[comp] = -1 + end + end +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 + for _, sync_target in ipairs(self.sync_targets) do + if type(sync_target) == 'function' then + sync_target() + else + sync_target.first_name = self.target.first_name + end + end +end + +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[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 + 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 + if type(sync_target) == 'function' then + sync_target() + else + sync_target.language = val + sync_target.first_name = self.target.first_name + end + end +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)].data.idx) +end + +function Rename:randomize_component_word(comp) + 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() + 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 = { + [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 + 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 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 + 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 - ) - else - verify_mode('unit') - - local vname = dfhack.units.getVisibleName(unit) - local vnick = '' - if vname and vname.has_name then - vnick = vname.nickname end + end + + self.cache[comp] = choices + return choices +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() - showRenameDialog( - 'Rename Unit', - 'Enter a new nickname for the unit:', - vnick, - curry(dfhack.units.setNickname, unit) - ) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == DEFAULT_NIL then + self.subviews[sort_widget]:cycle() + return end -elseif mode then - verify_mode(nil) + 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 = clist: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 + +-- +-- RenameScreen +-- + +RenameScreen = defclass(RenameScreen, gui.ZScreen) +RenameScreen.ATTRS { + focus_path='rename', +} + +function RenameScreen:init(info) + self:addviews{ + Rename{ + target=info.target, + sync_targets=info.sync_targets or {}, + show_selector=info.show_selector, + } + } +end + +function RenameScreen:onDismiss() + view = nil +end + +-- +-- WorldRenameOverlay +-- + +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}, + visible=function() return dfhack.isWorldLoaded() end, +} + +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=WorldRenameOverlay, +} + +-- +-- CLI +-- + +if dfhack_flags.module then + return +end + +if not dfhack.isWorldLoaded() then + qerror('This script requires a world to be loaded') +end + +local function get_target(opts) + local target, sync_targets = nil, {} + if opts.histfig_id then + 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)) + 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, 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, sync_targets = get_world_target() + end + return target, sync_targets +end + +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, 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) + end + 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 + +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