diff --git a/changelog.txt b/changelog.txt index b58d36042..c2fa9f442 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles +- `gui/gm-editor`: automatic display of semantic values for language_name fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. ## Removed diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 084b367ec..e2a34a488 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -16,9 +16,10 @@ Hold down :kbd:`Shift` and right click to exit, even if you are inspecting a substructure, no matter how deep. If you just want to browse without fear of accidentally changing anything, hit -:kbd:`Ctrl`:kbd:`D` to toggle read-only mode. If you want `gui/gm-editor` to -automatically pick up changes to game data in realtime, hit :kbd:`Alt`:kbd:`A` -to switch to auto update mode. +:kbd:`Ctrl`:kbd:`D` to toggle read-only mode. + +If you want `gui/gm-editor` to automatically pick up changes to game data in +realtime, hit :kbd:`Alt`:kbd:`A` to switch to auto update mode. .. warning:: @@ -28,18 +29,26 @@ to switch to auto update mode. your game before poking around in `gui/gm-editor`, especially if you are examining data while the game is unpaused. +.. warning:: + + Union data structures contain fields that occupy the same memory space. + When you see the ``[union structure]`` badge at the top of the screen, be + aware that only one of the fields in the structure is likely to make sense. + The "correct" field is usually indicated by some context in the parent + structure. If there are any pointers to substructures in the union, + inspecting the pointer when it is not the "correct" field may crash the + game. + Usage ----- -``gui/gm-editor [-f]`` - Open the editor on whatever is selected or viewed (e.g. unit/item/building/ - engraving/etc.) -``gui/gm-editor [-f] `` - Evaluate a lua expression and opens the editor on its results. Field - prefixes of ``df.global`` can be omitted. -``gui/gm-editor [-f] dialog`` - Show an in-game dialog to input the lua expression to evaluate. Works the - same as the version above. +:: + + gui/gm-editor [] [] + gui/gm-editor [] dialog + +When specifying a lua expression, field prefixes of ``df.global`` can be +omitted. Examples -------- @@ -48,15 +57,22 @@ Examples Opens the editor on the selected unit/item/job/workorder/stockpile etc. ``gui/gm-editor world.items.all`` Opens the editor on the items list. +``gui/gm-editor df.unit.find(12345)`` + Opens the editor on the unit with id 12345. +``gui/gm-editor reqscript('gui/quickfort').view`` + Opens the editor on a running instance of `gui/quickfort`. Useful for + debugging GUI tool state during development. ``gui/gm-editor --freeze scr`` Opens the editor on the current DF viewscreen data (bypassing any DFHack - layers) and prevents the underlying viewscreen from getting updates while - you have the editor open. + tools that may be open) and prevents the underlying viewscreen from getting + updates while you are inspecting the data. +``gui/gm-editor dialog`` + Show an in-game dialog to input the lua expression to evaluate. Options ------- -``-f``, ``--freeze`` +``-f``, ``--freeze``, ``--safe-mode`` Freeze the underlying viewscreen so that it does not receive any updates. This allows you to be sure that whatever you are inspecting or modifying will not be read or changed by the game until you are done with it. Note @@ -65,6 +81,12 @@ Options `gui/gm-editor` as usual when the game is frozen. The black background will disappear when the last `gui/gm-editor` window that was opened with the ``--freeze`` option is dismissed. +``--no-stringification`` + Don't attempt to provide helpful string representations of potentially + unsafe fields like language_name when browsing the data structures. Specify + this option when you know you will be browsing garbage data that could lead + to crashes if accessed for stringification. Note that fields in union data + structures are never stringified. Screenshot ---------- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 9ec7cdd2c..6d37f96c5 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -1,12 +1,13 @@ -- Interface powered memory object editor. --@module=true -local gui = require 'gui' -local json = require 'json' -local dialog = require 'gui.dialogs' -local widgets = require 'gui.widgets' -local guiScript = require 'gui.script' -local utils = require 'utils' +local argparse = require('argparse') +local gui = require('gui') +local json = require('json') +local dialog = require('gui.dialogs') +local widgets = require('gui.widgets') +local guiScript = require('gui.script') +local utils = require('utils') config = config or json.open('dfhack-config/gm-editor.json') @@ -124,7 +125,8 @@ GmEditorUi.ATTRS{ frame_inset=0, resizable=true, resize_min=RESIZE_MIN, - read_only=(config.data.read_only or false) + read_only=(config.data.read_only or false), + helpers=true, } function burning_red(input) -- todo does not work! bug angavrilov that so that he would add this, very important!! @@ -183,7 +185,7 @@ function GmEditorUi:init(args) local mainPage=widgets.Panel{ subviews={ mainList, - widgets.Label{text={{text="",id="name"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, + widgets.Label{text={{text="",id="name"},{text="",pen=COLOR_CYAN,id="union"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}} ,view_id='page_main'} @@ -621,27 +623,32 @@ function GmEditorUi:onInput(keys) end end -function getStringValue(trg,field) - local obj=trg.target +function GmEditorUi:getStringValue(trg, field) + local obj = trg.target local text=tostring(obj[field]) pcall(function() - if obj._field ~= nil then + if obj._field == nil then return end local f = obj:_field(field) - if df.coord:is_instance(f) then - text=('(%d, %d, %d) '):format(f.x, f.y, f.z) .. text - elseif df.coord2d:is_instance(f) then - text=('(%d, %d) '):format(f.x, f.y) .. text + if self.helpers and not obj._type._union then + if df.coord:is_instance(f) then + text=('(%d, %d, %d) %s'):format(f.x, f.y, f.z, text) + elseif df.coord2d:is_instance(f) then + text=('(%d, %d) %s'):format(f.x, f.y, text) + elseif df.language_name:is_instance(f) then + text=('%s (%s) %s'):format(dfhack.TranslateName(f, false), dfhack.TranslateName(f, true), text) + end end - local enum=f._type + local enum = f._type if enum._kind=="enum-type" then text=text.." ("..tostring(enum[obj[field]])..")" end + -- this will throw for types that have no ref target; pcall will catch it, but make sure this bit stays + -- at the end of the pcall function body local ref_target=f.ref_target if ref_target then text=text.. " (ref-target: "..getmetatable(ref_target)..")" end - end end) return text end @@ -681,10 +688,11 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) end end end + self.subviews.lbl_current_item:itemById('union').text = type(trg.target) == 'userdata' and trg.target._type._union and " [union structure]" or "" self.subviews.lbl_current_item:itemById('name').text=tostring(trg.target) local t={} for k,v in pairs(trg.keys) do - table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=2,text=getStringValue(trg,v)}}}) + table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=2,text=self:getStringValue(trg,v)}}}) end local last_selected, last_top if preserve_pos then @@ -824,7 +832,7 @@ function GmScreen:init(args) end end end - self:addviews{GmEditorUi{target=target}} + self:addviews{GmEditorUi{target=target, helpers=args.helpers}} views[self] = true end @@ -838,28 +846,26 @@ function GmScreen:onDismiss() end local function get_editor(args) - local freeze = false - if args[1] == '-f' or args[1] == '--freeze' then - freeze = true - table.remove(args, 1) - end - if #args~=0 then - if args[1]=="dialog" then - dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, + local freeze, helpers = false, true + local positionals = argparse.processArgsGetopt(args, { + {'f', 'freeze', 'safe-mode', handler=function() freeze = true end}, + {nil, 'no-stringification', handler=function() helpers = false end}, + }) + if #positionals == 0 then + GmScreen{freeze=freeze, helpers=helpers, target=getTargetFromScreens()}:show() + else + if positionals[1]=="dialog" then + dialog.showInputPrompt("GM Editor", "Object to edit:", COLOR_GRAY, "", function(entry) - GmScreen{freeze=freeze, target=eval(entry)}:show() + GmScreen{freeze=freeze, helpers=helpers, target=eval(entry)}:show() end) - elseif args[1]=="free" then - GmScreen{freeze=freeze, target=df.reinterpret_cast(df[args[2]],args[3])}:show() - elseif args[1]=="scr" then + elseif positionals[1]=="scr" then -- this will not work for more complicated expressions, like scr.fieldname, but -- it should capture the most common case - GmScreen{freeze=freeze, target=dfhack.gui.getDFViewscreen(true)}:show() + GmScreen{freeze=freeze, helpers=helpers, target=dfhack.gui.getDFViewscreen(true)}:show() else - GmScreen{freeze=freeze, target=eval(args[1])}:show() + GmScreen{freeze=freeze, helpers=helpers, target=eval(positionals[1])}:show() end - else - GmScreen{freeze=freeze, target=getTargetFromScreens()}:show() end end