game assets downloading

This commit is contained in:
Timerix 2024-09-24 16:18:06 +05:00
parent 581c4b5498
commit c2e2785a32
20 changed files with 690 additions and 350 deletions

View File

@ -2,9 +2,12 @@
public class LauncherLogger : FileLogger
{
public static readonly IOPath LogsDirectory = "launcher-logs";
public static readonly IOPath LogsDirectory = "launcher_logs";
public LauncherLogger() : base(LogsDirectory, "млаумчерб")
{
#if DEBUG
DebugLogEnabled = true;
#endif
}
}

View File

@ -1,72 +1,191 @@
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Buffers;
using System.Net.Http;
using Млаумчерб.Клиент.классы;
using Timer = DTLib.Timer;
namespace Млаумчерб.Клиент;
public record struct NetworkTransferResult(long BytesTotal, long BytesTransferred, long BytesPerSecond)
{
public override string ToString()
{
return $"transferred {BytesTransferred}/{BytesTotal} bytes ({BytesPerSecond}) bps";
}
}
public class NetworkTransferTask
{
public Task Task { get; private set; }
public Progress<NetworkTransferResult> Progress { get; private set; }
private Stream _src;
private Stream _dst;
private CancellationTokenSource _cts;
private DTLib.Timer _timer;
public NetworkTransferTask(Stream src, Stream dst)
{
_src = src;
_dst = dst;
_cts = new CancellationTokenSource();
Progress = new Progress<NetworkTransferResult>();
_timer = new(true, 1000, ReportProgress);
Task = Task.CompletedTask;
}
public void Start()
{
_timer.Start();
Task = _src.CopyToAsync(_dst);
_timer.Stop();
}
public void Stop()
{
_cts.Cancel();
_timer.Stop();
}
private long previousBytesTransferred;
private void ReportProgress()
{
long bytesTotal = _src.Length, bytesTransferred = _src.Position;
long bytesPerSecond = bytesTransferred - previousBytesTransferred;
previousBytesTransferred = bytesTransferred;
((IProgress<NetworkTransferResult>)Progress).Report(new NetworkTransferResult(bytesTotal, bytesTransferred, bytesPerSecond));
}
public TaskAwaiter GetAwaiter() => Task.GetAwaiter();
}
public static class NetworkHelper
public static class Network
{
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 NetworkTransferTask DownloadHTTPFileAsync(string url, Stream destinationStream)
public static async Task DownloadFileHTTP(string url, IOPath outPath,
Action<ArraySegment<byte>>? transformFunc = null, CancellationToken ct = default)
{
var sourceStream = http.GetStreamAsync(url).GetAwaiter().GetResult();
NetworkTransferTask task = new(sourceStream, destinationStream);
task.Start();
return task;
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(Network), 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)
{
}
}

View File

@ -0,0 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Млаумчерб.Клиент"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Млаумчерб.Клиент.VersionItemView">
<TextBlock Name="text" Background="Transparent"/>
</UserControl>

View File

@ -0,0 +1,31 @@
using Avalonia.Controls;
using Avalonia.Media;
using Млаумчерб.Клиент.классы;
namespace Млаумчерб.Клиент;
public partial class VersionItemView : ListBoxItem
{
public GameVersionProps Props { get; }
private SolidColorBrush _avaliableColor = new(Color.FromRgb(30, 130, 40));
private SolidColorBrush _unavaliableColor = new(Color.FromRgb(170, 70, 70));
public VersionItemView()
{
throw new NotImplementedException();
}
public VersionItemView(GameVersionProps props)
{
Props = props;
InitializeComponent();
text.Text = props.Name;
props.DownloadCompleted += UpdateBackground;
UpdateBackground();
}
private void UpdateBackground()
{
Background = Props.IsDownloaded ? _avaliableColor : _unavaliableColor;
}
}

View File

@ -1,7 +1,11 @@
global using System;
global using System.Collections;
global using System.Collections.Generic;
global using System.IO;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
global using Newtonsoft.Json;
global using DTLib.Logging;
global using DTLib.Filesystem;
global using File = DTLib.Filesystem.File;
@ -17,9 +21,16 @@ public class Главне
[STAThread]
public static void Main(string[] args)
{
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
try
{
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
catch (Exception ex)
{
Приложение.Логгер.LogError(nameof(Главне), ex);
}
}
// Avalonia configuration, don't remove; also used by visual designer.

View File

@ -1,56 +1,85 @@
using System.Threading;
using System.Threading.Tasks;
using CliWrap;
using CliWrap;
using DTLib.Extensions;
using Newtonsoft.Json;
using Млаумчерб.Клиент.классы;
using static Млаумчерб.Клиент.классы.Пролетариат;
namespace Млаумчерб.Клиент;
public interface IGame
public class GameVersionDescriptor
{
string Name { get; }
IOPath InstallationDirectory { get; }
Progress<NetworkTransferResult> BeginUpdate();
void CancelUpdate();
Task Launch();
void Close();
}
public class MinecraftVersion : IGame
{
public string Name { get; }
public IOPath InstallationDirectory { get; }
private readonly GameVersionProps _props;
public string Name => _props.Name;
public IOPath WorkingDirectory { get; }
private IOPath JavaExecutableFilePath;
private MinecraftVersionDescriptor descriptor;
private JavaArguments javaArgs;
private GameArguments gameArgs;
private CancellationTokenSource? cts;
private CancellationTokenSource? gameCts;
private CancellationTokenSource? downloadCts;
private CommandTask<CommandResult>? commandTask;
public MinecraftVersion(IOPath descriptorFilePath)
public static async Task<List<GameVersionProps>> GetAllVersionsAsync()
{
Name = descriptorFilePath.LastName().ToString().Replace(".json", "");
InstallationDirectory = Path.Concat(Приложение.Настройки.путь_к_кубачу, Name);
string descriptorText = File.ReadAllText(descriptorFilePath);
var propsList = new List<GameVersionProps>();
foreach (IOPath f in Directory.GetFiles(GetVersionDescriptorDir()))
{
string name = GetVersionDescriptorName(f);
propsList.Add(new GameVersionProps(name, null, f));
}
var remoteVersions = await Network.GetDownloadableVersions();
propsList.AddRange(remoteVersions);
return propsList;
}
public static async Task<GameVersionDescriptor> CreateFromPropsAsync(GameVersionProps props)
{
if(!File.Exists(props.LocalDescriptorPath))
{
if (props.RemoteDescriptorUrl is null)
throw new NullReferenceException("can't download game version descriptor '"
+ props.Name + "', because RemoteDescriptorUrl is null");
await Network.DownloadFileHTTP(props.RemoteDescriptorUrl, props.LocalDescriptorPath);
}
return new GameVersionDescriptor(props);
}
private GameVersionDescriptor(GameVersionProps props)
{
_props = props;
WorkingDirectory = Path.Concat(Приложение.Настройки.путь_к_кубачу, Name);
string descriptorText = File.ReadAllText(props.LocalDescriptorPath);
descriptor = JsonConvert.DeserializeObject<MinecraftVersionDescriptor>(descriptorText)
?? throw new Exception($"can't parse descriptor file '{descriptorFilePath}'");
?? throw new Exception($"can't parse descriptor file '{props.LocalDescriptorPath}'");
javaArgs = new JavaArguments(descriptor);
gameArgs = new GameArguments(descriptor);
JavaExecutableFilePath = Path.Concat(Приложение.Настройки.путь_к_жабе, "bin",
OperatingSystem.IsWindows() ? "javaw.exe" : "javaw");
}
public Progress<NetworkTransferResult> BeginUpdate()
public async void BeginUpdate(bool force)
{
throw new NotImplementedException();
try
{
downloadCts = new CancellationTokenSource();
if(Приложение.Настройки.скачатьабу)
await Network.DownloadJava(descriptor.javaVersion, Приложение.Настройки.путь_к_жабе, force);
await Network.DownloadAssets(descriptor.assetIndex, downloadCts.Token, force);
await Network.DownloadVersionFile(descriptor.downloads.client.url, GetVersionJarFilePath(Name), force);
await Network.DownloadLibraries(descriptor.libraries, GetLibrariesDir(), force);
// await Network.DownloadModpack(modpack, WorkingDirectory, force);
_props.IsDownloaded = true;
}
catch (Exception ex)
{
Ошибки.ПоказатьСообщение("GameUpdate", ex);
}
}
public void CancelUpdate()
{
throw new NotImplementedException();
downloadCts?.Cancel();
}
public async Task Launch()
@ -58,24 +87,24 @@ public class MinecraftVersion : IGame
var javaArgsList = javaArgs.FillPlaceholders([]);
var gameArgsList = gameArgs.FillPlaceholders([]);
var command = Cli.Wrap(JavaExecutableFilePath.ToString())
.WithWorkingDirectory(InstallationDirectory.ToString())
.WithWorkingDirectory(WorkingDirectory.ToString())
.WithArguments(javaArgsList)
.WithArguments(gameArgsList);
Приложение.Логгер.LogInfo(nameof(MinecraftVersion),
Приложение.Логгер.LogInfo(nameof(GameVersionDescriptor),
$"launching the game" +
"\njava: " + command.TargetFilePath +
"\nworking_dir: " + command.WorkingDirPath +
"\njava_arguments: \n\t" + javaArgsList.MergeToString("\n\t") +
"\ngame_arguments: \n\t" + gameArgsList.MergeToString("\n\t"));
cts = new();
commandTask = command.ExecuteAsync(cts.Token);
gameCts = new();
commandTask = command.ExecuteAsync(gameCts.Token);
var result = await commandTask;
Приложение.Логгер.LogInfo(nameof(MinecraftVersion), $"game exited with code {result.ExitCode}");
Приложение.Логгер.LogInfo(nameof(GameVersionDescriptor), $"game exited with code {result.ExitCode}");
}
public void Close()
{
cts?.Cancel();
gameCts?.Cancel();
}
public override string ToString() => Name;

View File

@ -10,6 +10,8 @@
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>капитал\кубе.ico</ApplicationIcon>
<AssemblyName>млаумчерб</AssemblyName>
<Configurations>Release;Debug</Configurations>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
@ -19,7 +21,7 @@
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.*" />
<PackageReference Include="Avalonia.Labs.Gif" Version="11.2.999-cibuild-00051673"/>
<PackageReference Include="CliWrap" Version="3.6.*" />
<PackageReference Include="DTLib" Version="1.4.0" />
<PackageReference Include="DTLib" Version="1.4.1" />
<PackageReference Include="MessageBox.Avalonia" Version="3.1.*" />
<PackageReference Include="Newtonsoft.Json" Version="13.*" />
</ItemGroup>

View File

@ -1,6 +1,4 @@
using Newtonsoft.Json;
namespace Млаумчерб.Клиент;
namespace Млаумчерб.Клиент;
public record Настройки
{
@ -10,6 +8,7 @@ public record Настройки
public string путь_к_кубачу { get; set; } = ".";
public string путь_к_жабе { get; set; } = "java";
public bool скачатьабу { get; set; } = true;
public string? последняяапущенная_версия;
public static Настройки ЗагрузитьИзФайла(string имяайла = "млаумчерб.настройки")
{
@ -27,7 +26,7 @@ public record Настройки
File.Move(имяайла, имяайла + ".старые", true);
н = new Настройки();
н.СохранитьВФайл();
Ошибки.ПоказатьСообщение($"Не удалось прочитать настройки.\n" +
Ошибки.ПоказатьСообщение("Настройки", $"Не удалось прочитать настройки.\n" +
$"Сломанный файл настроек переименован в '{имя_файла}.старые'.\n" +
$"Создан новый файл '{имя_файла}'.");
}

View File

@ -61,8 +61,8 @@
Fullscreen
</CheckBox>
<CheckBox IsChecked="{Binding #window.UpdateGameFiles}">
Update game files
<CheckBox IsChecked="{Binding #window.ForceUpdateGameFiles}">
Force update game files
</CheckBox>
</StackPanel>
<Button Grid.Row="1" Margin="10" Padding="0 0 0 4"

View File

@ -3,6 +3,8 @@ using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Млаумчерб.Клиент.классы;
namespace Млаумчерб.Клиент;
@ -35,13 +37,13 @@ public partial class Окне : Window
set => SetValue(FullscreenProperty, value);
}
public static readonly StyledProperty<bool> UpdateGameFilesProperty =
AvaloniaProperty.Register<Окне, bool>(nameof(UpdateGameFiles),
defaultBindingMode: BindingMode.TwoWay, defaultValue: true);
public bool UpdateGameFiles
public static readonly StyledProperty<bool> ForceUpdateGameFilesProperty =
AvaloniaProperty.Register<Окне, bool>(nameof(ForceUpdateGameFiles),
defaultBindingMode: BindingMode.TwoWay, defaultValue: false);
public bool ForceUpdateGameFiles
{
get => GetValue(UpdateGameFilesProperty);
set => SetValue(UpdateGameFilesProperty, value);
get => GetValue(ForceUpdateGameFilesProperty);
set => SetValue(ForceUpdateGameFilesProperty, value);
}
public Окне()
@ -49,7 +51,7 @@ public partial class Окне : Window
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
protected override async void OnLoaded(RoutedEventArgs e)
{
try
{
@ -58,58 +60,89 @@ public partial class Окне : Window
MemoryLimit = Приложение.Настройки.выделенная_память_мб;
Fullscreen = Приложение.Настройки.открыватьаесь_экран;
IOPath localDescriptorsDir = Path.Concat(Приложение.Настройки.путь_к_кубачу, "version_descriptors");
Directory.Create(localDescriptorsDir);
var descriptorFiles = Directory.GetFiles(localDescriptorsDir);
foreach(var descriptorFile in descriptorFiles)
Directory.Create(Пролетариат.GetVersionDescriptorDir());
VersionComboBox.SelectedIndex = 0;
VersionComboBox.IsEnabled = false;
var versions = await GameVersionDescriptor.GetAllVersionsAsync();
Dispatcher.UIThread.Invoke(() =>
{
MinecraftVersion mc = new(descriptorFile);
VersionComboBox.Items.Add(mc);
}
foreach (var p in versions)
{
VersionComboBox.Items.Add(new VersionItemView(p));
if (Приложение.Настройки.последняяапущенная_версия != null &&
p.Name == Приложение.Настройки.последняяапущенная_версия)
VersionComboBox.SelectedIndex = VersionComboBox.Items.Count - 1;
}
VersionComboBox.IsEnabled = true;
});
}
catch (Exception ex)
{
Ошибки.ПоказатьСообщение(ex);
Ошибки.ПоказатьСообщение(nameof(Окне), ex);
}
}
private void LaunchButtonHandler(object? sender, RoutedEventArgs e)
private async void LaunchButtonHandler(object? sender, RoutedEventArgs e)
{
try
{
var selectedVersionView = (VersionItemView?)VersionComboBox.SelectedItem;
var selectedVersion = selectedVersionView?.Props;
Приложение.Настройки.последняяапущенная_версия = selectedVersion?.Name;
Приложение.Настройки.имя_пользователя = Username;
Приложение.Настройки.выделенная_память_мб = MemoryLimit;
Приложение.Настройки.открыватьаесь_экран = Fullscreen;
Приложение.Настройки.СохранитьВФайл();
if (selectedVersion == null)
return;
IGame? game = (IGame?)VersionComboBox.SelectionBoxItem;
if (game != null)
{
if (UpdateGameFiles)
{
var progress = game.BeginUpdate();
progress.ProgressChanged += (o, result) => Приложение.Логгер.LogDebug("Downloads", result);
}
}
var v = await GameVersionDescriptor.CreateFromPropsAsync(selectedVersion);
v.BeginUpdate(ForceUpdateGameFiles);
Dispatcher.UIThread.Invoke(() => ForceUpdateGameFiles = false);
}
catch (Exception ex)
{
Ошибки.ПоказатьСообщение(ex);
Ошибки.ПоказатьСообщение(nameof(Окне), ex);
}
}
private void OpenLogsDirectory(object? s, RoutedEventArgs e)
{
Launcher.LaunchDirectoryInfoAsync(new DirectoryInfo(LauncherLogger.LogsDirectory.ToString()));
try
{
Launcher.LaunchDirectoryInfoAsync(new DirectoryInfo(LauncherLogger.LogsDirectory.ToString()))
.ConfigureAwait(false);
}
catch (Exception ex)
{
Ошибки.ПоказатьСообщение(nameof(Окне), ex);
}
}
private void OpenLogFile(object? sender, RoutedEventArgs e)
{
Launcher.LaunchFileInfoAsync(new FileInfo(Приложение.Логгер.LogfileName.ToString()));
try
{
Launcher.LaunchFileInfoAsync(new FileInfo(Приложение.Логгер.LogfileName.ToString()))
.ConfigureAwait(false);
}
catch (Exception ex)
{
Ошибки.ПоказатьСообщение(nameof(Окне), ex);
}
}
private void OpenSourceRepository(object? sender, RoutedEventArgs e)
{
Launcher.LaunchUriAsync(new Uri("https://timerix.ddns.net:3322/Timerix/mlaumcherb"));
try
{
Launcher.LaunchUriAsync(new Uri("https://timerix.ddns.net:3322/Timerix/mlaumcherb"))
.ConfigureAwait(false);
}
catch (Exception ex)
{
Ошибки.ПоказатьСообщение(nameof(Окне), ex);
}
}
}

View File

@ -1,5 +1,5 @@
using Avalonia.Controls;
using DTLib.Ben.Demystifier;
using DTLib.Demystifier;
using MsBox.Avalonia;
using MsBox.Avalonia.Dto;
using MsBox.Avalonia.Enums;
@ -9,10 +9,10 @@ namespace Млаумчерб.Клиент;
public static class Ошибки
{
internal static void ПоказатьСообщение(Exception err)
=> ПоказатьСообщение(err.ToStringDemystified());
internal static void ПоказатьСообщение(string context, Exception err)
=> ПоказатьСообщение(context, err.ToStringDemystified());
internal static async void ПоказатьСообщение(string err)
internal static async void ПоказатьСообщение(string context, string err)
{
Приложение.Логгер.LogError(nameof(Ошибки), err);
var box = MessageBoxManager.GetMessageBoxCustom(new MessageBoxCustomParams

View File

@ -0,0 +1,31 @@
namespace Млаумчерб.Клиент.классы;
public class ArgumentsWithPlaceholders
{
protected List<string> raw_args = new();
public List<string> FillPlaceholders(Dictionary<string, string> values)
{
List<string> result = new();
foreach (var a in raw_args)
{
var f = a;
int begin = a.IndexOf('$');
if (begin != -1)
{
int keyBegin = begin + 2;
int end = a.IndexOf('}', keyBegin);
if (end != -1)
{
var key = a.Substring(keyBegin, end - keyBegin);
if (!values.TryGetValue(key, out var v))
throw new Exception($"can't find value for placeholder '{key}'");
f = v;
}
}
result.Add(f);
}
return result;
}
}

View File

@ -0,0 +1,27 @@
using DTLib.Extensions;
namespace Млаумчерб.Клиент.классы;
public class GameArguments : ArgumentsWithPlaceholders
{
private static readonly string[] _enabled_features =
[
"has_custom_resolution"
];
public GameArguments(MinecraftVersionDescriptor d)
{
if (d.minecraftArguments is not null)
{
raw_args.AddRange(d.minecraftArguments.SplitToList(' ', quot: '"'));
}
else if (d.arguments is not null)
{
foreach (var av in d.arguments.game)
{
if(Буржуазия.CheckRules(av.rules, _enabled_features))
raw_args.AddRange(av.value);
}
}
}
}

View File

@ -0,0 +1,32 @@
namespace Млаумчерб.Клиент.классы;
public class GameVersionProps
{
public string Name { get; }
public IOPath LocalDescriptorPath { get; }
public string? RemoteDescriptorUrl { get; }
private bool _isDownloaded;
public bool IsDownloaded
{
get => _isDownloaded;
set
{
_isDownloaded = value;
DownloadCompleted?.Invoke();
}
}
public event Action? DownloadCompleted;
public GameVersionProps(string name, string? url, IOPath descriptorPath)
{
Name = name;
LocalDescriptorPath = descriptorPath;
RemoteDescriptorUrl = url;
IsDownloaded = File.Exists(Пролетариат.GetVersionJarFilePath(name));
}
public GameVersionProps(string name, string? url) :
this(name, url, Пролетариат.GetVersionDescriptorPath(name)) { }
public override string ToString() => Name;
}

View File

@ -0,0 +1,27 @@
namespace Млаумчерб.Клиент.классы;
public class JavaArguments : ArgumentsWithPlaceholders
{
private static readonly string[] _initial_arguments =
[
];
private static readonly string[] _enabled_features =
[
];
public JavaArguments(MinecraftVersionDescriptor d)
{
raw_args.AddRange(_initial_arguments);
if (d.arguments is not null)
{
foreach (var av in d.arguments.jvm)
{
if(Буржуазия.CheckRules(av.rules, _enabled_features))
raw_args.AddRange(av.value);
}
}
}
}

View File

@ -0,0 +1,126 @@
using System.Linq;
// ReSharper disable CollectionNeverUpdated.Global
namespace Млаумчерб.Клиент.классы;
public class MinecraftVersionDescriptor
{
[JsonRequired] public string id { get; set; } = "";
[JsonRequired] public DateTime time { get; set; }
[JsonRequired] public DateTime releaseTime { get; set; }
[JsonRequired] public string type { get; set; } = "";
[JsonRequired] public string mainClass { get; set; } = "";
[JsonRequired] public Downloads downloads { get; set; } = null!;
[JsonRequired] public JavaVersion javaVersion { get; set; } = null!;
[JsonRequired] public List<Library> libraries { get; set; } = null!;
[JsonRequired] public AssetIndexProperties assetIndex { get; set; } = null!;
[JsonRequired] public string assets { get; set; } = "";
public string? minecraftArguments { get; set; }
public ArgumentsNew? arguments { get; set; }
}
public class Artifact
{
[JsonRequired] public string url { get; set; } = "";
[JsonRequired] public string sha1 { get; set; } = "";
[JsonRequired] public int size { get; set; }
}
public class Os
{
public string? name { get; set; }
public string? arch { get; set; }
}
public class Rule
{
[JsonRequired] public string action { get; set; } = "";
public Os? os { 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 Artifact? artifact { get; set; }
public Classifiers? classifiers { get; set; }
}
public class Extract
{
public List<string>? exclude { get; set; }
}
public class Natives
{
public string? linux { get; set; }
public string? osx { get; set; }
public string? windows { get; set; }
}
public class Library
{
[JsonRequired] public string name { get; set; } = "";
public List<Rule>? rules { get; set; }
public Natives? natives { get; set; }
public Extract? extract { get; set; }
[JsonRequired] public LibraryDownloads downloads { get; set; } = null!;
}
public class AssetIndexProperties
{
[JsonRequired] public string id { get; set; } = "";
[JsonRequired] public string url { get; set; } = "";
[JsonRequired] public string sha1 { get; set; } = "";
[JsonRequired] public int size { get; set; }
[JsonRequired] public int totalSize { get; set; }
}
public class Downloads
{
[JsonRequired] public Artifact client { get; set; } = null!;
}
public class JavaVersion
{
[JsonRequired] public string component { get; set; } = "";
[JsonRequired] public int majorVersion { get; set; }
}
public class ArgValue
{
public struct StringOrArray : IEnumerable<string>
{
private string[] ar;
public StringOrArray(ICollection<string> v) => ar = v.ToArray();
public StringOrArray(string v) => ar = [v];
public static implicit operator StringOrArray(string[] v) => new(v);
public static implicit operator StringOrArray(string v) => new(v);
public static implicit operator string[](StringOrArray sar) => sar.ar;
public IEnumerator<string> GetEnumerator() => ar.AsEnumerable().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ar.GetEnumerator();
}
public ArgValue() { }
public ArgValue(string arg) => value = arg;
public static implicit operator ArgValue(string arg) => new(arg);
[JsonRequired] public StringOrArray value { get; set; } = [];
public List<Rule> rules { get; set; } = new();
}
public class ArgumentsNew
{
[JsonRequired] public List<ArgValue> jvm { get; set; } = new();
[JsonRequired] public List<ArgValue> game { get; set; } = new();
}

View File

@ -0,0 +1,27 @@
namespace Млаумчерб.Клиент.классы;
public class VersionCatalog
{
[JsonRequired] public List<RemoteVersionDescriptorProps> versions { get; set; } = null!;
}
public class AssetProperties
{
[JsonRequired] public string hash { get; set; } = "";
[JsonRequired] public int size { get; set; }
}
public class AssetIndex
{
[JsonRequired] public Dictionary<string, AssetProperties> objects { get; set; } = new();
}
public class RemoteVersionDescriptorProps
{
[JsonRequired] public string id { get; set; } = "";
[JsonRequired] public string type { get; set; } = "";
[JsonRequired] public string url { get; set; } = "";
[JsonRequired] public string sha1 { get; set; } = "";
[JsonRequired] public DateTime time { get; set; }
[JsonRequired] public DateTime releaseTime { get; set; }
}

View File

@ -1,128 +1,27 @@
using Newtonsoft.Json;
using System.Runtime.InteropServices;
// ReSharper disable CollectionNeverUpdated.Global
namespace Млаумчерб.Клиент.классы;
public class Artifact
{
[JsonRequired] public string url { get; set; } = "";
[JsonRequired] public string sha1 { get; set; } = "";
[JsonRequired] public int size { get; set; }
}
public class Os
{
[JsonRequired] public string name { get; set; } = "";
}
public class Rule
{
[JsonRequired] public string action { get; set; } = "";
public Os? os { 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 Artifact? artifact { get; set; }
public Classifiers? classifiers { get; set; }
}
public class Extract
{
public List<string>? exclude { get; set; }
}
public class Natives
{
public string? linux { get; set; }
public string? osx { get; set; }
public string? windows { get; set; }
}
public class Library
{
[JsonRequired] public string name { get; set; } = "";
public List<Rule>? rules { get; set; }
public Natives? natives { get; set; }
public Extract? extract { get; set; }
[JsonRequired] public LibraryDownloads downloads { get; set; } = null!;
}
public class AssetIndex
{
[JsonRequired] public string id { get; set; } = "";
[JsonRequired] public int totalSize { get; set; }
[JsonRequired] public bool known { get; set; }
[JsonRequired] public string url { get; set; } = "";
[JsonRequired] public string sha1 { get; set; } = "";
[JsonRequired] public int size { get; set; }
}
public class Downloads
{
[JsonRequired] public Artifact client { get; set; } = null!;
}
public class JavaVersion
{
[JsonRequired] public string component { get; set; } = "";
[JsonRequired] public int majorVersion { get; set; }
}
public class ArgValue
{
[JsonRequired] public string value { get; set; } = "";
public List<Rule> rules { get; set; } = new();
}
public class ArgumentsNew
{
[JsonRequired] public List<ArgValue> jvm { get; set; } = new();
[JsonRequired] public List<ArgValue> game { get; set; } = new();
}
public class MinecraftVersionDescriptor
{
[JsonRequired] public string id { get; set; } = "";
[JsonRequired] public string jar { get; set; } = "";
[JsonRequired] public string family { get; set; } = "";
[JsonRequired] public DateTime time { get; set; }
[JsonRequired] public DateTime releaseTime { get; set; }
[JsonRequired] public string type { get; set; } = "";
[JsonRequired] public string mainClass { get; set; } = "";
[JsonRequired] public Downloads downloads { get; set; } = null!;
[JsonRequired] public JavaVersion javaVersion { get; set; } = null!;
[JsonRequired] public List<Library> libraries { get; set; } = null!;
[JsonRequired] public AssetIndex assetIndex { get; set; } = null!;
[JsonRequired] public string assets { get; set; } = "";
public string? minecraftArguments { get; set; }
public ArgumentsNew? arguments { get; set; }
}
public static class Буржуазия
{
public static bool CheckOs(Os os)
{
return os.name switch
public static bool CheckOs(Os os) =>
os.name switch
{
null => true,
"osx" => OperatingSystem.IsWindows(),
"linux" => OperatingSystem.IsLinux(),
"windows" => OperatingSystem.IsWindows(),
_ => throw new ArgumentOutOfRangeException(os.name)
}
&& os.arch switch
{
null => true,
"x86" => RuntimeInformation.OSArchitecture == Architecture.X86,
"x64" => RuntimeInformation.OSArchitecture == Architecture.X64,
"arm64" => RuntimeInformation.OSArchitecture == Architecture.Arm64,
_ => false
};
}
public static bool CheckRules(ICollection<Rule> rules, ICollection<string> enabled_features)
{
@ -139,9 +38,8 @@ public static class Буржуазия
{
if (is_enabled)
{
if (r.action == "allow")
allowed = true;
else return false;
if (r.action != "allow")
return false;
}
}
}

View File

@ -1,89 +1,25 @@
using DTLib.Extensions;
namespace Млаумчерб.Клиент.классы;
namespace Млаумчерб.Клиент.классы;
public class Пролетариат
public static class Пролетариат
{
public static IOPath GetAssetIndexFilePath(string id) =>
Path.Concat(Приложение.Настройки.путь_к_кубачу, $"assets/indexes/{id}.json");
public static IOPath GetVersionDescriptorDir() =>
Path.Concat(Приложение.Настройки.путь_к_кубачу, "version_descriptors");
public static string GetVersionDescriptorName(IOPath path) =>
path.LastName().RemoveExtension().ToString();
public static IOPath GetVersionDescriptorPath(string name) =>
Path.Concat(GetVersionDescriptorDir(), Path.ReplaceRestrictedChars(name) + ".json");
public static IOPath GetVersionDir() =>
Path.Concat(Приложение.Настройки.путь_к_кубачу, "versions");
public static IOPath GetVersionJarFilePath(string name) =>
Path.Concat(GetVersionDir(), name + ".jar");
public static IOPath GetLibrariesDir() =>
Path.Concat(Приложение.Настройки.путь_к_кубачу, "libraries");
}
public class ArgumentsWithPlaceholders
{
protected List<string> raw_args = new();
public List<string> FillPlaceholders(Dictionary<string, string> values)
{
List<string> result = new();
foreach (var a in raw_args)
{
var f = a;
int begin = a.IndexOf('$');
if (begin != -1)
{
int keyBegin = begin + 2;
int end = a.IndexOf('}', keyBegin);
if (end != -1)
{
var key = a.Substring(keyBegin, end - keyBegin);
if (!values.TryGetValue(key, out var v))
throw new Exception($"can't find value for placeholder '{key}'");
f = v;
}
}
result.Add(f);
}
return result;
}
}
public class JavaArguments : ArgumentsWithPlaceholders
{
private static readonly string[] _initial_arguments =
[
];
private static readonly string[] _enabled_features =
[
];
public JavaArguments(MinecraftVersionDescriptor d)
{
raw_args.AddRange(_initial_arguments);
if (d.arguments is not null)
{
foreach (var av in d.arguments.jvm)
{
if(Буржуазия.CheckRules(av.rules, _enabled_features))
raw_args.Add(av.value);
}
}
}
}
public class GameArguments : ArgumentsWithPlaceholders
{
private static readonly string[] _enabled_features =
[
"has_custom_resolution"
];
public GameArguments(MinecraftVersionDescriptor d)
{
if (d.minecraftArguments is not null)
{
raw_args.AddRange(d.minecraftArguments.SplitToList(' ', quot: '"'));
}
else if (d.arguments is not null)
{
foreach (var av in d.arguments.game)
{
if(Буржуазия.CheckRules(av.rules, _enabled_features))
raw_args.Add(av.value);
}
}
}
}

View File

@ -9,13 +9,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionF
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Debug|x64 = Debug|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Release|Any CPU.Build.0 = Release|Any CPU
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Release|x64.ActiveCfg = Release|x64
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Release|x64.Build.0 = Release|x64
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Debug|x64.ActiveCfg = Debug|x64
{9B9D8B05-255F-49C3-89EC-3F43A66491D3}.Debug|x64.Build.0 = Debug|x64
EndGlobalSection
EndGlobal