diff --git a/MinecraftLaunch.Base/Enums/AccountType.cs b/MinecraftLaunch.Base/Enums/AccountType.cs index 8c09455..091e01a 100644 --- a/MinecraftLaunch.Base/Enums/AccountType.cs +++ b/MinecraftLaunch.Base/Enums/AccountType.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Enums; -public enum AccountType { +public enum AccountType +{ Offline = 0, Microsoft = 1, Yggdrasil = 2 diff --git a/MinecraftLaunch.Base/Enums/ClassId.cs b/MinecraftLaunch.Base/Enums/ClassId.cs new file mode 100644 index 0000000..4982b83 --- /dev/null +++ b/MinecraftLaunch.Base/Enums/ClassId.cs @@ -0,0 +1,14 @@ +namespace MinecraftLaunch.Base.Enums; + +public enum ClassId +{ + Modpacks = 4471, + Shaders = 6552, + Mods = 6, + BukkitPlugins = 5, + Addons = 4559, + Worlds = 17, + ResourcePacks = 12, + Customization = 4546, + DataPacks = 6945 +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Enums/CrashReasons.cs b/MinecraftLaunch.Base/Enums/CrashReasons.cs index 8558425..c7d5846 100644 --- a/MinecraftLaunch.Base/Enums/CrashReasons.cs +++ b/MinecraftLaunch.Base/Enums/CrashReasons.cs @@ -1,8 +1,10 @@ namespace MinecraftLaunch.Base.Enums; -public enum CrashReasons { +public enum CrashReasons +{ // Minecraft FileOrContentCheckFailed, + SpecificBlockCausedCrash, SpecificEntityCausedCrash, TextureTooLargeOrInsufficientGraphicsConfig, @@ -11,6 +13,7 @@ public enum CrashReasons { // Mod ModConfigCausedGameCrash, + ModMixinFailed, ModLoaderError, ModInitializationFailed, @@ -21,6 +24,7 @@ public enum CrashReasons { // ModLoader OptiFineIncompatibleWithForge, + FabricError, FabricErrorWithSolution, ForgeError, @@ -30,11 +34,13 @@ public enum CrashReasons { // Log CrashLogStackAnalysisFoundKeyword, + CrashLogStackAnalysisFoundModName, MCLogStackAnalysisFoundKeyword, // Jvm InsufficientMemory, + UsingJDK, GraphicsCardDoesNotSupportOpenGL, UsingOpenJ9, diff --git a/MinecraftLaunch.Base/Enums/DependencyType.cs b/MinecraftLaunch.Base/Enums/DependencyType.cs new file mode 100644 index 0000000..f6a0f3f --- /dev/null +++ b/MinecraftLaunch.Base/Enums/DependencyType.cs @@ -0,0 +1,11 @@ +namespace MinecraftLaunch.Base.Enums; + +public enum DependencyType +{ + Embedded = 1, + Optional, + Required, + Tool, + Incompatible, + Include +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Enums/DownloadEntryType.cs b/MinecraftLaunch.Base/Enums/DownloadEntryType.cs index 34f5c29..fb585e7 100644 --- a/MinecraftLaunch.Base/Enums/DownloadEntryType.cs +++ b/MinecraftLaunch.Base/Enums/DownloadEntryType.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Enums; -public enum DownloadEntryType { +public enum DownloadEntryType +{ Jar, Asset, Library diff --git a/MinecraftLaunch.Base/Enums/DownloadResultType.cs b/MinecraftLaunch.Base/Enums/DownloadResultType.cs index 211a6d4..1998d9c 100644 --- a/MinecraftLaunch.Base/Enums/DownloadResultType.cs +++ b/MinecraftLaunch.Base/Enums/DownloadResultType.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Enums; -public enum DownloadResultType { +public enum DownloadResultType +{ Successful, Cancelled, Failed diff --git a/MinecraftLaunch.Base/Enums/FileReleaseType.cs b/MinecraftLaunch.Base/Enums/FileReleaseType.cs new file mode 100644 index 0000000..c03bd1b --- /dev/null +++ b/MinecraftLaunch.Base/Enums/FileReleaseType.cs @@ -0,0 +1,8 @@ +namespace MinecraftLaunch.Base.Enums; + +public enum FileReleaseType +{ + Release = 1, + Beta, + Alpha +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Enums/HashType.cs b/MinecraftLaunch.Base/Enums/HashType.cs index b3c3811..7741824 100644 --- a/MinecraftLaunch.Base/Enums/HashType.cs +++ b/MinecraftLaunch.Base/Enums/HashType.cs @@ -1,7 +1,8 @@ namespace MinecraftLaunch.Base.Enums; -public enum HashType { - SHA1, +public enum HashType +{ + SHA1, SHA256, SHA384, SHA512 diff --git a/MinecraftLaunch.Base/Enums/InstallStep.cs b/MinecraftLaunch.Base/Enums/InstallStep.cs index 511ab4c..b3d6734 100644 --- a/MinecraftLaunch.Base/Enums/InstallStep.cs +++ b/MinecraftLaunch.Base/Enums/InstallStep.cs @@ -1,8 +1,10 @@ namespace MinecraftLaunch.Base.Enums; -public enum InstallStep { +public enum InstallStep +{ //Common Started, + DownloadVersionJson, ParseMinecraft, DownloadLibraries, @@ -11,12 +13,14 @@ public enum InstallStep { //Forge Optifine DownloadPackage, + ParsePackage, WriteVersionJsonAndSomeDependencies, RunInstallProcessor, //Modpack ParseFiles, + ParseDownloadUrls, DownloadMods, ExtractModpack, @@ -24,12 +28,14 @@ public enum InstallStep { //Composite ParseInstaller, + InstallVanilla, InstallPrimaryModLoader, InstallSecondaryModLoader, //Java FetchingMetadata, + DownloadJava, ExtractingFiles, diff --git a/MinecraftLaunch.Base/Enums/MinecraftLogLevel.cs b/MinecraftLaunch.Base/Enums/MinecraftLogLevel.cs index 60b47b4..fe70b0b 100644 --- a/MinecraftLaunch.Base/Enums/MinecraftLogLevel.cs +++ b/MinecraftLaunch.Base/Enums/MinecraftLogLevel.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Enums; -public enum MinecraftLogLevel { +public enum MinecraftLogLevel +{ Fatal, Error, Warning, diff --git a/MinecraftLaunch.Base/Enums/MinecraftVersionType.cs b/MinecraftLaunch.Base/Enums/MinecraftVersionType.cs index af30917..1a40db7 100644 --- a/MinecraftLaunch.Base/Enums/MinecraftVersionType.cs +++ b/MinecraftLaunch.Base/Enums/MinecraftVersionType.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Enums; -public enum MinecraftVersionType { +public enum MinecraftVersionType +{ Release, Snapshot, OldBeta, diff --git a/MinecraftLaunch.Base/Enums/ModLoaderType.cs b/MinecraftLaunch.Base/Enums/ModLoaderType.cs index 9ea53d8..1245c6b 100644 --- a/MinecraftLaunch.Base/Enums/ModLoaderType.cs +++ b/MinecraftLaunch.Base/Enums/ModLoaderType.cs @@ -1,7 +1,10 @@ namespace MinecraftLaunch.Base.Enums; -public enum ModLoaderType { +public enum ModLoaderType +{ + // 模组 Any = 0, + Forge = 1, Cauldron = 2, LiteLoader = 3, @@ -10,4 +13,10 @@ public enum ModLoaderType { NeoForge = 6, OptiFine = 7, Unknown, + + // 光影 + Canvas, + + Iris, + Vanilla, } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Enums/ModrinthSearchIndex.cs b/MinecraftLaunch.Base/Enums/ModrinthSearchIndex.cs index 37b662a..32cb5e3 100644 --- a/MinecraftLaunch.Base/Enums/ModrinthSearchIndex.cs +++ b/MinecraftLaunch.Base/Enums/ModrinthSearchIndex.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Enums; -public enum ModrinthSearchIndex { +public enum ModrinthSearchIndex +{ Follows, Downloads, Relevance, diff --git a/MinecraftLaunch.Base/Enums/ResourceType.cs b/MinecraftLaunch.Base/Enums/ResourceType.cs new file mode 100644 index 0000000..18e5055 --- /dev/null +++ b/MinecraftLaunch.Base/Enums/ResourceType.cs @@ -0,0 +1,12 @@ +namespace MinecraftLaunch.Base.Enums; + +public enum ResourceType +{ + Instance, + Mod, + Modpack, + Datapack, + Resourcepack, + Shaderpack, + World +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/EventArgs/DownloadProgressChangedEventArgs.cs b/MinecraftLaunch.Base/EventArgs/DownloadProgressChangedEventArgs.cs index 02efe68..0865c00 100644 --- a/MinecraftLaunch.Base/EventArgs/DownloadProgressChangedEventArgs.cs +++ b/MinecraftLaunch.Base/EventArgs/DownloadProgressChangedEventArgs.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.EventArgs; -public sealed class ResourceDownloadProgressChangedEventArgs : System.EventArgs { +public sealed class ResourceDownloadProgressChangedEventArgs : System.EventArgs +{ public double Speed { get; set; } public int TotalCount { get; set; } public int CompletedCount { get; set; } diff --git a/MinecraftLaunch.Base/EventArgs/InstallProgressChangedEventArgs.cs b/MinecraftLaunch.Base/EventArgs/InstallProgressChangedEventArgs.cs index 7d7fa4f..1cf7fe4 100644 --- a/MinecraftLaunch.Base/EventArgs/InstallProgressChangedEventArgs.cs +++ b/MinecraftLaunch.Base/EventArgs/InstallProgressChangedEventArgs.cs @@ -2,26 +2,29 @@ namespace MinecraftLaunch.Base.EventArgs; -public class InstallProgressChangedEventArgs : System.EventArgs { +public class InstallProgressChangedEventArgs : System.EventArgs +{ public double Speed { get; set; } - public required double Progress { get; set; } + public double Progress { get; set; } public int TotalStepTaskCount { get; set; } public int FinishedStepTaskCount { get; set; } - public required TaskStatus Status { get; set; } - public required InstallStep StepName { get; set; } - public required bool IsStepSupportSpeed { get; set; } + public TaskStatus Status { get; set; } + public InstallStep StepName { get; set; } + public bool IsStepSupportSpeed { get; set; } [Obsolete($"Replaced by {nameof(StepName)}")] public string ProgressStatus { get; set; } } -public class InstallComplatedEventArgs : System.EventArgs { +public class InstallComplatedEventArgs : System.EventArgs +{ public bool IsSuccessful { get; set; } public Exception Exception { get; set; } } -public sealed class CompositeInstallProgressChangedEventArgs : InstallProgressChangedEventArgs { +public sealed class CompositeInstallProgressChangedEventArgs : InstallProgressChangedEventArgs +{ public InstallStep PrimaryStepName { get; set; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/IDataProcessor.cs b/MinecraftLaunch.Base/Interfaces/IDataProcessor.cs index 5b3d954..7effad0 100644 --- a/MinecraftLaunch.Base/Interfaces/IDataProcessor.cs +++ b/MinecraftLaunch.Base/Interfaces/IDataProcessor.cs @@ -2,9 +2,11 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface IDataProcessor { +public interface IDataProcessor +{ Dictionary Datas { get; set; } void Handle(IEnumerable data); + Task SaveAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/IDownloadDependency.cs b/MinecraftLaunch.Base/Interfaces/IDownloadDependency.cs index d7e89f2..0571356 100644 --- a/MinecraftLaunch.Base/Interfaces/IDownloadDependency.cs +++ b/MinecraftLaunch.Base/Interfaces/IDownloadDependency.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface IDownloadDependency { +public interface IDownloadDependency +{ string Url { get; } string FullPath { get; } long? Size { get; } diff --git a/MinecraftLaunch.Base/Interfaces/IDownloadMirror.cs b/MinecraftLaunch.Base/Interfaces/IDownloadMirror.cs index 55d3934..f1f8e35 100644 --- a/MinecraftLaunch.Base/Interfaces/IDownloadMirror.cs +++ b/MinecraftLaunch.Base/Interfaces/IDownloadMirror.cs @@ -1,5 +1,6 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface IDownloadMirror { +public interface IDownloadMirror +{ public string TryFindUrl(string sourceUrl); } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/IDownloader.cs b/MinecraftLaunch.Base/Interfaces/IDownloader.cs index 3da22d8..4be1b1b 100644 --- a/MinecraftLaunch.Base/Interfaces/IDownloader.cs +++ b/MinecraftLaunch.Base/Interfaces/IDownloader.cs @@ -2,7 +2,9 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface IDownloader { +public interface IDownloader +{ Task DownloadAsync(DownloadRequest request, CancellationToken cancellationToken); + Task DownloadManyAsync(GroupDownloadRequest requests, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/IInstallEntry.cs b/MinecraftLaunch.Base/Interfaces/IInstallEntry.cs index 4e012b9..5378eb3 100644 --- a/MinecraftLaunch.Base/Interfaces/IInstallEntry.cs +++ b/MinecraftLaunch.Base/Interfaces/IInstallEntry.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface IInstallEntry { +public interface IInstallEntry +{ /// /// Minecraft 的版本号,例如 "1.20.1" /// diff --git a/MinecraftLaunch.Base/Interfaces/IInstaller.cs b/MinecraftLaunch.Base/Interfaces/IInstaller.cs index ffb0663..1187ed9 100644 --- a/MinecraftLaunch.Base/Interfaces/IInstaller.cs +++ b/MinecraftLaunch.Base/Interfaces/IInstaller.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface IInstaller { +public interface IInstaller +{ string MinecraftFolder { get; } Task InstallAsync(CancellationToken cancellationToken = default); diff --git a/MinecraftLaunch.Base/Interfaces/INbtParser.cs b/MinecraftLaunch.Base/Interfaces/INbtParser.cs index 70d8826..e7f913a 100644 --- a/MinecraftLaunch.Base/Interfaces/INbtParser.cs +++ b/MinecraftLaunch.Base/Interfaces/INbtParser.cs @@ -3,8 +3,11 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface INbtParser { +public interface INbtParser +{ NbtReader GetReader(NbtCompression compression = NbtCompression.None); + NbtWriter GetWriter(NbtCompression compression = NbtCompression.None); + Task ParseSaveAsync(string saveName, bool @bool = true, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/IResource.cs b/MinecraftLaunch.Base/Interfaces/IResource.cs index 3947322..28f4354 100644 --- a/MinecraftLaunch.Base/Interfaces/IResource.cs +++ b/MinecraftLaunch.Base/Interfaces/IResource.cs @@ -1,12 +1,18 @@ -namespace MinecraftLaunch.Base.Interfaces; +using MinecraftLaunch.Base.Enums; -public interface IResource { - string Name { get; init; } - string Summary { get; init; } - string IconUrl { get; init; } - int DownloadCount { get; init; } - DateTime DateModified { get; init; } - IEnumerable Categories { get; init; } - IEnumerable Screenshots { get; init; } - IEnumerable MinecraftVersions { get; init; } +namespace MinecraftLaunch.Base.Interfaces; + +public interface IResource +{ + public string Name { get; init; } + public string Summary { get; init; } + public string IconUrl { get; init; } + public string WebsiteUrl { get; init; } + public int DownloadCount { get; init; } + public DateTime DateModified { get; init; } + public IEnumerable Categories { get; init; } + public IEnumerable Screenshots { get; init; } + public IEnumerable MinecraftVersions { get; init; } + public IEnumerable Loaders { get; init; } + public ResourceType ResourceType { get; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/IResourceFile.cs b/MinecraftLaunch.Base/Interfaces/IResourceFile.cs new file mode 100644 index 0000000..07e53e8 --- /dev/null +++ b/MinecraftLaunch.Base/Interfaces/IResourceFile.cs @@ -0,0 +1,16 @@ +using MinecraftLaunch.Base.Enums; + +namespace MinecraftLaunch.Base.Interfaces; + +public interface IResourceFile +{ + public string DisplayName { get; init; } + public string FileName { get; init; } + public string DownloadUrl { get; init; } + public long DownloadCount { get; init; } + public DateTime Published { get; init; } + public FileReleaseType ReleaseType { get; init; } + public IEnumerable GameVersions { get; init; } + public IEnumerable Loaders { get; init; } + public long FileSize { get; init; } +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/ISearchResult.cs b/MinecraftLaunch.Base/Interfaces/ISearchResult.cs new file mode 100644 index 0000000..20a97aa --- /dev/null +++ b/MinecraftLaunch.Base/Interfaces/ISearchResult.cs @@ -0,0 +1,8 @@ +namespace MinecraftLaunch.Base.Interfaces; + +public interface ISearchResult +{ + public int Index { get; init; } + public int PageSize { get; init; } + public long TotalCount { get; init; } +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Interfaces/IVerifiableDependency.cs b/MinecraftLaunch.Base/Interfaces/IVerifiableDependency.cs index d24556f..53eaffc 100644 --- a/MinecraftLaunch.Base/Interfaces/IVerifiableDependency.cs +++ b/MinecraftLaunch.Base/Interfaces/IVerifiableDependency.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Interfaces; -public interface IVerifiableDependency { +public interface IVerifiableDependency +{ long? Size { get; } string Sha1 { get; } -} +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Authentication/Account.cs b/MinecraftLaunch.Base/Models/Authentication/Account.cs index a156700..f300fb5 100644 --- a/MinecraftLaunch.Base/Models/Authentication/Account.cs +++ b/MinecraftLaunch.Base/Models/Authentication/Account.cs @@ -6,18 +6,21 @@ namespace MinecraftLaunch.Base.Models.Authentication; [JsonDerivedType(typeof(OfflineAccount), typeDiscriminator: "offline")] [JsonDerivedType(typeof(MicrosoftAccount), typeDiscriminator: "microsoft")] [JsonDerivedType(typeof(YggdrasilAccount), typeDiscriminator: "yggdrasil")] -public abstract record Account(string Name, Guid Uuid, string AccessToken) { +public abstract record Account(string Name, Guid Uuid, string AccessToken) +{ public abstract AccountType Type { get; } public string Name { get; init; } = Name; public Guid Uuid { get; init; } = Uuid; public string AccessToken { get; set; } = AccessToken; - public override int GetHashCode() { + public override int GetHashCode() + { return Type.GetHashCode() ^ Name.GetHashCode() ^ Uuid.GetHashCode(); } - public virtual bool ProfileEquals(Account account) { + public virtual bool ProfileEquals(Account account) + { if (account.Type.Equals(this.Type) && account.Uuid.Equals(this.Uuid) && account.Name.Equals(this.Name)) @@ -32,14 +35,16 @@ public record MicrosoftAccount( Guid Uuid, string AccessToken, string RefreshToken, - DateTime LastRefreshTime) : Account(Name, Uuid, AccessToken) { + DateTime LastRefreshTime) : Account(Name, Uuid, AccessToken) +{ public override AccountType Type => AccountType.Microsoft; public DateTime LastRefreshTime { get; set; } = LastRefreshTime; public string RefreshToken { get; set; } = RefreshToken; - public override bool ProfileEquals(Account account) { + public override bool ProfileEquals(Account account) + { if (account is MicrosoftAccount microsoftAccount && microsoftAccount.Uuid.Equals(this.Uuid)) return true; @@ -55,7 +60,8 @@ public record YggdrasilAccount( Guid Uuid, string AccessToken, string YggdrasilServerUrl, - string ClientToken = default) : Account(Name, Uuid, AccessToken) { + string ClientToken = default) : Account(Name, Uuid, AccessToken) +{ public override AccountType Type => AccountType.Yggdrasil; public string ClientToken { get; set; } = ClientToken; @@ -63,7 +69,8 @@ public record YggdrasilAccount( public Dictionary MetaData { get; set; } = []; - public override bool ProfileEquals(Account account) { + public override bool ProfileEquals(Account account) + { if (account is YggdrasilAccount yggdrasilAccount && yggdrasilAccount.YggdrasilServerUrl.Equals(this.YggdrasilServerUrl) && yggdrasilAccount.Uuid.Equals(this.Uuid)) @@ -78,6 +85,7 @@ public override bool ProfileEquals(Account account) { public record OfflineAccount( string Name, Guid Uuid, - string AccessToken) : Account(Name, Uuid, AccessToken) { + string AccessToken) : Account(Name, Uuid, AccessToken) +{ public override AccountType Type => AccountType.Offline; } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Authentication/Microsoft/DeviceCodeResponse.cs b/MinecraftLaunch.Base/Models/Authentication/Microsoft/DeviceCodeResponse.cs index 821b394..7840252 100644 --- a/MinecraftLaunch.Base/Models/Authentication/Microsoft/DeviceCodeResponse.cs +++ b/MinecraftLaunch.Base/Models/Authentication/Microsoft/DeviceCodeResponse.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Models.Authentication.Microsoft; -public record DeviceCodeResponse { +public record DeviceCodeResponse +{ [JsonPropertyName("interval")] public int Interval { get; set; } [JsonPropertyName("message")] public string Message { get; set; } [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } diff --git a/MinecraftLaunch.Base/Models/Authentication/Microsoft/MicrosoftRequestPayload.cs b/MinecraftLaunch.Base/Models/Authentication/Microsoft/MicrosoftRequestPayload.cs index c6b3d15..f5caafe 100644 --- a/MinecraftLaunch.Base/Models/Authentication/Microsoft/MicrosoftRequestPayload.cs +++ b/MinecraftLaunch.Base/Models/Authentication/Microsoft/MicrosoftRequestPayload.cs @@ -4,30 +4,35 @@ namespace MinecraftLaunch.Base.Models.Authentication.Microsoft; public record MinecraftPayload(string identityToken); -public record XBLProperties { +public record XBLProperties +{ public required string SiteName { get; init; } public required string RpsTicket { get; init; } public required string AuthMethod { get; init; } } -public record XSTSProperties { +public record XSTSProperties +{ public required string SandboxId { get; init; } public required string[] UserTokens { get; init; } } -public record XBLTokenPayload { +public record XBLTokenPayload +{ public required string TokenType { get; init; } public required string RelyingParty { get; init; } public required XBLProperties Properties { get; init; } } -public record XSTSTokenPayload { +public record XSTSTokenPayload +{ public required string TokenType { get; init; } public required string RelyingParty { get; init; } public required XSTSProperties Properties { get; init; } } -public record RefreshTokenPayload { +public record RefreshTokenPayload +{ [JsonPropertyName("client_id")] public required string ClientId { get; init; } [JsonPropertyName("grant_type")] public required string GrantType { get; init; } [JsonPropertyName("refresh_token")] public required string RefreshToken { get; init; } diff --git a/MinecraftLaunch.Base/Models/Authentication/Microsoft/OAuth2TokenResponse.cs b/MinecraftLaunch.Base/Models/Authentication/Microsoft/OAuth2TokenResponse.cs index 32fdacd..f9f5e04 100644 --- a/MinecraftLaunch.Base/Models/Authentication/Microsoft/OAuth2TokenResponse.cs +++ b/MinecraftLaunch.Base/Models/Authentication/Microsoft/OAuth2TokenResponse.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Models.Authentication.Microsoft; -public record OAuth2TokenResponse { +public record OAuth2TokenResponse +{ [JsonPropertyName("foci")] public string Foci { get; set; } [JsonPropertyName("scope")] public string Scope { get; set; } [JsonPropertyName("user_id")] public string UserId { get; set; } diff --git a/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilRequestPayload.cs b/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilRequestPayload.cs index 16e7e14..69fa8cf 100644 --- a/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilRequestPayload.cs +++ b/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilRequestPayload.cs @@ -2,34 +2,39 @@ namespace MinecraftLaunch.Base.Models.Authentication.Yggdrasil; -public record Agent { +public record Agent +{ [JsonPropertyName("version")] public int Version { get; init; } [JsonPropertyName("name")] public required string Name { get; init; } } -public record SelectedProfile { +public record SelectedProfile +{ [JsonPropertyName("id")] public required string Id { get; init; } [JsonPropertyName("name")] public required string Name { get; init; } } -public record YggdrasilAuthenticatePayload { +public record YggdrasilAuthenticatePayload +{ [JsonPropertyName("requestUser")] public bool RequestUser { get; init; } [JsonPropertyName("username")] public required string Username { get; init; } [JsonPropertyName("password")] public required string Password { get; init; } [JsonPropertyName("clientToken")] public required string ClientToken { get; init; } [JsonPropertyName("agent")] - public Agent Agent { get; set; } = new Agent { + public Agent Agent { get; set; } = new Agent + { Name = "Minecraft", Version = 1 }; } -public class YggdrasilRefreshPayload { - [JsonPropertyName("accessToken")] public required string AccessToken { get; init; } - [JsonPropertyName("clientToken")] public required string ClientToken { get; init; } - [JsonPropertyName("requestUser")] public required bool RequestUser { get; init; } - [JsonPropertyName("selectedProfile")] public required SelectedProfile SelectedProfile { get; init; } +public class YggdrasilRefreshPayload +{ + [JsonPropertyName("accessToken")] public string AccessToken { get; init; } + [JsonPropertyName("clientToken")] public string ClientToken { get; init; } + [JsonPropertyName("requestUser")] public bool RequestUser { get; init; } + [JsonPropertyName("selectedProfile")] public SelectedProfile SelectedProfile { get; init; } } [JsonSerializable(typeof(YggdrasilRefreshPayload))] diff --git a/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilResponse.cs b/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilResponse.cs index b411025..c15488b 100644 --- a/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilResponse.cs +++ b/MinecraftLaunch.Base/Models/Authentication/Yggdrasil/YggdrasilResponse.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Models.Authentication.Yggdrasil; -public record YggdrasilResponse { +public record YggdrasilResponse +{ [JsonPropertyName("user")] public User User { get; set; } [JsonPropertyName("clientToken")] public string ClientToken { get; set; } [JsonPropertyName("accessToken")] public string AccessToken { get; set; } @@ -10,19 +11,22 @@ public record YggdrasilResponse { [JsonPropertyName("availableProfiles")] public IEnumerable AvailableProfiles { get; set; } } -public record User { +public record User +{ [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("properties")] public IEnumerable Properties { get; set; } } -public record PropertyModel { +public record PropertyModel +{ [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("value")] public string Value { get; set; } [JsonPropertyName("userId")] public string UserId { get; set; } [JsonPropertyName("profileId")] public string ProfileId { get; set; } } -public record ProfileModel { +public record ProfileModel +{ [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } } diff --git a/MinecraftLaunch.Base/Models/ComponentSettings.cs b/MinecraftLaunch.Base/Models/ComponentSettings.cs index 35d823d..4d21e75 100644 --- a/MinecraftLaunch.Base/Models/ComponentSettings.cs +++ b/MinecraftLaunch.Base/Models/ComponentSettings.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Models; -public record ComponentSettings { +public record ComponentSettings +{ public bool IsEnableMirror { get; set; } public bool IsEnableFragment { get; set; } = true; diff --git a/MinecraftLaunch.Base/Models/Game/JavaEntry.cs b/MinecraftLaunch.Base/Models/Game/JavaEntry.cs index 5726319..2adcbfc 100644 --- a/MinecraftLaunch.Base/Models/Game/JavaEntry.cs +++ b/MinecraftLaunch.Base/Models/Game/JavaEntry.cs @@ -1,6 +1,7 @@ namespace MinecraftLaunch.Base.Models.Game; -public record JavaEntry { +public record JavaEntry +{ public bool Is64bit { get; init; } public string JavaPath { get; init; } public string JavaType { get; init; } @@ -9,7 +10,8 @@ public record JavaEntry { public string JavaFolder => Path.GetDirectoryName(JavaPath); - public override string ToString() { + public override string ToString() + { return $"{JavaVersion} - {JavaType} - {JavaPath}"; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Game/LaunchConfig.cs b/MinecraftLaunch.Base/Models/Game/LaunchConfig.cs index 6774f58..8df169e 100644 --- a/MinecraftLaunch.Base/Models/Game/LaunchConfig.cs +++ b/MinecraftLaunch.Base/Models/Game/LaunchConfig.cs @@ -1,9 +1,9 @@ using MinecraftLaunch.Base.Models.Authentication; -using System.Diagnostics.CodeAnalysis; namespace MinecraftLaunch.Base.Models.Game; -public record LaunchConfig { +public record LaunchConfig +{ public Account Account { get; set; } public bool IsFullscreen { get; set; } @@ -23,7 +23,8 @@ public record LaunchConfig { public IEnumerable JvmArguments { get; set; } = []; } -public record ServerInfo { +public record ServerInfo +{ public int Port { get; set; } = 25565; public string Address { get; set; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Game/LauncherProfile.cs b/MinecraftLaunch.Base/Models/Game/LauncherProfile.cs index 00f46d8..3ce1fce 100644 --- a/MinecraftLaunch.Base/Models/Game/LauncherProfile.cs +++ b/MinecraftLaunch.Base/Models/Game/LauncherProfile.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Models.Game; -public record LauncherProfileEntry { +public record LauncherProfileEntry +{ [JsonPropertyName("clientToken")] public string ClientToken { get; set; } [JsonPropertyName("launcherVersion")] public LauncherVersionEntry LauncherVersion { get; set; } [JsonPropertyName("profiles")] public Dictionary Profiles { get; set; } @@ -12,7 +13,8 @@ public record LauncherProfileEntry { public SelectedUserEntry SelectedAccount { get; set; } } -public record GameProfileEntry { +public record GameProfileEntry +{ [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("gameDir")] public string GameFolder { get; set; } [JsonPropertyName("lastVersionId")] public string LastVersionId { get; set; } @@ -35,20 +37,22 @@ public record GameProfileEntry { [JsonPropertyName("resolution")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ResolutionEntry Resolution { get; set; } - } -public record SelectedUserEntry { +public record SelectedUserEntry +{ [JsonPropertyName("account")] public string Account { get; set; } [JsonPropertyName("profile")] public string Profile { get; set; } } -public record LauncherVersionEntry { +public record LauncherVersionEntry +{ [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("format")] public int Format { get; set; } } -public record ResolutionEntry { +public record ResolutionEntry +{ [JsonIgnore] public bool IsFullScreen { get; set; } [JsonPropertyName("width")] public int Width { get; set; } = 854; [JsonPropertyName("height")] public int Height { get; set; } = 480; diff --git a/MinecraftLaunch.Base/Models/Game/MinecraftEntry.cs b/MinecraftLaunch.Base/Models/Game/MinecraftEntry.cs index 3bcaa68..b20a1d2 100644 --- a/MinecraftLaunch.Base/Models/Game/MinecraftEntry.cs +++ b/MinecraftLaunch.Base/Models/Game/MinecraftEntry.cs @@ -10,53 +10,67 @@ namespace MinecraftLaunch.Base.Models.Game; [JsonDerivedType(typeof(VanillaMinecraftEntry), typeDiscriminator: "vanilla")] [JsonDerivedType(typeof(ModifiedMinecraftEntry), typeDiscriminator: "modified")] -public abstract class MinecraftEntry { - public required string Id { get; init; } - public required MinecraftVersion Version { get; init; } +public abstract class MinecraftEntry +{ + public string Id { get; init; } + public MinecraftVersion Version { get; init; } - public required string ClientJarPath { get; init; } - public required DateTime ReleaseTime { get; init; } - public required string ClientJsonPath { get; init; } - public required string AssetIndexJsonPath { get; init; } - public required string MinecraftFolderPath { get; init; } + public string ClientJarPath { get; init; } + public DateTime ReleaseTime { get; init; } + public string ClientJsonPath { get; init; } + public string AssetIndexJsonPath { get; init; } + public string MinecraftFolderPath { get; init; } public bool IsVanilla => this is VanillaMinecraftEntry; - private static bool IsLibraryEnabled(IEnumerable rules) { + private static bool IsLibraryEnabled(IEnumerable rules) + { bool windows, linux, osx; windows = linux = osx = false; - foreach (var item in rules) { - if (item.Action == "allow") { - if (item.System == null) { + foreach (var item in rules) + { + if (item.Action == "allow") + { + if (item.System == null) + { windows = linux = osx = true; continue; } - switch (item.System.Name) { + switch (item.System.Name) + { case "windows": windows = true; break; + case "linux": linux = true; break; + case "osx": osx = true; break; } - } else if (item.Action == "disallow") { - if (item.System == null) { + } + else if (item.Action == "disallow") + { + if (item.System == null) + { windows = linux = osx = false; continue; } - switch (item.System.Name) { + switch (item.System.Name) + { case "windows": windows = false; break; + case "linux": linux = false; break; + case "osx": osx = false; break; @@ -66,7 +80,8 @@ private static bool IsLibraryEnabled(IEnumerable rules) { // TODO: Check OS version and architecture? - return EnvironmentUtil.GetPlatformName() switch { + return EnvironmentUtil.GetPlatformName() switch + { "windows" => windows, "linux" => linux, "osx" => osx, @@ -74,7 +89,8 @@ private static bool IsLibraryEnabled(IEnumerable rules) { }; } - public IEnumerable GetRequiredAssets() { + public IEnumerable GetRequiredAssets() + { // Identify file paths string assetIndexJsonPath = AssetIndexJsonPath; if (this is ModifiedMinecraftEntry { HasInheritance: true } instance) @@ -87,11 +103,13 @@ public IEnumerable GetRequiredAssets() { ?? throw new InvalidDataException("Error in parsing asset index json file"); // Parse GameAsset objects - foreach (var (key, assetJsonNode) in assets) { + foreach (var (key, assetJsonNode) in assets) + { int size = assetJsonNode.Size; string hash = assetJsonNode.Hash ?? throw new InvalidDataException("Invalid asset index"); - yield return new MinecraftAsset { + yield return new MinecraftAsset + { MinecraftFolderPath = MinecraftFolderPath, Key = key, Sha1 = hash, @@ -100,7 +118,8 @@ public IEnumerable GetRequiredAssets() { } } - public (IEnumerable Libraries, IEnumerable NativeLibraries) GetRequiredLibraries() { + public (IEnumerable Libraries, IEnumerable NativeLibraries) GetRequiredLibraries() + { List libs = []; List nativeLibs = []; @@ -108,12 +127,14 @@ public IEnumerable GetRequiredAssets() { .Deserialize(LibraryEntriesContext.Default.IEnumerableLibraryEntry) ?? throw new InvalidDataException("client.json does not contain library information"); - foreach (var libNode in libNodes) { + foreach (var libNode in libNodes) + { if (libNode is null) continue; // Check if a library is enabled - if (libNode.Rules is IEnumerable libRules) { + if (libNode.Rules is IEnumerable libRules) + { if (!IsLibraryEnabled(libRules)) continue; } @@ -123,7 +144,8 @@ public IEnumerable GetRequiredAssets() { if (gameLib.IsNativeLibrary) nativeLibs.Add(gameLib); - else { + else + { libs.Add(gameLib); } } @@ -131,8 +153,10 @@ public IEnumerable GetRequiredAssets() { return (libs, nativeLibs); } - public override bool Equals(object obj) { - if (obj is MinecraftEntry other) { + public override bool Equals(object obj) + { + if (obj is MinecraftEntry other) + { return string.Equals(Id, other.Id, StringComparison.OrdinalIgnoreCase) && string.Equals(MinecraftFolderPath, other.MinecraftFolderPath, StringComparison.OrdinalIgnoreCase); } @@ -140,7 +164,8 @@ public override bool Equals(object obj) { return false; } - public override int GetHashCode() { + public override int GetHashCode() + { return HashCode.Combine(Id?.ToLowerInvariant(), MinecraftFolderPath?.ToLowerInvariant()); } @@ -151,8 +176,9 @@ public sealed partial class LibraryEntriesContext : JsonSerializerContext; public class VanillaMinecraftEntry : MinecraftEntry; -public class ModifiedMinecraftEntry : MinecraftEntry { - public required IEnumerable ModLoaders { get; init; } +public class ModifiedMinecraftEntry : MinecraftEntry +{ + public IEnumerable ModLoaders { get; init; } public VanillaMinecraftEntry InheritedMinecraft { get; init; } @@ -160,11 +186,12 @@ public class ModifiedMinecraftEntry : MinecraftEntry { public bool HasInheritance { get => InheritedMinecraft is not null; } } -public abstract class MinecraftDependency { +public abstract class MinecraftDependency +{ /// /// Absolute path of the .minecraft folder /// - public required string MinecraftFolderPath { get; init; } + public string MinecraftFolderPath { get; init; } /// /// File path relative to the .minecraft folder @@ -177,18 +204,21 @@ public abstract class MinecraftDependency { public string FullPath => Path.Combine(MinecraftFolderPath, FilePath); } -public abstract partial class MinecraftLibrary : MinecraftDependency { - private readonly static Regex MavenParseRegex = GenerateMavenParseRegex(); +public abstract partial class MinecraftLibrary : MinecraftDependency +{ + private static readonly Regex MavenParseRegex = GenerateMavenParseRegex(); public string MavenName { get; init; } - public required bool IsNativeLibrary { get; init; } + public bool IsNativeLibrary { get; init; } public override string FilePath => Path.Combine("libraries", GetLibraryPath()); - public MinecraftLibrary(string mavenName) { + public MinecraftLibrary(string mavenName) + { this.MavenName = mavenName; Match match = MavenParseRegex.Match(mavenName); - if (match.Success) { + if (match.Success) + { Domain = match.Groups["domain"].Value; Name = match.Groups["name"].Value; Version = match.Groups["version"].Value; @@ -205,11 +235,12 @@ public MinecraftLibrary(string mavenName) { public string Version { get; init; } public string Classifier { get; init; } - #endregion + #endregion Maven Package Info internal string GetLibraryPath() => GetLibraryPath(this.MavenName); - internal static string GetLibraryPath(string mavenName) { + internal static string GetLibraryPath(string mavenName) + { string path = ""; var extension = mavenName.Contains('@') ? mavenName.Split('@') : []; @@ -231,7 +262,8 @@ internal static string GetLibraryPath(string mavenName) { return Path.Combine(path, filename); } - public static MinecraftLibrary ParseJsonNode(LibraryEntry libNode, string minecraftFolderPath) { + public static MinecraftLibrary ParseJsonNode(LibraryEntry libNode, string minecraftFolderPath) + { // Check platform-specific library name if (libNode.MavenName is null) throw new InvalidDataException("Invalid library name"); @@ -239,15 +271,18 @@ public static MinecraftLibrary ParseJsonNode(LibraryEntry libNode, string minecr if (libNode.NativeClassifierNames is not null) libNode.MavenName += ":" + libNode.NativeClassifierNames[EnvironmentUtil.GetPlatformName()].Replace("${arch}", EnvironmentUtil.Arch); - if (libNode.DownloadInformation != null) { + if (libNode.DownloadInformation != null) + { DownloadArtifactEntry artifactNode = GetLibraryArtifactInfo(libNode); if (artifactNode.Sha1 is null || artifactNode.Size is null || artifactNode.Url is null) throw new InvalidDataException("Invalid artifact node"); #region Vanilla Pattern - if (artifactNode.Url.StartsWith("https://libraries.minecraft.net/")) { - return new VanillaLibrary(libNode.MavenName) { + if (artifactNode.Url.StartsWith("https://libraries.minecraft.net/")) + { + return new VanillaLibrary(libNode.MavenName) + { MinecraftFolderPath = minecraftFolderPath, Sha1 = artifactNode.Sha1, Size = (long)artifactNode.Size, @@ -255,12 +290,14 @@ public static MinecraftLibrary ParseJsonNode(LibraryEntry libNode, string minecr }; } - #endregion + #endregion Vanilla Pattern #region Forge Pattern - if (artifactNode.Url.StartsWith("https://maven.minecraftforge.net/")) { - return new ForgeLibrary(libNode.MavenName) { + if (artifactNode.Url.StartsWith("https://maven.minecraftforge.net/")) + { + return new ForgeLibrary(libNode.MavenName) + { MinecraftFolderPath = minecraftFolderPath, Sha1 = artifactNode.Sha1, Size = (long)artifactNode.Size, @@ -269,12 +306,14 @@ public static MinecraftLibrary ParseJsonNode(LibraryEntry libNode, string minecr }; } - #endregion + #endregion Forge Pattern #region NeoForge Pattern - if (artifactNode.Url.StartsWith("https://maven.neoforged.net/")) { - return new NeoForgeLibrary(libNode.MavenName) { + if (artifactNode.Url.StartsWith("https://maven.neoforged.net/")) + { + return new NeoForgeLibrary(libNode.MavenName) + { MinecraftFolderPath = minecraftFolderPath, Sha1 = artifactNode.Sha1, Size = (long)artifactNode.Size, @@ -283,42 +322,48 @@ public static MinecraftLibrary ParseJsonNode(LibraryEntry libNode, string minecr }; } - #endregion + #endregion NeoForge Pattern } #region Other Patterns - if (libNode.MavenName.StartsWith("net.minecraft:launchwrapper")) { - return new DownloadableDependency(libNode.MavenName, $"https://libraries.minecraft.net/{GetLibraryPath(libNode.MavenName).Replace("\\", "/")}") { + if (libNode.MavenName.StartsWith("net.minecraft:launchwrapper")) + { + return new DownloadableDependency(libNode.MavenName, $"https://libraries.minecraft.net/{GetLibraryPath(libNode.MavenName).Replace("\\", "/")}") + { MinecraftFolderPath = minecraftFolderPath, IsNativeLibrary = libNode.NativeClassifierNames is not null }; } - #endregion + #endregion Other Patterns #region Legacy Forge Pattern if (libNode.MavenUrl == "https://maven.minecraftforge.net/" || libNode.ClientRequest != null - || libNode.ServerRequest != null) { + || libNode.ServerRequest != null) + { string legacyForgeLibraryUrl = (libNode.MavenUrl == "https://maven.minecraftforge.net/" ? "https://maven.minecraftforge.net/" : "https://libraries.minecraft.net/") + GetLibraryPath(libNode.MavenName).Replace("\\", "/"); - return new LegacyForgeLibrary(libNode.MavenName, legacyForgeLibraryUrl) { + return new LegacyForgeLibrary(libNode.MavenName, legacyForgeLibraryUrl) + { MinecraftFolderPath = minecraftFolderPath, IsNativeLibrary = false, ClientRequest = libNode.ClientRequest.Value || (libNode.ClientRequest == null && libNode.ServerRequest == null) }; } - #endregion + #endregion Legacy Forge Pattern #region Fabric Pattern - if (libNode.MavenUrl == "https://maven.fabricmc.net/") { - return new FabricLibrary(libNode.MavenName) { + if (libNode.MavenUrl == "https://maven.fabricmc.net/") + { + return new FabricLibrary(libNode.MavenName) + { MinecraftFolderPath = minecraftFolderPath, IsNativeLibrary = false, Size = libNode?.Size, @@ -326,44 +371,51 @@ public static MinecraftLibrary ParseJsonNode(LibraryEntry libNode, string minecr }; } - #endregion + #endregion Fabric Pattern #region Quilt Pattern if (libNode.MavenUrl == "https://maven.quiltmc.org/repository/release/" - && libNode.Sha1 == null && libNode.Size == null && libNode.DownloadInformation == null) { - return new QuiltLibrary(libNode.MavenName) { + && libNode.Sha1 == null && libNode.Size == null && libNode.DownloadInformation == null) + { + return new QuiltLibrary(libNode.MavenName) + { MinecraftFolderPath = minecraftFolderPath, IsNativeLibrary = false }; } - #endregion + #endregion Quilt Pattern #region OptiFine Pattern if (libNode.MavenName.StartsWith("optifine:optifine", StringComparison.CurrentCultureIgnoreCase) - || libNode.MavenName.StartsWith("optifine:launchwrapper-of", StringComparison.CurrentCultureIgnoreCase)) { - return new OptiFineLibrary(libNode.MavenName) { + || libNode.MavenName.StartsWith("optifine:launchwrapper-of", StringComparison.CurrentCultureIgnoreCase)) + { + return new OptiFineLibrary(libNode.MavenName) + { IsNativeLibrary = false, MinecraftFolderPath = minecraftFolderPath }; } - #endregion + #endregion OptiFine Pattern - return new UnknownLibrary(libNode.MavenName) { + return new UnknownLibrary(libNode.MavenName) + { IsNativeLibrary = false, MinecraftFolderPath = minecraftFolderPath }; } - private static DownloadArtifactEntry GetLibraryArtifactInfo(LibraryEntry libNode) { + private static DownloadArtifactEntry GetLibraryArtifactInfo(LibraryEntry libNode) + { if (libNode.DownloadInformation is null) throw new InvalidDataException("The library does not contain download information"); DownloadArtifactEntry artifact = libNode.DownloadInformation.Artifact; - if (libNode.NativeClassifierNames is not null) { + if (libNode.NativeClassifierNames is not null) + { string nativeClassifier = libNode.NativeClassifierNames[EnvironmentUtil.GetPlatformName()] .Replace("${arch}", EnvironmentUtil.Arch); artifact = libNode.DownloadInformation.Classifiers?[nativeClassifier]; @@ -372,7 +424,8 @@ private static DownloadArtifactEntry GetLibraryArtifactInfo(LibraryEntry libNode return artifact ?? throw new InvalidDataException("Invalid artifact information"); } - public override bool Equals(object obj) { + public override bool Equals(object obj) + { if (obj is MinecraftLibrary library) return library.FullPath.Equals(FullPath); @@ -385,33 +438,37 @@ public override bool Equals(object obj) { private static partial Regex GenerateMavenParseRegex(); } -public class MinecraftClient : MinecraftDependency, IDownloadDependency, IVerifiableDependency { +public class MinecraftClient : MinecraftDependency, IDownloadDependency, IVerifiableDependency +{ public override string FilePath => Path.Combine("versions", ClientId, $"{ClientId}.jar"); - public required string ClientId { get; init; } - public required string Url { get; init; } - public required long? Size { get; init; } + public string ClientId { get; init; } + public string Url { get; init; } + public long? Size { get; init; } long? IVerifiableDependency.Size => Size; - public required string Sha1 { get; init; } + public string Sha1 { get; init; } } -public sealed class MinecraftAsset : MinecraftDependency, IDownloadDependency, IVerifiableDependency { - public required string Key { get; set; } - public required long? Size { get; init; } - public required string Sha1 { get; init; } +public sealed class MinecraftAsset : MinecraftDependency, IDownloadDependency, IVerifiableDependency +{ + public string Key { get; set; } + public long? Size { get; init; } + public string Sha1 { get; init; } public string Url => $"https://resources.download.minecraft.net/{Sha1[0..2]}/{Sha1}"; public override string FilePath => Path.Combine("assets", "objects", Sha1[0..2], Sha1); long? IVerifiableDependency.Size => Size; } -public record DownloadArtifactEntry { +public record DownloadArtifactEntry +{ [JsonPropertyName("url")] public string Url { get; set; } [JsonPropertyName("size")] public long? Size { get; set; } [JsonPropertyName("path")] public string Path { get; set; } [JsonPropertyName("sha1")] public string Sha1 { get; set; } } -public record LibraryEntry { +public record LibraryEntry +{ [JsonPropertyName("size")] public long? Size { get; set; } [JsonPropertyName("sha1")] public string Sha1 { get; set; } [JsonPropertyName("url")] public string MavenUrl { get; set; } @@ -426,50 +483,57 @@ public record LibraryEntry { public string MavenName { get; set; } } -public record DownloadInformationEntry { +public record DownloadInformationEntry +{ [JsonPropertyName("artifact")] public DownloadArtifactEntry Artifact { get; set; } [JsonPropertyName("classifiers")] public Dictionary Classifiers { get; set; } } -public record RuleEntry { +public record RuleEntry +{ [JsonPropertyName("os")] public Os System { get; set; } [JsonPropertyName("action")] public string Action { get; set; } } -public record Os { +public record Os +{ [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("arch")] public string Arch { get; set; } [JsonPropertyName("version")] public string Version { get; set; } } -public class ForgeLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency { +public class ForgeLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency +{ long? IVerifiableDependency.Size => Size; - public required long? Size { get; init; } - public required string Url { get; init; } - public required string Sha1 { get; init; } + public long? Size { get; init; } + public string Url { get; init; } + public string Sha1 { get; init; } } -public sealed class VanillaLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency { +public sealed class VanillaLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency +{ long? IVerifiableDependency.Size => Size; - public required long? Size { get; init; } - public required string Sha1 { get; init; } + public long? Size { get; init; } + public string Sha1 { get; init; } public string Url => $"https://libraries.minecraft.net/{GetLibraryPath().Replace("\\", "/")}"; } public sealed class NeoForgeLibrary(string mavenName) : ForgeLibrary(mavenName); -public sealed class LegacyForgeLibrary(string mavenName, string url) : MinecraftLibrary(mavenName), IDownloadDependency { +public sealed class LegacyForgeLibrary(string mavenName, string url) : MinecraftLibrary(mavenName), IDownloadDependency +{ long? IDownloadDependency.Size => throw new NotSupportedException(); public string Url { get; init; } = url; - public required bool ClientRequest { get; init; } + public bool ClientRequest { get; init; } } public sealed class OptiFineLibrary(string mavenName) : MinecraftLibrary(mavenName); -public sealed class FabricLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency { +public sealed class FabricLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency +{ long? IVerifiableDependency.Size => Size; public long? Size { get; set; } @@ -477,7 +541,8 @@ public sealed class FabricLibrary(string mavenName) : MinecraftLibrary(mavenName public string Url => $"https://maven.fabricmc.net/{GetLibraryPath().Replace("\\", "/")}"; } -public class QuiltLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency { +public class QuiltLibrary(string mavenName) : MinecraftLibrary(mavenName), IDownloadDependency, IVerifiableDependency +{ long? IVerifiableDependency.Size => Size; public long? Size { get; set; } @@ -485,7 +550,8 @@ public class QuiltLibrary(string mavenName) : MinecraftLibrary(mavenName), IDown public string Url => $"https://maven.quiltmc.org/repository/release/{GetLibraryPath().Replace("\\", "/")}"; } -public class DownloadableDependency(string mavenName, string url) : MinecraftLibrary(mavenName), IDownloadDependency { +public class DownloadableDependency(string mavenName, string url) : MinecraftLibrary(mavenName), IDownloadDependency +{ long? IDownloadDependency.Size => throw new NotSupportedException(); public string Url { get; init; } = url; @@ -493,7 +559,8 @@ public class DownloadableDependency(string mavenName, string url) : MinecraftLib public sealed class UnknownLibrary(string mavenName) : MinecraftLibrary(mavenName); -public record AssetJsonEntry { +public record AssetJsonEntry +{ [JsonPropertyName("size")] public int Size { get; set; } [JsonPropertyName("hash")] public string Hash { get; set; } } diff --git a/MinecraftLaunch.Base/Models/Game/MinecraftJsonEntry.cs b/MinecraftLaunch.Base/Models/Game/MinecraftJsonEntry.cs index 571cb6d..ddf81dc 100644 --- a/MinecraftLaunch.Base/Models/Game/MinecraftJsonEntry.cs +++ b/MinecraftLaunch.Base/Models/Game/MinecraftJsonEntry.cs @@ -4,7 +4,8 @@ namespace MinecraftLaunch.Base.Models.Game; -public class AssstIndex : MinecraftDependency, IDownloadDependency, IVerifiableDependency { +public class AssstIndex : MinecraftDependency, IDownloadDependency, IVerifiableDependency +{ long? IVerifiableDependency.Size => Size; [JsonPropertyName("id")] public string Id { get; set; } @@ -15,7 +16,8 @@ public class AssstIndex : MinecraftDependency, IDownloadDependency, IVerifiableD [JsonIgnore] public override string FilePath => Path.Combine("assets", "indexes", $"{Id}.json"); } -public record MinecraftJsonEntry { +public record MinecraftJsonEntry +{ [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("type")] public string Type { get; set; } [JsonPropertyName("assets")] public string Assets { get; set; } @@ -29,7 +31,8 @@ public record MinecraftJsonEntry { [JsonPropertyName("minecraftArguments")] public string MinecraftArguments { get; set; } } -public record AssstIndexJsonEntry { +public record AssstIndexJsonEntry +{ [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("size")] public int Size { get; set; } [JsonPropertyName("url")] public string Url { get; set; } @@ -37,7 +40,8 @@ public record AssstIndexJsonEntry { [JsonPropertyName("totalSize")] public int TotalSize { get; set; } } -public record OptifineMinecraftEntry { +public record OptifineMinecraftEntry +{ [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("time")] public string Time { get; set; } [JsonPropertyName("type")] public string Type { get; set; } @@ -48,7 +52,8 @@ public record OptifineMinecraftEntry { [JsonPropertyName("minecraftArguments")] public string MinecraftArguments { get; set; } } -public record struct OptifineMinecraftLibrary { +public record struct OptifineMinecraftLibrary +{ [JsonPropertyName("name")] public string Name { get; set; } } diff --git a/MinecraftLaunch.Base/Models/Game/MinecraftLogEntry.cs b/MinecraftLaunch.Base/Models/Game/MinecraftLogEntry.cs index 256988c..dc76b6c 100644 --- a/MinecraftLaunch.Base/Models/Game/MinecraftLogEntry.cs +++ b/MinecraftLaunch.Base/Models/Game/MinecraftLogEntry.cs @@ -2,14 +2,16 @@ namespace MinecraftLaunch.Base.Models.Game; -public readonly record struct MinecraftLogEntry { +public readonly record struct MinecraftLogEntry +{ public string Log { get; init; } public string Time { get; init; } public string Source { get; init; } public string SourceText { get; init; } public MinecraftLogLevel LogLevel { get; init; } - public override string ToString() { + public override string ToString() + { return SourceText; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Game/MinecraftVersion.cs b/MinecraftLaunch.Base/Models/Game/MinecraftVersion.cs index 49a0834..e6e9442 100644 --- a/MinecraftLaunch.Base/Models/Game/MinecraftVersion.cs +++ b/MinecraftLaunch.Base/Models/Game/MinecraftVersion.cs @@ -3,7 +3,8 @@ namespace MinecraftLaunch.Base.Models.Game; -public partial record struct MinecraftVersion(string VersionId, MinecraftVersionType Type) { +public partial record struct MinecraftVersion(string VersionId, MinecraftVersionType Type) +{ [GeneratedRegex(@"^\d+\.\d+(\.\d+)?$")] private static partial Regex ReleaseRegex(); @@ -13,7 +14,8 @@ public partial record struct MinecraftVersion(string VersionId, MinecraftVersion [GeneratedRegex(@"^\d+\.\d+(\.\d+)?-pre\d+$")] private static partial Regex PreReleaseRegex(); - public static MinecraftVersion Parse(string id) { + public static MinecraftVersion Parse(string id) + { if (ReleaseRegex().IsMatch(id)) return new MinecraftVersion(id, MinecraftVersionType.Release); else if (PreReleaseRegex().IsMatch(id)) diff --git a/MinecraftLaunch.Base/Models/Game/SaveEntry.cs b/MinecraftLaunch.Base/Models/Game/SaveEntry.cs index 96c74eb..41c6688 100644 --- a/MinecraftLaunch.Base/Models/Game/SaveEntry.cs +++ b/MinecraftLaunch.Base/Models/Game/SaveEntry.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace MinecraftLaunch.Base.Models.Game; -namespace MinecraftLaunch.Base.Models.Game; - -public record SaveEntry { +public record SaveEntry +{ public long Seed { get; set; } public int GameType { get; set; } public string Folder { get; set; } diff --git a/MinecraftLaunch.Base/Models/JsonConverter/DateTimeJsonConverter.cs b/MinecraftLaunch.Base/Models/JsonConverter/DateTimeJsonConverter.cs index 1398bba..8195d68 100644 --- a/MinecraftLaunch.Base/Models/JsonConverter/DateTimeJsonConverter.cs +++ b/MinecraftLaunch.Base/Models/JsonConverter/DateTimeJsonConverter.cs @@ -3,9 +3,12 @@ namespace MinecraftLaunch.Base.Models.JsonConverter; -public sealed class DateTimeJsonConverter : JsonConverter { - public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (typeToConvert != typeof(DateTime)) { +public sealed class DateTimeJsonConverter : JsonConverter +{ + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert != typeof(DateTime)) + { throw new ArgumentException( $"{nameof(DateTimeJsonConverter)} cannot deserialize " + $"an object of {typeToConvert.Name}", nameof(typeToConvert)); @@ -14,7 +17,8 @@ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, Jso return DateTime.Parse(reader.GetString() ?? string.Empty); } - public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { writer.WriteStringValue(value); } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Logging/LogAnalyzerResult.cs b/MinecraftLaunch.Base/Models/Logging/LogAnalyzerResult.cs index 72c8f7e..da32e68 100644 --- a/MinecraftLaunch.Base/Models/Logging/LogAnalyzerResult.cs +++ b/MinecraftLaunch.Base/Models/Logging/LogAnalyzerResult.cs @@ -3,7 +3,8 @@ namespace MinecraftLaunch.Base.Models.Logging; -public record LogAnalyzerResult { +public record LogAnalyzerResult +{ public MinecraftEntry Minecraft { get; init; } public IReadOnlyCollection SuspiciousMods { get; init; } public IReadOnlyCollection CrashReasons { get; init; } diff --git a/MinecraftLaunch.Base/Models/Network/CurseforgeCategoryEntry.cs b/MinecraftLaunch.Base/Models/Network/CurseforgeCategoryEntry.cs new file mode 100644 index 0000000..2b35ab7 --- /dev/null +++ b/MinecraftLaunch.Base/Models/Network/CurseforgeCategoryEntry.cs @@ -0,0 +1,10 @@ +using MinecraftLaunch.Base.Enums; + +namespace MinecraftLaunch.Base.Models.Network; + +public record CurseforgeCategoryEntry +{ + public int Id { get; init; } + public string Name { get; init; } + public ClassId ClassId { get; init; } +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/CurseforgeModpackInstallEntry.cs b/MinecraftLaunch.Base/Models/Network/CurseforgeModpackInstallEntry.cs index a3d4f93..04d48ad 100644 --- a/MinecraftLaunch.Base/Models/Network/CurseforgeModpackInstallEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/CurseforgeModpackInstallEntry.cs @@ -1,10 +1,10 @@ -using System.Text; -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace MinecraftLaunch.Base.Models.Network; -public record CurseforgeModpackInstallEntry { +public record CurseforgeModpackInstallEntry +{ [JsonPropertyName("name")] public string Id { get; set; } [JsonPropertyName("author")] public string Author { get; set; } [JsonPropertyName("version")] public string Version { get; set; } @@ -17,12 +17,14 @@ public record CurseforgeModpackInstallEntry { [JsonIgnore] public string PrimaryModLoader => Minecraft.ModLoaders?.FirstOrDefault()?["id"]?.GetValue().Split("-")?.First(); } -public record CurseforgeModpackMinecraftEntry { +public record CurseforgeModpackMinecraftEntry +{ [JsonPropertyName("version")] public string McVersion { get; set; } [JsonPropertyName("modLoaders")] public IEnumerable ModLoaders { get; set; } } -public record CurseforgeModpackFileEntry { +public record CurseforgeModpackFileEntry +{ [JsonPropertyName("fileID")] public long FileId { get; set; } [JsonPropertyName("projectID")] public long ProjectId { get; set; } [JsonPropertyName("required")] public bool IsRequired { get; set; } diff --git a/MinecraftLaunch.Base/Models/Network/CurseforgeResource.cs b/MinecraftLaunch.Base/Models/Network/CurseforgeResource.cs index 3d5e265..f85531a 100644 --- a/MinecraftLaunch.Base/Models/Network/CurseforgeResource.cs +++ b/MinecraftLaunch.Base/Models/Network/CurseforgeResource.cs @@ -1,12 +1,24 @@ -using MinecraftLaunch.Base.Interfaces; +using MinecraftLaunch.Base.Enums; +using MinecraftLaunch.Base.Interfaces; namespace MinecraftLaunch.Base.Models.Network; -public record CurseforgeResource : IResource { +public record CurseForgeSearchResult : ISearchResult +{ + public int Index { get; init; } + public int PageSize { get; init; } + public long TotalCount { get; init; } + + public IEnumerable Resources { get; init; } +} + +public record CurseforgeResource : IResource +{ public required int Id { get; init; } public required int ClassId { get; init; } public required int DownloadCount { get; init; } public required string Name { get; init; } + public required string Slug { get; init; } public required string IconUrl { get; init; } public required string Summary { get; init; } public required string WebsiteUrl { get; init; } @@ -16,19 +28,44 @@ public record CurseforgeResource : IResource { public required IEnumerable Categories { get; init; } public required IEnumerable Screenshots { get; init; } public IEnumerable LatestFiles { get; init; } + public IEnumerable Loaders { get; init; } + public ResourceType ResourceType => (ClassId)ClassId switch + { + Enums.ClassId.Mods => ResourceType.Mod, + Enums.ClassId.Modpacks => ResourceType.Modpack, + Enums.ClassId.ResourcePacks => ResourceType.Resourcepack, + Enums.ClassId.Shaders => ResourceType.Shaderpack, + Enums.ClassId.DataPacks => ResourceType.Datapack, + Enums.ClassId.Worlds => ResourceType.World, + _ => ResourceType.Mod + }; } -public record CurseforgeResourceFile { +public record CurseforgeResourceFile : IResourceFile +{ public required int Id { get; init; } public required int ModId { get; init; } - public required int ReleaseType { get; init; } + public required int GameId { get; init; } + public required int AlternateFileId { get; init; } + public required uint FileFingerprint { get; init; } + + public required bool IsApproved { get; init; } public required bool IsAvailable { get; init; } + public required bool IsServerPack { get; init; } + + public string Sha1 { get; init; } public required string FileName { get; init; } public required string DisplayName { get; init; } public required string DownloadUrl { get; init; } + + public required long FileSize { get; init; } + public required long DownloadCount { get; init; } + public required DateTime Published { get; init; } - public required IEnumerable MinecraftVersions { get; init; } + public required FileReleaseType ReleaseType { get; init; } - public bool IsReleased => ReleaseType is 1; + public required IEnumerable GameVersions { get; init; } + public required IDictionary Dependencies { get; init; } + public IEnumerable Loaders { get; init; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/CurseforgeSearchOptions.cs b/MinecraftLaunch.Base/Models/Network/CurseforgeSearchOptions.cs index 7ba6fbd..2171ab3 100644 --- a/MinecraftLaunch.Base/Models/Network/CurseforgeSearchOptions.cs +++ b/MinecraftLaunch.Base/Models/Network/CurseforgeSearchOptions.cs @@ -2,9 +2,12 @@ namespace MinecraftLaunch.Base.Models.Network; -public record CurseforgeSearchOptions { - public int ClassId { get; set; } = 6; // 默认为模组类别 - public int CategoryId { get; set; } = 0; +public record CurseforgeSearchOptions +{ + public ClassId ClassId { get; set; } = ClassId.Mods; + public int? CategoryId { get; set; } = null; + public int PageSize { get; set; } = 50; + public int Index { get; set; } = 0; public string GameVersion { get; set; } public required string SearchFilter { get; set; } @@ -14,7 +17,8 @@ public record CurseforgeSearchOptions { public ModLoaderType ModLoaderType { get; set; } } -public enum SortOrder { +public enum SortOrder +{ /// /// 升序排序 /// @@ -26,7 +30,8 @@ public enum SortOrder { Desc } -public enum SortField { +public enum SortField +{ Featured = 1, Popularity, LastUpdated, diff --git a/MinecraftLaunch.Base/Models/Network/DownloadRequest.cs b/MinecraftLaunch.Base/Models/Network/DownloadRequest.cs index e113245..85177cf 100644 --- a/MinecraftLaunch.Base/Models/Network/DownloadRequest.cs +++ b/MinecraftLaunch.Base/Models/Network/DownloadRequest.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Models.Network; -public record DownloadRequest { +public record DownloadRequest +{ public string Url { get; set; } public FileInfo FileInfo { get; set; } public long Size { get; set; } = -1; @@ -11,14 +12,16 @@ public record DownloadRequest { public Action ProgressChanged { get; set; } public DownloadRequest() { } - public DownloadRequest(string url, string localPath, long size = -1) { + public DownloadRequest(string url, string localPath, long size = -1) + { Url = url; Size = size; FileInfo = new(localPath); } } -public record GroupDownloadRequest { +public record GroupDownloadRequest +{ public bool IsDownloaded { get; set; } = false; public DateTime StartTime { get; init; } @@ -27,7 +30,8 @@ public record GroupDownloadRequest { public Action Completed { get; set; } public Action ProgressChanged { get; set; } - public GroupDownloadRequest(IEnumerable files) { + public GroupDownloadRequest(IEnumerable files) + { Files = files; StartTime = DateTime.Now; } diff --git a/MinecraftLaunch.Base/Models/Network/DownloadResult.cs b/MinecraftLaunch.Base/Models/Network/DownloadResult.cs index 2a10544..2146190 100644 --- a/MinecraftLaunch.Base/Models/Network/DownloadResult.cs +++ b/MinecraftLaunch.Base/Models/Network/DownloadResult.cs @@ -2,16 +2,19 @@ namespace MinecraftLaunch.Base.Models.Network; -public record DownloadResult { +public record DownloadResult +{ public Exception Exception { get; init; } public DownloadResultType Type { get; init; } - public DownloadResult(DownloadResultType type) { + public DownloadResult(DownloadResultType type) + { Type = type; } } -public record GroupDownloadResult { +public record GroupDownloadResult +{ public required DownloadResultType Type { get; init; } public required IEnumerable Failed { get; init; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/FabricInstallEntry.cs b/MinecraftLaunch.Base/Models/Network/FabricInstallEntry.cs index abc5107..0105a83 100644 --- a/MinecraftLaunch.Base/Models/Network/FabricInstallEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/FabricInstallEntry.cs @@ -1,11 +1,11 @@ using MinecraftLaunch.Base.Enums; using MinecraftLaunch.Base.Interfaces; -using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace MinecraftLaunch.Base.Models.Network; -public record FabricInstallEntry : IInstallEntry { +public record FabricInstallEntry : IInstallEntry +{ [JsonPropertyName("loader")] public FabricMavenItem Loader { get; set; } [JsonPropertyName("intermediary")] public FabricMavenItem Intermediary { get; set; } @@ -16,7 +16,8 @@ public record FabricInstallEntry : IInstallEntry { [JsonIgnore] public string Description => Loader.IsStable ? $"Release" : "Preview"; } -public record FabricMavenItem { +public record FabricMavenItem +{ [JsonPropertyName("stable")] public bool IsStable { get; set; } [JsonPropertyName("maven")] public string Maven { get; set; } diff --git a/MinecraftLaunch.Base/Models/Network/FileHashes.cs b/MinecraftLaunch.Base/Models/Network/FileHashes.cs new file mode 100644 index 0000000..d211ca2 --- /dev/null +++ b/MinecraftLaunch.Base/Models/Network/FileHashes.cs @@ -0,0 +1,7 @@ +namespace MinecraftLaunch.Base.Models.Network; + +public record FileHashes +{ + public required string Sha512 { get; init; } + public required string Sha1 { get; init; } +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/ForgeInstallEntry.cs b/MinecraftLaunch.Base/Models/Network/ForgeInstallEntry.cs index 2ad5e99..bbb2bf1 100644 --- a/MinecraftLaunch.Base/Models/Network/ForgeInstallEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/ForgeInstallEntry.cs @@ -4,7 +4,8 @@ namespace MinecraftLaunch.Base.Models.Network; -public record ForgeInstallEntry : IInstallEntry { +public record ForgeInstallEntry : IInstallEntry +{ [JsonIgnore] public bool IsNeoforge { get; set; } [JsonPropertyName("build")] public int Build { get; set; } [JsonPropertyName("branch")] public string Branch { get; set; } @@ -14,9 +15,10 @@ public record ForgeInstallEntry : IInstallEntry { [JsonIgnore] public string DisplayVersion => ForgeVersion; [JsonIgnore] public ModLoaderType ModLoaderType => IsNeoforge ? ModLoaderType.NeoForge : ModLoaderType.Forge; - [JsonIgnore] public string Description => IsNeoforge - ? ForgeVersion.Contains("beta", StringComparison.OrdinalIgnoreCase) - ? "Preview" + [JsonIgnore] + public string Description => IsNeoforge + ? ForgeVersion.Contains("beta", StringComparison.OrdinalIgnoreCase) + ? "Preview" : "Release" : ModifiedTime.ToString(); } diff --git a/MinecraftLaunch.Base/Models/Network/McbbsModpackInstallEntry.cs b/MinecraftLaunch.Base/Models/Network/McbbsModpackInstallEntry.cs index d9be4eb..6d2caf5 100644 --- a/MinecraftLaunch.Base/Models/Network/McbbsModpackInstallEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/McbbsModpackInstallEntry.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Models.Network; -public record McbbsModpackInstallEntry { +public record McbbsModpackInstallEntry +{ [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("author")] public string Author { get; set; } [JsonPropertyName("version")] public string Version { get; set; } @@ -13,12 +14,14 @@ public record McbbsModpackInstallEntry { [JsonIgnore] public string McVersion => Addons?.FirstOrDefault(x => x.Id.Equals("game")).Version; } -public record McbbsModpackAddons { +public record McbbsModpackAddons +{ [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("version")] public string Version { get; set; } } -public record McbbsModpackFileEntry { +public record McbbsModpackFileEntry +{ [JsonPropertyName("path")] public string Path { get; set; } [JsonPropertyName("hash")] public string Hash { get; set; } [JsonPropertyName("type")] public string Type { get; set; } diff --git a/MinecraftLaunch.Base/Models/Network/ModrinthCategoryEntry.cs b/MinecraftLaunch.Base/Models/Network/ModrinthCategoryEntry.cs new file mode 100644 index 0000000..eea8e6e --- /dev/null +++ b/MinecraftLaunch.Base/Models/Network/ModrinthCategoryEntry.cs @@ -0,0 +1,7 @@ +namespace MinecraftLaunch.Base.Models.Network; + +public record ModrinthCategoryEntry +{ + public string Name { get; init; } + public string ProjectType { get; init; } +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/ModrinthFileDependency.cs b/MinecraftLaunch.Base/Models/Network/ModrinthFileDependency.cs new file mode 100644 index 0000000..9df3346 --- /dev/null +++ b/MinecraftLaunch.Base/Models/Network/ModrinthFileDependency.cs @@ -0,0 +1,12 @@ +using MinecraftLaunch.Base.Enums; + +namespace MinecraftLaunch.Base.Models.Network; + +public record ModrinthFileDependency +{ + public string FileName { get; init; } + public string VersionId { get; init; } + public string ProjectId { get; init; } + + public required DependencyType Type { get; init; } +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/ModrinthModpackInstallEntry.cs b/MinecraftLaunch.Base/Models/Network/ModrinthModpackInstallEntry.cs index 60d364e..e1fcb3e 100644 --- a/MinecraftLaunch.Base/Models/Network/ModrinthModpackInstallEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/ModrinthModpackInstallEntry.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Base.Models.Network; -public record ModrinthModpackInstallEntry { +public record ModrinthModpackInstallEntry +{ [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("summary")] public string Summary { get; set; } [JsonPropertyName("versionId")] public string VersionId { get; set; } @@ -13,7 +14,8 @@ public record ModrinthModpackInstallEntry { [JsonIgnore] public string McVersion => Dependencies["minecraft"]; } -public record ModrinthModpackFileEntry { +public record ModrinthModpackFileEntry +{ [JsonPropertyName("path")] public string Path { get; set; } [JsonPropertyName("fileSize")] public long Size { get; set; } [JsonPropertyName("downloads")] public IEnumerable Downloads { get; set; } diff --git a/MinecraftLaunch.Base/Models/Network/ModrinthResource.cs b/MinecraftLaunch.Base/Models/Network/ModrinthResource.cs index c79dd9d..6a9a6fe 100644 --- a/MinecraftLaunch.Base/Models/Network/ModrinthResource.cs +++ b/MinecraftLaunch.Base/Models/Network/ModrinthResource.cs @@ -1,14 +1,25 @@ -using MinecraftLaunch.Base.Interfaces; +using MinecraftLaunch.Base.Enums; +using MinecraftLaunch.Base.Interfaces; namespace MinecraftLaunch.Base.Models.Network; -public record ModrinthResource : IResource { - public string Id { get; init; } +public record ModrinthSearchResult : ISearchResult +{ + public int Index { get; init; } + public int PageSize { get; init; } + public long TotalCount { get; init; } + + public IEnumerable Resources { get; init; } +} + +public record ModrinthResource : IResource +{ public string Slug { get; init; } public string Name { get; init; } public string Author { get; set; } public string Summary { get; init; } public string IconUrl { get; init; } + public string ProjectId { get; init; } public string ProjectType { get; init; } public int DownloadCount { get; init; } @@ -18,30 +29,43 @@ public record ModrinthResource : IResource { public IEnumerable Categories { get; init; } public IEnumerable Screenshots { get; init; } public IEnumerable MinecraftVersions { get; init; } - - public string WebLink => $"https://modrinth.com/{ProjectType}/{Slug}"; + public IEnumerable Loaders { get; init; } + public string WebsiteUrl { get; init; } + public ResourceType ResourceType => ProjectType switch + { + "mod" => ResourceType.Mod, + "modpack" => ResourceType.Modpack, + "resourcepack" => ResourceType.Resourcepack, + "shader" => ResourceType.Shaderpack, + _ => ResourceType.Mod + }; } -public record ModrinthResourceFiles { - public string Id { get; set; } - public string ChangeLog { get; set; } - public string SourceHash { get; set; } +public record ModrinthResourceFile : IResourceFile +{ + public string ChangeLog { get; init; } + public string DisplayName { get; init; } + public string VersionNumber { get; init; } - public bool IsFeatured { get; set; } + public FileReleaseType ReleaseType { get; init; } - public int DownloadCount { get; set; } + public required string Sha1 { get; init; } + public required string Sha512 { get; init; } + public required string FileName { get; init; } + public required string DownloadUrl { get; init; } - public DateTime Published { get; set; } - public IEnumerable Files { get; set; } -} + public required string AuthorId { get; init; } + public required string ProjectId { get; init; } + public required string VersionId { get; init; } + + public required DateTime Published { get; init; } -public record ModrinthResourceFile { - public string Sha1 { get; set; } - public string Sha512 { get; set; } - public string FileName { get; set; } - public string DownloadUrl { get; set; } + public required bool IsPrimary { get; init; } - public bool IsPrimary { get; set; } + public required long FileSize { get; init; } + public required long DownloadCount { get; init; } - public long FileSize { get; set; } + public IEnumerable GameVersions { get; init; } + public IEnumerable Loaders { get; init; } + public IEnumerable Dependencies { get; init; } } \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/ModrinthSearchOptions.cs b/MinecraftLaunch.Base/Models/Network/ModrinthSearchOptions.cs new file mode 100644 index 0000000..bf7490b --- /dev/null +++ b/MinecraftLaunch.Base/Models/Network/ModrinthSearchOptions.cs @@ -0,0 +1,15 @@ +using MinecraftLaunch.Base.Enums; + +namespace MinecraftLaunch.Base.Models.Network; + +public record ModrinthSearchOptions +{ + public string SearchFilter { get; set; } + public string Version { get; set; } = ""; + public string Category { get; set; } = ""; + public string ProjectType { get; set; } = "mod"; + public ModLoaderType ModLoader { get; set; } = ModLoaderType.Any; + public ModrinthSearchIndex Index { get; set; } = ModrinthSearchIndex.Relevance; + public int Limit { get; set; } = 10; + public int Offset { get; set; } = 0; +} \ No newline at end of file diff --git a/MinecraftLaunch.Base/Models/Network/OptiFineInstallEntry.cs b/MinecraftLaunch.Base/Models/Network/OptiFineInstallEntry.cs index 4703e9f..537cdbe 100644 --- a/MinecraftLaunch.Base/Models/Network/OptiFineInstallEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/OptiFineInstallEntry.cs @@ -4,7 +4,8 @@ namespace MinecraftLaunch.Base.Models.Network; -public record OptifineInstallEntry : IInstallEntry { +public record OptifineInstallEntry : IInstallEntry +{ [JsonPropertyName("type")] public string Type { get; set; } [JsonPropertyName("patch")] public string Patch { get; set; } [JsonPropertyName("filename")] public string FileName { get; set; } diff --git a/MinecraftLaunch.Base/Models/Network/QuiltInstallEntry.cs b/MinecraftLaunch.Base/Models/Network/QuiltInstallEntry.cs index adc97f9..ce38609 100644 --- a/MinecraftLaunch.Base/Models/Network/QuiltInstallEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/QuiltInstallEntry.cs @@ -4,7 +4,8 @@ namespace MinecraftLaunch.Base.Models.Network; -public record QuiltInstallEntry : IInstallEntry { +public record QuiltInstallEntry : IInstallEntry +{ [JsonPropertyName("loader")] public required FabricMavenItem Loader { get; set; } [JsonPropertyName("intermediary")] public required FabricMavenItem Intermediary { get; set; } diff --git a/MinecraftLaunch.Base/Models/Network/VersionManifestEntry.cs b/MinecraftLaunch.Base/Models/Network/VersionManifestEntry.cs index 6aa3666..9974593 100644 --- a/MinecraftLaunch.Base/Models/Network/VersionManifestEntry.cs +++ b/MinecraftLaunch.Base/Models/Network/VersionManifestEntry.cs @@ -4,7 +4,8 @@ namespace MinecraftLaunch.Base.Models.Network; -public record VersionManifestEntry : IInstallEntry { +public record VersionManifestEntry : IInstallEntry +{ [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("url")] public string Url { get; set; } [JsonPropertyName("type")] public string Type { get; set; } diff --git a/MinecraftLaunch.Base/Utilities/EnvironmentUtil.cs b/MinecraftLaunch.Base/Utilities/EnvironmentUtil.cs index e8ccf1a..2343a3d 100644 --- a/MinecraftLaunch.Base/Utilities/EnvironmentUtil.cs +++ b/MinecraftLaunch.Base/Utilities/EnvironmentUtil.cs @@ -3,7 +3,8 @@ namespace MinecraftLaunch.Base.Utilities; -public static class EnvironmentUtil { +public static class EnvironmentUtil +{ private const ushort PE_SIGNATURE = 23117; private const ushort IMAGE_FILE_MACHINE_IA64 = 267; private const ushort IMAGE_FILE_MACHINE_AMD64 = 523; @@ -21,12 +22,18 @@ public static bool IsLinux public static bool IsWindow => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - public static string GetPlatformName() { - if (IsMac) { + public static string GetPlatformName() + { + if (IsMac) + { return "osx"; - } else if (IsLinux) { + } + else if (IsLinux) + { return "linux"; - } else if (IsWindow) { + } + else if (IsWindow) + { return "windows"; } @@ -34,23 +41,28 @@ public static string GetPlatformName() { } [SupportedOSPlatform("Windows")] - public static bool Is64BitJavaForWindow(string path) { + public static bool Is64BitJavaForWindow(string path) + { ushort architecture = 0; - try { + try + { using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); using var binaryReader = new BinaryReader(fileStream); - if (binaryReader.ReadUInt16() == PE_SIGNATURE) { + if (binaryReader.ReadUInt16() == PE_SIGNATURE) + { fileStream.Seek(0x3A, SeekOrigin.Current); fileStream.Seek(binaryReader.ReadUInt32(), SeekOrigin.Begin); - if (binaryReader.ReadUInt32() == PE_OPTIONAL_HEADER_SIGNATURE) { + if (binaryReader.ReadUInt32() == PE_OPTIONAL_HEADER_SIGNATURE) + { fileStream.Seek(20, SeekOrigin.Current); architecture = binaryReader.ReadUInt16(); } } - } catch (Exception) { } + } + catch (Exception) { } return architecture is IMAGE_FILE_MACHINE_AMD64 or IMAGE_FILE_MACHINE_IA64; } diff --git a/MinecraftLaunch/Components/Authenticator/MicrosoftAuthenticator.cs b/MinecraftLaunch/Components/Authenticator/MicrosoftAuthenticator.cs index c1cdc7d..695e5bb 100644 --- a/MinecraftLaunch/Components/Authenticator/MicrosoftAuthenticator.cs +++ b/MinecraftLaunch/Components/Authenticator/MicrosoftAuthenticator.cs @@ -7,24 +7,27 @@ using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; -using System.Web; namespace MinecraftLaunch.Components.Authenticator; -public sealed class MicrosoftAuthenticator { +public sealed class MicrosoftAuthenticator +{ private readonly string _clientId; private readonly IEnumerable _scopes = ["XboxLive.signin", "offline_access", "openid", "profile", "email"]; /// /// Authenticator for Microsoft accounts. /// - public MicrosoftAuthenticator(string clientId) { + public MicrosoftAuthenticator(string clientId) + { _clientId = clientId; } - public async Task RefreshAsync(MicrosoftAccount account, CancellationToken cancellationToken = default) { + public async Task RefreshAsync(MicrosoftAccount account, CancellationToken cancellationToken = default) + { var request = HttpUtil.Request("https://login.live.com/oauth20_token.srf"); - Dictionary payload = new() { + Dictionary payload = new() + { ["client_id"] = _clientId, ["refresh_token"] = account.RefreshToken, ["grant_type"] = "refresh_token" @@ -43,8 +46,10 @@ public async Task RefreshAsync(MicrosoftAccount account, Cance /// Asynchronously authenticates the Microsoft account. /// /// A ValueTask that represents the asynchronous operation. The task result contains the authenticated Microsoft account. - public async Task AuthenticateAsync(OAuth2TokenResponse oAuth2Token, CancellationToken cancellationToken = default) { - try { + public async Task AuthenticateAsync(OAuth2TokenResponse oAuth2Token, CancellationToken cancellationToken = default) + { + try + { if (oAuth2Token is null) ArgumentException.ThrowIfNullOrEmpty(nameof(oAuth2Token)); @@ -54,7 +59,9 @@ public async Task AuthenticateAsync(OAuth2TokenResponse oAuth2 var profile = await GetMinecraftProfileAsync(minecraftAccessToken.GetString("access_token"), oAuth2Token.RefreshToken, cancellationToken); return profile; - } catch (Exception) { + } + catch (Exception) + { throw; } } @@ -65,11 +72,13 @@ public async Task AuthenticateAsync(OAuth2TokenResponse oAuth2 /// The action to be performed with the device code response. /// The cancellation token source to be used to cancel the operation. /// A Task that represents the asynchronous operation. The task result contains the OAuth2 token response. - public async Task DeviceFlowAuthAsync(Action deviceCode, CancellationToken cancellationToken = default) { + public async Task DeviceFlowAuthAsync(Action deviceCode, CancellationToken cancellationToken = default) + { if (string.IsNullOrEmpty(_clientId)) ArgumentException.ThrowIfNullOrEmpty("ClientId"); - var parameters = new Dictionary { + var parameters = new Dictionary + { ["client_id"] = _clientId, ["tenant"] = "/consumers", ["scope"] = string.Join(" ", _scopes) @@ -92,12 +101,14 @@ public async Task DeviceFlowAuthAsync(Action x.ExceptionHandled = true) - .PostUrlEncodedAsync(new Dictionary { + .PostUrlEncodedAsync(new Dictionary + { ["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code", ["client_id"] = _clientId, ["device_code"] = codeResponse.DeviceCode @@ -106,8 +117,10 @@ public async Task DeviceFlowAuthAsync(Action DeviceFlowAuthAsync(Action /// Get Xbox live token & userhash /// - private static async Task GetXBLTokenAsync(string token, CancellationToken cancellationToken = default) { + private static async Task GetXBLTokenAsync(string token, CancellationToken cancellationToken = default) + { var request = HttpUtil.Request("https://user.auth.xboxlive.com/user/authenticate"); - var xblContent = new XBLTokenPayload { - Properties = new XBLProperties { + var xblContent = new XBLTokenPayload + { + Properties = new XBLProperties + { AuthMethod = "RPS", SiteName = "user.auth.xboxlive.com", RpsTicket = $"d={token}" @@ -151,10 +167,13 @@ private static async Task GetXBLTokenAsync(string token, CancellationT /// /// /// - private static async Task GetXSTSTokenAsync(JsonNode xblTokenNode, CancellationToken cancellationToken = default) { + private static async Task GetXSTSTokenAsync(JsonNode xblTokenNode, CancellationToken cancellationToken = default) + { var request = HttpUtil.Request("https://xsts.auth.xboxlive.com/xsts/authorize"); - var xstsContent = new XSTSTokenPayload { - Properties = new XSTSProperties { + var xstsContent = new XSTSTokenPayload + { + Properties = new XSTSProperties + { SandboxId = "RETAIL", UserTokens = [xblTokenNode.GetString("Token")] }, @@ -171,7 +190,8 @@ private static async Task GetXSTSTokenAsync(JsonNode xblTokenNode, Can /// /// Get Minecraft access token /// - private static async Task GetMinecraftAccessTokenAsync((JsonNode xblTokenNode, JsonNode xstsTokenNode) nodes, CancellationToken cancellationToken = default) { + private static async Task GetMinecraftAccessTokenAsync((JsonNode xblTokenNode, JsonNode xstsTokenNode) nodes, CancellationToken cancellationToken = default) + { var request = HttpUtil.Request("https://api.minecraftservices.com/authentication/login_with_xbox"); var xstsToken = nodes.xstsTokenNode.GetString("Token"); var uhsToken = nodes.xblTokenNode.Select("DisplayClaims") @@ -192,7 +212,8 @@ private static async Task GetMinecraftAccessTokenAsync((JsonNode xblTo /// Minecraft access token /// Minecraft refresh token /// If authenticated user don't have minecraft, the exception will be thrown - private static async Task GetMinecraftProfileAsync(string accessToken, string refreshToken, CancellationToken cancellationToken = default) { + private static async Task GetMinecraftProfileAsync(string accessToken, string refreshToken, CancellationToken cancellationToken = default) + { var request = HttpUtil.Request("https://api.minecraftservices.com/minecraft/profile") .WithHeader("Authorization", $"Bearer {accessToken}"); @@ -205,5 +226,5 @@ private static async Task GetMinecraftProfileAsync(string acce : new MicrosoftAccount(profileNode.GetString("name"), Guid.Parse(profileNode.GetString("id")), accessToken, refreshToken, DateTime.Now); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Authenticator/OfflineAuthenticator.cs b/MinecraftLaunch/Components/Authenticator/OfflineAuthenticator.cs index a17e3b9..c8d3107 100644 --- a/MinecraftLaunch/Components/Authenticator/OfflineAuthenticator.cs +++ b/MinecraftLaunch/Components/Authenticator/OfflineAuthenticator.cs @@ -8,8 +8,10 @@ namespace MinecraftLaunch.Components.Authenticator; -public sealed class OfflineAuthenticator { - public OfflineAccount Authenticate(string name, Guid guid = default) { +public sealed class OfflineAuthenticator +{ + public OfflineAccount Authenticate(string name, Guid guid = default) + { var uuid = guid; if (uuid == default) TryParseUuidFromName(name, out uuid); @@ -17,7 +19,8 @@ public OfflineAccount Authenticate(string name, Guid guid = default) { return new OfflineAccount(name, uuid, Guid.NewGuid().ToString("N")); } - private static void TryParseUuidFromName(string name, out Guid uuid) { + private static void TryParseUuidFromName(string name, out Guid uuid) + { byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes("OfflinePlayer:" + name)); hash[6] = (byte)((hash[6] & 0x0f) | 0x30); @@ -28,22 +31,27 @@ private static void TryParseUuidFromName(string name, out Guid uuid) { } [StructLayout(LayoutKind.Sequential)] -public unsafe struct Uuid { +public unsafe struct Uuid +{ private const ushort MaximalChar = 103; private static readonly uint* TableToHex; private static readonly byte* TableFromHexToBytes; - static Uuid() { + static Uuid() + { TableToHex = (uint*)Marshal.AllocHGlobal(sizeof(uint) * 256).ToPointer(); - for (int i = 0; i < 256; i++) { + for (int i = 0; i < 256; i++) + { string chars = Convert.ToString(i, 16).PadLeft(2, '0'); TableToHex[i] = ((uint)chars[1] << 16) | chars[0]; } TableFromHexToBytes = (byte*)Marshal.AllocHGlobal(103).ToPointer(); - for (int i = 0; i < 103; i++) { - TableFromHexToBytes[i] = (char)i switch { + for (int i = 0; i < 103; i++) + { + TableFromHexToBytes[i] = (char)i switch + { '0' => 0x0, '1' => 0x1, '2' => 0x2, @@ -94,9 +102,11 @@ static Uuid() { /// A 16-element byte array containing values with which to initialize the . /// is . /// is not 16 bytes long. - public Uuid(byte[] bytes) { + public Uuid(byte[] bytes) + { ArgumentNullException.ThrowIfNull(bytes); - if (bytes.Length != 16) { + if (bytes.Length != 16) + { throw new ArgumentException("Byte array for Uuid must be exactly 16 bytes long.", nameof(bytes)); } @@ -104,9 +114,12 @@ public Uuid(byte[] bytes) { } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void FormatN(char* dest) { - if (Avx2.IsSupported) { - fixed (Uuid* thisPtr = &this) { + private void FormatN(char* dest) + { + if (Avx2.IsSupported) + { + fixed (Uuid* thisPtr = &this) + { Vector256 uuidVector = Avx2.ConvertToVector256Int16(Sse3.LoadDquVector128((byte*)thisPtr)); Vector256 hi = Avx2.ShiftRightLogical(uuidVector, 4).AsByte(); Vector256 lo = Avx2.Shuffle(uuidVector.AsByte(), @@ -121,7 +134,9 @@ private void FormatN(char* dest) { Avx.Store((short*)dest, Avx2.ConvertToVector256Int16(asciiBytes.GetLower())); Avx.Store((short*)dest + 16, Avx2.ConvertToVector256Int16(asciiBytes.GetUpper())); } - } else { + } + else + { uint* destUints = (uint*)dest; destUints[0] = TableToHex[_byte0]; destUints[1] = TableToHex[_byte1]; @@ -146,7 +161,8 @@ private void FormatN(char* dest) { /// 仅摘取了原实现中的 FormatN 方法。 /// /// - public override string ToString() { + public override string ToString() + { string uuidString = new('\0', 32); fixed (char* uuidChars = &uuidString.GetPinnableReference()) FormatN(uuidChars); diff --git a/MinecraftLaunch/Components/Authenticator/YggdrasilAuthenticator.cs b/MinecraftLaunch/Components/Authenticator/YggdrasilAuthenticator.cs index c02fd72..ca5bd60 100644 --- a/MinecraftLaunch/Components/Authenticator/YggdrasilAuthenticator.cs +++ b/MinecraftLaunch/Components/Authenticator/YggdrasilAuthenticator.cs @@ -8,7 +8,8 @@ namespace MinecraftLaunch.Components.Authenticator; -public sealed class YggdrasilAuthenticator { +public sealed class YggdrasilAuthenticator +{ private readonly string _url; private readonly string _email; private readonly string _password; @@ -19,19 +20,23 @@ public sealed class YggdrasilAuthenticator { /// The URL for authentication. /// The email of the account. /// The password of the account. - public YggdrasilAuthenticator(string url, string email, string password) { + public YggdrasilAuthenticator(string url, string email, string password) + { _url = url; _email = email; _password = password; } - public async Task RefreshAsync(YggdrasilAccount account, CancellationToken cancellationToken = default) { + public async Task RefreshAsync(YggdrasilAccount account, CancellationToken cancellationToken = default) + { var request = HttpUtil.Request(new Url(_url), "authserver", "refresh"); - var payload = new YggdrasilRefreshPayload { + var payload = new YggdrasilRefreshPayload + { RequestUser = true, AccessToken = account.AccessToken, ClientToken = account.ClientToken, - SelectedProfile = new SelectedProfile { + SelectedProfile = new SelectedProfile + { Name = account.Name, Id = account.Uuid.ToString("N"), } @@ -52,9 +57,11 @@ public async Task RefreshAsync(YggdrasilAccount account, Cance /// Asynchronously authenticates the Yggdrasil account. /// /// A ValueTask that represents the asynchronous operation. The task result contains the authenticated Yggdrasil account. - public async Task> AuthenticateAsync(CancellationToken cancellationToken = default) { + public async Task> AuthenticateAsync(CancellationToken cancellationToken = default) + { var request = HttpUtil.Request(new Url(_url), "authserver", "authenticate"); - var payload = new YggdrasilAuthenticatePayload { + var payload = new YggdrasilAuthenticatePayload + { ClientToken = Guid.NewGuid().ToString("N"), Username = _email, Password = _password, diff --git a/MinecraftLaunch/Components/Downloader/DefaultDownloader.cs b/MinecraftLaunch/Components/Downloader/DefaultDownloader.cs index ab739ae..80f4485 100644 --- a/MinecraftLaunch/Components/Downloader/DefaultDownloader.cs +++ b/MinecraftLaunch/Components/Downloader/DefaultDownloader.cs @@ -12,7 +12,8 @@ namespace MinecraftLaunch.Components.Downloader; -public class DefaultDownloader : IDownloader { +public class DefaultDownloader : IDownloader +{ private readonly SemaphoreSlim _globalSemaphore; private const int BufferSize = 4096; @@ -21,21 +22,28 @@ public class DefaultDownloader : IDownloader { private const double megabyte = kilobyte * 1024.0; private const double gigabyte = megabyte * 1024.0; - public DefaultDownloader() { + public DefaultDownloader() + { _globalSemaphore = new SemaphoreSlim(DownloadManager.MaxThread, DownloadManager.MaxThread); } - public static string FormatSize(double bytes, bool includePerSecond = false) { + public static string FormatSize(double bytes, bool includePerSecond = false) + { string suffix; if (bytes < kilobyte) suffix = "B"; - else if (bytes < megabyte) { + else if (bytes < megabyte) + { bytes /= kilobyte; suffix = "KB"; - } else if (bytes < gigabyte) { + } + else if (bytes < gigabyte) + { bytes /= megabyte; suffix = "MB"; - } else { + } + else + { bytes /= gigabyte; suffix = "GB"; } @@ -45,33 +53,45 @@ public static string FormatSize(double bytes, bool includePerSecond = false) { return includePerSecond ? result + "/s" : result; } - public async Task DownloadAsync(DownloadRequest request, CancellationToken cancellationToken = default) { + public async Task DownloadAsync(DownloadRequest request, CancellationToken cancellationToken = default) + { await _globalSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - try { - for (int attempt = 0; attempt < DownloadManager.MaxRetryCount; attempt++) { - try { + try + { + for (int attempt = 0; attempt < DownloadManager.MaxRetryCount; attempt++) + { + try + { await DownloadFileDriverAsync(request, cancellationToken); request.Completed?.Invoke(EventArgs.Empty); return new DownloadResult(DownloadResultType.Successful); - } catch (OperationCanceledException) { + } + catch (OperationCanceledException) + { return new DownloadResult(DownloadResultType.Cancelled); - } catch (Exception ex) { + } + catch (Exception ex) + { if (attempt == DownloadManager.MaxRetryCount - 1) return new DownloadResult(DownloadResultType.Failed) { Exception = ex }; await Task.Delay(1000 * (attempt + 1), cancellationToken).ConfigureAwait(false); } } - } finally { + } + finally + { _globalSemaphore.Release(); } return new DownloadResult(DownloadResultType.Failed); } - public async Task DownloadManyAsync(GroupDownloadRequest requests, CancellationToken cancellationToken = default) { - var downloadStates = new GroupDownloadStates { + public async Task DownloadManyAsync(GroupDownloadRequest requests, CancellationToken cancellationToken = default) + { + var downloadStates = new GroupDownloadStates + { DownloadedCount = 0, TotalCount = requests.Files.Count(), TotalBytes = requests.Files.Sum(x => x.Size) @@ -88,7 +108,8 @@ public async Task DownloadManyAsync(GroupDownloadRequest re var type = DownloadResultType.Successful; - requests.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs { + requests.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs + { Speed = 0, TotalBytes = downloadStates.TotalBytes, EstimatedRemaining = TimeSpan.Zero, @@ -97,25 +118,31 @@ public async Task DownloadManyAsync(GroupDownloadRequest re CompletedCount = downloadStates.TotalCount }); - return new GroupDownloadResult { + return new GroupDownloadResult + { Failed = [], Type = type }; } - private async Task DownloadInGroupAsync(GroupDownloadStates downloadStates, DownloadRequest request, CancellationToken cancellationToken = default) { + private async Task DownloadInGroupAsync(GroupDownloadStates downloadStates, DownloadRequest request, CancellationToken cancellationToken = default) + { await _globalSemaphore.WaitAsync(cancellationToken); - try { + try + { if (!request.FileInfo.Directory.Exists) request.FileInfo.Directory.Create(); - for (int attempt = 0; attempt < DownloadManager.MaxRetryCount; attempt++) { - try { + for (int attempt = 0; attempt < DownloadManager.MaxRetryCount; attempt++) + { + try + { string url = request.Url; var (response, finalUrl) = await PrepareForDownloadAsync(url, cancellationToken).ConfigureAwait(false); - var states = new DownloadStates { + var states = new DownloadStates + { Url = finalUrl, FragmentSize = SegmentThreshold, Stopwatch = Stopwatch.StartNew(), @@ -128,9 +155,11 @@ private async Task DownloadInGroupAsync(GroupDownloadStates downloadStates, Down else states.TotalBytes = request.Size; - if (DownloadManager.IsEnableFragment) { + if (DownloadManager.IsEnableFragment) + { bool supportsRange = await ValidateRangeSupport(finalUrl, cancellationToken).ConfigureAwait(false); - if (supportsRange) { + if (supportsRange) + { await DownloadMultiPartAsync(states, request, cancellationToken).ConfigureAwait(false); Interlocked.Increment(ref downloadStates.DownloadedCount); return; @@ -141,18 +170,24 @@ private async Task DownloadInGroupAsync(GroupDownloadStates downloadStates, Down Interlocked.Increment(ref downloadStates.DownloadedCount); request.Completed?.Invoke(EventArgs.Empty); break; - } catch (OperationCanceledException) { - } catch (Exception) { + } + catch (OperationCanceledException) + { + } + catch (Exception) + { await Task.Delay(1000 * (attempt + 1), cancellationToken).ConfigureAwait(false); } } - } finally { + } + finally + { _globalSemaphore.Release(); } - } - private static async Task ValidateRangeSupport(string url, CancellationToken cancellationToken) { + private static async Task ValidateRangeSupport(string url, CancellationToken cancellationToken) + { using var rangeRequest = new HttpRequestMessage(HttpMethod.Get, url); rangeRequest.Headers.Range = new RangeHeaderValue(0, 0); var response = await HttpUtil.DownloaderClient.SendAsync(rangeRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken) @@ -161,11 +196,13 @@ private static async Task ValidateRangeSupport(string url, CancellationTok return response.StatusCode == HttpStatusCode.PartialContent; } - private static async Task DownloadFileDriverAsync(DownloadRequest request, CancellationToken cancellationToken) { + private static async Task DownloadFileDriverAsync(DownloadRequest request, CancellationToken cancellationToken) + { string url = request.Url; var (response, finalUrl) = await PrepareForDownloadAsync(url, cancellationToken).ConfigureAwait(false); - var states = new DownloadStates { + var states = new DownloadStates + { Url = finalUrl, FragmentSize = SegmentThreshold, Stopwatch = Stopwatch.StartNew(), @@ -180,9 +217,11 @@ private static async Task DownloadFileDriverAsync(DownloadRequest request, Cance else states.TotalBytes = request.Size; - if (DownloadManager.IsEnableFragment) { + if (DownloadManager.IsEnableFragment) + { bool supportsRange = await ValidateRangeSupport(finalUrl, cancellationToken); - if (supportsRange) { + if (supportsRange) + { var progressTask = ReportProgressAsync(states, request, cancellationToken); var downloadTask = DownloadMultiPartAsync(states, request, cancellationToken); await Task.WhenAll(progressTask, downloadTask).ConfigureAwait(false); @@ -193,7 +232,8 @@ private static async Task DownloadFileDriverAsync(DownloadRequest request, Cance await DownloadSinglePartAsync(states, request, cancellationToken).ConfigureAwait(false); } - private static async Task<(HttpResponseMessage, string)> PrepareForDownloadAsync(string url, CancellationToken cancellationToken) { + private static async Task<(HttpResponseMessage, string)> PrepareForDownloadAsync(string url, CancellationToken cancellationToken) + { var response = await HttpUtil.FlurlClient.Request(url) .AllowAnyHttpStatus() .HeadAsync(HttpCompletionOption.ResponseHeadersRead, cancellationToken) @@ -207,7 +247,8 @@ private static async Task DownloadFileDriverAsync(DownloadRequest request, Cance return (response.ResponseMessage, url); } - private static async Task DownloadMultiPartAsync(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) { + private static async Task DownloadMultiPartAsync(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) + { long fileSize = states.TotalBytes; long totalSegments = (fileSize + SegmentThreshold - 1) / SegmentThreshold; states.TotalFragments = totalSegments; @@ -223,7 +264,8 @@ private static async Task DownloadMultiPartAsync(DownloadStates states, Download await Task.WhenAll(tasks).ConfigureAwait(false); } - private static async Task DownloadSinglePartAsync(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) { + private static async Task DownloadSinglePartAsync(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) + { using var response = await HttpUtil.DownloaderClient.GetAsync(states.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); @@ -234,13 +276,17 @@ private static async Task DownloadSinglePartAsync(DownloadStates states, Downloa fileStream.SetLength(size); byte[] buffer = ArrayPool.Shared.Rent(BufferSize); - try { + try + { await WriteStreamToFile(contentStream, fileStream, buffer, request, states, cancellationToken).ConfigureAwait(false); - } finally { + } + finally + { ArrayPool.Shared.Return(buffer); } - request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs { + request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs + { Speed = 0, EstimatedRemaining = TimeSpan.Zero, TotalBytes = states.TotalBytes, @@ -250,11 +296,14 @@ private static async Task DownloadSinglePartAsync(DownloadStates states, Downloa }); } - private static async Task MultipartDownloadWorker(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) { + private static async Task MultipartDownloadWorker(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) + { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); - try { - while (states.NextFragment() is (long start, long end)) { + try + { + while (states.NextFragment() is (long start, long end)) + { using var httpRequest = new HttpRequestMessage(HttpMethod.Get, states.Url); httpRequest.Headers.Range = new RangeHeaderValue(start, end); var response = await HttpUtil.FlurlClient.HttpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); @@ -265,18 +314,22 @@ private static async Task MultipartDownloadWorker(DownloadStates states, Downloa fileStream.Seek(start, SeekOrigin.Begin); await WriteStreamToFile(contentStream, fileStream, buffer, request, states, cancellationToken).ConfigureAwait(false); } - } finally { + } + finally + { ArrayPool.Shared.Return(buffer); } } - private static async Task ReportProgressAsync(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) { + private static async Task ReportProgressAsync(DownloadStates states, DownloadRequest request, CancellationToken cancellationToken) + { var sw = Stopwatch.StartNew(); long prevBytes = 0; double prevTime = 0; using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1000)); - while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) { + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { long nowBytes = Interlocked.Read(ref states.DownloadedBytes); long totalBytes = Interlocked.Read(ref states.TotalBytes); @@ -295,7 +348,8 @@ private static async Task ReportProgressAsync(DownloadStates states, DownloadReq prevTime = nowTime; prevBytes = nowBytes; - request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs { + request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs + { Speed = speed, EstimatedRemaining = eta, TotalBytes = totalBytes, @@ -305,7 +359,8 @@ private static async Task ReportProgressAsync(DownloadStates states, DownloadReq }); } - request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs { + request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs + { Speed = 0, TotalBytes = states.TotalBytes, EstimatedRemaining = TimeSpan.Zero, @@ -315,13 +370,15 @@ private static async Task ReportProgressAsync(DownloadStates states, DownloadReq }); } - private static async Task ReportGroupProgressAsync(GroupDownloadStates states, GroupDownloadRequest request, CancellationToken cancellationToken) { + private static async Task ReportGroupProgressAsync(GroupDownloadStates states, GroupDownloadRequest request, CancellationToken cancellationToken) + { var sw = Stopwatch.StartNew(); long prevBytes = 0; double prevTime = 0; using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1000)); - while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) { + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { Interlocked.Exchange(ref states.TotalBytes, states.States.Sum(x => x.TotalBytes)); Interlocked.Exchange(ref states.DownloadedBytes, states.States.Sum(x => x.DownloadedBytes)); @@ -349,7 +406,8 @@ private static async Task ReportGroupProgressAsync(GroupDownloadStates states, G prevTime = nowTime; prevBytes = nowBytes; - request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs { + request.ProgressChanged?.Invoke(new ResourceDownloadProgressChangedEventArgs + { Speed = speed, EstimatedRemaining = eta, TotalBytes = totalBytes, @@ -360,15 +418,18 @@ private static async Task ReportGroupProgressAsync(GroupDownloadStates states, G } } - private static async Task WriteStreamToFile(Stream contentStream, FileStream fileStream, byte[] buffer, DownloadRequest request, DownloadStates states, CancellationToken cancellationToken) { + private static async Task WriteStreamToFile(Stream contentStream, FileStream fileStream, byte[] buffer, DownloadRequest request, DownloadStates states, CancellationToken cancellationToken) + { int bytesRead; - while ((bytesRead = await contentStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken)) > 0) { + while ((bytesRead = await contentStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken)) > 0) + { await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); Interlocked.Add(ref states.DownloadedBytes, bytesRead); } } - private class DownloadStates { + private class DownloadStates + { private long _fragmentScheduled = -1; public long TotalBytes; @@ -382,7 +443,8 @@ private class DownloadStates { public Stopwatch Stopwatch = new(); - public (long start, long end)? NextFragment() { + public (long start, long end)? NextFragment() + { long index = Interlocked.Increment(ref _fragmentScheduled); if (index >= TotalFragments) return null; @@ -393,7 +455,8 @@ private class DownloadStates { } } - private class GroupDownloadStates { + private class GroupDownloadStates + { public long TotalBytes; public long DownloadedBytes; diff --git a/MinecraftLaunch/Components/Downloader/MinecraftResourceDownloader.cs b/MinecraftLaunch/Components/Downloader/MinecraftResourceDownloader.cs index 6d500d2..0608f5d 100644 --- a/MinecraftLaunch/Components/Downloader/MinecraftResourceDownloader.cs +++ b/MinecraftLaunch/Components/Downloader/MinecraftResourceDownloader.cs @@ -10,7 +10,8 @@ namespace MinecraftLaunch.Components.Downloader; -public sealed class MinecraftResourceDownloader { +public sealed class MinecraftResourceDownloader +{ private readonly MinecraftEntry _entry; private readonly DefaultDownloader _downloader; private readonly List _dependencies = []; @@ -21,7 +22,8 @@ public sealed class MinecraftResourceDownloader { public bool AllowVerifyAssets { get; init; } = true; public bool AllowInheritedDependencies { get; init; } = true; - public MinecraftResourceDownloader(MinecraftEntry entry, IEnumerable extraDependencies = null) { + public MinecraftResourceDownloader(MinecraftEntry entry, IEnumerable extraDependencies = null) + { if (extraDependencies is not null) _dependencies.AddRange(extraDependencies); @@ -29,7 +31,8 @@ public MinecraftResourceDownloader(MinecraftEntry entry, IEnumerable VerifyAndDownloadDependenciesAsync(int fileVerificationParallelism = 10, CancellationToken cancellationToken = default) { + public async Task VerifyAndDownloadDependenciesAsync(int fileVerificationParallelism = 10, CancellationToken cancellationToken = default) + { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(fileVerificationParallelism); #region 1.1 Libraries & Inherited Libraries @@ -40,30 +43,34 @@ public async Task VerifyAndDownloadDependenciesAsync(int fi if (AllowInheritedDependencies && _entry is ModifiedMinecraftEntry modInstance - && modInstance.HasInheritance) { + && modInstance.HasInheritance) + { (libs, nativeLibs) = modInstance.InheritedMinecraft.GetRequiredLibraries(); _dependencies.AddRange(libs); _dependencies.AddRange(nativeLibs); } - #endregion + #endregion 1.1 Libraries & Inherited Libraries #region 1.2 Client.jar var jar = _entry.GetJarElement(); - if (jar != null) { + if (jar != null) + { _dependencies.Add(jar); } - #endregion + #endregion 1.2 Client.jar #region 1.3 AssetIndex & Assets - if (AllowVerifyAssets) { + if (AllowVerifyAssets) + { var assetIndex = _entry.GetAssetIndex(); // 验证 AssetIndex 文件 - if (!VerifyDependency(assetIndex, cancellationToken)) { + if (!VerifyDependency(assetIndex, cancellationToken)) + { await assetIndex.Url.DownloadFileAsync(Path.Combine(assetIndex.MinecraftFolderPath, "assets", "indexes"), $"{assetIndex.Id}.json", 65536, HttpCompletionOption.ResponseHeadersRead, cancellationToken); } @@ -72,12 +79,14 @@ await assetIndex.Url.DownloadFileAsync(Path.Combine(assetIndex.MinecraftFolderPa _dependencies.AddRange(_entry.GetRequiredAssets()); } - #endregion + #endregion 1.3 AssetIndex & Assets // 2. 验证依赖项 ConcurrentBag invalidDeps = []; - Parallel.ForEach(_dependencies, new ParallelOptions { MaxDegreeOfParallelism = fileVerificationParallelism }, dep => { - if (!VerifyDependency(dep, cancellationToken)) { + Parallel.ForEach(_dependencies, new ParallelOptions { MaxDegreeOfParallelism = fileVerificationParallelism }, dep => + { + if (!VerifyDependency(dep, cancellationToken)) + { invalidDeps.Add(dep); } }); @@ -100,7 +109,8 @@ await assetIndex.Url.DownloadFileAsync(Path.Combine(assetIndex.MinecraftFolderPa #region Privates - private static bool VerifyDependency(MinecraftDependency dep, CancellationToken cancellationToken = default) { + private static bool VerifyDependency(MinecraftDependency dep, CancellationToken cancellationToken = default) + { cancellationToken.ThrowIfCancellationRequested(); Debug.WriteLineIf(dep is FabricLibrary, dep.FullPath); if (!File.Exists(dep.FullPath)) @@ -109,7 +119,8 @@ private static bool VerifyDependency(MinecraftDependency dep, CancellationToken if (dep is not IVerifiableDependency verifiableDependency) return true; - bool VerifySha1() { + bool VerifySha1() + { using var fileStream = File.OpenRead(dep.FullPath); byte[] sha1Bytes = SHA1.HashData(fileStream); @@ -122,7 +133,8 @@ bool VerifySha1() { return sha1Str == verifiableDependency.Sha1; } - bool VerifySize() { + bool VerifySize() + { var file = new FileInfo(dep.FullPath); return verifiableDependency.Size == file.Length; } @@ -135,5 +147,5 @@ bool VerifySize() { return true; } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/CompositeInstaller.cs b/MinecraftLaunch/Components/Installer/CompositeInstaller.cs index f5ef9ab..716504c 100644 --- a/MinecraftLaunch/Components/Installer/CompositeInstaller.cs +++ b/MinecraftLaunch/Components/Installer/CompositeInstaller.cs @@ -7,7 +7,8 @@ namespace MinecraftLaunch.Components.Installer; -public sealed class CompositeInstaller : InstallerBase { +public sealed class CompositeInstaller : InstallerBase +{ public string JavaPath { get; init; } public string CustomId { get; init; } public override string MinecraftFolder { get; init; } @@ -19,8 +20,10 @@ public sealed class CompositeInstaller : InstallerBase { internal InstallerBase SecondaryInstaller { get; set; } internal VanillaInstaller VanillaInstaller { get; set; } - public static CompositeInstaller Create(IEnumerable installEntries, string mcFolder, string javaPath = default, string customId = default) { - return new CompositeInstaller { + public static CompositeInstaller Create(IEnumerable installEntries, string mcFolder, string javaPath = default, string customId = default) + { + return new CompositeInstaller + { JavaPath = javaPath, CustomId = customId, MinecraftFolder = mcFolder, @@ -28,12 +31,14 @@ public static CompositeInstaller Create(IEnumerable installEntrie }; } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { MinecraftEntry minecraft = null; ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { ParseInstaller(cancellationToken); minecraft = await InstallVanillaAsync(cancellationToken); @@ -44,7 +49,9 @@ public override async Task InstallAsync(CancellationToken cancel minecraft = modifiedMinecraft; ReportProgress(InstallStep.RanToCompletion, 1.0d, TaskStatus.RanToCompletion, 1, 1); ReportCompleted(true); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(false, ex); } @@ -54,7 +61,8 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private void ParseInstaller(CancellationToken cancellationToken) { + private void ParseInstaller(CancellationToken cancellationToken) + { ReportProgress(InstallStep.ParseInstaller, 0.1d, TaskStatus.Running, 1, 0); if (!InstallEntries.Any()) @@ -63,22 +71,30 @@ private void ParseInstaller(CancellationToken cancellationToken) { if (InstallEntries.Count() > 3) throw new ArgumentOutOfRangeException(); - foreach (var entry in InstallEntries) { - if (entry is VersionManifestEntry ve) { + foreach (var entry in InstallEntries) + { + if (entry is VersionManifestEntry ve) + { VanillaInstaller = VanillaInstaller.Create(MinecraftFolder, ve); continue; } - if (entry is OptifineInstallEntry oe) { + if (entry is OptifineInstallEntry oe) + { SecondaryInstaller = OptifineInstaller.Create(MinecraftFolder, JavaPath, oe, CustomId); continue; } - if (entry is ForgeInstallEntry fe) { + if (entry is ForgeInstallEntry fe) + { PrimaryInstaller = ForgeInstaller.Create(MinecraftFolder, JavaPath, fe, CustomId); - } else if (entry is FabricInstallEntry fae) { + } + else if (entry is FabricInstallEntry fae) + { PrimaryInstaller = FabricInstaller.Create(MinecraftFolder, fae, CustomId); - } else if (entry is QuiltInstallEntry qe) { + } + else if (entry is QuiltInstallEntry qe) + { PrimaryInstaller = QuiltInstaller.Create(MinecraftFolder, qe, CustomId); } } @@ -86,8 +102,10 @@ private void ParseInstaller(CancellationToken cancellationToken) { ReportProgress(InstallStep.ParseInstaller, 0.2d, TaskStatus.Running, 1, 1); } - private Task InstallVanillaAsync(CancellationToken cancellationToken) { - if (VanillaInstaller is null) { + private Task InstallVanillaAsync(CancellationToken cancellationToken) + { + if (VanillaInstaller is null) + { throw new ArgumentNullException(nameof(VanillaInstaller)); } @@ -96,7 +114,8 @@ private Task InstallVanillaAsync(CancellationToken cancellationT arg.Status, arg.TotalStepTaskCount, arg.FinishedStepTaskCount, InstallStep.InstallVanilla, arg.Speed, arg.IsStepSupportSpeed); - VanillaInstaller.Completed += (_, arg) => { + VanillaInstaller.Completed += (_, arg) => + { if (!arg.IsSuccessful) throw arg.Exception; }; @@ -104,8 +123,10 @@ private Task InstallVanillaAsync(CancellationToken cancellationT return VanillaInstaller.InstallAsync(cancellationToken); } - private Task InstallPrimaryModLoaderAsync(MinecraftEntry entry, CancellationToken cancellationToken) { - if (PrimaryInstaller is null) { + private Task InstallPrimaryModLoaderAsync(MinecraftEntry entry, CancellationToken cancellationToken) + { + if (PrimaryInstaller is null) + { return Task.FromResult(entry); } @@ -114,7 +135,8 @@ private Task InstallPrimaryModLoaderAsync(MinecraftEntry entry, arg.Status, arg.TotalStepTaskCount, arg.FinishedStepTaskCount, InstallStep.InstallPrimaryModLoader, arg.Speed, arg.IsStepSupportSpeed); - PrimaryInstaller.Completed += (_, arg) => { + PrimaryInstaller.Completed += (_, arg) => + { if (!arg.IsSuccessful) throw arg.Exception; }; @@ -122,12 +144,15 @@ private Task InstallPrimaryModLoaderAsync(MinecraftEntry entry, return PrimaryInstaller.InstallAsync(cancellationToken); } - private Task InstallSecondaryModLoaderAsync(MinecraftEntry entry, CancellationToken cancellationToken) { - if (SecondaryInstaller is null) { + private Task InstallSecondaryModLoaderAsync(MinecraftEntry entry, CancellationToken cancellationToken) + { + if (SecondaryInstaller is null) + { return Task.FromResult(entry); } - if (SecondaryInstaller is OptifineInstaller oi) { + if (SecondaryInstaller is OptifineInstaller oi) + { oi.Minecraft = entry; } @@ -136,7 +161,8 @@ private Task InstallSecondaryModLoaderAsync(MinecraftEntry entry arg.Status, arg.TotalStepTaskCount, arg.FinishedStepTaskCount, InstallStep.InstallSecondaryModLoader, arg.Speed, arg.IsStepSupportSpeed); - SecondaryInstaller.Completed += (_, arg) => { + SecondaryInstaller.Completed += (_, arg) => + { if (!arg.IsSuccessful) throw arg.Exception; }; @@ -145,8 +171,10 @@ private Task InstallSecondaryModLoaderAsync(MinecraftEntry entry } internal void ReportProgress(InstallStep step, double progress, TaskStatus status, int totalCount, int finshedCount, - InstallStep primaryStep = InstallStep.Undefined, double speed = -1, bool isSupportStep = false) { - ProgressChanged?.Invoke(this, new CompositeInstallProgressChangedEventArgs { + InstallStep primaryStep = InstallStep.Undefined, double speed = -1, bool isSupportStep = false) + { + ProgressChanged?.Invoke(this, new CompositeInstallProgressChangedEventArgs + { Speed = speed, Status = status, StepName = step, @@ -158,5 +186,5 @@ internal void ReportProgress(InstallStep step, double progress, TaskStatus statu }); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/FabricInstaller.cs b/MinecraftLaunch/Components/Installer/FabricInstaller.cs index cd86f67..5e8e0bc 100644 --- a/MinecraftLaunch/Components/Installer/FabricInstaller.cs +++ b/MinecraftLaunch/Components/Installer/FabricInstaller.cs @@ -10,21 +10,25 @@ namespace MinecraftLaunch.Components.Installer; -public sealed class FabricInstaller : InstallerBase { +public sealed class FabricInstaller : InstallerBase +{ public string CustomId { get; init; } public FabricInstallEntry Entry { get; init; } public override string MinecraftFolder { get; init; } public MinecraftEntry InheritedMinecraft { get; init; } - public static FabricInstaller Create(string mcFolder, FabricInstallEntry installEntry, string customId = default) { - return new FabricInstaller { + public static FabricInstaller Create(string mcFolder, FabricInstallEntry installEntry, string customId = default) + { + return new FabricInstaller + { CustomId = customId, Entry = installEntry, MinecraftFolder = mcFolder, }; } - public static async Task> EnumerableFabricAsync(string mcVersion, CancellationToken cancellationToken = default) { + public static async Task> EnumerableFabricAsync(string mcVersion, CancellationToken cancellationToken = default) + { string json = await HttpUtil.FlurlClient.Request($"https://meta.fabricmc.net/v2/versions/loader/{mcVersion}") .GetStringAsync(cancellationToken: cancellationToken); @@ -34,17 +38,21 @@ public static async Task> EnumerableFabricAsync( return entries; } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { ModifiedMinecraftEntry entry = default; ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { var inheritedEntry = ParseMinecraft(cancellationToken); var jsonFile = await DownloadVersionJsonAsync(inheritedEntry, cancellationToken); entry = ParseModifiedMinecraft(jsonFile, cancellationToken); await CompleteFabricLibrariesAsync(entry, cancellationToken); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(false, ex); } @@ -56,11 +64,13 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { + private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParseMinecraft, 0.10d, TaskStatus.Running, 1, 0); - if (InheritedMinecraft is not null) { + if (InheritedMinecraft is not null) + { return InheritedMinecraft; } @@ -71,7 +81,8 @@ private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { return inheritedMinecraft ?? throw new InvalidOperationException("The corresponding version's parent was not found."); ; } - private async Task DownloadVersionJsonAsync(MinecraftEntry entry, CancellationToken cancellationToken) { + private async Task DownloadVersionJsonAsync(MinecraftEntry entry, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadVersionJson, 0.20d, TaskStatus.Running, 1, 0); @@ -97,7 +108,8 @@ private async Task DownloadVersionJsonAsync(MinecraftEntry entry, Canc return jsonFile; } - private async Task CompleteFabricLibrariesAsync(MinecraftEntry minecraft, CancellationToken cancellationToken) { + private async Task CompleteFabricLibrariesAsync(MinecraftEntry minecraft, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadLibraries, 0.5d, TaskStatus.Running, 0, 0); @@ -113,12 +125,13 @@ private async Task CompleteFabricLibrariesAsync(MinecraftEntry minecraft, Cancel // throw new InvalidOperationException("Some dependent files encountered errors during download"); } - private static ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) { + private static ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); var entry = MinecraftParser.Parse(file.Directory, null, out var _) as ModifiedMinecraftEntry; return entry ?? throw new InvalidOperationException("An incorrect modified entry was encountered"); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/ForgeInstaller.cs b/MinecraftLaunch/Components/Installer/ForgeInstaller.cs index 90f4c07..844ed44 100644 --- a/MinecraftLaunch/Components/Installer/ForgeInstaller.cs +++ b/MinecraftLaunch/Components/Installer/ForgeInstaller.cs @@ -17,15 +17,18 @@ namespace MinecraftLaunch.Components.Installer; /// /// Forge(Neo)通用安装器 /// -public sealed class ForgeInstaller : InstallerBase { +public sealed class ForgeInstaller : InstallerBase +{ public string CustomId { get; init; } public string JavaPath { get; init; } public ForgeInstallEntry Entry { get; init; } public override string MinecraftFolder { get; init; } public MinecraftEntry InheritedMinecraft { get; init; } - public static ForgeInstaller Create(string folder, string javaPath, ForgeInstallEntry installEntry, string customId = default) { - return new ForgeInstaller { + public static ForgeInstaller Create(string folder, string javaPath, ForgeInstallEntry installEntry, string customId = default) + { + return new ForgeInstaller + { CustomId = customId, JavaPath = javaPath, Entry = installEntry, @@ -33,14 +36,16 @@ public static ForgeInstaller Create(string folder, string javaPath, ForgeInstall }; } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { FileInfo forgePackageFile = default; MinecraftEntry inheritedEntry = default; ModifiedMinecraftEntry entry = default; ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { inheritedEntry = ParseMinecraft(cancellationToken); forgePackageFile = await DownloadForgePackageAsync(cancellationToken); @@ -50,13 +55,16 @@ public override async Task InstallAsync(CancellationToken cancel entry = ParseModifiedMinecraft(forgeClientFile, cancellationToken); await CompleteForgeDependenciesAsync(isLegacy, installProfile, entry, cancellationToken); - if (!isLegacy) { + if (!isLegacy) + { await RunInstallProcessorAsync(forgePackageFile.FullName, installProfile, entry, cancellationToken); } ReportProgress(InstallStep.RanToCompletion, 1.0d, TaskStatus.RanToCompletion, 1, 1); ReportCompleted(true); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(false, ex); } @@ -64,7 +72,8 @@ public override async Task InstallAsync(CancellationToken cancel return entry ?? throw new ArgumentNullException(nameof(entry), "Unexpected null reference to variable"); } - public static async Task> EnumerableForgeAsync(string mcVersion, bool isNeoforge = false, CancellationToken cancellationToken = default) { + public static async Task> EnumerableForgeAsync(string mcVersion, bool isNeoforge = false, CancellationToken cancellationToken = default) + { var packagesUrl = isNeoforge ? $"https://bmclapi2.bangbang93.com/neoforge/list/{mcVersion}" : $"https://bmclapi2.bangbang93.com/forge/minecraft/{mcVersion}"; @@ -81,11 +90,13 @@ public static async Task> EnumerableForgeAsync(st #region Privates - private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { + private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParseMinecraft, 0.15d, TaskStatus.Running, 1, 0); - if (InheritedMinecraft is not null) { + if (InheritedMinecraft is not null) + { return InheritedMinecraft; } @@ -96,16 +107,20 @@ private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { return inheritedMinecraft ?? throw new InvalidOperationException("The corresponding version's parent was not found."); ; } - private async Task DownloadForgePackageAsync(CancellationToken cancellationToken) { + private async Task DownloadForgePackageAsync(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadPackage, 0.30d, TaskStatus.Running, 1, 0); string packageUrl; - if (Entry.IsNeoforge) { + if (Entry.IsNeoforge) + { string prefix = Entry.McVersion is "1.20.1" ? "forge" : "neoforge"; packageUrl = $"https://maven.neoforged.net/releases/net/neoforged/{prefix}/" + Entry.ForgeVersion + $"/{prefix}-{Entry.ForgeVersion}-installer.jar"; - } else { + } + else + { List identifiers = [Entry.McVersion, Entry.ForgeVersion]; string loaderVersion = string.Join('-', identifiers); @@ -130,7 +145,8 @@ private async Task DownloadForgePackageAsync(CancellationToken cancell return packageFile; } - private (ZipArchive package, JsonNode installProfile, bool isLegacy) ParseForgePackage(string packageFilePath, CancellationToken cancellationToken) { + private (ZipArchive package, JsonNode installProfile, bool isLegacy) ParseForgePackage(string packageFilePath, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParsePackage, 0.45d, TaskStatus.Running, 1, 0); @@ -147,13 +163,15 @@ private async Task DownloadForgePackageAsync(CancellationToken cancell return (packageArchive, installProfileNode, isLegacyForgeVersion); } - private async Task WriteVersionJsonAndSomeDependenciesAsync(bool isLegacyForgeVersion, JsonNode installProfile, ZipArchive packageArchive, CancellationToken cancellationToken) { + private async Task WriteVersionJsonAndSomeDependenciesAsync(bool isLegacyForgeVersion, JsonNode installProfile, ZipArchive packageArchive, CancellationToken cancellationToken) + { string forgeVersion = $"{Entry.McVersion}-{Entry.ForgeVersion}"; string forgeLibsFolder = Path.Combine(MinecraftFolder, "libraries\\net\\minecraftforge\\forge", forgeVersion); ReportProgress(InstallStep.WriteVersionJsonAndSomeDependencies, 0.50d, TaskStatus.Running, 1, 0); - if (isLegacyForgeVersion) { + if (isLegacyForgeVersion) + { var universalFilePath = installProfile.Select("install").GetString("filePath") ?? throw new InvalidDataException("Unable to resolve location of universal file in archive"); @@ -188,7 +206,8 @@ private async Task WriteVersionJsonAndSomeDependenciesAsync(bool isLeg return jsonFile; } - private async Task CompleteForgeDependenciesAsync(bool isLegacyForgeVersion, JsonNode installProfile, MinecraftEntry minecraft, CancellationToken cancellationToken) { + private async Task CompleteForgeDependenciesAsync(bool isLegacyForgeVersion, JsonNode installProfile, MinecraftEntry minecraft, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadLibraries, 0.50d, TaskStatus.Running, 1, 0); @@ -202,7 +221,8 @@ private async Task CompleteForgeDependenciesAsync(bool isLegacyForgeVersion, Jso dependencies.AddRange(libraries); - if (!isLegacyForgeVersion) { + if (!isLegacyForgeVersion) + { var processorLibraries = installProfile.Select("libraries") .Deserialize(LibraryEntryContext.Default.IEnumerableLibraryEntry)? .Select(lib => MinecraftLibrary.ParseJsonNode(lib, MinecraftFolder)) @@ -217,7 +237,7 @@ private async Task CompleteForgeDependenciesAsync(bool isLegacyForgeVersion, Jso .Select(x => new DownloadRequest(DownloadManager.BmclApi.TryFindUrl(x.Url), x.FullPath))); groupDownloadRequest.ProgressChanged = args - => ReportProgress(InstallStep.DownloadLibraries, args.Percentage.ToPercentage(0.50d, 0.70d), + => ReportProgress(InstallStep.DownloadLibraries, args.Percentage.ToPercentage(0.50d, 0.70d), TaskStatus.Running, args.TotalCount, args.CompletedCount, args.Speed, true); var groupDownloadResult = await new DefaultDownloader() @@ -227,7 +247,8 @@ private async Task CompleteForgeDependenciesAsync(bool isLegacyForgeVersion, Jso // throw new InvalidOperationException("Some dependent files encountered errors during download"); } - private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode installProfile, MinecraftEntry entry, CancellationToken cancellationToken) { + private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode installProfile, MinecraftEntry entry, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); Dictionary> forgeDataDictionary = installProfile.Select("data") @@ -236,7 +257,8 @@ private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode ins string forgeVersion = $"{Entry.McVersion}-{Entry.ForgeVersion}"; - if (forgeDataDictionary.TryGetValue("BINPATCH", out Dictionary value)) { + if (forgeDataDictionary.TryGetValue("BINPATCH", out Dictionary value)) + { value["client"] = $"[net.minecraftforge:forge:{forgeVersion}:clientdata@lzma]"; value["server"] = $"[net.minecraftforge:forge:{forgeVersion}:serverdata@lzma]"; } @@ -251,7 +273,8 @@ private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode ins }; var replaceProcessorArgs = forgeDataDictionary.ToDictionary( - kvp => $"{{{kvp.Key}}}", kvp => { + kvp => $"{{{kvp.Key}}}", kvp => + { var value = kvp.Value["client"]; if (!value.StartsWith('[')) return value; @@ -270,10 +293,12 @@ private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode ins int totalCount = forgeProcessors.Length; ReportProgress(InstallStep.RunInstallProcessor, 0.70d, TaskStatus.Running, totalCount, count); - foreach (var processor in forgeProcessors) { + foreach (var processor in forgeProcessors) + { cancellationToken.ThrowIfCancellationRequested(); - processor.Args = processor.Args.Select(x => { + processor.Args = processor.Args.Select(x => + { if (x.StartsWith('[')) return Path.Combine(MinecraftFolder, "libraries", x.TrimStart('[').TrimEnd(']').FormatLibraryNameToRelativePath()) .ToPath(); @@ -307,7 +332,8 @@ private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode ins args.AddRange(processor.Args); - using var process = Process.Start(new ProcessStartInfo(JavaPath) { + using var process = Process.Start(new ProcessStartInfo(JavaPath) + { Arguments = string.Join(" ", args), UseShellExecute = false, WorkingDirectory = MinecraftFolder, @@ -317,7 +343,8 @@ private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode ins List _errorOutputs = []; - process.ErrorDataReceived += (_, args) => { + process.ErrorDataReceived += (_, args) => + { if (args.Data is string data && !string.IsNullOrEmpty(data)) _errorOutputs.Add(args.Data); }; @@ -332,17 +359,19 @@ private async Task RunInstallProcessorAsync(string packageFilePath, JsonNode ins } } - private static ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) { + private static ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); var entry = MinecraftParser.Parse(file.Directory, null, out var _) as ModifiedMinecraftEntry; return entry ?? throw new InvalidOperationException("An incorrect modified entry was encountered"); } - #endregion + #endregion Privates } -public record ForgeProcessorData { +public record ForgeProcessorData +{ [JsonPropertyName("jar")] public string Jar { get; set; } = null!; [JsonPropertyName("sides")] public List Sides { get; set; } = []; [JsonPropertyName("args")] public IEnumerable Args { get; set; } = null!; diff --git a/MinecraftLaunch/Components/Installer/InstallerBase.cs b/MinecraftLaunch/Components/Installer/InstallerBase.cs index 2c12db9..7e05a00 100644 --- a/MinecraftLaunch/Components/Installer/InstallerBase.cs +++ b/MinecraftLaunch/Components/Installer/InstallerBase.cs @@ -5,23 +5,29 @@ namespace MinecraftLaunch.Components.Installer; -public abstract class InstallerBase : IInstaller { +public abstract class InstallerBase : IInstaller +{ public abstract string MinecraftFolder { get; init; } public event EventHandler Completed; + public event EventHandler ProgressChanged; public abstract Task InstallAsync(CancellationToken cancellationToken = default); - internal void ReportCompleted(bool isSuccessful, Exception exception = default) { - Completed?.Invoke(this, new InstallComplatedEventArgs { + internal void ReportCompleted(bool isSuccessful, Exception exception = default) + { + Completed?.Invoke(this, new InstallComplatedEventArgs + { Exception = exception, IsSuccessful = isSuccessful }); } - protected internal virtual void ReportProgress(InstallStep step, double progress, TaskStatus status, int totalCount, int finshedCount, double speed = -1d, bool isSupportSpeed = false) { - ProgressChanged?.Invoke(this, new InstallProgressChangedEventArgs { + protected internal virtual void ReportProgress(InstallStep step, double progress, TaskStatus status, int totalCount, int finshedCount, double speed = -1d, bool isSupportSpeed = false) + { + ProgressChanged?.Invoke(this, new InstallProgressChangedEventArgs + { Speed = speed, Status = status, StepName = step, diff --git a/MinecraftLaunch/Components/Installer/JavaInstaller.cs b/MinecraftLaunch/Components/Installer/JavaInstaller.cs index ac532be..6e78f8b 100644 --- a/MinecraftLaunch/Components/Installer/JavaInstaller.cs +++ b/MinecraftLaunch/Components/Installer/JavaInstaller.cs @@ -1,38 +1,35 @@ using Flurl.Http; using MinecraftLaunch.Base.Enums; -using MinecraftLaunch.Base.Interfaces; -using MinecraftLaunch.Base.Models.Game; +using MinecraftLaunch.Base.EventArgs; using MinecraftLaunch.Base.Models.Network; using MinecraftLaunch.Components.Downloader; -using MinecraftLaunch.Components.Parser; -using MinecraftLaunch.Extensions; -using System.Diagnostics; using System.IO.Compression; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using System.Runtime.InteropServices; -using MinecraftLaunch.Base.EventArgs; +using System.Text.Json.Nodes; namespace MinecraftLaunch.Components.Installer; /// /// 跨平台 Java 安装器 /// -public sealed class JavaInstaller{ +public sealed class JavaInstaller +{ public string JavaFolder { get; init; } - public string MinecraftFolder { get; init; } + public string MinecraftFolder { get; init; } public event EventHandler Completed; + public event EventHandler ProgressChanged; - void ReportCompleted() { + private void ReportCompleted() + { Completed?.Invoke(this, EventArgs.Empty); } - void ReportProgress(InstallStep step, double progress, TaskStatus status, int totalCount, int finshedCount, double speed = -1d, bool isSupportStep = false) { - ProgressChanged?.Invoke(this, new InstallProgressChangedEventArgs { + private void ReportProgress(InstallStep step, double progress, TaskStatus status, int totalCount, int finshedCount, double speed = -1d, bool isSupportStep = false) + { + ProgressChanged?.Invoke(this, new InstallProgressChangedEventArgs + { Speed = speed, Status = status, StepName = step, @@ -43,8 +40,10 @@ void ReportProgress(InstallStep step, double progress, TaskStatus status, int to }); } - public static JavaInstaller Create(string javaFolder) { - return new JavaInstaller { + public static JavaInstaller Create(string javaFolder) + { + return new JavaInstaller + { JavaFolder = javaFolder, }; } @@ -54,18 +53,22 @@ public static JavaInstaller Create(string javaFolder) { /// /// 取消令牌 /// - - public async Task InstallAsync(CancellationToken cancellationToken = default) { + + public async Task InstallAsync(CancellationToken cancellationToken = default) + { ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); // 先汇报进度,避免 UI 卡死 - try { + try + { var javaInfo = await FetchJavaInfoAsync(cancellationToken); // 获取 Java 信息 var javaFile = await DownloadJavaAsync(javaInfo, cancellationToken); // 异步下载 await ExtractJavaAsync(javaFile, cancellationToken); // 异步解压缩 ReportProgress(InstallStep.RanToCompletion, 1.0d, TaskStatus.RanToCompletion, 1, 1);// 完成 ReportCompleted(); // 汇报完成 - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(); throw new InvalidOperationException("Java 安装失败", ex); @@ -78,7 +81,8 @@ public async Task InstallAsync(CancellationToken cancellationToken = default) { /// 取消令牌 /// /// - private async Task FetchJavaInfoAsync(CancellationToken cancellationToken) { + private async Task FetchJavaInfoAsync(CancellationToken cancellationToken) + { ReportProgress(InstallStep.FetchingMetadata, 0.1d, TaskStatus.Running, 1, 0); string url = "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"; @@ -87,17 +91,19 @@ private async Task FetchJavaInfoAsync(CancellationToken cancellationTo string platformKey = GetPlatformKey(); // 获取平台 var javaInfo = JsonNode.Parse(json)?[platformKey]?["java-runtime-gamma"]?.AsArray() ?? throw new InvalidOperationException($"无法获取 Java 元数据,平台:{platformKey}"); // 意思:解析Json内容,如果不是数组就报错 - if (javaInfo == null) { + if (javaInfo == null) + { throw new InvalidOperationException($"无法获取 Java 元数据,平台:{platformKey}"); } ReportProgress(InstallStep.FetchingMetadata, 0.2d, TaskStatus.Running, 1, 1); // 汇报进度 return javaInfo; } - private async Task DownloadJavaAsync(JsonNode javaInfo, CancellationToken cancellationToken) { + private async Task DownloadJavaAsync(JsonNode javaInfo, CancellationToken cancellationToken) + { ReportProgress(InstallStep.DownloadPackage, 0.3d, TaskStatus.Running, 1, 0); // 汇报进度 - string javaUrl = javaInfo [0] ["manifest"]?["url"]?.ToString() // [0] 代表第一个元素,[""manifest"] 代表 manifest 属性 + string javaUrl = javaInfo[0]["manifest"]?["url"]?.ToString() // [0] 代表第一个元素,[""manifest"] 代表 manifest 属性 ?? javaInfo[0]["url"]?.ToString() // ["url"] 代表 url 属性 ?? throw new InvalidOperationException("无法解析 Java manifest 下载地址"); // 意思:如果没有这个地址就报错 @@ -111,11 +117,13 @@ private async Task DownloadJavaAsync(JsonNode javaInfo, CancellationTo return new FileInfo(fileName); } - private async Task ExtractJavaAsync(FileInfo javaFile, CancellationToken cancellationToken) { + private async Task ExtractJavaAsync(FileInfo javaFile, CancellationToken cancellationToken) + { ReportProgress(InstallStep.ExtractingFiles, 0.7d, TaskStatus.Running, 1, 0); // 汇报进度 string extractPath = Path.Combine(JavaFolder, "runtime"); - if (!Directory.Exists(extractPath)) { + if (!Directory.Exists(extractPath)) + { Directory.CreateDirectory(extractPath); } @@ -124,14 +132,22 @@ private async Task ExtractJavaAsync(FileInfo javaFile, CancellationToken cancell ReportProgress(InstallStep.ExtractingFiles, 0.9d, TaskStatus.Running, 1, 1); } - private string GetPlatformKey() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + private string GetPlatformKey() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { return RuntimeInformation.OSArchitecture == Architecture.X64 ? "windows-x64" : "windows-x86"; - } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { return "linux"; - } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { return "mac-os"; - } else { + } + else + { throw new PlatformNotSupportedException("不支持的操作系统平台"); } } diff --git a/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs index e0ab671..ae67e41 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs @@ -10,7 +10,8 @@ namespace MinecraftLaunch.Components.Installer.Modpack; -public sealed class CurseforgeModpackInstaller : InstallerBase { +public sealed class CurseforgeModpackInstaller : InstallerBase +{ public string ModpackPath { get; init; } public MinecraftEntry Minecraft { get; init; } public override string MinecraftFolder { get; init; } @@ -19,8 +20,10 @@ public sealed class CurseforgeModpackInstaller : InstallerBase { [Obsolete("Implemented processing method")] public List FaildParseModProjectId { get; set; } = []; - public static CurseforgeModpackInstaller Create(string mcFolder, string modpackPath, CurseforgeModpackInstallEntry installEntry, MinecraftEntry entry) { - return new CurseforgeModpackInstaller { + public static CurseforgeModpackInstaller Create(string mcFolder, string modpackPath, CurseforgeModpackInstallEntry installEntry, MinecraftEntry entry) + { + return new CurseforgeModpackInstaller + { Minecraft = entry, Entry = installEntry, ModpackPath = modpackPath, @@ -28,7 +31,8 @@ public static CurseforgeModpackInstaller Create(string mcFolder, string modpackP }; } - public static CurseforgeModpackInstallEntry ParseModpackInstallEntry(string modpackPath) { + public static CurseforgeModpackInstallEntry ParseModpackInstallEntry(string modpackPath) + { using var zipArchive = ZipFile.OpenRead(modpackPath); var json = zipArchive?.GetEntry("manifest.json")?.ReadAsString() ?? throw new ArgumentException("Not found manifest.json"); @@ -39,22 +43,26 @@ public static CurseforgeModpackInstallEntry ParseModpackInstallEntry(string modp return entry; } - public static async IAsyncEnumerable ParseModLoaderEntryByManifestAsync(CurseforgeModpackInstallEntry entry, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - foreach (var loader in entry.Minecraft.ModLoaders) { + public static async IAsyncEnumerable ParseModLoaderEntryByManifestAsync(CurseforgeModpackInstallEntry entry, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var loader in entry.Minecraft.ModLoaders) + { cancellationToken.ThrowIfCancellationRequested(); (bool isPrimary, string id) = (loader.GetBool("primary"), loader.GetString("id")); var idDatas = id.Split('-'); var loaderVersion = idDatas.Last(); - var loaderType = idDatas.First() switch { + var loaderType = idDatas.First() switch + { "forge" => ModLoaderType.Forge, "fabric" => ModLoaderType.Fabric, "neoforge" => ModLoaderType.NeoForge, _ => throw new NotSupportedException("Unsupported installer type") }; - IInstallEntry installEntry = loaderType switch { + IInstallEntry installEntry = loaderType switch + { ModLoaderType.Forge => (await ForgeInstaller.EnumerableForgeAsync(entry.McVersion, cancellationToken: cancellationToken)) .First(x => x.ForgeVersion.Equals(loaderVersion)), @@ -71,10 +79,12 @@ public static async IAsyncEnumerable ParseModLoaderEntryByManifes } } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { var modInfoGroup = (await ParseModFilesAsync(cancellationToken)) .ToLookup(x => string.IsNullOrEmpty(x.url)); @@ -87,7 +97,9 @@ public override async Task InstallAsync(CancellationToken cancel await DownloadModsAsync(downloadUrls, cancellationToken); await ExtractModpackAsync(cancellationToken); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(false, ex); } @@ -101,11 +113,13 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates [Obsolete] - private void ParseMinecraft(CancellationToken cancellationToken) { + private void ParseMinecraft(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParseMinecraft, 0.05d, TaskStatus.Running, 1, 0); - if (Minecraft is not null && Minecraft is ModifiedMinecraftEntry && Minecraft.Version.VersionId.Equals(Entry.McVersion)) { + if (Minecraft is not null && Minecraft is ModifiedMinecraftEntry && Minecraft.Version.VersionId.Equals(Entry.McVersion)) + { ReportProgress(InstallStep.ParseMinecraft, 0.1d, TaskStatus.Running, 1, 1); return; } @@ -113,7 +127,8 @@ private void ParseMinecraft(CancellationToken cancellationToken) { throw new NotSupportedException("Your entry is incorrect or does not exist"); } - private async Task> ParseModFilesAsync(CancellationToken cancellationToken) { + private async Task> ParseModFilesAsync(CancellationToken cancellationToken) + { int count = 0; int totalCount = Entry.ModFiles.Count(); List requestTasks = []; @@ -122,16 +137,19 @@ private void ParseMinecraft(CancellationToken cancellationToken) { ReportProgress(InstallStep.ParseDownloadUrls, 0.1d, TaskStatus.Running, totalCount, count); - foreach (var modpackFile in Entry.ModFiles) { + foreach (var modpackFile in Entry.ModFiles) + { string downloadUrl = string.Empty; - requestTasks.Add(Task.Run(async () => { + requestTasks.Add(Task.Run(async () => + { await semaphoreSlim.WaitAsync(cancellationToken); if (modpackFile.IsRequired) downloadUrl = await CurseforgeProvider.GetModDownloadUrlAsync(modpackFile.ProjectId, modpackFile.FileId, cancellationToken); else return; - lock (requestTasks) { + lock (requestTasks) + { var progress = (double)Interlocked.Increment(ref count) / (double)totalCount; ReportProgress(InstallStep.ParseDownloadUrls, progress.ToPercentage(0.1d, 0.5d), TaskStatus.Running, totalCount, count); @@ -147,17 +165,20 @@ private void ParseMinecraft(CancellationToken cancellationToken) { return downloadInfoGroup; } - private async IAsyncEnumerable RedirectInvalidModsAsync(IEnumerable modpacks, [EnumeratorCancellation] CancellationToken cancellationToken) { + private async IAsyncEnumerable RedirectInvalidModsAsync(IEnumerable modpacks, [EnumeratorCancellation] CancellationToken cancellationToken) + { ReportProgress(InstallStep.RedirectInvalidMod, 0.5d, TaskStatus.Running, modpacks.Count(), 0); int count = 0; int totalCount = modpacks.Count(); - foreach (var modpackFile in modpacks) { + foreach (var modpackFile in modpacks) + { var modFileName = (await CurseforgeProvider .GetModFileEntryAsync(modpackFile.ProjectId, modpackFile.FileId, cancellationToken)) .GetString("fileName"); - lock (modpacks) { + lock (modpacks) + { var progress = (double)count / (double)totalCount; ReportProgress(InstallStep.RedirectInvalidMod, progress.ToPercentage(0.5d, 0.6d), TaskStatus.Running, totalCount, @@ -168,7 +189,8 @@ private async IAsyncEnumerable RedirectInvalidModsAsync(IEnumerable asyncUrls, CancellationToken cancellationToken) { + private async Task DownloadModsAsync(IEnumerable asyncUrls, CancellationToken cancellationToken) + { List downloadTasks = []; var urls = asyncUrls.ToList(); @@ -190,14 +212,17 @@ private async Task DownloadModsAsync(IEnumerable asyncUrls, Cancellation await new DefaultDownloader().DownloadManyAsync(groupRequest, cancellationToken); } - private async Task ExtractModpackAsync(CancellationToken cancellationToken) { + private async Task ExtractModpackAsync(CancellationToken cancellationToken) + { var zipArchive = ZipFile.OpenRead(ModpackPath); var entries = zipArchive?.Entries; ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, entries.Count, 0); int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { + var tasks = entries.Select(x => Task.Run(() => + { + lock (zipArchive) + { ReportProgress(InstallStep.ExtractModpack, ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.85d, 1.0d), TaskStatus.Running, entries.Count, count); @@ -210,7 +235,8 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { return; var filePath = new FileInfo(Path.Combine(Path.GetFullPath(Minecraft.ToWorkingPath(true)), subPath)); - if (x.FullName.EndsWith('/')) { + if (x.FullName.EndsWith('/')) + { Directory.CreateDirectory(filePath.FullName); return; } @@ -223,5 +249,5 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { zipArchive.Dispose(); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs index a641b2b..7f24fe0 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs @@ -8,14 +8,17 @@ namespace MinecraftLaunch.Components.Installer.Modpack; -public sealed class McbbsModpackInstaller : InstallerBase { +public sealed class McbbsModpackInstaller : InstallerBase +{ public string ModpackPath { get; init; } public MinecraftEntry Minecraft { get; init; } public McbbsModpackInstallEntry Entry { get; init; } public override string MinecraftFolder { get; init; } - public static McbbsModpackInstaller Create(string mcFolder, string modpackPath, McbbsModpackInstallEntry installEntry, MinecraftEntry entry) { - return new McbbsModpackInstaller { + public static McbbsModpackInstaller Create(string mcFolder, string modpackPath, McbbsModpackInstallEntry installEntry, MinecraftEntry entry) + { + return new McbbsModpackInstaller + { MinecraftFolder = mcFolder, ModpackPath = modpackPath, Entry = installEntry, @@ -23,11 +26,14 @@ public static McbbsModpackInstaller Create(string mcFolder, string modpackPath, }; } - public static async IAsyncEnumerable ParseModLoaderEntryAsync(McbbsModpackInstallEntry modpack, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - foreach (var addon in modpack.Addons) { + public static async IAsyncEnumerable ParseModLoaderEntryAsync(McbbsModpackInstallEntry modpack, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var addon in modpack.Addons) + { cancellationToken.ThrowIfCancellationRequested(); - IInstallEntry entry = addon.Id switch { + IInstallEntry entry = addon.Id switch + { "fabric" => (await FabricInstaller.EnumerableFabricAsync(modpack.McVersion, cancellationToken)) .First(x => x.BuildVersion.Equals(addon.Version)), "quilt" => (await QuiltInstaller.EnumerableQuiltAsync(modpack.McVersion, cancellationToken)) @@ -46,7 +52,8 @@ public static async IAsyncEnumerable ParseModLoaderEntryAsync(Mcb } } - public static McbbsModpackInstallEntry ParseModpackInstallEntry(string modpackPath) { + public static McbbsModpackInstallEntry ParseModpackInstallEntry(string modpackPath) + { using var zipArchive = ZipFile.OpenRead(modpackPath); var json = zipArchive?.GetEntry("mcbbs.packmeta")?.ReadAsString() ?? throw new ArgumentException("Not found mcbbs.packmeta"); @@ -55,15 +62,19 @@ public static McbbsModpackInstallEntry ParseModpackInstallEntry(string modpackPa ?? throw new InvalidOperationException("Failed to parsemcbbs.packmeta"); } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { await ExtractModpackAsync(cancellationToken); ReportProgress(InstallStep.RanToCompletion, 1.0d, TaskStatus.RanToCompletion, 1, 1); ReportCompleted(true); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(false, ex); } @@ -73,7 +84,8 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private async Task ExtractModpackAsync(CancellationToken cancellationToken) { + private async Task ExtractModpackAsync(CancellationToken cancellationToken) + { var zipArchive = ZipFile.OpenRead(ModpackPath); var entries = zipArchive?.Entries; ReportProgress(InstallStep.ExtractModpack, 0.10d, TaskStatus.Running, entries.Count, 0); @@ -82,8 +94,10 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { string woringPath = Minecraft.ToWorkingPath(true); int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { + var tasks = entries.Select(x => Task.Run(() => + { + lock (zipArchive) + { ReportProgress(InstallStep.ExtractModpack, ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.1d, 1.0d), TaskStatus.Running, entries.Count, count); @@ -96,7 +110,8 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { return; var filePath = new FileInfo(Path.Combine(woringPath, subPath)); - if (x.FullName.EndsWith('/')) { + if (x.FullName.EndsWith('/')) + { filePath.Directory.Create(); return; } @@ -109,5 +124,5 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { zipArchive.Dispose(); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs index a92ecd2..9fc33f9 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs @@ -4,18 +4,19 @@ using MinecraftLaunch.Base.Models.Network; using MinecraftLaunch.Components.Downloader; using MinecraftLaunch.Extensions; -using System.IO; using System.IO.Compression; namespace MinecraftLaunch.Components.Installer.Modpack; -public sealed class ModrinthModpackInstaller : InstallerBase { +public sealed class ModrinthModpackInstaller : InstallerBase +{ public string ModpackPath { get; init; } public MinecraftEntry Minecraft { get; init; } public override string MinecraftFolder { get; init; } public ModrinthModpackInstallEntry Entry { get; init; } - public static ModrinthModpackInstallEntry ParseModpackInstallEntry(string modpackPath) { + public static ModrinthModpackInstallEntry ParseModpackInstallEntry(string modpackPath) + { using var zipArchive = ZipFile.OpenRead(modpackPath); var json = zipArchive?.GetEntry("modrinth.index.json")?.ReadAsString() ?? throw new ArgumentException("Not found modrinth.index.json"); @@ -24,7 +25,8 @@ public static ModrinthModpackInstallEntry ParseModpackInstallEntry(string modpac ?? throw new InvalidOperationException("Failed to parse modrinth.index.json"); } - public static async Task ParseModLoaderEntryAsync(ModrinthModpackInstallEntry modpack, CancellationToken cancellationToken = default) { + public static async Task ParseModLoaderEntryAsync(ModrinthModpackInstallEntry modpack, CancellationToken cancellationToken = default) + { if (modpack.Dependencies.ContainsKey("fabric-loader")) return (await FabricInstaller.EnumerableFabricAsync(modpack.McVersion, cancellationToken: cancellationToken)) .First(x => x.BuildVersion.Equals(modpack.Dependencies["fabric-loader"])); @@ -41,8 +43,10 @@ public static async Task ParseModLoaderEntryAsync(ModrinthModpack throw new NotSupportedException(); } - public static ModrinthModpackInstaller Create(string mcFolder, string modpackPath, ModrinthModpackInstallEntry installEntry, MinecraftEntry entry) { - return new ModrinthModpackInstaller { + public static ModrinthModpackInstaller Create(string mcFolder, string modpackPath, ModrinthModpackInstallEntry installEntry, MinecraftEntry entry) + { + return new ModrinthModpackInstaller + { Entry = installEntry, ModpackPath = modpackPath, MinecraftFolder = mcFolder, @@ -50,15 +54,19 @@ public static ModrinthModpackInstaller Create(string mcFolder, string modpackPat }; } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { var downloadRequests = ParseModFiles(cancellationToken); await DownloadModsAsync(downloadRequests, cancellationToken); await ExtractModpackAsync(cancellationToken); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportCompleted(false, ex); } @@ -70,16 +78,19 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private IEnumerable ParseModFiles(CancellationToken cancellationToken) { + private IEnumerable ParseModFiles(CancellationToken cancellationToken) + { int totalCount = Entry.Files.Count(); ReportProgress(InstallStep.ParseDownloadUrls, 0.1d, TaskStatus.Running, totalCount, 0); int count = 0; string versionPath = Minecraft.ToWorkingPath(true); - foreach (var file in Entry.Files.AsParallel()) { + foreach (var file in Entry.Files.AsParallel()) + { cancellationToken.ThrowIfCancellationRequested(); - lock (Entry) { + lock (Entry) + { double progress = (double)Interlocked.Increment(ref count) / (double)totalCount; ReportProgress(InstallStep.ParseDownloadUrls, progress.ToPercentage(0.1d, 0.45d), TaskStatus.Running, totalCount, count); @@ -96,12 +107,14 @@ private IEnumerable ParseModFiles(CancellationToken cancellatio } } - private Task DownloadModsAsync(IEnumerable downloadRequests, CancellationToken cancellationToken) { + private Task DownloadModsAsync(IEnumerable downloadRequests, CancellationToken cancellationToken) + { List downloadTasks = []; var groupRequest = new GroupDownloadRequest(downloadRequests); - groupRequest.ProgressChanged = args => { + groupRequest.ProgressChanged = args => + { ReportProgress(InstallStep.DownloadMods, args.Percentage.ToPercentage(0.45d, 0.7d), TaskStatus.Running, args.TotalCount, args.CompletedCount, args.Speed, true); }; @@ -110,7 +123,8 @@ private Task DownloadModsAsync(IEnumerable .DownloadManyAsync(groupRequest, cancellationToken); } - private async Task ExtractModpackAsync(CancellationToken cancellationToken) { + private async Task ExtractModpackAsync(CancellationToken cancellationToken) + { var zipArchive = ZipFile.OpenRead(ModpackPath); var entries = zipArchive?.Entries; ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, entries.Count, 0); @@ -119,13 +133,15 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { string woringPath = Minecraft.ToWorkingPath(true); int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { + var tasks = entries.Select(x => Task.Run(() => + { + lock (zipArchive) + { ReportProgress(InstallStep.ExtractModpack, ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.85d, 1.0d), TaskStatus.Running, entries.Count, count); - if (!x.FullName.StartsWith(decompressPrefix)) + if (!x.FullName.StartsWith(decompressPrefix)) return; var subPath = x.FullName[(decompressPrefix.Length + 1)..]; @@ -133,7 +149,8 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { return; var filePath = new FileInfo(Path.Combine(woringPath, subPath)); - if (x.FullName.EndsWith('/')) { + if (x.FullName.EndsWith('/')) + { filePath.Directory.Create(); return; } @@ -146,5 +163,5 @@ private async Task ExtractModpackAsync(CancellationToken cancellationToken) { zipArchive.Dispose(); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/OptifineInstaller.cs b/MinecraftLaunch/Components/Installer/OptifineInstaller.cs index 162a53a..a474e40 100644 --- a/MinecraftLaunch/Components/Installer/OptifineInstaller.cs +++ b/MinecraftLaunch/Components/Installer/OptifineInstaller.cs @@ -7,11 +7,11 @@ using MinecraftLaunch.Extensions; using System.Diagnostics; using System.IO.Compression; -using System.Runtime.CompilerServices; namespace MinecraftLaunch.Components.Installer; -public sealed class OptifineInstaller : InstallerBase { +public sealed class OptifineInstaller : InstallerBase +{ public string CustomId { get; init; } public string JavaPath { get; init; } public OptifineInstallEntry Entry { get; init; } @@ -19,8 +19,10 @@ public sealed class OptifineInstaller : InstallerBase { public MinecraftEntry InheritedMinecraft { get; init; } public MinecraftEntry Minecraft { get; set; } - public static OptifineInstaller Create(string mcFolder, string javaPath, OptifineInstallEntry optifineInstallEntry, string customId = default) { - return new OptifineInstaller { + public static OptifineInstaller Create(string mcFolder, string javaPath, OptifineInstallEntry optifineInstallEntry, string customId = default) + { + return new OptifineInstaller + { MinecraftFolder = mcFolder, Entry = optifineInstallEntry, CustomId = customId, @@ -28,15 +30,18 @@ public static OptifineInstaller Create(string mcFolder, string javaPath, Optifin }; } - public static OptifineInstaller Create(string mcFolder, OptifineInstallEntry optifineInstallEntry, MinecraftEntry minecraft) { - return new OptifineInstaller { + public static OptifineInstaller Create(string mcFolder, OptifineInstallEntry optifineInstallEntry, MinecraftEntry minecraft) + { + return new OptifineInstaller + { MinecraftFolder = mcFolder, Entry = optifineInstallEntry, Minecraft = minecraft, }; } - public static async Task> EnumerableOptifineAsync(string mcVersion, CancellationToken cancellationToken = default) { + public static async Task> EnumerableOptifineAsync(string mcVersion, CancellationToken cancellationToken = default) + { string url = $"https://bmclapi2.bangbang93.com/optifine/{mcVersion}"; string json = await url.GetStringAsync(cancellationToken: cancellationToken); @@ -46,17 +51,20 @@ public static async Task> EnumerableOptifineAs return entries; } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { FileInfo optifinePackageFile = default; ModifiedMinecraftEntry entry = default; MinecraftEntry inheritedEntry = default; ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { inheritedEntry = ParseMinecraft(cancellationToken); optifinePackageFile = await DownloadOptifinePackageAsync(cancellationToken); - if (Minecraft is ModifiedMinecraftEntry modifiedMinecraft) { + if (Minecraft is ModifiedMinecraftEntry modifiedMinecraft) + { CopyToMods(optifinePackageFile); ReportProgress(InstallStep.RanToCompletion, 1.0d, TaskStatus.RanToCompletion, 1, 1); @@ -73,7 +81,9 @@ public override async Task InstallAsync(CancellationToken cancel ReportProgress(InstallStep.RanToCompletion, 1.0d, TaskStatus.RanToCompletion, 1, 1); ReportCompleted(true); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(false, ex); } @@ -83,11 +93,13 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { + private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParseMinecraft, 0.10d, TaskStatus.Running, 1, 0); - if (InheritedMinecraft is not null) { + if (InheritedMinecraft is not null) + { return InheritedMinecraft; } @@ -98,7 +110,8 @@ private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { return inheritedMinecraft ?? throw new InvalidOperationException("The corresponding version's parent was not found."); ; } - private async Task DownloadOptifinePackageAsync(CancellationToken cancellationToken) { + private async Task DownloadOptifinePackageAsync(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadPackage, 0.2d, TaskStatus.Running, 1, 0); @@ -115,7 +128,8 @@ private async Task DownloadOptifinePackageAsync(CancellationToken canc return packageFile; } - private void CopyToMods(FileInfo packageInfo) { + private void CopyToMods(FileInfo packageInfo) + { var fileInfo = new FileInfo(Path.Combine(Minecraft.ToWorkingPath(true), "mods", packageInfo.Name)); if (!fileInfo.Directory.Exists) @@ -124,7 +138,8 @@ private void CopyToMods(FileInfo packageInfo) { packageInfo.MoveTo(fileInfo.FullName, true); } - private (ZipArchive package, string launchwrapperVersion, string launchwrapperName) ParseOptifinePackage(string packageFilePath, CancellationToken cancellationToken) { + private (ZipArchive package, string launchwrapperVersion, string launchwrapperName) ParseOptifinePackage(string packageFilePath, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParsePackage, 0.3d, TaskStatus.Running, 1, 0); @@ -143,11 +158,13 @@ private async Task WriteVersionJsonAndSomeDependenciesAsync( string launchwrapperVersion, string launchwrapperName, ZipArchive packageArchive, - CancellationToken cancellationToken) { + CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.WriteVersionJsonAndSomeDependencies, 0.45d, TaskStatus.Running, 1, 0); - if (launchwrapperVersion is not "1.12") { + if (launchwrapperVersion is not "1.12") + { var launchwrapperJar = packageArchive.GetEntry($"launchwrapper-of-{launchwrapperVersion}.jar") ?? throw new FileNotFoundException("Invalid OptiFine package"); @@ -161,7 +178,8 @@ private async Task WriteVersionJsonAndSomeDependenciesAsync( jsonFile.Directory.Create(); var time = Minecraft.ReleaseTime.ToString("s"); - var jsonEntry = new OptifineMinecraftEntry { + var jsonEntry = new OptifineMinecraftEntry + { Id = entryId, InheritsFrom = minecraft.Id, Time = time, @@ -187,14 +205,16 @@ await File.WriteAllTextAsync(jsonFile.FullName, return jsonFile; } - private ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) { + private ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); var entry = MinecraftParser.Parse(file.Directory, null, out var _) as ModifiedMinecraftEntry; return entry ?? throw new InvalidOperationException("An incorrect modified entry was encountered"); } - private async Task RunInstallProcessorAsync(string packageFilePath, MinecraftEntry entry, CancellationToken cancellationToken) { + private async Task RunInstallProcessorAsync(string packageFilePath, MinecraftEntry entry, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.RunInstallProcessor, 0.65d, TaskStatus.Running, 1, 0); @@ -206,7 +226,8 @@ private async Task RunInstallProcessorAsync(string packageFilePath, MinecraftEnt optifineLibraryFile.Directory.Create(); using var process = Process.Start( - new ProcessStartInfo(JavaPath) { + new ProcessStartInfo(JavaPath) + { UseShellExecute = false, WorkingDirectory = MinecraftFolder, RedirectStandardError = true, @@ -230,5 +251,5 @@ private async Task RunInstallProcessorAsync(string packageFilePath, MinecraftEnt ReportProgress(InstallStep.RunInstallProcessor, 1.0d, TaskStatus.Running, 1, 1); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/QuiltInstaller.cs b/MinecraftLaunch/Components/Installer/QuiltInstaller.cs index 990cb31..5e519b6 100644 --- a/MinecraftLaunch/Components/Installer/QuiltInstaller.cs +++ b/MinecraftLaunch/Components/Installer/QuiltInstaller.cs @@ -10,21 +10,25 @@ namespace MinecraftLaunch.Components.Installer; -public sealed class QuiltInstaller : InstallerBase { +public sealed class QuiltInstaller : InstallerBase +{ public string CustomId { get; init; } public QuiltInstallEntry Entry { get; init; } public override string MinecraftFolder { get; init; } public MinecraftEntry InheritedMinecraft { get; init; } - public static QuiltInstaller Create(string mcFolder, QuiltInstallEntry installEntry, string customId = default) { - return new QuiltInstaller { + public static QuiltInstaller Create(string mcFolder, QuiltInstallEntry installEntry, string customId = default) + { + return new QuiltInstaller + { CustomId = customId, Entry = installEntry, MinecraftFolder = mcFolder, }; } - public static async Task> EnumerableQuiltAsync(string mcVersion, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + public static async Task> EnumerableQuiltAsync(string mcVersion, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { string json = await $"https://meta.quiltmc.org/v3/versions/loader/{mcVersion}" .GetStringAsync(cancellationToken: cancellationToken); @@ -32,19 +36,23 @@ public static async Task> EnumerableQuiltAsync(st return entries; } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { ModifiedMinecraftEntry entry = default; MinecraftEntry inheritedEntry = default; ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { inheritedEntry = ParseMinecraft(cancellationToken); var jsonFile = await DownloadVersionJsonAsync(inheritedEntry, cancellationToken); entry = ParseModifiedMinecraft(jsonFile, cancellationToken); await CompleteQuiltLibrariesAsync(entry, cancellationToken); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(false, ex); } @@ -56,11 +64,13 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { + private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParseMinecraft, 0.10d, TaskStatus.Running, 1, 0); - if (InheritedMinecraft is not null) { + if (InheritedMinecraft is not null) + { return InheritedMinecraft; } @@ -71,7 +81,8 @@ private MinecraftEntry ParseMinecraft(CancellationToken cancellationToken) { return inheritedMinecraft ?? throw new InvalidOperationException("The corresponding version's parent was not found."); ; } - private async Task DownloadVersionJsonAsync(MinecraftEntry entry, CancellationToken cancellationToken) { + private async Task DownloadVersionJsonAsync(MinecraftEntry entry, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadVersionJson, 0.20d, TaskStatus.Running, 1, 0); @@ -95,14 +106,16 @@ private async Task DownloadVersionJsonAsync(MinecraftEntry entry, Canc return jsonFile; } - private ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) { + private ModifiedMinecraftEntry ParseModifiedMinecraft(FileInfo file, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); var entry = MinecraftParser.Parse(file.Directory, null, out var _) as ModifiedMinecraftEntry; return entry ?? throw new InvalidOperationException("An incorrect modified entry was encountered"); } - private async Task CompleteQuiltLibrariesAsync(MinecraftEntry minecraft, CancellationToken cancellationToken) { + private async Task CompleteQuiltLibrariesAsync(MinecraftEntry minecraft, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadLibraries, 0.5d, TaskStatus.Running, 0, 0); @@ -119,5 +132,5 @@ private async Task CompleteQuiltLibrariesAsync(MinecraftEntry minecraft, Cancell // throw new InvalidOperationException("Some dependent files encountered errors during download"); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Installer/VanillaInstaller.cs b/MinecraftLaunch/Components/Installer/VanillaInstaller.cs index 96edb2f..4cffe08 100644 --- a/MinecraftLaunch/Components/Installer/VanillaInstaller.cs +++ b/MinecraftLaunch/Components/Installer/VanillaInstaller.cs @@ -9,18 +9,22 @@ namespace MinecraftLaunch.Components.Installer; -public sealed class VanillaInstaller : InstallerBase { +public sealed class VanillaInstaller : InstallerBase +{ public VersionManifestEntry Entry { get; init; } public override string MinecraftFolder { get; init; } - public static VanillaInstaller Create(string minecraftFolder, VersionManifestEntry entry) { - return new VanillaInstaller { + public static VanillaInstaller Create(string minecraftFolder, VersionManifestEntry entry) + { + return new VanillaInstaller + { Entry = entry, MinecraftFolder = minecraftFolder }; } - public static async Task> EnumerableMinecraftAsync(CancellationToken cancellationToken = default) { + public static async Task> EnumerableMinecraftAsync(CancellationToken cancellationToken = default) + { var url = DownloadManager.BmclApi .TryFindUrl("https://launchermeta.mojang.com/mc/game/version_manifest.json"); @@ -31,12 +35,14 @@ public static async Task> EnumerableMinecraftA return entries; } - public override async Task InstallAsync(CancellationToken cancellationToken = default) { + public override async Task InstallAsync(CancellationToken cancellationToken = default) + { MinecraftEntry entry = null; ReportProgress(InstallStep.Started, 0.0d, TaskStatus.WaitingToRun, 1, 1); - try { + try + { var dir = await DownloadVersionJsonAsync(cancellationToken); var minecraft = ParseMinecraft(dir.Directory, cancellationToken); var assetIndex = await DownloadAssetIndexFileAsync(minecraft, cancellationToken); @@ -44,10 +50,14 @@ public override async Task InstallAsync(CancellationToken cancel await CompleteMinecraftDependenciesAsync(minecraft, cancellationToken); entry = minecraft; - } catch (OperationCanceledException) { + } + catch (OperationCanceledException) + { ReportProgress(InstallStep.Interrupted, 1.0d, TaskStatus.Canceled, 1, 1); ReportCompleted(true); - } catch (Exception ex) { + } + catch (Exception ex) + { ReportCompleted(false, ex); } @@ -58,7 +68,8 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private async Task DownloadVersionJsonAsync(CancellationToken cancellationToken) { + private async Task DownloadVersionJsonAsync(CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadVersionJson, 0.15d, TaskStatus.Running, 1, 0); @@ -66,7 +77,8 @@ private async Task DownloadVersionJsonAsync(CancellationToken cancella var json = await requestUrl.GetStringAsync(HttpCompletionOption.ResponseContentRead, cancellationToken); var jsonPath = new FileInfo(Path.Combine(MinecraftFolder, "versions", Entry.Id, $"{Entry.Id}.json")); - if (!jsonPath.Directory.Exists) { + if (!jsonPath.Directory.Exists) + { jsonPath.Directory.Create(); } @@ -76,7 +88,8 @@ private async Task DownloadVersionJsonAsync(CancellationToken cancella return jsonPath; } - private async Task DownloadAssetIndexFileAsync(MinecraftEntry entry, CancellationToken cancellationToken) { + private async Task DownloadAssetIndexFileAsync(MinecraftEntry entry, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadAssetIndexFile, 0.4d, TaskStatus.Running, 1, 0); @@ -86,7 +99,8 @@ private async Task DownloadAssetIndexFileAsync(MinecraftEntry entry, C string requestUrl = DownloadManager.BmclApi.TryFindUrl(assetIndex.Url); var json = await requestUrl.GetStringAsync(HttpCompletionOption.ResponseContentRead, cancellationToken); - if (!jsonFile.Directory.Exists) { + if (!jsonFile.Directory.Exists) + { jsonFile.Directory.Create(); } @@ -96,7 +110,8 @@ private async Task DownloadAssetIndexFileAsync(MinecraftEntry entry, C return jsonFile; } - private async Task CompleteMinecraftDependenciesAsync(MinecraftEntry entry, CancellationToken cancellationToken) { + private async Task CompleteMinecraftDependenciesAsync(MinecraftEntry entry, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.DownloadLibraries, 0.5d, TaskStatus.Running, 0, 0); @@ -111,7 +126,8 @@ private async Task CompleteMinecraftDependenciesAsync(MinecraftEntry entry, Canc throw new InvalidOperationException("Some dependent files encountered errors during download"); } - private MinecraftEntry ParseMinecraft(DirectoryInfo dir, CancellationToken cancellationToken) { + private MinecraftEntry ParseMinecraft(DirectoryInfo dir, CancellationToken cancellationToken) + { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(InstallStep.ParseMinecraft, 0.35d, TaskStatus.Running, 1, 1); @@ -119,5 +135,5 @@ private MinecraftEntry ParseMinecraft(DirectoryInfo dir, CancellationToken cance ?? throw new InvalidOperationException("An incorrect vanilla entry was encountered"); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Logging/LogAnalyzer.cs b/MinecraftLaunch/Components/Logging/LogAnalyzer.cs index 6fb8b61..8689bff 100644 --- a/MinecraftLaunch/Components/Logging/LogAnalyzer.cs +++ b/MinecraftLaunch/Components/Logging/LogAnalyzer.cs @@ -7,63 +7,72 @@ namespace MinecraftLaunch.Components.Logging; -public sealed partial class LogAnalyzer { +public sealed partial class LogAnalyzer +{ public MinecraftEntry Minecraft { get; set; } public IReadOnlyList LogFiles { get; set; } - public LogAnalyzer(MinecraftEntry minecraft, IReadOnlyList logFiles = default) { + public LogAnalyzer(MinecraftEntry minecraft, IReadOnlyList logFiles = default) + { Minecraft = minecraft; LogFiles = logFiles ?? []; } - public LogAnalyzerResult Analyze() { + public LogAnalyzerResult Analyze() + { var logs = GetAllLogs(); var reasons = new List(); var suspiciousMods = new List(); - foreach (var log in logs) { + foreach (var log in logs) + { //roughly match the crash reason foreach (var (key, value) in RoughCrashReasons) if (log.Contains(key)) reasons.Add(value); - if (log.Contains("Could not reserve enough space")) { + if (log.Contains("Could not reserve enough space")) + { reasons.Add(log.Contains("for 1048576KB object heap") ? CrashReasons.Using32BitJavaCausedInsufficientJVMMemory : CrashReasons.InsufficientMemory); } - if (log.Contains("Caught exception from")) { + if (log.Contains("Caught exception from")) + { reasons.Add(CrashReasons.ModCausedGameCrash); suspiciousMods.AddRange(FindSuspiciousModId(ModCrashIdentifier() .Match(log).Value.TrimEnd('\r', '\n', ' '), log)); } - if (log.Contains("Failed to create mod instance.")) { + if (log.Contains("Failed to create mod instance.")) + { reasons.Add(CrashReasons.ModInitializationFailed); suspiciousMods.AddRange(FindSuspiciousModId((FailedModInstanceIdentifier1().IsMatch(log) - ? FailedModInstanceIdentifier1().Match(log).Value + ? FailedModInstanceIdentifier1().Match(log).Value : FailedModInstanceIdentifier2().Match(log).Value).TrimEnd('\r', '\n'), log)); } if (MixinInjectionFailureIdentifier().IsMatch(log) || log.Contains("mixin.injection.throwables.") || - log.Contains(".mixins.json] FAILED during )")) { + log.Contains(".mixins.json] FAILED during )")) + { var mod = MixinFailureIdentifierRegex().Match(log).Value; - if(string.IsNullOrWhiteSpace(mod)) + if (string.IsNullOrWhiteSpace(mod)) mod = FallbackModIdentifier().Match(log).Value; - if(string.IsNullOrWhiteSpace(mod)) + if (string.IsNullOrWhiteSpace(mod)) mod = FailedDuringIdentifier().Match(log).Value; - if(string.IsNullOrWhiteSpace(mod)) + if (string.IsNullOrWhiteSpace(mod)) mod = MixinCallbackFailureIdentifier().Match(log).Value; reasons.Add(CrashReasons.ModMixinFailed); suspiciousMods.AddRange(FindSuspiciousModId(mod.TrimEnd(("\r\n" + " ").ToCharArray()), log)); } - if (log.Contains("-- MOD ")) { + if (log.Contains("-- MOD ")) + { var loglast = log.Split("-- MOD") .LastOrDefault(); @@ -73,7 +82,8 @@ public LogAnalyzerResult Analyze() { } } - return new LogAnalyzerResult { + return new LogAnalyzerResult + { Minecraft = Minecraft, CrashReasons = reasons.Distinct().ToList(), SuspiciousMods = [] @@ -147,7 +157,8 @@ public LogAnalyzerResult Analyze() { [GeneratedRegex("(?<= in callback )[^./ ]+(?=.mixins.json:)")] private static partial Regex MixinCallbackFailureIdentifier(); - private IEnumerable GetAllLogs() { + private IEnumerable GetAllLogs() + { ArgumentException.ThrowIfNullOrEmpty(nameof(Minecraft)); List logFiles = []; @@ -161,7 +172,8 @@ private IEnumerable GetAllLogs() { logFiles = logFiles.Distinct().Where(File.Exists) .ToList(); - foreach (var logFile in logFiles) { + foreach (var logFile in logFiles) + { var log = File.ReadAllText(logFile); if (string.IsNullOrWhiteSpace(log)) continue; @@ -170,15 +182,17 @@ private IEnumerable GetAllLogs() { } } - private IEnumerable FindSuspiciousModId(string log, string fullLog) { - if(string.IsNullOrWhiteSpace(log)) + private IEnumerable FindSuspiciousModId(string log, string fullLog) + { + if (string.IsNullOrWhiteSpace(log)) yield break; - foreach(var modId in TryFindSuspiciousModId([log], fullLog)) + foreach (var modId in TryFindSuspiciousModId([log], fullLog)) yield return modId; } - private IEnumerable TryFindSuspiciousModId(IEnumerable logs, string fullLog) { + private IEnumerable TryFindSuspiciousModId(IEnumerable logs, string fullLog) + { var realLogs = logs.SelectMany(x => x.Split('(')) .Select(x => x.Trim(' ', ')')); @@ -202,7 +216,8 @@ private IEnumerable TryFindSuspiciousModId(IEnumerable logs, str var hintLines = new List(); foreach (var log in realLogs) - foreach (var modLine in modLines) { + foreach (var modLine in modLines) + { var realMod = modLine.ToLower().Replace("_", ""); if (!realMod.Contains(modLine.ToLower().Replace("_", ""))) continue; @@ -217,15 +232,16 @@ private IEnumerable TryFindSuspiciousModId(IEnumerable logs, str hintLines = hintLines.Distinct().ToList(); //cnm regex - foreach (var line in hintLines) { - var modId = isFabricMod - ? ExtractFabricModIdentifier().Match(line).Value - : ExtractOtherModIdentifier().Match(line).Value; + foreach (var line in hintLines) + { + var modId = isFabricMod + ? ExtractFabricModIdentifier().Match(line).Value + : ExtractOtherModIdentifier().Match(line).Value; - if(!string.IsNullOrWhiteSpace(modId)) + if (!string.IsNullOrWhiteSpace(modId)) yield return modId; } } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Parser/ArgumentsParser.cs b/MinecraftLaunch/Components/Parser/ArgumentsParser.cs index ddb9a02..fa2fcd1 100644 --- a/MinecraftLaunch/Components/Parser/ArgumentsParser.cs +++ b/MinecraftLaunch/Components/Parser/ArgumentsParser.cs @@ -11,14 +11,16 @@ namespace MinecraftLaunch.Components.Parser; -public sealed class ArgumentsParser { +public sealed class ArgumentsParser +{ private IReadOnlyList _natives; private IReadOnlyList _libraries; public LaunchConfig LaunchConfig { get; init; } public MinecraftEntry MinecraftEntry { get; init; } - public ArgumentsParser(MinecraftEntry minecraftEntry, LaunchConfig launchConfig) { + public ArgumentsParser(MinecraftEntry minecraftEntry, LaunchConfig launchConfig) + { LaunchConfig = launchConfig; MinecraftEntry = minecraftEntry; @@ -31,11 +33,13 @@ private bool CanParse() => internal IReadOnlyList GetNatives() => _natives; - private void LoadLibraries() { + private void LoadLibraries() + { var natives = new List(); var libraries = new List(); - if (MinecraftEntry is ModifiedMinecraftEntry { HasInheritance: true } modifiedMinecraftInstance) { + if (MinecraftEntry is ModifiedMinecraftEntry { HasInheritance: true } modifiedMinecraftInstance) + { (var inheritedLibs, var inheritedNatives) = modifiedMinecraftInstance.InheritedMinecraft.GetRequiredLibraries(); libraries.AddRange(inheritedLibs); @@ -45,23 +49,29 @@ private void LoadLibraries() { (var libs, var nats) = MinecraftEntry.GetRequiredLibraries(); natives.AddRange(nats); - foreach (var lib in libs) { + foreach (var lib in libs) + { MinecraftLibrary existsEqualLib = null; MinecraftLibrary sameNameLib = null; - foreach (var containedLib in libraries) { - if (lib.Equals(containedLib)) { + foreach (var containedLib in libraries) + { + if (lib.Equals(containedLib)) + { existsEqualLib = containedLib; break; - } else if (lib.Name == containedLib.Name + } + else if (lib.Name == containedLib.Name && lib.Classifier == containedLib.Classifier - && lib.Domain == lib.Domain) { + && lib.Domain == lib.Domain) + { sameNameLib = containedLib; break; } } - if (existsEqualLib == null) { + if (existsEqualLib == null) + { libraries.Add(lib); if (sameNameLib != null) @@ -73,7 +83,8 @@ private void LoadLibraries() { _natives = natives; } - public IEnumerable Parse() { + public IEnumerable Parse() + { if (!CanParse()) throw new InvalidOperationException("Missing required parameters"); @@ -90,7 +101,8 @@ public IEnumerable Parse() { var vmParameters = JvmArgumentParser.Parse(entity); var gameParameters = GameArgumentParser.Parse(entity); - if (MinecraftEntry is ModifiedMinecraftEntry { HasInheritance: true } inst) { + if (MinecraftEntry is ModifiedMinecraftEntry { HasInheritance: true } inst) + { var inheritedVersionEntry = JsonNode.Parse(File.ReadAllText(inst.InheritedMinecraft.ClientJsonPath)) .Deserialize(MinecraftJsonEntryContext.Default.MinecraftJsonEntry) ?? throw new JsonException("Failed to parse version.json"); @@ -135,7 +147,8 @@ public IEnumerable Parse() { ?? throw new InvalidOperationException("Invalid asset index path"); string versionType = string.IsNullOrEmpty(LaunchConfig.LauncherName) - ? MinecraftEntry.Version.Type switch { + ? MinecraftEntry.Version.Type switch + { MinecraftVersionType.Release => "release", MinecraftVersionType.Snapshot => "snapshot", MinecraftVersionType.OldBeta => "old_beta", @@ -175,7 +188,8 @@ public IEnumerable Parse() { foreach (var arg in gameParameters) yield return arg.ReplaceFromDictionary(gameParametersReplace); - if (LaunchConfig.Width != 0 && LaunchConfig.Height != 0) { + if (LaunchConfig.Width != 0 && LaunchConfig.Height != 0) + { yield return $"--width {LaunchConfig.Width}"; yield return $"--height {LaunchConfig.Height}"; } @@ -184,8 +198,10 @@ public IEnumerable Parse() { if (!string.IsNullOrWhiteSpace(LaunchConfig.SaveName) && isHighVersion) yield return $"--quickPlaySingleplayer {LaunchConfig.SaveName.ToPath()}"; - if (LaunchConfig.ServerInfo is not null) { - if (isHighVersion) { + if (LaunchConfig.ServerInfo is not null) + { + if (isHighVersion) + { yield return $"--quickPlayMultiplayer {LaunchConfig.ServerInfo.Address.ToPath()}"; yield break; } @@ -199,19 +215,24 @@ public IEnumerable Parse() { /// /// 游戏参数解析器 /// -internal sealed class GameArgumentParser { +internal sealed class GameArgumentParser +{ /// /// 解析参数 /// /// - public static IEnumerable Parse(MinecraftJsonEntry gameJsonEntry) { - if (!string.IsNullOrEmpty(gameJsonEntry.MinecraftArguments)) { - foreach (var arg in gameJsonEntry.MinecraftArguments.Split(' ').GroupArguments()) { + public static IEnumerable Parse(MinecraftJsonEntry gameJsonEntry) + { + if (!string.IsNullOrEmpty(gameJsonEntry.MinecraftArguments)) + { + foreach (var arg in gameJsonEntry.MinecraftArguments.Split(' ').GroupArguments()) + { yield return arg; } } - if (gameJsonEntry.Arguments?.GetEnumerable("game") is null) { + if (gameJsonEntry.Arguments?.GetEnumerable("game") is null) + { yield break; } @@ -228,11 +249,14 @@ public static IEnumerable Parse(MinecraftJsonEntry gameJsonEntry) { /// /// Jvm 虚拟机参数解析器 /// -internal sealed class JvmArgumentParser { - public static IEnumerable Parse(MinecraftJsonEntry gameJsonEntry) { +internal sealed class JvmArgumentParser +{ + public static IEnumerable Parse(MinecraftJsonEntry gameJsonEntry) + { var jvm = new List(); - if (gameJsonEntry.Arguments.GetEnumerable("jvm") is null) { + if (gameJsonEntry.Arguments.GetEnumerable("jvm") is null) + { yield return "-Djava.library.path=${natives_directory}"; yield return "-Dminecraft.launcher.brand=${launcher_name}"; yield return "-Dminecraft.launcher.version=${launcher_version}"; @@ -240,8 +264,10 @@ public static IEnumerable Parse(MinecraftJsonEntry gameJsonEntry) { yield break; } - foreach (var arg in gameJsonEntry.Arguments.GetEnumerable("jvm")) { - if (arg.GetValueKind() is JsonValueKind.String) { + foreach (var arg in gameJsonEntry.Arguments.GetEnumerable("jvm")) + { + if (arg.GetValueKind() is JsonValueKind.String) + { var argValue = arg.GetString().Trim(); if (argValue.Contains(' ')) @@ -263,15 +289,19 @@ public static IEnumerable Parse(MinecraftJsonEntry gameJsonEntry) { /// 获取虚拟机环境参数 /// /// - public static IEnumerable GetEnvironmentJvmArguments() { - switch (EnvironmentUtil.GetPlatformName()) { + public static IEnumerable GetEnvironmentJvmArguments() + { + switch (EnvironmentUtil.GetPlatformName()) + { case "windows": yield return "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"; - if (Environment.OSVersion.Version.Major == 10) { + if (Environment.OSVersion.Version.Major == 10) + { yield return "-Dos.name=\"Windows 10\""; yield return "-Dos.version=10.0"; } break; + case "osx": yield return "-XstartOnFirstThread"; break; diff --git a/MinecraftLaunch/Components/Parser/LauncherProfileParser.cs b/MinecraftLaunch/Components/Parser/LauncherProfileParser.cs index 65063e8..9108fe4 100644 --- a/MinecraftLaunch/Components/Parser/LauncherProfileParser.cs +++ b/MinecraftLaunch/Components/Parser/LauncherProfileParser.cs @@ -13,18 +13,21 @@ namespace MinecraftLaunch.Components.Parser; /// /// 取自 launcher_profile.json /// -public sealed class DefaultLauncherProfileParser : IDataProcessor { +public sealed class DefaultLauncherProfileParser : IDataProcessor +{ private readonly Guid _clientToken; private string _filePath = string.Empty; private LauncherProfileEntry _launcherProfile = new(); public Dictionary Datas { get; set; } = []; - public DefaultLauncherProfileParser(Guid clientToken = default) { + public DefaultLauncherProfileParser(Guid clientToken = default) + { _clientToken = clientToken; } - public void Handle(IEnumerable minecrafts) { + public void Handle(IEnumerable minecrafts) + { Datas.Clear(); var mcList = minecrafts as IList ?? [.. minecrafts]; @@ -33,23 +36,30 @@ public void Handle(IEnumerable minecrafts) { _filePath = Path.Combine(mcList[0].MinecraftFolderPath, "launcher_profiles.json"); - if (File.Exists(_filePath)) { + if (File.Exists(_filePath)) + { var launcherProfileJson = File.ReadAllText(_filePath, Encoding.UTF8); _launcherProfile = launcherProfileJson.Deserialize(new LauncherProfileEntryContext(JsonSerializerUtil .GetDefaultOptions()).LauncherProfileEntry) ?? new LauncherProfileEntry(); - } else { - _launcherProfile = new LauncherProfileEntry { + } + else + { + _launcherProfile = new LauncherProfileEntry + { Profiles = [], ClientToken = _clientToken.ToString("N"), - LauncherVersion = new LauncherVersionEntry { + LauncherVersion = new LauncherVersionEntry + { Format = 6, Name = "MinecraftLaunch" } }; } - foreach (var minecraft in mcList) { - _launcherProfile.Profiles.TryAdd(minecraft.Id, new GameProfileEntry { + foreach (var minecraft in mcList) + { + _launcherProfile.Profiles.TryAdd(minecraft.Id, new GameProfileEntry + { Type = "custom", Name = minecraft.Id, Created = DateTime.Now, @@ -62,7 +72,8 @@ public void Handle(IEnumerable minecrafts) { Datas = _launcherProfile.Profiles.ToDictionary(x => x.Key, x1 => x1.Value as object); } - public Task SaveAsync(CancellationToken cancellationToken = default) { + public Task SaveAsync(CancellationToken cancellationToken = default) + { _launcherProfile.Profiles = Datas.ToDictionary(x => x.Key, x1 => x1.Value as GameProfileEntry); var json = _launcherProfile.Serialize( new LauncherProfileEntryContext(JsonSerializerUtil.GetDefaultOptions()).LauncherProfileEntry diff --git a/MinecraftLaunch/Components/Parser/MinecraftLoggingParser.cs b/MinecraftLaunch/Components/Parser/MinecraftLoggingParser.cs index e15e393..1a9f930 100644 --- a/MinecraftLaunch/Components/Parser/MinecraftLoggingParser.cs +++ b/MinecraftLaunch/Components/Parser/MinecraftLoggingParser.cs @@ -4,13 +4,16 @@ namespace MinecraftLaunch.Components.Parser; -public static partial class MinecraftLoggingParser { - public static MinecraftLogEntry Parse(string log) => new() { +public static partial class MinecraftLoggingParser +{ + public static MinecraftLogEntry Parse(string log) => new() + { SourceText = log, Log = GetLog(log), Source = GetSource(log), Time = TimeRegex().IsMatch(log) ? GetLogTime(log) : DateTime.Now.ToString("T"), - LogLevel = GetLogType(log) switch { + LogLevel = GetLogType(log) switch + { "FATAL" => MinecraftLogLevel.Fatal, "ERROR" => MinecraftLogLevel.Error, "WARN" => MinecraftLogLevel.Warning, @@ -22,7 +25,8 @@ public static partial class MinecraftLoggingParser { }, }; - public static string GetLog(string log) { + public static string GetLog(string log) + { var res = GetTotalPrefix(log); var s = log.Split(res); return (s.Length >= 2 ? s[1] : log).Trim(); @@ -33,14 +37,17 @@ public static string GetLog(string log) { /// /// /// - public static string GetLogType(string log) { + public static string GetLogType(string log) + { //是否是堆栈信息 - if (StackTraceRegex().IsMatch(log)) { + if (StackTraceRegex().IsMatch(log)) + { return "STACK"; } //是否是异常信息 - if (ExceptionRegex().IsMatch(log)) { + if (ExceptionRegex().IsMatch(log)) + { return "Exception"; } @@ -52,7 +59,8 @@ public static string GetLogType(string log) { /// /// /// - public static string GetSource(string log) { + public static string GetSource(string log) + { var content = SourceRegex() .Match(log) .Value @@ -73,7 +81,8 @@ public static string GetSource(string log) { /// /// /// - public static string GetLogTime(string log) { + public static string GetLogTime(string log) + { return TimeRegex().Match(log).Value; } @@ -82,7 +91,8 @@ public static string GetLogTime(string log) { /// /// /// - public static string GetTotalPrefix(string log) { + public static string GetTotalPrefix(string log) + { return TotalPrefixRegex().Match(log).Value; } @@ -109,5 +119,5 @@ public static string GetTotalPrefix(string log) { [GeneratedRegex("\\[(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d\\] \\[[\\w\\W\\s]{2,}/(FATAL|ERROR|WARN|INFO|DEBUG)\\]")] private static partial Regex TotalPrefixRegex(); - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Components/Parser/MinecraftParser.cs b/MinecraftLaunch/Components/Parser/MinecraftParser.cs index 8c7c29c..c0f28d9 100644 --- a/MinecraftLaunch/Components/Parser/MinecraftParser.cs +++ b/MinecraftLaunch/Components/Parser/MinecraftParser.cs @@ -14,7 +14,8 @@ namespace MinecraftLaunch.Components.Parser; string MinecraftFolderPath, string ClientJsonPath); -public sealed class MinecraftParser { +public sealed class MinecraftParser +{ private static readonly FrozenDictionary)> _modLoaderLibs = new Dictionary)>() { { "net.minecraftforge:forge:", (ModLoaderType.Forge, libVersion => libVersion.Split('-')[1]) }, { "net.minecraftforge:fmlloader:", (ModLoaderType.Forge, libVersion => libVersion.Split('-')[1]) }, @@ -28,45 +29,55 @@ public sealed class MinecraftParser { public DirectoryInfo Root { set; get; } public static Dictionary DataProcessors { get; } = []; - public MinecraftParser(string root) { + public MinecraftParser(string root) + { Root = new(root); } - public static implicit operator MinecraftParser(string minecraftRootPath) { + public static implicit operator MinecraftParser(string minecraftRootPath) + { return new(minecraftRootPath); } - public static implicit operator string(MinecraftParser resolver) { + public static implicit operator string(MinecraftParser resolver) + { return resolver.Root.FullName; } - public MinecraftEntry GetMinecraft(string id) { + public MinecraftEntry GetMinecraft(string id) + { var versionDirectory = new DirectoryInfo(Path.Combine(Root.FullName, "versions", id)); return Parse(versionDirectory, null, out var _); } - public List GetMinecrafts() { + public List GetMinecrafts() + { var list = new List(); var versionsDirectory = new DirectoryInfo(Path.Combine(Root.FullName, "versions")); if (!versionsDirectory.Exists) return []; - foreach (DirectoryInfo dir in versionsDirectory.EnumerateDirectories()) { - try { + foreach (DirectoryInfo dir in versionsDirectory.EnumerateDirectories()) + { + try + { var entry = Parse(dir, list, out bool inheritedInstanceAlreadyFound); int index = list.FindIndex(i => i.Id == entry.Id); - if (index != -1) { + if (index != -1) + { list.RemoveAt(index); } list.Add(entry); if (entry is ModifiedMinecraftEntry m && m.HasInheritance && !inheritedInstanceAlreadyFound) list.Add(m.InheritedMinecraft); - } catch (Exception) { } + } + catch (Exception) { } } - foreach (var processor in DataProcessors.Values) { + foreach (var processor in DataProcessors.Values) + { processor.Handle(list); _ = processor.SaveAsync(); } @@ -74,7 +85,8 @@ public List GetMinecrafts() { return list; } - internal static MinecraftEntry Parse(DirectoryInfo clientDir, IEnumerable parsedInstances, out bool foundInheritedInstanceInParsed) { + internal static MinecraftEntry Parse(DirectoryInfo clientDir, IEnumerable parsedInstances, out bool foundInheritedInstanceInParsed) + { foundInheritedInstanceInParsed = false; if (!clientDir.Exists) @@ -110,7 +122,8 @@ internal static MinecraftEntry Parse(DirectoryInfo clientDir, IEnumerable(); - } else if (clientJsonNode["clientVersion"] is JsonNode pclClientVersionNode) { + } + else if (clientJsonNode["clientVersion"] is JsonNode pclClientVersionNode) + { versionId = pclClientVersionNode.GetValue(); } if (versionId is null) throw new FormatException(); - } catch (Exception e) when (e is InvalidOperationException || e is FormatException) { + } + catch (Exception e) when (e is InvalidOperationException || e is FormatException) + { throw new FormatException("Failed to parse version id"); } return versionId; } - private static VanillaMinecraftEntry ParseVanilla(PartialData partialData, MinecraftJsonEntry gameJsonEntry, JsonNode clientJsonNode) { + private static VanillaMinecraftEntry ParseVanilla(PartialData partialData, MinecraftJsonEntry gameJsonEntry, JsonNode clientJsonNode) + { // Check if client.jar exists string clientJarPath = partialData.ClientJsonPath[..^"json".Length] + "jar"; @@ -168,7 +189,8 @@ private static VanillaMinecraftEntry ParseVanilla(PartialData partialData, Minec ?? throw new InvalidDataException("Asset index ID does not exist in client.json"); string assetIndexJsonPath = Path.Combine(partialData.MinecraftFolderPath, "assets", "indexes", $"{assetIndexId}.json"); - return new VanillaMinecraftEntry { + return new VanillaMinecraftEntry + { Version = version, ClientJarPath = clientJarPath, Id = partialData.VersionFolderName, @@ -181,12 +203,14 @@ private static VanillaMinecraftEntry ParseVanilla(PartialData partialData, Minec private static ModifiedMinecraftEntry ParseModified(PartialData partialData, MinecraftJsonEntry minecraftJsonEntry, JsonNode clientJsonNode, IEnumerable minecraftEntries, - out bool foundInheritedInstanceInParsed) { + out bool foundInheritedInstanceInParsed) + { foundInheritedInstanceInParsed = false; bool hasInheritance = !string.IsNullOrEmpty(minecraftJsonEntry.InheritsFrom); VanillaMinecraftEntry inheritedEntry = null!; - if (hasInheritance) { + if (hasInheritance) + { // Find the inherited instance string inheritedInstanceId = minecraftJsonEntry.InheritsFrom ?? throw new InvalidOperationException("InheritsFrom is not defined in client.json"); @@ -195,9 +219,12 @@ private static ModifiedMinecraftEntry ParseModified(PartialData partialData, Min .Where(i => i is VanillaMinecraftEntry v && v.Version.VersionId == inheritedInstanceId) .FirstOrDefault() as VanillaMinecraftEntry; - if (inheritedEntry is not null) { + if (inheritedEntry is not null) + { foundInheritedInstanceInParsed = true; - } else { + } + else + { string inheritedInstancePath = Path.Combine(partialData.MinecraftFolderPath, "versions", inheritedInstanceId); var inheritedInstanceDir = new DirectoryInfo(inheritedInstancePath); @@ -219,11 +246,13 @@ private static ModifiedMinecraftEntry ParseModified(PartialData partialData, Min // Parse version MinecraftVersion? version; - if (hasInheritance) { + if (hasInheritance) + { // Use version from the inherited instance version = inheritedEntry.Version; - - } else { + } + else + { // Read from client.json string versionId = ReadVersionIdFromNonInheritingClientJson(minecraftJsonEntry, clientJsonNode); version = MinecraftVersion.Parse(versionId); @@ -232,18 +261,21 @@ private static ModifiedMinecraftEntry ParseModified(PartialData partialData, Min // Parse mod loaders List modLoaders = []; var libraries = minecraftJsonEntry.Libraries ?? []; - foreach (var lib in libraries) { + foreach (var lib in libraries) + { string libNameLowered = lib.GetString("name")?.ToLower(); if (libNameLowered is null) continue; - foreach (var key in _modLoaderLibs.Keys) { + foreach (var key in _modLoaderLibs.Keys) + { if (!libNameLowered.Contains(key)) continue; // Mod loader library detected var id = libNameLowered.Split(':')[2]; - var loader = new ModLoaderInfo { + var loader = new ModLoaderInfo + { Type = _modLoaderLibs[key].Item1, Version = _modLoaderLibs[key].Item2(id) }; @@ -259,7 +291,8 @@ private static ModifiedMinecraftEntry ParseModified(PartialData partialData, Min ? inheritedEntry.ReleaseTime : minecraftJsonEntry.ReleaseTime; - return new ModifiedMinecraftEntry { + return new ModifiedMinecraftEntry + { ReleaseTime = releaseTime, Id = partialData.VersionFolderName, Version = (MinecraftVersion)version, diff --git a/MinecraftLaunch/Components/Parser/NbtParser.cs b/MinecraftLaunch/Components/Parser/NbtParser.cs index 5d11147..ef953b3 100644 --- a/MinecraftLaunch/Components/Parser/NbtParser.cs +++ b/MinecraftLaunch/Components/Parser/NbtParser.cs @@ -5,22 +5,27 @@ namespace MinecraftLaunch.Components.Parser; -public sealed class NbtParser : INbtParser { +public sealed class NbtParser : INbtParser +{ private NbtReader _reader; private readonly string _nbtFilePath; private readonly MinecraftEntry _entry; - internal NbtParser(string nbtFile) { + internal NbtParser(string nbtFile) + { _nbtFilePath = nbtFile; } - internal NbtParser(MinecraftEntry minecraftEntry) { + internal NbtParser(MinecraftEntry minecraftEntry) + { _entry = minecraftEntry; } - public NbtReader GetReader(NbtCompression compression = NbtCompression.None) { - if (string.IsNullOrEmpty(_nbtFilePath)) { + public NbtReader GetReader(NbtCompression compression = NbtCompression.None) + { + if (string.IsNullOrEmpty(_nbtFilePath)) + { throw new ArgumentNullException(nameof(_nbtFilePath)); } @@ -28,8 +33,10 @@ public NbtReader GetReader(NbtCompression compression = NbtCompression.None) { return new NbtReader(fileStream, compression, true); } - public NbtWriter GetWriter(NbtCompression compression = NbtCompression.None) { - if (string.IsNullOrEmpty(_nbtFilePath)) { + public NbtWriter GetWriter(NbtCompression compression = NbtCompression.None) + { + if (string.IsNullOrEmpty(_nbtFilePath)) + { throw new ArgumentNullException(nameof(_nbtFilePath)); } @@ -37,17 +44,20 @@ public NbtWriter GetWriter(NbtCompression compression = NbtCompression.None) { return new NbtWriter(fileStream, compression, false); } - public async Task ParseSaveAsync(string saveName, bool @bool = true, CancellationToken cancellationToken = default) { + public async Task ParseSaveAsync(string saveName, bool @bool = true, CancellationToken cancellationToken = default) + { if (_entry is null) throw new InvalidOperationException("Initialization error"); var saveFolder = Path.Combine(_entry.ToWorkingPath(@bool), "saves", saveName); - var saveEntry = new SaveEntry { + var saveEntry = new SaveEntry + { FolderName = new DirectoryInfo(saveFolder).Name, Folder = saveFolder }; - await Task.Run(() => { + await Task.Run(() => + { var time = DateTime.Now; using var fileStream = new FileStream(Path.Combine(saveFolder, "level.dat"), FileMode.Open, FileAccess.Read); @@ -70,7 +80,6 @@ await Task.Run(() => { if (File.Exists(Path.Combine(saveFolder, "icon.png"))) saveEntry.IconFilePath = Path.Combine(saveFolder, "icon.png"); - }, cancellationToken); return saveEntry; diff --git a/MinecraftLaunch/Components/Provider/CurseforgeProvider.cs b/MinecraftLaunch/Components/Provider/CurseforgeProvider.cs index 8f16287..4cb2c27 100644 --- a/MinecraftLaunch/Components/Provider/CurseforgeProvider.cs +++ b/MinecraftLaunch/Components/Provider/CurseforgeProvider.cs @@ -12,10 +12,12 @@ namespace MinecraftLaunch.Components.Provider; -public sealed class CurseforgeProvider { - public readonly static string CurseforgeApi = "https://api.curseforge.com/v1"; +public sealed class CurseforgeProvider +{ + public static readonly string CurseforgeApi = "https://api.curseforge.com/v1"; - public async Task>> GetResourceFilesByFingerprintsAsync(uint[] modFingerprints, CancellationToken cancellationToken = default) { + public async Task>> GetResourceFilesByFingerprintsAsync(uint[] modFingerprints, CancellationToken cancellationToken = default) + { var request = CreateRequest("fingerprints", "432"); var payload = new CurseforgeFingerprintsRequestPayload(modFingerprints); @@ -35,7 +37,35 @@ public async Task x1.GetEnumerable("latestFiles").Select(ParseFile)); } - public async Task> GetResourcesByModIdsAsync(IEnumerable modIds, CancellationToken cancellationToken = default) { + public async Task> GetResourceFilesByModIdAsync(int modId, int pageSize = 50, int delayBetweenRequests = 50, int maxRequests = 16, CancellationToken cancellationToken = default) + { + if (pageSize <= 0 || pageSize > 50) + { + throw new ArgumentException("pageSize must be between 1 and 50", nameof(pageSize)); + } + + var allFiles = new List(); + + var (firstPageFiles, totalCount) = await GetFirstPageWithTotalCountAsync(modId, pageSize, cancellationToken); + + if (firstPageFiles != null) + { + allFiles.AddRange(firstPageFiles); + } + + if (totalCount <= pageSize) + { + return allFiles; + } + int totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + + await ProcessRemainingPagesAsync(modId, totalPages, delayBetweenRequests, allFiles, cancellationToken, pageSize, maxRequests); + + return allFiles; + } + + public async Task> GetResourcesByModIdsAsync(IEnumerable modIds, CancellationToken cancellationToken = default) + { var request = CreateRequest("mods"); var payload = new CurseforgeResourcesRequestPayload([.. modIds]); @@ -47,10 +77,11 @@ public async Task> GetResourcesByModIdsAsync(IEn var jsonNode = json.AsNode() .Select("data"); - return jsonNode.GetEnumerable().Select(Parse); + return jsonNode.GetEnumerable().Select(ParseResource); } - public async Task> GetFeaturedResourcesAsync(CancellationToken cancellationToken = default) { + public async Task> GetFeaturedResourcesAsync(CancellationToken cancellationToken = default) + { var request = CreateRequest("mods", "featured"); var payload = new CurseforgeFeaturedRequestPayload(432, [0]); @@ -71,19 +102,22 @@ public async Task> GetFeaturedResourcesAsync(Can else return []; - return resources.Select(Parse); + return resources.Select(ParseResource); } - public async Task> SearchResourcesAsync( + /* + public async Task SearchResourcesAsync( string searchFilter, int classId = 6, int category = 0, string gameVersion = null, ModLoaderType modLoaderType = ModLoaderType.Any, - CancellationToken cancellationToken = default) { + CancellationToken cancellationToken = default) + { var url = new Url(CurseforgeApi) .AppendPathSegment("mods/search") - .SetQueryParams(new { + .SetQueryParams(new + { gameId = 432, sortField = "Featured", sortOrder = "desc", @@ -100,24 +134,38 @@ public async Task> SearchResourcesAsync( var jsonNode = json.AsNode(); if (jsonNode == null) - return []; + return null; - return jsonNode.GetEnumerable("data").Select(Parse); + return ParseResult(jsonNode); } + */ - public async Task> SearchResourcesAsync( - CurseforgeSearchOptions searchOptions, - CancellationToken cancellationToken = default) { + public async Task> GetCategoriesAsync(CancellationToken cancellationToken = default) + { + using var reponseMessage = await CreateRequest("categories").GetAsync(cancellationToken: cancellationToken); + var json = await reponseMessage.GetStringAsync(); + var jsonNode = json.AsNode() + .Select("data"); + + return jsonNode.GetEnumerable().Select(ParseCategory); + } + public async Task SearchResourcesAsync( + CurseforgeSearchOptions searchOptions, + CancellationToken cancellationToken = default) + { var url = new Url(CurseforgeApi) .AppendPathSegment("mods/search") - .SetQueryParams(new { + .SetQueryParams(new + { gameId = 432, sortOrder = searchOptions.SortOrder is SortOrder.Desc ? "desc" : "asc", categoryId = searchOptions.CategoryId, sortField = searchOptions.SortField, - classId = searchOptions.ClassId, + classId = (int)searchOptions.ClassId, gameVersion = searchOptions.GameVersion, + pageSize = searchOptions.PageSize, + index = searchOptions.Index, searchFilter = HttpUtility.UrlEncode(searchOptions.SearchFilter) }); @@ -129,38 +177,129 @@ public async Task> SearchResourcesAsync( var jsonNode = json.AsNode(); if (jsonNode == null) - return []; + return null; - return jsonNode.GetEnumerable("data").Select(Parse); + return ParseResult(jsonNode); } #region Private and internals - internal static async Task GetModFileEntryAsync(long modId, long fileId, CancellationToken cancellationToken = default) { + internal async Task<(IEnumerable files, int totalCount)> GetFirstPageWithTotalCountAsync(int modId, int pageSize, CancellationToken cancellationToken) + { + var url = new Url(CurseforgeApi) + .AppendPathSegments("mods", modId.ToString(), "files") + .SetQueryParams(new + { + index = 0, + pageSize + }); + + var request = CreateRequest(url); + var response = await request.GetStringAsync(cancellationToken: cancellationToken); + + var jsonNode = response.AsNode(); + + // ȡҳϢ + var paginationNode = jsonNode.Select("pagination"); + int totalCount = paginationNode?.GetInt32("totalCount") ?? 0; + + // ȡļ + var dataNode = jsonNode.Select("data"); + IEnumerable files = []; + + if (dataNode != null) + { + var fileNodes = dataNode.GetEnumerable(); + if (fileNodes != null) + { + files = fileNodes.Select(ParseFile); + } + } + + return (files, totalCount); + } + + internal async Task ProcessRemainingPagesAsync(int modId, int totalPages, int delayBetweenRequests, List allFiles, CancellationToken cancellationToken = default, int pageSize = 50, int maxRequests = 16) + { + // ӵڶҳʼһҳѾȡ + var remainingPages = Enumerable.Range(1, totalPages - 1); + + // ʹ SemaphoreSlim Ʋ + var semaphore = new SemaphoreSlim(maxRequests); // ͬʱ8 + + var tasks = remainingPages.Select(async pageIndex => + { + await semaphore.WaitAsync(cancellationToken); + // ӳ + if (delayBetweenRequests > 0) + { + await Task.Delay(delayBetweenRequests, cancellationToken); + } + + var pageFiles = await GetResourceFilesPageAsync(modId, pageIndex * pageSize, pageSize, cancellationToken); + + if (pageFiles != null && pageFiles.Any()) + { + lock (allFiles) + { + allFiles.AddRange(pageFiles); + } + } + semaphore.Release(); + }); + + await Task.WhenAll(tasks); + } + + internal async Task> GetResourceFilesPageAsync(int modId, int pageIndex, int pageSize, CancellationToken cancellationToken, int maxRetries = 3) + { + var url = new Url(CurseforgeApi) + .AppendPathSegments("mods", modId.ToString(), "files") + .SetQueryParams(new + { + index = pageIndex, + pageSize + }); + + var request = CreateRequest(url); + var response = await request.GetStringAsync(cancellationToken: cancellationToken); + + var jsonNode = response.AsNode().Select("data").GetEnumerable(); + return jsonNode.Select(ParseFile); + } + + internal static async Task GetModFileEntryAsync(long modId, long fileId, CancellationToken cancellationToken = default) + { CheckApiKey(); string json = string.Empty; - try { + try + { using var responseMessage = await CreateRequest("mods", "files", $"{fileId}") .GetAsync(cancellationToken: cancellationToken); ; json = await responseMessage.GetStringAsync(); - } catch (Exception) { } + } + catch (Exception) { } return json?.AsNode()?.Select("data") ?? throw new InvalidModpackFileException(); } - internal static async Task GetModDownloadUrlAsync(long modId, long fileId, CancellationToken cancellationToken = default) { + internal static async Task GetModDownloadUrlAsync(long modId, long fileId, CancellationToken cancellationToken = default) + { CheckApiKey(); string json = string.Empty; - try { + try + { using var responseMessage = await CreateRequest("mods", $"{modId}", "files", $"{fileId}", "download-url") .GetAsync(cancellationToken: cancellationToken); json = await responseMessage.GetStringAsync(); - } catch (FlurlHttpException ex) { + } + catch (FlurlHttpException ex) + { if (ex.StatusCode is 403) return string.Empty; } @@ -169,7 +308,8 @@ internal static async Task GetModDownloadUrlAsync(long modId, long fileI ?? throw new InvalidModpackFileException(); } - internal static async Task TestDownloadUrlAsync(long fileId, string fileName, CancellationToken cancellationToken = default) { + internal static async Task TestDownloadUrlAsync(long fileId, string fileName, CancellationToken cancellationToken = default) + { CheckApiKey(); var fileIdStr = fileId.ToString(); @@ -178,8 +318,10 @@ internal static async Task TestDownloadUrlAsync(long fileId, string file $"https://mediafiles.forgecdn.net/files/{fileIdStr[..4]}/{fileIdStr[4..]}/{fileName}" ]; - try { - foreach (var url in urls) { + try + { + foreach (var url in urls) + { var response = await HttpUtil.Request(url) .HeadAsync(cancellationToken: cancellationToken); @@ -188,17 +330,21 @@ internal static async Task TestDownloadUrlAsync(long fileId, string file return url; } - } catch (Exception) { } + } + catch (Exception) { } throw new InvalidOperationException(); } - private static CurseforgeResource Parse(JsonNode node) { - return new CurseforgeResource { + private static CurseforgeResource ParseResource(JsonNode node) + { + return new CurseforgeResource + { Id = node.GetInt32("id"), ClassId = node.GetInt32("classId"), DownloadCount = node.GetInt32("downloadCount"), Name = node.GetString("name"), + Slug = node.GetString("slug"), Summary = node.GetString("summary"), DateModified = node.GetDateTime("dateModified"), IconUrl = node.Select("logo").GetString("thumbnailUrl"), @@ -207,58 +353,114 @@ private static CurseforgeResource Parse(JsonNode node) { Categories = node.GetEnumerable("categories", "name"), Screenshots = node.GetEnumerable("screenshots", "url"), LatestFiles = node.GetEnumerable("latestFiles").Select(ParseFile), - MinecraftVersions = node.GetEnumerable("latestFilesIndexes", "gameVersion").Distinct() + MinecraftVersions = node.GetEnumerable("latestFilesIndexes", "gameVersion").Distinct(), + Loaders = node.GetEnumerable("latestFilesIndexes", "modLoader").Distinct().Select(x => (ModLoaderType)x) }; + } + private static CurseForgeSearchResult ParseResult(JsonNode node) + { + var pagination = node.Select("pagination"); + var data = node.Select("data"); + return new CurseForgeSearchResult + { + Index = pagination.GetInt32("index"), + PageSize = pagination.GetInt32("pageSize"), + TotalCount = pagination.GetInt64("totalCount").Value, + Resources = data.GetEnumerable().Select(ParseResource) + }; } - private static CurseforgeResourceFile ParseFile(JsonNode node) { - if (node is null) - return null; + private static CurseforgeCategoryEntry ParseCategory(JsonNode node) + { + return new CurseforgeCategoryEntry() + { + Id = node.GetInt32("id"), + Name = node.GetString("name"), + ClassId = (ClassId)node.GetInt32("classId") + }; + } - return new CurseforgeResourceFile { + private static CurseforgeResourceFile ParseFile(JsonNode node) + { + var gGameVersions = node.GetEnumerable("gameVersions").Where(x => x != "Client" && x != "Server"); + List loaders = []; + List gameVersions = []; + foreach (var ver in gGameVersions) + { + if (Enum.TryParse(ver, out var loader)) + { + loaders.Add(loader); + } + else + { + gameVersions.Add(ver); + } + } + return node is null ? null : new CurseforgeResourceFile + { Id = node.GetInt32("id"), ModId = node.GetInt32("modId"), + GameId = node.GetInt32("gameId"), FileName = node.GetString("fileName"), Published = node.GetDateTime("fileDate"), IsAvailable = node.GetBool("isAvailable"), - ReleaseType = node.GetInt32("releaseType"), DisplayName = node.GetString("displayName"), + IsServerPack = node.GetBool("isServerPack"), DownloadUrl = node.GetString("downloadUrl"), + DownloadCount = node.GetInt32("downloadCount"), + AlternateFileId = node.GetInt32("alternateFileId"), FileFingerprint = node.GetUInt32("fileFingerprint"), - MinecraftVersions = node.GetEnumerable("gameVersions") + GameVersions = gameVersions, + Loaders = loaders, + IsApproved = node.GetInt32("fileStatus") is 4, + FileSize = node.GetInt64("fileLength").Value, + ReleaseType = (FileReleaseType)node.GetInt32("releaseType"), + Sha1 = node.GetEnumerable("hashes").FirstOrDefault(x => x.GetInt32("algo") == 1)?.GetString("value"), + Dependencies = node.GetEnumerable("dependencies").DistinctBy(x => x.GetInt32("modId")).ToDictionary(x => x.GetInt32("modId"), x => (DependencyType)x.GetInt32("relationType")) }; } - private static IFlurlRequest CreateRequest(Url url) { + private static IFlurlRequest CreateRequest(Url url) + { CheckApiKey(); return HttpUtil.Request(url) .WithHeader("x-api-key", DownloadManager.CurseforgeApiKey); } - private static IFlurlRequest CreateRequest(params string[] path) { + private static IFlurlRequest CreateRequest(params string[] path) + { CheckApiKey(); return HttpUtil.Request(CurseforgeApi, path) .WithHeader("x-api-key", DownloadManager.CurseforgeApiKey); } - private static void CheckApiKey() { + private static void CheckApiKey() + { if (string.IsNullOrWhiteSpace(DownloadManager.CurseforgeApiKey)) throw new InvalidOperationException("Curseforge API key is not set."); } - #endregion + #endregion Private and internals } [Serializable] -public class InvalidModpackFileException : Exception { +public class InvalidModpackFileException : Exception +{ public long ProjectId { get; set; } - public InvalidModpackFileException() { } - public InvalidModpackFileException(string message) : base(message) { } - public InvalidModpackFileException(string message, Exception inner) : base(message, inner) { } + public InvalidModpackFileException() + { } + + public InvalidModpackFileException(string message) : base(message) + { + } + + public InvalidModpackFileException(string message, Exception inner) : base(message, inner) + { + } } internal record CurseforgeResourcesRequestPayload(long[] modIds); diff --git a/MinecraftLaunch/Components/Provider/ModrinthProvider.cs b/MinecraftLaunch/Components/Provider/ModrinthProvider.cs index 007351a..75873a4 100644 --- a/MinecraftLaunch/Components/Provider/ModrinthProvider.cs +++ b/MinecraftLaunch/Components/Provider/ModrinthProvider.cs @@ -4,7 +4,6 @@ using MinecraftLaunch.Base.Models.Network; using MinecraftLaunch.Extensions; using MinecraftLaunch.Utilities; -using System.Linq; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -12,15 +11,17 @@ namespace MinecraftLaunch.Components.Provider; -public sealed class ModrinthProvider { +public sealed class ModrinthProvider +{ public readonly string ModrinthApi = "https://api.modrinth.com/v2"; - public async Task> GetModFilesBySha1Async( + public async Task> GetModFilesByHashAsync( string[] hashes, string version, ModLoaderType modLoaderType, HashType type = HashType.SHA1, - CancellationToken cancellationToken = default) { + CancellationToken cancellationToken = default) + { var url = new Url(ModrinthApi) .AppendPathSegments("version_files", "update"); @@ -46,16 +47,46 @@ public async Task> GetModFilesBySha1Async( .Where(x => x is not null); } - public async Task SearchByProjectIdAsync(string projectId, CancellationToken cancellationToken = default) { + public async Task> GetFeaturedResourcesAsync(CancellationToken cancellationToken = default) + { + var request = HttpUtil.Request(ModrinthApi, "search"); + + var json = await request.GetStringAsync(cancellationToken: cancellationToken); + var jsonNode = json.AsNode(); + + if (jsonNode is null) + return []; + + return jsonNode.GetEnumerable("hits").Select(x => ParseResource(x)); + } + + public async Task> GetModFilesByProjectIdAsync(string projectId, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(projectId); + + var request = HttpUtil.Request(ModrinthApi, "project", projectId, "version"); + + var json = await request.GetStringAsync(cancellationToken: cancellationToken); + var jsonArray = json.AsNode().AsArray(); + + if (jsonArray is null) + return null; + + return jsonArray.Select(ParseFile); + } + + public async Task SearchByProjectIdAsync(string projectId, CancellationToken cancellationToken = default) + { var url = new Url(ModrinthApi) .AppendPathSegments("project", projectId); var request = HttpUtil.Request(url); var responseMessage = await request.GetStringAsync(cancellationToken: cancellationToken); - return Parse(responseMessage.AsNode()); + return ParseResource(responseMessage.AsNode()); } - public async Task> SearchByProjectIdsAsync(IEnumerable projectIds, CancellationToken cancellationToken = default) { + public async Task> SearchByProjectIdsAsync(IEnumerable projectIds, CancellationToken cancellationToken = default) + { var idsJson = projectIds.Serialize(ModrinthProviderContext.Default.IEnumerableString); var url = new Url(ModrinthApi).AppendPathSegment("projects") @@ -65,22 +96,11 @@ public async Task> SearchByProjectIdsAsync(IEnumer var responseMessage = await request.GetStringAsync(cancellationToken: cancellationToken); var jsonNode = responseMessage.AsNode(); - return jsonNode.GetEnumerable().Select(x => Parse(x, true)); - } - - public async Task> GetFeaturedResourcesAsync(CancellationToken cancellationToken = default) { - var request = HttpUtil.Request(ModrinthApi, "search"); - - var json = await request.GetStringAsync(cancellationToken: cancellationToken); - var jsonNode = json.AsNode(); - - if (jsonNode is null) - return []; - - return jsonNode.GetEnumerable("hits").Select(x => Parse(x)); + return jsonNode.GetEnumerable().Select(x => ParseResource(x, true)); } - public async Task> SearchByUserAsync(string user, CancellationToken cancellationToken = default) { + public async Task> SearchByUserAsync(string user, CancellationToken cancellationToken = default) + { var request = HttpUtil.Request(ModrinthApi, "user", user, "projects"); var json = await request.GetStringAsync(cancellationToken: cancellationToken); @@ -89,21 +109,26 @@ public async Task> SearchByUserAsync(string user, if (jsonNode is null) return []; - return jsonNode.GetEnumerable().Select(x => { - var resource = Parse(x, true); + return jsonNode.GetEnumerable().Select(x => + { + var resource = ParseResource(x, true); resource.Author = user; return resource; }); } - public async Task> SearchAsync( + /* + public async Task SearchAsync( string searchFilter, string version = "", string category = "", string projectType = "mod", ModLoaderType modLoader = ModLoaderType.Any, ModrinthSearchIndex index = ModrinthSearchIndex.Relevance, - CancellationToken cancellationToken = default) { + int limit = 10, + int offset = 0, + CancellationToken cancellationToken = default) + { List> facetsList = [[$"project_type:{projectType}"]]; if (!string.IsNullOrEmpty(version)) @@ -114,8 +139,10 @@ public async Task> SearchAsync( if (!string.IsNullOrEmpty(category)) categories.Add($"categories:{category}"); - if (modLoader is not ModLoaderType.Any) { - var loaderCategory = modLoader switch { + if (modLoader is not ModLoaderType.Any) + { + var loaderCategory = modLoader switch + { ModLoaderType.Quilt => "quilt", ModLoaderType.Forge => "forge", ModLoaderType.Fabric => "fabric", @@ -134,17 +161,21 @@ public async Task> SearchAsync( // 构建 URL var url = new Url(ModrinthApi) .AppendPathSegment("search") - .SetQueryParams(new { + .SetQueryParams(new + { query = searchFilter, facets, - index = index switch { + index = index switch + { ModrinthSearchIndex.Follows => "follows", ModrinthSearchIndex.Downloads => "downloads", ModrinthSearchIndex.Relevance => "relevance", ModrinthSearchIndex.DateUpdated => "updated", ModrinthSearchIndex.DatePublished => "newest", _ => "relevance" - } + }, + limit, + offset }); var request = HttpUtil.Request(url); @@ -152,22 +183,117 @@ public async Task> SearchAsync( var json = await request.GetStringAsync(cancellationToken: cancellationToken); var jsonNode = json.AsNode(); - return jsonNode.GetEnumerable("hits").Select(x => Parse(x)); + return ParseResult(jsonNode); + } + */ + + public async Task SearchAsync( + ModrinthSearchOptions searchOptions, + CancellationToken cancellationToken = default) + { + List> facetsList = [[$"project_type:{searchOptions.ProjectType}"]]; + + if (!string.IsNullOrEmpty(searchOptions.Version)) + facetsList.Add([$"versions:{searchOptions.Version}"]); + + // 构建 categories + var categories = new List(); + if (!string.IsNullOrEmpty(searchOptions.Category)) + categories.Add($"categories:{searchOptions.Category}"); + + if (searchOptions.ModLoader is not ModLoaderType.Any) + { + var loaderCategory = searchOptions.ModLoader switch + { + ModLoaderType.Quilt => "quilt", + ModLoaderType.Forge => "forge", + ModLoaderType.Fabric => "fabric", + ModLoaderType.NeoForge => "neoforge", + _ => throw new ArgumentOutOfRangeException(nameof(searchOptions.ModLoader), searchOptions.ModLoader, null) + }; + + categories.Add($"categories:{loaderCategory}"); + } + + if (categories.Count > 0) + facetsList.Add(categories); + + var facets = facetsList.Serialize(ModrinthProviderContext.Default.ListListString); + + // 构建 URL + var url = new Url(ModrinthApi) + .AppendPathSegment("search") + .SetQueryParams(new + { + query = searchOptions.SearchFilter, + facets, + index = searchOptions.Index switch + { + ModrinthSearchIndex.Follows => "follows", + ModrinthSearchIndex.Downloads => "downloads", + ModrinthSearchIndex.Relevance => "relevance", + ModrinthSearchIndex.DateUpdated => "updated", + ModrinthSearchIndex.DatePublished => "newest", + _ => "relevance" + }, + limit = searchOptions.Limit, + offset = searchOptions.Offset + }); + + var request = HttpUtil.Request(url); + + var json = await request.GetStringAsync(cancellationToken: cancellationToken); + var jsonNode = json.AsNode(); + + return ParseResult(jsonNode); + } + + public async Task> GetCategories(CancellationToken cancellationToken = default) + { + var request = HttpUtil.Request(ModrinthApi, "tag", "category"); + + var json = await request.GetStringAsync(cancellationToken: cancellationToken); + var jsonNode = json.AsNode(); + + if (jsonNode is null) + return []; + + return jsonNode.GetEnumerable().Select(ParseCategory); } #region Private - private static ModrinthResource Parse(JsonNode jsonNode, bool isDetail = false) { - return new ModrinthResource { - Id = jsonNode.GetString("id"), - Slug = jsonNode.GetString("slug"), + private static ModrinthResource ParseResource(JsonNode jsonNode, bool isDetail = false) + { + var gCategories = jsonNode.GetEnumerable("categories"); + List categories = []; + List loaders = []; + foreach (var category in gCategories) + { + if (Enum.TryParse(category, true, out var loader)) + { + loaders.Add(loader); + } + else + { + categories.Add(category); + } + } + + var projectType = jsonNode.GetString("project_type"); + var slug = jsonNode.GetString("slug"); + return new ModrinthResource + { + Slug = slug, Name = jsonNode.GetString("title"), + ProjectId = jsonNode.GetString("project_id"), Author = jsonNode.GetString("author"), IconUrl = jsonNode.GetString("icon_url"), + WebsiteUrl = $"https://modrinth.com/{projectType}/{slug}", Summary = jsonNode.GetString("description"), - ProjectType = jsonNode.GetString("project_type"), + ProjectType = projectType, DownloadCount = jsonNode.GetInt32("downloads"), - Categories = jsonNode.GetEnumerable("categories"), + Categories = categories, Screenshots = isDetail ? jsonNode?.GetEnumerable("gallery", "url") : jsonNode?.GetEnumerable("gallery"), @@ -179,32 +305,91 @@ private static ModrinthResource Parse(JsonNode jsonNode, bool isDetail = false) : jsonNode.GetDateTime("updated"), DateModified = jsonNode.TryGetValue("date_created", out var published) ? published - : jsonNode.GetDateTime("published") + : jsonNode.GetDateTime("published"), + Loaders = loaders }; } - private static ModrinthResourceFiles ParseFile(JsonNode node) { - if (node == null) return null; + private static ModrinthCategoryEntry ParseCategory(JsonNode node) + { + return new() + { + Name = node.GetString("name"), + ProjectType = node.GetString("project_type") + }; + } - return new ModrinthResourceFiles { - Id = node.GetString("project_id"), - SourceHash = node.GetPropertyName(), - IsFeatured = node.GetBool("featured"), - ChangeLog = node.GetString("changelog"), - DownloadCount = node.GetInt32("downloads"), + private static ModrinthSearchResult ParseResult(JsonNode node) + { + return new ModrinthSearchResult + { + Index = node.GetInt32("offset"), + PageSize = node.GetInt32("limit"), + TotalCount = node.GetInt32("total_hits"), + Resources = node.GetEnumerable("hits").Select(x => ParseResource(x)) + }; + } + + private static ModrinthResourceFile ParseFile(JsonNode node) + { + var file = node.GetEnumerable("files"); + var primaryFileNode = file.FirstOrDefault(x => x.GetBool("primary")) ?? file.FirstOrDefault(); + + return new() + { + VersionId = node.GetString("id"), + AuthorId = node.GetString("author_id"), + ProjectId = node.GetString("project_id"), Published = node.GetDateTime("date_published"), - Files = node.GetEnumerable("files").Select(x => new ModrinthResourceFile { - FileSize = x.GetInt64("size").Value, - DownloadUrl = x.GetString("url"), - IsPrimary = x.GetBool("primary"), - FileName = x.GetString("filename"), - Sha1 = x.Select("hashes").GetString("sha1"), - Sha512 = x.Select("hashes").GetString("sha512"), - }).Where(x => x.IsPrimary) + DownloadCount = node.GetInt64("downloads").Value, + + DisplayName = node.GetString("name"), + ChangeLog = node.GetString("changelog"), + VersionNumber = node.GetString("version_number"), + GameVersions = node.GetEnumerable("game_versions"), + + DownloadUrl = primaryFileNode.GetString("url"), + IsPrimary = primaryFileNode.GetBool("primary"), + FileName = primaryFileNode.GetString("filename"), + FileSize = primaryFileNode.GetInt64("size").Value, + Sha1 = primaryFileNode.Select("hashes").GetString("sha1"), + Sha512 = primaryFileNode.Select("hashes").GetString("sha512"), + + ReleaseType = node.GetString("version_type") switch + { + "release" => FileReleaseType.Release, + "beta" => FileReleaseType.Beta, + "alpha" => FileReleaseType.Alpha, + _ => throw new NotImplementedException() + }, + + Dependencies = node.GetEnumerable("dependencies").Select(x => new ModrinthFileDependency + { + FileName = x.GetString("file_name"), + VersionId = x.GetString("version_id"), + ProjectId = x.GetString("project_id"), + Type = x.GetString("dependency_type") switch + { + "required" => DependencyType.Required, + "optional" => DependencyType.Optional, + "incompatible" => DependencyType.Incompatible, + "embedded" => DependencyType.Embedded, + _ => throw new NotImplementedException() + } + }), + + Loaders = node.GetEnumerable("loaders").Select(x => x switch + { + "fabric" => ModLoaderType.Fabric, + "forge" => ModLoaderType.Forge, + "quilt" => ModLoaderType.Quilt, + "neoforge" => ModLoaderType.NeoForge, + _ => ModLoaderType.Any + }) }; } - #endregion + #endregion Private } internal record ModrinthFilesUpdateCheckRequestPayload(string[] hashes, string[] game_versions, string[] loaders, string algorithm = "sha1"); diff --git a/MinecraftLaunch/DownloadManager.cs b/MinecraftLaunch/DownloadManager.cs index a73a337..68842a2 100644 --- a/MinecraftLaunch/DownloadManager.cs +++ b/MinecraftLaunch/DownloadManager.cs @@ -3,7 +3,8 @@ namespace MinecraftLaunch; -public static class DownloadManager { +public static class DownloadManager +{ public static string CurseforgeApiKey { get; set; } = string.Empty; public static int MaxThread { get; set; } = 64; @@ -16,7 +17,8 @@ public static class DownloadManager { public static readonly IDownloadMirror BmclApi = new BmclApiSource(); } -public sealed class BmclApiSource : IDownloadMirror { +public sealed class BmclApiSource : IDownloadMirror +{ private static readonly FrozenDictionary _replacementMap = new Dictionary { { "https://resources.download.minecraft.net", "https://bmclapi2.bangbang93.com/assets" }, { "https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com" }, @@ -30,7 +32,8 @@ public sealed class BmclApiSource : IDownloadMirror { { "https://maven.neoforged.net/releases/net/neoforged/forge", "https://bmclapi2.bangbang93.com/maven/net/neoforged/forge" } }.ToFrozenDictionary(); - public string TryFindUrl(string sourceUrl) { + public string TryFindUrl(string sourceUrl) + { if (!DownloadManager.IsEnableMirror) return sourceUrl; diff --git a/MinecraftLaunch/Extensions/DirectoryExtension.cs b/MinecraftLaunch/Extensions/DirectoryExtension.cs index d87d1e9..25da867 100644 --- a/MinecraftLaunch/Extensions/DirectoryExtension.cs +++ b/MinecraftLaunch/Extensions/DirectoryExtension.cs @@ -1,7 +1,9 @@ namespace MinecraftLaunch.Extensions; -public static class DirectoryExtension { - public static IEnumerable FindAll(this DirectoryInfo directory, string file) { +public static class DirectoryExtension +{ + public static IEnumerable FindAll(this DirectoryInfo directory, string file) + { foreach (var item in directory.EnumerateFiles()) if (item.Name == file) yield return item; @@ -10,4 +12,4 @@ public static IEnumerable FindAll(this DirectoryInfo directory, string foreach (var info in item.FindAll(file)) yield return info; } -} +} \ No newline at end of file diff --git a/MinecraftLaunch/Extensions/JsonNodeExtension.cs b/MinecraftLaunch/Extensions/JsonNodeExtension.cs index 9d7210a..84a47bf 100644 --- a/MinecraftLaunch/Extensions/JsonNodeExtension.cs +++ b/MinecraftLaunch/Extensions/JsonNodeExtension.cs @@ -5,12 +5,20 @@ namespace MinecraftLaunch.Extensions; -public static partial class JsonNodeExtension { +public static partial class JsonNodeExtension +{ + public static TValue? GetValueOrDefault(this JsonNode node, string propertyName) where TValue : struct + { + _ = node.TryGetValue(propertyName, out var value); + return value; + } + public static string FixJson(this string errorJson) => errorJson .FixJsonStringNewlines() .FixDuplicateEmptyKeys(); - public static string FixJsonStringNewlines(this string json) => JsonFixRegex().Replace(json, match => { + public static string FixJsonStringNewlines(this string json) => JsonFixRegex().Replace(json, match => + { var value = match.Groups[1].Value; if (JsonFieldRegex().IsMatch(match.Value) || !value.Contains('\n') && !value.Contains('\r')) return match.Value; @@ -25,10 +33,13 @@ public static string FixJsonStringNewlines(this string json) => JsonFixRegex().R return $"\"{fixedValue}\""; }); - public static string FixDuplicateEmptyKeys(this string json) { + public static string FixDuplicateEmptyKeys(this string json) + { bool firstFound = false; - return JsonDuplicateEmptyKeysRegex().Replace(json, match => { - if (!firstFound) { + return JsonDuplicateEmptyKeysRegex().Replace(json, match => + { + if (!firstFound) + { firstFound = true; return match.Value; } @@ -37,27 +48,33 @@ public static string FixDuplicateEmptyKeys(this string json) { }); } - public static string Serialize(this T value, JsonTypeInfo jsonType) { + public static string Serialize(this T value, JsonTypeInfo jsonType) + { return JsonSerializer.Serialize(value, jsonType); } - public static T Deserialize(this string json, JsonTypeInfo jsonType) { + public static T Deserialize(this string json, JsonTypeInfo jsonType) + { return JsonSerializer.Deserialize(json, jsonType); } - public static JsonNode AsNode(this string json) { + public static JsonNode AsNode(this string json) + { return JsonNode.Parse(json); } - public static JsonArray AsArray(this IEnumerable jsonNodes) { + public static JsonArray AsArray(this IEnumerable jsonNodes) + { return [.. jsonNodes]; } - public static JsonNode Select(this JsonNode node, string name) { + public static JsonNode Select(this JsonNode node, string name) + { return node[name]; } - public static bool TryGetValue(this JsonNode node, string name, out T value) { + public static bool TryGetValue(this JsonNode node, string name, out T value) + { var cNode = node[name]; var flag = cNode is not null; @@ -65,77 +82,95 @@ public static bool TryGetValue(this JsonNode node, string name, out T value) return flag; } - public static int GetInt32(this JsonNode node) { + public static int GetInt32(this JsonNode node) + { return node.GetValue(); } - public static int GetInt32(this JsonNode node, string name) { + public static int GetInt32(this JsonNode node, string name) + { return node.Select(name).GetValue(); } - public static uint GetUInt32(this JsonNode node) { + public static uint GetUInt32(this JsonNode node) + { return node.GetValue(); } - public static uint GetUInt32(this JsonNode node, string name) { + public static uint GetUInt32(this JsonNode node, string name) + { return node.Select(name).GetValue(); } - public static long? GetInt64(this JsonNode node) { + public static long? GetInt64(this JsonNode node) + { return node?.GetValue(); } - public static long? GetInt64(this JsonNode node, string name) { + public static long? GetInt64(this JsonNode node, string name) + { return node.Select(name)?.GetValue(); } - public static bool GetBool(this JsonNode node) { + public static bool GetBool(this JsonNode node) + { return node.GetValue(); } - public static bool GetBool(this JsonNode node, string name) { + public static bool GetBool(this JsonNode node, string name) + { return node.Select(name).GetValue(); } - public static string GetString(this JsonNode node) { + public static string GetString(this JsonNode node) + { return node?.GetValue(); } - public static string GetString(this JsonNode node, string name) { + public static string GetString(this JsonNode node, string name) + { return node.Select(name)?.GetValue(); } - public static DateTime GetDateTime(this JsonNode node) { + public static DateTime GetDateTime(this JsonNode node) + { return node.GetValue(); } - public static DateTime GetDateTime(this JsonNode node, string name) { + public static DateTime GetDateTime(this JsonNode node, string name) + { return node.Select(name).GetValue(); } - public static JsonArray GetEnumerable(this JsonNode node) { + public static JsonArray GetEnumerable(this JsonNode node) + { return node.AsArray(); } - public static JsonArray GetEnumerable(this JsonNode node, string name) { + public static JsonArray GetEnumerable(this JsonNode node, string name) + { return node?.Select(name)?.AsArray(); } - public static IEnumerable GetEnumerable(this JsonNode node) { + public static IEnumerable GetEnumerable(this JsonNode node) + { return node.AsArray() .Select(x => x.GetValue()); } - public static IEnumerable GetEnumerable(this JsonNode node, string name) { + public static IEnumerable GetEnumerable(this JsonNode node, string name) + { return node.Select(name) .AsArray() ?.Select(x => x.GetValue()); } - public static IEnumerable GetEnumerable(this JsonNode node, string name, string elementName) { + public static IEnumerable GetEnumerable(this JsonNode node, string name, string elementName) + { return node.Select(name) .AsArray()? - .Select(x => { + .Select(x => + { var child = x.Select(elementName); return child is JsonValue value ? value.GetValue() diff --git a/MinecraftLaunch/Extensions/MathExtension.cs b/MinecraftLaunch/Extensions/MathExtension.cs index b819cde..ad5585f 100644 --- a/MinecraftLaunch/Extensions/MathExtension.cs +++ b/MinecraftLaunch/Extensions/MathExtension.cs @@ -2,7 +2,8 @@ namespace MinecraftLaunch.Extensions; -public static class MathExtension { +public static class MathExtension +{ public static double Normalize(this double value) => value / 100.0; /// @@ -10,7 +11,8 @@ public static class MathExtension { /// /// The download progress arguments. /// The download progress as a percentage. - public static double ToPercentage(this ResourceDownloadProgressChangedEventArgs args) { + public static double ToPercentage(this ResourceDownloadProgressChangedEventArgs args) + { return (double)args.CompletedCount / (double)args.TotalCount; } @@ -21,7 +23,8 @@ public static double ToPercentage(this ResourceDownloadProgressChangedEventArgs /// The minimum value of the range. /// The maximum value of the range. /// The progress value as a percentage within the specified range. - public static double ToPercentage(this double progress, double mini, double max) { + public static double ToPercentage(this double progress, double mini, double max) + { if (progress > 1) progress = progress.Normalize(); diff --git a/MinecraftLaunch/Extensions/MinecraftEntryExtension.cs b/MinecraftLaunch/Extensions/MinecraftEntryExtension.cs index 438b174..d41c8ac 100644 --- a/MinecraftLaunch/Extensions/MinecraftEntryExtension.cs +++ b/MinecraftLaunch/Extensions/MinecraftEntryExtension.cs @@ -6,14 +6,17 @@ namespace MinecraftLaunch.Extensions; -public static class MinecraftEntryExtension { - public static JavaEntry GetAppropriateJava(this MinecraftEntry minecraft, IEnumerable javas) { +public static class MinecraftEntryExtension +{ + public static JavaEntry GetAppropriateJava(this MinecraftEntry minecraft, IEnumerable javas) + { var targetJavaVersion = minecraft.GetAppropriateJavaVersion(); bool isForgeOrNeoForge = false; List possiblyAvailableJavas = []; - if (minecraft is ModifiedMinecraftEntry modifiedMinecraft) { + if (minecraft is ModifiedMinecraftEntry modifiedMinecraft) + { var loaders = modifiedMinecraft.ModLoaders.Select(x => x.Type); isForgeOrNeoForge = loaders.Contains(ModLoaderType.Forge) || loaders.Contains(ModLoaderType.NeoForge); } @@ -31,7 +34,8 @@ public static JavaEntry GetAppropriateJava(this MinecraftEntry minecraft, IEnume return possiblyAvailableJavas.First(); } - public static int GetAppropriateJavaVersion(this MinecraftEntry minecraft) { + public static int GetAppropriateJavaVersion(this MinecraftEntry minecraft) + { if (minecraft is ModifiedMinecraftEntry { HasInheritance: true } mc) return mc.InheritedMinecraft.GetAppropriateJavaVersion(); @@ -44,7 +48,8 @@ public static int GetAppropriateJavaVersion(this MinecraftEntry minecraft) { : majorJavaVersionNode.GetInt32(); } - public static MinecraftClient GetJarElement(this MinecraftEntry entry) { + public static MinecraftClient GetJarElement(this MinecraftEntry entry) + { string clientJsonPath = entry.ClientJsonPath; if (entry is ModifiedMinecraftEntry { HasInheritance: true } inst) clientJsonPath = inst.InheritedMinecraft.ClientJsonPath; @@ -68,7 +73,8 @@ public static MinecraftClient GetJarElement(this MinecraftEntry entry) { if (sha1 is null || url is null || size is null) throw new InvalidDataException("Invalid client info"); - return new MinecraftClient { + return new MinecraftClient + { MinecraftFolderPath = entry.MinecraftFolderPath, ClientId = Path.GetFileNameWithoutExtension(clientJarPath), Url = url, @@ -77,7 +83,8 @@ public static MinecraftClient GetJarElement(this MinecraftEntry entry) { }; } - public static AssstIndex GetAssetIndex(this MinecraftEntry minecraftEntry) { + public static AssstIndex GetAssetIndex(this MinecraftEntry minecraftEntry) + { // Identify file paths string clientJsonPath = minecraftEntry is ModifiedMinecraftEntry { HasInheritance: true } entry ? entry.InheritedMinecraft.ClientJsonPath @@ -93,7 +100,8 @@ public static AssstIndex GetAssetIndex(this MinecraftEntry minecraftEntry) { string url = assetIndex.GetString("url") ?? throw new InvalidDataException(); string sha1 = assetIndex.GetString("sha1") ?? throw new InvalidDataException(); - return new AssstIndex { + return new AssstIndex + { Id = id, Url = url, Size = size, @@ -102,24 +110,30 @@ public static AssstIndex GetAssetIndex(this MinecraftEntry minecraftEntry) { }; } - public static void ExtractNatives(this MinecraftEntry minecraftEntry, IReadOnlyList natives) { + public static void ExtractNatives(this MinecraftEntry minecraftEntry, IReadOnlyList natives) + { if (!natives.Any()) return; - var extension = EnvironmentUtil.GetPlatformName() switch { + var extension = EnvironmentUtil.GetPlatformName() switch + { "windows" => ".dll", "linux" => ".so", "osx" => ".dylib", _ => "." }; - foreach (var file in natives) { + foreach (var file in natives) + { using ZipArchive zip = ZipFile.OpenRead(file.FullPath); - foreach (ZipArchiveEntry entry in zip.Entries) { - if (Path.HasExtension(entry.FullName)) { + foreach (ZipArchiveEntry entry in zip.Entries) + { + if (Path.HasExtension(entry.FullName)) + { var toExtract = new FileInfo(Path.Combine(minecraftEntry.MinecraftFolderPath, "versions", minecraftEntry.Id, "natives", entry.Name)); toExtract.Directory?.Create(); - if (!toExtract.Exists) { + if (!toExtract.Exists) + { entry.ExtractToFile(toExtract.FullName, true); } } @@ -127,24 +141,30 @@ public static void ExtractNatives(this MinecraftEntry minecraftEntry, IReadOnlyL } } - public static Task ExtractNativesAsync(this MinecraftEntry minecraftEntry, IReadOnlyList natives, CancellationToken cancellationToken = default) => Task.Run(() => { + public static Task ExtractNativesAsync(this MinecraftEntry minecraftEntry, IReadOnlyList natives, CancellationToken cancellationToken = default) => Task.Run(() => + { if (!natives.Any()) return; - var extension = EnvironmentUtil.GetPlatformName() switch { + var extension = EnvironmentUtil.GetPlatformName() switch + { "windows" => ".dll", "linux" => ".so", "osx" => ".dylib", _ => "." }; - foreach (var file in natives) { + foreach (var file in natives) + { using ZipArchive zip = ZipFile.OpenRead(file.FullPath); - foreach (ZipArchiveEntry entry in zip.Entries) { - if (Path.HasExtension(entry.FullName)) { + foreach (ZipArchiveEntry entry in zip.Entries) + { + if (Path.HasExtension(entry.FullName)) + { var toExtract = new FileInfo(Path.Combine(minecraftEntry.MinecraftFolderPath, "versions", minecraftEntry.Id, "natives", entry.Name)); toExtract.Directory?.Create(); - if (!toExtract.Exists) { + if (!toExtract.Exists) + { entry.ExtractToFile(toExtract.FullName, true); } } diff --git a/MinecraftLaunch/Extensions/NbtExtension.cs b/MinecraftLaunch/Extensions/NbtExtension.cs index 518d453..a2d1d4e 100644 --- a/MinecraftLaunch/Extensions/NbtExtension.cs +++ b/MinecraftLaunch/Extensions/NbtExtension.cs @@ -4,16 +4,20 @@ namespace MinecraftLaunch.Extensions; -public static class NbtExtension { - public static INbtParser GetNBTParser(this string nbtFilePath) { - if (string.IsNullOrEmpty(nbtFilePath)) { +public static class NbtExtension +{ + public static INbtParser GetNBTParser(this string nbtFilePath) + { + if (string.IsNullOrEmpty(nbtFilePath)) + { throw new ArgumentNullException(nameof(nbtFilePath)); } return new NbtParser(nbtFilePath); } - public static INbtParser GetNBTParser(this MinecraftEntry entry) { + public static INbtParser GetNBTParser(this MinecraftEntry entry) + { return new NbtParser(entry); } } \ No newline at end of file diff --git a/MinecraftLaunch/Extensions/PathExtension.cs b/MinecraftLaunch/Extensions/PathExtension.cs index 554a18f..90bc596 100644 --- a/MinecraftLaunch/Extensions/PathExtension.cs +++ b/MinecraftLaunch/Extensions/PathExtension.cs @@ -2,9 +2,12 @@ namespace MinecraftLaunch.Extensions; -public static class PathExtension { - public static string ToPath(this string raw) { - if (!Enumerable.Contains(raw, ' ')) { +public static class PathExtension +{ + public static string ToPath(this string raw) + { + if (!Enumerable.Contains(raw, ' ')) + { return raw; } return "\"" + raw + "\""; diff --git a/MinecraftLaunch/Extensions/StringExtension.cs b/MinecraftLaunch/Extensions/StringExtension.cs index 061d2b4..22330bc 100644 --- a/MinecraftLaunch/Extensions/StringExtension.cs +++ b/MinecraftLaunch/Extensions/StringExtension.cs @@ -1,17 +1,24 @@ namespace MinecraftLaunch.Extensions; -public static class StringExtension { - public static IEnumerable GroupArguments(this IEnumerable parameters) { +public static class StringExtension +{ + public static IEnumerable GroupArguments(this IEnumerable parameters) + { List group = []; Queue queue = new(parameters); - while (queue.Count > 0) { + while (queue.Count > 0) + { var next = queue.Dequeue(); - if (group.Count == 0) { + if (group.Count == 0) + { group.Add(next); - } else { - if (group.First().StartsWith('-') && next.StartsWith('-')) { + } + else + { + if (group.First().StartsWith('-') && next.StartsWith('-')) + { yield return string.Join(group.First().EndsWith('=') ? "" : " ", group); group.Clear(); } @@ -20,12 +27,14 @@ public static IEnumerable GroupArguments(this IEnumerable parame } } - if (group.Count > 0) { + if (group.Count > 0) + { yield return string.Join(group.First().EndsWith('=') ? "" : " ", group); } } - public static string ReplaceFromDictionary(this string text, Dictionary keyValuePairs) { + public static string ReplaceFromDictionary(this string text, Dictionary keyValuePairs) + { string replacedText = text; foreach (var item in keyValuePairs) @@ -34,7 +43,8 @@ public static string ReplaceFromDictionary(this string text, Dictionary FormatLibraryName(this string Name) { + public static IEnumerable FormatLibraryName(this string Name) + { var extension = Name.Contains('@') ? Name.Split('@') : Array.Empty(); var subString = extension.Any() ? Name.Replace($"@{extension[1]}", string.Empty).Split(':') @@ -51,7 +61,8 @@ public static IEnumerable FormatLibraryName(this string Name) { else yield return $"{subString[1]}-{subString[2]}{(subString.Length > 3 ? $"-{subString[3]}" : string.Empty)}.jar".Replace("jar", extension[1]); } - public static string FormatLibraryNameToRelativePath(this string name) { + public static string FormatLibraryNameToRelativePath(this string name) + { string path = string.Empty; foreach (var subPath in name.FormatLibraryName()) diff --git a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs index 181a846..2d376c1 100644 --- a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs +++ b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs @@ -1,14 +1,18 @@ using System.IO.Compression; namespace MinecraftLaunch.Extensions; -public static class ZipArchiveExtension { - public static string ReadAsString(this ZipArchiveEntry archiveEntry) { + +public static class ZipArchiveExtension +{ + public static string ReadAsString(this ZipArchiveEntry archiveEntry) + { using var stream = archiveEntry.Open(); using var reader = new StreamReader(stream); return reader.ReadToEnd(); } - public static void ExtractTo(this ZipArchiveEntry zipArchiveEntry, string destinationFile) { + public static void ExtractTo(this ZipArchiveEntry zipArchiveEntry, string destinationFile) + { var file = new FileInfo(destinationFile); if (file.Directory is null) diff --git a/MinecraftLaunch/InitializeHelper.cs b/MinecraftLaunch/InitializeHelper.cs index 710d331..ba5bb88 100644 --- a/MinecraftLaunch/InitializeHelper.cs +++ b/MinecraftLaunch/InitializeHelper.cs @@ -5,8 +5,10 @@ namespace MinecraftLaunch; -public static class InitializeHelper { - public static void Initialize(Action settingsProvider) { +public static class InitializeHelper +{ + public static void Initialize(Action settingsProvider) + { var componentSettings = new ComponentSettings(); settingsProvider(componentSettings); @@ -16,7 +18,8 @@ public static void Initialize(Action settingsProvider) { DownloadManager.IsEnableFragment = componentSettings.IsEnableFragment; DownloadManager.CurseforgeApiKey = componentSettings.CurseForgeApiKey; - HttpUtil.FlurlClient = new FlurlClient { + HttpUtil.FlurlClient = new FlurlClient + { Settings = { Timeout = TimeSpan.FromSeconds(15), JsonSerializer = new DefaultJsonSerializer(JsonSerializerUtil.GetDefaultOptions()), diff --git a/MinecraftLaunch/Launch/MinecraftProcess.cs b/MinecraftLaunch/Launch/MinecraftProcess.cs index d8af540..eb1f2a1 100644 --- a/MinecraftLaunch/Launch/MinecraftProcess.cs +++ b/MinecraftLaunch/Launch/MinecraftProcess.cs @@ -5,23 +5,29 @@ namespace MinecraftLaunch.Launch; -public sealed class MinecraftProcess : IDisposable { +public sealed class MinecraftProcess : IDisposable +{ public Process Process { get; private set; } public IEnumerable ArgumentList { get; init; } public IReadOnlyList Natives { get; private set; } public nint MainWindowHandle => Process.MainWindowHandle; public event EventHandler Started; + public event EventHandler Exited; + public event EventHandler OutputLogReceived; - public MinecraftProcess(LaunchConfig launchConfig, MinecraftEntry minecraft, IEnumerable launchArgs) { + public MinecraftProcess(LaunchConfig launchConfig, MinecraftEntry minecraft, IEnumerable launchArgs) + { ArgumentList = launchArgs; if (!ArgumentList.Any()) return; - Process = new Process { - StartInfo = new ProcessStartInfo(launchConfig.JavaPath.JavaPath) { + Process = new Process + { + StartInfo = new ProcessStartInfo(launchConfig.JavaPath.JavaPath) + { WorkingDirectory = minecraft.ToWorkingPath(launchConfig.IsEnableIndependency), Arguments = string.Join(' ', launchArgs), UseShellExecute = false, @@ -38,25 +44,30 @@ public MinecraftProcess(LaunchConfig launchConfig, MinecraftEntry minecraft, IEn Start(); } - public void Start() { + public void Start() + { Process.Start(); Process.BeginOutputReadLine(); Process.BeginErrorReadLine(); Started?.Invoke(this, EventArgs.Empty); } - public void Close() { + public void Close() + { Process.Kill(); } public void Dispose() => Process?.Dispose(); - private void OnMinecraftProcessExited(object sender, EventArgs e) { + private void OnMinecraftProcessExited(object sender, EventArgs e) + { Exited?.Invoke(this, new()); } - private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) { - if (!string.IsNullOrEmpty(e.Data)) { + private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrEmpty(e.Data)) + { OutputLogReceived?.Invoke(this, new LogReceivedEventArgs(MinecraftLoggingParser.Parse(e.Data))); } } diff --git a/MinecraftLaunch/Launch/MinecraftRunner.cs b/MinecraftLaunch/Launch/MinecraftRunner.cs index 299adf3..5032bac 100644 --- a/MinecraftLaunch/Launch/MinecraftRunner.cs +++ b/MinecraftLaunch/Launch/MinecraftRunner.cs @@ -1,50 +1,57 @@ using MinecraftLaunch.Base.Models.Game; using MinecraftLaunch.Components.Parser; using MinecraftLaunch.Extensions; -using System.Threading; namespace MinecraftLaunch.Launch; -public sealed class MinecraftRunner { +public sealed class MinecraftRunner +{ private readonly MinecraftParser _minecraftParser; public LaunchConfig LaunchConfig { get; set; } - public MinecraftRunner(LaunchConfig launchConfig, MinecraftParser parser) { + public MinecraftRunner(LaunchConfig launchConfig, MinecraftParser parser) + { _minecraftParser = parser; LaunchConfig = launchConfig; } - public MinecraftProcess Run(string id) { + public MinecraftProcess Run(string id) + { MinecraftEntry minecraft = default; IEnumerable arguments = []; - try { + try + { minecraft = _minecraftParser.GetMinecraft(id); ArgumentsParser parser = new(minecraft, LaunchConfig); arguments = parser.Parse(); if (string.IsNullOrEmpty(LaunchConfig.NativesFolder)) minecraft.ExtractNatives(parser.GetNatives()); - } catch (Exception) {} + } + catch (Exception) { } return new MinecraftProcess(LaunchConfig, minecraft, arguments); } public MinecraftProcess Run(MinecraftEntry minecraft) => Run(minecraft.Id); - public async Task RunAsync(string id, CancellationToken cancellationToken = default) { + public async Task RunAsync(string id, CancellationToken cancellationToken = default) + { MinecraftEntry minecraft = default; IEnumerable arguments = []; - try { + try + { minecraft = _minecraftParser.GetMinecraft(id); ArgumentsParser parser = new(minecraft, LaunchConfig); arguments = parser.Parse(); if (string.IsNullOrEmpty(LaunchConfig.NativesFolder)) await minecraft.ExtractNativesAsync(parser.GetNatives(), cancellationToken); - } catch (Exception) {} + } + catch (Exception) { } return new MinecraftProcess(LaunchConfig, minecraft, arguments); } diff --git a/MinecraftLaunch/MinecraftLaunch.csproj b/MinecraftLaunch/MinecraftLaunch.csproj index 961bccb..f57ffc5 100644 --- a/MinecraftLaunch/MinecraftLaunch.csproj +++ b/MinecraftLaunch/MinecraftLaunch.csproj @@ -1,6 +1,6 @@  - 4.0.6-preview13 + 4.0.6-preview14 @@ -13,7 +13,7 @@ net8.0;net9.0 enable Lunova-Studio - latest + 13.0 MinecraftLaunch disable False diff --git a/MinecraftLaunch/Utilities/HttpUtil.cs b/MinecraftLaunch/Utilities/HttpUtil.cs index e3388da..80f7d8f 100644 --- a/MinecraftLaunch/Utilities/HttpUtil.cs +++ b/MinecraftLaunch/Utilities/HttpUtil.cs @@ -3,25 +3,29 @@ namespace MinecraftLaunch.Utilities; -public static class HttpUtil { +public static class HttpUtil +{ internal static HttpClient DownloaderClient { get; } = new(); public static IFlurlClient FlurlClient { get; internal set; } - public static IFlurlRequest Request(Url url) { + public static IFlurlRequest Request(Url url) + { if (FlurlClient is null) throw new InvalidOperationException("FlurlClient is not initialized."); return FlurlClient.Request(url); } - public static IFlurlRequest Request(string url) { + public static IFlurlRequest Request(string url) + { if (FlurlClient is null) throw new InvalidOperationException("FlurlClient is not initialized."); return FlurlClient.Request(url); } - public static IFlurlRequest Request(Url baseUrl, params string[] paths) { + public static IFlurlRequest Request(Url baseUrl, params string[] paths) + { if (FlurlClient is null) throw new InvalidOperationException("FlurlClient is not initialized."); diff --git a/MinecraftLaunch/Utilities/JavaUtil.cs b/MinecraftLaunch/Utilities/JavaUtil.cs index 2df9377..a00b4b9 100644 --- a/MinecraftLaunch/Utilities/JavaUtil.cs +++ b/MinecraftLaunch/Utilities/JavaUtil.cs @@ -12,13 +12,17 @@ namespace MinecraftLaunch.Utilities; -public static partial class JavaUtil { - public static async Task GetJavaInfoAsync(string javaPath, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(javaPath) || !File.Exists(javaPath)) { +public static partial class JavaUtil +{ + public static async Task GetJavaInfoAsync(string javaPath, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(javaPath) || !File.Exists(javaPath)) + { return null; } - using var process = Process.Start(new ProcessStartInfo(javaPath) { + using var process = Process.Start(new ProcessStartInfo(javaPath) + { Arguments = "-version", CreateNoWindow = true, UseShellExecute = false, @@ -42,7 +46,8 @@ public static async Task GetJavaInfoAsync(string javaPath, Cancellati await process.WaitForExitAsync(cancellationToken); var versionParts = javaVersion.Split("."); - return new JavaEntry { + return new JavaEntry + { Is64bit = is64bit, JavaPath = javaPath, JavaType = javaType, @@ -51,9 +56,12 @@ public static async Task GetJavaInfoAsync(string javaPath, Cancellati }; } - public static async IAsyncEnumerable EnumerableJavaAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { - if (EnvironmentUtil.IsWindow) { - foreach (var java in GetJavasForWindows()) { + public static async IAsyncEnumerable EnumerableJavaAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (EnvironmentUtil.IsWindow) + { + foreach (var java in GetJavasForWindows()) + { if (File.Exists(java)) yield return await GetJavaInfoAsync(java, cancellationToken); } @@ -61,7 +69,8 @@ public static async IAsyncEnumerable EnumerableJavaAsync([EnumeratorC yield break; } - using var process = Process.Start(new ProcessStartInfo("whereis") { + using var process = Process.Start(new ProcessStartInfo("whereis") + { CreateNoWindow = true, UseShellExecute = false, RedirectStandardError = true, @@ -75,7 +84,8 @@ public static async IAsyncEnumerable EnumerableJavaAsync([EnumeratorC if (process == null) yield break; - do { + do + { cancellationToken.ThrowIfCancellationRequested(); var line = process.StandardOutput.ReadLine(); @@ -98,14 +108,17 @@ public static async IAsyncEnumerable EnumerableJavaAsync([EnumeratorC private static partial Regex JavaVersionRegex(); [SupportedOSPlatform("Windows")] - private static IEnumerable GetJavasForWindows() { + private static IEnumerable GetJavasForWindows() + { //Use by:https://github.com/Xcube-Studio/Natsurainko.FluentCore/blob/main/Natsurainko.FluentCore/Environment/JavaUtils.cs List result = []; #region Cmd: Find Java by running "where javaw" command in cmd.exe - using var process = new Process() { - StartInfo = new ProcessStartInfo() { + using var process = new Process() + { + StartInfo = new ProcessStartInfo() + { FileName = "cmd", UseShellExecute = false, RedirectStandardOutput = true, @@ -134,22 +147,25 @@ private static IEnumerable GetJavasForWindows() { )!; // null checked in the where clause result.AddRange(javaPaths); - #endregion + #endregion Cmd: Find Java by running "where javaw" command in cmd.exe #region Registry: Find Java by searching the registry var javaHomePaths = new List(); // Local function: recursively search for the keyName in the registry - static List ForRegistryKey(RegistryKey registryKey, string keyName) { + static List ForRegistryKey(RegistryKey registryKey, string keyName) + { var result = new List(); - foreach (string valueName in registryKey.GetValueNames()) { + foreach (string valueName in registryKey.GetValueNames()) + { if (valueName == keyName) // Check that the valueName exists result.Add((string)registryKey.GetValue(valueName)!); } - foreach (string registrySubKey in registryKey.GetSubKeyNames()) { + foreach (string registrySubKey in registryKey.GetSubKeyNames()) + { using var subKey = registryKey.OpenSubKey(registrySubKey); if (subKey is not null) // Check that the registrySubKey exists result.AddRange(ForRegistryKey(subKey, keyName)); @@ -161,15 +177,18 @@ static List ForRegistryKey(RegistryKey registryKey, string keyName) { using var reg = Registry.LocalMachine.OpenSubKey("SOFTWARE"); - if (reg is not null && reg.GetSubKeyNames().Contains("JavaSoft")) { + if (reg is not null && reg.GetSubKeyNames().Contains("JavaSoft")) + { using var registryKey = reg.OpenSubKey("JavaSoft"); if (registryKey is not null) javaHomePaths.AddRange(ForRegistryKey(registryKey, "JavaHome")); } - if (reg is not null && reg.GetSubKeyNames().Contains("WOW6432Node")) { + if (reg is not null && reg.GetSubKeyNames().Contains("WOW6432Node")) + { using var registryKey = reg.OpenSubKey("WOW6432Node"); - if (registryKey is not null && registryKey.GetSubKeyNames().Contains("JavaSoft")) { + if (registryKey is not null && registryKey.GetSubKeyNames().Contains("JavaSoft")) + { using var registrySubKey = reg.OpenSubKey("JavaSoft"); if (registrySubKey is not null) ForRegistryKey(registrySubKey, "JavaHome").ForEach(x => javaHomePaths.Add(x)); @@ -180,7 +199,7 @@ static List ForRegistryKey(RegistryKey registryKey, string keyName) { if (Directory.Exists(item)) result.AddRange(new DirectoryInfo(item).FindAll("javaw.exe").Select(x => x.FullName)); - #endregion + #endregion Registry: Find Java by searching the registry #region Special Folders @@ -214,10 +233,10 @@ static List ForRegistryKey(RegistryKey registryKey, string keyName) { if (Directory.Exists(folder)) result.AddRange(new DirectoryInfo(folder).FindAll("javaw.exe").Select(x => x.FullName)); - #endregion + #endregion Special Folders return result.Distinct(); } - #endregion + #endregion Privates } \ No newline at end of file diff --git a/MinecraftLaunch/Utilities/JsonSerializerUtil.cs b/MinecraftLaunch/Utilities/JsonSerializerUtil.cs index 1ef8c28..ce2785d 100644 --- a/MinecraftLaunch/Utilities/JsonSerializerUtil.cs +++ b/MinecraftLaunch/Utilities/JsonSerializerUtil.cs @@ -1,17 +1,15 @@ using MinecraftLaunch.Base.Models.JsonConverter; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Threading.Tasks; namespace MinecraftLaunch.Utilities; -public static class JsonSerializerUtil { - public static JsonSerializerOptions GetDefaultOptions() { - var options = new JsonSerializerOptions { +public static class JsonSerializerUtil +{ + public static JsonSerializerOptions GetDefaultOptions() + { + var options = new JsonSerializerOptions + { MaxDepth = 100, WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -23,4 +21,4 @@ public static JsonSerializerOptions GetDefaultOptions() { return options; } -} +} \ No newline at end of file