NetworkTaskFactory
This commit is contained in:
parent
45c3f90da0
commit
4704f1217a
@ -2,11 +2,13 @@
|
|||||||
using DTLib.Extensions;
|
using DTLib.Extensions;
|
||||||
using Млаумчерб.Клиент.видимое;
|
using Млаумчерб.Клиент.видимое;
|
||||||
using Млаумчерб.Клиент.классы;
|
using Млаумчерб.Клиент.классы;
|
||||||
|
using Млаумчерб.Клиент.сеть;
|
||||||
|
using Млаумчерб.Клиент.сеть.NetworkTaskFactories;
|
||||||
using static Млаумчерб.Клиент.классы.Пути;
|
using static Млаумчерб.Клиент.классы.Пути;
|
||||||
|
|
||||||
namespace Млаумчерб.Клиент;
|
namespace Млаумчерб.Клиент;
|
||||||
|
|
||||||
public class GameVersionDescriptor
|
public class GameVersion
|
||||||
{
|
{
|
||||||
private readonly GameVersionProps _props;
|
private readonly GameVersionProps _props;
|
||||||
public string Name => _props.Name;
|
public string Name => _props.Name;
|
||||||
@ -14,11 +16,11 @@ public class GameVersionDescriptor
|
|||||||
|
|
||||||
private IOPath JavaExecutableFilePath;
|
private IOPath JavaExecutableFilePath;
|
||||||
|
|
||||||
private MinecraftVersionDescriptor descriptor;
|
private VersionDescriptor descriptor;
|
||||||
private JavaArguments javaArgs;
|
private JavaArguments javaArgs;
|
||||||
private GameArguments gameArgs;
|
private GameArguments gameArgs;
|
||||||
|
private Libraries libraries;
|
||||||
private CancellationTokenSource? gameCts;
|
private CancellationTokenSource? gameCts;
|
||||||
private CancellationTokenSource? downloadCts;
|
|
||||||
private CommandTask<CommandResult>? commandTask;
|
private CommandTask<CommandResult>? commandTask;
|
||||||
|
|
||||||
public static async Task<List<GameVersionProps>> GetAllVersionsAsync()
|
public static async Task<List<GameVersionProps>> GetAllVersionsAsync()
|
||||||
@ -29,58 +31,76 @@ public class GameVersionDescriptor
|
|||||||
string name = GetVersionDescriptorName(f);
|
string name = GetVersionDescriptorName(f);
|
||||||
propsList.Add(new GameVersionProps(name, null, f));
|
propsList.Add(new GameVersionProps(name, null, f));
|
||||||
}
|
}
|
||||||
|
|
||||||
var remoteVersions = await Сеть.GetDownloadableVersions();
|
var remoteVersions = await Сеть.GetDownloadableVersions();
|
||||||
propsList.AddRange(remoteVersions);
|
propsList.AddRange(remoteVersions);
|
||||||
return propsList;
|
return propsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<GameVersionDescriptor> CreateFromPropsAsync(GameVersionProps props)
|
public static async Task<GameVersion> CreateFromPropsAsync(GameVersionProps props)
|
||||||
{
|
{
|
||||||
if(!File.Exists(props.LocalDescriptorPath))
|
if (!File.Exists(props.LocalDescriptorPath))
|
||||||
{
|
{
|
||||||
if (props.RemoteDescriptorUrl is null)
|
if (props.RemoteDescriptorUrl is null)
|
||||||
throw new NullReferenceException("can't download game version descriptor '"
|
throw new NullReferenceException("can't download game version descriptor '"
|
||||||
+ props.Name + "', because RemoteDescriptorUrl is null");
|
+ props.Name + "', because RemoteDescriptorUrl is null");
|
||||||
await Сеть.DownloadFileHTTP(props.RemoteDescriptorUrl, props.LocalDescriptorPath);
|
await Сеть.DownloadFileHTTP(props.RemoteDescriptorUrl, props.LocalDescriptorPath);
|
||||||
}
|
}
|
||||||
return new GameVersionDescriptor(props);
|
|
||||||
|
return new GameVersion(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
private GameVersionDescriptor(GameVersionProps props)
|
private GameVersion(GameVersionProps props)
|
||||||
{
|
{
|
||||||
_props = props;
|
_props = props;
|
||||||
WorkingDirectory = Path.Concat(Приложение.Настройки.путь_к_кубачу, Name);
|
|
||||||
string descriptorText = File.ReadAllText(props.LocalDescriptorPath);
|
string descriptorText = File.ReadAllText(props.LocalDescriptorPath);
|
||||||
descriptor = JsonConvert.DeserializeObject<MinecraftVersionDescriptor>(descriptorText)
|
descriptor = JsonConvert.DeserializeObject<VersionDescriptor>(descriptorText)
|
||||||
?? throw new Exception($"can't parse descriptor file '{props.LocalDescriptorPath}'");
|
?? throw new Exception($"can't parse descriptor file '{props.LocalDescriptorPath}'");
|
||||||
javaArgs = new JavaArguments(descriptor);
|
javaArgs = new JavaArguments(descriptor);
|
||||||
gameArgs = new GameArguments(descriptor);
|
gameArgs = new GameArguments(descriptor);
|
||||||
JavaExecutableFilePath = Path.Concat(Приложение.Настройки.путь_к_жабе, "bin",
|
libraries = new Libraries(descriptor);
|
||||||
OperatingSystem.IsWindows() ? "javaw.exe" : "javaw");
|
WorkingDirectory = GetVersionDir(descriptor.id);
|
||||||
|
JavaExecutableFilePath = GetJavaExecutablePath(descriptor.javaVersion.component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void BeginUpdate(bool force)
|
public async Task<List<NetworkTask>> CreateUpdateTasksAsync(bool checkHashes)
|
||||||
{
|
{
|
||||||
try
|
List<INetworkTaskFactory> taskFactories =
|
||||||
|
[
|
||||||
|
new AssetsDownloadTaskFactory(descriptor),
|
||||||
|
new LibrariesDownloadTaskFactory(descriptor, libraries),
|
||||||
|
new VersionFileDownloadTaskFactory(descriptor),
|
||||||
|
];
|
||||||
|
/*if(Приложение.Настройки.скачать_жабу)
|
||||||
|
{
|
||||||
|
taskFactories.Add(new JavaDownloadTaskFactory(descriptor));
|
||||||
|
}*/
|
||||||
|
/*if (modpack != null)
|
||||||
|
{
|
||||||
|
taskFactories.Add(new ModpackDownloadTaskFactory(modpack));
|
||||||
|
}*/
|
||||||
|
|
||||||
|
List<NetworkTask> tasks = new();
|
||||||
|
for (int i = 0; i < taskFactories.Count; i++)
|
||||||
|
{
|
||||||
|
var nt = await taskFactories[i].CreateAsync(checkHashes);
|
||||||
|
if (nt != null)
|
||||||
|
tasks.Add(nt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.Count == 0)
|
||||||
{
|
{
|
||||||
downloadCts = new CancellationTokenSource();
|
|
||||||
if(Приложение.Настройки.скачать_жабу)
|
|
||||||
await Сеть.DownloadJava(descriptor.javaVersion, Приложение.Настройки.путь_к_жабе, force);
|
|
||||||
await Сеть.DownloadAssets(descriptor.assetIndex, downloadCts.Token, force);
|
|
||||||
await Сеть.DownloadVersionFile(descriptor.downloads.client.url, GetVersionJarFilePath(Name), force);
|
|
||||||
await Сеть.DownloadLibraries(descriptor.libraries, GetLibrariesDir(), force);
|
|
||||||
// await Network.DownloadModpack(modpack, WorkingDirectory, force);
|
|
||||||
_props.IsDownloaded = true;
|
_props.IsDownloaded = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
Ошибки.ПоказатьСообщение("GameUpdate", ex);
|
tasks[^1].OnStop += status =>
|
||||||
|
{
|
||||||
|
if (status == NetworkTask.Status.Completed)
|
||||||
|
_props.IsDownloaded = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
return tasks;
|
||||||
|
|
||||||
public void CancelUpdate()
|
|
||||||
{
|
|
||||||
downloadCts?.Cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Launch()
|
public async Task Launch()
|
||||||
@ -91,7 +111,7 @@ public class GameVersionDescriptor
|
|||||||
.WithWorkingDirectory(WorkingDirectory.ToString())
|
.WithWorkingDirectory(WorkingDirectory.ToString())
|
||||||
.WithArguments(javaArgsList)
|
.WithArguments(javaArgsList)
|
||||||
.WithArguments(gameArgsList);
|
.WithArguments(gameArgsList);
|
||||||
Приложение.Логгер.LogInfo(nameof(GameVersionDescriptor),
|
Приложение.Логгер.LogInfo(nameof(GameVersion),
|
||||||
$"launching the game" +
|
$"launching the game" +
|
||||||
"\njava: " + command.TargetFilePath +
|
"\njava: " + command.TargetFilePath +
|
||||||
"\nworking_dir: " + command.WorkingDirPath +
|
"\nworking_dir: " + command.WorkingDirPath +
|
||||||
@ -100,7 +120,7 @@ public class GameVersionDescriptor
|
|||||||
gameCts = new();
|
gameCts = new();
|
||||||
commandTask = command.ExecuteAsync(gameCts.Token);
|
commandTask = command.ExecuteAsync(gameCts.Token);
|
||||||
var result = await commandTask;
|
var result = await commandTask;
|
||||||
Приложение.Логгер.LogInfo(nameof(GameVersionDescriptor), $"game exited with code {result.ExitCode}");
|
Приложение.Логгер.LogInfo(nameof(GameVersion), $"game exited with code {result.ExitCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Млаумчерб.Клиент.видимое;
|
using DTLib.Extensions;
|
||||||
|
using Млаумчерб.Клиент.видимое;
|
||||||
|
|
||||||
namespace Млаумчерб.Клиент;
|
namespace Млаумчерб.Клиент;
|
||||||
|
|
||||||
@ -8,13 +9,14 @@ public record Настройки
|
|||||||
public int выделенная_память_мб { get; set; } = 4096;
|
public int выделенная_память_мб { get; set; } = 4096;
|
||||||
public bool открывать_на_весь_экран { get; set; }
|
public bool открывать_на_весь_экран { get; set; }
|
||||||
public string путь_к_кубачу { get; set; } = ".";
|
public string путь_к_кубачу { get; set; } = ".";
|
||||||
public string путь_к_жабе { get; set; } = "java";
|
|
||||||
public bool скачать_жабу { get; set; } = true;
|
public bool скачать_жабу { get; set; } = true;
|
||||||
public string? последняя_запущенная_версия;
|
public string? последняя_запущенная_версия { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] private Stream? fileWriteStream;
|
||||||
|
|
||||||
public static Настройки ЗагрузитьИзФайла(string имя_файла = "млаумчерб.настройки")
|
public static Настройки ЗагрузитьИзФайла(string имя_файла = "млаумчерб.настройки")
|
||||||
{
|
{
|
||||||
Приложение.Логгер.LogInfo(nameof(Настройки), $"попытка загрузить настройки из файла '{имя_файла}'");
|
Приложение.Логгер.LogInfo(nameof(Настройки), $"загружаются настройки из файла '{имя_файла}'");
|
||||||
if(!File.Exists(имя_файла))
|
if(!File.Exists(имя_файла))
|
||||||
{
|
{
|
||||||
Приложение.Логгер.LogInfo(nameof(Настройки), "файл не существует");
|
Приложение.Логгер.LogInfo(nameof(Настройки), "файл не существует");
|
||||||
@ -26,11 +28,11 @@ public record Настройки
|
|||||||
if (н == null)
|
if (н == null)
|
||||||
{
|
{
|
||||||
File.Move(имя_файла, имя_файла + ".старые", true);
|
File.Move(имя_файла, имя_файла + ".старые", true);
|
||||||
н = new Настройки();
|
|
||||||
н.СохранитьВФайл();
|
|
||||||
Ошибки.ПоказатьСообщение("Настройки", $"Не удалось прочитать настройки.\n" +
|
Ошибки.ПоказатьСообщение("Настройки", $"Не удалось прочитать настройки.\n" +
|
||||||
$"Сломанный файл настроек переименован в '{имя_файла}.старые'.\n" +
|
$"Сломанный файл настроек переименован в '{имя_файла}.старые'.\n" +
|
||||||
$"Создан новый файл '{имя_файла}'.");
|
$"Создаётся новый файл '{имя_файла}'.");
|
||||||
|
н = new Настройки();
|
||||||
|
н.СохранитьВФайл();
|
||||||
}
|
}
|
||||||
|
|
||||||
Приложение.Логгер.LogInfo(nameof(Настройки), $"настройки загружены: {н}");
|
Приложение.Логгер.LogInfo(nameof(Настройки), $"настройки загружены: {н}");
|
||||||
@ -39,9 +41,12 @@ public record Настройки
|
|||||||
|
|
||||||
public void СохранитьВФайл(string имя_файла = "млаумчерб.настройки")
|
public void СохранитьВФайл(string имя_файла = "млаумчерб.настройки")
|
||||||
{
|
{
|
||||||
Приложение.Логгер.LogInfo(nameof(Настройки), $"попытка сохранить настройки в файл '{имя_файла}'");
|
//TODO: file backup and restore
|
||||||
|
Приложение.Логгер.LogDebug(nameof(Настройки), $"настройки сохраняются в файл '{имя_файла}'");
|
||||||
|
fileWriteStream ??= File.OpenWrite(имя_файла);
|
||||||
var текст = JsonConvert.SerializeObject(this, Formatting.Indented);
|
var текст = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||||
File.WriteAllText(имя_файла, текст);
|
fileWriteStream.Seek(0, SeekOrigin.Begin);
|
||||||
Приложение.Логгер.LogInfo(nameof(Настройки), $"настройки сохранены: {текст}");
|
fileWriteStream.FluentWriteString(текст).Flush();
|
||||||
|
Приложение.Логгер.LogDebug(nameof(Настройки), $"настройки сохранены: {текст}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,192 +0,0 @@
|
|||||||
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<ArraySegment<byte>>? 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<ArraySegment<byte>>? transformFunc = null, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// default dotnet runtime buffer size
|
|
||||||
int bufferSize = 81920;
|
|
||||||
byte[] readBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
|
||||||
byte[] writeBuffer = ArrayPool<byte>.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<byte>(writeBuffer, 0, readCount));
|
|
||||||
dst.Write(writeBuffer, 0, readCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{ }
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(readBuffer);
|
|
||||||
ArrayPool<byte>.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<AssetIndex>(indexFileText)
|
|
||||||
?? throw new Exception($"can't deserialize asset index file '{indexFilePathTmp}'");
|
|
||||||
|
|
||||||
var assets = new List<KeyValuePair<string, AssetProperties>>();
|
|
||||||
HashSet<string> assetHashes = new HashSet<string>();
|
|
||||||
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<byte> 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<List<RemoteVersionDescriptorProps>> GetRemoteVersionDescriptorsAsync()
|
|
||||||
{
|
|
||||||
List<RemoteVersionDescriptorProps> descriptors = new();
|
|
||||||
foreach (var url in VERSION_MANIFEST_URLS)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var manifestText = await http.GetStringAsync(url);
|
|
||||||
var catalog = JsonConvert.DeserializeObject<VersionCatalog>(manifestText);
|
|
||||||
if (catalog != null)
|
|
||||||
descriptors.AddRange(catalog.versions);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Приложение.Логгер.LogWarn(nameof(Сеть), ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return descriptors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<GameVersionProps>? _versionPropsList;
|
|
||||||
/// <returns>empty list if couldn't find any remote versions</returns>
|
|
||||||
public static async Task<IReadOnlyList<GameVersionProps>> 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<Library> libraries, IOPath librariesDir, bool force)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
Млаумчерб.Клиент/видимое/DownloadItemView.axaml
Normal file
20
Млаумчерб.Клиент/видимое/DownloadItemView.axaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="Млаумчерб.Клиент.видимое.DownloadTaskView"
|
||||||
|
Padding="4" MaxHeight="60" MinWidth="200"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
BorderThickness="1" BorderBrush="#999999">
|
||||||
|
<Grid RowDefinitions="30 30" ColumnDefinitions="* 30">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Name="NameText"/>
|
||||||
|
<Button Grid.Row="0" Grid.Column="1"
|
||||||
|
Classes="button_no_border"
|
||||||
|
Background="Transparent"
|
||||||
|
Foreground="#FF4040"
|
||||||
|
FontSize="12"
|
||||||
|
Click="RemoveFromList">
|
||||||
|
[X]
|
||||||
|
</Button>
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Name="DownloadedProgressText"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
42
Млаумчерб.Клиент/видимое/DownloadItemView.axaml.cs
Normal file
42
Млаумчерб.Клиент/видимое/DownloadItemView.axaml.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Млаумчерб.Клиент.сеть;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.видимое;
|
||||||
|
|
||||||
|
public partial class DownloadTaskView : UserControl
|
||||||
|
{
|
||||||
|
private readonly NetworkTask _task;
|
||||||
|
private readonly Action<DownloadTaskView> _removeFromList;
|
||||||
|
|
||||||
|
|
||||||
|
public DownloadTaskView()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DownloadTaskView(NetworkTask task, Action<DownloadTaskView> removeFromList)
|
||||||
|
{
|
||||||
|
_task = task;
|
||||||
|
_removeFromList = removeFromList;
|
||||||
|
InitializeComponent();
|
||||||
|
NameText.Text = task.Name;
|
||||||
|
task.OnProgress += ReportProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ReportProgress(DownloadProgress progress)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
DownloadedProgressText.Text = progress.ToString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveFromList(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_task.Cancel();
|
||||||
|
Dispatcher.UIThread.Invoke(() => _removeFromList.Invoke(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ public partial class VersionItemView : ListBoxItem
|
|||||||
Props = props;
|
Props = props;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
text.Text = props.Name;
|
text.Text = props.Name;
|
||||||
props.DownloadCompleted += UpdateBackground;
|
props.OnDownloadCompleted += UpdateBackground;
|
||||||
UpdateBackground();
|
UpdateBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,8 +33,9 @@
|
|||||||
VerticalScrollBarVisibility="Visible"
|
VerticalScrollBarVisibility="Visible"
|
||||||
Background="Transparent">
|
Background="Transparent">
|
||||||
<TextBox Name="LogTextBox"
|
<TextBox Name="LogTextBox"
|
||||||
FontSize="12"
|
FontSize="14"
|
||||||
IsReadOnly="True" TextWrapping="Wrap"
|
IsReadOnly="True" TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Top"
|
||||||
Background="Transparent" BorderThickness="0"/>
|
Background="Transparent" BorderThickness="0"/>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -92,10 +93,13 @@
|
|||||||
Загрузки
|
Загрузки
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Border>
|
</Border>
|
||||||
<ScrollViewer Name="DownloadsScrollViewer" Grid.Row="1"
|
<ScrollViewer Grid.Row="1"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
VerticalScrollBarVisibility="Visible"
|
VerticalScrollBarVisibility="Visible"
|
||||||
Background="Transparent"/>
|
Background="Transparent"
|
||||||
|
Padding="1">
|
||||||
|
<StackPanel Name="DownloadsPanel" VerticalAlignment="Top"/>
|
||||||
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Presenters;
|
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using Млаумчерб.Клиент.классы;
|
using Млаумчерб.Клиент.классы;
|
||||||
|
|
||||||
namespace Млаумчерб.Клиент.видимое;
|
namespace Млаумчерб.Клиент.видимое;
|
||||||
@ -59,19 +57,25 @@ public partial class Окне : Window
|
|||||||
{
|
{
|
||||||
Приложение.Логгер.OnLogMessage += (context, severity, message, format) =>
|
Приложение.Логгер.OnLogMessage += (context, severity, message, format) =>
|
||||||
{
|
{
|
||||||
|
if(severity == LogSeverity.Debug)
|
||||||
|
return;
|
||||||
|
|
||||||
StringBuilder b = new();
|
StringBuilder b = new();
|
||||||
b.Append(DateTime.Now.ToString("[HH:mm:ss]["));
|
b.Append(DateTime.Now.ToString("[HH:mm:ss]["));
|
||||||
b.Append(severity);
|
b.Append(severity);
|
||||||
b.Append("]: ");
|
b.Append("]: ");
|
||||||
b.Append(message);
|
b.Append(message);
|
||||||
b.Append('\n');
|
b.Append('\n');
|
||||||
double offsetFromBottom = LogScrollViewer.Extent.Height
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
- LogScrollViewer.Offset.Y
|
{
|
||||||
- LogScrollViewer.Viewport.Height;
|
double offsetFromBottom = LogScrollViewer.Extent.Height
|
||||||
bool is_scrolled_to_end = offsetFromBottom < 20.0; // scrolled less then one line up
|
- LogScrollViewer.Offset.Y
|
||||||
LogTextBox.Text += b.ToString();
|
- LogScrollViewer.Viewport.Height;
|
||||||
if(is_scrolled_to_end)
|
bool is_scrolled_to_end = offsetFromBottom < 20.0; // scrolled less then one line up
|
||||||
LogScrollViewer.ScrollToEnd();
|
LogTextBox.Text += b.ToString();
|
||||||
|
if (is_scrolled_to_end)
|
||||||
|
LogScrollViewer.ScrollToEnd();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Username = Приложение.Настройки.имя_пользователя;
|
Username = Приложение.Настройки.имя_пользователя;
|
||||||
@ -81,7 +85,7 @@ public partial class Окне : Window
|
|||||||
Directory.Create(Пути.GetVersionDescriptorDir());
|
Directory.Create(Пути.GetVersionDescriptorDir());
|
||||||
VersionComboBox.SelectedIndex = 0;
|
VersionComboBox.SelectedIndex = 0;
|
||||||
VersionComboBox.IsEnabled = false;
|
VersionComboBox.IsEnabled = false;
|
||||||
var versions = await GameVersionDescriptor.GetAllVersionsAsync();
|
var versions = await GameVersion.GetAllVersionsAsync();
|
||||||
Dispatcher.UIThread.Invoke(() =>
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
{
|
{
|
||||||
foreach (var p in versions)
|
foreach (var p in versions)
|
||||||
@ -114,8 +118,18 @@ public partial class Окне : Window
|
|||||||
if (selectedVersion == null)
|
if (selectedVersion == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var v = await GameVersionDescriptor.CreateFromPropsAsync(selectedVersion);
|
var v = await GameVersion.CreateFromPropsAsync(selectedVersion);
|
||||||
v.BeginUpdate(CheckGameFiles);
|
var updateTasks = await v.CreateUpdateTasksAsync(CheckGameFiles);
|
||||||
|
foreach (var t in updateTasks)
|
||||||
|
{
|
||||||
|
var updateTask = t.StartAsync();
|
||||||
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
var view = new DownloadTaskView(t, view => DownloadsPanel.Children.Remove(view));
|
||||||
|
DownloadsPanel.Children.Add(view);
|
||||||
|
});
|
||||||
|
await updateTask;
|
||||||
|
}
|
||||||
Dispatcher.UIThread.Invoke(() => CheckGameFiles = false);
|
Dispatcher.UIThread.Invoke(() => CheckGameFiles = false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ public class GameArguments : ArgumentsWithPlaceholders
|
|||||||
"has_custom_resolution"
|
"has_custom_resolution"
|
||||||
];
|
];
|
||||||
|
|
||||||
public GameArguments(MinecraftVersionDescriptor d)
|
public GameArguments(VersionDescriptor d)
|
||||||
{
|
{
|
||||||
if (d.minecraftArguments is not null)
|
if (d.minecraftArguments is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -11,11 +11,13 @@ public class GameVersionProps
|
|||||||
get => _isDownloaded;
|
get => _isDownloaded;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
bool downloadCompleted = value && !_isDownloaded;
|
||||||
_isDownloaded = value;
|
_isDownloaded = value;
|
||||||
DownloadCompleted?.Invoke();
|
if(downloadCompleted)
|
||||||
|
OnDownloadCompleted?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public event Action? DownloadCompleted;
|
public event Action? OnDownloadCompleted;
|
||||||
|
|
||||||
public GameVersionProps(string name, string? url, IOPath descriptorPath)
|
public GameVersionProps(string name, string? url, IOPath descriptorPath)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -12,7 +12,7 @@ public class JavaArguments : ArgumentsWithPlaceholders
|
|||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public JavaArguments(MinecraftVersionDescriptor d)
|
public JavaArguments(VersionDescriptor d)
|
||||||
{
|
{
|
||||||
raw_args.AddRange(_initial_arguments);
|
raw_args.AddRange(_initial_arguments);
|
||||||
if (d.arguments is not null)
|
if (d.arguments is not null)
|
||||||
|
|||||||
67
Млаумчерб.Клиент/классы/Libraries.cs
Normal file
67
Млаумчерб.Клиент/классы/Libraries.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using DTLib.Extensions;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.классы;
|
||||||
|
|
||||||
|
public class Libraries
|
||||||
|
{
|
||||||
|
private static readonly string[] enabled_features = [];
|
||||||
|
|
||||||
|
public record JarLib(string name, IOPath jarFilePath, Artifact artifact);
|
||||||
|
public record NativeLib(string name, IOPath jarFilePath, Artifact artifact, IOPath nativeFilesDir, Extract? extractionOptions)
|
||||||
|
: JarLib(name, jarFilePath, artifact);
|
||||||
|
|
||||||
|
public IReadOnlyCollection<JarLib> Libs { get; }
|
||||||
|
|
||||||
|
public Libraries(VersionDescriptor descriptor)
|
||||||
|
{
|
||||||
|
List<JarLib> libs = new();
|
||||||
|
HashSet<string> libHashes = new();
|
||||||
|
|
||||||
|
foreach (var l in descriptor.libraries)
|
||||||
|
{
|
||||||
|
if (l.rules != null && !Буржуазия.CheckRules(l.rules, enabled_features))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (l.natives != null)
|
||||||
|
{
|
||||||
|
string? nativesKey;
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
nativesKey = l.natives.windows;
|
||||||
|
else if (OperatingSystem.IsLinux())
|
||||||
|
nativesKey = l.natives.linux;
|
||||||
|
else if (OperatingSystem.IsMacOS())
|
||||||
|
nativesKey = l.natives.osx;
|
||||||
|
else throw new PlatformNotSupportedException();
|
||||||
|
if(nativesKey is null)
|
||||||
|
throw new Exception($"nativesKey for '{l.name}' is null");
|
||||||
|
|
||||||
|
Artifact artifact = null!;
|
||||||
|
if(l.downloads.classifiers != null && !l.downloads.classifiers.TryGetValue(nativesKey, out artifact!))
|
||||||
|
throw new Exception($"can't find artifact for '{l.name}' with nativesKey '{nativesKey}'");
|
||||||
|
|
||||||
|
// skipping duplicates (WHO THE HELL CREATES THIS DISCRIPTORS AAAAAAAAA)
|
||||||
|
if(!libHashes.Add(artifact.sha1))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
IOPath dir = Пути.GetVersionDir(descriptor.id);
|
||||||
|
IOPath jarPath = Path.Concat(dir, Path.ReplaceRestrictedChars(l.name));
|
||||||
|
libs.Add(new NativeLib(l.name, jarPath, artifact, dir, l.extract));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Artifact? artifact = l.downloads.artifact;
|
||||||
|
if (artifact == null)
|
||||||
|
throw new NullReferenceException($"artifact for '{l.name}' is null");
|
||||||
|
|
||||||
|
// skipping duplicates
|
||||||
|
if(!libHashes.Add(artifact.sha1))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
IOPath path = artifact.url.AsSpan().After("://").After('/').ToString();
|
||||||
|
libs.Add(new JarLib(l.name, Path.Concat(Пути.GetLibrariesDir(), path), artifact));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Libs = libs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
namespace Млаумчерб.Клиент.классы;
|
namespace Млаумчерб.Клиент.классы;
|
||||||
|
|
||||||
public class MinecraftVersionDescriptor
|
public class VersionDescriptor
|
||||||
{
|
{
|
||||||
[JsonRequired] public string id { get; set; } = "";
|
[JsonRequired] public string id { get; set; } = "";
|
||||||
[JsonRequired] public DateTime time { get; set; }
|
[JsonRequired] public DateTime time { get; set; }
|
||||||
@ -39,22 +39,10 @@ public class Rule
|
|||||||
public Dictionary<string, bool>? features { get; set; }
|
public Dictionary<string, bool>? features { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Classifiers
|
|
||||||
{
|
|
||||||
[JsonProperty("natives-linux")]
|
|
||||||
public Artifact? nativeslinux { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("natives-osx")]
|
|
||||||
public Artifact? nativesosx { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("natives-windows")]
|
|
||||||
public Artifact? nativeswindows { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LibraryDownloads
|
public class LibraryDownloads
|
||||||
{
|
{
|
||||||
public Artifact? artifact { get; set; }
|
public Artifact? artifact { get; set; }
|
||||||
public Classifiers? classifiers { get; set; }
|
public Dictionary<string, Artifact>? classifiers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Extract
|
public class Extract
|
||||||
@ -9,7 +9,7 @@ public static class Буржуазия
|
|||||||
os.name switch
|
os.name switch
|
||||||
{
|
{
|
||||||
null => true,
|
null => true,
|
||||||
"osx" => OperatingSystem.IsWindows(),
|
"osx" => OperatingSystem.IsMacOS(),
|
||||||
"linux" => OperatingSystem.IsLinux(),
|
"linux" => OperatingSystem.IsLinux(),
|
||||||
"windows" => OperatingSystem.IsWindows(),
|
"windows" => OperatingSystem.IsWindows(),
|
||||||
_ => throw new ArgumentOutOfRangeException(os.name)
|
_ => throw new ArgumentOutOfRangeException(os.name)
|
||||||
|
|||||||
@ -16,12 +16,23 @@ public static class Пути
|
|||||||
public static IOPath GetVersionDescriptorPath(string name) =>
|
public static IOPath GetVersionDescriptorPath(string name) =>
|
||||||
Path.Concat(GetVersionDescriptorDir(), Path.ReplaceRestrictedChars(name) + ".json");
|
Path.Concat(GetVersionDescriptorDir(), Path.ReplaceRestrictedChars(name) + ".json");
|
||||||
|
|
||||||
public static IOPath GetVersionDir() =>
|
public static IOPath GetVersionDir(string id) =>
|
||||||
Path.Concat(Приложение.Настройки.путь_к_кубачу, "versions");
|
Path.Concat(Приложение.Настройки.путь_к_кубачу, "versions", id);
|
||||||
|
|
||||||
public static IOPath GetVersionJarFilePath(string name) =>
|
public static IOPath GetVersionJarFilePath(string id) =>
|
||||||
Path.Concat(GetVersionDir(), name + ".jar");
|
Path.Concat(GetVersionDir(id), id + ".jar");
|
||||||
|
|
||||||
public static IOPath GetLibrariesDir() =>
|
public static IOPath GetLibrariesDir() =>
|
||||||
Path.Concat(Приложение.Настройки.путь_к_кубачу, "libraries");
|
Path.Concat(Приложение.Настройки.путь_к_кубачу, "libraries");
|
||||||
|
|
||||||
|
public static IOPath GetJavaRuntimesDir() =>
|
||||||
|
Path.Concat(Приложение.Настройки.путь_к_кубачу, "java");
|
||||||
|
|
||||||
|
|
||||||
|
public static IOPath GetJavaRuntimeDir(string id) =>
|
||||||
|
Path.Concat(GetJavaRuntimesDir(), id);
|
||||||
|
|
||||||
|
public static IOPath GetJavaExecutablePath(string id) =>
|
||||||
|
Path.Concat(GetJavaRuntimeDir(id), "bin",
|
||||||
|
OperatingSystem.IsWindows() ? "javaw.exe" : "javaw");
|
||||||
}
|
}
|
||||||
|
|||||||
23
Млаумчерб.Клиент/сеть/DataSize.cs
Normal file
23
Млаумчерб.Клиент/сеть/DataSize.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace Млаумчерб.Клиент.сеть;
|
||||||
|
|
||||||
|
public record struct DataSize(long Bytes)
|
||||||
|
{
|
||||||
|
static string BytesToHumanReadable(long bytes)
|
||||||
|
{
|
||||||
|
long K = bytes / 1024;
|
||||||
|
if (K == 0)
|
||||||
|
return $"{bytes}b";
|
||||||
|
float M = K / 1024f;
|
||||||
|
if (M < 1)
|
||||||
|
return $"{K:N1}Kb";
|
||||||
|
float G = M / 1024f;
|
||||||
|
if (G < 1)
|
||||||
|
return $"{M:N1}Mb";
|
||||||
|
return $"{G:N1}Gb";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => BytesToHumanReadable(Bytes);
|
||||||
|
|
||||||
|
public static implicit operator DataSize(long bytes) => new DataSize(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
62
Млаумчерб.Клиент/сеть/NetworkProgressReporter.cs
Normal file
62
Млаумчерб.Клиент/сеть/NetworkProgressReporter.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using Млаумчерб.Клиент.видимое;
|
||||||
|
using Timer = DTLib.Timer;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.сеть;
|
||||||
|
|
||||||
|
public record struct DownloadProgress(DataSize Downloaded, DataSize Total, DataSize PerSecond)
|
||||||
|
{
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Downloaded}/{Total} ({PerSecond}/s)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NetworkProgressReporter : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Action<DownloadProgress> _reportProgressDelegate;
|
||||||
|
private long _totalSize;
|
||||||
|
private long _curSize;
|
||||||
|
private long _prevSize;
|
||||||
|
private int _timerDelay = 1000;
|
||||||
|
private Timer _timer;
|
||||||
|
|
||||||
|
public NetworkProgressReporter(long totalSize, Action<DownloadProgress> reportProgressDelegate)
|
||||||
|
{
|
||||||
|
_totalSize = totalSize;
|
||||||
|
_reportProgressDelegate = reportProgressDelegate;
|
||||||
|
_timer = new Timer(true, _timerDelay, ReportProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// atomic add
|
||||||
|
public void AddBytesCount(ArraySegment<byte> chunk)
|
||||||
|
{
|
||||||
|
long chunkSize = chunk.Count;
|
||||||
|
Interlocked.Add(ref _curSize, chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_timer.Stop();
|
||||||
|
_timer.InvokeAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReportProgress()
|
||||||
|
{
|
||||||
|
long bytesPerSec = (_curSize - _prevSize) / (_timerDelay / 1000);
|
||||||
|
_prevSize = _curSize;
|
||||||
|
var p = new DownloadProgress(_curSize, _totalSize, bytesPerSec);
|
||||||
|
Приложение.Логгер.LogDebug(nameof(ReportProgress),
|
||||||
|
$"download progress {p}");
|
||||||
|
_reportProgressDelegate(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Млаумчерб.Клиент/сеть/NetworkTask.cs
Normal file
71
Млаумчерб.Клиент/сеть/NetworkTask.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
namespace Млаумчерб.Клиент.сеть;
|
||||||
|
|
||||||
|
public class NetworkTask : IDisposable
|
||||||
|
{
|
||||||
|
public readonly string Name;
|
||||||
|
|
||||||
|
public enum Status
|
||||||
|
{
|
||||||
|
Initialized,
|
||||||
|
Running,
|
||||||
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
Failed
|
||||||
|
};
|
||||||
|
|
||||||
|
public Status DownloadStatus { get; private set; } = Status.Initialized;
|
||||||
|
|
||||||
|
public event Action<DownloadProgress>? OnProgress;
|
||||||
|
public event Action<Status>? OnStop;
|
||||||
|
|
||||||
|
public delegate Task DownloadAction(NetworkProgressReporter progressReporter, CancellationToken ct);
|
||||||
|
|
||||||
|
private readonly DownloadAction _downloadAction;
|
||||||
|
private CancellationTokenSource _cts = new();
|
||||||
|
private NetworkProgressReporter _progressReporter;
|
||||||
|
|
||||||
|
public NetworkTask(string name, long dataSize, DownloadAction downloadAction)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
_downloadAction = downloadAction;
|
||||||
|
_progressReporter = new NetworkProgressReporter(dataSize, ReportProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReportProgress(DownloadProgress p) => OnProgress?.Invoke(p);
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
if(DownloadStatus == Status.Running || DownloadStatus == Status.Completed)
|
||||||
|
return;
|
||||||
|
DownloadStatus = Status.Running;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_progressReporter.Start();
|
||||||
|
await _downloadAction(_progressReporter, _cts.Token);
|
||||||
|
DownloadStatus = Status.Completed;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DownloadStatus = Status.Failed;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_progressReporter.Stop();
|
||||||
|
OnStop?.Invoke(DownloadStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
DownloadStatus = Status.Cancelled;
|
||||||
|
_cts.Cancel();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts.Dispose();
|
||||||
|
_progressReporter.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using DTLib.Extensions;
|
||||||
|
using Млаумчерб.Клиент.видимое;
|
||||||
|
using Млаумчерб.Клиент.классы;
|
||||||
|
using static Млаумчерб.Клиент.сеть.Сеть;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.сеть.NetworkTaskFactories;
|
||||||
|
|
||||||
|
public class AssetsDownloadTaskFactory : INetworkTaskFactory
|
||||||
|
{
|
||||||
|
private const string ASSET_SERVER_URL = "https://resources.download.minecraft.net/";
|
||||||
|
private VersionDescriptor _descriptor;
|
||||||
|
private SHA1 _hasher;
|
||||||
|
private IOPath _indexFilePath;
|
||||||
|
List<AssetDownloadProperties> _assetsToDownload = new();
|
||||||
|
|
||||||
|
public AssetsDownloadTaskFactory(VersionDescriptor descriptor)
|
||||||
|
{
|
||||||
|
_descriptor = descriptor;
|
||||||
|
_hasher = SHA1.Create();
|
||||||
|
_indexFilePath = Пути.GetAssetIndexFilePath(_descriptor.assetIndex.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NetworkTask?> CreateAsync(bool checkHashes)
|
||||||
|
{
|
||||||
|
if (!await CheckFilesAsync(checkHashes))
|
||||||
|
return new NetworkTask(
|
||||||
|
$"assets '{_descriptor.assetIndex.id}'",
|
||||||
|
GetTotalSize(),
|
||||||
|
Download
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private async Task<bool> CheckFilesAsync(bool checkHashes)
|
||||||
|
{
|
||||||
|
if(!File.Exists(_indexFilePath))
|
||||||
|
{
|
||||||
|
Приложение.Логгер.LogInfo(nameof(Сеть), $"started downloading asset index to '{_indexFilePath}'");
|
||||||
|
await DownloadFileHTTP(_descriptor.assetIndex.url, _indexFilePath);
|
||||||
|
Приложение.Логгер.LogInfo(nameof(Сеть), "finished downloading asset index");
|
||||||
|
}
|
||||||
|
|
||||||
|
string indexFileText = File.ReadAllText(_indexFilePath);
|
||||||
|
var assetIndex = JsonConvert.DeserializeObject<AssetIndex>(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<string> assetHashes = new HashSet<string>();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Приложение.Логгер.LogInfo(nameof(Сеть), "started downloading assets");
|
||||||
|
ParallelOptions opt = new() { MaxDegreeOfParallelism = ParallelDownloadsN, CancellationToken = ct };
|
||||||
|
await Parallel.ForEachAsync(_assetsToDownload, opt,
|
||||||
|
async (a, _ct) =>
|
||||||
|
{
|
||||||
|
Приложение.Логгер.LogDebug(nameof(Сеть), $"downloading asset '{a.name}' {a.hash}");
|
||||||
|
await DownloadFileHTTP(a.url, a.filePath, _ct, pr.AddBytesCount);
|
||||||
|
});
|
||||||
|
Приложение.Логгер.LogInfo(nameof(Сеть), "finished downloading assets");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace Млаумчерб.Клиент.сеть.NetworkTaskFactories;
|
||||||
|
|
||||||
|
public interface INetworkTaskFactory
|
||||||
|
{
|
||||||
|
/// <returns>unstarted network task or null if there is nothing to download</returns>
|
||||||
|
Task<NetworkTask?> CreateAsync(bool checkHashes);
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using Млаумчерб.Клиент.классы;
|
||||||
|
using static Млаумчерб.Клиент.сеть.Сеть;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.сеть.NetworkTaskFactories;
|
||||||
|
|
||||||
|
public class JavaDownloadTaskFactory : INetworkTaskFactory
|
||||||
|
{
|
||||||
|
private VersionDescriptor _descriptor;
|
||||||
|
private SHA1 _hasher;
|
||||||
|
IOPath _javaVersionDir;
|
||||||
|
|
||||||
|
public JavaDownloadTaskFactory(VersionDescriptor descriptor)
|
||||||
|
{
|
||||||
|
_descriptor = descriptor;
|
||||||
|
_hasher = SHA1.Create();
|
||||||
|
_javaVersionDir = Пути.GetJavaRuntimeDir(_descriptor.javaVersion.component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<NetworkTask?> CreateAsync(bool checkHashes)
|
||||||
|
{
|
||||||
|
NetworkTask? networkTask = null;
|
||||||
|
if (!CheckFiles(checkHashes))
|
||||||
|
networkTask = new(
|
||||||
|
$"java runtime '{_descriptor.javaVersion.component}'",
|
||||||
|
GetTotalSize(),
|
||||||
|
Download
|
||||||
|
);
|
||||||
|
return Task.FromResult(networkTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckFiles(bool checkHashes)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long GetTotalSize()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task Download(NetworkProgressReporter pr, CancellationToken ct)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using DTLib.Extensions;
|
||||||
|
using Млаумчерб.Клиент.видимое;
|
||||||
|
using Млаумчерб.Клиент.классы;
|
||||||
|
using static Млаумчерб.Клиент.сеть.Сеть;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.сеть.NetworkTaskFactories;
|
||||||
|
|
||||||
|
public class LibrariesDownloadTaskFactory : INetworkTaskFactory
|
||||||
|
{
|
||||||
|
private VersionDescriptor _descriptor;
|
||||||
|
private Libraries _libraries;
|
||||||
|
private SHA1 _hasher;
|
||||||
|
private List<Libraries.JarLib> _libsToDownload = new();
|
||||||
|
|
||||||
|
public LibrariesDownloadTaskFactory(VersionDescriptor descriptor, Libraries libraries)
|
||||||
|
{
|
||||||
|
_descriptor = descriptor;
|
||||||
|
_libraries = libraries;
|
||||||
|
_hasher = SHA1.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<NetworkTask?> CreateAsync(bool checkHashes)
|
||||||
|
{
|
||||||
|
NetworkTask? networkTask = null;
|
||||||
|
if (!CheckFiles(checkHashes))
|
||||||
|
networkTask = new NetworkTask(
|
||||||
|
$"libraries '{_descriptor.id}'",
|
||||||
|
GetTotalSize(),
|
||||||
|
Download
|
||||||
|
);
|
||||||
|
return Task.FromResult(networkTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckFiles(bool checkHashes)
|
||||||
|
{
|
||||||
|
_libsToDownload.Clear();
|
||||||
|
|
||||||
|
foreach (var l in _libraries.Libs)
|
||||||
|
{
|
||||||
|
if (!File.Exists(l.jarFilePath))
|
||||||
|
{
|
||||||
|
_libsToDownload.Add(l);
|
||||||
|
}
|
||||||
|
else if (checkHashes)
|
||||||
|
{
|
||||||
|
using var fs = File.OpenRead(l.jarFilePath);
|
||||||
|
string hash = _hasher.ComputeHash(fs).HashToString();
|
||||||
|
if(hash != l.artifact.sha1)
|
||||||
|
_libsToDownload.Add(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _libsToDownload.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long GetTotalSize()
|
||||||
|
{
|
||||||
|
long total = 0;
|
||||||
|
foreach (var l in _libsToDownload)
|
||||||
|
total += l.artifact.size;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Download(NetworkProgressReporter pr, CancellationToken ct)
|
||||||
|
{
|
||||||
|
Приложение.Логгер.LogInfo(nameof(Сеть), "started downloading libraries");
|
||||||
|
ParallelOptions opt = new() { MaxDegreeOfParallelism = ParallelDownloadsN, CancellationToken = ct };
|
||||||
|
await Parallel.ForEachAsync(_libsToDownload, opt, async (l, _ct) =>
|
||||||
|
{
|
||||||
|
Приложение.Логгер.LogDebug(nameof(Сеть),
|
||||||
|
$"downloading library '{l.name}' to '{l.jarFilePath}'");
|
||||||
|
await DownloadFileHTTP(l.artifact.url, l.jarFilePath, _ct, pr.AddBytesCount);
|
||||||
|
//TODO: extract natives from jar
|
||||||
|
});
|
||||||
|
Приложение.Логгер.LogInfo(nameof(Сеть), "finished downloading libraries");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using DTLib.Extensions;
|
||||||
|
using Млаумчерб.Клиент.классы;
|
||||||
|
using static Млаумчерб.Клиент.сеть.Сеть;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.сеть.NetworkTaskFactories;
|
||||||
|
|
||||||
|
public class VersionFileDownloadTaskFactory : INetworkTaskFactory
|
||||||
|
{
|
||||||
|
private VersionDescriptor _descriptor;
|
||||||
|
private IOPath _filePath;
|
||||||
|
private SHA1 _hasher;
|
||||||
|
|
||||||
|
public VersionFileDownloadTaskFactory(VersionDescriptor descriptor)
|
||||||
|
{
|
||||||
|
_descriptor = descriptor;
|
||||||
|
_filePath = Пути.GetVersionJarFilePath(_descriptor.id);
|
||||||
|
_hasher = SHA1.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<NetworkTask?> CreateAsync(bool checkHashes)
|
||||||
|
{
|
||||||
|
NetworkTask? networkTask = null;
|
||||||
|
if (!CheckFiles(checkHashes))
|
||||||
|
networkTask = new NetworkTask(
|
||||||
|
$"version file '{_descriptor.id}'",
|
||||||
|
GetTotalSize(),
|
||||||
|
Download
|
||||||
|
);
|
||||||
|
return Task.FromResult(networkTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckFiles(bool checkHashes)
|
||||||
|
{
|
||||||
|
if (!File.Exists(_filePath))
|
||||||
|
return false;
|
||||||
|
if (!checkHashes)
|
||||||
|
return true;
|
||||||
|
using var fs = File.OpenRead(_filePath);
|
||||||
|
string hash = _hasher.ComputeHash(fs).HashToString();
|
||||||
|
return hash == _descriptor.downloads.client.sha1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long GetTotalSize()
|
||||||
|
{
|
||||||
|
return _descriptor.downloads.client.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task Download(NetworkProgressReporter pr, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return DownloadFileHTTP(_descriptor.downloads.client.url, _filePath, ct, pr.AddBytesCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
97
Млаумчерб.Клиент/сеть/Сеть.cs
Normal file
97
Млаумчерб.Клиент/сеть/Сеть.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Млаумчерб.Клиент.видимое;
|
||||||
|
using Млаумчерб.Клиент.классы;
|
||||||
|
|
||||||
|
namespace Млаумчерб.Клиент.сеть;
|
||||||
|
|
||||||
|
public static class Сеть
|
||||||
|
{
|
||||||
|
public static int ParallelDownloadsN = 32;
|
||||||
|
public static HttpClient http = new();
|
||||||
|
|
||||||
|
public static async Task DownloadFileHTTP(string url, IOPath outPath, CancellationToken ct = default,
|
||||||
|
Action<ArraySegment<byte>>? transformFunc = null)
|
||||||
|
{
|
||||||
|
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<ArraySegment<byte>>? transformFunc = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// default dotnet runtime buffer size
|
||||||
|
int bufferSize = 81920;
|
||||||
|
byte[] readBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||||
|
byte[] writeBuffer = ArrayPool<byte>.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<byte>(writeBuffer, 0, readCount));
|
||||||
|
dst.Write(writeBuffer, 0, readCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(readBuffer);
|
||||||
|
ArrayPool<byte>.Shared.Return(writeBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] VERSION_MANIFEST_URLS =
|
||||||
|
{
|
||||||
|
"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static async Task<List<RemoteVersionDescriptorProps>> GetRemoteVersionDescriptorsAsync()
|
||||||
|
{
|
||||||
|
List<RemoteVersionDescriptorProps> descriptors = new();
|
||||||
|
foreach (var url in VERSION_MANIFEST_URLS)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var manifestText = await http.GetStringAsync(url);
|
||||||
|
var catalog = JsonConvert.DeserializeObject<VersionCatalog>(manifestText);
|
||||||
|
if (catalog != null)
|
||||||
|
descriptors.AddRange(catalog.versions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Приложение.Логгер.LogWarn(nameof(Сеть), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<GameVersionProps>? _versionPropsList;
|
||||||
|
|
||||||
|
/// <returns>empty list if couldn't find any remote versions</returns>
|
||||||
|
public static async Task<IReadOnlyList<GameVersionProps>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user