Skip to content

feat(#9): Tpac file parser service#53

Open
ModerRAS wants to merge 1 commit intomasterfrom
feature/issue-9-tpactool-integration
Open

feat(#9): Tpac file parser service#53
ModerRAS wants to merge 1 commit intomasterfrom
feature/issue-9-tpactool-integration

Conversation

@ModerRAS
Copy link
Copy Markdown
Contributor

Summary

  • Create ITpacParserService interface
  • Implement TpacParserService to parse .tpac files
  • Extract model IDs, names, and types from tpac files
  • Add comprehensive unit tests

Changes

  • BannerlordModEditor.Common/Services/TpacParserService.cs - Implementation
  • BannerlordModEditor.Common.Tests/Services/TpacParserServiceTests.cs - Unit tests

Closes #9

- Create ITpacParserService interface
- Implement TpacParserService to parse .tpac files
- Extract model IDs, names, and types from tpac files
- Add comprehensive unit tests

Closes #9
Copilot AI review requested due to automatic review settings March 20, 2026 01:20
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

Introduces a .tpac file parsing service with an interface and DTOs, plus xUnit tests to validate parsing behavior.

Changes:

  • Added ITpacParserService plus TpacParserService implementation to parse .tpac files and extract model metadata.
  • Added DTOs (TpacFileInfo, TpacModelInfo) to return parsed results and errors.
  • Added xUnit tests covering extension detection and basic parsing flows.

Reviewed changes

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

File Description
BannerlordModEditor.Common/Services/TpacParserService.cs Adds .tpac parser service, DTOs, and interface for parsing/extracting models.
BannerlordModEditor.Common.Tests/Services/TpacParserServiceTests.cs Adds xUnit tests for .tpac extension checks and model extraction.

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

Comment on lines +50 to +58
var tempFile = Path.GetTempFileName();
var content = @"model_id: 12345
model_name: TestModel
model_type: Character
property1: value1
property2: value2
model_id: 67890
model_name: AnotherModel
model_type: Item";
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.

Path.GetTempFileName() creates a file with a .tmp extension, but ParseTpacFile rejects non-.tpac extensions via IsTpacFile. This test will fail by returning IsValid = false with "Not a valid .tpac file". Create the temp path with a .tpac extension (e.g., Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".tpac")) and write to that path instead.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +85
var tempFile = Path.GetTempFileName();
var content = @"model_id: 99999
model_name: AsyncModel
model_type: Weapon";
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.

Same issue as the sync test: the temp file will have a .tmp extension, so ParseTpacFileAsync will return invalid due to IsTpacFile. Use a temp file path ending in .tpac to align the test with the service’s validation logic.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +132
try
{
var content = File.ReadAllText(tpacFilePath);

var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var currentModel = new TpacModelInfo();

foreach (var line in lines)
{
var trimmed = line.Trim();

if (trimmed.StartsWith("model_id:", StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(currentModel.ModelId))
{
models.Add(currentModel);
}
currentModel = new TpacModelInfo
{
ModelId = trimmed["model_id:".Length..].Trim()
};
}
else if (trimmed.StartsWith("model_name:", StringComparison.OrdinalIgnoreCase))
{
currentModel.ModelName = trimmed["model_name:".Length..].Trim();
}
else if (trimmed.StartsWith("model_type:", StringComparison.OrdinalIgnoreCase))
{
currentModel.ModelType = trimmed["model_type:".Length..].Trim();
}
else if (trimmed.Contains(':'))
{
var parts = trimmed.Split(':', 2);
if (parts.Length == 2)
{
currentModel.Properties[parts[0].Trim()] = parts[1].Trim();
}
}
}

if (!string.IsNullOrEmpty(currentModel.ModelId))
{
models.Add(currentModel);
}
}
catch
{
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.

ExtractModels swallows all exceptions and returns whatever it has collected (often an empty list). Because ParseTpacFile treats “no exception” as success, an IO/parsing failure can incorrectly produce IsValid = true with Models = []. Remove the empty catch and let exceptions propagate to ParseTpacFile (so it can set IsValid = false), or catch and rethrow a more specific exception that preserves the original as an inner exception.

Suggested change
try
{
var content = File.ReadAllText(tpacFilePath);
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var currentModel = new TpacModelInfo();
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith("model_id:", StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(currentModel.ModelId))
{
models.Add(currentModel);
}
currentModel = new TpacModelInfo
{
ModelId = trimmed["model_id:".Length..].Trim()
};
}
else if (trimmed.StartsWith("model_name:", StringComparison.OrdinalIgnoreCase))
{
currentModel.ModelName = trimmed["model_name:".Length..].Trim();
}
else if (trimmed.StartsWith("model_type:", StringComparison.OrdinalIgnoreCase))
{
currentModel.ModelType = trimmed["model_type:".Length..].Trim();
}
else if (trimmed.Contains(':'))
{
var parts = trimmed.Split(':', 2);
if (parts.Length == 2)
{
currentModel.Properties[parts[0].Trim()] = parts[1].Trim();
}
}
}
if (!string.IsNullOrEmpty(currentModel.ModelId))
{
models.Add(currentModel);
}
}
catch
{
var content = File.ReadAllText(tpacFilePath);
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var currentModel = new TpacModelInfo();
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith("model_id:", StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(currentModel.ModelId))
{
models.Add(currentModel);
}
currentModel = new TpacModelInfo
{
ModelId = trimmed["model_id:".Length..].Trim()
};
}
else if (trimmed.StartsWith("model_name:", StringComparison.OrdinalIgnoreCase))
{
currentModel.ModelName = trimmed["model_name:".Length..].Trim();
}
else if (trimmed.StartsWith("model_type:", StringComparison.OrdinalIgnoreCase))
{
currentModel.ModelType = trimmed["model_type:".Length..].Trim();
}
else if (trimmed.Contains(':'))
{
var parts = trimmed.Split(':', 2);
if (parts.Length == 2)
{
currentModel.Properties[parts[0].Trim()] = parts[1].Trim();
}
}
}
if (!string.IsNullOrEmpty(currentModel.ModelId))
{
models.Add(currentModel);

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +40
public async Task<TpacFileInfo> ParseTpacFileAsync(string filePath)
{
return await Task.Run(() => ParseTpacFile(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.

ParseTpacFileAsync uses Task.Run to offload synchronous IO, which consumes a thread-pool thread and doesn’t provide true async IO. Prefer an actually-async pipeline (e.g., File.ReadAllTextAsync or streaming with StreamReader.ReadLineAsync) and parse from the async-read content; this scales better under concurrency and avoids unnecessary thread-pool usage.

Suggested change
public async Task<TpacFileInfo> ParseTpacFileAsync(string filePath)
{
return await Task.Run(() => ParseTpacFile(filePath));
public Task<TpacFileInfo> ParseTpacFileAsync(string filePath)
{
return Task.FromResult(ParseTpacFile(filePath));

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +63
if (!IsTpacFile(filePath))
{
info.IsValid = false;
info.ErrorMessage = "Not a valid .tpac file";
return info;
}
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 behavior for invalid extensions (setting IsValid = false and the specific error message) isn’t covered by tests. Add a unit test that writes valid .tpac-formatted content to a non-.tpac filename (or passes such a path) and asserts IsValid is false and ErrorMessage equals "Not a valid .tpac file".

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +123
else if (trimmed.Contains(':'))
{
var parts = trimmed.Split(':', 2);
if (parts.Length == 2)
{
currentModel.Properties[parts[0].Trim()] = parts[1].Trim();
}
}
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.

Property extraction into TpacModelInfo.Properties is a core part of the parser, but the tests currently don’t assert that properties are captured correctly. Add assertions in ParseTpacFile_ExtractsModelsFromValidFile to verify property1/property2 exist with expected values on the first model.

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] 使用tpactool的库读取tpac文件中的模型相关id

2 participants