Skip to content

Commit

Permalink
reinstate startdwarf and add scrollbar overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
myk002 committed Sep 26, 2023
1 parent 58e24f9 commit 2dcf6d9
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 108 deletions.
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ Template for new versions:
# Future

## New Tools
- `startdwarf`: (reinstated) set number of starting dwarves

## New Features
- `startdwarf`: overlay scrollbar so you can scroll through your starting dwarves if they don't all fit on the screen

## Fixes

Expand Down
27 changes: 20 additions & 7 deletions docs/startdwarf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ startdwarf
==========

.. dfhack-tool::
:summary: Increase the number of dwarves you embark with.
:tags: unavailable embark fort armok
:summary: Change the number of dwarves you embark with.
:tags: embark fort armok

You must use this tool before embarking (e.g. at the site selection screen or
any time before) to change the number of dwarves you embark with from the
default of 7.
You must use this tool before you get to the embark preparation screen (e.g. at
the site selection screen or any time before) to change the number of dwarves
you embark with from the default of 7. The value that you set will remain in
effect until DF is restarted (or you use `startdwarf` to set a new value).

Note that the game requires that you embark with no fewer than 7 dwarves, so
this tool can only increase the starting dwarf count, not decrease it.
The maximum number of dwarves you can have is 32,767, but that is far more than
the game can handle.

Usage
-----
Expand All @@ -24,6 +25,18 @@ Examples

``startdwarf 10``
Start with a few more warm bodies to help you get started.
``startdwarf 1``
Hermit fort! (also see the `hermit` tool for keeping it that way)
``startdwarf 500``
Start with a teeming army of dwarves (leading to immediate food shortage and
FPS issues).

Overlay
-------

The vanilla DF screen doesn't provide a way to scroll through the starting
dwarves, so if you start with more dwarves than can fit on your screen, this
tool provides a scrollbar that you can use to scroll through them. The vanilla
list was *not* designed for scrolling, so there is some odd behavior. When you
click on a dwarf to set skills, the list will jump so that the dwarf you
clicked on will be at the top of the page.
86 changes: 74 additions & 12 deletions startdwarf.lua
Original file line number Diff line number Diff line change
@@ -1,17 +1,79 @@
-- change number of dwarves on initial embark
--@ module=true

local addr = dfhack.internal.getAddress('start_dwarf_count')
if not addr then
qerror('start_dwarf_count address not available - cannot patch')
local argparse = require('argparse')
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')

StartDwarfOverlay = defclass(StartDwarfOverlay, overlay.OverlayWidget)
StartDwarfOverlay.ATTRS{
default_pos={x=5, y=9},
default_enabled=true,
viewscreens='setupdwarfgame/Dwarves',
frame={w=5, h=10},
}

function StartDwarfOverlay:init()
self:addviews{
widgets.Scrollbar{
view_id='scrollbar',
frame={r=0, t=0, w=2, b=0},
on_scroll=self:callback('on_scrollbar'),
},
}
end

function StartDwarfOverlay:on_scrollbar(scroll_spec)
local scr = dfhack.gui.getDFViewscreen(true)
local _, sh = dfhack.screen.getWindowSize()
local list_height = sh - 17
local num_units = #scr.s_unit
local units_per_page = list_height // 3

local v = 0
if tonumber(scroll_spec) then
v = tonumber(scroll_spec) - 1
elseif scroll_spec == 'down_large' then
v = scr.selected_u + units_per_page // 2
elseif scroll_spec == 'up_large' then
v = scr.selected_u - units_per_page // 2
elseif scroll_spec == 'down_small' then
v = scr.selected_u + 1
elseif scroll_spec == 'up_small' then
v = scr.selected_u - 1
end

scr.selected_u = math.max(0, math.min(num_units-1, v))
end

function StartDwarfOverlay:render(dc)
local sw, sh = dfhack.screen.getWindowSize()
local list_height = sh - 17
local scr = dfhack.gui.getDFViewscreen(true)
local num_units = #scr.s_unit
local units_per_page = list_height // 3
local scrollbar = self.subviews.scrollbar
self.frame.w = sw // 2 - 4
self.frame.h = list_height
self:updateLayout()

local top = math.min(scr.selected_u + 1, num_units - units_per_page + 1)
scrollbar:update(top, units_per_page, num_units)

StartDwarfOverlay.super.render(self, dc)
end

OVERLAY_WIDGETS = {
overlay=StartDwarfOverlay,
}

if dfhack_flags.module then
return
end

local num = tonumber(({...})[1])
if not num or num < 7 then
qerror('argument must be a number no less than 7')
local num = argparse.positiveInt(({...})[1])
if num > 32767 then
qerror(('value must be no more than 32,767: %d'):format(num))
end
df.global.start_dwarf_count = num

dfhack.with_temp_object(df.new('uint32_t'), function(temp)
temp.value = num
local temp_size, temp_addr = temp:sizeof()
dfhack.internal.patchMemory(addr, temp_addr, temp_size)
end)
print(('starting dwarf count set to %d. good luck!'):format(num))
96 changes: 7 additions & 89 deletions test/startdwarf.lua
Original file line number Diff line number Diff line change
@@ -1,104 +1,22 @@
local utils = require('utils')

local function with_patches(callback, custom_mocks)
dfhack.with_temp_object(df.new('uint32_t'), function(temp_out)
local originalPatchMemory = dfhack.internal.patchMemory
local function safePatchMemory(target, source, length)
-- only allow patching the expected address - otherwise a buggy
-- script could corrupt the test environment
if target ~= utils.addressof(temp_out) then
return expect.fail(('attempted to patch invalid address 0x%x: expected 0x%x'):format(target, utils.addressof(temp_out)))
end
return originalPatchMemory(target, source, length)
end
local mocks = {
getAddress = mock.func(utils.addressof(temp_out)),
patchMemory = mock.observe_func(safePatchMemory),
}
if custom_mocks then
for k, v in pairs(custom_mocks) do
mocks[k] = v
end
end
mock.patch({
{dfhack.internal, 'getAddress', mocks.getAddress},
{dfhack.internal, 'patchMemory', mocks.patchMemory},
}, function()
callback(mocks, temp_out)
end)
end)
end
config.target = 'startdwarf'

local function run_startdwarf(...)
return dfhack.run_script('startdwarf', ...)
end

local function test_early_error(args, expected_message, custom_mocks)
with_patches(function(mocks, temp_out)
temp_out.value = 12345

expect.error_match(expected_message, function()
run_startdwarf(table.unpack(args))
end)

expect.eq(mocks.getAddress.call_count, 1, 'getAddress was not called')
expect.table_eq(mocks.getAddress.call_args[1], {'start_dwarf_count'})

expect.eq(mocks.patchMemory.call_count, 0, 'patchMemory was called unexpectedly')

-- make sure the script didn't attempt to write in some other way
expect.eq(temp_out.value, 12345, 'memory was changed unexpectedly')
end, custom_mocks)
end

local function test_invalid_args(args, expected_message)
test_early_error(args, expected_message)
end

local function test_patch_successful(expected_value)
with_patches(function(mocks, temp_out)
run_startdwarf(tostring(expected_value))
expect.eq(temp_out.value, expected_value)

expect.eq(mocks.getAddress.call_count, 1, 'getAddress was not called')
expect.table_eq(mocks.getAddress.call_args[1], {'start_dwarf_count'})

expect.eq(mocks.patchMemory.call_count, 1, 'patchMemory was not called')
expect.eq(mocks.patchMemory.call_args[1][1], utils.addressof(temp_out),
'patchMemory called with wrong destination')
-- skip checking source (arg 2) because it has already been freed by the script
expect.eq(mocks.patchMemory.call_args[1][3], df.sizeof(temp_out),
'patchMemory called with wrong length')
end)
end

function test.no_arg()
test_invalid_args({}, 'must be a number')
expect.error_match('expected positive integer', run_startdwarf)
end

function test.not_number()
test_invalid_args({'a'}, 'must be a number')
expect.error_match('expected positive integer', curry(run_startdwarf, 'a'))
end

function test.too_small()
test_invalid_args({'4'}, 'less than 7')
test_invalid_args({'6'}, 'less than 7')
test_invalid_args({'-1'}, 'less than 7')
end

function test.missing_address()
test_early_error({}, 'address not available', {getAddress = mock.func(nil)})
test_early_error({'8'}, 'address not available', {getAddress = mock.func(nil)})
end

function test.exactly_7()
test_patch_successful(7)
end

function test.above_7()
test_patch_successful(10)
expect.error_match('expected positive integer', curry(run_startdwarf, '0'))
expect.error_match('expected positive integer', curry(run_startdwarf, '-1'))
end

function test.uint8_overflow()
test_patch_successful(257)
function test.too_big()
expect.error_match('value must be no more than', curry(run_startdwarf, '32768'))
end

0 comments on commit 2dcf6d9

Please sign in to comment.