273 lines
10 KiB
Ruby
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
|