diff --git a/BannerlordModEditor.Common.Tests/Models/SubModule/SubModuleLoaderTests.cs b/BannerlordModEditor.Common.Tests/Models/SubModule/SubModuleLoaderTests.cs new file mode 100644 index 0000000..de6379c --- /dev/null +++ b/BannerlordModEditor.Common.Tests/Models/SubModule/SubModuleLoaderTests.cs @@ -0,0 +1,176 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using BannerlordModEditor.Common.Models.SubModule; + +namespace BannerlordModEditor.Common.Tests.Models.SubModule +{ + public class SubModuleLoaderTests + { + private readonly SubModuleLoader _loader; + + public SubModuleLoaderTests() + { + _loader = new SubModuleLoader(); + } + + [Fact] + public void Load_ReturnsNullForNonExistentFile() + { + var result = _loader.Load("/nonexistent/path/SubModule.xml"); + Assert.Null(result); + } + + [Fact] + public async Task LoadAsync_ReturnsNullForNonExistentFile() + { + var result = await _loader.LoadAsync("/nonexistent/path/SubModule.xml"); + Assert.Null(result); + } + + [Fact] + public void Load_ReadsValidSubModuleXml() + { + var tempFile = Path.GetTempFileName(); + var xml = @" + + Test Mod + TestMod + v1.0.0 + true + false + + + + + + TestSubModule + TestMod.dll + TestMod.SubModule + false + true + + + + + +"; + + try + { + File.WriteAllText(tempFile, xml); + + var result = _loader.Load(tempFile); + + Assert.NotNull(result); + Assert.Equal("Test Mod", result.Name); + Assert.Equal("TestMod", result.Id); + Assert.Equal("v1.0.0", result.Version); + Assert.True(result.SingleplayerModule); + Assert.False(result.MultiplayerModule); + Assert.Single(result.DependedModules); + Assert.Equal("Native", result.DependedModules[0].Id); + Assert.Single(result.SubModules); + Assert.Equal("TestSubModule", result.SubModules[0].Name); + Assert.Equal("TestMod.dll", result.SubModules[0].DLLName); + Assert.Single(result.SubModules[0].Tags); + Assert.Equal("DedicatedServerType", result.SubModules[0].Tags[0].Key); + Assert.Equal("none", result.SubModules[0].Tags[0].Value); + } + finally + { + File.Delete(tempFile); + } + } + + [Fact] + public void Save_CreatesValidXml() + { + var tempFile = Path.GetTempFileName(); + var data = new SubModuleDO + { + Name = "Save Test Mod", + Id = "SaveTestMod", + Version = "v2.0.0", + SingleplayerModule = true, + MultiplayerModule = false, + DependedModules = new() + { + new DependedModuleDO { Id = "Native" }, + new DependedModuleDO { Id = "Sandbox" } + }, + SubModules = new() + { + new SubModuleItemDO + { + Name = "SaveTestSubModule", + DLLName = "SaveTestMod.dll", + SubModuleClassType = "SaveTestMod.SubModule", + IsOptional = false, + IsTicked = true, + Tags = new() + { + new SubModuleTagDO { Key = "DedicatedServerType", Value = "none" } + } + } + } + }; + + try + { + _loader.Save(data, tempFile); + + Assert.True(File.Exists(tempFile)); + var loadedData = _loader.Load(tempFile); + Assert.NotNull(loadedData); + Assert.Equal("Save Test Mod", loadedData.Name); + Assert.Equal("SaveTestMod", loadedData.Id); + Assert.Equal(2, loadedData.DependedModules.Count); + Assert.Single(loadedData.SubModules); + } + finally + { + File.Delete(tempFile); + } + } + + [Fact] + public async Task SaveAsync_CreatesValidXml() + { + var tempFile = Path.GetTempFileName(); + var data = _loader.CreateDefault("Async Test Mod", "AsyncTestMod"); + + try + { + await _loader.SaveAsync(data, tempFile); + + Assert.True(File.Exists(tempFile)); + var loadedData = await _loader.LoadAsync(tempFile); + Assert.NotNull(loadedData); + Assert.Equal("Async Test Mod", loadedData.Name); + Assert.Equal("AsyncTestMod", loadedData.Id); + } + finally + { + File.Delete(tempFile); + } + } + + [Fact] + public void CreateDefault_CreatesValidDefault() + { + var result = _loader.CreateDefault("Default Test", "DefaultTest"); + + Assert.Equal("Default Test", result.Name); + Assert.Equal("DefaultTest", result.Id); + Assert.Equal("v1.0.0", result.Version); + Assert.True(result.SingleplayerModule); + Assert.False(result.MultiplayerModule); + Assert.Empty(result.DependedModules); + Assert.Empty(result.SubModules); + Assert.Empty(result.Xmls); + Assert.Empty(result.OptionalDependedModules); + } + } +} diff --git a/BannerlordModEditor.Common/Models/SubModule/SubModuleDO.cs b/BannerlordModEditor.Common/Models/SubModule/SubModuleDO.cs new file mode 100644 index 0000000..d421c35 --- /dev/null +++ b/BannerlordModEditor.Common/Models/SubModule/SubModuleDO.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace BannerlordModEditor.Common.Models.SubModule +{ + [XmlRoot("Module")] + public class SubModuleDO + { + [XmlElement("Name")] + public string Name { get; set; } = string.Empty; + + [XmlElement("Id")] + public string Id { get; set; } = string.Empty; + + [XmlElement("Version")] + public string Version { get; set; } = string.Empty; + + [XmlElement("SingleplayerModule")] + public bool SingleplayerModule { get; set; } = true; + + [XmlElement("MultiplayerModule")] + public bool MultiplayerModule { get; set; } + + [XmlArray("DependedModules")] + [XmlArrayItem("Module")] + public List DependedModules { get; set; } = new(); + + [XmlArray("SubModules")] + [XmlArrayItem("SubModule")] + public List SubModules { get; set; } = new(); + + [XmlArray("Xmls")] + [XmlArrayItem("XmlNode")] + public List Xmls { get; set; } = new(); + + [XmlArray("OptionalDependedModules")] + [XmlArrayItem("Module")] + public List OptionalDependedModules { get; set; } = new(); + } + + public class DependedModuleDO + { + [XmlAttribute("Id")] + public string Id { get; set; } = string.Empty; + } + + public class OptionalDependedModuleDO + { + [XmlAttribute("Id")] + public string Id { get; set; } = string.Empty; + } + + public class SubModuleItemDO + { + [XmlElement("Name")] + public string Name { get; set; } = string.Empty; + + [XmlElement("DLLName")] + public string DLLName { get; set; } = string.Empty; + + [XmlElement("SubModuleClassType")] + public string SubModuleClassType { get; set; } = string.Empty; + + [XmlElement("IsOptional")] + public bool IsOptional { get; set; } + + [XmlElement("IsTicked")] + public bool IsTicked { get; set; } = true; + + [XmlArray("Tags")] + [XmlArrayItem("Tag")] + public List Tags { get; set; } = new(); + } + + public class SubModuleTagDO + { + [XmlAttribute("Key")] + public string Key { get; set; } = string.Empty; + + [XmlAttribute("Value")] + public string Value { get; set; } = string.Empty; + } + + public class XmlNodeDO + { + [XmlAttribute("Id")] + public string Id { get; set; } = string.Empty; + + [XmlAttribute("Type")] + public string Type { get; set; } = string.Empty; + + [XmlText] + public string Path { get; set; } = string.Empty; + } +} diff --git a/BannerlordModEditor.Common/Models/SubModule/SubModuleDTO.cs b/BannerlordModEditor.Common/Models/SubModule/SubModuleDTO.cs new file mode 100644 index 0000000..268e4b9 --- /dev/null +++ b/BannerlordModEditor.Common/Models/SubModule/SubModuleDTO.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace BannerlordModEditor.Common.Models.SubModule +{ + [XmlRoot("Module")] + public class SubModuleDTO + { + [XmlElement("Name")] + public string Name { get; set; } = string.Empty; + + [XmlElement("Id")] + public string Id { get; set; } = string.Empty; + + [XmlElement("Version")] + public string Version { get; set; } = string.Empty; + + [XmlElement("SingleplayerModule")] + public bool SingleplayerModule { get; set; } = true; + + [XmlElement("MultiplayerModule")] + public bool MultiplayerModule { get; set; } + + [XmlArray("DependedModules")] + [XmlArrayItem("Module")] + public List DependedModules { get; set; } = new(); + + [XmlArray("SubModules")] + [XmlArrayItem("SubModule")] + public List SubModules { get; set; } = new(); + + [XmlArray("Xmls")] + [XmlArrayItem("XmlNode")] + public List Xmls { get; set; } = new(); + + [XmlArray("OptionalDependedModules")] + [XmlArrayItem("Module")] + public List OptionalDependedModules { get; set; } = new(); + } + + public class DependedModuleDTO + { + [XmlAttribute("Id")] + public string Id { get; set; } = string.Empty; + } + + public class OptionalDependedModuleDTO + { + [XmlAttribute("Id")] + public string Id { get; set; } = string.Empty; + } + + public class SubModuleItemDTO + { + [XmlElement("Name")] + public string Name { get; set; } = string.Empty; + + [XmlElement("DLLName")] + public string DLLName { get; set; } = string.Empty; + + [XmlElement("SubModuleClassType")] + public string SubModuleClassType { get; set; } = string.Empty; + + [XmlElement("IsOptional")] + public bool IsOptional { get; set; } + + [XmlElement("IsTicked")] + public bool IsTicked { get; set; } = true; + + [XmlArray("Tags")] + [XmlArrayItem("Tag")] + public List Tags { get; set; } = new(); + } + + public class SubModuleTagDTO + { + [XmlAttribute("Key")] + public string Key { get; set; } = string.Empty; + + [XmlAttribute("Value")] + public string Value { get; set; } = string.Empty; + } + + public class XmlNodeDTO + { + [XmlAttribute("Id")] + public string Id { get; set; } = string.Empty; + + [XmlAttribute("Type")] + public string Type { get; set; } = string.Empty; + + [XmlText] + public string Path { get; set; } = string.Empty; + } +} diff --git a/BannerlordModEditor.Common/Models/SubModule/SubModuleLoader.cs b/BannerlordModEditor.Common/Models/SubModule/SubModuleLoader.cs new file mode 100644 index 0000000..95d1af1 --- /dev/null +++ b/BannerlordModEditor.Common/Models/SubModule/SubModuleLoader.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace BannerlordModEditor.Common.Models.SubModule +{ + public interface ISubModuleLoader + { + Task LoadAsync(string filePath); + SubModuleDO? Load(string filePath); + Task SaveAsync(SubModuleDO data, string filePath); + void Save(SubModuleDO data, string filePath); + SubModuleDO CreateDefault(string modName, string modId); + } + + public class SubModuleLoader : ISubModuleLoader + { + private readonly XmlSerializer _serializer; + + public SubModuleLoader() + { + _serializer = new XmlSerializer(typeof(SubModuleDTO)); + } + + public async Task LoadAsync(string filePath) + { + return await Task.Run(() => Load(filePath)); + } + + public SubModuleDO? Load(string filePath) + { + if (!File.Exists(filePath)) + return null; + + try + { + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + var dto = (SubModuleDTO?)_serializer.Deserialize(stream); + return dto != null ? SubModuleMapper.ToDO(dto) : null; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to load SubModule.xml from {filePath}: {ex.Message}", ex); + } + } + + public async Task SaveAsync(SubModuleDO data, string filePath) + { + await Task.Run(() => Save(data, filePath)); + } + + public void Save(SubModuleDO data, string filePath) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + try + { + var dto = SubModuleMapper.ToDTO(data); + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var settings = new XmlWriterSettings + { + Indent = true, + IndentChars = " ", + NewLineChars = "\n", + OmitXmlDeclaration = false, + Encoding = new System.Text.UTF8Encoding(false) + }; + + using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write); + using var writer = XmlWriter.Create(stream, settings); + + var namespaces = new XmlSerializerNamespaces(); + namespaces.Add("", ""); + + _serializer.Serialize(writer, dto, namespaces); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to save SubModule.xml to {filePath}: {ex.Message}", ex); + } + } + + public SubModuleDO CreateDefault(string modName, string modId) + { + return new SubModuleDO + { + Name = modName, + Id = modId, + Version = "v1.0.0", + SingleplayerModule = true, + MultiplayerModule = false, + DependedModules = new(), + SubModules = new(), + Xmls = new(), + OptionalDependedModules = new() + }; + } + } +} diff --git a/BannerlordModEditor.Common/Models/SubModule/SubModuleMapper.cs b/BannerlordModEditor.Common/Models/SubModule/SubModuleMapper.cs new file mode 100644 index 0000000..840695e --- /dev/null +++ b/BannerlordModEditor.Common/Models/SubModule/SubModuleMapper.cs @@ -0,0 +1,71 @@ +using System.Linq; + +namespace BannerlordModEditor.Common.Models.SubModule +{ + public static class SubModuleMapper + { + public static SubModuleDTO ToDTO(SubModuleDO source) + { + if (source == null) + return new SubModuleDTO(); + + return new SubModuleDTO + { + Name = source.Name, + Id = source.Id, + Version = source.Version, + SingleplayerModule = source.SingleplayerModule, + MultiplayerModule = source.MultiplayerModule, + DependedModules = source.DependedModules?.Select(d => new DependedModuleDTO { Id = d.Id }).ToList() ?? new(), + SubModules = source.SubModules?.Select(s => ToSubModuleItemDTO(s)).ToList() ?? new(), + Xmls = source.Xmls?.Select(x => new XmlNodeDTO { Id = x.Id, Type = x.Type, Path = x.Path }).ToList() ?? new(), + OptionalDependedModules = source.OptionalDependedModules?.Select(o => new OptionalDependedModuleDTO { Id = o.Id }).ToList() ?? new() + }; + } + + public static SubModuleDO ToDO(SubModuleDTO source) + { + if (source == null) + return new SubModuleDO(); + + return new SubModuleDO + { + Name = source.Name, + Id = source.Id, + Version = source.Version, + SingleplayerModule = source.SingleplayerModule, + MultiplayerModule = source.MultiplayerModule, + DependedModules = source.DependedModules?.Select(d => new DependedModuleDO { Id = d.Id }).ToList() ?? new(), + SubModules = source.SubModules?.Select(s => ToSubModuleItemDO(s)).ToList() ?? new(), + Xmls = source.Xmls?.Select(x => new XmlNodeDO { Id = x.Id, Type = x.Type, Path = x.Path }).ToList() ?? new(), + OptionalDependedModules = source.OptionalDependedModules?.Select(o => new OptionalDependedModuleDO { Id = o.Id }).ToList() ?? new() + }; + } + + private static SubModuleItemDTO ToSubModuleItemDTO(SubModuleItemDO source) + { + return new SubModuleItemDTO + { + Name = source.Name, + DLLName = source.DLLName, + SubModuleClassType = source.SubModuleClassType, + IsOptional = source.IsOptional, + IsTicked = source.IsTicked, + Tags = source.Tags?.Select(t => new SubModuleTagDTO { Key = t.Key, Value = t.Value }).ToList() ?? new() + }; + } + + private static SubModuleItemDO ToSubModuleItemDO(SubModuleItemDTO source) + { + return new SubModuleItemDO + { + Name = source.Name, + DLLName = source.DLLName, + SubModuleClassType = source.SubModuleClassType, + IsOptional = source.IsOptional, + IsTicked = source.IsTicked, + Tags = source.Tags?.Select(t => new SubModuleTagDO { Key = t.Key, Value = t.Value }).ToList() ?? new() + }; + } + } +}