Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[gui/gm-editor] support stringification of language_name fields #1315

Merged
merged 6 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 37 additions & 15 deletions docs/gui/gm-editor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand All @@ -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] <lua expression>``
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 [<options>] [<lua expression>]
gui/gm-editor [<options>] dialog

When specifying a lua expression, field prefixes of ``df.global`` can be
omitted.

Examples
--------
Expand All @@ -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
Expand All @@ -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
----------
Expand Down
76 changes: 41 additions & 35 deletions gui/gm-editor.lua
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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!!
Expand Down Expand Up @@ -183,7 +185,7 @@ function GmEditorUi:init(args)
local mainPage=widgets.Panel{
subviews={
mainList,
widgets.Label{text={{text="<no item>",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="<no item>",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'}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down