summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/lua/class.lua150
-rw-r--r--library/lua/dfhack.lua24
-rw-r--r--library/lua/gui.lua33
-rw-r--r--library/lua/gui/dialogs.lua99
-rw-r--r--library/lua/gui/dwarfmode.lua4
-rw-r--r--scripts/gui/hello-world.lua20
-rw-r--r--scripts/gui/liquids.lua25
-rw-r--r--scripts/gui/mechanisms.lua10
-rw-r--r--scripts/gui/power-meter.lua8
-rw-r--r--scripts/gui/room-list.lua23
-rw-r--r--scripts/gui/siege-engine.lua39
11 files changed, 285 insertions, 150 deletions
diff --git a/library/lua/class.lua b/library/lua/class.lua
new file mode 100644
index 00000000..7b142e49
--- /dev/null
+++ b/library/lua/class.lua
@@ -0,0 +1,150 @@
+-- A trivial reloadable class system
+
+local _ENV = mkmodule('class')
+
+-- Metatable template for a class
+class_obj = {} or class_obj
+
+-- Methods shared by all classes
+common_methods = {} or common_methods
+
+-- Forbidden names for class fields and methods.
+reserved_names = { super = true, ATTRS = true }
+
+-- Attribute table metatable
+attrs_meta = {} or attrs_meta
+
+-- Create or updates a class; a class has metamethods and thus own metatable.
+function defclass(class,parent)
+ class = class or {}
+
+ local meta = getmetatable(class)
+ if not meta then
+ meta = {}
+ setmetatable(class, meta)
+ end
+
+ for k,v in pairs(class_obj) do meta[k] = v end
+
+ meta.__index = parent or common_methods
+
+ local attrs = rawget(class, 'ATTRS') or {}
+ setmetatable(attrs, attrs_meta)
+
+ rawset(class, 'super', parent)
+ rawset(class, 'ATTRS', attrs)
+ rawset(class, '__index', rawget(class, '__index') or class)
+
+ return class
+end
+
+-- An instance uses the class as metatable
+function mkinstance(class,table)
+ table = table or {}
+ setmetatable(table, class)
+ return table
+end
+
+-- Patch the stubs in the global environment
+dfhack.BASE_G.defclass = _ENV.defclass
+dfhack.BASE_G.mkinstance = _ENV.mkinstance
+
+-- Just verify the name, and then set.
+function class_obj:__newindex(name,val)
+ if reserved_names[name] or common_methods[name] then
+ error('Method name '..name..' is reserved.')
+ end
+ rawset(self, name, val)
+end
+
+function attrs_meta:__call(attrs)
+ for k,v in pairs(attrs) do
+ self[k] = v
+ end
+end
+
+local function apply_attrs(obj, attrs, init_table)
+ for k,v in pairs(attrs) do
+ if v == DEFAULT_NIL then
+ v = nil
+ end
+ obj[k] = init_table[k] or v
+ end
+end
+
+local function invoke_before_rec(self, class, method, ...)
+ local meta = getmetatable(class)
+ if meta then
+ local fun = rawget(class, method)
+ if fun then
+ fun(self, ...)
+ end
+
+ invoke_before_rec(self, meta.__index, method, ...)
+ end
+end
+
+local function invoke_after_rec(self, class, method, ...)
+ local meta = getmetatable(class)
+ if meta then
+ invoke_after_rec(self, meta.__index, method, ...)
+
+ local fun = rawget(class, method)
+ if fun then
+ fun(self, ...)
+ end
+ end
+end
+
+local function init_attrs_rec(obj, class, init_table)
+ local meta = getmetatable(class)
+ if meta then
+ init_attrs_rec(obj, meta.__index, init_table)
+ apply_attrs(obj, rawget(class, 'ATTRS'), init_table)
+ end
+end
+
+-- Call metamethod constructs the object
+function class_obj:__call(init_table)
+ -- The table is assumed to be a scratch temporary.
+ -- If it is not, copy it yourself before calling.
+ init_table = init_table or {}
+
+ local obj = mkinstance(self)
+
+ -- This initialization sequence is broadly based on how the
+ -- Common Lisp initialize-instance generic function works.
+
+ -- preinit screens input arguments in subclass to superclass order
+ invoke_before_rec(obj, self, 'preinit', init_table)
+ -- initialize the instance table from init table
+ init_attrs_rec(obj, self, init_table)
+ -- init in superclass -> subclass
+ invoke_after_rec(obj, self, 'init', init_table)
+ -- postinit in superclass -> subclass
+ invoke_after_rec(obj, self, 'postinit', init_table)
+
+ return obj
+end
+
+-- Common methods for all instances:
+
+function common_methods:callback(method, ...)
+ return dfhack.curry(self[method], self, ...)
+end
+
+function common_methods:assign(data)
+ for k,v in pairs(data) do
+ self[k] = v
+ end
+end
+
+function common_methods:invoke_before(method, ...)
+ return invoke_before_rec(self, getmetatable(self), method, ...)
+end
+
+function common_methods:invoke_after(method, ...)
+ return invoke_after_rec(self, getmetatable(self), method, ...)
+end
+
+return _ENV
diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua
index baf0d42e..ce3be5a8 100644
--- a/library/lua/dfhack.lua
+++ b/library/lua/dfhack.lua
@@ -113,26 +113,14 @@ function rawset_default(target,source)
end
end
-function defclass(class,parent)
- class = class or {}
- rawset_default(class, { __index = class })
- if parent then
- setmetatable(class, parent)
- else
- rawset_default(class, {
- init_fields = rawset_default,
- callback = function(self, name, ...)
- return dfhack.curry(self[name], self, ...)
- end
- })
- end
- return class
+DEFAULT_NIL = DEFAULT_NIL or {} -- Unique token
+
+function defclass(...)
+ return require('class').defclass(...)
end
-function mkinstance(class,table)
- table = table or {}
- setmetatable(table, class)
- return table
+function mkinstance(...)
+ return require('class').mkinstance(...)
end
-- Misc functions
diff --git a/library/lua/gui.lua b/library/lua/gui.lua
index f9b6ab6d..6eaa9860 100644
--- a/library/lua/gui.lua
+++ b/library/lua/gui.lua
@@ -74,18 +74,23 @@ end
Painter = defclass(Painter, nil)
-function Painter.new(rect, pen)
- rect = rect or mkdims_wh(0,0,dscreen.getWindowSize())
- local self = {
- x1 = rect.x1, clip_x1 = rect.x1,
- y1 = rect.y1, clip_y1 = rect.y1,
- x2 = rect.x2, clip_x2 = rect.x2,
- y2 = rect.y2, clip_y2 = rect.y2,
+function Painter:init(args)
+ local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
+ local crect = args.clip_rect or rect
+ self:assign{
+ x = rect.x1, y = rect.y1,
+ x1 = rect.x1, clip_x1 = crect.x1,
+ y1 = rect.y1, clip_y1 = crect.y1,
+ x2 = rect.x2, clip_x2 = crect.x2,
+ y2 = rect.y2, clip_y2 = crect.y2,
width = rect.x2-rect.x1+1,
height = rect.y2-rect.y1+1,
- cur_pen = to_pen(nil, pen or COLOR_GREY)
+ cur_pen = to_pen(nil, args.pen or COLOR_GREY)
}
- return mkinstance(Painter, self):seek(0,0)
+end
+
+function Painter.new(rect, pen)
+ return Painter{ rect = rect, pen = pen }
end
function Painter:isValidPos()
@@ -213,9 +218,8 @@ Screen = defclass(Screen)
Screen.text_input_mode = false
-function Screen:init()
+function Screen:postinit()
self:updateLayout()
- return self
end
Screen.isDismissed = dscreen.isDismissed
@@ -344,7 +348,12 @@ end
FramedScreen = defclass(FramedScreen, Screen)
-FramedScreen.frame_style = BOUNDARY_FRAME
+FramedScreen.ATTRS{
+ frame_style = BOUNDARY_FRAME,
+ frame_title = DEFAULT_NIL,
+ frame_width = DEFAULT_NIL,
+ frame_height = DEFAULT_NIL,
+}
local function hint_coord(gap,hint)
if hint and hint > 0 then
diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua
index eb883465..b1a96a55 100644
--- a/library/lua/gui/dialogs.lua
+++ b/library/lua/gui/dialogs.lua
@@ -10,24 +10,21 @@ local dscreen = dfhack.screen
MessageBox = defclass(MessageBox, gui.FramedScreen)
MessageBox.focus_path = 'MessageBox'
-MessageBox.frame_style = gui.GREY_LINE_FRAME
-
-function MessageBox:init(info)
- info = info or {}
- self:init_fields{
- text = info.text or {},
- frame_title = info.title,
- frame_width = info.frame_width,
- on_accept = info.on_accept,
- on_cancel = info.on_cancel,
- on_close = info.on_close,
- text_pen = info.text_pen
- }
- if type(self.text) == 'string' then
- self.text = utils.split_string(self.text, "\n")
+
+MessageBox.ATTRS{
+ frame_style = gui.GREY_LINE_FRAME,
+ -- new attrs
+ text = {},
+ on_accept = DEFAULT_NIL,
+ on_cancel = DEFAULT_NIL,
+ on_close = DEFAULT_NIL,
+ text_pen = DEFAULT_NIL,
+}
+
+function MessageBox:preinit(info)
+ if type(info.text) == 'string' then
+ info.text = utils.split_string(info.text, "\n")
end
- gui.FramedScreen.init(self, info)
- return self
end
function MessageBox:getWantedFrameSize()
@@ -82,9 +79,8 @@ function MessageBox:onInput(keys)
end
function showMessage(title, text, tcolor, on_close)
- mkinstance(MessageBox):init{
- text = text,
- title = title,
+ MessageBox{
+ frame_title = title,
text = text,
text_pen = tcolor,
on_close = on_close
@@ -92,8 +88,8 @@ function showMessage(title, text, tcolor, on_close)
end
function showYesNoPrompt(title, text, tcolor, on_accept, on_cancel)
- mkinstance(MessageBox):init{
- title = title,
+ MessageBox{
+ frame_title = title,
text = text,
text_pen = tcolor,
on_accept = on_accept,
@@ -105,25 +101,23 @@ InputBox = defclass(InputBox, MessageBox)
InputBox.focus_path = 'InputBox'
-function InputBox:init(info)
- info = info or {}
- self:init_fields{
- input = info.input or '',
- input_pen = info.input_pen,
- on_input = info.on_input,
- }
- MessageBox.init(self, info)
- self.on_accept = nil
- return self
+InputBox.ATTRS{
+ input = '',
+ input_pen = DEFAULT_NIL,
+ on_input = DEFAULT_NIL,
+}
+
+function InputBox:preinit(info)
+ info.on_accept = nil
end
function InputBox:getWantedFrameSize()
- local mw, mh = MessageBox.getWantedFrameSize(self)
+ local mw, mh = InputBox.super.getWantedFrameSize(self)
return mw, mh+2
end
function InputBox:onRenderBody(dc)
- MessageBox.onRenderBody(self, dc)
+ InputBox.super.onRenderBody(self, dc)
dc:newline(1)
dc:pen(self.input_pen or COLOR_LIGHTCYAN)
@@ -161,8 +155,8 @@ function InputBox:onInput(keys)
end
function showInputPrompt(title, text, tcolor, input, on_input, on_cancel, min_width)
- mkinstance(InputBox):init{
- title = title,
+ InputBox{
+ frame_title = title,
text = text,
text_pen = tcolor,
input = input,
@@ -176,27 +170,28 @@ ListBox = defclass(ListBox, MessageBox)
ListBox.focus_path = 'ListBox'
+ListBox.ATTRS{
+ selection = 0,
+ choices = {},
+ select_pen = DEFAULT_NIL,
+ on_input = DEFAULT_NIL
+}
+
+function InputBox:preinit(info)
+ info.on_accept = nil
+end
+
function ListBox:init(info)
- info = info or {}
- self:init_fields{
- selection = info.selection or 0,
- choices = info.choices or {},
- select_pen = info.select_pen,
- on_input = info.on_input,
- page_top = 0
- }
- MessageBox.init(self, info)
- self.on_accept = nil
- return self
+ self.page_top = 0
end
function ListBox:getWantedFrameSize()
- local mw, mh = MessageBox.getWantedFrameSize(self)
+ local mw, mh = ListBox.super.getWantedFrameSize(self)
return mw, mh+#self.choices
end
function ListBox:onRenderBody(dc)
- MessageBox.onRenderBody(self, dc)
+ ListBox.super.onRenderBody(self, dc)
dc:newline(1)
@@ -220,6 +215,7 @@ function ListBox:onRenderBody(dc)
end
end
end
+
function ListBox:moveCursor(delta)
local newsel=self.selection+delta
if #self.choices ~=0 then
@@ -229,6 +225,7 @@ function ListBox:moveCursor(delta)
end
self.selection=newsel
end
+
function ListBox:onInput(keys)
if keys.SELECT then
self:dismiss()
@@ -257,8 +254,8 @@ function ListBox:onInput(keys)
end
function showListPrompt(title, text, tcolor, choices, on_input, on_cancel, min_width)
- mkinstance(ListBox):init{
- title = title,
+ ListBox{
+ frame_title = title,
text = text,
text_pen = tcolor,
choices = choices,
diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua
index 661e1559..ba3cfbe6 100644
--- a/library/lua/gui/dwarfmode.lua
+++ b/library/lua/gui/dwarfmode.lua
@@ -353,7 +353,7 @@ end
MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
function MenuOverlay:updateLayout()
- DwarfOverlay.updateLayout(self)
+ MenuOverlay.super.updateLayout(self)
self.frame_rect = self.df_layout.menu
end
@@ -361,7 +361,7 @@ MenuOverlay.getWindowSize = gui.FramedScreen.getWindowSize
MenuOverlay.getMousePos = gui.FramedScreen.getMousePos
function MenuOverlay:onAboutToShow(below)
- DwarfOverlay.onAboutToShow(self,below)
+ MenuOverlay.super.onAboutToShow(self,below)
self:updateLayout()
if not self.df_layout.menu then
diff --git a/scripts/gui/hello-world.lua b/scripts/gui/hello-world.lua
index 80986bbf..c8cd3bd0 100644
--- a/scripts/gui/hello-world.lua
+++ b/scripts/gui/hello-world.lua
@@ -4,19 +4,21 @@ local gui = require 'gui'
local text = 'Woohoo, lua viewscreen :)'
-local screen = mkinstance(gui.FramedScreen, {
+local screen = gui.FramedScreen{
frame_style = gui.GREY_LINE_FRAME,
frame_title = 'Hello World',
frame_width = #text+6,
frame_height = 3,
- onRenderBody = function(self, dc)
- dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
- end,
- onInput = function(self,keys)
- if keys.LEAVESCREEN or keys.SELECT then
- self:dismiss()
- end
+}
+
+function screen:onRenderBody(dc)
+ dc:seek(3,1):string(text, COLOR_LIGHTGREEN)
+end
+
+function screen:onInput(keys)
+ if keys.LEAVESCREEN or keys.SELECT then
+ self:dismiss()
end
-}):init()
+end
screen:show()
diff --git a/scripts/gui/liquids.lua b/scripts/gui/liquids.lua
index 89f08b7c..cddb9f01 100644
--- a/scripts/gui/liquids.lua
+++ b/scripts/gui/liquids.lua
@@ -53,13 +53,7 @@ local permaflows = {
Toggle = defclass(Toggle)
-function Toggle:init(items)
- self:init_fields{
- items = items,
- selected = 1
- }
- return self
-end
+Toggle.ATTRS{ items = {}, selected = 1 }
function Toggle:get()
return self.items[self.selected]
@@ -89,16 +83,14 @@ LiquidsUI = defclass(LiquidsUI, guidm.MenuOverlay)
LiquidsUI.focus_path = 'liquids'
function LiquidsUI:init()
- self:init_fields{
- brush = mkinstance(Toggle):init(brushes),
- paint = mkinstance(Toggle):init(paints),
- flow = mkinstance(Toggle):init(flowbits),
- set = mkinstance(Toggle):init(setmode),
- permaflow = mkinstance(Toggle):init(permaflows),
+ self:assign{
+ brush = Toggle{ items = brushes },
+ paint = Toggle{ items = paints },
+ flow = Toggle{ items = flowbits },
+ set = Toggle{ items = setmode },
+ permaflow = Toggle{ items = permaflows },
amount = 7,
}
- guidm.MenuOverlay.init(self)
- return self
end
function LiquidsUI:onDestroy()
@@ -201,6 +193,7 @@ function LiquidsUI:onRenderBody(dc)
end
function ensure_blocks(cursor, size, cb)
+ size = size or xyz2pos(1,1,1)
local cx,cy,cz = pos2xyz(cursor)
local all = true
for x=1,size.x or 1,16 do
@@ -298,5 +291,5 @@ if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/LookAround') then
qerror("This script requires the main dwarfmode view in 'k' mode")
end
-local list = mkinstance(LiquidsUI):init()
+local list = LiquidsUI()
list:show()
diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua
index c14bfcbe..d1e8ec80 100644
--- a/scripts/gui/mechanisms.lua
+++ b/scripts/gui/mechanisms.lua
@@ -43,13 +43,11 @@ MechanismList = defclass(MechanismList, guidm.MenuOverlay)
MechanismList.focus_path = 'mechanisms'
-function MechanismList:init(building)
- self:init_fields{
+function MechanismList:init(info)
+ self:assign{
links = {}, selected = 1
}
- guidm.MenuOverlay.init(self)
- self:fillList(building)
- return self
+ self:fillList(info.building)
end
function MechanismList:fillList(building)
@@ -126,6 +124,6 @@ if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') t
qerror("This script requires the main dwarfmode view in 'q' mode")
end
-local list = mkinstance(MechanismList):init(df.global.world.selected_building)
+local list = MechanismList{ building = df.global.world.selected_building }
list:show()
list:changeSelected(1)
diff --git a/scripts/gui/power-meter.lua b/scripts/gui/power-meter.lua
index 8baf43e7..6c2f699a 100644
--- a/scripts/gui/power-meter.lua
+++ b/scripts/gui/power-meter.lua
@@ -13,15 +13,13 @@ PowerMeter = defclass(PowerMeter, guidm.MenuOverlay)
PowerMeter.focus_path = 'power-meter'
function PowerMeter:init()
- self:init_fields{
+ self:assign{
min_power = 0, max_power = -1, invert = false,
}
- guidm.MenuOverlay.init(self)
- return self
end
function PowerMeter:onShow()
- guidm.MenuOverlay.onShow(self)
+ PowerMeter.super.onShow(self)
-- Send an event to update the errors
bselector.plate_info.flags.whole = 0
@@ -112,5 +110,5 @@ then
qerror("This script requires the main dwarfmode view in build pressure plate mode")
end
-local list = mkinstance(PowerMeter):init()
+local list = PowerMeter()
list:show()
diff --git a/scripts/gui/room-list.lua b/scripts/gui/room-list.lua
index a4507466..0de82db5 100644
--- a/scripts/gui/room-list.lua
+++ b/scripts/gui/room-list.lua
@@ -78,15 +78,17 @@ RoomList = defclass(RoomList, guidm.MenuOverlay)
RoomList.focus_path = 'room-list'
-function RoomList:init(unit)
+RoomList.ATTRS{ unit = DEFAULT_NIL }
+
+function RoomList:init(info)
+ local unit = info.unit
local base_bld = df.global.world.selected_building
- self:init_fields{
- unit = unit, base_building = base_bld,
+ self:assign{
+ base_building = base_bld,
items = {}, selected = 1,
own_rooms = {}, spouse_rooms = {}
}
- guidm.MenuOverlay.init(self)
self.old_viewport = self:getViewport()
self.old_cursor = guidm.getCursorPos()
@@ -115,8 +117,6 @@ function RoomList:init(unit)
self.items = concat_lists({self.base_item}, self.items)
::found::
end
-
- return self
end
local sex_char = { [0] = 12, [1] = 11 }
@@ -235,12 +235,13 @@ function RoomList:onInput(keys)
end
local focus = dfhack.gui.getCurFocus()
-if focus == 'dwarfmode/QueryBuilding/Some' then
- local base = df.global.world.selected_building
- mkinstance(RoomList):init(base.owner):show()
-elseif focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
+
+if focus == 'dwarfmode/QueryBuilding/Some/Assign/Unit' then
local unit = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
- mkinstance(RoomList):init(unit):show()
+ RoomList{ unit = unit }:show()
+elseif string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some') then
+ local base = df.global.world.selected_building
+ RoomList{ unit = base.owner }:show()
else
qerror("This script requires the main dwarfmode view in 'q' mode")
end
diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua
index 7a76d767..c98cb167 100644
--- a/scripts/gui/siege-engine.lua
+++ b/scripts/gui/siege-engine.lua
@@ -34,30 +34,29 @@ SiegeEngine = defclass(SiegeEngine, guidm.MenuOverlay)
SiegeEngine.focus_path = 'siege-engine'
-function SiegeEngine:init(building)
- self:init_fields{
- building = building,
- center = utils.getBuildingCenter(building),
+SiegeEngine.ATTRS{ building = DEFAULT_NIL }
+
+function SiegeEngine:init()
+ self:assign{
+ center = utils.getBuildingCenter(self.building),
selected_pile = 1,
+ mode_main = {
+ render = self:callback 'onRenderBody_main',
+ input = self:callback 'onInput_main',
+ },
+ mode_aim = {
+ render = self:callback 'onRenderBody_aim',
+ input = self:callback 'onInput_aim',
+ },
+ mode_pile = {
+ render = self:callback 'onRenderBody_pile',
+ input = self:callback 'onInput_pile',
+ }
}
- guidm.MenuOverlay.init(self)
- self.mode_main = {
- render = self:callback 'onRenderBody_main',
- input = self:callback 'onInput_main',
- }
- self.mode_aim = {
- render = self:callback 'onRenderBody_aim',
- input = self:callback 'onInput_aim',
- }
- self.mode_pile = {
- render = self:callback 'onRenderBody_pile',
- input = self:callback 'onInput_pile',
- }
- return self
end
function SiegeEngine:onShow()
- guidm.MenuOverlay.onShow(self)
+ SiegeEngine.super.onShow(self)
self.old_cursor = guidm.getCursorPos()
self.old_viewport = self:getViewport()
@@ -487,5 +486,5 @@ if not df.building_siegeenginest:is_instance(building) then
qerror("A siege engine must be selected")
end
-local list = mkinstance(SiegeEngine):init(df.global.world.selected_building)
+local list = SiegeEngine{ building = building }
list:show()