├── MacAppBuilding
├── .gitignore
├── dmg_background.png
├── AppTemplate
│ ├── AppIcon.icns
│ ├── launch_wrapper
│ ├── WinterspringLauncherTerminal
│ └── Resources
│ │ └── Info.plist
├── build_dmg.sh
└── build_app.sh
├── GitVersion.yml
├── WinterspringLauncher
├── Assets
│ ├── translations
│ │ └── en.json
│ ├── icons
│ │ ├── language-icons
│ │ │ ├── source.txt
│ │ │ ├── chinese.png
│ │ │ └── english.png
│ │ ├── reset.png
│ │ ├── folder.png
│ │ ├── github.png
│ │ ├── settings.png
│ │ ├── winterspring-launcher-icon.ico
│ │ └── winterspring-launcher-icon.png
│ ├── fonts
│ │ └── RobotoMono-Regular.ttf
│ └── Resources.axaml
├── 7z.dll
├── App.axaml
├── LocaleDefaults.cs
├── App.axaml.cs
├── Utils
│ ├── UnixApi.cs
│ ├── UtilHelper.cs
│ ├── DirectoryCopy.cs
│ ├── HashHelper.cs
│ ├── SimpleFileDownloader.cs
│ ├── GitHubApi.cs
│ ├── BinaryPatchHandler.cs
│ ├── ProgressiveFileDownloader.cs
│ └── ArchiveCompression.cs
├── app.manifest
├── Views
│ ├── NewVersionAvailableDialog.axaml.cs
│ ├── MainWindow.axaml.cs
│ ├── NewVersionAvailableDialog.axaml
│ └── MainWindow.axaml
├── UiElements
│ └── HyperlinkSpan.cs
├── LauncherLogic.OpenGameFolder.cs
├── ProgramStartup.cs
├── LauncherVersion.cs
├── LauncherUpdateHandler.cs
├── WinterspringLauncher.csproj
├── ViewModels
│ └── MainWindowViewModel.cs
├── LauncherLogic.cs
├── LauncherActions.cs
├── LauncherConfig.cs
└── LauncherLogic.StartGame.cs
├── winterspring-launcher-icon.png
├── global.json
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── Build_Launcher.yml
├── WinterspringLauncher.sln.DotSettings
├── WinterspringLauncher.sln
├── LICENSE
├── README.md
└── .gitignore
/MacAppBuilding/.gitignore:
--------------------------------------------------------------------------------
1 | output/
2 | output_dmg/
3 |
4 |
--------------------------------------------------------------------------------
/GitVersion.yml:
--------------------------------------------------------------------------------
1 | branches:
2 | main:
3 | regex: ^stable$
4 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "start_game": "Start Game"
3 | }
4 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/language-icons/source.txt:
--------------------------------------------------------------------------------
1 | https://www.flaticon.com/packs/countrys-flags
2 |
--------------------------------------------------------------------------------
/WinterspringLauncher/7z.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/7z.dll
--------------------------------------------------------------------------------
/winterspring-launcher-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/winterspring-launcher-icon.png
--------------------------------------------------------------------------------
/MacAppBuilding/dmg_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/MacAppBuilding/dmg_background.png
--------------------------------------------------------------------------------
/MacAppBuilding/AppTemplate/AppIcon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/MacAppBuilding/AppTemplate/AppIcon.icns
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/reset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/reset.png
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "7.0.0",
4 | "rollForward": "latestMinor",
5 | "allowPrerelease": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/MacAppBuilding/AppTemplate/launch_wrapper:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | CURRENTPATH=`dirname "${0}"`
4 |
5 | open "$CURRENTPATH/WinterspringLauncherTerminal"
6 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/folder.png
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/github.png
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/settings.png
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/fonts/RobotoMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/fonts/RobotoMono-Regular.ttf
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/language-icons/chinese.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/language-icons/chinese.png
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/language-icons/english.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/language-icons/english.png
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/winterspring-launcher-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/winterspring-launcher-icon.ico
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/icons/winterspring-launcher-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0blu/WinterspringLauncher/HEAD/WinterspringLauncher/Assets/icons/winterspring-launcher-icon.png
--------------------------------------------------------------------------------
/WinterspringLauncher/Assets/Resources.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/MacAppBuilding/AppTemplate/WinterspringLauncherTerminal:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | CURRENTPATH=`dirname "${0}"`
4 |
5 | # Resize terminal
6 | printf '\e[8;27;110t'
7 |
8 | clear
9 |
10 | cd "$CURRENTPATH"
11 |
12 | DYLD_LIBRARY_PATH="$CURRENTPATH/Libs" ./WinterspringLauncher
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Launcher bug report
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Wait a moment,
11 | are you sure you want to create an issue with **this** launcher?
12 | If you have gameplay issues start a new BugReport at HermesProxy: https://github.com/WowLegacyCore/HermesProxy
13 |
--------------------------------------------------------------------------------
/MacAppBuilding/AppTemplate/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleExecutable
6 | launch_wrapper
7 | CFBundleGetInfoString
8 | WinterspringLauncher {{VERSION}}
9 | CFBundleVersion
10 | {{VERSION}}
11 | CFBundleShortVersionString
12 | {{VERSION}}
13 | CFBundleIconFile
14 | AppIcon.icns
15 |
16 |
17 |
--------------------------------------------------------------------------------
/MacAppBuilding/build_dmg.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | if [ ! -d "output" ]; then
6 | echo "Error: no 'output' directory"
7 | exit 1
8 | fi
9 |
10 | sips --setProperty dpiWidth 144 --setProperty dpiHeight 144 dmg_backgroung.png
11 |
12 | rm -rf output_dmg
13 | mkdir output_dmg
14 | create-dmg \
15 | --volname "Winterspring Launcher Installer" \
16 | --background dmg_background.png \
17 | --window-size 525 310 \
18 | --icon-size 90 \
19 | --icon "Winterspring Launcher.app" 0 120 \
20 | --hide-extension "Winterspring Launcher.app" \
21 | --app-drop-link 280 120 \
22 | output_dmg/WinterspringLauncher.dmg output
23 |
--------------------------------------------------------------------------------
/WinterspringLauncher.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
4 | True
5 | True
--------------------------------------------------------------------------------
/WinterspringLauncher/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 | avares://WinterspringLauncher/Assets/fonts/RobotoMono-Regular.ttf#
11 |
12 | #111111
13 |
14 |
15 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LocaleDefaults.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 |
4 | namespace WinterspringLauncher;
5 |
6 | public static class LocaleDefaults
7 | {
8 | public static bool ShouldUseAsiaPreferences { get; set; } = CultureInfo.CurrentCulture.Name.StartsWith("zh", StringComparison.InvariantCultureIgnoreCase);
9 |
10 | public static string GetBestWoWConfigLocale()
11 | {
12 | return ShouldUseAsiaPreferences ? "zhCN" : "enUS";
13 | }
14 |
15 | public static string? GetBestGitHubMirror()
16 | {
17 | return ShouldUseAsiaPreferences ? "https://asia.cdn.everlook.aclon.cn/github-mirror/api/" : null;
18 | }
19 |
20 | public static string GetBestServerName()
21 | {
22 | return ShouldUseAsiaPreferences ? "Everlook (Asia)" : "Everlook (Europe)";
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/WinterspringLauncher/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 | using WinterspringLauncher.ViewModels;
5 | using WinterspringLauncher.Views;
6 |
7 | namespace WinterspringLauncher;
8 |
9 | public partial class App : Application
10 | {
11 | public override void Initialize()
12 | {
13 | AvaloniaXamlLoader.Load(this);
14 | }
15 |
16 | public override void OnFrameworkInitializationCompleted()
17 | {
18 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
19 | {
20 | desktop.MainWindow = new MainWindow
21 | {
22 | DataContext = new MainWindowViewModel() // <-- Will also initialize LauncherLogic
23 | };
24 | }
25 |
26 | base.OnFrameworkInitializationCompleted();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/UnixApi.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace WinterspringLauncher.Utils;
4 |
5 | public static class UnixApi
6 | {
7 | [DllImport("libc", SetLastError = true)]
8 | public static extern int chmod(string pathname, int mode);
9 |
10 | // user permissions
11 | public const int PERM_USR_R = 0x100;
12 | public const int PERM_USR_W = 0x80;
13 | public const int PERM_USR_X = 0x40;
14 |
15 | // group permission
16 | public const int PERM_GRP_R = 0x20;
17 | public const int PERM_GRP_W = 0x10;
18 | public const int PERM_GRP_X = 0x8;
19 |
20 | // other permissions
21 | public const int PERM_OTH_R = 0x4;
22 | public const int PERM_OTH_W = 0x2;
23 | public const int PERM_OTH_X = 0x1;
24 |
25 | public const int PERM_0777 =
26 | PERM_USR_R | PERM_USR_X | PERM_USR_W |
27 | PERM_GRP_R | PERM_GRP_X | PERM_GRP_W |
28 | PERM_OTH_R | PERM_OTH_X | PERM_OTH_W;
29 | }
30 |
--------------------------------------------------------------------------------
/WinterspringLauncher/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/WinterspringLauncher.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinterspringLauncher", "WinterspringLauncher\WinterspringLauncher.csproj", "{605A76A1-9D23-4C31-8699-FC89E4B5394A}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(SolutionProperties) = preSolution
14 | HideSolutionNode = FALSE
15 | EndGlobalSection
16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
17 | {605A76A1-9D23-4C31-8699-FC89E4B5394A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18 | {605A76A1-9D23-4C31-8699-FC89E4B5394A}.Debug|Any CPU.Build.0 = Debug|Any CPU
19 | {605A76A1-9D23-4C31-8699-FC89E4B5394A}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {605A76A1-9D23-4C31-8699-FC89E4B5394A}.Release|Any CPU.Build.0 = Release|Any CPU
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/UtilHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace WinterspringLauncher.Utils;
4 |
5 | public static class UtilHelper
6 | {
7 | // Converts some arbitrary byte number to binary human unit (1024 -> "1.0 KiB")
8 | public static string ToHumanFileSize(long sizeInByte)
9 | {
10 | string[] units = { "Byte", "KiB", "MiB", "GiB", "TiB" };
11 | int unitIdx = 0;
12 |
13 | double size = sizeInByte;
14 | while (size >= 1024 && unitIdx < units.Length - 1) {
15 | unitIdx++;
16 | size /= 1024;
17 | }
18 |
19 | string unit = units[unitIdx];
20 | return $"{size:0.0} {unit}";
21 | }
22 |
23 | public static string ReplaceFirstOccurrence(this string source, string needle, string replacement, StringComparison comparison = StringComparison.InvariantCulture)
24 | {
25 | int pos = source.IndexOf(needle, comparison);
26 | if (pos == -1)
27 | return source;
28 |
29 | string result = source.Remove(pos, needle.Length).Insert(pos, replacement);
30 | return result;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 _BLU (https://github.com/0blu)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Views/NewVersionAvailableDialog.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Interactivity;
4 | using Avalonia.Markup.Xaml;
5 | using WinterspringLauncher.UiElements;
6 |
7 | namespace WinterspringLauncher.Views;
8 |
9 | public partial class NewVersionAvailableDialog : Window
10 | {
11 | public string NewVersion { get; set; }
12 |
13 | public NewVersionAvailableDialog(LauncherVersion.UpdateInformation updateInformation)
14 | {
15 | InitializeComponent();
16 | #if DEBUG
17 | this.AttachDevTools();
18 | #endif
19 |
20 | TextBlock version = this.Find("VersionIndicator")!;
21 | HyperlinkTextBlock dlLinkIndicator = this.Find("DlLinkIndicator")!;
22 |
23 | version.Text = updateInformation.VersionName;
24 | dlLinkIndicator.NavigateUri = updateInformation.URLLinkToReleasePage;
25 | dlLinkIndicator.Text = updateInformation.URLLinkToReleasePage;
26 | }
27 |
28 | private void InitializeComponent()
29 | {
30 | AvaloniaXamlLoader.Load(this);
31 | }
32 |
33 | private void CloseButtonClick(object? sender, RoutedEventArgs e)
34 | {
35 | Close();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Avalonia;
4 | using Avalonia.Controls;
5 | using WinterspringLauncher.ViewModels;
6 |
7 | namespace WinterspringLauncher.Views;
8 |
9 | public partial class MainWindow : Window
10 | {
11 | public new MainWindowViewModel DataContext
12 | {
13 | get => base.DataContext as MainWindowViewModel;
14 | set => base.DataContext = value;
15 | }
16 |
17 | public MainWindow()
18 | {
19 | InitializeComponent();
20 | LogScroller.PropertyChanged += LogChanged;
21 | }
22 |
23 | private void LogChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
24 | {
25 | LogScroller.ScrollToEnd();
26 | }
27 |
28 | private void ServerSelectionChanged(object? sender, SelectionChangedEventArgs e)
29 | {
30 | if (sender is ComboBox comboBox)
31 | {
32 | DataContext._selectedServerIdx = comboBox.SelectedIndex;
33 | DataContext.Logic.ChangeServerIdx();
34 | }
35 | }
36 |
37 | protected override void OnClosing(WindowClosingEventArgs e)
38 | {
39 | DataContext.Logic.KillHermesProxy();
40 | base.OnClosing(e);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/DirectoryCopy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace WinterspringLauncher.Utils;
5 |
6 | public static class DirectoryCopy
7 | {
8 | public static void Copy(string sourceDirectory, string targetDirectory)
9 | {
10 | DirectoryInfo diSource = new DirectoryInfo(sourceDirectory);
11 | DirectoryInfo diTarget = new DirectoryInfo(targetDirectory);
12 |
13 | CopyAll(diSource, diTarget);
14 | }
15 |
16 | public static void CopyAll(DirectoryInfo source, DirectoryInfo target)
17 | {
18 | Directory.CreateDirectory(target.FullName);
19 |
20 | // Copy each file into the new directory.
21 | foreach (FileInfo fi in source.GetFiles())
22 | {
23 | Console.WriteLine(@"Copying {0}\{1}", target.FullName, fi.Name);
24 | fi.CopyTo(Path.Combine(target.FullName, fi.Name), true);
25 | }
26 |
27 | // Copy each subdirectory using recursion.
28 | foreach (DirectoryInfo diSourceSubDir in source.GetDirectories())
29 | {
30 | DirectoryInfo nextTargetSubDir =
31 | target.CreateSubdirectory(diSourceSubDir.Name);
32 | CopyAll(diSourceSubDir, nextTargetSubDir);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/HashHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Security.Cryptography;
4 | using System.Text;
5 |
6 | namespace WinterspringLauncher.Utils;
7 |
8 | public class HashHelper
9 | {
10 | public static string CreateHexSha256HashFromFilename(string filePath)
11 | {
12 | using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
13 | using (SHA256 sha256 = SHA256.Create())
14 | {
15 | byte[] hashBytes = sha256.ComputeHash(stream);
16 | return ConvertBinarySha256ToHex(hashBytes);
17 | }
18 | }
19 |
20 | public static string CreateHexSha256HashFromFileBytes(byte[] fileContent)
21 | {
22 | using (SHA256 sha256 = SHA256.Create())
23 | {
24 | byte[] hashBytes = sha256.ComputeHash(fileContent);
25 | return ConvertBinarySha256ToHex(hashBytes);
26 | }
27 | }
28 |
29 | public static string ConvertBinarySha256ToHex(byte[] binarySha256Hash)
30 | {
31 | if (binarySha256Hash.Length != 32)
32 | throw new ArgumentException("Expected a 32byte long Sha256 hash");
33 |
34 | StringBuilder hashBuilder = new StringBuilder(32);
35 | foreach (byte b in binarySha256Hash)
36 | hashBuilder.Append(b.ToString("X2"));
37 | return hashBuilder.ToString();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/WinterspringLauncher/UiElements/HyperlinkSpan.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Input;
4 | using Avalonia.Media;
5 |
6 | namespace WinterspringLauncher.UiElements;
7 |
8 | public class HyperlinkTextBlock : TextBlock
9 | {
10 | public static readonly DirectProperty NavigateUriProperty =
11 | AvaloniaProperty.RegisterDirect(
12 | nameof(NavigateUri),
13 | o => o.NavigateUri,
14 | (o, v) => o.NavigateUri = v);
15 |
16 | private string _navigateUri;
17 |
18 | public string NavigateUri
19 | {
20 | get => _navigateUri;
21 | set => SetAndRaise(NavigateUriProperty, ref _navigateUri, value);
22 | }
23 |
24 | public HyperlinkTextBlock()
25 | {
26 | AddHandler(PointerPressedEvent, OnPointerPressed);
27 | PseudoClasses.Add(":pointerover");
28 | Cursor = new Cursor(StandardCursorType.Hand);
29 | Foreground = Brush.Parse("#2E95D3");
30 | }
31 |
32 | private void OnPointerPressed(object sender, PointerPressedEventArgs e)
33 | {
34 | if (!string.IsNullOrEmpty(NavigateUri))
35 | {
36 | // Open the link here, for example, by launching a browser
37 | System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(NavigateUri) { UseShellExecute = true });
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/MacAppBuilding/build_app.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | if [ $# -ne 2 ]; then
6 | echo "Error: Invalid arguments"
7 | echo "Run: buid_app.sh "
8 | exit 1
9 | fi
10 |
11 | if [ ! -d "AppTemplate" ]; then
12 | echo "Error: AppTemplate folder is not in cwd"
13 | exit 1
14 | fi
15 |
16 | VERSION="$1"
17 | EXE_FILE="$2"
18 |
19 | if [ ! -f "$EXE_FILE" ]; then
20 | echo "Error: '$EXE_FILE' is not a valid file"
21 | exit 1
22 | fi
23 |
24 | OPENSSL_DL="https://github.com/0blu/prebuilt-openssl3-for-macos/releases/download/openssl-3.0.7/openssl-3.0.7.zip"
25 | APP_PATH="output/Winterspring Launcher.app"
26 |
27 | echo "Building for:"
28 | echo "- Version: $VERSION"
29 | echo "- Binary: $EXE_FILE"
30 | echo "- Result: $APP_PATH"
31 |
32 | echo "Deleting existing app"
33 | rm -rf "$APP_PATH"
34 | mkdir -p "$APP_PATH"
35 |
36 | echo "Copy template"
37 | cp -r AppTemplate/* "$APP_PATH/."
38 |
39 | echo "Download openssl3"
40 | mkdir "$APP_PATH/Libs"
41 | curl --fail -SL "$OPENSSL_DL" -o "$APP_PATH/Libs/openssl3.zip"
42 | (cd "$APP_PATH/Libs/" \
43 | && unzip ./openssl3.zip \
44 | && mv openssl-3.*/*.3.dylib . \
45 | && rm openssl3.zip \
46 | && rm -rf openssl-3.* \
47 | )
48 |
49 | echo "Copying launcher executable"
50 | cp "$EXE_FILE" "$APP_PATH"
51 |
52 | echo "Replace version in info.plist"
53 | sed -i.bak "s/{{VERSION}}/$VERSION/g" "$APP_PATH/Resources/info.plist"
54 |
55 | echo "Making everthing executable"
56 | chmod -R a+x "$APP_PATH"
57 |
58 | echo "Done building '$APP_PATH'"
59 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Views/NewVersionAvailableDialog.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/SimpleFileDownloader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data;
3 | using System.Net.Http;
4 | using System.Text.Json;
5 |
6 | namespace WinterspringLauncher.Utils;
7 |
8 | public static class SimpleFileDownloader
9 | {
10 | public static string PerformGetStringRequest(string url)
11 | {
12 | using var client = new HttpClient();
13 | client.DefaultRequestHeaders.Add("User-Agent", "curl/7.0.0"); // otherwise we get blocked
14 | var response = client.GetAsync(url).GetAwaiter().GetResult();
15 | response.EnsureSuccessStatusCode();
16 | var rawData = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
17 | return rawData;
18 | }
19 |
20 | public static TJsonResponse PerformGetJsonRequest(string url)
21 | {
22 | var rawData = PerformGetStringRequest(url);
23 | var parsedJson = JsonSerializer.Deserialize(rawData);
24 | if (parsedJson == null)
25 | {
26 | Console.WriteLine($"Debug: {rawData}");
27 | throw new NoNullAllowedException("The web response resulted in an null object");
28 | }
29 | return parsedJson;
30 | }
31 |
32 | public static byte[] PerformGetBytesRequest(string url)
33 | {
34 | using var client = new HttpClient();
35 | client.DefaultRequestHeaders.Add("User-Agent", "curl/7.0.0"); // otherwise we get blocked
36 | var response = client.GetAsync(url).GetAwaiter().GetResult();
37 | response.EnsureSuccessStatusCode();
38 | var rawData = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
39 | return rawData;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LauncherLogic.OpenGameFolder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices;
7 |
8 | namespace WinterspringLauncher;
9 |
10 | public partial class LauncherLogic
11 | {
12 | public void OpenGameFolder()
13 | {
14 | bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
15 |
16 | var serverInfo = _config.KnownServers.ElementAtOrDefault(_model.SelectedServerIdx);
17 | if (serverInfo == null)
18 | {
19 | _model.AddLogEntry("Error invalid server settings");
20 | _model.InputIsAllowed = true;
21 | return;
22 | }
23 |
24 | var gameInstallation = _config.GameInstallations.GetValueOrDefault(serverInfo.UsedInstallation);
25 | if (gameInstallation == null)
26 | {
27 | _model.AddLogEntry($"Error cant find '{serverInfo.UsedInstallation}' installation in settings");
28 | _model.InputIsAllowed = true;
29 | return;
30 | }
31 |
32 | var absPath = Path.GetFullPath(gameInstallation.Directory);
33 | if (!Directory.Exists(absPath))
34 | {
35 | _model.AddLogEntry("Game folder does not exists");
36 | _model.AddLogEntry($"Expected path: {absPath}");
37 | return;
38 | }
39 |
40 | _model.AddLogEntry("Opening game folder");
41 | try
42 | {
43 | if (weAreOnMacOs)
44 | Process.Start("open", $"-R \"{absPath}\"");
45 | else
46 | Process.Start("explorer.exe", absPath);
47 | }
48 | catch (Exception e)
49 | {
50 | _model.AddLogEntry($"An error occured while opening game folder");
51 | Console.WriteLine(e);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/WinterspringLauncher/ProgramStartup.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 | using System.Globalization;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices;
7 | using System.Threading;
8 | using Avalonia.Logging;
9 |
10 | namespace WinterspringLauncher;
11 |
12 | class ProgramStartup
13 | {
14 | // Initialization code. Don't use any Avalonia, third-party APIs or any
15 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
16 | // yet and stuff might break.
17 | [STAThread]
18 | public static void Main(string[] args)
19 | {
20 | CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
21 | Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
22 |
23 | if (LauncherUpdateHandler.HandleStartArguments(args))
24 | return;
25 |
26 | if (args.Contains("--use-asia-defaults"))
27 | LocaleDefaults.ShouldUseAsiaPreferences = true;
28 |
29 | bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
30 | if (weAreOnMacOs)
31 | {
32 | string home = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "WinterspringLauncher");
33 | Directory.CreateDirectory(home);
34 | Environment.CurrentDirectory = home;
35 | }
36 | else
37 | {
38 | Environment.CurrentDirectory = Path.GetDirectoryName(AppContext.BaseDirectory)!;
39 | }
40 |
41 | BuildAvaloniaApp()
42 | .StartWithClassicDesktopLifetime(args);
43 | }
44 |
45 | // Avalonia configuration, don't remove; also used by visual designer.
46 | public static AppBuilder BuildAvaloniaApp()
47 | => AppBuilder.Configure()
48 | .UsePlatformDetect()
49 | .WithInterFont()
50 | .LogToTrace(LogEventLevel.Verbose);
51 | }
52 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LauncherVersion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using WinterspringLauncher.Utils;
4 |
5 | namespace WinterspringLauncher;
6 |
7 | public static class LauncherVersion
8 | {
9 | public static string ShortVersionString
10 | {
11 | get
12 | {
13 | string version = GitVersionInformation.MajorMinorPatch;
14 |
15 | if (GitVersionInformation.CommitsSinceVersionSource != "0")
16 | version += $"+{GitVersionInformation.CommitsSinceVersionSource}";
17 |
18 | if (GitVersionInformation.UncommittedChanges != "0")
19 | version += " dirty";
20 |
21 | return version;
22 | }
23 | }
24 |
25 | public static string DetailedVersionString => GitVersionInformation.InformationalVersion;
26 |
27 | public static bool IsNotMainBranch => GitVersionInformation.CommitsSinceVersionSource != "0" || GitVersionInformation.UncommittedChanges != "0";
28 |
29 | public static bool CheckIfUpdateIsAvailable([NotNullWhen(true)] out UpdateInformation? updateInformation)
30 | {
31 | updateInformation = null;
32 |
33 | if (IsNotMainBranch)
34 | {
35 | Console.WriteLine("Skip update check because not main branch (or local dev version)");
36 | return false; // we are probably in a test branch
37 | }
38 |
39 | var latestLauncherVersion = GitHubApi.LatestReleaseVersion("0blu/WinterspringLauncher");
40 | if (latestLauncherVersion.TagName == null)
41 | throw new Exception("No latest version?");
42 |
43 | var myVersion = Version.Parse(GitVersionInformation.MajorMinorPatch);
44 | var newVersion = Version.Parse(latestLauncherVersion.TagName);
45 | if (newVersion > myVersion)
46 | {
47 | Console.WriteLine($"New launcher update {myVersion.ToString(fieldCount: 2)} => {newVersion.ToString(fieldCount: 2)}");
48 | updateInformation = new UpdateInformation
49 | {
50 | ReleaseDate = latestLauncherVersion.PublishedAt,
51 | VersionName = latestLauncherVersion.TagName,
52 | URLLinkToReleasePage = "https://github.com/0blu/WinterspringLauncher/releases",
53 | };
54 | return true;
55 | }
56 |
57 | return false;
58 | }
59 |
60 | public class UpdateInformation
61 | {
62 | public DateTime ReleaseDate;
63 | public string VersionName;
64 | public string URLLinkToReleasePage;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/GitHubApi.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Data;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Text.Json;
7 | using System.Text.Json.Serialization;
8 |
9 | namespace WinterspringLauncher.Utils;
10 |
11 | public static class GitHubApi
12 | {
13 | public static string GitHubApiAddress { get; set; } = "https://api.github.com/";
14 |
15 | public static GitHubReleaseInfo LatestReleaseVersion(string repoName)
16 | {
17 | var releaseUrl = new Uri(new Uri(GitHubApiAddress), $"repos/{repoName}/releases/latest").ToString();
18 | var releaseInfo = PerformWebRequest(releaseUrl);
19 | return releaseInfo;
20 | }
21 |
22 | private static TJsonResponse PerformWebRequest(string url) where TJsonResponse : new()
23 | {
24 | using var client = new HttpClient();
25 | client.DefaultRequestHeaders.Add("User-Agent", "curl/7.0.0"); // otherwise we get blocked
26 | var response = client.GetAsync(url).GetAwaiter().GetResult();
27 | if (response.StatusCode == HttpStatusCode.Forbidden)
28 | {
29 | if (response.ReasonPhrase == "rate limit exceeded")
30 | {
31 | Console.WriteLine("You are being rate-limited, did you open the launcher too many times in a short time?");
32 | return new TJsonResponse();
33 | }
34 | }
35 | response.EnsureSuccessStatusCode();
36 | var rawJson = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); // easier to debug with a string and the performance is negligible for such small jsons
37 | var parsedJson = JsonSerializer.Deserialize(rawJson);
38 | if (parsedJson == null)
39 | {
40 | Console.WriteLine($"Debug: {rawJson}");
41 | throw new NoNullAllowedException("The web response resulted in an null object");
42 | }
43 | return parsedJson;
44 | }
45 | }
46 |
47 | public class GitHubReleaseInfo
48 | {
49 | [JsonPropertyName("name")]
50 | public string? Name { get; set; }
51 |
52 | [JsonPropertyName("published_at")]
53 | public DateTime PublishedAt { get; set; }
54 |
55 | [JsonPropertyName("tag_name")]
56 | public string? TagName { get; set; }
57 |
58 | [JsonPropertyName("assets")]
59 | public List? Assets { get; set; }
60 |
61 | public class Asset
62 | {
63 | [JsonPropertyName("name")]
64 | public string Name { get; set; } = null!;
65 |
66 | [JsonPropertyName("browser_download_url")]
67 | public string DownloadUrl { get; set; } = null!;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/.github/workflows/Build_Launcher.yml:
--------------------------------------------------------------------------------
1 | name: Build Launcher
2 |
3 | on: ['push']
4 |
5 | env:
6 | DOTNET_VERSION: '7.0.x'
7 |
8 | jobs:
9 | build_windows:
10 | strategy:
11 | matrix:
12 | os: ['windows']
13 | runs-on: ${{ matrix.os }}-latest
14 |
15 | steps:
16 | - name: Checkout repository content
17 | uses: actions/checkout@v3
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Setup .NET Core SDK
22 | uses: actions/setup-dotnet@v2
23 | with:
24 | dotnet-version: ${{ env.DOTNET_VERSION }}
25 |
26 | - name: Install dependencies
27 | run: dotnet restore
28 |
29 | - name: Publish
30 | run: dotnet publish --configuration Release --use-current-runtime -p:UsePublishBuildSettings=true
31 |
32 | - name: Copy files
33 | run: cp -r ./WinterspringLauncher/bin/Release/*/publish/ publish
34 |
35 | - name: Upload build artifact
36 | uses: actions/upload-artifact@v3
37 | with:
38 | name: WinterspringLauncher-${{ matrix.os }}-${{ runner.arch }}-${{ github.sha }}
39 | path: publish
40 | if-no-files-found: error
41 |
42 | build_macos:
43 | strategy:
44 | matrix:
45 | os: ['macos']
46 | runs-on: ${{ matrix.os }}-latest
47 |
48 | steps:
49 | - name: Checkout repository content
50 | uses: actions/checkout@v3
51 | with:
52 | fetch-depth: 0
53 |
54 | - name: Setup .NET Core SDK
55 | uses: actions/setup-dotnet@v2
56 | with:
57 | dotnet-version: ${{ env.DOTNET_VERSION }}
58 |
59 | - name: Install dependencies
60 | run: dotnet restore
61 |
62 | - name: Publish
63 | run: dotnet publish --configuration Release --runtime osx-arm64 -p:UsePublishBuildSettings=true
64 |
65 | - name: Determinante tag
66 | run: echo "GIT_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
67 |
68 | - name: Create .app
69 | working-directory: MacAppBuilding
70 | run: ./build_app.sh "$GIT_TAG" ../WinterspringLauncher/bin/Release/*/publish/WinterspringLauncher
71 |
72 | - name: Create .app zip
73 | working-directory: MacAppBuilding
74 | run: |
75 | cd output
76 | zip -vr ../../WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}-APP.zip *
77 |
78 | - name: Upload .app
79 | uses: actions/upload-artifact@v3
80 | with:
81 | name: WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}.app
82 | path: WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}-APP.zip
83 | if-no-files-found: error
84 |
85 | - name: Install create-dmg
86 | run: brew install create-dmg
87 |
88 | - name: Create .dmg
89 | working-directory: MacAppBuilding
90 | run: ./build_dmg.sh
91 |
92 | - name: Upload .dmg
93 | uses: actions/upload-artifact@v3
94 | with:
95 | name: WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}.dmg
96 | path: MacAppBuilding/output_dmg/
97 | if-no-files-found: error
98 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LauncherUpdateHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Runtime.InteropServices;
5 | using System.Threading;
6 |
7 | namespace WinterspringLauncher;
8 |
9 | public static class LauncherUpdateHandler
10 | {
11 | public static bool/*exitNow*/ HandleStartArguments(string[] args)
12 | {
13 | if (args.Length != 2)
14 | return false;
15 |
16 | var actionName = args[0];
17 | var targetPath = args[1];
18 | if (string.IsNullOrWhiteSpace(targetPath))
19 | {
20 | Console.WriteLine("AutoUpdate: Target path is empty");
21 | return true;
22 | }
23 |
24 | switch (actionName)
25 | {
26 | case "--copy-self-to":
27 | {
28 | CreateTerminalWindowIfPossible();
29 | Console.WriteLine($"Updating launcher '{targetPath}'");
30 | var ourPath = Process.GetCurrentProcess().MainModule!.FileName!;
31 | bool wasSuccessful = false;
32 | const int maxTries = 20;
33 | for (int i = 0; i < maxTries; i++)
34 | {
35 | try
36 | {
37 | File.Copy(ourPath, targetPath, overwrite: true);
38 | wasSuccessful = true;
39 | }
40 | catch(IOException)
41 | {
42 | Console.WriteLine($"Need to wait for old process to close (this might take a bit) (try {i + 1}/{maxTries})");
43 | Thread.Sleep(TimeSpan.FromMilliseconds(500));
44 | }
45 | }
46 |
47 | if (!wasSuccessful)
48 | {
49 | Console.WriteLine("Update was not successful, please try again or update manually");
50 | Thread.Sleep(TimeSpan.FromSeconds(10));
51 | return true;
52 | }
53 |
54 | Console.WriteLine("Start new launcher");
55 | Process.Start(new ProcessStartInfo{
56 | FileName = targetPath,
57 | Arguments = $"--delete-tmp-updater-file \"{ourPath}\"",
58 | UseShellExecute = true,
59 | });
60 | return true;
61 | }
62 | case "--delete-tmp-updater-file":
63 | {
64 | Thread.Sleep(TimeSpan.FromMilliseconds(500));
65 | try
66 | {
67 | Console.WriteLine($"Removing tmp file '{targetPath}'");
68 | File.Delete(targetPath);
69 | }
70 | catch
71 | {
72 | // Ignore
73 | }
74 | return false; // keep our current instance
75 | }
76 | default:
77 | return false;
78 | }
79 | }
80 |
81 | #if PLATFORM_WINDOWS
82 | [DllImport("kernel32.dll")]
83 | static extern bool AttachConsole(int dwProcessId);
84 | private const int ATTACH_PARENT_PROCESS = -1;
85 | #endif
86 |
87 | private static void CreateTerminalWindowIfPossible()
88 | {
89 | #if PLATFORM_WINDOWS
90 | AttachConsole(ATTACH_PARENT_PROCESS);
91 | #endif
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/WinterspringLauncher/WinterspringLauncher.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | WinExe
5 | net7.0
6 | enable
7 | true
8 | app.manifest
9 | Assets/icons/winterspring-launcher-icon.ico
10 | false
11 | embedded
12 | _BLU
13 |
14 |
15 |
16 |
17 |
18 | win-x64
19 | true
20 | true
21 | true
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | true
50 |
51 |
52 |
53 | PLATFORM_WINDOWS
54 |
55 |
56 |
57 |
58 | All
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Winterspring Launcher
6 | Allows you to play on [Everlook.org](https://everlook.org/) with modern 1.14 WoW Client!
7 | _This is not an official project from the Everlook team._
8 | _Do not ask the Everlook team for support_
9 | _(you can still ask in [#addons-and-ui](https://discord.com/channels/973529971740008448/983067524797177996) on Discord)_
10 |
11 | ## Easy setup
12 | The launcher will do everything for you.
13 | It will download the 1.14 client, setup HermesProxy and launch the game.
14 | When HermesProxy has an update it is automatically applied.
15 |
16 | ### Windows
17 | 1. **Download [the latest .exe release](https://github.com/0blu/EverlookClassicLauncher/releases/latest)**
18 | 2. Place it in a separate directory (On first launch it will create subfolders and a desktop icon)
19 | 3. Run it
20 | 4. **Enjoy your stay on Everlook**
21 |
22 | ### MacOS
23 | 1. **Download [the latest .dmg release](https://github.com/0blu/EverlookClassicLauncher/releases/latest)**
24 | 2. Click on the downloaded .dmg file to open the installer
25 | 3. Drag the Launcher into your Applications folder
26 | 4. Go into your Applications folder, right click the Launcher and **click "Open"**
27 | 5. **Enjoy your stay on Everlook**
28 | _On MacOS the client will be stored in `/Users//WinterspringLauncher/`_
29 |
30 | ## Addons
31 | Most addons for 1.14.0 should work.
32 | You can download older versions from CurseForge under the "Files" tab. ([Example](https://www.curseforge.com/wow/addons/questie/files/all?filter-game-version=2020709689%3A9094))
33 | Here are some working recommendations:
34 | (click the [[dl](#)] link to get a working zip)
35 | ### Questing
36 | - [[dl](https://www.curseforge.com/wow/addons/questie/download/3519759)] [Questie](https://www.curseforge.com/wow/addons/questie) <- Must have. Even in combination with other addons.
37 | - [[dl](https://www.curseforge.com/wow/addons/guidelime/download/4026001)] [Guidelime (base)](https://www.curseforge.com/wow/addons/guidelime)
38 | - [[dl](https://www.curseforge.com/wow/addons/guidelime_sage/download/3810259)] [Guidelime: Sage Guide](https://www.curseforge.com/wow/addons/guidelime_sage) (Alliance)
39 | - [[dl](https://www.curseforge.com/wow/addons/guidelime-busteas-1-60-leveling/download/3521451)] [Guidelime: Busteas Guide](https://www.curseforge.com/wow/addons/guidelime-busteas-1-60-leveling) (Horde)
40 |
41 | ### Interface
42 | - [[dl](https://www.curseforge.com/wow/addons/modern-targetframe/download/4024275)] [ModernTargetFrame](https://www.curseforge.com/wow/addons/modern-targetframe) (To see the HP of mobs)
43 | - [[dl](https://github.com/tukui-org/ElvUI/archive/refs/tags/v1.48-classic.zip)] [ElvUI](https://github.com/tukui-org/ElvUI/releases/tag/v1.48-classic) (slightly older working version)
44 |
45 | (Feel free to give me new recommendations to extend the list)
46 |
47 | ## Why?
48 | The modern WoW-Classic client provides many improvements in hardware compatibility and accessibility.
49 |
50 | With Everlook we have the unique chance to improve HermesProxy with a large testing audience.
51 | The launcher will make the classic client accessible to everyone.
52 | So if you find any bugs [please report them](https://github.com/WowLegacyCore/HermesProxy/issues/new/choose).
53 | (If you find any bugs associated to the launcher report them [here](https://github.com/0blu/WinterspringLauncher/issues))
54 |
55 | ## Is this allowed?
56 | Currently HermesProxy is tolerated on Everlook.
57 | ⚠️You **will** get suspended if you exploit any game breaking bugs.
58 | So far no-one has been banned for using official HermesProxy.
59 | Just use common sense and **play fair**!
60 |
61 | # Many thanks to
62 | - [HermesProxy](https://github.com/WowLegacyCore/HermesProxy) to translate legacy traffic to modern one
63 | - [Arctium WoW-Launcher](https://github.com/Arctium/WoW-Launcher) to patch client for custom server connections
64 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/BinaryPatchHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Security.Cryptography;
5 | using System.Text.Json.Serialization;
6 |
7 | namespace WinterspringLauncher.Utils;
8 |
9 | public class BinaryPatchHandler
10 | {
11 | public class PatchSummary
12 | {
13 | [JsonPropertyName("windows")]
14 | public PatchSummaryEntry? Windows { get; set; }
15 |
16 | [JsonPropertyName("macos")]
17 | public PatchSummaryEntry? MacOs { get; set; }
18 |
19 | public class PatchSummaryEntry
20 | {
21 | [JsonPropertyName("from_sha256")]
22 | public string FromSha256 { get; set; } = null!;
23 |
24 | [JsonPropertyName("to_sha256")]
25 | public string ToSha256 { get; set; } = null!;
26 |
27 | [JsonPropertyName("last_update")]
28 | public ulong LastUpdate { get; set; } = 0;
29 |
30 | [JsonPropertyName("patch_filename")]
31 | public string PatchFilename { get; set; } = null!;
32 | }
33 | }
34 |
35 | public static void ApplyPatch(byte[] patchFileContent, string sourceFile, string targetFile)
36 | {
37 | // Read header information
38 | byte[] magic = patchFileContent.Take(4).ToArray();
39 | if (!magic.SequenceEqual(new byte[] { 0x42, 0x42, 0x50, 0x31 }))
40 | throw new ArgumentException("Invalid patch file format (expected BBP1)");
41 |
42 | // Verifying signature
43 | {
44 | byte[] everythingButSignature = patchFileContent.SkipLast(256).ToArray();
45 | byte[] signature = patchFileContent.TakeLast(256).ToArray();
46 |
47 | VerifySignatureOrThrow(everythingButSignature, signature);
48 | }
49 |
50 | byte[] fileBytes = File.ReadAllBytes(sourceFile);
51 |
52 | string expectedOriginalHash = HashHelper.ConvertBinarySha256ToHex(patchFileContent.Skip(4).Take(32).ToArray());
53 | string actualOriginalHash = HashHelper.CreateHexSha256HashFromFileBytes(fileBytes);
54 | if (actualOriginalHash != expectedOriginalHash)
55 | throw new Exception($"Cannot apply patch because the hash of source file is incorrect. Expected '{expectedOriginalHash}' Actual: '{actualOriginalHash}'");
56 |
57 | string expectedHashAfterPatch = HashHelper.ConvertBinarySha256ToHex(patchFileContent.Skip(36).Take(32).ToArray());
58 | ulong patchCount = BitConverter.ToUInt64(patchFileContent, startIndex: 68);
59 |
60 | long currentPosition = 76; // Start after the header
61 | ulong patchesApplied = 0;
62 |
63 | for (ulong patchEntryIdx = 0; patchEntryIdx < patchCount; patchEntryIdx++)
64 | {
65 | if (currentPosition + 12 > patchFileContent.Length)
66 | {
67 | throw new ArgumentException("Patch file is incomplete");
68 | }
69 |
70 | int fileOffset = (int)BitConverter.ToUInt64(patchFileContent, (int)currentPosition);
71 | uint patchSize = BitConverter.ToUInt32(patchFileContent, (int)(currentPosition + 8));
72 |
73 | currentPosition += 12;
74 |
75 | // If the file offset is out of bounds, extend the file and initialize with 0x00
76 | if (fileOffset > fileBytes.Length)
77 | Array.Resize(ref fileBytes, (int)(fileOffset + patchSize));
78 |
79 | // Apply the patch to the source file
80 | for (int patchByteIdx = 0; patchByteIdx < patchSize; patchByteIdx++)
81 | fileBytes[fileOffset + patchByteIdx] = patchFileContent[currentPosition + patchByteIdx];
82 |
83 | currentPosition += patchSize;
84 |
85 | patchesApplied++;
86 | }
87 |
88 | if (patchesApplied != patchCount)
89 | throw new InvalidOperationException("Not all patches were applied");
90 |
91 | // Verify the integrity of the patched file
92 | string actualHashAfterPatch = HashHelper.CreateHexSha256HashFromFileBytes(fileBytes);
93 | if (actualHashAfterPatch != expectedHashAfterPatch)
94 | throw new Exception($"Invalid patch result. Expected '{expectedHashAfterPatch}' Actual: '{actualHashAfterPatch}'");
95 |
96 | File.WriteAllBytes(targetFile, fileBytes);
97 | }
98 |
99 | private static void VerifySignatureOrThrow(byte[] bytesToVerify, byte[] signature)
100 | {
101 | if (signature.Length != 256)
102 | throw new ArgumentException("Signature must be 256 bytes long");
103 |
104 | // ref https://wow-patches.blu.wtf/sign_key.pub
105 | const string publicKey = @"
106 | -----BEGIN PUBLIC KEY-----
107 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmy/cX6/VOlOpgLnQWnWS
108 | tFVqf9xAO2uNjeSeUHmiMTQTwfm8hnDbcEAz5V4ou987dfDxXZb5WGVxoHnugMS/
109 | rUrOSZ8VQolH+3IanhFNrqRxRTOVk+ZlTrxV9k1iC34kXeoRryiQcqMYLlX4jT3E
110 | EupzAivNsJYm2X/jVGFgPfrDObwOjq23aLdey2uI3YA6SgIg/ayp/YyJEp775lr4
111 | Z+49t3p7WMNZw8VJkQvDB5/t64Bjd9bdIQxsO9jWyHl/z7QOrnAKv0uUPdcCCwWp
112 | kERTaAnq6tK0rAvcYMlJ230cihY+s/7QpIHpsq091La9n4nJCpFIunaaG1JyNHk5
113 | GQIDAQAB
114 | -----END PUBLIC KEY-----
115 | ";
116 | var rsa = new RSACryptoServiceProvider();
117 | rsa.ImportFromPem(publicKey);
118 |
119 | bool signatureIsValid = rsa.VerifyData(bytesToVerify, signature, hashAlgorithm: HashAlgorithmName.SHA256, padding: RSASignaturePadding.Pkcs1);
120 | if (!signatureIsValid)
121 | throw new Exception("Signature not valid");
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/ProgressiveFileDownloader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Threading.Tasks;
6 | using System.Timers;
7 |
8 | namespace WinterspringLauncher.Utils;
9 |
10 | public class ProgressiveFileDownloader : IDisposable
11 | {
12 | private readonly string _downloadUrl;
13 | private readonly string _destinationFilePath;
14 |
15 | private readonly HttpClient _httpClient;
16 |
17 | public delegate void InitialFileInfoHandler(long? totalFileSize);
18 | public delegate void ProgressFixedChangedHandler(long? totalFileSize, long alreadyReceived, long currentBytesPerSecond);
19 | public delegate void DownloadDoneHandler(long downloadedBytes);
20 |
21 | public event InitialFileInfoHandler? InitialInfo;
22 | public event ProgressFixedChangedHandler? ProgressChangedFixedDelay;
23 | public event DownloadDoneHandler? DownloadDone;
24 |
25 | private readonly System.Timers.Timer _updateTimer;
26 | private DateTime? _lastUpdateInvoke;
27 | private long _lastReceivedBytes;
28 | private long? _totalFileSize;
29 | private long _alreadyReceivedBytes;
30 |
31 | private int _lastDownloadRatesIdx = 0;
32 | private readonly double?[] _lastDownloadRates = new double?[15];
33 | private bool _hadZeroRate = false;
34 |
35 | public ProgressiveFileDownloader(string downloadUrl, string destinationFilePath)
36 | {
37 | _downloadUrl = downloadUrl;
38 | _destinationFilePath = destinationFilePath;
39 | _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
40 | _updateTimer = new System.Timers.Timer(500 /*ms*/);
41 | _updateTimer.Elapsed += TimerElapsed;
42 | }
43 |
44 | public async Task StartGetDownload()
45 | {
46 | using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
47 | await DownloadFileFromHttpResponseMessage(response);
48 | }
49 |
50 | private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
51 | {
52 | response.EnsureSuccessStatusCode();
53 |
54 | var totalBytes = response.Content.Headers.ContentLength;
55 | _totalFileSize = totalBytes;
56 |
57 | TriggerInitialInfo(totalBytes);
58 |
59 | await using (var contentStream = await response.Content.ReadAsStreamAsync())
60 | {
61 | await ProcessContentStream(contentStream);
62 | }
63 | }
64 |
65 | private async Task ProcessContentStream(Stream contentStream)
66 | {
67 | long totalBytesRead = 0;
68 | long readCount = 0;
69 | var buffer = new byte[4096];
70 | var isMoreToRead = true;
71 |
72 | _updateTimer.Start();
73 | try
74 | {
75 | using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, buffer.Length, true))
76 | {
77 | do
78 | {
79 | var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
80 | if (bytesRead == 0)
81 | {
82 | isMoreToRead = false;
83 | UpdateInternalProgress(totalBytesRead);
84 | continue;
85 | }
86 |
87 | await fileStream.WriteAsync(buffer, 0, bytesRead);
88 |
89 | totalBytesRead += bytesRead;
90 | readCount += 1;
91 |
92 | if (readCount % 1000 == 0)
93 | UpdateInternalProgress(totalBytesRead);
94 | } while (isMoreToRead);
95 | }
96 | }
97 | finally
98 | {
99 | _updateTimer.Stop();
100 | }
101 |
102 | TriggerDownloadDone(totalBytesRead);
103 | }
104 |
105 | private void TriggerInitialInfo(long? totalDownloadSize)
106 | {
107 | InitialInfo?.Invoke(totalDownloadSize);
108 | }
109 |
110 | private void UpdateInternalProgress(long alreadyReceivedBytes)
111 | {
112 | _alreadyReceivedBytes = alreadyReceivedBytes;
113 | }
114 |
115 | private void TimerElapsed(object? sender, ElapsedEventArgs e)
116 | {
117 | UpdateAndTriggerProgressChanged();
118 | }
119 |
120 | private void UpdateAndTriggerProgressChanged()
121 | {
122 | DateTime now = DateTime.Now;
123 | var elapsed = now - _lastUpdateInvoke;
124 | long amountDownloadedInPeriod = _alreadyReceivedBytes - _lastReceivedBytes;
125 | if (amountDownloadedInPeriod == 0 && !_hadZeroRate)
126 | {
127 | _hadZeroRate = true;
128 | return;
129 | }
130 | _hadZeroRate = false;
131 | _lastUpdateInvoke = now;
132 | _lastReceivedBytes = _alreadyReceivedBytes;
133 |
134 | if (elapsed != null)
135 | {
136 | double thisBytePerSec = amountDownloadedInPeriod / elapsed.Value.TotalSeconds;
137 | _lastDownloadRates[_lastDownloadRatesIdx] = thisBytePerSec;
138 | _lastDownloadRatesIdx = (_lastDownloadRatesIdx + 1) % _lastDownloadRates.Length;
139 |
140 | TriggerProgressChanged();
141 | }
142 | }
143 |
144 | private void TriggerProgressChanged()
145 | {
146 | double dlRate = _lastDownloadRates.Where(x => x != null).Select(x => x!.Value).Average();
147 | ProgressChangedFixedDelay?.Invoke(_totalFileSize, _alreadyReceivedBytes, (long)dlRate);
148 | }
149 |
150 | private void TriggerDownloadDone(long bytesDownloaded)
151 | {
152 | DownloadDone?.Invoke(bytesDownloaded);
153 | }
154 |
155 | public void Dispose()
156 | {
157 | _httpClient?.Dispose();
158 | _updateTimer.Elapsed -= TimerElapsed;
159 | _updateTimer.Stop();
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/WinterspringLauncher/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Linq;
5 | using Avalonia.Media;
6 | using Avalonia.Metadata;
7 | using CommunityToolkit.Mvvm.ComponentModel;
8 |
9 | namespace WinterspringLauncher.ViewModels;
10 |
11 | public partial class MainWindowViewModel : ObservableObject
12 | {
13 | public LauncherLogic Logic { get; }
14 |
15 | public MainWindowViewModel()
16 | {
17 | Logic = new LauncherLogic(this);
18 | }
19 |
20 | [ObservableProperty]
21 | private LanguageHolder _language = new LanguageHolder();
22 |
23 | [ObservableProperty]
24 | private bool _inputIsAllowed = true;
25 |
26 | [ObservableProperty]
27 | public int _selectedServerIdx;
28 |
29 | [ObservableProperty]
30 | public string _thisLauncherVersion = LauncherVersion.ShortVersionString;
31 |
32 | [ObservableProperty]
33 | public string _thisLauncherVersionDetailed = LauncherVersion.DetailedVersionString;
34 |
35 | [ObservableProperty]
36 | private bool _gameFolderExists = false;
37 |
38 | [ObservableProperty]
39 | private bool _gameIsInstalled = false;
40 |
41 | [ObservableProperty]
42 | private string _gameVersion = "";
43 |
44 | [ObservableProperty]
45 | public string? _hermesPidToolTipString = null;
46 |
47 | [ObservableProperty]
48 | public bool _hermesIsRunning = false;
49 |
50 | [ObservableProperty]
51 | public string? _detectedHermesVersion;
52 |
53 | [ObservableProperty]
54 | public bool _hermesIsInstalled;
55 |
56 | public void SetHermesPid(int? pid)
57 | {
58 | // TODO How to I remove this function and just have a HermesProxyPid Property that will assign the other ones?
59 | HermesPidToolTipString = pid.HasValue ? $"Hermes PID: {pid.Value}" : null;
60 | HermesIsRunning = pid.HasValue;
61 | }
62 |
63 | public void SetHermesVersion(string? versionStr)
64 | {
65 | DetectedHermesVersion = versionStr;
66 | HermesIsInstalled = versionStr != null;
67 | }
68 |
69 | [ObservableProperty]
70 | public ObservableCollection _knownServerList = new ObservableCollection();
71 |
72 | public string LogEntriesCombined { get; private set; }
73 |
74 | public List LogEntriesArray = new List();
75 |
76 | public void AddLogEntry(string logEntry)
77 | {
78 | OnPropertyChanging(nameof(LogEntriesCombined));
79 | if (LogEntriesArray.Count > 50)
80 | LogEntriesArray.RemoveAt(0);
81 | LogEntriesArray.Add(logEntry);
82 | LogEntriesCombined = string.Join('\n', LogEntriesArray);
83 | OnPropertyChanged(nameof(LogEntriesCombined));
84 | }
85 |
86 | public class LanguageHolder
87 | {
88 | public void SetLanguage(string languageShortName)
89 | {
90 |
91 | }
92 | }
93 |
94 |
95 | [ObservableProperty]
96 | private string _progressbarText = "";
97 |
98 | // not observable
99 | private string _progressbarInternalTitle = "";
100 |
101 | // not observable
102 | private ProgressbarInternalTimeTracker _progressbarInternalTimeTracker;
103 |
104 | [ObservableProperty]
105 | private double _progressbarPercent = 0;
106 |
107 | [ObservableProperty]
108 | private IBrush _progressbarColor = Brush.Parse("#FFFFFF");
109 |
110 | public void SetProgressbar(string title, double progressPercent, IBrush color)
111 | {
112 | _progressbarInternalTitle = title;
113 | _progressbarInternalTimeTracker = new ProgressbarInternalTimeTracker();
114 | ProgressbarPercent = progressPercent;
115 | ProgressbarColor = color;
116 | ProgressbarText = $"{progressPercent:0}% {_progressbarInternalTitle}";
117 | }
118 |
119 | public void UpdateProgress(double progressPercent, string additionalText)
120 | {
121 | ProgressbarPercent = progressPercent;
122 | TimeSpan? estimatedTime = _progressbarInternalTimeTracker.GetEstimatedTimeAndUpdateRates(progressPercent);
123 | string timeLeft = estimatedTime.HasValue
124 | ? TimeSpan.FromSeconds((long) estimatedTime.Value.TotalSeconds).ToString()
125 | : "?".PadLeft("00:00:00".Length);
126 |
127 | ProgressbarText = $"{progressPercent:0}% {_progressbarInternalTitle} {additionalText} estimated time: {timeLeft}";
128 | }
129 |
130 | private class ProgressbarInternalTimeTracker
131 | {
132 | private double _lastPercent = 0;
133 | private DateTime? _lastUpdateTime = null;
134 | private int _lastProgressRatesIdx = 0;
135 | private readonly double?[] _lastProgressRates = new double?[15];
136 |
137 | public TimeSpan? GetEstimatedTimeAndUpdateRates(double percent)
138 | {
139 | var now = DateTime.Now;
140 | if (_lastUpdateTime != null)
141 | {
142 | TimeSpan timeDiff = now - _lastUpdateTime.Value;
143 | double progressDiff = percent - _lastPercent;
144 | double progressDiffPerSec = progressDiff / timeDiff.TotalSeconds;
145 | _lastProgressRates[_lastProgressRatesIdx] = progressDiffPerSec;
146 | _lastProgressRatesIdx = (_lastProgressRatesIdx + 1) % _lastProgressRates.Length;
147 | }
148 | _lastUpdateTime = now;
149 | _lastPercent = percent;
150 |
151 | var avgRate = _lastProgressRates.Where(x => x.HasValue).Select(x => x!.Value).DefaultIfEmpty(0).Average();
152 | if (avgRate == 0)
153 | return null;
154 |
155 | const double maxPercent = 100;
156 | double time = (maxPercent - percent) / avgRate;
157 | if (double.IsNaN(time))
158 | return null;
159 |
160 | return TimeSpan.FromSeconds(time);
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LauncherLogic.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Threading.Tasks;
7 | using Avalonia;
8 | using Avalonia.Controls.ApplicationLifetimes;
9 | using Avalonia.Threading;
10 | using WinterspringLauncher.Utils;
11 | using WinterspringLauncher.ViewModels;
12 | using WinterspringLauncher.Views;
13 |
14 | namespace WinterspringLauncher;
15 |
16 | public partial class LauncherLogic
17 | {
18 | private const string CONFIG_FILE_NAME = "winterspring-launcher-config.json";
19 |
20 | private readonly MainWindowViewModel _model;
21 | private readonly LauncherConfig _config;
22 |
23 | private string FullPath(string subPath) => Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, subPath));
24 |
25 | private static readonly string SubPathToWowOriginal = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
26 | ? "_classic_era_/World of Warcraft Classic.app/Contents/MacOS/World of Warcraft Classic"
27 | : "_classic_era_/WowClassic.exe";
28 |
29 | private static readonly string SubPathToWowForCustomServers = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
30 | ? "_classic_era_/WoW For Custom Servers.app/Contents/MacOS/WoW For Custom Servers"
31 | : "_classic_era_/WowClassic_ForCustomServers.exe";
32 |
33 | public LauncherLogic(MainWindowViewModel model)
34 | {
35 | _model = model;
36 |
37 | _config = LauncherConfig.LoadOrCreateDefault(CONFIG_FILE_NAME);
38 |
39 | if (_config.LastSelectedServerName == "") // first configuration
40 | {
41 | _config.LastSelectedServerName = LocaleDefaults.GetBestServerName();
42 | _config.GitHubApiMirror = LocaleDefaults.GetBestGitHubMirror();
43 | }
44 |
45 | if (!string.IsNullOrWhiteSpace(_config.GitHubApiMirror))
46 | GitHubApi.GitHubApiAddress = _config.GitHubApiMirror;
47 |
48 | for (var i = 0; i < _config.KnownServers.Length; i++)
49 | {
50 | var knownServer = _config.KnownServers[i];
51 | _model.KnownServerList.Add(knownServer.Name);
52 | if (_config.LastSelectedServerName == knownServer.Name)
53 | _model.SelectedServerIdx = i;
54 | }
55 |
56 | _model.Language.SetLanguage(_config.LauncherLanguage);
57 |
58 | _model.AddLogEntry($"Launcher started");
59 | _model.AddLogEntry($"Base path: \"{FullPath(".")}\"");
60 | _model.AddLogEntry($"GitHub API Address: {GitHubApi.GitHubApiAddress}");
61 |
62 | string? localHermesVersion = null;
63 | var hermesProxyVersionFile = Path.Combine(_config.HermesProxyLocation, "version.txt");
64 | if (File.Exists(hermesProxyVersionFile))
65 | {
66 | localHermesVersion = File.ReadLines(hermesProxyVersionFile).First().Split("|")[0];
67 | }
68 | _model.SetHermesVersion(localHermesVersion);
69 |
70 |
71 | if (_config.CheckForLauncherUpdates)
72 | {
73 | Task.Run(() =>
74 | {
75 | try
76 | {
77 | if (LauncherVersion.CheckIfUpdateIsAvailable(out var updateInformation))
78 | {
79 | _model.AddLogEntry($"--------------------------");
80 | _model.AddLogEntry($"This launcher has a new version {updateInformation.VersionName} ({updateInformation.ReleaseDate:yyyy-MM-dd})");
81 | _model.AddLogEntry($"You can download it here {updateInformation.URLLinkToReleasePage}");
82 | _model.AddLogEntry($"--------------------------");
83 | CreateUpdatePopup(updateInformation);
84 | }
85 | Console.WriteLine("Launcher update check done");
86 | }
87 | catch (Exception e)
88 | {
89 | _model.AddLogEntry("An error occured while checking for a launcher update");
90 | Console.WriteLine(e);
91 | }
92 | });
93 | }
94 | }
95 |
96 | private void CreateUpdatePopup(LauncherVersion.UpdateInformation updateInformation)
97 | {
98 | Dispatcher.UIThread.Post(() =>
99 | {
100 | if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null)
101 | {
102 | var dialog = new NewVersionAvailableDialog(updateInformation);
103 | dialog.ShowDialog(desktop.MainWindow);
104 | }
105 | });
106 | }
107 |
108 | public void ChangeServerIdx()
109 | {
110 | var serverInfo = _config.KnownServers.ElementAtOrDefault(_model.SelectedServerIdx);
111 | if (serverInfo == null)
112 | {
113 | _model.AddLogEntry("Error invalid server settings");
114 | _model.InputIsAllowed = true;
115 | return;
116 | }
117 |
118 | Console.WriteLine($"Selected Server: {serverInfo.Name}");
119 | var gameInstallation = _config.GameInstallations.GetValueOrDefault(serverInfo.UsedInstallation);
120 | if (gameInstallation == null)
121 | {
122 | _model.AddLogEntry($"Error cant find '{serverInfo.UsedInstallation}' installation in settings");
123 | _model.InputIsAllowed = true;
124 | return;
125 | }
126 |
127 | _config.LastSelectedServerName = serverInfo.Name;
128 | _config.SaveConfig(CONFIG_FILE_NAME);
129 |
130 | var expectedPatchedClientLocation = Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers);
131 | _model.GameFolderExists = Directory.Exists(gameInstallation.Directory);
132 | _model.GameIsInstalled = File.Exists(expectedPatchedClientLocation);
133 |
134 | _model.GameVersion = string.Join('.', gameInstallation.Version.Split('.').SkipLast(1));
135 | }
136 |
137 | private void RunDownload(string downloadUrl, string destLocation)
138 | {
139 | LauncherActions.DownloadFile(downloadUrl, destLocation,
140 | (totalBytes, alreadyDownloadedBytes, bytesPerSec) =>
141 | {
142 | double percent = (totalBytes != null)
143 | ? (alreadyDownloadedBytes / (double)totalBytes.Value) * 100
144 | : 0;
145 |
146 | string additionalText = $" {UtilHelper.ToHumanFileSize(alreadyDownloadedBytes)}/{UtilHelper.ToHumanFileSize(totalBytes ?? 0)} {UtilHelper.ToHumanFileSize(bytesPerSec)}/s ";
147 | _model.UpdateProgress(percent, additionalText);
148 | });
149 | }
150 |
151 | private void RunUnpack(string archiveLocation, string targetDir)
152 | {
153 | LauncherActions.Unpack(archiveLocation, targetDir,
154 | (totalFileCount, alreadyUnpacked) =>
155 | {
156 | double percent = (alreadyUnpacked / (double)totalFileCount) * 100;
157 | _model.UpdateProgress(percent, $" {alreadyUnpacked} / {totalFileCount} ");
158 | });
159 | }
160 |
161 | public void KillHermesProxy()
162 | {
163 | try
164 | {
165 | _hermesProcess?.Kill();
166 | }
167 | catch(Exception e)
168 | {
169 | _model.AddLogEntry("Fail to stop HermesProxy");
170 | Console.WriteLine("Fail to kill HermesProxy");
171 | Console.WriteLine(e);
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LauncherActions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Globalization;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Runtime.InteropServices;
8 | using System.Text;
9 | using System.Threading;
10 | using WinterspringLauncher.Utils;
11 |
12 | namespace WinterspringLauncher;
13 |
14 | public static class LauncherActions
15 | {
16 | public delegate void DownloadProgressInfoHandler(long? totalBytes, long alreadyDownloadedBytes, long bytesPerSec);
17 | public delegate void UnpackProgressInfoHandler(long totalFileCount, long alreadyUnpackedFileCount);
18 |
19 | public static void DownloadFile(string downloadUrl, string downloadDestLocation, DownloadProgressInfoHandler progressInfoHandler)
20 | {
21 | using (var client = new ProgressiveFileDownloader(downloadUrl, downloadDestLocation))
22 | {
23 | client.ProgressChangedFixedDelay += (totalBytes, alreadyDownloadedBytes, bytePerSec) =>
24 | {
25 | progressInfoHandler(totalBytes, alreadyDownloadedBytes, bytePerSec);
26 | };
27 |
28 | client.DownloadDone += (downloadedBytes) => {
29 | progressInfoHandler(downloadedBytes, downloadedBytes, 0);
30 | };
31 |
32 | client.StartGetDownload().Wait();
33 | }
34 | }
35 |
36 | public static void PrepareGameConfigWtf(string gamePath, string portalAddress)
37 | {
38 | var configWtfPath = Path.Combine(gamePath, "_classic_era_", "WTF", "Config.wtf");
39 | var dirName = Path.GetDirectoryName(configWtfPath);
40 | Directory.CreateDirectory(dirName!);
41 |
42 | List configContent;
43 | if (!File.Exists(configWtfPath))
44 | {
45 | configContent = new List();
46 | string bestDefaultTextLocale = LocaleDefaults.GetBestWoWConfigLocale();
47 | configContent.Add($"SET textLocale {bestDefaultTextLocale}");
48 | }
49 | else
50 | {
51 | configContent = File.ReadAllLines(configWtfPath).ToList();
52 | }
53 |
54 | var newLine = $"SET portal \"{portalAddress}\"";
55 | bool wasChanged = false;
56 |
57 | // Add SET PORTAL ...
58 | var currentPortalLine = configContent.FindIndex(l => l.StartsWith("SET portal "));
59 | if (currentPortalLine != -1)
60 | {
61 | if (configContent[currentPortalLine] != newLine)
62 | {
63 | configContent[currentPortalLine] = newLine;
64 | wasChanged = true;
65 | }
66 | }
67 | else
68 | {
69 | configContent.Add(newLine);
70 | wasChanged = true;
71 | }
72 |
73 | // Remove disableServerNagle
74 | var disableServerNagleLine = configContent.FindIndex(l => l.StartsWith("SET disableServerNagle "));
75 | if (disableServerNagleLine != -1)
76 | {
77 | configContent.RemoveAt(disableServerNagleLine);
78 | wasChanged = true;
79 | }
80 |
81 | if (wasChanged)
82 | {
83 | File.WriteAllLines(configWtfPath, configContent, Encoding.UTF8);
84 | }
85 | }
86 |
87 | public static void Unpack(string compressedArchivePath, string targetDir, UnpackProgressInfoHandler progressInfoHandler)
88 | {
89 | ArchiveCompression.Decompress(compressedArchivePath, targetDir, folderToSkipName: "World of Warcraft", // TODO: <-- move this check somewhere else
90 | (totalFileCount, alreadyUnpackedFileCount) =>
91 | {
92 | progressInfoHandler(totalFileCount, alreadyUnpackedFileCount);
93 | }, shouldBeDecompressedPredicate: (filePath) => !filePath.Contains("World of Warcraft Launcher.exe")); // TODO: <-- move this check somewhere else
94 | }
95 |
96 | public delegate void OnLogLine(string logLine);
97 |
98 | public static Process StartHermesProxy(string hermesDir, ushort modernClientBuild, Dictionary settingsOverwrite, OnLogLine logLine)
99 | {
100 | bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
101 |
102 | var executableName = weAreOnMacOs
103 | ? "HermesProxy"
104 | : "HermesProxy.exe";
105 |
106 | var executablePath = Path.Combine(hermesDir, executableName);
107 |
108 | var procInfo = new ProcessStartInfo
109 | {
110 | FileName = executablePath,
111 | WorkingDirectory = hermesDir,
112 | RedirectStandardOutput = true,
113 | CreateNoWindow = true,
114 | ArgumentList = {
115 | "--no-version-check",
116 | "--set", $"ClientBuild={modernClientBuild}",
117 | },
118 | };
119 |
120 | foreach (var (key, value) in settingsOverwrite)
121 | {
122 | procInfo.ArgumentList.Add("--set");
123 | procInfo.ArgumentList.Add($"{key}={value}");
124 | }
125 |
126 | Console.WriteLine("Starting HermesProxy with arguments: ");
127 | for (var i = 0; i < procInfo.ArgumentList.Count; i++)
128 | Console.WriteLine($"[{i}] {procInfo.ArgumentList[i]}");
129 | var process = Process.Start(procInfo)!;
130 |
131 | process.EnableRaisingEvents = true;
132 | process.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
133 | {
134 | if (!String.IsNullOrEmpty(e.Data))
135 | {
136 | logLine(e.Data);
137 | }
138 | });
139 | process.BeginOutputReadLine();
140 |
141 | return process;
142 | }
143 |
144 | public static void StartGame(string executablePath)
145 | {
146 | bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
147 |
148 | ProcessStartInfo startInfo;
149 | if (weAreOnMacOs)
150 | {
151 | startInfo = new ProcessStartInfo
152 | {
153 | // We are here "_classic_era_/WoW For Custom Servers.app/Contents/MacOS/WoW For Custom Servers"
154 | // and want to be here "_classic_era_"
155 |
156 | FileName = "/usr/bin/open",
157 | ArgumentList = { "--new", "--wait-apps", $"./{Path.GetDirectoryName(Path.Combine(executablePath, "..", ".."))}" },
158 | WorkingDirectory = Path.GetDirectoryName(Path.Combine(executablePath, "..", "..", "..")),
159 | UseShellExecute = true,
160 | CreateNoWindow = false,
161 | RedirectStandardError = false,
162 | RedirectStandardInput = false,
163 | RedirectStandardOutput = false,
164 | };
165 | }
166 | else
167 | {
168 | startInfo = new ProcessStartInfo
169 | {
170 | FileName = Path.GetFileName(executablePath),
171 | WorkingDirectory = Path.GetDirectoryName(executablePath),
172 | UseShellExecute = true,
173 | CreateNoWindow = false,
174 | RedirectStandardError = false,
175 | RedirectStandardInput = false,
176 | RedirectStandardOutput = false,
177 | };
178 | }
179 |
180 | //startInfo.EnvironmentVariables.Clear();
181 | var process = Process.Start(startInfo)!;
182 | while (!process.HasExited && process.VirtualMemorySize64 < 100 * 1024 * 1024)
183 | {
184 | Thread.Sleep(TimeSpan.FromSeconds(1));
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Utils/ArchiveCompression.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Compression;
4 | using System.Linq;
5 | using System.Reflection;
6 |
7 | #if PLATFORM_WINDOWS
8 | using SevenZip;
9 | #endif
10 |
11 | namespace WinterspringLauncher.Utils;
12 |
13 | public static class ArchiveCompression
14 | {
15 | public delegate void UnpackProgressInfoHandler(long totalFileCount, long alreadyUnpackedFileCount);
16 |
17 | public static void Decompress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func shouldBeDecompressedPredicate)
18 | {
19 | byte[] buffer = new byte[4];
20 | using (FileStream fileHandle = File.OpenRead(archiveFilePath))
21 | {
22 | fileHandle.Read(buffer, 0, 4);
23 | }
24 |
25 | if (buffer.SequenceEqual(new byte[] { 0x52, 0x61, 0x72, 0x21 })) // Rar!
26 | {
27 | Decompress7ZWithProgress(archiveFilePath, extractionFolderPath, folderToSkipName, progressHandler, shouldBeDecompressedPredicate);
28 | }
29 | else if (buffer[..2].SequenceEqual(new byte[] { 0x50, 0x4B })) // Zip
30 | {
31 | DecompressZipWithProgress(archiveFilePath, extractionFolderPath, folderToSkipName, progressHandler, shouldBeDecompressedPredicate);
32 | }
33 | else // Error
34 | {
35 | throw new Exception("Unknown file format. Cannot decompress");
36 | }
37 | }
38 |
39 | private static void DecompressZipWithProgress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func shouldBeDecompressedPredicate)
40 | {
41 | using var zip = ZipFile.OpenRead(archiveFilePath);
42 | bool ShouldBeDecompressed(ZipArchiveEntry entry) => !entry.FullName.EndsWith("\\") && !entry.FullName.EndsWith("/") && shouldBeDecompressedPredicate(entry.FullName);
43 | var totalSize = zip.Entries.Where(ShouldBeDecompressed).Sum(x => x.Length);
44 | var totalCount = zip.Entries.Where(ShouldBeDecompressed).Count();
45 |
46 | string ToPath(string path) => Path.Combine(extractionFolderPath, path);
47 |
48 | Console.WriteLine($"Total size to decompress {UtilHelper.ToHumanFileSize(totalSize)}");
49 | long alreadyDecompressedCount = 0;
50 | foreach (var entry in zip.Entries.Where(ShouldBeDecompressed))
51 | {
52 | var destPath = ToPath(entry.FullName);
53 | Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
54 | entry.ExtractToFile(destPath, overwrite: true);
55 | alreadyDecompressedCount++;
56 | progressHandler(totalCount, alreadyDecompressedCount);
57 | }
58 | progressHandler(totalCount, totalCount);
59 | }
60 |
61 | #if !PLATFORM_WINDOWS
62 | private static void Decompress7ZWithProgress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func shouldBeDecompressed)
63 | {
64 | throw new NotSupportedException("7z is only supported on Windows");
65 | }
66 | #else
67 | private static void Decompress7ZWithProgress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func shouldBeDecompressedPredicate)
68 | {
69 | var assembly = Assembly.GetExecutingAssembly();
70 | var resourceName = "WinterspringLauncher.7z.dll";
71 |
72 | using (Stream stream = assembly.GetManifestResourceStream(resourceName)!)
73 | {
74 | try
75 | {
76 | using (var file = File.Open("7z.dll", FileMode.Create, FileAccess.Write))
77 | {
78 | stream.CopyTo(file);
79 | }
80 | }
81 | catch(Exception e)
82 | {
83 | // Maybe the file is somehow already in use
84 | Console.WriteLine("Failed to write 7z.dll");
85 | Console.WriteLine(e);
86 | }
87 | }
88 |
89 | SevenZipBase.SetLibraryPath("7z.dll");
90 | string downloadedFile = Path.Combine(archiveFilePath);
91 | Console.WriteLine($"Extracting archive into {extractionFolderPath}");
92 | using (var archiveFile = new SevenZipExtractor(downloadedFile))
93 | {
94 | bool ShouldBeDecompressed(ArchiveFileInfo entry) => !entry.IsDirectory && shouldBeDecompressedPredicate(entry.FileName);
95 | string ToPath(string path) => path.ReplaceFirstOccurrence(folderToSkipName, extractionFolderPath);
96 |
97 | long totalSize = 0;
98 | long totalCount = 0;
99 | foreach (var entry in archiveFile.ArchiveFileData)
100 | {
101 | if (ShouldBeDecompressed(entry))
102 | {
103 | totalSize += (long) entry.Size;
104 | totalCount++;
105 | }
106 | }
107 |
108 | Console.WriteLine($"Total size to decompress {UtilHelper.ToHumanFileSize(totalSize)}");
109 | long alreadyDecompressedCount = 0;
110 | foreach (var entry in archiveFile.ArchiveFileData)
111 | {
112 | if (ShouldBeDecompressed(entry))
113 | {
114 | var destName = ToPath(entry.FileName);
115 | Directory.CreateDirectory(Path.GetDirectoryName(destName)!);
116 | using (var fStream = File.Open(destName, FileMode.Create, FileAccess.Write))
117 | {
118 | archiveFile.ExtractFile(entry.FileName, fStream);
119 | }
120 | alreadyDecompressedCount++;
121 | progressHandler(totalCount, alreadyDecompressedCount);
122 | }
123 | }
124 | progressHandler(totalCount, totalCount);
125 | }
126 |
127 | try
128 | {
129 | File.Delete("7z.dll");
130 | }
131 | catch
132 | {
133 | // ignored
134 | }
135 | }
136 | #endif
137 | public static void DecompressSmartSkipFirstFolder(string zipFilePath, string outputDirectory)
138 | {
139 | using var zip = ZipFile.OpenRead(zipFilePath);
140 | string? zipBaseFolder = GetBaseFolderFromZip(zip);
141 |
142 | Console.WriteLine($"Unzipping {zipFilePath}, detected '{zipBaseFolder ?? ""}' as first folder");
143 |
144 | string ToFilteredPath(string path) => zipBaseFolder != null
145 | ? path.ReplaceFirstOccurrence(zipBaseFolder, outputDirectory)
146 | : path;
147 |
148 | string GetCompletePath(ZipArchiveEntry entry) => Path.Combine(outputDirectory, ToFilteredPath(entry.FullName));
149 |
150 | foreach (var entry in zip.Entries.Where(e => !e.IsFolder()))
151 | {
152 | var completePath = GetCompletePath(entry);
153 | Directory.CreateDirectory(Path.GetDirectoryName(completePath)!);
154 | entry.ExtractToFile(completePath, overwrite: true);
155 | }
156 | }
157 |
158 | private static bool IsFolder(this ZipArchiveEntry entry)
159 | {
160 | return entry.FullName.EndsWith("/");
161 | }
162 |
163 | static string? GetBaseFolderFromZip(ZipArchive archive)
164 | {
165 | string[] entryPaths = archive.Entries.Select(entry => entry.FullName).ToArray();
166 | if (entryPaths.Length == 0)
167 | return null;
168 |
169 | string[] parts = entryPaths[0].Split('/');
170 |
171 | for (int i = 1; i < entryPaths.Length; i++)
172 | {
173 | string[] currentParts = entryPaths[i].Split('/');
174 |
175 | int commonParts = parts.Zip(currentParts, (p1, p2) => p1 == p2).TakeWhile(b => b).Count();
176 | if (commonParts == 0)
177 | return null;
178 |
179 | Array.Resize(ref parts, commonParts);
180 | }
181 |
182 | return string.Join("/", parts);
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,dotnetcore
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,rider,dotnetcore
3 |
4 | ### DotnetCore ###
5 | # .NET Core build folders
6 | bin/
7 | obj/
8 |
9 | # Common node modules locations
10 | /node_modules
11 | /wwwroot/node_modules
12 |
13 | ### Rider ###
14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
16 |
17 | # User-specific stuff
18 | .idea/**/workspace.xml
19 | .idea/**/tasks.xml
20 | .idea/**/usage.statistics.xml
21 | .idea/**/dictionaries
22 | .idea/**/shelf
23 |
24 | # AWS User-specific
25 | .idea/**/aws.xml
26 |
27 | # Generated files
28 | .idea/**/contentModel.xml
29 |
30 | # Sensitive or high-churn files
31 | .idea/**/dataSources/
32 | .idea/**/dataSources.ids
33 | .idea/**/dataSources.local.xml
34 | .idea/**/sqlDataSources.xml
35 | .idea/**/dynamic.xml
36 | .idea/**/uiDesigner.xml
37 | .idea/**/dbnavigator.xml
38 |
39 | # Gradle
40 | .idea/**/gradle.xml
41 | .idea/**/libraries
42 |
43 | # Gradle and Maven with auto-import
44 | # When using Gradle or Maven with auto-import, you should exclude module files,
45 | # since they will be recreated, and may cause churn. Uncomment if using
46 | # auto-import.
47 | # .idea/artifacts
48 | # .idea/compiler.xml
49 | # .idea/jarRepositories.xml
50 | # .idea/modules.xml
51 | # .idea/*.iml
52 | # .idea/modules
53 | # *.iml
54 | # *.ipr
55 |
56 | # CMake
57 | cmake-build-*/
58 |
59 | # Mongo Explorer plugin
60 | .idea/**/mongoSettings.xml
61 |
62 | # File-based project format
63 | *.iws
64 |
65 | # IntelliJ
66 | out/
67 |
68 | # mpeltonen/sbt-idea plugin
69 | .idea_modules/
70 |
71 | # JIRA plugin
72 | atlassian-ide-plugin.xml
73 |
74 | # Cursive Clojure plugin
75 | .idea/replstate.xml
76 |
77 | # SonarLint plugin
78 | .idea/sonarlint/
79 |
80 | # Crashlytics plugin (for Android Studio and IntelliJ)
81 | com_crashlytics_export_strings.xml
82 | crashlytics.properties
83 | crashlytics-build.properties
84 | fabric.properties
85 |
86 | # Editor-based Rest Client
87 | .idea/httpRequests
88 |
89 | # Android studio 3.1+ serialized cache file
90 | .idea/caches/build_file_checksums.ser
91 |
92 | ### VisualStudioCode ###
93 | .vscode/*
94 | !.vscode/settings.json
95 | !.vscode/tasks.json
96 | !.vscode/launch.json
97 | !.vscode/extensions.json
98 | !.vscode/*.code-snippets
99 |
100 | # Local History for Visual Studio Code
101 | .history/
102 |
103 | # Built Visual Studio Code Extensions
104 | *.vsix
105 |
106 | ### VisualStudioCode Patch ###
107 | # Ignore all local history of files
108 | .history
109 | .ionide
110 |
111 | ### VisualStudio ###
112 | ## Ignore Visual Studio temporary files, build results, and
113 | ## files generated by popular Visual Studio add-ons.
114 | ##
115 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
116 |
117 | # User-specific files
118 | *.rsuser
119 | *.suo
120 | *.user
121 | *.userosscache
122 | *.sln.docstates
123 |
124 | # User-specific files (MonoDevelop/Xamarin Studio)
125 | *.userprefs
126 |
127 | # Mono auto generated files
128 | mono_crash.*
129 |
130 | # Build results
131 | [Dd]ebug/
132 | [Dd]ebugPublic/
133 | [Rr]elease/
134 | [Rr]eleases/
135 | x64/
136 | x86/
137 | [Ww][Ii][Nn]32/
138 | [Aa][Rr][Mm]/
139 | [Aa][Rr][Mm]64/
140 | bld/
141 | [Bb]in/
142 | [Oo]bj/
143 | [Ll]og/
144 | [Ll]ogs/
145 |
146 | # Visual Studio 2015/2017 cache/options directory
147 | .vs/
148 | # Uncomment if you have tasks that create the project's static files in wwwroot
149 | #wwwroot/
150 |
151 | # Visual Studio 2017 auto generated files
152 | Generated\ Files/
153 |
154 | # MSTest test Results
155 | [Tt]est[Rr]esult*/
156 | [Bb]uild[Ll]og.*
157 |
158 | # NUnit
159 | *.VisualState.xml
160 | TestResult.xml
161 | nunit-*.xml
162 |
163 | # Build Results of an ATL Project
164 | [Dd]ebugPS/
165 | [Rr]eleasePS/
166 | dlldata.c
167 |
168 | # Benchmark Results
169 | BenchmarkDotNet.Artifacts/
170 |
171 | # .NET Core
172 | project.lock.json
173 | project.fragment.lock.json
174 | artifacts/
175 |
176 | # ASP.NET Scaffolding
177 | ScaffoldingReadMe.txt
178 |
179 | # StyleCop
180 | StyleCopReport.xml
181 |
182 | # Files built by Visual Studio
183 | *_i.c
184 | *_p.c
185 | *_h.h
186 | *.ilk
187 | *.meta
188 | *.obj
189 | *.iobj
190 | *.pch
191 | *.pdb
192 | *.ipdb
193 | *.pgc
194 | *.pgd
195 | *.rsp
196 | *.sbr
197 | *.tlb
198 | *.tli
199 | *.tlh
200 | *.tmp
201 | *.tmp_proj
202 | *_wpftmp.csproj
203 | *.log
204 | *.tlog
205 | *.vspscc
206 | *.vssscc
207 | .builds
208 | *.pidb
209 | *.svclog
210 | *.scc
211 |
212 | # Chutzpah Test files
213 | _Chutzpah*
214 |
215 | # Visual C++ cache files
216 | ipch/
217 | *.aps
218 | *.ncb
219 | *.opendb
220 | *.opensdf
221 | *.sdf
222 | *.cachefile
223 | *.VC.db
224 | *.VC.VC.opendb
225 |
226 | # Visual Studio profiler
227 | *.psess
228 | *.vsp
229 | *.vspx
230 | *.sap
231 |
232 | # Visual Studio Trace Files
233 | *.e2e
234 |
235 | # TFS 2012 Local Workspace
236 | $tf/
237 |
238 | # Guidance Automation Toolkit
239 | *.gpState
240 |
241 | # ReSharper is a .NET coding add-in
242 | _ReSharper*/
243 | *.[Rr]e[Ss]harper
244 | *.DotSettings.user
245 |
246 | # TeamCity is a build add-in
247 | _TeamCity*
248 |
249 | # DotCover is a Code Coverage Tool
250 | *.dotCover
251 |
252 | # AxoCover is a Code Coverage Tool
253 | .axoCover/*
254 | !.axoCover/settings.json
255 |
256 | # Coverlet is a free, cross platform Code Coverage Tool
257 | coverage*.json
258 | coverage*.xml
259 | coverage*.info
260 |
261 | # Visual Studio code coverage results
262 | *.coverage
263 | *.coveragexml
264 |
265 | # NCrunch
266 | _NCrunch_*
267 | .*crunch*.local.xml
268 | nCrunchTemp_*
269 |
270 | # MightyMoose
271 | *.mm.*
272 | AutoTest.Net/
273 |
274 | # Web workbench (sass)
275 | .sass-cache/
276 |
277 | # Installshield output folder
278 | [Ee]xpress/
279 |
280 | # DocProject is a documentation generator add-in
281 | DocProject/buildhelp/
282 | DocProject/Help/*.HxT
283 | DocProject/Help/*.HxC
284 | DocProject/Help/*.hhc
285 | DocProject/Help/*.hhk
286 | DocProject/Help/*.hhp
287 | DocProject/Help/Html2
288 | DocProject/Help/html
289 |
290 | # Click-Once directory
291 | publish/
292 |
293 | # Publish Web Output
294 | *.[Pp]ublish.xml
295 | *.azurePubxml
296 | # Note: Comment the next line if you want to checkin your web deploy settings,
297 | # but database connection strings (with potential passwords) will be unencrypted
298 | *.pubxml
299 | *.publishproj
300 |
301 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
302 | # checkin your Azure Web App publish settings, but sensitive information contained
303 | # in these scripts will be unencrypted
304 | PublishScripts/
305 |
306 | # NuGet Packages
307 | *.nupkg
308 | # NuGet Symbol Packages
309 | *.snupkg
310 | # The packages folder can be ignored because of Package Restore
311 | **/[Pp]ackages/*
312 | # except build/, which is used as an MSBuild target.
313 | !**/[Pp]ackages/build/
314 | # Uncomment if necessary however generally it will be regenerated when needed
315 | #!**/[Pp]ackages/repositories.config
316 | # NuGet v3's project.json files produces more ignorable files
317 | *.nuget.props
318 | *.nuget.targets
319 |
320 | # Microsoft Azure Build Output
321 | csx/
322 | *.build.csdef
323 |
324 | # Microsoft Azure Emulator
325 | ecf/
326 | rcf/
327 |
328 | # Windows Store app package directories and files
329 | AppPackages/
330 | BundleArtifacts/
331 | Package.StoreAssociation.xml
332 | _pkginfo.txt
333 | *.appx
334 | *.appxbundle
335 | *.appxupload
336 |
337 | # Visual Studio cache files
338 | # files ending in .cache can be ignored
339 | *.[Cc]ache
340 | # but keep track of directories ending in .cache
341 | !?*.[Cc]ache/
342 |
343 | # Others
344 | ClientBin/
345 | ~$*
346 | *~
347 | *.dbmdl
348 | *.dbproj.schemaview
349 | *.jfm
350 | *.pfx
351 | *.publishsettings
352 | orleans.codegen.cs
353 |
354 | # Including strong name files can present a security risk
355 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
356 | #*.snk
357 |
358 | # Since there are multiple workflows, uncomment next line to ignore bower_components
359 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
360 | #bower_components/
361 |
362 | # RIA/Silverlight projects
363 | Generated_Code/
364 |
365 | # Backup & report files from converting an old project file
366 | # to a newer Visual Studio version. Backup files are not needed,
367 | # because we have git ;-)
368 | _UpgradeReport_Files/
369 | Backup*/
370 | UpgradeLog*.XML
371 | UpgradeLog*.htm
372 | ServiceFabricBackup/
373 | *.rptproj.bak
374 |
375 | # SQL Server files
376 | *.mdf
377 | *.ldf
378 | *.ndf
379 |
380 | # Business Intelligence projects
381 | *.rdl.data
382 | *.bim.layout
383 | *.bim_*.settings
384 | *.rptproj.rsuser
385 | *- [Bb]ackup.rdl
386 | *- [Bb]ackup ([0-9]).rdl
387 | *- [Bb]ackup ([0-9][0-9]).rdl
388 |
389 | # Microsoft Fakes
390 | FakesAssemblies/
391 |
392 | # GhostDoc plugin setting file
393 | *.GhostDoc.xml
394 |
395 | # Node.js Tools for Visual Studio
396 | .ntvs_analysis.dat
397 | node_modules/
398 |
399 | # Visual Studio 6 build log
400 | *.plg
401 |
402 | # Visual Studio 6 workspace options file
403 | *.opt
404 |
405 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
406 | *.vbw
407 |
408 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
409 | *.vbp
410 |
411 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
412 | *.dsw
413 | *.dsp
414 |
415 | # Visual Studio 6 technical files
416 |
417 | # Visual Studio LightSwitch build output
418 | **/*.HTMLClient/GeneratedArtifacts
419 | **/*.DesktopClient/GeneratedArtifacts
420 | **/*.DesktopClient/ModelManifest.xml
421 | **/*.Server/GeneratedArtifacts
422 | **/*.Server/ModelManifest.xml
423 | _Pvt_Extensions
424 |
425 | # Paket dependency manager
426 | .paket/paket.exe
427 | paket-files/
428 |
429 | # FAKE - F# Make
430 | .fake/
431 |
432 | # CodeRush personal settings
433 | .cr/personal
434 |
435 | # Python Tools for Visual Studio (PTVS)
436 | __pycache__/
437 | *.pyc
438 |
439 | # Cake - Uncomment if you are using it
440 | # tools/**
441 | # !tools/packages.config
442 |
443 | # Tabs Studio
444 | *.tss
445 |
446 | # Telerik's JustMock configuration file
447 | *.jmconfig
448 |
449 | # BizTalk build output
450 | *.btp.cs
451 | *.btm.cs
452 | *.odx.cs
453 | *.xsd.cs
454 |
455 | # OpenCover UI analysis results
456 | OpenCover/
457 |
458 | # Azure Stream Analytics local run output
459 | ASALocalRun/
460 |
461 | # MSBuild Binary and Structured Log
462 | *.binlog
463 |
464 | # NVidia Nsight GPU debugger configuration file
465 | *.nvuser
466 |
467 | # MFractors (Xamarin productivity tool) working folder
468 | .mfractor/
469 |
470 | # Local History for Visual Studio
471 | .localhistory/
472 |
473 | # Visual Studio History (VSHistory) files
474 | .vshistory/
475 |
476 | # BeatPulse healthcheck temp database
477 | healthchecksdb
478 |
479 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
480 | MigrationBackup/
481 |
482 | # Ionide (cross platform F# VS Code tools) working folder
483 | .ionide/
484 |
485 | # Fody - auto-generated XML schema
486 | FodyWeavers.xsd
487 |
488 | # VS Code files for those working on multiple tools
489 | *.code-workspace
490 |
491 | # Local History for Visual Studio Code
492 |
493 | # Windows Installer files from build outputs
494 | *.cab
495 | *.msi
496 | *.msix
497 | *.msm
498 | *.msp
499 |
500 | # JetBrains Rider
501 | *.sln.iml
502 |
503 | ### VisualStudio Patch ###
504 | # Additional files built by Visual Studio
505 |
506 | # End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,dotnetcore
507 |
508 | .idea/
509 | .DS_Store
510 |
511 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LauncherConfig.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Text;
7 | using System.Text.Json;
8 | using WinterspringLauncher.Utils;
9 |
10 | namespace WinterspringLauncher;
11 |
12 | public enum OperatingSystem
13 | {
14 | Windows,
15 | MacOs
16 | }
17 |
18 | public class VersionedBaseConfig
19 | {
20 | public int ConfigVersion { get; set; } = 3;
21 | }
22 |
23 | public class LauncherConfig : VersionedBaseConfig
24 | {
25 | private string _internalLastLoadedJsonString = string.Empty;
26 |
27 | public string LauncherLanguage { get; set; } = "en";
28 | public string? GitHubApiMirror { get; set; } = null; // example "http://asia.cdn.everlook.aclon.cn/github-mirror/api/" + "/repos/{repoName}/releases/latest"
29 | public string LastSelectedServerName { get; set; } = "";
30 | public bool CheckForLauncherUpdates { get; set; } = true;
31 | public bool CheckForHermesUpdates { get; set; } = true;
32 | public bool CheckForClientPatchUpdates { get; set; } = true;
33 | public bool CheckForClientBuildInfoUpdates { get; set; } = true;
34 |
35 | public ServerInfo[] KnownServers { get; set; } = new ServerInfo[]
36 | {
37 | new ServerInfo
38 | {
39 | Name = "Everlook (Europe)",
40 | RealmlistAddress = "logon.everlook.org",
41 | UsedInstallation = "Everlook EU 1.14.2 installation"
42 | },
43 | new ServerInfo
44 | {
45 | Name = "Everlook (Asia)",
46 | RealmlistAddress = "asia.everlook-wow.net",
47 | UsedInstallation = "Everlook Asia 1.14.2 installation",
48 | },
49 | new ServerInfo
50 | {
51 | Name = "Localhost (1.14.2)",
52 | RealmlistAddress = "127.0.0.1",
53 | UsedInstallation = "Default 1.14.2 installation",
54 | HermesSettings = new Dictionary
55 | {
56 | ["DebugOutput"] = "true",
57 | ["PacketsLog"] = "true",
58 | }
59 | },
60 | };
61 |
62 | public Dictionary GameInstallations { get; set; } = new Dictionary
63 | {
64 | ["Everlook EU 1.14.2 installation"] = new InstallationLocation
65 | {
66 | Directory = "./winterspring-data/WoW 1.14.2 Everlook",
67 | Version = "1.14.2.42597",
68 | ClientPatchInfoURL = "https://wow-patches.blu.wtf/patches/1.14.2.42597_summary.json",
69 | CustomBuildInfoURL = "https://eu.cdn.everlook.org/game-client-patch-cdn/everlook_eu_prod_1_14_2/latest-build-info",
70 | BaseClientDownloadURL = new Dictionary() {
71 | [OperatingSystem.Windows] = "https://download.wowdl.net/downloadFiles/Clients/WoW%20Classic%201.14.2.42597%20All%20Languages.rar",
72 | [OperatingSystem.MacOs] = "https://download.wowdl.net/downloadFiles/Clients/WoW_Classic_1.14.2.42597_macOS.zip",
73 | },
74 | },
75 | ["Everlook Asia 1.14.2 installation"] = new InstallationLocation
76 | {
77 | Directory = "./winterspring-data/WoW 1.14.2 Everlook Asia",
78 | Version = "1.14.2.42597",
79 | ClientPatchInfoURL = "https://wow-patches.blu.wtf/patches/1.14.2.42597_summary.json",
80 | CustomBuildInfoURL = "http://asia.cdn.everlook.aclon.cn/game-client-patch-cdn/everlook_asia_prod_1_14_2/latest-build-info",
81 | BaseClientDownloadURL = new Dictionary() {
82 | [OperatingSystem.Windows] = "http://asia.cdn.everlook.aclon.cn/game-client-patch-cdn/wow_classic_1_14_2_42597_all_languages.rar",
83 | [OperatingSystem.MacOs] = "http://asia.cdn.everlook.aclon.cn/game-client-patch-cdn/wow_classic_1_14_2_42597_all_languages_macos.rar",
84 | },
85 | },
86 | ["Default 1.14.2 installation"] = new InstallationLocation
87 | {
88 | Directory = "./winterspring-data/WoW 1.14.2",
89 | Version = "1.14.2.42597",
90 | ClientPatchInfoURL = "https://wow-patches.blu.wtf/patches/1.14.2.42597_summary.json",
91 | BaseClientDownloadURL = new Dictionary() {
92 | [OperatingSystem.Windows] = "https://download.wowdl.net/downloadFiles/Clients/WoW%20Classic%201.14.2.42597%20All%20Languages.rar",
93 | [OperatingSystem.MacOs] = "https://download.wowdl.net/downloadFiles/Clients/WoW_Classic_1.14.2.42597_macOS.zip",
94 | },
95 | }
96 | };
97 |
98 | public string HermesProxyLocation { get; set; } = "./winterspring-data/HermesProxy";
99 |
100 | public class ServerInfo
101 | {
102 | public string Name { get; set; }
103 | public string RealmlistAddress { get; set; }
104 | public string UsedInstallation { get; set; }
105 | //public bool? RequiresHermes { get; set; }
106 | public Dictionary? HermesSettings { get; set; }
107 | }
108 |
109 | public class InstallationLocation
110 | {
111 | public string Version { get; set; }
112 | public string Directory { get; set; }
113 | public string ClientPatchInfoURL { get; set; }
114 | public string? CustomBuildInfoURL { get; set; } // Optional
115 | public Dictionary BaseClientDownloadURL { get; set; }
116 | }
117 |
118 | public static LauncherConfig GetDefaultConfig() => new LauncherConfig();
119 |
120 | public void SaveConfig(string configPath)
121 | {
122 | var options = new JsonSerializerOptions { WriteIndented = true };
123 | string jsonString = JsonSerializer.Serialize(this, options);
124 | if (jsonString != _internalLastLoadedJsonString)
125 | {
126 | File.WriteAllText(configPath, jsonString, Encoding.UTF8);
127 | }
128 | }
129 |
130 | public static LauncherConfig LoadOrCreateDefault(string configPath)
131 | {
132 | LauncherConfig config;
133 | if (!File.Exists(configPath))
134 | {
135 | config = GetDefaultConfig();
136 | }
137 | else
138 | {
139 | string configTextContent = File.ReadAllText(configPath, Encoding.UTF8);
140 | string updatedConfig = PatchConfigIfNeeded(configTextContent);
141 | var loadedJson = JsonSerializer.Deserialize(updatedConfig);
142 | if (loadedJson != null)
143 | {
144 | config = loadedJson;
145 | config._internalLastLoadedJsonString = configTextContent;
146 | }
147 | else
148 | {
149 | Console.WriteLine("Config is null after loading? Replacing it with default one");
150 | config = GetDefaultConfig();
151 | }
152 | }
153 |
154 | config.SaveConfig(configPath);
155 |
156 | return config;
157 | }
158 |
159 | private static string PatchConfigIfNeeded(string currentConfig)
160 | {
161 | var configVersion = JsonSerializer.Deserialize(currentConfig);
162 | if (configVersion == null)
163 | {
164 | Console.WriteLine("Unable to determine config version");
165 | return currentConfig;
166 | }
167 |
168 | if (configVersion.ConfigVersion >= 3)
169 | return currentConfig; // already on latest version
170 |
171 | if (configVersion.ConfigVersion == 1)
172 | {
173 | var v1Config = JsonSerializer.Deserialize(currentConfig);
174 | if (v1Config == null)
175 | return currentConfig; // Error ?
176 |
177 | var newConfig = new LauncherConfig();
178 |
179 | // If a official everlook server is detected switch the installation directory, so the client does not need to redownload it
180 | if (v1Config.Realmlist.Contains("everlook-wow.net", StringComparison.InvariantCultureIgnoreCase))
181 | {
182 | var knownServer = newConfig.KnownServers.First(g => g.RealmlistAddress.Contains("everlook-wow", StringComparison.InvariantCultureIgnoreCase));
183 | var knownInstallation = newConfig.GameInstallations.First(g => g.Key == knownServer.UsedInstallation);
184 | newConfig.GitHubApiMirror = "http://asia.cdn.everlook.aclon.cn/github-mirror/api/";
185 | newConfig.LastSelectedServerName = knownServer.Name;
186 | TryUpgradeOldGameFolder(knownInstallation.Value.Directory, v1Config.GamePath);
187 | }
188 | else if (v1Config.Realmlist.Contains("everlook.org", StringComparison.InvariantCultureIgnoreCase))
189 | {
190 | var knownServer = newConfig.KnownServers.First(g => g.RealmlistAddress.Contains("everlook.org", StringComparison.InvariantCultureIgnoreCase));
191 | var knownInstallation = newConfig.GameInstallations.First(g => g.Key == knownServer.UsedInstallation);
192 | newConfig.LastSelectedServerName = knownServer.Name;
193 | TryUpgradeOldGameFolder(oldGameFolder: v1Config.GamePath, newGameFolder: knownInstallation.Value.Directory);
194 | }
195 |
196 | return JsonSerializer.Serialize(newConfig);
197 | }
198 |
199 | if (configVersion.ConfigVersion == 2)
200 | {
201 | var newConfig = JsonSerializer.Deserialize(currentConfig);
202 |
203 | if (newConfig.GitHubApiMirror == "http://asia.cdn.everlook-wow.net/github-mirror/api/")
204 | newConfig.GitHubApiMirror = "http://asia.cdn.everlook.aclon.cn/github-mirror/api/";
205 |
206 | newConfig.ConfigVersion = 3;
207 |
208 | return JsonSerializer.Serialize(newConfig);
209 | }
210 |
211 | Console.WriteLine("Unknown version");
212 | return currentConfig;
213 | }
214 |
215 | private static void TryUpgradeOldGameFolder(string oldGameFolder, string newGameFolder)
216 | {
217 | try
218 | {
219 | bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
220 | if (!weAreOnMacOs)
221 | {
222 | string known_1_14_2_client_hash = "43F407C7915602D195812620D68C3E5AE10F20740549D2D63A0B04658C02A123";
223 |
224 | var gameExecutablePath = Path.Combine(oldGameFolder, "_classic_era_", "WoWClassic.exe");
225 |
226 | if (File.Exists(gameExecutablePath) && HashHelper.CreateHexSha256HashFromFilename(gameExecutablePath) == known_1_14_2_client_hash)
227 | {
228 | // We can just move the whole folder
229 | Directory.Move(oldGameFolder, newGameFolder); // <-- might fail if target is not empty
230 | }
231 | else
232 | {
233 | // Just copy the WTF and Interface folder
234 |
235 | var oldInterfaceFolder = Path.Combine(oldGameFolder, "_classic_era_", "Interface");
236 | var newInterfaceFolder = Path.Combine(newGameFolder, "_classic_era_", "Interface");
237 | DirectoryCopy.Copy(oldInterfaceFolder, newInterfaceFolder);
238 |
239 | var oldWtfFolder = Path.Combine(oldGameFolder, "_classic_era_", "WTF");
240 | var newWtfFolder = Path.Combine(newGameFolder, "_classic_era_", "WTF");
241 | DirectoryCopy.Copy(oldWtfFolder, newWtfFolder);
242 | }
243 | }
244 | }
245 | catch (Exception e)
246 | {
247 | Console.WriteLine("Error while TryUpgradeOldGameFolder");
248 | Console.WriteLine(e);
249 | }
250 | }
251 |
252 | private class LegacyV1Config : VersionedBaseConfig
253 | {
254 | public string GitRepoWinterspringLauncher { get; set; }
255 | public string GitRepoHermesProxy { get; set; }
256 | public string GitRepoArctiumLauncher { get; set; }
257 |
258 | public string WindowsGameDownloadUrl { get; set; }
259 | public string MacGameDownloadUrl { get; set; }
260 | public string GamePatcherUrl { get; set; }
261 |
262 | public string HermesProxyPath { get; set; }
263 | public string GamePath { get; set; }
264 | public string ArctiumLauncherPath { get; set; }
265 | public bool RecreateDesktopShortcut { get; set; }
266 | public bool AutoUpdateThisLauncher { get; set; }
267 |
268 | public string Realmlist { get; set; }
269 | }
270 | }
271 |
272 |
--------------------------------------------------------------------------------
/WinterspringLauncher/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
52 |
53 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
75 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
142 |
143 |
144 | Log
145 |
146 |
147 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | (running)
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | (installed)
186 |
187 |
188 | (Will be downloaded/patched)
189 |
190 |
191 |
192 |
204 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
--------------------------------------------------------------------------------
/WinterspringLauncher/LauncherLogic.StartGame.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices;
7 | using System.Threading.Tasks;
8 | using Avalonia.Media;
9 | using WinterspringLauncher.Utils;
10 |
11 | namespace WinterspringLauncher;
12 |
13 | public partial class LauncherLogic
14 | {
15 | private Process? _hermesProcess;
16 |
17 | public void StartGame()
18 | {
19 | _model.InputIsAllowed = false;
20 | if (!_model.HermesIsRunning)
21 | {
22 | _model.AddLogEntry($"Launching...");
23 | }
24 |
25 | bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
26 |
27 | var serverInfo = _config.KnownServers.ElementAtOrDefault(_model.SelectedServerIdx);
28 | if (serverInfo == null)
29 | {
30 | _model.AddLogEntry("Error invalid server settings");
31 | _model.InputIsAllowed = true;
32 | return;
33 | }
34 |
35 | var gameInstallation = _config.GameInstallations.GetValueOrDefault(serverInfo.UsedInstallation);
36 | if (gameInstallation == null)
37 | {
38 | _model.AddLogEntry($"Error cant find '{serverInfo.UsedInstallation}' installation in settings");
39 | _model.InputIsAllowed = true;
40 | return;
41 | }
42 |
43 | if (!_model.HermesIsRunning)
44 | {
45 | _model.AddLogEntry($"---------- Selected Server Config ----------");
46 | _model.AddLogEntry($"Name: {serverInfo.Name}");
47 | _model.AddLogEntry($"Realmlist: {serverInfo.RealmlistAddress}");
48 | _model.AddLogEntry($"Game Directory: {gameInstallation.Directory}");
49 | _model.AddLogEntry($"Game Version: {gameInstallation.Version}");
50 | _model.AddLogEntry($"--------------------------------------------");
51 | }
52 |
53 | IBrush overallProgressColor = Brush.Parse("#4caf50");
54 | IBrush sideProgressColor = Brush.Parse("#553399");
55 | Task.Run(async () =>
56 | {
57 | if (_model.HermesIsRunning)
58 | {
59 | _model.AddLogEntry("Starting another game instance");
60 | _model.SetProgressbar("Starting Game", 90, overallProgressColor);
61 | string bnetPortStr = serverInfo.HermesSettings?.GetValueOrDefault("BNetPort") ?? "1119";
62 | LauncherActions.PrepareGameConfigWtf(gameInstallation.Directory, portalAddress: $"127.0.0.1:{bnetPortStr}");
63 |
64 | _model.SetProgressbar("Starting Game", 95, overallProgressColor);
65 | await Task.Delay(TimeSpan.FromSeconds(0.5));
66 | LauncherActions.StartGame(Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers));
67 | await Task.Delay(TimeSpan.FromSeconds(5));
68 | return;
69 | }
70 |
71 | _model.SetProgressbar("Checking WoW installation", 10, overallProgressColor);
72 | _model.AddLogEntry("Checking WoW installation");
73 | await Task.Delay(TimeSpan.FromSeconds(0.5));
74 |
75 | bool clientWasDownloadedInThisSession = false; // required to get at least the .build.info once, even if disabled
76 | var expectedPatchedClientLocation = Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers);
77 | if (!File.Exists(expectedPatchedClientLocation))
78 | {
79 | _model.AddLogEntry($"Patched client was NOT found at \"{expectedPatchedClientLocation}\"");
80 |
81 | // Checking default WoW installation
82 | var expectedDefaultClientLocation = Path.Combine(gameInstallation.Directory, SubPathToWowOriginal);
83 | if (!File.Exists(expectedDefaultClientLocation))
84 | {
85 | _model.AddLogEntry($"Default wow client was NOT found at \"{expectedDefaultClientLocation}\"");
86 |
87 | _model.AddLogEntry("Downloading WoW Client...");
88 |
89 | if (!gameInstallation.BaseClientDownloadURL.TryGetValue(weAreOnMacOs ? OperatingSystem.MacOs : OperatingSystem.Windows, out string? downloadUrl))
90 | {
91 | _model.AddLogEntry($"Cant find download url for \"{(weAreOnMacOs ? OperatingSystem.MacOs : OperatingSystem.Windows)}\"");
92 | return;
93 | }
94 |
95 | _model.AddLogEntry($"Download URL: {downloadUrl}");
96 | var targetDir = new DirectoryInfo(FullPath(gameInstallation.Directory)).FullName;
97 | if (!Directory.Exists(targetDir))
98 | Directory.CreateDirectory(targetDir);
99 |
100 | var downloadDestLocation = targetDir + ".partial-download";
101 | _model.AddLogEntry($"Download Location: {downloadDestLocation}");
102 |
103 | var exisingFile = new FileInfo(downloadDestLocation);
104 | if (exisingFile.Exists && exisingFile.Length > 2_000_000_000) // >2GB
105 | {
106 | await Task.Delay(TimeSpan.FromSeconds(0.5));
107 | _model.AddLogEntry("Detected downloaded file. Is it already downloaded?");
108 | await Task.Delay(TimeSpan.FromSeconds(5));
109 | _model.AddLogEntry("Skipping download");
110 | await Task.Delay(TimeSpan.FromSeconds(5));
111 | }
112 | else
113 | {
114 | _model.SetProgressbar("Downloading WoW", 0, Brush.Parse("#1976d2"));
115 | try
116 | {
117 | RunDownload(downloadUrl, downloadDestLocation);
118 | }
119 | catch when (false)
120 | {
121 | // TODO: Ask user for manual selecting a zip/rar file
122 | }
123 | }
124 |
125 | _model.AddLogEntry($"Unpack to: {targetDir}");
126 | _model.SetProgressbar("Unpack WoW", 0, Brush.Parse("#d84315"));
127 | RunUnpack(downloadDestLocation, targetDir);
128 |
129 | #if !DEBUG
130 | try
131 | {
132 | File.Delete(downloadDestLocation);
133 | }
134 | catch(Exception e)
135 | {
136 | _model.AddLogEntry($"Failed to delete tmp file '{downloadDestLocation}'");
137 | await Task.Delay(TimeSpan.FromSeconds(5));
138 | }
139 | #endif
140 | }
141 |
142 | try {
143 | if (!File.Exists(expectedPatchedClientLocation) || (_config.CheckForClientPatchUpdates))
144 | {
145 | _model.SetProgressbar("Checking WoW patch status", 30, overallProgressColor);
146 | await Task.Delay(TimeSpan.FromSeconds(0.5));
147 |
148 | string summaryUrl = gameInstallation.ClientPatchInfoURL;
149 |
150 | _model.AddLogEntry($"Summary URL: {summaryUrl}");
151 | var patchSummary = SimpleFileDownloader.PerformGetJsonRequest(summaryUrl);
152 |
153 | var selectedPatchInfo = weAreOnMacOs ? patchSummary.MacOs : patchSummary.Windows;
154 | if (selectedPatchInfo == null)
155 | throw new Exception($"No path for '{(weAreOnMacOs ? "macos" : "windows")}' was found");
156 |
157 | if (!File.Exists(expectedPatchedClientLocation) || selectedPatchInfo.ToSha256 != HashHelper.CreateHexSha256HashFromFilename(expectedDefaultClientLocation))
158 | {
159 | _model.AddLogEntry("Patched client update required");
160 | var patchUrl = string.Join("/", summaryUrl.Split("/").SkipLast(1)) + $"/{selectedPatchInfo.PatchFilename}";
161 | _model.AddLogEntry($"Patch URL: {patchUrl}");
162 | await Task.Delay(TimeSpan.FromSeconds(0.5));
163 |
164 | var patchFileContent = SimpleFileDownloader.PerformGetBytesRequest(patchUrl);
165 | BinaryPatchHandler.ApplyPatch(patchFileContent, sourceFile: expectedDefaultClientLocation, targetFile: expectedPatchedClientLocation);
166 | _model.AddLogEntry("Patch was applied!");
167 | await Task.Delay(TimeSpan.FromSeconds(0.5));
168 | }
169 | }
170 |
171 | clientWasDownloadedInThisSession = true;
172 | }
173 | catch (Exception e) when (File.Exists(expectedPatchedClientLocation))
174 | {
175 | _model.AddLogEntry("Failed to check for an update for the client");
176 | _model.AddLogEntry("But since the file exists this error can be ignored");
177 | await Task.Delay(TimeSpan.FromSeconds(0.5));
178 | _model.AddLogEntry(e.ToString());
179 | await Task.Delay(TimeSpan.FromSeconds(5));
180 | Console.WriteLine(e);
181 | }
182 |
183 | _model.GameIsInstalled = true;
184 | }
185 |
186 | bool buildInfoWasChanged = false;
187 | if (gameInstallation.CustomBuildInfoURL != null && (clientWasDownloadedInThisSession || _config.CheckForClientBuildInfoUpdates))
188 | {
189 | _model.SetProgressbar("Checking BuildInfo status", 35, overallProgressColor);
190 |
191 | string buildInfoFilePath = Path.Combine(gameInstallation.Directory, ".build.info");
192 |
193 | string newBuildInfo;
194 | try
195 | {
196 | newBuildInfo = SimpleFileDownloader.PerformGetStringRequest(gameInstallation.CustomBuildInfoURL);
197 | }
198 | catch
199 | {
200 | _model.AddLogEntry($"BuildInfo URL: {gameInstallation.CustomBuildInfoURL}");
201 | throw;
202 | }
203 |
204 | string existingBuildInfo = File.Exists(buildInfoFilePath) ? File.ReadAllText(buildInfoFilePath) : string.Empty;
205 |
206 | if (newBuildInfo.ReplaceLineEndings() != existingBuildInfo.ReplaceLineEndings())
207 | {
208 | _model.AddLogEntry("BuildInfo update detected");
209 | await Task.Delay(TimeSpan.FromSeconds(0.5));
210 | File.WriteAllText(buildInfoFilePath, newBuildInfo);
211 | buildInfoWasChanged = true;
212 | }
213 | }
214 |
215 | _model.AddLogEntry("Checking HermesProxy status");
216 | _model.SetProgressbar("Checking HermesProxy status", 50, overallProgressColor);
217 | await Task.Delay(TimeSpan.FromSeconds(0.5));
218 |
219 | await UpdateHermesProxyIfNecessary();
220 |
221 | var modernBuild = ushort.Parse(gameInstallation.Version.Split(".").Last());
222 |
223 | _model.AddLogEntry($"-----------------");
224 | _model.SetProgressbar("Starting HermesProxy", 75, overallProgressColor);
225 | await Task.Delay(TimeSpan.FromSeconds(0.5));
226 |
227 | _model.AddLogEntry($"ModernBuild: {modernBuild}");
228 |
229 | var hermesSettingsOverwrite = new Dictionary();
230 |
231 | var splittedRealmlist = serverInfo.RealmlistAddress.Split(':');
232 | hermesSettingsOverwrite.Add("ServerAddress", splittedRealmlist.First());
233 | if (splittedRealmlist.Length == 2)
234 | hermesSettingsOverwrite.Add("ServerPort", splittedRealmlist.Last());
235 |
236 | if (serverInfo.HermesSettings != null)
237 | {
238 | foreach (var customSettings in serverInfo.HermesSettings)
239 | hermesSettingsOverwrite.Add(customSettings.Key, customSettings.Value);
240 | }
241 |
242 | _hermesProcess = LauncherActions.StartHermesProxy(_config.HermesProxyLocation, modernBuild, hermesSettingsOverwrite, (logLine) => { _model.AddLogEntry(logLine); });
243 | _model.SetHermesPid(_hermesProcess.Id);
244 | _hermesProcess.Exited += (a, e) =>
245 | {
246 | _model.AddLogEntry($"HERMES PROXY HAS CLOSED! Status: {_hermesProcess.ExitCode}");
247 | _model.SetHermesPid(null);
248 | };
249 | await Task.Delay(TimeSpan.FromSeconds(1));
250 | if (_hermesProcess.HasExited)
251 | {
252 | _model.AddLogEntry($"HERMES PROXY HAS CLOSED PREMATURELY! Status: {_hermesProcess.ExitCode}");
253 | _model.SetHermesPid(null);
254 | }
255 |
256 | _model.SetProgressbar("Starting Game", 90, overallProgressColor);
257 | {
258 | string bnetPortStr = serverInfo.HermesSettings?.GetValueOrDefault("BNetPort") ?? "1119";
259 | LauncherActions.PrepareGameConfigWtf(gameInstallation.Directory, portalAddress: $"127.0.0.1:{bnetPortStr}");
260 | }
261 |
262 | if (buildInfoWasChanged)
263 | _model.SetProgressbar("Your game is updating please wait a bit (check Task Manager!)", 95, sideProgressColor);
264 | else
265 | _model.SetProgressbar("Starting Game", 95, overallProgressColor);
266 |
267 | LauncherActions.StartGame(Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers));
268 | await Task.Delay(TimeSpan.FromSeconds(5));
269 | }).ContinueWith((t) =>
270 | {
271 | if (t.Exception != null)
272 | {
273 | _model.AddLogEntry(t.Exception.ToString());
274 | }
275 | else if (t.IsCompletedSuccessfully)
276 | {
277 | _model.SetProgressbar("Done", 100, overallProgressColor);
278 | _model.InputIsAllowed = true;
279 | }
280 | });
281 | }
282 |
283 | private async Task UpdateHermesProxyIfNecessary()
284 | {
285 | string? localHermesVersion = null;
286 | var hermesProxyVersionFile = Path.Combine(_config.HermesProxyLocation, "version.txt");
287 | if (File.Exists(hermesProxyVersionFile))
288 | {
289 | localHermesVersion = File.ReadLines(hermesProxyVersionFile).First();
290 | }
291 |
292 | if (localHermesVersion == null || _config.CheckForHermesUpdates)
293 | {
294 | GitHubReleaseInfo? releaseInfo;
295 | try
296 | {
297 | releaseInfo = GitHubApi.LatestReleaseVersion("WowLegacyCore/HermesProxy");
298 | }
299 | catch (Exception e) when (localHermesVersion != null)
300 | {
301 | _model.AddLogEntry("Error: Failed to check HermesProxy version!");
302 | Console.WriteLine("Exception while checking GitHub status of HermesProxy");
303 | Console.WriteLine(e);
304 | await Task.Delay(TimeSpan.FromSeconds(5));
305 | return;
306 | }
307 |
308 | bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
309 |
310 | var versionString = $"{releaseInfo.TagName}|{releaseInfo.Name}";
311 | if (localHermesVersion != versionString)
312 | {
313 | var osName = weAreOnMacOs ? "mac" : "win";
314 | var possibleDownloads = releaseInfo.Assets!.FindAll(a => a.Name.Contains(osName, StringComparison.CurrentCultureIgnoreCase));
315 | if (possibleDownloads.Count != 1)
316 | throw new Exception($"Found {possibleDownloads.Count} HermesProxy versions for your OS");
317 |
318 | var targetDir = new DirectoryInfo(FullPath(_config.HermesProxyLocation)).FullName;
319 | if (!Directory.Exists(targetDir))
320 | Directory.CreateDirectory(targetDir);
321 |
322 | var downloadDestLocation = targetDir + ".partial-download";
323 |
324 | _model.SetProgressbar("Downloading HermesProxy", 0, Brush.Parse("#1976d2"));
325 | var downloadUrl = possibleDownloads[0].DownloadUrl;
326 | _model.AddLogEntry($"Download URL: {downloadUrl}");
327 | _model.AddLogEntry($"Download Location: {downloadDestLocation}");
328 | RunDownload(downloadUrl, downloadDestLocation);
329 |
330 | var directories = Directory.GetDirectories(targetDir);
331 | foreach (string directory in directories)
332 | {
333 | if (!directory.Contains("AccountData")) // we want to keep our AccountData
334 | Directory.Delete(directory, recursive: true);
335 | }
336 |
337 | Directory.CreateDirectory(targetDir);
338 |
339 | _model.SetProgressbar("Unpack HermesProxy", 0, Brush.Parse("#d84315"));
340 | RunUnpack(downloadDestLocation, targetDir);
341 |
342 | #if !DEBUG
343 | try
344 | {
345 | File.Delete(downloadDestLocation);
346 | }
347 | catch(Exception e)
348 | {
349 | _model.AddLogEntry($"Failed to delete tmp file '{downloadDestLocation}'");
350 | await Task.Delay(TimeSpan.FromSeconds(5));
351 | }
352 | #endif
353 |
354 | File.WriteAllLines(hermesProxyVersionFile, new string[]
355 | {
356 | versionString,
357 | $"Source: {downloadUrl}"
358 | });
359 |
360 | _model.SetHermesVersion(releaseInfo.TagName);
361 | }
362 | }
363 | }
364 | }
365 |
--------------------------------------------------------------------------------