You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
492 lines
14 KiB
Ruby
492 lines
14 KiB
Ruby
#! /usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require "rbtree"
|
|
|
|
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"
|
|
IF = IF_RAW.unpack1('b*').each_char.map {|c| c == '1' }.freeze
|
|
|
|
class Chunk
|
|
end
|
|
|
|
class UnknownChunk < Chunk
|
|
attr_reader :pos, :buf
|
|
def initialize pos, buf
|
|
@pos = pos
|
|
@buf = buf
|
|
end
|
|
|
|
def size = buf.size
|
|
def print
|
|
printf "%04x: unk %p\n", @pos, @buf.get_string
|
|
end
|
|
end
|
|
|
|
class UnknownChunkView
|
|
attr_reader :uc, :offs
|
|
attr_accessor :get_pos
|
|
def initialize uc, offs
|
|
@uc = uc
|
|
@offs = offs
|
|
@get_pos = 0
|
|
end
|
|
|
|
def abs_pos
|
|
@uc.pos + @offs
|
|
end
|
|
|
|
def next_pos
|
|
@uc.pos + @offs + @get_pos
|
|
end
|
|
|
|
def get_byte i = @get_pos
|
|
@get_pos = i + 1
|
|
@uc.buf.get_value :U8, @offs + i
|
|
end
|
|
|
|
def get_sbyte i = @get_pos
|
|
@get_pos = i + 1
|
|
@uc.buf.get_value :S8, @offs + i
|
|
end
|
|
|
|
def get_short i = @get_pos
|
|
@get_pos = i + 2
|
|
@uc.buf.get_value :u16, @offs + i
|
|
end
|
|
|
|
def get_str n, i = @get_pos
|
|
@get_pos = i+n
|
|
@uc.buf.get_string(@offs+i, n, Encoding::SJIS).encode Encoding.default_external
|
|
end
|
|
|
|
def get_strz i = @get_pos
|
|
j = i
|
|
j += 1 while @uc.buf.get_value(:U8, @offs+j) != 0
|
|
@get_pos = j+1
|
|
@uc.buf.get_string(@offs+i, j-i, Encoding::SJIS).encode Encoding.default_external
|
|
end
|
|
end
|
|
|
|
class ParsedChunk < Chunk
|
|
attr_reader :str
|
|
def initialize str
|
|
@str = str
|
|
end
|
|
|
|
def print = puts @str
|
|
end
|
|
|
|
class CodeChunk < ParsedChunk
|
|
end
|
|
|
|
class DataChunk < ParsedChunk
|
|
end
|
|
|
|
class Parser
|
|
def initialize input, is_dungeon
|
|
@chunks = RBTree[0, UnknownChunk.new(0, input)]
|
|
@queue = [[method(is_dungeon ? :parse_dungeon : :parse_code), 0]]
|
|
@queue2 = []
|
|
end
|
|
|
|
def print
|
|
@chunks.each {|k,v| v.print }
|
|
end
|
|
|
|
def parse
|
|
until @queue.empty? && @queue2.empty?
|
|
func, pos, *rest = @queue.pop || @queue2.pop
|
|
begin
|
|
c = get_unk_chunk pos
|
|
func.call c, *rest if c
|
|
rescue
|
|
$stderr.puts $!.full_message
|
|
end
|
|
end
|
|
self
|
|
end
|
|
|
|
private
|
|
def parse_code c
|
|
b = c.get_byte
|
|
if IF[b]
|
|
case b
|
|
when 0x00; scode_r c, "return;\n"
|
|
when 0x01; scode c, "wait_clear;"
|
|
when 0x03; scode_r c, "return_3;\n"
|
|
when 0x04; scode_r c, "exec #{c.get_strz.inspect};\n"
|
|
when 0x05; scode c, "call_script #{c.get_strz.inspect};"
|
|
when 0x06; scode c, "mci_play #{c.get_byte};"
|
|
when 0x07; scode c, "vn itoa(var[#{c.get_byte}]);"
|
|
when 0x08; scode c, "wait;"
|
|
when 0x09; scode c, "nop(9) #{c.get_byte};"
|
|
when 0x0a; scode c, "nl;"
|
|
when 0x0b; parse_0b c
|
|
when 0x0c; parse_0c c
|
|
when 0x0d; scode_r c, "return_58(d);\n"
|
|
when 0x0f; scode_r c, "return_58(f);\n"
|
|
when 0x10; scode_r c, "return_58(10);\n"
|
|
when 0x11; parse_11 c
|
|
when 0x13; parse_13 c
|
|
when 0x14; fail "todo 14"
|
|
when 0x15; parse_15 c
|
|
when 0x16; scode c, "flash_window n = #{c.get_byte};"
|
|
when 0x17; scode c, "sleep #{c.get_byte * 100}ms;"
|
|
when 0x18; scode c, "y_shake? n = #{c.get_byte};"
|
|
when 0x19; scode c, "clear;"
|
|
when 0x25; scode c, "interpolate #{Integer c.get_str 3};"
|
|
else
|
|
scode c, "ignore_code %02x (%p);" % [b, b.chr]
|
|
end
|
|
else
|
|
start = c.get_pos - 1
|
|
nil until IF[c.get_byte]
|
|
scode c, "vn #{c.get_str(c.get_pos - start - 1, start).inspect};"
|
|
end
|
|
end
|
|
|
|
def parse_0b_mul_block c, name, op
|
|
n = c.get_byte - 1
|
|
dst_vari = c.get_sbyte # probably bug
|
|
info = line c.abs_pos, "multiple_#{name}_block (#{n}):"
|
|
|
|
n.times do |i|
|
|
pos = c.next_pos
|
|
last = i == n-1 ? ';' : ','
|
|
info << line(pos, " var[#{dst_vari}] #{op}= var[#{c.get_byte}]#{last}")
|
|
end
|
|
|
|
split c, CodeChunk.new(info)
|
|
@queue << [method(:parse_code), c.next_pos]
|
|
end
|
|
|
|
def parse_selection c
|
|
sdata c, "selection #{c.get_byte}: #{c.get_strz.inspect};"
|
|
end
|
|
|
|
def parse_0b_if c, op
|
|
var = c.get_byte
|
|
tgt = c.get_short
|
|
@queue << [method(:parse_code), tgt]
|
|
scode c, "if (var[#{var}] #{op} 0) jmp #{addr_fmt tgt};\n"
|
|
end
|
|
|
|
# Unfortunately the jump table doesn't encode its length. So what we do:
|
|
# + only try to parse a jmp table if we have nothing else to do, to have as
|
|
# much ParsedChunks as possible
|
|
# + greedily match adresses until we run out of addresses in the current
|
|
# chunk, or end up with a pointer that doesn't point to the beginning of a
|
|
# parsed CodeChunk
|
|
# + when we end up with a pointer into an UnknownChunk, we parse that and try
|
|
# again (if it doesn't successfully parse as code, we also end the jmp table)
|
|
def parse_jmp_table c, last_i
|
|
fail unless c.get_byte == 0x0b
|
|
fail unless c.get_byte == 0x37
|
|
var = c.get_byte
|
|
|
|
info = line c.abs_pos, "switch(var[#{var}]):"
|
|
last_ok = c.get_pos
|
|
(0..).each do |i|
|
|
pos = c.next_pos
|
|
addr = c.get_short rescue break
|
|
tcpos, tc = @chunks.upper_bound addr
|
|
# info << line(pos, [addr_fmt(addr), addr, tcpos, tc, i, last_i].inspect)
|
|
|
|
case tc
|
|
when UnknownChunk
|
|
break if addr >= tc.pos + tc.size
|
|
break if last_i >= i # a parse_code failed on us
|
|
|
|
@queue << [method(:parse_code), addr]
|
|
@queue2 << [method(:parse_jmp_table), c.abs_pos, i]
|
|
return
|
|
when CodeChunk
|
|
break unless tcpos == addr
|
|
|
|
else
|
|
break
|
|
end
|
|
|
|
info << line(pos, " case #{i}: jmp #{addr_fmt addr},")
|
|
last_ok = c.get_pos
|
|
end
|
|
info[-2] = ";"
|
|
info << "\n"
|
|
|
|
c.get_pos = last_ok
|
|
split c, CodeChunk.new(info)
|
|
end
|
|
|
|
def parse_0b_select_var c, n, info
|
|
n.times do |i|
|
|
pos = c.abs_pos
|
|
sel_ptr = c.get_short
|
|
|
|
@queue << [method(:parse_selection), sel_ptr]
|
|
last = i == n-1 ? ';' : ','
|
|
info << line(pos, " #{i}: option sel_ptr = #{addr_fmt sel_ptr}#{last}")
|
|
end
|
|
split c, CodeChunk.new(info)
|
|
@queue << [method(:parse_code), c.next_pos]
|
|
end
|
|
|
|
def parse_0b c
|
|
case b = c.get_byte
|
|
when 0x01; scode c, "var[#{c.get_byte}] += #{c.get_sbyte};"
|
|
when 0x02; scode c, "var[#{c.get_byte}] += var[#{c.get_byte}];"
|
|
when 0x03; scode c, "var[#{c.get_byte}] -= var[#{c.get_byte}];"
|
|
when 0x04; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}];"
|
|
# sbyte: this is probably buggy in lamp initialization (initialized to -2
|
|
# + 126 + 126 = 250 instead of the more likely 254 + 126 + 126 = 506), but
|
|
# in TITEL cdata reading, it expects 0xff to be parsed as -1, so the bug
|
|
# is in the OVL compiler, not in the VM...
|
|
when 0x05; scode c, "var[#{c.get_byte}] = #{c.get_sbyte};"
|
|
when 0x06; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] - var[#{c.get_byte}];"
|
|
when 0x07; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] - #{c.get_byte};"
|
|
when 0x08; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] & var[#{c.get_byte}];"
|
|
when 0x09; scode c, "var[#{c.get_byte}] = var[#{c.get_byte}] | var[#{c.get_byte}];"
|
|
when 0x0b; parse_0b_mul_block c, "and", "&"
|
|
when 0x0c; parse_0b_mul_block c, "or", "|"
|
|
when 0x14;
|
|
n = c.get_byte
|
|
ign = c.get_byte
|
|
scode c, "var[#{n}] = rand() % #{n+1} (ignore: #{ign});" # probably bug
|
|
when 0x15; scode c, "mci_play var[#{c.get_byte}];"
|
|
|
|
when 0x32; parse_0b_if c, ">"
|
|
when 0x33; parse_0b_if c, "=="
|
|
when 0x34; parse_0b_if c, "<"
|
|
when 0x35; parse_0b_if c, "!="
|
|
when 0x36
|
|
tgt = c.get_short
|
|
@queue << [method(:parse_code), tgt]
|
|
scode_r c, "jmp #{addr_fmt tgt};\n"
|
|
|
|
when 0x37
|
|
@queue2 << [method(:parse_jmp_table), c.abs_pos, -1]
|
|
when 0x38
|
|
tgt = c.get_short
|
|
@queue << [method(:parse_code), tgt]
|
|
scode c, "call #{addr_fmt tgt};"
|
|
|
|
when 0x46
|
|
var = c.get_byte
|
|
n = c.get_byte
|
|
x = c.get_short
|
|
y = c.get_short
|
|
info = line c.abs_pos, "var[#{var}] = sel(n = #{n}, x = #{x}, y = #{y}:"
|
|
parse_0b_select_var c, n, info
|
|
|
|
when 0x49
|
|
var = c.get_byte
|
|
n = c.get_byte
|
|
a = c.get_byte
|
|
b = c.get_byte
|
|
info = line c.abs_pos, "var[#{var}] = sel(n = #{n}, mask = (var[#{a}] << 8) | var[#{b}]):"
|
|
parse_0b_select_var c, n, info
|
|
|
|
when 0x4b
|
|
n = c.get_byte
|
|
info = line c.abs_pos, "jmp sel(#{n}):"
|
|
|
|
n.times do |i|
|
|
pos = c.next_pos
|
|
sel_ptr = c.get_short
|
|
tgt_ptr = c.get_short
|
|
|
|
@queue << [method(:parse_selection), sel_ptr]
|
|
@queue << [method(:parse_code), tgt_ptr]
|
|
last = i == n-1 ? ";\n" : ","
|
|
info << line(pos, " #{i}: option sel_ptr = #{addr_fmt sel_ptr}, "\
|
|
"target = #{addr_fmt tgt_ptr}#{last}")
|
|
end
|
|
split c, CodeChunk.new(info)
|
|
else
|
|
fail "todo #{b.to_s 16}"
|
|
end
|
|
end
|
|
|
|
def parse_0c c
|
|
case b = c.get_byte
|
|
when 1; scode c, "var[#{c.get_byte}] = var2[#{c.get_byte}];"
|
|
when 2; scode c, "var2[#{c.get_byte}] = var[#{c.get_byte}];"
|
|
when 3; scode c, "var2[#{c.get_byte}] = #{c.get_byte};"
|
|
else fail "unknown 0c: #{b.to_s 16}"
|
|
end
|
|
end
|
|
|
|
def parse_map c
|
|
sdata c, "map_entry #{c.get_byte}, #{c.get_strz.inspect};"
|
|
@queue << [method(:parse_code), c.next_pos]
|
|
end
|
|
|
|
def parse_11 c
|
|
case b = c.get_byte
|
|
when 0x01; scode_r c, "return_99 (11, 1);\n"
|
|
when 0x02; scode c, "load;"
|
|
when 0x03
|
|
pars = [c.get_short, c.get_short, c.get_byte, c.get_byte, c.get_byte,
|
|
c.get_strz]
|
|
scode c, "put_str #{pars.map(&:inspect).join ', '};"
|
|
when 0x04; scode c, "save;"
|
|
when 0x05; scode c, "ask_name;"
|
|
when 0x06; scode c, "cdata[#{c.get_sbyte}] = 1;"
|
|
when 0x07; scode c, "var[#{c.get_byte}] = cdata[var[#{c.get_byte}]];"
|
|
when 0x08; scode c, "fade_out_in #{c.get_byte*10}ms;"
|
|
when 0x09
|
|
tgts = 13.times.map { c.get_short }
|
|
tgts.each {|t| @queue << [method(:parse_map), t] }
|
|
scode_r c, "open_map #{tgts.map{|t| addr_fmt t }.join ', '};\n"
|
|
when 0x0a;
|
|
i = c.get_byte
|
|
y = c.get_byte
|
|
x = c.get_byte
|
|
scode_r c, "dungeon i = #{i}, x = #{x-1}, y = #{y-1};\n"
|
|
when 0x0b; scode c, "??11/0b;"
|
|
when 0x0c; scode c, "??11/0c;"
|
|
when 0x0d; scode c, "??11/0d;"
|
|
when 0x0e; scode c, "??11/0e;"
|
|
when 0x0f; scode c, "??11/0f;"
|
|
when 0x10
|
|
str = sprintf "map_data[x = %d, y = %d] = 0x%02x;",
|
|
c.get_byte-1, c.get_byte-1, c.get_byte
|
|
scode c, str
|
|
when 0x14; scode c, "free_vn_bmi_global;"
|
|
when 0x15; scode c, "set_vn_offset x = #{c.get_short}, y = #{c.get_short};"
|
|
else fail b.to_s 16
|
|
end
|
|
end
|
|
|
|
def parse_13 c
|
|
info = line c.abs_pos, "block_bmi:"
|
|
|
|
# flag 1: clear window
|
|
loop do
|
|
pos = c.next_pos
|
|
b = c.get_byte
|
|
if IF[b]
|
|
case b
|
|
when 0x00; info << line(pos, " do;"); break
|
|
when 0x01; info << line(pos, " do_draw = true,")
|
|
when 0x02; info << line(pos, " do_draw = false,")
|
|
when 0x07; info << line(pos, " flags |= clear,")
|
|
when 0x08; info << line(pos, " flags &= ~clear,")
|
|
when 0x09; info << line(pos, " flags |= 4|clear,")
|
|
when 0x0b; info << line(pos, " src_x = #{c.get_short}, src_y = #{c.get_short},")
|
|
when 0x0c; info << line(pos, " width = #{c.get_short}, height = #{c.get_short},")
|
|
when 0x0d; info << line(pos, " dst_x = #{c.get_short}, dst_y = #{c.get_short},")
|
|
when 0x0e; info << line(pos, " cmd = #{c.get_byte},")
|
|
when 0x0f; info << line(pos, " copy dst=#{c.get_byte}, src=#{c.get_byte};"); break
|
|
when 0x10; info << line(pos, " bmi_index = #{c.get_byte},")
|
|
when 0x12; info << line(pos, " do_draw = true, flags |= 2,")
|
|
else info << line(pos, " unk 0x%02x" % b)
|
|
end
|
|
else
|
|
info << line(pos, " fname = #{c.get_strz(c.get_pos-1).inspect};")
|
|
break
|
|
end
|
|
end
|
|
split c, CodeChunk.new(info)
|
|
@queue << [method(:parse_code), c.next_pos]
|
|
end
|
|
|
|
def parse_15 c
|
|
case type = c.get_byte
|
|
when 1
|
|
fail unless c.get_byte == 13 # eek
|
|
scode c, "load_ani p0 = #{c.get_short}, p1 = #{c.get_short}, fname = #{c.get_strz.inspect}"
|
|
when 4,5
|
|
scode c, "clear_anis #{type}"
|
|
else
|
|
scode c, "nop(15, #{type.to_s 16})"
|
|
end
|
|
end
|
|
|
|
|
|
# data & 0xc0 != 0: has trigger_step
|
|
# data & 0xc0 == 0x40: has trigger_view
|
|
# data & 0x80: blocked
|
|
def parse_dungeon c
|
|
blocked = c.get_short
|
|
trigger_view = c.get_short
|
|
trigger_step = c.get_short
|
|
lamp = c.get_short
|
|
|
|
sdata c, "dungeon_hdr blocked = #{addr_fmt blocked}, "\
|
|
"trigger_view = #{addr_fmt trigger_view}, "\
|
|
"trigger_step = #{addr_fmt trigger_step}, "\
|
|
"lamp = #{addr_fmt lamp};\n"
|
|
@queue << [method(:parse_code), blocked]
|
|
# these lengths are from the game's TA_00DG.OVL. The engine in principle
|
|
# allows 0x40 ids (some are hard-coded), but since they are not encoded in
|
|
# the, we just skip them for now. (Maybe this should use the same heuristic
|
|
# as the jump table)
|
|
@queue << [method(:parse_dungeon_list), trigger_view, "trigger_view", 10, 1]
|
|
@queue << [method(:parse_dungeon_list), trigger_step, "trigger_step", 40, 1]
|
|
@queue << [method(:parse_dungeon_list), lamp, "lamp", 5, 0]
|
|
end
|
|
|
|
def parse_dungeon_list c, name, n, offset
|
|
info = line c.abs_pos, "dungeon_list_#{name}:"
|
|
n.times do |i|
|
|
pos = c.next_pos
|
|
tgt = c.get_short
|
|
last = i == n-1 ? ";\n" : ","
|
|
info << line(pos, " #{'%02x' % (i+offset)}: jmp #{addr_fmt tgt}#{last}")
|
|
@queue << [method(:parse_code), tgt]
|
|
end
|
|
split c, DataChunk.new(info)
|
|
end
|
|
|
|
|
|
def line pos, str
|
|
sprintf "%04x: %s\n", pos, str
|
|
end
|
|
|
|
def addr_fmt addr
|
|
"%04x" % addr
|
|
end
|
|
|
|
def sdata ucv, str
|
|
split ucv, DataChunk.new(line ucv.abs_pos, str)
|
|
end
|
|
|
|
def scode_r ucv, str
|
|
split ucv, CodeChunk.new(line ucv.abs_pos, str)
|
|
end
|
|
|
|
def scode ucv, str
|
|
split ucv, CodeChunk.new(line ucv.abs_pos, str)
|
|
@queue << [method(:parse_code), ucv.next_pos]
|
|
end
|
|
|
|
def get_unk_chunk pos
|
|
c = @chunks.upper_bound pos
|
|
unless c[1].is_a? UnknownChunk
|
|
return nil if c[0] == pos
|
|
fail "Already handled chunk #{pos.to_s 16}"
|
|
end
|
|
UnknownChunkView.new c[1], pos-c[0]
|
|
end
|
|
|
|
def split ucv, middle, uc: ucv.uc, beg: ucv.offs, size: ucv.get_pos
|
|
fail unless @chunks.delete(uc.pos) == uc
|
|
|
|
if beg > 0
|
|
a = UnknownChunk.new uc.pos, uc.buf.slice(0, beg)
|
|
@chunks[a.pos] = a
|
|
end
|
|
if beg+size < uc.size
|
|
st = beg+size
|
|
b = UnknownChunk.new uc.pos+beg+size, uc.buf.slice(st, uc.buf.size - st)
|
|
@chunks[b.pos] = b
|
|
end
|
|
|
|
@chunks[uc.pos+beg] = middle
|
|
end
|
|
end
|
|
|
|
buf = IO::Buffer.map(File.open(ARGV[0]), nil, 0, IO::Buffer::READONLY)
|
|
is_dungeon = File.basename(ARGV[0]).casecmp("ta_00dg.ovl") == 0
|
|
Parser.new(buf, is_dungeon).parse.print
|