MainWindowViewModel.cs 33 KB

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