diff --git a/PCL.Neo.Core/Models/Minecraft/Game/Data/GameInfo.cs b/PCL.Neo.Core/Models/Minecraft/Game/Data/GameInfo.cs index d05d75d7..10e4cbba 100644 --- a/PCL.Neo.Core/Models/Minecraft/Game/Data/GameInfo.cs +++ b/PCL.Neo.Core/Models/Minecraft/Game/Data/GameInfo.cs @@ -4,6 +4,11 @@ namespace PCL.Neo.Core.Models.Minecraft.Game.Data; public record GameInfo { + /// + /// The name of the game version. + /// + public required string Name { get; set; } + /// /// .minecraft folder path. /// @@ -15,21 +20,20 @@ public record GameInfo public required string RootDirectory { get; set; } /// - /// The name of the game version. + /// The loader type. /// - public required string Name { get; set; } + public GameType Type { get; set; } = GameType.Unknown; /// - /// The loader type. + /// The game version. /// - public GameType Type { get; set; } = GameType.Unknown; + public required string Version { get; set; } /// /// Demonstrate if the version has been loaded (runed). /// - [JsonIgnore] - public bool IsRunning { get; set; } = false; + public bool IsRunning { get; set; } private bool? _isIndie; @@ -57,6 +61,7 @@ public bool IsIndie public static GameInfo Factory( string targetDir, string gameDir, string versionName, + string verison, bool isIndie, GameType type) { @@ -66,7 +71,8 @@ public static GameInfo Factory( RootDirectory = targetDir, GameDirectory = gameDir, IsIndie = isIndie, - Type = type + Type = type, + Version = verison }; } } diff --git a/PCL.Neo.Core/Models/Minecraft/Game/Data/GameVersionId.cs b/PCL.Neo.Core/Models/Minecraft/Game/Data/GameVersionId.cs deleted file mode 100644 index 17cf88db..00000000 --- a/PCL.Neo.Core/Models/Minecraft/Game/Data/GameVersionId.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace PCL.Neo.Core.Models.Minecraft.Game.Data; - -/// -/// 常规游戏版本的版本号,后续可能会拓展到模组版本 -/// -public record GameVersionId(byte Sub, byte? Fix = null) : IComparable -{ - private readonly (byte Major, byte Sub, byte Fix) _version = (1, Sub, Fix ?? 0); - - public byte Major => _version.Major; - public byte Sub => _version.Sub; - public byte? Fix => _version.Fix > 0 ? _version.Fix : null; - - public int CompareTo(GameVersionId? other) - { - return other == null ? 1 : (Major, Sub, Fix ?? 0).CompareTo((other.Major, other.Sub, other.Fix ?? 0)); - } - - public override string ToString() - { - return Fix.HasValue ? $"{Major}.{Sub}.{Fix}" : $"{Major}.{Sub}"; - } - - /// - public override int GetHashCode() - { - return HashCode.Combine(_version.Major, _version.Sub, _version.Fix); - } - - /// - public virtual bool Equals(GameVersionId? other) - { - if (other is null) return false; - if (ReferenceEquals(this, other)) return true; - - return _version.Major == other._version.Major && - _version.Sub == other._version.Sub && - _version.Fix == other._version.Fix; - } -} diff --git a/PCL.Neo.Core/Models/Minecraft/MetadataFile.cs b/PCL.Neo.Core/Models/Minecraft/MetadataFile.cs deleted file mode 100644 index d6969377..00000000 --- a/PCL.Neo.Core/Models/Minecraft/MetadataFile.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace PCL.Neo.Core.Models.Minecraft; - -public class MetadataFile -{ - private JsonObject _rawMetadata = new(); - - #region Model Classes - - public class Rule - { - [JsonConverter(typeof(JsonStringEnumConverter))] - public enum ActionEnum - { - Unknown, - - [JsonStringEnumMemberName("allow")] - Allow, - - [JsonStringEnumMemberName("disallow")] - Disallow - } - - [JsonPropertyName("action")] - public ActionEnum Action { get; set; } = ActionEnum.Allow; - - [JsonPropertyName("features")] - public Dictionary? Features { get; set; } = null; - - [JsonPropertyName("os")] - public OsModel? Os { get; set; } = null; - - public class OsModel - { - [JsonConverter(typeof(JsonStringEnumConverter))] - public enum ArchEnum - { - Unknown, - - [JsonStringEnumMemberName("x64")] - X64, - - [JsonStringEnumMemberName("x86")] - X86 - } - - [JsonConverter(typeof(JsonStringEnumConverter))] - public enum NameEnum - { - Unknown, - - [JsonStringEnumMemberName("windows")] - Windows, - - [JsonStringEnumMemberName("linux")] - Linux, - - [JsonStringEnumMemberName("osx")] - Osx - } - - [JsonPropertyName("arch")] - public ArchEnum? Arch { get; set; } = null; - - [JsonPropertyName("name")] - public NameEnum? Name { get; set; } = null; - - [JsonPropertyName("version")] - public string? Version { get; set; } = null; // regex - } - } - - public class ConditionalArg - { - [JsonPropertyName("rules")] - public List? Rules { get; set; } - - [JsonPropertyName("value")] - public List Value { get; set; } - } - - public class ArgumentsModel - { - public List Game { get; set; } = []; - public List Jvm { get; set; } = []; - } - - public class JavaVersionModel - { - [JsonPropertyName("component")] - public string Component { get; set; } = string.Empty; - - [JsonPropertyName("majorVersion")] - public int MajorVersion { get; set; } - } - - public class RemoteFileModel - { - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("path")] - public string? Path { get; set; } = null; - - [JsonPropertyName("sha1")] - public string Sha1 { get; set; } = string.Empty; - - [JsonPropertyName("size")] - public int Size { get; set; } - - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; - } - - public class AssetIndexModel : RemoteFileModel - { - [JsonPropertyName("totalSize")] - public int TotalSize { get; set; } - } - - public class LibraryModel - { - [JsonPropertyName("downloads")] - public DownloadsModel Downloads { get; set; } = new(); - - [JsonPropertyName("extract")] - public ExtractModel? Extract { get; set; } = null; - - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("natives")] - public Dictionary? Natives { get; set; } = null; - - [JsonPropertyName("rules")] - public List? Rules { get; set; } = null; - - public class DownloadsModel - { - [JsonPropertyName("artifact")] - public RemoteFileModel? Artifact { get; set; } = null; - - [JsonPropertyName("classifiers")] - public Dictionary? Classifiers { get; set; } = null; - } - - public class ExtractModel - { - [JsonPropertyName("exclude")] - public List Exclude { get; set; } = []; - } - } - - public class LoggingModel - { - [JsonPropertyName("argument")] - public string Argument { get; set; } = string.Empty; - - [JsonPropertyName("file")] - public RemoteFileModel File { get; set; } = new(); - - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; - } - - #endregion - - #region Metadata Fields - - public ArgumentsModel Arguments { get; set; } = new(); - public AssetIndexModel AssetIndex { get; set; } = new(); - public string Assets { get; set; } = string.Empty; - public int? ComplianceLevel { get; set; } // field missing in 1.6.4.json - public Dictionary Downloads { get; set; } = []; - public string Id { get; set; } = string.Empty; - public JavaVersionModel? JavaVersion { get; set; } = new(); // field missing in 1.6.1.json - public List Libraries { get; set; } = []; - public Dictionary? Logging { get; set; } - public string MainClass { get; set; } = string.Empty; - public int MinimumLauncherVersion { get; set; } - public string ReleaseTime { get; set; } = string.Empty; - public string Time { get; set; } = string.Empty; - public ReleaseTypeEnum Type { get; set; } - - #endregion - - #region Parse Methods - - // For simplicity, we assume the metadata files are always valid -// ReSharper disable PossibleNullReferenceException -// ReSharper disable AssignNullToNotNullAttribute -#nullable disable -#pragma warning disable IL2026 - public static MetadataFile Parse(string json) - { - return Parse(JsonNode.Parse(json)!.AsObject()); - } - - public static MetadataFile Parse(JsonNode json) - { - return Parse(json.AsObject()); - } - - public static MetadataFile Parse(JsonObject json) - { - var mf = new MetadataFile { _rawMetadata = json }; - - #region Arguments - - if (mf._rawMetadata.ContainsKey("arguments")) - { - ParseArguments(mf.Arguments.Game, "game"); - ParseArguments(mf.Arguments.Jvm, "jvm"); - - // TODO: convert this to json converter - void ParseArguments(List toBeFilled, string propertyName) - { - toBeFilled.Clear(); - foreach (var param in mf._rawMetadata["arguments"][propertyName].AsArray()) - { - if (param.GetValueKind() == JsonValueKind.String) - toBeFilled.Add(new ConditionalArg { Value = [param.GetValue()] }); - else if (param.GetValueKind() == JsonValueKind.Object) - { - var rules = param["rules"].Deserialize>(); - List value = null; - if (param["value"].GetValueKind() == JsonValueKind.String) - value = [param["value"].GetValue()]; - else if (param["value"].GetValueKind() == JsonValueKind.Array) - value = param["value"].Deserialize>(); - toBeFilled.Add(new ConditionalArg { Rules = rules, Value = value }); - } - } - } - } - else if (mf._rawMetadata.ContainsKey("minecraftArguments")) - { - var argStr = mf._rawMetadata["minecraftArguments"].GetValue(); - mf.Arguments.Game = argStr - .Split(' ') - .Select(x => new ConditionalArg { Value = [x] }) - .ToList(); - } - else - throw new Exception("Unknown Metadata File version"); - - #endregion - - #region Logging - - if (mf._rawMetadata.ContainsKey("logging")) - mf.Logging = mf._rawMetadata["logging"].Deserialize>(); - - #endregion - - #region Common Fields - - mf.AssetIndex = mf._rawMetadata["assetIndex"].Deserialize(); - mf.Assets = mf._rawMetadata["assets"].GetValue(); - - mf.ComplianceLevel = mf._rawMetadata["complianceLevel"]?.GetValue(); // field missing in 1.6.4.json - - mf.Downloads = mf._rawMetadata["downloads"].Deserialize>(); - - mf.Id = mf._rawMetadata["id"].GetValue(); - mf.JavaVersion = mf._rawMetadata["javaVersion"].Deserialize(); - - mf.Libraries = mf._rawMetadata["libraries"].Deserialize>(); - - mf.MainClass = mf._rawMetadata["mainClass"].GetValue(); - mf.MinimumLauncherVersion = mf._rawMetadata["minimumLauncherVersion"].GetValue(); - mf.ReleaseTime = mf._rawMetadata["releaseTime"].GetValue(); - mf.Time = mf._rawMetadata["time"].GetValue(); - mf.Type = mf._rawMetadata["type"].Deserialize(); - - #endregion - - return mf; - } -#pragma warning restore IL2026 - // ReSharper restore AssignNullToNotNullAttribute -// ReSharper restore PossibleNullReferenceException - - #endregion -} diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/Data/FabricModInfo.cs b/PCL.Neo.Core/Models/Minecraft/Mod/Data/FabricModInfo.cs new file mode 100644 index 00000000..3f636d41 --- /dev/null +++ b/PCL.Neo.Core/Models/Minecraft/Mod/Data/FabricModInfo.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace PCL.Neo.Core.Models.Minecraft.Mod.Data; + +/// +/// 模组信息 +/// +internal record FabricModInfo +{ + public record ContactInfo + { + [JsonPropertyName("email ")] + public string Email { get; set; } = string.Empty; + + [JsonPropertyName("irc")] + public string Irc { get; set; } = string.Empty; + + [JsonPropertyName("homepage")] + public string Homepage { get; set; } = string.Empty; + + [JsonPropertyName("issues")] + public string Issues { set; get; } = string.Empty; + + [JsonPropertyName("sources")] + public string Sources { get; set; } = string.Empty; + } + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + [JsonPropertyName("icon")] + public string Icon { get; set; } = string.Empty; + + [JsonPropertyName("contact")] + public ContactInfo? Contact { get; set; } +} diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/Data/MetaModInfo.cs b/PCL.Neo.Core/Models/Minecraft/Mod/Data/MetaModInfo.cs new file mode 100644 index 00000000..2c3a56d3 --- /dev/null +++ b/PCL.Neo.Core/Models/Minecraft/Mod/Data/MetaModInfo.cs @@ -0,0 +1,48 @@ +namespace PCL.Neo.Core.Models.Minecraft.Mod.Data; + +internal class MetaModInfo +{ + public record ModInfo + { + /// + /// 模组ID,映射到 "modId"。 + /// + public string ModId { get; set; } = string.Empty; + + /// + /// 模组版本,映射到 "version"。 + /// + public string Version { get; set; } = string.Empty; + + /// + /// 显示名称,映射到 "displayName"。 + /// + public string DisplayName { get; set; } = string.Empty; + + /// + /// 主页URL,映射到 "displayURL"。 + /// + public string DisplayUrl { get; set; } = string.Empty; + + /// + /// Logo文件名,映射到 "logoFile"。 + /// + public string LogoFile { get; set; } = string.Empty; + + /// + /// 制作人员/致谢名单,映射到 "credits"。 + /// + public string Credits { get; set; } = string.Empty; + + /// + /// 模组描述,映射到 "description"。 + /// + public string Description { get; set; } = string.Empty; + } + + /// + /// 映射 TOML 中的 [[mods]] 数组。 + /// Tomlyn 会自动将 TOML 中的 "mods" 键映射到这个名为 "Mods" 的属性。 + /// + public List Mods { get; set; } = []; +} diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/Data/ModInfo.cs b/PCL.Neo.Core/Models/Minecraft/Mod/Data/ModInfo.cs new file mode 100644 index 00000000..df644bee --- /dev/null +++ b/PCL.Neo.Core/Models/Minecraft/Mod/Data/ModInfo.cs @@ -0,0 +1,31 @@ +namespace PCL.Neo.Core.Models.Minecraft.Mod.Data; + +public record ModInfo : IDisposable +{ + public string Name { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public string Version { get; set; } = string.Empty; + + public string Icon { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + + private bool _disposed; + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + if (File.Exists(Icon)) + { + File.Delete(Icon); + } + + _disposed = true; + } +} diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/Modpack.cs b/PCL.Neo.Core/Models/Minecraft/Mod/Data/Modpack.cs similarity index 79% rename from PCL.Neo.Core/Models/Minecraft/Mod/Modpack.cs rename to PCL.Neo.Core/Models/Minecraft/Mod/Data/Modpack.cs index 51ab0002..95b2d486 100644 --- a/PCL.Neo.Core/Models/Minecraft/Mod/Modpack.cs +++ b/PCL.Neo.Core/Models/Minecraft/Mod/Data/Modpack.cs @@ -1,13 +1,12 @@ using System.IO.Compression; -namespace PCL.Neo.Core.Models.Minecraft.Mod; +namespace PCL.Neo.Core.Models.Minecraft.Mod.Data; public class ModPack { public static void InstallPackModrinth(string mrpack, string directory) { - if (!File.Exists(mrpack)) { throw new FileNotFoundException(); } - + if (!File.Exists(mrpack)) throw new FileNotFoundException(); // ZipFile.ExtractToDirectory(mrpack, directory); using var archive = ZipFile.OpenRead(mrpack); var modrinthOptions = archive.GetEntry("modrinth.index.json"); diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/ModInfo.cs b/PCL.Neo.Core/Models/Minecraft/Mod/ModInfo.cs deleted file mode 100644 index 94c3491a..00000000 --- a/PCL.Neo.Core/Models/Minecraft/Mod/ModInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace PCL.Neo.Core.Models.Minecraft.Mod; - -/// -/// 模组信息 -/// -public record ModInfo -{ - public required string Id { get; init; } - public required string Name { get; set; } - public string Version { get; set; } = string.Empty; - public bool Enabled { get; set; } = true; - public string FilePath { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; -} diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/ModInfoReader.cs b/PCL.Neo.Core/Models/Minecraft/Mod/ModInfoReader.cs new file mode 100644 index 00000000..9a258baa --- /dev/null +++ b/PCL.Neo.Core/Models/Minecraft/Mod/ModInfoReader.cs @@ -0,0 +1,164 @@ +using PCL.Neo.Core.Models.Minecraft.Mod.Data; +using System.IO.Compression; +using System.Text.Json; +using Tomlyn; + +namespace PCL.Neo.Core.Models.Minecraft.Mod; + +public class ModInfoReader +{ + private enum ModInfoType + { + Unknown, + MetaInf, + JsonInfo + } + + private static (ModInfoType, ZipArchiveEntry?) GetModInfoType(ZipArchive archive) + { + var entry = archive.GetEntry("META-INF/mods.toml"); + if (entry != null) + { + return (ModInfoType.MetaInf, entry); + } + + entry = archive.GetEntry("fabric.mod.json"); + if (entry != null) + { + return (ModInfoType.JsonInfo, entry); + } + + return (ModInfoType.Unknown, null); + } + + /// + /// Get mod information from the specified mod directory. + /// + /// The directory that storage mods. + /// + /// Throw if given directory not found. + /// Throw if needed file is not found. + public static async Task> GetModInfo(string modDir) + { + if (!Directory.Exists(modDir)) + { + throw new DirectoryNotFoundException("Mods direcotry not found."); + } + + var mods = new List(); + var modFiles = Directory.GetFiles(modDir, "*.jar"); + + foreach (var modFile in modFiles) + { + using var zipFile = ZipFile.OpenRead(modFile); + + var (type, archiveEntry) = GetModInfoType(zipFile); // get info type and entry + + ArgumentNullException.ThrowIfNull(archiveEntry); + + using var reader = new StreamReader(archiveEntry.Open()); + var rawContent = await reader.ReadToEndAsync(); + + switch (type) + { + case ModInfoType.JsonInfo: + var content = new string(rawContent.Where(it => it != '\n').ToArray()); + var jsonContent = JsonSerializer.Deserialize(content); + ArgumentNullException.ThrowIfNull(jsonContent); // ensure deserialization was successful + + // copy mod icon + if (!string.IsNullOrEmpty(jsonContent.Icon)) + { + jsonContent.Icon = await CopyIcon(zipFile, jsonContent.Icon); + } + else + { + jsonContent.Icon = "Unknown"; + } + + // convert to ModInfo + string modSource; + if (jsonContent.Contact == null) + { + modSource = string.Empty; + } + else + { + modSource = string.IsNullOrEmpty(jsonContent.Contact.Sources) + ? jsonContent.Contact.Homepage + : jsonContent.Contact.Sources; + } + + var modInfo = new ModInfo + { + Version = jsonContent.Version, + Name = string.IsNullOrEmpty(jsonContent.Name) ? jsonContent.Id : jsonContent.Name, + Description = jsonContent.Description, + Icon = jsonContent.Icon, + Url = modSource + }; + mods.Add(modInfo); + break; + case ModInfoType.MetaInf: + var tomlContent = Toml.ToModel(rawContent).Mods.First(); + + // copy mod icon + if (!string.IsNullOrEmpty(tomlContent.LogoFile)) + { + tomlContent.LogoFile = await CopyIcon(zipFile, tomlContent.LogoFile); + } + else + { + tomlContent.LogoFile = "Unknown"; + } + + // convert to ModInfo + var metaInfo = new ModInfo + { + Name = string.IsNullOrEmpty(tomlContent.DisplayName) + ? tomlContent.ModId + : tomlContent.DisplayName, + Version = tomlContent.Version, + Icon = tomlContent.LogoFile, + Description = tomlContent.Description, + Url = tomlContent.DisplayUrl + }; + mods.Add(metaInfo); + break; + + case ModInfoType.Unknown: + var unknownInfo = new ModInfo + { + Name = "Unknown", + Description = "Can not read mod information.", + Icon = "Unknown", + }; + mods.Add(unknownInfo); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + return mods; + } + + private static readonly string ModIconDir = Path.Combine(Const.AppData, "modIcons"); + + private static async Task CopyIcon(ZipArchive archive, string iconPath) + { + var iconEntry = archive.GetEntry(iconPath); + if (iconEntry == null) + { + throw new ArgumentNullException(nameof(iconEntry), "Icon not found."); + } + + var tempIconPath = Path.Combine(ModIconDir, $"{Guid.NewGuid().ToString()[..8]}.png"); + await using var iconStream = iconEntry.Open(); + await using var fileStream = File.Create(tempIconPath); + + await iconStream.CopyToAsync(fileStream); + + return tempIconPath; + } +} diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/README.md b/PCL.Neo.Core/Models/Minecraft/Mod/README.md new file mode 100644 index 00000000..e47de19d --- /dev/null +++ b/PCL.Neo.Core/Models/Minecraft/Mod/README.md @@ -0,0 +1,49 @@ +# ModInfoReader 使用说明 + +## 简介 +`ModInfoReader` 是一个用于读取 Minecraft 模组(mod)信息的工具类。它支持从指定的模组文件夹中批量解析 `.jar` 文件,自动识别并提取模组的名称、描述、版本、图标等信息,兼容 Fabric 和 Forge 两种主流模组格式。 + +## 使用方法 +1. 引用命名空间: +```csharp +using PCL.Neo.Core.Models.Minecraft.Mod; +``` +2. 调用静态方法 `GetModInfo`,传入模组文件夹路径: +```csharp +var mods = await ModInfoReader.GetModInfo("你的mods文件夹路径"); +``` +3. 遍历返回的模组信息: +```csharp +foreach (var modInfo in mods) +{ + Console.WriteLine($"名称: {modInfo.Name}"); + Console.WriteLine($"描述: {modInfo.Description}"); + Console.WriteLine($"版本: {modInfo.Version}"); + Console.WriteLine($"图标路径: {modInfo.Icon}"); + Console.WriteLine($"主页: {modInfo.Url}"); +} +``` + +## 示例 +```csharp +const string modDir = @"C:\你的Minecraft路径\mods"; +var mods = await ModInfoReader.GetModInfo(modDir); +foreach (var modInfo in mods) +{ + Console.WriteLine(modInfo.Name); + Console.WriteLine(modInfo.Description); + Console.WriteLine(modInfo.Version); + Console.WriteLine(modInfo.Icon); + Console.WriteLine("--------"); +} +``` + +## 注意事项 +- 传入的文件夹路径必须存在且包含 `.jar` 格式的模组文件,否则会抛出异常。 +- 仅支持 Fabric(`fabric.mod.json`)和 Forge(`META-INF/mods.toml`)格式的模组,其他格式会返回默认信息。 +- 解析出的图标会被复制到本地缓存目录(`modIcons`),请注意及时清理无用图标文件。 +- `ModInfo` 实现了 `IDisposable`,如需释放资源(删除图标文件),请在使用完毕后调用 `Dispose()` 方法。 +- 该方法为异步方法,需使用 `await` 调用。 + +## 联系与反馈 +如有问题或建议,欢迎在项目仓库提交 issue。 diff --git a/PCL.Neo.Core/PCL.Neo.Core.csproj b/PCL.Neo.Core/PCL.Neo.Core.csproj index c89fda24..ef7d6e9c 100644 --- a/PCL.Neo.Core/PCL.Neo.Core.csproj +++ b/PCL.Neo.Core/PCL.Neo.Core.csproj @@ -19,6 +19,7 @@ + diff --git a/PCL.Neo.Core/Service/Profiles/IProfileService.cs b/PCL.Neo.Core/Service/Profiles/IProfileService.cs index 2d4ec76e..823445d2 100644 --- a/PCL.Neo.Core/Service/Profiles/IProfileService.cs +++ b/PCL.Neo.Core/Service/Profiles/IProfileService.cs @@ -33,7 +33,7 @@ public interface IProfileService /// The directory where the profile is located. /// The name of the profile to load. /// A game profile object. - Task LoadTargetGameAsync(string targetDir, string gameName); + Task GetTargetGameAsync(string targetDir, string gameName); /// /// Save profile to the specified directory. diff --git a/PCL.Neo.Core/Service/Profiles/ProfileService.cs b/PCL.Neo.Core/Service/Profiles/ProfileService.cs index 3c6d448a..88982b2b 100644 --- a/PCL.Neo.Core/Service/Profiles/ProfileService.cs +++ b/PCL.Neo.Core/Service/Profiles/ProfileService.cs @@ -63,14 +63,15 @@ public async Task GetProfileAsync(string targetDir, string profileN var isIndie = Directory.Exists(Path.Combine(version, "saves")); var gameType = await GetGameType(version, versionName); - profiles.Games.Add(GameInfo.Factory(targetDir, version, versionName, isIndie, gameType)); + var cliVer = GetClientVersion(jsonFile); + profiles.Games.Add(GameInfo.Factory(targetDir, version, versionName, cliVer, isIndie, gameType)); } return profiles; } /// - public async Task LoadTargetGameAsync(string targetDir, string gameName) + public async Task GetTargetGameAsync(string targetDir, string gameName) { if (!ValidateDir(targetDir)) { @@ -96,7 +97,8 @@ public async Task LoadTargetGameAsync(string targetDir, string gameNam var isIndie = Directory.Exists(Path.Combine(gameDir, "saves")); var gameType = await GetGameType(gameDir, gameName); - var gameInfo = GameInfo.Factory(targetDir, gameDir, gameName, isIndie, gameType); + var cliVer = GetClientVersion(jsonFile); + var gameInfo = GameInfo.Factory(targetDir, gameDir, gameName, cliVer, isIndie, gameType); return gameInfo; } @@ -152,6 +154,18 @@ public bool DeleteGame(GameInfo game, ProfileInfo profile) return true; } + private static string GetClientVersion(string jsonFilePath) + { + using var jsonFile = File.OpenRead(jsonFilePath); + var jsonDoc = JsonDocument.Parse(jsonFile); + var versionElement = jsonDoc.RootElement.GetProperty("clientVersion"); + var versionStr = versionElement.GetString(); + + ArgumentNullException.ThrowIfNull(versionStr); + + return versionStr; + } + private static bool ValidateDir(string targetDir) { return RequiredSubDirectories.All(subDir => diff --git a/PCL.Neo.Core/Service/Profiles/README.md b/PCL.Neo.Core/Service/Profiles/README.md index cf79f94a..f9467db6 100644 --- a/PCL.Neo.Core/Service/Profiles/README.md +++ b/PCL.Neo.Core/Service/Profiles/README.md @@ -41,10 +41,10 @@ Task GetProfileAsync(string targetDir, string profileName) ### 2.2 游戏加载方法 -#### LoadTargetGameAsync +#### GetTargetGameAsync ```csharp -Task LoadTargetGameAsync(string targetDir, string gameName) +Task GetTargetGameAsync(string targetDir, string gameName) ``` 从指定目录加载特定的游戏版本信息。 @@ -124,7 +124,7 @@ await _profileService.SaveProfilesDefaultAsync(profile); ```csharp // 加载特定游戏版本 -var game = await _profileService.LoadTargetGameAsync(targetDir, "1.20.6-Fabric"); +var game = await _profileService.GetTargetGameAsync(targetDir, "1.20.6-Fabric"); // 将游戏信息添加到档案 await _profileService.SaveGameInfoToProfileAsync(profile, game, targetDir); diff --git a/PCL.Neo.Tests/Core/Models/FileHelper/FileTest.cs b/PCL.Neo.Tests/Core/Models/FileHelper/FileTest.cs index 5f8f811b..3bce3311 100644 --- a/PCL.Neo.Tests/Core/Models/FileHelper/FileTest.cs +++ b/PCL.Neo.Tests/Core/Models/FileHelper/FileTest.cs @@ -1,6 +1,6 @@ using PCL.Neo.Core.Download; using PCL.Neo.Core.Models.Minecraft.Java; -using PCL.Neo.Core.Models.Minecraft.Mod; +using PCL.Neo.Core.Models.Minecraft.Mod.Data; using System; using System.IO; using System.Threading.Tasks; @@ -42,10 +42,4 @@ public void MojangVersionTest() { Console.WriteLine(JavaManager.MojangJavaVersion.Δ.Value); } - - [Test] - public async Task SelectFileTest() - { - // await Helpers.FileExtension.SelectFile("Test"); - } } diff --git a/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameEntityTest.cs b/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameEntityTest.cs index ef943a4b..0cf753c5 100644 --- a/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameEntityTest.cs +++ b/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameEntityTest.cs @@ -52,7 +52,8 @@ await JavaRuntime.CreateJavaEntityAsync( GameDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft\versions\Create", RootDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft", - Name = "Create" + Name = "Create", + Version = "Unknow" }, launchOptions ); diff --git a/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameLauncherTest.cs b/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameLauncherTest.cs index 33e89460..fea36f70 100644 --- a/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameLauncherTest.cs +++ b/PCL.Neo.Tests/Core/Models/Minecraft/Game/GameLauncherTest.cs @@ -52,7 +52,8 @@ await JavaRuntime.CreateJavaEntityAsync( GameDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft\versions\Create", RootDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft", - Name = "Create" + Name = "Create", + Version = "Unknow" }, launchOptions ); diff --git a/PCL.Neo.Tests/Core/Models/Minecraft/Game/IGameLauncherServiceTest.cs b/PCL.Neo.Tests/Core/Models/Minecraft/Game/IGameLauncherServiceTest.cs index 1e719776..03e2b206 100644 --- a/PCL.Neo.Tests/Core/Models/Minecraft/Game/IGameLauncherServiceTest.cs +++ b/PCL.Neo.Tests/Core/Models/Minecraft/Game/IGameLauncherServiceTest.cs @@ -56,7 +56,8 @@ await JavaRuntime.CreateJavaEntityAsync( GameDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft\versions\1.20.4-Fabric 0.15.11-[轻量通用]", RootDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft", - Name = "1.20.4-Fabric 0.15.11-[轻量通用]" + Name = "1.20.4-Fabric 0.15.11-[轻量通用]", + Version = "Unknow" }, launchOptions ); diff --git a/PCL.Neo.Tests/Core/Models/Minecraft/MetadataFileTest.cs b/PCL.Neo.Tests/Core/Models/Minecraft/MetadataFileTest.cs deleted file mode 100644 index 31e076d3..00000000 --- a/PCL.Neo.Tests/Core/Models/Minecraft/MetadataFileTest.cs +++ /dev/null @@ -1,249 +0,0 @@ -using PCL.Neo.Core.Models.Minecraft; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json.Nodes; - -namespace PCL.Neo.Tests.Core.Models.Minecraft; - -public class MetadataFileTest -{ - [Test] - public void Parse() - { - foreach (var metadataFilePath in Directory.EnumerateFiles("./MCMetadataFiles")) - { - var jsonObj = JsonNode.Parse(File.ReadAllText(metadataFilePath))!.AsObject(); - var meta = MetadataFile.Parse(jsonObj); - Assert.That(meta.Arguments.Game, Is.Not.Empty); - if (jsonObj.ContainsKey("arguments")) - { - Assert.That(meta.Arguments.Game.Count, Is.EqualTo(jsonObj["arguments"]!["game"]!.AsArray().Count)); - } - - Assert.Multiple(() => - { - Assert.That(meta.Assets, Is.Not.Empty); - Assert.That(meta.AssetIndex.Id, Is.Not.Empty); - Assert.That(meta.AssetIndex.Path, Is.Null); - Assert.That(meta.AssetIndex.Sha1, Is.Not.Empty); - Assert.That(meta.AssetIndex.Size, Is.Not.Zero); - Assert.That(meta.AssetIndex.TotalSize, Is.Not.Zero); - }); - Assert.That(meta.Downloads, Is.Not.Empty); - foreach (var (id, file) in meta.Downloads) - { - Assert.Multiple(() => - { - Assert.That(id, Is.Not.Empty); - Assert.That(file.Path, Is.Null); - Assert.That(file.Sha1, Is.Not.Empty); - Assert.That(file.Size, Is.Not.Zero); - Assert.That(file.Url, Is.Not.Empty); - }); - } - - Assert.That(meta.Id, Is.Not.Empty); - Assert.Multiple(() => - { - if (meta.JavaVersion is null) - return; - Assert.That(meta.JavaVersion.Component, Is.Not.Empty); - Assert.That(meta.JavaVersion.MajorVersion, Is.Not.Zero); - }); - Assert.That(meta.Libraries.Count, Is.EqualTo(jsonObj["libraries"]!.AsArray().Count)); - Assert.Multiple(() => - { - if (meta.Logging is null) - return; - Assert.That(meta.Logging, Is.Not.Empty); - foreach (var (id, logging) in meta.Logging) - { - Assert.That(id, Is.Not.Empty); - Assert.That(logging.Argument, Is.Not.Empty); - Assert.That(logging.File, Is.Not.Null); - Assert.Multiple(() => - { - Assert.That(logging.File.Id, Is.Not.Empty); - Assert.That(logging.File.Path, Is.Null); - Assert.That(logging.File.Sha1, Is.Not.Empty); - Assert.That(logging.File.Size, Is.Not.Zero); - Assert.That(logging.File.Url, Is.Not.Empty); - }); - Assert.That(logging.Type, Is.Not.Empty); - } - }); - Assert.That(meta.MainClass, Is.Not.Empty); - Assert.That(meta.MinimumLauncherVersion, Is.Not.Zero); - Assert.That(meta.ReleaseTime, Is.Not.Empty); - Assert.That(meta.Time, Is.Not.Empty); - Assert.That(meta.Type, Is.Not.EqualTo(ReleaseTypeEnum.Unknown)); - } - } - - [Test] - public void ArgumentsParsing() - { - object[] testGameArgs = - [ - "--username", - "${auth_player_name}", - "--version", - "${version_name}", - "--gameDir", - "${game_directory}", - "--assetsDir", - "${assets_root}", - "--assetIndex", - "${assets_index_name}", - "--uuid", - "${auth_uuid}", - "--accessToken", - "${auth_access_token}", - "--clientId", - "${clientid}", - "--xuid", - "${auth_xuid}", - "--userType", - "${user_type}", - "--versionType", - "${version_type}", - new MetadataFile.ConditionalArg - { - Rules = - [ - new MetadataFile.Rule - { - Action = MetadataFile.Rule.ActionEnum.Allow, - Features = new Dictionary { ["is_demo_user"] = true } - } - ], - Value = ["--demo"] - }, - new MetadataFile.ConditionalArg - { - Rules = - [ - new MetadataFile.Rule - { - Action = MetadataFile.Rule.ActionEnum.Allow, - Features = new Dictionary { ["has_custom_resolution"] = true } - } - ], - Value = - [ - "--width", - "${resolution_width}", - "--height", - "${resolution_height}" - ] - }, - new MetadataFile.ConditionalArg - { - Rules = - [ - new MetadataFile.Rule - { - Action = MetadataFile.Rule.ActionEnum.Allow, - Features = new Dictionary { ["has_quick_plays_support"] = true } - } - ], - Value = - [ - "--quickPlayPath", - "${quickPlayPath}" - ] - }, - new MetadataFile.ConditionalArg - { - Rules = - [ - new MetadataFile.Rule - { - Action = MetadataFile.Rule.ActionEnum.Allow, - Features = new Dictionary { ["is_quick_play_singleplayer"] = true } - } - ], - Value = - [ - "--quickPlaySingleplayer", - "${quickPlaySingleplayer}" - ] - }, - new MetadataFile.ConditionalArg - { - Rules = - [ - new MetadataFile.Rule - { - Action = MetadataFile.Rule.ActionEnum.Allow, - Features = new Dictionary { ["is_quick_play_multiplayer"] = true } - } - ], - Value = - [ - "--quickPlayMultiplayer", - "${quickPlayMultiplayer}" - ] - }, - new MetadataFile.ConditionalArg - { - Rules = - [ - new MetadataFile.Rule - { - Action = MetadataFile.Rule.ActionEnum.Allow, - Features = new Dictionary { ["is_quick_play_realms"] = true } - } - ], - Value = - [ - "--quickPlayRealms", - "${quickPlayRealms}" - ] - } - ]; - - var jsonObj = JsonNode.Parse(File.ReadAllText("./MCMetadataFiles/1.21.5.json"))!.AsObject(); - var meta = MetadataFile.Parse(jsonObj); - Assert.That(meta.Arguments.Game.Count, Is.EqualTo(testGameArgs.Length)); - for (var i = 0; i < meta.Arguments.Game.Count; i++) - { - if (testGameArgs[i] is string) - { - Assert.That(meta.Arguments.Game[i].Value.Count, Is.EqualTo(1)); - Assert.That(meta.Arguments.Game[i].Value[0], Is.EqualTo(testGameArgs[i])); - } - else - { - var arg = meta.Arguments.Game[i]; - var testArg = (MetadataFile.ConditionalArg)testGameArgs[i]; - - Assert.That(arg.Value.SequenceEqual(testArg.Value), Is.True); - Assert.That( - (arg.Rules is null && testArg.Rules is null) || - (arg.Rules is not null && testArg.Rules is not null)); - if (arg.Rules is not null && testArg.Rules is not null) - { - Assert.That(arg.Rules.Count, Is.EqualTo(testArg.Rules.Count)); - foreach (var (rule, testRule) in arg.Rules.Zip(testArg.Rules)) - { - Assert.That(rule.Action, Is.EqualTo(testRule.Action)); - Assert.That((rule.Features is null && testRule.Features is null) || - (rule.Features is not null && testRule.Features is not null)); - if (rule.Features is not null && testRule.Features is not null) - Assert.That(rule.Features.SequenceEqual(testRule.Features)); - Assert.That((rule.Os is null && testRule.Os is null) || - (rule.Os is not null && testRule.Os is not null)); - if (rule.Os is not null && testRule.Os is not null) - { - Assert.That(rule.Os.Arch, Is.EqualTo(testRule.Os.Arch)); - Assert.That(rule.Os.Name, Is.EqualTo(testRule.Os.Name)); - Assert.That(rule.Os.Version, Is.EqualTo(testRule.Os.Version)); - } - } - } - } - } - } -} diff --git a/PCL.Neo.Tests/Core/Models/Minecraft/Mod/ModInfoReaderTest.cs b/PCL.Neo.Tests/Core/Models/Minecraft/Mod/ModInfoReaderTest.cs new file mode 100644 index 00000000..1741a8f0 --- /dev/null +++ b/PCL.Neo.Tests/Core/Models/Minecraft/Mod/ModInfoReaderTest.cs @@ -0,0 +1,27 @@ +using PCL.Neo.Core.Models.Minecraft.Mod; +using System; +using System.Threading.Tasks; + +namespace PCL.Neo.Tests.Core.Models.Minecraft.Mod; + +[TestFixture] +[TestOf(typeof(ModInfoReader))] +public class ModInfoReaderTest +{ + [Test] + public async Task GetModInfo_ShouldGetModInfos() + { + const string modDir = + @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft\versions\1.20.4-Fabric 0.15.11-[轻量通用]\mods"; + var mods = await ModInfoReader.GetModInfo(modDir); + + foreach (var modInfo in mods) + { + Console.WriteLine(modInfo.Name); + Console.WriteLine(modInfo.Description); + Console.WriteLine(modInfo.Version); + Console.WriteLine(modInfo.Icon); + Console.WriteLine("--------"); + } + } +} diff --git a/PCL.Neo.Tests/Core/Service/Profiles/ProfileServiceTest.cs b/PCL.Neo.Tests/Core/Service/Profiles/ProfileServiceTest.cs index 7b644f8c..1a25640d 100644 --- a/PCL.Neo.Tests/Core/Service/Profiles/ProfileServiceTest.cs +++ b/PCL.Neo.Tests/Core/Service/Profiles/ProfileServiceTest.cs @@ -4,7 +4,7 @@ using PCL.Neo.Core.Service.Profiles.Data; using System; using System.IO; -using System.Text.Json; +using System.Linq; using System.Threading.Tasks; namespace PCL.Neo.Tests.Core.Service.Profiles; @@ -27,25 +27,40 @@ public void SetUp() } } + [TearDown] + public void TearDown() + { + if (Directory.Exists(SaveTempDir)) + { + Directory.Delete(SaveTempDir, true); + } + } + + [Test] + public async Task LoadProfilesDefaultAsync_ShouldLoadProfiles() + { + var profiles = await _service.LoadProfilesDefaultAsync(); + var profileInfos = profiles as ProfileInfo[] ?? profiles.ToArray(); + Assert.That(profileInfos, Is.Not.Empty); + foreach (var profile in profileInfos) + { + Console.WriteLine($"Profile Name: {profile.ProfileName}"); + Console.WriteLine($"Target Directory: {profile.TargetDir}"); + Console.WriteLine($"Games Count: {profile.Games.Count}"); + Console.WriteLine(new string('-', 20)); + } + } + [Test] - public async Task SaveProfilesDefaultAsync_ShouldLoadAllGamesAndSave() + public async Task SaveProfilesDefaultAsync_ShouldGetAllGamesAndSave() { var profile = await _service.GetProfileAsync(TempDir, "Test_SaveProfiles"); Assert.That(profile, Is.Not.Null); - Assert.That(profile.Games.Count, Is.EqualTo(33)); + Assert.That(profile.Games.Count, Is.EqualTo(36)); - await _service.SaveProfilesDefaultAsync(profile); + var result = await _service.SaveProfilesDefaultAsync(profile); - var profiles = await _service.LoadProfilesDefaultAsync(); - foreach (var pro in profiles) - { - foreach (var gameInfo in pro.Games) - { - Console.WriteLine(gameInfo.Name); - Console.WriteLine(gameInfo.Type); - Console.WriteLine(new string('-', 10)); - } - } + Assert.That(result, Is.True); } [Test] @@ -55,13 +70,9 @@ public async Task GetProfileAsync_ShouldSuccess() await _service.GetProfileAsync(@"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft", "Test_LoadAndGetProfile"); Assert.That(profiles, Is.Not.Null); - await _service.SaveProfilesDefaultAsync(profiles); - - var secProfiles = await _service.LoadProfilesDefaultAsync(); - Assert.That(secProfiles, Is.Not.Null); - - var content = JsonSerializer.Serialize(secProfiles, new JsonSerializerOptions { WriteIndented = true }); + var result = await _service.SaveProfilesDefaultAsync(profiles); + Assert.That(result, Is.True); } [Test] @@ -74,9 +85,9 @@ public void GetProfileAsync_ShouldThrowOnInvalidDir() } [Test] - public async Task LoadTargetGameAsync_ShouldLoadGame() + public async Task GetTargetGameAsync_ShouldGetGame() { - var game = await _service.LoadTargetGameAsync(TempDir, "1.20.6-Fabric 0.15.11"); + var game = await _service.GetTargetGameAsync(TempDir, "1.20.6-Fabric 0.15.11"); Assert.That(game, Is.Not.Null); Assert.That(game.Name, Is.EqualTo("1.20.6-Fabric 0.15.11")); Assert.That(game.IsIndie, Is.True); @@ -86,7 +97,7 @@ public async Task LoadTargetGameAsync_ShouldLoadGame() public void LoadTargetGameAsync_ShouldThrowOnMissingGame() { var ex = Assert.ThrowsAsync(async () => - await _service.LoadTargetGameAsync(TempDir, "not_exist")); + await _service.GetTargetGameAsync(TempDir, "not_exist")); StringAssert.Contains("Game directory not found", ex!.Message); } @@ -121,6 +132,7 @@ public async Task SaveGameInfoToProfileDefaultAsync_AddsGame() RootDirectory = TempDir, GameDirectory = "gd", IsIndie = false, + Version = "23w41a", Type = GameType.Vanilla }; var result = await _service.SaveGameInfoToProfileDefaultAsync(profile, game); @@ -140,6 +152,7 @@ public async Task SaveGameInfoToProfileAsync_AddsGame() Name = "test2", RootDirectory = TempDir, GameDirectory = "gd2", + Version = "23w41a", IsIndie = true, Type = GameType.Vanilla }; @@ -159,6 +172,7 @@ public void DeleteGame_ShouldDeleteGame() GameDirectory = string.Empty, RootDirectory = string.Empty, IsIndie = true, + Version = "23w41a", Type = GameType.Vanilla, Name = "None" }; diff --git a/PCL.Neo/ViewModels/MainWindowViewModel.cs b/PCL.Neo/ViewModels/MainWindowViewModel.cs index 56e16059..1af536a8 100644 --- a/PCL.Neo/ViewModels/MainWindowViewModel.cs +++ b/PCL.Neo/ViewModels/MainWindowViewModel.cs @@ -309,7 +309,8 @@ await JavaRuntime.CreateJavaEntityAsync( GameDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft\versions\Create", RootDirectory = @"C:\Users\WhiteCAT\Desktop\Games\PCL2\.minecraft", - Name = "Create" + Name = "Create", + Version = "Unknow" }, launchOptions );