using System; using System.Diagnostics; using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Net.Http; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; using Glitonea.Mvvm; using Glitonea.Mvvm.Messaging; using Saradomin.Infrastructure; using Saradomin.Infrastructure.Services; using Saradomin.Model.Settings.Launcher; using Saradomin.Utilities; using static Saradomin.Utilities.SingleplayerManagement; namespace Saradomin.ViewModel.Controls { public class SingleplayerViewModel : ViewModelBase { private readonly ISingleplayerUpdateService _singleplayerUpdateService; private readonly ISettingsService _settingsService; private readonly IJavaUpdateService _javaUpdateService; public string SingleplayerDownloadText { get; private set; } = Directory.Exists(CrossPlatform.GetSingleplayerHome()) ? "Update Singleplayer" : "Download Singleplayer"; public bool CanLaunch { get; private set; } = File.Exists(CrossPlatform.LocateSingleplayerExecutable()); public TextBox SingleplayerLogsTextBox { get; } public bool ShowLogPanel { get; private set; } public LauncherSettings Launcher => _settingsService.Launcher; private string _oldServerAddress, _oldManagementAddress; private Process _serverProcess; public SingleplayerViewModel(ISettingsService settingsService, IJavaUpdateService javaUpdateService, ISingleplayerUpdateService iSingleplayerUpdateService) { _settingsService = settingsService; _javaUpdateService = javaUpdateService; _singleplayerUpdateService = iSingleplayerUpdateService; _singleplayerUpdateService.SingleplayerDownloadProgressChanged += OnSingleplayerDownloadProgressChanged; SingleplayerLogsTextBox = new TextBox { Foreground = new SolidColorBrush(Colors.Black), FontSize = 12, Margin = new Thickness(8, 0, 0, 0), IsReadOnly = true, BorderThickness = new Thickness(0), Background = Brushes.Transparent, AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, }; Message.Subscribe(this, OnClientClosed); } private void OnClientClosed(ClientClosedMessage _) { if (_serverProcess == null) return; _serverProcess.Kill(); _serverProcess = null; CanLaunch = true; _settingsService.Client.GameServerAddress = _oldServerAddress; _settingsService.Client.ManagementServerAddress = _oldManagementAddress; } private void OnSingleplayerDownloadProgressChanged(object sender, Tuple e) { float progress = e.Item1; bool finished = e.Item2; if (finished) { SingleplayerDownloadText = "Update Singleplayer"; ApplyLatestBackup(PrintLog); PrintLog($"Singleplayer Download complete"); PrintLog($""); CanLaunch = true; return; } if (progress >= 1f) { SingleplayerDownloadText = "Extracting..."; return; } SingleplayerDownloadText = $"Downloading... {progress * 100:F2}%"; } public void DownloadSingleplayer() { if (File.Exists(CrossPlatform.LocateSingleplayerExecutable()) && !CanLaunch) return; // While the button could be disabled, this looks nicer visually (since we update the download progress in the button text) CanLaunch = false; MakeBackup(); PrintLog($"Starting singleplayer download..."); _singleplayerUpdateService.DownloadSingleplayer(); } private void MakeBackup() { PrintLog("Starting backup of player saves and economy files.."); SingleplayerManagement.MakeBackup(PrintLog); PrintLog($"Done backing up player saves and economy files"); PrintLog($""); } public void OpenBackupFolder() { if (!Directory.Exists(CrossPlatform.GetSingleplayerBackupsHome())) Directory.CreateDirectory(CrossPlatform.GetSingleplayerBackupsHome()); CrossPlatform.OpenFolder(CrossPlatform.GetSingleplayerBackupsHome()); } [Pure] public static bool IsServerTerminationLog(string log) { return Regex.IsMatch(log, @"^\[\d{2}:\d{2}:\d{2}\]: \[SystemTermination\] Server successfully terminated!\s*$"); } private void PrintLog(string message) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) message = Regex.Replace(message, @"\e\[[0-9;]*m", string.Empty); ShowLogPanel = true; Dispatcher.UIThread.Post(() => { SingleplayerLogsTextBox.Text += message + Environment.NewLine; }, DispatcherPriority.Background); if (IsServerTerminationLog(message)) CanLaunch = true; } public async void LaunchSingleplayer() { string javaVersionOutput = CrossPlatform.RunCommandAndGetOutput( $"\"{Launcher.JavaExecutableLocation}\" -version" ); if (!javaVersionOutput.Contains("11")) { PrintLog("You don't have Java 11 set! Saradomin will grab it's own copy.."); await _javaUpdateService.DownloadAndSetJava11(_settingsService); } CanLaunch = false; PrintLog("Starting Singleplayer.. The Singleplayer client will launch when 2009scape is ready. Sit tight!"); _oldServerAddress = _settingsService.Client.GameServerAddress; _oldManagementAddress = _settingsService.Client.ManagementServerAddress; _settingsService.Client.GameServerAddress = _settingsService.Client.ManagementServerAddress = "localhost"; new Task(() => LaunchServerAndClient(Launcher.JavaExecutableLocation, PrintLog)).Start(); } private void LaunchServerAndClient (string javaExecutableLocation, Action log) { var serverJar = CrossPlatform.GetSingleplayerHome() + @"/game/server.jar"; if (IsPortInUse(43595)) { log("Port 43595 is in use. Cannot start the server again."); return; } _serverProcess = CrossPlatform.StartJavaProcess(javaExecutableLocation, serverJar, "2G", log, null); while (!IsPortInUse(43595)) Thread.Sleep(1000); Message.Broadcast(); } private static bool IsPortInUse(int port) { var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); var tcpConnInfoArray = ipGlobalProperties.GetActiveTcpListeners(); return tcpConnInfoArray.Any(endpoint => endpoint.Port == port); } private void LaunchFaq() { CrossPlatform.LaunchURL("https://2009scape.org/site/game_guide/singleplayer.html"); } private void LaunchForums() { CrossPlatform.LaunchURL("https://forum.2009scape.org/viewforum.php?f=8-support"); } public bool Cheats { get => ParseConf("noauth_default_admin"); set => WriteConf("noauth_default_admin", value); } public bool FakePlayers { get => ParseConf("enable_bots", true); set => WriteConf("enable_bots", value); } public bool GEAutoBuySell { get => ParseConf("i_want_to_cheat"); set => WriteConf("i_want_to_cheat", value); } public bool Debug { get => ParseConf("debug"); set => WriteConf("debug", value); } public async void ResetToDefaults() { using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(5); MakeBackup(); PrintLog("Grabbing the default config from the latest singleplayer codebase.."); try { string defaultConf = await httpClient.GetStringAsync("https://gitlab.com/2009scape/singleplayer/windows/-/raw/master/game/worldprops/default.conf"); await File.WriteAllTextAsync(CrossPlatform.GetSingleplayerHome() + "/game/worldprops/default.conf", defaultConf); } catch (Exception ex) { PrintLog($"Couldn't reset game properties"); PrintLog(ex.Message); return; } Cheats = GEAutoBuySell = Debug = false; // Force update UI FakePlayers = true; PrintLog("Saved new config."); PrintLog(""); } } }