471 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
			
		
		
	
	
			471 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Lua
		
	
| -- Interface powered item editor.
 | |
| 
 | |
| --[[=begin
 | |
| 
 | |
| gui/gm-editor
 | |
| =============
 | |
| There are three ways to open this editor:
 | |
| 
 | |
| * using gui/gm-editor command/keybinding - opens editor on what is selected
 | |
|   or viewed (e.g. unit/item description screen)
 | |
| 
 | |
| * using gui/gm-editor <lua command> - executes lua command and opens editor on
 | |
|   its results (e.g. ``gui/gm-editor "df.global.world.items.all"`` shows all items)
 | |
| 
 | |
| * using gui/gm-editor dialog - shows an in game dialog to input lua command. Works
 | |
|   the same as version above.
 | |
| 
 | |
| .. image:: /docs/images/gm-editor.png
 | |
| 
 | |
| This editor allows to change and modify almost anything in df. Press :kbd:`?` for
 | |
| in-game help.
 | |
| 
 | |
| =end]]
 | |
| local gui = require 'gui'
 | |
| local dialog = require 'gui.dialogs'
 | |
| local widgets =require 'gui.widgets'
 | |
| local guiScript = require 'gui.script'
 | |
| local args={...}
 | |
| 
 | |
| local keybindings={
 | |
|     offset={key="CUSTOM_ALT_O",desc="Show current items offset"},
 | |
|     find={key="CUSTOM_F",desc="Find a value by entering a predicate"},
 | |
|     lua_set={key="CUSTOM_ALT_S",desc="Set by using a lua function"},
 | |
|     insert={key="CUSTOM_ALT_I",desc="Insert a new value to the vector"},
 | |
|     delete={key="CUSTOM_ALT_D",desc="Delete selected entry"},
 | |
|     reinterpret={key="CUSTOM_ALT_R",desc="Open selected entry as something else"},
 | |
|     start_filter={key="CUSTOM_S",desc="Start typing filter, Enter to finish"},
 | |
|     help={key="HELP",desc="Show this help"},
 | |
|     displace={key="STRING_A093",desc="Open reference offseted by index"},
 | |
|     NOT_USED={key="SEC_SELECT",desc="Edit selected entry as a number (for enums)"}, --not a binding...
 | |
| }
 | |
| function getTargetFromScreens()
 | |
|     local my_trg
 | |
|     if dfhack.gui.getCurFocus() == 'item' then
 | |
|         my_trg=dfhack.gui.getCurViewscreen().item
 | |
|     elseif dfhack.gui.getCurFocus() == 'joblist' then
 | |
|         local t_screen=dfhack.gui.getCurViewscreen()
 | |
|         my_trg=t_screen.jobs[t_screen.cursor_pos]
 | |
|     elseif dfhack.gui.getCurFocus() == 'createquota' then
 | |
|         local t_screen=dfhack.gui.getCurViewscreen()
 | |
|         my_trg=t_screen.orders[t_screen.sel_idx]
 | |
|     elseif dfhack.gui.getCurFocus() == 'dwarfmode/LookAround/Flow' then
 | |
|         local t_look=df.global.ui_look_list.items[df.global.ui_look_cursor]
 | |
|         my_trg=t_look.flow
 | |
| 
 | |
|     elseif dfhack.gui.getSelectedUnit(true) then
 | |
|         my_trg=dfhack.gui.getSelectedUnit(true)
 | |
|     elseif dfhack.gui.getSelectedItem(true) then
 | |
|         my_trg=dfhack.gui.getSelectedItem(true)
 | |
|     elseif dfhack.gui.getSelectedJob(true) then
 | |
|         my_trg=dfhack.gui.getSelectedJob(true)
 | |
|     else
 | |
|         qerror("No valid target found")
 | |
|     end
 | |
|     return my_trg
 | |
| end
 | |
| 
 | |
| GmEditorUi = defclass(GmEditorUi, gui.FramedScreen)
 | |
| GmEditorUi.ATTRS={
 | |
|     frame_style = gui.GREY_LINE_FRAME,
 | |
|     frame_title = "GameMaster's editor",
 | |
|     }
 | |
| function GmEditorUi:onHelp()
 | |
|     self.subviews.pages:setSelected(2)
 | |
| end
 | |
| function burning_red(input) -- todo does not work! bug angavrilov that so that he would add this, very important!!
 | |
|     local col=COLOR_LIGHTRED
 | |
|     return {text=input,pen=dfhack.pen.parse{fg=COLOR_LIGHTRED,bg=0}}
 | |
| end
 | |
| function Disclaimer(tlb)
 | |
|     local dsc={"Association Of ",{text="Psychic ",pen=dfhack.pen.parse{fg=COLOR_YELLOW,bg=0}},
 | |
|         "Dwarves (AOPD) is not responsible for all the damage",NEWLINE,"that this tool can (and will) cause to you and your loved dwarves",NEWLINE,"and/or saves.Please use with caution.",NEWLINE,{text="Magma not included.",pen=dfhack.pen.parse{fg=COLOR_LIGHTRED,bg=0}}}
 | |
|     if tlb then
 | |
|         for _,v in ipairs(dsc) do
 | |
|             table.insert(tlb,v)
 | |
|         end
 | |
|     end
 | |
|     return dsc
 | |
| end
 | |
| 
 | |
| function GmEditorUi:init(args)
 | |
|     self.stack={}
 | |
|     self.item_count=0
 | |
|     self.keys={}
 | |
|     local helptext={{text="Help"},NEWLINE,NEWLINE}
 | |
|     for k,v in pairs(keybindings) do
 | |
|         table.insert(helptext,{text=v.desc,key=v.key,key_sep=': '})
 | |
|         table.insert(helptext,NEWLINE)
 | |
|     end
 | |
|     table.insert(helptext,NEWLINE)
 | |
|     Disclaimer(helptext)
 | |
| 
 | |
|     local helpPage=widgets.Panel{
 | |
|         subviews={widgets.Label{text=helptext,frame = {l=1,t=1,yalign=0}}}}
 | |
|     local mainList=widgets.List{view_id="list_main",choices={},frame = {l=1,t=3,yalign=0},on_submit=self:callback("editSelected"),
 | |
|         on_submit2=self:callback("editSelectedRaw"),
 | |
|         text_pen=dfhack.pen.parse{fg=COLOR_DARKGRAY,bg=0},cursor_pen=dfhack.pen.parse{fg=COLOR_YELLOW,bg=0}}
 | |
|     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="Search",key=keybindings.start_filter.key,key_sep = '()'},{text=": "}},frame={l=1,t=2}},
 | |
|             widgets.EditField{frame={l=12,t=2},active=false,on_change=self:callback('text_input'),on_submit=self:callback("enable_input",false),view_id="filter_input"},
 | |
|             --widgets.Label{text="BLAH2"}
 | |
|                 }
 | |
|         ,view_id='page_main'}
 | |
| 
 | |
|     local pages=widgets.Pages{subviews={mainPage,helpPage},view_id="pages"}
 | |
|     self:addviews{
 | |
|         pages
 | |
|     }
 | |
|     self:pushTarget(args.target)
 | |
| end
 | |
| function GmEditorUi:text_input(new_text)
 | |
|     self:updateTarget(true,true)
 | |
| end
 | |
| function GmEditorUi:enable_input(enable)
 | |
|     self.subviews.filter_input.active=enable
 | |
| end
 | |
| function GmEditorUi:find(test)
 | |
|     local trg=self:currentTarget()
 | |
| 
 | |
|     if test== nil then
 | |
|         dialog.showInputPrompt("Test function","Input function that tests(k,v as argument):",COLOR_WHITE,"",dfhack.curry(self.find,self))
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     local e,what=load("return function(k,v) return "..test.." end")
 | |
|     if e==nil then
 | |
|         dialog.showMessage("Error!","function failed to compile\n"..what,COLOR_RED)
 | |
|     end
 | |
| 
 | |
|     if trg.target and trg.target._kind and trg.target._kind=="container" then
 | |
| 
 | |
|         for k,v in pairs(trg.target) do
 | |
|             if e()(k,v)==true then
 | |
|                 self:pushTarget(v)
 | |
|                 return
 | |
|             end
 | |
|         end
 | |
|     else
 | |
|         local i=1
 | |
|         for k,v in pairs(trg.target) do
 | |
|             if e()(k,v)==true then
 | |
|                 self.subviews.list_main:setSelected(i)
 | |
|                 return
 | |
|             end
 | |
|             i=i+1
 | |
|         end
 | |
|     end
 | |
| end
 | |
| function GmEditorUi:insertNew(typename)
 | |
|     local tp=typename
 | |
|     if typename== nil then
 | |
|         dialog.showInputPrompt("Class type","Input class type:",COLOR_WHITE,"",self:callback("insertNew"))
 | |
|         return
 | |
|     end
 | |
|     local ntype=df[tp]
 | |
|     if ntype== nil then
 | |
|         dialog.showMessage("Error!","Type '"..tp.." not found",COLOR_RED)
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     local trg=self:currentTarget()
 | |
|     if trg.target and trg.target._kind and trg.target._kind=="container" then
 | |
|         local thing=ntype:new()
 | |
|         dfhack.call_with_finalizer(1,false,df.delete,thing,function (tscreen,target,to_insert)
 | |
|             target:insert("#",to_insert); tscreen:updateTarget(true,true);end,self,trg.target,thing)
 | |
| 
 | |
|     end
 | |
| end
 | |
| function GmEditorUi:deleteSelected(key)
 | |
|     local trg=self:currentTarget()
 | |
|     if trg.target and trg.target._kind and trg.target._kind=="container" then
 | |
|         trg.target:erase(key)
 | |
|         self:updateTarget(true,true)
 | |
|     end
 | |
| end
 | |
| function GmEditorUi:getSelectedKey()
 | |
|     return self:currentTarget().keys[self.subviews.list_main:getSelected()]
 | |
| end
 | |
| function GmEditorUi:currentTarget()
 | |
|     return self.stack[#self.stack]
 | |
| end
 | |
| function GmEditorUi:getSelectedEnumType()
 | |
|     local trg=self:currentTarget()
 | |
|     local trg_key=trg.keys[self.subviews.list_main:getSelected()]
 | |
|     if trg.target._field==nil then return nil end
 | |
|     if trg.target:_field(trg_key)==nil then return nil end
 | |
|     local enum=trg.target:_field(trg_key)._type
 | |
|     if enum._kind=="enum-type" then
 | |
|         return enum
 | |
|     else
 | |
|         return nil
 | |
|     end
 | |
| end
 | |
| function GmEditorUi:editSelectedEnum(index,choice)
 | |
|     local enum=self:getSelectedEnumType()
 | |
|     if enum then
 | |
|         local trg=self:currentTarget()
 | |
|         local trg_key=self:getSelectedKey()
 | |
|         local list={}
 | |
|         for i=enum._first_item, enum._last_item do
 | |
|             table.insert(list,{text=('%s (%i)'):format(tostring(enum[i]), i),value=i})
 | |
|         end
 | |
|         guiScript.start(function()
 | |
|             local ret,idx,choice=guiScript.showListPrompt("Choose item:",nil,3,list,nil,true)
 | |
|             if ret then
 | |
|                 trg.target[trg_key]=choice.value
 | |
|                 self:updateTarget(true)
 | |
|             end
 | |
|         end)
 | |
| 
 | |
|     else
 | |
|         qerror("not an enum")
 | |
|     end
 | |
| end
 | |
| function GmEditorUi:openReinterpret(key)
 | |
|     local trg=self:currentTarget()
 | |
|     dialog.showInputPrompt(tostring(trg_key),"Enter new type:",COLOR_WHITE,
 | |
|                 "",function(choice)
 | |
|                     local ntype=df[tp]
 | |
|                     self:pushTarget(df.reinterpret_cast(ntype,trg.target[key]))
 | |
|                 end)
 | |
| end
 | |
| function GmEditorUi:openOffseted(index,choice)
 | |
|     local trg=self:currentTarget()
 | |
|     local trg_key=trg.keys[index]
 | |
| 
 | |
|     dialog.showInputPrompt(tostring(trg_key),"Enter offset:",COLOR_WHITE,"",
 | |
|         function(choice)
 | |
|             self:pushTarget(trg.target[trg_key]:_displace(tonumber(choice)))
 | |
|         end)
 | |
| end
 | |
| function GmEditorUi:editSelectedRaw(index,choice)
 | |
|     self:editSelected(index, choice, {raw=true})
 | |
| end
 | |
| function GmEditorUi:editSelected(index,choice,opts)
 | |
|     opts = opts or {}
 | |
|     local trg=self:currentTarget()
 | |
|     local trg_key=trg.keys[index]
 | |
|     if trg.target and trg.target._kind and trg.target._kind=="bitfield" then
 | |
|         trg.target[trg_key]= not trg.target[trg_key]
 | |
|         self:updateTarget(true)
 | |
|     else
 | |
|         --print(type(trg.target[trg.keys[trg.selected]]),trg.target[trg.keys[trg.selected]]._kind or "")
 | |
|         local trg_type=type(trg.target[trg_key])
 | |
|         if self:getSelectedEnumType() and not opts.raw then
 | |
|             self:editSelectedEnum()
 | |
|         elseif trg_type=='number' or trg_type=='string' then --ugly TODO: add metatable get selected
 | |
|             dialog.showInputPrompt(tostring(trg_key),"Enter new value:",COLOR_WHITE,
 | |
|                 tostring(trg.target[trg_key]),self:callback("commitEdit",trg_key))
 | |
| 
 | |
|         elseif trg_type == 'boolean' then
 | |
|             trg.target[trg_key] = not trg.target[trg_key]
 | |
|             self:updateTarget(true)
 | |
|         elseif trg_type == 'userdata' or trg_type == 'table' then
 | |
|             self:pushTarget(trg.target[trg_key])
 | |
|         elseif trg_type == 'nil' or trg_type == 'function' then
 | |
|             -- ignore
 | |
|         else
 | |
|             print("Unknown type:"..trg_type)
 | |
|             pcall(function() print("Subtype:"..tostring(trg.target[trg_key]._kind)) end)
 | |
|         end
 | |
|     end
 | |
| end
 | |
| 
 | |
| function GmEditorUi:commitEdit(key,value)
 | |
|     local trg=self:currentTarget()
 | |
|     if type(trg.target[key])=='number' then
 | |
|         trg.target[key]=tonumber(value)
 | |
|     elseif type(trg.target[key])=='string' then
 | |
|         trg.target[key]=value
 | |
|     end
 | |
|     self:updateTarget(true)
 | |
| end
 | |
| 
 | |
| function GmEditorUi:set(key,input)
 | |
|     local trg=self:currentTarget()
 | |
| 
 | |
|     if input== nil then
 | |
|         dialog.showInputPrompt("Set to what?","Lua code to set to (v cur target):",COLOR_WHITE,"",self:callback("set",key))
 | |
|         return
 | |
|     end
 | |
|     local e,what=load("return function(v) return "..input.." end")
 | |
|     if e==nil then
 | |
|         dialog.showMessage("Error!","function failed to compile\n"..what,COLOR_RED)
 | |
|         return
 | |
|     end
 | |
|     trg.target[key]=e()(trg)
 | |
|     self:updateTarget(true)
 | |
| end
 | |
| function GmEditorUi:onInput(keys)
 | |
|     if keys.LEAVESCREEN  then
 | |
|         if self.subviews.filter_input.active then
 | |
|             self:enable_input(false)
 | |
|             return
 | |
|         end
 | |
|         if self.subviews.pages:getSelected()==2 then
 | |
|             self.subviews.pages:setSelected(1)
 | |
|         else
 | |
|             self:popTarget()
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     if self.subviews.filter_input.active then
 | |
|         self.super.onInput(self,keys)
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     if keys[keybindings.offset.key] then
 | |
|         local trg=self:currentTarget()
 | |
|         local _,stoff=df.sizeof(trg.target)
 | |
|         local size,off=df.sizeof(trg.target:_field(self:getSelectedKey()))
 | |
|         dialog.showMessage("Offset",string.format("Size hex=%x,%x dec=%d,%d\nRelative hex=%x dec=%d",size,off,size,off,off-stoff,off-stoff),COLOR_WHITE)
 | |
|     elseif keys[keybindings.displace.key] then
 | |
|         self:openOffseted(self.subviews.list_main:getSelected())
 | |
|     elseif keys[keybindings.find.key] then
 | |
|         self:find()
 | |
|     elseif keys[keybindings.lua_set.key] then
 | |
|         self:set(self:getSelectedKey())
 | |
|     elseif keys[keybindings.insert.key] then --insert
 | |
|         self:insertNew()
 | |
|     elseif keys[keybindings.delete.key] then --delete
 | |
|         self:deleteSelected(self:getSelectedKey())
 | |
|     elseif keys[keybindings.reinterpret.key] then
 | |
|         self:openReinterpret(self:getSelectedKey())
 | |
|     elseif keys[keybindings.start_filter.key] then
 | |
|         self:enable_input(true)
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     self.super.onInput(self,keys)
 | |
| end
 | |
| function getStringValue(trg,field)
 | |
|     local obj=trg.target
 | |
| 
 | |
|     local text=tostring(obj[field])
 | |
|     pcall(function()
 | |
|     if obj._field ~= nil then
 | |
|         local enum=obj:_field(field)._type
 | |
|         if enum._kind=="enum-type" then
 | |
|             text=text.." ("..tostring(enum[obj[field]])..")"
 | |
|         end
 | |
|     end
 | |
|     end)
 | |
|     return text
 | |
| end
 | |
| function GmEditorUi:updateTarget(preserve_pos,reindex)
 | |
|     local trg=self:currentTarget()
 | |
|     local filter=self.subviews.filter_input.text
 | |
| 
 | |
|     if reindex then
 | |
|         trg.keys={}
 | |
|         for k,v in pairs(trg.target) do
 | |
|             if filter~= "" then
 | |
|                 local ok,ret=dfhack.pcall(string.match,tostring(k),filter)
 | |
|                 if not ok then
 | |
|                     table.insert(trg.keys,k)
 | |
|                 elseif ret then
 | |
|                     table.insert(trg.keys,k)
 | |
|                 end
 | |
|             else
 | |
|                 table.insert(trg.keys,k)
 | |
|             end
 | |
|         end
 | |
|     end
 | |
|     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("%-25s",tostring(v))},{gap=1,text=getStringValue(trg,v)}}})
 | |
|     end
 | |
|     local last_pos
 | |
|     if preserve_pos then
 | |
|         last_pos=self.subviews.list_main:getSelected()
 | |
|     end
 | |
|     self.subviews.list_main:setChoices(t)
 | |
|     if last_pos then
 | |
|         self.subviews.list_main:setSelected(last_pos)
 | |
|     else
 | |
|         self.subviews.list_main:setSelected(trg.selected)
 | |
|     end
 | |
| end
 | |
| function GmEditorUi:pushTarget(target_to_push)
 | |
|     local new_tbl={}
 | |
|     new_tbl.target=target_to_push
 | |
|     new_tbl.keys={}
 | |
|     new_tbl.selected=1
 | |
|     new_tbl.filter=""
 | |
|     if self:currentTarget()~=nil then
 | |
|         self:currentTarget().selected=self.subviews.list_main:getSelected()
 | |
|         self.stack[#self.stack].filter=self.subviews.filter_input.text
 | |
|     end
 | |
|     for k,v in pairs(target_to_push) do
 | |
|         table.insert(new_tbl.keys,k)
 | |
|     end
 | |
|     new_tbl.item_count=#new_tbl.keys
 | |
|     table.insert(self.stack,new_tbl)
 | |
|     self.subviews.filter_input.text=""
 | |
|     self:updateTarget()
 | |
| end
 | |
| function GmEditorUi:popTarget()
 | |
|     table.remove(self.stack) --removes last element
 | |
|     if #self.stack==0 then
 | |
|         self:dismiss()
 | |
|         return
 | |
|     end
 | |
|     self.subviews.filter_input.text=self.stack[#self.stack].filter --restore filter
 | |
|     self:updateTarget()
 | |
| end
 | |
| function show_editor(trg)
 | |
|     if not trg then
 | |
|         qerror('Target not found')
 | |
|     end
 | |
|     local screen = GmEditorUi{target=trg}
 | |
|     screen:show()
 | |
| end
 | |
| eval_env = {}
 | |
| setmetatable(eval_env, {__index = function(_, k)
 | |
|     if k == 'scr' or k == 'screen' then
 | |
|         return dfhack.gui.getCurViewscreen()
 | |
|     elseif k == 'bld' or k == 'building' then
 | |
|         return dfhack.gui.getSelectedBuilding()
 | |
|     elseif k == 'item' then
 | |
|         return dfhack.gui.getSelectedItem()
 | |
|     elseif k == 'job' then
 | |
|         return dfhack.gui.getSelectedJob()
 | |
|     elseif k == 'wsjob' or k == 'workshop_job' then
 | |
|         return dfhack.gui.getSelectedWorkshopJob()
 | |
|     elseif k == 'unit' then
 | |
|         return dfhack.gui.getSelectedUnit()
 | |
|     else
 | |
|         for g in pairs(df.global) do
 | |
|             if g == k then
 | |
|                 return df.global[k]
 | |
|             end
 | |
|         end
 | |
|         return _G[k]
 | |
|     end
 | |
| end})
 | |
| function eval(s)
 | |
|     local f, err = load("return " .. s, "expression", "t", eval_env)
 | |
|     if err then qerror(err) end
 | |
|     return f()
 | |
| end
 | |
| if #args~=0 then
 | |
|     if args[1]=="dialog" then
 | |
|         function thunk(entry)
 | |
|             show_editor(eval(entry))
 | |
|         end
 | |
|         dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, "",thunk)
 | |
|     elseif args[1]=="free" then
 | |
|         show_editor(df.reinterpret_cast(df[args[2]],args[3]))
 | |
|     else
 | |
|         show_editor(eval(args[1]))
 | |
|     end
 | |
| else
 | |
|     show_editor(getTargetFromScreens())
 | |
| end
 | |
| 
 |