# metasm framework needed for os interaction / memory reading (http://metasm.cr0.org/) require 'metasm/os/windows' # TODO items, buildings, dwarf jobs # find a way to mark tile designation as pending, reveal map from here, send keystrokes to df # main class: holds handle to DF process/memory # scans for important structure at startup # holds some game info (job names) # most of this was taken from Dwarf Companion by Bartavelle (GPL) class DwarfDbg attr_accessor :mem, :pe_base, :pe def initialize(exe='dwarfort.exe') raise 'dwarf fortress not running ?' if not pr = Metasm::OS.current.find_process(exe) raise 'cannot open memory' if not @mem = pr.memory @pe_base = pr.modules[0].addr find_offsets end # off_map_data points to an array of x_ptr, pointing to y_ptr, pointing to z_ptr, pointing to a map_block # # mapsize is [nr_block_x, nr_block_y, nr_z] attr_accessor :off_map_data, :mapsize, :off_cursor, :off_creatures, :playerrace def scan_regexp(re, patname='', offset=0) end def find_offsets # regexp => regexp offset name regexps = { :map => /(\x89\x1d....){35}\xc6....\xbf....\x89\x1d....\x88\x1d/, :cursor => /\x8b.....\x8b.....\xb8\xd0\x8a\xff\xff(\xa3....){3}/, :screen => /\xa3....\xa3....\xa3....\xa1....\x66\x89\x1d....\x89.....\xa3....\x89/, :creatures => /\x8b\x85\x9c\x00\x00\x00\x8b\x90\x18\x02\x00\x00\x83\xfa\xff\x74/, :lang => /\xe8....(\x89\x1d....){9}\x68....\xc6....\xe8/, :mood => /\x66\xc7\x05....\xe8\x03/, } offsets = {} # try my offsets first [0x47a00, 0xb3a00, 0x1c5000, 0x1f4500].each { |tmp| chunk = @mem[@pe_base+tmp, 0x800].to_str regexps.delete_if { |name, re| if off = (chunk =~ re) offsets[name] = @pe_base + tmp + off end } } if not regexps.empty? # find all regexp offsets pehdr = readlong(@pe_base+0x3c) pesize = pehdr < 0x1000 ? readlong(@pe_base+pehdr+4+0x14+0x38) : 0x1800000 tmp = 0 while tmp < pesize and not regexps.empty? # read a chunk, check all regexps chunk = @mem[@pe_base+tmp, 0x11000].to_str regexps.delete_if { |name, re| if off = (chunk =~ re) offsets[name] = @pe_base + tmp + off end } tmp += 0x10000 # overlap to avoid boundary miss end end puts "could not find #{regexps.keys.inspect}" if not regexps.empty? if off = offsets[:map] off += 0xea @off_map_data = readlong(off) puts "off_map_data = 0x%08X" % @off_map_data if $VERBOSE @off_map_size = (1..3).map { |i| readlong(off + 6*i) } update_map_size end if off = offsets[:cursor] off += 0x12 @off_cursor = readlong(off) end if off = offsets[:screen] @off_screen = [29, 34, 40].map { |d| readslong(off+d) } end if off = offsets[:creatures] off += 0x12 @off_creatures = readlong(off) puts "off_creatures = 0x%08X" % @off_creatures if $VERBOSE end if off = offsets[:lang] off += 0x2b @off_lang = readlong(off+0x32) + 0x20 end if off = offsets[:mood] off = readlong(off+3) @playerrace = readlong(off+14) end end def update_map_size @mapsize = @off_map_size.map { |ptr| readlong(ptr) } puts "map size: #{@mapsize.inspect}" if $VERBOSE end def cursorpos readslongs(@off_cursor, 3) end def cursorpos=(pos) writelongs(@off_cursor, 3, pos) end def screenpos @off_screen.map { |p| readslong(p) } end def screenpos=(pos) @off_screen.zip(pos).map { |p, v| writeslong(p, v) } end def load_words race_words = [] readvector(@off_lang).each { |raceptr| race_words << [] readvector(raceptr+0x3c).each { |wptr| race_words.last << readstring(wptr) } } puts "#{race_words.flatten.length} words loaded for #{race_words.length} races" if $VERBOSE race_words end # [langid => [wordlist]] def race_words @race_words ||= load_words end def skillname @skillname ||= %w[Miner Wood_Cutter Carpenter Engraver Mason Animal_Trainer Animal_Caretaker Fish_Dissector Animal_Dissector Fish_Cleaner Butcher Trapper Tanner Weaver Brewer Alchemist Clothier Miller Thresher Cheese_Maker Milker Cook Grower Herbalist Fisherman Furnace_Operator Strand_Extractor Weaponsmith Armorsmith Metalsmith Gem_Cutter Gem_Setter Wood_Crafter Stone_Crafter Metal_Crafter Glassmaker Leatherworker Bone_Carver Wrestler Axeman Swordsman Knife_User Maceman Hammerman Spearman Crossbowman Shield_User Armor_User Siege_Engineer Siege_Operator Bowyer Pikeman Lasher Bowman Blowgunner Thrower Mechanic Druid Ambusher Building_Designer Wood_Burner Lye_Maker Soaper Potash_Maker Dyer Pump_Operator Swimmer Persuader Negotiator Judge_of_Intent Appraiser Organizer Record_Keeper Liar Intimidator Conversationalist Comedian Flatterer Consoler Pacifier] end STRUCT_TYPES = [['long', 'L', 4], ['slong', 'l', 4], ['short', 'S', 2], ['sshort', 's', 2], ['byte', 'C', 1], ['sbyte', 'c', 1]] STRUCT_TYPES.each { |name, fmt, len| define_method("read#{name}") { |addr| @mem[addr, len].unpack(fmt).first } define_method("read#{name}s") { |addr, nr| @mem[addr, len*nr].unpack(fmt+'*') } define_method("write#{name}") { |addr, val| @mem[addr, len] = [val].pack(fmt) } define_method("write#{name}s") { |addr, nr, vals| @mem[addr, len*nr] = vals.pack(fmt+'*') } } STRUCT_TYPES << ['string', nil, 0x1c] # decode c++ string # 0: Ljnk, 4: Lptr / 16Cbuf, 0x14: Llen, 0x18: Lalloclen def readstring(addr) len = readlong(addr+0x14) if len < 0x10 ptr = addr+4 else ptr = readlong(addr+4) end mem[ptr, len] end # overwrite a string # must not overflow already allocated size # will write a 0 after the string, which may overflow advertized allocated size (but seems ok) def writestring(addr, val) if val.length > readlong(addr+0x18) raise 'string would overflow' end ptr = ((readlong(addr+14) < 0x10) ? addr+4 : readlong(addr+4)) writelong(addr+14, val.length) @mem[ptr, val.length+1] = val + 0.chr end STRUCT_TYPES << ['vector', nil, 0x10] # decode c++ vector # 0: Ljnk, 4: Lptr, 8: Lptrend, 0xC: Lptrendalloc def readvector(addr, type='long') sizeof = STRUCT_TYPES.find { |st| st[0] == type }[2] jnk, ptr, ptrend, allocend = readlongs(addr, 4) if ptrend < ptr puts "bad #{type} vector #{'%x' % addr}", caller return [] end len = (ptrend-ptr)/sizeof send "read#{type}s", ptr, len end def writevector(addr, val, type='long') sizeof = STRUCT_TYPES.find { |st| st[0] == type }[2] ptr = readlong(addr+4) newend = ptr + sizeof*val.length if newend > readlong(addr+12) raise 'vector would overflow' end writelong(addr+8, newend) send "write#{type}s", ptr, val.length, val end # iterate the map (skipping unallocated vectors) # yields x, y, z, blockptr def each_map_block xptr = readlong(@off_map_data) puts "empty map?" if $VERBOSE and @mapsize[0] == 0 readlongs(xptr, @mapsize[0]).each_with_index { |yptr, x| puts "#{x}/#{@mapsize[0]}" if $VERBOSE next if yptr == 0 readlongs(yptr, @mapsize[1]).each_with_index { |zptr, y| next if zptr == 0 readlongs(zptr, @mapsize[2]).each_with_index { |bptr, z| next if bptr == 0 yield x*16, y*16, z, bptr } } } end # returns the blockptr associated to x/y/z # nil if not allocated def map_block(x, y, z) xptr = readlong(@off_map_data) return if xptr == 0 ys = readlongs(xptr, @mapsize[0]) yptr = ys[x/16] return if not yptr or yptr == 0 zs = readlongs(yptr, @mapsize[1]) zptr = zs[y/16] return if not zptr or zptr == 0 bs = readlongs(zptr, @mapsize[2]) b = bs[z] return if not b or b == 0 b end def creatures(ptr = @off_creatures) readvector(ptr).map { |cptr| Creature.new(self, cptr) } end def dig_at(x, y, z, design=:dig) return if not ptr = map_block(x, y, z) flag = {:dig => 0x10, :up => 0x60, :down => 0x50, :updown => 0x20}[design] || design x &= 0xf y &= 0xf dat = readlong(ptr+0x264+4*(16*x+y)) dat &= ~0x70 dat |= flag writelong(ptr+0x264+4*(16*x+y), dat) # TODO mark work as pending end end # abstract class for a structure in memory # with offsets associated to name and data type class MemStruct class << self @@fieldlist = {} # class => [field names] (to_s) @@fields = {} # class => [[fld name, off, type]] (external: gtk) @@build_off = {} def fieldlist ; @@fieldlist[self] ||= [] end def fields ; @@fields[self] ||= [] end def build_off ; @@build_off[self] ||= 0 end def build_off=(o) @@build_off[self] = o end DwarfDbg::STRUCT_TYPES.each { |type, fmt, len| define_method(type) { |name, *args| if type == 'vector' and vt = args.grep(String).first args.delete vt end off = args.first || build_off raise "build dup #{name}" if fieldlist.include? name fieldlist << name fields << [name, off, type] self.build_off = off+len if vt define_method(name) { @dbg.send("read#{type}", @off+off, vt) } define_method(name+'=') { |val| @dbg.send("write#{type}", @off+off, val, vt) } else define_method(name) { @dbg.send("read#{type}", @off+off) } define_method(name+'=') { |val| @dbg.send("write#{type}", @off+off, val) } end } } end attr_accessor :dbg, :off def initialize(dbg, off) @dbg, @off = dbg, off end def to_s ['@%08x' % off] + self.class.fieldlist.map { |f| " #{f}: #{send(f).inspect}" } end end # DF creature (dwarf, kobold, unicorn, ...) class Creature < MemStruct string 'firstname' string 'nickname' def lastname words = @dbg.readslongs(@off+0x38, 7) # if lang == -2 ; foo = @dbg.readsshorts(0x54, 7) words.delete_if { |w| w < 0 } return '' if words.empty? # skip retrieve race_words list = @dbg.race_words[lang] words.map { |w| list[w] }.join end fieldlist << 'lastname' slong 'lang', 0x64 sshort 'prof', 0x88 # drafted set here sshort 'type2' slong 'race' slong '_90' sshort 'x' ; sshort 'y' ; sshort 'z' def pos() [x, y, z] end sshort 'dstx', 0xac ; sshort 'dsty' ; sshort 'dstz' def dst() [dstx, dsty, dstz] end vector 'pathx', 'sshort', 0xb4 ; vector 'pathy', 'sshort' ; vector 'pathz', 'sshort' long '_e4' # 2 dead? 8 made_artifact 1000 zombie 2000 skeleton 400_0000 tame 2000_0000 fortressguard long '_e8' # 80 dead? 8000 ground 10000 flying 20000 slaughter 40000 from_underworld sbyte 'sex', 0xf2 long 'id', 0xf4 slong 'side', 0x100 # squad[0x158, 0x60] sshort 'mood', 0x1f8 slong 'warownptr', 0x20c slong 'war' sshort 'warownid' slong 'consortid', 0x218 ; slong 'fatherid' ; slong 'motherid' ; slong '_224' ; slong 'superiorid' vector 'inventory', 0x240 # inventory: ptr to malloc(12h) # +0: dd objectptr? # +4: dw locationmod (0:hauled 1246:worn(cloth=2?) 3:hold(in) 5:stuck 7:bug:location=leftshldr 8+:bug:nothing) # +6: dw location (01:uplowbody 2:head 3+4:r/l|up/low|arm 78:rlhand 9abcde:leg/foot f+4:eye/ear nose rllung heart... vector 'splatters' slong 'money', 0x290 slong 'occupation', 0x2ac # occupation: ptr to malloc(78h) # +0: task nr ? (~0x2780d, find other ints with similar values in pointers) # +4: dbllink: ptr to dd &self, &nextdbllink, &prevdbllink # +8: dw stepsleftuntilfinish (not started, moving to => -1) ; startval = skilllvl (novice engrave ~ 50, accomplished ~ 16) # +0a,+0c,+0e: dw task pos x, y, z change => dig this case (even if far away/not marked, digged => reveal around) # +10: dd 0, 0 # gotosleep: dd -1, 1 ; sleep: dd 1, 1 # +18: dw 10, -1, -1, -1, 0, -1, 0, 0 # +28: dw 0ch dup(0), 0fh, 15h dup(0) # +6c: ptr to (malloc(4)) = ptr to dd 0x9b96e8, 0x34fe # +70: =+74: = +6c+4 = ptr to ptr to malloc(204) with many ptrs to 204 # job finished -> new job update same malloc # where is link back to dwarf ? vector 'mooditem', 0x2b0 ; vector 'moodtype' ; vector 'moodcolor' ; vector 'moodstype' ; short 'moodskill' vector 'damage', 0x2fc, 'sshort' vector 'bprelated', 0x30c ; long 'bpnames' # XXX bp=? slong 'counterstrike', 0x334 slong 'bleeding', 0x340 sshort 'nauseous', 0x346 ; sshort 'drowning' ; sshort 'webbed' ; sshort 'paralysis' sshort 'tantrum', 0x362 slong 'pain' ; slong 'exhaustion' ; slong 'hunger' ; slong 'thirst' ; slong 'drowsiness' slong 'strength', 0x438 ; slong 'agility' ; slong 'toughness' vector 'skillptrs' def skills skillptrs.map { |ptr| id, _, rating, _, xp = @dbg.mem[ptr, 12].unpack('SSSSL') name = @dbg.skillname[id] [(name || id), rating, xp] } end vector 'likes' # jobs: 0x474 +0x4a, bytes: 0disabled 1enabled # weaponpref inside: 0x4a7 +13 #474 mine stonehaul woodhaul burial # foodhaul refusehaul itemhaul furnithaul animalhaul clean woodcut carpent #480 stonedetail mason architect animtrain animcare healthcare butcher trap # smandissect leatherwk tan brew ? soap weave cloth #490 mill plantproc cheese milk cook farm plantgath fish # fishclean fishdissect hunt furnace weapon armor blacksmith metalcraft #4a0 gemcut gemset woodcraft stonecraft bonecarv glassmake strandextract fight:axe # fsword fmace fhammer fspear ? fcrossbow f? f? #4b0 f? f? f:shldlvl f:armlvl siegeeng siegeop crossbowmake mechanic # f:nrweapon potashmake lyemake dye woodburn pumpop byte 'job_mine', 0x474 byte 'job_pump', 0x4bd slong 'xp', 0x4dc vector 'recentevents', 0x500 slong 'happiness', 0x520 long 'legendid', 0x5a8 # traits[0x5b8, 0x3c], shorts def dead? ; _e4 & 2 > 0 ; end def dwarf? ; race == @dbg.playerrace and _e4 & 0x8c0 == 0 ; end def name ([firstname, nickname, lastname] - ['']) * ' ' end def short_s profession = @dbg.skillname[prof] "#{id.to_s.ljust(5)}#{' (DEAD)' if dead?} #{name.ljust(25)} #{profession}" end def to_s ['@%08x' % off] + self.class.fieldlist.map { |f| case f when 'skillptrs'; skills.map { |n, r, x| " #{n}[#{r}]" }.join when 'x', 'y'; next when 'z'; "pos: #{x}:#{y}:#{z}" when 'dstx', 'dsty'; next when 'dstz'; "dest: #{dstx}:#{dsty}:#{dstz}" when 'pathx', 'pathy'; next when 'pathz'; ' path: ' + pathx.zip(pathy, pathz).map { |x, y, z| "#{x}:#{y}:#{z}" }.join(', ') when 'damage'; dmg = damage ; dmg.all? { |e| e == 15 } ? ' full health' : " damage: #{dmg.inspect}" else " #{f}: #{send(f).inspect}" end }.compact end end # DF map element class Mapblock < MemStruct long 'ptrstr' # ptr to c-str ? (seen "\0") long '?0' # 1 long '?1' # 0 long 'ptrlong' # ptr to pointer table long 'ptrlongend' # ptr to end of previous table long 'ptrlongendagain' # ptr to end of previous table sshort '?2' # 0 sshort '?3' # -1 # db 18 dup(0) # +0x62: array of short per tile (y+16*x) (0x62..0x261) # tile content: 2:murkpool db:wall 1b8:vein 150&3:floor # +0x264: array of dword per tile (y+16*x) (0x264..0x663) # &0000_0007 water level # &0000_0070 designated job for the tile # 0none 1dig 2updownstair 3channel 4ramp 5downstair 6upstair 7? # &0000_0180 flag job? 8smooth 10engrave # 200 visible (discovered) # 0001_c000 10outside 8light 4subterranean # 0020_0000 magma/lava (in/outdoor) # 1000_0000 'mossy' end # windows heap walker class Heap # # http://www.securinfos.info/jerome/DOC/heap-windows-exploitation.html # kernel32!HeapWalk # # windows PEB (fs:[18h]?) # long defaultheaphandle, 0x18 # long nrheaps, 0x88 (incl defaultheap) # long heaphandlelistptr, 0x90 # # windows heap structure (allocated chunk): # [@chunk_ptr-8] # short size # allocated size/8, see unusedbytes # short prevsize # byte segmindex # byte flags # 1 = busy # byte unusedbytes # 8 <= unusedbytes < 16, next chunk metadata will be in those 8 bytes # byte smalltagidx # dbg only # [@chunk ptr] # def initialize(mem) @mem = mem end # returns some raw data if this is a chunk (heuristically), or nil def chunkat(addr) if sz = chunksize(addr) @mem[addr, sz].to_str end end alias [] chunkat def chunksize(addr) # TODO check against heap limits size, psize, seg, flags, ubytes, smtag = @mem[addr-8, 8].unpack('SSCCCC') size*8-ubytes if flags == 1 and ubytes >= 8 and ubytes < 16 and size >= 1 end # from a chunk address, retrieve it, dump it, find all pointers to other chunks, recurse def walk(entry, rec=0, off=0) @done ||= {} # check if already seen chunk if sz = @done[entry] puts ' ' * rec + ('+%02x %x [%d] ...' % [off, entry, sz]) return end # check if chunk ck = chunkat entry return if not ck @done[entry] = ck.size # dump puts ' ' * rec + ('+%02x %x [%d] ' % [off, entry, ck.size]), ck.unpack('H*').first.scan(/.{1,128}/) # max recursion check (dwarf struct goes faaaar without it) if rec > 12 puts ' ' * rec + ' rec off' return end # find pointers, recurse ck.unpack('L*').each_with_index { |ptr, i| walk(ptr, rec+1, 4*i) } end end if $0 == __FILE__ # show big integers in hex class Integer def inspect ; (abs > 0x40000) ? (self < 0 ? '-' : '') + ('0x%08x' % abs) : to_s end end # hexdump a String # from metasm/misc class String def hexdump(ctx={}) fmt = ctx[:fmt] ||= ['c', 'd', 'a'] ctx[:pos] ||= 0 ctx[:linelen] ||= 16 scan(/.{1,#{ctx[:linelen]}}/m) { |s| if s != ctx[:lastline] ctx[:lastdup] = false print '%04x ' % ctx[:pos] print s.unpack('C*').map { |b| '%02x' % b }.join(' ').ljust(3*16-1) + ' ' if fmt.include? 'c' print s.unpack('L*').map { |b| '%08x' % b }.join(' ').ljust(9*4-1) + ' ' if fmt.include? 'd' print s.tr("\0-\x1f\x7f-\xff", '.') if fmt.include? 'a' puts elsif not ctx[:lastdup] ctx[:lastdup] = true puts '*' end ctx[:lastline] = s ctx[:pos] += s.length } puts '%04x' % ctx[:pos] if not ctx[:noend] end end #$VERBOSE = true dbg = DwarfDbg.new case cmd = ARGV.shift when 'kill' # kill the dwarfort process (quit without saving) Metasm::WinAPI.terminateprocess(dbg.mem.handle, 0) when 'reveal' # mark all allocated tiles as visible - you may want to (select the whole bottom level for digging & cancel) to alloc all map dbg.each_map_block { |x, y, z, ptr| mp = dbg.readlongs(ptr+0x264, 16*16) 16.times { |dx| 16.times { |dy| mp[16*dx + dy] &= ~0x200 } } dbg.writelongs(ptr+0x264, 16*16, mp) } when 'revealveins' # marks the veins of the map as seen (revealed) dbg.each_map_block { |x, y, z, ptr| veins = dbg.readvector(ptr+8).map { |vein| # type = dbg.readshort(vein+4) bitmap = dbg.readshorts(vein+6, 16) # [y][x] } mp = dbg.readlongs(ptr+0x264, 16*16) 16.times { |dx| 16.times { |dy| if veins.find { |v| v[dy][dx] == 1 } mp[16*dx + dy] &= ~0x200 end } } dbg.writelongs(ptr+0x264, 16*16, mp) } when 'digveins' # mark the veins of a certain type for digging, starting with the nearest, dig the path to known space too # type may be part of rock name or rock name index (raw vein type), special: 'open', 'flux', 'samez' # using '-nr' stops after nr veins are marked (eg digveins gold -1 => dig 1st gold vein) # TODO 'iron' => iron ores/ingredients # types: # 'open', 'flux' # matgloss_stone_gem 0x00-0x7e # matgloss_stone_layer 0x7f-0x98 # matgloss_stone_mineral 0x99-0xd1 # matgloss_stone_soil 0xd2-0xe5 types = %w[ ONYX MORION SCHORL LACE_AGATE BLUE_JADE LAPIS_LAZULI PRASE PRASE_OPAL BLOODSTONE MOSS_AGATE MOSS_OPAL VARISCITE CHRYSOPRASE CHRYSOCOLLA SARD CARNELIAN BANDED_AGATE SARDONYX CHERRY_OPAL LAVENDAR_JADE PINK_JADE TUBE_AGATE FIRE_AGATE PLUME_AGATE BROWN_JASPER PICTURE_JASPER SMOKY_QUARTZ WAX_OPAL WOOD_OPAL AMBER_OPAL GOLD_OPAL CITRINE YELLOW_JASPER TIGEREYE TIGER_IRON SUNSTONE RESIN_OPAL PYRITE CLEAR_TOURMALINE GRAY_CHALCEDONY DENDRITIC_AGATE SHELL_OPAL BONE_OPAL WHITE_CHALCEDONY FORTIFICATION_AGATE MILK_QUARTZ MOONSTONE WHITE_JADE JASPER_OPAL PINEAPPLE_OPAL ONYX_OPAL MILK_OPAL PIPE_OPAL AVENTURINE TURQUOISE QUARTZ_ROSE CRYSTAL_ROCK BLACK_ZIRCON BLACK_PYROPE MELANITE INDIGO_TOURMALINE BLUE_GARNET TSAVORITE GREEN_TOURMALINE DEMANTOID GREEN_ZIRCON GREEN_JADE HELIODOR PERIDOT RED_ZIRCON RED_TOURMALINE RED_PYROPE ALMANDINE RED_GROSSULAR PINK_TOURMALINE RED_BERYL FIRE_OPAL RHODOLITE SPINEL_PURPLE ALEXANDRITE TANZANITE MORGANITE VIOLET_SPESSARTINE PINK_GARNET KUNZITE CINNAMON_GROSSULAR HONEY_YELLOW_BERYL JELLY_OPAL BROWN_ZIRCON YELLOW_ZIRCON GOLDEN_BERYL YELLOW_SPESSARTINE TOPAZ TOPAZOLITE YELLOW_GROSSULAR RUBICELLE CLEAR_GARNET GOSHENITE CAT'S_EYE CLEAR_ZIRCON AMETHYST AQUAMARINE SPINEL_RED CHRYSOBERYL OPAL_PFIRE OPAL_REDFLASH OPAL_BLACK OPAL_WHITE OPAL_CRYSTAL OPAL_CLARO OPAL_LEVIN OPAL_HARLEQUIN OPAL_PINFIRE OPAL_BANDFIRE DIAMOND_LY DIAMOND_FY EMERALD RUBY SAPPHIRE DIAMOND_CLEAR DIAMOND_RED DIAMOND_GREEN DIAMOND_BLUE DIAMOND_YELLOW DIAMOND_BLACK SAPPHIRE_STAR RUBY_STAR SANDSTONE SILTSTONE MUDSTONE SHALE CLAYSTONE ROCK_SALT LIMESTONE CONGLOMERATE DOLOMITE FLINT CHERT CHALK GRANITE DIORITE GABBRO RHYOLITE BASALT ANDESITE FELSITE OBSIDIAN QUARTZITE SLATE PHYLLITE SCHIST GNEISS MARBLE HEMATITE LIMONITE GARNIERITE GOLD SILVER COPPER MALACHITE GALENA SPHALERITE CASSITERITE COAL_BITUMINOUS LIGNITE PLATINUM CINNABAR COBALTITE TETRAHEDRITE HORN_SILVER GYPSUM TALC JET PUDDINGSTONE PETRIFIED_WOOD GRAPHITE BRIMSTONE KIMBERLITE BISMUTHINITE REALGAR ORPIMENT STIBNITE MARCASITE SYLVITE CRYOLITE PERICLASE ILMENITE RUTILE MAGNETITE CHROMITE PYROLUSITE PITCHBLENDE BAUXITE ALUMINUM BORAX OLIVINE HORNBLENDE KAOLINITE SERPENTINE ORTHOCLASE MICROCLINE MICA CALCITE SALTPETER ALABASTER SELENITE SATINSPAR ANHYDRITE ALUNITE RAW_ADAMANTINE CLAY SILTY_CLAY SANDY_CLAY CLAY_LOAM SANDY_CLAY_LOAM SILTY_CLAY_LOAM LOAM SANDY_LOAM SILT_LOAM LOAMY_SAND SILT SAND_TAN SAND_YELLOW SAND_WHITE SAND_BLACK SAND_RED PEAT PELAGIC_CLAY CALCAREOUS_OOZE SILICEOUS_OOZE] findtype = lambda { |type| if not i = types.index(type.upcase) t = types.grep(/#{type}/i) if t.length == 1 i = types.index(t[0]) elsif t.length > 1 puts "#{type} could be #{types.inspect}" end end i } limit = nil wantz = nil targets = ARGV.map { |a| if i = findtype[a] i else case a when 'samez'; wantz = dbg.cursorpos[2] when 'open'; open when 'flux'; %w[marble calcite chalk dolomite limestone].map { |f| findtype[f] } when /^-(.*)/; limit = Integer($1) ; nil else Integer(a) rescue puts "unknown #{a}" end end }.flatten.compact open = [] blacklist = [] vein = [] zmin = zmax = nil puts "scanning map..." dbg.each_map_block { |x, y, z, ptr| next if wantz and z != wantz zmin = z if not zmin or zmin > z zmax = z if not zmax or zmax < z tile = dbg.readshorts(ptr+0x62, 16*16) info = dbg.readlongs(ptr+0x264, 16*16) veins = dbg.readvector(ptr+8).map { |v| type = dbg.readshort(v+4) next if not targets.empty? and not targets.include? type dbg.readshorts(v+6, 16) }.compact 16.times { |dx| 16.times { |dy| i = dx*16+dy t = [x+dx, y+dy, z] if info[i] & 7 > 0 # water blacklist += [[x+dx, y+dy, z], [x+dx+1, y+dy, z], [x+dx-1, y+dy, z], [x+dx, y+dy+1, z], [x+dx, y+dy-1, z], [x+dx+1, y+dy+1, z], [x+dx+1, y+dy-1, z], [x+dx-1, y+dy+1, z], [x+dx-1, y+dy-1, z] ] #blacklist += [[x+dx, y+dy, z-1]] # dangerous terrain warning elsif tile[i] == 0x20 # channel # wall gem sand dig elsif not [0xdb, 0x1b8, 0x109].include?(tile[i]) or (info[i] & 0x70 > 0) if ((info[i] & 0x200) > 0) and ((info[i] & 0x70) == 0)# hidden vein << t if targets.include? 'open' else open << t end elsif veins.find { |v| v[dy][dx] == 1 } vein << t end } } } blacklist.uniq! open -= blacklist ; vein -= blacklist open = open.sort_by { rand } vein = vein.sort_by { rand } puts "#{vein.length} vein - #{open.length} open" hashpos = proc { |v| (v[0] << 16 | v[1]) } h = nil neigh = proc { |v| # check if v has a neigh in h h[v+0x10000+1] or h[v+0x10000] or h[v+0x10000-1] or h[v+1] or h[v-1] or h[v-0x10000+1] or h[v-0x10000] or h[v-0x10000-1] } hollow = proc { |grp| ret = [] (zmin..zmax).each { |cz| print '.' gz = grp.find_all { |x, y ,z| z == cz } h = gz.inject({}) { |h, v| h.update hashpos[v] => true } ret += gz.find_all { |p| next if p[0] & 1 != 0 or p[1] & 1 != 0 v = hashpos[p] (p[0] & 7 == 0 and p[1] & 7 == 0) or # keep a few inner points for surface or really big zones not h[v+0x10000+1] or not h[v+0x10000-1] or not h[v+1] or not h[v-1] } } print "\r" + ' '*32 + "\r" ret.empty? ? grp : ret } dist = proc { |p1, p2| _d = p1.zip(p2).map { |e1, e2| (e1>e2) ? (e1-e2) : (e2-e1) } _d[2] *= 6 _d[2] += 3 if _d[2] > 0 _d.max + (_d[0]+_d[1])/100.0 } open_hollow = hollow[open] vein_hollow = hollow[vein] puts "hollowed: #{vein_hollow.length} - #{open_hollow.length}" while not vein.empty? break if limit == 0 limit -= 1 if limit vein_hollow = vein if vein_hollow.empty? puts "search vein (in #{vein.length})" #v = vein.first sample = vein_hollow while sample.length > 1000 # too long, just pick a few to search from sample = (0..sample.length/10).map { sample[rand(sample.length)] }.uniq end i = 0 v = sample.sort_by { |v| i += 1 print "%.2f \r" % (i*100.0/sample.length) if i & 31 == 0 o = open_hollow.sort_by { |o| dist[o, v] }.first d = dist[o, v] break [v] if d <= 8.4 d }.first print "found nearest vein: #{v.inspect} " curvein = [v] dbg.dig_at(v[0], v[1], v[2], :dig) dbg.screenpos = [(curvein.last[0]-16)&~15, (curvein.last[1]-16)&~15, curvein.last[2]] h = {hashpos[v] => true} while tmp = vein.find_all { |vv| next if vv[2] != curvein.first[2] ; vh = hashpos[vv] ; neigh[vh] and not h[vh] } and tmp.first tmp.each { |tmp| curvein << tmp h[hashpos[tmp]] = true dbg.dig_at(tmp[0], tmp[1], tmp[2], :dig) } end puts "#{curvein.length} tiles" pt = curvein.first o = open_hollow.sort_by { |o| dist[pt, o] }.first print '.' bdist = curvein.map { |v| dist[v, o] }.max print '.' _open = open.find_all { |o| dist[pt, o] <= bdist } print '.' veinnearest = hollow[curvein].sort_by { |v| _open.map { |o| dist[o, v] }.min }.first print '.' opennearest = _open.sort_by { |o| dist[o, veinnearest] }.first print '.'+"\r" open_hollow += vein_hollow & curvein vein_hollow -= curvein vein -= curvein open += curvein #curvein.each { |x, y, z| dbg.dig_at(x, y, z, :dig) } # 1 step towards dst nxt = proc { |src, dst| # once = 0 ; src.zip(dst).map { |s,d| s + (once != 0 ? 0 : (once = (d<=>s))) } # one coord at a time src.zip(dst).map { |s,d| s + (d<=>s) } } cur = veinnearest while ncur = nxt[cur, opennearest] and (ncur != opennearest or cur[2] != ncur[2]) if blacklist.include? ncur elsif cur[0] == ncur[0] and cur[1] == ncur[2] # XXX begin/end vertical should :up or :down only dbg.dig_at(ncur[0], ncur[1], cur[2], :updown) dbg.dig_at(ncur[0], ncur[1], ncur[2], :updown) open_hollow << ncur vein.delete cur vein_hollow.delete cur elsif cur[2] < ncur[2] dbg.dig_at(ncur[0], ncur[1], cur[2], :up) dbg.dig_at(ncur[0], ncur[1], ncur[2], :down) open_hollow << ncur vein.delete cur vein_hollow.delete cur elsif cur[2] > ncur[2] dbg.dig_at(ncur[0], ncur[1], cur[2], :down) dbg.dig_at(ncur[0], ncur[1], ncur[2], :up) open_hollow << ncur vein.delete cur vein_hollow.delete cur else dbg.dig_at(ncur[0], ncur[1], ncur[2], :dig) end cur = ncur vein.delete cur vein_hollow.delete cur open << cur end end when 'creatures' # list all creatures i = 0 dbg.creatures.each { |cre| puts "#{'%3d' % i} #{cre.short_s}" i += 1 } when 'dwarves' # list all dwarves i = 0 dbg.creatures.each { |cre| puts "#{'%3d' % i} #{cre.short_s}" if cre.dwarf? i += 1 } when 'creature', 'dwarf' # dump a creature by idx (will vary from time to time), may patch a value, 'heal' => rm damages, 'zoom' => set cursor pos on # dwarf 12 # dwarf 12 heal # dwarf 12 zoom # dwarf 12 strength 5 if ARGV.empty? pos = dbg.cursorpos cs = dbg.creatures cre = cs.find { |cre| cre.pos == pos and not cre.dead? } puts "no #{cs.index(cre)}" else arg = ARGV.shift begin nr = Integer(arg) # index in cre table? cre = dbg.creatures[nr] rescue # name fragment: check =~ short_to_s cs = dbg.creatures if arg == 'cursor' pos = dbg.cursorpos lst = cs.find_all { |cre| cre.pos == pos and not cre.dead? } else lst = cs.find_all { |cre| cre.short_s =~ /#{arg}/ } end if lst.length > 1 puts lst.map { |cre| "#{cs.index(cre)} #{cre.short_s}" } abort 'ambiguous name' end cre = lst.first end end abort "no creature found" if not cre case action = ARGV.shift when 'heal' cre.damage = cre.damage.map { 15 } when 'forget' cre.recentevents = [] when 'zoom' dbg.screenpos = [(cre.x-8) & ~15, (cre.y-8) & ~15, cre.z] dbg.cursorpos = cre.pos when 'gtk' require 'metasm' exe = Metasm::Shellcode.decode(dbg.mem, Metasm::Ia32.new) dasm = exe.init_disassembler dasm.set_label_at(cre.off, 'creature') xr = proc { |off, len| dasm.add_xref(off, Metasm::Xref.new(:addr, nil, len)) } cre.class.fields.each { |n, o, t| dasm.set_label_at(cre.off+o, n) if o > 0 case t when /^s?long$/; xr[cre.off+o, 4] when /^s?short$/; xr[cre.off+o, 2] when /^s?byte$/; xr[cre.off+o, 1] when 'vector'; xr[cre.off+o, 4] ; xr[cre.off+o+4, 4] ; xr[cre.off+o+8, 4] ; xr[cre.off+o+12, 4] when 'string'; xr[cre.off+o, 4] ; xr[cre.off+o+0x14, 4] ; xr[cre.off+o+0x18, 4] else p t end } w = Metasm::GtkGui::MainWindow.new("metasm").display(dasm, []) w.dasm_widget.focus_addr cre.off w.dasm_widget.keyboard_callback[Gdk::Keyval::GDK_F5] = proc { w.dasm_widget.keep_focus_while { dbg.mem.invalidate } ; true } w.signal_connect('destroy') { Gtk.main_quit } else if action and cre.respond_to? action val = Integer(ARGV.shift) cre.send("#{action}=", val) end end puts cre.to_s Gtk.main if w when 'alldwarves' # patch a value for all dwarves - 'fresh' => reset hunger/thirst/drowsiness action = ARGV.shift val = Integer(ARGV.shift) dbg.creatures.each { |cre| next if not cre.dwarf? case action when 'fresh' cre.hunger = 1 cre.thirst = 1 cre.drowsiness = 1 when 'heal' cre.damage = cre.damage.map { 15 } when 'forget' cre.recentevents = [] else cre.send "#{action}=", val end } when 'skills' # show the skills of a dwarf - TODO patch nr = Integer(ARGV.shift) cre = dbg.creatures[nr] cre.skills.each { |sk, rating, xp| puts "#{sk} #{rating} #{xp}" } when 'skilled' # show dwarves having a (set of) skill, sorted skl = ARGV.shift out = [] dbg.creatures.each { |cre| next if cre.dead? cre.skills.each { |sk, rating, xp| out << [rating, xp, sk, cre.name] if sk =~ /#{skl}/i } } puts out.sort.map { |r, x, s, n| "#{s} #{r} #{x} #{n}" } when 'dump', 'hexdump' # dump raw memory addr = Integer(ARGV.shift) dbg.mem[addr, Integer(ARGV.shift || 0x200)].to_str.hexdump(:pos => addr, :fmt => ['d', 'a']) when /^reads?(long|short|byte)$/ p dbg.send(cmd, Integer(ARGV.shift)) when /^writes?(long|short|byte)$/ dbg.send(cmd, Integer(ARGV.shift), Integer(ARGV.shift)) when 'readvector' p dbg.readvector(Integer(ARGV.shift), ARGV.shift) when 'readstring' p dbg.readstring(Integer(ARGV.shift)) when 'heapwalk' # heap walk from a chunk pointer # (interactive) graph of chunk pointers would be awesome.. Heap.new(dbg.mem).walk(Integer(ARGV.shift), Integer(ARGV.shift)) when 'showpos', 'show', 'goto' # move cursor to pos dbg.cursorpos = [Integer(ARGV.shift), Integer(ARGV.shift), Integer(ARGV.shift)] when 'waterfill' # change the water level for all tiles within the 16x16 area with current water >= 1 curs = dbg.cursorpos lvl = Integer(ARGV.shift || 7) ptr = dbg.map_block(*curs) dat = dbg.readlongs(ptr+0x264, 16*16) dat.map! { |l| ((l & 0xf) == 0) ? l : ((l & ~0xf) | lvl) } dbg.writelongs(ptr+0x264, 16*16, dat) when 'dbgmap' # dump cursor map block curs = dbg.cursorpos curs[0] &= ~15 curs[1] &= ~15 p curs dbg.cursorpos = curs if not ptr = dbg.map_block(*curs) puts 'not allocated' else puts '@%x' % ptr Heap.new(dbg.mem)[ptr].hexdump(:pos => 0, :fmt => ['d', 'a'], :linelen => 32) end when 'testmap' # misc map interaction, change tile under cursor curs = dbg.cursorpos if not ptr = dbg.map_block(*curs) puts 'not allocated' else mode = ARGV.shift arg = ARGV.shift cidx = (curs[0] & 0xf) * 16 + (curs[1] & 0xf) data = (0...256).map { |i| case mode when nil tile = dbg.readshort(ptr+0x62+2*cidx) info = dbg.readlong(ptr+0x264+4*cidx) veins = dbg.readvector(ptr+8).map { |v| [dbg.readshort(v+4), dbg.readshorts(v+6, 16)] } vinfo = veins.find_all { |v| v[1][curs[1]&0xf][curs[0]&0xf] == 1 }.map { |v| v[0] } puts "@#{curs.inspect} tile=#{'%x' % tile} info=#{'%x' % info} #{'veins=' + vinfo.map { |v| '%x' % v }.join(', ') if not vinfo.empty?}" break when 'setvein' newtype = Integer(arg) v0 = dbg.readvector(ptr+8)[0] dbg.writeshort(v0+4, newtype) if v0 break when 'fill': Integer(arg) when /0x.*/: Integer(mode)+i when 'water', 'lava', 'visible', 'hidden', 'inside', 'outside' ptr += 0x264+4*cidx v = dbg.readlong(ptr) case mode when 'water', 'lava' arg = Integer(arg || 7) v = (v & ~0x0020_000f) + arg v |= 0x20_0000 if mode == 'lava' when 'visible'; v &= ~0x200 when 'hidden'; v |= 0x200 when 'inside'; v = (v & ~0x14000) | 0x8000 when 'outside'; v = (v & ~0x8000) | 0x14000 end dbg.writelong(ptr, v) break when 'wall', 'floor', 'source', 'gem', 'raw', 'chasm', 'hole' ptr += 0x62+2*cidx arg = {'source' => 0x5a, 'wall' => 0xdb, 'gem' => 0x1b8, 'floor' => 0x150, 'chasm' => 0x23, 'hole' => 0x20}[mode] || Integer(arg) dbg.writeshort(ptr, arg) break when 'rawinfo' ptr += 0x264+4*cidx dbg.writelong(ptr, Integer(arg)) break when 'infinitegem' loop do # XXX not working sleep 1 next if dbg.readshort(ptr+0x62+2*cidx) == 0x1b8 dbg.writeshort(ptr+0x62+2*cidx, 0x1b8) # regenerate gem dbg.writeshort(ptr+0x264+4*cidx, dbg.readshort(ptr+0x264+4*cidx) | 0x10) # mark as dig end break else raise 'need mode' end } dbg.writeshorts(ptr+0x62, 16*16, data) if data end when 'hexdiff' # show raw memory diff between two instants addr, len = Integer(ARGV.shift), Integer(ARGV.shift || 0x2000) pre = dbg.mem[addr, len].to_str puts 'press enter to diff' gets dbg.mem.invalidate post = dbg.mem[addr, len].to_str puts '@%x' % addr (len/16).times { |i| prel = pre[16*i, 16] postl = post[16*i, 16] if prel != postl print '-' ; prel.hexdump(:pos => 16*i, :fmt => ['d', 'a'], :noend => true) print '+' ; postl.hexdump(:pos => 16*i, :fmt => ['d', 'a'], :noend => true) end } else puts "unknown command #{cmd.inspect}" end #puts 'done, press [enter]' #$stdin.gets end