Skip to content

feat(#10): SubModule.xml support#48

Open
ModerRAS wants to merge 1 commit intomasterfrom
feature/issue-10-submodule-xml-support
Open

feat(#10): SubModule.xml support#48
ModerRAS wants to merge 1 commit intomasterfrom
feature/issue-10-submodule-xml-support

Conversation

@ModerRAS
Copy link
Copy Markdown
Contributor

Summary

  • Create SubModule models (DO, DTO, Mapper)
  • Implement SubModuleLoader with async support
  • Support reading and writing SubModule.xml files
  • Add comprehensive unit tests
  • Support creating default SubModule.xml

Changes

  • BannerlordModEditor.Common/Models/SubModule/SubModuleDO.cs - Domain objects
  • BannerlordModEditor.Common/Models/SubModule/SubModuleDTO.cs - Data transfer objects
  • BannerlordModEditor.Common/Models/SubModule/SubModuleMapper.cs - DO/DTO mapper
  • BannerlordModEditor.Common/Models/SubModule/SubModuleLoader.cs - XML loader
  • BannerlordModEditor.Common.Tests/Models/SubModule/SubModuleLoaderTests.cs - Unit tests

Closes #10

- Create SubModule models (DO, DTO, Mapper)
- Implement SubModuleLoader with async support
- Support reading and writing SubModule.xml files
- Add comprehensive unit tests
- Support creating default SubModule.xml

Closes #10
Copilot AI review requested due to automatic review settings March 20, 2026 01:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for Bannerlord SubModule.xml by introducing domain/DTO models, mapping, an XML loader with async APIs, and unit tests validating load/save/default behaviors.

Changes:

  • Added SubModule DO/DTO model graphs and a DO↔DTO mapper.
  • Implemented SubModuleLoader to load/save SubModule.xml and create a default template.
  • Added unit tests covering load/save (sync+async) and default creation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
BannerlordModEditor.Common/Models/SubModule/SubModuleMapper.cs Maps between DO and DTO representations used by serialization.
BannerlordModEditor.Common/Models/SubModule/SubModuleLoader.cs Implements XML load/save + async wrappers + default template creation.
BannerlordModEditor.Common/Models/SubModule/SubModuleDTO.cs Defines XML-serializable DTO object model for SubModule.xml.
BannerlordModEditor.Common/Models/SubModule/SubModuleDO.cs Defines domain object model mirroring the DTO structure.
BannerlordModEditor.Common.Tests/Models/SubModule/SubModuleLoaderTests.cs Validates loader behavior with temp files and basic XML samples.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +27 to +30
public async Task<SubModuleDO?> LoadAsync(string filePath)
{
return await Task.Run(() => Load(filePath));
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task.Run around synchronous file IO/serialization can cause unnecessary thread-pool usage and contention under load. Prefer implementing true async IO (e.g., async file read/write with useAsync: true, deserialize/serialize from/to an in-memory buffer) or remove the async methods and let callers decide whether to offload to the thread pool.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +52
public async Task SaveAsync(SubModuleDO data, string filePath)
{
await Task.Run(() => Save(data, filePath));
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task.Run around synchronous file IO/serialization can cause unnecessary thread-pool usage and contention under load. Prefer implementing true async IO (e.g., async file read/write with useAsync: true, deserialize/serialize from/to an in-memory buffer) or remove the async methods and let callers decide whether to offload to the thread pool.

Copilot uses AI. Check for mistakes.

try
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This overload defaults to FileShare.None, which can unnecessarily lock SubModule.xml while reading and break scenarios where another process/thread needs to read the file concurrently. Consider using a FileStream overload that specifies an explicit FileShare (commonly FileShare.Read or FileShare.ReadWrite) for the read path.

Suggested change
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +93
[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<DependedModuleDO> DependedModules { get; set; } = new();

[XmlArray("SubModules")]
[XmlArrayItem("SubModule")]
public List<SubModuleItemDO> SubModules { get; set; } = new();

[XmlArray("Xmls")]
[XmlArrayItem("XmlNode")]
public List<XmlNodeDO> Xmls { get; set; } = new();

[XmlArray("OptionalDependedModules")]
[XmlArrayItem("Module")]
public List<OptionalDependedModuleDO> 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<SubModuleTagDO> 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]
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DO types are decorated with XML serialization attributes even though serialization is performed via SubModuleDTO + XmlSerializer(typeof(SubModuleDTO)). This duplicates the schema across DO/DTO and increases the risk of them drifting out of sync. Consider either (a) removing XML attributes from the DO classes (keep DTO as the serialization contract), or (b) consolidating to a single XML model to eliminate the mapper if separation isn’t needed.

Suggested change
[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<DependedModuleDO> DependedModules { get; set; } = new();
[XmlArray("SubModules")]
[XmlArrayItem("SubModule")]
public List<SubModuleItemDO> SubModules { get; set; } = new();
[XmlArray("Xmls")]
[XmlArrayItem("XmlNode")]
public List<XmlNodeDO> Xmls { get; set; } = new();
[XmlArray("OptionalDependedModules")]
[XmlArrayItem("Module")]
public List<OptionalDependedModuleDO> 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<SubModuleTagDO> 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 class SubModuleDO
{
public string Name { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public bool SingleplayerModule { get; set; } = true;
public bool MultiplayerModule { get; set; }
public List<DependedModuleDO> DependedModules { get; set; } = new();
public List<SubModuleItemDO> SubModules { get; set; } = new();
public List<XmlNodeDO> Xmls { get; set; } = new();
public List<OptionalDependedModuleDO> OptionalDependedModules { get; set; } = new();
}
public class DependedModuleDO
{
public string Id { get; set; } = string.Empty;
}
public class OptionalDependedModuleDO
{
public string Id { get; set; } = string.Empty;
}
public class SubModuleItemDO
{
public string Name { get; set; } = string.Empty;
public string DLLName { get; set; } = string.Empty;
public string SubModuleClassType { get; set; } = string.Empty;
public bool IsOptional { get; set; }
public bool IsTicked { get; set; } = true;
public List<SubModuleTagDO> Tags { get; set; } = new();
}
public class SubModuleTagDO
{
public string Key { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
public class XmlNodeDO
{
public string Id { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +28
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");
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a hard-coded absolute path like /nonexistent/path/... can be brittle across OSes/environments (and in rare cases could exist or be accessible). Prefer generating a guaranteed-nonexistent path under the temp directory (e.g., Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml")) so the test remains deterministic.

Suggested change
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");
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = _loader.Load(nonExistentPath);
Assert.Null(result);
}
[Fact]
public async Task LoadAsync_ReturnsNullForNonExistentFile()
{
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = await _loader.LoadAsync(nonExistentPath);

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +28
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");
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a hard-coded absolute path like /nonexistent/path/... can be brittle across OSes/environments (and in rare cases could exist or be accessible). Prefer generating a guaranteed-nonexistent path under the temp directory (e.g., Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml")) so the test remains deterministic.

Suggested change
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");
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = _loader.Load(nonExistentPath);
Assert.Null(result);
}
[Fact]
public async Task LoadAsync_ReturnsNullForNonExistentFile()
{
var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "SubModule.xml");
var result = await _loader.LoadAsync(nonExistentPath);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 支持读写Submodule.xml

2 participants