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