improve server selector

This commit is contained in:
Mikhail
2024-08-24 17:20:09 -04:00
parent 7ddaef38df
commit d00ab6d185
12 changed files with 269 additions and 107 deletions

View File

@@ -1,8 +1,6 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using ServerSelector.ViewModels;
using ServerSelector.Views;
namespace ServerSelector;
@@ -18,17 +16,11 @@ public partial class App : Application
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainViewModel()
};
desktop.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
{
DataContext = new MainViewModel()
};
singleViewPlatform.MainView = new MainView();
}
base.OnFrameworkInitializationCompleted();

View File

@@ -12,9 +12,7 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.3" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.3" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0" />

View File

@@ -10,23 +10,123 @@ namespace ServerSelector
private static int GameAssemblySodiumIntegrityFuncHint = 0x5877FFB;
private static byte[] GameAssemblySodiumIntegrityFuncOrg = [0xE8, 0xF0, 0x9D, 0x43, 0xFB];
private static byte[] GameAssemblySodiumIntegrityFuncPatch = [0xb0, 0x01, 0x90, 0x90, 0x90];
private const string HostsStartMarker = "# begin ServerSelector entries";
private const string HostsEndMarker = "# end ServerSelector entries";
public static bool IsUsingOfficalServer()
{
var hostsFile = File.ReadAllText("C:\\Windows\\System32\\drivers\\etc\\hosts");
return !hostsFile.Contains("cloud.nikke-kr.com");
return !hostsFile.Contains("global-lobby.nikke-kr.com");
}
public static string CheckIntegrity()
public static bool IsOffline()
{
var hostsFile = File.ReadAllText("C:\\Windows\\System32\\drivers\\etc\\hosts");
return hostsFile.Contains("cloud.nikke-kr.com");
}
public static async Task<string> CheckIntegrity(string gamePath, string launcherPath)
{
if (IsUsingOfficalServer())
return "Official server";
// TODO
if (!Directory.Exists(gamePath))
{
return "Game path does not exist";
}
if (!Directory.Exists(launcherPath))
{
return "Launcher path does not exist";
}
if (!File.Exists(Path.Combine(launcherPath, "nikke_launcher.exe")))
{
return "Launcher path is invalid. Make sure that the game executable exists in the launcher folder";
}
string sodiumLib = AppDomain.CurrentDomain.BaseDirectory + "sodium.dll";
string gameSodium = gamePath + "/nikke_Data/Plugins/x86_64/sodium.dll";
string gameAssembly = gamePath + "/GameAssembly.dll";
string sodiumBackup = gameSodium + ".bak";
string hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers/etc/hosts");
var CAcert = await File.ReadAllTextAsync(AppDomain.CurrentDomain.BaseDirectory + "myCA.pem");
string launcherCertList = launcherPath + "/intl_service/cacert.pem";
string gameCertList = gamePath + "/nikke_Data/Plugins/x86_64/cacert.pem";
var certList1 = await File.ReadAllTextAsync(launcherCertList);
int goodSslIndex1 = certList1.IndexOf("Good SSL Ca");
if (goodSslIndex1 == -1)
return "Patch missing";
var certList2 = await File.ReadAllTextAsync(gameCertList);
int goodSslIndex2 = certList2.IndexOf("Good SSL Ca");
if (goodSslIndex2 == -1)
return "Patch missing";
// TODO: Check sodium lib
// TODO: Check if gameassembly was patched
return "OK";
}
public static async Task SaveCfg(bool useOffical, string gamePath, string launcherPath, string ip)
public static async Task RevertHostsFile()
{
string hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers/etc/hosts");
var txt = await File.ReadAllTextAsync(hostsFilePath);
// remove stuff
try
{
int startIdx = txt.IndexOf(HostsStartMarker);
int endIdx;
if (startIdx == -1)
{
startIdx = txt.IndexOf("cloud.nikke-kr.com");
}
string endIndexStr = HostsEndMarker;
if (!txt.Contains(endIndexStr))
{
// old code, find new line character before start index
for (int i = startIdx - 1; i >= 0; i--)
{
var c = txt[i];
if (c == '\n')
{
startIdx = i + 1;
break;
}
}
endIndexStr = "y.io";
endIdx = txt.IndexOf(endIndexStr) + endIndexStr.Length;
}
else
{
// add/subtract 2 to take into account newline
startIdx = txt.IndexOf(HostsStartMarker) - 2;
endIdx = txt.IndexOf(endIndexStr) + endIndexStr.Length;
}
txt = txt.Substring(0, startIdx) + txt.Substring(endIdx);
await File.WriteAllTextAsync(hostsFilePath, txt);
}
catch
{
}
}
public static async Task SaveCfg(bool useOffical, string gamePath, string launcherPath, string ip, bool offlineMode)
{
string sodiumLib = AppDomain.CurrentDomain.BaseDirectory + "sodium.dll";
string gameSodium = gamePath + "/nikke_Data/Plugins/x86_64/sodium.dll";
@@ -42,40 +142,12 @@ namespace ServerSelector
// TODO: allow changing ip address
if (useOffical)
{
var txt = await File.ReadAllTextAsync(hostsFilePath);
// remove stuff
try
{
int startIdx = txt.IndexOf("cloud.nikke-kr.com");
// find new line character before start index
for (int i = startIdx - 1; i >= 0; i--)
{
var c = txt[i];
if (c == '\n')
{
startIdx = i + 1;
break;
}
}
int endIdx = txt.IndexOf("y.io") + 4;
txt = txt.Substring(0, startIdx) + txt.Substring(endIdx);
await File.WriteAllTextAsync(hostsFilePath, txt);
}
catch
{
}
await RevertHostsFile();
// remove cert
X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
store.Remove(new X509Certificate2(X509Certificate2.CreateFromCertFile(AppDomain.CurrentDomain.BaseDirectory + "myCA.pfx")));
store.Remove(new X509Certificate2(X509Certificate.CreateFromCertFile(AppDomain.CurrentDomain.BaseDirectory + "myCA.pfx")));
store.Close();
// restore sodium
@@ -117,17 +189,29 @@ namespace ServerSelector
}
var certList1 = await File.ReadAllTextAsync(launcherCertList);
await File.WriteAllTextAsync(launcherCertList, certList1.Substring(0, certList1.IndexOf("Good SSL Ca")));
int goodSslIndex1 = certList1.IndexOf("Good SSL Ca");
if (goodSslIndex1 != -1)
await File.WriteAllTextAsync(launcherCertList, certList1.Substring(0, goodSslIndex1));
var certList2 = await File.ReadAllTextAsync(gameCertList);
await File.WriteAllTextAsync(gameCertList, certList2.Substring(0, certList2.IndexOf("Good SSL Ca")));
int goodSslIndex2 = certList2.IndexOf("Good SSL Ca");
if (goodSslIndex2 != -1)
await File.WriteAllTextAsync(gameCertList, certList2.Substring(0, goodSslIndex2));
}
else
{
// add to hosts file
string hosts = $@"{ip} cloud.nikke-kr.com
string hosts = $@"{HostsStartMarker}
{ip} global-lobby.nikke-kr.com
{ip} jp-lobby.nikke-kr.com
";
if (offlineMode)
{
hosts += $"{ip} cloud.nikke-kr.com" + Environment.NewLine;
}
hosts += $@"{ip} jp-lobby.nikke-kr.com
{ip} us-lobby.nikke-kr.com
{ip} kr-lobby.nikke-kr.com
{ip} sea-lobby.nikke-kr.com
@@ -141,13 +225,15 @@ namespace ServerSelector
255.255.221.21 na.fleetlogd.com
{ip} www.jupiterlauncher.com
{ip} data-aws-na.intlgame.com
255.255.221.21 sentry.io";
255.255.221.21 sentry.io
{HostsEndMarker}";
await RevertHostsFile();
if (!(await File.ReadAllTextAsync(hostsFilePath)).Contains("global-lobby.nikke-kr.com"))
{
using StreamWriter w = File.AppendText(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers/etc/hosts"));
w.WriteLine();
w.WriteLine(hosts);
w.Write(hosts);
}

View File

@@ -1,6 +0,0 @@
namespace ServerSelector.ViewModels;
public class MainViewModel : ViewModelBase
{
public string Greeting => "Welcome to Avalonia!";
}

View File

@@ -1,7 +0,0 @@
using ReactiveUI;
namespace ServerSelector.ViewModels;
public class ViewModelBase : ReactiveObject
{
}

View File

@@ -3,17 +3,10 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:ServerSelector.ViewModels"
mc:Ignorable="d" d:DesignWidth="370" d:DesignHeight="250"
mc:Ignorable="d" d:DesignWidth="370" d:DesignHeight="300"
x:Class="ServerSelector.Views.MainView"
x:DataType="vm:MainViewModel"
xmlns:ui="using:FluentAvalonia.UI.Controls"
>
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainViewModel />
</Design.DataContext>
<Grid>
<TabControl x:Name="MainUI">
<TabItem Header="PC" x:Name="TabPc">
@@ -34,15 +27,17 @@
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="5"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="5"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" Margin="5" Grid.Row="0" Grid.Column="0">Game path: </TextBlock>
<TextBox x:Name="txtGamePath" Grid.Row="0" Grid.Column="1">C:\NIKKE\NIKKE\game</TextBox>
<TextBox x:Name="txtGamePath" Grid.Row="0" Grid.Column="1" TextChanged="GamePath_TextChanged">C:\NIKKE\NIKKE\game</TextBox>
<Button x:Name="btnSelectGamePath" Grid.Row="0" Grid.Column="2" Content="..." Click="BtnSelectGamePath_Click"></Button>
<TextBlock VerticalAlignment="Center" Margin="5" Grid.Row="2" Grid.Column="0">Launcher path: </TextBlock>
<TextBox x:Name="txtLauncherPath" Grid.Row="2" Grid.Column="1">C:\NIKKE\Launcher</TextBox>
<TextBox x:Name="txtLauncherPath" Grid.Row="2" Grid.Column="1" TextChanged="LauncherPath_TextChanged">C:\NIKKE\Launcher</TextBox>
<Button x:Name="btnSelectLauncherPath" Grid.Row="2" Grid.Column="2" Content="..." Click="BtnSelectLauncherPath_Click"></Button>
@@ -55,11 +50,13 @@
</ComboBox.Items>
</ComboBox>
<TextBlock VerticalAlignment="Center" Margin="5" Grid.Row="6" Grid.Column="0">IP: </TextBlock>
<TextBlock x:Name="LblIp" VerticalAlignment="Center" Margin="5" Grid.Row="6" Grid.Column="0">IP: </TextBlock>
<TextBox x:Name="TxtIpAddress" Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="2">127.0.0.1</TextBox>
<TextBlock Grid.Row="8" Grid.Column="0" VerticalAlignment="Center" Margin="5" x:Name="LblStatus">Status: OK</TextBlock>
<Button HorizontalAlignment="Right" Margin="5" Click="Save_Click" Grid.Row="8" Grid.Column="1" Grid.ColumnSpan="2" Classes="accent">Save</Button>
<CheckBox x:Name="ChkOffline" VerticalAlignment="Center" Margin="5" Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="3" ToolTip.Tip="Enables the ability to run the game offline by making the game download the assets from the server, and not the official server. This only works if enableOffline is enabled on the server. Please note that this should not be enabled on public servers due to copyright issues.">Enable offline mode</CheckBox>
<TextBlock Grid.Row="10" Grid.Column="0" VerticalAlignment="Center" Margin="5" x:Name="LblStatus" ToolTip.Tip="Shows the status of the patches to the game. All patches are reverted when server is set to official.">Status: OK</TextBlock>
<Button HorizontalAlignment="Right" Margin="5" Click="Save_Click" Grid.Row="10" Grid.Column="1" Grid.ColumnSpan="2" Classes="accent">Save</Button>
</Grid>
</TabItem>
<TabItem Header="Android">

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using FluentAvalonia.UI.Controls;
using System;
@@ -17,6 +18,7 @@ public partial class MainView : UserControl
CmbServerSelection.SelectedIndex = ServerSwitcher.IsUsingOfficalServer() ? 0 : 1;
TxtIpAddress.IsEnabled = !ServerSwitcher.IsUsingOfficalServer();
ChkOffline.IsChecked = ServerSwitcher.IsOffline();
if (OperatingSystem.IsWindows() && !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
{
@@ -28,40 +30,108 @@ public partial class MainView : UserControl
};
}
LblStatus.Text = "Status: " + ServerSwitcher.CheckIntegrity();
UpdateIntegrityLabel();
}
private void Save_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
private void SetGamePathValid(bool isValid)
{
if (string.IsNullOrEmpty(txtGamePath.Text) || string.IsNullOrEmpty(txtLauncherPath.Text))
if (isValid)
{
ShowWarningMsg("Game path / launcher path is empty", "Error");
return;
txtGamePath.BorderBrush = null;
}
else
{
txtGamePath.BorderBrush = new SolidColorBrush(Color.FromRgb(255,0,0));
}
}
private void SetLauncherPathValid(bool isValid)
{
if (isValid)
{
txtLauncherPath.BorderBrush = null;
}
else
{
txtLauncherPath.BorderBrush = new SolidColorBrush(Color.FromRgb(255, 0, 0));
}
}
private bool ValidatePaths(bool showMessage)
{
if (string.IsNullOrEmpty(txtGamePath.Text))
{
SetGamePathValid(false);
if (showMessage)
ShowWarningMsg("Game path is blank", "Error");
return false;
}
if (string.IsNullOrEmpty(txtLauncherPath.Text))
{
SetLauncherPathValid(false);
if (showMessage)
ShowWarningMsg("Launcher path is blank", "Error");
return false;
}
if (!Directory.Exists(txtGamePath.Text))
{
ShowWarningMsg("Game path folder does not exist", "Error");
return;
SetGamePathValid(false);
if (showMessage)
ShowWarningMsg("Game path does not exist", "Error");
return false;
}
if (!Directory.Exists(txtLauncherPath.Text))
{
ShowWarningMsg("Launcher folder does not exist", "Error");
return;
SetLauncherPathValid(false);
if (showMessage)
ShowWarningMsg("Launcher path does not exist", "Error");
return false;
}
if (!File.Exists(Path.Combine(txtLauncherPath.Text, "nikke_launcher.exe")))
{
ShowWarningMsg("Launcher path is invalid. Make sure that the game executable exists in the launcher folder", "Error");
return;
SetGamePathValid(false);
if (showMessage)
ShowWarningMsg("Launcher path is invalid. Make sure that the game executable exists in the launcher folder", "Error");
return false;
}
if (!File.Exists(Path.Combine(txtGamePath.Text, "nikke.exe")))
{
ShowWarningMsg("Game path is invalid. Make sure that nikke.exe exists in the launcher folder", "Error");
SetGamePathValid(true);
SetLauncherPathValid(true);
return true;
}
private async void UpdateIntegrityLabel()
{
if (!ValidatePaths(false) || txtGamePath.Text == null || txtLauncherPath.Text == null)
return;
SetLoadingScreenVisible(true);
LblStatus.Text = "Status: " + await ServerSwitcher.CheckIntegrity(txtGamePath.Text, txtLauncherPath.Text);
SetLoadingScreenVisible(false);
}
private void SetLoadingScreenVisible(bool visible)
{
if (visible)
{
MainUI.IsVisible = false;
LoadingUI.IsVisible = true;
}
else
{
LoadingUI.IsVisible = false;
MainUI.IsVisible = true;
}
}
private async void Save_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (!ValidatePaths(true) || txtGamePath.Text == null || txtLauncherPath.Text == null)
return;
if (CmbServerSelection.SelectedIndex == 1)
{
@@ -73,26 +143,27 @@ public partial class MainView : UserControl
}
if (TxtIpAddress.Text == null) TxtIpAddress.Text = "";
MainUI.IsVisible = false;
LoadingUI.IsVisible = true;
SetLoadingScreenVisible(true);
try
{
ServerSwitcher.SaveCfg(CmbServerSelection.SelectedIndex == 0, txtGamePath.Text, txtLauncherPath.Text, TxtIpAddress.Text);
await ServerSwitcher.SaveCfg(CmbServerSelection.SelectedIndex == 0, txtGamePath.Text, txtLauncherPath.Text, TxtIpAddress.Text, ChkOffline.IsChecked ?? false);
}
catch (Exception ex)
{
ShowWarningMsg("Failed to save configuration: " + ex.ToString(), "Error");
}
LoadingUI.IsVisible = false;
MainUI.IsVisible = true;
UpdateIntegrityLabel();
SetLoadingScreenVisible(false);
}
private void CmbServerSelection_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (CmbServerSelection != null)
{
TxtIpAddress.IsEnabled = CmbServerSelection.SelectedIndex == 1;
ChkOffline.IsEnabled = CmbServerSelection.SelectedIndex == 1;
LblIp.IsEnabled = CmbServerSelection.SelectedIndex == 1;
}
}
public static void ShowWarningMsg(string text, string title)
@@ -127,6 +198,7 @@ public partial class MainView : UserControl
return;
}
}
UpdateIntegrityLabel();
}
}
}
@@ -156,8 +228,16 @@ public partial class MainView : UserControl
return;
}
}
UpdateIntegrityLabel();
}
}
}
private void GamePath_TextChanged(object? sender, Avalonia.Controls.TextChangedEventArgs e)
{
UpdateIntegrityLabel();
}
private void LauncherPath_TextChanged(object? sender, Avalonia.Controls.TextChangedEventArgs e)
{
UpdateIntegrityLabel();
}
}

View File

@@ -4,8 +4,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:ServerSelector.Views"
mc:Ignorable="d" d:DesignWidth="370" d:DesignHeight="250"
mc:Ignorable="d" d:DesignWidth="370" d:DesignHeight="300"
x:Class="ServerSelector.Views.MainWindow"
Title="Server Switcher" Width="370" Height="260" CanResize="False">
Title="Server Switcher" Width="370" Height="300" CanResize="False">
<views:MainView />
</Window>

21
ServerSelector/myCA.cer Normal file
View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUcJP1SCl3PCMhEjSXcn1Ps1Wx+2kwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA3MDQxNTAwNDRaFw0yNjEw
MDcxNTAwNDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCl2jEFtcvfIyvmqz0+RgsaijaU6YsWT4N2Xm+UE5SK
k+C5EuGK/hhaFkIOjWHsxtxvVJQuXJcRZSjGhWLllOnvYtaIvKhvqQ/d1w6AhtoR
KdZw76T4v7p+gcV+3OpJOcYpZMHY9N4i4+gg3ZSXjOMqCbzgmpF5Fyv75nFq+HR/
ZERt29OH6jLul1oAAcHGbfm5eZY9RS5HS3/uADZjhjnt7MURUes6P/xrWb75wcdD
yLaEwN+DzOZ+VvgbpJmS8hSWkm4e6h/vRsIjKTqnvw7dJIhrkeF7juIM8Z7QY96y
CYWME+hHWWEz0M6yg1GDWt7EuVHzzINpSgbZZhvizDgPAgMBAAGjUzBRMB0GA1Ud
DgQWBBTVvYY5ktXDZNPcsQjIoLMOhb88LTAfBgNVHSMEGDAWgBTVvYY5ktXDZNPc
sQjIoLMOhb88LTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBx
cXZMZwV8Q4Lfxm3z9cOlG8zp0b5JujeVqv6OV2p+CQ8Py8/R7/DD+IjpZ2QpKJ74
UUSuqyyrL8zM21yiksgjYYZVxK4USjjL8kIB28Ml0+UmYtfkKmiXJNWs1gGPt8Q3
aKLMNAj2aHx2vt1+Eu3iex8Pn9RZFsf/tUmxkara1jfVjEeNG/qjt1iM8yXbeyc0
bx1y8LJKxWo8alux7ncNnDBF3td1ZchhoSg36IX+i6RTM8j/BNCTx3IiiY3hbfaH
hH3FPD4U/Do9+GjidDFVku8OK8MIggYdD02AqR56tG+Sw2RNP35Ky23HcqUyRK5/
NkmdLtIH+gwww/0oBb4I
-----END CERTIFICATE-----