now it works

This commit is contained in:
2024-11-06 00:04:12 +05:00
parent 612976dfe6
commit 1f663902e2
54 changed files with 763 additions and 586 deletions

View File

@@ -0,0 +1,66 @@
using Mlaumcherb.Client.Avalonia.зримое;
using Mlaumcherb.Client.Avalonia.холопы;
namespace Mlaumcherb.Client.Avalonia;
public record Config
{
public bool debug { get; set; } =
#if DEBUG
true;
#else
false;
#endif
public string player_name { get; set; } = "";
public int max_memory { get; set; } = 4096;
public string minecraft_dir { get; set; } = ".";
public bool download_java { get; set; } = true;
public string? last_launched_version { get; set; }
public int max_parallel_downloads { get; set; } = 16;
[JsonIgnore] static IOPath _filePath = "config.json";
public static Config LoadFromFile()
{
LauncherApp.Logger.LogInfo(nameof(Config), $"loading config from file '{_filePath}'");
if(!File.Exists(_filePath))
{
LauncherApp.Logger.LogInfo(nameof(Config), "file doesn't exist");
return new Config();
}
string text = File.ReadAllText(_filePath);
string errorMessage = "config is empty";
Config? config = null;
try
{
config = JsonConvert.DeserializeObject<Config>(text);
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
if (config == null)
{
IOPath _brokenPath = _filePath + ".broken";
File.Move(_filePath, _brokenPath, true);
ErrorHelper.ShowMessageBox(nameof(Config),
$"Can't reed config file '{_filePath}'.\n" +
$"New config file has been created. Old is renamed to '{_brokenPath}'.\n\n" +
$"Config parser error: {errorMessage}");
config = new Config();
config.SaveToFile();
}
LauncherApp.Logger.LogDebug(nameof(Config), $"config has been loaded: {config}");
return config;
}
public void SaveToFile()
{
LauncherApp.Logger.LogInfo(nameof(Config), $"saving config to file '{_filePath}'");
var text = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(_filePath, text);
LauncherApp.Logger.LogDebug(nameof(Config), $"config has been saved: {text}");
}
}

View File

@@ -0,0 +1,229 @@
using System.Linq;
using System.Security.Cryptography;
using CliWrap;
using DTLib.Extensions;
using Mlaumcherb.Client.Avalonia.зримое;
using Mlaumcherb.Client.Avalonia.классы;
using Mlaumcherb.Client.Avalonia.сеть;
using Mlaumcherb.Client.Avalonia.сеть.TaskFactories;
using Mlaumcherb.Client.Avalonia.холопы;
using static Mlaumcherb.Client.Avalonia.холопы.PathHelper;
namespace Mlaumcherb.Client.Avalonia;
public class GameVersion
{
private readonly GameVersionProps _props;
public string Name => _props.Name;
public IOPath WorkingDirectory { get; }
private IOPath JavaExecutableFilePath;
private GameVersionDescriptor _descriptor;
private JavaArguments _javaArgs;
private GameArguments _gameArgs;
private Libraries _libraries;
private CancellationTokenSource? _gameCts;
private CommandTask<CommandResult>? _commandTask;
public static async Task<List<GameVersionProps>> GetAllVersionsAsync()
{
var propsSet = new HashSet<GameVersionProps>();
// local descriptors
Directory.Create(GetVersionsDir());
foreach (IOPath subdir in Directory.GetDirectories(GetVersionsDir()))
{
string name = subdir.LastName().ToString();
var d = new GameVersionProps(name, null);
if(!File.Exists(d.LocalDescriptorPath))
throw new Exception("Can't find version descriptor file in directory '{subdir}'. Rename it as directory name.");
propsSet.Add(d);
}
// remote non-duplicating versions
foreach (var removeVersion in await Сеть.GetDownloadableVersions())
{
propsSet.Add(removeVersion);
}
// reverse sort
var propsList = propsSet.ToList();
propsList.Sort((a, b) => b.CompareTo(a));
return propsList;
}
public static async Task<GameVersion> 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 Сеть.DownloadFile(props.RemoteDescriptorUrl, props.LocalDescriptorPath);
}
return new GameVersion(props);
}
private GameVersion(GameVersionProps props)
{
_props = props;
string descriptorText = File.ReadAllText(props.LocalDescriptorPath);
_descriptor = JsonConvert.DeserializeObject<GameVersionDescriptor>(descriptorText)
?? throw new Exception($"can't parse descriptor file '{props.LocalDescriptorPath}'");
_javaArgs = new JavaArguments(_descriptor);
_gameArgs = new GameArguments(_descriptor);
_libraries = new Libraries(_descriptor);
WorkingDirectory = GetVersionDir(_descriptor.id);
JavaExecutableFilePath = GetJavaExecutablePath(_descriptor.javaVersion.component, LauncherApp.Config.debug);
}
public async Task UpdateFiles(bool checkHashes, Action<NetworkTask> networkTaskCreatedCallback)
{
LauncherApp.Logger.LogInfo(Name, $"started updating version {Name}");
List<INetworkTaskFactory> taskFactories =
[
new AssetsDownloadTaskFactory(_descriptor),
new LibrariesDownloadTaskFactory(_descriptor, _libraries),
new VersionFileDownloadTaskFactory(_descriptor),
];
if(LauncherApp.Config.download_java)
{
taskFactories.Add(new JavaDownloadTaskFactory(_descriptor));
}
//TODO: modpack
/*if (modpack != null)
{
taskFactories.Add(new ModpackDownloadTaskFactory(modpack));
}*/
var networkTasks = new List<NetworkTask>();
for (int i = 0; i < taskFactories.Count; i++)
{
var nt = await taskFactories[i].CreateAsync(checkHashes);
if (nt != null)
{
networkTasks.Add(nt);
networkTaskCreatedCallback.Invoke(nt);
}
}
foreach (var nt in networkTasks)
{
await nt.StartAsync();
}
// create log4j config file
if (!File.Exists(GetLog4jConfigFilePath()))
{
await using var res = EmbeddedResources.GetResourceStream(
$"Mlaumcherb.Client.Avalonia.встроенное.{GetLog4jConfigFileName()}");
await using var file = File.OpenWrite(GetLog4jConfigFilePath());
await res.CopyToAsync(file);
}
_props.IsDownloaded = true;
LauncherApp.Logger.LogInfo(Name, $"finished updating version {Name}");
}
//minecraft player uuid explanation
//https://gist.github.com/CatDany/0e71ca7cd9b42a254e49/
//java uuid generation in c#
//https://stackoverflow.com/questions/18021808/uuid-interop-with-c-sharp-code
public static string GetPlayerUUID(string name)
{
byte[] name_bytes = Encoding.UTF8.GetBytes("OfflinePlayer:" + name);
var md5 = MD5.Create();
byte[] hash = md5.ComputeHash(name_bytes);
hash[6] &= 0x0f;
hash[6] |= 0x30;
hash[8] &= 0x3f;
hash[8] |= 0x80;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
sb.Append(hash[i].ToString("x2"));
sb.Insert(8, '-').Insert(13, '-').Insert(18, '-').Insert(23, '-');
return sb.ToString();
}
public async Task Launch()
{
if (string.IsNullOrWhiteSpace(LauncherApp.Config.player_name))
throw new Exception("invalid player name");
Directory.Create(WorkingDirectory);
string uuid = GetPlayerUUID(LauncherApp.Config.player_name);
Dictionary<string, string> placeholder_values = new() {
{ "resolution_width", "1600" },
{ "resolution_height", "900" },
{ "xms", "2048" },
{ "xmx", LauncherApp.Config.max_memory.ToString() },
{ "auth_player_name", LauncherApp.Config.player_name },
{ "auth_access_token", uuid },
{ "auth_session", $"token:{uuid}:{uuid}" },
{ "auth_xuid", "" },
{ "auth_uuid", uuid },
{ "clientid", "" },
{ "user_properties", "{}" },
{ "user_type", "userType" },
{ "launcher_name", "java-minecraft-launcher" },
{ "launcher_version", "1.6.84-j" },
{ "classpath", _libraries.Libs.Select(l => l.jarFilePath)
.Append(GetVersionJarFilePath(_descriptor.id))
.MergeToString(';') },
{ "assets_index_name", _descriptor.assets },
{ "assets_root", GetAssetsDir().ToString() },
{ "game_assets", GetAssetsDir().ToString() },
{ "game_directory", WorkingDirectory.ToString() },
{ "natives_directory", GetNativeLibrariesDir(_descriptor.id).ToString() },
{ "path", GetLog4jConfigFilePath().ToString() },
{ "version_name", _descriptor.id },
{ "version_type", _descriptor.type },
{ "arch", PlatformHelper.GetArchOld() },
// { "quickPlayMultiplayer", "" },
// { "quickPlayPath", "" },
// { "quickPlayRealms", "" },
// { "quickPlaySingleplayer", "" },
{ "client_jar", GetVersionJarFilePath(_descriptor.id).ToString() },
};
List<string> argsList = new();
argsList.AddRange(_javaArgs.FillPlaceholders(placeholder_values));
argsList.Add(_descriptor.mainClass);
argsList.AddRange(_gameArgs.FillPlaceholders(placeholder_values));
var command = Cli.Wrap(JavaExecutableFilePath.ToString())
.WithWorkingDirectory(WorkingDirectory.ToString())
.WithArguments(argsList)
.WithStandardOutputPipe(PipeTarget.ToDelegate(LogGameOut))
.WithStandardErrorPipe(PipeTarget.ToDelegate(LogGameError))
.WithValidation(CommandResultValidation.None);
LauncherApp.Logger.LogInfo(Name, "launching the game");
LauncherApp.Logger.LogDebug(Name, "java: " + command.TargetFilePath);
LauncherApp.Logger.LogDebug(Name, "working_dir: " + command.WorkingDirPath);
LauncherApp.Logger.LogDebug(Name, "arguments: \n\t" + argsList.MergeToString("\n\t"));
_gameCts = new();
_commandTask = command.ExecuteAsync(_gameCts.Token);
var result = await _commandTask;
LauncherApp.Logger.LogInfo(Name, $"game exited with code {result.ExitCode}");
}
private void LogGameOut(string line)
{
LauncherApp.Logger.LogInfo(Name, line);
}
private void LogGameError(string line)
{
LauncherApp.Logger.LogWarn(Name, line);
}
public void Close()
{
_gameCts?.Cancel();
}
public override string ToString() => Name;
}

View File

@@ -0,0 +1,79 @@
using Mlaumcherb.Client.Avalonia.зримое;
namespace Mlaumcherb.Client.Avalonia;
public class LauncherLogger : ILogger
{
private CompositeLogger _compositeLogger;
private FileLogger _fileLogger;
public static readonly IOPath LogsDirectory = "launcher_logs";
public IOPath LogfileName => _fileLogger.LogfileName;
public LauncherLogger()
{
_fileLogger = new FileLogger(LogsDirectory, "млаумчерб");
ILogger[] loggers =
[
_fileLogger,
#if DEBUG
new ConsoleLogger(),
#endif
];
_compositeLogger = new CompositeLogger(loggers);
}
public record LogMessage(string context, LogSeverity severity, object message, ILogFormat format);
public delegate void LogHandler(LogMessage msg);
public event LogHandler? OnLogMessage;
public void Log(string context, LogSeverity severity, object message, ILogFormat format)
{
_compositeLogger.Log(context, severity, message, format);
bool isEnabled = severity switch
{
LogSeverity.Debug => DebugLogEnabled,
LogSeverity.Info => InfoLogEnabled,
LogSeverity.Warn => WarnLogEnabled,
LogSeverity.Error => ErrorLogEnabled,
_ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null)
};
if(isEnabled)
OnLogMessage?.Invoke(new(context, severity, message, format));
}
public void Dispose()
{
_compositeLogger.Dispose();
}
public ILogFormat Format
{
get => _compositeLogger.Format;
set => _compositeLogger.Format = value;
}
public bool DebugLogEnabled
{
get => _compositeLogger.DebugLogEnabled;
set => _compositeLogger.DebugLogEnabled = value;
}
public bool InfoLogEnabled
{
get => _compositeLogger.InfoLogEnabled;
set => _compositeLogger.InfoLogEnabled = value;
}
public bool WarnLogEnabled
{
get => _compositeLogger.WarnLogEnabled;
set => _compositeLogger.WarnLogEnabled = value;
}
public bool ErrorLogEnabled
{
get => _compositeLogger.ErrorLogEnabled;
set => _compositeLogger.ErrorLogEnabled = value;
}
}

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType Condition="'$(Configuration)' == 'Debug'">Exe</OutputType>
<OutputType Condition="'$(Configuration)' != 'Debug'">WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>гойда.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>капитал\кубе.ico</ApplicationIcon>
<AssemblyName>млаумчерб</AssemblyName>
<Configurations>Release;Debug</Configurations>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.0" />
<PackageReference Include="Avalonia.Themes.Simple" Version="11.2.0" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.0" />
<PackageReference Include="Avalonia.Labs.Gif" Version="11.2.0" />
<PackageReference Include="CliWrap" Version="3.6.7" />
<PackageReference Include="DTLib" Version="1.6.1" />
<PackageReference Include="LZMA-SDK" Version="22.1.1" />
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.*" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="капитал\**"/>
<EmbeddedResource Include="встроенное\**" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,43 @@
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;
global using DTLib.Logging;
global using DTLib.Filesystem;
global using File = DTLib.Filesystem.File;
global using Directory = DTLib.Filesystem.Directory;
global using Path = DTLib.Filesystem.Path;
using System.Globalization;
using Avalonia;
using Mlaumcherb.Client.Avalonia.зримое;
namespace Mlaumcherb.Client.Avalonia;
public class Program
{
[STAThread]
public static void Main(string[] args)
{
try
{
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
catch (Exception ex)
{
LauncherApp.Logger.LogError(nameof(Program), ex);
}
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<LauncherApp>()
.UsePlatformDetect()
.LogToTrace();
}

View File

@@ -0,0 +1,28 @@
<Configuration status="WARN" packages="com.mojang.util">
<Appenders>
<Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n"/>
</Console>
<Queue name="ServerGuiConsole">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL"/>
<RegexFilter regex="(?s).*\$\{[^}]*\}.*" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="SysOut"/>
<AppenderRef ref="File"/>
<AppenderRef ref="ServerGuiConsole"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="млаумчерб.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,26 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Mlaumcherb.Client.Avalonia.зримое.NetworkTaskView"
Padding="4" MinHeight="90" MinWidth="200"
VerticalAlignment="Top"
HorizontalAlignment="Stretch"
BorderThickness="1" BorderBrush="#999999">
<Grid RowDefinitions="* * *" ColumnDefinitions="* 30">
<TextBlock Name="NameText"
Grid.Row="0" Grid.Column="0" />
<Button Grid.Row="0" Grid.Column="1"
Classes="button_no_border"
Background="Transparent"
Foreground="#FF4040"
FontSize="12"
Click="RemoveFromList">
[X]
</Button>
<TextBlock Name="StatusText"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="15"/>
<TextBlock Name="ProgressText"
Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="14"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,66 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Mlaumcherb.Client.Avalonia.сеть;
namespace Mlaumcherb.Client.Avalonia.зримое;
public partial class NetworkTaskView : UserControl
{
public readonly NetworkTask Task;
private readonly Action<NetworkTaskView> _removeFromList;
public NetworkTaskView()
{
throw new Exception();
}
public NetworkTaskView(NetworkTask task, Action<NetworkTaskView> removeFromList)
{
Task = task;
_removeFromList = removeFromList;
InitializeComponent();
NameText.Text = task.Name;
StatusText.Text = task.DownloadStatus.ToString();
task.OnStart += OnTaskOnStart;
task.OnProgress += ReportProgress;
task.OnStop += OnTaskStop;
}
private void OnTaskOnStart()
{
Dispatcher.UIThread.Invoke(() =>
{
StatusText.Text = Task.DownloadStatus.ToString();
});
}
private void OnTaskStop(NetworkTask.Status status)
{
Dispatcher.UIThread.Invoke(() =>
{
StatusText.Text = status.ToString();
if(!string.IsNullOrEmpty(ProgressText.Text))
{
int speedIndex = ProgressText.Text.IndexOf('(');
if(speedIndex > 0)
ProgressText.Text = ProgressText.Text.Remove(speedIndex);
}
});
}
void ReportProgress(DownloadProgress progress)
{
Dispatcher.UIThread.Invoke(() =>
{
ProgressText.Text = progress.ToString();
});
}
private void RemoveFromList(object? sender, RoutedEventArgs e)
{
Task.Cancel();
Dispatcher.UIThread.Invoke(() => _removeFromList.Invoke(this));
}
}

View File

@@ -0,0 +1,59 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Mlaumcherb.Client.Avalonia.зримое.LauncherApp"
RequestedThemeVariant="Dark">
<Application.Styles>
<SimpleTheme />
<Style Selector="Border.dark_tr_bg">
<Setter Property="Background" Value="#cc232333"/>
</Style>
<Style Selector="Border.white_border">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="White"/>
</Style>
<Style Selector="Button.button_no_border">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="Transparent"/>
</Style>
<Style Selector="Button.menu_button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="4 0"/>
<Setter Property="MinWidth" Value="50"/>
</Style>
<Style Selector="Border.menu_separator">
<Setter Property="Background" Value="#ff505050"/>
<Setter Property="Width" Value="1"/>
<Setter Property="Margin" Value="4"/>
</Style>
<Style Selector="ScrollBar /template/ Border">
<Setter Property="Width" Value="5"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="ClipToBounds" Value="True"/>
<Setter Property="CornerRadius" Value="0"/>
</Style>
<Style Selector="ScrollBar /template/ Rectangle">
<Setter Property="Fill" Value="#d8ceb9"/>
</Style>
<Style Selector="ScrollBar /template/ Thumb">
<Setter Property="Background" Value="#fd7300"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="CornerRadius" Value="0"/>
</Style>
<Style Selector="ScrollBar /template/ Thumb /template/ Border">
<Setter Property="ClipToBounds" Value="True"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="Width" Value="5"/>
</Style>
</Application.Styles>
<Application.Resources>
<FontFamily x:Key="MonospaceFont">avares://млаумчерб/капитал/IBMPlexMono-Regular.ttf</FontFamily>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,29 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Mlaumcherb.Client.Avalonia.зримое;
public class LauncherApp : Application
{
public static LauncherLogger Logger = new();
public static Config Config = new();
public override void Initialize()
{
Logger.LogInfo(nameof(LauncherApp), "приложение запущено");
Config = Config.LoadFromFile();
Logger.DebugLogEnabled = Config.debug;
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

View File

@@ -0,0 +1,13 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Mlaumcherb.Client.Avalonia.зримое.LogMessageView"
FontSize="14">
<SelectableTextBlock Name="ContentTextBox"
FontSize="{Binding $parent.FontSize}"
TextWrapping="Wrap"
VerticalAlignment="Top"
Background="Transparent"/>
</UserControl>

View File

@@ -0,0 +1,23 @@
using Avalonia.Controls;
namespace Mlaumcherb.Client.Avalonia.зримое;
public partial class LogMessageView : UserControl
{
public static ILogFormat ShortLogFormat = new DefaultLogFormat
{
TimeStampFormat = MyTimeFormat.TimeOnly,
PrintContext = false
};
public LogMessageView()
{
throw new Exception();
}
public LogMessageView(LauncherLogger.LogMessage m)
{
InitializeComponent();
ContentTextBox.Text = ShortLogFormat.CreateMessage(m.context, m.severity, m.message);
}
}

View File

@@ -0,0 +1,139 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gif="clr-namespace:Avalonia.Labs.Gif;assembly=Avalonia.Labs.Gif"
xmlns:local="clr-namespace:Mlaumcherb.Client.Avalonia"
x:Class="Mlaumcherb.Client.Avalonia.зримое.MainWindow"
Name="window"
Title="млаумчерб"
Icon="avares://млаумчерб/капитал/кубе.ico"
FontFamily="{StaticResource MonospaceFont}" FontSize="18"
MinWidth="1200" MinHeight="700"
Width="800" Height="500"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>* 30</Grid.RowDefinitions>
<Image Grid.RowSpan="2" Stretch="UniformToFill"
Source="avares://млаумчерб/капитал/фоне.png"/>
<Grid Grid.Row="0" Margin="10">
<Grid.ColumnDefinitions>* 300 *</Grid.ColumnDefinitions>
<!-- Central panel -->
<Border Grid.Column="1"
Classes="dark_tr_bg white_border"
Margin="10 0">
<Grid>
<Grid.RowDefinitions>* 60</Grid.RowDefinitions>
<StackPanel Orientation="Vertical" Margin="10" Spacing="10">
<TextBlock>Версия:</TextBlock>
<ComboBox Name="VersionComboBox"/>
<TextBlock>Ник:</TextBlock>
<TextBox Background="Transparent"
Text="{Binding #window.PlayerName}"/>
<TextBlock>
<Run>Выделенная память:</Run>
<TextBox Background="Transparent" Padding="0"
BorderThickness="1"
BorderBrush="#777777"
Text="{Binding #window.MemoryLimit}">
</TextBox>
<Run>Мб</Run>
</TextBlock>
<Slider Minimum="2048" Maximum="8192"
Value="{Binding #window.MemoryLimit}">
</Slider>
<CheckBox IsChecked="{Binding #window.CheckGameFiles}">
Проверять файлы игры
</CheckBox>
<CheckBox IsChecked="{Binding #window.EnableJavaDownload}">
Скачивать джаву
</CheckBox>
</StackPanel>
<Button Name="LaunchButton" Grid.Row="1"
Margin="10" Padding="0 0 0 4"
Classes="button_no_border"
Background="#BBfd7300"
Click="Запуск">
Запуск
</Button>
</Grid>
</Border>
<!-- Left panel -->
<Border Grid.Column="0"
Classes="dark_tr_bg white_border">
<Grid>
<Grid.RowDefinitions>36 *</Grid.RowDefinitions>
<Border Classes="white_border" Margin="-1" Padding="4">
<DockPanel>
<TextBlock FontWeight="Bold"
DockPanel.Dock="Left">
Лог
</TextBlock>
<Button Click="ClearLogPanel"
Classes="menu_button"
DockPanel.Dock="Right"
HorizontalAlignment="Right">
очистить
</Button>
</DockPanel>
</Border>
<ScrollViewer Name="LogScrollViewer" Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Visible"
Background="Transparent">
<StackPanel Name="LogPanel"
VerticalAlignment="Top"/>
</ScrollViewer>
</Grid>
</Border>
<!-- Right panel -->
<Border Grid.Column="2" Classes="dark_tr_bg white_border">
<Grid>
<Grid.RowDefinitions>36 *</Grid.RowDefinitions>
<Border Classes="white_border" Margin="-1" Padding="4">
<DockPanel>
<TextBlock FontWeight="Bold"
DockPanel.Dock="Left">
Загрузки
</TextBlock>
<Button Click="ClearDownloadsPanel"
Classes="menu_button"
DockPanel.Dock="Right"
HorizontalAlignment="Right">
очистить
</Button>
</DockPanel>
</Border>
<ScrollViewer Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Visible"
Background="Transparent"
Padding="1">
<StackPanel Name="DownloadsPanel"
VerticalAlignment="Top"/>
</ScrollViewer>
</Grid>
</Border>
</Grid>
<Border Grid.Row="1" Background="#954808B0">
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button Classes="menu_button button_no_border" Click="ОткрытьПапкуЛаунчера">папка лаунчера</Button>
<Border Classes="menu_separator"/>
<Button Classes="menu_button button_no_border" Click="ОткрытьФайлЛогов">лог-файл</Button>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Classes="menu_button button_no_border" Click="ОткрытьРепозиторий">исходный код</Button>
<gif:GifImage
Width="30" Height="30" Stretch="Uniform"
Source="avares://млаумчерб/капитал/лисик.gif"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,199 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Mlaumcherb.Client.Avalonia.классы;
using Mlaumcherb.Client.Avalonia.холопы;
namespace Mlaumcherb.Client.Avalonia.зримое;
public partial class MainWindow : Window
{
public static readonly StyledProperty<string> PlayerNameProperty =
AvaloniaProperty.Register<MainWindow, string>(nameof(PlayerName),
defaultBindingMode: BindingMode.TwoWay);
public string PlayerName
{
get => GetValue(PlayerNameProperty);
set => SetValue(PlayerNameProperty, value);
}
public static readonly StyledProperty<int> MemoryLimitProperty =
AvaloniaProperty.Register<MainWindow, int>(nameof(MemoryLimit),
defaultBindingMode: BindingMode.TwoWay, defaultValue: 2048);
public int MemoryLimit
{
get => GetValue(MemoryLimitProperty);
set => SetValue(MemoryLimitProperty, value);
}
public static readonly StyledProperty<bool> CheckGameFilesProperty =
AvaloniaProperty.Register<MainWindow, bool>(nameof(CheckGameFiles),
defaultBindingMode: BindingMode.TwoWay, defaultValue: false);
public bool CheckGameFiles
{
get => GetValue(CheckGameFilesProperty);
set => SetValue(CheckGameFilesProperty, value);
}
public static readonly StyledProperty<bool> EnableJavaDownloadProperty =
AvaloniaProperty.Register<MainWindow, bool>(nameof(EnableJavaDownload),
defaultBindingMode: BindingMode.TwoWay, defaultValue: true);
public bool EnableJavaDownload
{
get => GetValue(EnableJavaDownloadProperty);
set => SetValue(EnableJavaDownloadProperty, value);
}
public MainWindow()
{
InitializeComponent();
}
protected override async void OnLoaded(RoutedEventArgs e)
{
try
{
LauncherApp.Logger.OnLogMessage += GuiLogMessage;
PlayerName = LauncherApp.Config.player_name;
MemoryLimit = LauncherApp.Config.max_memory;
EnableJavaDownload = LauncherApp.Config.download_java;
VersionComboBox.SelectedIndex = 0;
VersionComboBox.IsEnabled = false;
var versions = await GameVersion.GetAllVersionsAsync();
Dispatcher.UIThread.Invoke(() =>
{
foreach (var p in versions)
{
VersionComboBox.Items.Add(new VersionItemView(p));
if (LauncherApp.Config.last_launched_version != null &&
p.Name == LauncherApp.Config.last_launched_version)
VersionComboBox.SelectedIndex = VersionComboBox.Items.Count - 1;
}
VersionComboBox.IsEnabled = true;
});
}
catch (Exception ex)
{
ErrorHelper.ShowMessageBox(nameof(MainWindow), ex);
}
}
private void GuiLogMessage(LauncherLogger.LogMessage msg)
{
if (msg.severity == LogSeverity.Debug) return;
Dispatcher.UIThread.Invoke(() =>
{
double offsetFromBottom = LogScrollViewer.Extent.Height - LogScrollViewer.Offset.Y - LogScrollViewer.Viewport.Height;
bool is_scrolled_to_end = offsetFromBottom < 20.0; // scrolled less then one line up
LogPanel.Children.Add(new LogMessageView(msg));
if (is_scrolled_to_end) LogScrollViewer.ScrollToEnd();
});
}
private async void Запуск(object? sender, RoutedEventArgs e)
{
try
{
Dispatcher.UIThread.Invoke(() => LaunchButton.IsEnabled = false);
var selectedVersionView = (VersionItemView?)VersionComboBox.SelectedItem;
var selectedVersion = selectedVersionView?.Props;
LauncherApp.Config.last_launched_version = selectedVersion?.Name;
LauncherApp.Config.player_name = PlayerName;
LauncherApp.Config.max_memory = MemoryLimit;
LauncherApp.Config.download_java = EnableJavaDownload;
LauncherApp.Config.SaveToFile();
if (selectedVersion == null)
return;
var v = await GameVersion.CreateFromPropsAsync(selectedVersion);
await v.UpdateFiles(CheckGameFiles, nt =>
{
Dispatcher.UIThread.Invoke(() =>
{
DownloadsPanel.Children.Add(new NetworkTaskView(nt,
ntv => DownloadsPanel.Children.Remove(ntv)));
});
}
);
Dispatcher.UIThread.Invoke(() =>
{
CheckGameFiles = false;
});
await v.Launch();
}
catch (Exception ex)
{
ErrorHelper.ShowMessageBox(nameof(MainWindow), ex);
}
finally
{
Dispatcher.UIThread.Invoke(() =>
{
LaunchButton.IsEnabled = true;
});
}
}
private void ОткрытьПапкуЛаунчера(object? s, RoutedEventArgs e)
{
try
{
Launcher.LaunchDirectoryInfoAsync(new DirectoryInfo(Directory.GetCurrent().ToString()))
.ConfigureAwait(false);
}
catch (Exception ex)
{
ErrorHelper.ShowMessageBox(nameof(MainWindow), ex);
}
}
private void ОткрытьФайлЛогов(object? sender, RoutedEventArgs e)
{
try
{
Launcher.LaunchFileInfoAsync(new FileInfo(LauncherApp.Logger.LogfileName.ToString()))
.ConfigureAwait(false);
}
catch (Exception ex)
{
ErrorHelper.ShowMessageBox(nameof(MainWindow), ex);
}
}
private void ОткрытьРепозиторий(object? sender, RoutedEventArgs e)
{
try
{
Launcher.LaunchUriAsync(new Uri("https://timerix.ddns.net:3322/Timerix/mlaumcherb"))
.ConfigureAwait(false);
}
catch (Exception ex)
{
ErrorHelper.ShowMessageBox(nameof(MainWindow), ex);
}
}
private void ClearLogPanel(object? sender, RoutedEventArgs e)
{
LogPanel.Children.Clear();
}
private void ClearDownloadsPanel(object? sender, RoutedEventArgs e)
{
foreach (var control in DownloadsPanel.Children)
{
var nt = (NetworkTaskView)control;
nt.Task.Cancel();
}
DownloadsPanel.Children.Clear();
}
}

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:Mlaumcherb.Client.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Mlaumcherb.Client.Avalonia.зримое.VersionItemView">
<TextBlock Name="text" Background="Transparent"/>
</UserControl>

View File

@@ -0,0 +1,31 @@
using Avalonia.Controls;
using Avalonia.Media;
using Mlaumcherb.Client.Avalonia.классы;
namespace Mlaumcherb.Client.Avalonia.зримое;
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 Exception();
}
public VersionItemView(GameVersionProps props)
{
Props = props;
InitializeComponent();
text.Text = props.Name;
props.OnDownloadCompleted += UpdateBackground;
UpdateBackground();
}
private void UpdateBackground()
{
Background = Props.IsDownloaded ? _avaliableColor : _unavaliableColor;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,27 @@
namespace Mlaumcherb.Client.Avalonia.классы;
public class GameVersionCatalog
{
[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

@@ -0,0 +1,114 @@
using System.Linq;
// ReSharper disable CollectionNeverUpdated.Global
namespace Mlaumcherb.Client.Avalonia.классы;
public class GameVersionDescriptor
{
[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 List<Library> libraries { get; set; } = null!;
[JsonRequired] public AssetIndexProperties assetIndex { get; set; } = null!;
[JsonRequired] public string assets { get; set; } = "";
public JavaVersion javaVersion { get; set; } = new() { component = "jre-legacy", majorVersion = 8 };
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 LibraryDownloads
{
public Artifact? artifact { get; set; }
public Dictionary<string, Artifact>? 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,85 @@
using System.Runtime.InteropServices;
using Mlaumcherb.Client.Avalonia.холопы;
namespace Mlaumcherb.Client.Avalonia.классы;
public class JavaVersionCatalog
{
[JsonProperty("linux")]
public Dictionary<string, JavaVersionProps[]>? linux_x86 { get; set; }
[JsonProperty("linux-i386")]
public Dictionary<string, JavaVersionProps[]>? linux_x64 { get; set; }
[JsonProperty("mac-os")]
public Dictionary<string, JavaVersionProps[]>? osx_x64 { get; set; }
[JsonProperty("mac-os-arm64")]
public Dictionary<string, JavaVersionProps[]>? osx_arm64 { get; set; }
[JsonProperty("windows-arm64")]
public Dictionary<string, JavaVersionProps[]>? windows_arm64 { get; set; }
[JsonProperty("windows-x64")]
public Dictionary<string, JavaVersionProps[]>? windows_x64 { get; set; }
[JsonProperty("windows-x86")]
public Dictionary<string, JavaVersionProps[]>? windows_x86 { get; set; }
public JavaVersionProps GetVersionProps(JavaVersion version)
{
var arch = RuntimeInformation.OSArchitecture;
Dictionary<string, JavaVersionProps[]>? propsDict = null;
switch (arch)
{
case Architecture.X86:
if (OperatingSystem.IsWindows())
propsDict = windows_x86;
else if (OperatingSystem.IsLinux())
propsDict = linux_x86;
break;
case Architecture.X64:
if (OperatingSystem.IsWindows())
propsDict = windows_x64;
else if (OperatingSystem.IsLinux())
propsDict = linux_x64;
else if (OperatingSystem.IsMacOS())
propsDict = osx_x64;
break;
case Architecture.Arm64:
if (OperatingSystem.IsWindows())
propsDict = windows_arm64;
else if (OperatingSystem.IsMacOS())
propsDict = osx_arm64;
break;
}
if (propsDict != null && propsDict.TryGetValue(version.component, out var props_array))
{
if (props_array.Length != 0)
return props_array[0];
}
throw new PlatformNotSupportedException($"Can't download java {version.majorVersion} for your operating system. " +
$"Download it manually to directory {PathHelper.GetJavaRuntimeDir(version.component)}");
}
}
public class JavaVersionProps
{
/// url of JavaDistributiveManifest
[JsonRequired] public Artifact manifest { get; set; } = null!;
}
public class JavaDistributiveManifest
{
[JsonRequired] public Dictionary<string, JavaDistributiveElementProps> files { get; set; } = null!;
}
public class JavaDistributiveElementProps
{
/// "directory" / "file"
[JsonRequired] public string type { get; set; } = "";
public bool? executable { get; set; }
public JavaCompressedArtifact? downloads { get; set; }
}
public class JavaCompressedArtifact
{
public Artifact? lzma { get; set; }
[JsonRequired] public Artifact raw { get; set; } = null!;
}

View File

@@ -0,0 +1,40 @@
using Mlaumcherb.Client.Avalonia.холопы;
namespace Mlaumcherb.Client.Avalonia.классы;
public static class Rules
{
public static bool Check(ICollection<Rule>? rules, ICollection<string> features)
{
if(rules is null || rules.Count == 0)
return true;
bool allowed = false;
foreach (var r in rules)
{
if (r.os != null && !PlatformHelper.CheckOs(r.os))
continue;
if (r.features == null)
allowed = r.action == "allow";
else
{
foreach (var feature in features)
{
if (r.features.TryGetValue(feature, out bool is_enabled))
{
if (is_enabled)
{
allowed = r.action == "allow";
}
}
}
}
if(allowed)
break;
}
return allowed;
}
}

View File

@@ -0,0 +1,31 @@
namespace Mlaumcherb.Client.Avalonia.классы;
public class ArgumentsWithPlaceholders
{
protected List<string> _raw_args = new();
public IEnumerable<string> FillPlaceholders(Dictionary<string, string> values)
{
foreach (var _s in _raw_args)
{
string arg = _s;
int begin = arg.IndexOf("${", StringComparison.Ordinal);
while(begin != -1)
{
int keyBegin = begin + 2;
int end = arg.IndexOf('}', keyBegin);
if (end != -1)
{
var key = arg.Substring(keyBegin, end - keyBegin);
if (!values.TryGetValue(key, out var value))
throw new Exception($"can't find value for placeholder '{key}'");
arg = arg.Replace("${"+ key + "}", value);
}
if(end + 1 < arg.Length)
begin = arg.IndexOf("${", end + 1, StringComparison.Ordinal);
else break;
}
yield return arg;
}
}
}

View File

@@ -0,0 +1,41 @@
using DTLib.Extensions;
namespace Mlaumcherb.Client.Avalonia.классы;
public class GameArguments : ArgumentsWithPlaceholders
{
private static readonly string[] _initial_arguments =
[
"--width", "${resolution_width}",
"--height", "${resolution_height}",
];
private static readonly string[] _enabled_features =
[
];
public GameArguments(GameVersionDescriptor d)
{
_raw_args.AddRange(_initial_arguments);
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(Rules.Check(av.rules, _enabled_features))
{
foreach (var arg in av.value)
{
if(!_raw_args.Contains(arg))
_raw_args.Add(arg);
}
}
}
}
else throw new Exception("no game arguments specified in descriptor");
}
}

View File

@@ -0,0 +1,62 @@
using Mlaumcherb.Client.Avalonia.холопы;
namespace Mlaumcherb.Client.Avalonia.классы;
public class GameVersionProps : IComparable<GameVersionProps>, IEquatable<GameVersionProps>
{
public string Name { get; }
public IOPath LocalDescriptorPath { get; }
public string? RemoteDescriptorUrl { get; }
private bool _isDownloaded;
public bool IsDownloaded
{
get => _isDownloaded;
set
{
bool downloadCompleted = value && !_isDownloaded;
_isDownloaded = value;
if(downloadCompleted)
OnDownloadCompleted?.Invoke();
}
}
public event Action? OnDownloadCompleted;
public GameVersionProps(string name, string? url)
{
Name = name;
LocalDescriptorPath = PathHelper.GetVersionDescriptorPath(name);
RemoteDescriptorUrl = url;
IsDownloaded = File.Exists(PathHelper.GetVersionJarFilePath(name));
}
public override string ToString() => Name;
public override int GetHashCode() => Name.GetHashCode();
public int CompareTo(GameVersionProps? other)
{
if (ReferenceEquals(this, other)) return 0;
if (other is null) return 1;
if (Version.TryParse(Name, out var version1) && Version.TryParse(other.Name, out var version2))
{
return version1.CompareTo(version2);
}
return String.Compare(Name, other.Name, StringComparison.InvariantCulture);
}
public bool Equals(GameVersionProps? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Name == other.Name;
}
public override bool Equals(object? obj)
{
if (obj is GameVersionProps other) return Equals(other);
if (ReferenceEquals(this, obj)) return true;
return false;
}
}

View File

@@ -0,0 +1,49 @@
namespace Mlaumcherb.Client.Avalonia.классы;
public class JavaArguments : ArgumentsWithPlaceholders
{
private static readonly string[] _initial_arguments =
[
"-XX:+UnlockExperimentalVMOptions",
"-XX:+UseG1GC",
"-XX:G1NewSizePercent=20",
"-XX:G1ReservePercent=20",
"-XX:MaxGCPauseMillis=50",
"-XX:G1HeapRegionSize=32M",
"-XX:+DisableExplicitGC",
"-XX:+AlwaysPreTouch",
"-XX:+ParallelRefProcEnabled",
"-Xms${xms}M",
"-Xmx${xmx}M",
"-Dfile.encoding=UTF-8",
"-Dlog4j.configurationFile=${path}",
"-Djava.library.path=${natives_directory}",
"-Dminecraft.client.jar=${client_jar}",
"-Dminecraft.launcher.brand=${launcher_name}",
"-Dminecraft.launcher.version=${launcher_version}",
"-cp", "${classpath}"
];
private static readonly string[] _enabled_features =
[
];
public JavaArguments(GameVersionDescriptor d)
{
_raw_args.AddRange(_initial_arguments);
if (d.arguments is not null)
{
foreach (var av in d.arguments.jvm)
{
if (Rules.Check(av.rules, _enabled_features))
{
foreach (var arg in av.value)
{
if(!_raw_args.Contains(arg))
_raw_args.Add(arg);
}
}
}
}
}
}

View File

@@ -0,0 +1,80 @@
using DTLib.Extensions;
using Mlaumcherb.Client.Avalonia.холопы;
namespace Mlaumcherb.Client.Avalonia.классы;
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, Extract? extractionOptions)
: JarLib(name, jarFilePath, artifact);
public IReadOnlyCollection<JarLib> Libs { get; }
public Libraries(GameVersionDescriptor descriptor)
{
List<JarLib> libs = new();
HashSet<string> libHashes = new();
foreach (var l in descriptor.libraries)
{
if (l.rules != null && !Rules.Check(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");
// example: "natives-windows-${arch}"
if (nativesKey.Contains('$'))
{
var span = nativesKey.AsSpan();
nativesKey = span.After("${").Before('}') switch
{
"arch" => span.Before("${").ToString() + PlatformHelper.GetArchOld(),
_ => throw new Exception($"unknown placeholder in {nativesKey}")
};
}
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;
string urlTail = artifact.url.AsSpan().After("://").After('/').ToString();
IOPath jarFilePath = Path.Concat(PathHelper.GetLibrariesDir(), urlTail);
libs.Add(new NativeLib(l.name, jarFilePath, artifact, 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;
string urlTail = artifact.url.AsSpan().After("://").After('/').ToString();
IOPath jarFilePath = Path.Concat(PathHelper.GetLibrariesDir(), urlTail);
libs.Add(new JarLib(l.name, jarFilePath, artifact));
}
}
Libs = libs;
}
}

View File

@@ -0,0 +1,23 @@
namespace Mlaumcherb.Client.Avalonia.сеть;
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);
}

View File

@@ -0,0 +1,61 @@
using Mlaumcherb.Client.Avalonia.зримое;
using Timer = DTLib.Timer;
namespace Mlaumcherb.Client.Avalonia.сеть;
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(byte[] buffer, int offset, int count)
{
Interlocked.Add(ref _curSize, count);
}
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);
LauncherApp.Logger.LogDebug(nameof(ReportProgress),
$"download progress {p}");
_reportProgressDelegate(p);
}
}

View File

@@ -0,0 +1,77 @@
namespace Mlaumcherb.Client.Avalonia.сеть;
public class NetworkTask : IDisposable
{
public readonly string Name;
public enum Status
{
Initialized,
Started,
Completed,
Cancelled,
Failed
};
public Status DownloadStatus { get; private set; } = Status.Initialized;
public event Action? OnStart;
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.Started || DownloadStatus == Status.Completed)
return;
DownloadStatus = Status.Started;
try
{
_progressReporter.Start();
OnStart?.Invoke();
await _downloadAction(_progressReporter, _cts.Token);
DownloadStatus = Status.Completed;
}
catch (OperationCanceledException)
{
DownloadStatus = Status.Cancelled;
}
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();
}
}

View File

@@ -0,0 +1,137 @@
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using DTLib.Extensions;
using Mlaumcherb.Client.Avalonia.зримое;
using Mlaumcherb.Client.Avalonia.классы;
using Mlaumcherb.Client.Avalonia.холопы;
using static Mlaumcherb.Client.Avalonia.сеть.Сеть;
namespace Mlaumcherb.Client.Avalonia.сеть.TaskFactories;
public class AssetsDownloadTaskFactory : INetworkTaskFactory
{
private const string ASSET_SERVER_URL = "https://resources.download.minecraft.net/";
private GameVersionDescriptor _descriptor;
private SHA1 _hasher;
private IOPath _indexFilePath;
List<AssetDownloadProperties> _assetsToDownload = new();
public AssetsDownloadTaskFactory(GameVersionDescriptor descriptor)
{
_descriptor = descriptor;
_hasher = SHA1.Create();
_indexFilePath = PathHelper.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))
{
LauncherApp.Logger.LogInfo(nameof(Сеть), $"started downloading asset index to '{_indexFilePath}'");
await DownloadFile(_descriptor.assetIndex.url, _indexFilePath);
LauncherApp.Logger.LogInfo(nameof(Сеть), "finished downloading asset index");
}
string indexFileText = File.ReadAllText(_indexFilePath);
var assetIndex = JsonConvert.DeserializeObject<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)
{
LauncherApp.Logger.LogInfo(nameof(Сеть), $"started downloading assets '{_descriptor.assetIndex.id}'");
ParallelOptions opt = new()
{
MaxDegreeOfParallelism = LauncherApp.Config.max_parallel_downloads,
CancellationToken = ct
};
await Parallel.ForEachAsync(_assetsToDownload, opt,
async (a, _ct) =>
{
bool completed = false;
while(!completed)
{
LauncherApp.Logger.LogDebug(nameof(Сеть), $"downloading asset '{a.name}' {a.hash}");
try
{
await DownloadFile(a.url, a.filePath, _ct, pr.AddBytesCount);
completed = true;
}
catch (HttpRequestException httpException)
{
// wait on rate limit
if(httpException.StatusCode == HttpStatusCode.TooManyRequests)
{
LauncherApp.Logger.LogDebug(nameof(Сеть), "rate limit hit");
await Task.Delay(1000, _ct);
}
else throw;
}
}
});
LauncherApp.Logger.LogInfo(nameof(Сеть), $"finished downloading assets '{_descriptor.assetIndex.id}'");
}
}

View File

@@ -0,0 +1,7 @@
namespace Mlaumcherb.Client.Avalonia.сеть.TaskFactories;
public interface INetworkTaskFactory
{
/// <returns>unstarted network task or null if there is nothing to download</returns>
Task<NetworkTask?> CreateAsync(bool checkHashes);
}

View File

@@ -0,0 +1,123 @@
using System.Security.Cryptography;
using DTLib.Extensions;
using Mlaumcherb.Client.Avalonia.зримое;
using Mlaumcherb.Client.Avalonia.классы;
using Mlaumcherb.Client.Avalonia.холопы;
using static Mlaumcherb.Client.Avalonia.сеть.Сеть;
namespace Mlaumcherb.Client.Avalonia.сеть.TaskFactories;
public class JavaDownloadTaskFactory : INetworkTaskFactory
{
private const string CATALOG_URL =
"https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
private GameVersionDescriptor _descriptor;
private IOPath _javaVersionDir;
private SHA1 _hasher;
private JavaDistributiveManifest? _distributiveManifest;
private List<(IOPath path, JavaDistributiveElementProps props)> _filesToDownload = new();
public JavaDownloadTaskFactory(GameVersionDescriptor descriptor)
{
_descriptor = descriptor;
_javaVersionDir = PathHelper.GetJavaRuntimeDir(_descriptor.javaVersion.component);
_hasher = SHA1.Create();
}
public async Task<NetworkTask?> CreateAsync(bool checkHashes)
{
var catalog = await DownloadStringAndDeserialize<JavaVersionCatalog>(CATALOG_URL);
var versionProps = catalog.GetVersionProps(_descriptor.javaVersion);
_distributiveManifest = await DownloadStringAndDeserialize<JavaDistributiveManifest>(versionProps.manifest.url);
NetworkTask? networkTask = null;
if (!CheckFiles(checkHashes))
networkTask = new(
$"java runtime '{_descriptor.javaVersion.component}'",
GetTotalSize(),
Download
);
return networkTask;
}
private bool CheckFiles(bool checkHashes)
{
_filesToDownload.Clear();
foreach (var pair in _distributiveManifest!.files)
{
if (pair.Value.type != "file")
continue;
if (pair.Value.downloads != null)
{
var artifact = pair.Value.downloads;
IOPath file_path = Path.Concat(_javaVersionDir, pair.Key);
if (!File.Exists(file_path))
{
_filesToDownload.Add((file_path, pair.Value));
}
else if(checkHashes)
{
using var fs = File.OpenRead(file_path);
if (_hasher.ComputeHash(fs).HashToString() != artifact.raw.sha1)
{
_filesToDownload.Add((file_path, pair.Value));
}
}
}
}
return _filesToDownload.Count == 0;
}
private long GetTotalSize()
{
long totalSize = 0;
foreach (var file in _filesToDownload)
{
if(file.props.downloads == null)
continue;
totalSize += file.props.downloads.lzma?.size ?? file.props.downloads.raw.size;
}
return totalSize;
}
private async Task Download(NetworkProgressReporter pr, CancellationToken ct)
{
LauncherApp.Logger.LogInfo(nameof(Сеть), "started downloading java runtime " +
$"{_descriptor.javaVersion.majorVersion} '{_descriptor.javaVersion.component}'");
ParallelOptions opt = new()
{
MaxDegreeOfParallelism = LauncherApp.Config.max_parallel_downloads,
CancellationToken = ct
};
await Parallel.ForEachAsync(_filesToDownload, opt, async (f, _ct) =>
{
if (f.props.downloads!.lzma != null)
{
LauncherApp.Logger.LogDebug(nameof(Сеть), $"downloading lzma-compressed file '{f.path}'");
await using var pipe = new TransformStream(await GetStream(f.props.downloads.lzma.url, _ct));
pipe.AddTransform(pr.AddBytesCount);
await using var fs = File.OpenWrite(f.path);
LZMAHelper.Decompress(pipe, fs);
}
else
{
LauncherApp.Logger.LogDebug(nameof(Сеть), $"downloading raw file '{f.path}'");
await DownloadFile(f.props.downloads.raw.url, f.path, _ct, pr.AddBytesCount);
}
if(!OperatingSystem.IsWindows() && f.props.executable is true)
{
LauncherApp.Logger.LogDebug(nameof(Сеть), $"adding execute rights to file '{f.path}'");
System.IO.File.SetUnixFileMode(f.path.ToString(), UnixFileMode.UserExecute);
}
});
LauncherApp.Logger.LogInfo(nameof(Сеть), "finished downloading java runtime " +
$"{_descriptor.javaVersion.majorVersion} '{_descriptor.javaVersion.component}'");
}
}

View File

@@ -0,0 +1,107 @@
using System.IO.Compression;
using System.Security.Cryptography;
using DTLib.Extensions;
using Mlaumcherb.Client.Avalonia.зримое;
using Mlaumcherb.Client.Avalonia.классы;
using Mlaumcherb.Client.Avalonia.холопы;
using static Mlaumcherb.Client.Avalonia.сеть.Сеть;
namespace Mlaumcherb.Client.Avalonia.сеть.TaskFactories;
public class LibrariesDownloadTaskFactory : INetworkTaskFactory
{
private GameVersionDescriptor _descriptor;
private Libraries _libraries;
private SHA1 _hasher;
private List<Libraries.JarLib> _libsToDownload = new();
private IOPath _nativesDir;
public LibrariesDownloadTaskFactory(GameVersionDescriptor descriptor, Libraries libraries)
{
_descriptor = descriptor;
_libraries = libraries;
_hasher = SHA1.Create();
_nativesDir = PathHelper.GetNativeLibrariesDir(descriptor.id);
}
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();
bool nativeDirExists = Directory.Exists(_nativesDir);
foreach (var l in _libraries.Libs)
{
if (!File.Exists(l.jarFilePath))
{
_libsToDownload.Add(l);
}
else if (!nativeDirExists && l is Libraries.NativeLib)
{
_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)
{
LauncherApp.Logger.LogInfo(nameof(Сеть), $"started downloading libraries '{_descriptor.id}'");
ParallelOptions opt = new()
{
MaxDegreeOfParallelism = LauncherApp.Config.max_parallel_downloads,
CancellationToken = ct
};
await Parallel.ForEachAsync(_libsToDownload, opt, async (l, _ct) =>
{
LauncherApp.Logger.LogDebug(nameof(Сеть), $"downloading library '{l.name}' to '{l.jarFilePath}'");
await DownloadFile(l.artifact.url, l.jarFilePath, _ct, pr.AddBytesCount);
if (l is Libraries.NativeLib n)
{
var zipf = File.OpenRead(n.jarFilePath);
ZipFile.ExtractToDirectory(zipf, _nativesDir.ToString(), true);
if (n.extractionOptions?.exclude != null)
{
foreach (var excluded in n.extractionOptions.exclude)
{
IOPath path = Path.Concat(_nativesDir, excluded);
if(Directory.Exists(path))
Directory.Delete(path);
if(File.Exists(path))
File.Delete(path);
}
}
}
});
LauncherApp.Logger.LogInfo(nameof(Сеть), $"finished downloading libraries '{_descriptor.id}'");
}
}

View File

@@ -0,0 +1,57 @@
using System.Security.Cryptography;
using DTLib.Extensions;
using Mlaumcherb.Client.Avalonia.зримое;
using Mlaumcherb.Client.Avalonia.классы;
using Mlaumcherb.Client.Avalonia.холопы;
using static Mlaumcherb.Client.Avalonia.сеть.Сеть;
namespace Mlaumcherb.Client.Avalonia.сеть.TaskFactories;
public class VersionFileDownloadTaskFactory : INetworkTaskFactory
{
private GameVersionDescriptor _descriptor;
private IOPath _filePath;
private SHA1 _hasher;
public VersionFileDownloadTaskFactory(GameVersionDescriptor descriptor)
{
_descriptor = descriptor;
_filePath = PathHelper.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 async Task Download(NetworkProgressReporter pr, CancellationToken ct)
{
LauncherApp.Logger.LogInfo(nameof(Сеть), $"started downloading version file '{_descriptor.id}'");
await DownloadFile(_descriptor.downloads.client.url, _filePath, ct, pr.AddBytesCount);
LauncherApp.Logger.LogInfo(nameof(Сеть), $"finished downloading version file '{_descriptor.id}'");
}
}

View File

@@ -0,0 +1,87 @@
using System.Net.Http;
using Mlaumcherb.Client.Avalonia.зримое;
using Mlaumcherb.Client.Avalonia.классы;
namespace Mlaumcherb.Client.Avalonia.сеть;
public static class Сеть
{
private static HttpClient _http = new();
static Сеть()
{
// thanks for Sashok :3
// https://github.com/new-sashok724/Launcher/blob/23485c3f7de6620d2c6b7b2dd9339c3beb6a0366/Launcher/source/helper/IOHelper.java#L259
_http.DefaultRequestHeaders.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
}
public static Task<string> GetString(string url, CancellationToken ct = default) => _http.GetStringAsync(url, ct);
public static Task<Stream> GetStream(string url, CancellationToken ct = default) => _http.GetStreamAsync(url, ct);
public static async Task DownloadFile(string url, Stream outStream, CancellationToken ct = default,
params TransformStream.TransformFuncDelegate[] transforms)
{
await using var pipe = new TransformStream(await GetStream(url, ct));
if (transforms.Length > 0)
pipe.AddTransforms(transforms);
await pipe.CopyToAsync(outStream, ct);
}
public static async Task DownloadFile(string url, IOPath outPath, CancellationToken ct = default,
params TransformStream.TransformFuncDelegate[] transforms)
{
await using var file = File.OpenWrite(outPath);
await DownloadFile(url, file, ct, transforms);
}
public static async Task<T> DownloadStringAndDeserialize<T>(string url)
{
var text = await _http.GetStringAsync(url);
var result = JsonConvert.DeserializeObject<T>(text)
?? throw new Exception($"can't deserialize {typeof(T).Name}");
return result;
}
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 catalog = await DownloadStringAndDeserialize<GameVersionCatalog>(url);
descriptors.AddRange(catalog.versions);
}
catch (Exception ex)
{
LauncherApp.Logger.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;
}
}

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -eo pipefail
mode="$1"
outdir="bin/publish"
args_selfcontained="
--self-contained
--use-current-runtime
-p:PublishSingleFile=true
-p:PublishTrimmed=true
-p:TrimMode=partial
-p:EnableCompressionInSingleFile=true
-p:OptimizationPreference=Size
-p:InvariantGlobalization=true
-p:DebugType=none
-p:IncludeNativeLibrariesForSelfExtract=true"
args_aot="
-p:PublishAot=true
-p:OptimizationPreference=Size
-p:DebugType=none"
case "$mode" in
aot | native | бинарное)
args="$args_aot"
;;
self-contained | selfcontained | небинарное)
args="$args_selfcontained"
;;
*)
echo "ПОЛЬЗОВАНИЕ: ./собрать.sh [способ]"
echo " СПОСОБЫ:"
echo " бинарное - компилирует промежуточный (управляемый) код в машинный вместе с рантаймом"
echo " небинарное - приделывает промежуточный (управляемый) код к рантайму"
echo " Оба способа собирают программу в один файл, который не является 80-мегабайтовым умственно отсталым кубом. Он 20-мегабайтовый >w<"
exit 1
;;
esac
rm -rf "$outdir"
command="dotnet publish -c Release -o $outdir $args"
echo "$command"
$command
find "$outdir" -name '*.pdb' -delete -printf "deleted '%p'\n"
ls -shk "$outdir" | sort -h

View File

@@ -0,0 +1,36 @@
using Avalonia.Controls;
using DTLib.Demystifier;
using Mlaumcherb.Client.Avalonia.зримое;
using MsBox.Avalonia;
using MsBox.Avalonia.Dto;
using MsBox.Avalonia.Enums;
using MsBox.Avalonia.Models;
namespace Mlaumcherb.Client.Avalonia.холопы;
public static class ErrorHelper
{
internal static void ShowMessageBox(string context, Exception err)
=> ShowMessageBox(context, err.ToStringDemystified());
internal static async void ShowMessageBox(string context, string err)
{
LauncherApp.Logger.LogError(context, err);
var box = MessageBoxManager.GetMessageBoxCustom(new MessageBoxCustomParams
{
ButtonDefinitions = new List<ButtonDefinition> { new() { Name = "пон" } },
ContentTitle = "ОШИБКА",
ContentMessage = err,
Icon = Icon.Error,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
CanResize = true,
MaxWidth = 1000,
MaxHeight = 1000,
SizeToContent = SizeToContent.WidthAndHeight,
ShowInCenter = true,
Topmost = true
}
);
await box.ShowAsync().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,26 @@
namespace Mlaumcherb.Client.Avalonia.холопы;
/// <summary>
/// https://gist.github.com/ststeiger/cb9750664952f775a341
/// </summary>
public static class LZMAHelper
{
public static void Decompress(Stream inputStream, Stream outputStream)
{
var decoder = new SevenZip.Compression.LZMA.Decoder();
var properties = new byte[5];
// Read decoder properties
if (inputStream.Read(properties, 0, 5) != 5)
throw new Exception("lzma stream is too short");
decoder.SetDecoderProperties(properties);
// Read decompressed data size
var fileLengthBytes = new byte[8];
if(inputStream.Read(fileLengthBytes, 0, 8) != 8)
throw new Exception("lzma stream is too short");
long fileLength = BitConverter.ToInt64(fileLengthBytes, 0);
// Decode
decoder.Code(inputStream, outputStream, -1, fileLength, null);
}
}

View File

@@ -0,0 +1,57 @@
using Mlaumcherb.Client.Avalonia.зримое;
namespace Mlaumcherb.Client.Avalonia.холопы;
public static class PathHelper
{
public static IOPath GetRootFullPath() =>
System.IO.Path.GetFullPath(new IOPath(LauncherApp.Config.minecraft_dir).ToString());
public static IOPath GetAssetsDir() =>
Path.Concat(GetRootFullPath(), "assets");
public static IOPath GetAssetIndexFilePath(string id) =>
Path.Concat(GetAssetsDir(), $"assets/indexes/{id}.json");
public static IOPath GetVersionDescriptorPath(string id) =>
Path.Concat(GetVersionDir(id), id + ".json");
public static IOPath GetVersionsDir() =>
Path.Concat(GetRootFullPath(), "versions");
public static IOPath GetVersionDir(string id) =>
Path.Concat(GetVersionsDir(), id);
public static IOPath GetVersionJarFilePath(string id) =>
Path.Concat(GetVersionDir(id), id + ".jar");
public static IOPath GetLibrariesDir() =>
Path.Concat(GetRootFullPath(), "libraries");
public static IOPath GetNativeLibrariesDir(string id) =>
Path.Concat(GetVersionDir(id), "natives", PlatformHelper.GetOsAndArch());
public static IOPath GetJavaRuntimesDir() =>
Path.Concat(GetRootFullPath(), "java");
public static IOPath GetJavaRuntimeDir(string id) =>
Path.Concat(GetJavaRuntimesDir(), PlatformHelper.GetOsAndArch(), id);
public static IOPath GetJavaBinDir(string id) =>
Path.Concat(GetJavaRuntimeDir(id), "bin");
public static IOPath GetJavaExecutablePath(string id, bool debug)
{
string executable_name = "java";
if (debug)
executable_name += "w";
if(OperatingSystem.IsWindows())
executable_name += ".exe";
return Path.Concat(GetJavaBinDir(id), executable_name);
}
public static string GetLog4jConfigFileName() => "log4j.xml";
public static IOPath GetLog4jConfigFilePath() =>
Path.Concat(GetAssetsDir(), GetLog4jConfigFileName());
}

View File

@@ -0,0 +1,55 @@
using System.Runtime.InteropServices;
using Mlaumcherb.Client.Avalonia.классы;
// ReSharper disable CollectionNeverUpdated.Global
namespace Mlaumcherb.Client.Avalonia.холопы;
public static class PlatformHelper
{
public static bool CheckOs(Os os) =>
os.name switch
{
null => true,
"osx" => OperatingSystem.IsMacOS(),
"linux" => OperatingSystem.IsLinux(),
"windows" => OperatingSystem.IsWindows(),
_ => throw new ArgumentOutOfRangeException(os.name)
}
&& os.arch switch
{
null => true,
"i386" or "x86" => RuntimeInformation.OSArchitecture == Architecture.X86,
"x64" => RuntimeInformation.OSArchitecture == Architecture.X64,
"arm64" => RuntimeInformation.OSArchitecture == Architecture.Arm64,
_ => false
};
public static string GetOs() =>
Environment.OSVersion.Platform switch
{
PlatformID.Win32NT => "windows",
PlatformID.Unix => "linux",
PlatformID.MacOSX => "osx",
_ => throw new PlatformNotSupportedException("OS not supported")
};
public static string GetArch() =>
RuntimeInformation.OSArchitecture switch
{
Architecture.X86 => "x86",
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
_ => throw new PlatformNotSupportedException("OS not supported")
};
public static string GetArchOld() =>
RuntimeInformation.OSArchitecture switch
{
Architecture.X86 => "32",
Architecture.X64 => "64",
_ => throw new PlatformNotSupportedException("OS not supported")
};
public static string GetOsAndArch() => GetOs() + '-' + GetArch();
}