diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..b0c2bfd --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,42 @@ +AllCops: + TargetRubyVersion: 2.6 + +Metrics/AbcSize: + Max: 170 + +Metrics/BlockLength: + Max: 250 + +Metrics/ClassLength: + Max: 1000 + +Metrics/CyclomaticComplexity: + Max: 70 + +Metrics/MethodLength: + Max: 100 + +Metrics/ModuleLength: + Max: 1000 + +Metrics/ParameterLists: + Max: 50 + +Metrics/PerceivedComplexity: + Max: 80 + +Layout/SpaceInsideBlockBraces: + EnforcedStyle: no_space + SpaceBeforeBlockParameters: false + +Style/Documentation: + Enabled: false + +Style/NumericPredicate: + EnforcedStyle: comparison + +Style/MultilineBlockChain: + Enabled: false + +Style/SpecialGlobalVars: + EnforcedStyle: use_perl_names diff --git a/lib/mikunyan/asset.rb b/lib/mikunyan/asset.rb index f8c0839..1c673fb 100644 --- a/lib/mikunyan/asset.rb +++ b/lib/mikunyan/asset.rb @@ -120,8 +120,10 @@ module Mikunyan def containers obj = @path_id_table[1] return nil unless obj.klass&.type_tree&.tree&.type == 'AssetBundle' + parse_object(obj).m_Container.value.map do |e| - ContainerInfo.new(e.first.value, e.second.preloadIndex.value, e.second.preloadSize.value, e.second.asset.m_FileID.value, e.second.asset.m_PathID.value) + ContainerInfo.new(e.first.value, e.second.preloadIndex.value, e.second.preloadSize.value, + e.second.asset.m_FileID.value, e.second.asset.m_PathID.value) end end @@ -129,8 +131,9 @@ module Mikunyan # @param [Integer,ObjectEntry] obj path ID or object # @return [Mikunyan::BaseObject,nil] parsed object def parse_object(obj) - obj = @path_id_table[obj] if obj.class == Integer + obj = @path_id_table[obj] if obj.instance_of?(Integer) return nil unless obj.klass&.type_tree + value_klass = Mikunyan::CustomTypes.get_custom_type(obj.klass.type_tree.tree.type, obj.class_id) ret = parse_object_private(BinaryReader.new(obj.data, @endian), obj.klass.type_tree.tree, value_klass) ret.object_entry = obj @@ -148,7 +151,7 @@ module Mikunyan # @param [Integer,ObjectEntry] obj path ID or object # @return [String,nil] type name def object_type(obj) - obj = @path_id_table[obj] if obj.class == Integer + obj = @path_id_table[obj] if obj.instance_of?(Integer) obj&.type end @@ -226,7 +229,7 @@ module Mikunyan end end - @path_id_table = @objects.map{|e| [e.path_id, e]}.to_h + @path_id_table = @objects.map {|e| [e.path_id, e]}.to_h if @format >= 11 add_id_count = br.i32u @@ -238,7 +241,8 @@ module Mikunyan reference_count = br.i32u @references = Array.new(reference_count) do - Reference.new(@format >= 6 ? br.cstr : nil, @format >= 5 ? br.read(16) : nil, @format >= 5 ? br.i32s : nil, br.cstr) + Reference.new(@format >= 6 ? br.cstr : nil, @format >= 5 ? br.read(16) : nil, @format >= 5 ? br.i32s : nil, + br.cstr) end @comment = br.cstr if @format >= 5 @@ -247,7 +251,11 @@ module Mikunyan @objects.each do |e| br.jmp(data_offset + e.offset) e.data = br.read(e.size) - e.klass = e.class_idx ? @klasses[e.class_idx] : @klasses.find{|e2| e2.class_id == e.class_id} || @klasses.find{|e2| e2.class_id == e.type_id} + e.klass = if e.class_idx + @klasses[e.class_idx] + else + @klasses.find {|e2| e2.class_id == e.class_id} || @klasses.find {|e2| e2.class_id == e.type_id} + end end end @@ -290,6 +298,7 @@ module Mikunyan elsif node.array? children.each do |child| next ret[child.name] = parse_object_private(br, child) unless child.name == 'data' + size = ret['size']&.value || raise('`size` node must appear before `data` node in array node') ret.value = if child.children.empty? && (!child.need_align? || br.pos % 4 == 0 && child.size % 4 == 0) @@ -300,7 +309,7 @@ module Mikunyan br.read(size * child.size).force_encoding('utf-8') end end - ret.value ||= Array.new(size){parse_object_private(br, child)} + ret.value ||= Array.new(size) {parse_object_private(br, child)} ret['data'] = ret.value end elsif children.size == 1 && children[0].array? && children[0].type == 'Array' && children[0].name == 'Array' @@ -308,7 +317,7 @@ module Mikunyan ret.name = node.name ret.type = node.type else - ret.attr = children.map{|c| [c.name, parse_object_private(br, c)]}.to_h + ret.attr = children.map {|c| [c.name, parse_object_private(br, c)]}.to_h if node.type == 'StreamingInfo' ret.value = get_stream_blob(ret['path'].value, ret['offset'].value, ret['size'].value) else @@ -322,6 +331,7 @@ module Mikunyan def get_stream_blob(path, offset, size) return nil unless path && @bundle return nil if path.empty? + path["archive:/#{@name}/"] = '' if path.start_with?("archive:/#{@name}/") @bundle.blobs[path]&.byteslice(offset, size) end diff --git a/lib/mikunyan/asset_bundle.rb b/lib/mikunyan/asset_bundle.rb index 21428d5..71912f1 100644 --- a/lib/mikunyan/asset_bundle.rb +++ b/lib/mikunyan/asset_bundle.rb @@ -21,7 +21,7 @@ module Mikunyan # @param [String,Integer] index # @return [Mikunyan::Asset,nil] def [](index) - index.is_a?(String) ? @assets.find{|e| e.name == index} : @assets[index] + index.is_a?(String) ? @assets.find {|e| e.name == index} : @assets[index] end # Same as assets.each @@ -94,7 +94,8 @@ module Mikunyan br.align(16) if @format >= 7 - 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)) + head_bin = flags & 0x80 == 0 ? br.read(ci_block_size) : br.read_abs(ci_block_size, file_size - ci_block_size) + head = BinaryReader.new(uncompress(head_bin, ui_block_size, flags)) @guid = head.read(16) block_count = head.i32u @@ -116,7 +117,7 @@ module Mikunyan end def process_asset_entries(asset_entries) - @blobs = asset_entries.select(&:blob?).map{|e| [e.name, e.data]}.to_h + @blobs = asset_entries.select(&:blob?).map {|e| [e.name, e.data]}.to_h @assets = asset_entries.reject(&:blob?).map do |e| Asset.load(e.data, e.name, self) end diff --git a/lib/mikunyan/binary_reader.rb b/lib/mikunyan/binary_reader.rb index a3fc223..fa71301 100644 --- a/lib/mikunyan/binary_reader.rb +++ b/lib/mikunyan/binary_reader.rb @@ -56,6 +56,7 @@ module Mikunyan def read(size) ret = @io.read(size) raise EOFError if ret.nil? || size && ret.bytesize < size + ret end @@ -75,6 +76,7 @@ module Mikunyan # @return [String] string def cstr raise EOFError if @io.eof? + @io.each_byte.take_while(&:nonzero?).pack('C*') end diff --git a/lib/mikunyan/constants.rb b/lib/mikunyan/constants.rb index 1d47c38..11df78b 100644 --- a/lib/mikunyan/constants.rb +++ b/lib/mikunyan/constants.rb @@ -416,91 +416,91 @@ module Mikunyan [1126, 'PackedAssets'], [1127, 'VideoClipImporter'], [2000, 'ActivationLogComponent'], - [100000, 'int'], - [100001, 'bool'], - [100002, 'float'], - [100003, 'MonoObject'], - [100004, 'Collision'], - [100005, 'Vector3f'], - [100006, 'RootMotionData'], - [100007, 'Collision2D'], - [100008, 'AudioMixerLiveUpdateFloat'], - [100009, 'AudioMixerLiveUpdateBool'], - [100010, 'Polygon2D'], - [100011, 'void'], - [19719996, 'TilemapCollider2D'], - [41386430, 'AssetImporterLog'], - [73398921, 'VFXRenderer'], - [76251197, 'SerializableManagedRefTestClass'], - [156049354, 'Grid'], - [181963792, 'Preset'], - [277625683, 'EmptyObject'], - [285090594, 'IConstraint'], - [293259124, 'TestObjectWithSpecialLayoutOne'], - [294290339, 'AssemblyDefinitionReferenceImporter'], - [334799969, 'SiblingDerived'], - [342846651, 'TestObjectWithSerializedMapStringNonAlignedStruct'], - [367388927, 'SubDerived'], - [369655926, 'AssetImportInProgressProxy'], - [382020655, 'PluginBuildInfo'], - [426301858, 'EditorProjectAccess'], - [468431735, 'PrefabImporter'], - [478637458, 'TestObjectWithSerializedArray'], - [478637459, 'TestObjectWithSerializedAnimationCurve'], - [483693784, 'TilemapRenderer'], - [638013454, 'SpriteAtlasDatabase'], - [641289076, 'AudioBuildInfo'], - [644342135, 'CachedSpriteAtlasRuntimeData'], - [646504946, 'RendererFake'], - [662584278, 'AssemblyDefinitionReferenceAsset'], - [668709126, 'BuiltAssetBundleInfoSet'], - [687078895, 'SpriteAtlas'], - [877146078, 'PlatformModuleSetup'], - [895512359, 'AimConstraint'], - [937362698, 'VFXManager'], - [994735392, 'VisualEffectSubgraph'], - [994735403, 'VisualEffectSubgraphOperator'], - [994735404, 'VisualEffectSubgraphBlock'], - [1001480554, 'Prefab'], - [1027052791, 'LocalizationImporter'], - [1091556383, 'Derived'], - [1111377672, 'PropertyModificationsTargetTestObject'], - [1114811875, 'ReferencesArtifactGenerator'], - [1152215463, 'AssemblyDefinitionAsset'], - [1154873562, 'SceneVisibilityState'], - [1183024399, 'LookAtConstraint'], - [1223240404, 'MultiArtifactTestImporter'], - [1268269756, 'GameObjectRecorder'], - [1325145578, 'LightingDataAssetParent'], - [1386491679, 'PresetManager'], - [1392443030, 'TestObjectWithSpecialLayoutTwo'], - [1403656975, 'StreamingManager'], - [1480428607, 'LowerResBlitTexture'], - [1542919678, 'StreamingController'], - [1571458007, 'RenderPassAttachment'], - [1628831178, 'TestObjectVectorPairStringBool'], - [1742807556, 'GridLayout'], - [1766753193, 'AssemblyDefinitionImporter'], - [1773428102, 'ParentConstraint'], - [1803986026, 'FakeComponent'], - [1818360608, 'PositionConstraint'], - [1818360609, 'RotationConstraint'], - [1818360610, 'ScaleConstraint'], - [1839735485, 'Tilemap'], - [1896753125, 'PackageManifest'], - [1896753126, 'PackageManifestImporter'], - [1953259897, 'TerrainLayer'], - [1971053207, 'SpriteShapeRenderer'], - [1977754360, 'NativeObjectType'], - [1981279845, 'TestObjectWithSerializedMapStringBool'], - [1995898324, 'SerializableManagedHost'], - [2058629509, 'VisualEffectAsset'], - [2058629510, 'VisualEffectImporter'], - [2058629511, 'VisualEffectResource'], - [2059678085, 'VisualEffectObject'], - [2083052967, 'VisualEffect'], - [2083778819, 'LocalizationAsset'], - [2089858483, 'ScriptedImporter'] + [100_000, 'int'], + [100_001, 'bool'], + [100_002, 'float'], + [100_003, 'MonoObject'], + [100_004, 'Collision'], + [100_005, 'Vector3f'], + [100_006, 'RootMotionData'], + [100_007, 'Collision2D'], + [100_008, 'AudioMixerLiveUpdateFloat'], + [100_009, 'AudioMixerLiveUpdateBool'], + [100_010, 'Polygon2D'], + [100_011, 'void'], + [19_719_996, 'TilemapCollider2D'], + [41_386_430, 'AssetImporterLog'], + [73_398_921, 'VFXRenderer'], + [76_251_197, 'SerializableManagedRefTestClass'], + [156_049_354, 'Grid'], + [181_963_792, 'Preset'], + [277_625_683, 'EmptyObject'], + [285_090_594, 'IConstraint'], + [293_259_124, 'TestObjectWithSpecialLayoutOne'], + [294_290_339, 'AssemblyDefinitionReferenceImporter'], + [334_799_969, 'SiblingDerived'], + [342_846_651, 'TestObjectWithSerializedMapStringNonAlignedStruct'], + [367_388_927, 'SubDerived'], + [369_655_926, 'AssetImportInProgressProxy'], + [382_020_655, 'PluginBuildInfo'], + [426_301_858, 'EditorProjectAccess'], + [468_431_735, 'PrefabImporter'], + [478_637_458, 'TestObjectWithSerializedArray'], + [478_637_459, 'TestObjectWithSerializedAnimationCurve'], + [483_693_784, 'TilemapRenderer'], + [638_013_454, 'SpriteAtlasDatabase'], + [641_289_076, 'AudioBuildInfo'], + [644_342_135, 'CachedSpriteAtlasRuntimeData'], + [646_504_946, 'RendererFake'], + [662_584_278, 'AssemblyDefinitionReferenceAsset'], + [668_709_126, 'BuiltAssetBundleInfoSet'], + [687_078_895, 'SpriteAtlas'], + [877_146_078, 'PlatformModuleSetup'], + [895_512_359, 'AimConstraint'], + [937_362_698, 'VFXManager'], + [994_735_392, 'VisualEffectSubgraph'], + [994_735_403, 'VisualEffectSubgraphOperator'], + [994_735_404, 'VisualEffectSubgraphBlock'], + [1_001_480_554, 'Prefab'], + [1_027_052_791, 'LocalizationImporter'], + [1_091_556_383, 'Derived'], + [1_111_377_672, 'PropertyModificationsTargetTestObject'], + [1_114_811_875, 'ReferencesArtifactGenerator'], + [1_152_215_463, 'AssemblyDefinitionAsset'], + [1_154_873_562, 'SceneVisibilityState'], + [1_183_024_399, 'LookAtConstraint'], + [1_223_240_404, 'MultiArtifactTestImporter'], + [1_268_269_756, 'GameObjectRecorder'], + [1_325_145_578, 'LightingDataAssetParent'], + [1_386_491_679, 'PresetManager'], + [1_392_443_030, 'TestObjectWithSpecialLayoutTwo'], + [1_403_656_975, 'StreamingManager'], + [1_480_428_607, 'LowerResBlitTexture'], + [1_542_919_678, 'StreamingController'], + [1_571_458_007, 'RenderPassAttachment'], + [1_628_831_178, 'TestObjectVectorPairStringBool'], + [1_742_807_556, 'GridLayout'], + [1_766_753_193, 'AssemblyDefinitionImporter'], + [1_773_428_102, 'ParentConstraint'], + [1_803_986_026, 'FakeComponent'], + [1_818_360_608, 'PositionConstraint'], + [1_818_360_609, 'RotationConstraint'], + [1_818_360_610, 'ScaleConstraint'], + [1_839_735_485, 'Tilemap'], + [1_896_753_125, 'PackageManifest'], + [1_896_753_126, 'PackageManifestImporter'], + [1_953_259_897, 'TerrainLayer'], + [1_971_053_207, 'SpriteShapeRenderer'], + [1_977_754_360, 'NativeObjectType'], + [1_981_279_845, 'TestObjectWithSerializedMapStringBool'], + [1_995_898_324, 'SerializableManagedHost'], + [2_058_629_509, 'VisualEffectAsset'], + [2_058_629_510, 'VisualEffectImporter'], + [2_058_629_511, 'VisualEffectResource'], + [2_059_678_085, 'VisualEffectObject'], + [2_083_052_967, 'VisualEffect'], + [2_083_778_819, 'LocalizationAsset'], + [2_089_858_483, 'ScriptedImporter'] ] CLASS_ID2NAME = CLASS_ID_TABLE.to_h.freeze diff --git a/lib/mikunyan/decoders/image_decoder.rb b/lib/mikunyan/decoders/image_decoder.rb index fddbb88..814b870 100644 --- a/lib/mikunyan/decoders/image_decoder.rb +++ b/lib/mikunyan/decoders/image_decoder.rb @@ -187,7 +187,8 @@ module Mikunyan # @param [Symbol] endian endianness of binary # @return [ChunkyPNG::Image] decoded image def self.decode_rgb565(width, height, bin, endian = :big) - ChunkyPNG::Image.from_rgb_stream(width, height, DecodeHelper.decode_rgb565(bin, width * height, endian == :big)).flip + ChunkyPNG::Image.from_rgb_stream(width, height, + DecodeHelper.decode_rgb565(bin, width * height, endian == :big)).flip end # Decode image from R16 binary @@ -197,7 +198,8 @@ module Mikunyan # @param [Symbol] endian endianness of binary # @return [ChunkyPNG::Image] decoded image def self.decode_r16(width, height, bin, endian = :big) - ChunkyPNG::Image.from_rgb_stream(width, height, DecodeHelper.decode_r16(bin, width * height, endian == :big)).flip + ChunkyPNG::Image.from_rgb_stream(width, height, + DecodeHelper.decode_r16(bin, width * height, endian == :big)).flip end # Decode image from RGBA4444 binary @@ -270,7 +272,8 @@ module Mikunyan # @param [Symbol] endian endianness of binary # @return [ChunkyPNG::Image] decoded image def self.decode_rhalf(width, height, bin, endian = :big) - ChunkyPNG::Image.from_rgb_stream(width, height, DecodeHelper.decode_rhalf(bin, width * height, endian == :big)).flip + ChunkyPNG::Image.from_rgb_stream(width, height, + DecodeHelper.decode_rhalf(bin, width * height, endian == :big)).flip end # Decode image from RG Half-float binary @@ -280,7 +283,8 @@ module Mikunyan # @param [Symbol] endian endianness of binary # @return [ChunkyPNG::Image] decoded image def self.decode_rghalf(width, height, bin, endian = :big) - ChunkyPNG::Image.from_rgb_stream(width, height, DecodeHelper.decode_rghalf(bin, width * height, endian == :big)).flip + ChunkyPNG::Image.from_rgb_stream(width, height, + DecodeHelper.decode_rghalf(bin, width * height, endian == :big)).flip end # Decode image from RGBA Half-float binary @@ -290,7 +294,8 @@ module Mikunyan # @param [Symbol] endian endianness of binary # @return [ChunkyPNG::Image] decoded image def self.decode_rgbahalf(width, height, bin, endian = :big) - ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_rgbahalf(bin, width * height, endian == :big)).flip + ChunkyPNG::Image.from_rgba_stream(width, height, + DecodeHelper.decode_rgbahalf(bin, width * height, endian == :big)).flip end # Decode image from R float binary @@ -366,7 +371,8 @@ module Mikunyan # @param [Integer] bpp bit per pixel (2 or 4) # @return [ChunkyPNG::Image] decoded image def self.decode_pvrtc1(width, height, bin, bpp) - raise 'bpp of PVRTC1 must be 2 or 4' unless bpp == 2 || bpp == 4 + raise 'bpp of PVRTC1 must be 2 or 4' unless [2, 4].include?(bpp) + ChunkyPNG::Image.from_rgba_stream(width, height, DecodeHelper.decode_pvrtc1(bin, width, height, bpp == 2)) end @@ -449,7 +455,8 @@ module Mikunyan # @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)) + ChunkyPNG::Image.from_rgba_stream(width, height, + DecodeHelper.decode_astc(bin, width, height, blocksize, blocksize)) end # Decode image from crunched texture binary @@ -488,8 +495,10 @@ module Mikunyan fmt = object['m_TextureFormat']&.value bin = object['image data']&.value return unless width && height && fmt && bin && astc_list[fmt] + bin = object['m_StreamData']&.value if bin.empty? return unless bin + header = [0x13, 0xab, 0xa1, 0x5c, astc_list[fmt], astc_list[fmt], 1].pack('C*') header << [width].pack('V').byteslice(0, 3) header << [height].pack('V').byteslice(0, 3) @@ -500,6 +509,7 @@ module Mikunyan # [0.0,1.0] -> [0,255] def self.f2i(val) return 0 unless val.finite? + (val * 255).round.clamp(0, 255) end end diff --git a/lib/mikunyan/object_value.rb b/lib/mikunyan/object_value.rb index 4e78de5..b2bbc96 100644 --- a/lib/mikunyan/object_value.rb +++ b/lib/mikunyan/object_value.rb @@ -95,11 +95,11 @@ module Mikunyan if @type == 'pair' [@attr['first'].simplify, @attr['second'].simplify] elsif @type == 'map' && @value.is_a?(Array) - @value.map{|e| [e['first'].simplify, e['second'].simplify]}.to_h + @value.map {|e| [e['first'].simplify, e['second'].simplify]}.to_h elsif is_struct - @attr.map{|key, val| [key, val.simplify]}.to_h + @attr.transform_values(&:simplify) elsif @value.is_a?(Array) - @value.map{|e| e.is_a?(ObjectValue) ? e.simplify : e} + @value.map {|e| e.is_a?(ObjectValue) ? e.simplify : e} elsif @value.is_a?(ObjectValue) @value.simplify else diff --git a/lib/mikunyan/type_tree.rb b/lib/mikunyan/type_tree.rb index 52c0329..8852dac 100644 --- a/lib/mikunyan/type_tree.rb +++ b/lib/mikunyan/type_tree.rb @@ -20,7 +20,8 @@ module Mikunyan # @attr [Integer,nil] v18meta # @attr [Mikunyan::TypeTree::Node,nil] parent ̀‘ # @attr [Array] children - Node = Struct.new(:version, :level, :array?, :type, :name, :size, :index, :flags, :v18meta, :parent, :children, keyword_init: true) do + Node = Struct.new(:version, :level, :array?, :type, :name, :size, :index, :flags, :v18meta, :parent, :children, + keyword_init: true) do def need_align? flags & 0x4000 != 0 end @@ -119,6 +120,7 @@ module Mikunyan def self.load_default(class_id, hash) file = File.expand_path("../typetrees/#{class_id}/#{hash.unpack1('H*')}.json", __FILE__) return nil unless File.file?(file) + TypeTree.deserialize(JSON.parse(File.read(file))) end