MainWindowViewModel.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. using Avalonia.Controls.ApplicationLifetimes;
  2. using ReactiveUI;
  3. using System;
  4. using System.Collections.ObjectModel;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Text;
  8. using System.Text.Json;
  9. using System.Threading.Tasks;
  10. using VeloeMinecraftLauncher.Entity.LauncherProfiles;
  11. using VeloeMinecraftLauncher.Utils;
  12. using VeloeMinecraftLauncher.Views;
  13. using Microsoft.AspNetCore.SignalR.Client;
  14. using VeloeMinecraftLauncher.Entity.McStatus;
  15. using System.Timers;
  16. using System.Reflection;
  17. using Serilog;
  18. using Avalonia.Controls;
  19. using Avalonia.Threading;
  20. using System.IO.Compression;
  21. using ReactiveUI.Validation.Extensions;
  22. using Avalonia.Media;
  23. using Avalonia.Data;
  24. using System.Collections.Generic;
  25. using VeloeMinecraftLauncher.Models.Entity;
  26. using VeloeMinecraftLauncher.Models;
  27. using System.Linq;
  28. using System.Threading;
  29. using System.ComponentModel;
  30. namespace VeloeMinecraftLauncher.ViewModels;
  31. public class MainWindowViewModel : ViewModelBase
  32. {
  33. public MainWindowViewModel()
  34. {
  35. Task.Run(async () =>
  36. {
  37. try
  38. {
  39. //creating logger
  40. EventSink eventSink = new(null);
  41. eventSink.DataReceived += LogHandler;
  42. var hook = new CaptureFilePathHook();
  43. _logger = new LoggerConfiguration()
  44. .MinimumLevel.Debug()
  45. .WriteTo.Sink(eventSink, Settings.consoleLogEventLevel)
  46. .WriteTo.File("launcher.log", Settings.fileLogEventLevel, fileSizeLimitBytes: Settings.maxLog * 1024, rollOnFileSizeLimit: true, hooks: hook)// restricted... is Optional
  47. .CreateLogger();
  48. Settings.logger = _logger;
  49. Settings.logFilePath = hook;
  50. //loading settings
  51. _logger.Debug("Loading settings.");
  52. Settings.LoadSettings();
  53. _username = Settings.username;
  54. //loading local verions
  55. _logger.Debug("Loading local versions.");
  56. updateAvailable();
  57. }
  58. catch (Exception ex)
  59. {
  60. OpenErrorWindow(ex);
  61. if (_logger is null)
  62. {
  63. _logger = new LoggerConfiguration().MinimumLevel.Debug().CreateLogger();
  64. }
  65. }
  66. this.ValidationRule(
  67. viewModel => viewModel.Username,
  68. value => { return !string.IsNullOrEmpty(value); },
  69. "Empty username.");
  70. UpdateUpdater();
  71. try
  72. {
  73. //check launcher update
  74. _logger.Information("Checking launcher versions updates.");
  75. _latestLauncherInfo = await Downloader.DownloadAndDeserializeJsonData<LatestLauncherVersion>("https://files.veloe.link/launcher/update/versions.json");
  76. if (_latestLauncherInfo is not null && _latestLauncherInfo.Latest is not null)
  77. {
  78. _logger.Information("Launcher version on server: {0}", _latestLauncherInfo.Latest);
  79. _logger.Information("Launcher version: {0}", Assembly.GetExecutingAssembly().GetName().Version);
  80. if (new Version(_latestLauncherInfo.Latest) > Assembly.GetExecutingAssembly().GetName().Version)
  81. {
  82. _logger.Debug("Update available!");
  83. IsUpdateAvailable = true;
  84. }
  85. }
  86. else
  87. _logger.Warning("Can't get latest verion info! Skipping...");
  88. }
  89. catch (Exception ex)
  90. {
  91. OpenErrorWindow(ex);
  92. }
  93. try
  94. {
  95. var changelog = await Downloader.DownloadAndDeserializeJsonData<List<Changelog>>("https://files.veloe.link/launcher/changelog.json");
  96. if (changelog is not null && Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  97. {
  98. #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  99. Dispatcher.UIThread.InvokeAsync(() =>
  100. {
  101. var stackpanel = desktop.MainWindow.GetControl<StackPanel>("ChangeLogStackPanel");
  102. foreach (var version in changelog)
  103. {
  104. if (version.Title is not null)
  105. {
  106. stackpanel.Children.Add(new TextBlock()
  107. {
  108. Text = version.Title,
  109. VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch,
  110. TextWrapping = TextWrapping.Wrap,
  111. FontSize = 16
  112. });
  113. }
  114. stackpanel.Children.Add(new TextBlock()
  115. {
  116. Text = version.Text,
  117. VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch,
  118. TextWrapping = TextWrapping.Wrap
  119. });
  120. }
  121. });
  122. #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  123. }
  124. }
  125. catch (Exception ex)
  126. {
  127. OpenErrorWindow(ex);
  128. }
  129. try
  130. {
  131. _logger.Debug("Connecting to WebSoket");
  132. //conection to my servers
  133. var serverNames = await Downloader.DownloadAndDeserializeJsonData<List<string>>("https://files.veloe.link/launcher/servers.json");
  134. if (serverNames is not null)
  135. {
  136. _connection = new HubConnectionBuilder()
  137. .WithUrl("https://monitor.veloe.link/hubs/data")
  138. .WithAutomaticReconnect()
  139. .Build();
  140. Func<Exception, Task> reconnecting = ex => Task.Run(() =>
  141. {
  142. _logger.Warning("Reconnecting to WebCoket...");
  143. });
  144. Func<string, Task> reconnected = str => Task.Run(() =>
  145. {
  146. _logger.Warning("Reconnected to WebCoket.");
  147. foreach (var server in serverNames)
  148. {
  149. _connection.InvokeAsync("ConnectToGroup", server);
  150. }
  151. });
  152. _connection.Reconnecting += reconnecting;
  153. _connection.Reconnected += reconnected;
  154. if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  155. {
  156. #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  157. Dispatcher.UIThread.InvokeAsync(() =>
  158. {
  159. var stackpanel = desktop.MainWindow.GetControl<StackPanel>("ServersStackPanel");
  160. foreach (var server in serverNames)
  161. stackpanel.Children.Add(CreateServerPanel(server));
  162. });
  163. #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  164. }
  165. await _connection.StartAsync();
  166. foreach (var server in serverNames)
  167. {
  168. await _connection.InvokeAsync("ConnectToGroup", server);
  169. }
  170. _logger.Debug("Connected to WebSoket");
  171. }
  172. _consoleOutputTimer.Elapsed += UpdateConsoleOutput;
  173. _consoleOutputTimer.AutoReset = false;
  174. }
  175. catch (Exception ex)
  176. {
  177. OpenErrorWindow(ex);
  178. }
  179. try
  180. {
  181. _logger.Debug("Checking modpacks updates...");
  182. var modpacksInfo = await Downloader.DownloadAndDeserializeJsonData<List<Modpack>>("https://files.veloe.link/launcher/modpacks.json") ?? new List<Modpack>();
  183. var installedModpacks = //DownloadedVersions.Where(v => modpacksInfo.Select(m => m.Name).Contains(v.version)).Join(modpacksInfo, (v, m) => v.version == m.);
  184. from downloadedVersion in DownloadedVersions
  185. join modpack in modpacksInfo on downloadedVersion.version equals modpack.Name
  186. select new
  187. {
  188. Name = downloadedVersion.version,
  189. Path = downloadedVersion.path,
  190. Revision = modpack.Revision
  191. };
  192. var modpacksToUpdate = new List<string>();
  193. var modpacksRevsionUnknown = new List<string>();
  194. foreach (var modpack in installedModpacks)
  195. {
  196. if (File.Exists(Path.GetDirectoryName(modpack.Path) + "/revision.json"))
  197. {
  198. try
  199. {
  200. if (modpack.Revision > JsonSerializer.Deserialize<int>(File.ReadAllText(Path.GetDirectoryName(modpack.Path) + "/revision.json")))
  201. modpacksToUpdate.Add(modpack.Name);
  202. }
  203. catch (Exception)
  204. {
  205. modpacksRevsionUnknown.Add(modpack.Name);
  206. }
  207. }
  208. else
  209. modpacksRevsionUnknown.Add(modpack.Name);
  210. }
  211. var message = string.Empty;
  212. if (modpacksToUpdate.Any())
  213. message = $"Found updates for selected installed modpacks:{Environment.NewLine} {string.Join($"{Environment.NewLine} ", modpacksToUpdate)}";
  214. if (modpacksRevsionUnknown.Any())
  215. {
  216. if (!string.IsNullOrEmpty(message))
  217. message += Environment.NewLine;
  218. message += $"Can't get revision for selected installed modpacks:{Environment.NewLine} {string.Join($"{Environment.NewLine} ", modpacksRevsionUnknown)}{Environment.NewLine}Please update them for correct verion checking!";
  219. }
  220. if (!string.IsNullOrEmpty(message))
  221. OpenErrorWindow(message);
  222. _logger.Debug("Checking modpacks updates finished successfully!");
  223. }
  224. catch (Exception ex)
  225. {
  226. OpenErrorWindow(ex);
  227. }
  228. });
  229. }
  230. System.Timers.Timer _consoleOutputTimer = new(250);
  231. private HubConnection? _connection;
  232. private string _downloadButton = "Download versions";
  233. private string _startButton = "Start Minecraft";
  234. private string _username = string.Empty;
  235. private StringBuilder _consoleText = new();
  236. private int _downloadedIndex;
  237. private string _argumentsBox = string.Empty;
  238. private bool _isNoGameRunning = true;
  239. private bool _isUpdateAvailable = false;
  240. private string _settingsButton = "Settings";
  241. private string _startButtonOutput = string.Empty;
  242. private CancellationTokenSource _tokenSource = new();
  243. ILogger _logger;
  244. LatestLauncherVersion? _latestLauncherInfo;
  245. DownloadedVersion _downloadedVersion;
  246. DownloadedVersion _startedVersion;
  247. ObservableCollection<DownloadedVersion> _downloadedVersions;
  248. public ObservableCollection<DownloadedVersion> DownloadedVersions {
  249. get => _downloadedVersions;
  250. set
  251. {
  252. this.RaiseAndSetIfChanged(ref _downloadedVersions, value);
  253. }
  254. }
  255. public DownloadedVersion DownloadedVersion
  256. {
  257. get => _downloadedVersion;
  258. set
  259. {
  260. this.RaiseAndSetIfChanged(ref _downloadedVersion, value);
  261. //Settings.lastChosenVersion = value.version;
  262. //logger.Debug("Version choosen: {0}",value.version);
  263. //logger.Debug("Version json: {0}", value.path);
  264. }
  265. }
  266. public int DownloadedIndex
  267. {
  268. get => _downloadedIndex;
  269. set
  270. {
  271. this.RaiseAndSetIfChanged(ref _downloadedIndex, value);
  272. if (value >= 0 && value < DownloadedVersions.Count)
  273. DownloadedVersion = DownloadedVersions[value];
  274. }
  275. }
  276. public string Greeting => "Welcome to Cringe Launcher!";
  277. public string DownloadButton {
  278. get => _downloadButton;
  279. set => this.RaiseAndSetIfChanged(ref _downloadButton, value);
  280. }
  281. public string StartButton
  282. {
  283. get => _startButton;
  284. set => this.RaiseAndSetIfChanged(ref _startButton, value);
  285. }
  286. public string Username
  287. {
  288. get => _username;
  289. set
  290. {
  291. this.RaiseAndSetIfChanged(ref _username, value);
  292. this.RaisePropertyChanged(nameof(IsStartButtonEnabled));
  293. }
  294. }
  295. public string ConsoleText
  296. {
  297. get
  298. {
  299. if (_consoleText.Length < UInt16.MaxValue)
  300. return _consoleText.ToString();
  301. else
  302. return _consoleText.ToString(_consoleText.Length - UInt16.MaxValue, UInt16.MaxValue);
  303. }
  304. set
  305. {
  306. _consoleText.Clear();
  307. _consoleText.Append(value);
  308. this.RaisePropertyChanged(nameof(ConsoleText));
  309. ConsoleTextCaretIndex = int.MaxValue;
  310. }
  311. }
  312. public string StartButtonOutput
  313. {
  314. get => _startButtonOutput;
  315. set => this.RaiseAndSetIfChanged(ref _startButtonOutput, value);
  316. }
  317. public string ArgumentsBox
  318. {
  319. get => _argumentsBox;
  320. set => this.RaiseAndSetIfChanged(ref _argumentsBox, value);
  321. }
  322. public string SettingsButton
  323. {
  324. get => _settingsButton;
  325. set => this.RaiseAndSetIfChanged(ref _settingsButton, value);
  326. }
  327. public bool IsNoGameRunning
  328. {
  329. get => _isNoGameRunning;
  330. set
  331. {
  332. this.RaiseAndSetIfChanged(ref _isNoGameRunning, value);
  333. this.RaisePropertyChanged(nameof(IsStartButtonEnabled));
  334. }
  335. }
  336. public bool IsStartButtonEnabled
  337. {
  338. get => IsNoGameRunning && !string.IsNullOrEmpty(Username);
  339. }
  340. public bool IsUpdateAvailable
  341. {
  342. get => _isUpdateAvailable;
  343. set => this.RaiseAndSetIfChanged(ref _isUpdateAvailable, value);
  344. }
  345. public int ConsoleTextCaretIndex
  346. {
  347. get => int.MaxValue;
  348. set => this.RaisePropertyChanged(nameof(ConsoleTextCaretIndex));
  349. }
  350. public void OnClickCommand()
  351. {
  352. var versionsDownloader = new VersionsDownloader
  353. {
  354. DataContext = new VersionsDownloaderViewModel()
  355. };
  356. if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  357. {
  358. versionsDownloader.Closed += updateAvailable;
  359. versionsDownloader.ShowDialog(desktop.MainWindow);
  360. }
  361. }
  362. public async void StartMinecraft()
  363. {
  364. await Task.Run(async() =>
  365. {
  366. try
  367. {
  368. _logger.Debug("Starting minecraft.");
  369. if (DownloadedVersion is null)
  370. {
  371. IsNoGameRunning = true;
  372. return;
  373. }
  374. int version = 0;
  375. using (StreamReader reader = new StreamReader(DownloadedVersion.path))
  376. {
  377. string json = reader.ReadToEnd();
  378. var versionJson = JsonSerializer.Deserialize<Entity.Version.Version>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  379. if (versionJson is null)
  380. throw new ArgumentNullException(nameof(versionJson));
  381. if (Settings.checkGameAssets)
  382. {
  383. TaskStatus result;
  384. if (string.IsNullOrEmpty(versionJson.InheritsFrom))
  385. result = await Downloader.StartDownload(value => StartButtonOutput = value, value => { } ,value => IsNoGameRunning = value, value => { }, versionJson, _tokenSource.Token);
  386. else
  387. {
  388. using (StreamReader inheritsFromReader = new StreamReader(Settings.minecraftForlderPath + "versions/" + versionJson.InheritsFrom + "/" + versionJson.InheritsFrom + ".json"))
  389. {
  390. string jsonInheritsFrom = inheritsFromReader.ReadToEnd();
  391. var inheritsFromJson = JsonSerializer.Deserialize<Entity.Version.Version>(jsonInheritsFrom, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  392. if (inheritsFromJson is not null)
  393. result = await Downloader.StartDownload(value => StartButtonOutput = value, value => { }, value => IsNoGameRunning = value, value => { }, inheritsFromJson, _tokenSource.Token);
  394. else
  395. result = TaskStatus.Faulted;
  396. }
  397. }
  398. if (result != TaskStatus.RanToCompletion)
  399. {
  400. IsNoGameRunning = true;
  401. StartButtonOutput = "Checking game files task faulted.";
  402. return;
  403. }
  404. }
  405. string arguments = StartCommandBuilder.Build(versionJson, Username);
  406. if (!Settings.useCustomJava)
  407. {
  408. if (versionJson?.JavaVersion?.MajorVersion is not null)
  409. {
  410. _logger.Debug("Java version required: {0}", versionJson.JavaVersion.MajorVersion);
  411. version = versionJson.JavaVersion.MajorVersion.Value;
  412. }
  413. else
  414. {
  415. if (!string.IsNullOrEmpty(versionJson?.InheritsFrom))
  416. {
  417. using (StreamReader inheritsFromReader = new StreamReader(Settings.minecraftForlderPath + "versions/" + versionJson.InheritsFrom + "/" + versionJson.InheritsFrom + ".json"))
  418. {
  419. string jsonInheritsFrom = inheritsFromReader.ReadToEnd();
  420. var inheritsFromJson = JsonSerializer.Deserialize<Entity.Version.Version>(jsonInheritsFrom, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  421. if (inheritsFromJson?.JavaVersion?.MajorVersion is not null)
  422. {
  423. _logger.Debug("Java version required: {0}", inheritsFromJson.JavaVersion.MajorVersion);
  424. version = inheritsFromJson.JavaVersion.MajorVersion.Value;
  425. }
  426. }
  427. }
  428. else
  429. {
  430. version = 8;
  431. }
  432. }
  433. }
  434. //ConsoleText += arguments;
  435. ArgumentsBox = arguments;
  436. }
  437. if (DownloadedVersion is null)
  438. return;
  439. string javaPath = Settings.javaPath;
  440. if (!Settings.useCustomJava)
  441. {
  442. javaPath = Path.GetFullPath(Settings.minecraftForlderPath + "javaruntime/" + version + "/bin/java");
  443. if (OperatingSystem.IsWindows())
  444. javaPath += ".exe";
  445. }
  446. else
  447. {
  448. javaPath = Path.Combine(Settings.javaPath, "bin/java");
  449. if (OperatingSystem.IsWindows())
  450. javaPath += ".exe";
  451. }
  452. _logger.Debug("Java version path: {0}", Path.Combine(Settings.minecraftForlderPath, javaPath));
  453. _logger.Debug("Minecraft arguments: {0}", ArgumentsBox);
  454. ProcessStartInfo proc;
  455. proc = new ProcessStartInfo
  456. {
  457. UseShellExecute = false,
  458. RedirectStandardOutput = true,
  459. RedirectStandardError = true,
  460. WindowStyle = ProcessWindowStyle.Hidden,
  461. CreateNoWindow = true,
  462. FileName = Path.GetFullPath(Path.Combine(Settings.minecraftForlderPath, javaPath)),
  463. StandardErrorEncoding = Encoding.UTF8,
  464. WorkingDirectory = Path.GetDirectoryName(Path.Combine(Settings.minecraftForlderPath, javaPath)),
  465. Arguments = ArgumentsBox
  466. };
  467. Process minecraft = new Process();
  468. minecraft.StartInfo = proc;
  469. if (!Settings.useCustomJava)
  470. minecraft.StartInfo.EnvironmentVariables["JAVA_HOME"] = $"{Settings.minecraftForlderPath}javaruntime/{version}";
  471. else
  472. minecraft.StartInfo.EnvironmentVariables["JAVA_HOME"] = Settings.javaPath;
  473. minecraft.StartInfo.EnvironmentVariables["PATH"] = "%JAVA_HOME%\bin;%PATH%";
  474. #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  475. Task.Run(() =>
  476. {
  477. try
  478. {
  479. _logger.Debug("Starting java process.");
  480. minecraft.OutputDataReceived += OutputHandler;
  481. minecraft.ErrorDataReceived += OutputHandler;
  482. //* Start process and handlers
  483. //minecraft.WaitForExit();
  484. minecraft.EnableRaisingEvents = true;
  485. IsNoGameRunning = false;
  486. _startedVersion = DownloadedVersion;
  487. minecraft.Start();
  488. minecraft.Exited += ProcessExited;
  489. minecraft.BeginOutputReadLine();
  490. minecraft.BeginErrorReadLine();
  491. if (!Settings.gameLogToLauncher)
  492. {
  493. minecraft.OutputDataReceived -= OutputHandler;
  494. minecraft.CancelOutputRead();
  495. }
  496. return Task.CompletedTask;
  497. }
  498. catch (Exception ex)
  499. {
  500. IsNoGameRunning = true;
  501. OpenErrorWindow(ex);
  502. return Task.CompletedTask;
  503. }
  504. });
  505. #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  506. _logger.Debug("Updating Settings");
  507. Settings.username = _username;
  508. Settings.lastChosenVersion = DownloadedVersion.version;
  509. Settings.SaveSettings();
  510. }
  511. catch (Exception ex)
  512. {
  513. OpenErrorWindow(ex);
  514. }
  515. });
  516. }
  517. void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
  518. {
  519. //Todo create multiple TextBlocks if char limit
  520. if (!_consoleOutputTimer.Enabled)
  521. {
  522. _consoleOutputTimer.Stop();
  523. _consoleOutputTimer.Start();
  524. }
  525. _consoleText.Append(outLine.Data + "\n");
  526. }
  527. void LogHandler(object? sendingProcess, EventArgs args)
  528. {
  529. if (!_consoleOutputTimer.Enabled)
  530. {
  531. _consoleOutputTimer.Stop();
  532. _consoleOutputTimer.Start();
  533. }
  534. _consoleText.Append(((MyEventArgs)args).Data);
  535. }
  536. void UpdateConsoleOutput(object? source, ElapsedEventArgs e)
  537. {
  538. this.RaisePropertyChanged(nameof(ConsoleText));
  539. ScrollToEnd("ConsoleScroll");
  540. }
  541. void ScrollToEnd(string scrollName)
  542. {
  543. if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  544. {
  545. Dispatcher.UIThread.InvokeAsync(() =>
  546. {
  547. var scroll = desktop.MainWindow.GetControl<ScrollViewer>(scrollName);
  548. scroll?.ScrollToEnd();
  549. });
  550. }
  551. }
  552. void ProcessExited(object? sendingProcess, EventArgs e)
  553. {
  554. if(sendingProcess is Process minecraftProcess)
  555. {
  556. minecraftProcess.Exited -= ProcessExited;
  557. minecraftProcess.OutputDataReceived -= OutputHandler;
  558. minecraftProcess.ErrorDataReceived -= OutputHandler;
  559. minecraftProcess.CancelOutputRead();
  560. minecraftProcess.CancelErrorRead();
  561. _logger.Debug("Exit code: {0}", minecraftProcess.ExitCode);
  562. StartButtonOutput = "";
  563. IsNoGameRunning = true;
  564. if (minecraftProcess.ExitCode is not (0 or 2))
  565. OpenErrorWindow(new JavaProcessException($"Minecraft process exited with an error code {minecraftProcess.ExitCode}.\nCheck log in game folder or launcher console.", $"{Settings.minecraftForlderPath}versions/{_startedVersion.version}/crash-reports"));
  566. else if (minecraftProcess.ExitCode is 2)
  567. {
  568. OpenErrorWindow(new Exception("JVM exited on the startup (Exit code 2). Check your Java installation. Get more info in the laucher console."));
  569. }
  570. minecraftProcess.Dispose();
  571. }
  572. }
  573. public void updateAvailable()
  574. {
  575. if (DownloadedVersions is null)
  576. DownloadedVersions = new();
  577. DownloadedVersions.Clear();
  578. DirectoryInfo versions = new( Settings.minecraftForlderPath + "versions");
  579. try
  580. {
  581. var dirs = versions.GetDirectories("*", SearchOption.TopDirectoryOnly);
  582. LauncherProfiles profiles = new LauncherProfiles();
  583. profiles.SelectedProfile = "NotImplemented";
  584. foreach (var dir in dirs)
  585. {
  586. _logger.Debug("Checking folder {0}", dir.Name);
  587. string checkedPath;
  588. if (File.Exists(checkedPath = dir.FullName + "/" + dir.Name + ".json"))
  589. {
  590. _logger.Debug("Found version {0}",dir.Name);
  591. DownloadedVersions.Add(new DownloadedVersion(checkedPath,dir.Name));
  592. profiles.Profiles.Add($"Version {dir.Name}", new Profile() { Name = dir.Name, LastVersionId = dir.Name, LauncherVisibilityOnGameClose = "keep the launcher open" });
  593. }
  594. }
  595. if (Settings.lastChosenVersion != null)
  596. {
  597. DownloadedIndex = 0;
  598. for (int i = 0; i < DownloadedVersions.Count; i++)
  599. {
  600. if (DownloadedVersions[i].version == Settings.lastChosenVersion)
  601. {
  602. DownloadedIndex = i;
  603. break;
  604. }
  605. }
  606. }
  607. if (!File.Exists(Settings.minecraftForlderPath + "launcher_profiles.json"))
  608. {
  609. var file = File.Create(Settings.minecraftForlderPath + "launcher_profiles.json");
  610. file.Close();
  611. file.Dispose();
  612. }
  613. var lpString = JsonSerializer.Serialize(profiles, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  614. File.WriteAllText(Settings.minecraftForlderPath + "launcher_profiles.json", lpString);
  615. }
  616. catch (DirectoryNotFoundException)
  617. {
  618. Directory.CreateDirectory(Settings.minecraftForlderPath + "versions");
  619. return;
  620. }
  621. }
  622. public void updateAvailable(object? sendingObject, EventArgs e)
  623. {
  624. try
  625. {
  626. updateAvailable();
  627. }
  628. catch (Exception ex)
  629. {
  630. OpenErrorWindow(ex);
  631. }
  632. switch (sendingObject)
  633. {
  634. case VersionsDownloader:
  635. ((VersionsDownloader)sendingObject).Closed -= updateAvailable;
  636. break;
  637. case SettingsWindow:
  638. ((SettingsWindow)sendingObject).Closed -= updateAvailable;
  639. break;
  640. }
  641. }
  642. public void OpenSettings()
  643. {
  644. var settingsWindow = new SettingsWindow { DataContext = new SettingsWindowViewModel() };
  645. if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  646. {
  647. settingsWindow.Closed += updateAvailable;
  648. settingsWindow.ShowDialog(desktop.MainWindow);
  649. }
  650. }
  651. public void DownloadUpdate()
  652. {
  653. _logger.Debug("Started updater.exe");
  654. Process updater = new Process();
  655. updater.StartInfo.FileName = "Updater";
  656. if (OperatingSystem.IsWindows())
  657. updater.StartInfo.FileName += ".exe";
  658. if (!File.Exists(updater.StartInfo.FileName))
  659. Task.Run(()=>UpdateUpdater()).Wait();
  660. if (File.Exists(updater.StartInfo.FileName))
  661. {
  662. updater.Start();
  663. if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  664. {
  665. desktop.MainWindow.Close();
  666. }
  667. }
  668. else
  669. OpenErrorWindow(new FileNotFoundException($"File {updater.StartInfo.FileName} not found!"));
  670. }
  671. private async void UpdateUpdater()
  672. {
  673. try
  674. {
  675. string fileName = "Updater";
  676. if (OperatingSystem.IsWindows())
  677. fileName += ".exe";
  678. if (File.Exists(fileName))
  679. {
  680. FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(fileName);
  681. var latestLauncherVersion = await Downloader.DownloadAndDeserializeJsonData<LatestLauncherVersion>("https://files.veloe.link/launcher/update/versions.json");
  682. if (OperatingSystem.IsLinux())
  683. {
  684. _logger.Information("Manual updates only.");
  685. return;
  686. }
  687. if (latestLauncherVersion?.Updater is not null && fileVersionInfo?.FileVersion is not null)
  688. {
  689. _logger.Information("Latest updater version on server: {0}", latestLauncherVersion.Updater);
  690. _logger.Information("Updater version: {0}", fileVersionInfo.FileVersion);
  691. if (!(new Version(latestLauncherVersion.Updater) > new Version(fileVersionInfo.FileVersion)))
  692. {
  693. _logger.Information($"No update for \"{fileName}\" required.");
  694. return;
  695. }
  696. }
  697. else
  698. _logger.Warning("Version verification for {0} failed: latestLauncherVersion or fileVersionInfo does not contain version info or null!", fileName);
  699. }
  700. try
  701. {
  702. _logger.Information("Downloading updater.zip");
  703. string url = String.Empty;
  704. if (OperatingSystem.IsWindows())
  705. url = "https://files.veloe.link/launcher/update/updater.zip";
  706. if (OperatingSystem.IsLinux())
  707. url = "https://files.veloe.link/launcher/update/updater_linux-x64.zip";
  708. if (url == String.Empty)
  709. return;
  710. await Downloader.DownloadFileAsync(url, "updater.zip", _tokenSource.Token);
  711. }
  712. catch (Exception)
  713. {
  714. _logger.Error("Error occured on getting updater.zip from the server.");
  715. throw;
  716. }
  717. _logger.Information("Unpacking updater.zip");
  718. ZipFile.ExtractToDirectory($"updater.zip", Directory.GetCurrentDirectory(), true);
  719. _logger.Information("Deleting updater.zip");
  720. File.Delete($"updater.zip");
  721. }
  722. catch (Exception ex)
  723. {
  724. OpenErrorWindow(ex);
  725. }
  726. }
  727. private Panel CreateServerPanel(string name)
  728. {
  729. ServerPanelModel serverPanelModel = new(name, "Wait for update...", "No players.",string.Empty);
  730. Panel panel = new Panel()
  731. {
  732. VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top,
  733. HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch,
  734. Height = 75,
  735. Width = 150,
  736. Margin = Avalonia.Thickness.Parse("0 0 10 10")
  737. };
  738. panel.Children.Add(new Border()
  739. {
  740. Background = Brushes.Black,
  741. Opacity = 0.2,
  742. CornerRadius = Avalonia.CornerRadius.Parse("15")
  743. });
  744. var tip = new ToolTip()
  745. {
  746. Content = new TextBlock() {TextWrapping = TextWrapping.Wrap, [!TextBlock.TextProperty] = new Binding { Source = serverPanelModel,Path = nameof(serverPanelModel.Tip) } }
  747. };
  748. ToolTip.SetTip(panel, tip);
  749. StackPanel textPanel = new() { VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
  750. textPanel.Children.Add(new TextBlock() { VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, [!TextBlock.TextProperty] = new Binding { Source = serverPanelModel, Path = nameof(serverPanelModel.Status) } });
  751. textPanel.Children.Add(new TextBlock() { FontSize = 20, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, [!TextBlock.TextProperty] = new Binding { Source = serverPanelModel, Path = nameof(serverPanelModel.Players) } });
  752. panel.Children.Add(textPanel);
  753. System.Timers.Timer timeoutTimer = new System.Timers.Timer(30000);
  754. timeoutTimer.Elapsed += (object? source, ElapsedEventArgs e) =>
  755. {
  756. serverPanelModel.Status = $"{serverPanelModel.Name}: Offline";
  757. serverPanelModel.Players = string.Empty;
  758. };
  759. timeoutTimer.Start();
  760. _connection?.On<string>($"Update{serverPanelModel.Name}", (message) =>
  761. {
  762. McStatus? status = JsonSerializer.Deserialize<McStatus>(message, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
  763. if(status is not null)
  764. {
  765. serverPanelModel.Status = $"{serverPanelModel.Name}: Online";
  766. serverPanelModel.Players = $"{status.NumPlayers}/{status.MaxPlayers}";
  767. serverPanelModel.Tip = String.Empty;
  768. if (UInt16.Parse(status.NumPlayers ?? "0") > 0)
  769. foreach (var player in status.Players ?? new())
  770. {
  771. serverPanelModel.Tip += player;
  772. serverPanelModel.Tip += "\n";
  773. }
  774. else
  775. serverPanelModel.Tip = "No players.";
  776. }
  777. timeoutTimer.Stop();
  778. timeoutTimer.Start();
  779. });
  780. return panel;
  781. }
  782. public void OnClosing(object sender, CancelEventArgs args)
  783. {
  784. _tokenSource.Cancel();
  785. _tokenSource.Dispose();
  786. }
  787. }