diff options
| -rw-r--r-- | TimeHACK.Engine/SaveSystem.cs | 370 | ||||
| -rw-r--r-- | TimeHACK.Main/SaveDialogs/SaveFileTroubleShooter.cs | 27 |
2 files changed, 342 insertions, 55 deletions
diff --git a/TimeHACK.Engine/SaveSystem.cs b/TimeHACK.Engine/SaveSystem.cs index 81aa322..cda4464 100644 --- a/TimeHACK.Engine/SaveSystem.cs +++ b/TimeHACK.Engine/SaveSystem.cs @@ -1,4 +1,10 @@ -using System; +// Define BINARY_SAVE before release so the player has +// to put some effort into cheating ;) +// During development, leave it undefined to use the +// easily modifiable JSON serialised format +//#define BINARY_SAVE + +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,6 +13,7 @@ using System.Threading.Tasks; using Newtonsoft.Json; using System.Diagnostics; using System.Windows.Forms; +using System.Runtime.CompilerServices; namespace TimeHACK.Engine { @@ -19,6 +26,11 @@ namespace TimeHACK.Engine public static Theme currentTheme { get; set; } +#if BINARY_SAVE + private static readonly byte[] magic = Encoding.UTF8.GetBytes("THSv"); + private static readonly IOrderedEnumerable<System.Reflection.PropertyInfo> properties = typeof(Save).GetProperties().OrderBy(p => (p.GetCustomAttributes(typeof(OrderAttribute), false).SingleOrDefault() as OrderAttribute).Order); +#endif + public static string GameDirectory { get @@ -102,36 +114,6 @@ namespace TimeHACK.Engine } } - public static bool LoadSave() - { - try - { - // ON A FINAL RELEASE USE THE "FINAL RELEASE THINGS" - #region Final Release Things - //Read base64 string from file - //string b64 = File.ReadAllText(Path.Combine(ProfileDirectory, ProfileFile)); - //Get Unicode byte array - //byte[] bytes = Convert.FromBase64String(b64); - //Decode the Unicode - //string json = Encoding.UTF8.GetString(bytes); - //Deserialize save object. - #endregion - // USE THE THINGS IN THE "DEVELOPER THINGS" FOR A DEVELOPMENT RELEASE - #region Developer Things - string json = File.ReadAllText(Path.Combine(ProfileDirectory, ProfileFile)); - #endregion - CurrentSave = JsonConvert.DeserializeObject<Save>(json); - - } catch - { - MessageBox.Show("WARNING! It looks like this save is corrupt!"); - MessageBox.Show("We will now open the Save troubleshooter"); - - troubleshooter.ShowDialog(); - } - return true; - } - public static void NewGame() { var save = new Save(); @@ -247,21 +229,297 @@ namespace TimeHACK.Engine File.WriteAllText(Path.Combine(directory, "_data.info"), toWrite); } +#if BINARY_SAVE + // Be careful with this... it trusts that the calling code has already checked + // that T can be written by BinaryWriter. + // No generics, because that'd be near-impossible to read back. + private static void WriteList<T>(BinaryWriter write, List<T> list) + { + if (list == null) + write.Write(0); + else + { + write.Write(list.Count); + foreach (T obj in list) + ((dynamic)write).Write(obj); + } + } + + private static List<T> ReadList<T>(BinaryReader read, string reader) + { + int count = read.ReadInt32(); + var ret = new List<T>(count); + var function = typeof(BinaryReader).GetMethod(reader); + for (int i = 0; i < count; i++) + ret.Add((T) function.Invoke(read, new object[] { })); + return ret; + } + + private static void WriteBitfield(Stream fobj, IEnumerable<bool> bools) + { + sbyte bit = 7; + int cur = 0; + var bitfields = new byte[bools.Count() / 8 + 1]; + foreach (bool mybool in bools) + { + if (mybool) + bitfields[cur] |= (byte) (1 << bit); + bit--; + if (bit < 0) + { + bit = 7; + cur++; + } + } + fobj.Write(bitfields, 0, bitfields.Length); + } + + private static List<bool> ReadBitfield(Stream fobj, int count) + { + sbyte bit = 7; + int cur = 0; + var bitfields = new byte[count / 8 + 1]; + var bools = new List<bool>(count); + byte val = (byte) fobj.ReadByte(); + fobj.Read(bitfields, 0, bitfields.Length); + for (int i = 0; i < count; i++) + { + bools.Add(((val >> bit) & 1) == 1); + bit--; + if (bit < 0) + { + bit = 7; + cur++; + } + } + return bools; + } +#endif + + public static Save ReadSave(string fname) + { +#if BINARY_SAVE + using (var fobj = File.OpenRead(fname)) + { + var save = new Save(); + var header = new byte[magic.Length]; + var read = new BinaryReader(fobj); + fobj.Read(header, 0, magic.Length); + if (!magic.SequenceEqual(header)) + throw new InvalidDataException("This is not a TimeHACK binary save"); + int numprops = read.ReadInt32(); + var bools = new List<System.Reflection.PropertyInfo>(); + // Holy code duplication, Batman. + // If you know a better way to get C# to do this, I'm all ears. + foreach (var property in properties.Take(numprops)) + { + if (property.PropertyType == typeof(string)) + property.SetValue(save, read.ReadString()); + else if (property.PropertyType == typeof(int)) + property.SetValue(save, read.ReadInt32()); + else if (property.PropertyType == typeof(uint)) + property.SetValue(save, read.ReadUInt32()); + else if (property.PropertyType == typeof(long)) + property.SetValue(save, read.ReadInt64()); + else if (property.PropertyType == typeof(ulong)) + property.SetValue(save, read.ReadUInt64()); + else if (property.PropertyType == typeof(short)) + property.SetValue(save, read.ReadInt16()); + else if (property.PropertyType == typeof(ushort)) + property.SetValue(save, read.ReadUInt16()); + else if (property.PropertyType == typeof(byte)) + property.SetValue(save, read.ReadByte()); + else if (property.PropertyType == typeof(sbyte)) + property.SetValue(save, read.ReadSByte()); + else if (property.PropertyType == typeof(char)) + property.SetValue(save, read.ReadChar()); + else if (property.PropertyType == typeof(float)) + property.SetValue(save, read.ReadSingle()); + else if (property.PropertyType == typeof(double)) + property.SetValue(save, read.ReadDouble()); + else if (property.PropertyType == typeof(decimal)) + property.SetValue(save, read.ReadDecimal()); + + else if (property.PropertyType == typeof(List<string>)) + property.SetValue(save, ReadList<string>(read, "ReadString")); + else if (property.PropertyType == typeof(List<int>)) + property.SetValue(save, ReadList<string>(read, "ReadInt32")); + else if (property.PropertyType == typeof(List<uint>)) + property.SetValue(save, ReadList<string>(read, "ReadUInt32")); + else if (property.PropertyType == typeof(List<long>)) + property.SetValue(save, ReadList<string>(read, "ReadInt64")); + else if (property.PropertyType == typeof(List<ulong>)) + property.SetValue(save, ReadList<string>(read, "ReadUInt64")); + else if (property.PropertyType == typeof(List<short>)) + property.SetValue(save, ReadList<string>(read, "ReadInt16")); + else if (property.PropertyType == typeof(List<ushort>)) + property.SetValue(save, ReadList<string>(read, "ReadUInt16")); + else if (property.PropertyType == typeof(List<byte>)) + property.SetValue(save, ReadList<string>(read, "ReadByte")); + else if (property.PropertyType == typeof(List<sbyte>)) + property.SetValue(save, ReadList<string>(read, "ReadSByte")); + else if (property.PropertyType == typeof(List<char>)) + property.SetValue(save, ReadList<string>(read, "ReadChar")); + else if (property.PropertyType == typeof(List<float>)) + property.SetValue(save, ReadList<string>(read, "ReadSingle")); + else if (property.PropertyType == typeof(List<double>)) + property.SetValue(save, ReadList<string>(read, "ReadDouble")); + else if (property.PropertyType == typeof(List<decimal>)) + property.SetValue(save, ReadList<string>(read, "ReadDecimal")); + + // Remember to read this boolean from the bitfield at the end. + else if (property.PropertyType == typeof(bool)) + bools.Add(property); + + else if (property.PropertyType == typeof(List<bool>)) + property.SetValue(save, ReadBitfield(fobj, read.ReadInt32())); + + // RIP + else + throw new InvalidDataException("There is no deserialisation method specified for " + property.PropertyType.ToString()); + } + + // Let's read the ultra tiny bitfield. + var loaded = ReadBitfield(fobj, bools.Count); + foreach (var item in bools.Zip(loaded, (p, b) => new { Property = p, Value = b })) + item.Property.SetValue(save, item.Value); + + return save; + } +#else + return JsonConvert.DeserializeObject<Save>(File.ReadAllText(fname)); +#endif + } + + public static void WriteSave(string fname, Save save) + { +#if BINARY_SAVE + using (var fobj = File.OpenWrite(fname)) + { + var write = new BinaryWriter(fobj); + var bools = new List<bool>(); + fobj.Write(magic, 0, magic.Length); + write.Write(properties.Count()); // The number of properties basically acts as the version number. + + foreach (var property in properties) + { + if (property == null) + continue; + + // Types that can be written by BinaryWriter, except booleans. + if (property.PropertyType == typeof(string)) + { + var val = property.GetValue(save) as string; + if (val == null) + write.Write(""); + else + write.Write(val); + } + else if (property.PropertyType == typeof(int)) + write.Write((int) property.GetValue(save)); + else if (property.PropertyType == typeof(uint)) + write.Write((uint) property.GetValue(save)); + else if (property.PropertyType == typeof(long)) + write.Write((long) property.GetValue(save)); + else if (property.PropertyType == typeof(ulong)) + write.Write((ulong) property.GetValue(save)); + else if (property.PropertyType == typeof(short)) + write.Write((short) property.GetValue(save)); + else if (property.PropertyType == typeof(ushort)) + write.Write((ushort) property.GetValue(save)); + else if (property.PropertyType == typeof(byte)) + write.Write((byte) property.GetValue(save)); + else if (property.PropertyType == typeof(sbyte)) + write.Write((sbyte) property.GetValue(save)); + else if (property.PropertyType == typeof(char)) + write.Write((char) property.GetValue(save)); + else if (property.PropertyType == typeof(float)) + write.Write((float) property.GetValue(save)); + else if (property.PropertyType == typeof(double)) + write.Write((double) property.GetValue(save)); + else if (property.PropertyType == typeof(decimal)) + write.Write((double) property.GetValue(save)); + + // ... and their lists. + else if (property.PropertyType == typeof(List<string>)) + WriteList(write, property.GetValue(save) as List<string>); + else if (property.PropertyType == typeof(List<int>)) + WriteList(write, property.GetValue(save) as List<int>); + else if (property.PropertyType == typeof(List<uint>)) + WriteList(write, property.GetValue(save) as List<uint>); + else if (property.PropertyType == typeof(List<long>)) + WriteList(write, property.GetValue(save) as List<long>); + else if (property.PropertyType == typeof(List<ulong>)) + WriteList(write, property.GetValue(save) as List<ulong>); + else if (property.PropertyType == typeof(List<short>)) + WriteList(write, property.GetValue(save) as List<short>); + else if (property.PropertyType == typeof(List<ushort>)) + WriteList(write, property.GetValue(save) as List<ushort>); + else if (property.PropertyType == typeof(List<byte>)) + WriteList(write, property.GetValue(save) as List<byte>); + else if (property.PropertyType == typeof(List<sbyte>)) + WriteList(write, property.GetValue(save) as List<sbyte>); + else if (property.PropertyType == typeof(List<char>)) + WriteList(write, property.GetValue(save) as List<char>); + else if (property.PropertyType == typeof(List<float>)) + WriteList(write, property.GetValue(save) as List<float>); + else if (property.PropertyType == typeof(List<double>)) + WriteList(write, property.GetValue(save) as List<double>); + else if (property.PropertyType == typeof(List<decimal>)) + WriteList(write, property.GetValue(save) as List<decimal>); + + // Booleans - they go in the bitfield at the end. + else if (property.PropertyType == typeof(bool)) + bools.Add((bool) property.GetValue(save)); + + // List of booleans - it gets its own bitfield. + else if (property.PropertyType == typeof(List<bool>)) + { + var val = property.GetValue(save) as List<bool>; + if (val == null) + write.Write(0); + else + { + write.Write(val.Count()); + WriteBitfield(fobj, val); + } + } + + // Now what? + else + throw new InvalidDataException("There is no serialisation method specified for " + property.PropertyType.ToString()); + } + + // In order to save space, we store bools in a bitfield at the end. + // One byte can store 8 bools, saving a whopping 7 bytes which can then be used for + // extremely short text documents or something. + WriteBitfield(fobj, bools); + } +#else + // Serialize the save to JSON. + File.WriteAllText(fname, JsonConvert.SerializeObject(save, Formatting.Indented)); +#endif + } public static void SaveGame() { - //Serialize the save to JSON. - string json = JsonConvert.SerializeObject(CurrentSave, Formatting.Indented); - - // ADD THE TWO LINES OF CODE BELOW ON A FINAL RELEASE - //Get JSON bytes (Unicode format). - //var bytes = Encoding.UTF8.GetBytes(json); - //Encode the array into Base64. - //string b64 = Convert.ToBase64String(bytes); - //Write to disk. - - // CHANGE THE "JSON" TO "B64" ON A FINAL RELEASE! - File.WriteAllText(Path.Combine(ProfileDirectory, ProfileFile), json); + WriteSave(Path.Combine(ProfileDirectory, ProfileFile), CurrentSave); + } + + public static bool LoadSave() + { + string savefile = Path.Combine(ProfileDirectory, ProfileFile); + try + { + CurrentSave = ReadSave(savefile); + } + catch + { + MessageBox.Show("WARNING! It looks like this save is corrupt! We will now open the Save troubleshooter"); + + troubleshooter.ShowDialog(); + } + return true; } public static byte[] GetAchievements() @@ -295,14 +553,40 @@ namespace TimeHACK.Engine } } + + // This lets us preserve the order of properties. + // Thanks to "ghord" from StackOverflow. + public sealed class OrderAttribute : Attribute + { + private readonly int order_; + public OrderAttribute([CallerLineNumber]int order = 0) + { + order_ = order; + } + public int Order { get { return order_; } } + } + public class Save { + // To maintain binary save compatibility, + // add all new properties to the end and don't remove any. + // Also, every property needs an "Order" attribute. + + [Order] public string Username { get; set; } + [Order] public string CurrentOS { get; set; } + // public Dictionary<string, bool> InstalledPrograms { get; set; } InstallProgram is no longer needed... we have that data in the FileSystem + + [Order] public List<string> ExperiencedStories { get; set; } + + [Order] public bool FTime95 { get; set; } + + [Order] public string ThemeName { get; set; } } diff --git a/TimeHACK.Main/SaveDialogs/SaveFileTroubleShooter.cs b/TimeHACK.Main/SaveDialogs/SaveFileTroubleShooter.cs index 5ec84be..410d2d6 100644 --- a/TimeHACK.Main/SaveDialogs/SaveFileTroubleShooter.cs +++ b/TimeHACK.Main/SaveDialogs/SaveFileTroubleShooter.cs @@ -15,7 +15,7 @@ namespace TimeHACK.SaveDialogs public partial class SaveFileTroubleShooter : Form { public string log; - Save savedata = new Save(); + Save savedata; string json; public SaveFileTroubleShooter() { @@ -43,7 +43,9 @@ namespace TimeHACK.SaveDialogs // Check if the main.save file exists - if (!File.Exists(Path.Combine(SaveSystem.ProfileDirectory, "main.save"))) + string savefile = Path.Combine(SaveSystem.ProfileDirectory, "main.save"); + + if (!File.Exists(savefile)) { WriteToLog("ISSUE FOUND! File main.save doesn't exist"); @@ -58,12 +60,9 @@ namespace TimeHACK.SaveDialogs } else { WriteToLog("File main.save does exist - checking contents"); - // Read the main.save file - json = File.ReadAllText(Path.Combine(SaveSystem.ProfileDirectory, "main.save")); - try { - savedata = Newtonsoft.Json.JsonConvert.DeserializeObject<Save>(json); + savedata = SaveSystem.ReadSave(savefile); } catch { @@ -71,16 +70,18 @@ namespace TimeHACK.SaveDialogs WriteToLog("Sorry, there is no repairing it easily, your data will be lost"); - if (Directory.Exists(Path.Combine(SaveSystem.ProfileDirectory, "main.backup"))) Directory.Delete(Path.Combine(SaveSystem.ProfileDirectory, "main.backup")); + string backupfile = Path.Combine(SaveSystem.ProfileDirectory, "main.backup"); - File.Copy(Path.Combine(SaveSystem.ProfileDirectory, "main.save"), Path.Combine(SaveSystem.ProfileDirectory, "main.backup")); + if (Directory.Exists(backupfile)) Directory.Delete(backupfile); + + File.Copy(savefile, backupfile); SaveSystem.NewGame(); // Make sure the username is set SaveSystem.CurrentSave.Username = SaveSystem.ProfileName; - WriteToLog($"The corrupt file has been stored in {Path.Combine(SaveSystem.ProfileDirectory, "main.backup")}"); + WriteToLog($"The corrupt file has been stored in {backupfile}"); EndScan(true); } @@ -108,10 +109,12 @@ namespace TimeHACK.SaveDialogs } } - if (!Directory.Exists(Path.Combine(SaveSystem.ProfileDirectory, "folders"))) + string folderspath = Path.Combine(SaveSystem.ProfileDirectory, "folders"); + + if (!Directory.Exists(folderspath)) { WriteToLog("ISSUE FOUND! Directory 'folders' doesn't exist! Creating one..."); - Directory.CreateDirectory(Path.Combine(SaveSystem.ProfileDirectory, "folders")); + Directory.CreateDirectory(folderspath); SaveSystem.CheckFiles(); } @@ -130,7 +133,7 @@ namespace TimeHACK.SaveDialogs // Set the main.save file to the resolved one - File.WriteAllText(Path.Combine(SaveSystem.ProfileDirectory, "main.save"), Newtonsoft.Json.JsonConvert.SerializeObject(savedata, Newtonsoft.Json.Formatting.Indented)); + SaveSystem.WriteSave(Path.Combine(SaveSystem.ProfileDirectory, "main.save"), savedata); textBox1.Text = log; } else { |
