//! \file Asset.cs //! \date Wed Apr 05 18:58:07 2017 //! \brief Unity asset class. // // Based on the [UnityPack](https://github.com/HearthSim/UnityPack) // // Copyright (c) Jerome Leclanche // // C# port copyright (C) 2017 by morkt // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using GameRes.Utility; namespace GameRes.Formats.Unity { internal class Asset { int m_format; uint m_data_offset; bool m_is_little_endian; UnityTypeData m_tree = new UnityTypeData(); Dictionary m_adds; List m_refs; Dictionary m_types = new Dictionary(); Dictionary m_objects = new Dictionary(); public int Format { get { return m_format; } } public bool IsLittleEndian { get { return m_is_little_endian; } } public long DataOffset { get { return m_data_offset; } } public UnityTypeData Tree { get { return m_tree; } } public IEnumerable Objects { get { return m_objects.Values; } } public void Load (AssetReader input) { input.ReadInt32(); // header_size input.ReadUInt32(); // file_size m_format = input.ReadInt32(); m_data_offset = input.ReadUInt32(); if (m_format >= 9) m_is_little_endian = 0 == input.ReadInt32(); input.SetupReaders (this); m_tree.Load (input); bool long_ids = Format >= 14; if (Format >= 7 && Format < 14) long_ids = 0 != input.ReadInt32(); input.SetupReadId (long_ids); int obj_count = input.ReadInt32(); for (int i = 0; i < obj_count; ++i) { input.Align(); var obj = new UnityObject (this); obj.Load (input); RegisterObject (obj); } if (Format >= 11) { int count = input.ReadInt32(); m_adds = new Dictionary (count); for (int i = 0; i < count; ++i) { input.Align(); var id = input.ReadId(); m_adds[id] = input.ReadInt32(); } } if (Format >= 6) { int count = input.ReadInt32(); m_refs = new List (count); for (int i = 0; i < count; ++i) { var asset_ref = AssetRef.Load (input); m_refs.Add (asset_ref); } } input.ReadCString(); } void RegisterObject (UnityObject obj) { if (m_tree.TypeTrees.ContainsKey (obj.TypeId)) { m_types[obj.TypeId] = m_tree.TypeTrees[obj.TypeId]; } else if (!m_types.ContainsKey (obj.TypeId)) { /* var trees = TypeTree.Default (this).TypeTrees; if (trees.ContainsKey (obj.ClassId)) { m_types[obj.TypeId] = trees[obj.ClassId]; } else */ { Trace.WriteLine ("Unknown type id", obj.ClassId.ToString()); m_types[obj.TypeId] = null; } } if (m_objects.ContainsKey (obj.PathId)) throw new ApplicationException (string.Format ("Duplicate asset object {0} (PathId: {1})", obj, obj.PathId)); m_objects[obj.PathId] = obj; } } internal class AssetRef { public string AssetPath; public Guid Guid; public int Type; public string FilePath; public object Asset; public static AssetRef Load (AssetReader reader) { var r = new AssetRef(); r.AssetPath = reader.ReadCString(); r.Guid = new Guid (reader.ReadBytes (16)); r.Type = reader.ReadInt32(); r.FilePath = reader.ReadCString(); r.Asset = null; return r; } } internal class UnityObject { public Asset Asset; public long PathId; public long Offset; public uint Size; public int TypeId; public int ClassId; public bool IsDestroyed; public UnityObject (Asset owner) { Asset = owner; } public AssetReader Open (Stream input) { var stream = new StreamRegion (input, Offset, Size, true); var reader = new AssetReader (stream, ""); reader.SetupReaders (Asset); return reader; } public void Load (AssetReader reader) { PathId = reader.ReadId(); Offset = reader.ReadUInt32() + Asset.DataOffset; Size = reader.ReadUInt32(); if (Asset.Format < 17) { TypeId = reader.ReadInt32(); ClassId = reader.ReadInt16(); } else { var type_id = reader.ReadInt32(); var class_id = Asset.Tree.ClassIds[type_id]; TypeId = class_id; ClassId = class_id; } if (Asset.Format <= 10) IsDestroyed = reader.ReadInt16() != 0; if (Asset.Format >= 11 && Asset.Format < 17) reader.ReadInt16(); if (Asset.Format >= 15 && Asset.Format < 17) reader.ReadByte(); } public string Type { get { var type_tree = Asset.Tree.TypeTrees; if (type_tree.ContainsKey (TypeId)) return type_tree[TypeId].Type; return string.Format ("[TypeId:{0}]", TypeId); } } public override string ToString () { return string.Format ("<{0} {1}>", Type, ClassId); } } internal class TypeTree { int m_format; List m_children = new List(); public int Version; public bool IsArray; public string Type; public string Name; public int Size; public uint Index; public int Flags; public IList Children { get { return m_children; } } static readonly string Null = "(null)"; static readonly Lazy StringsDat = new Lazy (() => LoadResource ("strings.dat")); public TypeTree (int format) { m_format = format; } public void Load (AssetReader reader) { if (10 == m_format || m_format >= 12) LoadBlob (reader); else LoadRaw (reader); } void LoadRaw (AssetReader reader) { throw new NotImplementedException(); } byte[] m_data; void LoadBlob (AssetReader reader) { int count = reader.ReadInt32(); int buffer_bytes = reader.ReadInt32(); var node_data = reader.ReadBytes (24 * count); m_data = reader.ReadBytes (buffer_bytes); var parents = new Stack(); parents.Push (this); using (var buf = new BinMemoryStream (node_data)) { for (int i = 0; i < count; ++i) { int version = buf.ReadInt16(); int depth = buf.ReadUInt8(); TypeTree current; if (0 == depth) { current = this; } else { while (parents.Count > depth) parents.Pop(); current = new TypeTree (m_format); parents.Peek().Children.Add (current); parents.Push (current); } current.Version = version; current.IsArray = buf.ReadUInt8() != 0; current.Type = GetString (buf.ReadInt32()); current.Name = GetString (buf.ReadInt32()); current.Size = buf.ReadInt32(); current.Index = buf.ReadUInt32(); current.Flags = buf.ReadInt32(); } } } string GetString (int offset) { byte[] strings; if (offset < 0) { offset &= 0x7FFFFFFF; strings = StringsDat.Value; } else if (offset < m_data.Length) strings = m_data; else return Null; return Binary.GetCString (strings, offset, strings.Length-offset, Encoding.UTF8); } internal static Stream OpenResource (string name) { var qualified_name = ".Unity." + name; var assembly = Assembly.GetExecutingAssembly(); var res_name = assembly.GetManifestResourceNames().Single (r => r.EndsWith (qualified_name)); Stream stream = assembly.GetManifestResourceStream (res_name); if (null == stream) throw new FileNotFoundException ("Resource not found.", name); return stream; } internal static byte[] LoadResource (string name) { using (var stream = OpenResource (name)) { var res = new byte[stream.Length]; stream.Read (res, 0, res.Length); return res; } } } internal class UnityTypeData { string m_version; List m_class_ids = new List (); Dictionary m_hashes = new Dictionary (); Dictionary m_type_trees = new Dictionary (); public string Version { get { return m_version; } } public IList ClassIds { get { return m_class_ids; } } public IDictionary Hashes { get { return m_hashes; } } public IDictionary TypeTrees { get { return m_type_trees; } } public void Load (AssetReader reader) { int format = reader.Format; m_version = reader.ReadCString(); var platform = reader.ReadInt32 (); if (format >= 13) { bool has_type_trees = reader.ReadBool (); int count = reader.ReadInt32 (); for (int i = 0; i < count; ++i) { int class_id = reader.ReadInt32 (); if (format >= 17) { reader.ReadByte (); int script_id = reader.ReadInt16 (); if (114 == class_id) { if (script_id >= 0) class_id = -2 - script_id; else class_id = -1; } } m_class_ids.Add (class_id); byte[] hash = reader.ReadBytes (class_id < 0 ? 0x20 : 0x10); m_hashes[class_id] = hash; if (has_type_trees) { var tree = new TypeTree (format); tree.Load (reader); m_type_trees[class_id] = tree; } } } else { int count = reader.ReadInt32 (); for (int i = 0; i < count; ++i) { int class_id = reader.ReadInt32 (); var tree = new TypeTree (format); tree.Load (reader); m_type_trees[class_id] = tree; } } } } internal class AudioClip { public string m_Name; public int m_LoadType; public int m_Channels; public int m_Frequency; public int m_BitsPerSample; public float m_Length; public bool m_IsTrackerFormat; public int m_SubsoundIndex; public bool m_PreloadAudioData; public bool m_LoadInBackground; public bool m_Legacy3D; public string m_Source; public long m_Offset; public long m_Size; public int m_CompressionFormat; public void Load (AssetReader reader) { m_Name = reader.ReadString(); reader.Align(); m_LoadType = reader.ReadInt32(); m_Channels = reader.ReadInt32(); m_Frequency = reader.ReadInt32(); m_BitsPerSample = reader.ReadInt32(); m_Length = reader.ReadFloat(); m_IsTrackerFormat = reader.ReadBool(); reader.Align(); m_SubsoundIndex = reader.ReadInt32(); m_PreloadAudioData = reader.ReadBool(); m_LoadInBackground = reader.ReadBool(); m_Legacy3D = reader.ReadBool(); reader.Align(); m_Source = reader.ReadString(); reader.Align(); m_Offset = reader.ReadInt64(); m_Size = reader.ReadInt64(); m_CompressionFormat = reader.ReadInt32(); } } internal class StreamingInfo { public uint Offset; public uint Size; public string Path; public void Load (AssetReader reader) { Offset = reader.ReadUInt32(); Size = reader.ReadUInt32(); Path = reader.ReadString(); } } }