aboutsummaryrefslogtreecommitdiff
path: root/TimeHACK.Engine
diff options
context:
space:
mode:
authorRogueAI42 <[email protected]>2017-07-23 21:46:02 +1000
committerRogueAI42 <[email protected]>2017-07-23 21:46:02 +1000
commitc446a76e0c5b7b35638f1e223ad167b46f30941e (patch)
tree98e163375aa4c5ae750df9f0ff5ded06fb702429 /TimeHACK.Engine
parent5afd2ec6c6d7c458c1caffc55566d4365602c122 (diff)
downloadhistacom2-c446a76e0c5b7b35638f1e223ad167b46f30941e.tar.gz
histacom2-c446a76e0c5b7b35638f1e223ad167b46f30941e.tar.bz2
histacom2-c446a76e0c5b7b35638f1e223ad167b46f30941e.zip
Binary Saves
This replaces the Base64 encoded save format with a new binary one. It's tiny. It's hard to modify. It shouldn't get in the way much - you can break binary compatibility before release if you want, but to preserve it, all you need to do is: * Not remove properties from the save format * Add new properties AFTER all existing ones * Give every property the [Order] decorator/attribute You don't need to touch the save/load code unless you're adding a type it doesn't recognise. Also, the SaveSystem provides the ReadSave() and WriteSave() functions now, so other components of TimeHACK don't need to care about the actual save format.
Diffstat (limited to 'TimeHACK.Engine')
-rw-r--r--TimeHACK.Engine/SaveSystem.cs370
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; }
}