From 2dcf6d903e7f3ba069a7f65d8c3cc1c73e32e774 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 26 Sep 2023 04:19:42 -0700 Subject: [PATCH] reinstate startdwarf and add scrollbar overlay --- changelog.txt | 2 + docs/startdwarf.rst | 27 +++++++++---- startdwarf.lua | 86 ++++++++++++++++++++++++++++++++++------ test/startdwarf.lua | 96 ++++----------------------------------------- 4 files changed, 103 insertions(+), 108 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5efb93672f..c0604214da 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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 diff --git a/docs/startdwarf.rst b/docs/startdwarf.rst index 4f63442a99..339ceed4bd 100644 --- a/docs/startdwarf.rst +++ b/docs/startdwarf.rst @@ -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 ----- @@ -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. diff --git a/startdwarf.lua b/startdwarf.lua index 143dce23ff..5f779a5933 100644 --- a/startdwarf.lua +++ b/startdwarf.lua @@ -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)) diff --git a/test/startdwarf.lua b/test/startdwarf.lua index af58ce9198..511b325d35 100644 --- a/test/startdwarf.lua +++ b/test/startdwarf.lua @@ -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