Skip to content

Instantly share code, notes, and snippets.

@Ishotihadus
Created July 1, 2017 15:23
Show Gist options
  • Save Ishotihadus/c6b4d906ab0e2cf0a5f5db7df3704bc0 to your computer and use it in GitHub Desktop.
Save Ishotihadus/c6b4d906ab0e2cf0a5f5db7df3704bc0 to your computer and use it in GitHub Desktop.
UnityFS unpack tool for MLTD (Million live theater days).
# UnityFS unpack tool for MLTD (Million live theater days).
# Output directory is created automatically.
# Usage: ruby unityfs2asset.rb data.unity3d
require 'fileutils'
require 'extlz4'
require 'bin_utils'
class BinaryReader
def initialize(data)
@data = data
@pos = 0
end
def read(size)
data = @data.byteslice(@pos, size)
@pos += size
data
end
def cstr
r = @data.unpack("@#{@pos}Z*")[0]
@pos += r.bytesize + 1
r
end
def i16u
r = BinUtils.get_int16_be(@data, @pos)
@pos += 2
r
end
def i32u
r = BinUtils.get_int32_be(@data, @pos)
@pos += 4
r
end
def i64u
r = BinUtils.get_int64_be(@data, @pos)
@pos += 8
r
end
end
def pinfo(str, d)
puts "#{str.ljust(25)}: #{d}"
end
def comptype(flags)
case flags & 0x3f
when 0
'None'
when 1
'LZMA'
when 2
'LZ4'
when 3
'LZ4HC'
when 4
'LZHAM'
end
end
def uncompress(block, flags)
case flags & 0x3f
when 0
block
when 2, 3
LZ4::raw_decode(block)
end
end
bin = BinaryReader.new(File.binread(ARGV[0]))
sign = bin.cstr
pinfo("Signature", sign)
raise "Signature does not match: #{sign}" unless sign == 'UnityFS'
fmt = bin.i32u
unt_ver = bin.cstr
gen_ver = bin.cstr
pinfo("Format", fmt)
pinfo("Unity Version", unt_ver)
pinfo("Generator Version", gen_ver)
size = bin.i64u
ci_size = bin.i32u
ui_size = bin.i32u
flags = bin.i32u
pinfo("File Size", size)
pinfo("Compressed Block Size", ci_size)
pinfo("Uncompressed Block Size", ui_size)
pinfo("Compression Flag", "#{flags} (#{comptype(flags)})")
data = BinaryReader.new(uncompress(bin.read(ci_size), flags))
guid = data.read(16)
num_blocks = data.i32u
pinfo("GUID", "#{guid}")
pinfo("Block Count", "#{num_blocks}")
BlockInfo = Struct.new(:ui_size, :ci_size, :flags)
blocks = []
num_blocks.times{ blocks << BlockInfo.new(data.i32u, data.i32u, data.i16u) }
num_nodes = data.i32u
pinfo("Node Count", "#{num_nodes}")
NodeInfo = Struct.new(:offset, :size, :status, :name)
nodes = []
num_nodes.times{ nodes << NodeInfo.new(data.i64u, data.i64u, data.i32u, data.cstr) }
storage = ''
blocks.each do |block|
storage << uncompress(bin.read(block.ci_size), block.flags)
end
outputdir = ARGV[1] || ARGV[0].match(/\A(.*?)(\.unity3d)?\z/)[1]
nodes.each do |node|
puts "Node #{node.name.ljust(30)} (#{node.size} bytes)"
outfile = outputdir + '/' + node.name
dirname = File.dirname(outfile)
FileUtils.mkdir_p(dirname) unless Dir.exist?(dirname)
File.binwrite(outfile, storage.byteslice(node.offset, node.size))
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment