# frozen_string_literal: true require 'extlz4' require 'extlzma2' require 'mikunyan/asset' require 'mikunyan/binary_reader' 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 for this AssetBundle # @attr_reader [String] generator_version version string of generator # @attr_reader [String] guid unique identifier (can be zero) # @attr_reader [Array] assets contained Assets class AssetBundle attr_reader :signature, :format, :unity_version, :generator_version, :guid, :assets, :blobs AssetEntry = Struct.new(:name, :data, :blob?, :status, keyword_init: true) # @param [String,Integer] index # @return [Mikunyan::Asset,nil] def [](index) index.is_a?(String) ? @assets.find {|e| e.name == index} : @assets[index] end # Same as assets.each # @return [Enumerator,Array] def each_asset(&block) @assets.each(&block) end # Loads AssetBundle from binary string # @param [String,IO] bin binary data # @return [Mikunyan::AssetBundle] deserialized AssetBundle object def self.load(bin) r = AssetBundle.new r.send(:load, bin) r end # Loads AssetBundle from file # @param [String] file file name # @return [Mikunyan::AssetBundle] deserialized AssetBundle object def self.file(file) File.open(file, 'rb') do |io| AssetBundle.load(io) end end private def load(bin) br = BinaryReader.new(bin) @signature = br.cstr raise("Invalid signature: #{@signature}") unless @signature.start_with?('Unity') @format = br.i32 @unity_version = br.cstr @generator_version = br.cstr @format == 6 || @signature == 'UnityFS' ? load_unity_fs(br, @signature) : load_unity_raw(br) end # @param [Mikunyan::BinaryReader] br def load_unity_raw(br) @assets = [] _file_size = br.i32u header_size = br.i32u br.pos = header_size # この部分全然わからん(ファイルの最後まで読まないとダメらしい?) block = br.read(nil) data = @signature == 'UnityRaw' ? block : uncompress_lzma(block, true) br = BinaryReader.new(data) asset_count = br.i32u asset_entries = Array.new(asset_count) do name = br.cstr offset = br.i32u size = br.i32u is_asset = ['', '.assets'].include?(split_name(name)[1]) && size > 16 AssetEntry.new(name: name, data: br.read_abs(size, offset), blob?: !is_asset) end process_asset_entries(asset_entries) end # @param [Mikunyan::BinaryReader] br def load_unity_fs(br, signature) file_size = br.i64u ci_block_size = br.i32u ui_block_size = br.i32u flags = br.i32u br.adv(1) unless signature == 'UnityFS' br.align(16) if @format >= 7 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) br.align(16) unless flags & 0x200 == 0 block_count = head.i32u raw_data = Array.new(block_count) do u_size = head.i32u c_size = head.i32u flags = head.i16u uncompress(br.read(c_size), u_size, flags) end.join asset_count = head.i32u asset_entries = Array.new(asset_count) do offset = head.i64u size = head.i64u status = head.i32 AssetEntry.new(name: head.cstr, data: raw_data.byteslice(offset, size), blob?: status != 4, status: status) end process_asset_entries(asset_entries) end def process_asset_entries(asset_entries) @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 end def uncompress(block, max_dest_size, flags) case flags & 0x3f when 0 block when 1 uncompress_lzma(block) when 2, 3 LZ4.block_decode(block, max_dest_size) # when 4 # LZHMA else warn("Unknown compression flag: #{flags}") block end end def uncompress_lzma(block, with_max_len = false) prop = block.ord filter = LZMA.lzma1(dictsize: block.unpack1('@1V'), lc: prop % 9, lp: (prop / 9) % 5, pb: prop / 45) max_len = block.unpack1('@5V') | block.unpack1('@9V') << 32 if with_max_len StringIO.open(block) do |io| io.seek(with_max_len ? 13 : 5) LZMA.raw_decode(io, filter) do |lzma| lzma.read(max_len) end end end def split_name(str) m = str.match(/\A(.*?)(\.[^.]*)?\z/) [m[1], m[2] || ''] end end end