diff --git a/dfhack.init-example b/dfhack.init-example index 4af4246fc..f5f40196c 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -42,3 +42,6 @@ keybinding add Shift-I "job-material CINNABAR" keybinding add Shift-B "job-material COBALTITE" keybinding add Shift-O "job-material OBSIDIAN" keybinding add Shift-G "job-material GLASS_GREEN" + +# browse linked mechanisms +keybinding add Ctrl-M@dwarfmode/QueryBuilding/Some gui/mechanisms diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 112cc4fdc..b8c1248b3 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1180,6 +1180,28 @@ static int screen_isDismissed(lua_State *L) return 1; } +static int screen_doSimulateInput(lua_State *L) +{ + auto screen = Lua::CheckDFObject(L, 1); + luaL_checktype(L, 2, LUA_TTABLE); + + if (!screen) + luaL_argerror(L, 1, "NULL screen"); + + int sz = lua_rawlen(L, 2); + std::set keys; + + for (int j = 1; j <= sz; j++) + { + lua_rawgeti(L, 2, j); + keys.insert((df::interface_key)lua_tointeger(L, -1)); + lua_pop(L, 1); + } + + screen->feed(&keys); + return 0; +} + } static const luaL_Reg dfhack_screen_funcs[] = { @@ -1192,6 +1214,7 @@ static const luaL_Reg dfhack_screen_funcs[] = { { "show", &Lua::CallWithCatchWrapper }, { "dismiss", screen_dismiss }, { "isDismissed", screen_isDismissed }, + { "_doSimulateInput", screen_doSimulateInput }, { NULL, NULL } }; diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 0e4e4cc6e..5883a2eea 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -4,6 +4,42 @@ local _ENV = mkmodule('gui') local dscreen = dfhack.screen +CLEAR_PEN = {ch=32,fg=0,bg=0} + +function simulateInput(screen,...) + local keys = {} + local function push_key(arg) + local kv = arg + if type(arg) == 'string' then + kv = df.interface_key[arg] + if kv == nil then + error('Invalid keycode: '..arg) + end + end + if type(arg) == 'number' then + keys[#keys+1] = kv + end + end + for i = 1,select('#',...) do + local arg = select(i,...) + if arg ~= nil then + local t = type(arg) + if type(arg) == 'table' then + for k,v in pairs(arg) do + if v == true then + push_key(k) + else + push_key(v) + end + end + else + push_key(arg) + end + end + end + dscreen._doSimulateInput(screen, keys) +end + Screen = defclass(Screen, dfhack.screen) function Screen.new(attrs) @@ -26,6 +62,12 @@ function Screen:renderParent() end end +function Screen:inputToParent(...) + if self._native and self._native.parent then + simulateInput(self._native.parent, ...) + end +end + function paint_border(x1,y1,x2,y2,color,title) local pen = { ch = ' ', fg = COLOR_BLACK, bg = color } dscreen.fillRect(pen,x1,y1,x2,y1) @@ -40,20 +82,37 @@ function paint_border(x1,y1,x2,y2,color,title) end end -function Screen:renderFrame(color,title,width,height) +local function hint_coord(gap,hint) + if hint and hint > 0 then + return math.min(hint,gap) + elseif hint and hint < 0 then + return math.max(0,gap-hint) + else + return math.floor(gap/2) + end +end + +function mkdims_xy(x1,y1,x2,y2) + return { x1=x1, y1=y1, x2=x2, y2=y2, width=x2-x1+1, height=y2-y1+1 } +end +function mkdims_wh(x1,y1,w,h) + return { x1=x1, y1=y1, x2=x1+w-1, y2=y1+h-1, width=w, height=h } +end + +function Screen:renderFrame(color,title,width,height,xhint,yhint) local sw, sh = dscreen.getWindowSize() local iw, ih = sw-2, sh-2 width = math.min(width or iw, iw) height = math.min(height or ih, ih) local gw, gh = iw-width, ih-height - local x1, y1 = math.floor(gw/2), math.floor(gh/2) + local x1, y1 = hint_coord(gw,xhint), hint_coord(gh,yhint) local x2, y2 = x1+width+1, y1+height+1 if gw == 0 and gh == 0 then dscreen.clear() else self:renderParent() - dscreen.fillRect({ch=' ',fg=0,bg=0},x1,y1,x2,y2) + dscreen.fillRect(CLEAR_PEN,x1,y1,x2,y2) end paint_border(x1,y1,x2,y2,color,title) diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua new file mode 100644 index 000000000..2c62751ec --- /dev/null +++ b/library/lua/gui/dwarfmode.lua @@ -0,0 +1,119 @@ +-- Support for messing with the dwarfmode screen + +local _ENV = mkmodule('gui.dwarfmode') + +local gui = require('gui') +local dscreen = dfhack.screen + +AREA_MAP_WIDTH = 23 +MENU_WIDTH = 30 + +function getPanelLayout() + local sw, sh = dscreen.getWindowSize() + local view_height = sh-2 + local view_rb = sw-1 + local area_x2 = sw-AREA_MAP_WIDTH-2 + local menu_x2 = sw-MENU_WIDTH-2 + local menu_x1 = area_x2-MENU_WIDTH-1 + local area_pos = df.global.ui_area_map_width + local menu_pos = df.global.ui_menu_width + local rv = {} + + if area_pos < 3 then + rv.area_map = gui.mkdims_xy(area_x2+1,1,view_rb-1,view_height) + view_rb = area_x2 + end + if menu_pos < area_pos or df.global.ui.main.mode ~= 0 then + if menu_pos >= area_pos then + rv.menu_forced = true + menu_pos = area_pos-1 + end + local menu_x = menu_x2 + if menu_pos < 2 then menu_x = menu_x1 end + rv.menu = gui.mkdims_xy(menu_x+1,1,view_rb-1,view_height) + view_rb = menu_x + end + rv.area_pos = area_pos + rv.menu_pos = menu_pos + rv.map = gui.mkdims_xy(1,1,view_rb-1,view_height) + return rv +end + +function getViewportPos() + return { + x = df.global.window_x, + y = df.global.window_y, + z = df.global.window_z + } +end + +function getCursorPos() + return copyall(df.global.cursor) +end + +function setCursorPos(cursor) + df.global.cursor = cursor +end + +function setViewportPos(layout,view) + local map = df.global.world.map + df.global.window_x = math.max(0, math.min(view.x, map.x_count-layout.map.width)) + df.global.window_y = math.max(0, math.min(view.y, map.y_count-layout.map.height)) + df.global.window_z = math.max(0, math.min(view.z, map.z_count-1)) + return getViewportPos() +end + +function centerViewportOn(layout,target) + local mapsz = layout.map + local view = xyz2pos( + target.x-math.floor(layout.map.width/2), + target.y-math.floor(layout.map.height/2), + target.z + ) + return setViewportPos(layout, view) +end + +function isInViewport(layout,view,target,gap) + gap = gap or 0 + return target.x-gap >= view.x and target.x+gap < view.x+layout.map.width + and target.y-gap >= view.y and target.y+gap < view.y+layout.map.height + and target.z == view.z +end + +DwarfOverlay = defclass(DwarfOverlay, gui.Screen) + +function DwarfOverlay:updateLayout() + self.df_layout = getPanelLayout() + self.df_viewport = getViewportPos() + self.df_cursor = getCursorPos() +end + +local move_keys = { + 'CURSOR_UP', 'CURSOR_DOWN', 'CURSOR_LEFT', 'CURSOR_RIGHT', + 'CURSOR_UPLEFT', 'CURSOR_UPRIGHT', 'CURSOR_DOWNLEFT', 'CURSOR_DOWNRIGHT', + 'CURSOR_UP_FAST', 'CURSOR_DOWN_FAST', 'CURSOR_LEFT_FAST', 'CURSOR_RIGHT_FAST', + 'CURSOR_UPLEFT_FAST', 'CURSOR_UPRIGHT_FAST', 'CURSOR_DOWNLEFT_FAST', 'CURSOR_DOWNRIGHT_FAST', + 'CURSOR_UP_Z', 'CURSOR_DOWN_Z', 'CURSOR_UP_Z_AUX', 'CURSOR_DOWN_Z_AUX' +} + +function DwarfOverlay:propagateMoveKeys(keys) + for _,v in ipairs(move_keys) do + if keys[v] then + self:inputToParent(v) + return + end + end +end + +function DwarfOverlay:onIdle() + dscreen.invalidate() +end + +function DwarfOverlay:clearMenu() + local menu = self.df_layout.menu + if not menu then return nil end + dscreen.fillRect(gui.CLEAR_PEN,menu.x1,menu.y1,menu.x2,menu.y2) + return menu.x1,menu.y1,menu.width,menu.height +end + +return _ENV diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua new file mode 100644 index 000000000..8c585334f --- /dev/null +++ b/scripts/gui/mechanisms.lua @@ -0,0 +1,143 @@ +-- Shows mechanisms linked to the current building. + +local gui = require 'gui' +local guidm = require 'gui.dwarfmode' + +function getBuildingName(building) + return dfhack.with_temp_object( + df.new "string", + function(str) + building:getName(str) + return str.value + end + ) +end + +function listMechanismLinks(building) + local lst = {} + local function push(item, mode) + if item then + lst[#lst+1] = { obj = item, name = getBuildingName(item), mode = mode } + end + end + + push(building, 'self') + + if not df.building_actual:is_instance(building) then + return lst + end + + local item, tref, tgt + for _,v in ipairs(building.contained_items) do + item = v.item + if df.item_trappartsst:is_instance(item) then + tref = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_TRIGGER) + if tref then + push(tref:getBuilding(), 'trigger') + end + tref = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_TRIGGERTARGET) + if tref then + push(tref:getBuilding(), 'target') + end + end + end + + return lst +end + +MechanismList = defclass(MechanismList, guidm.DwarfOverlay) + +MechanismList.focus_path = 'mechanisms' + +function MechanismList.new(building) + local self = { + old_cursor = guidm.getCursorPos(), + links = listMechanismLinks(building), + selected = 1 + } + self.links[1].viewport = guidm.getViewportPos() + self.links[1].cursor = guidm.getCursorPos() + return mkinstance(MechanismList, self) +end + +local colors = { + self = COLOR_CYAN, trigger = COLOR_MAGENTA, target = COLOR_GREEN +} +local icons = { + self = 128, trigger = 17, target = 16 +} + +function MechanismList:onRender() + self:renderParent() + self:updateLayout() + local x,y,w,h = self:clearMenu() + + self.paintString({fg=COLOR_WHITE},x+1,y+1,"Mechanism Links") + + for i,v in ipairs(self.links) do + local pen = { fg=colors[v.mode], bold = (i == self.selected) } + self.paintTile(pen, x+2, y+2+i, icons[v.mode]) + self.paintString(pen, x+4, y+2+i, v.name) + end + + local nlinks = #self.links + local line = y+4+nlinks + + if nlinks <= 1 then + self.paintString({fg=COLOR_LIGHTRED},x+1,line,"This building has no links") + line = line+2 + end + + self.paintString({fg=COLOR_LIGHTGREEN},x+1,line,"Esc") + self.paintString({fg=COLOR_WHITE},x+4,line,": Back,") + self.paintString({fg=COLOR_LIGHTGREEN},x+12,line,"Enter") + self.paintString({fg=COLOR_WHITE},x+17,line,": Switch") +end + +function MechanismList:zoomToLink(link) + df.global.world.selected_building = link.obj + + local cursor = link.cursor + if not cursor then + cursor = xyz2pos(link.obj.centerx, link.obj.centery, link.obj.z) + end + guidm.setCursorPos(cursor) + + if not guidm.isInViewport(self.df_layout, self.df_viewport, cursor, 5) then + local vp = link.viewport + if vp then + guidm.setViewportPos(self.df_layout,vp) + else + guidm.centerViewportOn(self.df_layout,cursor) + end + end +end + +function MechanismList:changeSelected(delta) + self.selected = 1 + (self.selected + delta - 1) % #self.links + self:zoomToLink(self.links[self.selected]) +end + +function MechanismList:onInput(keys) + if keys.STANDARDSCROLL_UP or keys.SECONDSCROLL_UP then + self:changeSelected(-1) + elseif keys.STANDARDSCROLL_DOWN or keys.SECONDSCROLL_DOWN then + self:changeSelected(1) + elseif keys.LEAVESCREEN then + if self.selected ~= 1 then + self:zoomToLink(self.links[1]) + end + self:dismiss() + elseif keys.SELECT then + self:dismiss() + end +end + +if not df.viewscreen_dwarfmodest:is_instance(dfhack.gui.getCurViewscreen()) then + qerror("This script requires the main dwarfmode view") +end +if df.global.ui.main.mode ~= df.ui_sidebar_mode.QueryBuilding then + qerror("This script requires the 'q' interface mode") +end + +MechanismList.new(df.global.world.selected_building):show()