#region References using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; #endregion namespace Ultima { public sealed class FileIndex { public Entry3D[] Index { get; private set; } public Stream Stream { get; private set; } public long IdxLength { get; private set; } private readonly string MulPath; public Stream Seek(int index, out int length, out int extra, out bool patched) { if (index < 0 || index >= Index.Length) { length = extra = 0; patched = false; return null; } Entry3D e = Index[index]; if (e.lookup < 0) { length = extra = 0; patched = false; return null; } length = e.length & 0x7FFFFFFF; extra = e.extra; if ((e.length & (1 << 31)) != 0) { patched = true; Verdata.Seek(e.lookup); return Verdata.Stream; } if (e.length < 0) { length = extra = 0; patched = false; return null; } if ((Stream == null) || (!Stream.CanRead) || (!Stream.CanSeek)) { if (MulPath == null) { Stream = null; } else { Stream = new FileStream(MulPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); } } if (Stream == null) { length = extra = 0; patched = false; return null; } else if (Stream.Length < e.lookup) { length = extra = 0; patched = false; return null; } patched = false; Stream.Seek(e.lookup, SeekOrigin.Begin); return Stream; } public bool Valid(int index, out int length, out int extra, out bool patched) { if (index < 0 || index >= Index.Length) { length = extra = 0; patched = false; return false; } Entry3D e = Index[index]; if (e.lookup < 0) { length = extra = 0; patched = false; return false; } length = e.length & 0x7FFFFFFF; extra = e.extra; if ((e.length & (1 << 31)) != 0) { patched = true; return true; } if (e.length < 0) { length = extra = 0; patched = false; return false; } if ((MulPath == null) || !File.Exists(MulPath)) { length = extra = 0; patched = false; return false; } if ((Stream == null) || (!Stream.CanRead) || (!Stream.CanSeek)) { Stream = new FileStream(MulPath, FileMode.Open, FileAccess.Read, FileShare.Read); } if (Stream.Length < e.lookup) { length = extra = 0; patched = false; return false; } patched = false; return true; } public FileIndex(string idxFile, string mulFile, int length, int file) : this(idxFile, mulFile, null, length, file, ".dat", -1, false) { } public FileIndex( string idxFile, string mulFile, string uopFile, int length, int file, string uopEntryExtension, int idxLength, bool hasExtra) { Index = new Entry3D[length]; string idxPath = null; MulPath = null; string uopPath = null; if (Files.MulPath == null) { Files.LoadMulPath(); } if (Files.MulPath.Count > 0) { idxPath = Files.MulPath[idxFile.ToLower()]; MulPath = Files.MulPath[mulFile.ToLower()]; if (!String.IsNullOrEmpty(uopFile) && Files.MulPath.ContainsKey(uopFile.ToLower())) { uopPath = Files.MulPath[uopFile.ToLower()]; } if (String.IsNullOrEmpty(idxPath)) { idxPath = null; } else { if (String.IsNullOrEmpty(Path.GetDirectoryName(idxPath))) { idxPath = Path.Combine(Files.RootDir, idxPath); } if (!File.Exists(idxPath)) { idxPath = null; } } if (String.IsNullOrEmpty(MulPath)) { MulPath = null; } else { if (String.IsNullOrEmpty(Path.GetDirectoryName(MulPath))) { MulPath = Path.Combine(Files.RootDir, MulPath); } if (!File.Exists(MulPath)) { MulPath = null; } } if (String.IsNullOrEmpty(uopPath)) { uopPath = null; } else { if (String.IsNullOrEmpty(Path.GetDirectoryName(uopPath))) { uopPath = Path.Combine(Files.RootDir, uopPath); } if (!File.Exists(uopPath)) { uopPath = null; } else { MulPath = uopPath; } } } /* UOP files support code, written by Wyatt (c) www.ruosi.org * idxLength variable was added for compatibility with legacy code for art (see art.cs) * At the moment the only UOP file having entries with extra field is gumpartlegacy.uop, * and it's two dwords in the beginning of the entry. * It's possible that UOP can include some entries with unknown hash: not really unknown for me, but * not useful for reading legacy entries. That's why i removed unknown hash exception throwing from this code */ if (MulPath != null && MulPath.EndsWith(".uop")) { using (var index = new FileStream(MulPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) { Stream = new FileStream(MulPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); var fi = new FileInfo(MulPath); string uopPattern = fi.Name.Replace(fi.Extension, "").ToLowerInvariant(); using (var br = new BinaryReader(Stream)) { br.BaseStream.Seek(0, SeekOrigin.Begin); if (br.ReadInt32() != 0x50594D) { throw new ArgumentException("Bad UOP file."); } br.ReadInt64(); // version + signature long nextBlock = br.ReadInt64(); br.ReadInt32(); // block capacity int count = br.ReadInt32(); if (idxLength > 0) { IdxLength = idxLength * 12; } var hashes = new Dictionary(); for (int i = 0; i < length; i++) { string entryName = string.Format("build/{0}/{1:D8}{2}", uopPattern, i, uopEntryExtension); ulong hash = HashFileName(entryName); if (!hashes.ContainsKey(hash)) { hashes.Add(hash, i); } } br.BaseStream.Seek(nextBlock, SeekOrigin.Begin); do { int filesCount = br.ReadInt32(); nextBlock = br.ReadInt64(); for (int i = 0; i < filesCount; i++) { long offset = br.ReadInt64(); int headerLength = br.ReadInt32(); int compressedLength = br.ReadInt32(); int decompressedLength = br.ReadInt32(); ulong hash = br.ReadUInt64(); br.ReadUInt32(); // Adler32 short flag = br.ReadInt16(); int entryLength = flag == 1 ? compressedLength : decompressedLength; if (offset == 0) { continue; } int idx; if (hashes.TryGetValue(hash, out idx)) { if (idx < 0 || idx > Index.Length) { throw new IndexOutOfRangeException("hashes dictionary and files collection have different count of entries!"); } Index[idx].lookup = (int)(offset + headerLength); Index[idx].length = entryLength; if (hasExtra) { long curPos = br.BaseStream.Position; br.BaseStream.Seek(offset + headerLength, SeekOrigin.Begin); byte[] extra = br.ReadBytes(8); var extra1 = (ushort)((extra[3] << 24) | (extra[2] << 16) | (extra[1] << 8) | extra[0]); var extra2 = (ushort)((extra[7] << 24) | (extra[6] << 16) | (extra[5] << 8) | extra[4]); Index[idx].lookup += 8; Index[idx].extra = extra1 << 16 | extra2; br.BaseStream.Seek(curPos, SeekOrigin.Begin); } } } } while (br.BaseStream.Seek(nextBlock, SeekOrigin.Begin) != 0); } } } else if ((idxPath != null) && (MulPath != null)) { using (var index = new FileStream(idxPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) { Stream = new FileStream(MulPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); var count = (int)(index.Length / 12); IdxLength = index.Length; GCHandle gc = GCHandle.Alloc(Index, GCHandleType.Pinned); var buffer = new byte[index.Length]; index.Read(buffer, 0, (int)index.Length); Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)Math.Min(IdxLength, length * 12)); gc.Free(); for (int i = count; i < length; ++i) { Index[i].lookup = -1; Index[i].length = -1; Index[i].extra = -1; } } } else { Stream = null; return; } Entry5D[] patches = Verdata.Patches; if (file > -1) { for (int i = 0; i < patches.Length; ++i) { Entry5D patch = patches[i]; if (patch.file == file && patch.index >= 0 && patch.index < length) { Index[patch.index].lookup = patch.lookup; Index[patch.index].length = patch.length | (1 << 31); Index[patch.index].extra = patch.extra; } } } } public FileIndex(string idxFile, string mulFile, int file) { string idxPath = null; MulPath = null; if (Files.MulPath == null) { Files.LoadMulPath(); } if (Files.MulPath.Count > 0) { idxPath = Files.MulPath[idxFile.ToLower()]; MulPath = Files.MulPath[mulFile.ToLower()]; if (String.IsNullOrEmpty(idxPath)) { idxPath = null; } else { if (String.IsNullOrEmpty(Path.GetDirectoryName(idxPath))) { idxPath = Path.Combine(Files.RootDir, idxPath); } if (!File.Exists(idxPath)) { idxPath = null; } } if (String.IsNullOrEmpty(MulPath)) { MulPath = null; } else { if (String.IsNullOrEmpty(Path.GetDirectoryName(MulPath))) { MulPath = Path.Combine(Files.RootDir, MulPath); } if (!File.Exists(MulPath)) { MulPath = null; } } } if ((idxPath != null) && (MulPath != null)) { using (var index = new FileStream(idxPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) { Stream = new FileStream(MulPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); var count = (int)(index.Length / 12); IdxLength = index.Length; Index = new Entry3D[count]; GCHandle gc = GCHandle.Alloc(Index, GCHandleType.Pinned); var buffer = new byte[index.Length]; index.Read(buffer, 0, (int)index.Length); Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)index.Length); gc.Free(); } } else { Stream = null; Index = new Entry3D[1]; return; } Entry5D[] patches = Verdata.Patches; if (file > -1) { for (int i = 0; i < patches.Length; ++i) { Entry5D patch = patches[i]; if (patch.file == file && patch.index >= 0 && patch.index < Index.Length) { Index[patch.index].lookup = patch.lookup; Index[patch.index].length = patch.length | (1 << 31); Index[patch.index].extra = patch.extra; } } } } /// /// Method for calculating entry hash by it's name. /// Taken from Mythic.Package.dll /// /// /// public static ulong HashFileName(string s) { uint eax, ecx, edx, ebx, esi, edi; eax = ecx = edx = ebx = esi = edi = 0; ebx = edi = esi = (uint)s.Length + 0xDEADBEEF; int i = 0; for (i = 0; i + 12 < s.Length; i += 12) { edi = (uint)((s[i + 7] << 24) | (s[i + 6] << 16) | (s[i + 5] << 8) | s[i + 4]) + edi; esi = (uint)((s[i + 11] << 24) | (s[i + 10] << 16) | (s[i + 9] << 8) | s[i + 8]) + esi; edx = (uint)((s[i + 3] << 24) | (s[i + 2] << 16) | (s[i + 1] << 8) | s[i]) - esi; edx = (edx + ebx) ^ (esi >> 28) ^ (esi << 4); esi += edi; edi = (edi - edx) ^ (edx >> 26) ^ (edx << 6); edx += esi; esi = (esi - edi) ^ (edi >> 24) ^ (edi << 8); edi += edx; ebx = (edx - esi) ^ (esi >> 16) ^ (esi << 16); esi += edi; edi = (edi - ebx) ^ (ebx >> 13) ^ (ebx << 19); ebx += esi; esi = (esi - edi) ^ (edi >> 28) ^ (edi << 4); edi += ebx; } if (s.Length - i > 0) { switch (s.Length - i) { case 12: esi += (uint)s[i + 11] << 24; goto case 11; case 11: esi += (uint)s[i + 10] << 16; goto case 10; case 10: esi += (uint)s[i + 9] << 8; goto case 9; case 9: esi += s[i + 8]; goto case 8; case 8: edi += (uint)s[i + 7] << 24; goto case 7; case 7: edi += (uint)s[i + 6] << 16; goto case 6; case 6: edi += (uint)s[i + 5] << 8; goto case 5; case 5: edi += s[i + 4]; goto case 4; case 4: ebx += (uint)s[i + 3] << 24; goto case 3; case 3: ebx += (uint)s[i + 2] << 16; goto case 2; case 2: ebx += (uint)s[i + 1] << 8; goto case 1; case 1: ebx += s[i]; break; } esi = (esi ^ edi) - ((edi >> 18) ^ (edi << 14)); ecx = (esi ^ ebx) - ((esi >> 21) ^ (esi << 11)); edi = (edi ^ ecx) - ((ecx >> 7) ^ (ecx << 25)); esi = (esi ^ edi) - ((edi >> 16) ^ (edi << 16)); edx = (esi ^ ecx) - ((esi >> 28) ^ (esi << 4)); edi = (edi ^ edx) - ((edx >> 18) ^ (edx << 14)); eax = (esi ^ edi) - ((edi >> 8) ^ (edi << 24)); return ((ulong)edi << 32) | eax; } return ((ulong)esi << 32) | eax; } } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct Entry3D { public int lookup; public int length; public int extra; } }