using System.Net; using System.Net.Http; using System.Security.Cryptography; using DTLib.Extensions; using Mlaumcherb.Client.Avalonia.зримое; using Mlaumcherb.Client.Avalonia.классы; using Mlaumcherb.Client.Avalonia.холопы; using static Mlaumcherb.Client.Avalonia.сеть.Сеть; namespace Mlaumcherb.Client.Avalonia.сеть.TaskFactories; public class AssetsDownloadTaskFactory : INetworkTaskFactory { private const string ASSET_SERVER_URL = "https://resources.download.minecraft.net/"; private GameVersionDescriptor _descriptor; private SHA1 _hasher; private IOPath _indexFilePath; List _assetsToDownload = new(); public AssetsDownloadTaskFactory(GameVersionDescriptor descriptor) { _descriptor = descriptor; _hasher = SHA1.Create(); _indexFilePath = PathHelper.GetAssetIndexFilePath(_descriptor.assetIndex.id); } public async Task CreateAsync(bool checkHashes) { if (!await CheckFilesAsync(checkHashes)) return new NetworkTask( $"assets '{_descriptor.assetIndex.id}'", GetTotalSize(), Download ); return null; } private async Task CheckFilesAsync(bool checkHashes) { if(!File.Exists(_indexFilePath)) { LauncherApp.Logger.LogInfo(nameof(Сеть), $"started downloading asset index to '{_indexFilePath}'"); await DownloadFile(_descriptor.assetIndex.url, _indexFilePath); LauncherApp.Logger.LogInfo(nameof(Сеть), "finished downloading asset index"); } string indexFileText = File.ReadAllText(_indexFilePath); var assetIndex = JsonConvert.DeserializeObject(indexFileText) ?? throw new Exception($"can't deserialize asset index file '{_indexFilePath}'"); _assetsToDownload.Clear(); // removing duplicates for Dictionary (idk how can it be possible, but Newtonsoft.Json creates them) HashSet assetHashes = new HashSet(); foreach (var pair in assetIndex.objects) { if (assetHashes.Add(pair.Value.hash)) { var a = new AssetDownloadProperties(pair.Key, pair.Value); if (!File.Exists(a.filePath)) { _assetsToDownload.Add(a); } else if(checkHashes) { await using var fs = File.OpenRead(a.filePath); string hash = _hasher.ComputeHash(fs).HashToString(); if (hash != a.hash) _assetsToDownload.Add(a); } } } return _assetsToDownload.Count == 0; } private long GetTotalSize() { long totalSize = 0; foreach (var a in _assetsToDownload) totalSize += a.size; return totalSize; } private class AssetDownloadProperties { public string name; public string hash; public long size; public string url; public IOPath filePath; public AssetDownloadProperties(string key, AssetProperties p) { name = key; hash = p.hash; size = p.size; string hashStart = hash.Substring(0, 2); url = $"{ASSET_SERVER_URL}/{hashStart}/{hash}"; filePath = Path.Concat(IOPath.ArrayCast(["assets", "objects", hashStart, hash], true)); } } private async Task Download(NetworkProgressReporter pr, CancellationToken ct) { LauncherApp.Logger.LogInfo(nameof(Сеть), $"started downloading assets '{_descriptor.assetIndex.id}'"); ParallelOptions opt = new() { MaxDegreeOfParallelism = LauncherApp.Config.max_parallel_downloads, CancellationToken = ct }; await Parallel.ForEachAsync(_assetsToDownload, opt, async (a, _ct) => { bool completed = false; while(!completed) { LauncherApp.Logger.LogDebug(nameof(Сеть), $"downloading asset '{a.name}' {a.hash}"); try { await DownloadFile(a.url, a.filePath, _ct, pr.AddBytesCount); completed = true; } catch (HttpRequestException httpException) { // wait on rate limit if(httpException.StatusCode == HttpStatusCode.TooManyRequests) { LauncherApp.Logger.LogDebug(nameof(Сеть), "rate limit hit"); await Task.Delay(1000, _ct); } else throw; } } }); LauncherApp.Logger.LogInfo(nameof(Сеть), $"finished downloading assets '{_descriptor.assetIndex.id}'"); } }