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

#! /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