ArchiveMap.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. using K4os.Compression.LZ4;
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. namespace OpenVIII
  8. {
  9. public class ArchiveMap : IReadOnlyDictionary<string, FI>
  10. {
  11. #region Fields
  12. private readonly Dictionary<string, FI> _entries;
  13. #endregion Fields
  14. #region Constructors
  15. public ArchiveMap(StreamWithRangeValues fI, StreamWithRangeValues fL, int max)
  16. {
  17. if (fI == null || fL == null)
  18. throw new ArgumentNullException($"{nameof(ArchiveMap)}::{nameof(ArchiveMap)} {nameof(fI)} or {nameof(fL)} is null");
  19. Max = max;
  20. var s1 = Uncompress(fL, out var flOffset);
  21. var s2 = Uncompress(fI, out var fiOffset);
  22. var fiSize = fI.UncompressedSize == 0 ? fI.Size : fI.UncompressedSize;
  23. using (var sr = new StreamReader(s1, System.Text.Encoding.UTF8))
  24. using (var br = new BinaryReader(s2))
  25. {
  26. s1.Seek(flOffset, SeekOrigin.Begin);
  27. s2.Seek(fiOffset, SeekOrigin.Begin);
  28. _entries = new Dictionary<string, FI>();
  29. var count = fiSize / 12;
  30. while (count-- > 0)
  31. _entries.Add(sr.ReadLine()?.TrimEnd() ?? throw new InvalidOperationException(), Extended.ByteArrayToClass<FI>(br.ReadBytes(12)));
  32. }
  33. CorrectCompressionForLzsFiles();
  34. }
  35. public ArchiveMap(int count) => _entries = new Dictionary<string, FI>(count);
  36. #endregion Constructors
  37. #region Properties
  38. public int Count => ((IReadOnlyDictionary<string, FI>)_entries).Count;
  39. public IEnumerable<string> Keys => ((IReadOnlyDictionary<string, FI>)FilteredEntries).Keys;
  40. public int Max { get; }
  41. public string ArchiveLangFolder
  42. {
  43. get;
  44. } = $"lang-{Memory.Languages}";
  45. public IReadOnlyList<KeyValuePair<string, FI>> OrderedByName => FilteredEntries.OrderBy(x => x.Key).ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase).ToList();
  46. public IReadOnlyList<KeyValuePair<string, FI>> OrderedByOffset => FilteredEntries.OrderBy(x => x.Value.Offset).ThenBy(x => x.Key).ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase).ToList();
  47. public IEnumerable<FI> Values => ((IReadOnlyDictionary<string, FI>)FilteredEntries).Values;
  48. private IEnumerable<KeyValuePair<string, FI>> FilteredEntries => _entries.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Value.UncompressedSize > 0);
  49. #endregion Properties
  50. #region Indexers
  51. public FI this[string key] => ((IReadOnlyDictionary<string, FI>)_entries)[key];
  52. #endregion Indexers
  53. #region Methods
  54. public static byte[] Lz4Uncompress(byte[] input, int fsUncompressedSize, int offset = 12)
  55. {
  56. Memory.Log.WriteLine($"{nameof(ArchiveMap)}::{nameof(Lz4Uncompress)} :: decompressing data");
  57. //ReadOnlySpan<byte> input = new ReadOnlySpan<byte>(file);
  58. var output = new byte[fsUncompressedSize];
  59. //Span<byte> output = new Span<byte>(r);
  60. while (input.Length - offset > 0 && LZ4Codec.Decode(input, offset, input.Length - offset, output, 0, output.Length) <= -1)
  61. {
  62. offset++;
  63. }
  64. if (offset > -1)
  65. {
  66. Memory.Log.WriteLine($"{nameof(ArchiveWorker)}::{nameof(Lz4Uncompress)}::{nameof(offset)}: {offset}");
  67. return output;
  68. }
  69. else
  70. throw new Exception($"{nameof(ArchiveWorker)}::{nameof(Lz4Uncompress)} Failed to uncompress...");
  71. }
  72. public void Add(KeyValuePair<string, FI> keyValuePair) => _entries.Add(keyValuePair.Key, keyValuePair.Value);
  73. public bool ContainsKey(string key) => ((IReadOnlyDictionary<string, FI>)_entries).ContainsKey(key);
  74. public FI FindString(ref string input, out int size)
  75. {
  76. var localInput = input;
  77. var result = GetSpecificLangFileData(input);
  78. if (string.IsNullOrWhiteSpace(result.Key))
  79. {
  80. result = OrderedByName.FirstOrDefault(x => x.Key.IndexOf(localInput, StringComparison.OrdinalIgnoreCase) > -1);
  81. }
  82. if (string.IsNullOrWhiteSpace(result.Key) || result.Value == default)
  83. {
  84. size = 0;
  85. return null;
  86. }
  87. var result2 = OrderedByOffset.FirstOrDefault(x => x.Value.Offset > result.Value.Offset);
  88. if (result2.Value == default)
  89. {
  90. switch (result.Value.CompressionType)
  91. {
  92. case CompressionType.None:
  93. case CompressionType.LZSS_UnknownSize:
  94. size = result.Value.UncompressedSize;
  95. break;
  96. default:
  97. size = Max - result.Value.Offset;
  98. if (size < 0) size = 0; // could be problems.
  99. break;
  100. }
  101. }
  102. else
  103. size = result2.Value.Offset - result.Value.Offset;
  104. input = result.Key;
  105. return result.Value;
  106. }
  107. public byte[] GetBinaryFile(FI fi, Stream data, string input, int size, long offset = 0)
  108. {
  109. var max = data.Length;
  110. if (data is StreamWithRangeValues s)
  111. {
  112. max = s.Max;
  113. }
  114. if (fi == null)
  115. {
  116. Memory.Log.WriteLine($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} failed to extract {input}");
  117. return null;
  118. }
  119. else
  120. Memory.Log.WriteLine($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} extracting {input}");
  121. if (size == 0)
  122. size = checked((int)(max - (fi.Offset + offset)));
  123. byte[] buffer;
  124. using (var br = new BinaryReader(data))
  125. {
  126. br.BaseStream.Seek(fi.Offset + offset, SeekOrigin.Begin);
  127. switch (fi.CompressionType)
  128. {
  129. case CompressionType.LZSS:
  130. case CompressionType.LZSS_UnknownSize:
  131. case CompressionType.LZSS_LZSS:
  132. var readSize = br.ReadInt32();
  133. if (size != readSize + sizeof(int))
  134. throw new InvalidDataException($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} Size inside of lzs {{{readSize}}} doesn't match size calculated by region {{{size}}}.");
  135. size = readSize;
  136. break;
  137. }
  138. buffer = br.ReadBytes(size);
  139. }
  140. switch (fi.CompressionType)
  141. {
  142. case 0:
  143. return buffer;
  144. case CompressionType.LZSS:
  145. return LZSS.DecompressAllNew(buffer, fi.UncompressedSize);
  146. case CompressionType.LZSS_LZSS:
  147. return LZSS.DecompressAllNew(LZSS.DecompressAllNew(buffer, fi.UncompressedSize), 0, true);
  148. case CompressionType.LZSS_UnknownSize:
  149. return LZSS.DecompressAllNew(buffer, 0);
  150. case CompressionType.LZ4:
  151. return Lz4Uncompress(buffer, fi.UncompressedSize);
  152. default:
  153. throw new InvalidDataException($"{nameof(fi.CompressionType)}: {fi.CompressionType} is invalid...");
  154. }
  155. }
  156. public byte[] GetBinaryFile(string input, Stream data)
  157. {
  158. if (data == null)
  159. throw new ArgumentNullException($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} {nameof(data)} cannot be null.");
  160. if (string.IsNullOrWhiteSpace(input))
  161. throw new ArgumentNullException($"{nameof(ArchiveMap)}::{nameof(GetBinaryFile)} {nameof(input)} cannot be empty or null");
  162. long offset = 0;
  163. if (data.GetType() == typeof(StreamWithRangeValues))
  164. {
  165. var s = (StreamWithRangeValues)data;
  166. data = Uncompress(s, out offset);
  167. //do I need to do something here? :P
  168. }
  169. var fi = FindString(ref input, out var size);
  170. return GetBinaryFile(fi, data, input, size, offset);
  171. }
  172. public IEnumerator<KeyValuePair<string, FI>> GetEnumerator() => ((IReadOnlyDictionary<string, FI>)_entries).GetEnumerator();
  173. IEnumerator IEnumerable.GetEnumerator() => ((IReadOnlyDictionary<string, FI>)_entries).GetEnumerator();
  174. public KeyValuePair<string, FI> GetFileData(string fileName)
  175. {
  176. if (!TryGetValue(fileName, out var value))
  177. return OrderedByName.FirstOrDefault(x => x.Key.IndexOf(fileName, StringComparison.OrdinalIgnoreCase) >= 0);
  178. return new KeyValuePair<string, FI>(fileName, value);
  179. }
  180. /// <summary>
  181. /// Merge two file lists together.
  182. /// </summary>
  183. /// <param name="child">the map being merged.</param>
  184. /// <param name="offsetForFs">offset where the file data is.</param>
  185. public void MergeMaps(ArchiveMap child, int offsetForFs) => _entries.AddRange(child._entries.ToDictionary(x => x.Key, x => x.Value.Adjust(offsetForFs)));
  186. public bool TryGetValue(string key, out FI value) => ((IReadOnlyDictionary<string, FI>)_entries).TryGetValue(key, out value);
  187. /// <summary>
  188. /// This forces the compression to be set to lzss so the file comes out uncompressed.
  189. /// </summary>
  190. private void CorrectCompressionForLzsFiles()
  191. {
  192. var lszFiles = _entries.Where(x => x.Key.EndsWith("lzs", StringComparison.OrdinalIgnoreCase));
  193. // ReSharper disable once PossibleMultipleEnumeration
  194. lszFiles.Where(x => x.Value.CompressionType == CompressionType.None).ForEach(x => _entries[x.Key].CompressionType = CompressionType.LZSS_UnknownSize);
  195. // ReSharper disable once PossibleMultipleEnumeration
  196. lszFiles.Where(x => x.Value.CompressionType == CompressionType.LZSS).ForEach(x => _entries[x.Key].CompressionType = CompressionType.LZSS_LZSS);
  197. }
  198. private KeyValuePair<string, FI> GetSpecificLangFileData(string input)
  199. {
  200. return OrderedByName.FirstOrDefault(x =>
  201. {
  202. return x.Key.IndexOf(ArchiveLangFolder, StringComparison.OrdinalIgnoreCase) > 1 && x.Key.IndexOf(input, StringComparison.OrdinalIgnoreCase) > -1;
  203. });
  204. }
  205. private Stream Uncompress(StreamWithRangeValues @in, out long offset)
  206. {
  207. offset = 0;
  208. if (@in == null) return null;
  209. byte[] buffer;
  210. byte[] open(int skip = 0)
  211. {
  212. @in.Seek(@in.Offset + skip, SeekOrigin.Begin);
  213. using (var br = new BinaryReader(@in))
  214. return br.ReadBytes(checked((int)(@in.Size - skip)));
  215. }
  216. if (@in.Compression > 0)
  217. switch (@in.Compression)
  218. {
  219. case CompressionType.LZSS:
  220. buffer = open();
  221. var compressedSize = BitConverter.ToInt32(buffer, 0);
  222. if (compressedSize != buffer.Length - sizeof(int))
  223. throw new InvalidDataException($"{nameof(ArchiveMap)}::{nameof(Uncompress)} buffer size incorrect ({compressedSize}) != ({buffer.Length - sizeof(int)})");
  224. return new MemoryStream(LZSS.DecompressAllNew(buffer, @in.UncompressedSize, true));
  225. case CompressionType.LZ4:
  226. buffer = open();
  227. return new MemoryStream(Lz4Uncompress(buffer, @in.UncompressedSize));
  228. }
  229. offset = @in.Offset;
  230. return @in;
  231. }
  232. #endregion Methods
  233. }
  234. }