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