using System.Buffers; using System.Net.Http; using Млаумчерб.Клиент.видимое; using Млаумчерб.Клиент.классы; using Timer = DTLib.Timer; namespace Млаумчерб.Клиент; public static class Сеть { private static HttpClient http = new(); private const string ASSET_SERVER_URL = "https://resources.download.minecraft.net/"; private static readonly string[] VERSION_MANIFEST_URLS = { "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" }; public static async Task DownloadFileHTTP(string url, IOPath outPath, Action>? transformFunc = null, CancellationToken ct = default) { await using var src = await http.GetStreamAsync(url, ct); await using var dst = File.OpenWrite(outPath); await src.CopyTransformAsync(dst, transformFunc, ct).ConfigureAwait(false); } public static async Task CopyTransformAsync(this Stream src, Stream dst, Action>? transformFunc = null, CancellationToken ct = default) { // default dotnet runtime buffer size int bufferSize = 81920; byte[] readBuffer = ArrayPool.Shared.Rent(bufferSize); byte[] writeBuffer = ArrayPool.Shared.Rent(bufferSize); try { var readTask = src.ReadAsync(readBuffer, 0, bufferSize, ct).ConfigureAwait(false); while (true) { int readCount = await readTask; if (readCount == 0) break; (readBuffer, writeBuffer) = (writeBuffer, readBuffer); readTask = src.ReadAsync(readBuffer, 0, bufferSize, ct).ConfigureAwait(false); transformFunc?.Invoke(new ArraySegment(writeBuffer, 0, readCount)); dst.Write(writeBuffer, 0, readCount); } } catch (OperationCanceledException) { } finally { ArrayPool.Shared.Return(readBuffer); ArrayPool.Shared.Return(writeBuffer); } } public static async Task DownloadAssets(AssetIndexProperties assetIndexProperties, CancellationToken ct, bool force) { IOPath indexFilePath = Пути.GetAssetIndexFilePath(assetIndexProperties.id); if (File.Exists(indexFilePath) && !force) return; IOPath indexFilePathTmp = indexFilePath + ".tmp"; if(File.Exists(indexFilePathTmp)) File.Delete(indexFilePathTmp); // TODO: add something to Downloads ScrollList Приложение.Логгер.LogInfo(nameof(DownloadAssets), $"started downloading asset index to '{indexFilePathTmp}'"); await DownloadFileHTTP(assetIndexProperties.url, indexFilePathTmp, null, ct); Приложение.Логгер.LogInfo(nameof(DownloadAssets), "finished downloading asset index"); string indexFileText = File.ReadAllText(indexFilePathTmp); AssetIndex assetIndex = JsonConvert.DeserializeObject(indexFileText) ?? throw new Exception($"can't deserialize asset index file '{indexFilePathTmp}'"); var assets = new List>(); HashSet assetHashes = new HashSet(); long totalSize = 0; long currentSize = 0; foreach (var pair in assetIndex.objects) { if (!assetHashes.Contains(pair.Value.hash)) { assets.Add(pair); assetHashes.Add(pair.Value.hash); totalSize += pair.Value.size; } } void AddBytesCountAtomic(ArraySegment chunk) { long chunkSize = chunk.Count; Interlocked.Add(ref currentSize, chunkSize); } long prevSize = 0; int timerDelay = 2000; void ReportProgress() { // TODO: add something to Downloads ScrollList long totalSizeM = totalSize/(1024*1024); long currentSizeM = currentSize/(1024*1024); long bytesPerSec = (currentSize - prevSize) / (timerDelay / 1000); float KbytesPerSec = bytesPerSec / 1024f; prevSize = currentSize; Приложение.Логгер.LogDebug(nameof(DownloadAssets), $"download progress {currentSizeM}Mb/{totalSizeM}Mb ({KbytesPerSec}Kb/s)"); } using Timer timer = new Timer(true, timerDelay, ReportProgress); timer.Start(); Приложение.Логгер.LogInfo(nameof(DownloadAssets), "started downloading assets"); int parallelDownloads = 32; var tasks = new Task[parallelDownloads]; var currentlyDownloadingFileHashes = new string[parallelDownloads]; int i = 0; foreach (var a in assets) { string hash = a.Value.hash; string hashStart = hash.Substring(0, 2); var assetUrl = $"{ASSET_SERVER_URL}/{hashStart}/{hash}"; IOPath assetFilePath = Path.Concat("assets", "objects", hashStart, hash); Приложение.Логгер.LogDebug(nameof(DownloadAssets), $"downloading asset '{a.Key}' {hash}"); tasks[i] = DownloadFileHTTP(assetUrl, assetFilePath, AddBytesCountAtomic, ct); currentlyDownloadingFileHashes[i] = hash; if (++i == parallelDownloads) { await Task.WhenAll(tasks); i = 0; } } await Task.WhenAll(tasks); timer.Stop(); timer.InvokeAction(); File.Move(indexFilePathTmp, indexFilePath, true); Приложение.Логгер.LogInfo(nameof(DownloadAssets), "finished downloading assets"); } private static async Task> GetRemoteVersionDescriptorsAsync() { List descriptors = new(); foreach (var url in VERSION_MANIFEST_URLS) { try { var manifestText = await http.GetStringAsync(url); var catalog = JsonConvert.DeserializeObject(manifestText); if (catalog != null) descriptors.AddRange(catalog.versions); } catch (Exception ex) { Приложение.Логгер.LogWarn(nameof(Сеть), ex); } } return descriptors; } private static List? _versionPropsList; /// empty list if couldn't find any remote versions public static async Task> GetDownloadableVersions() { if(_versionPropsList == null) { _versionPropsList = new(); var rvdlist = await GetRemoteVersionDescriptorsAsync(); foreach (var r in rvdlist) { if(r.type == "release") _versionPropsList.Add(new GameVersionProps(r.id, r.url)); } } return _versionPropsList; } public static async Task DownloadVersionFile(string url, IOPath filePath, bool force) { if(File.Exists(filePath) && !force) return; await DownloadFileHTTP(url, filePath); } public static async Task DownloadJava(JavaVersion javaVersion, IOPath path, bool force) { } public static async Task DownloadLibraries(List libraries, IOPath librariesDir, bool force) { } }