Files
mikunyan/lib/mikunyan/asset.rb
T
2017-07-05 01:37:28 +09:00

273 lines
10 KiB
Ruby

module Mikunyan
class Asset
attr_accessor :name, :format, :generator_version, :target_platform, :endian, :klasses, :objects, :add_ids, :references
Klass = Struct.new(:class_id, :script_id, :hash, :type_tree)
ObjectData = Struct.new(:path_id, :offset, :size, :type_id, :class_id, :class_idx, :destroyed?, :data)
Reference = Struct.new(:path, :guid, :type, :file_path)
def initialize(name)
@name = name
@endian = :big
end
def self.file(file, name)
r = Asset.new(name)
r.load(File.binread(file))
r
end
def load(bin)
br = BinaryReader.new(bin)
metadata_size = br.i32u
size = br.i32u
@format = br.i32u
data_offset = br.i32u
if @format >= 9
@endian = :little if br.i32 == 0
br.endian = @endian
end
@generator_version = br.cstr
@target_platform = br.i32
@klasses = []
if @format >= 17
has_type_trees = (br.i8 != 0)
type_tree_count = br.i32u
type_tree_count.times do
class_id = br.i32
br.adv(1)
script_id = br.i16
if class_id < 0 || class_id == 114
hash = br.read(32)
else
hash = br.read(16)
end
@klasses << Klass.new(class_id, script_id, hash, has_type_trees ? TypeTree.load(br) : TypeTree.load_default(hash))
end
elsif @format >= 13
has_type_trees = (br.i8 != 0)
type_tree_count = br.i32u
type_tree_count.times do
class_id = br.i32
if class_id < 0
hash = br.read(32)
else
hash = br.read(16)
end
@klasses << Klass.new(class_id, nil, hash, has_type_trees ? TypeTree.load(br) : TypeTree.load_default(hash))
end
else
@type_trees = {}
type_tree_count = br.i32u
type_tree_count.times do
class_id = br.i32
@klasses << Klass.new(class_id, nil, nil, @format == 10 || @format == 12 ? TypeTree.load(br) : TypeTree.load_legacy(br))
end
end
long_object_ids = (@format >= 14 || (7 <= @format && @format <= 13 && br.i32 != 0))
@objects = []
object_count = br.i32u
object_count.times do
br.align(4) if @format >= 14
path_id = long_object_ids ? br.i64 : br.i32
offset = br.i32u
size = br.i32u
if @format >= 17
@objects << ObjectData.new(path_id, offset, size, nil, nil, br.i32u, @format <= 10 && br.i16 != 0)
else
@objects << ObjectData.new(path_id, offset, size, br.i32, br.i16, nil, @format <= 10 && br.i16 != 0)
end
br.adv(2) if 11 <= @format && @format <= 16
br.adv(1) if 15 <= @format && @format <= 16
end
if @format >= 11
@add_ids = []
add_id_count = br.i32u
add_id_count.times do
br.align(4) if @format >= 14
@add_ids << [(long_object_ids ? br.i64 : br.i32), br.i32]
end
end
if @format >= 6
@references = []
reference_count = br.i32u
reference_count.times do
@references << Reference.new(br.cstr, br.read(16), br.i32, br.cstr)
end
end
@objects.each do |e|
br.jmp(data_offset + e.offset)
e.data = br.read(e.size)
end
end
def path_ids
@objects.map{|e| e.path_id}
end
def containers
obj = parse_object(1)
return nil unless obj && obj.m_Container && obj.m_Container.array?
obj.m_Container.value.map do |e|
{:name => e.first.value, :preload_index => e.second.preloadIndex.value, :path_id => e.second.asset.m_PathID.value}
end
end
def parse_object(path_id)
if path_id.class == Integer
obj = @objects.find{|e| e.path_id == path_id}
return nil unless obj
elsif path_id.class == ObjectData
obj = path_id
else
return nil
end
klass = (obj.class_idx ? @klasses[obj.class_idx] : @klasses.find{|e| e.class_id == obj.class_id} || @klasses.find{|e| e.class_id == obj.type_id})
type_tree = Asset.parse_type_tree(klass)
return nil unless type_tree
parse_object_private(BinaryReader.new(obj.data, @endian), type_tree)
end
def parse_object_simple(path_id)
Asset.object_simplify(parse_object(path_id))
end
def object_type(path_id)
if path_id.class == Integer
obj = @objects.find{|e| e.path_id == path_id}
return nil unless obj
elsif path_id.class == ObjectData
obj = path_id
else
return nil
end
klass = (obj.class_idx ? @klasses[obj.class_idx] : @klasses.find{|e| e.class_id == obj.class_id} || @klasses.find{|e| e.class_id == obj.type_id})
if klass && klass.type_tree && klass.type_tree.nodes[0]
klass.type_tree.nodes[0].type
elsif klass
Mikunyan::CLASS_ID[klass.class_id]
else
nil
end
end
def self.parse_type_tree(klass)
return nil unless klass.type_tree
nodes = klass.type_tree.nodes
tree = {}
stack = []
nodes.each do |node|
this = {:name => node.name, :node => node, :children => []}
if node.depth == 0
tree = this
else
stack[node.depth - 1][:children] << this
end
stack[node.depth] = this
end
tree
end
def self.object_simplify(obj)
if obj.class != ObjectValue
obj
elsif obj.type == 'pair'
[object_simplify(obj['first']), object_simplify(obj['second'])]
elsif obj.type == 'map' && obj.array?
obj.value.map{|e| [object_simplify(e['first']), object_simplify(e['second'])] }.to_h
elsif obj.value?
object_simplify(obj.value)
elsif obj.array?
obj.value.map{|e| object_simplify(e)}
else
hash = {}
obj.keys.each do |key|
hash[key] = object_simplify(obj[key])
end
hash
end
end
private
def parse_object_private(br, type_tree)
r = nil
node = type_tree[:node]
children = type_tree[:children]
if node.array?
data = []
size = parse_object_private(br, children.find{|e| e[:name] == 'size'})
data_type_tree = children.find{|e| e[:name] == 'data'}
size.value.times do |i|
data << parse_object_private(br, data_type_tree)
end
data = data.map{|e| e.value}.pack('C*') if node.type == 'TypelessData'
r = ObjectValue.new(node.name, node.type, br.endian, data)
elsif node.size == -1
r = ObjectValue.new(node.name, node.type, br.endian)
if children.size == 1 && children[0][:name] == 'Array' && children[0][:node].type == 'Array' && children[0][:node].array?
r.value = parse_object_private(br, children[0]).value
r.value = r.value.map{|e| e.value}.pack('C*').force_encoding("utf-8") if node.type == 'string'
else
children.each do |child|
r[child[:name]] = parse_object_private(br, child)
end
end
elsif children.size > 0
pos = br.pos
r = ObjectValue.new(node.name, node.type, br.endian)
r.is_struct = true
children.each do |child|
r[child[:name]] = parse_object_private(br, child)
end
br.jmp(pos + node.size)
else
pos = br.pos
value = nil
case node.type
when 'bool'
value = (br.i8 != 0)
when 'SInt8'
value = br.i8s
when 'UInt8', 'char'
value = br.i8u
when 'SInt16', 'short'
value = br.i16s
when 'UInt16', 'unsigned short'
value = br.i16u
when 'SInt32', 'int'
value = br.i32s
when 'UInt32', 'unsigned int'
value = br.i32u
when 'SInt64', 'long long'
value = br.i64s
when 'UInt64', 'unsigned long long'
value = br.i64u
when 'float'
value = br.float
when 'double'
value = br.double
when 'ColorRGBA'
value = [br.i8u, br.i8u, br.i8u, br.i8u]
else
value = br.read(node.size)
end
br.jmp(pos + node.size)
r = ObjectValue.new(node.name, node.type, br.endian, value)
end
br.align(4) if node.flags & 0x4000 != 0
r
end
end
end