diff --git a/Rakefile b/Rakefile index b146588..5033390 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,16 @@ -require "bundler/gem_tasks" -require "rake/extensiontask" +# frozen_string_literal: true + +require 'bundler/gem_tasks' +require 'rake/extensiontask' task :scream do - puts "みくは自分を曲げないよ!" + puts 'みくは自分を曲げないよ!' end -task :build => :compile +task build: :compile Rake::ExtensionTask.new('decoders/native') do |ext| - ext.lib_dir = 'lib/mikunyan' + ext.lib_dir = 'lib/mikunyan' end -task :default => [:clobber, :compile, :spec] +task default: %i[clobber compile spec] diff --git a/lib/mikunyan.rb b/lib/mikunyan.rb index 86037a6..7e2d83e 100644 --- a/lib/mikunyan.rb +++ b/lib/mikunyan.rb @@ -1,11 +1,13 @@ -require "mikunyan/version" +# frozen_string_literal: true -require "mikunyan/asset_bundle" -require "mikunyan/asset" -require "mikunyan/binary_reader" -require "mikunyan/object_value" -require "mikunyan/type_tree" -require "mikunyan/constants" +require 'mikunyan/version' + +require 'mikunyan/asset_bundle' +require 'mikunyan/asset' +require 'mikunyan/binary_reader' +require 'mikunyan/object_value' +require 'mikunyan/type_tree' +require 'mikunyan/constants' # Module for deserializing Unity Assets and AssetBundles module Mikunyan diff --git a/lib/mikunyan/asset.rb b/lib/mikunyan/asset.rb index 5e32a21..afb72ba 100644 --- a/lib/mikunyan/asset.rb +++ b/lib/mikunyan/asset.rb @@ -1,340 +1,340 @@ +# frozen_string_literal: true + module Mikunyan - # Class for representing Unity Asset - # @attr_reader [String] name Asset name - # @attr_reader [Integer] format file format number - # @attr_reader [String] generator_version version string of generator - # @attr_reader [Integer] target_platform target platform number - # @attr_reader [Symbol] endian data endianness (:little or :big) - # @attr_reader [Array] klasses defined classes - # @attr_reader [Array] objects included objects - # @attr_reader [Array] add_ids ? - # @attr_reader [Array] references reference data - class Asset - attr_reader :name, :format, :generator_version, :target_platform, :endian, :klasses, :objects, :add_ids, :references, :res_s + # Class for representing Unity Asset + # @attr_reader [String] name Asset name + # @attr_reader [Integer] format file format number + # @attr_reader [String] generator_version version string of generator + # @attr_reader [Integer] target_platform target platform number + # @attr_reader [Symbol] endian data endianness (:little or :big) + # @attr_reader [Array] klasses defined classes + # @attr_reader [Array] objects included objects + # @attr_reader [Array] add_ids ? + # @attr_reader [Array] references reference data + class Asset + attr_reader :name, :format, :generator_version, :target_platform, :endian, :klasses, :objects, :add_ids, :references, :res_s - # Struct for representing Asset class definition - # @attr [Integer] class_id class ID - # @attr [Integer,nil] script_id script ID - # @attr [String] hash hash value (16 or 32 bytes) - # @attr [Mikunyan::TypeTree, nil] type_tree given TypeTree - Klass = Struct.new(:class_id, :script_id, :hash, :type_tree) + # Struct for representing Asset class definition + # @attr [Integer] class_id class ID + # @attr [Integer,nil] script_id script ID + # @attr [String] hash hash value (16 or 32 bytes) + # @attr [Mikunyan::TypeTree, nil] type_tree given TypeTree + Klass = Struct.new(:class_id, :script_id, :hash, :type_tree) - # Struct for representing Asset object information - # @attr [Integer] path_id path ID - # @attr [Integer] offset data offset - # @attr [Integer] size data size - # @attr [Integer,nil] type_id type ID - # @attr [Integer,nil] class_id class ID - # @attr [Integer,nil] class_idx class definition index - # @attr [Boolean] destroyed? destroyed or not - # @attr [String] data binary data of object - ObjectData = Struct.new(:path_id, :offset, :size, :type_id, :class_id, :class_idx, :destroyed?, :data) + # Struct for representing Asset object information + # @attr [Integer] path_id path ID + # @attr [Integer] offset data offset + # @attr [Integer] size data size + # @attr [Integer,nil] type_id type ID + # @attr [Integer,nil] class_id class ID + # @attr [Integer,nil] class_idx class definition index + # @attr [Boolean] destroyed? destroyed or not + # @attr [String] data binary data of object + ObjectData = Struct.new(:path_id, :offset, :size, :type_id, :class_id, :class_idx, :destroyed?, :data) - # Struct for representing Asset reference information - # @attr [String] path path - # @attr [String] guid GUID (16 bytes) - # @attr [Integer] type ? - # @attr [String] file_path Asset name - Reference = Struct.new(:path, :guid, :type, :file_path) + # Struct for representing Asset reference information + # @attr [String] path path + # @attr [String] guid GUID (16 bytes) + # @attr [Integer] type ? + # @attr [String] file_path Asset name + Reference = Struct.new(:path, :guid, :type, :file_path) - # Load Asset from binary string - # @param [String] bin binary data - # @param [String] name Asset name - # @param [String] res_s resS data - # @return [Mikunyan::Asset] deserialized Asset object - def self.load(bin, name, res_s = nil) - r = Asset.new(name, res_s) - r.send(:load, bin) - r - end - - # Load Asset from file - # @param [String] file file name - # @param [String] name Asset name (automatically generated if not specified) - # @return [Mikunyan::Asset] deserialized Asset object - def self.file(file, name=nil) - name = File.basename(name, '.*') unless name - Asset.load(File.binread(file), name) - end - - # Returns list of all path IDs - # @return [Array] list of all path IDs - def path_ids - @objects.map{|e| e.path_id} - end - - # Returns list of containers - # @return [Array,nil] list of all containers - 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 - - # Parse object of given path ID - # @param [Integer,ObjectData] path_id path ID or object - # @return [Mikunyan::ObjectValue,nil] parsed object - 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 - - # Parse object of given path ID and simplify it - # @param [Integer,ObjectData] path_id path ID or object - # @return [Hash,nil] parsed object - def parse_object_simple(path_id) - Asset.object_simplify(parse_object(path_id)) - end - - # Returns object type name string - # @param [Integer,ObjectData] path_id path ID or object - # @return [String,nil] type name - 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 - - private - - def initialize(name, res_s = nil) - @name = name - @endian = :big - @res_s = res_s - 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 parse_object_private(br, type_tree) - r = nil - node = type_tree[:node] - children = type_tree[:children] - - if node.array? - data = nil - size = parse_object_private(br, children.find{|e| e[:name] == 'size'}).value - data_type_tree = children.find{|e| e[:name] == 'data'} - if node.type == 'TypelessData' - data = br.read(size * data_type_tree[:node].size) - else - data = size.times.map{ parse_object_private(br, data_type_tree) } - end - 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? - if node.type == 'string' - size = parse_object_private(br, children[0][:children].find{|e| e[:name] == 'size'}).value - r.value = br.read(size * children[0][:children].find{|e| e[:name] == 'data'}[:node].size).force_encoding("utf-8") - br.align(4) if children[0][:node].flags & 0x4000 != 0 - else - r.value = parse_object_private(br, children[0]).value - end - elsif node.type == 'StreamingInfo' - children.each{|child| r[child[:name]] = parse_object_private(br, child)} - r.value = @res_s.byteslice(r['offset'].value, r['size'].value) if r['path'].value == "archive:/#{name}/#{name}.resS" - 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 - 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 - - 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 - - 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 + # Load Asset from binary string + # @param [String] bin binary data + # @param [String] name Asset name + # @param [String] res_s resS data + # @return [Mikunyan::Asset] deserialized Asset object + def self.load(bin, name, res_s = nil) + r = Asset.new(name, res_s) + r.send(:load, bin) + r end + + # Load Asset from file + # @param [String] file file name + # @param [String] name Asset name (automatically generated if not specified) + # @return [Mikunyan::Asset] deserialized Asset object + def self.file(file, name = nil) + name ||= File.basename(name, '.*') + Asset.load(File.binread(file), name) + end + + # Returns list of all path IDs + # @return [Array] list of all path IDs + def path_ids + @objects.map(&:path_id) + end + + # Returns list of containers + # @return [Array,nil] list of all containers + def containers + obj = parse_object(1) + return nil unless 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 + + # Parse object of given path ID + # @param [Integer,ObjectData] path_id path ID or object + # @return [Mikunyan::ObjectValue,nil] parsed object + 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 + + # Parse object of given path ID and simplify it + # @param [Integer,ObjectData] path_id path ID or object + # @return [Hash,nil] parsed object + def parse_object_simple(path_id) + Asset.object_simplify(parse_object(path_id)) + end + + # Returns object type name string + # @param [Integer,ObjectData] path_id path ID or object + # @return [String,nil] type name + 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&.type_tree && klass.type_tree.nodes[0] + klass.type_tree.nodes[0].type + elsif klass + Mikunyan::CLASS_ID[klass.class_id] + end + end + + private + + def initialize(name, res_s = nil) + @name = name + @endian = :big + @res_s = res_s + 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 + hash = if class_id < 0 || class_id == 114 + br.read(32) + else + 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 + hash = if class_id < 0 + br.read(32) + else + 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 + @objects << if @format >= 17 + ObjectData.new(path_id, offset, size, nil, nil, br.i32u, @format <= 10 && br.i16 != 0) + else + 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 parse_object_private(br, type_tree) + r = nil + node = type_tree[:node] + children = type_tree[:children] + + if node.array? + data = nil + size = parse_object_private(br, children.find{|e| e[:name] == 'size'}).value + data_type_tree = children.find{|e| e[:name] == 'data'} + data = if node.type == 'TypelessData' + br.read(size * data_type_tree[:node].size) + else + size.times.map{parse_object_private(br, data_type_tree)} + end + 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? + if node.type == 'string' + size = parse_object_private(br, children[0][:children].find{|e| e[:name] == 'size'}).value + r.value = br.read(size * children[0][:children].find{|e| e[:name] == 'data'}[:node].size).force_encoding('utf-8') + br.align(4) if children[0][:node].flags & 0x4000 != 0 + else + r.value = parse_object_private(br, children[0]).value + end + elsif node.type == 'StreamingInfo' + children.each{|child| r[child[:name]] = parse_object_private(br, child)} + r.value = @res_s.byteslice(r['offset'].value, r['size'].value) if r['path'].value == "archive:/#{name}/#{name}.resS" + else + children.each do |child| + r[child[:name]] = parse_object_private(br, child) + end + end + elsif !children.empty? + 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 + 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 + + 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 + + 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 + end end diff --git a/lib/mikunyan/asset_bundle.rb b/lib/mikunyan/asset_bundle.rb index 0236b4f..c7bc2bb 100644 --- a/lib/mikunyan/asset_bundle.rb +++ b/lib/mikunyan/asset_bundle.rb @@ -1,132 +1,134 @@ +# frozen_string_literal: true + require 'extlz4' require 'extlzma' module Mikunyan - # Class for representing Unity AssetBundle - # @attr_reader [String] signature file signature (UnityRaw or UnityFS) - # @attr_reader [Integer] format file format number - # @attr_reader [String] unity_version version string of Unity to use this AssetBundle - # @attr_reader [String] generator_version version string of generator - # @attr_reader [Array] assets included Assets - class AssetBundle - attr_reader :signature, :format, :unity_version, :generator_version, :assets + # Class for representing Unity AssetBundle + # @attr_reader [String] signature file signature (UnityRaw or UnityFS) + # @attr_reader [Integer] format file format number + # @attr_reader [String] unity_version version string of Unity to use this AssetBundle + # @attr_reader [String] generator_version version string of generator + # @attr_reader [Array] assets included Assets + class AssetBundle + attr_reader :signature, :format, :unity_version, :generator_version, :assets - # Load AssetBundle from binary string - # @param [String] bin binary data - # @return [Mikunyan::AssetBundle] deserialized AssetBundle object - def self.load(bin) - r = AssetBundle.new - r.send(:load, bin) - r - end - - # Load AssetBundle from file - # @param [String] file file name - # @return [Mikunyan::AssetBundle] deserialized AssetBundle object - def self.file(file) - AssetBundle.load(File.binread(file)) - end - - private - - def load(bin) - br = BinaryReader.new(bin) - @signature = br.cstr - @format = br.i32 - @unity_version = br.cstr - @generator_version = br.cstr - - case @signature - when 'UnityRaw' - load_unity_raw(br, false) - when 'UnityFS' - load_unity_fs(br) - when 'UnityWeb' - if @format <= 3 - load_unity_raw(br, true) - else - load_unity_fs(br) - end - else - raise("Unknown signature: #{@signature}") - end - end - - def load_unity_raw(br, compressed) - @assets = [] - - file_size = br.i32u - header_size = br.i32u - - if compressed - br.adv(4) - block_count = br.i32u - blocks = block_count.times.map{{ :c => br.i32u, :u => br.i32u }} - br.jmp(header_size) - data = String.new - blocks.each{|b| data << LZMA.decode(br.read(b[:c]))} - br = BinaryReader.new(data) - else - br.jmp(header_size) - end - - asset_count = br.i32u - asset_count.times do - asset_pos = br.pos - asset_name = br.cstr - asset_header_size = br.i32u - asset_size = br.i32u - br.jmp(asset_pos + asset_header_size - 4) - asset_data = br.read(asset_size) - asset = Asset.load(asset_data, asset_name) - @assets << asset - end - end - - def load_unity_fs(br) - @assets = [] - - file_size = br.i64u - ci_block_size = br.i32u - ui_block_size = br.i32u - flags = br.i32u - - head = BinaryReader.new(uncompress(flags & 0x80 == 0 ? br.read(ci_block_size) : br.read_abs(ci_block_size, file_size - ci_block_size), ui_block_size, flags)) - guid = head.read(16) - - blocks = [] - block_count = head.i32u - block_count.times{ blocks << {:u => head.i32u, :c => head.i32u, :f => head.i16u} } - - asset_blocks = [] - asset_count = head.i32u - asset_count.times{ asset_blocks << {:offset => head.i64u, :size => head.i64u, :status => head.i32, :name => head.cstr} } - - raw_data = String.new - blocks.each{|b| raw_data << uncompress(br.read(b[:c]), b[:u], b[:f])} - - asset_blocks.each do |b| - next if b[:name].end_with?('.resS') - res_s = asset_blocks.find{|e| e[:name] == "#{b[:name]}.resS"} - asset = Asset.load(raw_data.byteslice(b[:offset], b[:size]), b[:name], res_s && raw_data.byteslice(res_s[:offset], res_s[:size])) - @assets << asset - end - end - - def uncompress(block, max_dest_size, flags) - case flags & 0x3f - when 0 - block - # when 1 - # LZMA - when 2, 3 - LZ4.raw_decode(block, max_dest_size) - # when 4 - # LZHMA - else - warn("Unknown compression flag: #{@flags}") - block - end - end + # Load AssetBundle from binary string + # @param [String] bin binary data + # @return [Mikunyan::AssetBundle] deserialized AssetBundle object + def self.load(bin) + r = AssetBundle.new + r.send(:load, bin) + r end + + # Load AssetBundle from file + # @param [String] file file name + # @return [Mikunyan::AssetBundle] deserialized AssetBundle object + def self.file(file) + AssetBundle.load(File.binread(file)) + end + + private + + def load(bin) + br = BinaryReader.new(bin) + @signature = br.cstr + @format = br.i32 + @unity_version = br.cstr + @generator_version = br.cstr + + case @signature + when 'UnityRaw' + load_unity_raw(br, false) + when 'UnityFS' + load_unity_fs(br) + when 'UnityWeb' + if @format <= 3 + load_unity_raw(br, true) + else + load_unity_fs(br) + end + else + raise("Unknown signature: #{@signature}") + end + end + + def load_unity_raw(br, compressed) + @assets = [] + + file_size = br.i32u + header_size = br.i32u + + if compressed + br.adv(4) + block_count = br.i32u + blocks = block_count.times.map{{ c: br.i32u, u: br.i32u }} + br.jmp(header_size) + data = '' + blocks.each{|b| data << LZMA.decode(br.read(b[:c]))} + br = BinaryReader.new(data) + else + br.jmp(header_size) + end + + asset_count = br.i32u + asset_count.times do + asset_pos = br.pos + asset_name = br.cstr + asset_header_size = br.i32u + asset_size = br.i32u + br.jmp(asset_pos + asset_header_size - 4) + asset_data = br.read(asset_size) + asset = Asset.load(asset_data, asset_name) + @assets << asset + end + end + + def load_unity_fs(br) + @assets = [] + + file_size = br.i64u + ci_block_size = br.i32u + ui_block_size = br.i32u + flags = br.i32u + + head = BinaryReader.new(uncompress(flags & 0x80 == 0 ? br.read(ci_block_size) : br.read_abs(ci_block_size, file_size - ci_block_size), ui_block_size, flags)) + guid = head.read(16) + + blocks = [] + block_count = head.i32u + block_count.times{blocks << { u: head.i32u, c: head.i32u, f: head.i16u }} + + asset_blocks = [] + asset_count = head.i32u + asset_count.times{asset_blocks << { offset: head.i64u, size: head.i64u, status: head.i32, name: head.cstr }} + + raw_data = '' + blocks.each{|b| raw_data << uncompress(br.read(b[:c]), b[:u], b[:f])} + + asset_blocks.each do |b| + next if b[:name].end_with?('.resS') + res_s = asset_blocks.find{|e| e[:name] == "#{b[:name]}.resS"} + asset = Asset.load(raw_data.byteslice(b[:offset], b[:size]), b[:name], res_s && raw_data.byteslice(res_s[:offset], res_s[:size])) + @assets << asset + end + end + + def uncompress(block, max_dest_size, flags) + case flags & 0x3f + when 0 + block + # when 1 + # LZMA + when 2, 3 + LZ4.raw_decode(block, max_dest_size) + # when 4 + # LZHMA + else + warn("Unknown compression flag: #{@flags}") + block + end + end + end end diff --git a/lib/mikunyan/binary_reader.rb b/lib/mikunyan/binary_reader.rb index 19da576..ddc8715 100644 --- a/lib/mikunyan/binary_reader.rb +++ b/lib/mikunyan/binary_reader.rb @@ -1,160 +1,162 @@ +# frozen_string_literal: true + require 'bin_utils' module Mikunyan - # Class for manipulating binary string - # @attr [Symbol] endian endianness - # @attr [Integer] pos position - # @attr [Integer] length data size - class BinaryReader - attr_accessor :endian, :pos, :length + # Class for manipulating binary string + # @attr [Symbol] endian endianness + # @attr [Integer] pos position + # @attr [Integer] length data size + class BinaryReader + attr_accessor :endian, :pos, :length - # Constructor - # @param [String] data binary string - # @param [Symbol] endian endianness - def initialize(data, endian = :big) - @data = data - @pos = 0 - @length = data.bytesize - @endian = endian - end - - # Returns whether little endian or not - # @return [Boolean] - def little? - @endian == :little - end - - # Jump to given position - # @param [Integer] pos position - def jmp(pos=0) - @pos = pos - end - - # Advance position given size - # @param [Integer] size size - def adv(size=0) - @pos += size - end - - # Round up position to multiple of given size - # @param [Integer] size size - def align(size) - @pos = (@pos + size - 1) / size * size - end - - # Read given size of binary string and seek - # @param [Integer] size size - # @return [String] data - def read(size) - data = @data.byteslice(@pos, size) - @pos += size - data - end - - # Read given size of binary string from specified position. This method does not seek. - # @param [Integer] size size - # @param [Integer] pos position - # @return [String] data - def read_abs(size, pos) - @data.byteslice(pos, size) - end - - # Read string until null character - # @return [String] string - def cstr - r = @data.unpack("@#{pos}Z*")[0] - @pos += r.bytesize + 1 - r - end - - # Read 8bit signed integer - def i8 - i8s - end - - # Read 8bit signed integer - def i8s - r = BinUtils.get_sint8(@data, @pos) - @pos += 1 - r - end - - # Read 8bit unsigned integer - def i8u - r = BinUtils.get_int8(@data, @pos) - @pos += 1 - r - end - - # Read 16bit signed integer - def i16 - i16s - end - - # Read 16bit signed integer - def i16s - r = little? ? BinUtils.get_sint16_le(@data, @pos) : BinUtils.get_sint16_be(@data, @pos) - @pos += 2 - r - end - - # Read 16bit unsigned integer - def i16u - r = little? ? BinUtils.get_int16_le(@data, @pos) : BinUtils.get_int16_be(@data, @pos) - @pos += 2 - r - end - - # Read 32bit signed integer - def i32 - i32s - end - - # Read 32bit signed integer - def i32s - r = little? ? BinUtils.get_sint32_le(@data, @pos) : BinUtils.get_sint32_be(@data, @pos) - @pos += 4 - r - end - - # Read 32bit unsigned integer - def i32u - r = little? ? BinUtils.get_int32_le(@data, @pos) : BinUtils.get_int32_be(@data, @pos) - @pos += 4 - r - end - - # Read 64bit signed integer - def i64 - i64s - end - - # Read 64bit signed integer - def i64s - r = little? ? BinUtils.get_sint64_le(@data, @pos) : BinUtils.get_sint64_be(@data, @pos) - @pos += 8 - r - end - - # Read 64bit unsigned integer - def i64u - r = little? ? BinUtils.get_int64_le(@data, @pos) : BinUtils.get_int64_be(@data, @pos) - @pos += 8 - r - end - - # Read 32bit floating point value - def float - r = little? ? @data.byteslice(@pos, 4).unpack('e')[0] : @data.byteslice(@pos, 4).unpack('g')[0] - @pos += 4 - r - end - - # Read 64bit floating point value - def double - r = little? ? @data.byteslice(@pos, 8).unpack('E')[0] : @data.byteslice(@pos, 8).unpack('G')[0] - @pos += 8 - r - end + # Constructor + # @param [String] data binary string + # @param [Symbol] endian endianness + def initialize(data, endian = :big) + @data = data + @pos = 0 + @length = data.bytesize + @endian = endian end + + # Returns whether little endian or not + # @return [Boolean] + def little? + @endian == :little + end + + # Jump to given position + # @param [Integer] pos position + def jmp(pos = 0) + @pos = pos + end + + # Advance position given size + # @param [Integer] size size + def adv(size = 0) + @pos += size + end + + # Round up position to multiple of given size + # @param [Integer] size size + def align(size) + @pos = (@pos + size - 1) / size * size + end + + # Read given size of binary string and seek + # @param [Integer] size size + # @return [String] data + def read(size) + data = @data.byteslice(@pos, size) + @pos += size + data + end + + # Read given size of binary string from specified position. This method does not seek. + # @param [Integer] size size + # @param [Integer] pos position + # @return [String] data + def read_abs(size, pos) + @data.byteslice(pos, size) + end + + # Read string until null character + # @return [String] string + def cstr + r = @data.unpack1("@#{pos}Z*") + @pos += r.bytesize + 1 + r + end + + # Read 8bit signed integer + def i8 + i8s + end + + # Read 8bit signed integer + def i8s + r = BinUtils.get_sint8(@data, @pos) + @pos += 1 + r + end + + # Read 8bit unsigned integer + def i8u + r = BinUtils.get_int8(@data, @pos) + @pos += 1 + r + end + + # Read 16bit signed integer + def i16 + i16s + end + + # Read 16bit signed integer + def i16s + r = little? ? BinUtils.get_sint16_le(@data, @pos) : BinUtils.get_sint16_be(@data, @pos) + @pos += 2 + r + end + + # Read 16bit unsigned integer + def i16u + r = little? ? BinUtils.get_int16_le(@data, @pos) : BinUtils.get_int16_be(@data, @pos) + @pos += 2 + r + end + + # Read 32bit signed integer + def i32 + i32s + end + + # Read 32bit signed integer + def i32s + r = little? ? BinUtils.get_sint32_le(@data, @pos) : BinUtils.get_sint32_be(@data, @pos) + @pos += 4 + r + end + + # Read 32bit unsigned integer + def i32u + r = little? ? BinUtils.get_int32_le(@data, @pos) : BinUtils.get_int32_be(@data, @pos) + @pos += 4 + r + end + + # Read 64bit signed integer + def i64 + i64s + end + + # Read 64bit signed integer + def i64s + r = little? ? BinUtils.get_sint64_le(@data, @pos) : BinUtils.get_sint64_be(@data, @pos) + @pos += 8 + r + end + + # Read 64bit unsigned integer + def i64u + r = little? ? BinUtils.get_int64_le(@data, @pos) : BinUtils.get_int64_be(@data, @pos) + @pos += 8 + r + end + + # Read 32bit floating point value + def float + r = little? ? @data.byteslice(@pos, 4).unpack1('e') : @data.byteslice(@pos, 4).unpack1('g') + @pos += 4 + r + end + + # Read 64bit floating point value + def double + r = little? ? @data.byteslice(@pos, 8).unpack1('E') : @data.byteslice(@pos, 8).unpack1('G') + @pos += 8 + r + end + end end diff --git a/lib/mikunyan/constants.rb b/lib/mikunyan/constants.rb index 4142c13..e19c0d6 100644 --- a/lib/mikunyan/constants.rb +++ b/lib/mikunyan/constants.rb @@ -1,344 +1,347 @@ -module Mikunyan - private - STRING_TABLE = { - 0=>'AABB', - 5=>'AnimationClip', - 19=>'AnimationCurve', - 34=>'AnimationState', - 49=>'Array', - 55=>'Base', - 60=>'BitField', - 69=>'bitset', - 76=>'bool', - 81=>'char', - 86=>'ColorRGBA', - 96=>'Component', - 106=>'data', - 111=>'deque', - 117=>'double', - 124=>'dynamic_array', - 138=>'FastPropertyName', - 155=>'first', - 161=>'float', - 167=>'Font', - 172=>'GameObject', - 183=>'Generic Mono', - 196=>'GradientNEW', - 208=>'GUID', - 213=>'GUIStyle', - 222=>'int', - 226=>'list', - 231=>'long long', - 241=>'map', - 245=>'Matrix4x4f', - 256=>'MdFour', - 263=>'MonoBehaviour', - 277=>'MonoScript', - 288=>'m_ByteSize', - 299=>'m_Curve', - 307=>'m_EditorClassIdentifier', - 331=>'m_EditorHideFlags', - 349=>'m_Enabled', - 359=>'m_ExtensionPtr', - 374=>'m_GameObject', - 387=>'m_Index', - 395=>'m_IsArray', - 405=>'m_IsStatic', - 416=>'m_MetaFlag', - 427=>'m_Name', - 434=>'m_ObjectHideFlags', - 452=>'m_PrefabInternal', - 469=>'m_PrefabParentObject', - 490=>'m_Script', - 499=>'m_StaticEditorFlags', - 519=>'m_Type', - 526=>'m_Version', - 536=>'Object', - 543=>'pair', - 548=>'PPtr', - 564=>'PPtr', - 581=>'PPtr', - 596=>'PPtr', - 616=>'PPtr', - 633=>'PPtr', - 646=>'PPtr', - 659=>'PPtr', - 672=>'PPtr', - 688=>'PPtr', - 702=>'PPtr', - 718=>'PPtr', - 734=>'Prefab', - 741=>'Quaternionf', - 753=>'Rectf', - 759=>'RectInt', - 767=>'RectOffset', - 778=>'second', - 785=>'set', - 789=>'short', - 795=>'size', - 800=>'SInt16', - 807=>'SInt32', - 814=>'SInt64', - 821=>'SInt8', - 827=>'staticvector', - 840=>'string', - 847=>'TextAsset', - 857=>'TextMesh', - 866=>'Texture', - 874=>'Texture2D', - 884=>'Transform', - 894=>'TypelessData', - 907=>'UInt16', - 914=>'UInt32', - 921=>'UInt64', - 928=>'UInt8', - 934=>'unsigned int', - 947=>'unsigned long long', - 966=>'unsigned short', - 981=>'vector', - 988=>'Vector2f', - 997=>'Vector3f', - 1006=>'Vector4f', - 1015=>'m_ScriptingClassIdentifier', - 1042=>'Gradient' - } +# frozen_string_literal: true - CLASS_ID = { - 1=>'GameObject', - 2=>'Component', - 3=>'LevelGameManager', - 4=>'Transform', - 5=>'TimeManager', - 6=>'GlobalGameManager', - 8=>'Behaviour', - 9=>'GameManager', - 11=>'AudioManager', - 12=>'ParticleAnimator', - 13=>'InputManager', - 15=>'EllipsoidParticleEmitter', - 17=>'Pipeline', - 18=>'EditorExtension', - 19=>'Physics2DSettings', - 20=>'Camera', - 21=>'Material', - 23=>'MeshRenderer', - 25=>'Renderer', - 26=>'ParticleRenderer', - 27=>'Texture', - 28=>'Texture2D', - 29=>'SceneSettings', - 30=>'GraphicsSettings', - 33=>'MeshFilter', - 41=>'OcclusionPortal', - 43=>'Mesh', - 45=>'Skybox', - 47=>'QualitySettings', - 48=>'Shader', - 49=>'TextAsset', - 50=>'Rigidbody2D', - 51=>'Physics2DManager', - 53=>'Collider2D', - 54=>'Rigidbody', - 55=>'PhysicsManager', - 56=>'Collider', - 57=>'Joint', - 58=>'CircleCollider2D', - 59=>'HingeJoint', - 60=>'PolygonCollider2D', - 61=>'BoxCollider2D', - 62=>'PhysicsMaterial2D', - 64=>'MeshCollider', - 65=>'BoxCollider', - 66=>'SpriteCollider2D', - 68=>'EdgeCollider2D', - 72=>'ComputeShader', - 74=>'AnimationClip', - 75=>'ConstantForce', - 76=>'WorldParticleCollider', - 78=>'TagManager', - 81=>'AudioListener', - 82=>'AudioSource', - 83=>'AudioClip', - 84=>'RenderTexture', - 87=>'MeshParticleEmitter', - 88=>'ParticleEmitter', - 89=>'Cubemap', - 90=>'Avatar', - 91=>'AnimatorController', - 92=>'GUILayer', - 93=>'RuntimeAnimatorController', - 94=>'ScriptMapper', - 95=>'Animator', - 96=>'TrailRenderer', - 98=>'DelayedCallManager', - 102=>'TextMesh', - 104=>'RenderSettings', - 108=>'Light', - 109=>'CGProgram', - 110=>'BaseAnimationTrack', - 111=>'Animation', - 114=>'MonoBehaviour', - 115=>'MonoScript', - 116=>'MonoManager', - 117=>'Texture3D', - 118=>'NewAnimationTrack', - 119=>'Projector', - 120=>'LineRenderer', - 121=>'Flare', - 122=>'Halo', - 123=>'LensFlare', - 124=>'FlareLayer', - 125=>'HaloLayer', - 126=>'NavMeshAreas', - 127=>'HaloManager', - 128=>'Font', - 129=>'PlayerSettings', - 130=>'NamedObject', - 131=>'GUITexture', - 132=>'GUIText', - 133=>'GUIElement', - 134=>'PhysicMaterial', - 135=>'SphereCollider', - 136=>'CapsuleCollider', - 137=>'SkinnedMeshRenderer', - 138=>'FixedJoint', - 140=>'RaycastCollider', - 141=>'BuildSettings', - 142=>'AssetBundle', - 143=>'CharacterController', - 144=>'CharacterJoint', - 145=>'SpringJoint', - 146=>'WheelCollider', - 147=>'ResourceManager', - 148=>'NetworkView', - 149=>'NetworkManager', - 150=>'PreloadData', - 152=>'MovieTexture', - 153=>'ConfigurableJoint', - 154=>'TerrainCollider', - 155=>'MasterServerInterface', - 156=>'TerrainData', - 157=>'LightmapSettings', - 158=>'WebCamTexture', - 159=>'EditorSettings', - 160=>'InteractiveCloth', - 161=>'ClothRenderer', - 162=>'EditorUserSettings', - 163=>'SkinnedCloth', - 164=>'AudioReverbFilter', - 165=>'AudioHighPassFilter', - 166=>'AudioChorusFilter', - 167=>'AudioReverbZone', - 168=>'AudioEchoFilter', - 169=>'AudioLowPassFilter', - 170=>'AudioDistortionFilter', - 171=>'SparseTexture', - 180=>'AudioBehaviour', - 181=>'AudioFilter', - 182=>'WindZone', - 183=>'Cloth', - 184=>'SubstanceArchive', - 185=>'ProceduralMaterial', - 186=>'ProceduralTexture', - 191=>'OffMeshLink', - 192=>'OcclusionArea', - 193=>'Tree', - 194=>'NavMeshObsolete', - 195=>'NavMeshAgent', - 196=>'NavMeshSettings', - 197=>'LightProbesLegacy', - 198=>'ParticleSystem', - 199=>'ParticleSystemRenderer', - 200=>'ShaderVariantCollection', - 205=>'LODGroup', - 206=>'BlendTree', - 207=>'Motion', - 208=>'NavMeshObstacle', - 210=>'TerrainInstance', - 212=>'SpriteRenderer', - 213=>'Sprite', - 214=>'CachedSpriteAtlas', - 215=>'ReflectionProbe', - 216=>'ReflectionProbes', - 218=>'Terrain', - 220=>'LightProbeGroup', - 221=>'AnimatorOverrideController', - 222=>'CanvasRenderer', - 223=>'Canvas', - 224=>'RectTransform', - 225=>'CanvasGroup', - 226=>'BillboardAsset', - 227=>'BillboardRenderer', - 228=>'SpeedTreeWindAsset', - 229=>'AnchoredJoint2D', - 230=>'Joint2D', - 231=>'SpringJoint2D', - 232=>'DistanceJoint2D', - 233=>'HingeJoint2D', - 234=>'SliderJoint2D', - 235=>'WheelJoint2D', - 238=>'NavMeshData', - 240=>'AudioMixer', - 241=>'AudioMixerController', - 243=>'AudioMixerGroupController', - 244=>'AudioMixerEffectController', - 245=>'AudioMixerSnapshotController', - 246=>'PhysicsUpdateBehaviour2D', - 247=>'ConstantForce2D', - 248=>'Effector2D', - 249=>'AreaEffector2D', - 250=>'PointEffector2D', - 251=>'PlatformEffector2D', - 252=>'SurfaceEffector2D', - 258=>'LightProbes', - 271=>'SampleClip', - 272=>'AudioMixerSnapshot', - 273=>'AudioMixerGroup', - 290=>'AssetBundleManifest', - 1001=>'Prefab', - 1002=>'EditorExtensionImpl', - 1003=>'AssetImporter', - 1004=>'AssetDatabase', - 1005=>'Mesh3DSImporter', - 1006=>'TextureImporter', - 1007=>'ShaderImporter', - 1008=>'ComputeShaderImporter', - 1011=>'AvatarMask', - 1020=>'AudioImporter', - 1026=>'HierarchyState', - 1027=>'GUIDSerializer', - 1028=>'AssetMetaData', - 1029=>'DefaultAsset', - 1030=>'DefaultImporter', - 1031=>'TextScriptImporter', - 1032=>'SceneAsset', - 1034=>'NativeFormatImporter', - 1035=>'MonoImporter', - 1037=>'AssetServerCache', - 1038=>'LibraryAssetImporter', - 1040=>'ModelImporter', - 1041=>'FBXImporter', - 1042=>'TrueTypeFontImporter', - 1044=>'MovieImporter', - 1045=>'EditorBuildSettings', - 1046=>'DDSImporter', - 1048=>'InspectorExpandedState', - 1049=>'AnnotationManager', - 1050=>'PluginImporter', - 1051=>'EditorUserBuildSettings', - 1052=>'PVRImporter', - 1053=>'ASTCImporter', - 1054=>'KTXImporter', - 1101=>'AnimatorStateTransition', - 1102=>'AnimatorState', - 1105=>'HumanTemplate', - 1107=>'AnimatorStateMachine', - 1108=>'PreviewAssetType', - 1109=>'AnimatorTransition', - 1110=>'SpeedTreeImporter', - 1111=>'AnimatorTransitionBase', - 1112=>'SubstanceImporter', - 1113=>'LightmapParameters', - 1120=>'LightmapSnapshot' - } +module Mikunyan + private + + STRING_TABLE = { + 0 => 'AABB', + 5 => 'AnimationClip', + 19 => 'AnimationCurve', + 34 => 'AnimationState', + 49 => 'Array', + 55 => 'Base', + 60 => 'BitField', + 69 => 'bitset', + 76 => 'bool', + 81 => 'char', + 86 => 'ColorRGBA', + 96 => 'Component', + 106 => 'data', + 111 => 'deque', + 117 => 'double', + 124 => 'dynamic_array', + 138 => 'FastPropertyName', + 155 => 'first', + 161 => 'float', + 167 => 'Font', + 172 => 'GameObject', + 183 => 'Generic Mono', + 196 => 'GradientNEW', + 208 => 'GUID', + 213 => 'GUIStyle', + 222 => 'int', + 226 => 'list', + 231 => 'long long', + 241 => 'map', + 245 => 'Matrix4x4f', + 256 => 'MdFour', + 263 => 'MonoBehaviour', + 277 => 'MonoScript', + 288 => 'm_ByteSize', + 299 => 'm_Curve', + 307 => 'm_EditorClassIdentifier', + 331 => 'm_EditorHideFlags', + 349 => 'm_Enabled', + 359 => 'm_ExtensionPtr', + 374 => 'm_GameObject', + 387 => 'm_Index', + 395 => 'm_IsArray', + 405 => 'm_IsStatic', + 416 => 'm_MetaFlag', + 427 => 'm_Name', + 434 => 'm_ObjectHideFlags', + 452 => 'm_PrefabInternal', + 469 => 'm_PrefabParentObject', + 490 => 'm_Script', + 499 => 'm_StaticEditorFlags', + 519 => 'm_Type', + 526 => 'm_Version', + 536 => 'Object', + 543 => 'pair', + 548 => 'PPtr', + 564 => 'PPtr', + 581 => 'PPtr', + 596 => 'PPtr', + 616 => 'PPtr', + 633 => 'PPtr', + 646 => 'PPtr', + 659 => 'PPtr', + 672 => 'PPtr', + 688 => 'PPtr', + 702 => 'PPtr', + 718 => 'PPtr', + 734 => 'Prefab', + 741 => 'Quaternionf', + 753 => 'Rectf', + 759 => 'RectInt', + 767 => 'RectOffset', + 778 => 'second', + 785 => 'set', + 789 => 'short', + 795 => 'size', + 800 => 'SInt16', + 807 => 'SInt32', + 814 => 'SInt64', + 821 => 'SInt8', + 827 => 'staticvector', + 840 => 'string', + 847 => 'TextAsset', + 857 => 'TextMesh', + 866 => 'Texture', + 874 => 'Texture2D', + 884 => 'Transform', + 894 => 'TypelessData', + 907 => 'UInt16', + 914 => 'UInt32', + 921 => 'UInt64', + 928 => 'UInt8', + 934 => 'unsigned int', + 947 => 'unsigned long long', + 966 => 'unsigned short', + 981 => 'vector', + 988 => 'Vector2f', + 997 => 'Vector3f', + 1006 => 'Vector4f', + 1015 => 'm_ScriptingClassIdentifier', + 1042 => 'Gradient' + }.freeze + + CLASS_ID = { + 1 => 'GameObject', + 2 => 'Component', + 3 => 'LevelGameManager', + 4 => 'Transform', + 5 => 'TimeManager', + 6 => 'GlobalGameManager', + 8 => 'Behaviour', + 9 => 'GameManager', + 11 => 'AudioManager', + 12 => 'ParticleAnimator', + 13 => 'InputManager', + 15 => 'EllipsoidParticleEmitter', + 17 => 'Pipeline', + 18 => 'EditorExtension', + 19 => 'Physics2DSettings', + 20 => 'Camera', + 21 => 'Material', + 23 => 'MeshRenderer', + 25 => 'Renderer', + 26 => 'ParticleRenderer', + 27 => 'Texture', + 28 => 'Texture2D', + 29 => 'SceneSettings', + 30 => 'GraphicsSettings', + 33 => 'MeshFilter', + 41 => 'OcclusionPortal', + 43 => 'Mesh', + 45 => 'Skybox', + 47 => 'QualitySettings', + 48 => 'Shader', + 49 => 'TextAsset', + 50 => 'Rigidbody2D', + 51 => 'Physics2DManager', + 53 => 'Collider2D', + 54 => 'Rigidbody', + 55 => 'PhysicsManager', + 56 => 'Collider', + 57 => 'Joint', + 58 => 'CircleCollider2D', + 59 => 'HingeJoint', + 60 => 'PolygonCollider2D', + 61 => 'BoxCollider2D', + 62 => 'PhysicsMaterial2D', + 64 => 'MeshCollider', + 65 => 'BoxCollider', + 66 => 'SpriteCollider2D', + 68 => 'EdgeCollider2D', + 72 => 'ComputeShader', + 74 => 'AnimationClip', + 75 => 'ConstantForce', + 76 => 'WorldParticleCollider', + 78 => 'TagManager', + 81 => 'AudioListener', + 82 => 'AudioSource', + 83 => 'AudioClip', + 84 => 'RenderTexture', + 87 => 'MeshParticleEmitter', + 88 => 'ParticleEmitter', + 89 => 'Cubemap', + 90 => 'Avatar', + 91 => 'AnimatorController', + 92 => 'GUILayer', + 93 => 'RuntimeAnimatorController', + 94 => 'ScriptMapper', + 95 => 'Animator', + 96 => 'TrailRenderer', + 98 => 'DelayedCallManager', + 102 => 'TextMesh', + 104 => 'RenderSettings', + 108 => 'Light', + 109 => 'CGProgram', + 110 => 'BaseAnimationTrack', + 111 => 'Animation', + 114 => 'MonoBehaviour', + 115 => 'MonoScript', + 116 => 'MonoManager', + 117 => 'Texture3D', + 118 => 'NewAnimationTrack', + 119 => 'Projector', + 120 => 'LineRenderer', + 121 => 'Flare', + 122 => 'Halo', + 123 => 'LensFlare', + 124 => 'FlareLayer', + 125 => 'HaloLayer', + 126 => 'NavMeshAreas', + 127 => 'HaloManager', + 128 => 'Font', + 129 => 'PlayerSettings', + 130 => 'NamedObject', + 131 => 'GUITexture', + 132 => 'GUIText', + 133 => 'GUIElement', + 134 => 'PhysicMaterial', + 135 => 'SphereCollider', + 136 => 'CapsuleCollider', + 137 => 'SkinnedMeshRenderer', + 138 => 'FixedJoint', + 140 => 'RaycastCollider', + 141 => 'BuildSettings', + 142 => 'AssetBundle', + 143 => 'CharacterController', + 144 => 'CharacterJoint', + 145 => 'SpringJoint', + 146 => 'WheelCollider', + 147 => 'ResourceManager', + 148 => 'NetworkView', + 149 => 'NetworkManager', + 150 => 'PreloadData', + 152 => 'MovieTexture', + 153 => 'ConfigurableJoint', + 154 => 'TerrainCollider', + 155 => 'MasterServerInterface', + 156 => 'TerrainData', + 157 => 'LightmapSettings', + 158 => 'WebCamTexture', + 159 => 'EditorSettings', + 160 => 'InteractiveCloth', + 161 => 'ClothRenderer', + 162 => 'EditorUserSettings', + 163 => 'SkinnedCloth', + 164 => 'AudioReverbFilter', + 165 => 'AudioHighPassFilter', + 166 => 'AudioChorusFilter', + 167 => 'AudioReverbZone', + 168 => 'AudioEchoFilter', + 169 => 'AudioLowPassFilter', + 170 => 'AudioDistortionFilter', + 171 => 'SparseTexture', + 180 => 'AudioBehaviour', + 181 => 'AudioFilter', + 182 => 'WindZone', + 183 => 'Cloth', + 184 => 'SubstanceArchive', + 185 => 'ProceduralMaterial', + 186 => 'ProceduralTexture', + 191 => 'OffMeshLink', + 192 => 'OcclusionArea', + 193 => 'Tree', + 194 => 'NavMeshObsolete', + 195 => 'NavMeshAgent', + 196 => 'NavMeshSettings', + 197 => 'LightProbesLegacy', + 198 => 'ParticleSystem', + 199 => 'ParticleSystemRenderer', + 200 => 'ShaderVariantCollection', + 205 => 'LODGroup', + 206 => 'BlendTree', + 207 => 'Motion', + 208 => 'NavMeshObstacle', + 210 => 'TerrainInstance', + 212 => 'SpriteRenderer', + 213 => 'Sprite', + 214 => 'CachedSpriteAtlas', + 215 => 'ReflectionProbe', + 216 => 'ReflectionProbes', + 218 => 'Terrain', + 220 => 'LightProbeGroup', + 221 => 'AnimatorOverrideController', + 222 => 'CanvasRenderer', + 223 => 'Canvas', + 224 => 'RectTransform', + 225 => 'CanvasGroup', + 226 => 'BillboardAsset', + 227 => 'BillboardRenderer', + 228 => 'SpeedTreeWindAsset', + 229 => 'AnchoredJoint2D', + 230 => 'Joint2D', + 231 => 'SpringJoint2D', + 232 => 'DistanceJoint2D', + 233 => 'HingeJoint2D', + 234 => 'SliderJoint2D', + 235 => 'WheelJoint2D', + 238 => 'NavMeshData', + 240 => 'AudioMixer', + 241 => 'AudioMixerController', + 243 => 'AudioMixerGroupController', + 244 => 'AudioMixerEffectController', + 245 => 'AudioMixerSnapshotController', + 246 => 'PhysicsUpdateBehaviour2D', + 247 => 'ConstantForce2D', + 248 => 'Effector2D', + 249 => 'AreaEffector2D', + 250 => 'PointEffector2D', + 251 => 'PlatformEffector2D', + 252 => 'SurfaceEffector2D', + 258 => 'LightProbes', + 271 => 'SampleClip', + 272 => 'AudioMixerSnapshot', + 273 => 'AudioMixerGroup', + 290 => 'AssetBundleManifest', + 1001 => 'Prefab', + 1002 => 'EditorExtensionImpl', + 1003 => 'AssetImporter', + 1004 => 'AssetDatabase', + 1005 => 'Mesh3DSImporter', + 1006 => 'TextureImporter', + 1007 => 'ShaderImporter', + 1008 => 'ComputeShaderImporter', + 1011 => 'AvatarMask', + 1020 => 'AudioImporter', + 1026 => 'HierarchyState', + 1027 => 'GUIDSerializer', + 1028 => 'AssetMetaData', + 1029 => 'DefaultAsset', + 1030 => 'DefaultImporter', + 1031 => 'TextScriptImporter', + 1032 => 'SceneAsset', + 1034 => 'NativeFormatImporter', + 1035 => 'MonoImporter', + 1037 => 'AssetServerCache', + 1038 => 'LibraryAssetImporter', + 1040 => 'ModelImporter', + 1041 => 'FBXImporter', + 1042 => 'TrueTypeFontImporter', + 1044 => 'MovieImporter', + 1045 => 'EditorBuildSettings', + 1046 => 'DDSImporter', + 1048 => 'InspectorExpandedState', + 1049 => 'AnnotationManager', + 1050 => 'PluginImporter', + 1051 => 'EditorUserBuildSettings', + 1052 => 'PVRImporter', + 1053 => 'ASTCImporter', + 1054 => 'KTXImporter', + 1101 => 'AnimatorStateTransition', + 1102 => 'AnimatorState', + 1105 => 'HumanTemplate', + 1107 => 'AnimatorStateMachine', + 1108 => 'PreviewAssetType', + 1109 => 'AnimatorTransition', + 1110 => 'SpeedTreeImporter', + 1111 => 'AnimatorTransitionBase', + 1112 => 'SubstanceImporter', + 1113 => 'LightmapParameters', + 1120 => 'LightmapSnapshot' + }.freeze end diff --git a/lib/mikunyan/decoders.rb b/lib/mikunyan/decoders.rb index fe6acf5..0951069 100644 --- a/lib/mikunyan/decoders.rb +++ b/lib/mikunyan/decoders.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require 'mikunyan/decoders/image_decoder' module Mikunyan - # Module for helper classes for decoding object - module DecodeHelper - end + # Module for helper classes for decoding object + module DecodeHelper + end end diff --git a/lib/mikunyan/decoders/image_decoder.rb b/lib/mikunyan/decoders/image_decoder.rb index fb2949f..a86b83d 100644 --- a/lib/mikunyan/decoders/image_decoder.rb +++ b/lib/mikunyan/decoders/image_decoder.rb @@ -1,481 +1,483 @@ -begin; require 'oily_png'; rescue LoadError; require 'chunky_png'; end +# frozen_string_literal: true + +begin + require 'oily_png' +rescue LoadError + require 'chunky_png' +end require 'bin_utils' require 'mikunyan/decoders/native' module Mikunyan - # Class for image decoding tools - class ImageDecoder - # Decode image from Mikunyan::ObjectValue - # @param [Mikunyan::ObjectValue] object object to decode - # @return [ChunkyPNG::Image,nil] decoded image - def self.decode_object(object) - return nil unless object.class == ObjectValue + # Class for image decoding tools + class ImageDecoder + # Decode image from Mikunyan::ObjectValue + # @param [Mikunyan::ObjectValue] object object to decode + # @return [ChunkyPNG::Image,nil] decoded image + def self.decode_object(object) + return nil unless object.class == ObjectValue - endian = object.endian - width = object['m_Width'] - height = object['m_Height'] - bin = object['image data'] - fmt = object['m_TextureFormat'] - return nil unless width && height && bin && fmt + endian = object.endian + width = object['m_Width'] + height = object['m_Height'] + bin = object['image data'] + fmt = object['m_TextureFormat'] + return nil unless width && height && bin && fmt - width = width.value - height = height.value - bin = bin.value - fmt = fmt.value + width = width.value + height = height.value + bin = bin.value + fmt = fmt.value - if bin.size == 0 && object['m_StreamData'] - bin = object['m_StreamData'].value - return nil unless bin - end + if bin.empty? && object['m_StreamData'] + bin = object['m_StreamData'].value + return nil unless bin + end - case fmt - when 1 - decode_a8(width, height, bin) - when 2 - decode_argb4444(width, height, bin, endian) - when 3 - decode_rgb24(width, height, bin) - when 4 - decode_rgba32(width, height, bin) - when 5 - decode_argb32(width, height, bin) - when 7 - decode_rgb565(width, height, bin, endian) - when 9 - decode_r16(width, height, bin) - when 10 - decode_dxt1(width, height, bin) - when 12 - decode_dxt5(width, height, bin) - when 13 - decode_rgba4444(width, height, bin, endian) - when 14 - decode_bgra32(width, height, bin) - when 15 - decode_rhalf(width, height, bin, endian) - when 16 - decode_rghalf(width, height, bin, endian) - when 17 - decode_rgbahalf(width, height, bin, endian) - when 18 - decode_rfloat(width, height, bin, endian) - when 19 - decode_rgfloat(width, height, bin, endian) - when 20 - decode_rgbafloat(width, height, bin, endian) - when 22 - decode_rgb9e5float(width, height, bin, endian) - when 34 - decode_etc1(width, height, bin) - when 45 - decode_etc2rgb(width, height, bin) - when 46 - decode_etc2rgba1(width, height, bin) - when 47 - decode_etc2rgba8(width, height, bin) - when 48, 54 - decode_astc(width, height, 4, bin) - when 49, 55 - decode_astc(width, height, 5, bin) - when 50, 56 - decode_astc(width, height, 6, bin) - when 51, 57 - decode_astc(width, height, 8, bin) - when 52, 58 - decode_astc(width, height, 10, bin) - when 53, 59 - decode_astc(width, height, 12, bin) - when 62 - decode_rg16(width, height, bin) - when 63 - decode_r8(width, height, bin) - else - nil - end - end - - # Decode image from RGBA4444 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgba4444(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 4) - (width * height).times do |i| - c = endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2) - c = ((c & 0xf000) << 12) | ((c & 0x0f00) << 8) | ((c & 0x00f0) << 4) | (c & 0x000f) - BinUtils.append_int32_be!(mem, c << 4 | c) - end - ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip - end - - # Decode image from ARGB4444 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_argb4444(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 4) - (width * height).times do |i| - c = endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2) - c = ((c & 0x0f00) << 16) | ((c & 0x00f0) << 12) | ((c & 0x000f) << 8) | ((c & 0xf000) >> 12) - BinUtils.append_int32_be!(mem, c << 4 | c) - end - ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip - end - - # Decode image from RGB565 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgb565(width, height, bin, endian = :big) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_rgb565(bin, width * height, endian == :big)).flip - end - - # Decode image from A8 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_a8(width, height, bin) - mem = String.new(capacity: width * height * 3) - (width * height).times do |i| - c = BinUtils.get_int8(bin, i) - BinUtils.append_int8!(mem, c, c, c) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from R8 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_r8(width, height, bin) - decode_a8(width, height, bin) - end - - # Decode image from RG16 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_rg16(width, height, bin) - mem = String.new(capacity: width * height * 3) - (width * height).times do |i| - BinUtils.append_int16_int8_be!(mem, BinUtils.get_int16_be(bin, i*2), 0) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from RGB24 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgb24(width, height, bin) - ChunkyPNG::Image.from_rgb_stream(width, height, bin).flip - end - - # Decode image from RGBA32 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgba32(width, height, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, bin).flip - end - - # Decode image from ARGB32 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_argb32(width, height, bin) - mem = String.new(capacity: width * height * 4) - (width * height).times do |i| - c = BinUtils.get_int32_be(bin, i*4) - BinUtils.append_int32_be!(mem, ((c & 0x00ffffff) << 8) | ((c & 0xff000000) >> 24)) - end - ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip - end - - # Decode image from BGRA32 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_bgra32(width, height, bin) - mem = String.new(capacity: width * height * 4) - (width * height).times do |i| - c = BinUtils.get_int32_le(bin, i*4) - BinUtils.append_int32_be!(mem, ((c & 0x00ffffff) << 8) | ((c & 0xff000000) >> 24)) - end - ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip - end - - # Decode image from R16 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_r16(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 3) - (width * height).times do |i| - c = endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2) - c = f2i(r / 65535.0) - BinUtils.append_int8!(mem, c, c, c) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from RGB9e5 binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgb9e5float(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 3) - (width * height).times do |i| - n = endian == :little ? BinUtils.get_int32_le(bin, i*4) : BinUtils.get_int32_be(bin, i*4) - e = (n & 0xf8000000) >> 27 - r = (n & 0x7fc0000) >> 9 - g = (n & 0x3fe00) >> 9 - b = n & 0x1ff - r = (r / 512r + 1) * (2**(e-15)) - g = (g / 512r + 1) * (2**(e-15)) - b = (b / 512r + 1) * (2**(e-15)) - BinUtils.append_int8!(mem, f2i(r), f2i(g), f2i(b)) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from R Half-float binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rhalf(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 3) - (width * height).times do |i| - c = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2))) - BinUtils.append_int8!(mem, c, c, c) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from RG Half-float binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rghalf(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 3) - (width * height).times do |i| - r = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*4) : BinUtils.get_int16_be(bin, i*4))) - g = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*4+2) : BinUtils.get_int16_be(bin, i*4+2))) - BinUtils.append_int8!(mem, r, g, 0) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from RGBA Half-float binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgbahalf(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 4) - (width * height).times do |i| - r = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8) : BinUtils.get_int16_be(bin, i*8))) - g = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8+2) : BinUtils.get_int16_be(bin, i*8+2))) - b = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8+4) : BinUtils.get_int16_be(bin, i*8+4))) - a = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8+6) : BinUtils.get_int16_be(bin, i*8+6))) - BinUtils.append_int8!(mem, r, g, b, a) - end - ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip - end - - # Decode image from R float binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rfloat(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 3) - unpackstr = endian == :little ? 'e' : 'g' - (width * height).times do |i| - c = f2i(bin.byteslice(i*4, 4).unpack(unpackstr)[0]) - BinUtils.append_int8!(mem, c, c, c) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from RG float binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgfloat(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 3) - unpackstr = endian == :little ? 'e2' : 'g2' - (width * height).times do |i| - r, g = bin.byteslice(i*8, 8).unpack(unpackstr) - BinUtils.append_int8!(mem, f2i(r), f2i(g), 0) - end - ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip - end - - # Decode image from RGBA float binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @param [Symbol] endian endianness of binary - # @return [ChunkyPNG::Image] decoded image - def self.decode_rgbafloat(width, height, bin, endian = :big) - mem = String.new(capacity: width * height * 4) - unpackstr = endian == :little ? 'e4' : 'g4' - (width * height).times do |i| - r, g, b, a = bin.byteslice(i*16, 16).unpack(unpackstr) - BinUtils.append_int8!(mem, f2i(r), f2i(g), f2i(b), f2i(a)) - end - ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip - end - - # Decode image from DXT1 compressed binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_dxt1(width, height, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_dxt1(bin, width, height)) - end - - # Decode image from DXT5 compressed binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_dxt5(width, height, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_dxt5(bin, width, height)) - end - - # Decode image from ETC1 compressed binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_etc1(width, height, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc1(bin, width, height)) - end - - # Decode image from ETC2 compressed binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_etc2rgb(width, height, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc2(bin, width, height)) - end - - # Decode image from ETC2 Alpha1 compressed binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_etc2rgba1(width, height, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc2a1(bin, width, height)) - end - - # Decode image from ETC2 Alpha8 compressed binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_etc2rgba8(width, height, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc2a8(bin, width, height)) - end - - # Decode image from ASTC compressed binary - # @param [Integer] width image width - # @param [Integer] height image height - # @param [Integer] blocksize block size - # @param [String] bin binary to decode - # @return [ChunkyPNG::Image] decoded image - def self.decode_astc(width, height, blocksize, bin) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_astc(bin, width, height, blocksize, blocksize)) - end - - # Create ASTC file data from ObjectValue - # @param [Mikunyan::ObjectValue,Hash] object target object - # @return [String,nil] created file - def self.create_astc_file(object) - astc_list = { - 48 => 4, 49 => 5, 50 => 6, 51 => 8, 52 => 10, 53 => 12, - 54 => 4, 55 => 5, 56 => 6, 57 => 8, 58 => 10, 59 => 12 - } - width = object['m_Width'] - height = object['m_Height'] - fmt = object['m_TextureFormat'] - bin = object['image data'] - width = width.value if width.class == ObjectValue - height = height.value if height.class == ObjectValue - fmt = fmt.value if fmt.class == ObjectValue - bin = bin.value if bin.class == ObjectValue - if width && height && fmt && astc_list[fmt] - header = "\x13\xAB\xA1\x5C".force_encoding('ascii-8bit') - header << [astc_list[fmt], astc_list[fmt], 1].pack("C*") - header << [width].pack("V").byteslice(0, 3) - header << [height].pack("V").byteslice(0, 3) - header << "\x01\x00\x00" - header + bin - else - nil - end - end - - private - - # convert 16bit float - def self.n2f(n) - case n - when 0x0000 - 0.0 - when 0x8000 - -0.0 - when 0x7c00 - Float::INFINITY - when 0xfc00 - -Float::INFINITY - else - s = n & 0x8000 != 0 - e = n & 0x7c00 - f = n & 0x03ff - case e - when 0x7c00 - Float::NAN - when 0 - (s ? -f : f) * 2.0**-24 - else - (s ? -1 : 1) * (f / 1024.0 + 1) * (2.0 ** ((e >> 10)-15)) - end - end - end - - # [0.0,1.0] -> [0,255] - def self.f2i(d) - (d * 255).round.clamp(0, 255) - end + case fmt + when 1 + decode_a8(width, height, bin) + when 2 + decode_argb4444(width, height, bin, endian) + when 3 + decode_rgb24(width, height, bin) + when 4 + decode_rgba32(width, height, bin) + when 5 + decode_argb32(width, height, bin) + when 7 + decode_rgb565(width, height, bin, endian) + when 9 + decode_r16(width, height, bin) + when 10 + decode_dxt1(width, height, bin) + when 12 + decode_dxt5(width, height, bin) + when 13 + decode_rgba4444(width, height, bin, endian) + when 14 + decode_bgra32(width, height, bin) + when 15 + decode_rhalf(width, height, bin, endian) + when 16 + decode_rghalf(width, height, bin, endian) + when 17 + decode_rgbahalf(width, height, bin, endian) + when 18 + decode_rfloat(width, height, bin, endian) + when 19 + decode_rgfloat(width, height, bin, endian) + when 20 + decode_rgbafloat(width, height, bin, endian) + when 22 + decode_rgb9e5float(width, height, bin, endian) + when 34 + decode_etc1(width, height, bin) + when 45 + decode_etc2rgb(width, height, bin) + when 46 + decode_etc2rgba1(width, height, bin) + when 47 + decode_etc2rgba8(width, height, bin) + when 48, 54 + decode_astc(width, height, 4, bin) + when 49, 55 + decode_astc(width, height, 5, bin) + when 50, 56 + decode_astc(width, height, 6, bin) + when 51, 57 + decode_astc(width, height, 8, bin) + when 52, 58 + decode_astc(width, height, 10, bin) + when 53, 59 + decode_astc(width, height, 12, bin) + when 62 + decode_rg16(width, height, bin) + when 63 + decode_r8(width, height, bin) + end end + + # Decode image from RGBA4444 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgba4444(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 4) + (width * height).times do |i| + c = endian == :little ? BinUtils.get_int16_le(bin, i * 2) : BinUtils.get_int16_be(bin, i * 2) + c = ((c & 0xf000) << 12) | ((c & 0x0f00) << 8) | ((c & 0x00f0) << 4) | (c & 0x000f) + BinUtils.append_int32_be!(mem, c << 4 | c) + end + ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip + end + + # Decode image from ARGB4444 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_argb4444(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 4) + (width * height).times do |i| + c = endian == :little ? BinUtils.get_int16_le(bin, i * 2) : BinUtils.get_int16_be(bin, i * 2) + c = ((c & 0x0f00) << 16) | ((c & 0x00f0) << 12) | ((c & 0x000f) << 8) | ((c & 0xf000) >> 12) + BinUtils.append_int32_be!(mem, c << 4 | c) + end + ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip + end + + # Decode image from RGB565 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgb565(width, height, bin, endian = :big) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_rgb565(bin, width * height, endian == :big)).flip + end + + # Decode image from A8 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_a8(width, height, bin) + mem = String.new(capacity: width * height * 3) + (width * height).times do |i| + c = BinUtils.get_int8(bin, i) + BinUtils.append_int8!(mem, c, c, c) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from R8 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_r8(width, height, bin) + decode_a8(width, height, bin) + end + + # Decode image from RG16 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_rg16(width, height, bin) + mem = String.new(capacity: width * height * 3) + (width * height).times do |i| + BinUtils.append_int16_int8_be!(mem, BinUtils.get_int16_be(bin, i * 2), 0) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from RGB24 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgb24(width, height, bin) + ChunkyPNG::Image.from_rgb_stream(width, height, bin).flip + end + + # Decode image from RGBA32 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgba32(width, height, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, bin).flip + end + + # Decode image from ARGB32 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_argb32(width, height, bin) + mem = String.new(capacity: width * height * 4) + (width * height).times do |i| + c = BinUtils.get_int32_be(bin, i * 4) + BinUtils.append_int32_be!(mem, ((c & 0x00ffffff) << 8) | ((c & 0xff000000) >> 24)) + end + ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip + end + + # Decode image from BGRA32 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_bgra32(width, height, bin) + mem = String.new(capacity: width * height * 4) + (width * height).times do |i| + c = BinUtils.get_int32_le(bin, i * 4) + BinUtils.append_int32_be!(mem, ((c & 0x00ffffff) << 8) | ((c & 0xff000000) >> 24)) + end + ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip + end + + # Decode image from R16 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_r16(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 3) + (width * height).times do |i| + c = endian == :little ? BinUtils.get_int16_le(bin, i * 2) : BinUtils.get_int16_be(bin, i * 2) + c = f2i(r / 65535.0) + BinUtils.append_int8!(mem, c, c, c) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from RGB9e5 binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgb9e5float(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 3) + (width * height).times do |i| + n = endian == :little ? BinUtils.get_int32_le(bin, i * 4) : BinUtils.get_int32_be(bin, i * 4) + e = (n & 0xf8000000) >> 27 + r = (n & 0x7fc0000) >> 9 + g = (n & 0x3fe00) >> 9 + b = n & 0x1ff + r = (r / 512r + 1) * (2**(e - 15)) + g = (g / 512r + 1) * (2**(e - 15)) + b = (b / 512r + 1) * (2**(e - 15)) + BinUtils.append_int8!(mem, f2i(r), f2i(g), f2i(b)) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from R Half-float binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rhalf(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 3) + (width * height).times do |i| + c = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i * 2) : BinUtils.get_int16_be(bin, i * 2))) + BinUtils.append_int8!(mem, c, c, c) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from RG Half-float binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rghalf(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 3) + (width * height).times do |i| + r = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i * 4) : BinUtils.get_int16_be(bin, i * 4))) + g = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i * 4 + 2) : BinUtils.get_int16_be(bin, i * 4 + 2))) + BinUtils.append_int8!(mem, r, g, 0) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from RGBA Half-float binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgbahalf(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 4) + (width * height).times do |i| + r = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i * 8) : BinUtils.get_int16_be(bin, i * 8))) + g = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i * 8 + 2) : BinUtils.get_int16_be(bin, i * 8 + 2))) + b = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i * 8 + 4) : BinUtils.get_int16_be(bin, i * 8 + 4))) + a = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i * 8 + 6) : BinUtils.get_int16_be(bin, i * 8 + 6))) + BinUtils.append_int8!(mem, r, g, b, a) + end + ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip + end + + # Decode image from R float binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rfloat(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 3) + unpackstr = endian == :little ? 'e' : 'g' + (width * height).times do |i| + c = f2i(bin.byteslice(i * 4, 4).unpack1(unpackstr)) + BinUtils.append_int8!(mem, c, c, c) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from RG float binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgfloat(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 3) + unpackstr = endian == :little ? 'e2' : 'g2' + (width * height).times do |i| + r, g = bin.byteslice(i * 8, 8).unpack(unpackstr) + BinUtils.append_int8!(mem, f2i(r), f2i(g), 0) + end + ChunkyPNG::Image.from_rgb_stream(width, height, mem).flip + end + + # Decode image from RGBA float binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @param [Symbol] endian endianness of binary + # @return [ChunkyPNG::Image] decoded image + def self.decode_rgbafloat(width, height, bin, endian = :big) + mem = String.new(capacity: width * height * 4) + unpackstr = endian == :little ? 'e4' : 'g4' + (width * height).times do |i| + r, g, b, a = bin.byteslice(i * 16, 16).unpack(unpackstr) + BinUtils.append_int8!(mem, f2i(r), f2i(g), f2i(b), f2i(a)) + end + ChunkyPNG::Image.from_rgba_stream(width, height, mem).flip + end + + # Decode image from DXT1 compressed binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_dxt1(width, height, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_dxt1(bin, width, height)) + end + + # Decode image from DXT5 compressed binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_dxt5(width, height, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_dxt5(bin, width, height)) + end + + # Decode image from ETC1 compressed binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_etc1(width, height, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc1(bin, width, height)) + end + + # Decode image from ETC2 compressed binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_etc2rgb(width, height, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc2(bin, width, height)) + end + + # Decode image from ETC2 Alpha1 compressed binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_etc2rgba1(width, height, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc2a1(bin, width, height)) + end + + # Decode image from ETC2 Alpha8 compressed binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_etc2rgba8(width, height, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_etc2a8(bin, width, height)) + end + + # Decode image from ASTC compressed binary + # @param [Integer] width image width + # @param [Integer] height image height + # @param [Integer] blocksize block size + # @param [String] bin binary to decode + # @return [ChunkyPNG::Image] decoded image + def self.decode_astc(width, height, blocksize, bin) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_astc(bin, width, height, blocksize, blocksize)) + end + + # Create ASTC file data from ObjectValue + # @param [Mikunyan::ObjectValue,Hash] object target object + # @return [String,nil] created file + def self.create_astc_file(object) + astc_list = { + 48 => 4, 49 => 5, 50 => 6, 51 => 8, 52 => 10, 53 => 12, + 54 => 4, 55 => 5, 56 => 6, 57 => 8, 58 => 10, 59 => 12 + } + width = object['m_Width'] + height = object['m_Height'] + fmt = object['m_TextureFormat'] + bin = object['image data'] + width = width.value if width.class == ObjectValue + height = height.value if height.class == ObjectValue + fmt = fmt.value if fmt.class == ObjectValue + bin = bin.value if bin.class == ObjectValue + if width && height && fmt && astc_list[fmt] + header = "\x13\xAB\xA1\x5C".force_encoding('ascii-8bit') + header << [astc_list[fmt], astc_list[fmt], 1].pack('C*') + header << [width].pack('V').byteslice(0, 3) + header << [height].pack('V').byteslice(0, 3) + header << "\x01\x00\x00" + header + bin + end + end + + private + + # convert 16bit float + def self.n2f(n) + case n + when 0x0000 + 0.0 + when 0x8000 + -0.0 + when 0x7c00 + Float::INFINITY + when 0xfc00 + -Float::INFINITY + else + s = n & 0x8000 != 0 + e = n & 0x7c00 + f = n & 0x03ff + case e + when 0x7c00 + Float::NAN + when 0 + (s ? -f : f) * 2.0**-24 + else + (s ? -1 : 1) * (f / 1024.0 + 1) * (2.0**((e >> 10) - 15)) + end + end + end + + # [0.0,1.0] -> [0,255] + def self.f2i(d) + (d * 255).round.clamp(0, 255) + end + end end diff --git a/lib/mikunyan/object_value.rb b/lib/mikunyan/object_value.rb index a65ae55..7a1043d 100644 --- a/lib/mikunyan/object_value.rb +++ b/lib/mikunyan/object_value.rb @@ -1,93 +1,95 @@ +# frozen_string_literal: true + module Mikunyan - # Class for representing decoded object - # @attr [String] name object name - # @attr [String] type object type name - # @attr [Object] value object - # @attr [Symbol] endian endianness - # @attr [Boolean] is_struct - class ObjectValue - attr_accessor :name, :type, :value, :endian, :is_struct + # Class for representing decoded object + # @attr [String] name object name + # @attr [String] type object type name + # @attr [Object] value object + # @attr [Symbol] endian endianness + # @attr [Boolean] is_struct + class ObjectValue + attr_accessor :name, :type, :value, :endian, :is_struct - # Constructor - # @param [String] name object name - # @param [String] type object type name - # @param [Symbol] endian endianness - # @param [Object] value object - def initialize(name, type, endian, value = nil) - @name = name - @type = type - @endian = endian - @value = value - @is_struct = false - @attr = {} - end - - # Return whether object is array or not - # @return [Boolean] - def array? - value && value.class == Array - end - - # Return whether object is value or not - # @return [Boolean] - def value? - value && value.class != Array - end - - # Return whether object is struct or not - # @return [Boolean] - def struct? - is_struct - end - - # Return all keys - # @return [Array] list of keys - def keys - @attr.keys - end - - # Return whether object contains key - # @param [String] key - # @return [Boolean] - def key?(key) - @attr.key?(key) - end - - # Return value - # @return [Object] value - def [] - @value - end - - # Return value of selected index or key - # @param [Integer,String] i index or key - # @return [Object] value - def [](i) - if array? && i.class == Integer - @value[i] - else - @attr[i] - end - end - - # Set value of selected key - # @param [String] name key - # @param [Object] value value - # @return [Object] value - def []=(name, value) - @attr[name] = value - end - - # Return value of called key - # @param [String] name key - # @return [Object] value - def method_missing(name, *args) - @attr[name.to_s] - end - - # Implementation of respond_to_missing? - def respond_to_missing?(symbol, include_private) - @attr.key?(symbol.to_s) - end + # Constructor + # @param [String] name object name + # @param [String] type object type name + # @param [Symbol] endian endianness + # @param [Object] value object + def initialize(name, type, endian, value = nil) + @name = name + @type = type + @endian = endian + @value = value + @is_struct = false + @attr = {} end + + # Return whether object is array or not + # @return [Boolean] + def array? + value && value.class == Array + end + + # Return whether object is value or not + # @return [Boolean] + def value? + value && value.class != Array + end + + # Return whether object is struct or not + # @return [Boolean] + def struct? + is_struct + end + + # Return all keys + # @return [Array] list of keys + def keys + @attr.keys + end + + # Return whether object contains key + # @param [String] key + # @return [Boolean] + def key?(key) + @attr.key?(key) + end + + # Return value + # @return [Object] value + def [] + @value + end + + # Return value of selected index or key + # @param [Integer,String] i index or key + # @return [Object] value + def [](i) + if array? && i.class == Integer + @value[i] + else + @attr[i] + end + end + + # Set value of selected key + # @param [String] name key + # @param [Object] value value + # @return [Object] value + def []=(name, value) + @attr[name] = value + end + + # Return value of called key + # @param [String] name key + # @return [Object] value + def method_missing(name, *_args) + @attr[name.to_s] + end + + # Implementation of respond_to_missing? + def respond_to_missing?(symbol, _include_private) + @attr.key?(symbol.to_s) + end + end end diff --git a/lib/mikunyan/type_tree.rb b/lib/mikunyan/type_tree.rb index d0ad4ad..15cd123 100644 --- a/lib/mikunyan/type_tree.rb +++ b/lib/mikunyan/type_tree.rb @@ -1,82 +1,84 @@ +# frozen_string_literal: true + module Mikunyan - # Class for representing TypeTree - # @attr [Array] nodes list of all nodes - class TypeTree - attr_accessor :nodes + # Class for representing TypeTree + # @attr [Array] nodes list of all nodes + class TypeTree + attr_accessor :nodes - # Struct for representing Node in TypeTree - # @attr [String] version version string - # @attr [Integer] depth depth of node (>= 0) - # @attr [Boolean] array? array node or not - # @attr [String] type type name - # @attr [String] name node (attribute) name - # @attr [Integer] index index in node list - # @attr [Integer] flags flags of node - Node = Struct.new(:version, :depth, :array?, :type, :name, :size, :index, :flags) + # Struct for representing Node in TypeTree + # @attr [String] version version string + # @attr [Integer] depth depth of node (>= 0) + # @attr [Boolean] array? array node or not + # @attr [String] type type name + # @attr [String] name node (attribute) name + # @attr [Integer] index index in node list + # @attr [Integer] flags flags of node + Node = Struct.new(:version, :depth, :array?, :type, :name, :size, :index, :flags) - # Create TypeTree from binary string (new version) - # @param [Mikunyan::BinaryReader] br - # @return [Mikunyan::TypeTree] created TypeTree - def self.load(br) - nodes = [] - node_count = br.i32u - buffer_size = br.i32u - node_count.times do - node = Node.new(br.i16u, br.i8u, br.i8u != 0, br.i32, br.i32, br.i32, br.i32u, br.i32u) - nodes << node - end - buffer = br.read(buffer_size) - nodes.each do |n| - if n.type >= 0 - n.type = buffer.unpack("@#{n.type}Z*")[0] - else - n.type = Mikunyan::STRING_TABLE[n.type + 2**31] - end - if n.name >= 0 - n.name = buffer.unpack("@#{n.name}Z*")[0] - else - n.name = Mikunyan::STRING_TABLE[n.name + 2**31] - end - end - r = TypeTree.new - r.nodes = nodes - r - end - - # Create TypeTree from binary string (legacy version) - # @param [Mikunyan::BinaryReader] br - # @return [Mikunyan::TypeTree] created TypeTree - def self.load_legacy(br) - nodes = [] - stack = [0] - while stack.size > 0 - depth = stack.pop - type = br.cstr - name = br.cstr - size = br.i32 - index = br.i32u - is_array = (br.i32 != 0) - version = br.i32u - flags = br.i32u - child_count = br.i32u - child_count.times{ stack << depth + 1 } - nodes << Node.new(version, depth, is_array, type, name, size, index, flags) - end - r = TypeTree.new - r.nodes = nodes - r - end - - # Create default TypeTree from hash string (if exists) - # @param [String] hash - # @return [Mikunyan::TypeTree,nil] created TypeTree - def self.load_default(hash) - hash_str = hash.unpack('H*')[0] - file = File.expand_path("../typetrees/#{hash_str}.dat", __FILE__) - return nil unless File.file?(file) - r = TypeTree.new - r.nodes = Marshal.load(File.binread(file)) - r - end + # Create TypeTree from binary string (new version) + # @param [Mikunyan::BinaryReader] br + # @return [Mikunyan::TypeTree] created TypeTree + def self.load(br) + nodes = [] + node_count = br.i32u + buffer_size = br.i32u + node_count.times do + node = Node.new(br.i16u, br.i8u, br.i8u != 0, br.i32, br.i32, br.i32, br.i32u, br.i32u) + nodes << node + end + buffer = br.read(buffer_size) + nodes.each do |n| + n.type = if n.type >= 0 + buffer.unpack1("@#{n.type}Z*") + else + Mikunyan::STRING_TABLE[n.type + 2**31] + end + n.name = if n.name >= 0 + buffer.unpack1("@#{n.name}Z*") + else + Mikunyan::STRING_TABLE[n.name + 2**31] + end + end + r = TypeTree.new + r.nodes = nodes + r end + + # Create TypeTree from binary string (legacy version) + # @param [Mikunyan::BinaryReader] br + # @return [Mikunyan::TypeTree] created TypeTree + def self.load_legacy(br) + nodes = [] + stack = [0] + until stack.empty? + depth = stack.pop + type = br.cstr + name = br.cstr + size = br.i32 + index = br.i32u + is_array = (br.i32 != 0) + version = br.i32u + flags = br.i32u + child_count = br.i32u + child_count.times{stack << depth + 1} + nodes << Node.new(version, depth, is_array, type, name, size, index, flags) + end + r = TypeTree.new + r.nodes = nodes + r + end + + # Create default TypeTree from hash string (if exists) + # @param [String] hash + # @return [Mikunyan::TypeTree,nil] created TypeTree + def self.load_default(hash) + hash_str = hash.unpack1('H*') + file = File.expand_path("../typetrees/#{hash_str}.dat", __FILE__) + return nil unless File.file?(file) + r = TypeTree.new + r.nodes = Marshal.load(File.binread(file)) + r + end + end end diff --git a/lib/mikunyan/version.rb b/lib/mikunyan/version.rb index 6692d73..8449809 100644 --- a/lib/mikunyan/version.rb +++ b/lib/mikunyan/version.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + module Mikunyan - # version string - VERSION = "3.9.6" + # version string + VERSION = '3.9.6' end diff --git a/mikunyan.gemspec b/mikunyan.gemspec index 68ce8fb..e9c4693 100644 --- a/mikunyan.gemspec +++ b/mikunyan.gemspec @@ -1,31 +1,32 @@ -# coding: utf-8 -lib = File.expand_path("../lib", __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require "mikunyan/version" +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) +$:.unshift(lib) unless $:.include?(lib) +require 'mikunyan/version' Gem::Specification.new do |spec| - spec.name = "mikunyan" + spec.name = 'mikunyan' spec.version = Mikunyan::VERSION - spec.authors = ["Ishotihadus"] - spec.email = ["hanachan.pao@gmail.com"] + spec.authors = ['Ishotihadus'] + spec.email = ['hanachan.pao@gmail.com'] - spec.summary = "Unity asset deserializer for Ruby" - spec.description = "Library to deserialize Unity assetbundles and assets." - spec.homepage = "https://github.com/Ishotihadus/mikunyan" - spec.license = "MIT" + spec.summary = 'Unity asset deserializer for Ruby' + spec.description = 'Library to deserialize Unity assetbundles and assets.' + spec.homepage = 'https://github.com/Ishotihadus/mikunyan' + spec.license = 'MIT' spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] - spec.extensions = ["ext/decoders/native/extconf.rb"] + spec.bindir = 'exe' + spec.executables = spec.files.grep(%r{^exe/}){|f| File.basename(f)} + spec.require_paths = ['lib'] + spec.extensions = ['ext/decoders/native/extconf.rb'] - spec.add_dependency 'extlz4', '~> 0' - spec.add_dependency 'extlzma', '~> 0' spec.add_dependency 'bin_utils', '~> 0' spec.add_dependency 'chunky_png', '~> 1' + spec.add_dependency 'extlz4', '~> 0' + spec.add_dependency 'extlzma', '~> 0' spec.add_dependency 'json', '~> 2' spec.add_development_dependency 'bundler', '~> 1'