tasogare

Random shit related to Tasogare no Kyoukai from Tiare
git clone https://git.neptards.moe/u3shit/tasogare.git
Log | Files | Refs | README

vm_dump.rb (14831B)


      1 #! /usr/bin/env ruby
      2 # frozen_string_literal: true
      3 
      4 require "rbtree"
      5 
      6 IF_RAW = "\xFF\xFF\xFF\xFF\xFE\xFF\x00\xFC\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE0"
      7 IF = IF_RAW.unpack1('b*').each_char.map {|c| c == '1' }.freeze
      8 
      9 class Chunk
     10 end
     11 
     12 class UnknownChunk < Chunk
     13   attr_reader :pos, :buf
     14   def initialize pos, buf
     15     @pos = pos
     16     @buf = buf
     17   end
     18 
     19   def size = buf.size
     20   def print
     21     printf "%04x: unk %p\n", @pos, @buf.get_string
     22   end
     23 end
     24 
     25 class UnknownChunkView
     26   attr_reader :uc, :offs
     27   attr_accessor :get_pos
     28   def initialize uc, offs
     29     @uc = uc
     30     @offs = offs
     31     @get_pos = 0
     32   end
     33 
     34   def abs_pos
     35     @uc.pos + @offs
     36   end
     37 
     38   def next_pos
     39     @uc.pos + @offs + @get_pos
     40   end
     41 
     42   def get_byte i = @get_pos
     43     @get_pos = i + 1
     44     @uc.buf.get_value :U8, @offs + i
     45   end
     46 
     47   def get_sbyte i = @get_pos
     48     @get_pos = i + 1
     49     @uc.buf.get_value :S8, @offs + i
     50   end
     51 
     52   def get_short i = @get_pos
     53     @get_pos = i + 2
     54     @uc.buf.get_value :u16, @offs + i
     55   end
     56 
     57   def get_str n, i = @get_pos
     58     @get_pos = i+n
     59     @uc.buf.get_string(@offs+i, n, Encoding::SJIS).encode Encoding.default_external
     60   end
     61 
     62   def get_strz i = @get_pos
     63     j = i
     64     j += 1 while @uc.buf.get_value(:U8, @offs+j) != 0
     65     @get_pos = j+1
     66     @uc.buf.get_string(@offs+i, j-i, Encoding::SJIS).encode Encoding.default_external
     67   end
     68 end
     69 
     70 class ParsedChunk < Chunk
     71   attr_reader :str
     72   def initialize str
     73     @str = str
     74   end
     75 
     76   def print = puts @str
     77 end
     78 
     79 class CodeChunk < ParsedChunk
     80 end
     81 
     82 class DataChunk < ParsedChunk
     83 end
     84 
     85 class Parser
     86   def initialize input, is_dungeon
     87     @chunks = RBTree[0, UnknownChunk.new(0, input)]
     88     @queue = [[method(is_dungeon ? :parse_dungeon : :parse_code), 0]]
     89     @queue2 = []
     90   end
     91 
     92   def print
     93     @chunks.each {|k,v| v.print }
     94   end
     95 
     96   def parse
     97     until @queue.empty? && @queue2.empty?
     98       func, pos, *rest = @queue.pop || @queue2.pop
     99       begin
    100         c = get_unk_chunk pos
    101         func.call c, *rest if c
    102       rescue
    103         $stderr.puts $!.full_message
    104       end
    105     end
    106     self
    107   end
    108 
    109   private
    110   def parse_code c
    111     b = c.get_byte
    112     if IF[b]
    113       case b
    114       when 0x00; scode_r  c, "return;\n"
    115       when 0x01; scode    c, "wait_clear;"
    116       when 0x03; scode_r  c, "return_3;\n"
    117       when 0x04; scode_r  c, "exec #{c.get_strz.inspect};\n"
    118       when 0x05; scode    c, "call_script #{c.get_strz.inspect};"
    119       when 0x06; scode    c, "mci_play #{c.get_byte};"
    120       when 0x07; scode    c, "vn itoa(var[#{c.get_byte}]);"
    121       when 0x08; scode    c, "wait;"
    122       when 0x09; scode    c, "nop(9) #{c.get_byte};"
    123       when 0x0a; scode    c, "nl;"
    124       when 0x0b; parse_0b c
    125       when 0x0c; parse_0c c
    126       when 0x0d; scode_r  c, "return_58(d);\n"
    127       when 0x0f; scode_r  c, "return_58(f);\n"
    128       when 0x10; scode_r  c, "return_58(10);\n"
    129       when 0x11; parse_11 c
    130       when 0x13; parse_13 c
    131       when 0x14; fail "todo 14"
    132       when 0x15; parse_15 c
    133       when 0x16; scode    c, "flash_window n = #{c.get_byte};"
    134       when 0x17; scode    c, "sleep #{c.get_byte * 100}ms;"
    135       when 0x18; scode    c, "y_shake? n = #{c.get_byte};"
    136       when 0x19; scode    c, "clear;"
    137       when 0x25; scode    c, "interpolate #{Integer c.get_str 3};"
    138       else
    139         scode c, "ignore_code %02x (%p);" % [b, b.chr]
    140       end
    141     else
    142       start = c.get_pos - 1
    143       nil until IF[c.get_byte]
    144       scode c, "vn #{c.get_str(c.get_pos - start - 1, start).inspect};"
    145     end
    146   end
    147 
    148   def parse_0b_mul_block c, name, op
    149     n = c.get_byte - 1
    150     dst_vari = c.get_sbyte # probably bug
    151     info = line c.abs_pos, "multiple_#{name}_block (#{n}):"
    152 
    153     n.times do |i|
    154       pos = c.next_pos
    155       last = i == n-1 ? ';' : ','
    156       info << line(pos, "  var[#{dst_vari}] #{op}= var[#{c.get_byte}]#{last}")
    157     end
    158 
    159     split c, CodeChunk.new(info)
    160     @queue << [method(:parse_code), c.next_pos]
    161   end
    162 
    163   def parse_selection c
    164     sdata c, "selection #{c.get_byte}: #{c.get_strz.inspect};"
    165   end
    166 
    167   def parse_0b_if c, op
    168     var = c.get_byte
    169     tgt = c.get_short
    170     @queue << [method(:parse_code), tgt]
    171     scode c, "if (var[#{var}] #{op} 0) jmp #{addr_fmt tgt};\n"
    172   end
    173 
    174   # Unfortunately the jump table doesn't encode its length. So what we do:
    175   # + only try to parse a jmp table if we have nothing else to do, to have as
    176   #   much ParsedChunks as possible
    177   # + greedily match adresses until we run out of addresses in the current
    178   #   chunk, or end up with a pointer that doesn't point to the beginning of a
    179   #   parsed CodeChunk
    180   # + when we end up with a pointer into an UnknownChunk, we parse that and try
    181   #   again (if it doesn't successfully parse as code, we also end the jmp table)
    182   def parse_jmp_table c, last_i
    183     fail unless c.get_byte == 0x0b
    184     fail unless c.get_byte == 0x37
    185     var = c.get_byte
    186 
    187     info = line c.abs_pos, "switch(var[#{var}]):"
    188     last_ok = c.get_pos
    189     (0..).each do |i|
    190       pos = c.next_pos
    191       addr = c.get_short rescue break
    192       tcpos, tc = @chunks.upper_bound addr
    193       # info << line(pos, [addr_fmt(addr), addr, tcpos, tc, i, last_i].inspect)
    194 
    195       case tc
    196       when UnknownChunk
    197         break if addr >= tc.pos + tc.size
    198         break if last_i >= i # a parse_code failed on us
    199 
    200         @queue << [method(:parse_code), addr]
    201         @queue2 << [method(:parse_jmp_table), c.abs_pos, i]
    202         return
    203       when CodeChunk
    204         break unless tcpos == addr
    205 
    206       else
    207         break
    208       end
    209 
    210       info << line(pos, "  case #{i}: jmp #{addr_fmt addr},")
    211       last_ok = c.get_pos
    212     end
    213     info[-2] = ";"
    214     info << "\n"
    215 
    216     c.get_pos = last_ok
    217     split c, CodeChunk.new(info)
    218   end
    219 
    220   def parse_0b_select_var c, n, info
    221     n.times do |i|
    222       pos = c.abs_pos
    223       sel_ptr = c.get_short
    224 
    225       @queue << [method(:parse_selection), sel_ptr]
    226       last = i == n-1 ? ';' : ','
    227       info << line(pos, "  #{i}: option sel_ptr = #{addr_fmt sel_ptr}#{last}")
    228     end
    229     split c, CodeChunk.new(info)
    230     @queue << [method(:parse_code), c.next_pos]
    231   end
    232 
    233   def parse_0b c
    234     case b = c.get_byte
    235     when 0x01; scode c, "var[#{c.get_byte}] += #{c.get_sbyte};"
    236     when 0x02; scode c, "var[#{c.get_byte}] += var[#{c.get_byte}];"
    237     when 0x03; scode c, "var[#{c.get_byte}] -= var[#{c.get_byte}];"
    238     when 0x04; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}];"
    239       # sbyte: this is probably buggy in lamp initialization (initialized to -2
    240       # + 126 + 126 = 250 instead of the more likely 254 + 126 + 126 = 506), but
    241       # in TITEL cdata reading, it expects 0xff to be parsed as -1, so the bug
    242       # is in the OVL compiler, not in the VM...
    243     when 0x05; scode c, "var[#{c.get_byte}] = #{c.get_sbyte};"
    244     when 0x06; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] - var[#{c.get_byte}];"
    245     when 0x07; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] - #{c.get_byte};"
    246     when 0x08; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] & var[#{c.get_byte}];"
    247     when 0x09; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] | var[#{c.get_byte}];"
    248     when 0x0b; parse_0b_mul_block c, "and", "&"
    249     when 0x0c; parse_0b_mul_block c, "or", "|"
    250     when 0x14;
    251       n = c.get_byte
    252       ign = c.get_byte
    253       scode c, "var[#{n}] = rand() % #{n+1} (ignore: #{ign});" # probably bug
    254     when 0x15; scode c, "mci_play var[#{c.get_byte}];"
    255 
    256     when 0x32; parse_0b_if c, ">"
    257     when 0x33; parse_0b_if c, "=="
    258     when 0x34; parse_0b_if c, "<"
    259     when 0x35; parse_0b_if c, "!="
    260     when 0x36
    261       tgt = c.get_short
    262       @queue << [method(:parse_code), tgt]
    263       scode_r c, "jmp #{addr_fmt tgt};\n"
    264 
    265     when 0x37
    266       @queue2 << [method(:parse_jmp_table), c.abs_pos, -1]
    267     when 0x38
    268       tgt = c.get_short
    269       @queue << [method(:parse_code), tgt]
    270       scode c, "call #{addr_fmt tgt};"
    271 
    272     when 0x46
    273       var = c.get_byte
    274       n = c.get_byte
    275       x = c.get_short
    276       y = c.get_short
    277       info = line c.abs_pos, "var[#{var}] = sel(n = #{n}, x = #{x}, y = #{y}:"
    278       parse_0b_select_var c, n, info
    279 
    280     when 0x49
    281       var = c.get_byte
    282       n = c.get_byte
    283       a = c.get_byte
    284       b = c.get_byte
    285       info = line c.abs_pos, "var[#{var}] = sel(n = #{n}, mask = (var[#{a}] << 8) | var[#{b}]):"
    286       parse_0b_select_var c, n, info
    287 
    288     when 0x4b
    289       n = c.get_byte
    290       info = line c.abs_pos, "jmp sel(#{n}):"
    291 
    292       n.times do |i|
    293         pos = c.next_pos
    294         sel_ptr = c.get_short
    295         tgt_ptr = c.get_short
    296 
    297         @queue << [method(:parse_selection), sel_ptr]
    298         @queue << [method(:parse_code), tgt_ptr]
    299         last = i == n-1 ? ";\n" : ","
    300         info << line(pos, "  #{i}: option sel_ptr = #{addr_fmt sel_ptr}, "\
    301                           "target = #{addr_fmt tgt_ptr}#{last}")
    302       end
    303       split c, CodeChunk.new(info)
    304     else
    305       fail "todo #{b.to_s 16}"
    306     end
    307   end
    308 
    309   def parse_0c c
    310     case b = c.get_byte
    311     when 1; scode c, "var[#{c.get_byte}] = var2[#{c.get_byte}];"
    312     when 2; scode c, "var2[#{c.get_byte}] = var[#{c.get_byte}];"
    313     when 3; scode c, "var2[#{c.get_byte}] = #{c.get_byte};"
    314     else fail "unknown 0c: #{b.to_s 16}"
    315     end
    316   end
    317 
    318   def parse_map c
    319     sdata c, "map_entry #{c.get_byte}, #{c.get_strz.inspect};"
    320     @queue << [method(:parse_code), c.next_pos]
    321   end
    322 
    323   def parse_11 c
    324     case b = c.get_byte
    325     when 0x01; scode_r c, "return_99 (11, 1);\n"
    326     when 0x02; scode c, "load;"
    327     when 0x03
    328       pars =  [c.get_short, c.get_short, c.get_byte, c.get_byte, c.get_byte,
    329                c.get_strz]
    330       scode c, "put_str #{pars.map(&:inspect).join ', '};"
    331     when 0x04; scode c, "save;"
    332     when 0x05; scode c, "ask_name;"
    333     when 0x06; scode c, "cdata[#{c.get_sbyte}] = 1;"
    334     when 0x07; scode c, "var[#{c.get_byte}] = cdata[var[#{c.get_byte}]];"
    335     when 0x08; scode c, "fade_out_in #{c.get_byte*10}ms;"
    336     when 0x09
    337       tgts = 13.times.map { c.get_short }
    338       tgts.each {|t| @queue << [method(:parse_map), t] }
    339       scode_r c, "open_map #{tgts.map{|t| addr_fmt t }.join ', '};\n"
    340     when 0x0a;
    341       i = c.get_byte
    342       y = c.get_byte
    343       x = c.get_byte
    344       scode_r c, "dungeon i = #{i}, x = #{x-1}, y = #{y-1};\n"
    345     when 0x0b; scode c, "??11/0b;"
    346     when 0x0c; scode c, "??11/0c;"
    347     when 0x0d; scode c, "??11/0d;"
    348     when 0x0e; scode c, "??11/0e;"
    349     when 0x0f; scode c, "??11/0f;"
    350     when 0x10
    351       str = sprintf "map_data[x = %d, y = %d] = 0x%02x;",
    352                     c.get_byte-1, c.get_byte-1, c.get_byte
    353       scode c, str
    354     when 0x14; scode c, "free_vn_bmi_global;"
    355     when 0x15; scode c, "set_vn_offset x = #{c.get_short}, y = #{c.get_short};"
    356     else fail b.to_s 16
    357     end
    358   end
    359 
    360   def parse_13 c
    361     info = line c.abs_pos, "block_bmi:"
    362 
    363     # flag 1: clear window
    364     loop do
    365       pos = c.next_pos
    366       b = c.get_byte
    367       if IF[b]
    368         case b
    369         when 0x00; info << line(pos, "  do;"); break
    370         when 0x01; info << line(pos, "  do_draw = true,")
    371         when 0x02; info << line(pos, "  do_draw = false,")
    372         when 0x07; info << line(pos, "  flags |= clear,")
    373         when 0x08; info << line(pos, "  flags &= ~clear,")
    374         when 0x09; info << line(pos, "  flags |= 4|clear,")
    375         when 0x0b; info << line(pos, "  src_x = #{c.get_short}, src_y = #{c.get_short},")
    376         when 0x0c; info << line(pos, "  width = #{c.get_short}, height = #{c.get_short},")
    377         when 0x0d; info << line(pos, "  dst_x = #{c.get_short}, dst_y = #{c.get_short},")
    378         when 0x0e; info << line(pos, "  cmd = #{c.get_byte},")
    379         when 0x0f; info << line(pos, "  copy dst=#{c.get_byte}, src=#{c.get_byte};"); break
    380         when 0x10; info << line(pos, "  bmi_index = #{c.get_byte},")
    381         when 0x12; info << line(pos, "  do_draw = true, flags |= 2,")
    382         else info << line(pos, "  unk 0x%02x" % b)
    383         end
    384       else
    385         info << line(pos, "  fname = #{c.get_strz(c.get_pos-1).inspect};")
    386         break
    387       end
    388     end
    389     split c, CodeChunk.new(info)
    390     @queue << [method(:parse_code), c.next_pos]
    391   end
    392 
    393   def parse_15 c
    394     case type = c.get_byte
    395     when 1
    396       fail unless c.get_byte == 13 # eek
    397       scode c, "load_ani p0 = #{c.get_short}, p1 = #{c.get_short}, fname = #{c.get_strz.inspect}"
    398     when 4,5
    399       scode c, "clear_anis #{type}"
    400     else
    401       scode c, "nop(15, #{type.to_s 16})"
    402     end
    403   end
    404 
    405 
    406   # data & 0xc0 != 0: has trigger_step
    407   # data & 0xc0 == 0x40: has trigger_view
    408   # data & 0x80: blocked
    409   def parse_dungeon c
    410     blocked = c.get_short
    411     trigger_view = c.get_short
    412     trigger_step = c.get_short
    413     lamp = c.get_short
    414 
    415     sdata c, "dungeon_hdr blocked = #{addr_fmt blocked}, "\
    416              "trigger_view = #{addr_fmt trigger_view}, "\
    417              "trigger_step = #{addr_fmt trigger_step}, "\
    418              "lamp = #{addr_fmt lamp};\n"
    419     @queue << [method(:parse_code), blocked]
    420     # these lengths are from the game's TA_00DG.OVL. The engine in principle
    421     # allows 0x40 ids (some are hard-coded), but since they are not encoded in
    422     # the, we just skip them for now. (Maybe this should use the same heuristic
    423     # as the jump table)
    424     @queue << [method(:parse_dungeon_list), trigger_view, "trigger_view", 10, 1]
    425     @queue << [method(:parse_dungeon_list), trigger_step, "trigger_step", 40, 1]
    426     @queue << [method(:parse_dungeon_list), lamp, "lamp", 5, 0]
    427   end
    428 
    429   def parse_dungeon_list c, name, n, offset
    430     info = line c.abs_pos, "dungeon_list_#{name}:"
    431     n.times do |i|
    432       pos = c.next_pos
    433       tgt = c.get_short
    434       last = i == n-1 ? ";\n" : ","
    435       info << line(pos, "  #{'%02x' % (i+offset)}: jmp #{addr_fmt tgt}#{last}")
    436       @queue << [method(:parse_code), tgt]
    437     end
    438     split c, DataChunk.new(info)
    439   end
    440 
    441 
    442   def line pos, str
    443     sprintf "%04x: %s\n", pos, str
    444   end
    445 
    446   def addr_fmt addr
    447     "%04x" % addr
    448   end
    449 
    450   def sdata ucv, str
    451     split ucv, DataChunk.new(line ucv.abs_pos, str)
    452   end
    453 
    454   def scode_r ucv, str
    455     split ucv, CodeChunk.new(line ucv.abs_pos, str)
    456   end
    457 
    458   def scode ucv, str
    459     split ucv, CodeChunk.new(line ucv.abs_pos, str)
    460     @queue << [method(:parse_code), ucv.next_pos]
    461   end
    462 
    463   def get_unk_chunk pos
    464     c = @chunks.upper_bound pos
    465     unless c[1].is_a? UnknownChunk
    466       return nil if c[0] == pos
    467       fail "Already handled chunk #{pos.to_s 16}"
    468     end
    469     UnknownChunkView.new c[1], pos-c[0]
    470   end
    471 
    472   def split ucv, middle, uc: ucv.uc, beg: ucv.offs, size: ucv.get_pos
    473     fail unless @chunks.delete(uc.pos) == uc
    474 
    475     if beg > 0
    476       a = UnknownChunk.new uc.pos, uc.buf.slice(0, beg)
    477       @chunks[a.pos] = a
    478     end
    479     if beg+size < uc.size
    480       st = beg+size
    481       b = UnknownChunk.new uc.pos+beg+size, uc.buf.slice(st, uc.buf.size - st)
    482       @chunks[b.pos] = b
    483     end
    484 
    485     @chunks[uc.pos+beg] = middle
    486   end
    487 end
    488 
    489 buf = IO::Buffer.map(File.open(ARGV[0]), nil, 0, IO::Buffer::READONLY)
    490 is_dungeon = File.basename(ARGV[0]).casecmp("ta_00dg.ovl") == 0
    491 Parser.new(buf, is_dungeon).parse.print