commit 48220f8b23573ef9659fd094da2bc75b7128cd0f Author: Max Nullov Date: Fri Feb 21 13:13:35 2025 +0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d30f12 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Plombir Launcher + +Small launcher for vanilla minecraft! + +## WIP diff --git a/src/Launcher/.gitignore b/src/Launcher/.gitignore new file mode 100644 index 0000000..6003b32 --- /dev/null +++ b/src/Launcher/.gitignore @@ -0,0 +1,6 @@ +/bin +/minecraft +/obj +/publish +/.vscode +*.env \ No newline at end of file diff --git a/src/Launcher/Launcher.cs b/src/Launcher/Launcher.cs new file mode 100644 index 0000000..4538625 --- /dev/null +++ b/src/Launcher/Launcher.cs @@ -0,0 +1,140 @@ +using System.Formats.Tar; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using CmlLib.Core; +using CmlLib.Core.Auth; +using CmlLib.Core.ProcessBuilder; + +namespace BlightFlame +{ + /* + Basically a class to manage everything launcher related. + TODO - minecraft downloading from webserver + */ + public class Launcher + { + private readonly string _version; + private readonly string _nickname; + static private MinecraftPath _mcPath; + static private MinecraftLauncher _mcLauncher; + + public int DownloadProgress; + public string DownloadStatus; + + public Launcher(string userName, string version) + { + _version = version; + _nickname = userName; + _mcPath = new($"./runtime/{version}/minecraft"); + _mcLauncher = new(_mcPath); + } + + // Using it for forge loading because i don't know event-based async. + //static TaskCompletionSource tcs = new(); + + + // Not used. Because launcher supports only VANILLA. + // public async Task FetchMinecraft() + // { + + // string archieve = "mc-archieve.tar.xz"; + // string runtimePath = "runtime"; + + // System.Uri download_uri = new("https://dixxe.top/static/distro/cmllibcore-minecraft-forge-1201.tar.xz"); + // WebClient client = new(); + + // if ((!File.Exists(archieve)) && (!Directory.Exists(runtimePath))) + // { + // DownloadStatus = "Fetching deploy-minecraft from host..."; + // Console.WriteLine(DownloadStatus); + + // // Implement async download + // client.DownloadFileAsync(download_uri, archieve); + // client.DownloadDataCompleted += (o, e) => + // { + // DownloadStatus = "Download completed"; + // Console.WriteLine(DownloadStatus); + // }; + // client.DownloadProgressChanged += (o, e) => + // { + // // Curentlly this scope doing nothing + // Console.WriteLine($"{e.ProgressPercentage}% downloaded"); + // DownloadProgress = e.ProgressPercentage; + // DownloadStatus = "Fetching modpack..."; + + // if (DownloadProgress == 100) + // { + // tcs.SetResult(true); + // } + + // }; + // // Waiting for archieve to download. + // await tcs.Task; + // } + + // DownloadStatus = "Unarchiving modpack..."; + // unarchiveMinecraft(archieve, runtimePath); + + // } + + // Not used. Because launcher supports only VANILLA. + // private void unarchiveMinecraft(string archieveName, string path) + // { + // if (Directory.Exists(path)) { return; } + // Console.WriteLine(DownloadStatus); + // Directory.CreateDirectory(path); + // Utils.UnzipTarGzFile(archieveName, path); + + // if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + // { + // Utils.LinuxExec($"chmod -R +x {path}/"); + // Console.WriteLine("Execute permissions added successfully."); + // } + + // File.Delete(archieveName); + // } + + async public Task BuildLauncher() + { + + + _mcLauncher.ByteProgressChanged += (_, args) => + { + Console.WriteLine($"{(int)(args.ProgressedBytes * 0.000001)} MBytes / {(int)(args.TotalBytes * 0.000001)} MBytes"); + DownloadProgress = (int)(args.ProgressedBytes * 100 / args.TotalBytes); + }; + + // Before installAsync I should fetch lightweight minecraft instance from webserver + // Then installAsync will download any runtime needed stuff. + // This method is FetchMinecraft() + // Currently this is only one viable option for forge installations. + // But it requires server, so forge (and all mod loaders) is disabled. + //await FetchMinecraft(); + + DownloadStatus = "Installing minecraft..."; + Console.WriteLine(DownloadStatus); + await _mcLauncher.InstallAsync(_version); + DownloadStatus = "Finished!"; + Console.WriteLine(DownloadStatus); + } + + async public Task RunLauncher() + { + var process = await _mcLauncher.BuildProcessAsync(_version, new MLaunchOption{ + + Session = MSession.CreateOfflineSession(_nickname), + MaximumRamMb = 4096, + + }) ?? throw new Exception("Failed to start minecraft process!"); + + var processUtil = new ProcessWrapper(process); + processUtil.OutputReceived += (s, e) => Console.WriteLine(e); + processUtil.StartWithEvents(); + + await processUtil.WaitForExitTaskAsync(); + } + + } +} + diff --git a/src/Launcher/Program.cs b/src/Launcher/Program.cs new file mode 100644 index 0000000..a304e9e --- /dev/null +++ b/src/Launcher/Program.cs @@ -0,0 +1,10 @@ +namespace BlightFlame +{ + sealed class Program + { + public static void Main() + { + Console.WriteLine("init: launcher"); + } + } +} \ No newline at end of file diff --git a/src/Launcher/Utils.cs b/src/Launcher/Utils.cs new file mode 100644 index 0000000..82c084b --- /dev/null +++ b/src/Launcher/Utils.cs @@ -0,0 +1,74 @@ +using System.IO.Compression; +using System; +using System.IO; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Readers; +using System.Diagnostics; +using CmlLib.Core; + +namespace BlightFlame +{ + static public class Utils + { + public static void UnzipTarGzFile(string filePath, string destinationPath) + { + using (Stream stream = File.OpenRead(filePath)) + { + using (var reader = ReaderFactory.Open(stream)) + { + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory(destinationPath, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + } + } + } + + public static void LinuxExec(string cmd) + { + var escapedArgs = cmd.Replace("\"", "\\\""); + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"" + } + }; + + process.Start(); + process.WaitForExit(); + } + + public static async Task> GetAllMcVersions() + { + return await Task.Run(async() => { + var ln = new MinecraftLauncher(); + var result = new List(); + foreach (var x in await ln.GetAllVersionsAsync()) + { + result.Add(x.Name); + } + return result; + }); + + } + + + + } + +} \ No newline at end of file diff --git a/src/Launcher/cmllibLauncher.csproj b/src/Launcher/cmllibLauncher.csproj new file mode 100644 index 0000000..839f8a6 --- /dev/null +++ b/src/Launcher/cmllibLauncher.csproj @@ -0,0 +1,23 @@ + + + + exe + net9.0 + enable + enable + true + true + + false + + + + + + + + + + + + diff --git a/src/LauncherGUI/.gitignore b/src/LauncherGUI/.gitignore new file mode 100644 index 0000000..b54a4ca --- /dev/null +++ b/src/LauncherGUI/.gitignore @@ -0,0 +1,6 @@ +/bin +/runtime +/obj +/publish +/.vscode +*.env diff --git a/src/LauncherGUI/App.axaml b/src/LauncherGUI/App.axaml new file mode 100644 index 0000000..7f394db --- /dev/null +++ b/src/LauncherGUI/App.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + avares://LauncherGUI/Assets/Fonts/Quicksand-SemiBold.ttf#Quicksand + + + \ No newline at end of file diff --git a/src/LauncherGUI/App.axaml.cs b/src/LauncherGUI/App.axaml.cs new file mode 100644 index 0000000..7f3bd5e --- /dev/null +++ b/src/LauncherGUI/App.axaml.cs @@ -0,0 +1,47 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using System.Linq; +using Avalonia.Markup.Xaml; +using LauncherGUI.ViewModels; +using LauncherGUI.Views; + +namespace LauncherGUI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Avoid duplicate validations from both Avalonia and the CommunityToolkit. + // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins + DisableAvaloniaDataAnnotationValidation(); + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } + + private void DisableAvaloniaDataAnnotationValidation() + { + // Get an array of plugins to remove + var dataValidationPluginsToRemove = + BindingPlugins.DataValidators.OfType().ToArray(); + + // remove each entry found + foreach (var plugin in dataValidationPluginsToRemove) + { + BindingPlugins.DataValidators.Remove(plugin); + } + } +} \ No newline at end of file diff --git a/src/LauncherGUI/Assets/Fonts/Quicksand-SemiBold.ttf b/src/LauncherGUI/Assets/Fonts/Quicksand-SemiBold.ttf new file mode 100644 index 0000000..52059c3 Binary files /dev/null and b/src/LauncherGUI/Assets/Fonts/Quicksand-SemiBold.ttf differ diff --git a/src/LauncherGUI/Assets/Fonts/Quicksand.ttf b/src/LauncherGUI/Assets/Fonts/Quicksand.ttf new file mode 100644 index 0000000..bd332b6 Binary files /dev/null and b/src/LauncherGUI/Assets/Fonts/Quicksand.ttf differ diff --git a/src/LauncherGUI/Assets/background.jpg b/src/LauncherGUI/Assets/background.jpg new file mode 100644 index 0000000..795bb74 Binary files /dev/null and b/src/LauncherGUI/Assets/background.jpg differ diff --git a/src/LauncherGUI/Assets/icon.png b/src/LauncherGUI/Assets/icon.png new file mode 100644 index 0000000..6e24919 Binary files /dev/null and b/src/LauncherGUI/Assets/icon.png differ diff --git a/src/LauncherGUI/Assets/title.png b/src/LauncherGUI/Assets/title.png new file mode 100644 index 0000000..b963581 Binary files /dev/null and b/src/LauncherGUI/Assets/title.png differ diff --git a/src/LauncherGUI/LauncherGUI.csproj b/src/LauncherGUI/LauncherGUI.csproj new file mode 100644 index 0000000..2b9cc47 --- /dev/null +++ b/src/LauncherGUI/LauncherGUI.csproj @@ -0,0 +1,36 @@ + + + WinExe + net9.0 + enable + true + app.manifest + true + + + + + + + + + true + + + + + + + + + + None + All + + + + + + + + diff --git a/src/LauncherGUI/Program.cs b/src/LauncherGUI/Program.cs new file mode 100644 index 0000000..116a17d --- /dev/null +++ b/src/LauncherGUI/Program.cs @@ -0,0 +1,22 @@ +using Avalonia; +using BlightFlame; +using System; + +namespace LauncherGUI; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/src/LauncherGUI/ViewLocator.cs b/src/LauncherGUI/ViewLocator.cs new file mode 100644 index 0000000..7b4a157 --- /dev/null +++ b/src/LauncherGUI/ViewLocator.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using LauncherGUI.ViewModels; + +namespace LauncherGUI; + +public class ViewLocator : IDataTemplate +{ + + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/src/LauncherGUI/ViewModels/LoadingWindowViewModel.cs b/src/LauncherGUI/ViewModels/LoadingWindowViewModel.cs new file mode 100644 index 0000000..e6821b1 --- /dev/null +++ b/src/LauncherGUI/ViewModels/LoadingWindowViewModel.cs @@ -0,0 +1,10 @@ +namespace LauncherGUI.ViewModels; + +public partial class LoadingWindowViewModel : ViewModelBase +{ + private long _loadingProgress; + public long Progress {get => _loadingProgress; set {_loadingProgress = value; OnPropertyChanged(nameof(Progress)); }} + + private string _loadingStatus = "Rotating transistors.."; + public string LoadingStatus {get => _loadingStatus; set {_loadingStatus = value; OnPropertyChanged(nameof(LoadingStatus)); }} +} diff --git a/src/LauncherGUI/ViewModels/MainWindowViewModel.cs b/src/LauncherGUI/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..6d46bca --- /dev/null +++ b/src/LauncherGUI/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using BlightFlame; + +namespace LauncherGUI.ViewModels; + +public partial class MainWindowViewModel : ViewModelBase +{ + public string Greeting { get; } = "Plombir launcher"; + + private string _userNick = "slugcat"; + public string Usernick {get => _userNick; set {_userNick = value; OnPropertyChanged(nameof(Usernick)); }} + + private string _selectedVersion = "1.20.1"; + public string SelectedVersion {get => _selectedVersion; set {_selectedVersion = value; OnPropertyChanged(nameof(SelectedVersion)); }} +} diff --git a/src/LauncherGUI/ViewModels/VersionSelectorWindowViewModel.cs b/src/LauncherGUI/ViewModels/VersionSelectorWindowViewModel.cs new file mode 100644 index 0000000..c5dba50 --- /dev/null +++ b/src/LauncherGUI/ViewModels/VersionSelectorWindowViewModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using BlightFlame; +using LauncherGUI.ViewModels; + +namespace LauncherGUI.ViewModels; + +public partial class VersionSelectorWindowViewModel : ViewModelBase +{ + +} \ No newline at end of file diff --git a/src/LauncherGUI/ViewModels/ViewModelBase.cs b/src/LauncherGUI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..0b1a0a9 --- /dev/null +++ b/src/LauncherGUI/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace LauncherGUI.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} diff --git a/src/LauncherGUI/Views/LoadingWindow.axaml b/src/LauncherGUI/Views/LoadingWindow.axaml new file mode 100644 index 0000000..1b11eaf --- /dev/null +++ b/src/LauncherGUI/Views/LoadingWindow.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/LauncherGUI/Views/LoadingWindow.axaml.cs b/src/LauncherGUI/Views/LoadingWindow.axaml.cs new file mode 100644 index 0000000..ae923b7 --- /dev/null +++ b/src/LauncherGUI/Views/LoadingWindow.axaml.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; +using BlightFlame; +using LauncherGUI.ViewModels; + +namespace LauncherGUI.Views; + +public partial class LoadingWindow : Window +{ + private Launcher ln; + public LoadingWindow(string nickname, string? version = null) + { + ln = new Launcher(nickname, version ?? "1.20.1"); + InitializeComponent(); + DataContext = new LoadingWindowViewModel(); + } + + public async Task InitLoading() + { + var viewModel = DataContext as LoadingWindowViewModel; + if (viewModel == null) + { + throw new InvalidOperationException("No DataContext set"); + } + Task updateGui = new Task(async () => { + while (ln.DownloadStatus != "Finished!"){ + viewModel.Progress = ln.DownloadProgress; + viewModel.LoadingStatus = ln.DownloadStatus; + await Task.Delay(1000); + } + viewModel.Progress = 100; + }); + updateGui.Start(); + + await ln.BuildLauncher(); + + + + } + + public async Task RunMinecraft() + { + await ln.RunLauncher(); + } +} \ No newline at end of file diff --git a/src/LauncherGUI/Views/MainWindow.axaml b/src/LauncherGUI/Views/MainWindow.axaml new file mode 100644 index 0000000..503a0e2 --- /dev/null +++ b/src/LauncherGUI/Views/MainWindow.axaml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +