diff --git a/BannerlordModEditor.Common.Tests/Services/GameDirectoryScannerTests.cs b/BannerlordModEditor.Common.Tests/Services/GameDirectoryScannerTests.cs new file mode 100644 index 00000000..9184b2bf --- /dev/null +++ b/BannerlordModEditor.Common.Tests/Services/GameDirectoryScannerTests.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using BannerlordModEditor.Common.Services; + +namespace BannerlordModEditor.Common.Tests.Services +{ + public class GameDirectoryScannerTests + { + private readonly GameDirectoryScanner _scanner; + + public GameDirectoryScannerTests() + { + _scanner = new GameDirectoryScanner(); + } + + [Fact] + public async Task ScanForGameDirectoriesAsync_ReturnsEmptyListWhenNoGameInstalled() + { + var result = await _scanner.ScanForGameDirectoriesAsync(); + Assert.NotNull(result); + } + + [Fact] + public void IsValidGameDirectory_ReturnsFalseForNullPath() + { + var result = _scanner.IsValidGameDirectory(null!); + Assert.False(result); + } + + [Fact] + public void IsValidGameDirectory_ReturnsFalseForEmptyPath() + { + var result = _scanner.IsValidGameDirectory(string.Empty); + Assert.False(result); + } + + [Fact] + public void IsValidGameDirectory_ReturnsFalseForNonExistentPath() + { + var result = _scanner.IsValidGameDirectory("/nonexistent/path"); + Assert.False(result); + } + + [Fact] + public void IsValidGameDirectory_ReturnsFalseForDirectoryWithoutModuleData() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var result = _scanner.IsValidGameDirectory(tempDir); + Assert.False(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void IsValidGameDirectory_ReturnsTrueForValidBannerlordDirectory() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var modulesDir = Path.Combine(tempDir, "Modules"); + var nativeDir = Path.Combine(modulesDir, "Native"); + + Directory.CreateDirectory(nativeDir); + + try + { + var result = _scanner.IsValidGameDirectory(tempDir); + Assert.True(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void GetGameVersion_ReturnsNullForNonExistentDirectory() + { + var result = _scanner.GetGameVersion("/nonexistent/path"); + Assert.Null(result); + } + + [Fact] + public void GetGameVersion_ReadsFromVersionFile() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var versionFile = Path.Combine(tempDir, "version.txt"); + File.WriteAllText(versionFile, "1.2.9.0"); + + var result = _scanner.GetGameVersion(tempDir); + Assert.Equal("1.2.9.0", result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void GetGameVersion_ReadsFromConfigFile() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var configFile = Path.Combine(tempDir, "config.txt"); + File.WriteAllText(configFile, "version=1.3.15.0\nother_setting=value"); + + var result = _scanner.GetGameVersion(tempDir); + Assert.Equal("1.3.15.0", result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + } +} diff --git a/BannerlordModEditor.Common/Services/GameDirectoryScanner.cs b/BannerlordModEditor.Common/Services/GameDirectoryScanner.cs new file mode 100644 index 00000000..d087adef --- /dev/null +++ b/BannerlordModEditor.Common/Services/GameDirectoryScanner.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace BannerlordModEditor.Common.Services +{ + public class GameDirectoryScanner : IGameDirectoryScanner + { + private const string BannerlordAppId = "261550"; + private const string BannerlordFolderName = "Mount & Blade II Bannerlord"; + private const string ModuleDataRelativePath = "Modules"; + + public async Task> ScanForGameDirectoriesAsync() + { + return await Task.Run(() => + { + var results = new List(); + + var steamPaths = GetSteamLibraryPaths(); + foreach (var steamPath in steamPaths) + { + var bannerlordPath = Path.Combine(steamPath, "steamapps", "common", BannerlordFolderName); + if (Directory.Exists(bannerlordPath)) + { + var info = CreateGameDirectoryInfo(bannerlordPath, "Steam"); + results.Add(info); + } + } + + var gogPaths = GetGogPaths(); + foreach (var gogPath in gogPaths) + { + if (Directory.Exists(gogPath)) + { + var info = CreateGameDirectoryInfo(gogPath, "GOG"); + results.Add(info); + } + } + + var epicPaths = GetEpicPaths(); + foreach (var epicPath in epicPaths) + { + if (Directory.Exists(epicPath)) + { + var info = CreateGameDirectoryInfo(epicPath, "Epic"); + results.Add(info); + } + } + + return results.Where(r => r.IsValid).ToList(); + }); + } + + public async Task GetFirstGameDirectoryAsync() + { + var directories = await ScanForGameDirectoriesAsync(); + return directories.FirstOrDefault()?.Path; + } + + public bool IsValidGameDirectory(string path) + { + if (string.IsNullOrEmpty(path) || !Directory.Exists(path)) + return false; + + var moduleDataPath = Path.Combine(path, ModuleDataRelativePath); + if (!Directory.Exists(moduleDataPath)) + return false; + + var nativeModulePath = Path.Combine(moduleDataPath, "Native"); + return Directory.Exists(nativeModulePath); + } + + public string? GetGameVersion(string gameDirectory) + { + if (string.IsNullOrEmpty(gameDirectory) || !Directory.Exists(gameDirectory)) + return null; + + var versionFile = Path.Combine(gameDirectory, "version.txt"); + if (File.Exists(versionFile)) + { + return File.ReadAllText(versionFile).Trim(); + } + + var configFile = Path.Combine(gameDirectory, "config.txt"); + if (File.Exists(configFile)) + { + var lines = File.ReadAllLines(configFile); + var versionLine = lines.FirstOrDefault(l => l.StartsWith("version=", StringComparison.OrdinalIgnoreCase)); + if (versionLine != null) + { + return versionLine["version=".Length..].Trim(); + } + } + + return null; + } + + private GameDirectoryInfo CreateGameDirectoryInfo(string path, string source) + { + var info = new GameDirectoryInfo + { + Path = path, + Source = source, + IsValid = IsValidGameDirectory(path), + ModuleDataPath = Path.Combine(path, ModuleDataRelativePath), + Version = GetGameVersion(path) + }; + return info; + } + + private List GetSteamLibraryPaths() + { + var paths = new List(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + paths.Add(Path.Combine(programFiles, "Steam")); + + var defaultPaths = new[] + { + @"C:\Program Files (x86)\Steam", + @"C:\Program Files\Steam", + @"D:\Steam", + @"E:\Steam", + @"F:\Steam" + }; + paths.AddRange(defaultPaths); + + var steamConfigFile = Path.Combine(programFiles, "Steam", "steamapps", "libraryfolders.vdf"); + paths.AddRange(ParseSteamLibraryFolders(steamConfigFile)); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + paths.Add(Path.Combine(home, ".steam", "steam")); + paths.Add(Path.Combine(home, ".local", "share", "Steam")); + + var steamConfigFile = Path.Combine(home, ".steam", "steam", "steamapps", "libraryfolders.vdf"); + paths.AddRange(ParseSteamLibraryFolders(steamConfigFile)); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + paths.Add(Path.Combine(home, "Library", "Application Support", "Steam")); + + var steamConfigFile = Path.Combine(home, "Library", "Application Support", "Steam", "steamapps", "libraryfolders.vdf"); + paths.AddRange(ParseSteamLibraryFolders(steamConfigFile)); + } + + return paths.Where(p => !string.IsNullOrEmpty(p)).Distinct().ToList(); + } + + private List ParseSteamLibraryFolders(string configFile) + { + var paths = new List(); + + if (!File.Exists(configFile)) + return paths; + + try + { + var lines = File.ReadAllLines(configFile); + foreach (var line in lines) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("\"path\"", StringComparison.OrdinalIgnoreCase)) + { + var parts = trimmed.Split('"', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 4) + { + var path = parts[3].Replace("\\\\", "\\"); + if (Directory.Exists(path)) + { + paths.Add(path); + } + } + } + } + } + catch + { + } + + return paths; + } + + private List GetGogPaths() + { + var paths = new List(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + + paths.Add(Path.Combine(programFiles, "GOG Galaxy", "Games", BannerlordFolderName)); + paths.Add(Path.Combine(programFilesX86, "GOG Galaxy", "Games", BannerlordFolderName)); + } + + return paths; + } + + private List GetEpicPaths() + { + var paths = new List(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + paths.Add(Path.Combine(programFiles, "Epic Games", BannerlordFolderName)); + + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var epicManifestsDir = Path.Combine(localAppData, "EpicGamesLauncher", "Saved", "Config", "Windows"); + + if (Directory.Exists(epicManifestsDir)) + { + var manifestFiles = Directory.GetFiles(epicManifestsDir, "*.item"); + foreach (var manifest in manifestFiles) + { + try + { + var content = File.ReadAllText(manifest); + if (content.Contains("Mount", StringComparison.OrdinalIgnoreCase) && + content.Contains("Blade", StringComparison.OrdinalIgnoreCase)) + { + var installDirMatch = System.Text.RegularExpressions.Regex.Match(content, @"""InstallLocation""\s*:\s*""([^""]+)"""); + if (installDirMatch.Success) + { + var installPath = installDirMatch.Groups[1].Value.Replace("\\\\", "\\"); + if (Directory.Exists(installPath)) + { + paths.Add(installPath); + } + } + } + } + catch + { + } + } + } + } + + return paths; + } + } +} diff --git a/BannerlordModEditor.Common/Services/IGameDirectoryScanner.cs b/BannerlordModEditor.Common/Services/IGameDirectoryScanner.cs new file mode 100644 index 00000000..e7f33a2d --- /dev/null +++ b/BannerlordModEditor.Common/Services/IGameDirectoryScanner.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace BannerlordModEditor.Common.Services +{ + /// + /// 游戏目录扫描服务接口 + /// 用于自动检测骑砍2的安装目录 + /// + public interface IGameDirectoryScanner + { + /// + /// 异步扫描所有可能的游戏安装目录 + /// + /// 找到的游戏目录列表 + Task> ScanForGameDirectoriesAsync(); + + /// + /// 获取第一个找到的游戏目录 + /// + /// 游戏目录路径,未找到则返回null + Task GetFirstGameDirectoryAsync(); + + /// + /// 验证指定路径是否为有效的骑砍2安装目录 + /// + /// 要验证的路径 + /// 是否为有效目录 + bool IsValidGameDirectory(string path); + + /// + /// 获取游戏版本信息 + /// + /// 游戏目录 + /// 游戏版本,无法识别则返回null + string? GetGameVersion(string gameDirectory); + } + + /// + /// 游戏目录信息 + /// + public class GameDirectoryInfo + { + /// + /// 游戏安装路径 + /// + public string Path { get; set; } = string.Empty; + + /// + /// 安装来源(Steam、GOG、Epic等) + /// + public string Source { get; set; } = string.Empty; + + /// + /// 游戏版本号 + /// + public string? Version { get; set; } + + /// + /// 是否为有效目录 + /// + public bool IsValid { get; set; } + + /// + /// ModuleData目录路径 + /// + public string ModuleDataPath { get; set; } = string.Empty; + } +} diff --git a/BannerlordModEditor.UI/App.axaml.cs b/BannerlordModEditor.UI/App.axaml.cs index 028d620f..2d45028c 100644 --- a/BannerlordModEditor.UI/App.axaml.cs +++ b/BannerlordModEditor.UI/App.axaml.cs @@ -71,6 +71,7 @@ private IServiceCollection ConfigureServices() // 注册Common层服务 services.AddTransient(); + services.AddSingleton(); // 注册所有编辑器ViewModel和View services.AddTransient();