#!/usr/bin/env ruby require 'pp' require 'digest/md5' module Obj class Header SIZE = 5*4 # 5x 32-bit little-endian integers attr_reader( :num_sprites, # Number of entries in the directory. Always dir_size/8? :dir_offset, # Offset of the sprite directory. Also the size of the main header :dir_size, # Number of bytes allocated to the sprite directory. Always num_sprites*8? :data_offset, # Number of bytes allocated to the sprites themselves :data_size # Total size of the sprite data # JUNGTIL.obj has 0x0000 0x0000 0x0000 following. Probably just padding to # allow the sprite directory to start at a word boundary. ) def self.parse(data) hdr = new(*data[0..SIZE - 1].unpack("V*")) pp hdr hdr.validate!(data.bytes.size) hdr end def initialize(*entries) @num_sprites, @dir_offset, @dir_size, @data_offset, @data_size = *entries end def validate!(overall_size) raise "Directory overlaps EOF" if overall_size < dir_size + dir_offset raise "Data overlaps EOF" if overall_size < data_offset + data_size raise "Bad dir_size or num_sprites" if num_sprites * DirEntry::SIZE != dir_size raise "Directory overlaps data" if data_range.cover?(dir_offset+1) raise "Data overlaps directory" if dir_range.cover?(data_offset+1) true end def dir_range dir_offset...(dir_offset+dir_size) end def data_range data_offset...(data_offset+data_size) end end class DirEntry SIZE = 8 attr_reader :rel_offset # Relative to the main header's data_offset attr_reader :sprite_size def self.parse(rel_data) new(*rel_data.unpack("VV")) end def initialize(rel_offset, sprite_size) @rel_offset = rel_offset @sprite_size = sprite_size end def sprite_range rel_offset...(rel_offset+sprite_size) end end class SpriteDir attr_reader :entries def self.parse(rel_data) count = rel_data.bytes.size num_entries = count / DirEntry::SIZE trailing = count%DirEntry::SIZE raise "SpriteDir block has #{trailing} trailing bytes" unless trailing == 0 entries = 0.upto(num_entries-1).map do |n| rel_offset = n * DirEntry::SIZE DirEntry.parse(rel_data.byteslice(rel_offset, DirEntry::SIZE)) end pp entries new(entries) end def initialize(entries) @entries = entries end # Convert the directory into an Array of bytes. Until we work out how to # parse sprites, anyway... def realize(rel_data) entries.map { |entry| rel_data[entry.sprite_range] } end end =begin SpriteHeader = Struct.new( :unknown0, # Possibly magic data? It's the same for every sprite in jungtil.obj :maybe_dimension, # Low nibble comes to 63 in jungtil.obj which would work for a 64x64 tile :size, # Number of bytes of pixel data following this header ) do SIZE = 4*6 # Seems to be, anyway. Based on def self.parse(rel_data) new(*sprite_data[0..SIZE-1]).unpack("V*") end def pixel_range SIZE...size # maybe,anyway end end Sprite = Struct.new( :header, :data ) do def self.parse(rel_data) hdr = SpriteHeader.parse(rel_data) sprite_pixels = rel_data[hdr.pixel_range] Sprite.new(hdr, sprite_pixels) end end =end class Parsed attr_reader :header attr_reader :directory attr_reader :sprites attr_accessor :filename def initialize(header, directory, sprites) @header = header @directory = directory @sprites = sprites end end def self.parse(data) hdr = Header.parse(data) dir = SpriteDir.parse(data[hdr.dir_range]) sprites = dir.realize(data[hdr.data_range]) Parsed.new(hdr, dir, sprites) end end def hex(num, leading=0) return " "*(leading) if num.nil? "%0#{leading}x"%num end VISIBLE_CHARS = ("0".ord)..("z".ord) def text(num) return " " if num.nil? if VISIBLE_CHARS.cover?(num) num.chr else " " end end def ascii(num) return " " if num.nil? "%3d"%num end def header!(blocksize=8) nums = (0...blocksize).to_a hdr = " #{nums.join(" ")} #{nums.join("")}" puts "-" * (hdr.size + 1) puts hdr puts "-" * (hdr.size + 1) end def display(data, blocksize=8, skip=0, header: false) bytes = data.bytes[skip..-1] nrows = (bytes.count / blocksize) 0.upto(nrows) do |i| header!(blocksize) if i%16==0 || i == skip block = bytes[(i*blocksize)...(i*blocksize+blocksize)] block.concat([nil]*(blocksize-block.size)) if block.size < blocksize out = [ "0x#{hex(i*blocksize, 4)}", block.map { |b| hex(b, 2) }, # hex " | " + block.map { |b| text(b) }.join("") + " |", # ascii block.map { |b| ascii(b) } ,# decimal bytes "",# decimal 2-bytes # decimal 4-bytes ] puts out.flatten.join(' ') # puts Digest::MD5.hexdigest(data[skip..-1]) end end def load_obj(filename) Obj.parse(File.read(filename).force_encoding("BINARY")) end def dump(filename, spriteno = -1) obj = load_obj(filename) if spriteno == -1 puts "Dumping all sprites for #{filename}" obj.sprites.each { |sprite| display(sprite, header: true) } else puts "Dumping sprite #{spriteno} for #{filename}" display(obj.sprites[spriteno]) end end # The per-sprite data in jungtil.obj is too small to represent a 64x64 block, # even at 1bpp, so try some decompression algorithms to see what comes out. # # Tried so far, with no success: # * DEFLATE / ZLIB # * LZO # * LZOP def decompress(filename) puts "\nAttempting decompression of #{filename}..." require 'zlib' obj = load_obj(filename) obj.sprites.each_with_index do |sprite, i| print "Sprite %02d..."%i # Step through the start of each sprite so we aren't stopped by a hypothetical header (0...64).each do |offset| block = sprite.byteslice(offset, sprite.size-offset) begin inflater = Zlib::Inflate.new(-32) decompressed = "" decompressed << inflater.inflate(block) raise "0 bytes" if decompressed.size == 0 puts "succeeded! sprite=#{i} offset=#{offset} decompressed_size=#{decompressed.size}" puts "data:" puts decompressed.inspect exit 0 rescue => err puts "failed: #{err}" end end #puts "failed" end end # Print the first 128 bytes of sprites in a friendly format. Permits into-obj comparisons def compare(filenames) objs = filenames.map { |filename| load_obj(filename).tap { |obj| obj.filename = filename } } objs.each do |obj| end sprites = if spriteno == -1 else end end case command = ARGV.shift when "dump" then ARGV.each { |filename| dump(filename) } when "compare" then compare(filenames) when "decompress" then ARGV.each { |filename| decompress(filename) } else puts "Unrecognized command #{command}" exit(1) end