1937 lines
54 KiB
Lua
1937 lines
54 KiB
Lua
-- Find some offsets for linux.
|
|
--[[=begin
|
|
|
|
devel/find-offsets
|
|
==================
|
|
WARNING: THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS.
|
|
|
|
Running this script on a new DF version will NOT
|
|
MAKE IT RUN CORRECTLY if any data structures
|
|
changed, thus possibly leading to CRASHES AND/OR
|
|
PERMANENT SAVE CORRUPTION.
|
|
|
|
Finding the first few globals requires this script to be
|
|
started immediately after loading the game, WITHOUT
|
|
first loading a world. The rest expect a loaded save,
|
|
not a fresh embark. Finding current_weather requires
|
|
a special save previously processed with `devel/prepare-save`
|
|
on a DF version with working dfhack.
|
|
|
|
The script expects vanilla game configuration, without
|
|
any custom tilesets or init file changes. Never unpause
|
|
the game unless instructed. When done, quit the game
|
|
without saving using 'die'.
|
|
|
|
Arguments:
|
|
|
|
* global names to force finding them
|
|
* ``all`` to force all globals
|
|
* ``nofeed`` to block automated fake input searches
|
|
* ``nozoom`` to disable neighboring object heuristics
|
|
|
|
=end]]
|
|
|
|
local utils = require 'utils'
|
|
local ms = require 'memscan'
|
|
local gui = require 'gui'
|
|
|
|
local is_known = dfhack.internal.getAddress
|
|
|
|
local os_type = dfhack.getOSType()
|
|
|
|
local force_scan = {}
|
|
for _,v in ipairs({...}) do
|
|
force_scan[v] = true
|
|
end
|
|
|
|
collectgarbage()
|
|
|
|
function prompt_proceed(indent)
|
|
if not indent then indent = 0 end
|
|
return utils.prompt_yes_no(string.rep(' ', indent) .. 'Proceed?', true)
|
|
end
|
|
|
|
print[[
|
|
WARNING: THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS.
|
|
|
|
Running this script on a new DF version will NOT
|
|
MAKE IT RUN CORRECTLY if any data structures
|
|
changed, thus possibly leading to CRASHES AND/OR
|
|
PERMANENT SAVE CORRUPTION.
|
|
|
|
Finding the first few globals requires this script to be
|
|
started immediately after loading the game, WITHOUT
|
|
first loading a world. The rest expect a loaded save,
|
|
not a fresh embark. Finding current_weather requires
|
|
a special save previously processed with devel/prepare-save
|
|
on a DF version with working dfhack.
|
|
|
|
The script expects vanilla game configuration, without
|
|
any custom tilesets or init file changes. Never unpause
|
|
the game unless instructed. When done, quit the game
|
|
without saving using 'die'.
|
|
]]
|
|
|
|
if not utils.prompt_yes_no('Proceed?') then
|
|
return
|
|
end
|
|
|
|
-- Data segment location
|
|
|
|
local data = ms.get_data_segment()
|
|
if not data then
|
|
qerror('Could not find data segment')
|
|
end
|
|
|
|
print('\nData section: '..tostring(data))
|
|
if data.size < 5000000 then
|
|
qerror('Data segment too short.')
|
|
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 prompt_proceed() 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 prompt_proceed() 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
|
|
obj = nil
|
|
end
|
|
ms.found_offset(name,obj)
|
|
end
|
|
|
|
local function zoomed_searcher(startn, end_or_sz)
|
|
if force_scan.nozoom then
|
|
return nil
|
|
end
|
|
local sv = is_known(startn)
|
|
if not sv then
|
|
return nil
|
|
end
|
|
local ev
|
|
if type(end_or_sz) == 'number' then
|
|
ev = sv + end_or_sz
|
|
if end_or_sz < 0 then
|
|
sv, ev = ev, sv
|
|
end
|
|
else
|
|
ev = is_known(end_or_sz)
|
|
if not ev then
|
|
return nil
|
|
end
|
|
end
|
|
sv = sv - (sv % 4)
|
|
ev = ev + 3
|
|
ev = ev - (ev % 4)
|
|
if data:contains_range(sv, ev-sv) then
|
|
return ms.DiffSearcher.new(ms.MemoryArea.new(sv,ev))
|
|
end
|
|
end
|
|
|
|
local finder_searches = {}
|
|
local function exec_finder(finder, names, validators)
|
|
if type(names) ~= 'table' then
|
|
names = { names }
|
|
end
|
|
if type(validators) ~= 'table' then
|
|
validators = { validators }
|
|
end
|
|
local search = force_scan['all']
|
|
for k,v in ipairs(names) do
|
|
if force_scan[v] or not is_known(v) then
|
|
table.insert(finder_searches, v)
|
|
search = true
|
|
elseif validators[k] then
|
|
if not validators[k](df.global[v]) then
|
|
dfhack.printerr('Validation failed for '..v..', will try to find again')
|
|
table.insert(finder_searches, v)
|
|
search = true
|
|
end
|
|
end
|
|
end
|
|
if search then
|
|
local ok, err = dfhack.safecall(finder)
|
|
if not ok then
|
|
if tostring(err):find('abort') or not utils.prompt_yes_no('Proceed with the rest of the script?') then
|
|
searcher:reset()
|
|
qerror('Quit')
|
|
end
|
|
end
|
|
else
|
|
print('Already known: '..table.concat(names,', '))
|
|
end
|
|
end
|
|
|
|
local ordinal_names = {
|
|
[0] = '1st entry',
|
|
[1] = '2nd entry',
|
|
[2] = '3rd entry'
|
|
}
|
|
setmetatable(ordinal_names, {
|
|
__index = function(self,idx) return (idx+1)..'th entry' end
|
|
})
|
|
|
|
local function list_index_choices(length_func)
|
|
return function(id)
|
|
if id > 0 then
|
|
local ok, len = pcall(length_func)
|
|
if not ok then
|
|
len = 5
|
|
elseif len > 10 then
|
|
len = 10
|
|
end
|
|
return id % len
|
|
else
|
|
return 0
|
|
end
|
|
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
|
|
try_save_cursor()
|
|
for _,v in ipairs({...}) do
|
|
gui.simulateInput(screen, v)
|
|
end
|
|
end
|
|
|
|
local function dwarfmode_step_frames(count)
|
|
local screen = screen_dwarfmode()
|
|
if not df.isvalid(screen) then
|
|
qerror('could not retrieve dwarfmode screen')
|
|
end
|
|
|
|
for i = 1,(count or 1) do
|
|
gui.simulateInput(screen, 'D_ONESTEP')
|
|
if screen.keyRepeat ~= 1 then
|
|
qerror('Could not step one frame: . did not work')
|
|
end
|
|
screen:logic()
|
|
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 prev_cursor = df.global.T_cursor:new()
|
|
prev_cursor.x = -30000
|
|
function try_save_cursor()
|
|
if not dfhack.internal.getAddress('cursor') then return end
|
|
for _, v in pairs(df.global.cursor) do
|
|
if v < 0 then
|
|
return
|
|
end
|
|
end
|
|
prev_cursor:assign(df.global.cursor)
|
|
end
|
|
|
|
function try_restore_cursor()
|
|
if not dfhack.internal.getAddress('cursor') then return end
|
|
if prev_cursor.x >= 0 then
|
|
df.global.cursor:assign(prev_cursor)
|
|
dwarfmode_feed_input('CURSOR_DOWN_Z', 'CURSOR_UP_Z')
|
|
end
|
|
end
|
|
|
|
local function feed_menu_choice(catnames,catkeys,enum,enter_seq,exit_seq,prompt)
|
|
return function (idx)
|
|
if idx == 0 and prompt and not prompt_proceed(2) then
|
|
return false
|
|
end
|
|
if idx > 0 then
|
|
dwarfmode_feed_input(table.unpack(exit_seq or {}))
|
|
end
|
|
idx = idx % #catnames + 1
|
|
dwarfmode_feed_input(table.unpack(enter_seq or {}))
|
|
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 prompt_proceed(2) then
|
|
return false
|
|
end
|
|
return true, 0
|
|
end
|
|
end
|
|
end
|
|
|
|
local function feed_menu_bool(enter_seq, exit_seq)
|
|
return function(idx)
|
|
if idx == 0 then
|
|
if not prompt_proceed(2) then
|
|
return false
|
|
end
|
|
return true, 0
|
|
end
|
|
if idx == 5 then
|
|
print(' Please resize the game window.')
|
|
if not prompt_proceed(2) then
|
|
return false
|
|
end
|
|
end
|
|
if idx%2 == 1 then
|
|
dwarfmode_feed_input(table.unpack(enter_seq))
|
|
return true, 1
|
|
else
|
|
dwarfmode_feed_input(table.unpack(exit_seq))
|
|
return true, 0
|
|
end
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Cursor group
|
|
--
|
|
|
|
local function find_cursor()
|
|
if not screen_title() then
|
|
return false
|
|
end
|
|
|
|
-- Unpadded version
|
|
local idx, addr = data.int32_t:find_one{
|
|
-30000, -30000, -30000,
|
|
-30000, -30000, -30000, -30000, -30000, -30000,
|
|
df.game_mode.NONE, df.game_type.NONE
|
|
}
|
|
if idx then
|
|
ms.found_offset('cursor', addr)
|
|
ms.found_offset('selection_rect', addr + 12)
|
|
ms.found_offset('gamemode', addr + 12 + 24)
|
|
ms.found_offset('gametype', addr + 12 + 24 + 4)
|
|
return true
|
|
end
|
|
|
|
-- Padded version
|
|
idx, addr = data.int32_t:find_one{
|
|
-30000, -30000, -30000, 0,
|
|
-30000, -30000, -30000, -30000, -30000, -30000, 0, 0,
|
|
df.game_mode.NONE, 0, 0, 0, df.game_type.NONE
|
|
}
|
|
if idx then
|
|
ms.found_offset('cursor', addr)
|
|
ms.found_offset('selection_rect', addr + 0x10)
|
|
ms.found_offset('gamemode', addr + 0x30)
|
|
ms.found_offset('gametype', addr + 0x40)
|
|
return true
|
|
end
|
|
|
|
dfhack.printerr('Could not find cursor.')
|
|
return false
|
|
end
|
|
|
|
--
|
|
-- Announcements
|
|
--
|
|
|
|
local function find_announcements()
|
|
local idx, addr = data.int32_t:find_one{
|
|
25, 25, 31, 31, 24, 24, 40, 40, 40, 40, 40, 40, 40
|
|
}
|
|
if idx then
|
|
ms.found_offset('announcements', addr)
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find announcements.')
|
|
end
|
|
|
|
--
|
|
-- d_init
|
|
--
|
|
|
|
local function is_valid_d_init(di)
|
|
if di.sky_tile ~= 178 then
|
|
print('Sky tile expected 178, found: '..di.sky_tile)
|
|
if not utils.prompt_yes_no('Ignore?') then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local ann = is_known 'announcements'
|
|
local size,ptr = di:sizeof()
|
|
if ann and ptr+size ~= ann then
|
|
print('Announcements not immediately after d_init.')
|
|
if not utils.prompt_yes_no('Ignore?') then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_d_init()
|
|
local idx, addr = data.int16_t:find_one{
|
|
1,0, 2,0, 5,0, 25,0, -- path_cost
|
|
4,4, -- embark_rect
|
|
20,1000,1000,1000,1000 -- store_dist
|
|
}
|
|
if idx then
|
|
validate_offset('d_init', is_valid_d_init, addr, df.d_init, 'path_cost')
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find d_init')
|
|
end
|
|
|
|
--
|
|
-- gview
|
|
--
|
|
|
|
local function find_gview()
|
|
local vs_vtable = dfhack.internal.getVTable('viewscreenst')
|
|
if not vs_vtable then
|
|
dfhack.printerr('Cannot search for gview - no viewscreenst vtable.')
|
|
return
|
|
end
|
|
|
|
local idx, addr = data.uint32_t:find_one{0, vs_vtable}
|
|
if idx then
|
|
ms.found_offset('gview', addr)
|
|
return
|
|
end
|
|
|
|
idx, addr = data.uint32_t:find_one{100, vs_vtable}
|
|
if idx then
|
|
ms.found_offset('gview', addr)
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find gview')
|
|
end
|
|
|
|
--
|
|
-- enabler
|
|
--
|
|
|
|
local function lookup_colors()
|
|
local f = io.open('data/init/colors.txt', 'r') or error('failed to open file')
|
|
local text = f:read('*all')
|
|
f:close()
|
|
local colors = {}
|
|
for _, color in pairs({'BLACK', 'BLUE', 'GREEN', 'CYAN', 'RED', 'MAGENTA',
|
|
'BROWN', 'LGRAY', 'DGRAY', 'LBLUE', 'LGREEN', 'LCYAN', 'LRED',
|
|
'LMAGENTA', 'YELLOW', 'WHITE'}) do
|
|
for _, part in pairs({'R', 'G', 'B'}) do
|
|
local opt = color .. '_' .. part
|
|
table.insert(colors, tonumber(text:match(opt .. ':(%d+)') or error('missing from colors.txt: ' .. opt)))
|
|
end
|
|
end
|
|
return colors
|
|
end
|
|
|
|
local function is_valid_enabler(e)
|
|
if not ms.is_valid_vector(e.textures.raws, 4)
|
|
or not ms.is_valid_vector(e.text_system, 4)
|
|
then
|
|
dfhack.printerr('Vector layout check failed.')
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_enabler()
|
|
-- Data from data/init/colors.txt
|
|
local default_colors = {
|
|
0, 0, 0, 0, 0, 128, 0, 128, 0,
|
|
0, 128, 128, 128, 0, 0, 128, 0, 128,
|
|
128, 128, 0, 192, 192, 192, 128, 128, 128,
|
|
0, 0, 255, 0, 255, 0, 0, 255, 255,
|
|
255, 0, 0, 255, 0, 255, 255, 255, 0,
|
|
255, 255, 255
|
|
}
|
|
local colors
|
|
local ok, ret = pcall(lookup_colors)
|
|
if not ok then
|
|
dfhack.printerr('Failed to look up colors, using defaults: \n' .. ret)
|
|
colors = default_colors
|
|
else
|
|
colors = ret
|
|
end
|
|
|
|
for i = 1,#colors do colors[i] = colors[i]/255 end
|
|
|
|
local idx, addr = data.float:find_one(colors)
|
|
if not idx then
|
|
idx, addr = data.float:find_one(default_colors)
|
|
end
|
|
if idx then
|
|
validate_offset('enabler', is_valid_enabler, addr, df.enabler, 'ccolor')
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find enabler')
|
|
end
|
|
|
|
--
|
|
-- gps
|
|
--
|
|
|
|
local function is_valid_gps(g)
|
|
if g.clipx[0] < 0 or g.clipx[0] > g.clipx[1] or g.clipx[1] >= g.dimx then
|
|
dfhack.printerr('Invalid clipx: ', g.clipx[0], g.clipx[1], g.dimx)
|
|
end
|
|
if g.clipy[0] < 0 or g.clipy[0] > g.clipy[1] or g.clipy[1] >= g.dimy then
|
|
dfhack.printerr('Invalid clipy: ', g.clipy[0], g.clipy[1], g.dimy)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_gps()
|
|
print('\nPlease ensure the mouse cursor is not over the game window.')
|
|
if not prompt_proceed() then
|
|
return
|
|
end
|
|
|
|
local zone
|
|
if os_type == 'windows' or os_type == 'linux' then
|
|
zone = zoomed_searcher('cursor', 0x1000)
|
|
elseif os_type == 'darwin' then
|
|
zone = zoomed_searcher('enabler', 0x1000)
|
|
end
|
|
zone = zone or searcher
|
|
|
|
local w,h = ms.get_screen_size()
|
|
|
|
local idx, addr = zone.area.int32_t:find_one{w, h, -1, -1}
|
|
if not idx then
|
|
idx, addr = data.int32_t:find_one{w, h, -1, -1}
|
|
end
|
|
if idx then
|
|
validate_offset('gps', is_valid_gps, addr, df.graphic, 'dimx')
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find gps')
|
|
end
|
|
|
|
--
|
|
-- World
|
|
--
|
|
|
|
local function is_valid_world(world)
|
|
if not ms.is_valid_vector(world.units.all, 4)
|
|
or not ms.is_valid_vector(world.units.active, 4)
|
|
or not ms.is_valid_vector(world.units.bad, 4)
|
|
or not ms.is_valid_vector(world.history.figures, 4)
|
|
or not ms.is_valid_vector(world.features.map_features, 4)
|
|
then
|
|
dfhack.printerr('Vector layout check failed.')
|
|
return false
|
|
end
|
|
|
|
if #world.units.all == 0 or #world.units.all ~= #world.units.bad then
|
|
print('Different or zero size of units.all and units.bad:'..#world.units.all..' vs '..#world.units.bad)
|
|
if not utils.prompt_yes_no('Ignore?') then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_world()
|
|
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', catnames, df.stockpile_category
|
|
)
|
|
end
|
|
|
|
validate_offset('world', is_valid_world, addr, df.world, 'selected_stockpile_type')
|
|
end
|
|
|
|
--
|
|
-- UI
|
|
--
|
|
|
|
local function is_valid_ui(ui)
|
|
if not ms.is_valid_vector(ui.economic_stone, 1)
|
|
or not ms.is_valid_vector(ui.dipscripts, 4)
|
|
then
|
|
dfhack.printerr('Vector layout check failed.')
|
|
return false
|
|
end
|
|
|
|
if ui.follow_item ~= -1 or ui.follow_unit ~= -1 then
|
|
print('Invalid follow state: '..ui.follow_item..', '..ui.follow_unit)
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_ui()
|
|
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', catnames, df.ui_sidebar_mode
|
|
)
|
|
end
|
|
|
|
validate_offset('ui', is_valid_ui, addr, df.ui, 'main', 'mode')
|
|
end
|
|
|
|
--
|
|
-- ui_sidebar_menus
|
|
--
|
|
|
|
local function is_valid_ui_sidebar_menus(usm)
|
|
if not ms.is_valid_vector(usm.workshop_job.choices_all, 4)
|
|
or not ms.is_valid_vector(usm.workshop_job.choices_visible, 4)
|
|
then
|
|
dfhack.printerr('Vector layout check failed.')
|
|
return false
|
|
end
|
|
|
|
if #usm.workshop_job.choices_all == 0
|
|
or #usm.workshop_job.choices_all ~= #usm.workshop_job.choices_visible then
|
|
print('Different or zero size of visible and all choices:'..
|
|
#usm.workshop_job.choices_all..' vs '..#usm.workshop_job.choices_visible)
|
|
if not utils.prompt_yes_no('Ignore?') then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_ui_sidebar_menus()
|
|
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's,
|
|
Craftsdwarf's, or Carpenter's workshop:]],
|
|
'int32_t',
|
|
function(idx)
|
|
if idx == 0 then
|
|
prompt_proceed(2)
|
|
-- ensure that the job list isn't full
|
|
dwarfmode_feed_input('BUILDJOB_CANCEL', 'BUILDJOB_ADD')
|
|
return true, 0
|
|
end
|
|
return feed_list_choice(7)(idx)
|
|
end,
|
|
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
|
|
)
|
|
end
|
|
|
|
validate_offset('ui_sidebar_menus', is_valid_ui_sidebar_menus,
|
|
addr, df.ui_sidebar_menus, 'workshop_job', 'cursor')
|
|
end
|
|
|
|
--
|
|
-- ui_build_selector
|
|
--
|
|
|
|
local function is_valid_ui_build_selector(ubs)
|
|
if not ms.is_valid_vector(ubs.requirements, 4)
|
|
or not ms.is_valid_vector(ubs.choices, 4)
|
|
then
|
|
dfhack.printerr('Vector layout check failed.')
|
|
return false
|
|
end
|
|
|
|
if ubs.building_type ~= df.building_type.Trap
|
|
or ubs.building_subtype ~= df.trap_type.PressurePlate then
|
|
print('Invalid building type and subtype:'..ubs.building_type..','..ubs.building_subtype)
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_ui_build_selector()
|
|
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_UP')
|
|
end
|
|
return true, 5000 + 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, knowing that the ui shows 5000 as 5K:]],
|
|
'int32_t',
|
|
{ 5000, 6000, 7000, 8000, 9000, 10000, 11000 }
|
|
)
|
|
end
|
|
|
|
validate_offset('ui_build_selector', is_valid_ui_build_selector,
|
|
addr, df.ui_build_selector, 'plate_info', 'unit_min')
|
|
end
|
|
|
|
--
|
|
-- init
|
|
--
|
|
|
|
local function is_valid_init(i)
|
|
-- derived from curses_*.png image sizes presumably
|
|
if i.font.small_font_dispx ~= 8 or i.font.small_font_dispy ~= 12 or
|
|
i.font.large_font_dispx ~= 10 or i.font.large_font_dispy ~= 12 then
|
|
print('Unexpected font sizes: ',
|
|
i.font.small_font_dispx, i.font.small_font_dispy,
|
|
i.font.large_font_dispx, i.font.large_font_dispy)
|
|
if not utils.prompt_yes_no('Ignore?') then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function find_init()
|
|
local zone
|
|
if os_type == 'windows' then
|
|
zone = zoomed_searcher('ui_build_selector', 0x3000)
|
|
elseif os_type == 'linux' or os_type == 'darwin' then
|
|
zone = zoomed_searcher('d_init', -0x2000)
|
|
end
|
|
zone = zone or searcher
|
|
|
|
local idx, addr = zone.area.int32_t:find_one{250, 150, 15, 0}
|
|
if idx then
|
|
validate_offset('init', is_valid_init, addr, df.init, 'input', 'hold_time')
|
|
return
|
|
end
|
|
|
|
local w,h = ms.get_screen_size()
|
|
|
|
local idx, addr = zone.area.int32_t:find_one{w, h}
|
|
if idx then
|
|
validate_offset('init', is_valid_init, addr, df.init, 'display', 'grid_x')
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find init')
|
|
end
|
|
|
|
--
|
|
-- current_weather
|
|
--
|
|
|
|
local function find_current_weather()
|
|
local zone
|
|
if os_type == 'windows' then
|
|
zone = zoomed_searcher('crime_next_id', 512)
|
|
elseif os_type == 'darwin' then
|
|
zone = zoomed_searcher('cursor', -64)
|
|
elseif os_type == 'linux' then
|
|
zone = zoomed_searcher('ui_building_assign_type', -512)
|
|
end
|
|
zone = zone or searcher
|
|
|
|
local wbytes = {
|
|
2, 1, 0, 2, 0,
|
|
1, 2, 1, 0, 0,
|
|
2, 0, 2, 1, 2,
|
|
1, 2, 0, 1, 1,
|
|
2, 0, 1, 0, 2
|
|
}
|
|
|
|
local idx, addr = zone.area.int8_t:find_one(wbytes)
|
|
if idx then
|
|
ms.found_offset('current_weather', addr)
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find current_weather - must be a wrong save.')
|
|
end
|
|
|
|
--
|
|
-- ui_menu_width
|
|
--
|
|
|
|
local function find_ui_menu_width()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
addr = searcher:find_interactive('Auto-searching for ui_menu_width', 'int8_t', function(idx)
|
|
local val = (idx % 3) + 1
|
|
if idx == 0 then
|
|
print('Switch to the default [map][menu][map] layout (with Tab)')
|
|
if not prompt_proceed(2) then return false end
|
|
else
|
|
dwarfmode_feed_input('CHANGETAB', val ~= 3 and 'CHANGETAB')
|
|
end
|
|
return true, val
|
|
end)
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_menu_cursor([[
|
|
Searching for ui_menu_width. Please exit to the main
|
|
dwarfmode menu, then use Tab to do as instructed below:]],
|
|
'int8_t',
|
|
{ 2, 3, 1 },
|
|
{ [2] = 'switch to the most usual [mapmap][menu] layout',
|
|
[3] = 'hide the menu completely',
|
|
[1] = 'switch to the default [map][menu][map] layout' }
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_menu_width', addr)
|
|
|
|
-- NOTE: Assume that the vars are adjacent, as always
|
|
ms.found_offset('ui_area_map_width', addr+1)
|
|
|
|
-- reset to make sure view is small enough for window_x/y scan on small maps
|
|
df.global.ui_menu_width = 2
|
|
df.global.ui_area_map_width = 3
|
|
end
|
|
|
|
--
|
|
-- ui_selected_unit
|
|
--
|
|
|
|
local function find_ui_selected_unit()
|
|
if not is_known 'world' then
|
|
dfhack.printerr('Cannot search for ui_selected_unit: no world')
|
|
return
|
|
end
|
|
|
|
for i,unit in ipairs(df.global.world.units.active) do
|
|
-- This function does a lot of things and accesses histfigs, souls and so on:
|
|
--dfhack.units.setNickname(unit, i)
|
|
|
|
-- Instead use just a simple bit of code that only requires the start of the
|
|
-- unit to be valid. It may not work properly with vampires or reset later
|
|
-- if unpaused, but is sufficient for this script and won't crash:
|
|
unit.name.nickname = tostring(i)
|
|
unit.name.has_name = true
|
|
end
|
|
|
|
local addr = searcher:find_menu_cursor([[
|
|
Searching for ui_selected_unit. Please activate the 'v'
|
|
mode, point it at units, and enter their numeric nickname
|
|
into the prompts below:]],
|
|
'int32_t',
|
|
function()
|
|
return utils.prompt_input(' Enter index: ', utils.check_number)
|
|
end,
|
|
'noprompt'
|
|
)
|
|
ms.found_offset('ui_selected_unit', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_unit_view_mode
|
|
--
|
|
|
|
local function find_ui_unit_view_mode()
|
|
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', catnames, df.ui_unit_view_mode.T_value
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_unit_view_mode', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_look_cursor
|
|
--
|
|
|
|
local function look_item_list_count()
|
|
return #df.global.ui_look_list.items
|
|
end
|
|
|
|
local function find_ui_look_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
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_look_cursor', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_building_item_cursor
|
|
--
|
|
|
|
local function building_item_list_count()
|
|
return #df.global.world.selected_building.contained_items
|
|
end
|
|
|
|
local function find_ui_building_item_cursor()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
dwarfmode_feed_input('D_BUILDITEM')
|
|
|
|
addr = searcher:find_interactive([[
|
|
Auto-searching for ui_building_item_cursor. Please highlight a
|
|
workshop, trade depot or other building with at least 5 contained
|
|
items, and select as instructed:]],
|
|
'int32_t',
|
|
feed_list_choice(building_item_list_count),
|
|
20
|
|
)
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_menu_cursor([[
|
|
Searching for ui_building_item_cursor. Please activate the 't'
|
|
mode, find a cluttered workshop, trade depot, or other building
|
|
with many contained items, and select as instructed:]],
|
|
'int32_t',
|
|
list_index_choices(building_item_list_count),
|
|
ordinal_names
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_building_item_cursor', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_workshop_in_add
|
|
--
|
|
|
|
local function find_ui_workshop_in_add()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
dwarfmode_feed_input('D_BUILDJOB')
|
|
|
|
addr = searcher:find_interactive([[
|
|
Auto-searching for ui_workshop_in_add. Please select a
|
|
workshop, e.g. Carpenters or Masons.]],
|
|
'int8_t',
|
|
feed_menu_bool(
|
|
{ 'BUILDJOB_CANCEL', 'BUILDJOB_ADD' },
|
|
{ 'SELECT', 'SELECT', 'SELECT', 'SELECT', 'SELECT' }
|
|
),
|
|
20
|
|
)
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_menu_cursor([[
|
|
Searching for ui_workshop_in_add. Please activate the 'q'
|
|
mode, find a workshop without jobs (or delete jobs),
|
|
and do as instructed below.
|
|
|
|
NOTE: If not done after first 3-4 steps, resize the game window.]],
|
|
'int8_t',
|
|
{ 1, 0 },
|
|
{ [1] = 'enter the add job menu',
|
|
[0] = 'add job, thus exiting the menu' }
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_workshop_in_add', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_workshop_job_cursor
|
|
--
|
|
|
|
local function workshop_job_list_count()
|
|
return #df.global.world.selected_building.jobs
|
|
end
|
|
|
|
local function find_ui_workshop_job_cursor()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
dwarfmode_feed_input('D_BUILDJOB')
|
|
addr = searcher:find_interactive([[
|
|
Auto-searching for ui_workshop_job_cursor. Please highlight a
|
|
Mason's or Carpenter's workshop, or any building with a job
|
|
selection interface navigable with just "Enter":]],
|
|
'int32_t',
|
|
function(idx)
|
|
if idx == 0 then prompt_proceed(2) end
|
|
for i = 1, 10 - workshop_job_list_count() do
|
|
dwarfmode_feed_input('BUILDJOB_ADD', 'SELECT', 'SELECT', 'SELECT', 'SELECT', 'SELECT')
|
|
end
|
|
dwarfmode_feed_input('SECONDSCROLL_DOWN')
|
|
-- adding jobs resets the cursor position, so it is difficult to determine here
|
|
return true
|
|
end,
|
|
20
|
|
)
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_menu_cursor([[
|
|
Searching for ui_workshop_job_cursor. Please activate the 'q'
|
|
mode, find a workshop with many jobs, and select as instructed:]],
|
|
'int32_t',
|
|
list_index_choices(workshop_job_list_count),
|
|
ordinal_names
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_workshop_job_cursor', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_building_in_assign
|
|
--
|
|
|
|
local function find_ui_building_in_assign()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
dwarfmode_feed_input('D_BUILDJOB')
|
|
try_restore_cursor()
|
|
|
|
addr = searcher:find_interactive([[
|
|
Auto-searching for ui_building_in_assign. Please select a room,
|
|
i.e. a bedroom, tomb, office, dining room or statue garden.]],
|
|
'int8_t',
|
|
feed_menu_bool(
|
|
{ { 'BUILDJOB_STATUE_ASSIGN', 'BUILDJOB_COFFIN_ASSIGN',
|
|
'BUILDJOB_CHAIR_ASSIGN', 'BUILDJOB_TABLE_ASSIGN',
|
|
'BUILDJOB_BED_ASSIGN' } },
|
|
{ 'LEAVESCREEN' }
|
|
),
|
|
20
|
|
)
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_menu_cursor([[
|
|
Searching for ui_building_in_assign. Please activate
|
|
the 'q' mode, select a room building (e.g. a bedroom)
|
|
and do as instructed below.
|
|
|
|
NOTE: If not done after first 3-4 steps, resize the game window.]],
|
|
'int8_t',
|
|
{ 1, 0 },
|
|
{ [1] = 'enter the Assign owner menu',
|
|
[0] = 'press Esc to exit assign' }
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_building_in_assign', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_building_in_resize
|
|
--
|
|
|
|
local function find_ui_building_in_resize()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
dwarfmode_feed_input('D_BUILDJOB')
|
|
try_restore_cursor()
|
|
|
|
addr = searcher:find_interactive([[
|
|
Auto-searching for ui_building_in_resize. Please select a room,
|
|
i.e. a bedroom, tomb, office, dining room or statue garden.]],
|
|
'int8_t',
|
|
feed_menu_bool(
|
|
{ { 'BUILDJOB_STATUE_SIZE', 'BUILDJOB_COFFIN_SIZE',
|
|
'BUILDJOB_CHAIR_SIZE', 'BUILDJOB_TABLE_SIZE',
|
|
'BUILDJOB_BED_SIZE' } },
|
|
{ 'LEAVESCREEN' }
|
|
),
|
|
20
|
|
)
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_menu_cursor([[
|
|
Searching for ui_building_in_resize. Please activate
|
|
the 'q' mode, select a room building (e.g. a bedroom)
|
|
and do as instructed below.
|
|
|
|
NOTE: If not done after first 3-4 steps, resize the game window.]],
|
|
'int8_t',
|
|
{ 1, 0 },
|
|
{ [1] = 'enter the Resize room mode',
|
|
[0] = 'press Esc to exit resize' }
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_building_in_resize', addr)
|
|
end
|
|
|
|
--
|
|
-- ui_lever_target_type
|
|
--
|
|
local function find_ui_lever_target_type()
|
|
local catnames = {
|
|
'Bridge', 'Door', 'Floodgate',
|
|
'Cage', 'Chain', 'TrackStop',
|
|
'GearAssembly',
|
|
}
|
|
local catkeys = {
|
|
'HOTKEY_TRAP_BRIDGE', 'HOTKEY_TRAP_DOOR', 'HOTKEY_TRAP_FLOODGATE',
|
|
'HOTKEY_TRAP_CAGE', 'HOTKEY_TRAP_CHAIN', 'HOTKEY_TRAP_TRACK_STOP',
|
|
'HOTKEY_TRAP_GEAR_ASSEMBLY',
|
|
}
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
dwarfmode_feed_input('D_BUILDJOB')
|
|
|
|
addr = searcher:find_interactive(
|
|
'Auto-searching for ui_lever_target_type. Please select a lever:',
|
|
'int8_t',
|
|
feed_menu_choice(catnames, catkeys, df.lever_target_type,
|
|
{'BUILDJOB_ADD'},
|
|
{'LEAVESCREEN', 'LEAVESCREEN'},
|
|
true -- prompt
|
|
),
|
|
20
|
|
)
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_menu_cursor([[
|
|
Searching for ui_lever_target_type. Please select a lever with
|
|
'q' and enter the "add task" menu with 'a':]],
|
|
'int8_t', catnames, df.lever_target_type
|
|
)
|
|
end
|
|
|
|
ms.found_offset('ui_lever_target_type', addr)
|
|
end
|
|
|
|
--
|
|
-- window_x
|
|
--
|
|
|
|
local function feed_window_xyz(dec,inc,step)
|
|
return function(idx)
|
|
if idx == 0 then
|
|
for i = 1,30 do dwarfmode_feed_input(dec) end
|
|
else
|
|
dwarfmode_feed_input(inc)
|
|
end
|
|
return true, nil, step
|
|
end
|
|
end
|
|
|
|
local function find_window_x()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
addr = searcher:find_interactive(
|
|
'Auto-searching for window_x.',
|
|
'int32_t',
|
|
feed_window_xyz('CURSOR_LEFT_FAST', 'CURSOR_RIGHT', 10),
|
|
20
|
|
)
|
|
|
|
dwarfmode_feed_input('D_HOTKEY1')
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_counter([[
|
|
Searching for window_x. Please exit to main dwarfmode menu,
|
|
scroll to the LEFT edge, then do as instructed:]],
|
|
'int32_t', 10,
|
|
'Please press Right to scroll right one step.'
|
|
)
|
|
end
|
|
|
|
ms.found_offset('window_x', addr)
|
|
end
|
|
|
|
--
|
|
-- window_y
|
|
--
|
|
|
|
local function find_window_y()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
addr = searcher:find_interactive(
|
|
'Auto-searching for window_y.',
|
|
'int32_t',
|
|
feed_window_xyz('CURSOR_UP_FAST', 'CURSOR_DOWN', 10),
|
|
20
|
|
)
|
|
|
|
dwarfmode_feed_input('D_HOTKEY1')
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_counter([[
|
|
Searching for window_y. Please exit to main dwarfmode menu,
|
|
scroll to the TOP edge, then do as instructed:]],
|
|
'int32_t', 10,
|
|
'Please press Down to scroll down one step.'
|
|
)
|
|
end
|
|
|
|
ms.found_offset('window_y', addr)
|
|
end
|
|
|
|
--
|
|
-- window_z
|
|
--
|
|
|
|
local function find_window_z()
|
|
local addr
|
|
|
|
if dwarfmode_to_top() then
|
|
addr = searcher:find_interactive(
|
|
'Auto-searching for window_z.',
|
|
'int32_t',
|
|
feed_window_xyz('CURSOR_UP_Z', 'CURSOR_DOWN_Z', -1),
|
|
30
|
|
)
|
|
|
|
dwarfmode_feed_input('D_HOTKEY1')
|
|
end
|
|
|
|
if not addr then
|
|
addr = searcher:find_counter([[
|
|
Searching for window_z. Please exit to main dwarfmode menu,
|
|
scroll to a Z level near surface, then do as instructed below.
|
|
|
|
NOTE: If not done after first 3-4 steps, resize the game window.]],
|
|
'int32_t', -1,
|
|
"Please press '>' to scroll one Z level down."
|
|
)
|
|
end
|
|
|
|
ms.found_offset('window_z', addr)
|
|
end
|
|
|
|
--
|
|
-- cur_year
|
|
--
|
|
|
|
local function find_cur_year()
|
|
local zone
|
|
if os_type == 'windows' then
|
|
zone = zoomed_searcher('formation_next_id', 32)
|
|
elseif os_type == 'darwin' then
|
|
zone = zoomed_searcher('cursor', -32)
|
|
elseif os_type == 'linux' then
|
|
zone = zoomed_searcher('ui_building_assign_type', -512)
|
|
end
|
|
if not zone then
|
|
dfhack.printerr('Cannot search for cur_year - prerequisites missing.')
|
|
return
|
|
end
|
|
|
|
local yvalue = utils.prompt_input('Please enter current in-game year: ', utils.check_number)
|
|
local idx, addr = zone.area.int32_t:find_one{yvalue}
|
|
if idx then
|
|
ms.found_offset('cur_year', addr)
|
|
return
|
|
end
|
|
|
|
dfhack.printerr('Could not find cur_year')
|
|
end
|
|
|
|
--
|
|
-- cur_year_tick
|
|
--
|
|
|
|
function stop_autosave()
|
|
if is_known 'd_init' then
|
|
local f = df.global.d_init.flags4
|
|
if f.AUTOSAVE_SEASONAL or f.AUTOSAVE_YEARLY then
|
|
f.AUTOSAVE_SEASONAL = false
|
|
f.AUTOSAVE_YEARLY = false
|
|
print('Disabled seasonal and yearly autosave.')
|
|
end
|
|
else
|
|
dfhack.printerr('Could not disable autosave!')
|
|
end
|
|
end
|
|
|
|
function step_n_frames(cnt, feed)
|
|
local world = df.global.world
|
|
local ctick = world.frame_counter
|
|
|
|
if feed then
|
|
print(" Auto-stepping "..cnt.." frames.")
|
|
dwarfmode_step_frames(cnt)
|
|
return world.frame_counter-ctick
|
|
end
|
|
|
|
local more = ''
|
|
while world.frame_counter-ctick < cnt do
|
|
print(" Please step the game "..(cnt-world.frame_counter+ctick)..more.." frames.")
|
|
more = ' more'
|
|
if not prompt_proceed(2) then
|
|
return nil
|
|
end
|
|
end
|
|
return world.frame_counter-ctick
|
|
end
|
|
|
|
local function find_cur_year_tick()
|
|
local zone
|
|
if os_type == 'windows' then
|
|
zone = zoomed_searcher('ui_unit_view_mode', 0x200)
|
|
else
|
|
zone = zoomed_searcher('cur_year', 128)
|
|
end
|
|
if not zone then
|
|
dfhack.printerr('Cannot search for cur_year_tick - prerequisites missing.')
|
|
return
|
|
end
|
|
|
|
stop_autosave()
|
|
|
|
local feed = dwarfmode_to_top()
|
|
local addr = zone:find_interactive(
|
|
'Searching for cur_year_tick.',
|
|
'int32_t',
|
|
function(idx)
|
|
if idx > 0 then
|
|
if not step_n_frames(1, feed) then
|
|
return false
|
|
end
|
|
end
|
|
return true, nil, 1
|
|
end,
|
|
20
|
|
)
|
|
|
|
ms.found_offset('cur_year_tick', addr)
|
|
end
|
|
|
|
local function find_cur_year_tick_advmode()
|
|
stop_autosave()
|
|
|
|
local feed = dwarfmode_to_top()
|
|
local addr = searcher:find_interactive(
|
|
'Searching for cur_year_tick_advmode.',
|
|
'int32_t',
|
|
function(idx)
|
|
if idx > 0 then
|
|
if not step_n_frames(1, feed) then
|
|
return false
|
|
end
|
|
end
|
|
return true, nil, 144
|
|
end,
|
|
20
|
|
)
|
|
|
|
ms.found_offset('cur_year_tick_advmode', addr)
|
|
end
|
|
|
|
--
|
|
-- cur_season_tick
|
|
--
|
|
|
|
local function find_cur_season_tick()
|
|
if not (is_known 'cur_year_tick') then
|
|
dfhack.printerr('Cannot search for cur_season_tick - prerequisites missing.')
|
|
return
|
|
end
|
|
|
|
stop_autosave()
|
|
|
|
local feed = dwarfmode_to_top()
|
|
local addr = searcher:find_interactive([[
|
|
Searching for cur_season_tick. Please exit to main dwarfmode
|
|
menu, then do as instructed below:]],
|
|
'int32_t',
|
|
function(ccursor)
|
|
if ccursor > 0 then
|
|
if not step_n_frames(10, feed) then
|
|
return false
|
|
end
|
|
end
|
|
return true, math.floor((df.global.cur_year_tick%100800)/10)
|
|
end
|
|
)
|
|
ms.found_offset('cur_season_tick', addr)
|
|
end
|
|
|
|
--
|
|
-- cur_season
|
|
--
|
|
|
|
local function find_cur_season()
|
|
if not (is_known 'cur_year_tick' and is_known 'cur_season_tick') then
|
|
dfhack.printerr('Cannot search for cur_season - prerequisites missing.')
|
|
return
|
|
end
|
|
|
|
stop_autosave()
|
|
|
|
local feed = dwarfmode_to_top()
|
|
local addr = searcher:find_interactive([[
|
|
Searching for cur_season. Please exit to main dwarfmode
|
|
menu, then do as instructed below:]],
|
|
'int8_t',
|
|
function(ccursor)
|
|
if ccursor > 0 then
|
|
local cst = df.global.cur_season_tick
|
|
df.global.cur_season_tick = 10079
|
|
df.global.cur_year_tick = df.global.cur_year_tick + (10079-cst)*10
|
|
if not step_n_frames(10, feed) then
|
|
return false
|
|
end
|
|
end
|
|
return true, math.floor(df.global.cur_year_tick/100800)%4
|
|
end
|
|
)
|
|
ms.found_offset('cur_season', addr)
|
|
end
|
|
|
|
--
|
|
-- process_jobs
|
|
--
|
|
|
|
local function get_process_zone()
|
|
if os_type == 'windows' then
|
|
return zoomed_searcher('ui_workshop_job_cursor', 'ui_building_in_resize')
|
|
elseif os_type == 'linux' or os_type == 'darwin' then
|
|
return zoomed_searcher('cur_year', 'cur_year_tick')
|
|
end
|
|
end
|
|
|
|
local function find_process_jobs()
|
|
local zone = get_process_zone() or searcher
|
|
local addr
|
|
|
|
stop_autosave()
|
|
|
|
if dwarfmode_to_top() and dfhack.internal.getAddress('cursor') then
|
|
local cursor = df.global.T_cursor:new()
|
|
addr = zone:find_interactive([[
|
|
Searching for process_jobs. Please position the cursor to the right
|
|
of at least 10 vacant natural floor tiles.]],
|
|
'int8_t',
|
|
function(idx)
|
|
if idx == 0 then
|
|
dwarfmode_feed_input('D_LOOK')
|
|
if not prompt_proceed(2) then return false end
|
|
cursor:assign(df.global.cursor)
|
|
elseif idx == 6 then
|
|
print(' Please resize the game window.')
|
|
if not prompt_proceed(2) then return false end
|
|
end
|
|
dwarfmode_to_top()
|
|
dwarfmode_step_frames(1)
|
|
if idx % 2 == 0 then
|
|
dwarfmode_feed_input(
|
|
'D_BUILDING',
|
|
'HOTKEY_BUILDING_CONSTRUCTION',
|
|
'HOTKEY_BUILDING_CONSTRUCTION_WALL'
|
|
)
|
|
df.global.cursor:assign(cursor)
|
|
df.global.cursor.x = df.global.cursor.x + (idx / 2)
|
|
dwarfmode_feed_input('CURSOR_RIGHT', 'CURSOR_LEFT', 'SELECT', 'SELECT')
|
|
return true, 1
|
|
else
|
|
return true, 0
|
|
end
|
|
end,
|
|
20)
|
|
end
|
|
|
|
if not addr then
|
|
local addr = zone:find_menu_cursor([[
|
|
Searching for process_jobs. Please do as instructed below:]],
|
|
'int8_t',
|
|
{ 1, 0 },
|
|
{ [1] = 'designate a building to be constructed, e.g a bed or a wall',
|
|
[0] = 'step or unpause the game to reset the flag' }
|
|
)
|
|
end
|
|
ms.found_offset('process_jobs', addr)
|
|
end
|
|
|
|
--
|
|
-- process_dig
|
|
--
|
|
|
|
local function find_process_dig()
|
|
local zone = get_process_zone() or searcher
|
|
local addr
|
|
|
|
stop_autosave()
|
|
|
|
if dwarfmode_to_top() and dfhack.internal.getAddress('cursor') then
|
|
local cursor = df.global.T_cursor:new()
|
|
addr = zone:find_interactive([[
|
|
Searching for process_dig. Please position the cursor to the right
|
|
of at least 10 unmined, unrevealed tiles.]],
|
|
'int8_t',
|
|
function(idx)
|
|
if idx == 0 then
|
|
dwarfmode_feed_input('D_LOOK')
|
|
if not prompt_proceed(2) then return false end
|
|
cursor:assign(df.global.cursor)
|
|
elseif idx == 6 then
|
|
print(' Please resize the game window.')
|
|
if not prompt_proceed(2) then return false end
|
|
end
|
|
dwarfmode_to_top()
|
|
dwarfmode_step_frames(1)
|
|
if idx % 2 == 0 then
|
|
dwarfmode_feed_input('D_DESIGNATE', 'DESIGNATE_DIG')
|
|
df.global.cursor:assign(cursor)
|
|
df.global.cursor.x = df.global.cursor.x + (idx / 2)
|
|
dwarfmode_feed_input('SELECT', 'SELECT')
|
|
return true, 1
|
|
else
|
|
return true, 0
|
|
end
|
|
end,
|
|
20)
|
|
end
|
|
|
|
if not addr then
|
|
addr = zone:find_menu_cursor([[
|
|
Searching for process_dig. Please do as instructed below:]],
|
|
'int8_t',
|
|
{ 1, 0 },
|
|
{ [1] = 'designate a tile to be mined out',
|
|
[0] = 'step or unpause the game to reset the flag' }
|
|
)
|
|
end
|
|
ms.found_offset('process_dig', addr)
|
|
end
|
|
|
|
--
|
|
-- pause_state
|
|
--
|
|
|
|
local function find_pause_state()
|
|
local zone, addr
|
|
if os_type == 'linux' or os_type == 'darwin' then
|
|
zone = zoomed_searcher('ui_look_cursor', 32)
|
|
elseif os_type == 'windows' then
|
|
zone = zoomed_searcher('ui_workshop_job_cursor', 80)
|
|
end
|
|
zone = zone or searcher
|
|
|
|
stop_autosave()
|
|
|
|
if dwarfmode_to_top() then
|
|
addr = zone:find_interactive(
|
|
'Auto-searching for pause_state',
|
|
'int8_t',
|
|
function(idx)
|
|
if idx%2 == 0 then
|
|
dwarfmode_feed_input('D_ONESTEP')
|
|
return true, 0
|
|
else
|
|
screen_dwarfmode():logic()
|
|
return true, 1
|
|
end
|
|
end,
|
|
20
|
|
)
|
|
end
|
|
|
|
if not addr then
|
|
addr = zone:find_menu_cursor([[
|
|
Searching for pause_state. Please do as instructed below:]],
|
|
'int8_t',
|
|
{ 1, 0 },
|
|
{ [1] = 'PAUSE the game',
|
|
[0] = 'UNPAUSE the game' }
|
|
)
|
|
end
|
|
|
|
ms.found_offset('pause_state', addr)
|
|
end
|
|
|
|
--
|
|
-- standing orders
|
|
--
|
|
|
|
local function find_standing_orders(gname, seq, depends)
|
|
if type(seq) ~= 'table' then seq = {seq} end
|
|
for k, v in pairs(depends) do
|
|
if not dfhack.internal.getAddress(k) then
|
|
qerror(('Cannot locate %s: %s not found'):format(gname, k))
|
|
end
|
|
df.global[k] = v
|
|
end
|
|
local addr
|
|
if dwarfmode_to_top() then
|
|
addr = searcher:find_interactive(
|
|
'Auto-searching for ' .. gname,
|
|
'uint8_t',
|
|
function(idx)
|
|
dwarfmode_feed_input('D_ORDERS')
|
|
dwarfmode_feed_input(table.unpack(seq))
|
|
return true
|
|
end
|
|
)
|
|
else
|
|
dfhack.printerr("Won't scan for standing orders global manually: " .. gname)
|
|
return
|
|
end
|
|
|
|
ms.found_offset(gname, addr)
|
|
end
|
|
|
|
local function exec_finder_so(gname, seq, _depends)
|
|
local depends = {}
|
|
for k, v in pairs(_depends or {}) do
|
|
if k:find('standing_orders_') ~= 1 then
|
|
k = 'standing_orders_' .. k
|
|
end
|
|
depends[k] = v
|
|
end
|
|
if force_scan['standing_orders'] then
|
|
force_scan[gname] = true
|
|
end
|
|
exec_finder(function()
|
|
return find_standing_orders(gname, seq, depends)
|
|
end, gname)
|
|
end
|
|
|
|
--
|
|
-- MAIN FLOW
|
|
--
|
|
|
|
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', is_valid_d_init)
|
|
exec_finder(find_enabler, 'enabler', is_valid_enabler)
|
|
exec_finder(find_gps, 'gps', is_valid_gps)
|
|
|
|
print('\nCompound globals (need loaded world):\n')
|
|
|
|
print('\nPlease load the save previously processed with prepare-save.')
|
|
if not prompt_proceed() then
|
|
searcher:reset()
|
|
return
|
|
end
|
|
|
|
exec_finder(find_world, 'world', is_valid_world)
|
|
exec_finder(find_ui, 'ui', is_valid_ui)
|
|
exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus')
|
|
exec_finder(find_ui_build_selector, 'ui_build_selector')
|
|
exec_finder(find_init, 'init', is_valid_init)
|
|
|
|
print('\nPrimitive globals:\n')
|
|
|
|
exec_finder(find_current_weather, 'current_weather')
|
|
exec_finder(find_ui_menu_width, { 'ui_menu_width', 'ui_area_map_width' })
|
|
exec_finder(find_ui_selected_unit, 'ui_selected_unit')
|
|
exec_finder(find_ui_unit_view_mode, 'ui_unit_view_mode')
|
|
exec_finder(find_ui_look_cursor, 'ui_look_cursor')
|
|
exec_finder(find_ui_building_item_cursor, 'ui_building_item_cursor')
|
|
exec_finder(find_ui_workshop_in_add, 'ui_workshop_in_add')
|
|
exec_finder(find_ui_workshop_job_cursor, 'ui_workshop_job_cursor')
|
|
exec_finder(find_ui_building_in_assign, 'ui_building_in_assign')
|
|
exec_finder(find_ui_building_in_resize, 'ui_building_in_resize')
|
|
exec_finder(find_ui_lever_target_type, 'ui_lever_target_type')
|
|
exec_finder(find_window_x, 'window_x')
|
|
exec_finder(find_window_y, 'window_y')
|
|
exec_finder(find_window_z, 'window_z')
|
|
|
|
print('\nUnpausing globals:\n')
|
|
|
|
exec_finder(find_cur_year, 'cur_year')
|
|
exec_finder(find_cur_year_tick, 'cur_year_tick')
|
|
exec_finder(find_cur_year_tick_advmode, 'cur_year_tick_advmode')
|
|
exec_finder(find_cur_season_tick, 'cur_season_tick')
|
|
exec_finder(find_cur_season, 'cur_season')
|
|
exec_finder(find_process_jobs, 'process_jobs')
|
|
exec_finder(find_process_dig, 'process_dig')
|
|
exec_finder(find_pause_state, 'pause_state')
|
|
|
|
print('\nStanding orders:\n')
|
|
|
|
exec_finder_so('standing_orders_gather_animals', 'ORDERS_GATHER_ANIMALS')
|
|
exec_finder_so('standing_orders_gather_bodies', 'ORDERS_GATHER_BODIES')
|
|
exec_finder_so('standing_orders_gather_food', 'ORDERS_GATHER_FOOD')
|
|
exec_finder_so('standing_orders_gather_furniture', 'ORDERS_GATHER_FURNITURE')
|
|
exec_finder_so('standing_orders_gather_minerals', 'ORDERS_GATHER_STONE')
|
|
exec_finder_so('standing_orders_gather_wood', 'ORDERS_GATHER_WOOD')
|
|
|
|
exec_finder_so('standing_orders_gather_refuse',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_GATHER'})
|
|
exec_finder_so('standing_orders_gather_refuse_outside',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_OUTSIDE'}, {gather_refuse=1})
|
|
exec_finder_so('standing_orders_gather_vermin_remains',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_OUTSIDE_VERMIN'}, {gather_refuse=1, gather_refuse_outside=1})
|
|
exec_finder_so('standing_orders_dump_bones',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_BONE'}, {gather_refuse=1})
|
|
exec_finder_so('standing_orders_dump_corpses',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_CORPSE'}, {gather_refuse=1})
|
|
exec_finder_so('standing_orders_dump_hair',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_STRAND_TISSUE'}, {gather_refuse=1})
|
|
exec_finder_so('standing_orders_dump_other',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_OTHER'}, {gather_refuse=1})
|
|
exec_finder_so('standing_orders_dump_shells',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_SHELL'}, {gather_refuse=1})
|
|
exec_finder_so('standing_orders_dump_skins',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_SKIN'}, {gather_refuse=1})
|
|
exec_finder_so('standing_orders_dump_skulls',
|
|
{'ORDERS_REFUSE', 'ORDERS_REFUSE_DUMP_SKULL'}, {gather_refuse=1})
|
|
|
|
|
|
exec_finder_so('standing_orders_auto_butcher',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_BUTCHER'})
|
|
exec_finder_so('standing_orders_auto_collect_webs',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_COLLECT_WEB'})
|
|
exec_finder_so('standing_orders_auto_fishery',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_AUTO_FISHERY'})
|
|
exec_finder_so('standing_orders_auto_kiln',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_AUTO_KILN'})
|
|
exec_finder_so('standing_orders_auto_kitchen',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_AUTO_KITCHEN'})
|
|
exec_finder_so('standing_orders_auto_loom',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_LOOM'})
|
|
exec_finder_so('standing_orders_auto_other',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_AUTO_OTHER'})
|
|
exec_finder_so('standing_orders_auto_slaughter',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_SLAUGHTER'})
|
|
exec_finder_so('standing_orders_auto_smelter',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_AUTO_SMELTER'})
|
|
exec_finder_so('standing_orders_auto_tan',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_TAN'})
|
|
exec_finder_so('standing_orders_use_dyed_cloth',
|
|
{'ORDERS_WORKSHOP', 'ORDERS_DYED_CLOTH'})
|
|
|
|
exec_finder_so('standing_orders_forbid_other_dead_items',
|
|
{'ORDERS_AUTOFORBID', 'ORDERS_FORBID_OTHER_ITEMS'})
|
|
exec_finder_so('standing_orders_forbid_other_nohunt',
|
|
{'ORDERS_AUTOFORBID', 'ORDERS_FORBID_OTHER_CORPSE'})
|
|
exec_finder_so('standing_orders_forbid_own_dead',
|
|
{'ORDERS_AUTOFORBID', 'ORDERS_FORBID_YOUR_CORPSE'})
|
|
exec_finder_so('standing_orders_forbid_own_dead_items',
|
|
{'ORDERS_AUTOFORBID', 'ORDERS_FORBID_YOUR_ITEMS'})
|
|
exec_finder_so('standing_orders_forbid_used_ammo',
|
|
{'ORDERS_AUTOFORBID', 'ORDERS_FORBID_PROJECTILE'})
|
|
|
|
exec_finder_so('standing_orders_farmer_harvest', 'ORDERS_ALL_HARVEST')
|
|
exec_finder_so('standing_orders_job_cancel_announce', 'ORDERS_EXCEPTIONS')
|
|
exec_finder_so('standing_orders_mix_food', 'ORDERS_MIXFOODS')
|
|
|
|
exec_finder_so('standing_orders_zoneonly_drink',
|
|
{'ORDERS_ZONE', 'ORDERS_ZONE_DRINKING'})
|
|
exec_finder_so('standing_orders_zoneonly_fish',
|
|
{'ORDERS_ZONE', 'ORDERS_ZONE_FISHING'})
|
|
|
|
dwarfmode_to_top()
|
|
print('\nDone. Now exit the game with the die command and add\n'..
|
|
'the newly-found globals to symbols.xml. You can find them\n'..
|
|
'in stdout.log or here:\n')
|
|
|
|
for _, global in ipairs(finder_searches) do
|
|
local addr = dfhack.internal.getAddress(global)
|
|
if addr ~= nil then
|
|
local ival = addr - dfhack.internal.getRebaseDelta()
|
|
print(string.format("<global-address name='%s' value='0x%x'/>", global, ival))
|
|
end
|
|
end
|
|
|
|
searcher:reset()
|