diff --git a/BannerlordModEditor.Common.Tests/Services/ProjectTypeDetectorTests.cs b/BannerlordModEditor.Common.Tests/Services/ProjectTypeDetectorTests.cs
new file mode 100644
index 00000000..9bee05f1
--- /dev/null
+++ b/BannerlordModEditor.Common.Tests/Services/ProjectTypeDetectorTests.cs
@@ -0,0 +1,146 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Xunit;
+using BannerlordModEditor.Common.Services;
+
+namespace BannerlordModEditor.Common.Tests.Services
+{
+ public class ProjectTypeDetectorTests
+ {
+ private readonly ProjectTypeDetector _detector;
+
+ public ProjectTypeDetectorTests()
+ {
+ _detector = new ProjectTypeDetector();
+ }
+
+ [Fact]
+ public void DetectProjectType_ReturnsUnknownForNonExistentPath()
+ {
+ var result = _detector.DetectProjectType("/nonexistent/path");
+ Assert.Equal(ProjectType.Unknown, result.Type);
+ }
+
+ [Fact]
+ public void DetectProjectType_ReturnsNativeModForSubmoduleOnly()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ File.WriteAllText(Path.Combine(tempDir, "SubModule.xml"), "");
+
+ var result = _detector.DetectProjectType(tempDir);
+ Assert.Equal(ProjectType.NativeMod, result.Type);
+ Assert.True(result.HasSubmoduleXml);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void DetectProjectType_ReturnsCustomTemplateForSubmoduleAndCsproj()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ File.WriteAllText(Path.Combine(tempDir, "SubModule.xml"), "");
+ File.WriteAllText(Path.Combine(tempDir, "TestMod.csproj"), "");
+
+ var result = _detector.DetectProjectType(tempDir);
+ Assert.Equal(ProjectType.CustomTemplate, result.Type);
+ Assert.True(result.HasSubmoduleXml);
+ Assert.True(result.HasCsprojFile);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void DetectProjectType_ReturnsButrTemplateForButrMarker()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ var butrDir = Path.Combine(tempDir, "BUTR");
+ Directory.CreateDirectory(butrDir);
+
+ try
+ {
+ File.WriteAllText(Path.Combine(tempDir, "SubModule.xml"), "");
+ File.WriteAllText(Path.Combine(tempDir, "TestMod.csproj"), "");
+ File.WriteAllText(Path.Combine(butrDir, "Bannerlord.ButterLib.dll"), "");
+
+ var result = _detector.DetectProjectType(tempDir);
+ Assert.Equal(ProjectType.ButrTemplate, result.Type);
+ Assert.True(result.HasButrMarker);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void IsButrTemplateProject_ReturnsTrueForButrProject()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ var butrDir = Path.Combine(tempDir, "BUTR");
+ Directory.CreateDirectory(butrDir);
+
+ try
+ {
+ File.WriteAllText(Path.Combine(butrDir, "marker.txt"), "BUTR marker");
+
+ var result = _detector.IsButrTemplateProject(tempDir);
+ Assert.True(result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void GetModName_ReturnsCsprojName()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ File.WriteAllText(Path.Combine(tempDir, "MyCoolMod.csproj"), "");
+
+ var result = _detector.GetModName(tempDir);
+ Assert.Equal("MyCoolMod", result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void GetModName_ReturnsDirectoryNameWhenNoCsproj()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), "TestModProject");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ var result = _detector.GetModName(tempDir);
+ Assert.Equal("TestModProject", result);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+}
diff --git a/BannerlordModEditor.Common/Services/ProjectTypeDetector.cs b/BannerlordModEditor.Common/Services/ProjectTypeDetector.cs
new file mode 100644
index 00000000..ddc4d53f
--- /dev/null
+++ b/BannerlordModEditor.Common/Services/ProjectTypeDetector.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace BannerlordModEditor.Common.Services
+{
+ public enum ProjectType
+ {
+ Unknown,
+ ButrTemplate,
+ CustomTemplate,
+ NativeMod
+ }
+
+ public class ProjectTypeInfo
+ {
+ public ProjectType Type { get; set; }
+ public string ProjectPath { get; set; } = string.Empty;
+ public bool HasSubmoduleXml { get; set; }
+ public bool HasCsprojFile { get; set; }
+ public bool HasButrMarker { get; set; }
+ public string? ModName { get; set; }
+ public string? ModuleDataPath { get; set; }
+ }
+
+ public interface IProjectTypeDetector
+ {
+ ProjectTypeInfo DetectProjectType(string projectPath);
+ bool IsButrTemplateProject(string projectPath);
+ string? GetModName(string projectPath);
+ }
+
+ public class ProjectTypeDetector : IProjectTypeDetector
+ {
+ private static readonly string[] ButrMarkers = new[]
+ {
+ "Bannerlord.ButterLib.dll",
+ "Bannerlord.ButterLib",
+ "BUTR",
+ "BUTRLoadingScreen"
+ };
+
+ private static readonly string[] ButrDirectories = new[]
+ {
+ "BUTR",
+ "ButterLib",
+ "LoadingScreen"
+ };
+
+ public ProjectTypeInfo DetectProjectType(string projectPath)
+ {
+ var info = new ProjectTypeInfo
+ {
+ ProjectPath = projectPath,
+ Type = ProjectType.Unknown
+ };
+
+ if (!Directory.Exists(projectPath))
+ return info;
+
+ info.HasSubmoduleXml = File.Exists(Path.Combine(projectPath, "SubModule.xml"));
+ info.HasCsprojFile = Directory.GetFiles(projectPath, "*.csproj").Any();
+ info.HasButrMarker = CheckForButrMarkers(projectPath);
+ info.ModName = GetModName(projectPath);
+
+ var moduleDataDir = Path.Combine(projectPath, "ModuleData");
+ if (Directory.Exists(moduleDataDir))
+ {
+ info.ModuleDataPath = moduleDataDir;
+ }
+
+ if (info.HasButrMarker)
+ {
+ info.Type = ProjectType.ButrTemplate;
+ }
+ else if (info.HasSubmoduleXml && info.HasCsprojFile)
+ {
+ info.Type = ProjectType.CustomTemplate;
+ }
+ else if (info.HasSubmoduleXml)
+ {
+ info.Type = ProjectType.NativeMod;
+ }
+
+ return info;
+ }
+
+ public bool IsButrTemplateProject(string projectPath)
+ {
+ return DetectProjectType(projectPath).Type == ProjectType.ButrTemplate;
+ }
+
+ public string? GetModName(string projectPath)
+ {
+ if (!Directory.Exists(projectPath))
+ return null;
+
+ var csprojFiles = Directory.GetFiles(projectPath, "*.csproj");
+ if (csprojFiles.Any())
+ {
+ var csprojPath = csprojFiles.First();
+ var csprojName = Path.GetFileNameWithoutExtension(csprojPath);
+ return csprojName;
+ }
+
+ var dirName = new DirectoryInfo(projectPath).Name;
+ return dirName;
+ }
+
+ private bool CheckForButrMarkers(string projectPath)
+ {
+ var allFiles = GetAllFiles(projectPath);
+
+ foreach (var file in allFiles)
+ {
+ var fileName = Path.GetFileName(file);
+ if (ButrMarkers.Any(marker => fileName.Contains(marker, StringComparison.OrdinalIgnoreCase)))
+ return true;
+
+ var content = SafeReadFile(file);
+ if (content != null && content.Contains("BUTR", StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+
+ var directories = Directory.GetDirectories(projectPath, "*", SearchOption.AllDirectories);
+ foreach (var dir in directories)
+ {
+ var dirName = new DirectoryInfo(dir).Name;
+ if (ButrDirectories.Any(marker => dirName.Contains(marker, StringComparison.OrdinalIgnoreCase)))
+ return true;
+ }
+
+ return false;
+ }
+
+ private IEnumerable GetAllFiles(string directory)
+ {
+ var files = new List();
+
+ try
+ {
+ files.AddRange(Directory.GetFiles(directory, "*.*", SearchOption.TopDirectoryOnly));
+
+ foreach (var subDir in Directory.GetDirectories(directory))
+ {
+ var dirName = new DirectoryInfo(subDir).Name;
+ if (dirName != "bin" && dirName != "obj" && dirName != ".git")
+ {
+ files.AddRange(GetAllFiles(subDir));
+ }
+ }
+ }
+ catch
+ {
+ }
+
+ return files;
+ }
+
+ private string? SafeReadFile(string filePath)
+ {
+ try
+ {
+ return File.ReadAllText(filePath);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/BannerlordModEditor.UI/App.axaml.cs b/BannerlordModEditor.UI/App.axaml.cs
index 028d620f..620bb523 100644
--- a/BannerlordModEditor.UI/App.axaml.cs
+++ b/BannerlordModEditor.UI/App.axaml.cs
@@ -71,6 +71,8 @@ private IServiceCollection ConfigureServices()
// 注册Common层服务
services.AddTransient();
+ services.AddSingleton();
+ services.AddSingleton();
// 注册所有编辑器ViewModel和View
services.AddTransient();