Repository: 0blu/WinterspringLauncher
Branch: stable
Commit: 4edfb9d3991d
Files: 47
Total size: 124.7 KB
Directory structure:
gitextract_9npmrwx3/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── bug_report.md
│ └── workflows/
│ └── Build_Launcher.yml
├── .gitignore
├── GitVersion.yml
├── LICENSE
├── MacAppBuilding/
│ ├── .gitignore
│ ├── AppTemplate/
│ │ ├── AppIcon.icns
│ │ ├── Resources/
│ │ │ └── Info.plist
│ │ ├── WinterspringLauncherTerminal
│ │ └── launch_wrapper
│ ├── build_app.sh
│ └── build_dmg.sh
├── README.md
├── WinterspringLauncher/
│ ├── App.axaml
│ ├── App.axaml.cs
│ ├── Assets/
│ │ ├── Resources.axaml
│ │ ├── icons/
│ │ │ └── language-icons/
│ │ │ └── source.txt
│ │ └── translations/
│ │ └── en.json
│ ├── LauncherActions.cs
│ ├── LauncherConfig.cs
│ ├── LauncherLogic.OpenGameFolder.cs
│ ├── LauncherLogic.StartGame.cs
│ ├── LauncherLogic.cs
│ ├── LauncherUpdateHandler.cs
│ ├── LauncherVersion.cs
│ ├── LocaleDefaults.cs
│ ├── ProgramStartup.cs
│ ├── UiElements/
│ │ └── HyperlinkSpan.cs
│ ├── Utils/
│ │ ├── ArchiveCompression.cs
│ │ ├── BinaryPatchHandler.cs
│ │ ├── DirectoryCopy.cs
│ │ ├── GitHubApi.cs
│ │ ├── HashHelper.cs
│ │ ├── ProgressiveFileDownloader.cs
│ │ ├── SimpleFileDownloader.cs
│ │ ├── UnixApi.cs
│ │ └── UtilHelper.cs
│ ├── ViewModels/
│ │ └── MainWindowViewModel.cs
│ ├── Views/
│ │ ├── MainWindow.axaml
│ │ ├── MainWindow.axaml.cs
│ │ ├── NewVersionAvailableDialog.axaml
│ │ └── NewVersionAvailableDialog.axaml.cs
│ ├── WinterspringLauncher.csproj
│ └── app.manifest
├── WinterspringLauncher.sln
├── WinterspringLauncher.sln.DotSettings
└── global.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Launcher bug report
title: ''
labels: ''
assignees: ''
---
Wait a moment,
are you sure you want to create an issue with **this** launcher?
If you have gameplay issues start a new BugReport at HermesProxy: https://github.com/WowLegacyCore/HermesProxy
================================================
FILE: .github/workflows/Build_Launcher.yml
================================================
name: Build Launcher
on: ['push']
env:
DOTNET_VERSION: '7.0.x'
jobs:
build_windows:
strategy:
matrix:
os: ['windows']
runs-on: ${{ matrix.os }}-latest
steps:
- name: Checkout repository content
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install dependencies
run: dotnet restore
- name: Publish
run: dotnet publish --configuration Release --use-current-runtime -p:UsePublishBuildSettings=true
- name: Copy files
run: cp -r ./WinterspringLauncher/bin/Release/*/publish/ publish
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: WinterspringLauncher-${{ matrix.os }}-${{ runner.arch }}-${{ github.sha }}
path: publish
if-no-files-found: error
build_macos:
strategy:
matrix:
os: ['macos']
runs-on: ${{ matrix.os }}-latest
steps:
- name: Checkout repository content
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install dependencies
run: dotnet restore
- name: Publish
run: dotnet publish --configuration Release --runtime osx-arm64 -p:UsePublishBuildSettings=true
- name: Determinante tag
run: echo "GIT_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Create .app
working-directory: MacAppBuilding
run: ./build_app.sh "$GIT_TAG" ../WinterspringLauncher/bin/Release/*/publish/WinterspringLauncher
- name: Create .app zip
working-directory: MacAppBuilding
run: |
cd output
zip -vr ../../WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}-APP.zip *
- name: Upload .app
uses: actions/upload-artifact@v3
with:
name: WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}.app
path: WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}-APP.zip
if-no-files-found: error
- name: Install create-dmg
run: brew install create-dmg
- name: Create .dmg
working-directory: MacAppBuilding
run: ./build_dmg.sh
- name: Upload .dmg
uses: actions/upload-artifact@v3
with:
name: WinterspringLauncher-${{ matrix.os }}-arm64-${{ github.sha }}.dmg
path: MacAppBuilding/output_dmg/
if-no-files-found: error
================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,dotnetcore
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,rider,dotnetcore
### DotnetCore ###
# .NET Core build folders
bin/
obj/
# Common node modules locations
/node_modules
/wwwroot/node_modules
### Rider ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
*.code-workspace
# Local History for Visual Studio Code
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### VisualStudio Patch ###
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,dotnetcore
.idea/
.DS_Store
================================================
FILE: GitVersion.yml
================================================
branches:
main:
regex: ^stable$
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 _BLU (https://github.com/0blu)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MacAppBuilding/.gitignore
================================================
output/
output_dmg/
================================================
FILE: MacAppBuilding/AppTemplate/Resources/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>launch_wrapper</string>
<key>CFBundleGetInfoString</key>
<string>WinterspringLauncher {{VERSION}}</string>
<key>CFBundleVersion</key>
<string>{{VERSION}}</string>
<key>CFBundleShortVersionString</key>
<string>{{VERSION}}</string>
<key>CFBundleIconFile</key>
<string>AppIcon.icns</string>
</dict>
</plist>
================================================
FILE: MacAppBuilding/AppTemplate/WinterspringLauncherTerminal
================================================
#!/bin/sh
CURRENTPATH=`dirname "${0}"`
# Resize terminal
printf '\e[8;27;110t'
clear
cd "$CURRENTPATH"
DYLD_LIBRARY_PATH="$CURRENTPATH/Libs" ./WinterspringLauncher
================================================
FILE: MacAppBuilding/AppTemplate/launch_wrapper
================================================
#!/bin/sh
CURRENTPATH=`dirname "${0}"`
open "$CURRENTPATH/WinterspringLauncherTerminal"
================================================
FILE: MacAppBuilding/build_app.sh
================================================
#!/bin/sh
set -e
if [ $# -ne 2 ]; then
echo "Error: Invalid arguments"
echo "Run: buid_app.sh <version> <WinterspringLauncherBinary>"
exit 1
fi
if [ ! -d "AppTemplate" ]; then
echo "Error: AppTemplate folder is not in cwd"
exit 1
fi
VERSION="$1"
EXE_FILE="$2"
if [ ! -f "$EXE_FILE" ]; then
echo "Error: '$EXE_FILE' is not a valid file"
exit 1
fi
OPENSSL_DL="https://github.com/0blu/prebuilt-openssl3-for-macos/releases/download/openssl-3.0.7/openssl-3.0.7.zip"
APP_PATH="output/Winterspring Launcher.app"
echo "Building for:"
echo "- Version: $VERSION"
echo "- Binary: $EXE_FILE"
echo "- Result: $APP_PATH"
echo "Deleting existing app"
rm -rf "$APP_PATH"
mkdir -p "$APP_PATH"
echo "Copy template"
cp -r AppTemplate/* "$APP_PATH/."
echo "Download openssl3"
mkdir "$APP_PATH/Libs"
curl --fail -SL "$OPENSSL_DL" -o "$APP_PATH/Libs/openssl3.zip"
(cd "$APP_PATH/Libs/" \
&& unzip ./openssl3.zip \
&& mv openssl-3.*/*.3.dylib . \
&& rm openssl3.zip \
&& rm -rf openssl-3.* \
)
echo "Copying launcher executable"
cp "$EXE_FILE" "$APP_PATH"
echo "Replace version in info.plist"
sed -i.bak "s/{{VERSION}}/$VERSION/g" "$APP_PATH/Resources/info.plist"
echo "Making everthing executable"
chmod -R a+x "$APP_PATH"
echo "Done building '$APP_PATH'"
================================================
FILE: MacAppBuilding/build_dmg.sh
================================================
#!/bin/sh
set -e
if [ ! -d "output" ]; then
echo "Error: no 'output' directory"
exit 1
fi
sips --setProperty dpiWidth 144 --setProperty dpiHeight 144 dmg_backgroung.png
rm -rf output_dmg
mkdir output_dmg
create-dmg \
--volname "Winterspring Launcher Installer" \
--background dmg_background.png \
--window-size 525 310 \
--icon-size 90 \
--icon "Winterspring Launcher.app" 0 120 \
--hide-extension "Winterspring Launcher.app" \
--app-drop-link 280 120 \
output_dmg/WinterspringLauncher.dmg output
================================================
FILE: README.md
================================================
<figure>
<img align="right" src="./winterspring-launcher-icon.png" alt="icon">
</figure>
# Winterspring Launcher
Allows you to play on [Everlook.org](https://everlook.org/) with modern 1.14 WoW Client!
_This is not an official project from the Everlook team._
_Do not ask the Everlook team for support_
_(you can still ask in [#addons-and-ui](https://discord.com/channels/973529971740008448/983067524797177996) on Discord)_
## Easy setup
The launcher will do everything for you.
It will download the 1.14 client, setup HermesProxy and launch the game.
When HermesProxy has an update it is automatically applied.
### Windows <img src="https://blu.wtf/icon_windows.png" alt="windows" width="24" height="24">
1. **Download [the latest .exe release](https://github.com/0blu/EverlookClassicLauncher/releases/latest)**
2. Place it in a separate directory (On first launch it will create subfolders and a desktop icon)
3. Run it
4. **Enjoy your stay on Everlook**
### MacOS <img src="https://blu.wtf/icon_macos.png?0" alt="macos" width="24" height="24">
1. **Download [the latest .dmg release](https://github.com/0blu/EverlookClassicLauncher/releases/latest)**
2. Click on the downloaded .dmg file to open the installer
3. Drag the Launcher into your Applications folder
4. Go into your Applications folder, right click the Launcher and **click "Open"**
5. **Enjoy your stay on Everlook**
_On MacOS the client will be stored in `/Users/<your name>/WinterspringLauncher/`_
## Addons
Most addons for 1.14.0 should work.
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))
Here are some working recommendations:
(click the [[dl](#)] link to get a working zip)
### Questing
- [[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.
- [[dl](https://www.curseforge.com/wow/addons/guidelime/download/4026001)] [Guidelime (base)](https://www.curseforge.com/wow/addons/guidelime)
- [[dl](https://www.curseforge.com/wow/addons/guidelime_sage/download/3810259)] [Guidelime: Sage Guide](https://www.curseforge.com/wow/addons/guidelime_sage) (Alliance)
- [[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)
### Interface
- [[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)
- [[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)
(Feel free to give me new recommendations to extend the list)
## Why?
The modern WoW-Classic client provides many improvements in hardware compatibility and accessibility.
With Everlook we have the unique chance to improve HermesProxy with a large testing audience.
The launcher will make the classic client accessible to everyone.
So if you find any bugs [please report them](https://github.com/WowLegacyCore/HermesProxy/issues/new/choose).
(If you find any bugs associated to the launcher report them [here](https://github.com/0blu/WinterspringLauncher/issues))
## Is this allowed?
Currently HermesProxy is tolerated on Everlook.
⚠️You **will** get suspended if you exploit any game breaking bugs.
So far no-one has been banned for using official HermesProxy.
Just use common sense and <u>**play fair**</u>!
# Many thanks to
- [HermesProxy](https://github.com/WowLegacyCore/HermesProxy) to translate legacy traffic to modern one
- [Arctium WoW-Launcher](https://github.com/Arctium/WoW-Launcher) to patch client for custom server connections
================================================
FILE: WinterspringLauncher/App.axaml
================================================
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WinterspringLauncher.App"
RequestedThemeVariant="Dark">
<Application.Styles>
<SimpleTheme />
<!--<FluentTheme />-->
</Application.Styles>
<Application.Resources>
<FontFamily x:Key="MonoFont">avares://WinterspringLauncher/Assets/fonts/RobotoMono-Regular.ttf#</FontFamily>
<!--<FontFamily x:Key="MonoFont">Assets\\fonts\\RobotoMono-Regular.ttf</FontFamily>-->
<SolidColorBrush x:Key="TerminalBackground">#111111</SolidColorBrush>
</Application.Resources>
</Application>
================================================
FILE: WinterspringLauncher/App.axaml.cs
================================================
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using WinterspringLauncher.ViewModels;
using WinterspringLauncher.Views;
namespace WinterspringLauncher;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel() // <-- Will also initialize LauncherLogic
};
}
base.OnFrameworkInitializationCompleted();
}
}
================================================
FILE: WinterspringLauncher/Assets/Resources.axaml
================================================
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
</ResourceDictionary>
================================================
FILE: WinterspringLauncher/Assets/icons/language-icons/source.txt
================================================
https://www.flaticon.com/packs/countrys-flags
================================================
FILE: WinterspringLauncher/Assets/translations/en.json
================================================
{
"start_game": "Start Game"
}
================================================
FILE: WinterspringLauncher/LauncherActions.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using WinterspringLauncher.Utils;
namespace WinterspringLauncher;
public static class LauncherActions
{
public delegate void DownloadProgressInfoHandler(long? totalBytes, long alreadyDownloadedBytes, long bytesPerSec);
public delegate void UnpackProgressInfoHandler(long totalFileCount, long alreadyUnpackedFileCount);
public static void DownloadFile(string downloadUrl, string downloadDestLocation, DownloadProgressInfoHandler progressInfoHandler)
{
using (var client = new ProgressiveFileDownloader(downloadUrl, downloadDestLocation))
{
client.ProgressChangedFixedDelay += (totalBytes, alreadyDownloadedBytes, bytePerSec) =>
{
progressInfoHandler(totalBytes, alreadyDownloadedBytes, bytePerSec);
};
client.DownloadDone += (downloadedBytes) => {
progressInfoHandler(downloadedBytes, downloadedBytes, 0);
};
client.StartGetDownload().Wait();
}
}
public static void PrepareGameConfigWtf(string gamePath, string portalAddress)
{
var configWtfPath = Path.Combine(gamePath, "_classic_era_", "WTF", "Config.wtf");
var dirName = Path.GetDirectoryName(configWtfPath);
Directory.CreateDirectory(dirName!);
List<string> configContent;
if (!File.Exists(configWtfPath))
{
configContent = new List<string>();
string bestDefaultTextLocale = LocaleDefaults.GetBestWoWConfigLocale();
configContent.Add($"SET textLocale {bestDefaultTextLocale}");
}
else
{
configContent = File.ReadAllLines(configWtfPath).ToList();
}
var newLine = $"SET portal \"{portalAddress}\"";
bool wasChanged = false;
// Add SET PORTAL ...
var currentPortalLine = configContent.FindIndex(l => l.StartsWith("SET portal "));
if (currentPortalLine != -1)
{
if (configContent[currentPortalLine] != newLine)
{
configContent[currentPortalLine] = newLine;
wasChanged = true;
}
}
else
{
configContent.Add(newLine);
wasChanged = true;
}
// Remove disableServerNagle
var disableServerNagleLine = configContent.FindIndex(l => l.StartsWith("SET disableServerNagle "));
if (disableServerNagleLine != -1)
{
configContent.RemoveAt(disableServerNagleLine);
wasChanged = true;
}
if (wasChanged)
{
File.WriteAllLines(configWtfPath, configContent, Encoding.UTF8);
}
}
public static void Unpack(string compressedArchivePath, string targetDir, UnpackProgressInfoHandler progressInfoHandler)
{
ArchiveCompression.Decompress(compressedArchivePath, targetDir, folderToSkipName: "World of Warcraft", // TODO: <-- move this check somewhere else
(totalFileCount, alreadyUnpackedFileCount) =>
{
progressInfoHandler(totalFileCount, alreadyUnpackedFileCount);
}, shouldBeDecompressedPredicate: (filePath) => !filePath.Contains("World of Warcraft Launcher.exe")); // TODO: <-- move this check somewhere else
}
public delegate void OnLogLine(string logLine);
public static Process StartHermesProxy(string hermesDir, ushort modernClientBuild, Dictionary<string, string> settingsOverwrite, OnLogLine logLine)
{
bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
var executableName = weAreOnMacOs
? "HermesProxy"
: "HermesProxy.exe";
var executablePath = Path.Combine(hermesDir, executableName);
var procInfo = new ProcessStartInfo
{
FileName = executablePath,
WorkingDirectory = hermesDir,
RedirectStandardOutput = true,
CreateNoWindow = true,
ArgumentList = {
"--no-version-check",
"--set", $"ClientBuild={modernClientBuild}",
},
};
foreach (var (key, value) in settingsOverwrite)
{
procInfo.ArgumentList.Add("--set");
procInfo.ArgumentList.Add($"{key}={value}");
}
Console.WriteLine("Starting HermesProxy with arguments: ");
for (var i = 0; i < procInfo.ArgumentList.Count; i++)
Console.WriteLine($"[{i}] {procInfo.ArgumentList[i]}");
var process = Process.Start(procInfo)!;
process.EnableRaisingEvents = true;
process.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
{
if (!String.IsNullOrEmpty(e.Data))
{
logLine(e.Data);
}
});
process.BeginOutputReadLine();
return process;
}
public static void StartGame(string executablePath)
{
bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
ProcessStartInfo startInfo;
if (weAreOnMacOs)
{
startInfo = new ProcessStartInfo
{
// We are here "_classic_era_/WoW For Custom Servers.app/Contents/MacOS/WoW For Custom Servers"
// and want to be here "_classic_era_"
FileName = "/usr/bin/open",
ArgumentList = { "--new", "--wait-apps", $"./{Path.GetDirectoryName(Path.Combine(executablePath, "..", ".."))}" },
WorkingDirectory = Path.GetDirectoryName(Path.Combine(executablePath, "..", "..", "..")),
UseShellExecute = true,
CreateNoWindow = false,
RedirectStandardError = false,
RedirectStandardInput = false,
RedirectStandardOutput = false,
};
}
else
{
startInfo = new ProcessStartInfo
{
FileName = Path.GetFileName(executablePath),
WorkingDirectory = Path.GetDirectoryName(executablePath),
UseShellExecute = true,
CreateNoWindow = false,
RedirectStandardError = false,
RedirectStandardInput = false,
RedirectStandardOutput = false,
};
}
//startInfo.EnvironmentVariables.Clear();
var process = Process.Start(startInfo)!;
while (!process.HasExited && process.VirtualMemorySize64 < 100 * 1024 * 1024)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
}
}
}
================================================
FILE: WinterspringLauncher/LauncherConfig.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using WinterspringLauncher.Utils;
namespace WinterspringLauncher;
public enum OperatingSystem
{
Windows,
MacOs
}
public class VersionedBaseConfig
{
public int ConfigVersion { get; set; } = 3;
}
public class LauncherConfig : VersionedBaseConfig
{
private string _internalLastLoadedJsonString = string.Empty;
public string LauncherLanguage { get; set; } = "en";
public string? GitHubApiMirror { get; set; } = null; // example "http://asia.cdn.everlook.aclon.cn/github-mirror/api/" + "/repos/{repoName}/releases/latest"
public string LastSelectedServerName { get; set; } = "";
public bool CheckForLauncherUpdates { get; set; } = true;
public bool CheckForHermesUpdates { get; set; } = true;
public bool CheckForClientPatchUpdates { get; set; } = true;
public bool CheckForClientBuildInfoUpdates { get; set; } = true;
public ServerInfo[] KnownServers { get; set; } = new ServerInfo[]
{
new ServerInfo
{
Name = "Everlook (Europe)",
RealmlistAddress = "logon.everlook.org",
UsedInstallation = "Everlook EU 1.14.2 installation"
},
new ServerInfo
{
Name = "Everlook (Asia)",
RealmlistAddress = "asia.everlook-wow.net",
UsedInstallation = "Everlook Asia 1.14.2 installation",
},
new ServerInfo
{
Name = "Localhost (1.14.2)",
RealmlistAddress = "127.0.0.1",
UsedInstallation = "Default 1.14.2 installation",
HermesSettings = new Dictionary<string, string>
{
["DebugOutput"] = "true",
["PacketsLog"] = "true",
}
},
};
public Dictionary<string, InstallationLocation> GameInstallations { get; set; } = new Dictionary<string, InstallationLocation>
{
["Everlook EU 1.14.2 installation"] = new InstallationLocation
{
Directory = "./winterspring-data/WoW 1.14.2 Everlook",
Version = "1.14.2.42597",
ClientPatchInfoURL = "https://wow-patches.blu.wtf/patches/1.14.2.42597_summary.json",
CustomBuildInfoURL = "https://eu.cdn.everlook.org/game-client-patch-cdn/everlook_eu_prod_1_14_2/latest-build-info",
BaseClientDownloadURL = new Dictionary<OperatingSystem, string>() {
[OperatingSystem.Windows] = "https://download.wowdl.net/downloadFiles/Clients/WoW%20Classic%201.14.2.42597%20All%20Languages.rar",
[OperatingSystem.MacOs] = "https://download.wowdl.net/downloadFiles/Clients/WoW_Classic_1.14.2.42597_macOS.zip",
},
},
["Everlook Asia 1.14.2 installation"] = new InstallationLocation
{
Directory = "./winterspring-data/WoW 1.14.2 Everlook Asia",
Version = "1.14.2.42597",
ClientPatchInfoURL = "https://wow-patches.blu.wtf/patches/1.14.2.42597_summary.json",
CustomBuildInfoURL = "http://asia.cdn.everlook.aclon.cn/game-client-patch-cdn/everlook_asia_prod_1_14_2/latest-build-info",
BaseClientDownloadURL = new Dictionary<OperatingSystem, string>() {
[OperatingSystem.Windows] = "http://asia.cdn.everlook.aclon.cn/game-client-patch-cdn/wow_classic_1_14_2_42597_all_languages.rar",
[OperatingSystem.MacOs] = "http://asia.cdn.everlook.aclon.cn/game-client-patch-cdn/wow_classic_1_14_2_42597_all_languages_macos.rar",
},
},
["Default 1.14.2 installation"] = new InstallationLocation
{
Directory = "./winterspring-data/WoW 1.14.2",
Version = "1.14.2.42597",
ClientPatchInfoURL = "https://wow-patches.blu.wtf/patches/1.14.2.42597_summary.json",
BaseClientDownloadURL = new Dictionary<OperatingSystem, string>() {
[OperatingSystem.Windows] = "https://download.wowdl.net/downloadFiles/Clients/WoW%20Classic%201.14.2.42597%20All%20Languages.rar",
[OperatingSystem.MacOs] = "https://download.wowdl.net/downloadFiles/Clients/WoW_Classic_1.14.2.42597_macOS.zip",
},
}
};
public string HermesProxyLocation { get; set; } = "./winterspring-data/HermesProxy";
public class ServerInfo
{
public string Name { get; set; }
public string RealmlistAddress { get; set; }
public string UsedInstallation { get; set; }
//public bool? RequiresHermes { get; set; }
public Dictionary<string, string>? HermesSettings { get; set; }
}
public class InstallationLocation
{
public string Version { get; set; }
public string Directory { get; set; }
public string ClientPatchInfoURL { get; set; }
public string? CustomBuildInfoURL { get; set; } // Optional
public Dictionary<OperatingSystem, string> BaseClientDownloadURL { get; set; }
}
public static LauncherConfig GetDefaultConfig() => new LauncherConfig();
public void SaveConfig(string configPath)
{
var options = new JsonSerializerOptions { WriteIndented = true };
string jsonString = JsonSerializer.Serialize(this, options);
if (jsonString != _internalLastLoadedJsonString)
{
File.WriteAllText(configPath, jsonString, Encoding.UTF8);
}
}
public static LauncherConfig LoadOrCreateDefault(string configPath)
{
LauncherConfig config;
if (!File.Exists(configPath))
{
config = GetDefaultConfig();
}
else
{
string configTextContent = File.ReadAllText(configPath, Encoding.UTF8);
string updatedConfig = PatchConfigIfNeeded(configTextContent);
var loadedJson = JsonSerializer.Deserialize<LauncherConfig>(updatedConfig);
if (loadedJson != null)
{
config = loadedJson;
config._internalLastLoadedJsonString = configTextContent;
}
else
{
Console.WriteLine("Config is null after loading? Replacing it with default one");
config = GetDefaultConfig();
}
}
config.SaveConfig(configPath);
return config;
}
private static string PatchConfigIfNeeded(string currentConfig)
{
var configVersion = JsonSerializer.Deserialize<VersionedBaseConfig>(currentConfig);
if (configVersion == null)
{
Console.WriteLine("Unable to determine config version");
return currentConfig;
}
if (configVersion.ConfigVersion >= 3)
return currentConfig; // already on latest version
if (configVersion.ConfigVersion == 1)
{
var v1Config = JsonSerializer.Deserialize<LegacyV1Config>(currentConfig);
if (v1Config == null)
return currentConfig; // Error ?
var newConfig = new LauncherConfig();
// If a official everlook server is detected switch the installation directory, so the client does not need to redownload it
if (v1Config.Realmlist.Contains("everlook-wow.net", StringComparison.InvariantCultureIgnoreCase))
{
var knownServer = newConfig.KnownServers.First(g => g.RealmlistAddress.Contains("everlook-wow", StringComparison.InvariantCultureIgnoreCase));
var knownInstallation = newConfig.GameInstallations.First(g => g.Key == knownServer.UsedInstallation);
newConfig.GitHubApiMirror = "http://asia.cdn.everlook.aclon.cn/github-mirror/api/";
newConfig.LastSelectedServerName = knownServer.Name;
TryUpgradeOldGameFolder(knownInstallation.Value.Directory, v1Config.GamePath);
}
else if (v1Config.Realmlist.Contains("everlook.org", StringComparison.InvariantCultureIgnoreCase))
{
var knownServer = newConfig.KnownServers.First(g => g.RealmlistAddress.Contains("everlook.org", StringComparison.InvariantCultureIgnoreCase));
var knownInstallation = newConfig.GameInstallations.First(g => g.Key == knownServer.UsedInstallation);
newConfig.LastSelectedServerName = knownServer.Name;
TryUpgradeOldGameFolder(oldGameFolder: v1Config.GamePath, newGameFolder: knownInstallation.Value.Directory);
}
return JsonSerializer.Serialize(newConfig);
}
if (configVersion.ConfigVersion == 2)
{
var newConfig = JsonSerializer.Deserialize<LauncherConfig>(currentConfig);
if (newConfig.GitHubApiMirror == "http://asia.cdn.everlook-wow.net/github-mirror/api/")
newConfig.GitHubApiMirror = "http://asia.cdn.everlook.aclon.cn/github-mirror/api/";
newConfig.ConfigVersion = 3;
return JsonSerializer.Serialize(newConfig);
}
Console.WriteLine("Unknown version");
return currentConfig;
}
private static void TryUpgradeOldGameFolder(string oldGameFolder, string newGameFolder)
{
try
{
bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
if (!weAreOnMacOs)
{
string known_1_14_2_client_hash = "43F407C7915602D195812620D68C3E5AE10F20740549D2D63A0B04658C02A123";
var gameExecutablePath = Path.Combine(oldGameFolder, "_classic_era_", "WoWClassic.exe");
if (File.Exists(gameExecutablePath) && HashHelper.CreateHexSha256HashFromFilename(gameExecutablePath) == known_1_14_2_client_hash)
{
// We can just move the whole folder
Directory.Move(oldGameFolder, newGameFolder); // <-- might fail if target is not empty
}
else
{
// Just copy the WTF and Interface folder
var oldInterfaceFolder = Path.Combine(oldGameFolder, "_classic_era_", "Interface");
var newInterfaceFolder = Path.Combine(newGameFolder, "_classic_era_", "Interface");
DirectoryCopy.Copy(oldInterfaceFolder, newInterfaceFolder);
var oldWtfFolder = Path.Combine(oldGameFolder, "_classic_era_", "WTF");
var newWtfFolder = Path.Combine(newGameFolder, "_classic_era_", "WTF");
DirectoryCopy.Copy(oldWtfFolder, newWtfFolder);
}
}
}
catch (Exception e)
{
Console.WriteLine("Error while TryUpgradeOldGameFolder");
Console.WriteLine(e);
}
}
private class LegacyV1Config : VersionedBaseConfig
{
public string GitRepoWinterspringLauncher { get; set; }
public string GitRepoHermesProxy { get; set; }
public string GitRepoArctiumLauncher { get; set; }
public string WindowsGameDownloadUrl { get; set; }
public string MacGameDownloadUrl { get; set; }
public string GamePatcherUrl { get; set; }
public string HermesProxyPath { get; set; }
public string GamePath { get; set; }
public string ArctiumLauncherPath { get; set; }
public bool RecreateDesktopShortcut { get; set; }
public bool AutoUpdateThisLauncher { get; set; }
public string Realmlist { get; set; }
}
}
================================================
FILE: WinterspringLauncher/LauncherLogic.OpenGameFolder.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace WinterspringLauncher;
public partial class LauncherLogic
{
public void OpenGameFolder()
{
bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
var serverInfo = _config.KnownServers.ElementAtOrDefault(_model.SelectedServerIdx);
if (serverInfo == null)
{
_model.AddLogEntry("Error invalid server settings");
_model.InputIsAllowed = true;
return;
}
var gameInstallation = _config.GameInstallations.GetValueOrDefault(serverInfo.UsedInstallation);
if (gameInstallation == null)
{
_model.AddLogEntry($"Error cant find '{serverInfo.UsedInstallation}' installation in settings");
_model.InputIsAllowed = true;
return;
}
var absPath = Path.GetFullPath(gameInstallation.Directory);
if (!Directory.Exists(absPath))
{
_model.AddLogEntry("Game folder does not exists");
_model.AddLogEntry($"Expected path: {absPath}");
return;
}
_model.AddLogEntry("Opening game folder");
try
{
if (weAreOnMacOs)
Process.Start("open", $"-R \"{absPath}\"");
else
Process.Start("explorer.exe", absPath);
}
catch (Exception e)
{
_model.AddLogEntry($"An error occured while opening game folder");
Console.WriteLine(e);
}
}
}
================================================
FILE: WinterspringLauncher/LauncherLogic.StartGame.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Media;
using WinterspringLauncher.Utils;
namespace WinterspringLauncher;
public partial class LauncherLogic
{
private Process? _hermesProcess;
public void StartGame()
{
_model.InputIsAllowed = false;
if (!_model.HermesIsRunning)
{
_model.AddLogEntry($"Launching...");
}
bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
var serverInfo = _config.KnownServers.ElementAtOrDefault(_model.SelectedServerIdx);
if (serverInfo == null)
{
_model.AddLogEntry("Error invalid server settings");
_model.InputIsAllowed = true;
return;
}
var gameInstallation = _config.GameInstallations.GetValueOrDefault(serverInfo.UsedInstallation);
if (gameInstallation == null)
{
_model.AddLogEntry($"Error cant find '{serverInfo.UsedInstallation}' installation in settings");
_model.InputIsAllowed = true;
return;
}
if (!_model.HermesIsRunning)
{
_model.AddLogEntry($"---------- Selected Server Config ----------");
_model.AddLogEntry($"Name: {serverInfo.Name}");
_model.AddLogEntry($"Realmlist: {serverInfo.RealmlistAddress}");
_model.AddLogEntry($"Game Directory: {gameInstallation.Directory}");
_model.AddLogEntry($"Game Version: {gameInstallation.Version}");
_model.AddLogEntry($"--------------------------------------------");
}
IBrush overallProgressColor = Brush.Parse("#4caf50");
IBrush sideProgressColor = Brush.Parse("#553399");
Task.Run(async () =>
{
if (_model.HermesIsRunning)
{
_model.AddLogEntry("Starting another game instance");
_model.SetProgressbar("Starting Game", 90, overallProgressColor);
string bnetPortStr = serverInfo.HermesSettings?.GetValueOrDefault("BNetPort") ?? "1119";
LauncherActions.PrepareGameConfigWtf(gameInstallation.Directory, portalAddress: $"127.0.0.1:{bnetPortStr}");
_model.SetProgressbar("Starting Game", 95, overallProgressColor);
await Task.Delay(TimeSpan.FromSeconds(0.5));
LauncherActions.StartGame(Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers));
await Task.Delay(TimeSpan.FromSeconds(5));
return;
}
_model.SetProgressbar("Checking WoW installation", 10, overallProgressColor);
_model.AddLogEntry("Checking WoW installation");
await Task.Delay(TimeSpan.FromSeconds(0.5));
bool clientWasDownloadedInThisSession = false; // required to get at least the .build.info once, even if disabled
var expectedPatchedClientLocation = Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers);
if (!File.Exists(expectedPatchedClientLocation))
{
_model.AddLogEntry($"Patched client was NOT found at \"{expectedPatchedClientLocation}\"");
// Checking default WoW installation
var expectedDefaultClientLocation = Path.Combine(gameInstallation.Directory, SubPathToWowOriginal);
if (!File.Exists(expectedDefaultClientLocation))
{
_model.AddLogEntry($"Default wow client was NOT found at \"{expectedDefaultClientLocation}\"");
_model.AddLogEntry("Downloading WoW Client...");
if (!gameInstallation.BaseClientDownloadURL.TryGetValue(weAreOnMacOs ? OperatingSystem.MacOs : OperatingSystem.Windows, out string? downloadUrl))
{
_model.AddLogEntry($"Cant find download url for \"{(weAreOnMacOs ? OperatingSystem.MacOs : OperatingSystem.Windows)}\"");
return;
}
_model.AddLogEntry($"Download URL: {downloadUrl}");
var targetDir = new DirectoryInfo(FullPath(gameInstallation.Directory)).FullName;
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
var downloadDestLocation = targetDir + ".partial-download";
_model.AddLogEntry($"Download Location: {downloadDestLocation}");
var exisingFile = new FileInfo(downloadDestLocation);
if (exisingFile.Exists && exisingFile.Length > 2_000_000_000) // >2GB
{
await Task.Delay(TimeSpan.FromSeconds(0.5));
_model.AddLogEntry("Detected downloaded file. Is it already downloaded?");
await Task.Delay(TimeSpan.FromSeconds(5));
_model.AddLogEntry("Skipping download");
await Task.Delay(TimeSpan.FromSeconds(5));
}
else
{
_model.SetProgressbar("Downloading WoW", 0, Brush.Parse("#1976d2"));
try
{
RunDownload(downloadUrl, downloadDestLocation);
}
catch when (false)
{
// TODO: Ask user for manual selecting a zip/rar file
}
}
_model.AddLogEntry($"Unpack to: {targetDir}");
_model.SetProgressbar("Unpack WoW", 0, Brush.Parse("#d84315"));
RunUnpack(downloadDestLocation, targetDir);
#if !DEBUG
try
{
File.Delete(downloadDestLocation);
}
catch(Exception e)
{
_model.AddLogEntry($"Failed to delete tmp file '{downloadDestLocation}'");
await Task.Delay(TimeSpan.FromSeconds(5));
}
#endif
}
try {
if (!File.Exists(expectedPatchedClientLocation) || (_config.CheckForClientPatchUpdates))
{
_model.SetProgressbar("Checking WoW patch status", 30, overallProgressColor);
await Task.Delay(TimeSpan.FromSeconds(0.5));
string summaryUrl = gameInstallation.ClientPatchInfoURL;
_model.AddLogEntry($"Summary URL: {summaryUrl}");
var patchSummary = SimpleFileDownloader.PerformGetJsonRequest<BinaryPatchHandler.PatchSummary>(summaryUrl);
var selectedPatchInfo = weAreOnMacOs ? patchSummary.MacOs : patchSummary.Windows;
if (selectedPatchInfo == null)
throw new Exception($"No path for '{(weAreOnMacOs ? "macos" : "windows")}' was found");
if (!File.Exists(expectedPatchedClientLocation) || selectedPatchInfo.ToSha256 != HashHelper.CreateHexSha256HashFromFilename(expectedDefaultClientLocation))
{
_model.AddLogEntry("Patched client update required");
var patchUrl = string.Join("/", summaryUrl.Split("/").SkipLast(1)) + $"/{selectedPatchInfo.PatchFilename}";
_model.AddLogEntry($"Patch URL: {patchUrl}");
await Task.Delay(TimeSpan.FromSeconds(0.5));
var patchFileContent = SimpleFileDownloader.PerformGetBytesRequest(patchUrl);
BinaryPatchHandler.ApplyPatch(patchFileContent, sourceFile: expectedDefaultClientLocation, targetFile: expectedPatchedClientLocation);
_model.AddLogEntry("Patch was applied!");
await Task.Delay(TimeSpan.FromSeconds(0.5));
}
}
clientWasDownloadedInThisSession = true;
}
catch (Exception e) when (File.Exists(expectedPatchedClientLocation))
{
_model.AddLogEntry("Failed to check for an update for the client");
_model.AddLogEntry("But since the file exists this error can be ignored");
await Task.Delay(TimeSpan.FromSeconds(0.5));
_model.AddLogEntry(e.ToString());
await Task.Delay(TimeSpan.FromSeconds(5));
Console.WriteLine(e);
}
_model.GameIsInstalled = true;
}
bool buildInfoWasChanged = false;
if (gameInstallation.CustomBuildInfoURL != null && (clientWasDownloadedInThisSession || _config.CheckForClientBuildInfoUpdates))
{
_model.SetProgressbar("Checking BuildInfo status", 35, overallProgressColor);
string buildInfoFilePath = Path.Combine(gameInstallation.Directory, ".build.info");
string newBuildInfo;
try
{
newBuildInfo = SimpleFileDownloader.PerformGetStringRequest(gameInstallation.CustomBuildInfoURL);
}
catch
{
_model.AddLogEntry($"BuildInfo URL: {gameInstallation.CustomBuildInfoURL}");
throw;
}
string existingBuildInfo = File.Exists(buildInfoFilePath) ? File.ReadAllText(buildInfoFilePath) : string.Empty;
if (newBuildInfo.ReplaceLineEndings() != existingBuildInfo.ReplaceLineEndings())
{
_model.AddLogEntry("BuildInfo update detected");
await Task.Delay(TimeSpan.FromSeconds(0.5));
File.WriteAllText(buildInfoFilePath, newBuildInfo);
buildInfoWasChanged = true;
}
}
_model.AddLogEntry("Checking HermesProxy status");
_model.SetProgressbar("Checking HermesProxy status", 50, overallProgressColor);
await Task.Delay(TimeSpan.FromSeconds(0.5));
await UpdateHermesProxyIfNecessary();
var modernBuild = ushort.Parse(gameInstallation.Version.Split(".").Last());
_model.AddLogEntry($"-----------------");
_model.SetProgressbar("Starting HermesProxy", 75, overallProgressColor);
await Task.Delay(TimeSpan.FromSeconds(0.5));
_model.AddLogEntry($"ModernBuild: {modernBuild}");
var hermesSettingsOverwrite = new Dictionary<string, string>();
var splittedRealmlist = serverInfo.RealmlistAddress.Split(':');
hermesSettingsOverwrite.Add("ServerAddress", splittedRealmlist.First());
if (splittedRealmlist.Length == 2)
hermesSettingsOverwrite.Add("ServerPort", splittedRealmlist.Last());
if (serverInfo.HermesSettings != null)
{
foreach (var customSettings in serverInfo.HermesSettings)
hermesSettingsOverwrite.Add(customSettings.Key, customSettings.Value);
}
_hermesProcess = LauncherActions.StartHermesProxy(_config.HermesProxyLocation, modernBuild, hermesSettingsOverwrite, (logLine) => { _model.AddLogEntry(logLine); });
_model.SetHermesPid(_hermesProcess.Id);
_hermesProcess.Exited += (a, e) =>
{
_model.AddLogEntry($"HERMES PROXY HAS CLOSED! Status: {_hermesProcess.ExitCode}");
_model.SetHermesPid(null);
};
await Task.Delay(TimeSpan.FromSeconds(1));
if (_hermesProcess.HasExited)
{
_model.AddLogEntry($"HERMES PROXY HAS CLOSED PREMATURELY! Status: {_hermesProcess.ExitCode}");
_model.SetHermesPid(null);
}
_model.SetProgressbar("Starting Game", 90, overallProgressColor);
{
string bnetPortStr = serverInfo.HermesSettings?.GetValueOrDefault("BNetPort") ?? "1119";
LauncherActions.PrepareGameConfigWtf(gameInstallation.Directory, portalAddress: $"127.0.0.1:{bnetPortStr}");
}
if (buildInfoWasChanged)
_model.SetProgressbar("Your game is updating please wait a bit (check Task Manager!)", 95, sideProgressColor);
else
_model.SetProgressbar("Starting Game", 95, overallProgressColor);
LauncherActions.StartGame(Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers));
await Task.Delay(TimeSpan.FromSeconds(5));
}).ContinueWith((t) =>
{
if (t.Exception != null)
{
_model.AddLogEntry(t.Exception.ToString());
}
else if (t.IsCompletedSuccessfully)
{
_model.SetProgressbar("Done", 100, overallProgressColor);
_model.InputIsAllowed = true;
}
});
}
private async Task UpdateHermesProxyIfNecessary()
{
string? localHermesVersion = null;
var hermesProxyVersionFile = Path.Combine(_config.HermesProxyLocation, "version.txt");
if (File.Exists(hermesProxyVersionFile))
{
localHermesVersion = File.ReadLines(hermesProxyVersionFile).First();
}
if (localHermesVersion == null || _config.CheckForHermesUpdates)
{
GitHubReleaseInfo? releaseInfo;
try
{
releaseInfo = GitHubApi.LatestReleaseVersion("WowLegacyCore/HermesProxy");
}
catch (Exception e) when (localHermesVersion != null)
{
_model.AddLogEntry("Error: Failed to check HermesProxy version!");
Console.WriteLine("Exception while checking GitHub status of HermesProxy");
Console.WriteLine(e);
await Task.Delay(TimeSpan.FromSeconds(5));
return;
}
bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
var versionString = $"{releaseInfo.TagName}|{releaseInfo.Name}";
if (localHermesVersion != versionString)
{
var osName = weAreOnMacOs ? "mac" : "win";
var possibleDownloads = releaseInfo.Assets!.FindAll(a => a.Name.Contains(osName, StringComparison.CurrentCultureIgnoreCase));
if (possibleDownloads.Count != 1)
throw new Exception($"Found {possibleDownloads.Count} HermesProxy versions for your OS");
var targetDir = new DirectoryInfo(FullPath(_config.HermesProxyLocation)).FullName;
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
var downloadDestLocation = targetDir + ".partial-download";
_model.SetProgressbar("Downloading HermesProxy", 0, Brush.Parse("#1976d2"));
var downloadUrl = possibleDownloads[0].DownloadUrl;
_model.AddLogEntry($"Download URL: {downloadUrl}");
_model.AddLogEntry($"Download Location: {downloadDestLocation}");
RunDownload(downloadUrl, downloadDestLocation);
var directories = Directory.GetDirectories(targetDir);
foreach (string directory in directories)
{
if (!directory.Contains("AccountData")) // we want to keep our AccountData
Directory.Delete(directory, recursive: true);
}
Directory.CreateDirectory(targetDir);
_model.SetProgressbar("Unpack HermesProxy", 0, Brush.Parse("#d84315"));
RunUnpack(downloadDestLocation, targetDir);
#if !DEBUG
try
{
File.Delete(downloadDestLocation);
}
catch(Exception e)
{
_model.AddLogEntry($"Failed to delete tmp file '{downloadDestLocation}'");
await Task.Delay(TimeSpan.FromSeconds(5));
}
#endif
File.WriteAllLines(hermesProxyVersionFile, new string[]
{
versionString,
$"Source: {downloadUrl}"
});
_model.SetHermesVersion(releaseInfo.TagName);
}
}
}
}
================================================
FILE: WinterspringLauncher/LauncherLogic.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using WinterspringLauncher.Utils;
using WinterspringLauncher.ViewModels;
using WinterspringLauncher.Views;
namespace WinterspringLauncher;
public partial class LauncherLogic
{
private const string CONFIG_FILE_NAME = "winterspring-launcher-config.json";
private readonly MainWindowViewModel _model;
private readonly LauncherConfig _config;
private string FullPath(string subPath) => Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, subPath));
private static readonly string SubPathToWowOriginal = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? "_classic_era_/World of Warcraft Classic.app/Contents/MacOS/World of Warcraft Classic"
: "_classic_era_/WowClassic.exe";
private static readonly string SubPathToWowForCustomServers = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? "_classic_era_/WoW For Custom Servers.app/Contents/MacOS/WoW For Custom Servers"
: "_classic_era_/WowClassic_ForCustomServers.exe";
public LauncherLogic(MainWindowViewModel model)
{
_model = model;
_config = LauncherConfig.LoadOrCreateDefault(CONFIG_FILE_NAME);
if (_config.LastSelectedServerName == "") // first configuration
{
_config.LastSelectedServerName = LocaleDefaults.GetBestServerName();
_config.GitHubApiMirror = LocaleDefaults.GetBestGitHubMirror();
}
if (!string.IsNullOrWhiteSpace(_config.GitHubApiMirror))
GitHubApi.GitHubApiAddress = _config.GitHubApiMirror;
for (var i = 0; i < _config.KnownServers.Length; i++)
{
var knownServer = _config.KnownServers[i];
_model.KnownServerList.Add(knownServer.Name);
if (_config.LastSelectedServerName == knownServer.Name)
_model.SelectedServerIdx = i;
}
_model.Language.SetLanguage(_config.LauncherLanguage);
_model.AddLogEntry($"Launcher started");
_model.AddLogEntry($"Base path: \"{FullPath(".")}\"");
_model.AddLogEntry($"GitHub API Address: {GitHubApi.GitHubApiAddress}");
string? localHermesVersion = null;
var hermesProxyVersionFile = Path.Combine(_config.HermesProxyLocation, "version.txt");
if (File.Exists(hermesProxyVersionFile))
{
localHermesVersion = File.ReadLines(hermesProxyVersionFile).First().Split("|")[0];
}
_model.SetHermesVersion(localHermesVersion);
if (_config.CheckForLauncherUpdates)
{
Task.Run(() =>
{
try
{
if (LauncherVersion.CheckIfUpdateIsAvailable(out var updateInformation))
{
_model.AddLogEntry($"--------------------------");
_model.AddLogEntry($"This launcher has a new version {updateInformation.VersionName} ({updateInformation.ReleaseDate:yyyy-MM-dd})");
_model.AddLogEntry($"You can download it here {updateInformation.URLLinkToReleasePage}");
_model.AddLogEntry($"--------------------------");
CreateUpdatePopup(updateInformation);
}
Console.WriteLine("Launcher update check done");
}
catch (Exception e)
{
_model.AddLogEntry("An error occured while checking for a launcher update");
Console.WriteLine(e);
}
});
}
}
private void CreateUpdatePopup(LauncherVersion.UpdateInformation updateInformation)
{
Dispatcher.UIThread.Post(() =>
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null)
{
var dialog = new NewVersionAvailableDialog(updateInformation);
dialog.ShowDialog(desktop.MainWindow);
}
});
}
public void ChangeServerIdx()
{
var serverInfo = _config.KnownServers.ElementAtOrDefault(_model.SelectedServerIdx);
if (serverInfo == null)
{
_model.AddLogEntry("Error invalid server settings");
_model.InputIsAllowed = true;
return;
}
Console.WriteLine($"Selected Server: {serverInfo.Name}");
var gameInstallation = _config.GameInstallations.GetValueOrDefault(serverInfo.UsedInstallation);
if (gameInstallation == null)
{
_model.AddLogEntry($"Error cant find '{serverInfo.UsedInstallation}' installation in settings");
_model.InputIsAllowed = true;
return;
}
_config.LastSelectedServerName = serverInfo.Name;
_config.SaveConfig(CONFIG_FILE_NAME);
var expectedPatchedClientLocation = Path.Combine(gameInstallation.Directory, SubPathToWowForCustomServers);
_model.GameFolderExists = Directory.Exists(gameInstallation.Directory);
_model.GameIsInstalled = File.Exists(expectedPatchedClientLocation);
_model.GameVersion = string.Join('.', gameInstallation.Version.Split('.').SkipLast(1));
}
private void RunDownload(string downloadUrl, string destLocation)
{
LauncherActions.DownloadFile(downloadUrl, destLocation,
(totalBytes, alreadyDownloadedBytes, bytesPerSec) =>
{
double percent = (totalBytes != null)
? (alreadyDownloadedBytes / (double)totalBytes.Value) * 100
: 0;
string additionalText = $" {UtilHelper.ToHumanFileSize(alreadyDownloadedBytes)}/{UtilHelper.ToHumanFileSize(totalBytes ?? 0)} {UtilHelper.ToHumanFileSize(bytesPerSec)}/s ";
_model.UpdateProgress(percent, additionalText);
});
}
private void RunUnpack(string archiveLocation, string targetDir)
{
LauncherActions.Unpack(archiveLocation, targetDir,
(totalFileCount, alreadyUnpacked) =>
{
double percent = (alreadyUnpacked / (double)totalFileCount) * 100;
_model.UpdateProgress(percent, $" {alreadyUnpacked} / {totalFileCount} ");
});
}
public void KillHermesProxy()
{
try
{
_hermesProcess?.Kill();
}
catch(Exception e)
{
_model.AddLogEntry("Fail to stop HermesProxy");
Console.WriteLine("Fail to kill HermesProxy");
Console.WriteLine(e);
}
}
}
================================================
FILE: WinterspringLauncher/LauncherUpdateHandler.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
namespace WinterspringLauncher;
public static class LauncherUpdateHandler
{
public static bool/*exitNow*/ HandleStartArguments(string[] args)
{
if (args.Length != 2)
return false;
var actionName = args[0];
var targetPath = args[1];
if (string.IsNullOrWhiteSpace(targetPath))
{
Console.WriteLine("AutoUpdate: Target path is empty");
return true;
}
switch (actionName)
{
case "--copy-self-to":
{
CreateTerminalWindowIfPossible();
Console.WriteLine($"Updating launcher '{targetPath}'");
var ourPath = Process.GetCurrentProcess().MainModule!.FileName!;
bool wasSuccessful = false;
const int maxTries = 20;
for (int i = 0; i < maxTries; i++)
{
try
{
File.Copy(ourPath, targetPath, overwrite: true);
wasSuccessful = true;
}
catch(IOException)
{
Console.WriteLine($"Need to wait for old process to close (this might take a bit) (try {i + 1}/{maxTries})");
Thread.Sleep(TimeSpan.FromMilliseconds(500));
}
}
if (!wasSuccessful)
{
Console.WriteLine("Update was not successful, please try again or update manually");
Thread.Sleep(TimeSpan.FromSeconds(10));
return true;
}
Console.WriteLine("Start new launcher");
Process.Start(new ProcessStartInfo{
FileName = targetPath,
Arguments = $"--delete-tmp-updater-file \"{ourPath}\"",
UseShellExecute = true,
});
return true;
}
case "--delete-tmp-updater-file":
{
Thread.Sleep(TimeSpan.FromMilliseconds(500));
try
{
Console.WriteLine($"Removing tmp file '{targetPath}'");
File.Delete(targetPath);
}
catch
{
// Ignore
}
return false; // keep our current instance
}
default:
return false;
}
}
#if PLATFORM_WINDOWS
[DllImport("kernel32.dll")]
static extern bool AttachConsole(int dwProcessId);
private const int ATTACH_PARENT_PROCESS = -1;
#endif
private static void CreateTerminalWindowIfPossible()
{
#if PLATFORM_WINDOWS
AttachConsole(ATTACH_PARENT_PROCESS);
#endif
}
}
================================================
FILE: WinterspringLauncher/LauncherVersion.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using WinterspringLauncher.Utils;
namespace WinterspringLauncher;
public static class LauncherVersion
{
public static string ShortVersionString
{
get
{
string version = GitVersionInformation.MajorMinorPatch;
if (GitVersionInformation.CommitsSinceVersionSource != "0")
version += $"+{GitVersionInformation.CommitsSinceVersionSource}";
if (GitVersionInformation.UncommittedChanges != "0")
version += " dirty";
return version;
}
}
public static string DetailedVersionString => GitVersionInformation.InformationalVersion;
public static bool IsNotMainBranch => GitVersionInformation.CommitsSinceVersionSource != "0" || GitVersionInformation.UncommittedChanges != "0";
public static bool CheckIfUpdateIsAvailable([NotNullWhen(true)] out UpdateInformation? updateInformation)
{
updateInformation = null;
if (IsNotMainBranch)
{
Console.WriteLine("Skip update check because not main branch (or local dev version)");
return false; // we are probably in a test branch
}
var latestLauncherVersion = GitHubApi.LatestReleaseVersion("0blu/WinterspringLauncher");
if (latestLauncherVersion.TagName == null)
throw new Exception("No latest version?");
var myVersion = Version.Parse(GitVersionInformation.MajorMinorPatch);
var newVersion = Version.Parse(latestLauncherVersion.TagName);
if (newVersion > myVersion)
{
Console.WriteLine($"New launcher update {myVersion.ToString(fieldCount: 2)} => {newVersion.ToString(fieldCount: 2)}");
updateInformation = new UpdateInformation
{
ReleaseDate = latestLauncherVersion.PublishedAt,
VersionName = latestLauncherVersion.TagName,
URLLinkToReleasePage = "https://github.com/0blu/WinterspringLauncher/releases",
};
return true;
}
return false;
}
public class UpdateInformation
{
public DateTime ReleaseDate;
public string VersionName;
public string URLLinkToReleasePage;
}
}
================================================
FILE: WinterspringLauncher/LocaleDefaults.cs
================================================
using System;
using System.Globalization;
namespace WinterspringLauncher;
public static class LocaleDefaults
{
public static bool ShouldUseAsiaPreferences { get; set; } = CultureInfo.CurrentCulture.Name.StartsWith("zh", StringComparison.InvariantCultureIgnoreCase);
public static string GetBestWoWConfigLocale()
{
return ShouldUseAsiaPreferences ? "zhCN" : "enUS";
}
public static string? GetBestGitHubMirror()
{
return ShouldUseAsiaPreferences ? "https://asia.cdn.everlook.aclon.cn/github-mirror/api/" : null;
}
public static string GetBestServerName()
{
return ShouldUseAsiaPreferences ? "Everlook (Asia)" : "Everlook (Europe)";
}
}
================================================
FILE: WinterspringLauncher/ProgramStartup.cs
================================================
using Avalonia;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Logging;
namespace WinterspringLauncher;
class ProgramStartup
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
if (LauncherUpdateHandler.HandleStartArguments(args))
return;
if (args.Contains("--use-asia-defaults"))
LocaleDefaults.ShouldUseAsiaPreferences = true;
bool weAreOnMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
if (weAreOnMacOs)
{
string home = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "WinterspringLauncher");
Directory.CreateDirectory(home);
Environment.CurrentDirectory = home;
}
else
{
Environment.CurrentDirectory = Path.GetDirectoryName(AppContext.BaseDirectory)!;
}
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace(LogEventLevel.Verbose);
}
================================================
FILE: WinterspringLauncher/UiElements/HyperlinkSpan.cs
================================================
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
namespace WinterspringLauncher.UiElements;
public class HyperlinkTextBlock : TextBlock
{
public static readonly DirectProperty<HyperlinkTextBlock, string> NavigateUriProperty =
AvaloniaProperty.RegisterDirect<HyperlinkTextBlock, string>(
nameof(NavigateUri),
o => o.NavigateUri,
(o, v) => o.NavigateUri = v);
private string _navigateUri;
public string NavigateUri
{
get => _navigateUri;
set => SetAndRaise(NavigateUriProperty, ref _navigateUri, value);
}
public HyperlinkTextBlock()
{
AddHandler(PointerPressedEvent, OnPointerPressed);
PseudoClasses.Add(":pointerover");
Cursor = new Cursor(StandardCursorType.Hand);
Foreground = Brush.Parse("#2E95D3");
}
private void OnPointerPressed(object sender, PointerPressedEventArgs e)
{
if (!string.IsNullOrEmpty(NavigateUri))
{
// Open the link here, for example, by launching a browser
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(NavigateUri) { UseShellExecute = true });
}
}
}
================================================
FILE: WinterspringLauncher/Utils/ArchiveCompression.cs
================================================
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
#if PLATFORM_WINDOWS
using SevenZip;
#endif
namespace WinterspringLauncher.Utils;
public static class ArchiveCompression
{
public delegate void UnpackProgressInfoHandler(long totalFileCount, long alreadyUnpackedFileCount);
public static void Decompress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func<string, bool> shouldBeDecompressedPredicate)
{
byte[] buffer = new byte[4];
using (FileStream fileHandle = File.OpenRead(archiveFilePath))
{
fileHandle.Read(buffer, 0, 4);
}
if (buffer.SequenceEqual(new byte[] { 0x52, 0x61, 0x72, 0x21 })) // Rar!
{
Decompress7ZWithProgress(archiveFilePath, extractionFolderPath, folderToSkipName, progressHandler, shouldBeDecompressedPredicate);
}
else if (buffer[..2].SequenceEqual(new byte[] { 0x50, 0x4B })) // Zip
{
DecompressZipWithProgress(archiveFilePath, extractionFolderPath, folderToSkipName, progressHandler, shouldBeDecompressedPredicate);
}
else // Error
{
throw new Exception("Unknown file format. Cannot decompress");
}
}
private static void DecompressZipWithProgress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func<string, bool> shouldBeDecompressedPredicate)
{
using var zip = ZipFile.OpenRead(archiveFilePath);
bool ShouldBeDecompressed(ZipArchiveEntry entry) => !entry.FullName.EndsWith("\\") && !entry.FullName.EndsWith("/") && shouldBeDecompressedPredicate(entry.FullName);
var totalSize = zip.Entries.Where(ShouldBeDecompressed).Sum(x => x.Length);
var totalCount = zip.Entries.Where(ShouldBeDecompressed).Count();
string ToPath(string path) => Path.Combine(extractionFolderPath, path);
Console.WriteLine($"Total size to decompress {UtilHelper.ToHumanFileSize(totalSize)}");
long alreadyDecompressedCount = 0;
foreach (var entry in zip.Entries.Where(ShouldBeDecompressed))
{
var destPath = ToPath(entry.FullName);
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
entry.ExtractToFile(destPath, overwrite: true);
alreadyDecompressedCount++;
progressHandler(totalCount, alreadyDecompressedCount);
}
progressHandler(totalCount, totalCount);
}
#if !PLATFORM_WINDOWS
private static void Decompress7ZWithProgress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func<string, bool> shouldBeDecompressed)
{
throw new NotSupportedException("7z is only supported on Windows");
}
#else
private static void Decompress7ZWithProgress(string archiveFilePath, string extractionFolderPath, string folderToSkipName, UnpackProgressInfoHandler progressHandler, Func<string, bool> shouldBeDecompressedPredicate)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "WinterspringLauncher.7z.dll";
using (Stream stream = assembly.GetManifestResourceStream(resourceName)!)
{
try
{
using (var file = File.Open("7z.dll", FileMode.Create, FileAccess.Write))
{
stream.CopyTo(file);
}
}
catch(Exception e)
{
// Maybe the file is somehow already in use
Console.WriteLine("Failed to write 7z.dll");
Console.WriteLine(e);
}
}
SevenZipBase.SetLibraryPath("7z.dll");
string downloadedFile = Path.Combine(archiveFilePath);
Console.WriteLine($"Extracting archive into {extractionFolderPath}");
using (var archiveFile = new SevenZipExtractor(downloadedFile))
{
bool ShouldBeDecompressed(ArchiveFileInfo entry) => !entry.IsDirectory && shouldBeDecompressedPredicate(entry.FileName);
string ToPath(string path) => path.ReplaceFirstOccurrence(folderToSkipName, extractionFolderPath);
long totalSize = 0;
long totalCount = 0;
foreach (var entry in archiveFile.ArchiveFileData)
{
if (ShouldBeDecompressed(entry))
{
totalSize += (long) entry.Size;
totalCount++;
}
}
Console.WriteLine($"Total size to decompress {UtilHelper.ToHumanFileSize(totalSize)}");
long alreadyDecompressedCount = 0;
foreach (var entry in archiveFile.ArchiveFileData)
{
if (ShouldBeDecompressed(entry))
{
var destName = ToPath(entry.FileName);
Directory.CreateDirectory(Path.GetDirectoryName(destName)!);
using (var fStream = File.Open(destName, FileMode.Create, FileAccess.Write))
{
archiveFile.ExtractFile(entry.FileName, fStream);
}
alreadyDecompressedCount++;
progressHandler(totalCount, alreadyDecompressedCount);
}
}
progressHandler(totalCount, totalCount);
}
try
{
File.Delete("7z.dll");
}
catch
{
// ignored
}
}
#endif
public static void DecompressSmartSkipFirstFolder(string zipFilePath, string outputDirectory)
{
using var zip = ZipFile.OpenRead(zipFilePath);
string? zipBaseFolder = GetBaseFolderFromZip(zip);
Console.WriteLine($"Unzipping {zipFilePath}, detected '{zipBaseFolder ?? "<null>"}' as first folder");
string ToFilteredPath(string path) => zipBaseFolder != null
? path.ReplaceFirstOccurrence(zipBaseFolder, outputDirectory)
: path;
string GetCompletePath(ZipArchiveEntry entry) => Path.Combine(outputDirectory, ToFilteredPath(entry.FullName));
foreach (var entry in zip.Entries.Where(e => !e.IsFolder()))
{
var completePath = GetCompletePath(entry);
Directory.CreateDirectory(Path.GetDirectoryName(completePath)!);
entry.ExtractToFile(completePath, overwrite: true);
}
}
private static bool IsFolder(this ZipArchiveEntry entry)
{
return entry.FullName.EndsWith("/");
}
static string? GetBaseFolderFromZip(ZipArchive archive)
{
string[] entryPaths = archive.Entries.Select(entry => entry.FullName).ToArray();
if (entryPaths.Length == 0)
return null;
string[] parts = entryPaths[0].Split('/');
for (int i = 1; i < entryPaths.Length; i++)
{
string[] currentParts = entryPaths[i].Split('/');
int commonParts = parts.Zip(currentParts, (p1, p2) => p1 == p2).TakeWhile(b => b).Count();
if (commonParts == 0)
return null;
Array.Resize(ref parts, commonParts);
}
return string.Join("/", parts);
}
}
================================================
FILE: WinterspringLauncher/Utils/BinaryPatchHandler.cs
================================================
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
namespace WinterspringLauncher.Utils;
public class BinaryPatchHandler
{
public class PatchSummary
{
[JsonPropertyName("windows")]
public PatchSummaryEntry? Windows { get; set; }
[JsonPropertyName("macos")]
public PatchSummaryEntry? MacOs { get; set; }
public class PatchSummaryEntry
{
[JsonPropertyName("from_sha256")]
public string FromSha256 { get; set; } = null!;
[JsonPropertyName("to_sha256")]
public string ToSha256 { get; set; } = null!;
[JsonPropertyName("last_update")]
public ulong LastUpdate { get; set; } = 0;
[JsonPropertyName("patch_filename")]
public string PatchFilename { get; set; } = null!;
}
}
public static void ApplyPatch(byte[] patchFileContent, string sourceFile, string targetFile)
{
// Read header information
byte[] magic = patchFileContent.Take(4).ToArray();
if (!magic.SequenceEqual(new byte[] { 0x42, 0x42, 0x50, 0x31 }))
throw new ArgumentException("Invalid patch file format (expected BBP1)");
// Verifying signature
{
byte[] everythingButSignature = patchFileContent.SkipLast(256).ToArray();
byte[] signature = patchFileContent.TakeLast(256).ToArray();
VerifySignatureOrThrow(everythingButSignature, signature);
}
byte[] fileBytes = File.ReadAllBytes(sourceFile);
string expectedOriginalHash = HashHelper.ConvertBinarySha256ToHex(patchFileContent.Skip(4).Take(32).ToArray());
string actualOriginalHash = HashHelper.CreateHexSha256HashFromFileBytes(fileBytes);
if (actualOriginalHash != expectedOriginalHash)
throw new Exception($"Cannot apply patch because the hash of source file is incorrect. Expected '{expectedOriginalHash}' Actual: '{actualOriginalHash}'");
string expectedHashAfterPatch = HashHelper.ConvertBinarySha256ToHex(patchFileContent.Skip(36).Take(32).ToArray());
ulong patchCount = BitConverter.ToUInt64(patchFileContent, startIndex: 68);
long currentPosition = 76; // Start after the header
ulong patchesApplied = 0;
for (ulong patchEntryIdx = 0; patchEntryIdx < patchCount; patchEntryIdx++)
{
if (currentPosition + 12 > patchFileContent.Length)
{
throw new ArgumentException("Patch file is incomplete");
}
int fileOffset = (int)BitConverter.ToUInt64(patchFileContent, (int)currentPosition);
uint patchSize = BitConverter.ToUInt32(patchFileContent, (int)(currentPosition + 8));
currentPosition += 12;
// If the file offset is out of bounds, extend the file and initialize with 0x00
if (fileOffset > fileBytes.Length)
Array.Resize(ref fileBytes, (int)(fileOffset + patchSize));
// Apply the patch to the source file
for (int patchByteIdx = 0; patchByteIdx < patchSize; patchByteIdx++)
fileBytes[fileOffset + patchByteIdx] = patchFileContent[currentPosition + patchByteIdx];
currentPosition += patchSize;
patchesApplied++;
}
if (patchesApplied != patchCount)
throw new InvalidOperationException("Not all patches were applied");
// Verify the integrity of the patched file
string actualHashAfterPatch = HashHelper.CreateHexSha256HashFromFileBytes(fileBytes);
if (actualHashAfterPatch != expectedHashAfterPatch)
throw new Exception($"Invalid patch result. Expected '{expectedHashAfterPatch}' Actual: '{actualHashAfterPatch}'");
File.WriteAllBytes(targetFile, fileBytes);
}
private static void VerifySignatureOrThrow(byte[] bytesToVerify, byte[] signature)
{
if (signature.Length != 256)
throw new ArgumentException("Signature must be 256 bytes long");
// ref https://wow-patches.blu.wtf/sign_key.pub
const string publicKey = @"
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmy/cX6/VOlOpgLnQWnWS
tFVqf9xAO2uNjeSeUHmiMTQTwfm8hnDbcEAz5V4ou987dfDxXZb5WGVxoHnugMS/
rUrOSZ8VQolH+3IanhFNrqRxRTOVk+ZlTrxV9k1iC34kXeoRryiQcqMYLlX4jT3E
EupzAivNsJYm2X/jVGFgPfrDObwOjq23aLdey2uI3YA6SgIg/ayp/YyJEp775lr4
Z+49t3p7WMNZw8VJkQvDB5/t64Bjd9bdIQxsO9jWyHl/z7QOrnAKv0uUPdcCCwWp
kERTaAnq6tK0rAvcYMlJ230cihY+s/7QpIHpsq091La9n4nJCpFIunaaG1JyNHk5
GQIDAQAB
-----END PUBLIC KEY-----
";
var rsa = new RSACryptoServiceProvider();
rsa.ImportFromPem(publicKey);
bool signatureIsValid = rsa.VerifyData(bytesToVerify, signature, hashAlgorithm: HashAlgorithmName.SHA256, padding: RSASignaturePadding.Pkcs1);
if (!signatureIsValid)
throw new Exception("Signature not valid");
}
}
================================================
FILE: WinterspringLauncher/Utils/DirectoryCopy.cs
================================================
using System;
using System.IO;
namespace WinterspringLauncher.Utils;
public static class DirectoryCopy
{
public static void Copy(string sourceDirectory, string targetDirectory)
{
DirectoryInfo diSource = new DirectoryInfo(sourceDirectory);
DirectoryInfo diTarget = new DirectoryInfo(targetDirectory);
CopyAll(diSource, diTarget);
}
public static void CopyAll(DirectoryInfo source, DirectoryInfo target)
{
Directory.CreateDirectory(target.FullName);
// Copy each file into the new directory.
foreach (FileInfo fi in source.GetFiles())
{
Console.WriteLine(@"Copying {0}\{1}", target.FullName, fi.Name);
fi.CopyTo(Path.Combine(target.FullName, fi.Name), true);
}
// Copy each subdirectory using recursion.
foreach (DirectoryInfo diSourceSubDir in source.GetDirectories())
{
DirectoryInfo nextTargetSubDir =
target.CreateSubdirectory(diSourceSubDir.Name);
CopyAll(diSourceSubDir, nextTargetSubDir);
}
}
}
================================================
FILE: WinterspringLauncher/Utils/GitHubApi.cs
================================================
using System;
using System.Collections.Generic;
using System.Data;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace WinterspringLauncher.Utils;
public static class GitHubApi
{
public static string GitHubApiAddress { get; set; } = "https://api.github.com/";
public static GitHubReleaseInfo LatestReleaseVersion(string repoName)
{
var releaseUrl = new Uri(new Uri(GitHubApiAddress), $"repos/{repoName}/releases/latest").ToString();
var releaseInfo = PerformWebRequest<GitHubReleaseInfo>(releaseUrl);
return releaseInfo;
}
private static TJsonResponse PerformWebRequest<TJsonResponse>(string url) where TJsonResponse : new()
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "curl/7.0.0"); // otherwise we get blocked
var response = client.GetAsync(url).GetAwaiter().GetResult();
if (response.StatusCode == HttpStatusCode.Forbidden)
{
if (response.ReasonPhrase == "rate limit exceeded")
{
Console.WriteLine("You are being rate-limited, did you open the launcher too many times in a short time?");
return new TJsonResponse();
}
}
response.EnsureSuccessStatusCode();
var rawJson = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); // easier to debug with a string and the performance is negligible for such small jsons
var parsedJson = JsonSerializer.Deserialize<TJsonResponse>(rawJson);
if (parsedJson == null)
{
Console.WriteLine($"Debug: {rawJson}");
throw new NoNullAllowedException("The web response resulted in an null object");
}
return parsedJson;
}
}
public class GitHubReleaseInfo
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("published_at")]
public DateTime PublishedAt { get; set; }
[JsonPropertyName("tag_name")]
public string? TagName { get; set; }
[JsonPropertyName("assets")]
public List<Asset>? Assets { get; set; }
public class Asset
{
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("browser_download_url")]
public string DownloadUrl { get; set; } = null!;
}
}
================================================
FILE: WinterspringLauncher/Utils/HashHelper.cs
================================================
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace WinterspringLauncher.Utils;
public class HashHelper
{
public static string CreateHexSha256HashFromFilename(string filePath)
{
using (FileStream stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(stream);
return ConvertBinarySha256ToHex(hashBytes);
}
}
public static string CreateHexSha256HashFromFileBytes(byte[] fileContent)
{
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(fileContent);
return ConvertBinarySha256ToHex(hashBytes);
}
}
public static string ConvertBinarySha256ToHex(byte[] binarySha256Hash)
{
if (binarySha256Hash.Length != 32)
throw new ArgumentException("Expected a 32byte long Sha256 hash");
StringBuilder hashBuilder = new StringBuilder(32);
foreach (byte b in binarySha256Hash)
hashBuilder.Append(b.ToString("X2"));
return hashBuilder.ToString();
}
}
================================================
FILE: WinterspringLauncher/Utils/ProgressiveFileDownloader.cs
================================================
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Timers;
namespace WinterspringLauncher.Utils;
public class ProgressiveFileDownloader : IDisposable
{
private readonly string _downloadUrl;
private readonly string _destinationFilePath;
private readonly HttpClient _httpClient;
public delegate void InitialFileInfoHandler(long? totalFileSize);
public delegate void ProgressFixedChangedHandler(long? totalFileSize, long alreadyReceived, long currentBytesPerSecond);
public delegate void DownloadDoneHandler(long downloadedBytes);
public event InitialFileInfoHandler? InitialInfo;
public event ProgressFixedChangedHandler? ProgressChangedFixedDelay;
public event DownloadDoneHandler? DownloadDone;
private readonly System.Timers.Timer _updateTimer;
private DateTime? _lastUpdateInvoke;
private long _lastReceivedBytes;
private long? _totalFileSize;
private long _alreadyReceivedBytes;
private int _lastDownloadRatesIdx = 0;
private readonly double?[] _lastDownloadRates = new double?[15];
private bool _hadZeroRate = false;
public ProgressiveFileDownloader(string downloadUrl, string destinationFilePath)
{
_downloadUrl = downloadUrl;
_destinationFilePath = destinationFilePath;
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
_updateTimer = new System.Timers.Timer(500 /*ms*/);
_updateTimer.Elapsed += TimerElapsed;
}
public async Task StartGetDownload()
{
using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
await DownloadFileFromHttpResponseMessage(response);
}
private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength;
_totalFileSize = totalBytes;
TriggerInitialInfo(totalBytes);
await using (var contentStream = await response.Content.ReadAsStreamAsync())
{
await ProcessContentStream(contentStream);
}
}
private async Task ProcessContentStream(Stream contentStream)
{
long totalBytesRead = 0;
long readCount = 0;
var buffer = new byte[4096];
var isMoreToRead = true;
_updateTimer.Start();
try
{
using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, buffer.Length, true))
{
do
{
var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
isMoreToRead = false;
UpdateInternalProgress(totalBytesRead);
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
readCount += 1;
if (readCount % 1000 == 0)
UpdateInternalProgress(totalBytesRead);
} while (isMoreToRead);
}
}
finally
{
_updateTimer.Stop();
}
TriggerDownloadDone(totalBytesRead);
}
private void TriggerInitialInfo(long? totalDownloadSize)
{
InitialInfo?.Invoke(totalDownloadSize);
}
private void UpdateInternalProgress(long alreadyReceivedBytes)
{
_alreadyReceivedBytes = alreadyReceivedBytes;
}
private void TimerElapsed(object? sender, ElapsedEventArgs e)
{
UpdateAndTriggerProgressChanged();
}
private void UpdateAndTriggerProgressChanged()
{
DateTime now = DateTime.Now;
var elapsed = now - _lastUpdateInvoke;
long amountDownloadedInPeriod = _alreadyReceivedBytes - _lastReceivedBytes;
if (amountDownloadedInPeriod == 0 && !_hadZeroRate)
{
_hadZeroRate = true;
return;
}
_hadZeroRate = false;
_lastUpdateInvoke = now;
_lastReceivedBytes = _alreadyReceivedBytes;
if (elapsed != null)
{
double thisBytePerSec = amountDownloadedInPeriod / elapsed.Value.TotalSeconds;
_lastDownloadRates[_lastDownloadRatesIdx] = thisBytePerSec;
_lastDownloadRatesIdx = (_lastDownloadRatesIdx + 1) % _lastDownloadRates.Length;
TriggerProgressChanged();
}
}
private void TriggerProgressChanged()
{
double dlRate = _lastDownloadRates.Where(x => x != null).Select(x => x!.Value).Average();
ProgressChangedFixedDelay?.Invoke(_totalFileSize, _alreadyReceivedBytes, (long)dlRate);
}
private void TriggerDownloadDone(long bytesDownloaded)
{
DownloadDone?.Invoke(bytesDownloaded);
}
public void Dispose()
{
_httpClient?.Dispose();
_updateTimer.Elapsed -= TimerElapsed;
_updateTimer.Stop();
}
}
================================================
FILE: WinterspringLauncher/Utils/SimpleFileDownloader.cs
================================================
using System;
using System.Data;
using System.Net.Http;
using System.Text.Json;
namespace WinterspringLauncher.Utils;
public static class SimpleFileDownloader
{
public static string PerformGetStringRequest(string url)
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "curl/7.0.0"); // otherwise we get blocked
var response = client.GetAsync(url).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
var rawData = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return rawData;
}
public static TJsonResponse PerformGetJsonRequest<TJsonResponse>(string url)
{
var rawData = PerformGetStringRequest(url);
var parsedJson = JsonSerializer.Deserialize<TJsonResponse>(rawData);
if (parsedJson == null)
{
Console.WriteLine($"Debug: {rawData}");
throw new NoNullAllowedException("The web response resulted in an null object");
}
return parsedJson;
}
public static byte[] PerformGetBytesRequest(string url)
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "curl/7.0.0"); // otherwise we get blocked
var response = client.GetAsync(url).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
var rawData = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
return rawData;
}
}
================================================
FILE: WinterspringLauncher/Utils/UnixApi.cs
================================================
using System.Runtime.InteropServices;
namespace WinterspringLauncher.Utils;
public static class UnixApi
{
[DllImport("libc", SetLastError = true)]
public static extern int chmod(string pathname, int mode);
// user permissions
public const int PERM_USR_R = 0x100;
public const int PERM_USR_W = 0x80;
public const int PERM_USR_X = 0x40;
// group permission
public const int PERM_GRP_R = 0x20;
public const int PERM_GRP_W = 0x10;
public const int PERM_GRP_X = 0x8;
// other permissions
public const int PERM_OTH_R = 0x4;
public const int PERM_OTH_W = 0x2;
public const int PERM_OTH_X = 0x1;
public const int PERM_0777 =
PERM_USR_R | PERM_USR_X | PERM_USR_W |
PERM_GRP_R | PERM_GRP_X | PERM_GRP_W |
PERM_OTH_R | PERM_OTH_X | PERM_OTH_W;
}
================================================
FILE: WinterspringLauncher/Utils/UtilHelper.cs
================================================
using System;
namespace WinterspringLauncher.Utils;
public static class UtilHelper
{
// Converts some arbitrary byte number to binary human unit (1024 -> "1.0 KiB")
public static string ToHumanFileSize(long sizeInByte)
{
string[] units = { "Byte", "KiB", "MiB", "GiB", "TiB" };
int unitIdx = 0;
double size = sizeInByte;
while (size >= 1024 && unitIdx < units.Length - 1) {
unitIdx++;
size /= 1024;
}
string unit = units[unitIdx];
return $"{size:0.0} {unit}";
}
public static string ReplaceFirstOccurrence(this string source, string needle, string replacement, StringComparison comparison = StringComparison.InvariantCulture)
{
int pos = source.IndexOf(needle, comparison);
if (pos == -1)
return source;
string result = source.Remove(pos, needle.Length).Insert(pos, replacement);
return result;
}
}
================================================
FILE: WinterspringLauncher/ViewModels/MainWindowViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Media;
using Avalonia.Metadata;
using CommunityToolkit.Mvvm.ComponentModel;
namespace WinterspringLauncher.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
public LauncherLogic Logic { get; }
public MainWindowViewModel()
{
Logic = new LauncherLogic(this);
}
[ObservableProperty]
private LanguageHolder _language = new LanguageHolder();
[ObservableProperty]
private bool _inputIsAllowed = true;
[ObservableProperty]
public int _selectedServerIdx;
[ObservableProperty]
public string _thisLauncherVersion = LauncherVersion.ShortVersionString;
[ObservableProperty]
public string _thisLauncherVersionDetailed = LauncherVersion.DetailedVersionString;
[ObservableProperty]
private bool _gameFolderExists = false;
[ObservableProperty]
private bool _gameIsInstalled = false;
[ObservableProperty]
private string _gameVersion = "";
[ObservableProperty]
public string? _hermesPidToolTipString = null;
[ObservableProperty]
public bool _hermesIsRunning = false;
[ObservableProperty]
public string? _detectedHermesVersion;
[ObservableProperty]
public bool _hermesIsInstalled;
public void SetHermesPid(int? pid)
{
// TODO How to I remove this function and just have a HermesProxyPid Property that will assign the other ones?
HermesPidToolTipString = pid.HasValue ? $"Hermes PID: {pid.Value}" : null;
HermesIsRunning = pid.HasValue;
}
public void SetHermesVersion(string? versionStr)
{
DetectedHermesVersion = versionStr;
HermesIsInstalled = versionStr != null;
}
[ObservableProperty]
public ObservableCollection<string> _knownServerList = new ObservableCollection<string>();
public string LogEntriesCombined { get; private set; }
public List<string> LogEntriesArray = new List<string>();
public void AddLogEntry(string logEntry)
{
OnPropertyChanging(nameof(LogEntriesCombined));
if (LogEntriesArray.Count > 50)
LogEntriesArray.RemoveAt(0);
LogEntriesArray.Add(logEntry);
LogEntriesCombined = string.Join('\n', LogEntriesArray);
OnPropertyChanged(nameof(LogEntriesCombined));
}
public class LanguageHolder
{
public void SetLanguage(string languageShortName)
{
}
}
[ObservableProperty]
private string _progressbarText = "";
// not observable
private string _progressbarInternalTitle = "";
// not observable
private ProgressbarInternalTimeTracker _progressbarInternalTimeTracker;
[ObservableProperty]
private double _progressbarPercent = 0;
[ObservableProperty]
private IBrush _progressbarColor = Brush.Parse("#FFFFFF");
public void SetProgressbar(string title, double progressPercent, IBrush color)
{
_progressbarInternalTitle = title;
_progressbarInternalTimeTracker = new ProgressbarInternalTimeTracker();
ProgressbarPercent = progressPercent;
ProgressbarColor = color;
ProgressbarText = $"{progressPercent:0}% {_progressbarInternalTitle}";
}
public void UpdateProgress(double progressPercent, string additionalText)
{
ProgressbarPercent = progressPercent;
TimeSpan? estimatedTime = _progressbarInternalTimeTracker.GetEstimatedTimeAndUpdateRates(progressPercent);
string timeLeft = estimatedTime.HasValue
? TimeSpan.FromSeconds((long) estimatedTime.Value.TotalSeconds).ToString()
: "?".PadLeft("00:00:00".Length);
ProgressbarText = $"{progressPercent:0}% {_progressbarInternalTitle} {additionalText} estimated time: {timeLeft}";
}
private class ProgressbarInternalTimeTracker
{
private double _lastPercent = 0;
private DateTime? _lastUpdateTime = null;
private int _lastProgressRatesIdx = 0;
private readonly double?[] _lastProgressRates = new double?[15];
public TimeSpan? GetEstimatedTimeAndUpdateRates(double percent)
{
var now = DateTime.Now;
if (_lastUpdateTime != null)
{
TimeSpan timeDiff = now - _lastUpdateTime.Value;
double progressDiff = percent - _lastPercent;
double progressDiffPerSec = progressDiff / timeDiff.TotalSeconds;
_lastProgressRates[_lastProgressRatesIdx] = progressDiffPerSec;
_lastProgressRatesIdx = (_lastProgressRatesIdx + 1) % _lastProgressRates.Length;
}
_lastUpdateTime = now;
_lastPercent = percent;
var avgRate = _lastProgressRates.Where(x => x.HasValue).Select(x => x!.Value).DefaultIfEmpty(0).Average();
if (avgRate == 0)
return null;
const double maxPercent = 100;
double time = (maxPercent - percent) / avgRate;
if (double.IsNaN(time))
return null;
return TimeSpan.FromSeconds(time);
}
}
}
================================================
FILE: WinterspringLauncher/Views/MainWindow.axaml
================================================
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:WinterspringLauncher.ViewModels"
mc:Ignorable="d" d:DesignWidth="850" d:DesignHeight="500"
x:Class="WinterspringLauncher.Views.MainWindow"
x:DataType="viewModels:MainWindowViewModel"
Icon="/Assets/icons/winterspring-launcher-icon.ico"
Title="Winterspring WoW Launcher"
Width="900" Height="500"
MinWidth="900" MinHeight="500"
Background="#181a1b"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="30"
Name="ThisWindow"
WindowStartupLocation="CenterScreen"
>
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<viewModels:MainWindowViewModel/>
</Design.DataContext>
<Window.Styles>
<Styles>
<Style Selector="Button">
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="TabItem.Header">
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="ComboBoxItem">
<Setter Property="FontStyle" Value="Italic" />
</Style>
</Styles>
</Window.Styles>
<Grid RowDefinitions="Auto, *">
<!-- Top system bar -->
<Panel Grid.Row="0" Height="30" Background="#11ffffff" IsHitTestVisible="False">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Margin="10,0,0,0"
Source="/Assets/icons/winterspring-launcher-icon.ico" Width="16" Height="16" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Margin="10,0,0,0" Padding="0, 2, 0, 0"
Text="{Binding Title, ElementName=ThisWindow}" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
</Panel>
<!-- Body -->
<Grid Grid.Row="1" RowDefinitions="Auto, *, Auto" Margin="10">
<!-- Top bar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto, *, Auto" IsEnabled="{Binding InputIsAllowed}">
<StackPanel Orientation="Horizontal" Grid.Column="0" Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Server" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0, 0, 5, 0"/>
<ComboBox SelectedIndex="{Binding SelectedServerIdx, Mode=TwoWay}" ItemsSource="{Binding KnownServerList}" SelectionChanged="ServerSelectionChanged" IsEnabled="{Binding !HermesIsRunning}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<Button Command="{Binding Logic.OpenGameFolder}" IsEnabled="{ Binding GameFolderExists }">
<Image Margin="0, 1, 0, 0" Source="/Assets/icons/folder.png" Width="20" Height="20" VerticalAlignment="Center" HorizontalAlignment="Center" IsEnabled="{Binding GameIsInstalled}"/>
</Button>
<!--
<StackPanel Orientation="Horizontal">
<TextBlock Text="Account" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0, 0, 5, 0"/>
<ComboBox SelectedIndex="0">
<ComboBoxItem><TextBlock Text="<Manual Login>" /></ComboBoxItem>
</ComboBox>
</StackPanel>
-->
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="2" Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Language" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0, 0, 5, 0"/>
<ComboBox SelectedIndex="0" IsEnabled="False"> <!-- TODO Multi language support -->
<ComboBoxItem>
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/icons/language-icons/english.png" Width="20" Height="20" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Text="English" Margin="2, 0, 0, 0" VerticalAlignment="Center" HorizontalAlignment="Left" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem>
<StackPanel Orientation="Horizontal">
<Image Source="/Assets/icons/language-icons/chinese.png" Width="20" Height="20" VerticalAlignment="Center" HorizontalAlignment="Right" />
<TextBlock Text="Chinese" Margin="2, 0, 0, 0" VerticalAlignment="Center" HorizontalAlignment="Left" />
</StackPanel>
</ComboBoxItem>
</ComboBox>
</StackPanel>
<!--
<Button>
<Image Margin="0, 1, 0, 0" Source="/Assets/icons/settings.png" Width="20" Height="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Button>
-->
</StackPanel>
</Grid>
<!-- Center content -->
<Grid RowDefinitions="*" ColumnDefinitions="*" Grid.Row="1" Background="#11ddddff" Margin="0, 10, 0, 0">
<TabControl SelectedIndex="0">
<!--
<TabItem Header="Server Announcements" VerticalContentAlignment="Center" IsEnabled="False">
<TextBlock Text="TODO" HorizontalAlignment="Left" VerticalAlignment="Center"/>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock VerticalAlignment="Center">Launcher Changelog</TextBlock>
</TabItem.Header>
<StackPanel>
<SelectableTextBlock HorizontalAlignment="Left" VerticalAlignment="Center">
<Run>Hello World!Hello World!Hello World!Hello World!</Run>
<LineBreak/>
<Run>Hello World!</Run>
<LineBreak/>
<Run>Hello World!Hello World!Hello World!Hello World!</Run>
<LineBreak/>
<Run>Hello World!</Run>
<LineBreak/>
<Run>Hello World!Hello World!Hello World!Hello World!</Run>
<LineBreak/>
<Run>Hello World!</Run>
<LineBreak/>
<Run>Hello World!Hello World!Hello World!Hello World!</Run>
<LineBreak/>
<Run>Hello World!</Run>
</SelectableTextBlock>
</StackPanel>
</TabItem>
-->
<TabItem>
<TabItem.Header>
<TextBlock VerticalAlignment="Center">Log</TextBlock>
</TabItem.Header>
<ScrollViewer Grid.Row="0" Name="LogScroller" Background="{ DynamicResource TerminalBackground }">
<SelectableTextBlock MaxHeight="900"
FontFamily="{StaticResource MonoFont}"
Text="{Binding LogEntriesCombined}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
/>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
<!--Bottom bar -->
<StackPanel Grid.Row="2" IsEnabled="{Binding InputIsAllowed}">
<Grid>
<ProgressBar Margin="0, 10" Height="20" Foreground="{ Binding ProgressbarColor }" Value="{Binding ProgressbarPercent}"/>
<TextBlock Text="{Binding ProgressbarText}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<Grid ColumnDefinitions="Auto, *, Auto">
<StackPanel Orientation="Vertical" Grid.Column="0" Spacing="5">
<StackPanel Orientation="Horizontal" ToolTip.Tip="{ Binding ThisLauncherVersionDetailed }">
<TextBlock Text="Launcher: " HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="{ Binding ThisLauncherVersion }" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" ToolTip.Tip="{ Binding HermesPidToolTipString, RelativeSource=HermesPid }">
<TextBlock Text="HermesProxy: " HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock IsVisible="{ Binding !HermesIsInstalled }" Text="will be installed" />
<TextBlock IsVisible="{ Binding HermesIsInstalled }" Text="{ Binding DetectedHermesVersion }" />
<TextBlock IsVisible="{ Binding HermesIsRunning }">
<Span> (</Span><Span Foreground="#689f38">running</Span><Span>)</Span>
<!-- Button to kill hermes process -->
</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Game Version: " HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{ Binding GameVersion }" />
<TextBlock IsVisible="{ Binding GameIsInstalled }" HorizontalAlignment="Center" VerticalAlignment="Center">
<Span> (</Span><Span Foreground="#689f38">installed</Span><Span>)</Span>
</TextBlock>
<TextBlock IsVisible="{ Binding !GameIsInstalled }" HorizontalAlignment="Center" VerticalAlignment="Center">
<Span> (</Span><Span Foreground="#7c2b11">Will be downloaded/patched</Span><Span>)</Span>
</TextBlock>
</StackPanel>
</StackPanel>
<Button Command="{Binding Logic.StartGame}" Grid.Column="2" Background="#689f38" Height="50">
<StackPanel>
<StackPanel IsVisible="{ Binding !InputIsAllowed }" Orientation="Vertical" VerticalAlignment="Center">
<TextBlock Text="Loading..." FontWeight="Bold" HorizontalAlignment="Center" />
</StackPanel>
<StackPanel IsVisible="{ Binding InputIsAllowed }" Orientation="Vertical" VerticalAlignment="Center">
<TextBlock IsVisible="{ Binding !GameIsInstalled }" Text="Download and start game" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock IsVisible="{ Binding GameIsInstalled }" Text="Start game" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock IsVisible="{ Binding !HermesIsRunning }" Text="HermesProxy will be started" Foreground="#7c2b11" HorizontalAlignment="Center" />
</StackPanel>
</StackPanel>
</Button>
<!--
<Button Grid.Column="2" Background="#689f38" Height="50">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock Text="Update and start game" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock Text="HermesProxy will be started" Foreground="#7c2b11" HorizontalAlignment="Center" />
</StackPanel>
</Button>
-->
</Grid>
</StackPanel>
</Grid>
</Grid>
</Window>
================================================
FILE: WinterspringLauncher/Views/MainWindow.axaml.cs
================================================
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using WinterspringLauncher.ViewModels;
namespace WinterspringLauncher.Views;
public partial class MainWindow : Window
{
public new MainWindowViewModel DataContext
{
get => base.DataContext as MainWindowViewModel;
set => base.DataContext = value;
}
public MainWindow()
{
InitializeComponent();
LogScroller.PropertyChanged += LogChanged;
}
private void LogChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
LogScroller.ScrollToEnd();
}
private void ServerSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (sender is ComboBox comboBox)
{
DataContext._selectedServerIdx = comboBox.SelectedIndex;
DataContext.Logic.ChangeServerIdx();
}
}
protected override void OnClosing(WindowClosingEventArgs e)
{
DataContext.Logic.KillHermesProxy();
base.OnClosing(e);
}
}
================================================
FILE: WinterspringLauncher/Views/NewVersionAvailableDialog.axaml
================================================
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uiElements="clr-namespace:WinterspringLauncher.UiElements"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="100"
x:Class="WinterspringLauncher.Views.NewVersionAvailableDialog"
Title="WinterspringLauncher Update Available!"
Width="450" Height="120"
MinWidth="450" MinHeight="120"
CanResize="False"
WindowStartupLocation="CenterScreen">
<Grid RowDefinitions="*, Auto" ColumnDefinitions="*">
<StackPanel Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock FontSize="20" Text="Update available!"/>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="15" Text="Version: " />
<TextBlock x:Name="VersionIndicator" FontSize="15" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Download: " />
<uiElements:HyperlinkTextBlock x:Name="DlLinkIndicator" NavigateUri="https://github.com/0blu/WinterspringLauncher" Text="https://github.com/0blu/WinterspringLauncher" />
</StackPanel>
</StackPanel>
<Button Grid.Row="1" Click="CloseButtonClick">Close</Button>
</Grid>
</Window>
================================================
FILE: WinterspringLauncher/Views/NewVersionAvailableDialog.axaml.cs
================================================
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using WinterspringLauncher.UiElements;
namespace WinterspringLauncher.Views;
public partial class NewVersionAvailableDialog : Window
{
public string NewVersion { get; set; }
public NewVersionAvailableDialog(LauncherVersion.UpdateInformation updateInformation)
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
TextBlock version = this.Find<TextBlock>("VersionIndicator")!;
HyperlinkTextBlock dlLinkIndicator = this.Find<HyperlinkTextBlock>("DlLinkIndicator")!;
version.Text = updateInformation.VersionName;
dlLinkIndicator.NavigateUri = updateInformation.URLLinkToReleasePage;
dlLinkIndicator.Text = updateInformation.URLLinkToReleasePage;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void CloseButtonClick(object? sender, RoutedEventArgs e)
{
Close();
}
}
================================================
FILE: WinterspringLauncher/WinterspringLauncher.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType Condition="'$(Configuration)' == 'Debug'">Exe</OutputType> <!-- With CMD window for debugging -->
<OutputType Condition="'$(Configuration)' != 'Debug'">WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets/icons/winterspring-launcher-icon.ico</ApplicationIcon>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<DebugType>embedded</DebugType>
<Authors>_BLU</Authors>
</PropertyGroup>
<PropertyGroup Condition="'$(UsePublishBuildSettings)' == 'true'">
<!-- Build/Publish config -->
<!-- we HAVE to set some runtime :( We overwrite this with 'use-current-runtime' inside build process -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract Condition="$([MSBuild]::IsOSPlatform('Windows'))">true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.5" />
<PackageReference Include="Avalonia.Themes.Simple" Version="11.0.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.5" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.5" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Assets\icons\language\chinese.png" />
<UpToDateCheckInput Remove="Assets\icons\language\english.png" />
<UpToDateCheckInput Remove="Assets\icons\language\source.txt" />
<UpToDateCheckInput Remove="Assets\language\chinese.png" />
<UpToDateCheckInput Remove="Assets\language\english.png" />
<UpToDateCheckInput Remove="Assets\language\source.txt" />
</ItemGroup>
<PropertyGroup>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<DefineConstants>PLATFORM_WINDOWS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="6.0.0-beta.3">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<EmbeddedResource Include="7z.dll" />
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.5.0.366" />
</ItemGroup>
</Project>
================================================
FILE: WinterspringLauncher/app.manifest
================================================
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AvaloniaUiTesting2.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
================================================
FILE: WinterspringLauncher.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinterspringLauncher", "WinterspringLauncher\WinterspringLauncher.csproj", "{605A76A1-9D23-4C31-8699-FC89E4B5394A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{605A76A1-9D23-4C31-8699-FC89E4B5394A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{605A76A1-9D23-4C31-8699-FC89E4B5394A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{605A76A1-9D23-4C31-8699-FC89E4B5394A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{605A76A1-9D23-4C31-8699-FC89E4B5394A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
================================================
FILE: WinterspringLauncher.sln.DotSettings
================================================
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Arctium/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Everlook/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=realmlist/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Winterspring/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
================================================
FILE: global.json
================================================
{
"sdk": {
"version": "7.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
}
}
gitextract_9npmrwx3/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ └── Build_Launcher.yml ├── .gitignore ├── GitVersion.yml ├── LICENSE ├── MacAppBuilding/ │ ├── .gitignore │ ├── AppTemplate/ │ │ ├── AppIcon.icns │ │ ├── Resources/ │ │ │ └── Info.plist │ │ ├── WinterspringLauncherTerminal │ │ └── launch_wrapper │ ├── build_app.sh │ └── build_dmg.sh ├── README.md ├── WinterspringLauncher/ │ ├── App.axaml │ ├── App.axaml.cs │ ├── Assets/ │ │ ├── Resources.axaml │ │ ├── icons/ │ │ │ └── language-icons/ │ │ │ └── source.txt │ │ └── translations/ │ │ └── en.json │ ├── LauncherActions.cs │ ├── LauncherConfig.cs │ ├── LauncherLogic.OpenGameFolder.cs │ ├── LauncherLogic.StartGame.cs │ ├── LauncherLogic.cs │ ├── LauncherUpdateHandler.cs │ ├── LauncherVersion.cs │ ├── LocaleDefaults.cs │ ├── ProgramStartup.cs │ ├── UiElements/ │ │ └── HyperlinkSpan.cs │ ├── Utils/ │ │ ├── ArchiveCompression.cs │ │ ├── BinaryPatchHandler.cs │ │ ├── DirectoryCopy.cs │ │ ├── GitHubApi.cs │ │ ├── HashHelper.cs │ │ ├── ProgressiveFileDownloader.cs │ │ ├── SimpleFileDownloader.cs │ │ ├── UnixApi.cs │ │ └── UtilHelper.cs │ ├── ViewModels/ │ │ └── MainWindowViewModel.cs │ ├── Views/ │ │ ├── MainWindow.axaml │ │ ├── MainWindow.axaml.cs │ │ ├── NewVersionAvailableDialog.axaml │ │ └── NewVersionAvailableDialog.axaml.cs │ ├── WinterspringLauncher.csproj │ └── app.manifest ├── WinterspringLauncher.sln ├── WinterspringLauncher.sln.DotSettings └── global.json
SYMBOL INDEX (116 symbols across 23 files)
FILE: WinterspringLauncher/App.axaml.cs
class App (line 9) | public partial class App : Application
method Initialize (line 11) | public override void Initialize()
method OnFrameworkInitializationCompleted (line 16) | public override void OnFrameworkInitializationCompleted()
FILE: WinterspringLauncher/LauncherActions.cs
class LauncherActions (line 14) | public static class LauncherActions
method DownloadFile (line 19) | public static void DownloadFile(string downloadUrl, string downloadDes...
method PrepareGameConfigWtf (line 36) | public static void PrepareGameConfigWtf(string gamePath, string portal...
method Unpack (line 87) | public static void Unpack(string compressedArchivePath, string targetD...
method StartHermesProxy (line 98) | public static Process StartHermesProxy(string hermesDir, ushort modern...
method StartGame (line 144) | public static void StartGame(string executablePath)
FILE: WinterspringLauncher/LauncherConfig.cs
type OperatingSystem (line 12) | public enum OperatingSystem
class VersionedBaseConfig (line 18) | public class VersionedBaseConfig
class LauncherConfig (line 23) | public class LauncherConfig : VersionedBaseConfig
class ServerInfo (line 100) | public class ServerInfo
class InstallationLocation (line 109) | public class InstallationLocation
method GetDefaultConfig (line 118) | public static LauncherConfig GetDefaultConfig() => new LauncherConfig();
method SaveConfig (line 120) | public void SaveConfig(string configPath)
method LoadOrCreateDefault (line 130) | public static LauncherConfig LoadOrCreateDefault(string configPath)
method PatchConfigIfNeeded (line 159) | private static string PatchConfigIfNeeded(string currentConfig)
method TryUpgradeOldGameFolder (line 215) | private static void TryUpgradeOldGameFolder(string oldGameFolder, stri...
class LegacyV1Config (line 252) | private class LegacyV1Config : VersionedBaseConfig
FILE: WinterspringLauncher/LauncherLogic.OpenGameFolder.cs
class LauncherLogic (line 10) | public partial class LauncherLogic
method OpenGameFolder (line 12) | public void OpenGameFolder()
FILE: WinterspringLauncher/LauncherLogic.StartGame.cs
class LauncherLogic (line 13) | public partial class LauncherLogic
method StartGame (line 17) | public void StartGame()
method UpdateHermesProxyIfNecessary (line 283) | private async Task UpdateHermesProxyIfNecessary()
FILE: WinterspringLauncher/LauncherLogic.cs
class LauncherLogic (line 16) | public partial class LauncherLogic
method FullPath (line 23) | private string FullPath(string subPath) => Path.GetFullPath(Path.Combi...
method LauncherLogic (line 33) | public LauncherLogic(MainWindowViewModel model)
method CreateUpdatePopup (line 96) | private void CreateUpdatePopup(LauncherVersion.UpdateInformation updat...
method ChangeServerIdx (line 108) | public void ChangeServerIdx()
method RunDownload (line 137) | private void RunDownload(string downloadUrl, string destLocation)
method RunUnpack (line 151) | private void RunUnpack(string archiveLocation, string targetDir)
method KillHermesProxy (line 161) | public void KillHermesProxy()
FILE: WinterspringLauncher/LauncherUpdateHandler.cs
class LauncherUpdateHandler (line 9) | public static class LauncherUpdateHandler
method HandleStartArguments (line 11) | public static bool/*exitNow*/ HandleStartArguments(string[] args)
method AttachConsole (line 82) | [DllImport("kernel32.dll")]
method CreateTerminalWindowIfPossible (line 87) | private static void CreateTerminalWindowIfPossible()
FILE: WinterspringLauncher/LauncherVersion.cs
class LauncherVersion (line 7) | public static class LauncherVersion
method CheckIfUpdateIsAvailable (line 29) | public static bool CheckIfUpdateIsAvailable([NotNullWhen(true)] out Up...
class UpdateInformation (line 60) | public class UpdateInformation
FILE: WinterspringLauncher/LocaleDefaults.cs
class LocaleDefaults (line 6) | public static class LocaleDefaults
method GetBestWoWConfigLocale (line 10) | public static string GetBestWoWConfigLocale()
method GetBestGitHubMirror (line 15) | public static string? GetBestGitHubMirror()
method GetBestServerName (line 20) | public static string GetBestServerName()
FILE: WinterspringLauncher/ProgramStartup.cs
class ProgramStartup (line 12) | class ProgramStartup
method Main (line 17) | [STAThread]
method BuildAvaloniaApp (line 46) | public static AppBuilder BuildAvaloniaApp()
FILE: WinterspringLauncher/UiElements/HyperlinkSpan.cs
class HyperlinkTextBlock (line 8) | public class HyperlinkTextBlock : TextBlock
method HyperlinkTextBlock (line 24) | public HyperlinkTextBlock()
method OnPointerPressed (line 32) | private void OnPointerPressed(object sender, PointerPressedEventArgs e)
FILE: WinterspringLauncher/Utils/ArchiveCompression.cs
class ArchiveCompression (line 13) | public static class ArchiveCompression
method Decompress (line 17) | public static void Decompress(string archiveFilePath, string extractio...
method DecompressZipWithProgress (line 39) | private static void DecompressZipWithProgress(string archiveFilePath, ...
method Decompress7ZWithProgress (line 62) | private static void Decompress7ZWithProgress(string archiveFilePath, s...
method Decompress7ZWithProgress (line 67) | private static void Decompress7ZWithProgress(string archiveFilePath, s...
method DecompressSmartSkipFirstFolder (line 137) | public static void DecompressSmartSkipFirstFolder(string zipFilePath, ...
method IsFolder (line 158) | private static bool IsFolder(this ZipArchiveEntry entry)
method GetBaseFolderFromZip (line 163) | static string? GetBaseFolderFromZip(ZipArchive archive)
FILE: WinterspringLauncher/Utils/BinaryPatchHandler.cs
class BinaryPatchHandler (line 9) | public class BinaryPatchHandler
class PatchSummary (line 11) | public class PatchSummary
class PatchSummaryEntry (line 19) | public class PatchSummaryEntry
method ApplyPatch (line 35) | public static void ApplyPatch(byte[] patchFileContent, string sourceFi...
method VerifySignatureOrThrow (line 99) | private static void VerifySignatureOrThrow(byte[] bytesToVerify, byte[...
FILE: WinterspringLauncher/Utils/DirectoryCopy.cs
class DirectoryCopy (line 6) | public static class DirectoryCopy
method Copy (line 8) | public static void Copy(string sourceDirectory, string targetDirectory)
method CopyAll (line 16) | public static void CopyAll(DirectoryInfo source, DirectoryInfo target)
FILE: WinterspringLauncher/Utils/GitHubApi.cs
class GitHubApi (line 11) | public static class GitHubApi
method LatestReleaseVersion (line 15) | public static GitHubReleaseInfo LatestReleaseVersion(string repoName)
method PerformWebRequest (line 22) | private static TJsonResponse PerformWebRequest<TJsonResponse>(string u...
class GitHubReleaseInfo (line 47) | public class GitHubReleaseInfo
class Asset (line 61) | public class Asset
FILE: WinterspringLauncher/Utils/HashHelper.cs
class HashHelper (line 8) | public class HashHelper
method CreateHexSha256HashFromFilename (line 10) | public static string CreateHexSha256HashFromFilename(string filePath)
method CreateHexSha256HashFromFileBytes (line 20) | public static string CreateHexSha256HashFromFileBytes(byte[] fileContent)
method ConvertBinarySha256ToHex (line 29) | public static string ConvertBinarySha256ToHex(byte[] binarySha256Hash)
FILE: WinterspringLauncher/Utils/ProgressiveFileDownloader.cs
class ProgressiveFileDownloader (line 10) | public class ProgressiveFileDownloader : IDisposable
method ProgressiveFileDownloader (line 35) | public ProgressiveFileDownloader(string downloadUrl, string destinatio...
method StartGetDownload (line 44) | public async Task StartGetDownload()
method DownloadFileFromHttpResponseMessage (line 50) | private async Task DownloadFileFromHttpResponseMessage(HttpResponseMes...
method ProcessContentStream (line 65) | private async Task ProcessContentStream(Stream contentStream)
method TriggerInitialInfo (line 105) | private void TriggerInitialInfo(long? totalDownloadSize)
method UpdateInternalProgress (line 110) | private void UpdateInternalProgress(long alreadyReceivedBytes)
method TimerElapsed (line 115) | private void TimerElapsed(object? sender, ElapsedEventArgs e)
method UpdateAndTriggerProgressChanged (line 120) | private void UpdateAndTriggerProgressChanged()
method TriggerProgressChanged (line 144) | private void TriggerProgressChanged()
method TriggerDownloadDone (line 150) | private void TriggerDownloadDone(long bytesDownloaded)
method Dispose (line 155) | public void Dispose()
FILE: WinterspringLauncher/Utils/SimpleFileDownloader.cs
class SimpleFileDownloader (line 8) | public static class SimpleFileDownloader
method PerformGetStringRequest (line 10) | public static string PerformGetStringRequest(string url)
method PerformGetJsonRequest (line 20) | public static TJsonResponse PerformGetJsonRequest<TJsonResponse>(strin...
method PerformGetBytesRequest (line 32) | public static byte[] PerformGetBytesRequest(string url)
FILE: WinterspringLauncher/Utils/UnixApi.cs
class UnixApi (line 5) | public static class UnixApi
method chmod (line 7) | [DllImport("libc", SetLastError = true)]
FILE: WinterspringLauncher/Utils/UtilHelper.cs
class UtilHelper (line 5) | public static class UtilHelper
method ToHumanFileSize (line 8) | public static string ToHumanFileSize(long sizeInByte)
method ReplaceFirstOccurrence (line 23) | public static string ReplaceFirstOccurrence(this string source, string...
FILE: WinterspringLauncher/ViewModels/MainWindowViewModel.cs
class MainWindowViewModel (line 11) | public partial class MainWindowViewModel : ObservableObject
method MainWindowViewModel (line 15) | public MainWindowViewModel()
method SetHermesPid (line 56) | public void SetHermesPid(int? pid)
method SetHermesVersion (line 63) | public void SetHermesVersion(string? versionStr)
method AddLogEntry (line 76) | public void AddLogEntry(string logEntry)
class LanguageHolder (line 86) | public class LanguageHolder
method SetLanguage (line 88) | public void SetLanguage(string languageShortName)
method SetProgressbar (line 110) | public void SetProgressbar(string title, double progressPercent, IBrus...
method UpdateProgress (line 119) | public void UpdateProgress(double progressPercent, string additionalText)
class ProgressbarInternalTimeTracker (line 130) | private class ProgressbarInternalTimeTracker
method GetEstimatedTimeAndUpdateRates (line 137) | public TimeSpan? GetEstimatedTimeAndUpdateRates(double percent)
FILE: WinterspringLauncher/Views/MainWindow.axaml.cs
class MainWindow (line 9) | public partial class MainWindow : Window
method MainWindow (line 17) | public MainWindow()
method LogChanged (line 23) | private void LogChanged(object? sender, AvaloniaPropertyChangedEventAr...
method ServerSelectionChanged (line 28) | private void ServerSelectionChanged(object? sender, SelectionChangedEv...
method OnClosing (line 37) | protected override void OnClosing(WindowClosingEventArgs e)
FILE: WinterspringLauncher/Views/NewVersionAvailableDialog.axaml.cs
class NewVersionAvailableDialog (line 9) | public partial class NewVersionAvailableDialog : Window
method NewVersionAvailableDialog (line 13) | public NewVersionAvailableDialog(LauncherVersion.UpdateInformation upd...
method InitializeComponent (line 28) | private void InitializeComponent()
method CloseButtonClick (line 33) | private void CloseButtonClick(object? sender, RoutedEventArgs e)
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (136K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 280,
"preview": "---\nname: Bug report\nabout: Launcher bug report\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\nWait a moment,\nare you sure you"
},
{
"path": ".github/workflows/Build_Launcher.yml",
"chars": 2723,
"preview": "name: Build Launcher\n\non: ['push']\n\nenv:\n DOTNET_VERSION: '7.0.x'\n\njobs:\n build_windows:\n strategy:\n matrix:\n "
},
{
"path": ".gitignore",
"chars": 9237,
"preview": "# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,rider,dotnetcore\n# Edit at ht"
},
{
"path": "GitVersion.yml",
"chars": 39,
"preview": "branches:\n main:\n regex: ^stable$\n"
},
{
"path": "LICENSE",
"chars": 1087,
"preview": "MIT License\n\nCopyright (c) 2023 _BLU (https://github.com/0blu)\n\nPermission is hereby granted, free of charge, to any per"
},
{
"path": "MacAppBuilding/.gitignore",
"chars": 21,
"preview": "output/\noutput_dmg/\n\n"
},
{
"path": "MacAppBuilding/AppTemplate/Resources/Info.plist",
"chars": 524,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "MacAppBuilding/AppTemplate/WinterspringLauncherTerminal",
"chars": 169,
"preview": "#!/bin/sh\n\nCURRENTPATH=`dirname \"${0}\"`\n\n# Resize terminal\nprintf '\\e[8;27;110t'\n\nclear\n\ncd \"$CURRENTPATH\"\n\nDYLD_LIBRARY"
},
{
"path": "MacAppBuilding/AppTemplate/launch_wrapper",
"chars": 90,
"preview": "#!/bin/sh\n\nCURRENTPATH=`dirname \"${0}\"`\n\nopen \"$CURRENTPATH/WinterspringLauncherTerminal\"\n"
},
{
"path": "MacAppBuilding/build_app.sh",
"chars": 1292,
"preview": "#!/bin/sh\n\nset -e\n\nif [ $# -ne 2 ]; then\n echo \"Error: Invalid arguments\"\n echo \"Run: buid_app.sh <version> <Winte"
},
{
"path": "MacAppBuilding/build_dmg.sh",
"chars": 541,
"preview": "#!/bin/sh\n\nset -e\n\nif [ ! -d \"output\" ]; then\n echo \"Error: no 'output' directory\"\n exit 1\nfi\n\nsips --setProperty "
},
{
"path": "README.md",
"chars": 3960,
"preview": "<figure>\n <img align=\"right\" src=\"./winterspring-launcher-icon.png\" alt=\"icon\">\n</figure>\n\n# Winterspring Launcher\nAl"
},
{
"path": "WinterspringLauncher/App.axaml",
"chars": 678,
"preview": "<Application xmlns=\"https://github.com/avaloniaui\"\n xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n "
},
{
"path": "WinterspringLauncher/App.axaml.cs",
"chars": 735,
"preview": "using Avalonia;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.Markup.Xaml;\nusing WinterspringLauncher.Vie"
},
{
"path": "WinterspringLauncher/Assets/Resources.axaml",
"chars": 157,
"preview": "<ResourceDictionary xmlns=\"https://github.com/avaloniaui\"\n xmlns:x=\"http://schemas.microsoft.com/win"
},
{
"path": "WinterspringLauncher/Assets/icons/language-icons/source.txt",
"chars": 47,
"preview": "https://www.flaticon.com/packs/countrys-flags\n"
},
{
"path": "WinterspringLauncher/Assets/translations/en.json",
"chars": 35,
"preview": "{\n \"start_game\": \"Start Game\"\n}\n"
},
{
"path": "WinterspringLauncher/LauncherActions.cs",
"chars": 6785,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Globalization;\nusing System.IO;\n"
},
{
"path": "WinterspringLauncher/LauncherConfig.cs",
"chars": 11545,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.InteropService"
},
{
"path": "WinterspringLauncher/LauncherLogic.OpenGameFolder.cs",
"chars": 1639,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing Syst"
},
{
"path": "WinterspringLauncher/LauncherLogic.StartGame.cs",
"chars": 16815,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing Sys"
},
{
"path": "WinterspringLauncher/LauncherLogic.cs",
"chars": 6808,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.InteropService"
},
{
"path": "WinterspringLauncher/LauncherUpdateHandler.cs",
"chars": 2946,
"preview": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Runtime.InteropServices;\nusing System.Threading;\n"
},
{
"path": "WinterspringLauncher/LauncherVersion.cs",
"chars": 2276,
"preview": "using System;\nusing System.Diagnostics.CodeAnalysis;\nusing WinterspringLauncher.Utils;\n\nnamespace WinterspringLauncher;"
},
{
"path": "WinterspringLauncher/LocaleDefaults.cs",
"chars": 705,
"preview": "using System;\nusing System.Globalization;\n\nnamespace WinterspringLauncher;\n\npublic static class LocaleDefaults\n{\n pu"
},
{
"path": "WinterspringLauncher/ProgramStartup.cs",
"chars": 1690,
"preview": "using Avalonia;\nusing System;\nusing System.Globalization;\nusing System.IO;\nusing System.Linq;\nusing System.Runtime.Inte"
},
{
"path": "WinterspringLauncher/UiElements/HyperlinkSpan.cs",
"chars": 1228,
"preview": "using Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Input;\nusing Avalonia.Media;\n\nnamespace WinterspringLauncher.Ui"
},
{
"path": "WinterspringLauncher/Utils/ArchiveCompression.cs",
"chars": 7347,
"preview": "using System;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Linq;\nusing System.Reflection;\n\n#if PLATFORM_WI"
},
{
"path": "WinterspringLauncher/Utils/BinaryPatchHandler.cs",
"chars": 5010,
"preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Security.Cryptography;\nusing System.Text.Json.Serializat"
},
{
"path": "WinterspringLauncher/Utils/DirectoryCopy.cs",
"chars": 1092,
"preview": "using System;\nusing System.IO;\n\nnamespace WinterspringLauncher.Utils;\n\npublic static class DirectoryCopy\n{\n public s"
},
{
"path": "WinterspringLauncher/Utils/GitHubApi.cs",
"chars": 2402,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Data;\nusing System.Net;\nusing System.Net.Http;\nusing Syste"
},
{
"path": "WinterspringLauncher/Utils/HashHelper.cs",
"chars": 1208,
"preview": "using System;\nusing System.IO;\nusing System.Security.Cryptography;\nusing System.Text;\n\nnamespace WinterspringLauncher.U"
},
{
"path": "WinterspringLauncher/Utils/ProgressiveFileDownloader.cs",
"chars": 5164,
"preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing System.Tim"
},
{
"path": "WinterspringLauncher/Utils/SimpleFileDownloader.cs",
"chars": 1494,
"preview": "using System;\nusing System.Data;\nusing System.Net.Http;\nusing System.Text.Json;\n\nnamespace WinterspringLauncher.Utils;\n"
},
{
"path": "WinterspringLauncher/Utils/UnixApi.cs",
"chars": 829,
"preview": "using System.Runtime.InteropServices;\n\nnamespace WinterspringLauncher.Utils;\n\npublic static class UnixApi\n{\n [DllImpo"
},
{
"path": "WinterspringLauncher/Utils/UtilHelper.cs",
"chars": 958,
"preview": "using System;\n\nnamespace WinterspringLauncher.Utils;\n\npublic static class UtilHelper\n{\n // Converts some arbitrary b"
},
{
"path": "WinterspringLauncher/ViewModels/MainWindowViewModel.cs",
"chars": 5195,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Linq;\nusing Avalonia"
},
{
"path": "WinterspringLauncher/Views/MainWindow.axaml",
"chars": 13480,
"preview": "<Window xmlns=\"https://github.com/avaloniaui\"\n xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n xml"
},
{
"path": "WinterspringLauncher/Views/MainWindow.axaml.cs",
"chars": 1038,
"preview": "using System;\nusing System.Threading.Tasks;\nusing Avalonia;\nusing Avalonia.Controls;\nusing WinterspringLauncher.ViewMode"
},
{
"path": "WinterspringLauncher/Views/NewVersionAvailableDialog.axaml",
"chars": 1510,
"preview": "<Window xmlns=\"https://github.com/avaloniaui\"\n xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n xm"
},
{
"path": "WinterspringLauncher/Views/NewVersionAvailableDialog.axaml.cs",
"chars": 1026,
"preview": "using Avalonia;\nusing Avalonia.Controls;\nusing Avalonia.Interactivity;\nusing Avalonia.Markup.Xaml;\nusing WinterspringLa"
},
{
"path": "WinterspringLauncher/WinterspringLauncher.csproj",
"chars": 3196,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n <PropertyGroup>\n <OutputType Condition=\"'$(Configuration)' == 'Debug'\">Exe"
},
{
"path": "WinterspringLauncher/app.manifest",
"chars": 917,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n <!--"
},
{
"path": "WinterspringLauncher.sln",
"chars": 1012,
"preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.0.3190"
},
{
"path": "WinterspringLauncher.sln.DotSettings",
"chars": 691,
"preview": "<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namesp"
},
{
"path": "global.json",
"chars": 106,
"preview": "{\n \"sdk\": {\n \"version\": \"7.0.0\",\n \"rollForward\": \"latestMinor\",\n \"allowPrerelease\": false\n }\n}\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the 0blu/WinterspringLauncher GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (124.7 KB), approximately 29.5k tokens, and a symbol index with 116 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.