Merge branch 'singleplayerlauncher' into 'master'

Add Singleplayer Support to Launcher

See merge request 2009scape/Saradomin-Launcher!48
This commit is contained in:
Ceikry 2023-11-19 15:08:03 +00:00
commit 6562434594
28 changed files with 1214 additions and 145 deletions

View file

@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Saradomin", "Saradomin\Sara
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Glitonea", "Glitonea\Glitonea.csproj", "{D2884654-2246-4AC0-B53B-5F1DF33DEDD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{6A66E5A7-9297-410F-B061-EB386679E430}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -18,5 +20,9 @@ Global
{D2884654-2246-4AC0-B53B-5F1DF33DEDD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2884654-2246-4AC0-B53B-5F1DF33DEDD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2884654-2246-4AC0-B53B-5F1DF33DEDD7}.Release|Any CPU.Build.0 = Release|Any CPU
{6A66E5A7-9297-410F-B061-EB386679E430}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A66E5A7-9297-410F-B061-EB386679E430}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A66E5A7-9297-410F-B061-EB386679E430}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A66E5A7-9297-410F-B061-EB386679E430}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Saradomin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Singleplayer/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -1,4 +1,8 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/usr/share/dotnet/sdk/7.0.100/MSBuild.dll</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/Build/SolBuilderDuo/UseMsbuildSolutionBuilder/@EntryValue">No</s:String></wpf:ResourceDictionary>
<s:String x:Key="/Default/Environment/Hierarchy/Build/SolBuilderDuo/UseMsbuildSolutionBuilder/@EntryValue">No</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7a951a1e_002Dc4b2_002D4516_002Db0cc_002D72fd4df57b19/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from FindMostRecentBackupDirectory.cs" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Project Location="\home\xacket\Projects\Saradomin-Launcher\UnitTests" Presentation="&amp;lt;UnitTests&amp;gt;" /&gt;&#xD;
&lt;/SessionState&gt;</s:String>
</wpf:ResourceDictionary>

View file

@ -37,9 +37,9 @@ namespace Saradomin.Infrastructure.Services
Arguments =
$"-Dsun.java2d.uiScale={_settingsService.Client.UiScale} "
+ $"-DclientFps={_settingsService.Client.Fps} "
+ $"-DclientHomeOverride=\"{_settingsService.Launcher.InstallationDirectory}/\" "
+ $"-DclientHomeOverride=\"{CrossPlatform.Get2009scapeHome()}/\" "
+ $"-jar \"{_clientUpdateService.PreferredTargetFilePath}\"",
WorkingDirectory = $"{_settingsService.Launcher.InstallationDirectory}",
WorkingDirectory = $"{CrossPlatform.Get2009scapeHome()}",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
}

View file

@ -20,7 +20,7 @@ namespace Saradomin.Infrastructure.Services
public string ClientHashURL => "https://gitlab.com/2009scape/rt4-client/-/jobs/artifacts/master/raw/client/build/libs/rt4-client.jar.sha256?job=build";
public string PreferredTargetFilePath =>
CrossPlatform.Locate2009scapeExecutable(_settingsService.Launcher.InstallationDirectory);
CrossPlatform.Get2009scapeExecutable();
public event EventHandler<float> DownloadProgressChanged;

View file

@ -6,7 +6,7 @@ namespace Saradomin.Infrastructure.Services
{
public interface IJavaUpdateService : IService
{
event EventHandler<float> JavaDownloadProgressChanged;
event EventHandler<Tuple<float, bool>> JavaDownloadProgressChanged;
Task DownloadAndSetJava11(ISettingsService settingsService);
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
using Glitonea.Mvvm;
namespace Saradomin.Infrastructure.Services
{
public interface ISingleplayerUpdateService : IService
{
event EventHandler<Tuple<float, bool>> SingleplayerDownloadProgressChanged;
Task DownloadSingleplayer();
}
}

View file

@ -10,18 +10,18 @@ namespace Saradomin.Infrastructure.Services
{
public class JavaUpdateService : IJavaUpdateService
{
public event EventHandler<float> JavaDownloadProgressChanged;
public event EventHandler<Tuple<float, bool>> JavaDownloadProgressChanged;
public async Task DownloadAndSetJava11(ISettingsService settingsService)
{
string downloadUrl = CrossPlatform.GetJava11DownloadUrl();
string downloadPath = Path.Combine(
CrossPlatform.LocateDefault2009scapeHome(),
CrossPlatform.Get2009scapeHome(),
"jre11" + Path.GetExtension(downloadUrl)
);
string extractedPath = Path.Combine(
CrossPlatform.LocateDefault2009scapeHome(),
CrossPlatform.Get2009scapeHome(),
"jre11"
);
@ -47,19 +47,20 @@ namespace Saradomin.Infrastructure.Services
await fileStream.WriteAsync(buffer, 0, bytesRead);
var progress = (float)totalRead / contentLength;
JavaDownloadProgressChanged?.Invoke(this, progress);
JavaDownloadProgressChanged?.Invoke(this, new Tuple<float, bool>(progress, false));
} while (bytesRead > 0);
}
}
}
JavaDownloadProgressChanged?.Invoke(this, 1f);
JavaDownloadProgressChanged?.Invoke(this, new Tuple<float, bool>(1f, false));
if (Directory.Exists(extractedPath)) Directory.Delete(extractedPath, true);
if (Path.GetExtension(downloadUrl) == ".zip")
{
string tempDir = Path.Combine(Path.GetTempPath(), "jre11_temp");
// Don't use /tmp because Directory.Move doesn't work cross-partition
string tempDir = Path.Combine(CrossPlatform.Get2009scapeHome(), "jre11_temp");
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
await Task.Run(() => ZipFile.ExtractToDirectory(downloadPath, tempDir));
Directory.Move(Directory.GetDirectories(tempDir)[0], extractedPath);
@ -96,6 +97,7 @@ namespace Saradomin.Infrastructure.Services
);
}
settingsService.SaveAll();
JavaDownloadProgressChanged?.Invoke(this, new Tuple<float, bool>(1f, true));
}
}
}

View file

@ -4,6 +4,7 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using Saradomin.Utilities;
namespace Saradomin.Infrastructure.Services
{
@ -13,7 +14,7 @@ namespace Saradomin.Infrastructure.Services
public PluginManagementService(ISettingsService settings)
{
PluginRepositoryPath = Path.Combine(settings.Launcher.InstallationDirectory, "plugins");
PluginRepositoryPath = Path.Combine(CrossPlatform.Get2009scapeHome(), "plugins");
}
public Task<List<string>> EnumerateInstalledPlugins()

View file

@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Text.Json;
using Saradomin.Model.Settings.Client;
@ -12,10 +13,10 @@ namespace Saradomin.Infrastructure.Services
public ClientSettings Client { get; private set; } = new();
private string ClientSettingsPath
=> Path.Combine(Launcher.InstallationDirectory, ClientSettings.FileName);
=> Path.Combine(CrossPlatform.Get2009scapeHome(), ClientSettings.FileName);
private string LauncherSettingsPath
=> Path.Combine(CrossPlatform.LocateSaradominHome(), LauncherSettings.FileName);
=> Path.Combine(CrossPlatform.GetSaradominHome(), LauncherSettings.FileName);
public SettingsService()
{
@ -30,7 +31,7 @@ namespace Saradomin.Infrastructure.Services
private void TryReadConfigurationData()
{
Directory.CreateDirectory(CrossPlatform.LocateSaradominHome());
Directory.CreateDirectory(CrossPlatform.GetSaradominHome());
if (File.Exists(LauncherSettingsPath))
{
@ -44,7 +45,7 @@ namespace Saradomin.Infrastructure.Services
SaveLauncherSettings();
}
Directory.CreateDirectory(Launcher!.InstallationDirectory);
Directory.CreateDirectory(CrossPlatform.Get2009scapeHome());
if (File.Exists(ClientSettingsPath))
{
using (var stream = File.OpenRead(ClientSettingsPath))

View file

@ -0,0 +1,70 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Threading.Tasks;
using Saradomin.Utilities;
namespace Saradomin.Infrastructure.Services
{
public class SingleplayerUpdateService : ISingleplayerUpdateService
{
public event EventHandler<Tuple<float, bool>> SingleplayerDownloadProgressChanged;
public async Task DownloadSingleplayer()
{
SingleplayerDownloadProgressChanged?.Invoke(this, new Tuple<float, bool>(0f, false));
string downloadUrl =
"https://gitlab.com/2009scape/singleplayer/windows/-/archive/master/windows-master.zip";
string downloadPath = Path.Combine(
CrossPlatform.Get2009scapeHome(),
"singleplayer" + Path.GetExtension(downloadUrl)
);
using (HttpClient httpClient = new HttpClient())
{
var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
var contentLength = response.Content.Headers.ContentLength ?? 40 * 1024 * 1024L;
var totalRead = 0L;
var buffer = new byte[8192];
// Create a FileStream to write the downloaded bytes to
await using (var fileStream =
new FileStream(downloadPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await using (var stream = await response.Content.ReadAsStreamAsync())
{
int bytesRead;
do
{
bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
totalRead += bytesRead;
// Write the bytes to the FileStream
await fileStream.WriteAsync(buffer, 0, bytesRead);
var progress = (float)totalRead / contentLength;
SingleplayerDownloadProgressChanged?.Invoke(this,
new Tuple<float, bool>(progress, false));
} while (bytesRead > 0);
}
}
}
SingleplayerDownloadProgressChanged?.Invoke(this, new Tuple<float, bool>(1f, false));
if (Directory.Exists(CrossPlatform.GetSingleplayerHome())) Directory.Delete(CrossPlatform.GetSingleplayerHome(), true);
// Don't use /tmp because Directory.Move doesn't work cross-partition
string tempDir = Path.Combine(CrossPlatform.Get2009scapeHome(), "singleplayer_temp");
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
await Task.Run(() => ZipFile.ExtractToDirectory(downloadPath, tempDir));
Directory.Move(Directory.GetDirectories(tempDir)[0], CrossPlatform.GetSingleplayerHome());
Directory.Delete(tempDir, true);
File.Delete(downloadPath);
SingleplayerDownloadProgressChanged?.Invoke(this, new Tuple<float, bool>(1f, true));
}
}
}

View file

@ -9,12 +9,9 @@ namespace Saradomin.Model.Settings.Launcher
public bool PlaceCloseButtonOnLeft { get; set; } = false;
public bool ExitAfterLaunchingClient { get; set; } = true;
public bool AllowMultiboxing { get; set; } = false;
public bool CheckForClientUpdatesOnLaunch { get; set; } = true;
public bool CheckForServerProfilesOnLaunch { get; set; } = true;
public string JavaExecutableLocation { get; set; }
public string InstallationDirectory { get; set; } = CrossPlatform.LocateDefault2009scapeHome();
protected override void OnSettingsModified(string propertyName)
{

View file

@ -23,4 +23,9 @@
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource DarkForegroundBrush}" />
</Style>
<Style Selector="Button.OutsideNavigator:disabled">
<Setter Property="Background" Value="{StaticResource DarkMediumBackgroundBrush}" />
<Setter Property="Foreground" Value="{StaticResource SemiDarkForegroundBrush}" />
</Style>
</Styles>

View file

@ -3,18 +3,10 @@
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Version>1.6.0</Version>
</PropertyGroup>
<PropertyGroup>
<TargetPlatform>x64</TargetPlatform>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>true</PublishReadyToRun>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<NoWarn>IL2026;CS0067</NoWarn>
<ApplicationIcon>Resources\Icons\saradomin.ico</ApplicationIcon>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<StartupObject>Saradomin.Program</StartupObject>
</PropertyGroup>
<PropertyGroup>
@ -66,6 +58,10 @@
<DependentUpon>NotificationBox.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\Controls\SingleplayerView.axaml.cs">
<DependentUpon>MainWindow.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\Controls\SettingsView.axaml.cs">
<DependentUpon>MainWindow.axaml</DependentUpon>
<SubType>Code</SubType>
@ -77,6 +73,10 @@
<Compile Update="View\Controls\PluginManagerView.axaml.cs">
<DependentUpon>PluginManagerView.axaml</DependentUpon>
</Compile>
<Compile Update="View\Controls\SingleplayerView.axaml.cs">
<DependentUpon>SingleplayerView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>

View file

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Win32;
using Mono.Unix;
@ -27,6 +28,22 @@ namespace Saradomin.Utilities
}
}
public static void OpenFolder(string path)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", path);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", path);
}
}
public static bool IsJavaExecutableValid(string location)
{
try
@ -165,13 +182,12 @@ namespace Saradomin.Utilities
);
}
public static string LocateDefault2009scapeHome()
public static string Get2009scapeHome()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|| RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
{
return Path.Combine(
// Get the XDG_DATA_HOME environment variable, or if it doesn't exist, use the default ~/.local/share
LocateUnixUserHome(),
"2009scape"
);
@ -185,7 +201,7 @@ namespace Saradomin.Utilities
}
}
public static string LocateSaradominHome()
public static string GetSaradominHome()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|| RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
@ -197,33 +213,44 @@ namespace Saradomin.Utilities
"saradomin"
);
}
else
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"2009scape",
"saradomin"
);
}
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"2009scape",
"saradomin"
);
}
public static string GetSingleplayerBackupsHome()
{
return Path.Combine(Get2009scapeHome(), "singleplayer_backups");
}
public static string GetSingleplayerHome()
{
return Path.Combine(Get2009scapeHome(), "singleplayer");
}
public static string LocateSingleplayerExecutable()
{
return Path.Combine(GetSingleplayerHome(), RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "launch.bat" : "launch.sh");
}
public static string Locate2009scapeExecutable(string baseDirectory)
public static string Get2009scapeExecutable()
{
baseDirectory ??= LocateDefault2009scapeHome();
return Path.Combine(baseDirectory, "2009scape.jar");
return Path.Combine(Get2009scapeHome(), "2009scape.jar");
}
public static string LocateServerProfilesPath(string baseDirectory)
public static string GetServerProfilePath(string baseDirectory)
{
baseDirectory ??= LocateDefault2009scapeHome();
baseDirectory ??= Get2009scapeHome();
return Path.Combine(baseDirectory, "server_profiles.json");
}
public static string RunCommandAndGetOutput(string command)
public static string RunCommandAndGetOutput(string command, Action<string> onOutputReceived = null, Action<string> onErrorReceived = null)
{
Process process = new Process();
StringBuilder output = new StringBuilder();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
process.StartInfo = new ProcessStartInfo("cmd.exe", "/c " + command)
@ -250,12 +277,16 @@ namespace Saradomin.Utilities
process.OutputDataReceived += (_, e) =>
{
if (e.Data != null) output.AppendLine(e.Data);
if (e.Data == null) return;
output.AppendLine(e.Data);
onOutputReceived?.Invoke(e.Data);
};
process.ErrorDataReceived += (_, e) =>
{
if (e.Data != null) output.AppendLine(e.Data);
if (e.Data == null) return;
output.AppendLine(e.Data);
onErrorReceived?.Invoke(e.Data);
};
process.Start();

View file

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
namespace Saradomin.Utilities;
public static class SingleplayerManagement
{
private static readonly string[] DirsToBackup =
{
"game/data/players",
"game/data/serverstore",
"game/worldprops"
};
private static readonly string[] FilesToBackup =
{
"game/data/eco/grandexchange.db"
};
public static void MakeBackup(Action<string> log)
{
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string newBackupDir = Path.Combine(
CrossPlatform.GetSingleplayerBackupsHome(),
timestamp
);
foreach (string dir in DirsToBackup)
{
string sourcePath = Path.Combine(CrossPlatform.GetSingleplayerHome(), dir);
if (!Directory.Exists(sourcePath))
{
log($" Skipping backup of {dir} because it doesn't exist (Full path attempted: {sourcePath})");
continue;
}
string destPath = Path.Combine(newBackupDir, dir);
CopyDirectory(sourcePath, destPath);
}
foreach (string file in FilesToBackup)
{
string sourceFilePath = Path.Combine(CrossPlatform.GetSingleplayerHome(), file);
if (!File.Exists(sourceFilePath))
{
log($" Skipping backup of {file} because it doesn't exist (Full path attempted: {sourceFilePath})");
continue;
}
string destFilePath = Path.Combine(newBackupDir, file);
string directoryForFile = Path.GetDirectoryName(destFilePath);
if (!Directory.Exists(directoryForFile))
Directory.CreateDirectory(directoryForFile);
File.Copy(sourceFilePath, destFilePath, true);
}
}
private static void CopyDirectory(string sourceDir, string destinationDir)
{
if (!Directory.Exists(destinationDir))
Directory.CreateDirectory(destinationDir);
foreach (string filePath in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(filePath);
string destFilePath = Path.Combine(destinationDir, fileName);
File.Copy(filePath, destFilePath, true);
}
foreach (string subDirPath in Directory.GetDirectories(sourceDir))
{
string subDirName = Path.GetFileName(subDirPath);
string destSubDirPath = Path.Combine(destinationDir, subDirName);
// Recurse!
CopyDirectory(subDirPath, destSubDirPath);
}
}
public static string FindMostRecentBackupDirectory(IEnumerable<string> directories)
{
return directories.Select(Path.GetFileName).MaxBy(name => name);
}
public static void ApplyLatestBackup(Action<string> log)
{
var backupHome = CrossPlatform.GetSingleplayerBackupsHome();
var singleplayerHome = CrossPlatform.GetSingleplayerHome();
// Get all backup directories
string[] backupDirectories = Directory.Exists(backupHome) ? Directory.GetDirectories(backupHome) : Array.Empty<string>();
string mostRecentBackupDirName = FindMostRecentBackupDirectory(backupDirectories);
if (mostRecentBackupDirName == null)
{
log(" No backups found.");
return;
}
string mostRecentBackupFullPath = Path.Combine(backupHome, mostRecentBackupDirName);
// Get the list of files in the most recent backup
var filesInBackup = Directory.GetFiles(
mostRecentBackupFullPath,
"*",
SearchOption.AllDirectories
);
foreach (var file in filesInBackup)
{
// Calculate the file's destination path in the singleplayer home directory
var relativePath = file.Substring(mostRecentBackupFullPath.Length)
.TrimStart(Path.DirectorySeparatorChar);
var destFilePath = Path.Combine(singleplayerHome, relativePath);
// Ensure the destination directory exists
var destDirectory = Path.GetDirectoryName(destFilePath);
if (!Directory.Exists(destDirectory))
{
Directory.CreateDirectory(destDirectory);
}
// Copy the file to the destination, overwriting any existing file
File.Copy(file, destFilePath, true);
}
log($" Backup from {mostRecentBackupDirName} has been successfully applied.");
}
private static Dictionary<string, string> _confCache;
private static string ConfPath => Path.Combine(CrossPlatform.GetSingleplayerHome(), "game", "worldprops", "default.conf");
public static Dictionary<string, string> GrabConfCache()
{
return File.ReadLines(ConfPath)
.Where(line => line.Contains('=') && !line.TrimStart().StartsWith("#"))
.Select(l => l.Split(new[] { '#', '=' }, 3))
.ToDictionary(parts => parts[0].Trim(), parts => parts[1].Trim());
}
public static T ParseConf<T>(string key, T defaultValue = default)
{
if (!File.Exists(ConfPath)) return defaultValue;
_confCache ??= GrabConfCache();
if (!_confCache.TryGetValue(key, out var value)) return defaultValue;
return (T)Convert.ChangeType(value, typeof(T));
}
public static void WriteConf(string key, object value)
{
_confCache[key] = value.ToString().ToLowerInvariant(); // Update cache
var lines = File.ReadAllLines(ConfPath).Select(l =>
l.StartsWith(key + " =")
? $"{key} = {_confCache[key]}" + (l.Contains("#") ? " " + l.Substring(l.IndexOf('#')) : "")
: l);
File.WriteAllLines(ConfPath, lines);
}
}

View file

@ -0,0 +1,77 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading;
namespace Saradomin.Utilities.Singleplayer;
public static class Windows
{
private static bool IsPortInUse(int port)
{
var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
var tcpConnInfoArray = ipGlobalProperties.GetActiveTcpListeners();
return tcpConnInfoArray.Any(endpoint => endpoint.Port == port);
}
private static Process StartJavaProcess(string javaExecutable, string jarPath, string memoryAllocation, Action<string> outputHandler, Action onExit)
{
Process process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = javaExecutable,
Arguments = $"-Xmx{memoryAllocation} -Xms{memoryAllocation} -jar \"{jarPath}\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.Combine(CrossPlatform.GetSingleplayerHome(), "game")
};
process.OutputDataReceived += (_, args) =>
{
if (string.IsNullOrEmpty(args.Data)) return;
outputHandler?.Invoke(args.Data);
};
process.ErrorDataReceived += (_, args) =>
{
if (string.IsNullOrEmpty(args.Data)) return;
outputHandler?.Invoke(args.Data);
};
process.EnableRaisingEvents = true;
process.Exited += (_, _) => onExit?.Invoke();
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
return process;
}
public static void WindowsLaunchServerAndClient(string javaExecutableLocation, Action<string> log)
{
string serverJar = CrossPlatform.GetSingleplayerHome() + @"\game\server.jar";
string clientJar = CrossPlatform.GetSingleplayerHome() + @"\game\client.jar";
if (IsPortInUse(43595))
{
log("Port 43595 is in use. Cannot start the server again.");
return;
}
Process serverProcess = StartJavaProcess(javaExecutableLocation, serverJar, "2G", log, null);
while (!IsPortInUse(43595)) Thread.Sleep(1000);
StartJavaProcess(javaExecutableLocation, clientJar, "1G", null, () =>
{
serverProcess.Kill();
});
}
}

View file

@ -44,6 +44,7 @@
Width="300"
Margin="2,0,0,0"
HorizontalAlignment="Left"
IsReadOnly="True"
Classes="NormalTextBox"
Text="{Binding Launcher.JavaExecutableLocation, Mode=TwoWay}"
Watermark="Enter a path to java or java.exe..." />
@ -59,40 +60,6 @@
ToolTip.Tip="Browse..." />
</Grid>
<Grid Margin="0,2,0,4"
ColumnDefinitions="*,Auto"
RowDefinitions="Auto,Auto">
<TextBlock Grid.Row="0"
Grid.ColumnSpan="2"
Margin="3,0,0,2"
Foreground="{StaticResource DarkForegroundBrush}"
Text="2009scape installation directory" />
<!--
Don't let the user edit it directly to avoid creating a ton of directories,
since we get a notification every time the textbox is updated.
-->
<TextBox Grid.Row="1"
Grid.Column="0"
Width="300"
Margin="2,0,0,0"
HorizontalAlignment="Left"
Classes="NormalTextBox"
IsReadOnly="True"
Text="{Binding Launcher.InstallationDirectory, Mode=TwoWay}"
Watermark="Enter a path to your preferred installation directory..." />
<Button Grid.Row="1"
Grid.Column="1"
Width="22"
Height="24"
Margin="2,0,0,0"
Classes="OutsideNavigator"
Command="{Binding BrowseForInstallationDirectory}"
Content="..."
ToolTip.Tip="Browse..." />
</Grid>
<Grid Margin="0,0,0,4"
RowDefinitions="Auto, Auto">
<TextBlock Grid.Row="0"

View file

@ -0,0 +1,197 @@
<UserControl x:Class="Saradomin.View.Controls.SingleplayerView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Saradomin.View.Controls"
xmlns:converters="clr-namespace:Glitonea.Mvvm.Converters;assembly=Glitonea"
xmlns:glitonea="clr-namespace:Glitonea;assembly=Glitonea"
xmlns:mvvm="clr-namespace:Glitonea.Mvvm;assembly=Glitonea"
xmlns:vm="clr-namespace:Saradomin.ViewModel.Controls"
DataContext="{mvvm:DataContextSource vm:SingleplayerViewModel}">
<Grid Margin="0,0,2,2"
ColumnDefinitions="400,*">
<DockPanel Grid.Column="0">
<HeaderedContentControl Classes="GroupBox"
DockPanel.Dock="Top"
Header="singleplayer logs"
IsVisible="{Binding ShowLogPanel}"
>
<ScrollViewer HorizontalAlignment="Left"
ClipToBounds="True"
Height="300"
Width="390"
Content="{Binding SingleplayerLogsTextBox}"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Visible" />
</HeaderedContentControl>
<HeaderedContentControl Classes="GroupBox"
DockPanel.Dock="Top"
Header="singleplayer info"
IsVisible="{Binding !ShowLogPanel}"
>
<StackPanel Orientation="Vertical" Margin="10">
<TextBlock Width="380" TextWrapping="Wrap" Foreground="Black">
Singleplayer is provided by the 2009scape team for advanced users only.
</TextBlock>
<TextBlock Width="380" TextWrapping="Wrap" Margin="0,10,0,0" Foreground="Black">
2009scape official servers offer free membership, multiple XP rates, and are hosted 24/7. Your player save file can be exported from the multiplayer server if you wish to switch to singleplayer.
</TextBlock>
<TextBlock Width="380" TextWrapping="Wrap" Foreground="Black">
However, we will not import your singleplayer save file into the multiplayer world. If you ever want to play with others, we recommend the multiplayer server.
</TextBlock>
<TextBlock Width="380" TextWrapping="Wrap" Margin="0,10,0,0" Foreground="Black">
If you have any questions, feel free to ask on our Discord or Forums.
</TextBlock>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Classes="GroupBox"
DockPanel.Dock="Top"
Header="singleplayer links">
<StackPanel Orientation="Vertical">
<Button
Margin="0,0,0,4"
Width="140"
Height="25"
BorderBrush="{StaticResource DarkBorderBrush}"
BorderThickness="1"
Classes="OutsideNavigator"
Command="{Binding LaunchFaq}"
Opacity="1"
FontSize="11"
Content="singleplayer faq"
ZIndex="9999" />
<Button
Margin="0,0,0,4"
Width="140"
Height="25"
BorderBrush="{StaticResource DarkBorderBrush}"
BorderThickness="1"
Classes="OutsideNavigator"
Command="{Binding LaunchForums}"
Opacity="1"
FontSize="11"
Content="support forums"
ZIndex="9999" />
</StackPanel>
</HeaderedContentControl>
</DockPanel>
<DockPanel Grid.Column="1">
<HeaderedContentControl Classes="GroupBox"
DockPanel.Dock="Top"
Header="singleplayer launch">
<StackPanel Orientation="Vertical">
<Button
Margin="0,0,0,4"
Width="140"
Height="25"
BorderBrush="{StaticResource DarkBorderBrush}"
BorderThickness="1"
Classes="OutsideNavigator"
Command="{Binding DownloadSingleplayer}"
Opacity="1"
FontSize="11"
Content="{Binding SingleplayerDownloadText}"
ZIndex="9999"
IsEnabled="{Binding CanDownload}" />
<Button
Margin="0,0,0,4"
Width="140"
Height="25"
BorderBrush="{StaticResource DarkBorderBrush}"
BorderThickness="1"
Classes="OutsideNavigator"
Command="{Binding LaunchSingleplayer}"
Opacity="1"
FontSize="11"
Content="Start Singleplayer"
ZIndex="9999"
IsEnabled="{Binding CanLaunch}" />
<Button
Margin="0,0,0,4"
Width="140"
Height="25"
BorderBrush="{StaticResource DarkBorderBrush}"
BorderThickness="1"
Classes="OutsideNavigator"
Command="{Binding MakeBackup}"
Opacity="1"
FontSize="11"
Content="Make Backup"
ZIndex="9999"/>
<Button
Margin="0,0,0,4"
Width="140"
Height="25"
BorderBrush="{StaticResource DarkBorderBrush}"
BorderThickness="1"
Classes="OutsideNavigator"
Command="{Binding OpenBackupFolder}"
Opacity="1"
FontSize="11"
Content="Open Backups"
ZIndex="9999" />
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Margin="2,2,0,0"
Classes="GroupBox"
DockPanel.Dock="Top"
Header="singleplayer config">
<HeaderedContentControl.HeaderTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Center"
Text="{Binding Header}" />
</DataTemplate>
</HeaderedContentControl.HeaderTemplate>
<StackPanel>
<TextBlock HorizontalAlignment="Center"
Foreground="{StaticResource DarkForegroundBrush}"
/>
<CheckBox Content="Cheats"
IsChecked="{Binding Cheats, Mode=TwoWay}"
IsEnabled="{Binding CanLaunch}"
/>
<CheckBox Content="Fake Players"
IsChecked="{Binding FakePlayers, Mode=TwoWay}"
IsEnabled="{Binding CanLaunch}"
/>
<CheckBox Content="GE Auto Buy and Sell"
IsChecked="{Binding GEAutoBuySell, Mode=TwoWay}"
IsEnabled="{Binding CanLaunch}"
/>
<CheckBox Content="Debug"
IsChecked="{Binding Debug, Mode=TwoWay}"
IsEnabled="{Binding CanLaunch}"
/>
<Button
Margin="0,0,0,0"
Width="140"
Height="25"
BorderBrush="{StaticResource DarkBorderBrush}"
BorderThickness="1"
Classes="OutsideNavigator"
Opacity="1"
FontSize="11"
Content="Reset Config"
Command="{Binding ResetToDefaults}"
IsEnabled="{Binding CanLaunch}"
ZIndex="9999"/>
</StackPanel>
</HeaderedContentControl>
</DockPanel>
</Grid>
</UserControl>

View file

@ -0,0 +1,20 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PropertyChanged;
namespace Saradomin.View.Controls
{
[DoNotNotify]
public class SingleplayerView : UserControl
{
public SingleplayerView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View file

@ -111,7 +111,7 @@
<TabItem Width="95"
HorizontalContentAlignment="Center"
CornerRadius="0,0,8,0"
CornerRadius="0,0,0,0"
Header="settings">
<Border Margin="4,0,4,0"
Background="{StaticResource MediumBackgroundBrush}"
@ -125,6 +125,23 @@
</ScrollViewer>
</Border>
</TabItem>
<TabItem Width="95"
HorizontalContentAlignment="Center"
CornerRadius="0,0,8,0"
Header="singleplayer">
<Border Margin="4,0,4,0"
Background="{StaticResource MediumBackgroundBrush}"
BorderBrush="{StaticResource SemiLightMediumBorderBrush}"
BorderThickness="1,1,1,0"
CornerRadius="6,6,0,0">
<ScrollViewer Classes="MainViewScrollViewer"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Visible">
<controls:SingleplayerView VerticalAlignment="Stretch" />
</ScrollViewer>
</Border>
</TabItem>
</TabControl>
<Border Grid.Row="2"

View file

@ -120,32 +120,5 @@ namespace Saradomin.ViewModel.Controls
Launcher.JavaExecutableLocation = paths[0];
}
}
private async Task BrowseForInstallationDirectory()
{
var ofd = new OpenFolderDialog
{
Title = "Browse for Installation Directory...",
Directory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
};
var path = await ofd.ShowAsync(Application.Current.GetMainWindow());
if (path != null)
{
if (CrossPlatform.IsDirectoryWritable(path))
{
Launcher.InstallationDirectory = path;
}
else
{
NotificationBox.DisplayNotification(
"Access denied",
"The location you have selected is not writable. Select the one you have permissions for.",
Application.Current.GetMainWindow()
);
}
}
}
}
}

View file

@ -0,0 +1,208 @@
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 Saradomin.Infrastructure.Services;
using Saradomin.Model.Settings.Launcher;
using Saradomin.Utilities;
using Saradomin.View.Windows;
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;
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,
};
}
private void OnSingleplayerDownloadProgressChanged(object sender, Tuple<float, bool> 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!");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
new Task(() => Utilities.Singleplayer.Windows.WindowsLaunchServerAndClient(Launcher.JavaExecutableLocation, PrintLog)).Start();
}
else
{
new Task(() =>
CrossPlatform.RunCommandAndGetOutput(
$"{CrossPlatform.LocateSingleplayerExecutable()} \"{Launcher.JavaExecutableLocation}\"",
PrintLog,
PrintLog)
).Start();
}
}
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<bool>("noauth_default_admin");
set => WriteConf("noauth_default_admin", value);
}
public bool FakePlayers
{
get => ParseConf<bool>("enable_bots", true);
set => WriteConf("enable_bots", value);
}
public bool GEAutoBuySell
{
get => ParseConf<bool>("i_want_to_cheat");
set => WriteConf("i_want_to_cheat", value);
}
public bool Debug
{
get => ParseConf<bool>("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("");
}
}

View file

@ -25,9 +25,6 @@ namespace Saradomin.ViewModel.Windows
private readonly IRemoteConfigService _remoteConfigService;
private readonly ISettingsService _settingsService;
private bool JavaExecutableValid
=> CrossPlatform.IsJavaExecutableValid(_settingsService.Launcher.JavaExecutableLocation);
private LauncherSettings Launcher { get; }
public string Title { get; set; } = "2009scape launcher";
@ -50,9 +47,7 @@ namespace Saradomin.ViewModel.Windows
_remoteConfigService = remoteConfigService;
_javaUpdateService = javaUpdateService;
_javaUpdateService.JavaDownloadProgressChanged += OnJavaDownloadProgressUpdated;
_settingsService = settingsService;
Launcher = _settingsService.Launcher;
@ -62,13 +57,9 @@ namespace Saradomin.ViewModel.Windows
};
Message.Subscribe<MainViewLoadedMessage>(this, MainViewLoaded);
Message.Subscribe<SettingsModifiedMessage>(this, SettingsModified);
Message.Subscribe<NotificationBoxStateChangedMessage>(this, NotificatationBoxStateChanged);
if (!JavaExecutableValid)
{
_settingsService.Launcher.JavaExecutableLocation = CrossPlatform.LocateJavaExecutable();
}
_settingsService.Launcher.JavaExecutableLocation ??= CrossPlatform.LocateJavaExecutable();
}
public void ExitApplication()
@ -102,23 +93,6 @@ namespace Saradomin.ViewModel.Windows
}
}
public void SettingsModified(SettingsModifiedMessage msg)
{
if (msg.SettingName == nameof(LauncherSettings.JavaExecutableLocation))
{
if (!JavaExecutableValid)
{
CanLaunch = false;
LaunchText = "Unable to locate Java. Find Java executable using Settings page.";
}
else
{
CanLaunch = true;
LaunchText = "Play!";
}
}
}
public void NotificatationBoxStateChanged(NotificationBoxStateChangedMessage msg)
{
DimContent = msg.WasOpened;
@ -174,7 +148,7 @@ namespace Saradomin.ViewModel.Windows
}
if (!File.Exists(CrossPlatform.LocateServerProfilesPath(Launcher.InstallationDirectory)) ||
if (!File.Exists(CrossPlatform.GetServerProfilePath(CrossPlatform.Get2009scapeHome())) ||
_settingsService.Launcher.CheckForServerProfilesOnLaunch)
await AttemptServerProfileUpdate();
@ -205,7 +179,7 @@ namespace Saradomin.ViewModel.Windows
private async Task AttemptServerProfileUpdate()
{
var serverProfilePath = CrossPlatform.LocateServerProfilesPath(Launcher.InstallationDirectory);
var serverProfilePath = CrossPlatform.GetServerProfilePath(CrossPlatform.Get2009scapeHome());
try
{
@ -269,7 +243,7 @@ namespace Saradomin.ViewModel.Windows
{
LaunchText = $"Updating... (Downloading client: 0%)";
Directory.CreateDirectory(Launcher.InstallationDirectory);
Directory.CreateDirectory(CrossPlatform.Get2009scapeHome());
try
{
@ -291,7 +265,7 @@ namespace Saradomin.ViewModel.Windows
private bool IsJavaVersion11()
{
string javaVersionOutput = CrossPlatform.RunCommandAndGetOutput(
$"\"{_settingsService.Launcher.JavaExecutableLocation}\" -version"
$"\"{Launcher.JavaExecutableLocation}\" -version"
);
return javaVersionOutput.Contains("11");
}
@ -300,14 +274,19 @@ namespace Saradomin.ViewModel.Windows
{
LaunchText = $"Updating... (Downloading client - {e * 100:F2}%)";
}
private void OnJavaDownloadProgressUpdated(object sender, float e)
private void OnJavaDownloadProgressUpdated(object sender, Tuple<float, bool> e)
{
if (e >= 0.999f)
if (e.Item2)
{
LaunchText = "Play! (Multiplayer)";
return;
}
if (e.Item1 >= 0.999f)
{
LaunchText = "Updating... (Extracting Java 11)";
return;
}
LaunchText = $"Updating... (Downloading Java 11 - {e * 100:F2}%)";
LaunchText = $"Updating... (Downloading Java 11 - {e.Item1 * 100:F2}%)";
}
}
}

View file

@ -0,0 +1,295 @@
namespace UnitTests.Saradomin.Utilities.SingleplayerManagement;
[TestFixture]
public class BackupManagerTests
{
[Test]
public void WithValidDirectories_ReturnsMostRecent()
{
// Arrange
var directories = new List<string>
{
"/backups/20230101_120000",
"/backups/20230102_120000",
"/backups/20230103_120000"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230103_120000".Equals(mostRecent));
}
[Test]
public void WithDifferentYears_ReturnsMostRecent()
{
// Arrange
var directories = new List<string>
{
"/backups/20210101_120000",
"/backups/20220101_120000",
"/backups/20230101_120000"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230101_120000".Equals(mostRecent));
}
[Test]
public void WithDifferentMonths_ReturnsMostRecent()
{
// Arrange
var directories = new List<string>
{
"/backups/20230101_120000",
"/backups/20230201_120000",
"/backups/20230301_120000"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230301_120000".Equals(mostRecent));
}
[Test]
public void WithDifferentDays_ReturnsMostRecent()
{
// Arrange
var directories = new List<string>
{
"/backups/20230301_120000",
"/backups/20230302_120000",
"/backups/20230303_120000"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230303_120000".Equals(mostRecent));
}
[Test]
public void WithDifferentHours_ReturnsMostRecent()
{
// Arrange
var directories = new List<string>
{
"/backups/20230301_110000",
"/backups/20230301_120000",
"/backups/20230301_130000"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230301_130000".Equals(mostRecent));
}
[Test]
public void WithDifferentMinutes_ReturnsMostRecent()
{
// Arrange
var directories = new List<string>
{
"/backups/20230301_120100",
"/backups/20230301_120200",
"/backups/20230301_120300"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230301_120300".Equals(mostRecent));
}
[Test]
public void WithDifferentSeconds_ReturnsMostRecent()
{
// Arrange
var directories = new List<string>
{
"/backups/20230301_120000",
"/backups/20230301_120015",
"/backups/20230301_120030"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230301_120030".Equals(mostRecent));
}
[Test]
public void WithMixedDateTimes_ReturnsMostRecent_Case1()
{
// Arrange
var directories = new List<string>
{
"/backups/20210101_235959",
"/backups/20201231_235959",
"/backups/20220228_235959"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20220228_235959".Equals(mostRecent));
}
[Test]
public void WithMixedDateTimes_ReturnsMostRecent_Case2()
{
// Arrange
var directories = new List<string>
{
"/backups/20211001_000001",
"/backups/20211001_000002",
"/backups/20210930_235959"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20211001_000002".Equals(mostRecent));
}
[Test]
public void WithMixedDateTimes_ReturnsMostRecent_Case3()
{
// Arrange
var directories = new List<string>
{
"/backups/20211231_235959",
"/backups/20220101_000000",
"/backups/20211231_235958"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20220101_000000".Equals(mostRecent));
}
[Test]
public void WithMixedDateTimes_ReturnsMostRecent_Case4()
{
// Arrange
var directories = new List<string>
{
"/backups/20230530_123456",
"/backups/20230529_123456",
"/backups/20220530_123457"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230530_123456".Equals(mostRecent));
}
[Test]
public void WithMixedDateTimes_ReturnsMostRecent_Case5()
{
// Arrange
var directories = new List<string>
{
"/backups/20230102_010101",
"/backups/20230101_222222",
"/backups/20221231_235959"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20230102_010101".Equals(mostRecent));
}
[Test]
public void WithMixedDateTimes_ReturnsMostRecent_Case6()
{
// Arrange
var directories = new List<string>
{
"/backups/20240101_120000",
"/backups/20231231_235959",
"/backups/20231231_235958"
};
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That("20240101_120000".Equals(mostRecent));
}
[Test]
public void WithNoDirectories_ReturnsNull()
{
// Arrange
// ReSharper disable once CollectionNeverUpdated.Local
var directories = new List<string>();
// Act
var mostRecent =
global::Saradomin.Utilities.SingleplayerManagement.FindMostRecentBackupDirectory(
directories
);
// Assert
Assert.That(mostRecent, Is.EqualTo(null));
}
}

View file

@ -0,0 +1,20 @@
namespace UnitTests.Saradomin.ViewModel.Controls.SingleplayerViewModel;
[TestFixture]
public class IsServerTerminationLogTests
{
[Test]
[TestCase("[23:43:26]: [SystemTermination] Server successfully terminated!", true)]
[TestCase("[23:00:26]: [SystemTermination] Server successfully terminated!", true)]
[TestCase("[23:43:26]: lol said [SystemTermination] Server successfully terminated!", false)]
[TestCase("[23:00:26]: [SystemTermination] Server successfully terminated! weee exploit", false)]
[TestCase("Gotcha [23:00:26]: [SystemTermination] Server successfully terminated!", false)]
public void ServerTerminationLogTests(string log, bool expectedOutcome)
{
// Act
var result = global::Saradomin.ViewModel.Controls.SingleplayerViewModel.IsServerTerminationLog(log);
// Assert
Assert.That(result, Is.EqualTo(expectedOutcome), $"Failed for log: {log}");
}
}

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
<PackageReference Include="NUnit.Analyzers" Version="3.3.0"/>
<PackageReference Include="coverlet.collector" Version="3.1.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Saradomin\Saradomin.csproj" />
</ItemGroup>
</Project>

1
UnitTests/Usings.cs Normal file
View file

@ -0,0 +1 @@
global using NUnit.Framework;