diff options
| author | Jason <[email protected]> | 2017-07-23 18:21:46 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2017-07-23 18:21:46 +0200 |
| commit | d42569b3e37e0a03835c7965d67cdf19e14ad4ba (patch) | |
| tree | 98e163375aa4c5ae750df9f0ff5ded06fb702429 /TimeHACK.Engine | |
| parent | bc9623bbdf635cc16cc95f8a86a241a6933a8fdd (diff) | |
| parent | 36931dc9860a6f1f3cf6f7b238dfda3b3c730554 (diff) | |
| download | histacom2-d42569b3e37e0a03835c7965d67cdf19e14ad4ba.tar.gz histacom2-d42569b3e37e0a03835c7965d67cdf19e14ad4ba.tar.bz2 histacom2-d42569b3e37e0a03835c7965d67cdf19e14ad4ba.zip | |
Merge pull request #134 from RogueAI42/master
Binary Saves
Diffstat (limited to 'TimeHACK.Engine')
| -rw-r--r-- | TimeHACK.Engine/SaveSystem.cs | 370 |
1 files changed, 327 insertions, 43 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; } } |
