develop
commit
59fb4daa9a
@ -0,0 +1,493 @@
|
|||||||
|
-- Utilities for offset scan scripts.
|
||||||
|
|
||||||
|
local _ENV = mkmodule('memscan')
|
||||||
|
|
||||||
|
local utils = require('utils')
|
||||||
|
|
||||||
|
-- A length-checked view on a memory buffer
|
||||||
|
|
||||||
|
CheckedArray = CheckedArray or {}
|
||||||
|
|
||||||
|
function CheckedArray.new(type,saddr,eaddr)
|
||||||
|
local data = df.reinterpret_cast(type,saddr)
|
||||||
|
local esize = data:sizeof()
|
||||||
|
local count = math.floor((eaddr-saddr)/esize)
|
||||||
|
local obj = {
|
||||||
|
type = type, start = saddr, size = count*esize,
|
||||||
|
esize = esize, data = data, count = count
|
||||||
|
}
|
||||||
|
setmetatable(obj, CheckedArray)
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
|
||||||
|
function CheckedArray:__len()
|
||||||
|
return self.count
|
||||||
|
end
|
||||||
|
function CheckedArray:__index(idx)
|
||||||
|
if type(idx) == number then
|
||||||
|
if idx >= self.count then
|
||||||
|
error('Index out of bounds: '..tostring(idx))
|
||||||
|
end
|
||||||
|
return self.data[idx]
|
||||||
|
else
|
||||||
|
return CheckedArray[idx]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function CheckedArray:__newindex(idx, val)
|
||||||
|
if idx >= self.count then
|
||||||
|
error('Index out of bounds: '..tostring(idx))
|
||||||
|
end
|
||||||
|
self.data[idx] = val
|
||||||
|
end
|
||||||
|
function CheckedArray:addr2idx(addr, round)
|
||||||
|
local off = addr - self.start
|
||||||
|
if off >= 0 and off < self.size and (round or (off % self.esize) == 0) then
|
||||||
|
return math.floor(off / self.esize), off
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function CheckedArray:idx2addr(idx)
|
||||||
|
if idx >= 0 and idx < self.count then
|
||||||
|
return self.start + idx*self.esize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Search methods
|
||||||
|
|
||||||
|
function CheckedArray:find(data,sidx,eidx,reverse)
|
||||||
|
local dcnt = #data
|
||||||
|
sidx = math.max(0, sidx or 0)
|
||||||
|
eidx = math.min(self.count, eidx or self.count)
|
||||||
|
if (eidx - sidx) >= dcnt and dcnt > 0 then
|
||||||
|
return dfhack.with_temp_object(
|
||||||
|
df.new(self.type, dcnt),
|
||||||
|
function(buffer)
|
||||||
|
for i = 1,dcnt do
|
||||||
|
buffer[i-1] = data[i]
|
||||||
|
end
|
||||||
|
local cnt = eidx - sidx - dcnt
|
||||||
|
local step = self.esize
|
||||||
|
local sptr = self.start + sidx*step
|
||||||
|
local ksize = dcnt*step
|
||||||
|
if reverse then
|
||||||
|
local idx, addr = dfhack.internal.memscan(sptr + cnt*step, cnt, -step, buffer, ksize)
|
||||||
|
if idx then
|
||||||
|
return sidx + cnt - idx, addr
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local idx, addr = dfhack.internal.memscan(sptr, cnt, step, buffer, ksize)
|
||||||
|
if idx then
|
||||||
|
return sidx + idx, addr
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function CheckedArray:find_one(data,sidx,eidx,reverse)
|
||||||
|
local idx, addr = self:find(data,sidx,eidx,reverse)
|
||||||
|
if idx then
|
||||||
|
-- Verify this is the only match
|
||||||
|
if reverse then
|
||||||
|
eidx = idx+#data-1
|
||||||
|
else
|
||||||
|
sidx = idx+1
|
||||||
|
end
|
||||||
|
if self:find(data,sidx,eidx,reverse) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return idx, addr
|
||||||
|
end
|
||||||
|
function CheckedArray:list_changes(old_arr,old_val,new_val,delta)
|
||||||
|
if old_arr.type ~= self.type or old_arr.count ~= self.count then
|
||||||
|
error('Incompatible arrays')
|
||||||
|
end
|
||||||
|
local eidx = self.count
|
||||||
|
local optr = old_arr.start
|
||||||
|
local nptr = self.start
|
||||||
|
local esize = self.esize
|
||||||
|
local rv
|
||||||
|
local sidx = 0
|
||||||
|
while true do
|
||||||
|
local idx = dfhack.internal.diffscan(optr, nptr, sidx, eidx, esize, old_val, new_val, delta)
|
||||||
|
if not idx then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
rv = rv or {}
|
||||||
|
rv[#rv+1] = idx
|
||||||
|
sidx = idx+1
|
||||||
|
end
|
||||||
|
return rv
|
||||||
|
end
|
||||||
|
function CheckedArray:filter_changes(prev_list,old_arr,old_val,new_val,delta)
|
||||||
|
if old_arr.type ~= self.type or old_arr.count ~= self.count then
|
||||||
|
error('Incompatible arrays')
|
||||||
|
end
|
||||||
|
local eidx = self.count
|
||||||
|
local optr = old_arr.start
|
||||||
|
local nptr = self.start
|
||||||
|
local esize = self.esize
|
||||||
|
local rv
|
||||||
|
for i=1,#prev_list do
|
||||||
|
local idx = prev_list[i]
|
||||||
|
if idx < 0 or idx >= eidx then
|
||||||
|
error('Index out of bounds: '..idx)
|
||||||
|
end
|
||||||
|
if dfhack.internal.diffscan(optr, nptr, idx, idx+1, esize, old_val, new_val, delta) then
|
||||||
|
rv = rv or {}
|
||||||
|
rv[#rv+1] = idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return rv
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A raw memory area class
|
||||||
|
|
||||||
|
MemoryArea = MemoryArea or {}
|
||||||
|
MemoryArea.__index = MemoryArea
|
||||||
|
|
||||||
|
function MemoryArea.new(astart, aend)
|
||||||
|
local obj = {
|
||||||
|
start_addr = astart, end_addr = aend, size = aend - astart,
|
||||||
|
int8_t = CheckedArray.new('int8_t',astart,aend),
|
||||||
|
uint8_t = CheckedArray.new('uint8_t',astart,aend),
|
||||||
|
int16_t = CheckedArray.new('int16_t',astart,aend),
|
||||||
|
uint16_t = CheckedArray.new('uint16_t',astart,aend),
|
||||||
|
int32_t = CheckedArray.new('int32_t',astart,aend),
|
||||||
|
uint32_t = CheckedArray.new('uint32_t',astart,aend),
|
||||||
|
float = CheckedArray.new('float',astart,aend)
|
||||||
|
}
|
||||||
|
setmetatable(obj, MemoryArea)
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
|
||||||
|
function MemoryArea:__gc()
|
||||||
|
df.delete(self.buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
function MemoryArea:__tostring()
|
||||||
|
return string.format('<MemoryArea: %x..%x>', self.start_addr, self.end_addr)
|
||||||
|
end
|
||||||
|
function MemoryArea:contains_range(start,size)
|
||||||
|
return size >= 0 and start >= self.start_addr and (start+size) <= self.end_addr
|
||||||
|
end
|
||||||
|
function MemoryArea:contains_obj(obj,count)
|
||||||
|
local size, base = df.sizeof(obj)
|
||||||
|
return size and base and self:contains_range(base, size*(count or 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
function MemoryArea:clone()
|
||||||
|
local buffer = df.new('int8_t', self.size)
|
||||||
|
local _, base = buffer:sizeof()
|
||||||
|
local rv = MemoryArea.new(base, base+self.size)
|
||||||
|
rv.buffer = buffer
|
||||||
|
return rv
|
||||||
|
end
|
||||||
|
function MemoryArea:copy_from(area2)
|
||||||
|
if area2.size ~= self.size then
|
||||||
|
error('Size mismatch')
|
||||||
|
end
|
||||||
|
dfhack.internal.memmove(self.start_addr, area2.start_addr, self.size)
|
||||||
|
end
|
||||||
|
function MemoryArea:delete()
|
||||||
|
setmetatable(self, nil)
|
||||||
|
df.delete(self.buffer)
|
||||||
|
for k,v in pairs(self) do self[k] = nil end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Static data segment search
|
||||||
|
|
||||||
|
local function find_data_segment()
|
||||||
|
local data_start, data_end
|
||||||
|
|
||||||
|
for i,mem in ipairs(dfhack.internal.getMemRanges()) do
|
||||||
|
if data_end then
|
||||||
|
if mem.start_addr == data_end and mem.read and mem.write then
|
||||||
|
data_end = mem.end_addr
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
elseif mem.read and mem.write
|
||||||
|
and (string.match(mem.name,'/dwarfort%.exe$')
|
||||||
|
or string.match(mem.name,'/Dwarf_Fortress$')
|
||||||
|
or string.match(mem.name,'Dwarf Fortress%.exe'))
|
||||||
|
then
|
||||||
|
data_start = mem.start_addr
|
||||||
|
data_end = mem.end_addr
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return data_start, data_end
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_data_segment()
|
||||||
|
local s, e = find_data_segment()
|
||||||
|
if s and e then
|
||||||
|
return MemoryArea.new(s, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Register a found offset, or report an error.
|
||||||
|
|
||||||
|
function found_offset(name,val)
|
||||||
|
local cval = dfhack.internal.getAddress(name)
|
||||||
|
|
||||||
|
if not val then
|
||||||
|
print('Could not find offset '..name)
|
||||||
|
if not cval and not utils.prompt_yes_no('Continue with the script?') then
|
||||||
|
qerror('User quit')
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if df.isvalid(val) then
|
||||||
|
_,val = val:sizeof()
|
||||||
|
end
|
||||||
|
|
||||||
|
print(string.format('Found offset %s: %x', name, val))
|
||||||
|
|
||||||
|
if cval then
|
||||||
|
if cval ~= val then
|
||||||
|
error(string.format('Mismatch with the current value: %x',val))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
dfhack.internal.setAddress(name, val)
|
||||||
|
|
||||||
|
local ival = val - dfhack.internal.getRebaseDelta()
|
||||||
|
local entry = string.format("<global-address name='%s' value='0x%x'/>\n", name, ival)
|
||||||
|
|
||||||
|
local ccolor = dfhack.color(COLOR_LIGHTGREEN)
|
||||||
|
dfhack.print(entry)
|
||||||
|
dfhack.color(ccolor)
|
||||||
|
|
||||||
|
io.stdout:write(entry)
|
||||||
|
io.stdout:flush()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Offset of a field within struct
|
||||||
|
|
||||||
|
function field_ref(handle,...)
|
||||||
|
local items = table.pack(...)
|
||||||
|
for i=1,items.n-1 do
|
||||||
|
handle = handle[items[i]]
|
||||||
|
end
|
||||||
|
return handle:_field(items[items.n])
|
||||||
|
end
|
||||||
|
|
||||||
|
function field_offset(type,...)
|
||||||
|
local handle = df.reinterpret_cast(type,1)
|
||||||
|
local _,addr = df.sizeof(field_ref(handle,...))
|
||||||
|
return addr-1
|
||||||
|
end
|
||||||
|
|
||||||
|
function MemoryArea:object_by_field(addr,type,...)
|
||||||
|
if not addr then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local base = addr - field_offset(type,...)
|
||||||
|
local obj = df.reinterpret_cast(type, base)
|
||||||
|
if not self:contains_obj(obj) then
|
||||||
|
obj = nil
|
||||||
|
end
|
||||||
|
return obj, base
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Validation
|
||||||
|
|
||||||
|
function is_valid_vector(ref,size)
|
||||||
|
local ints = df.reinterpret_cast('uint32_t', ref)
|
||||||
|
return ints[0] <= ints[1] and ints[1] <= ints[2]
|
||||||
|
and (size == nil or (ints[1] - ints[0]) % size == 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Difference search helper
|
||||||
|
|
||||||
|
DiffSearcher = DiffSearcher or {}
|
||||||
|
DiffSearcher.__index = DiffSearcher
|
||||||
|
|
||||||
|
function DiffSearcher.new(area)
|
||||||
|
local obj = { area = area }
|
||||||
|
setmetatable(obj, DiffSearcher)
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
|
||||||
|
function DiffSearcher:begin_search(type)
|
||||||
|
self.type = type
|
||||||
|
self.old_value = nil
|
||||||
|
self.search_sets = nil
|
||||||
|
if not self.save_area then
|
||||||
|
self.save_area = self.area:clone()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function DiffSearcher:advance_search(new_value,delta)
|
||||||
|
if self.search_sets then
|
||||||
|
local nsets = #self.search_sets
|
||||||
|
local ovec = self.save_area[self.type]
|
||||||
|
local nvec = self.area[self.type]
|
||||||
|
local new_set
|
||||||
|
if nsets > 0 then
|
||||||
|
local last_set = self.search_sets[nsets]
|
||||||
|
new_set = nvec:filter_changes(last_set,ovec,self.old_value,new_value,delta)
|
||||||
|
else
|
||||||
|
new_set = nvec:list_changes(ovec,self.old_value,new_value,delta)
|
||||||
|
end
|
||||||
|
if new_set then
|
||||||
|
self.search_sets[nsets+1] = new_set
|
||||||
|
self.old_value = new_value
|
||||||
|
self.save_area:copy_from(self.area)
|
||||||
|
return #new_set, new_set
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.old_value = new_value
|
||||||
|
self.search_sets = {}
|
||||||
|
self.save_area:copy_from(self.area)
|
||||||
|
return #self.save_area[self.type], nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function DiffSearcher:reset()
|
||||||
|
self.search_sets = nil
|
||||||
|
if self.save_area then
|
||||||
|
self.save_area:delete()
|
||||||
|
self.save_area = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function DiffSearcher:idx2addr(idx)
|
||||||
|
return self.area[self.type]:idx2addr(idx)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Interactive search utility
|
||||||
|
|
||||||
|
function DiffSearcher:find_interactive(prompt,data_type,condition_cb)
|
||||||
|
enum = enum or {}
|
||||||
|
|
||||||
|
-- Loop for restarting search from scratch
|
||||||
|
while true do
|
||||||
|
print('\n'..prompt)
|
||||||
|
|
||||||
|
self:begin_search(data_type)
|
||||||
|
|
||||||
|
local found = false
|
||||||
|
local ccursor = 0
|
||||||
|
|
||||||
|
-- Loop through choices
|
||||||
|
while true do
|
||||||
|
print('')
|
||||||
|
|
||||||
|
local ok, value, delta = condition_cb(ccursor)
|
||||||
|
|
||||||
|
ccursor = ccursor + 1
|
||||||
|
|
||||||
|
if not ok then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Search for it in the memory
|
||||||
|
local cnt, set = self:advance_search(value, delta)
|
||||||
|
if not cnt then
|
||||||
|
dfhack.printerr(' Converged to zero candidates; probably a mistake somewhere.')
|
||||||
|
break
|
||||||
|
elseif set and cnt == 1 then
|
||||||
|
-- To confirm, wait for two 1-candidate results in a row
|
||||||
|
if found then
|
||||||
|
local addr = self:idx2addr(set[1])
|
||||||
|
print(string.format(' Confirmed address: %x\n', addr))
|
||||||
|
return addr, set[1]
|
||||||
|
else
|
||||||
|
found = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print(' '..cnt..' candidates remaining.')
|
||||||
|
end
|
||||||
|
|
||||||
|
if not utils.prompt_yes_no('\nRetry search from the start?') then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DiffSearcher:find_menu_cursor(prompt,data_type,choices,enum)
|
||||||
|
enum = enum or {}
|
||||||
|
|
||||||
|
return self:find_interactive(
|
||||||
|
prompt, data_type,
|
||||||
|
function(ccursor)
|
||||||
|
local choice
|
||||||
|
|
||||||
|
-- Select the next value to search for
|
||||||
|
if type(choices) == 'function' then
|
||||||
|
choice = choices(ccursor)
|
||||||
|
|
||||||
|
if not choice then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
choice = choices[(ccursor % #choices) + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ask the user to select it
|
||||||
|
if enum ~= 'noprompt' then
|
||||||
|
local cname = enum[choice] or choice
|
||||||
|
if type(choice) == 'string' and type(cname) == 'number' then
|
||||||
|
choice, cname = cname, choice
|
||||||
|
end
|
||||||
|
if cname ~= choice then
|
||||||
|
cname = cname..' ('..choice..')'
|
||||||
|
end
|
||||||
|
|
||||||
|
print(' Please select: '..cname)
|
||||||
|
if not utils.prompt_yes_no(' Continue?', true) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, choice
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DiffSearcher:find_counter(prompt,data_type,delta,action_prompt)
|
||||||
|
delta = delta or 1
|
||||||
|
|
||||||
|
return self:find_interactive(
|
||||||
|
prompt, data_type,
|
||||||
|
function(ccursor)
|
||||||
|
if ccursor > 0 then
|
||||||
|
print(" "..(action_prompt or 'Please do the action.'))
|
||||||
|
end
|
||||||
|
if not utils.prompt_yes_no(' Continue?', true) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true, nil, delta
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Screen size
|
||||||
|
|
||||||
|
function get_screen_size()
|
||||||
|
-- Use already known globals
|
||||||
|
if dfhack.internal.getAddress('init') then
|
||||||
|
local d = df.global.init.display
|
||||||
|
return d.grid_x, d.grid_y
|
||||||
|
end
|
||||||
|
if dfhack.internal.getAddress('gps') then
|
||||||
|
local g = df.global.gps
|
||||||
|
return g.dimx, g.dimy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse stdout.log for resize notifications
|
||||||
|
io.stdout:flush()
|
||||||
|
|
||||||
|
local w,h = 80,25
|
||||||
|
for line in io.lines('stdout.log') do
|
||||||
|
local cw, ch = string.match(line, '^Resizing grid to (%d+)x(%d+)$')
|
||||||
|
if cw and ch then
|
||||||
|
w, h = tonumber(cw), tonumber(ch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return w,h
|
||||||
|
end
|
||||||
|
|
||||||
|
return _ENV
|
@ -0,0 +1,938 @@
|
|||||||
|
-- Find some offsets for linux.
|
||||||
|
|
||||||
|
local utils = require 'utils'
|
||||||
|
local ms = require 'memscan'
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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 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 function exec_finder(finder, names)
|
||||||
|
if type(names) ~= 'table' then
|
||||||
|
names = { names }
|
||||||
|
end
|
||||||
|
local search = force_scan['all']
|
||||||
|
for _,v in ipairs(names) do
|
||||||
|
if force_scan[v] or not is_known(v) then
|
||||||
|
search = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if search then
|
||||||
|
if not dfhack.safecall(finder) then
|
||||||
|
if 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
|
||||||
|
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
|
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
|
||||||
|
|
||||||
|
dfhack.printerr('Could not find gview')
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- enabler
|
||||||
|
--
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
for i = 1,#colors do colors[i] = colors[i]/255 end
|
||||||
|
|
||||||
|
local idx, addr = data.float:find_one(colors)
|
||||||
|
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 utils.prompt_yes_no('Proceed?', true) 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 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.bad, 4)
|
||||||
|
or not ms.is_valid_vector(world.history.figures, 4)
|
||||||
|
or not ms.is_valid_vector(world.cur_savegame.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 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
|
||||||
|
)
|
||||||
|
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 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
|
||||||
|
)
|
||||||
|
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 = 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
|
||||||
|
)
|
||||||
|
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 = 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 }
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
print('\nPlease load the save previously processed with prepare-save.')
|
||||||
|
if not utils.prompt_yes_no('Proceed?', true) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
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 = 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' }
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
dfhack.units.setNickname(unit, i)
|
||||||
|
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 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
|
||||||
|
)
|
||||||
|
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 = 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
|
||||||
|
)
|
||||||
|
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 = 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
|
||||||
|
)
|
||||||
|
ms.found_offset('ui_building_item_cursor', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- ui_workshop_in_add
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_ui_workshop_in_add()
|
||||||
|
local 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' }
|
||||||
|
)
|
||||||
|
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 = 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
|
||||||
|
)
|
||||||
|
ms.found_offset('ui_workshop_job_cursor', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- ui_building_in_assign
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_ui_building_in_assign()
|
||||||
|
local 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' }
|
||||||
|
)
|
||||||
|
ms.found_offset('ui_building_in_assign', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- ui_building_in_resize
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_ui_building_in_resize()
|
||||||
|
local 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' }
|
||||||
|
)
|
||||||
|
ms.found_offset('ui_building_in_resize', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- window_x
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_window_x()
|
||||||
|
local 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.'
|
||||||
|
)
|
||||||
|
ms.found_offset('window_x', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- window_y
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_window_y()
|
||||||
|
local 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.'
|
||||||
|
)
|
||||||
|
ms.found_offset('window_y', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- window_z
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_window_z()
|
||||||
|
local 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."
|
||||||
|
)
|
||||||
|
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
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_cur_year_tick()
|
||||||
|
local zone
|
||||||
|
if os_type == 'windows' then
|
||||||
|
zone = zoomed_searcher('artifact_next_id', -32)
|
||||||
|
else
|
||||||
|
zone = zoomed_searcher('cur_year', 128)
|
||||||
|
end
|
||||||
|
if not zone then
|
||||||
|
dfhack.printerr('Cannot search for cur_year_tick - prerequisites missing.')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local addr = zone:find_counter([[
|
||||||
|
Searching for cur_year_tick. Please exit to main dwarfmode
|
||||||
|
menu, then do as instructed below:]],
|
||||||
|
'int32_t', 1,
|
||||||
|
"Please press '.' to step the game one frame."
|
||||||
|
)
|
||||||
|
ms.found_offset('cur_year_tick', 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 = 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',
|
||||||
|
[0] = 'step or unpause the game to reset the flag' }
|
||||||
|
)
|
||||||
|
ms.found_offset('process_jobs', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- process_dig
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_process_dig()
|
||||||
|
local zone = get_process_zone() or searcher
|
||||||
|
|
||||||
|
local 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' }
|
||||||
|
)
|
||||||
|
ms.found_offset('process_dig', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- pause_state
|
||||||
|
--
|
||||||
|
|
||||||
|
local function find_pause_state()
|
||||||
|
local zone
|
||||||
|
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
|
||||||
|
|
||||||
|
local 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' }
|
||||||
|
)
|
||||||
|
ms.found_offset('pause_state', addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- MAIN FLOW
|
||||||
|
--
|
||||||
|
|
||||||
|
print('\nInitial globals (need title screen):\n')
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
print('\nCompound globals (need loaded world):\n')
|
||||||
|
|
||||||
|
exec_finder(find_world, 'world')
|
||||||
|
exec_finder(find_ui, 'ui')
|
||||||
|
exec_finder(find_ui_sidebar_menus, 'ui_sidebar_menus')
|
||||||
|
exec_finder(find_ui_build_selector, 'ui_build_selector')
|
||||||
|
exec_finder(find_init, '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_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_process_jobs, 'process_jobs')
|
||||||
|
exec_finder(find_process_dig, 'process_dig')
|
||||||
|
exec_finder(find_pause_state, 'pause_state')
|
||||||
|
|
||||||
|
print('\nDone. Now add newly-found globals to symbols.xml.')
|
||||||
|
searcher:reset()
|
@ -0,0 +1,71 @@
|
|||||||
|
-- Prepare the current save for use with devel/find-offsets.
|
||||||
|
|
||||||
|
df.global.pause_state = true
|
||||||
|
|
||||||
|
--[[print('Placing anchor...')
|
||||||
|
|
||||||
|
do
|
||||||
|
local wp = df.global.ui.waypoints
|
||||||
|
|
||||||
|
for _,pt in ipairs(wp.points) do
|
||||||
|
if pt.name == 'dfhack_anchor' then
|
||||||
|
print('Already placed.')
|
||||||
|
goto found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local x,y,z = pos2xyz(df.global.cursor)
|
||||||
|
|
||||||
|
if not x then
|
||||||
|
error("Place cursor at your preferred anchor point.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local id = wp.next_point_id
|
||||||
|
wp.next_point_id = id + 1
|
||||||
|
|
||||||
|
wp.points:insert('#',{
|
||||||
|
new = true, id = id, name = 'dfhack_anchor',
|
||||||
|
comment=(x..','..y..','..z),
|
||||||
|
tile = string.byte('!'), fg_color = COLOR_LIGHTRED, bg_color = COLOR_BLUE,
|
||||||
|
pos = xyz2pos(x,y,z)
|
||||||
|
})
|
||||||
|
|
||||||
|
::found::
|
||||||
|
end]]
|
||||||
|
|
||||||
|
print('Nicknaming units...')
|
||||||
|
|
||||||
|
for i,unit in ipairs(df.global.world.units.active) do
|
||||||
|
dfhack.units.setNickname(unit, i..':'..unit.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
print('Setting weather...')
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
for i=0,4 do
|
||||||
|
for j = 0,4 do
|
||||||
|
df.global.current_weather[i][j] = (wbytes[i*5+j+1] or 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local yearstr = df.global.cur_year..','..df.global.cur_year_tick
|
||||||
|
|
||||||
|
print('Cur year and tick: '..yearstr)
|
||||||
|
|
||||||
|
dfhack.persistent.save{
|
||||||
|
key='prepare-save/cur_year',
|
||||||
|
value=yearstr,
|
||||||
|
ints={df.global.cur_year, df.global.cur_year_tick}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Save
|
||||||
|
|
||||||
|
dfhack.run_script('quicksave')
|
||||||
|
|
Loading…
Reference in New Issue