Skip to content
Open
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 docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Template for new versions:
- `buildingplan`: fix roller material estimate asking for one chain per tile instead of one chain total
- `buildingplan`: fix "Unlink all" only unlinking some mechanisms (or crashing) when freeing mechanisms from the building
- `tiletypes`: remove plants (including trees) rooted on a tile that is painted into something that cannot host them, so cleared ground no longer regrows floating trees
- `eventful`: unhook all registered Lua callbacks on world unload; previously ``onReactionCompleting`` and ``onWorkshopFillSidebarMenu`` hooks leaked into newly loaded worlds
- `timestream`: deal properly with units who have breathing difficulties
- `stocks`: overlay now resets scroll position when collapsing categories so the item list is no longer left blank and unscrollable
- Fixed persistent site data API (``dfhack.persistent.saveSiteData``/``getSiteData``) failing on newly reclaimed fortresses until the first save
Expand Down
2 changes: 2 additions & 0 deletions plugins/lua/eventful.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ end
_registeredStuff={}
local function unregall(state)
if state==SC_WORLD_UNLOADED then
onReactionCompleting._library=nil
onReactionComplete._library=nil
onWorkshopFillSidebarMenu._library=nil
postWorkshopFillSidebarMenu._library=nil
dfhack.onStateChange.eventful= nil
_registeredStuff={}
Expand Down
101 changes: 101 additions & 0 deletions test/plugins/dig.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
config.mode = 'fortress'
config.target = 'dig'

local saved_cursor

config.wrapper = function(test_fn)
saved_cursor = xyz2pos(df.global.cursor.x, df.global.cursor.y,
df.global.cursor.z)
return dfhack.with_finalize(function()
df.global.cursor.x = saved_cursor.x
df.global.cursor.y = saved_cursor.y
df.global.cursor.z = saved_cursor.z
end, test_fn)
end

local function set_cursor(pos)
df.global.cursor.x = pos.x
df.global.cursor.y = pos.y
df.global.cursor.z = pos.z
end

local function designation_at(pos)
local block = dfhack.maps.getTileBlock(pos)
return block.designation[pos.x % 16][pos.y % 16].dig
end

-- find a solid, undesignated block-center wall tile to dig in.
-- a diameter-5 circle around the block center stays within the block
local function find_diggable_pos()
for _, block in ipairs(df.global.world.map.map_blocks) do
local attrs = df.tiletype.attrs[block.tiletype[8][8]]
local mat = attrs.material
local basic = df.tiletype_shape.attrs[attrs.shape].basic_shape
if (mat == df.tiletype_material.SOIL
or mat == df.tiletype_material.STONE)
and basic == df.tiletype_shape_basic.Wall
and block.designation[8][8].dig
== df.tile_dig_designation.No then
return xyz2pos(block.map_pos.x + 8,
block.map_pos.y + 8,
block.map_pos.z)
end
end
error('no diggable tile found')
end

function test.digcircle_help()
local output, status = dfhack.run_command_silent('digcircle', 'help')
expect.true_(output:find('designation of filled and hollow circles', 1, true)
~= nil)
end

function test.digcircle_requires_cursor()
set_cursor(xyz2pos(-30000, -30000, -30000))
local output, status = dfhack.run_command_silent('digcircle', 'filled', '5')
expect.true_(output:find("Can't get the cursor coords", 1, true) ~= nil)
end

function test.digcircle_designates_and_unsets()
-- digcircle remembers set/unset, diameter, and fill across calls
-- ("do it again" feature), so always pass them explicitly
local pos = find_diggable_pos()
set_cursor(pos)
dfhack.run_command_silent('digcircle', 'set', 'filled', '5', 'dig')
expect.eq(df.tile_dig_designation.Default, designation_at(pos))
dfhack.run_command_silent('digcircle', 'unset', 'filled', '5', 'dig')
expect.eq(df.tile_dig_designation.No, designation_at(pos))
end

-- channels apply to floors/stairs, not walls (see dig.cpp dig() checks)
local function find_floor_pos()
for _, block in ipairs(df.global.world.map.map_blocks) do
local tt = block.tiletype[8][8]
local attrs = df.tiletype.attrs[tt]
if attrs.shape == df.tiletype_shape.FLOOR
and attrs.material ~= df.tiletype_material.CONSTRUCTION
and block.designation[8][8].dig
== df.tile_dig_designation.No
and not block.designation[8][8].hidden then
return xyz2pos(block.map_pos.x + 8,
block.map_pos.y + 8,
block.map_pos.z)
end
end
error('no channelable floor tile found')
end

function test.digcircle_channel_designation()
-- channels designate on floors/stairs; this floor tile is a valid target
local pos = find_floor_pos()
set_cursor(pos)
dfhack.run_command_silent('digcircle', 'set', 'filled', '3', 'chan')
expect.eq(df.tile_dig_designation.Channel, designation_at(pos))
dfhack.run_command_silent('digcircle', 'unset', 'filled', '3', 'chan')
expect.eq(df.tile_dig_designation.No, designation_at(pos))
end

function test.digexp_help()
local output, status = dfhack.run_command_silent('digexp', 'help')
expect.true_(output:find('pattern', 1, true) ~= nil)
end
82 changes: 82 additions & 0 deletions test/plugins/eventful.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
config.mode = 'fortress'
config.target = 'eventful'

local eventful = require('plugins.eventful')

local function cleanup()
eventful._registeredStuff = {}
eventful.onReactionCompleting._library = nil
eventful.onReactionComplete._library = nil
eventful.onWorkshopFillSidebarMenu._library = nil
eventful.postWorkshopFillSidebarMenu._library = nil
dfhack.onStateChange.eventful = nil
end

config.wrapper = function(test_fn)
cleanup()
return dfhack.with_finalize(cleanup, test_fn)
end

function test.eventType_table()
expect.eq(0, eventful.eventType.TICK)
expect.eq(3, eventful.eventType.JOB_COMPLETED)
expect.eq(16, eventful.eventType.EVENT_MAX)
end

function test.registerReaction()
local cb = function() end
eventful.registerReaction('TEST_REACTION', cb)
expect.eq(cb, eventful._registeredStuff.reactionCallbacks.TEST_REACTION)
expect.ne(nil, eventful.onReactionCompleting._library)
expect.ne(nil, dfhack.onStateChange.eventful)
end

function test.reaction_callback_dispatches()
local got
eventful.registerReaction('TEST_REACTION',
function(reaction) got = reaction.code end)
eventful.onReactionCompleting._library({code='TEST_REACTION'})
expect.eq('TEST_REACTION', got)
end

function test.reaction_callback_ignores_unregistered()
local called = false
eventful.registerReaction('TEST_REACTION', function() called = true end)
eventful.onReactionCompleting._library({code='OTHER_REACTION'})
expect.false_(called)
end

function test.registerSidebar()
local cb = function() end
eventful.registerSidebar('CARPENTERS', cb)
expect.eq(cb, eventful._registeredStuff.customSidebar.CARPENTERS)
expect.ne(nil, eventful.onWorkshopFillSidebarMenu._library)
end

function test.addReactionToShop()
eventful.addReactionToShop('TEST_REACTION', 'MASONS')
expect.table_eq({'TEST_REACTION'},
eventful._registeredStuff.reactionToShop.MASONS)
expect.ne(nil, eventful.postWorkshopFillSidebarMenu._library)
end

function test.removeNative()
eventful.removeNative('STILL', 'COOK_FOOD')
expect.table_eq({'COOK_FOOD'},
eventful._registeredStuff.shopNonNative.STILL)
eventful.removeNative('STILL')
expect.true_(eventful._registeredStuff.shopNonNative.STILL.all)
end

function test.world_unload_clears_registration()
eventful.registerReaction('TEST_REACTION', function() end)
eventful.registerSidebar('CARPENTERS', function() end)
eventful.addReactionToShop('TEST_REACTION', 'MASONS')
eventful.removeNative('STILL')
dfhack.onStateChange.eventful(SC_WORLD_UNLOADED)
expect.table_eq({}, eventful._registeredStuff)
expect.nil_(eventful.onReactionCompleting._library)
expect.nil_(eventful.onWorkshopFillSidebarMenu._library)
expect.nil_(eventful.postWorkshopFillSidebarMenu._library)
expect.nil_(dfhack.onStateChange.eventful)
end
77 changes: 77 additions & 0 deletions test/plugins/hotkeys.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
config.mode = 'fortress'
config.target = 'hotkeys'

local hotkeys = require('plugins.hotkeys')

local BINDING_SPEC = 'Ctrl-Shift-Alt-F11@dwarfmode'
local BINDING_CMD = 'echo dfhack-test-binding'

config.wrapper = function(test_fn)
dfhack.run_command_silent{'keybinding', 'clear', BINDING_SPEC}
return dfhack.with_finalize(function()
dfhack.run_command_silent{'keybinding', 'clear', BINDING_SPEC}
hotkeys.cleanupHotkeys()
end, test_fn)
end

-- key symbol strings get normalized by the keybinding manager (e.g.
-- modifier order), so match added bindings by their command instead
local function find_binding(cmdline)
local keys, bindings = hotkeys.getHotkeys()
for _, sym in ipairs(keys) do
if bindings[sym] == cmdline then return sym end
end
end

local function with_mortal_mode(test_fn)
local saved = dfhack.getMortalMode()
dfhack.setMortalMode(true)
return dfhack.with_finalize(function()
dfhack.setMortalMode(saved)
end, test_fn)
end

function test.should_hide_armok()
-- createitem is tagged 'armok' in helpdb
with_mortal_mode(function()
expect.true_(hotkeys.should_hide_armok('createitem'))
expect.false_(hotkeys.should_hide_armok('ls'))
expect.false_(hotkeys.should_hide_armok('bogus-command'))
end)
end

function test.should_hide_armok_strips_prefix()
with_mortal_mode(function()
expect.true_(hotkeys.should_hide_armok(':createitem'))
expect.true_(hotkeys.should_hide_armok(' createitem --flags'))
expect.false_(hotkeys.should_hide_armok(':'))
end)
end

function test.should_hide_armok_not_mortal()
-- armok commands stay visible when mortal mode is off
with_mortal_mode(function()
dfhack.setMortalMode(false)
expect.false_(hotkeys.should_hide_armok('createitem'))
end)
end

function test.getHotkeys_returns_state()
local keys, bindings = hotkeys.getHotkeys()
expect.ne(nil, keys)
expect.ne(nil, bindings)
end

function test.getHotkeys_finds_added_binding()
dfhack.run_command_silent{'keybinding', 'add', BINDING_SPEC, BINDING_CMD}
expect.ne(nil, find_binding(BINDING_CMD))
end

function test.getHotkeys_filters_menu_bindings()
-- stub keybindings that invoke the hotkeys menu itself must not be listed
dfhack.run_command_silent{'keybinding', 'add', BINDING_SPEC,
'overlay trigger hotkeys.foo'}
expect.nil_(find_binding('overlay trigger hotkeys.foo'))
dfhack.run_command_silent{'keybinding', 'add', BINDING_SPEC, 'hotkeys'}
expect.nil_(find_binding('hotkeys'))
end
90 changes: 90 additions & 0 deletions test/plugins/stockpiles.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
config.mode = 'fortress'
config.target = 'stockpiles'

local stockpiles = require('plugins.stockpiles')

local TEST_NAME = 'test_dfstock'

local function test_file_path()
return ('%s/stockpiles/%s.dfstock'):format(dfhack.getConfigPath(),
TEST_NAME)
end

local function rm_test_file()
os.remove(test_file_path())
end

config.wrapper = function(test_fn)
rm_test_file()
return dfhack.with_finalize(rm_test_file, test_fn)
end

local function make_stockpile()
-- a 2x2 stockpile on any free floor tile
for _, block in ipairs(df.global.world.map.map_blocks) do
local tt = df.tiletype.attrs[block.tiletype[8][8]]
if tt.shape == df.tiletype_shape.FLOOR
and block.occupancy[8][8].building
== df.tile_building_occ.None then
local pos = xyz2pos(block.map_pos.x + 7, block.map_pos.y + 7,
block.map_pos.z)
local bld, err = dfhack.buildings.constructBuilding{
pos=pos, type=df.building_type.Stockpile,
width=2, height=2, abstract=true}
if bld then return bld end
end
end
error('could not place a test stockpile')
end

function test.status_and_help()
expect.true_(stockpiles.parse_commandline({'status'}))
expect.false_(stockpiles.parse_commandline({'help'}))
expect.false_(stockpiles.parse_commandline({'bogus'}))
end

function test.export_requires_name()
expect.error_match('name missing or empty', function()
stockpiles.parse_commandline({'export'})
end)
end

function test.export_rejects_unsafe_name()
expect.error_match('numbers, letters', function()
stockpiles.parse_commandline({'export', 'bad/name'})
end)
expect.error_match('numbers, letters', function()
stockpiles.parse_commandline({'export', 'bad name'})
end)
end

function test.export_requires_stockpile()
-- valid name, but no stockpile selected or specified
expect.error_match('select a stockpile', function()
stockpiles.parse_commandline({'export', TEST_NAME})
end)
end

function test.stockpile_opt_unknown_name()
expect.error_match('could not find stockpile', function()
stockpiles.parse_commandline({'status', '-s', 'no_such_pile'})
end)
end

function test.list_settings_files()
-- the library dir ships stock settings files, so list must succeed
expect.true_(stockpiles.parse_commandline({'list'}))
end

function test.export_import_roundtrip()
local sp = make_stockpile()
local ok, err = pcall(function()
expect.true_(stockpiles.parse_commandline(
{'export', TEST_NAME, '-s', tostring(sp.id)}))
expect.true_(dfhack.filesystem.isfile(test_file_path()))
expect.true_(stockpiles.parse_commandline(
{'import', TEST_NAME, '-s', tostring(sp.id)}))
end)
dfhack.buildings.deconstruct(sp)
if not ok then error(err, 0) end
end
Loading
Loading