diff --git a/build/common.props b/build/common.props index 978129b5..92ec1ea5 100644 --- a/build/common.props +++ b/build/common.props @@ -3,7 +3,7 @@ - 1.0.9 + 1.0.10 2.0.2 diff --git a/changelog.txt b/changelog.txt index bc3c1f42..070947fa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,8 @@ --------------------------------------------------------------------------------------------------- +Version: 1.0.10 +Game Versions: e1.4.3,e1.5.0,e1.5.1,e1.5.2,e1.5.3 +* Switched serialization from binary to json +--------------------------------------------------------------------------------------------------- Version: 1.0.9 Game Versions: e1.4.3,e1.5.0,e1.5.1,e1.5.2,e1.5.3 * Better nullable handling diff --git a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/BehaviorManagers/CampaignDescriptorManager.cs b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/BehaviorManagers/CampaignDescriptorManager.cs index 75f93a30..00518267 100644 --- a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/BehaviorManagers/CampaignDescriptorManager.cs +++ b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/BehaviorManagers/CampaignDescriptorManager.cs @@ -1,11 +1,11 @@ using Bannerlord.ButterLib.CampaignIdentifier; -using Bannerlord.ButterLib.Implementation.Common; +using Bannerlord.ButterLib.Common.Extensions; + +using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.Serialization.Formatters; -using System.Runtime.Serialization.Formatters.Binary; using TaleWorlds.CampaignSystem; using TaleWorlds.Core; @@ -26,20 +26,17 @@ internal sealed class CampaignDescriptorManager private const string InquiryLowerBody = "{=zed49rdkQR}Select '{NEW_ID}' if the loaded save is a separate campaign that has nothing to do with the suggested options and a new ID should be assigned to it."; - private static readonly string ExistingCampaignDescriptorsLogFile = Path.Combine(Utilities.GetConfigsPath(), "ButterLib", "CampaignIdentifier", "ExistingCampaignIdentifiers.bin"); - + private ICampaignDescriptorProvider? _campaignDescriptorSerializer; + private ICampaignDescriptorProvider CampaignDescriptorSerializer => _campaignDescriptorSerializer ??= + ButterLibSubModule.Instance?.GetServiceProvider()?.GetRequiredService() ?? new JsonCampaignDescriptorProvider(); [SaveableField(1)] - private CampaignDescriptorImplementation _campaignDescriptor; + private CampaignDescriptorImplementation _campaignDescriptor = null!; // Won't be null when properly accessed. private CampaignDescriptorImplementation? _descriptorToBeAssigned; internal CampaignDescriptorImplementation CampaignDescriptor => _campaignDescriptor; - internal CampaignDescriptorManager() - { - _campaignDescriptor = null!; // Won't be null when properly accessed. - } internal void GenerateNewGameDescriptor() { _campaignDescriptor = new CampaignDescriptorImplementation(Hero.MainHero); @@ -127,35 +124,22 @@ private void UpdateSavedDescriptors() var existingCampaignDescriptors = LoadExistingDescriptors(); existingCampaignDescriptors.Add(_campaignDescriptor); - var path = Path.GetDirectoryName(ExistingCampaignDescriptorsLogFile); - if (!string.IsNullOrEmpty(path) && !Directory.Exists(path)) - { - Directory.CreateDirectory(path!); - } - - using FileStream fileStream = File.OpenWrite(ExistingCampaignDescriptorsLogFile); - var binaryFormatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple, - Binder = new ButterLibSerializationBinder() - }; - binaryFormatter.Serialize(fileStream, existingCampaignDescriptors); + CampaignDescriptorSerializer.Save(existingCampaignDescriptors); } - private static List LoadExistingDescriptors() + private List LoadExistingDescriptors() { - if (!File.Exists(ExistingCampaignDescriptorsLogFile)) + // We are stuck with it for backwards compatibility sake + var binaryFile = Path.Combine(Utilities.GetConfigsPath(), "ButterLib", "CampaignIdentifier", "ExistingCampaignIdentifiers.bin"); + var binaryDescriptors = new List(); + if (File.Exists(binaryFile)) { - return new List(); + var binaryCampaignDescriptorProvider = new BinaryCampaignDescriptorProvider(); + binaryDescriptors.AddRange(binaryCampaignDescriptorProvider.Load()); + File.Delete(binaryFile); } - using FileStream fileStream = File.OpenRead(ExistingCampaignDescriptorsLogFile); - var binaryFormatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple, - Binder = new ButterLibSerializationBinder() - }; - return (List)binaryFormatter.Deserialize(fileStream); + return CampaignDescriptorSerializer.Load().Concat(binaryDescriptors).ToList(); } internal void Sync() diff --git a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/BinaryCampaignDescriptorProvider.cs b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/BinaryCampaignDescriptorProvider.cs new file mode 100644 index 00000000..441e5f7a --- /dev/null +++ b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/BinaryCampaignDescriptorProvider.cs @@ -0,0 +1,101 @@ +using Bannerlord.ButterLib.CampaignIdentifier; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters; +using System.Runtime.Serialization.Formatters.Binary; + +using TaleWorlds.Engine; + +using Path = System.IO.Path; + +namespace Bannerlord.ButterLib.Implementation.CampaignIdentifier.CampaignBehaviors +{ + internal sealed class BinaryCampaignDescriptorProvider : ICampaignDescriptorProvider + { + private static readonly string ExistingCampaignDescriptorsLogFile = + Path.Combine(Utilities.GetConfigsPath(), "ButterLib", "CampaignIdentifier", "ExistingCampaignIdentifiers.bin"); + + public IEnumerable Load() + { + try + { + var path = Path.GetDirectoryName(ExistingCampaignDescriptorsLogFile); + if (string.IsNullOrEmpty(path) || !Directory.Exists(path) || !File.Exists(ExistingCampaignDescriptorsLogFile)) + return Enumerable.Empty(); + + using var fileStream = File.OpenRead(ExistingCampaignDescriptorsLogFile); + var binaryFormatter = new BinaryFormatter + { + AssemblyFormat = FormatterAssemblyStyle.Simple, + Binder = new ButterLibSerializationBinder() + }; + return (List) binaryFormatter.Deserialize(fileStream); + } + catch (Exception e) when (e is SerializationException) + { + return Enumerable.Empty(); + } + } + + public void Save(IEnumerable descriptors) + { + var path = Path.GetDirectoryName(ExistingCampaignDescriptorsLogFile); + if (!string.IsNullOrEmpty(path) && !Directory.Exists(path)) + Directory.CreateDirectory(path!); + + using var fileStream = File.OpenWrite(ExistingCampaignDescriptorsLogFile); + var binaryFormatter = new BinaryFormatter + { + AssemblyFormat = FormatterAssemblyStyle.Simple, + Binder = new ButterLibSerializationBinder() + }; + binaryFormatter.Serialize(fileStream, descriptors.ToList()); + } + + internal sealed class ButterLibSerializationBinder : SerializationBinder + { + public override Type? BindToType(string assemblyName, string typeName) + { + if (assemblyName.StartsWith("Bannerlord.ButterLib.Implementation")) + return typeof(ButterLibSerializationBinder).Assembly.GetType(typeName); + + var type = Type.GetType($"{typeName}, {assemblyName}"); + if (type != null) + return type; + + var tokens = typeName.Split(new [] {"[[", "]]", "],["}, StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 1) + return Type.GetType(typeName, true); + + var generic = tokens[0]; + var genericTypeArgs = new List(); + foreach (var token in tokens.Skip(1)) + { + var (typeName1, assemblyName1) = GetTokenInfo(token); + var type1 = assemblyName1.StartsWith("Bannerlord.ButterLib.Implementation") + ? typeof(ButterLibSerializationBinder).Assembly.GetType(typeName1) + : Type.GetType($"{typeName1}, {assemblyName1}", true); + genericTypeArgs.Add(type1.AssemblyQualifiedName); + } + + return Type.GetType($"{generic}[[{string.Join("],[", genericTypeArgs)}]]", true); + } + + private static (string TypeName, string AssemblyName) GetTokenInfo(string str) + { + var split = str.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + return (split[0].Trim(), string.Join(",", split.Skip(1)).Trim()); + } + + public override void BindToName(Type serializedType, out string assemblyName, out string typeName) + { + assemblyName = "Bannerlord.ButterLib.Implementation"; + typeName = serializedType.FullName!; + } + } + } +} \ No newline at end of file diff --git a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/CampaignIdentifierBehavior.cs b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/CampaignIdentifierBehavior.cs index 36eb59e7..1107483f 100644 --- a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/CampaignIdentifierBehavior.cs +++ b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/CampaignIdentifierBehavior.cs @@ -12,7 +12,7 @@ namespace Bannerlord.ButterLib.Implementation.CampaignIdentifier.CampaignBehavio /// Custom behavior used by CampaignIdentifier. internal sealed class CampaignIdentifierBehavior : CampaignBehaviorBase { - private CampaignDescriptorManager _descriptorManager; + private CampaignDescriptorManager _descriptorManager = new CampaignDescriptorManager(); /// Alphanumeric campaign ID. public string CampaignId => _descriptorManager.CampaignDescriptor.KeyValue; @@ -20,12 +20,6 @@ internal sealed class CampaignIdentifierBehavior : CampaignBehaviorBase /// object corresponding with the campaign. public CampaignDescriptorImplementation CampaignDescriptor => _descriptorManager.CampaignDescriptor; - /// Initializes a new instance of the . - public CampaignIdentifierBehavior() - { - _descriptorManager = new CampaignDescriptorManager(); - } - public override void RegisterEvents() { CampaignIdentifierEvents.OnDescriptorRelatedDataChangedEvent.AddNonSerializedListener(this, UpdateCampaignDescriptorOnCharacterOrClanModified); diff --git a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/JsonCampaignDescriptorProvider.cs b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/JsonCampaignDescriptorProvider.cs new file mode 100644 index 00000000..72988da7 --- /dev/null +++ b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignBehaviors/JsonCampaignDescriptorProvider.cs @@ -0,0 +1,61 @@ +using Bannerlord.ButterLib.CampaignIdentifier; + +using Newtonsoft.Json; + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using TaleWorlds.Engine; + +using Path = System.IO.Path; + +namespace Bannerlord.ButterLib.Implementation.CampaignIdentifier.CampaignBehaviors +{ + internal sealed class JsonCampaignDescriptorProvider : ICampaignDescriptorProvider + { + private static readonly string ExistingCampaignDescriptorsLogFile = + Path.Combine(Utilities.GetConfigsPath(), "ButterLib", "CampaignIdentifier", "Existing.json"); + + public IEnumerable Load() + { + var path = Path.GetDirectoryName(ExistingCampaignDescriptorsLogFile); + if (string.IsNullOrEmpty(path) || !Directory.Exists(path) || !File.Exists(ExistingCampaignDescriptorsLogFile)) + return Enumerable.Empty(); + + var success = true; + var settings = new JsonSerializerSettings + { + Error = (sender, args) => { success = false; args.ErrorContext.Handled = true; }, + MissingMemberHandling = MissingMemberHandling.Error + }; + using var fileStream = File.OpenRead(ExistingCampaignDescriptorsLogFile); + var buffer = ReadFully(fileStream); + var result = JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(buffer), settings); + return success ? result : Enumerable.Empty(); + } + + public void Save(IEnumerable descriptors) + { + var path = Path.GetDirectoryName(ExistingCampaignDescriptorsLogFile); + if (!string.IsNullOrEmpty(path) && !Directory.Exists(path)) + Directory.CreateDirectory(path!); + + using var fileStream = File.OpenWrite(ExistingCampaignDescriptorsLogFile); + using var writer = new StreamWriter(fileStream); + var buffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(descriptors, Formatting.Indented)); + fileStream.Write(buffer, 0, buffer.Length); + } + + private static byte[] ReadFully(Stream input) + { + var buffer = new byte[16*1024]; + using MemoryStream ms = new MemoryStream(); + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + ms.Write(buffer, 0, read); + return ms.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignDescriptorImplementation.cs b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignDescriptorImplementation.cs index ffc70117..28838b0a 100644 --- a/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignDescriptorImplementation.cs +++ b/src/Bannerlord.ButterLib.Implementation/CampaignIdentifier/CampaignDescriptorImplementation.cs @@ -21,8 +21,7 @@ namespace Bannerlord.ButterLib.Implementation.CampaignIdentifier /// Also stores some general description on the campaign, based on a specified hero /// (default is the initial player character in the current campaign). /// - [Serializable] - [SaveableClass(1)] + [SaveableClass(1), Serializable] internal sealed class CampaignDescriptorImplementation : CampaignDescriptor, ISerializable { //Consts @@ -101,6 +100,7 @@ internal CampaignDescriptorImplementation(string value, Dictionary) info.GetValue("DecriptorAttributes", typeof(Dictionary)); _baseHero = null!; // Serialization will do it's thing. } @@ -110,6 +110,7 @@ private CampaignDescriptorImplementation(SerializationInfo info, StreamingContex public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(nameof(KeyValue), _value); + // Do not fix typo info.AddValue("DecriptorAttributes", _attributes); } diff --git a/src/Bannerlord.ButterLib.Implementation/Common/ButterLibSerializationBinder.cs b/src/Bannerlord.ButterLib.Implementation/Common/ButterLibSerializationBinder.cs deleted file mode 100644 index 39b4dabf..00000000 --- a/src/Bannerlord.ButterLib.Implementation/Common/ButterLibSerializationBinder.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; - -namespace Bannerlord.ButterLib.Implementation.Common -{ - internal sealed class ButterLibSerializationBinder : SerializationBinder - { - public override Type? BindToType(string assemblyName, string typeName) - { - if (assemblyName.StartsWith("Bannerlord.ButterLib.Implementation")) - return typeof(ButterLibSerializationBinder).Assembly.GetType(typeName); - - var type = Type.GetType($"{typeName}, {assemblyName}"); - if (type != null) - return type; - - var tokens = typeName.Split(new [] {"[[", "]]", "],["}, StringSplitOptions.RemoveEmptyEntries); - if (tokens.Length == 1) - return Type.GetType(typeName, true); - - var generic = tokens[0]; - var genericTypeArgs = new List(); - foreach (var token in tokens.Skip(1)) - { - var (typeName1, assemblyName1) = GetTokenInfo(token); - var type1 = assemblyName1.StartsWith("Bannerlord.ButterLib.Implementation") - ? typeof(ButterLibSerializationBinder).Assembly.GetType(typeName1) - : Type.GetType($"{typeName1}, {assemblyName1}", true); - genericTypeArgs.Add(type1.AssemblyQualifiedName); - } - - return Type.GetType($"{generic}[[{string.Join("],[", genericTypeArgs)}]]", true); - } - - private static (string TypeName, string AssemblyName) GetTokenInfo(string str) - { - var split = str.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); - return (split[0].Trim(), string.Join(",", split.Skip(1)).Trim()); - } - - public override void BindToName(Type serializedType, out string assemblyName, out string typeName) - { - assemblyName = "Bannerlord.ButterLib.Implementation"; - typeName = serializedType.FullName!; - } - } -} \ No newline at end of file diff --git a/src/Bannerlord.ButterLib.Implementation/SubModule.cs b/src/Bannerlord.ButterLib.Implementation/SubModule.cs index 003488c0..13294f42 100644 --- a/src/Bannerlord.ButterLib.Implementation/SubModule.cs +++ b/src/Bannerlord.ButterLib.Implementation/SubModule.cs @@ -51,6 +51,7 @@ protected override void OnSubModuleLoad() services.AddScoped(typeof(DistanceMatrix<>), typeof(DistanceMatrixImplementation<>)); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); DelayedSubModuleManager.Register(); DelayedSubModuleManager.Subscribe( diff --git a/src/Bannerlord.ButterLib/CampaignIdentifier/ICampaignDescriptorProvider.cs b/src/Bannerlord.ButterLib/CampaignIdentifier/ICampaignDescriptorProvider.cs new file mode 100644 index 00000000..a0524ea1 --- /dev/null +++ b/src/Bannerlord.ButterLib/CampaignIdentifier/ICampaignDescriptorProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bannerlord.ButterLib.CampaignIdentifier +{ + public interface ICampaignDescriptorProvider + { + IEnumerable Load(); + void Save(IEnumerable descriptors); + } +} \ No newline at end of file