diff --git a/library/lua/memscan.lua b/library/lua/memscan.lua index 970f821c2..6764a0d58 100644 --- a/library/lua/memscan.lua +++ b/library/lua/memscan.lua @@ -358,7 +358,7 @@ end -- Interactive search utility -function DiffSearcher:find_interactive(prompt,data_type,condition_cb) +function DiffSearcher:find_interactive(prompt,data_type,condition_cb,iter_limit) enum = enum or {} -- Loop for restarting search from scratch @@ -374,6 +374,11 @@ function DiffSearcher:find_interactive(prompt,data_type,condition_cb) while true do print('') + if iter_limit and ccursor >= iter_limit then + dfhack.printerr(' Iteration limit reached without a solution.') + break + end + local ok, value, delta = condition_cb(ccursor) ccursor = ccursor + 1 diff --git a/scripts/devel/find-offsets.lua b/scripts/devel/find-offsets.lua index 07a058474..45f59e154 100644 --- a/scripts/devel/find-offsets.lua +++ b/scripts/devel/find-offsets.lua @@ -2,6 +2,7 @@ local utils = require 'utils' local ms = require 'memscan' +local gui = require 'gui' local is_known = dfhack.internal.getAddress @@ -53,6 +54,35 @@ end local searcher = ms.DiffSearcher.new(data) +local function get_screen(class, prompt) + if not is_known('gview') then + print('Please navigate to '..prompt) + if not utils.prompt_yes_no('Proceed?', true) then + return nil + end + return true + end + + while true do + local cs = dfhack.gui.getCurViewscreen(true) + if not df.is_instance(class, cs) then + print('Please navigate to '..prompt) + if not utils.prompt_yes_no('Proceed?', true) then + return nil + end + else + return cs + end + end +end + +local function screen_title() + return get_screen(df.viewscreen_titlest, 'the title screen') +end +local function screen_dwarfmode() + return get_screen(df.viewscreen_dwarfmodest, 'the main dwarf mode screen') +end + local function validate_offset(name,validator,addr,tname,...) local obj = data:object_by_field(addr,tname,...) if obj and not validator(obj) then @@ -136,13 +166,95 @@ local function list_index_choices(length_func) end end +local function can_feed() + return not force_scan['nofeed'] and is_known 'gview' +end + +local function dwarfmode_feed_input(...) + local screen = screen_dwarfmode() + if not df.isvalid(screen) then + qerror('could not retrieve dwarfmode screen') + end + for _,v in ipairs({...}) do + gui.simulateInput(screen, v) + end +end + +local function dwarfmode_to_top() + if not can_feed() then + return false + end + + local screen = screen_dwarfmode() + if not df.isvalid(screen) then + return false + end + + for i=0,10 do + if is_known 'ui' and df.global.ui.main.mode == df.ui_sidebar_mode.Default then + break + end + gui.simulateInput(screen, 'LEAVESCREEN') + end + + -- force pause just in case + screen.keyRepeat = 1 + return true +end + +local function feed_menu_choice(catnames,catkeys,enum) + return function (idx) + idx = idx % #catnames + 1 + dwarfmode_feed_input(catkeys[idx]) + if enum then + return true, enum[catnames[idx]] + else + return true, catnames[idx] + end + end +end + +local function feed_list_choice(count,upkey,downkey) + return function(idx) + if idx > 0 then + local ok, len + if type(count) == 'number' then + ok, len = true, count + else + ok, len = pcall(count) + end + if not ok then + len = 5 + elseif len > 10 then + len = 10 + end + + local hcnt = len-1 + local rix = 1 + (idx-1) % (hcnt*2) + + if rix >= hcnt then + dwarfmode_feed_input(upkey or 'SECONDSCROLL_UP') + return true, hcnt*2 - rix + else + dwarfmode_feed_input(donwkey or 'SECONDSCROLL_DOWN') + return true, rix + end + else + print(' Please select the first list item.') + if not utils.prompt_yes_no(' Proceed?') then + return false + end + return true, 0 + end + end +end + -- -- Cursor group -- local function find_cursor() - print('\nPlease navigate to the title screen to find cursor.') - if not utils.prompt_yes_no('Proceed?', true) then + if not screen_title() then return false end @@ -354,13 +466,35 @@ local function is_valid_world(world) end local function find_world() - local addr = searcher:find_menu_cursor([[ + local catnames = { + 'Corpses', 'Refuse', 'Stone', 'Wood', 'Gems', 'Bars', 'Cloth', 'Leather', 'Ammo', 'Coins' + } + local catkeys = { + 'STOCKPILE_GRAVEYARD', 'STOCKPILE_REFUSE', 'STOCKPILE_STONE', 'STOCKPILE_WOOD', + 'STOCKPILE_GEM', 'STOCKPILE_BARBLOCK', 'STOCKPILE_CLOTH', 'STOCKPILE_LEATHER', + 'STOCKPILE_AMMO', 'STOCKPILE_COINS' + } + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_STOCKPILES') + + addr = searcher:find_interactive( + 'Auto-searching for world.', + 'int32_t', + feed_menu_choice(catnames, catkeys, df.stockpile_category), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for world. Please open the stockpile creation menu, and select different types as instructed below:]], - 'int32_t', - { 'Corpses', 'Refuse', 'Stone', 'Wood', 'Gems', 'Bars', 'Cloth', 'Leather', 'Ammo', 'Coins' }, - df.stockpile_category - ) + 'int32_t', catnames, df.stockpile_category + ) + end + validate_offset('world', is_valid_world, addr, df.world, 'selected_stockpile_type') end @@ -385,14 +519,37 @@ local function is_valid_ui(ui) end local function find_ui() - local addr = searcher:find_menu_cursor([[ + local catnames = { + 'DesignateMine', 'DesignateChannel', 'DesignateRemoveRamps', + 'DesignateUpStair', 'DesignateDownStair', 'DesignateUpDownStair', + 'DesignateUpRamp', 'DesignateChopTrees' + } + local catkeys = { + 'DESIGNATE_DIG', 'DESIGNATE_CHANNEL', 'DESIGNATE_DIG_REMOVE_STAIRS_RAMPS', + 'DESIGNATE_STAIR_UP', 'DESIGNATE_STAIR_DOWN', 'DESIGNATE_STAIR_UPDOWN', + 'DESIGNATE_RAMP', 'DESIGNATE_CHOP' + } + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_DESIGNATE') + + addr = searcher:find_interactive( + 'Auto-searching for ui.', + 'int16_t', + feed_menu_choice(catnames, catkeys, df.ui_sidebar_mode), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui. Please open the designation menu, and switch modes as instructed below:]], - 'int16_t', - { 'DesignateMine', 'DesignateChannel', 'DesignateRemoveRamps', 'DesignateUpStair', - 'DesignateDownStair', 'DesignateUpDownStair', 'DesignateUpRamp', 'DesignateChopTrees' }, - df.ui_sidebar_mode - ) + 'int16_t', catnames, df.ui_sidebar_mode + ) + end + validate_offset('ui', is_valid_ui, addr, df.ui, 'main', 'mode') end @@ -421,14 +578,32 @@ local function is_valid_ui_sidebar_menus(usm) end local function find_ui_sidebar_menus() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_BUILDJOB') + + addr = searcher:find_interactive([[ +Auto-searching for ui_sidebar_menus. Please select a Mason, +Craftsdwarfs, or Carpenters workshop, open the Add Job +menu, and move the cursor within:]], + 'int32_t', + feed_list_choice(7), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_sidebar_menus. Please switch to 'q' mode, select a Mason, Craftsdwarfs, or Carpenters workshop, open the Add Job menu, and move the cursor within:]], - 'int32_t', - { 0, 1, 2, 3, 4, 5, 6 }, - ordinal_names - ) + 'int32_t', + { 0, 1, 2, 3, 4, 5, 6 }, + ordinal_names + ) + end + validate_offset('ui_sidebar_menus', is_valid_ui_sidebar_menus, addr, df.ui_sidebar_menus, 'workshop_job', 'cursor') end @@ -455,14 +630,41 @@ local function is_valid_ui_build_selector(ubs) end local function find_ui_build_selector() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + addr = searcher:find_interactive([[ +Auto-searching for ui_build_selector. This requires mechanisms.]], + 'int32_t', + function(idx) + if idx == 0 then + dwarfmode_to_top() + dwarfmode_feed_input( + 'D_BUILDING', + 'HOTKEY_BUILDING_TRAP', + 'HOTKEY_BUILDING_TRAP_TRIGGER', + 'BUILDING_TRIGGER_ENABLE_CREATURE' + ) + else + dwarfmode_feed_input('BUILDING_TRIGGER_MIN_SIZE_DOWN') + end + return true, 50000 - 1000*idx + end, + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_build_selector. Please start constructing a pressure plate, and enable creatures. Then change the min weight as requested, remembering that the ui truncates the number, so when it shows "Min (5000df", it means 50000:]], - 'int32_t', - { 50000, 49000, 48000, 47000, 46000, 45000, 44000 } - ) + 'int32_t', + { 50000, 49000, 48000, 47000, 46000, 45000, 44000 } + ) + end + validate_offset('ui_build_selector', is_valid_ui_build_selector, addr, df.ui_build_selector, 'plate_info', 'unit_min') end @@ -601,13 +803,33 @@ end -- local function find_ui_unit_view_mode() - local addr = searcher:find_menu_cursor([[ + local catnames = { 'General', 'Inventory', 'Preferences', 'Wounds' } + local catkeys = { 'UNITVIEW_GEN', 'UNITVIEW_INV', 'UNITVIEW_PRF', 'UNITVIEW_WND' } + local addr + + if dwarfmode_to_top() and is_known('ui_selected_unit') then + dwarfmode_feed_input('D_VIEWUNIT') + + if df.global.ui_selected_unit < 0 then + df.global.ui_selected_unit = 0 + end + + addr = searcher:find_interactive( + 'Auto-searching for ui_unit_view_mode.', + 'int32_t', + feed_menu_choice(catnames, catkeys, df.ui_unit_view_mode.T_value), + 10 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_unit_view_mode. Having selected a unit with 'v', switch the pages as requested:]], - 'int32_t', - { 'General', 'Inventory', 'Preferences', 'Wounds' }, - df.ui_unit_view_mode.T_value - ) + 'int32_t', catnames, df.ui_unit_view_mode.T_value + ) + end + ms.found_offset('ui_unit_view_mode', addr) end @@ -620,14 +842,32 @@ local function look_item_list_count() end local function find_ui_look_cursor() - local addr = searcher:find_menu_cursor([[ + local addr + + if dwarfmode_to_top() then + dwarfmode_feed_input('D_LOOK') + + addr = searcher:find_interactive([[ +Auto-searching for ui_look_cursor. Please select a tile +with at least 5 items or units on the ground, and move +the cursor as instructed:]], + 'int32_t', + feed_list_choice(look_item_list_count), + 20 + ) + end + + if not addr then + addr = searcher:find_menu_cursor([[ Searching for ui_look_cursor. Please activate the 'k' mode, find a tile with many items or units on the ground, and select list entries as instructed:]], - 'int32_t', - list_index_choices(look_item_list_count), - ordinal_names - ) + 'int32_t', + list_index_choices(look_item_list_count), + ordinal_names + ) + end + ms.found_offset('ui_look_cursor', addr) end @@ -989,10 +1229,10 @@ end print('\nInitial globals (need title screen):\n') +exec_finder(find_gview, 'gview') exec_finder(find_cursor, { 'cursor', 'selection_rect', 'gamemode', 'gametype' }) exec_finder(find_announcements, 'announcements') exec_finder(find_d_init, 'd_init') -exec_finder(find_gview, 'gview') exec_finder(find_enabler, 'enabler') exec_finder(find_gps, 'gps')