diff --git a/BannerlordModEditor.Common.Tests/Services/XsdSchemaProviderTests.cs b/BannerlordModEditor.Common.Tests/Services/XsdSchemaProviderTests.cs
new file mode 100644
index 00000000..905a7da9
--- /dev/null
+++ b/BannerlordModEditor.Common.Tests/Services/XsdSchemaProviderTests.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+using BannerlordModEditor.Common.Services;
+
+namespace BannerlordModEditor.Common.Tests.Services
+{
+ public class XsdSchemaProviderTests
+ {
+ private readonly XsdSchemaProvider _provider;
+
+ public XsdSchemaProviderTests()
+ {
+ _provider = new XsdSchemaProvider();
+ }
+
+ [Fact]
+ public void GetXsdPathForXml_ReturnsNullForNonExistentGameDirectory()
+ {
+ var result = _provider.GetXsdPathForXml("test.xml", "/nonexistent/path");
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetXsdPathForXml_ReturnsNullForEmptyPaths()
+ {
+ Assert.Null(_provider.GetXsdPathForXml("", ""));
+ Assert.Null(_provider.GetXsdPathForXml(null!, null!));
+ }
+
+ [Fact]
+ public void GetXsdPathForXml_FindsXsdInXmlSchemasDirectory()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ var xsdDir = Path.Combine(tempDir, "XmlSchemas");
+ Directory.CreateDirectory(xsdDir);
+
+ try
+ {
+ var xsdPath = Path.Combine(xsdDir, "attributes.xsd");
+ File.WriteAllText(xsdPath, "");
+
+ var result = _provider.GetXsdPathForXml("attributes.xml", tempDir);
+ Assert.Equal(xsdPath, result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void GetAllXsdFiles_ReturnsEmptyForNonExistentDirectory()
+ {
+ var result = _provider.GetAllXsdFiles("/nonexistent/path");
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void GetAllXsdFiles_FindsAllXsdFiles()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ var xsdDir = Path.Combine(tempDir, "XmlSchemas");
+ Directory.CreateDirectory(xsdDir);
+
+ try
+ {
+ File.WriteAllText(Path.Combine(xsdDir, "file1.xsd"), "");
+ File.WriteAllText(Path.Combine(xsdDir, "file2.xsd"), "");
+
+ var result = _provider.GetAllXsdFiles(tempDir);
+ Assert.Equal(2, result.Count);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void LoadSchemaSet_ReturnsEmptyForNonExistentFile()
+ {
+ var result = _provider.LoadSchemaSet("/nonexistent/schema.xsd");
+ Assert.NotNull(result);
+ Assert.Equal(0, result.Count);
+ }
+
+ [Fact]
+ public void LoadSchemaSet_LoadsValidXsd()
+ {
+ var tempFile = Path.GetTempFileName();
+ var xsd = @"
+
+
+
+
+
+
+
+
+";
+
+ try
+ {
+ File.WriteAllText(tempFile, xsd);
+
+ var result = _provider.LoadSchemaSet(tempFile);
+ Assert.NotNull(result);
+ Assert.Equal(1, result.Count);
+ }
+ finally
+ {
+ File.Delete(tempFile);
+ }
+ }
+
+ [Fact]
+ public void ValidateXmlAgainstXsd_ReturnsFalseForNonExistentXml()
+ {
+ var result = _provider.ValidateXmlAgainstXsd("/nonexistent.xml", "/schema.xsd", out var errors);
+ Assert.False(result);
+ Assert.Contains("XML file not found", errors[0]);
+ }
+
+ [Fact]
+ public void ValidateXmlAgainstXsd_ReturnsFalseForNonExistentXsd()
+ {
+ var tempXml = Path.GetTempFileName();
+ File.WriteAllText(tempXml, "");
+
+ try
+ {
+ var result = _provider.ValidateXmlAgainstXsd(tempXml, "/nonexistent.xsd", out var errors);
+ Assert.False(result);
+ Assert.Contains("XSD schema not found", errors[0]);
+ }
+ finally
+ {
+ File.Delete(tempXml);
+ }
+ }
+ }
+}
diff --git a/BannerlordModEditor.Common/Services/XsdSchemaProvider.cs b/BannerlordModEditor.Common/Services/XsdSchemaProvider.cs
new file mode 100644
index 00000000..b39ec54d
--- /dev/null
+++ b/BannerlordModEditor.Common/Services/XsdSchemaProvider.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using System.Xml.Schema;
+
+namespace BannerlordModEditor.Common.Services
+{
+ public interface IXsdSchemaProvider
+ {
+ string? GetXsdPathForXml(string xmlFilePath, string gameDirectory);
+ List GetAllXsdFiles(string gameDirectory);
+ XmlSchemaSet LoadSchemaSet(string xsdPath);
+ bool ValidateXmlAgainstXsd(string xmlFilePath, string xsdPath, out List errors);
+ }
+
+ public class XsdSchemaProvider : IXsdSchemaProvider
+ {
+ private const string XsdDirectoryRelativePath = "XmlSchemas";
+ private const string ModuleDataRelativePath = "Modules";
+ private const string NativeModuleRelativePath = "Modules\\Native\\ModuleData";
+
+ public string? GetXsdPathForXml(string xmlFilePath, string gameDirectory)
+ {
+ if (string.IsNullOrEmpty(xmlFilePath) || string.IsNullOrEmpty(gameDirectory))
+ return null;
+
+ var xmlFileName = Path.GetFileNameWithoutExtension(xmlFilePath);
+
+ var xsdPaths = new[]
+ {
+ Path.Combine(gameDirectory, XsdDirectoryRelativePath, $"{xmlFileName}.xsd"),
+ Path.Combine(gameDirectory, XsdDirectoryRelativePath, "Native", $"{xmlFileName}.xsd"),
+ Path.Combine(gameDirectory, NativeModuleRelativePath, $"{xmlFileName}.xsd")
+ };
+
+ foreach (var xsdPath in xsdPaths)
+ {
+ if (File.Exists(xsdPath))
+ return xsdPath;
+ }
+
+ return null;
+ }
+
+ public List GetAllXsdFiles(string gameDirectory)
+ {
+ var xsdFiles = new List();
+
+ if (string.IsNullOrEmpty(gameDirectory) || !Directory.Exists(gameDirectory))
+ return xsdFiles;
+
+ var searchPaths = new[]
+ {
+ Path.Combine(gameDirectory, XsdDirectoryRelativePath),
+ Path.Combine(gameDirectory, NativeModuleRelativePath)
+ };
+
+ foreach (var searchPath in searchPaths)
+ {
+ if (Directory.Exists(searchPath))
+ {
+ try
+ {
+ xsdFiles.AddRange(Directory.GetFiles(searchPath, "*.xsd", SearchOption.AllDirectories));
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ return xsdFiles.Distinct().ToList();
+ }
+
+ public XmlSchemaSet LoadSchemaSet(string xsdPath)
+ {
+ var schemaSet = new XmlSchemaSet();
+
+ if (!File.Exists(xsdPath))
+ return schemaSet;
+
+ try
+ {
+ using var stream = new FileStream(xsdPath, FileMode.Open, FileAccess.Read);
+ var schema = XmlSchema.Read(stream, null);
+ if (schema != null)
+ {
+ schemaSet.Add(schema);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to load XSD schema from {xsdPath}: {ex.Message}", ex);
+ }
+
+ return schemaSet;
+ }
+
+ public bool ValidateXmlAgainstXsd(string xmlFilePath, string xsdPath, out List errors)
+ {
+ errors = new List();
+
+ if (!File.Exists(xmlFilePath))
+ {
+ errors.Add($"XML file not found: {xmlFilePath}");
+ return false;
+ }
+
+ if (!File.Exists(xsdPath))
+ {
+ errors.Add($"XSD schema not found: {xsdPath}");
+ return false;
+ }
+
+ try
+ {
+ var schemaSet = LoadSchemaSet(xsdPath);
+ var validationErrors = new List();
+ var settings = new XmlReaderSettings
+ {
+ ValidationType = ValidationType.Schema,
+ Schemas = schemaSet
+ };
+
+ settings.ValidationEventHandler += (sender, e) =>
+ {
+ var severity = e.Severity == XmlSeverityType.Error ? "Error" : "Warning";
+ validationErrors.Add($"{severity}: {e.Message}");
+ };
+
+ using var reader = XmlReader.Create(xmlFilePath, settings);
+ while (reader.Read())
+ {
+ }
+
+ errors = validationErrors;
+ return errors.Count == 0;
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"Validation error: {ex.Message}");
+ return false;
+ }
+ }
+ }
+}