//! \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; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using GameRes.Utility; namespace GameRes.Formats.Unity { internal class Asset { int m_format; long 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(); if (m_format >= 22) { input.ReadInt32(); // header_size input.ReadInt64(); // file_size m_data_offset = input.ReadInt64(); input.ReadInt64(); } 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 file_id = input.ReadInt32(); var id = input.ReadId(); m_adds[id] = file_id; } } 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 (string.Format ("Unknown type id {0}", obj.ClassId.ToString()), "[Unity.Asset]"); 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.ReadOffset(); Offset += 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 TypeName { get { var type = this.Type; if (type != null) return type.Type; return string.Format ("[TypeId:{0}]", TypeId); } } public TypeTree Type { get { TypeTree type; Asset.Tree.TypeTrees.TryGetValue (TypeId, out type); return type; } } public override string ToString () { return string.Format ("<{0} {1}>", Type, ClassId); } public IDictionary Deserialize (AssetReader input) { var type_tree = Asset.Tree.TypeTrees; if (!type_tree.ContainsKey (TypeId)) return null; var type_map = new Hashtable(); var type = type_tree[TypeId]; foreach (var node in type.Children) { type_map[node.Name] = DeserializeType (input, node); } return type_map; } object DeserializeType (AssetReader input, TypeTree node) { object obj = null; if (node.IsArray) { int size = input.ReadInt32(); var data_field = node.Children.FirstOrDefault (n => n.Name == "data"); if (data_field != null) { if ("TypelessData" == node.Type) obj = input.ReadBytes (size * data_field.Size); else obj = DeserializeArray (input, size, data_field); } } else if (node.Size < 0) { if (node.Type == "string") { obj = input.ReadString(); if (node.Children[0].IsAligned) input.Align(); } else if (node.Type == "StreamingInfo") { var info = new StreamingInfo(); info.Load (input); obj = info; } else throw new NotImplementedException ("Unknown class encountered in asset deserialzation."); } else if ("int" == node.Type) obj = input.ReadInt32(); else if ("unsigned int" == node.Type) obj = input.ReadUInt32(); else if ("bool" == node.Type) obj = input.ReadBool(); else input.Position += node.Size; if (node.IsAligned) input.Align(); return obj; } object[] DeserializeArray (AssetReader input, int length, TypeTree elem) { var array = new object[length]; for (int i = 0; i < length; ++i) array[i] = DeserializeType (input, elem); return array; } } 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; } } public bool IsAligned { get { return (Flags & 0x4000) != 0; } } 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) { Type = reader.ReadCString(); Name = reader.ReadCString(); Size = reader.ReadInt32(); Index = reader.ReadUInt32(); IsArray = reader.ReadInt32() != 0; Version = reader.ReadInt32(); Flags = reader.ReadInt32(); int count = reader.ReadInt32(); for (int i = 0; i < count; ++i) { var child = new TypeTree (m_format); child.Load (reader); Children.Add (child); } } byte[] m_data; void LoadBlob (AssetReader reader) { int count = reader.ReadInt32(); int buffer_bytes = reader.ReadInt32(); int node_size = m_format >= 18 ? 32 : 24; var node_data = reader.ReadBytes (node_size * count); m_data = reader.ReadBytes (buffer_bytes); if (m_format >= 21) reader.Skip (4); 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(); if (m_format >= 18) buf.ReadInt64(); } } } 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 byte[] LoadResource (string name) { var res = EmbeddedResource.Load (name, typeof(TypeTree)); if (null == res) throw new FileNotFoundException ("Resource not found.", name); 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; } } } } }