Saves.cs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text.RegularExpressions;
  6. namespace OpenVIII
  7. {
  8. /// <summary>
  9. /// parse data from save game files
  10. /// </summary>
  11. /// <see cref="http://wiki.ffrtt.ru/index.php/FF8/GameSaveFormat#The_save_format"/>
  12. /// <seealso cref="https://github.com/myst6re/hyne"/>
  13. /// <seealso cref="https://github.com/myst6re/hyne/blob/master/SaveData.h"/>
  14. /// <seealso cref="https://cdn.discordapp.com/attachments/552838120895283210/570733614656913408/ff8_save.zip"/>
  15. /// <remarks>
  16. /// antiquechrono was helping. he even wrote a whole class using kaitai. Though I donno if we
  17. /// wanna use kaitai. LordUrQuan helped get info on CD2000 version.
  18. /// </remarks>
  19. public static partial class Saves
  20. {
  21. #region Fields
  22. private const int GamesPerSlot = 30;
  23. private const int Slots = 2;
  24. private const int SteamOffset = 0x184;
  25. #endregion Fields
  26. #region Properties
  27. /// <summary>
  28. /// FF8DIR\Saves
  29. /// </summary>
  30. public static string CD2000Folder { get; private set; }
  31. public static Data[,] FileList { get; private set; }
  32. /// <summary>
  33. /// Documents\Square Enix\FINAL FANTASY VIII Steam\user_#######
  34. /// </summary>
  35. public static string Steam2013Folder { get; private set; }
  36. /// <summary>
  37. /// Documents\My Games\FINAL FANTASY VIII Remastered\Steam\#################\game_data\user\saves
  38. /// </summary>
  39. public static string Steam2019Folder { get; private set; }
  40. #endregion Properties
  41. #region Methods
  42. public static void Init()
  43. {
  44. Memory.Log.WriteLine($"{nameof(Saves)} :: {nameof(Init)}");
  45. FileList = new Data[Slots, GamesPerSlot];
  46. CD2000Folder = Path.Combine(Memory.FF8Dir, "Save");
  47. Memory.Log.WriteLine($"{nameof(Saves)} :: {nameof(CD2000Folder)} :: {CD2000Folder}");
  48. Steam2013Folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Square Enix", "FINAL FANTASY VIII Steam");
  49. Memory.Log.WriteLine($"{nameof(Saves)} :: {nameof(Steam2013Folder)} :: {Steam2013Folder}");
  50. Steam2019Folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "My Games", "FINAL FANTASY VIII Remastered", "Steam");
  51. Memory.Log.WriteLine($"{nameof(Saves)} :: {nameof(Steam2019Folder)} :: {Steam2019Folder}");
  52. if (Directory.Exists(Steam2013Folder))
  53. {
  54. var dirs = Directory.GetDirectories(Steam2013Folder);
  55. if (dirs.Length > 0)
  56. {
  57. var SteamFolders = Directory.GetDirectories(Steam2013Folder);
  58. if (SteamFolders.Length > 0)
  59. {
  60. Steam2013Folder = SteamFolders[0];
  61. GetFiles(Steam2013Folder, @"slot(\d+)_save(\d+).ff8$");
  62. }
  63. }
  64. }
  65. else if (Directory.Exists(CD2000Folder))
  66. {
  67. ProcessFiles(Directory.GetFiles(CD2000Folder, "*", SearchOption.AllDirectories), @"Slot(\d+)[\\/]save(\d+)$");
  68. }
  69. else if (Directory.Exists(Steam2019Folder))
  70. {
  71. var dirs = Directory.GetDirectories(Steam2019Folder);
  72. if (dirs.Length > 0)
  73. {
  74. var SteamFolders = Directory.GetDirectories(Steam2019Folder);
  75. if (SteamFolders.Length > 0)
  76. {
  77. Steam2019Folder = Path.Combine(SteamFolders[0], "game_data", "user", "saves");
  78. GetFiles(Steam2019Folder, @"slot(\d+)_save(\d+).ff8$");
  79. }
  80. }
  81. }
  82. }
  83. private static void GetFiles(string dir, string regex) => ProcessFiles(Directory.EnumerateFiles(dir), regex);
  84. private static void ProcessFiles(IEnumerable<string> files, string pattern)
  85. {
  86. //List<Action> actions = new List<Action>();
  87. var regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
  88. var actions = files.Select(x => new { file = x, match = regex.Match(x) })
  89. .Where(x => x.match.Success && x.match.Groups.Count >= 3)
  90. .Select(x =>
  91. {
  92. if (!int.TryParse(x.match.Groups[1].Value, out var slot) || !int.TryParse(x.match.Groups[2].Value, out var game))
  93. {
  94. slot = -1;
  95. game = -1;
  96. }
  97. return new { x.file, slot = slot - 1, game = game - 1 };
  98. })
  99. .Where(x => x.slot < Slots && x.game < GamesPerSlot && x.game >= 0 && x.slot >= 0)
  100. .Select(x => new Action(() => { Read(x.file, out FileList[x.slot, x.game]); }));
  101. Memory.ProcessActions(actions.ToArray());
  102. }
  103. private static void Read(string file, out Data d)
  104. {
  105. d = null;
  106. var size = 0;
  107. Memory.Log.WriteLine($"{nameof(Saves)}::{nameof(Read)} Extracting: {file}");
  108. byte[] decmp = null;
  109. MemoryStream ms = null;
  110. FileStream fs = null;
  111. // fs is disposed by binaryreader.
  112. using (var br = new BinaryReader(
  113. fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
  114. {
  115. if (fs.Length >= 5)
  116. {
  117. size = br.ReadInt32();
  118. if ((int)fs.Length - sizeof(uint) == size)
  119. {
  120. var tmp = br.ReadBytes((int)fs.Length - sizeof(uint));
  121. decmp = LZSS.DecompressAllNew(tmp, 0);
  122. }
  123. }
  124. fs = null;
  125. }
  126. if (decmp == null)
  127. {
  128. Memory.Log.WriteLine($"{nameof(Saves)}::{nameof(Read)} Invalid file: {file}");
  129. }
  130. else
  131. using (var br = new BinaryReader(ms = new MemoryStream(decmp)))
  132. {
  133. ms.Seek(SteamOffset, SeekOrigin.Begin);
  134. d = new Data();
  135. d.Read(br);
  136. ms = null;
  137. }
  138. }
  139. #endregion Methods
  140. }
  141. }