commit dda5edfbc6e2720a42fdb00d29b0203b4e483afe Author: Dev Date: Fri Mar 3 19:25:33 2023 +0000 Add repo diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a1e1e97 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f5eeee --- /dev/null +++ b/.gitignore @@ -0,0 +1,357 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# EmuTarkov +[Bb]uild/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Jetbrains IDEs database +.idea/ + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2ba409c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,31 @@ +# NCSA Open Source License + +Copyright (c) 2022 SPT-AKI. All rights reserved. + +Developed by: SPT-AKI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +with the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +* Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimers. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimers in the documentation +and/or other materials provided with the distribution. + +* Neither the names of SPT-AKI, nor the names of its contributors may be used +to endorse or promote products derived from this Software without specific +prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS WITH THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..042c0d1 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Launcher + +Custom launcher for Escape From Tarkov to start the game in offline mode + +**Project** | **Function** +------------------ | -------------------------------------------- +Aki.Build | Build script +Aki.ByteBanger | Assembly-CSharp.dll patcher +Aki.Launcher | Launcher frontend +Aki.Launcher.Base | Launcher backend + +## Requirements + +- Escape From Tarkov 19765 +- .NET 6 SDK +- Visual Studio Code + +### For UI Development + +- Visual Studio Community 2022 (.NET desktop workload) +- Avalonia Visual Studio Extension + +## Build + +1. Open Launcher.code-workspace in Visual Studio Code. +2. Run the build task: (top toolbar) Terminal -> Run Build Task... +3. Copy-paste all files inside `Build` into `game root directory`, overwrite when prompted. diff --git a/project/.config/dotnet-tools.json b/project/.config/dotnet-tools.json new file mode 100644 index 0000000..31e896e --- /dev/null +++ b/project/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "cake.tool": { + "version": "2.0.0", + "commands": [ + "dotnet-cake" + ] + } + } +} \ No newline at end of file diff --git a/project/Aki.Build/Aki.Build.csproj b/project/Aki.Build/Aki.Build.csproj new file mode 100644 index 0000000..7168f27 --- /dev/null +++ b/project/Aki.Build/Aki.Build.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + win10-x64 + + + + + + + + + + + + + diff --git a/project/Aki.ByteBanger/Aki.ByteBanger.csproj b/project/Aki.ByteBanger/Aki.ByteBanger.csproj new file mode 100644 index 0000000..fff7812 --- /dev/null +++ b/project/Aki.ByteBanger/Aki.ByteBanger.csproj @@ -0,0 +1,7 @@ + + + + net6.0 + + + diff --git a/project/Aki.ByteBanger/DiffResult.cs b/project/Aki.ByteBanger/DiffResult.cs new file mode 100644 index 0000000..1ba7fcf --- /dev/null +++ b/project/Aki.ByteBanger/DiffResult.cs @@ -0,0 +1,28 @@ +namespace Aki.ByteBanger +{ + public class DiffResult + { + public DiffResultType Result { get; } + public PatchInfo PatchInfo { get; } + + public DiffResult(DiffResultType result, PatchInfo patchInfo) + { + Result = result; + PatchInfo = patchInfo; + } + } + + public enum DiffResultType + { + Success, + OriginalFilePathInvalid, + OriginalFileNotFound, + OriginalFileReadFailed, + + PatchedFilePathInvalid, + PatchedFileNotFound, + PatchedFileReadFailed, + + FilesMatch + } +} diff --git a/project/Aki.ByteBanger/Docs/bpf-layout.md b/project/Aki.ByteBanger/Docs/bpf-layout.md new file mode 100644 index 0000000..a4b19c6 --- /dev/null +++ b/project/Aki.ByteBanger/Docs/bpf-layout.md @@ -0,0 +1,36 @@ +# ByteBanger Binary Layout V 1.0 + +```unformatted +42 59 42 41 # Identifier "BYBA" for ByteBanger +01 00 # File version 1.0 + +00 89 54 98 # Original file length, Int32 +00 01 02 03 04 05 06 07 # -\ +08 09 0A 0B 0C 0D 0E 0F # | Original checksum, 32 Bytes +10 11 12 13 14 15 16 17 # | SHA-256 +18 19 1A 1B 1C 1D 1E 1F # -/ +00 87 B8 00 # Patched file length, Int32 +20 21 22 23 24 25 26 27 # -\ +28 29 2A 2B 2C 2D 2E 2F # | Original checksum, 32 Bytes +30 31 32 33 34 35 36 37 # | SHA-256 +38 39 3A 3B 3C 3D 3E 3F # -/ +00 00 00 04 # Count of patch items, Int32 + +00 00 00 9D # Offset from file start, Int32 +00 00 00 04 # Patch content length +70 71 72 73 # Patch content + +00 00 00 A8 # Offset, Int32 +00 00 00 04 # Patch content length +80 81 82 83 # Content + +00 00 00 B1 # Offset +00 00 00 04 # Patch content length +90 91 92 93 # Content + +00 00 00 D1 # Offset +00 00 00 04 # Patch content length +A0 A1 A2 A3 # Content + +Binary Length: 204 Bytes +``` diff --git a/project/Aki.ByteBanger/PatchInfo.cs b/project/Aki.ByteBanger/PatchInfo.cs new file mode 100644 index 0000000..69b6d18 --- /dev/null +++ b/project/Aki.ByteBanger/PatchInfo.cs @@ -0,0 +1,103 @@ +/* PatchInfo.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Basuro + */ + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Aki.ByteBanger +{ + public class PatchInfo + { + public const string BYBA = "BYBA"; + public byte[] OriginalChecksum { get; set; } + public int OriginalLength { get; set; } + public byte[] PatchedChecksum { get; set; } + public int PatchedLength { get; set; } + public PatchItem[] Items { get; set; } + + public static PatchInfo FromBytes(byte[] bytes) + { + if (bytes.Length < 82) throw new Exception("Input data too short, cannot be a valid patch"); + + PatchInfo pi = new PatchInfo(); + + using (MemoryStream ms = new MemoryStream(bytes)) + using (BinaryReader br = new BinaryReader(ms)) + { + byte[] buf = null; + + buf = br.ReadBytes(4); + if (Encoding.ASCII.GetString(buf) != BYBA) throw new Exception("Invalid identifier"); + + if (br.ReadByte() != 1) throw new Exception("Invalid major file version (1 expected)"); + if (br.ReadByte() != 0) throw new Exception("Invalid minor file version (0 expected)"); + + pi.OriginalLength = br.ReadInt32(); + pi.OriginalChecksum = br.ReadBytes(32); + pi.PatchedLength = br.ReadInt32(); + pi.PatchedChecksum = br.ReadBytes(32); + + int itemCount = br.ReadInt32(); + + List items = new List(); + for (int i = 0; i < itemCount; i++) + items.Add(PatchItem.FromReader(br)); + pi.Items = items.ToArray(); + } + + return pi; + } + + public byte[] ToBytes() + { + byte[] data; + + using (MemoryStream ms = new MemoryStream()) + { + using (BinaryWriter bw = new BinaryWriter(ms, Encoding.ASCII, true)) + { + // identifier "BYBA" // 4B + byte[] byba = Encoding.ASCII.GetBytes(BYBA); + bw.Write(byba, 0, byba.Length); + + // version "1.0" // 2B + bw.Write((byte)1); + bw.Write((byte)0); + + // original len // 4B + bw.Write(OriginalLength); + + // original chk // 32B + bw.Write(OriginalChecksum, 0, OriginalChecksum.Length); + + // patched len // 4B + bw.Write(PatchedLength); + + // patched chk // 32B + bw.Write(PatchedChecksum, 0, PatchedChecksum.Length); + + // item count // 4B + bw.Write(Items.Length); + + // data + foreach (PatchItem pi in Items) + pi.ToWriter(bw); + } + + data = new byte[ms.Length]; + ms.Seek(0, SeekOrigin.Begin); + ms.Read(data, 0, (int)ms.Length); + } + + return data; + } + } +} diff --git a/project/Aki.ByteBanger/PatchItem.cs b/project/Aki.ByteBanger/PatchItem.cs new file mode 100644 index 0000000..086517a --- /dev/null +++ b/project/Aki.ByteBanger/PatchItem.cs @@ -0,0 +1,44 @@ +/* PatchItem.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Basuro + */ + + +using System.IO; + +namespace Aki.ByteBanger +{ + public class PatchItem + { + public int Offset { get; set; } + public byte[] Data { get; set; } + + public static PatchItem FromReader(BinaryReader br) + { + int offset = br.ReadInt32(); + int dataLength = br.ReadInt32(); + byte[] data = br.ReadBytes(dataLength); + + return new PatchItem + { + Offset = offset, + Data = data + }; + } + + internal void ToWriter(BinaryWriter bw) + { + // offset // 4B + bw.Write(Offset); + + // length // 4B + bw.Write(Data.Length); + + // data // xB + bw.Write(Data, 0, Data.Length); + } + } +} diff --git a/project/Aki.ByteBanger/PatchResult.cs b/project/Aki.ByteBanger/PatchResult.cs new file mode 100644 index 0000000..164b340 --- /dev/null +++ b/project/Aki.ByteBanger/PatchResult.cs @@ -0,0 +1,26 @@ +namespace Aki.ByteBanger +{ + public class PatchResult + { + public PatchResultType Result { get; } + public byte[] PatchedData { get; } + + public PatchResult(PatchResultType result, byte[] patchedData) + { + Result = result; + PatchedData = patchedData; + } + } + + public enum PatchResultType + { + Success, + + InputLengthMismatch, + InputChecksumMismatch, + + AlreadyPatched, + + OutputChecksumMismatch + } +} diff --git a/project/Aki.ByteBanger/PatchUtil.cs b/project/Aki.ByteBanger/PatchUtil.cs new file mode 100644 index 0000000..f5b67b5 --- /dev/null +++ b/project/Aki.ByteBanger/PatchUtil.cs @@ -0,0 +1,137 @@ +/* BB.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Basuro + */ + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; + +namespace Aki.ByteBanger +{ + public static class PatchUtil + { + public static DiffResult Diff(byte[] original, byte[] patched) + { + PatchInfo pi = new PatchInfo + { + OriginalLength = original.Length, + PatchedLength = patched.Length + }; + + using (SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider()) + { + pi.OriginalChecksum = sha256.ComputeHash(original); + pi.PatchedChecksum = sha256.ComputeHash(patched); + } + + if ((pi.OriginalLength == pi.PatchedLength) && ArraysMatch(pi.OriginalChecksum, pi.PatchedChecksum)) + return new DiffResult(DiffResultType.FilesMatch, null); + + int minLength = Math.Min(pi.OriginalLength, pi.PatchedLength); + + List items = new List(); + List currentData = null; + int diffOffsetStart = 0; + + for (int i = 0; i < minLength; i++) + { + if (original[i] != patched[i]) + { + if (currentData == null) + { + diffOffsetStart = i; + currentData = new List(); + } + + currentData.Add(patched[i]); + } + else + { + if (currentData != null) + items.Add(new PatchItem { Offset = diffOffsetStart, Data = currentData.ToArray() }); + + currentData = null; + diffOffsetStart = 0; + } + } + + if (currentData != null) + items.Add(new PatchItem { Offset = diffOffsetStart, Data = currentData.ToArray() }); + + if (pi.PatchedLength > pi.OriginalLength) + { + byte[] buf = new byte[pi.PatchedLength - pi.OriginalLength]; + Array.Copy(patched, pi.OriginalLength, buf, 0, buf.Length); + items.Add(new PatchItem { Offset = pi.OriginalLength, Data = buf }); + } + + pi.Items = items.ToArray(); + + return new DiffResult(DiffResultType.Success, pi); + } + + public static DiffResult Diff(string originalFile, string patchedFile) + { + if (string.IsNullOrWhiteSpace(originalFile)) return new DiffResult(DiffResultType.OriginalFilePathInvalid, null); + if (string.IsNullOrWhiteSpace(patchedFile)) return new DiffResult(DiffResultType.PatchedFilePathInvalid, null); + if (!File.Exists(originalFile)) return new DiffResult(DiffResultType.OriginalFileNotFound, null); + if (!File.Exists(patchedFile)) return new DiffResult(DiffResultType.PatchedFileNotFound, null); + + byte[] originalData, patchedData; + + try { originalData = File.ReadAllBytes(originalFile); } + catch { return new DiffResult(DiffResultType.OriginalFileReadFailed, null); } + + try { patchedData = File.ReadAllBytes(patchedFile); } + catch { return new DiffResult(DiffResultType.PatchedFileReadFailed, null); } + + return Diff(originalData, patchedData); + } + + public static PatchResult Patch(byte[] input, PatchInfo pi) + { + byte[] inputHash; + using (SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider()) + { + inputHash = sha256.ComputeHash(input); + } + + if (ArraysMatch(inputHash, pi.PatchedChecksum)) return new PatchResult(PatchResultType.AlreadyPatched, null); + if (!ArraysMatch(inputHash, pi.OriginalChecksum)) return new PatchResult(PatchResultType.InputChecksumMismatch, null); + if (input.Length != pi.OriginalLength) return new PatchResult(PatchResultType.InputLengthMismatch, null); + + byte[] patchedData = new byte[pi.PatchedLength]; + long minLen = Math.Min(pi.OriginalLength, pi.PatchedLength); + Array.Copy(input, patchedData, minLen); + + foreach (PatchItem itm in pi.Items) + Array.Copy(itm.Data, 0, patchedData, itm.Offset, itm.Data.Length); + + byte[] patchedHash; + using (SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider()) + { + patchedHash = sha256.ComputeHash(patchedData); + } + + if (!ArraysMatch(patchedHash, pi.PatchedChecksum)) return new PatchResult(PatchResultType.OutputChecksumMismatch, null); + + return new PatchResult(PatchResultType.Success, patchedData); + } + + private static bool ArraysMatch(byte[] a, byte[] b) + { + if (a.Length != b.Length) return false; + + for (int i = 0; i < a.Length; i++) + if (a[i] != b[i]) return false; + + return true; + } + } +} diff --git a/project/Aki.Launcher.Base/Aki.Launcher.Base.csproj b/project/Aki.Launcher.Base/Aki.Launcher.Base.csproj new file mode 100644 index 0000000..7f8cf16 --- /dev/null +++ b/project/Aki.Launcher.Base/Aki.Launcher.Base.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + Aki.Launch + + + + + ..\Aki.Launcher\References\zlib.net.dll + + + + + + + + + + + + + diff --git a/project/Aki.Launcher.Base/Controllers/AccountManager.cs b/project/Aki.Launcher.Base/Controllers/AccountManager.cs new file mode 100644 index 0000000..e2ae19f --- /dev/null +++ b/project/Aki.Launcher.Base/Controllers/AccountManager.cs @@ -0,0 +1,286 @@ +/* AccountManager.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + * Merijn Hendriks + */ + + +using Aki.Launcher.Helpers; +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models.Aki; +using Aki.Launcher.Models.Launcher; +using System.Threading.Tasks; + +namespace Aki.Launcher +{ + public enum AccountStatus + { + OK = 0, + NoConnection = 1, + LoginFailed = 2, + RegisterFailed = 3, + UpdateFailed = 4 + } + + public static class AccountManager + { + private const string STATUS_FAILED = "FAILED"; + private const string STATUS_OK = "OK"; + public static AccountInfo SelectedAccount { get; private set; } = null; + public static ProfileInfo SelectedProfileInfo { get; private set; } = null; + + public static void Logout() => SelectedAccount = null; + + public static async Task LoginAsync(LoginModel Creds) + { + return await Task.Run(() => + { + return Login(Creds.Username, Creds.Password); + }); + } + + public static async Task LoginAsync(string username, string password) + { + return await Task.Run(() => + { + return Login(username, password); + }); + } + + public static AccountStatus Login(string username, string password) + { + LoginRequestData data = new LoginRequestData(username, password); + string id = STATUS_FAILED; + string json = ""; + + try + { + id = RequestHandler.RequestLogin(data); + + if (id == STATUS_FAILED) + { + return AccountStatus.LoginFailed; + } + + json = RequestHandler.RequestAccount(data); + } + catch + { + return AccountStatus.NoConnection; + } + + SelectedAccount = Json.Deserialize(json); + RequestHandler.ChangeSession(SelectedAccount.id); + + UpdateProfileInfo(); + + return AccountStatus.OK; + } + + public static void UpdateProfileInfo() + { + LoginRequestData data = new LoginRequestData(SelectedAccount.username, SelectedAccount.password); + string profileInfoJson = RequestHandler.RequestProfileInfo(data); + + if (profileInfoJson != null) + { + ServerProfileInfo serverProfileInfo = Json.Deserialize(profileInfoJson); + SelectedProfileInfo = new ProfileInfo(serverProfileInfo); + } + } + + public static ServerProfileInfo[] GetExistingProfiles() + { + string profileJsonArray = RequestHandler.RequestExistingProfiles(); + + if(profileJsonArray != null) + { + ServerProfileInfo[] miniProfiles = Json.Deserialize(profileJsonArray); + + if (miniProfiles != null && miniProfiles.Length > 0) + { + return miniProfiles; + } + } + + return new ServerProfileInfo[0]; + } + + public static async Task RegisterAsync(string username, string password, string edition) + { + return await Task.Run(() => + { + return Register(username, password, edition); + }); + } + + public static AccountStatus Register(string username, string password, string edition) + { + RegisterRequestData data = new RegisterRequestData(username, password, edition); + string registerStatus = STATUS_FAILED; + + try + { + registerStatus = RequestHandler.RequestRegister(data); + + if (registerStatus != STATUS_OK) + { + return AccountStatus.RegisterFailed; + } + } + catch + { + return AccountStatus.NoConnection; + } + + return Login(username, password); + } + + //only added incase wanted for future use. + public static async Task RemoveAsync() + { + return await Task.Run(() => + { + return Remove(); + }); + } + + public static AccountStatus Remove() + { + LoginRequestData data = new LoginRequestData(SelectedAccount.username, SelectedAccount.password); + + try + { + string json = RequestHandler.RequestRemove(data); + + if(Json.Deserialize(json)) + { + SelectedAccount = null; + + return AccountStatus.OK; + } + else + { + return AccountStatus.UpdateFailed; + } + } + catch + { + return AccountStatus.NoConnection; + } + } + + public static async Task ChangeUsernameAsync(string username) + { + return await Task.Run(() => + { + return ChangeUsername(username); + }); + } + + public static AccountStatus ChangeUsername(string username) + { + ChangeRequestData data = new ChangeRequestData(SelectedAccount.username, SelectedAccount.password, username); + string json = STATUS_FAILED; + + try + { + json = RequestHandler.RequestChangeUsername(data); + + if (json != STATUS_OK) + { + return AccountStatus.UpdateFailed; + } + } + catch + { + return AccountStatus.NoConnection; + } + + ServerSetting DefaultServer = LauncherSettingsProvider.Instance.Server; + + if (DefaultServer.AutoLoginCreds != null) + { + DefaultServer.AutoLoginCreds.Username = username; + } + + SelectedAccount.username = username; + LauncherSettingsProvider.Instance.SaveSettings(); + + return AccountStatus.OK; + } + + public static async Task ChangePasswordAsync(string password) + { + return await Task.Run(() => + { + return ChangePassword(password); + }); + } + public static AccountStatus ChangePassword(string password) + { + ChangeRequestData data = new ChangeRequestData(SelectedAccount.username, SelectedAccount.password, password); + string json = STATUS_FAILED; + + try + { + json = RequestHandler.RequestChangePassword(data); + + if (json != STATUS_OK) + { + return AccountStatus.UpdateFailed; + } + } + catch + { + return AccountStatus.NoConnection; + } + + ServerSetting DefaultServer = LauncherSettingsProvider.Instance.Server; + + if (DefaultServer.AutoLoginCreds != null) + { + DefaultServer.AutoLoginCreds.Password = password; + } + + SelectedAccount.password = password; + LauncherSettingsProvider.Instance.SaveSettings(); + + return AccountStatus.OK; + } + + public static async Task WipeAsync(string edition) + { + return await Task.Run(() => + { + return Wipe(edition); + }); + } + + public static AccountStatus Wipe(string edition) + { + RegisterRequestData data = new RegisterRequestData(SelectedAccount.username, SelectedAccount.password, edition); + string json = STATUS_FAILED; + + try + { + json = RequestHandler.RequestWipe(data); + + if (json != STATUS_OK) + { + return AccountStatus.UpdateFailed; + } + } + catch + { + return AccountStatus.NoConnection; + } + + SelectedAccount.edition = edition; + return AccountStatus.OK; + } + } +} diff --git a/project/Aki.Launcher.Base/Controllers/GameStarter.cs b/project/Aki.Launcher.Base/Controllers/GameStarter.cs new file mode 100644 index 0000000..3fc8d31 --- /dev/null +++ b/project/Aki.Launcher.Base/Controllers/GameStarter.cs @@ -0,0 +1,305 @@ +/* GameStarter.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + * reider123 + * Merijn Hendriks + */ + + +using Aki.Launcher.Helpers; +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models.Launcher; +using Microsoft.Win32; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Aki.Launcher.Controllers; +using Aki.Launcher.Interfaces; +using System.Runtime.InteropServices; + +namespace Aki.Launcher +{ + public class GameStarter + { + private readonly IGameStarterFrontend _frontend; + private readonly bool _showOnly; + private readonly string _originalGamePath; + private readonly string _gamePath; + private readonly string[] _excludeFromCleanup; + private const string registryInstall = @"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\EscapeFromTarkov"; + + private const string registrySettings = @"Software\Battlestate Games\EscapeFromTarkov"; + + public GameStarter(IGameStarterFrontend frontend, string gamePath = null, string originalGamePath = null, + bool showOnly = false, string[] excludeFromCleanup = null) + { + _frontend = frontend; + _showOnly = showOnly; + _gamePath = gamePath ?? LauncherSettingsProvider.Instance.GamePath ?? Environment.CurrentDirectory; + _originalGamePath = originalGamePath ??= DetectOriginalGamePath(); + _excludeFromCleanup = excludeFromCleanup ?? LauncherSettingsProvider.Instance.ExcludeFromCleanup; + } + + private static string DetectOriginalGamePath() + { + // We can't detect the installed path on non-Windows + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return null; + + var uninstallStringValue = Registry.LocalMachine.OpenSubKey(registryInstall, false) + ?.GetValue("UninstallString"); + var info = (uninstallStringValue is string key) ? new FileInfo(key) : null; + return info?.DirectoryName; + } + + public async Task LaunchGame(ServerInfo server, AccountInfo account) + { + // setup directories + if (IsInstalledInLive()) + { + LogManager.Instance.Warning("Failed installed in live check"); + return GameStarterResult.FromError(-1); + } + + SetupGameFiles(); + + if (!ValidationUtil.Validate()) + { + LogManager.Instance.Warning("Failed validation check"); + return GameStarterResult.FromError(-2); + } + + if (account.wipe) + { + RemoveRegistryKeys(); + CleanTempFiles(); + } + + // check game path + var clientExecutable = Path.Join(_gamePath, "EscapeFromTarkov.exe"); + + if (!File.Exists(clientExecutable)) + { + LogManager.Instance.Warning($"Could not find {clientExecutable}"); + return GameStarterResult.FromError(-6); + } + + // apply patches + ProgressReportingPatchRunner patchRunner = new ProgressReportingPatchRunner(_gamePath); + + try + { + await _frontend.CompletePatchTask(patchRunner.PatchFiles()); + } + catch (TaskCanceledException) + { + LogManager.Instance.Warning("Failed to apply assembly patch"); + return GameStarterResult.FromError(-4); + } + + //start game + var args = + $"-force-gfx-jobs native -token={account.id} -config={Json.Serialize(new ClientConfig(server.backendUrl))}"; + + if (_showOnly) + { + Console.WriteLine($"{clientExecutable} {args}"); + } + else + { + var clientProcess = new ProcessStartInfo(clientExecutable) + { + Arguments = args, + UseShellExecute = false, + WorkingDirectory = _gamePath, + }; + + Process.Start(clientProcess); + } + + return GameStarterResult.FromSuccess(); + } + + bool IsInstalledInLive() + { + var isInstalledInLive = false; + + try + { + var files = new FileInfo[] + { + // aki files + new FileInfo(Path.Combine(_originalGamePath, @"Aki.Launcher.exe")), + new FileInfo(Path.Combine(_originalGamePath, @"Aki.Server.exe")), + new FileInfo(Path.Combine(_originalGamePath, @"EscapeFromTarkov_Data\Managed\Aki.Build.dll")), + new FileInfo(Path.Combine(_originalGamePath, @"EscapeFromTarkov_Data\Managed\Aki.Common.dll")), + new FileInfo(Path.Combine(_originalGamePath, @"EscapeFromTarkov_Data\Managed\Aki.Reflection.dll")), + + // bepinex files + new FileInfo(Path.Combine(_originalGamePath, @"doorstep_config.ini")), + new FileInfo(Path.Combine(_originalGamePath, @"winhttp.dll")), + + // licenses + new FileInfo(Path.Combine(_originalGamePath, @"LICENSE-BEPINEX.txt")), + new FileInfo(Path.Combine(_originalGamePath, @"LICENSE-ConfigurationManager.txt")), + new FileInfo(Path.Combine(_originalGamePath, @"LICENSE-Launcher.txt")), + new FileInfo(Path.Combine(_originalGamePath, @"LICENSE-Modules.txt")), + new FileInfo(Path.Combine(_originalGamePath, @"LICENSE-Server.txt")) + }; + var directories = new DirectoryInfo[] + { + new DirectoryInfo(Path.Combine(_originalGamePath, @"Aki_Data")), + new DirectoryInfo(Path.Combine(_originalGamePath, @"BepInEx")) + }; + + foreach (var file in files) + { + if (File.Exists(file.FullName)) + { + File.Delete(file.FullName); + isInstalledInLive = true; + } + } + + foreach (var directory in directories) + { + if (Directory.Exists(directory.FullName)) + { + RemoveFilesRecurse(directory); + isInstalledInLive = true; + } + } + } + catch (Exception ex) + { + LogManager.Instance.Exception(ex); + } + + return isInstalledInLive; + } + + void SetupGameFiles() + { + var files = new [] + { + GetFileForCleanup("BattlEye"), + GetFileForCleanup("Logs"), + GetFileForCleanup("ConsistencyInfo"), + GetFileForCleanup("EscapeFromTarkov_BE.exe"), + GetFileForCleanup("Uninstall.exe"), + GetFileForCleanup("UnityCrashHandler64.exe"), + GetFileForCleanup("WinPixEventRuntime.dll") + }; + + foreach (var file in files) + { + if (file == null) + { + continue; + } + + if (Directory.Exists(file)) + { + RemoveFilesRecurse(new DirectoryInfo(file)); + } + + if (File.Exists(file)) + { + File.Delete(file); + } + } + } + + private string GetFileForCleanup(string fileName) + { + if (_excludeFromCleanup.Contains(fileName)) + { + LogManager.Instance.Info($"Excluded {fileName} from file cleanup"); + return null; + } + + return Path.Combine(_gamePath, fileName); + } + + /// + /// Remove the registry keys + /// + /// returns true if the keys were removed. returns false if an exception occured + public bool RemoveRegistryKeys() + { + try + { + var key = Registry.CurrentUser.OpenSubKey(registrySettings, true); + + foreach (var value in key.GetValueNames()) + { + key.DeleteValue(value); + } + } + catch (Exception ex) + { + LogManager.Instance.Exception(ex); + return false; + } + + return true; + } + + /// + /// Clean the temp folder + /// + /// returns true if the temp folder was cleaned succefully or doesn't exist. returns false if something went wrong. + public bool CleanTempFiles() + { + var rootdir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), @"Battlestate Games\EscapeFromTarkov")); + + if (!rootdir.Exists) + { + return true; + } + + return RemoveFilesRecurse(rootdir); + } + + bool RemoveFilesRecurse(DirectoryInfo basedir) + { + if (!basedir.Exists) + { + return true; + } + + try + { + // remove subdirectories + foreach (var dir in basedir.EnumerateDirectories()) + { + RemoveFilesRecurse(dir); + } + + // remove files + var files = basedir.GetFiles(); + + foreach (var file in files) + { + file.IsReadOnly = false; + file.Delete(); + } + + // remove directory + basedir.Delete(); + } + catch (Exception ex) + { + LogManager.Instance.Exception(ex); + return false; + } + + return true; + } + } +} diff --git a/project/Aki.Launcher.Base/Controllers/LogManager.cs b/project/Aki.Launcher.Base/Controllers/LogManager.cs new file mode 100644 index 0000000..8a2a127 --- /dev/null +++ b/project/Aki.Launcher.Base/Controllers/LogManager.cs @@ -0,0 +1,51 @@ +/* LogManager.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using System; +using System.IO; + +namespace Aki.Launcher.Controllers +{ + /// + /// LogManager + /// + public class LogManager + { + //TODO - update this to use reflection to get the calling method, class, etc + private static LogManager _instance; + public static LogManager Instance => _instance ?? (_instance = new LogManager()); + private string filepath; + + public LogManager() + { + filepath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "user", "logs"); + } + + public void Write(string text) + { + if (!Directory.Exists(filepath)) + { + Directory.CreateDirectory(filepath); + } + + string filename = Path.Combine(filepath, "launcher.log"); + File.AppendAllLines(filename, new[] { $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}]{text}" }); + } + + public void Debug(string text) => Write($"[Debug]{text}"); + + public void Info(string text) => Write($"[Info]{text}"); + + public void Warning(string text) => Write($"[Warning]{text}"); + + public void Error(string text) => Write($"[Error]{text}"); + + public void Exception(Exception ex) => Write($"[Exception]{ex.Message}\nStacktrace:\n{ex.StackTrace}"); + } +} diff --git a/project/Aki.Launcher.Base/Controllers/RequestHandler.cs b/project/Aki.Launcher.Base/Controllers/RequestHandler.cs new file mode 100644 index 0000000..52e9447 --- /dev/null +++ b/project/Aki.Launcher.Base/Controllers/RequestHandler.cs @@ -0,0 +1,98 @@ +/* RequestHandler.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using Aki.Launcher.MiniCommon; + +namespace Aki.Launcher +{ + public static class RequestHandler + { + private static Request request = new Request(null, ""); + + public static string GetBackendUrl() + { + return request.RemoteEndPoint; + } + + public static void ChangeBackendUrl(string remoteEndPoint) + { + request.RemoteEndPoint = remoteEndPoint; + } + + public static void ChangeSession(string session) + { + request.Session = session; + } + + public static string RequestConnect() + { + return request.GetJson("/launcher/server/connect"); + } + + public static string RequestLogin(LoginRequestData data) + { + return request.PostJson("/launcher/profile/login", Json.Serialize(data)); + } + + public static string RequestRegister(RegisterRequestData data) + { + return request.PostJson("/launcher/profile/register", Json.Serialize(data)); + } + + public static string RequestRemove(LoginRequestData data) + { + return request.PostJson("/launcher/profile/remove", Json.Serialize(data)); + } + + public static string RequestAccount(LoginRequestData data) + { + return request.PostJson("/launcher/profile/get", Json.Serialize(data)); + } + + public static string RequestProfileInfo(LoginRequestData data) + { + return request.PostJson("/launcher/profile/info", Json.Serialize(data)); + } + + public static string RequestExistingProfiles() + { + return request.GetJson("/launcher/profiles"); + } + + public static string RequestChangeUsername(ChangeRequestData data) + { + return request.PostJson("/launcher/profile/change/username", Json.Serialize(data)); + } + + public static string RequestChangePassword(ChangeRequestData data) + { + return request.PostJson("/launcher/profile/change/password", Json.Serialize(data)); + } + + public static string RequestWipe(RegisterRequestData data) + { + return request.PostJson("/launcher/profile/change/wipe", Json.Serialize(data)); + } + + public static string SendPing() + { + return request.GetJson("/launcher/ping"); + } + + public static string RequestServerVersion() + { + return request.GetJson("/launcher/server/version"); + } + + public static string RequestCompatibleGameVersion() + { + return request.GetJson("/launcher/profile/compatibleTarkovVersion"); + } + } +} diff --git a/project/Aki.Launcher.Base/Controllers/ServerManager.cs b/project/Aki.Launcher.Base/Controllers/ServerManager.cs new file mode 100644 index 0000000..d047cc3 --- /dev/null +++ b/project/Aki.Launcher.Base/Controllers/ServerManager.cs @@ -0,0 +1,91 @@ +/* ServerManager.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using Aki.Launcher.MiniCommon; +using System.Threading.Tasks; + +namespace Aki.Launcher +{ + public static class ServerManager + { + public static ServerInfo SelectedServer { get; private set; } = null; + + public static bool PingServer() + { + string json = ""; + + try + { + json = RequestHandler.SendPing(); + + if(json != null) return true; + } + catch + { + return false; + } + + return false; + } + + public static string GetVersion() + { + try + { + string json = RequestHandler.RequestServerVersion(); + + return Json.Deserialize(json); + } + catch + { + return ""; + } + } + + public static string GetCompatibleGameVersion() + { + try + { + string json = RequestHandler.RequestCompatibleGameVersion(); + + return Json.Deserialize(json); + } + catch + { + return ""; + } + } + + public static void LoadServer(string backendUrl) + { + string json = ""; + + try + { + RequestHandler.ChangeBackendUrl(backendUrl); + json = RequestHandler.RequestConnect(); + } + catch + { + SelectedServer = null; + return; + } + + SelectedServer = Json.Deserialize(json); + } + + public static async Task LoadDefaultServerAsync(string server) + { + await Task.Run(() => + { + LoadServer(server); + }); + } + } +} diff --git a/project/Aki.Launcher.Base/Extensions/DictionaryExtensions.cs b/project/Aki.Launcher.Base/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000..d6d82db --- /dev/null +++ b/project/Aki.Launcher.Base/Extensions/DictionaryExtensions.cs @@ -0,0 +1,37 @@ +/* DictionaryExtension.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + + +using System.Collections.Generic; +using System.Linq; + +namespace Aki.Launcher.Extensions +{ + public static class DictionaryExtensions + { + public static TKey GetKeyByValue(this Dictionary Dic, TValue value) + { + List Keys = Dic.Keys.ToList(); + + for (int x = 0; x < Keys.Count(); x++) + { + TValue tempValue; + + if (Dic.TryGetValue(Keys[x], out tempValue)) + { + if (tempValue != null && tempValue.Equals(value)) + { + return Keys[x]; + } + } + } + + return default; + } + } +} diff --git a/project/Aki.Launcher.Base/Helpers/FilePatcher.cs b/project/Aki.Launcher.Base/Helpers/FilePatcher.cs new file mode 100644 index 0000000..05e3917 --- /dev/null +++ b/project/Aki.Launcher.Base/Helpers/FilePatcher.cs @@ -0,0 +1,130 @@ +/* FilePatcher.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + * waffle.lord + */ + +using System; +using System.IO; +using System.Reflection.Metadata.Ecma335; +using Aki.ByteBanger; +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models.Launcher; + +namespace Aki.Launcher.Helpers +{ + public static class FilePatcher + { + public static event EventHandler PatchProgress; + private static void RaisePatchProgress(int Percentage, string Message) + { + PatchProgress?.Invoke(null, new ProgressInfo(Percentage, Message)); + } + + public static PatchResultInfo Patch(string targetfile, string patchfile, bool IgnoreInputHashMismatch = false) + { + byte[] target = VFS.ReadFile(targetfile); + byte[] patch = VFS.ReadFile(patchfile); + + PatchResult result = PatchUtil.Patch(target, PatchInfo.FromBytes(patch)); + + switch (result.Result) + { + case PatchResultType.Success: + File.Copy(targetfile, $"{targetfile}.bak"); + VFS.WriteFile(targetfile, result.PatchedData); + break; + + case PatchResultType.InputChecksumMismatch: + if (IgnoreInputHashMismatch) + return new PatchResultInfo(PatchResultType.Success, 1, 1); + break; + } + + return new PatchResultInfo(result.Result, 1, 1); + } + + private static PatchResultInfo PatchAll(string targetpath, string patchpath, bool IgnoreInputHashMismatch = false) + { + DirectoryInfo di = new DirectoryInfo(patchpath); + + // get all patch files within patchpath and it's sub directories. + var patchfiles = di.GetFiles("*.bpf", SearchOption.AllDirectories); + + int countfiles = patchfiles.Length; + + int processed = 0; + + foreach (FileInfo file in patchfiles) + { + FileInfo target; + + int progress = (int)Math.Floor((double)processed / countfiles * 100); + RaisePatchProgress(progress, $"{LocalizationProvider.Instance.patching} {file.Name} ..."); + + // get the relative portion of the patch file that will be appended to targetpath in order to create an official target file. + var relativefile = file.FullName.Substring(patchpath.Length).TrimStart('\\', '/'); + + // create a target file from the relative patch file while utilizing targetpath as the root directory. + target = new FileInfo(VFS.Combine(targetpath, relativefile.Replace(".bpf", ""))); + + PatchResultInfo result = Patch(target.FullName, file.FullName, IgnoreInputHashMismatch); + + if (!result.OK) + { + // patch failed + return result; + } + + processed++; + } + + RaisePatchProgress(100, LocalizationProvider.Instance.ok); + + return new PatchResultInfo(PatchResultType.Success, processed, countfiles); + } + + public static PatchResultInfo Run(string targetPath, string patchPath, bool IgnoreInputHashMismatch = false) + { + return PatchAll(targetPath, patchPath, IgnoreInputHashMismatch); + } + + public static void Restore(string filepath) + { + RestoreRecurse(new DirectoryInfo(filepath)); + } + + static void RestoreRecurse(DirectoryInfo basedir) + { + // scan subdirectories + foreach (var dir in basedir.EnumerateDirectories()) + { + RestoreRecurse(dir); + } + + // scan files + var files = basedir.GetFiles(); + + foreach (var file in files) + { + if (file.Extension == ".bak") + { + var target = Path.ChangeExtension(file.FullName, null); + + // remove patched file + var patched = new FileInfo(target); + patched.IsReadOnly = false; + patched.Delete(); + + // restore from backup + File.Copy(file.FullName, target); + file.IsReadOnly = false; + file.Delete(); + } + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Launcher.Base/Helpers/LauncherSettingsProvider.cs b/project/Aki.Launcher.Base/Helpers/LauncherSettingsProvider.cs new file mode 100644 index 0000000..9bacb1b --- /dev/null +++ b/project/Aki.Launcher.Base/Helpers/LauncherSettingsProvider.cs @@ -0,0 +1,163 @@ +/* LauncherSettingsProvider.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + * Merijn Hendriks + */ + + +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models.Launcher; +using Newtonsoft.Json; +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32; + +namespace Aki.Launcher.Helpers +{ + public static class LauncherSettingsProvider + { + public static string DefaultSettingsFileLocation = Path.Join(Environment.CurrentDirectory, "user", "launcher", "config.json"); + public static Settings Instance { get; } = Json.Load(DefaultSettingsFileLocation) ?? new Settings(); + } + + public class Settings : INotifyPropertyChanged + { + public bool FirstRun { get; set; } = true; + + public void SaveSettings() + { + Json.SaveWithFormatting(LauncherSettingsProvider.DefaultSettingsFileLocation, this, Formatting.Indented); + } + + public string DefaultLocale { get; set; } = "English"; + + private bool _IsAddingServer; + [JsonIgnore] + public bool IsAddingServer + { + get => _IsAddingServer; + set + { + if (_IsAddingServer != value) + { + _IsAddingServer = value; + RaisePropertyChanged(nameof(IsAddingServer)); + } + } + } + + private bool _AllowSettings; + [JsonIgnore] + public bool AllowSettings + { + get => _AllowSettings; + set + { + if (_AllowSettings != value) + { + _AllowSettings = value; + RaisePropertyChanged(nameof(AllowSettings)); + } + } + } + + private bool _GameRunning; + [JsonIgnore] + public bool GameRunning + { + get => _GameRunning; + set + { + if (_GameRunning != value) + { + _GameRunning = value; + RaisePropertyChanged(nameof(GameRunning)); + } + } + } + + private LauncherAction _LauncherStartGameAction; + public LauncherAction LauncherStartGameAction + { + get => _LauncherStartGameAction; + set + { + if (_LauncherStartGameAction != value) + { + _LauncherStartGameAction = value; + RaisePropertyChanged(nameof(LauncherStartGameAction)); + } + } + } + + private bool _UseAutoLogin; + public bool UseAutoLogin + { + get => _UseAutoLogin; + set + { + if (_UseAutoLogin != value) + { + _UseAutoLogin = value; + RaisePropertyChanged(nameof(UseAutoLogin)); + } + } + } + + private string _GamePath; + public string GamePath + { + get => _GamePath; + set + { + if (_GamePath != value) + { + _GamePath = value; + RaisePropertyChanged(nameof(GamePath)); + } + } + } + + private string[] _ExcludeFromCleanup; + + public string[] ExcludeFromCleanup + { + get => _ExcludeFromCleanup ??= Array.Empty(); + set + { + if (_ExcludeFromCleanup != value) + { + _ExcludeFromCleanup = value; + RaisePropertyChanged(nameof(ExcludeFromCleanup)); + } + } + } + + public ServerSetting Server { get; set; } = new ServerSetting(); + + public Settings() + { + if (!File.Exists(LauncherSettingsProvider.DefaultSettingsFileLocation)) + { + LauncherStartGameAction = LauncherAction.MinimizeAction; + UseAutoLogin = true; + GamePath = Environment.CurrentDirectory; + + Server = new ServerSetting { Name = "SPT-AKI", Url = "http://127.0.0.1:6969" }; + SaveSettings(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Helpers/LocalizationProvider.cs b/project/Aki.Launcher.Base/Helpers/LocalizationProvider.cs new file mode 100644 index 0000000..97e3f2e --- /dev/null +++ b/project/Aki.Launcher.Base/Helpers/LocalizationProvider.cs @@ -0,0 +1,1514 @@ +/* LocalizationProvider.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + + +using Aki.Launcher.Extensions; +using Aki.Launcher.MiniCommon; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Aki.Launcher.Helpers +{ + public static class LocalizationProvider + { + public static string DefaultLocaleFolderPath = Path.Join(Environment.CurrentDirectory, "Aki_Data", "Launcher", "Locales"); + + public static Dictionary LocaleNameDictionary = GetLocaleDictionary(); + + public static event EventHandler LocaleChanged = delegate { }; + + public static void LoadLocaleFromFile(string localeName) + { + string localeRomanName = LocaleNameDictionary.GetKeyByValue(localeName); + + if (String.IsNullOrEmpty(localeRomanName)) + { + localeRomanName = localeName; + } + + LocaleData newLocale = Json.LoadClassWithoutSaving(Path.Join(DefaultLocaleFolderPath, $"{localeRomanName}.json")); + + if (newLocale != null) + { + foreach (var prop in Instance.GetType().GetProperties()) + { + prop.SetValue(Instance, newLocale.GetType().GetProperty(prop.Name).GetValue(newLocale)); + } + + LauncherSettingsProvider.Instance.DefaultLocale = localeRomanName; + LauncherSettingsProvider.Instance.SaveSettings(); + + LocaleChanged(null, EventArgs.Empty); + } + + //could possibly raise an event here to say why the local wasn't changed. + } + + private static string GetSystemLocale() + { + string UIlocaleName = CultureInfo.CurrentUICulture.DisplayName; + + var regexMatch = Regex.Match(UIlocaleName, @"^(\w+)"); + + if (regexMatch.Groups.Count == 2) + { + string localRomanName = LocaleNameDictionary.GetValueOrDefault(regexMatch.Groups[1].Value, ""); + + bool localExists = GetAvailableLocales().Where(x => x == localRomanName).Count() > 0; + + if (localExists) + { + return localRomanName; + } + } + + return "English"; + } + + public static void TryAutoSetLocale() + { + LoadLocaleFromFile(GetSystemLocale()); + } + + public static LocaleData GenerateEnglishLocale() + { + //Create default english locale data and save if the default locale data file dosen't exist. + //This is to (hopefully) prevent the launcher from becoming 100% broken if no locale files exist or the locale files are outdated (missing data). + LocaleData englishLocale = new LocaleData(); + + #region Set All English Defaults + englishLocale.native_name = "English"; + englishLocale.retry = "Retry"; + englishLocale.server_connecting = "Connecting"; + englishLocale.server_unavailable_format_1 = "Default server '{0}' is not available."; + englishLocale.no_servers_available = "No Servers found. Check server list in settings."; + englishLocale.settings_menu = "Settings"; + englishLocale.back = "Back"; + englishLocale.wipe_profile = "Wipe Profile"; + englishLocale.username = "Username"; + englishLocale.password = "Password"; + englishLocale.update = "Update"; + englishLocale.edit_account_update_error = "An issue occurred while updating your profile."; + englishLocale.register = "Register"; + englishLocale.go_to_register = "Go to Register"; + englishLocale.registration_failed = "Registration Failed."; + englishLocale.registration_question_format_1 = "Profile '{0}' does not exist.\n\nWould you like to create it?"; + englishLocale.login_or_register = "Login / Register"; + englishLocale.go_to_login = "Go to Login"; + englishLocale.login_automatically = "Login Automatically"; + englishLocale.incorrect_login = "Username or password is incorrect"; + englishLocale.login_failed = "Login Failed"; + englishLocale.edition = "Edition"; + englishLocale.id = "ID"; + englishLocale.logout = "Logout"; + englishLocale.account = "Account"; + englishLocale.edit_account = "Edit Account"; + englishLocale.start_game = "Start Game"; + englishLocale.installed_in_live_game_warning = "Aki shouldn't be installed into the live game directory. Please install Aki into a copy of the game directory elsewhere on your computer."; + englishLocale.no_official_game_warning = "Escape From Tarkov isn't installed on your computer. Please buy a copy of the game and support the developers!"; + englishLocale.eft_exe_not_found_warning = "EscapeFromTarkov.exe not found at game path. Please check that the directory is correct."; + englishLocale.account_exist = "Account already exists"; + englishLocale.url = "URL"; + englishLocale.default_language = "Default Language"; + englishLocale.game_path = "Game Path"; + englishLocale.clear_game_settings = "Clear Game Settings"; + englishLocale.clear_game_settings_warning = "You are about to remove your old game settings files. They will be backed up to:\n{0}\n\nAre you sure?"; + englishLocale.clear_game_settings_succeeded = "Game settings cleared."; + englishLocale.clear_game_settings_failed = "An issue occurred while clearing game settings."; + englishLocale.remove_registry_keys = "Remove Registry Keys"; + englishLocale.remove_registry_keys_succeeded = "Registry keys removed."; + englishLocale.remove_registry_keys_failed = "An issue occurred while removing registry keys."; + englishLocale.clean_temp_files = "Clean Temp Files"; + englishLocale.clean_temp_files_succeeded = "Temp files cleaned"; + englishLocale.clean_temp_files_failed = "Some issues occurred while cleaning temp files"; + englishLocale.select_folder = "Select Folder"; + englishLocale.minimize_action = "Minimize"; + englishLocale.do_nothing_action = "Do nothing"; + englishLocale.exit_action = "Close Launcher"; + englishLocale.on_game_start = "On Game Start"; + englishLocale.game = "Game"; + englishLocale.new_password = "New Password"; + englishLocale.wipe_warning = "Changing your account edition requires a profile wipe. This will reset your game prgrogess."; + englishLocale.cancel = "Cancel"; + englishLocale.need_an_account = "Don't have an account yet?"; + englishLocale.have_an_account = "Already have an account?"; + englishLocale.reapply_patch = "Reapply Patch"; + englishLocale.failed_to_receive_patches = "Failed to receive patches"; + englishLocale.failed_core_patch = "Core patch failed"; + englishLocale.failed_mod_patch = "Mod patch failed"; + englishLocale.ok = "OK"; + englishLocale.account_page_denied = "Account page denied. Either you are not logged in or the game is running."; + englishLocale.account_updated = "Your account has been updated"; + englishLocale.nickname = "Nickname"; + englishLocale.side = "Side"; + englishLocale.level = "Level"; + englishLocale.game_path = "Game Path"; + englishLocale.patching = "Patching"; + englishLocale.file_mismatch_dialog_message = "The input file hash doesn't match the expected hash. You may be using the wrong version\nof AKI for your client files.\n\nDo you want to continue?"; + englishLocale.yes = "Yes"; + englishLocale.no = "No"; + englishLocale.open_folder = "Open Folder"; + englishLocale.select_edition = "Select Edition"; + englishLocale.profile_created = "Profile Created"; + englishLocale.next_level_in = "Next level in"; + englishLocale.copied = "Copied"; + englishLocale.no_profile_data = "No profile data"; + englishLocale.profile_version_mismath = "Your profile was made using a different version of aki and may have issues"; + englishLocale.profile_removed = "Profile removed"; + englishLocale.profile_removal_failed = "Failed to remove profile"; + englishLocale.profile_remove_question_format_1 = "Permanently remove profile '{0}'?"; + englishLocale.i_understand = "I Understand"; + englishLocale.game_version_mismatch_format_2 = "SPT is unable to run, this is because SPT expected to find EFT version '{1}',\nbut instead found version '{0}'\n\nEnsure you've downgraded your EFT as described in the install guide\non the page you downloaded SPT from"; + #endregion + + Directory.CreateDirectory(LocalizationProvider.DefaultLocaleFolderPath); + LauncherSettingsProvider.Instance.DefaultLocale = "English"; + LauncherSettingsProvider.Instance.SaveSettings(); + Json.SaveWithFormatting(Path.Join(LocalizationProvider.DefaultLocaleFolderPath, "English.json"), englishLocale, Newtonsoft.Json.Formatting.Indented); + + return englishLocale; + } + + public static Dictionary GetLocaleDictionary() + { + List localeFiles = new List(Directory.GetFiles(DefaultLocaleFolderPath).Select(x => new FileInfo(x)).ToList()); + Dictionary localeDictionary = new Dictionary(); + + foreach (FileInfo file in localeFiles) + { + localeDictionary.Add(file.Name.Replace(".json", ""), Json.GetPropertyByName(file.FullName, "native_name")); + } + + return localeDictionary; + } + public static ObservableCollection GetAvailableLocales() + { + return new ObservableCollection(LocaleNameDictionary.Values); + } + + public static LocaleData Instance { get; private set; } = Json.LoadClassWithoutSaving(Path.Join(DefaultLocaleFolderPath, $"{LauncherSettingsProvider.Instance.DefaultLocale}.json")) ?? GenerateEnglishLocale(); + } + + public class LocaleData : INotifyPropertyChanged + { + //this is going to be some pretty long boiler plate code. So I'm putting everything into regions. + + #region All Properties + + #region native_name + private string _native_name; + public string native_name + { + get => _native_name; + set + { + if (_native_name != value) + { + _native_name = value; + RaisePropertyChanged(nameof(native_name)); + } + } + } + #endregion + + #region retry + private string _retry; + public string retry + { + get => _retry; + set + { + if (_retry != value) + { + _retry = value; + RaisePropertyChanged(nameof(retry)); + } + } + } + #endregion + + #region server_connecting + private string _server_connecting; + public string server_connecting + { + get => _server_connecting; + set + { + if (_server_connecting != value) + { + _server_connecting = value; + RaisePropertyChanged(nameof(server_connecting)); + } + } + } + #endregion + + #region server_unavailable_format_1 + private string _server_unavailable_format_1; + public string server_unavailable_format_1 + { + get => _server_unavailable_format_1; + set + { + if (_server_unavailable_format_1 != value) + { + _server_unavailable_format_1 = value; + RaisePropertyChanged(nameof(server_unavailable_format_1)); + } + } + } + #endregion + + #region no_servers_available + private string _no_servers_available; + public string no_servers_available + { + get => _no_servers_available; + set + { + if (_no_servers_available != value) + { + _no_servers_available = value; + RaisePropertyChanged(nameof(no_servers_available)); + } + } + } + #endregion + + #region settings_menu + private string _settings_menu; + public string settings_menu + { + get => _settings_menu; + set + { + if (_settings_menu != value) + { + _settings_menu = value; + RaisePropertyChanged(nameof(settings_menu)); + } + } + } + #endregion + + #region back + private string _back; + public string back + { + get => _back; + set + { + if (_back != value) + { + _back = value; + RaisePropertyChanged(nameof(back)); + } + } + } + #endregion + + #region wipe_profile + private string _wipe_profile; + public string wipe_profile + { + get => _wipe_profile; + set + { + if (_wipe_profile != value) + { + _wipe_profile = value; + RaisePropertyChanged(nameof(wipe_profile)); + } + } + } + #endregion + + #region username + private string _username; + public string username + { + get => _username; + set + { + if (_username != value) + { + _username = value; + RaisePropertyChanged(nameof(username)); + } + } + } + #endregion + + #region password + private string _password; + public string password + { + get => _password; + set + { + if (_password != value) + { + _password = value; + RaisePropertyChanged(nameof(password)); + } + } + } + #endregion + + #region update + private string _update; + public string update + { + get => _update; + set + { + if (_update != value) + { + _update = value; + RaisePropertyChanged(nameof(update)); + } + } + } + #endregion + + #region edit_account_update_error + private string _edit_account_update_error; + public string edit_account_update_error + { + get => _edit_account_update_error; + set + { + if (_edit_account_update_error != value) + { + _edit_account_update_error = value; + RaisePropertyChanged(nameof(edit_account_update_error)); + } + } + } + #endregion + + #region register + private string _register; + public string register + { + get => _register; + set + { + if (_register != value) + { + _register = value; + RaisePropertyChanged(nameof(register)); + } + } + } + #endregion + + #region go_to_register + private string _go_to_register; + public string go_to_register + { + get => _go_to_register; + set + { + if (_go_to_register != value) + { + _go_to_register = value; + RaisePropertyChanged(nameof(_go_to_register)); + } + } + } + #endregion + + #region login_or_register + private string _login_or_register; + public string login_or_register + { + get => _login_or_register; + set + { + if (_login_or_register != value) + { + _login_or_register = value; + RaisePropertyChanged(nameof(login_or_register)); + } + } + } + #endregion + + #region go_to_login + private string _go_to_login; + public string go_to_login + { + get => _go_to_login; + set + { + if (_go_to_login != value) + { + _go_to_login = value; + RaisePropertyChanged(nameof(go_to_login)); + } + } + } + #endregion + + #region login_automatically + private string _login_automatically; + public string login_automatically + { + get => _login_automatically; + set + { + if (_login_automatically != value) + { + _login_automatically = value; + RaisePropertyChanged(nameof(login_automatically)); + } + } + } + #endregion + + #region incorrect_login + private string _incorrect_login; + public string incorrect_login + { + get => _incorrect_login; + set + { + if (_incorrect_login != value) + { + _incorrect_login = value; + RaisePropertyChanged(nameof(incorrect_login)); + } + } + } + #endregion + + #region login_failed + private string _login_failed; + public string login_failed + { + get => _login_failed; + set + { + if (_login_failed != value) + { + _login_failed = value; + RaisePropertyChanged(nameof(login_failed)); + } + } + } + #endregion + + #region edition + private string _edition; + public string edition + { + get => _edition; + set + { + if (_edition != value) + { + _edition = value; + RaisePropertyChanged(nameof(edition)); + } + } + } + #endregion + + #region id + private string _id; + public string id + { + get => _id; + set + { + if (_id != value) + { + _id = value; + RaisePropertyChanged(nameof(id)); + } + } + } + #endregion + + #region logout + private string _logout; + public string logout + { + get => _logout; + set + { + if (_logout != value) + { + _logout = value; + RaisePropertyChanged(nameof(logout)); + } + } + } + #endregion + + #region account + private string _account; + public string account + { + get => _account; + set + { + if (_account != value) + { + _account = value; + RaisePropertyChanged(nameof(account)); + } + } + } + #endregion + + #region edit_account + private string _edit_account; + public string edit_account + { + get => _edit_account; + set + { + if (_edit_account != value) + { + _edit_account = value; + RaisePropertyChanged(nameof(edit_account)); + } + } + } + #endregion + + #region start_game + private string _start_game; + public string start_game + { + get => _start_game; + set + { + if (_start_game != value) + { + _start_game = value; + RaisePropertyChanged(nameof(start_game)); + } + } + } + #endregion + + #region installed_in_live_game_warning + private string _installed_in_live_game_warning; + public string installed_in_live_game_warning + { + get => _installed_in_live_game_warning; + set + { + if (_installed_in_live_game_warning != value) + { + _installed_in_live_game_warning = value; + RaisePropertyChanged(nameof(installed_in_live_game_warning)); + } + } + } + #endregion + + #region no_official_game_warning + private string _no_official_game_warning; + public string no_official_game_warning + { + get => _no_official_game_warning; + set + { + if (_no_official_game_warning != value) + { + _no_official_game_warning = value; + RaisePropertyChanged(nameof(no_official_game_warning)); + } + } + } + #endregion + + #region eft_exe_not_found_warning + private string _eft_exe_not_found_warning; + public string eft_exe_not_found_warning + { + get => _eft_exe_not_found_warning; + set + { + if (_eft_exe_not_found_warning != value) + { + _eft_exe_not_found_warning = value; + RaisePropertyChanged(nameof(eft_exe_not_found_warning)); + } + } + } + #endregion + + #region account_exist + private string _account_exist; + public string account_exist + { + get => _account_exist; + set + { + if (_account_exist != value) + { + _account_exist = value; + RaisePropertyChanged(nameof(account_exist)); + } + } + } + #endregion + + #region url + private string _url; + public string url + { + get => _url; + set + { + if (_url != value) + { + _url = value; + RaisePropertyChanged(nameof(url)); + } + } + } + #endregion + + #region default_language + private string _default_language; + public string default_language + { + get => _default_language; + set + { + if (_default_language != value) + { + _default_language = value; + RaisePropertyChanged(nameof(default_language)); + } + } + } + #endregion + + #region game_path + private string _game_path; + public string game_path + { + get => _game_path; + set + { + if (_game_path != value) + { + _game_path = value; + RaisePropertyChanged(nameof(game_path)); + } + } + } + #endregion + + #region clear_game_settings + private string _clear_game_settings; + public string clear_game_settings + { + get => _clear_game_settings; + set + { + if (_clear_game_settings != value) + { + _clear_game_settings = value; + RaisePropertyChanged(nameof(clear_game_settings)); + } + } + } + #endregion + + #region clear_game_settings_warning + private string _clear_game_settings_warning; + public string clear_game_settings_warning + { + get => _clear_game_settings_warning; + set + { + if (_clear_game_settings_warning != value) + { + _clear_game_settings_warning = value; + RaisePropertyChanged(nameof(clear_game_settings_warning)); + } + } + } + #endregion + + #region clear_game_settings_succeeded + private string _clear_game_settings_succeeded; + public string clear_game_settings_succeeded + { + get => _clear_game_settings_succeeded; + set + { + if (_clear_game_settings_succeeded != value) + { + _clear_game_settings_succeeded = value; + RaisePropertyChanged(nameof(clear_game_settings_succeeded)); + } + } + } + #endregion + + #region clear_game_settings_failed + private string _clear_game_settings_failed; + public string clear_game_settings_failed + { + get => _clear_game_settings_failed; + set + { + if (_clear_game_settings_failed != value) + { + _clear_game_settings_failed = value; + RaisePropertyChanged(nameof(clear_game_settings_failed)); + } + } + } + #endregion + + #region remove_registry_keys + private string _remove_registry_keys; + public string remove_registry_keys + { + get => _remove_registry_keys; + set + { + if (_remove_registry_keys != value) + { + _remove_registry_keys = value; + RaisePropertyChanged(nameof(remove_registry_keys)); + } + } + } + #endregion + + #region remove_registry_keys_succeeded + private string _remove_registry_keys_succeeded; + public string remove_registry_keys_succeeded + { + get => _remove_registry_keys_succeeded; + set + { + if (_remove_registry_keys_succeeded != value) + { + _remove_registry_keys_succeeded = value; + RaisePropertyChanged(nameof(remove_registry_keys_succeeded)); + } + } + } + #endregion + + #region remove_registry_keys_failed + private string _remove_registry_keys_failed; + public string remove_registry_keys_failed + { + get => _remove_registry_keys_failed; + set + { + if (_remove_registry_keys_failed != value) + { + _remove_registry_keys_failed = value; + RaisePropertyChanged(nameof(remove_registry_keys_failed)); + } + } + } + #endregion + + #region clean_temp_files + private string _clean_temp_files; + public string clean_temp_files + { + get => _clean_temp_files; + set + { + if (_clean_temp_files != value) + { + _clean_temp_files = value; + RaisePropertyChanged(nameof(clean_temp_files)); + } + } + } + #endregion + + #region clean_temp_files_succeeded + private string _clean_temp_files_succeeded; + public string clean_temp_files_succeeded + { + get => _clean_temp_files_succeeded; + set + { + if (_clean_temp_files_succeeded != value) + { + _clean_temp_files_succeeded = value; + RaisePropertyChanged(nameof(clean_temp_files_succeeded)); + } + } + } + #endregion + + #region clean_temp_files_failed + private string _clean_temp_files_failed; + public string clean_temp_files_failed + { + get => _clean_temp_files_failed; + set + { + if (_clean_temp_files_failed != value) + { + _clean_temp_files_failed = value; + RaisePropertyChanged(nameof(clean_temp_files_failed)); + } + } + } + #endregion + + #region select_folder + private string _select_folder; + public string select_folder + { + get => _select_folder; + set + { + if (_select_folder != value) + { + _select_folder = value; + RaisePropertyChanged(nameof(select_folder)); + } + } + } + #endregion + + #region registration_failed + private string _registration_failed; + public string registration_failed + { + get => _registration_failed; + set + { + if (_registration_failed != value) + { + _registration_failed = value; + RaisePropertyChanged(nameof(registration_failed)); + } + } + } + #endregion + + #region registration_question_format_1 + private string _registration_question_format_1; + public string registration_question_format_1 + { + get => _registration_question_format_1; + set + { + if(_registration_question_format_1 != value) + { + _registration_question_format_1 = value; + RaisePropertyChanged(nameof(registration_question_format_1)); + } + } + } + #endregion + + #region minimize_action + private string _minimize_action; + public string minimize_action + { + get => _minimize_action; + set + { + if (_minimize_action != value) + { + _minimize_action = value; + RaisePropertyChanged(nameof(minimize_action)); + } + } + } + #endregion + + #region do_nothing_action + private string _do_nothing_action; + public string do_nothing_action + { + get => _do_nothing_action; + set + { + if (_do_nothing_action != value) + { + _do_nothing_action = value; + RaisePropertyChanged(nameof(do_nothing_action)); + } + } + } + #endregion + + #region exit_action + private string _exit_action; + public string exit_action + { + get => _exit_action; + set + { + if (_exit_action != value) + { + _exit_action = value; + RaisePropertyChanged(nameof(exit_action)); + } + } + } + #endregion + + #region on_game_start + private string _on_game_start; + public string on_game_start + { + get => _on_game_start; + set + { + if (_on_game_start != value) + { + _on_game_start = value; + RaisePropertyChanged(nameof(on_game_start)); + } + } + } + #endregion + + #region game + private string _game; + public string game + { + get => _game; + set + { + if (_game != value) + { + _game = value; + RaisePropertyChanged(nameof(game)); + } + } + } + #endregion + + #region new_password + private string _new_password; + public string new_password + { + get => _new_password; + set + { + if (_new_password != value) + { + _new_password = value; + RaisePropertyChanged(nameof(new_password)); + } + } + } + #endregion + + #region wipe_warning + private string _wipe_warning; + public string wipe_warning + { + get => _wipe_warning; + set + { + if (_wipe_warning != value) + { + _wipe_warning = value; + RaisePropertyChanged(nameof(wipe_warning)); + } + } + } + #endregion + + #region cancel + private string _cancel; + public string cancel + { + get => _cancel; + set + { + if (_cancel != value) + { + _cancel = value; + RaisePropertyChanged(nameof(cancel)); + } + } + } + #endregion + + #region need_an_account + private string _need_an_account; + public string need_an_account + { + get => _need_an_account; + set + { + if (_need_an_account != value) + { + _need_an_account = value; + RaisePropertyChanged(nameof(need_an_account)); + } + } + } + #endregion + + #region have_an_account + private string _have_an_account; + public string have_an_account + { + get => _have_an_account; + set + { + if (_have_an_account != value) + { + _have_an_account = value; + RaisePropertyChanged(nameof(have_an_account)); + } + } + } + #endregion + + #region reapply_patch + private string _reapply_patch; + public string reapply_patch + { + get => _reapply_patch; + set + { + if (_reapply_patch != value) + { + _reapply_patch = value; + RaisePropertyChanged(nameof(reapply_patch)); + } + } + } + #endregion + + #region failed_to_receive_patches + private string _failed_to_receive_patches; + public string failed_to_receive_patches + { + get => _failed_to_receive_patches; + set + { + if (_failed_to_receive_patches != value) + { + _failed_to_receive_patches = value; + RaisePropertyChanged(nameof(failed_to_receive_patches)); + } + } + } + #endregion + + #region failed_core_patch + private string _failed_core_patch; + public string failed_core_patch + { + get => _failed_core_patch; + set + { + if (_failed_core_patch != value) + { + _failed_core_patch = value; + RaisePropertyChanged(nameof(failed_core_patch)); + } + } + } + #endregion + + #region failed_mod_patch + private string _failed_mod_patch; + public string failed_mod_patch + { + get => _failed_mod_patch; + set + { + if (_failed_mod_patch != value) + { + _failed_mod_patch = value; + RaisePropertyChanged(nameof(failed_mod_patch)); + } + } + } + #endregion + + #region OK + private string _ok; + public string ok + { + get => _ok; + set + { + if (_ok != value) + { + _ok = value; + RaisePropertyChanged(nameof(ok)); + } + } + } + #endregion + + #region account_page_denied + private string _account_page_denied; + public string account_page_denied + { + get => _account_page_denied; + set + { + if (_account_page_denied != value) + { + _account_page_denied = value; + RaisePropertyChanged(nameof(account_page_denied)); + } + } + } + #endregion + + #region account_updated + private string _account_updated; + public string account_updated + { + get => _account_updated; + set + { + if (_account_updated != value) + { + _account_updated = value; + RaisePropertyChanged(nameof(_account_updated)); + } + } + } + #endregion + + #region nickname + private string _nickname; + public string nickname + { + get => _nickname; + set + { + if (_nickname != value) + { + _nickname = value; + RaisePropertyChanged(nameof(nickname)); + } + } + } + #endregion + + #region side + private string _side; + public string side + { + get => _side; + set + { + if (_side != value) + { + _side = value; + RaisePropertyChanged(nameof(side)); + } + } + } + #endregion + + #region level + private string _level; + public string level + { + get => _level; + set + { + if (_level != value) + { + _level = value; + RaisePropertyChanged(nameof(level)); + } + } + } + #endregion + + #region patching + private string _patching; + public string patching + { + get => _patching; + set + { + if(_patching != value) + { + _patching = value; + RaisePropertyChanged(nameof(patching)); + } + } + } + #endregion + + #region file_mismatch_dialog_message + private string _file_mismatch_dialog_message; + public string file_mismatch_dialog_message + { + get => _file_mismatch_dialog_message; + set + { + if(_file_mismatch_dialog_message != value) + { + _file_mismatch_dialog_message = value; + RaisePropertyChanged(nameof(file_mismatch_dialog_message)); + } + } + } + #endregion + + #region yes + private string _yes; + public string yes + { + get => _yes; + set + { + if(_yes != value) + { + _yes = value; + RaisePropertyChanged(nameof(yes)); + } + } + } + #endregion + + #region no + private string _no; + public string no + { + get => _no; + set + { + if(_no != value) + { + _no = value; + RaisePropertyChanged(nameof(no)); + } + } + } + #endregion + + #region profile_created + private string _profile_created; + public string profile_created + { + get => _profile_created; + set + { + if(_profile_created != value) + { + _profile_created = value; + RaisePropertyChanged(nameof(profile_created)); + } + } + } + #endregion + + #region open_folder + private string _open_folder; + public string open_folder + { + get => _open_folder; + set + { + if(_open_folder != value) + { + _open_folder = value; + RaisePropertyChanged(nameof(open_folder)); + } + } + } + #endregion + + #region select_edition + private string _select_edition; + public string select_edition + { + get => _select_edition; + set + { + if(_select_edition != value) + { + _select_edition = value; + RaisePropertyChanged(nameof(select_edition)); + } + } + } + #endregion + + #region copied + private string _copied; + public string copied + { + get => _copied; + set + { + if(_copied != value) + { + _copied = value; + RaisePropertyChanged(nameof(copied)); + } + } + } + #endregion + + #region next_level_in + private string _next_level_in; + public string next_level_in + { + get => _next_level_in; + set + { + if(_next_level_in != value) + { + _next_level_in = value; + RaisePropertyChanged(nameof(next_level_in)); + } + } + } + #endregion + + #region no_profile_data + private string _no_profile_data; + public string no_profile_data + { + get => _no_profile_data; + set + { + if (_no_profile_data != value) + { + _no_profile_data = value; + RaisePropertyChanged(nameof(no_profile_data)); + } + } + } + #endregion + + #region profile_version_mismatch + private string _profile_version_mismath; + public string profile_version_mismath + { + get => _profile_version_mismath; + set + { + if(_profile_version_mismath != value) + { + _profile_version_mismath = value; + RaisePropertyChanged(nameof(profile_version_mismath)); + } + } + } + #endregion + + #region profile_removed + private string _profile_removed; + public string profile_removed + { + get => _profile_removed; + set + { + if(_profile_removed != value) + { + _profile_removed = value; + RaisePropertyChanged(nameof(profile_removed)); + } + } + } + #endregion + + #region profile_removal_failed + private string _profile_removal_failed; + public string profile_removal_failed + { + get => _profile_removal_failed; + set + { + if(_profile_removal_failed != value) + { + _profile_removal_failed = value; + RaisePropertyChanged(nameof(profile_removal_failed)); + } + } + } + #endregion + + #region profile_remove_question_format_1 + private string _profile_remove_question_format_1; + public string profile_remove_question_format_1 + { + get => _profile_remove_question_format_1; + set + { + if(_profile_remove_question_format_1 != value) + { + _profile_remove_question_format_1 = value; + RaisePropertyChanged(nameof(profile_remove_question_format_1)); + } + } + } + #endregion + + #region i_understand + private string _i_understand; + public string i_understand + { + get => _i_understand; + set + { + if(_i_understand != value) + { + _i_understand = value; + RaisePropertyChanged(nameof(i_understand)); + } + } + } + #endregion + + #region game_version_mismatch_format_2 + private string _game_version_mismatch_format_2; + public string game_version_mismatch_format_2 + { + get => _game_version_mismatch_format_2; + set + { + if(_game_version_mismatch_format_2 != value) + { + _game_version_mismatch_format_2 = value; + RaisePropertyChanged(nameof(game_version_mismatch_format_2)); + } + } + } + #endregion + + #endregion + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Helpers/ProgressReportingPatchRunner.cs b/project/Aki.Launcher.Base/Helpers/ProgressReportingPatchRunner.cs new file mode 100644 index 0000000..ce08837 --- /dev/null +++ b/project/Aki.Launcher.Base/Helpers/ProgressReportingPatchRunner.cs @@ -0,0 +1,76 @@ +/* ProgressReportingPatchRunner.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models.Launcher; +using System.Collections.Generic; +using System.Threading.Tasks; +using Aki.ByteBanger; + +namespace Aki.Launcher.Helpers +{ + public class ProgressReportingPatchRunner + { + private string GamePath; + private string[] Patches; + + private async IAsyncEnumerable TryPatchFiles(bool IgnoreInputHashMismatch) + { + FilePatcher.Restore(GamePath); + + int processed = 0; + int countpatches = Patches.Length; + + var _patches = Patches; + foreach (var patch in _patches) + { + var result = + await Task.Factory.StartNew(() => FilePatcher.Run(GamePath, patch, IgnoreInputHashMismatch)); + if (!result.OK) + { + yield return new PatchResultInfo(result.Status, processed, countpatches); + yield break; + } + + processed++; + var ourResult = new PatchResultInfo(PatchResultType.Success, processed, countpatches); + yield return ourResult; + } + } + + public async IAsyncEnumerable PatchFiles() + { + await foreach (var info in TryPatchFiles(false)) + { + yield return info; + + if (info.OK) + continue; + + // This will run _after_ the caller decides to continue iterating. + await foreach (var secondInfo in TryPatchFiles(true)) + { + yield return secondInfo; + } + + yield break; + } + } + + private string[] GetCorePatches() + { + return VFS.GetDirectories(VFS.Combine(GamePath, "Aki_Data/Launcher/Patches/")); + } + + public ProgressReportingPatchRunner(string GamePath, string[] Patches = null) + { + this.GamePath = GamePath; + this.Patches = Patches ?? GetCorePatches(); + } + } +} diff --git a/project/Aki.Launcher.Base/Helpers/ValidationUtil.cs b/project/Aki.Launcher.Base/Helpers/ValidationUtil.cs new file mode 100644 index 0000000..2e894c2 --- /dev/null +++ b/project/Aki.Launcher.Base/Helpers/ValidationUtil.cs @@ -0,0 +1,46 @@ +using Microsoft.Win32; +using System.IO; + +namespace Aki.Launcher.Helpers +{ + public static class ValidationUtil + { + public static bool Validate() + { + var c0 = @"Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\EscapeFromTarkov"; + var v0 = 0; + + try + { + var v1 = Registry.LocalMachine.OpenSubKey(c0, false).GetValue("UninstallString"); + var v2 = (v1 != null) ? v1.ToString() : string.Empty; + var v3 = new FileInfo(v2); + var v4 = new FileInfo[] + { + v3, + new FileInfo(v2.Replace(v3.Name, @"BattlEye\BEClient_x64.dll")), + new FileInfo(v2.Replace(v3.Name, @"BattlEye\BEService_x64.dll")), + new FileInfo(v2.Replace(v3.Name, @"ConsistencyInfo")), + new FileInfo(v2.Replace(v3.Name, @"Uninstall.exe")), + new FileInfo(v2.Replace(v3.Name, @"UnityCrashHandler64.exe")) + }; + + v0 = v4.Length - 1; + + foreach (var value in v4) + { + if (File.Exists(value.FullName)) + { + --v0; + } + } + } + catch + { + v0 = -1; + } + + return v0 == 0; + } + } +} diff --git a/project/Aki.Launcher.Base/Interfaces/IGameStarterFrontend.cs b/project/Aki.Launcher.Base/Interfaces/IGameStarterFrontend.cs new file mode 100644 index 0000000..19e2c74 --- /dev/null +++ b/project/Aki.Launcher.Base/Interfaces/IGameStarterFrontend.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Aki.Launcher.Models.Launcher; + +namespace Aki.Launcher.Interfaces +{ + public interface IGameStarterFrontend + { + Task CompletePatchTask(IAsyncEnumerable task); + } +} \ No newline at end of file diff --git a/project/Aki.Launcher.Base/Interfaces/IUpdateProgress.cs b/project/Aki.Launcher.Base/Interfaces/IUpdateProgress.cs new file mode 100644 index 0000000..a203bea --- /dev/null +++ b/project/Aki.Launcher.Base/Interfaces/IUpdateProgress.cs @@ -0,0 +1,31 @@ +/* IUpdateProgress + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +using Aki.Launcher.Models.Launcher; +using System; + +namespace Aki.Launcher.Interfaces +{ + public interface IUpdateProgress + { + /// + /// The task that will report progress to the + /// + public Action ProgressableTask { get; } + + /// + /// Cancel the ProgressableTask with a reason. + /// + public event EventHandler TaskCancelled; + + /// + /// The will subscribe to this event to update its main progress bar (top bar) + /// + public event EventHandler ProgressChanged; + } +} diff --git a/project/Aki.Launcher.Base/MiniCommon/ImageRequest.cs b/project/Aki.Launcher.Base/MiniCommon/ImageRequest.cs new file mode 100644 index 0000000..bfafbb9 --- /dev/null +++ b/project/Aki.Launcher.Base/MiniCommon/ImageRequest.cs @@ -0,0 +1,66 @@ +/* ImageRequest.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +using Aki.Launcher.Controllers; +using Aki.Launcher.Helpers; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Aki.Launcher.MiniCommon +{ + + public static class ImageRequest + { + public static string ImageCacheFolder = Path.Join(LauncherSettingsProvider.Instance.GamePath, "Aki_Data", "Launcher", "Image_Cache"); + + private static List CachedRoutes = new List(); + + private static string LauncherRoute = "/files/launcher/"; + public static void CacheBackgroundImage() => CacheImage($"{LauncherRoute}bg.png", Path.Combine(ImageCacheFolder, "bg.png")); + public static void CacheSideImage(string Side) + { + if (Side == null || string.IsNullOrWhiteSpace(Side) || Side.ToLower() == "unknown") return; + + string SideImagePath = Path.Combine(ImageCacheFolder, $"side_{Side.ToLower()}.png"); + + CacheImage($"{LauncherRoute}side_{Side.ToLower()}.png", SideImagePath); + } + + private static void CacheImage(string route, string filePath) + { + try + { + Directory.CreateDirectory(ImageCacheFolder); + + if (String.IsNullOrWhiteSpace(route) || CachedRoutes.Contains(route)) //Don't want to request the image if it was already cached this session. + { + return; + } + + using Stream s = new Request(null, LauncherSettingsProvider.Instance.Server.Url).Send(route, "GET", null, false); + + using MemoryStream ms = new MemoryStream(); + + s.CopyTo(ms); + + if (ms.Length == 0) return; + + using FileStream fs = File.Create(filePath); + ms.Seek(0, SeekOrigin.Begin); + ms.CopyTo(fs); + + CachedRoutes.Add(route); + } + catch (Exception ex) + { + LogManager.Instance.Exception(ex); + } + } + } +} diff --git a/project/Aki.Launcher.Base/MiniCommon/Json.cs b/project/Aki.Launcher.Base/MiniCommon/Json.cs new file mode 100644 index 0000000..ac7da63 --- /dev/null +++ b/project/Aki.Launcher.Base/MiniCommon/Json.cs @@ -0,0 +1,119 @@ +/* Json.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + * Merijn Hendriks + */ + + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.IO; +using System.Linq; + +namespace Aki.Launcher.MiniCommon +{ + public static class Json + { + public static string Serialize(T data) + { + return JsonConvert.SerializeObject(data); + } + + public static T Deserialize(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public static void Save(string filepath, T data) + { + string json = Serialize(data); + File.WriteAllText(filepath, json); + } + + /// + /// Save an object as json with formatting + /// + /// + /// Full path to file + /// Object to save to json file + /// NewtonSoft.Json Formatting + public static void SaveWithFormatting(string filepath, T data, Formatting format) + { + if (!File.Exists(filepath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(filepath)); + } + + File.WriteAllText(filepath, JsonConvert.SerializeObject(data, format)); + } + + /// + /// Load a class from file and don't save it if it doesn't exist. + /// + /// + /// Full path to the file to load + /// Allow null class property values to be returned. Default is false + /// Returns a class object or null + public static T LoadClassWithoutSaving(string filepath, bool AllowNullValues = false) where T : class + { + if (File.Exists(filepath)) + { + string json = File.ReadAllText(filepath); + + T classObject = JsonConvert.DeserializeObject(json); + + if (!AllowNullValues) + { + if (classObject.GetType().GetProperties().Any(x => x.GetValue(classObject) == null)) + { + return null; + } + } + + return classObject; + } + + return null; + } + + public static T Load(string filepath) where T : new() + { + if (!File.Exists(filepath)) + { + Save(filepath, new T()); + return Load(filepath); + } + + string json = File.ReadAllText(filepath); + return Deserialize(json); + } + + /// + /// Get a single property back from a json file. + /// + /// + /// Full Path to json file + /// Name of property to return + /// + public static T GetPropertyByName(string FilePath, string PropertyName) + { + using (StreamReader sr = new StreamReader(FilePath)) + { + var tempData = JObject.Parse(sr.ReadToEnd()); + + if (tempData != null) + { + if (tempData[PropertyName].Value() is T requestedData) + { + return requestedData; + } + } + } + + return default; + } + } +} diff --git a/project/Aki.Launcher.Base/MiniCommon/ProcessMonitor.cs b/project/Aki.Launcher.Base/MiniCommon/ProcessMonitor.cs new file mode 100644 index 0000000..477c7cf --- /dev/null +++ b/project/Aki.Launcher.Base/MiniCommon/ProcessMonitor.cs @@ -0,0 +1,59 @@ +/* ProcessMonitor.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using System; +using System.Diagnostics; +using System.Timers; + +namespace Aki.Launcher.MiniCommon +{ + public class ProcessMonitor + { + private Timer monitor; + private readonly string processName; + private readonly Action aliveCallback; + private readonly Action exitCallback; + + public ProcessMonitor(string processName, double interval, Action aliveCallback, Action exitCallback) + { + monitor = new Timer(interval); + monitor.Elapsed += OnPollEvent; + monitor.AutoReset = true; + + this.processName = processName; + this.aliveCallback = aliveCallback; + this.exitCallback = exitCallback; + } + + public void Start() + { + monitor.Enabled = true; + } + + public void Stop() + { + monitor.Enabled = false; + } + + private void OnPollEvent(object source, ElapsedEventArgs e) + { + Process[] clientProcess = Process.GetProcessesByName(processName); + + // client instances still running + if (clientProcess.Length > 0) + { + aliveCallback(this); + return; + } + + // all client instances stopped running + exitCallback(this); + } + } +} diff --git a/project/Aki.Launcher.Base/MiniCommon/Request.cs b/project/Aki.Launcher.Base/MiniCommon/Request.cs new file mode 100644 index 0000000..625efe7 --- /dev/null +++ b/project/Aki.Launcher.Base/MiniCommon/Request.cs @@ -0,0 +1,105 @@ +/* Request.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using ComponentAce.Compression.Libs.zlib; +using System; +using System.IO; +using System.Net; +using System.Text; + +namespace Aki.Launcher.MiniCommon +{ + public class Request + { + public string Session; + public string RemoteEndPoint; + + public Request(string session, string remoteEndPoint) + { + Session = session; + RemoteEndPoint = remoteEndPoint; + } + + public Stream Send(string url, string method = "GET", string data = null, bool compress = true) + { + // disable SSL encryption + ServicePointManager.Expect100Continue = true; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; + + // set session headers + var request = WebRequest.Create(new Uri(RemoteEndPoint + url)); + + if (!string.IsNullOrWhiteSpace(Session)) + { + request.Headers.Add("Cookie", $"PHPSESSID={Session}"); + request.Headers.Add("SessionId", Session); + } + + request.Headers.Add("Accept-Encoding", "deflate"); + request.Method = method; + + if (method != "GET" && !string.IsNullOrWhiteSpace(data)) + { + // set request body + var bytes = (compress) ? SimpleZlib.CompressToBytes(data, zlibConst.Z_BEST_COMPRESSION) : Encoding.UTF8.GetBytes(data); + + request.ContentType = "application/json"; + request.ContentLength = bytes.Length; + + if (compress) + { + request.Headers.Add("Content-Encoding", "deflate"); + } + + using (var stream = request.GetRequestStream()) + { + stream.Write(bytes, 0, bytes.Length); + } + } + + // get response stream + try + { + var response = request.GetResponse(); + return response.GetResponseStream(); + } + catch (Exception) + { + // Not sure why this was a unityengine debug logger. Possilby used by another module? + } + + return null; + } + + public string GetJson(string url, bool compress = true) + { + using (var stream = Send(url, "GET", null, compress)) + { + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + return SimpleZlib.Decompress(ms.ToArray(), null); + } + } + } + + public string PostJson(string url, string data, bool compress = true) + { + using (var stream = Send(url, "POST", data, compress)) + { + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + return SimpleZlib.Decompress(ms.ToArray(), null); + } + } + } + } +} diff --git a/project/Aki.Launcher.Base/MiniCommon/VFS.cs b/project/Aki.Launcher.Base/MiniCommon/VFS.cs new file mode 100644 index 0000000..32b3fae --- /dev/null +++ b/project/Aki.Launcher.Base/MiniCommon/VFS.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Aki.Launcher.MiniCommon +{ + public static class VFS + { + public static string Cwd { get; private set; } + private static object mutex; + + static VFS() + { + Cwd = Environment.CurrentDirectory; + mutex = new object(); + } + + /// + /// Combine two filepaths. + /// + public static string Combine(string path1, string path2) + { + return Path.Combine(path1, path2); + } + + /// + /// Combines the filepath with the current working directory. + /// + public static string FromCwd(this string filepath) + { + return Combine(Cwd, filepath); + } + + /// + /// Get directory path of a filepath. + /// + public static string GetDirectory(this string filepath) + { + string value = Path.GetDirectoryName(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : ""; + } + + /// + /// Get file of a filepath + /// + public static string GetFile(this string filepath) + { + string value = Path.GetFileName(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : ""; + } + + /// + /// Get file name of a filepath + /// + public static string GetFileName(this string filepath) + { + string value = Path.GetFileNameWithoutExtension(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : ""; + } + + /// + /// Get file extension of a filepath. + /// + public static string GetFileExtension(this string filepath) + { + string value = Path.GetExtension(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : ""; + } + + /// + /// Move file from one place to another + /// + public static void MoveFile(string a, string b) + { + lock (mutex) + { + new FileInfo(a).MoveTo(b); + } + } + + /// + /// Does the filepath exist? + /// + public static bool Exists(string filepath) + { + lock (mutex) + { + return Directory.Exists(filepath) || File.Exists(filepath); + } + } + + /// + /// Create directory (recursive). + /// + public static void CreateDirectory(string filepath) + { + lock (mutex) + { + Directory.CreateDirectory(filepath); + } + } + + /// + /// Get file content as bytes. + /// + public static byte[] ReadFile(string filepath) + { + lock (mutex) + { + return File.ReadAllBytes(filepath); + } + } + + /// + /// Get file content as string. + /// + public static string ReadFile(string filepath, Encoding encoding = null) + { + return (encoding ?? Encoding.UTF8).GetString(ReadFile(filepath)); + } + + /// + /// Write data to file. + /// + public static void WriteFile(string filepath, byte[] data, bool append = false) + { + lock (mutex) + { + if (!Exists(filepath)) + { + CreateDirectory(filepath.GetDirectory()); + } + + File.WriteAllBytes(filepath, data); + } + } + + /// + /// Write string to file. + /// + public static void WriteFile(string filepath, string data, bool append = false, Encoding encoding = null) + { + WriteFile(filepath, (encoding ?? Encoding.UTF8).GetBytes(data), append); + } + + /// + /// Get directories in directory by full path. + /// + public static string[] GetDirectories(string filepath) + { + lock (mutex) + { + DirectoryInfo di = new DirectoryInfo(filepath); + List paths = new List(); + + foreach (DirectoryInfo directory in di.GetDirectories()) + { + paths.Add(directory.FullName); + } + + return paths.ToArray(); + } + } + + /// + /// Get files in directory by full path. + /// + public static string[] GetFiles(string filepath) + { + lock (mutex) + { + DirectoryInfo di = new DirectoryInfo(filepath); + List paths = new List(); + + foreach (FileInfo file in di.GetFiles()) + { + paths.Add(file.FullName); + } + + return paths.ToArray(); + } + } + + /// + /// Delete directory. + /// + public static void DeleteDirectory(string filepath) + { + lock (mutex) + { + DirectoryInfo di = new DirectoryInfo(filepath); + + foreach (FileInfo file in di.GetFiles()) + { + file.IsReadOnly = false; + file.Delete(); + } + + foreach (DirectoryInfo directory in di.GetDirectories()) + { + DeleteDirectory(directory.FullName); + } + + di.Delete(); + } + } + + /// + /// Delete file. + /// + public static void DeleteFile(string filepath) + { + lock (mutex) + { + FileInfo file = new FileInfo(filepath); + + file.IsReadOnly = false; + file.Delete(); + } + } + + /// + /// Get files count inside directory recusively + /// + public static int GetFilesCount(string filepath) + { + lock (mutex) + { + DirectoryInfo di = new DirectoryInfo(filepath); + int count = 0; + + foreach (FileInfo file in di.GetFiles()) + { + ++count; + } + + foreach (DirectoryInfo directory in di.GetDirectories()) + { + count += GetFilesCount(directory.FullName); + } + + return count; + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Launcher.Base/Models/Aki/AccountInfo.cs b/project/Aki.Launcher.Base/Models/Aki/AccountInfo.cs new file mode 100644 index 0000000..82b119e --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/AccountInfo.cs @@ -0,0 +1,31 @@ +/* AccountInfo.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +namespace Aki.Launcher +{ + public class AccountInfo + { + public string id; + public string nickname; + public string username; + public string password; + public bool wipe; + public string edition; + + public AccountInfo() + { + id = ""; + nickname = ""; + username = ""; + password = ""; + wipe = false; + edition = ""; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Aki/AkiData.cs b/project/Aki.Launcher.Base/Models/Aki/AkiData.cs new file mode 100644 index 0000000..8ed6d3d --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/AkiData.cs @@ -0,0 +1,7 @@ +namespace Aki.Launcher.Models.Aki +{ + public class AkiData + { + public string version { get; set; } + } +} \ No newline at end of file diff --git a/project/Aki.Launcher.Base/Models/Aki/AkiVersion.cs b/project/Aki.Launcher.Base/Models/Aki/AkiVersion.cs new file mode 100644 index 0000000..255af0e --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/AkiVersion.cs @@ -0,0 +1,64 @@ +using System.ComponentModel; + +namespace Aki.Launch.Models.Aki +{ + public class AkiVersion : INotifyPropertyChanged + { + public int Major; + public int Minor; + public int Build; + + public bool HasTag => Tag != null; + + private string _Tag = null; + public string Tag + { + get => _Tag; + set + { + if(_Tag != value) + { + _Tag = value; + RaisePropertyChanged(nameof(Tag)); + RaisePropertyChanged(nameof(HasTag)); + } + } + } + + public void ParseVersionInfo(string AkiVersion) + { + if (AkiVersion.Contains('-')) + { + string[] versionInfo = AkiVersion.Split('-'); + + AkiVersion = versionInfo[0]; + + Tag = versionInfo[1]; + return; + } + + string[] splitVersion = AkiVersion.Split('.'); + + if (splitVersion.Length == 3) + { + int.TryParse(splitVersion[0], out Major); + int.TryParse(splitVersion[1], out Minor); + int.TryParse(splitVersion[2], out Build); + } + } + + public AkiVersion() { } + + public AkiVersion(string AkiVersion) + { + ParseVersionInfo(AkiVersion); + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Aki/ChangeRequestData.cs b/project/Aki.Launcher.Base/Models/Aki/ChangeRequestData.cs new file mode 100644 index 0000000..7a1f0fe --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/ChangeRequestData.cs @@ -0,0 +1,25 @@ +/* ChangeRequestData.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +namespace Aki.Launcher +{ + public struct ChangeRequestData + { + public string username; + public string password; + public string change; + + public ChangeRequestData(string username, string password, string change) + { + this.username = username; + this.password = password; + this.change = change; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Aki/LoginRequestData.cs b/project/Aki.Launcher.Base/Models/Aki/LoginRequestData.cs new file mode 100644 index 0000000..7132a90 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/LoginRequestData.cs @@ -0,0 +1,23 @@ +/* LoginRequestData.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +namespace Aki.Launcher +{ + public struct LoginRequestData + { + public string username; + public string password; + + public LoginRequestData(string username, string password) + { + this.username = username; + this.password = password; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Aki/RegisterRequestData.cs b/project/Aki.Launcher.Base/Models/Aki/RegisterRequestData.cs new file mode 100644 index 0000000..f0a5abf --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/RegisterRequestData.cs @@ -0,0 +1,25 @@ +/* RegisterRequestData.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +namespace Aki.Launcher +{ + public struct RegisterRequestData + { + public string username; + public string password; + public string edition; + + public RegisterRequestData(string username, string password, string edition) + { + this.username = username; + this.password = password; + this.edition = edition; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Aki/ServerInfo.cs b/project/Aki.Launcher.Base/Models/Aki/ServerInfo.cs new file mode 100644 index 0000000..56ffb73 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/ServerInfo.cs @@ -0,0 +1,25 @@ +/* ServerInfo.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +namespace Aki.Launcher +{ + public class ServerInfo + { + public string backendUrl; + public string name; + public string[] editions; + + public ServerInfo() + { + backendUrl = "http://127.0.0.1:6969"; + name = "Local SPT-AKI Server"; + editions = new string[0]; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Aki/ServerProfileInfo.cs b/project/Aki.Launcher.Base/Models/Aki/ServerProfileInfo.cs new file mode 100644 index 0000000..e974a13 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Aki/ServerProfileInfo.cs @@ -0,0 +1,23 @@ +/* ServerProfileInfo.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +namespace Aki.Launcher.Models.Aki +{ + public class ServerProfileInfo + { + public string username { get; set; } + public string nickname { get; set; } + public string side { get; set; } + public int currlvl { get; set; } + public long currexp { get; set; } + public long prevexp { get; set; } + public long nextlvl { get; set; } + public int maxlvl { get; set; } + public AkiData akiData { get; set; } + } +} diff --git a/project/Aki.Launcher.Base/Models/EFT/ClientConfig.cs b/project/Aki.Launcher.Base/Models/EFT/ClientConfig.cs new file mode 100644 index 0000000..6e87ad1 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/EFT/ClientConfig.cs @@ -0,0 +1,29 @@ +/* ClientConfig.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +namespace Aki.Launcher +{ + public class ClientConfig + { + public string BackendUrl; + public string Version; + + public ClientConfig() + { + BackendUrl = "http://127.0.0.1:6969"; + Version = "live"; + } + + public ClientConfig(string backendUrl) + { + BackendUrl = backendUrl; + Version = "live"; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/EFT/LoginToken.cs b/project/Aki.Launcher.Base/Models/EFT/LoginToken.cs new file mode 100644 index 0000000..7d7a435 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/EFT/LoginToken.cs @@ -0,0 +1,27 @@ +/* LoginToken.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +namespace Aki.Launcher +{ + public struct LoginToken + { + public string username; + public string password; + public bool toggle; + public long timestamp; + + public LoginToken(string username, string password) + { + this.username = username; + this.password = password; + toggle = true; + timestamp = 0; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/ConnectServerModel.cs b/project/Aki.Launcher.Base/Models/Launcher/ConnectServerModel.cs new file mode 100644 index 0000000..6837d96 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/ConnectServerModel.cs @@ -0,0 +1,51 @@ +/* ConnectServerModel.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using System.ComponentModel; + +namespace Aki.Launcher.Models.Launcher +{ + public class ConnectServerModel : INotifyPropertyChanged + { + private string _InfoText; + public string InfoText + { + get => _InfoText; + set + { + if (_InfoText != value) + { + _InfoText = value; + RaisePropertyChanged(nameof(InfoText)); + } + } + } + + private bool _ConnectionFailed; + public bool ConnectionFailed + { + get => _ConnectionFailed; + set + { + if(_ConnectionFailed != value) + { + _ConnectionFailed = value; + RaisePropertyChanged(nameof(ConnectionFailed)); + } + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/EditionCollection.cs b/project/Aki.Launcher.Base/Models/Launcher/EditionCollection.cs new file mode 100644 index 0000000..22911c8 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/EditionCollection.cs @@ -0,0 +1,72 @@ +/* EditionCollection.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using System.Collections.ObjectModel; +using System.ComponentModel; + +namespace Aki.Launcher.Models.Launcher +{ + public class EditionCollection : INotifyPropertyChanged + { + private bool _HasSelection; + public bool HasSelection + { + get => _HasSelection; + set + { + if(_HasSelection != value) + { + _HasSelection = value; + RaisePropertyChanged(nameof(HasSelection)); + } + } + } + private int _SelectedEditionIndex; + public int SelectedEditionIndex + { + get => _SelectedEditionIndex; + set + { + if (_SelectedEditionIndex != value) + { + _SelectedEditionIndex = value; + RaisePropertyChanged(nameof(SelectedEditionIndex)); + } + } + } + + private string _SelectedEdition; + public string SelectedEdition + { + get => _SelectedEdition; + set + { + if (_SelectedEdition != value) + { + _SelectedEdition = value; + HasSelection = _SelectedEdition != null; + RaisePropertyChanged(nameof(SelectedEdition)); + } + } + } + public ObservableCollection AvailableEditions { get; private set; } = new ObservableCollection(ServerManager.SelectedServer.editions); + + public EditionCollection() + { + SelectedEditionIndex = 0; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/GameStarterResult.cs b/project/Aki.Launcher.Base/Models/Launcher/GameStarterResult.cs new file mode 100644 index 0000000..912254a --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/GameStarterResult.cs @@ -0,0 +1,59 @@ +/* GameStarterResult.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + * waffle.lord + */ + +using Aki.Launcher.Helpers; + +namespace Aki.Launcher.Models.Launcher +{ + public class GameStarterResult + { + public bool Succeeded => Message == null; + public string Message { get; } = null; + protected GameStarterResult(int ServerStatus) + { + switch (ServerStatus) + { + case 1: + break; + case -1: + Message = LocalizationProvider.Instance.installed_in_live_game_warning; + break; + + case -2: + Message = LocalizationProvider.Instance.no_official_game_warning; + break; + + case -3: + Message = LocalizationProvider.Instance.failed_to_receive_patches; + break; + + case -4: + Message = LocalizationProvider.Instance.failed_core_patch; + break; + + case -5: + Message = LocalizationProvider.Instance.failed_mod_patch; + break; + + case -6: + Message = LocalizationProvider.Instance.eft_exe_not_found_warning; + break; + + default: + Message = LocalizationProvider.Instance.login_failed; + break; + } + } + + public static GameStarterResult FromSuccess() => + new GameStarterResult(1); + public static GameStarterResult FromError(int ServerStatus) => + new GameStarterResult(ServerStatus); + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/LauncherAction.cs b/project/Aki.Launcher.Base/Models/Launcher/LauncherAction.cs new file mode 100644 index 0000000..0fef268 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/LauncherAction.cs @@ -0,0 +1,18 @@ +/* LauncherAction.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + + +namespace Aki.Launcher.Models.Launcher +{ + public enum LauncherAction + { + MinimizeAction, + DoNothingAction, + ExitAction + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/LocaleCollection.cs b/project/Aki.Launcher.Base/Models/Launcher/LocaleCollection.cs new file mode 100644 index 0000000..586554b --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/LocaleCollection.cs @@ -0,0 +1,48 @@ +/* LocaleCollection.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + + +using Aki.Launcher.Helpers; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; + +namespace Aki.Launcher.Models.Launcher +{ + public class LocaleCollection : INotifyPropertyChanged + { + private string _SelectedLocale; + public string SelectedLocale + { + get => _SelectedLocale; + set + { + if (_SelectedLocale != value) + { + _SelectedLocale = value; + RaisePropertyChanged(nameof(SelectedLocale)); + LocalizationProvider.LoadLocaleFromFile(value); + } + } + } + + public ObservableCollection AvailableLocales { get; set; } = LocalizationProvider.GetAvailableLocales(); + + public event PropertyChangedEventHandler PropertyChanged; + + public LocaleCollection() + { + SelectedLocale = LocalizationProvider.LocaleNameDictionary.GetValueOrDefault(LauncherSettingsProvider.Instance.DefaultLocale, "English"); + } + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/LocalizedLauncherAction.cs b/project/Aki.Launcher.Base/Models/Launcher/LocalizedLauncherAction.cs new file mode 100644 index 0000000..7aa3481 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/LocalizedLauncherAction.cs @@ -0,0 +1,60 @@ +using Aki.Launcher.Helpers; +using Aki.Launcher.Models.Launcher; +using System.ComponentModel; +using System.Text.RegularExpressions; + +namespace Aki.Launcher.Models +{ + public class LocalizedLauncherAction : INotifyPropertyChanged + { + public LauncherAction Action { get; set; } + + private string _Name; + public string Name + { + get => _Name; + set + { + if(_Name != value) + { + _Name = value; + RaisePropertyChanged(nameof(Name)); + } + } + } + + public void UpdateLocaleName() + { + string value = Action.ToString(); + + //this adds an underscore before capitalized letters, except if it is the first letter in the string. Then it is lower cased. + //The result should be the name of the localization providers property you want to use. + //Example: MinimizeAction -> minimize_action + string localePropertyName = Regex.Replace(value, "(? _Username; + set + { + if (_Username != value) + { + _Username = value; + RaisePropertyChanged(nameof(Username)); + } + } + } + + private string _Password = ""; + public string Password + { + get => _Password; + set + { + if (_Password != value) + { + _Password = value; + RaisePropertyChanged(nameof(Password)); + } + } + } + + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/MenuBarItem.cs b/project/Aki.Launcher.Base/Models/Launcher/MenuBarItem.cs new file mode 100644 index 0000000..ca26e2c --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/MenuBarItem.cs @@ -0,0 +1,70 @@ +/* MenuBarItem.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +using System; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Aki.Launcher.Models.Launcher +{ + public class MenuBarItem : INotifyPropertyChanged + { + private string _Name; + public string Name + { + get => _Name; + set + { + if (_Name != value) + { + _Name = value; + RaisePropertyChanged(nameof(Name)); + } + } + } + + private bool _IsSelected; + public bool IsSelected + { + get => _IsSelected; + set + { + if (_IsSelected != value) + { + _IsSelected = value; + RaisePropertyChanged(nameof(IsSelected)); + } + } + } + + private Action _ItemAction; + public Action ItemAction + { + get => _ItemAction; + set + { + if (_ItemAction != value) + { + _ItemAction = value; + RaisePropertyChanged(nameof(ItemAction)); + } + } + } + + public Func> CanUseAction = async () => await Task.FromResult(true); + + public Action OnFailedToUseAction = null; + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/Notifications/NotificationItem.cs b/project/Aki.Launcher.Base/Models/Launcher/Notifications/NotificationItem.cs new file mode 100644 index 0000000..634e754 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/Notifications/NotificationItem.cs @@ -0,0 +1,83 @@ +/* NotificationItem.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + + +using System; +using System.ComponentModel; + +namespace Aki.Launcher.Models.Launcher.Notifications +{ + public class NotificationItem : INotifyPropertyChanged + { + private string _Message; + public string Message + { + get => _Message; + set + { + if (_Message != value) + { + _Message = value; + RaisePropertyChanged(nameof(Message)); + } + } + } + + private string _ButtonText; + public string ButtonText + { + get => _ButtonText; + set + { + if (_ButtonText != value) + { + _ButtonText = value; + RaisePropertyChanged(nameof(ButtonText)); + } + } + } + + private bool _HasButton; + public bool HasButton + { + get => _HasButton; + set + { + if (_HasButton != value) + { + _HasButton = value; + RaisePropertyChanged(nameof(HasButton)); + } + } + } + + public Action ItemAction = null; + + public NotificationItem(string Message) + { + this.Message = Message; + ButtonText = string.Empty; + HasButton = false; + } + + public NotificationItem(string Message, string ButtonText, Action ItemAction) + { + this.Message = Message; + this.ButtonText = ButtonText; + HasButton = true; + this.ItemAction = ItemAction; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/Notifications/NotificationQueue.cs b/project/Aki.Launcher.Base/Models/Launcher/Notifications/NotificationQueue.cs new file mode 100644 index 0000000..ab4328b --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/Notifications/NotificationQueue.cs @@ -0,0 +1,159 @@ +/* NotificationQueue.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + + +using Aki.Launcher.Helpers; +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Timers; + +namespace Aki.Launcher.Models.Launcher.Notifications +{ + public class NotificationQueue : INotifyPropertyChanged, IDisposable + { + public Timer queueTimer = new Timer(); + private Timer animateChangeTimer = new Timer(230); + private Timer animateCloseTimer = new Timer(230); + + public ObservableCollection queue { get; set; } = new ObservableCollection(); + + private bool _ShowBanner; + public bool ShowBanner + { + get => _ShowBanner; + set + { + if (_ShowBanner != value) + { + _ShowBanner = value; + RaisePropertyChanged(nameof(ShowBanner)); + } + } + } + + public NotificationQueue(int ShowTimeInMiliseconds) + { + ShowBanner = false; + queueTimer.Interval = ShowTimeInMiliseconds; + queueTimer.Elapsed += QueueTimer_Elapsed; + + animateChangeTimer.Elapsed += AnimateChange_Elapsed; + animateCloseTimer.Elapsed += AnimateCloseTimer_Elapsed; + } + + private void AnimateCloseTimer_Elapsed(object sender, ElapsedEventArgs e) + { + animateCloseTimer.Stop(); + + queue.Clear(); + queueTimer.Stop(); + } + + public void CloseQueue() + { + ShowBanner = false; + animateCloseTimer.Start(); + } + + private void CheckAndShowNotifications() + { + if (!queueTimer.Enabled) + { + ShowBanner = true; + queueTimer.Start(); + } + } + + public void Enqueue(string Message, bool AutowNext = false, bool NoDefaultButton = false) + { + if (queue.Where(x => x.Message == Message).Count() == 0) + { + if (NoDefaultButton) + { + queue.Add(new NotificationItem(Message)); + } + else + { + queue.Add(new NotificationItem(Message, LocalizationProvider.Instance.ok, () => { })); + } + + CheckAndShowNotifications(); + + if (AutowNext && queue.Count == 2) + { + Next(true); + } + } + } + + public void Enqueue(string Message, string ButtonText, Action ButtonAction, bool AllowNext = false) + { + if (queue.Where(x => x.Message == Message && x.ButtonText == ButtonText).Count() == 0) + { + queue.Add(new NotificationItem(Message, ButtonText, ButtonAction)); + CheckAndShowNotifications(); + + if (AllowNext && queue.Count == 2) + { + Next(true); + } + } + } + + public void Next(bool ResetTimer = false) + { + if (queue.Count - 1 <= 0) + { + CloseQueue(); + return; + } + + if (ResetTimer) + { + queueTimer.Stop(); + queueTimer.Start(); + } + + ShowBanner = false; + animateChangeTimer.Start(); + } + + private void QueueTimer_Elapsed(object sender, ElapsedEventArgs e) + { + Next(); + } + + private void AnimateChange_Elapsed(object sender, ElapsedEventArgs e) + { + animateChangeTimer.Stop(); + + if (queue.Count > 0) + { + queue.RemoveAt(0); + } + + ShowBanner = true; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + + public void Dispose() + { + queueTimer.Dispose(); + animateChangeTimer.Dispose(); + animateCloseTimer.Dispose(); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/PatchResultInfo.cs b/project/Aki.Launcher.Base/Models/Launcher/PatchResultInfo.cs new file mode 100644 index 0000000..489a318 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/PatchResultInfo.cs @@ -0,0 +1,32 @@ +/* PatchResultInfo.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +using Aki.ByteBanger; + +namespace Aki.Launcher.Models.Launcher +{ + public class PatchResultInfo + { + public PatchResultType Status { get; } + + public int NumCompleted { get; } + + public int NumTotal { get; } + + public bool OK => (Status == PatchResultType.Success) || (Status == PatchResultType.AlreadyPatched); + + public int PercentComplete => (NumCompleted * 100) / NumTotal; + + public PatchResultInfo(PatchResultType Status, int NumCompleted, int NumTotal) + { + this.Status = Status; + this.NumCompleted = NumCompleted; + this.NumTotal = NumTotal; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/ProfileInfo.cs b/project/Aki.Launcher.Base/Models/Launcher/ProfileInfo.cs new file mode 100644 index 0000000..0503cbc --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/ProfileInfo.cs @@ -0,0 +1,281 @@ +/* ProfileInfo.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +using Aki.Launch.Models.Aki; +using Aki.Launcher.Helpers; +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models.Aki; +using System; +using System.ComponentModel; +using System.IO; + +namespace Aki.Launcher.Models.Launcher +{ + public class ProfileInfo : INotifyPropertyChanged + { + private string _Username; + public string Username + { + get => _Username; + set + { + if(_Username != value) + { + _Username = value; + RaisePropertyChanged(nameof(Username)); + } + } + } + + private string _Nickname; + public string Nickname + { + get => _Nickname; + set + { + if (_Nickname != value) + { + _Nickname = value; + RaisePropertyChanged(nameof(Nickname)); + } + } + } + + private string _SideImage; + public string SideImage + { + get => _SideImage; + set + { + if (_SideImage != value) + { + _SideImage = value; + RaisePropertyChanged(nameof(SideImage)); + } + } + } + + private string _Side; + public string Side + { + get => _Side; + set + { + if (_Side != value) + { + _Side = value; + RaisePropertyChanged(nameof(Side)); + } + } + } + + private string _Level; + public string Level + { + get => _Level; + set + { + if (_Level != value) + { + _Level = value; + RaisePropertyChanged(nameof(Level)); + } + } + } + + private int _XPLevelProgress; + public int XPLevelProgress + { + get => _XPLevelProgress; + set + { + if (_XPLevelProgress != value) + { + _XPLevelProgress = value; + RaisePropertyChanged(nameof(XPLevelProgress)); + } + } + } + + private long _CurrentXP; + public long CurrentExp + { + get => _CurrentXP; + set + { + if (_CurrentXP != value) + { + _CurrentXP = value; + RaisePropertyChanged(nameof(CurrentExp)); + } + } + } + + private long _RemainingExp; + public long RemainingExp + { + get => _RemainingExp; + set + { + if (_RemainingExp != value) + { + _RemainingExp = value; + RaisePropertyChanged(nameof(RemainingExp)); + } + } + } + + private long _NextLvlExp; + public long NextLvlExp + { + get => _NextLvlExp; + set + { + if (_NextLvlExp != value) + { + _NextLvlExp = value; + RaisePropertyChanged(nameof(NextLvlExp)); + } + } + } + + private bool _HasData; + public bool HasData + { + get => _HasData; + set + { + if (_HasData != value) + { + _HasData = value; + RaisePropertyChanged(nameof(HasData)); + } + } + } + + public string MismatchMessage => VersionMismatch ? LocalizationProvider.Instance.profile_version_mismath : null; + + private bool _VersionMismatch; + public bool VersionMismatch + { + get => _VersionMismatch; + set + { + if(_VersionMismatch != value) + { + _VersionMismatch = value; + RaisePropertyChanged(nameof(VersionMismatch)); + } + } + } + + private AkiData _Aki; + public AkiData Aki + { + get => _Aki; + set + { + if(_Aki != value) + { + _Aki = value; + RaisePropertyChanged(nameof(Aki)); + } + } + } + + public void UpdateDisplayedProfile(ProfileInfo PInfo) + { + if (PInfo.Side == null || string.IsNullOrWhiteSpace(PInfo.Side) || PInfo.Side == "unknown") return; + + HasData = true; + Nickname = PInfo.Nickname; + Side = PInfo.Side; + SideImage = PInfo.SideImage; + Level = PInfo.Level; + CurrentExp = PInfo.CurrentExp; + NextLvlExp = PInfo.NextLvlExp; + RemainingExp = PInfo.RemainingExp; + XPLevelProgress = PInfo.XPLevelProgress; + + Aki = PInfo.Aki; + } + + /// + /// Checks if the aki versions are compatible (non-major changes) + /// + /// + /// + /// + private bool CompareVersions(string CurrentVersion, string ExpectedVersion) + { + if (ExpectedVersion == "") return false; + + AkiVersion v1 = new AkiVersion(CurrentVersion); + AkiVersion v2 = new AkiVersion(ExpectedVersion); + + // check 'X'.x.x + if (v1.Major != v2.Major) return false; + + // check x.'X'.x + if(v1.Minor != v2.Minor) return false; + + //otherwise probably good + return true; + } + + public ProfileInfo(ServerProfileInfo serverProfileInfo) + { + Username = serverProfileInfo.username; + Nickname = serverProfileInfo.nickname; + Side = serverProfileInfo.side; + + Aki = serverProfileInfo.akiData; + + if (Aki != null) + { + VersionMismatch = !CompareVersions(Aki.version, ServerManager.GetVersion()); + } + + SideImage = Path.Combine(ImageRequest.ImageCacheFolder, $"side_{Side.ToLower()}.png"); + + if (Side != null && !string.IsNullOrWhiteSpace(Side) && Side != "unknown") + { + HasData = true; + } + else + { + HasData = false; + } + + Level = serverProfileInfo.currlvl.ToString(); + CurrentExp = serverProfileInfo.currexp; + + //check if player is max level + if (Level == serverProfileInfo.maxlvl.ToString()) + { + NextLvlExp = 0; + XPLevelProgress = 100; + return; + } + + NextLvlExp = serverProfileInfo.nextlvl; + RemainingExp = NextLvlExp - CurrentExp; + + long currentLvlTotal = NextLvlExp - serverProfileInfo.prevexp; + + XPLevelProgress = (int)Math.Floor((((double)currentLvlTotal) - RemainingExp) / currentLvlTotal * 100); + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/ProgressInfo.cs b/project/Aki.Launcher.Base/Models/Launcher/ProgressInfo.cs new file mode 100644 index 0000000..4a238ff --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/ProgressInfo.cs @@ -0,0 +1,22 @@ +/* ProgressInfo.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + */ + +namespace Aki.Launcher.Models.Launcher +{ + public class ProgressInfo + { + public int Percentage { get; private set; } + public string Message { get; private set; } + + public ProgressInfo(int Percentage, string Message) + { + this.Percentage = Percentage; + this.Message = Message; + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/RegisterModel.cs b/project/Aki.Launcher.Base/Models/Launcher/RegisterModel.cs new file mode 100644 index 0000000..4482b1e --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/RegisterModel.cs @@ -0,0 +1,53 @@ +/* RegisterModel.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + + +using System.ComponentModel; + +namespace Aki.Launcher.Models.Launcher +{ + public class RegisterModel : INotifyPropertyChanged + { + private string _Username; + public string Username + { + get => _Username; + set + { + if (_Username != value) + { + _Username = value; + RaisePropertyChanged(nameof(Username)); + } + } + } + + private string _Password; + public string Password + { + get => _Password; + set + { + if (_Password != value) + { + _Password = value; + RaisePropertyChanged(nameof(Password)); + } + } + } + + public EditionCollection EditionsCollection { get; set; } = new EditionCollection(); + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/ServerSetting.cs b/project/Aki.Launcher.Base/Models/Launcher/ServerSetting.cs new file mode 100644 index 0000000..31cf094 --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/ServerSetting.cs @@ -0,0 +1,54 @@ +/* ServerSetting.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * waffle.lord + * Merijn Hendriks + */ + + +using System.ComponentModel; + +namespace Aki.Launcher.Models.Launcher +{ + public class ServerSetting : INotifyPropertyChanged + { + public LoginModel AutoLoginCreds { get; set; } = null; + + private string _Name; + public string Name + { + get => _Name; + set + { + if (_Name != value) + { + _Name = value; + RaisePropertyChanged(nameof(Name)); + } + } + } + + private string _Url; + public string Url + { + get => _Url; + set + { + if (_Url != value) + { + _Url = value; + RaisePropertyChanged(nameof(Url)); + } + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void RaisePropertyChanged(string property) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + } +} diff --git a/project/Aki.Launcher.Base/Models/Launcher/WipeProfileModel.cs b/project/Aki.Launcher.Base/Models/Launcher/WipeProfileModel.cs new file mode 100644 index 0000000..e8f7dba --- /dev/null +++ b/project/Aki.Launcher.Base/Models/Launcher/WipeProfileModel.cs @@ -0,0 +1,15 @@ +/* WipeProfileModel.cs + * License: NCSA Open Source License + * + * Copyright: Merijn Hendriks + * AUTHORS: + * Merijn Hendriks + */ + +namespace Aki.Launcher.Models.Launcher +{ + public class WipeProfileModel + { + public EditionCollection EditionsCollection { get; set; } = new EditionCollection(); + } +} diff --git a/project/Aki.Launcher/.gitignore b/project/Aki.Launcher/.gitignore new file mode 100644 index 0000000..8afdcb6 --- /dev/null +++ b/project/Aki.Launcher/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/project/Aki.Launcher/Aki.Launcher.csproj b/project/Aki.Launcher/Aki.Launcher.csproj new file mode 100644 index 0000000..29ff631 --- /dev/null +++ b/project/Aki.Launcher/Aki.Launcher.csproj @@ -0,0 +1,49 @@ + + + WinExe + net6.0 + win10-x64 + true + enable + Assets\icon.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + References\Newtonsoft.Json.dll + + + References\zlib.net.dll + + + + + Designer + + + diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/Chinese (Simplified).json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Chinese (Simplified).json new file mode 100644 index 0000000..21bc6dd --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Chinese (Simplified).json @@ -0,0 +1,83 @@ +{ + "native_name": "简体中文", + "retry": "重试", + "server_connecting": "连接中", + "server_unavailable_format_1": "默认服务器'{0}'不可用。", + "no_servers_available": "找不到服务器。请检查设置中的服务器列表。", + "settings_menu": "设置", + "back": "返回", + "wipe_profile": "清除该存档", + "username": "电子邮箱", + "password": "密码", + "update": "更新", + "edit_account_update_error": "更新存档时出现了一些问题。", + "register": "注册", + "go_to_register": "去注册", + "login_or_register": "登录 / 注册", + "go_to_login": "去登录", + "login_automatically": "自动登录", + "incorrect_login": "邮箱或密码不正确", + "login_failed": "登录失败", + "edition": "游戏版本", + "id": "ID", + "logout": "登出", + "account": "Account", + "edit_account": "编辑个人存档", + "start_game": "开始游戏", + "installed_in_live_game_warning": "Aki不应该安装在在线版塔科夫的目录里,请复制游戏文件并在其他地方安装。", + "no_official_game_warning": "您的电脑上未安装《逃离塔科夫》。请购买游戏,支持游戏开发商!", + "eft_exe_not_found_warning": "在当前路径中找不到EscapeFromTarkov.exe。", + "account_exist": "该账户已存在", + "url": "URL", + "default_language": "默认语言", + "game_path": "游戏路径", + "clear_game_settings": "恢复默认游戏设置", + "clear_game_settings_warning": "您将要删除旧的游戏设置文件。它们将备份到:\n{0}\n\n您确定吗?", + "clear_game_settings_succeeded": "游戏设置已恢复默认", + "clear_game_settings_failed": "游戏设置恢复失败", + "remove_registry_keys": "移除注册表", + "remove_registry_keys_succeeded": "注册表已移除", + "remove_registry_keys_failed": "注册表移除失败", + "clean_temp_files": "清理临时文件", + "clean_temp_files_succeeded": "临时文件已清理", + "clean_temp_files_failed": "临时文件清理失败", + "select_folder": "选择文件夹", + "registration_failed": "注册失败", + "minimize_action": "最小化", + "do_nothing_action": "无动作", + "exit_action": "关闭启动器", + "on_game_start": "登录器在启动游戏后", + "game": "游戏", + "new_password": "新密码", + "cancel": "取消", + "need_an_account": "还没有账号吗?", + "have_an_account": "已经有账号了?", + "reapply_patch": "重新打包", + "failed_to_receive_patches": "接收补丁失败", + "failed_core_patch": "核心补丁安装失败", + "failed_mod_patch": "Mod补丁安装失败", + "ok": "好", + "account_page_denied": "帐户页被拒绝。您未登录或游戏正在运行。", + "account_updated": "您的帐户已更新", + "nickname": "昵称", + "side": "阵营", + "level": "等级", + "patching": "Patching", + "file_mismatch_dialog_message": "输入文件哈希与预期哈希不匹配。您的客户端文件可能使用了错误的AKI版本。\n\n是否继续?", + "yes": "确定", + "no": "取消", + "open_folder": "Open Folder", + "select_edition": "Select Edition", + "profile_created": "Profile Created", + "registration_question_format_1": "Profile '{0}' does not exist.\n\nWould you like to create it?", + "next_level_in": "Next level in", + "wipe_warning": "Changing your account edition requires a profile wipe. This will reset your game progress.", + "copied": "Copied", + "no_profile_data": "No profile data", + "profile_version_mismath": "Your profile was made using a different version of aki and may have issues", + "profile_removed": "Profile removed", + "profile_removal_failed": "Failed to remove profile", + "profile_remove_question_format_1": "Permanently remove profile '{0}'?", + "i_understand": "I Understand", + "game_version_mismatch_format_2": "SPT is unable to run, this is because SPT expected to find EFT version '{1}',\nbut instead found version '{0}'\n\nEnsure you've downgraded your EFT as described in the install guide\non the page you downloaded SPT from" +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/Chinese (Traditional).json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Chinese (Traditional).json new file mode 100644 index 0000000..4ef2cea --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Chinese (Traditional).json @@ -0,0 +1,83 @@ +{ + "native_name": "繁體中文", + "retry": "重試", + "server_connecting": "正在連接伺服器", + "server_unavailable_format_1": "伺服器'{0}'無法使用。", + "no_servers_available": "無法找出伺服器,請於設置瀏覽伺服器列表", + "settings_menu": "設定", + "back": "返回", + "wipe_profile": "刪除存檔", + "username": "用戶名稱", + "password": "密碼", + "update": "更新", + "edit_account_update_error": "更新存檔過程中發生錯誤", + "register": "登錄", + "go_to_register": "前往登錄", + "login_or_register": "登入 / 登錄", + "go_to_login": "前往登入", + "login_automatically": "自動登入", + "incorrect_login": "用戶名稱或密碼不正確", + "login_failed": "登入失敗", + "edition": "版本", + "id": "認證序碼", + "logout": "登出", + "account": "Account", + "edit_account": "編輯個人存檔", + "start_game": "開始遊戲", + "installed_in_live_game_warning": "Aki 並不建議於在線版塔哥夫檔案位置上安裝,請複制遊戲客戶端檔案並於其他位置安裝", + "no_official_game_warning": "您的電腦並未安裝正版逃離塔哥夫,請購買遊戲並支持遊戲開發者!", + "eft_exe_not_found_warning": "在當前路徑並未能找到 EscapeFromTarkov.exe, 請檢查檔案路徑是否正確", + "account_exist": "用戶名稱已存在", + "url": "URL", + "default_language": "預設語言", + "game_path": "遊戲路徑", + "clear_game_settings": "重置遊戲設定", + "clear_game_settings_warning": "You are about to remove your old game settings files. They will be backed up to:\n{0}\n\nAre you sure?", + "clear_game_settings_succeeded": "遊戲設定已重置", + "clear_game_settings_failed": "遊戲設定重置過程中發生錯誤", + "remove_registry_keys": "移除登錄檔機碼", + "remove_registry_keys_succeeded": "登錄檔機碼已移除", + "remove_registry_keys_failed": "移除登錄檔機碼過程中發生錯誤", + "clean_temp_files": "清理快取", + "clean_temp_files_succeeded": "已清理快取檔案", + "clean_temp_files_failed": "清理快取檔案過程中發生錯誤", + "select_folder": "請選擇文件夾", + "registration_failed": "登錄失敗", + "minimize_action": "視窗最小化", + "do_nothing_action": "不作任何變更", + "exit_action": "關閉啟動器", + "on_game_start": "於遊戲啟動後", + "game": "遊戲", + "new_password": "新密碼", + "cancel": "取消", + "need_an_account": "沒有帳號?", + "have_an_account": "已有帳號?", + "reapply_patch": "重新應用補缺包", + "failed_to_receive_patches": "接收補缺包失敗", + "failed_core_patch": "核心補缺失敗", + "failed_mod_patch": "改裝程式補缺失敗", + "ok": "確定", + "account_page_denied": "Account page denied. Either you are not logged in or the game is running.", + "account_updated": "Your account has been updated", + "nickname": "Nickname", + "side": "Side", + "level": "Level", + "patching": "Patching", + "file_mismatch_dialog_message": "The input file hash doesn't match the expected hash. You may be using the wrong version\nof AKI for your client files.\n\nDo you want to continue?", + "yes": "Yes", + "no": "No", + "open_folder": "Open Folder", + "select_edition": "Select Edition", + "profile_created": "Profile Created", + "registration_question_format_1": "Profile '{0}' does not exist.\n\nWould you like to create it?", + "next_level_in": "Next level in", + "wipe_warning": "Changing your account edition requires a profile wipe. This will reset your game progress.", + "copied": "Copied", + "no_profile_data": "No profile data", + "profile_version_mismath": "Your profile was made using a different version of aki and may have issues", + "profile_removed": "Profile removed", + "profile_removal_failed": "Failed to remove profile", + "profile_remove_question_format_1": "Permanently remove profile '{0}'?", + "i_understand": "I Understand", + "game_version_mismatch_format_2": "SPT is unable to run, this is because SPT expected to find EFT version '{1}',\nbut instead found version '{0}'\n\nEnsure you've downgraded your EFT as described in the install guide\non the page you downloaded SPT from" +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/English.json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/English.json new file mode 100644 index 0000000..d967441 --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/English.json @@ -0,0 +1,83 @@ +{ + "native_name": "English", + "retry": "Retry", + "server_connecting": "Connecting", + "server_unavailable_format_1": "Default server '{0}' is not available.", + "no_servers_available": "No Servers found. Check server list in settings.", + "settings_menu": "Settings", + "back": "Back", + "wipe_profile": "Wipe Profile", + "username": "Username", + "password": "Password", + "update": "Update", + "edit_account_update_error": "An issue occurred while updating your profile.", + "register": "Register", + "go_to_register": "Go to Register", + "login_or_register": "Login / Register", + "go_to_login": "Go to Login", + "login_automatically": "Login Automatically", + "incorrect_login": "Username or password is incorrect.", + "login_failed": "Login Failed", + "edition": "Edition", + "id": "ID", + "logout": "Logout", + "account": "Profile", + "edit_account": "Edit Profile", + "start_game": "Start Game", + "installed_in_live_game_warning": "Aki shouldn't be installed into the live game directory. Please install Aki into a copy of the game directory elsewhere on your computer.", + "no_official_game_warning": "Escape From Tarkov isn't installed on your computer. Please buy a copy of the game and support the developers!", + "eft_exe_not_found_warning": "EscapeFromTarkov.exe not found at game path. Please check that the directory is correct.", + "account_exist": "Profile already exists", + "url": "URL", + "default_language": "Default Language", + "game_path": "SPT Game Path", + "clear_game_settings": "Clear Game Settings", + "clear_game_settings_warning": "You are about to remove your old game settings files. They will be backed up to:\n{0}\n\nAre you sure?", + "clear_game_settings_succeeded": "Game settings cleared.", + "clear_game_settings_failed": "An issue occurred while clearing game settings.", + "remove_registry_keys": "Remove Registry Keys", + "remove_registry_keys_succeeded": "Registry keys removed.", + "remove_registry_keys_failed": "An issue occurred while removing registry keys.", + "clean_temp_files": "Clean Temp Files", + "clean_temp_files_succeeded": "Temp files cleaned.", + "clean_temp_files_failed": "An issue occurred while cleaning temp files.", + "select_folder": "Select Folder", + "registration_failed": "Registration Failed.", + "minimize_action": "Minimize", + "do_nothing_action": "Do nothing", + "exit_action": "Close Launcher", + "on_game_start": "On Game Start", + "game": "Game", + "new_password": "New Password", + "cancel": "Cancel", + "need_an_account": "Don't have a profile yet?", + "have_an_account": "Already have a profile?", + "reapply_patch": "Reapply Patch", + "failed_to_receive_patches": "Failed to receive patches", + "failed_core_patch": "Core patch failed", + "failed_mod_patch": "Mod patch failed", + "ok": "OK", + "account_page_denied": "Profile page denied. Either you are not logged in or the game is running.", + "account_updated": "Your profile has been updated", + "nickname": "Nickname", + "side": "Side", + "level": "Level", + "patching": "Patching", + "file_mismatch_dialog_message": "The input file hash doesn't match the expected hash. You may be using the wrong version\nof AKI for your client files.\n\nDo you want to continue?", + "yes": "Yes", + "no": "No", + "open_folder": "Open Folder", + "select_edition": "Select Edition", + "profile_created": "Profile Created", + "registration_question_format_1": "Profile '{0}' does not exist.\n\nWould you like to create it?", + "next_level_in": "Next level in", + "wipe_warning": "Changing your account edition requires a profile wipe. This will reset your game progress.", + "copied": "Copied", + "no_profile_data": "No profile data", + "profile_version_mismath": "Your profile was made using a different version of aki and may have issues", + "profile_removed": "Profile removed", + "profile_removal_failed": "Failed to remove profile", + "profile_remove_question_format_1": "Permanently remove profile '{0}'?", + "i_understand": "I Understand", + "game_version_mismatch_format_2": "SPT is unable to run, this is because SPT expected to find EFT version '{1}',\nbut instead found version '{0}'\n\nEnsure you've downgraded your EFT as described in the install guide\non the page you downloaded SPT from" +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/French.json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/French.json new file mode 100644 index 0000000..e0f66fa --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/French.json @@ -0,0 +1,83 @@ +{ + "native_name": "Français", + "retry": "Réessayer", + "server_connecting": "Connexion", + "server_unavailable_format_1": "Le serveur '{0}' n'est pas disponible.", + "no_servers_available": "Aucun serveur détecté. Veuillez vérifier la liste des serveurs dans les paramètres.", + "settings_menu": "Paramètres", + "back": "Retour", + "wipe_profile": "Réinitialiser le profil", + "username": "Nom d'utilisateur", + "password": "Mot de passe", + "update": "Mettre à jour", + "edit_account_update_error": "Une erreur est survenue pendant la mise à jour de votre compte.", + "register": "Inscription", + "go_to_register": "S'inscrire", + "login_or_register": "Connexion / Inscription", + "go_to_login": "Se connecter", + "login_automatically": "Se connecter automatiquement", + "incorrect_login": "Nom d'utilisateur ou mot de passe invalide.", + "login_failed": "Échec de la connexion", + "edition": "Édition", + "id": "ID", + "logout": "Se déconnecter", + "account": "Account", + "edit_account": "Modifier le compte", + "start_game": "Lancer le jeu", + "installed_in_live_game_warning": "Le Launcher Aki ne devrait pas être installé dans le répertoire du jeu d'origine. Veuillez créer une copie des fichiers du jeu et installer le Launcher Aki dedans.", + "no_official_game_warning": "Escape From Tarkov n'est pas installé sur votre ordinateur. S’il vous plaît, achetez une copie officielle et soutenez les développeurs !", + "eft_exe_not_found_warning": "EscapeFromTarkov.exe est introuvable dans le répertoire du jeu.", + "account_exist": "Ce compte existe déjà.", + "url": "URL", + "default_language": "Langue par défaut", + "game_path": "Répertoire du jeu", + "clear_game_settings": "Réinitialiser les paramètres du jeu", + "clear_game_settings_warning": "You are about to remove your old game settings files. They will be backed up to:\n{0}\n\nAre you sure?", + "clear_game_settings_succeeded": "Paramètres du jeu réinitialisés.", + "clear_game_settings_failed": "Un problème est survenu pendant la réinitialisation des paramètres de jeu.", + "remove_registry_keys": "Réinitialiser les clés du registre", + "remove_registry_keys_succeeded": "Clés de registre réinitialisés.", + "remove_registry_keys_failed": "Un problème est survenu pendant la réinitialisation des clés de registre.", + "clean_temp_files": "Nettoyer les fichiers temporaires", + "clean_temp_files_succeeded": "Fichiers temporaires nettoyés.", + "clean_temp_files_failed": "Un problème est survenu pendant le nettoyage des fichiers temporaires.", + "select_folder": "Sélectionner un dossier", + "registration_failed": "L'inscription a échoué", + "minimize_action": "Réduire le Launcher", + "do_nothing_action": "Ne rien faire", + "exit_action": "Fermer le Launcher", + "on_game_start": "Au démarrage du jeu", + "game": "Jeu", + "new_password": "Nouveau mot de passe", + "cancel": "Annuler", + "need_an_account": "Pas encore de compte ?", + "have_an_account": "Déjà un compte ?", + "reapply_patch": "Repatcher", + "failed_to_receive_patches": "Erreur lors du téléchargement des patchs", + "failed_core_patch": "Erreur lors de la mise à jour de l’application", + "failed_mod_patch": "Erreur lors de la mise à jour des mods", + "ok": "OK", + "account_page_denied": "Account page denied. Either you are not logged in or the game is running.", + "account_updated": "Your account has been updated", + "nickname": "Nickname", + "side": "Side", + "level": "Level", + "patching": "Patching", + "file_mismatch_dialog_message": "The input file hash doesn't match the expected hash. You may be using the wrong version\nof AKI for your client files.\n\nDo you want to continue?", + "yes": "Yes", + "no": "No", + "open_folder": "Open Folder", + "select_edition": "Select Edition", + "profile_created": "Profile Created", + "registration_question_format_1": "Profile '{0}' does not exist.\n\nWould you like to create it?", + "next_level_in": "Next level in", + "wipe_warning": "Changing your account edition requires a profile wipe. This will reset your game progress.", + "copied": "Copied", + "no_profile_data": "No profile data", + "profile_version_mismath": "Your profile was made using a different version of aki and may have issues", + "profile_removed": "Profile removed", + "profile_removal_failed": "Failed to remove profile", + "profile_remove_question_format_1": "Permanently remove profile '{0}'?", + "i_understand": "I Understand", + "game_version_mismatch_format_2": "SPT is unable to run, this is because SPT expected to find EFT version '{1}',\nbut instead found version '{0}'\n\nEnsure you've downgraded your EFT as described in the install guide\non the page you downloaded SPT from" +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/German.json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/German.json new file mode 100644 index 0000000..2e11e2e --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/German.json @@ -0,0 +1,83 @@ +{ + "native_name": "Deutsch", + "retry": "Erneut versuchen", + "server_connecting": "Verbinden", + "server_unavailable_format_1": "Der Server '{0}' ist nicht verfügbar.", + "no_servers_available": "Kein Server gefunden. Die Serverliste kann in den Einstellungen bearbeitet werden.", + "settings_menu": "Einstellungen", + "back": "Zurück", + "wipe_profile": "Profil löschen", + "username": "Benutzername", + "password": "Passwort", + "update": "Aktualisieren", + "edit_account_update_error": "Das Profil konnte aufgrund eines Fehlers nicht aktualisiert werden.", + "register": "Registrieren", + "go_to_register": "Zur Registrierung gehen", + "login_or_register": "Anmelden / Registrieren", + "go_to_login": "Zum Login gehen", + "login_automatically": "Automatisch anmelden", + "incorrect_login": "Die E-Mail-Adresse und das Passwort stimmen nicht überein.", + "login_failed": "Anmeldung fehlgeschlagen", + "edition": "Edition", + "id": "ID", + "logout": "Abmelden", + "account": "Account", + "edit_account": "Profil bearbeiten", + "start_game": "Spiel starten", + "installed_in_live_game_warning": "Aki sollte nicht im \"Online\"-Spielordner installiert werden. Bitte kopieren Sie die Spieldateien in einen neuen Ordner und installieren Sie Aki dort.", + "no_official_game_warning": "Escape From Tarkov ist auf dem System nicht installiert. Bitte erwerben Sie eine originale Kopie des Spiels, um die Entwickler zu unterstützen.", + "eft_exe_not_found_warning": "EscapeFromTarkov.exe konnte nicht gefunden werden.", + "account_exist": "Dieser Account existiert bereits.", + "url": "URL", + "default_language": "Standardsprache", + "game_path": "Spielpfad", + "clear_game_settings": "Spieleinstellungen zurücksetzen", + "clear_game_settings_warning": "You are about to remove your old game settings files. They will be backed up to:\n{0}\n\nAre you sure?", + "clear_game_settings_succeeded": "Spieleinstellungen zurückgesetzt", + "clear_game_settings_failed": "Die Spieleinstellungen konnten aufgrund eines Fehlers nicht zurückgesetzt werden.", + "remove_registry_keys": "Registry-Schlüssel entfernen?", + "remove_registry_keys_succeeded": "Registry-Schlüssel entfernt", + "remove_registry_keys_failed": "Die Registry-Schlüssel konnten aufgrund eines Fehlers nicht entfernt werden.", + "clean_temp_files": "Temporäre Dateien löschen.", + "clean_temp_files_succeeded": "Temporäre Dateien gelöscht.", + "clean_temp_files_failed": "Die temporären Dateien konnten aufgrund eines Fehlers nicht gelöscht werden.", + "select_folder": "Ordner auswählen", + "registration_failed": "Registrierung fehlgeschlagen", + "minimize_action": "Minimieren", + "do_nothing_action": "Nichts tun", + "exit_action": "Launcher schließen", + "on_game_start": "Bei Spielstart", + "game": "Spiel", + "new_password": "Neues Passwort", + "cancel": "Abbrechen", + "need_an_account": "Noch kein eigenes Konto?", + "have_an_account": "Schon ein eigenes Konto?", + "reapply_patch": "Nochmal patchen", + "failed_to_receive_patches": "Fehler beim Erhalt von Patches", + "failed_core_patch": "Kernpatch fehlgeschlagen", + "failed_mod_patch": "Modpatch fehlgeschlagen", + "ok": "OK", + "account_page_denied": "Account page denied. Either you are not logged in or the game is running.", + "account_updated": "Your account has been updated", + "nickname": "Nickname", + "side": "Side", + "level": "Level", + "patching": "Patching", + "file_mismatch_dialog_message": "The input file hash doesn't match the expected hash. You may be using the wrong version\nof AKI for your client files.\n\nDo you want to continue?", + "yes": "Yes", + "no": "No", + "open_folder": "Open Folder", + "select_edition": "Select Edition", + "profile_created": "Profile Created", + "registration_question_format_1": "Profile '{0}' does not exist.\n\nWould you like to create it?", + "next_level_in": "Next level in", + "wipe_warning": "Changing your account edition requires a profile wipe. This will reset your game progress.", + "copied": "Copied", + "no_profile_data": "No profile data", + "profile_version_mismath": "Your profile was made using a different version of aki and may have issues", + "profile_removed": "Profile removed", + "profile_removal_failed": "Failed to remove profile", + "profile_remove_question_format_1": "Permanently remove profile '{0}'?", + "i_understand": "I Understand", + "game_version_mismatch_format_2": "SPT is unable to run, this is because SPT expected to find EFT version '{1}',\nbut instead found version '{0}'\n\nEnsure you've downgraded your EFT as described in the install guide\non the page you downloaded SPT from" +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/Japanese.json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Japanese.json new file mode 100644 index 0000000..ed9df79 --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Japanese.json @@ -0,0 +1,83 @@ +{ + "native_name": "日本語", + "retry": "リトライ", + "server_connecting": "接続中", + "server_unavailable_format_1": "デフォルト サーバー '{0}' は利用できません。", + "no_servers_available": "サーバーが見つかりません。 設定でサーバーリストを確認してください。", + "settings_menu": "設定", + "back": "戻る", + "wipe_profile": "プロファイルをワイプ", + "username": "ユーザー名", + "password": "パスワード", + "update": "アップデート", + "edit_account_update_error": "プロファイルの更新中に問題が発生しました。", + "register": "登録", + "go_to_register": "登録する", + "login_or_register": "ログイン / 新規登録", + "go_to_login": "ログインする", + "login_automatically": "自動ログイン", + "incorrect_login": "ユーザー名かパスワードが間違っています。", + "login_failed": "ログイン失敗", + "edition": "エディション", + "id": "ID", + "logout": "ログアウト", + "account": "プロファイル", + "edit_account": "プロファイル編集", + "start_game": "ゲーム開始", + "installed_in_live_game_warning": "AKI はライブ ゲーム ディレクトリにインストールしないでください。 コンピューターの別の場所にあるゲーム ディレクトリのコピーに AKI をインストールしてください。", + "no_official_game_warning": "Escape From Tarkov がコンピューターにインストールされていません。 ゲームのコピーを購入して、開発者をサポートしてください!", + "eft_exe_not_found_warning": "EscapeFromTarkov.exe がゲーム パスに見つかりません。 ディレクトリが正しいことを確認してください。", + "account_exist": "プロファイルは既に存在します", + "url": "URL", + "default_language": "デフォルト言語", + "game_path": "SPTゲームパス", + "clear_game_settings": "ゲーム設定をクリア", + "clear_game_settings_warning": "古いゲーム設定ファイルを削除しようとしています。 次の場所にバックアップされます:\n{0}\n\nよろしいですか?", + "clear_game_settings_succeeded": "ゲーム設定をクリアしました。", + "clear_game_settings_failed": "ゲーム設定のクリア中に問題が発生しました。", + "remove_registry_keys": "レジストリキーを削除する", + "remove_registry_keys_succeeded": "レジストリキーが削除されました。", + "remove_registry_keys_failed": "レジストリキーの削除中に問題が発生しました。", + "clean_temp_files": "一時ファイルの消去", + "clean_temp_files_succeeded": "一時ファイルが消去されました。", + "clean_temp_files_failed": "一時ファイルのクリーニング中に問題が発生しました", + "select_folder": "フォルダ選択", + "registration_failed": "登録に失敗しました。", + "minimize_action": "最小化", + "do_nothing_action": "何もしない", + "exit_action": "ランチャーを閉じる", + "on_game_start": "ゲーム開始時", + "game": "ゲーム", + "new_password": "新規パスワード", + "cancel": "キャンセル", + "need_an_account": "まだプロファイルを持っていませんか?", + "have_an_account": "すでにプロフィールをお持ちですか?", + "reapply_patch": "パッチの再適用", + "failed_to_receive_patches": "パッチの受信に失敗しました", + "failed_core_patch": "コアパッチに失敗しました", + "failed_mod_patch": "MODパッチ失敗", + "ok": "OK", + "account_page_denied": "プロファイルページが拒否されました。ログインしていないか、ゲームが実行中です。", + "account_updated": "プロファイルが更新されました", + "nickname": "ニックネーム", + "side": "派閥", + "level": "レベル", + "patching": "パッチ適用", + "file_mismatch_dialog_message": "入力ファイルのハッシュが予想されるハッシュと一致しません。 クライアントファイルに\n間違ったバージョンの AKI を使用している可能性があります。\n\n続行しますか?", + "yes": "はい", + "no": "いいえ", + "open_folder": "フォルダ展開", + "select_edition": "エディションを選択", + "profile_created": "プロファイルを作成しました", + "registration_question_format_1": "プロファイル '{0}' は存在しません。\n\n作成しますか?", + "next_level_in": "次のレベル", + "wipe_warning": "アカウントのエディションを変更するには、プロファイルのワイプが必要です。 これにより、ゲームの進行状況がリセットされます。", + "copied": "コピーしました", + "no_profile_data": "プロファイル データがありません", + "profile_version_mismath": "あなたのプロファイルは別のバージョンの AKI を使用して作成されており、問題がある可能性があります", + "profile_removed": "プロファイルを削除しました", + "profile_removal_failed": "プロファイルを削除できませんでした", + "profile_remove_question_format_1": "プロファイル '{0}' を完全に削除しますか?", + "i_understand": "了解", + "game_version_mismatch_format_2": "SPT を実行できません。これは、SPT が EFT バージョン '{1}' を検出することを期待していたためです\n代わりにバージョン '{0}' を検出しました\n\nインストール ガイドの説明に従って EFT をダウングレードしたことを確認してください\n SPT をダウンロードしたページ" +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/Korean.json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Korean.json new file mode 100644 index 0000000..2fa133e --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Korean.json @@ -0,0 +1,83 @@ +{ + "native_name": "한국어", + "retry": "재시도", + "server_connecting": "연결 중", + "server_unavailable_format_1": "기본 서버 '{0}'를 이용할 수 없습니다.", + "no_servers_available": "서버를 찾을 수 없습니다. 설정에서 서버 목록을 확인하세요.", + "settings_menu": "설정", + "back": "뒤로가기", + "wipe_profile": "프로필 초기화", + "username": "유저명", + "password": "비밀번호", + "update": "업데이트", + "edit_account_update_error": "프로필을 업데이트하는 도중에 문제가 발생하였습니다.", + "register": "등록", + "go_to_register": "등록하기", + "login_or_register": "로그인 / 등록", + "go_to_login": "로그인하기", + "login_automatically": "자동 로그인", + "incorrect_login": "유저명 혹은 비밀번호가 틀립니다.", + "login_failed": "로그인 실패", + "edition": "에디션", + "id": "아이디", + "logout": "로그아웃", + "account": "프로필", + "edit_account": "프로필 수정", + "start_game": "게임 시작", + "installed_in_live_game_warning": "Aki는 원본 게임 폴더에 직접 설치할 수 없습니다. 게임 폴더를 다른 위치에 복사하신 후 설치하세요.", + "no_official_game_warning": "Escape From Tarkov가 설치되어 있지 않습니다. 게임을 구매하여 개발자들을 지원해주세요!", + "eft_exe_not_found_warning": "EscapeFromTarkov.exe 파일이 경로에 없습니다. 경로를 확인하여 주세요.", + "account_exist": "프로필이 이미 존재합니다", + "url": "URL", + "default_language": "기본 언어", + "game_path": "SPT 게임 경로", + "clear_game_settings": "게임 설정 초기화", + "clear_game_settings_warning": "기존 게임의 설정 파일을 초기화 합니다. 기존 파일들은\n{0}\n\n위치에 백업됩니다. 초기화하시겠습니까?", + "clear_game_settings_succeeded": "게임 설정이 초기화되었습니다.", + "clear_game_settings_failed": "게임 설정을 초기화하는 도중에 문제가 발생하였습니다.", + "remove_registry_keys": "레지스트리 키 삭제", + "remove_registry_keys_succeeded": "레지스트리 키가 삭제되었습니다.", + "remove_registry_keys_failed": "레지스트리 키 삭제 도중에 문제가 발생하였습니다.", + "clean_temp_files": "임시 파일 삭제", + "clean_temp_files_succeeded": "임시 파일들이 삭제되었습니다.", + "clean_temp_files_failed": "임시 파일들을 삭제하는 도중에 문제가 발생하였습니다.", + "select_folder": "폴더 선택", + "registration_failed": "등록 실패.", + "minimize_action": "최소화", + "do_nothing_action": "아무것도 하지 않음", + "exit_action": "런쳐 종료", + "on_game_start": "게임 시작 시", + "game": "게임", + "new_password": "새 비밀번호", + "cancel": "취소", + "need_an_account": "프로필이 아직 없습니까?", + "have_an_account": "프로필이 있습니까?", + "reapply_patch": "패치 재적용", + "failed_to_receive_patches": "패치를 받아오는데 실패하였습니다", + "failed_core_patch": "코어 패치에 실패하였습니다", + "failed_mod_patch": "모드 패치에 실패하였습니다", + "ok": "확인", + "account_page_denied": "프로필 페이지에 접근할 수 없습니다. 로그인하지 않았거나 이미 게임이 진행 중입니다.", + "account_updated": "프로필이 업데이트 되었습니다", + "nickname": "닉네임", + "side": "진영", + "level": "레벨", + "patching": "패치 중", + "file_mismatch_dialog_message": "입력된 파일의 해시 값이 지정된 해시 값과 틀립니다.\nAKI와 현재 게임의 버전이 맞지 않을 수 있습니다.\n\n계속 진행하시겠습니까?", + "yes": "예", + "no": "아니오", + "open_folder": "폴더 열기", + "select_edition": "에디션 선택", + "profile_created": "프로필이 생성되었습니다", + "registration_question_format_1": "프로필 '{0}'가 존재하지 않습니다.\n\n새로 생성하시겠습니까?", + "next_level_in": "다음 레벨까지 필요한 경험치", + "wipe_warning": "프로필의 에디션을 변경할 경우 프로필 초기화가 필요합니다. 현재까지 저장된 프로필의 게임 진행이 초기화 됩니다.", + "copied": "복사됨", + "no_profile_data": "프로필 데이터가 없음", + "profile_version_mismath": "다른 버전의 AKI에서 생성된 프로필이므로 문제가 발생할 수 있습니다", + "profile_removed": "프로필 삭제됨", + "profile_removal_failed": "프로필 삭제에 실패하였습니다", + "profile_remove_question_format_1": "프로필 '{0}'을 영구적으로 삭제하시겠습니까?", + "i_understand": "이해하였습니다", + "game_version_mismatch_format_2": "당신의 게임 버전은 '{0}'이며 호환되는 버전은 '{1}' 입니다.\n\n게임 실행에 문제가 발생하거나 되지 않을 수 있습니다." +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/Russian.json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Russian.json new file mode 100644 index 0000000..1e85fa4 --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Russian.json @@ -0,0 +1,83 @@ +{ + "native_name": "Русский", + "retry": "Повторить", + "server_connecting": "Соединение", + "server_unavailable_format_1": "Сервер по-умолчанию '{0}' не доступен.", + "no_servers_available": "Сервер недоступен. Проверьте список серверов в настройках.", + "settings_menu": "Настройки", + "back": "Назад", + "wipe_profile": "Очистить профиль", + "username": "Имя пользователя", + "password": "Пароль", + "update": "Обновление", + "edit_account_update_error": "Произошла ошибка при обновлении профиля.", + "register": "Регистрация", + "go_to_register": "Перейти к регистриции", + "login_or_register": "Вход / Регистрация", + "go_to_login": "Перейти ко входу", + "login_automatically": "Войти автоматически", + "incorrect_login": "Неверное имя пользователя или пароль", + "login_failed": "Неудачный вход", + "edition": "Издание", + "id": "ИД", + "logout": "Выход", + "account": "Аккаунт", + "edit_account": "Редактировать профиль", + "start_game": "Запустить игру", + "installed_in_live_game_warning": "Aki не должен быть установлен в установленную игру. Пожалуйста, сделайте копию игры и скопируйте файлы игры и установите Aki туда.", + "no_official_game_warning": "Escape From Tarkov не установлен на компьютере. Пожалуйста, поддержите разработчиков и купите копию игры!", + "eft_exe_not_found_warning": "EscapeFromTarkov.exe не найден.", + "account_exist": "Профиль уже существует", + "url": "URL", + "default_language": "Язык по-уолчанию", + "game_path": "Путь к игре", + "clear_game_settings": "Очистить настройки игры", + "clear_game_settings_warning": "Вы собираетесь удалить старые файлы настроек игры. Их копия будет расположена здесь:\n{0}\n\n Продолжить?", + "clear_game_settings_succeeded": "Настройки игры очищены", + "clear_game_settings_failed": "Неизвестная проблема при очистке настроек игры", + "remove_registry_keys": "Очистить реестр", + "remove_registry_keys_succeeded": "Реестр очищен", + "remove_registry_keys_failed": "Неизвестная проблема при очистке реестра", + "clean_temp_files": "Очистка временных фалов", + "clean_temp_files_succeeded": "Временные файлы очищены", + "clean_temp_files_failed": "Неизвестная проблема при очистке временных фалов", + "select_folder": "Выбрать директорию", + "registration_failed": "Неудачная попытка регистрации", + "minimize_action": "Минимизировать", + "do_nothing_action": "Ничего не делать", + "exit_action": "Закрыть лаунчер", + "on_game_start": "Запуск игры через лаунчер", + "game": "Игра", + "new_password": "Новый пароль", + "cancel": "Отмена", + "need_an_account": "Еще нет профиля?", + "have_an_account": "Уже есть профиль?", + "reapply_patch": "Применить патч снова", + "failed_to_receive_patches": "Не удалось получить патчи", + "failed_core_patch": "Не удалось пропатчить ядро", + "failed_mod_patch": "Не удалось пропатчит мод", + "ok": "Ок", + "account_page_denied": "Страница профиля недоступна. Возможно вы не авторизованы, либо игра запущена.", + "account_updated": "Ваш аккаут обновлен", + "nickname": "Никнейм", + "side": "Сторона", + "level": "Уровень", + "patching": "Патчинг", + "file_mismatch_dialog_message": "Хеш-сумма исходного файла не соотвествует требуемому.\nВозможно используется несовместимая версия клиента.\n\nВы хотите продолжить?", + "yes": "Да", + "no": "Нет", + "open_folder": "Открыть папку", + "select_edition": "Выбор издания", + "profile_created": "Профиль создан", + "registration_question_format_1": "Профиль '{0}' не сущестует.\n\nВы желаете его создать?", + "next_level_in": "Следующий уровень через", + "wipe_warning": "Смена игрового издания требует вайп профиля. Весь текущий прогресс будет сброшен.", + "copied": "Скопировано", + "no_profile_data": "Данные профиля отсутствуют", + "profile_version_mismath": "Ваш профиль был создан с использованием другой версии aki и может содержать ошибки", + "profile_removed": "Профиль удален", + "profile_removal_failed": "Ошибка удаления профиля", + "profile_remove_question_format_1": "Удалить профиль '{0}' безвозвратно?", + "i_understand": "Я понимаю", + "game_version_mismatch_format_2": "Ваша версия игры: '{0}' и совместимая версия: '{1}'.\n\nИгра может работать некорректно или не работать вообще." +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Locales/Spanish.json b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Spanish.json new file mode 100644 index 0000000..7812ff7 --- /dev/null +++ b/project/Aki.Launcher/Aki_Data/Launcher/Locales/Spanish.json @@ -0,0 +1,83 @@ +{ + "native_name": "Español", + "retry": "Reintentar", + "server_connecting": "Conectando", + "server_unavailable_format_1": "El servidor '{0}' no está disponible.", + "no_servers_available": "No se han encontrado servidores. Revisa la lista de servidores en ajustes.", + "settings_menu": "Ajustes", + "back": "Volver", + "wipe_profile": "Wipe", + "username": "Nombre de Usuario", + "password": "Contraseña", + "update": "Cambiar", + "edit_account_update_error": "Ha ocurrido un problema actualizando el perfil.", + "register": "Registrar", + "go_to_register": "Ir al registro", + "login_or_register": "Login / Registro", + "go_to_login": "Ir a iniciar sesión", + "login_automatically": "Entrar automáticamente", + "incorrect_login": "Usuario y/o contraseña erróneos", + "login_failed": "Error la iniciar sesión", + "edition": "Edición", + "id": "ID", + "logout": "Cerrar Sesión", + "account": "Perfil", + "edit_account": "Editar Perfil", + "start_game": "Entrar", + "installed_in_live_game_warning": "Aki no debería ser instalado dentro del directorio del juego Live. Favor instala Aki en una copia del juego Live en algún otro sitio de tu equipo.", + "no_official_game_warning": "Escape From Tarkov no está instalado en tu equipo. ¡Debes omprar una copia del juego y ayuda a los desarrolladores!isn't installed on your computer. Please buy a copy of the game and support the developers!", + "eft_exe_not_found_warning": "EscapeFromTarkov.exe No encontrado. Verifica que tal archivo esté en el directorio del juego.", + "account_exist": "El Perfil ya existe", + "url": "IP del servidor", + "default_language": "Idioma", + "game_path": "Ruta para SPT", + "clear_game_settings": "Limpiar ajustes del juego", + "clear_game_settings_warning": "Estás a punto de borrar tus antiguos ajustes del juego. Ellos estarán respaldados en:\n{0}\n\n¿Estás seguro?", + "clear_game_settings_succeeded": "Ajustes limpiados.", + "clear_game_settings_failed": "Ha habido un problema mientras se limpiaban los ajustes del juego.", + "remove_registry_keys": "Eliminar claves de registro", + "remove_registry_keys_succeeded": "Claves de registro borradas.", + "remove_registry_keys_failed": "Ha habido un problema mientras se borraban las claves de registro.", + "clean_temp_files": "Limpiar archivos temporales", + "clean_temp_files_succeeded": "Archivos temporales borrados.", + "clean_temp_files_failed": "Ha habido un problema mientras se limpiaban los archivos temporales.", + "select_folder": "Elegir carpeta", + "registration_failed": "Error al registrarse.", + "minimize_action": "Minimizar", + "do_nothing_action": "No hacer nada", + "exit_action": "Cerrar el launcher", + "on_game_start": "Al inicio del juego", + "game": "Juego", + "new_password": "Nueva contraseña", + "cancel": "Cancelar", + "need_an_account": "¿Aún no tienes un perfil?", + "have_an_account": "¿Ya tienes un perfil?", + "reapply_patch": "Reaplicar parche", + "failed_to_receive_patches": "Error al recibir los parches", + "failed_core_patch": "Error al parchear el núcleo", + "failed_mod_patch": "Error al parchear el mod", + "ok": "OK", + "account_page_denied": "Acceso denegado al perfil. Quizás no estás logueado o el juego está en funcionamiento.", + "account_updated": "Tu perfil se ha actualizado", + "nickname": "Apodo", + "side": "Facción", + "level": "Nivel", + "patching": "Parcheando", + "file_mismatch_dialog_message": "El hash del archivo ingresado no concuerda con el hash esperado. Puede que estés utilizando la versión equivocada\nde AKI en tus archvos del cliente.\n\n¿Quieres continuar?", + "yes": "Sí", + "no": "No", + "open_folder": "Abrir carpeta", + "select_edition": "Elegir edición", + "profile_created": "Perfil creado", + "registration_question_format_1": "El perfil '{0}' no existe.\n\n¿Quieres crear uno?", + "next_level_in": "Siguiente nivel en", + "wipe_warning": "Para cambiar la edición de tu juego, es necesario reiniciar a tu perfil.\nEsto hará que se reinicie el progreso actual.", + "copied": "Copiado", + "no_profile_data": "Perfil sin datos", + "profile_version_mismath": "Debido a que tu perfil fue creado con una versión diferente de AKI, puede que te encuentres algunos problemas.", + "profile_removed": "Perfil borrado", + "profile_removal_failed": "Error al borrar el perfil", + "profile_remove_question_format_1": "Confirma si quieres borrar el perfil: '{0}'", + "i_understand": "Lo entiendo", + "game_version_mismatch_format_2": "SPT no puede iniciar, esto es debido a que la versión de EFT esperada es '{1}',\npero la versión encontrada es '{0}'\n\nAsegurate de haber realizado el downgrade de tu versión de EFT, tal como se indica en la guía de instalación\nen la página en la que has descargado SPT" +} \ No newline at end of file diff --git a/project/Aki.Launcher/Aki_Data/Launcher/Patches/aki-core/EscapeFromTarkov_Data/Managed/Assembly-CSharp.dll.bpf b/project/Aki.Launcher/Aki_Data/Launcher/Patches/aki-core/EscapeFromTarkov_Data/Managed/Assembly-CSharp.dll.bpf new file mode 100644 index 0000000..350244d Binary files /dev/null and b/project/Aki.Launcher/Aki_Data/Launcher/Patches/aki-core/EscapeFromTarkov_Data/Managed/Assembly-CSharp.dll.bpf differ diff --git a/project/Aki.Launcher/App.axaml b/project/Aki.Launcher/App.axaml new file mode 100644 index 0000000..e32e261 --- /dev/null +++ b/project/Aki.Launcher/App.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + #121212 + #FFC107 + #FFFFFF + #282828 + #323947 + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/Aki.Launcher/App.axaml.cs b/project/Aki.Launcher/App.axaml.cs new file mode 100644 index 0000000..47bbc0b --- /dev/null +++ b/project/Aki.Launcher/App.axaml.cs @@ -0,0 +1,38 @@ +using Aki.Launcher.Controllers; +using Aki.Launcher.ViewModels; +using Aki.Launcher.Views; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using ReactiveUI; +using System; +using System.Reactive; + +namespace Aki.Launcher +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + RxApp.DefaultExceptionHandler = Observer.Create((exception) => + { + LogManager.Instance.Exception(exception); + }); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/project/Aki.Launcher/Assets/Styles.axaml b/project/Aki.Launcher/Assets/Styles.axaml new file mode 100644 index 0000000..150ea41 --- /dev/null +++ b/project/Aki.Launcher/Assets/Styles.axaml @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + diff --git a/project/Aki.Launcher/CustomControls/TitleBar.axaml.cs b/project/Aki.Launcher/CustomControls/TitleBar.axaml.cs new file mode 100644 index 0000000..26565c3 --- /dev/null +++ b/project/Aki.Launcher/CustomControls/TitleBar.axaml.cs @@ -0,0 +1,87 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using System.Windows.Input; + +namespace Aki.Launcher.CustomControls +{ + public partial class TitleBar : UserControl + { + public TitleBar() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public static readonly StyledProperty TitleProperty = + AvaloniaProperty.Register(nameof(Title)); + + public string Title + { + get => GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public static readonly StyledProperty ButtonForegroundProperty = + AvaloniaProperty.Register(nameof(ButtonForeground)); + + public IBrush ButtonForeground + { + get => GetValue(ButtonForegroundProperty); + set => SetValue(ButtonForegroundProperty, value); + } + + public static new readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); + + public new IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static new readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background)); + + public new IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + //Close Button Command (X Button) Property + public static readonly StyledProperty XButtonCommandProperty = + AvaloniaProperty.Register(nameof(XButtonCommand)); + + public ICommand XButtonCommand + { + get => GetValue(XButtonCommandProperty); + set => SetValue(XButtonCommandProperty, value); + } + + //Minimize Button Command (- Button) Property + public static readonly StyledProperty MinButtonCommandProperty = + AvaloniaProperty.Register(nameof(MinButtonCommand)); + + public ICommand MinButtonCommand + { + get => GetValue(MinButtonCommandProperty); + set => SetValue(MinButtonCommandProperty, value); + } + + //Setting Button Command Property + public static readonly StyledProperty SettingsButtonCommandProperty = + AvaloniaProperty.Register(nameof(SettingsButtonCommand)); + + public ICommand SettingsButtonCommand + { + get => GetValue(SettingsButtonCommandProperty); + set => SetValue(SettingsButtonCommandProperty, value); + } + } +} diff --git a/project/Aki.Launcher/Models/GameStarterFrontend.cs b/project/Aki.Launcher/Models/GameStarterFrontend.cs new file mode 100644 index 0000000..9e57273 --- /dev/null +++ b/project/Aki.Launcher/Models/GameStarterFrontend.cs @@ -0,0 +1,46 @@ +using Aki.Launcher.Helpers; +using Aki.Launcher.Interfaces; +using Aki.Launcher.Models.Launcher; +using Aki.Launcher.ViewModels.Dialogs; +using Aki.Launcher.ViewModels.Notifications; +using Avalonia.Controls.Notifications; +using Splat; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Aki.Launcher.Models +{ + public class GameStarterFrontend : IGameStarterFrontend + { + private WindowNotificationManager notificationManager => Locator.Current.GetService(); + + public async Task CompletePatchTask(IAsyncEnumerable task) + { + notificationManager.Show(new AkiNotificationViewModel(null, "", $"{LocalizationProvider.Instance.patching} ...")); + + var iter = task.GetAsyncEnumerator(); + while (await iter.MoveNextAsync()) + { + var info = iter.Current; + if (!info.OK) + { + if(info.Status == ByteBanger.PatchResultType.InputChecksumMismatch) + { + var result = await DialogHost.DialogHost.Show(new ConfirmationDialogViewModel(null, LocalizationProvider.Instance.file_mismatch_dialog_message)); + + if(result != null && result is bool confirmation && !confirmation) + { + notificationManager.Show(new AkiNotificationViewModel(null, "", LocalizationProvider.Instance.failed_core_patch, NotificationType.Error)); + throw new TaskCanceledException(); + } + } + else + { + notificationManager.Show(new AkiNotificationViewModel(null, "", LocalizationProvider.Instance.failed_core_patch, NotificationType.Error)); + throw new TaskCanceledException(); + } + } + } + } + } +} diff --git a/project/Aki.Launcher/Models/ImageHelper.cs b/project/Aki.Launcher/Models/ImageHelper.cs new file mode 100644 index 0000000..c31e350 --- /dev/null +++ b/project/Aki.Launcher/Models/ImageHelper.cs @@ -0,0 +1,27 @@ +using ReactiveUI; + +namespace Aki.Launcher.Models +{ + public class ImageHelper : ReactiveObject + { + private string _Path; + public string Path + { + get => _Path; + set => this.RaiseAndSetIfChanged(ref _Path, value); + } + + /// + /// Force property changed by touching the image path. + /// + /// Can be used to force image re-loading + public void Touch() + { + string tmp = Path; + + Path = ""; + + Path = tmp; + } + } +} diff --git a/project/Aki.Launcher/Models/NavigationPreConditionResult.cs b/project/Aki.Launcher/Models/NavigationPreConditionResult.cs new file mode 100644 index 0000000..6f15441 --- /dev/null +++ b/project/Aki.Launcher/Models/NavigationPreConditionResult.cs @@ -0,0 +1,22 @@ +using Aki.Launcher.ViewModels; + +namespace Aki.Launcher.Models +{ + public class NavigationPreConditionResult + { + public bool Succeeded => ErrorMessage == null; + public string? ErrorMessage { get; private set; } = null; + + public ViewModelBase? ViewModel { get; private set; } = null; + + protected NavigationPreConditionResult(string? ErrorMessage = null, ViewModelBase? OnFailedViewModel = null) + { + this.ErrorMessage = ErrorMessage; + ViewModel = OnFailedViewModel; + } + + public static NavigationPreConditionResult FromSuccess() => new NavigationPreConditionResult(); + + public static NavigationPreConditionResult FromError(string ErrorMessage, ViewModelBase ViewModel) => new NavigationPreConditionResult(ErrorMessage, ViewModel); + } +} diff --git a/project/Aki.Launcher/Program.cs b/project/Aki.Launcher/Program.cs new file mode 100644 index 0000000..bd5c958 --- /dev/null +++ b/project/Aki.Launcher/Program.cs @@ -0,0 +1,42 @@ +using Aki.Launcher.Controllers; +using Avalonia; +using Avalonia.ReactiveUI; +using ReactiveUI; +using Splat; +using System; +using System.Reflection; + +namespace Aki.Launcher +{ + internal class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + { + try + { + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + catch (Exception ex) + { + LogManager.Instance.Exception(ex); + } + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + { + Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly()); + + return AppBuilder.Configure() + .UseReactiveUI() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI(); + } + } +} diff --git a/project/Aki.Launcher/References/Newtonsoft.Json.dll b/project/Aki.Launcher/References/Newtonsoft.Json.dll new file mode 100644 index 0000000..bcfcd85 Binary files /dev/null and b/project/Aki.Launcher/References/Newtonsoft.Json.dll differ diff --git a/project/Aki.Launcher/References/zlib.net.dll b/project/Aki.Launcher/References/zlib.net.dll new file mode 100644 index 0000000..5d425d9 Binary files /dev/null and b/project/Aki.Launcher/References/zlib.net.dll differ diff --git a/project/Aki.Launcher/ViewLocator.cs b/project/Aki.Launcher/ViewLocator.cs new file mode 100644 index 0000000..9863f99 --- /dev/null +++ b/project/Aki.Launcher/ViewLocator.cs @@ -0,0 +1,30 @@ +using Aki.Launcher.ViewModels; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using System; + +namespace Aki.Launcher +{ + public class ViewLocator : IDataTemplate + { + public IControl Build(object data) + { + var name = data.GetType().FullName!.Replace("ViewModel", "View"); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + else + { + return new TextBlock { Text = "Not Found: " + name }; + } + } + + public bool Match(object data) + { + return data is ViewModelBase; + } + } +} diff --git a/project/Aki.Launcher/ViewModels/ConnectServerViewModel.cs b/project/Aki.Launcher/ViewModels/ConnectServerViewModel.cs new file mode 100644 index 0000000..0245deb --- /dev/null +++ b/project/Aki.Launcher/ViewModels/ConnectServerViewModel.cs @@ -0,0 +1,65 @@ +using Aki.Launch.Models.Aki; +using Aki.Launcher.Helpers; +using Aki.Launcher.Models.Launcher; +using ReactiveUI; +using Splat; +using System.Reactive.Disposables; +using System.Threading.Tasks; + +namespace Aki.Launcher.ViewModels +{ + public class ConnectServerViewModel : ViewModelBase + { + private bool noAutoLogin = false; + + public ConnectServerModel connectModel { get; set; } = new ConnectServerModel() + { + InfoText = LocalizationProvider.Instance.server_connecting + }; + + public ConnectServerViewModel(IScreen Host, bool NoAutoLogin = false) : base(Host) + { + noAutoLogin = NoAutoLogin; + + this.WhenActivated((CompositeDisposable disposables) => + { + Task.Run(async () => + { + await ConnectServer(); + }); + }); + } + + public async Task ConnectServer() + { + await ServerManager.LoadDefaultServerAsync(LauncherSettingsProvider.Instance.Server.Url); + + bool connected = ServerManager.PingServer(); + + connectModel.ConnectionFailed = !connected; + + connectModel.InfoText = connected ? LocalizationProvider.Instance.ok : string.Format(LocalizationProvider.Instance.server_unavailable_format_1, LauncherSettingsProvider.Instance.Server.Name); + + if (connected) + { + AkiVersion version = Locator.Current.GetService("akiversion"); + + version.ParseVersionInfo(ServerManager.GetVersion()); + + NavigateTo(new LoginViewModel(HostScreen, noAutoLogin)); + } + } + + public void RetryCommand() + { + connectModel.InfoText = LocalizationProvider.Instance.server_connecting; + + connectModel.ConnectionFailed = false; + + Task.Run(async () => + { + await ConnectServer(); + }); + } + } +} diff --git a/project/Aki.Launcher/ViewModels/Dialogs/ChangeEditionDialogViewModel.cs b/project/Aki.Launcher/ViewModels/Dialogs/ChangeEditionDialogViewModel.cs new file mode 100644 index 0000000..5bf9690 --- /dev/null +++ b/project/Aki.Launcher/ViewModels/Dialogs/ChangeEditionDialogViewModel.cs @@ -0,0 +1,14 @@ +using Aki.Launcher.Models.Launcher; +using ReactiveUI; + +namespace Aki.Launcher.ViewModels.Dialogs +{ + public class ChangeEditionDialogViewModel : ViewModelBase + { + public EditionCollection editions { get; set; } = new EditionCollection(); + + public ChangeEditionDialogViewModel(IScreen Host) : base(Host) + { + } + } +} diff --git a/project/Aki.Launcher/ViewModels/Dialogs/ConfirmationDialogViewModel.cs b/project/Aki.Launcher/ViewModels/Dialogs/ConfirmationDialogViewModel.cs new file mode 100644 index 0000000..8aa8294 --- /dev/null +++ b/project/Aki.Launcher/ViewModels/Dialogs/ConfirmationDialogViewModel.cs @@ -0,0 +1,26 @@ +using Aki.Launcher.Helpers; +using ReactiveUI; + +namespace Aki.Launcher.ViewModels.Dialogs +{ + public class ConfirmationDialogViewModel : ViewModelBase + { + public string Question { get; set; } + public string ConfirmButtonText { get; set; } + public string DenyButtonText { get; set; } + + /// + /// A confirmation dialog + /// + /// Set to null when is used, since the dialog host is handling routing + /// + /// + /// + public ConfirmationDialogViewModel(IScreen Host, string Question, string? ConfirmButtonText = null, string? DenyButtonText = null) : base(Host) + { + this.Question = Question; + this.ConfirmButtonText = ConfirmButtonText ?? LocalizationProvider.Instance.yes; + this.DenyButtonText = DenyButtonText ?? LocalizationProvider.Instance.no; + } + } +} diff --git a/project/Aki.Launcher/ViewModels/Dialogs/RegisterDialogViewModel.cs b/project/Aki.Launcher/ViewModels/Dialogs/RegisterDialogViewModel.cs new file mode 100644 index 0000000..64f84ee --- /dev/null +++ b/project/Aki.Launcher/ViewModels/Dialogs/RegisterDialogViewModel.cs @@ -0,0 +1,31 @@ +using Aki.Launcher.Helpers; +using Aki.Launcher.Models.Launcher; +using ReactiveUI; + +namespace Aki.Launcher.ViewModels.Dialogs +{ + public class RegisterDialogViewModel : ViewModelBase + { + public string Question { get; set; } + public string RegisterButtonText { get; set; } + public string CancelButtonText { get; set; } + public string ComboBoxPlaceholderText { get; set; } + public EditionCollection Editions { get; set; } = new EditionCollection(); + + /// + /// A registration dialog + /// + /// Set to null when is used, since the dialog host is handling routing + /// + public RegisterDialogViewModel(IScreen Host, string Username) : base(Host) + { + Question = string.Format(LocalizationProvider.Instance.registration_question_format_1, Username); + + RegisterButtonText = LocalizationProvider.Instance.register; + + CancelButtonText = LocalizationProvider.Instance.cancel; + + ComboBoxPlaceholderText = LocalizationProvider.Instance.select_edition; + } + } +} diff --git a/project/Aki.Launcher/ViewModels/Dialogs/WarningDialogViewModel.cs b/project/Aki.Launcher/ViewModels/Dialogs/WarningDialogViewModel.cs new file mode 100644 index 0000000..d533bb4 --- /dev/null +++ b/project/Aki.Launcher/ViewModels/Dialogs/WarningDialogViewModel.cs @@ -0,0 +1,23 @@ +using Aki.Launcher.Helpers; +using ReactiveUI; + +namespace Aki.Launcher.ViewModels.Dialogs +{ + public class WarningDialogViewModel : ViewModelBase + { + public string ButtonText { get; set; } + public string WarningMessage { get; set; } + + /// + /// A warning dialog + /// + /// Set to null when is used, since the dialog host is handling routing + /// + /// + public WarningDialogViewModel(IScreen Host, string WarningMessage, string? ButtonText = null) : base(Host) + { + this.WarningMessage = WarningMessage; + this.ButtonText = ButtonText ?? LocalizationProvider.Instance.ok; + } + } +} diff --git a/project/Aki.Launcher/ViewModels/LoginViewModel.cs b/project/Aki.Launcher/ViewModels/LoginViewModel.cs new file mode 100644 index 0000000..b3b1a5c --- /dev/null +++ b/project/Aki.Launcher/ViewModels/LoginViewModel.cs @@ -0,0 +1,165 @@ +using Aki.Launcher.Attributes; +using Aki.Launcher.Helpers; +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models; +using Aki.Launcher.Models.Aki; +using Aki.Launcher.Models.Launcher; +using Aki.Launcher.ViewModels.Dialogs; +using Avalonia.Controls.Notifications; +using ReactiveUI; +using Splat; +using System.Collections.ObjectModel; +using System.Reactive; +using System.Threading.Tasks; + +namespace Aki.Launcher.ViewModels +{ + [RequireServerConnected] + public class LoginViewModel : ViewModelBase + { + public ObservableCollection ExistingProfiles { get; set; } = new ObservableCollection(); + + public LoginModel Login { get; set; } = new LoginModel(); + + public ReactiveCommand LoginCommand { get; set; } + + public LoginViewModel(IScreen Host, bool NoAutoLogin = false) : base(Host) + { + //setup reactive commands + LoginCommand = ReactiveCommand.CreateFromTask(async () => + { + AccountStatus status = await AccountManager.LoginAsync(Login); + + switch (status) + { + case AccountStatus.OK: + { + if (LauncherSettingsProvider.Instance.UseAutoLogin && LauncherSettingsProvider.Instance.Server.AutoLoginCreds != Login) + { + LauncherSettingsProvider.Instance.Server.AutoLoginCreds = Login; + } + + LauncherSettingsProvider.Instance.SaveSettings(); + NavigateTo(new ProfileViewModel(HostScreen)); + break; + } + case AccountStatus.LoginFailed: + { + // Create account if it doesn't exist + if (!string.IsNullOrWhiteSpace(Login.Username)) + { + var result = await ShowDialog(new RegisterDialogViewModel(null, Login.Username)); + + if (result != null && result is string edition) + { + AccountStatus registerResult = await AccountManager.RegisterAsync(Login.Username, Login.Password, edition); + + switch (registerResult) + { + case AccountStatus.OK: + { + if (LauncherSettingsProvider.Instance.UseAutoLogin && LauncherSettingsProvider.Instance.Server.AutoLoginCreds != Login) + { + LauncherSettingsProvider.Instance.Server.AutoLoginCreds = Login; + } + + LauncherSettingsProvider.Instance.SaveSettings(); + SendNotification(LocalizationProvider.Instance.profile_created, Login.Username, NotificationType.Success); + NavigateTo(new ProfileViewModel(HostScreen)); + break; + } + case AccountStatus.RegisterFailed: + { + SendNotification("", LocalizationProvider.Instance.registration_failed, NotificationType.Error); + break; + } + case AccountStatus.NoConnection: + { + NavigateTo(new ConnectServerViewModel(HostScreen)); + break; + } + default: + { + SendNotification("", registerResult.ToString(), NotificationType.Error); + break; + } + } + + return; + } + } + + SendNotification("", LocalizationProvider.Instance.login_failed, NotificationType.Error); + + break; + } + case AccountStatus.NoConnection: + { + NavigateTo(new ConnectServerViewModel(HostScreen)); + break; + } + } + }); + + //cache and touch background image + var backgroundImage = Locator.Current.GetService("bgimage"); + + ImageRequest.CacheBackgroundImage(); + + backgroundImage.Touch(); + + //handle auto-login + if (LauncherSettingsProvider.Instance.UseAutoLogin && LauncherSettingsProvider.Instance.Server.AutoLoginCreds != null && !NoAutoLogin) + { + Task.Run(() => + { + Login = LauncherSettingsProvider.Instance.Server.AutoLoginCreds; + LoginCommand.Execute(); + }); + + return; + } + + Task.Run(() => + { + GetExistingProfiles(); + }); + } + + public void LoginProfileCommand(object parameter) + { + if (parameter == null) return; + + Task.Run(() => + { + if (parameter is string username) + { + Login.Username = username; + LoginCommand.Execute(); + } + }); + } + + public void GetExistingProfiles() + { + ServerProfileInfo[] existingProfiles = AccountManager.GetExistingProfiles(); + + if(existingProfiles != null) + { + ExistingProfiles.Clear(); + + foreach(ServerProfileInfo profile in existingProfiles) + { + ProfileInfo profileInfo = new ProfileInfo(profile); + + ExistingProfiles.Add(profileInfo); + + ImageRequest.CacheSideImage(profileInfo.Side); + + ImageHelper sideImage = new ImageHelper() { Path = profileInfo.SideImage }; + sideImage.Touch(); + } + } + } + } +} diff --git a/project/Aki.Launcher/ViewModels/MainWindowViewModel.cs b/project/Aki.Launcher/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..52b2e55 --- /dev/null +++ b/project/Aki.Launcher/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,61 @@ +using Avalonia; +using ReactiveUI; +using System.Reactive.Disposables; +using Aki.Launcher.Models; +using Aki.Launcher.MiniCommon; +using System.IO; +using Splat; +using Aki.Launch.Models.Aki; +using Aki.Launcher.Helpers; + +namespace Aki.Launcher.ViewModels +{ + public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScreen + { + public AkiVersion VersionInfo { get; set; } = new AkiVersion(); + public RoutingState Router { get; } = new RoutingState(); + public ViewModelActivator Activator { get; } = new ViewModelActivator(); + + public ImageHelper Background { get; } = new ImageHelper() + { + Path = Path.Join(ImageRequest.ImageCacheFolder, "bg.png") + }; + + public MainWindowViewModel() + { + Locator.CurrentMutable.RegisterConstant(Background, "bgimage"); + + Locator.CurrentMutable.RegisterConstant(VersionInfo, "akiversion"); + + LauncherSettingsProvider.Instance.AllowSettings = true; + + this.WhenActivated((CompositeDisposable disposables) => + { + Router.Navigate.Execute(new ConnectServerViewModel(this)); + }); + } + + public void CloseCommand() + { + if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp) + { + desktopApp.MainWindow.Close(); + } + } + + public void MinimizeCommand() + { + if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp) + { + desktopApp.MainWindow.WindowState = Avalonia.Controls.WindowState.Minimized; + } + } + + public void GoToSettingsCommand() + { + LauncherSettingsProvider.Instance.AllowSettings = false; + + Router.Navigate.Execute(new SettingsViewModel(this)); + } + } +} diff --git a/project/Aki.Launcher/ViewModels/Notifications/AkiNotificationViewModel.cs b/project/Aki.Launcher/ViewModels/Notifications/AkiNotificationViewModel.cs new file mode 100644 index 0000000..a50d53d --- /dev/null +++ b/project/Aki.Launcher/ViewModels/Notifications/AkiNotificationViewModel.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls.Notifications; +using Avalonia.Media; +using ReactiveUI; + +namespace Aki.Launcher.ViewModels.Notifications +{ + public class AkiNotificationViewModel : ViewModelBase + { + public string Title { get; set; } + public string Message { get; set; } + public IBrush BarColor { get; set; } + + public AkiNotificationViewModel(IScreen Host, string Title, string Message, NotificationType Type = NotificationType.Information) : base(Host) + { + this.Title = Title; + this.Message = Message; + + switch(Type) + { + case NotificationType.Information: + { + BarColor = new SolidColorBrush(Colors.DodgerBlue); + break; + } + case NotificationType.Warning: + { + BarColor = new SolidColorBrush(Colors.Gold); + break; + } + case NotificationType.Success: + { + BarColor = new SolidColorBrush(Colors.ForestGreen); + break; + } + case NotificationType.Error: + { + BarColor = new SolidColorBrush(Colors.IndianRed); + break; + } + default: + { + BarColor = new SolidColorBrush(Colors.Gray); + break; + } + } + } + } +} diff --git a/project/Aki.Launcher/ViewModels/ProfileViewModel.cs b/project/Aki.Launcher/ViewModels/ProfileViewModel.cs new file mode 100644 index 0000000..51b79ec --- /dev/null +++ b/project/Aki.Launcher/ViewModels/ProfileViewModel.cs @@ -0,0 +1,282 @@ +using Aki.Launcher.Helpers; +using Aki.Launcher.MiniCommon; +using Aki.Launcher.Models; +using Aki.Launcher.Models.Launcher; +using Avalonia; +using ReactiveUI; +using System.Threading.Tasks; +using Aki.Launcher.Attributes; +using Aki.Launcher.ViewModels.Dialogs; +using Avalonia.Threading; +using System.Reactive.Disposables; +using System.Diagnostics; +using System.IO; + +namespace Aki.Launcher.ViewModels +{ + [RequireLoggedIn] + public class ProfileViewModel : ViewModelBase + { + public string CurrentUsername { get; set; } + + private string _CurrentEdition; + public string CurrentEdition + { + get => _CurrentEdition; + set => this.RaiseAndSetIfChanged(ref _CurrentEdition, value); + } + + public string CurrentID { get; set; } + + public ProfileInfo ProfileInfo { get; set; } = AccountManager.SelectedProfileInfo; + + public ImageHelper SideImage { get; } = new ImageHelper(); + + private GameStarter gameStarter = new GameStarter(new GameStarterFrontend()); + + private ProcessMonitor monitor { get; set; } + + public ProfileViewModel(IScreen Host) : base(Host) + { + this.WhenActivated((CompositeDisposable disposables) => + { + Task.Run(() => + { + GameVersionCheck(); + }); + }); + + // cache and load side image if profile has a side + if(AccountManager.SelectedProfileInfo != null && AccountManager.SelectedProfileInfo.Side != null) + { + ImageRequest.CacheSideImage(AccountManager.SelectedProfileInfo.Side); + SideImage.Path = AccountManager.SelectedProfileInfo.SideImage; + SideImage.Touch(); + } + + monitor = new ProcessMonitor("EscapeFromTarkov", 1000, aliveCallback: GameAliveCallBack, exitCallback: GameExitCallback); + + CurrentUsername = AccountManager.SelectedAccount.username; + + CurrentEdition = AccountManager.SelectedAccount.edition; + + CurrentID = AccountManager.SelectedAccount.id; + } + + private async Task GameVersionCheck() + { + string compatibleGameVersion = ServerManager.GetCompatibleGameVersion(); + + if (compatibleGameVersion == "") return; + + // get the product version of the exe + string gameVersion = FileVersionInfo.GetVersionInfo(Path.Join(LauncherSettingsProvider.Instance.GamePath, "EscapeFromTarkov.exe")).FileVersion; + + if (gameVersion == null) return; + + // if the compatible version isn't the same as the game version show a warning dialog + if(compatibleGameVersion != gameVersion) + { + WarningDialogViewModel warning = new WarningDialogViewModel(null, + string.Format(LocalizationProvider.Instance.game_version_mismatch_format_2, gameVersion, compatibleGameVersion), + LocalizationProvider.Instance.i_understand); + Dispatcher.UIThread.InvokeAsync(async() => + { + await ShowDialog(warning); + }); + } + } + + public void LogoutCommand() + { + AccountManager.Logout(); + + NavigateTo(new ConnectServerViewModel(HostScreen, true)); + } + + public void ChangeWindowState(Avalonia.Controls.WindowState? State, bool Close = false) + { + Dispatcher.UIThread.InvokeAsync(() => + { + if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) + { + if (Close) + { + desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnMainWindowClose; + desktop.Shutdown(); + } + else + { + desktop.MainWindow.WindowState = State ?? Avalonia.Controls.WindowState.Normal; + } + } + }); + } + + public async Task StartGameCommand() + { + LauncherSettingsProvider.Instance.AllowSettings = false; + + AccountStatus status = await AccountManager.LoginAsync(AccountManager.SelectedAccount.username, AccountManager.SelectedAccount.password); + + LauncherSettingsProvider.Instance.AllowSettings = true; + + switch (status) + { + case AccountStatus.NoConnection: + NavigateTo(new ConnectServerViewModel(HostScreen)); + return; + } + + LauncherSettingsProvider.Instance.GameRunning = true; + + GameStarterResult gameStartResult = await gameStarter.LaunchGame(ServerManager.SelectedServer, AccountManager.SelectedAccount); + + if (gameStartResult.Succeeded) + { + monitor.Start(); + + switch (LauncherSettingsProvider.Instance.LauncherStartGameAction) + { + case LauncherAction.MinimizeAction: + { + ChangeWindowState(Avalonia.Controls.WindowState.Minimized); + break; + } + case LauncherAction.ExitAction: + { + ChangeWindowState(null, true); + break; + } + } + } + else + { + SendNotification("", gameStartResult.Message, Avalonia.Controls.Notifications.NotificationType.Error); + LauncherSettingsProvider.Instance.GameRunning = false; + } + } + + public async Task ChangeEditionCommand() + { + var result = await ShowDialog(new ChangeEditionDialogViewModel(null)); + + if(result != null && result is string edition) + { + AccountStatus status = await AccountManager.WipeAsync(edition); + + switch (status) + { + case AccountStatus.OK: + { + CurrentEdition = AccountManager.SelectedAccount.edition; + SendNotification("", LocalizationProvider.Instance.account_updated); + break; + } + case AccountStatus.NoConnection: + { + NavigateTo(new ConnectServerViewModel(HostScreen)); + break; + } + default: + { + SendNotification("", LocalizationProvider.Instance.edit_account_update_error); + break; + } + } + } + } + + public async Task CopyCommand(object parameter) + { + if (Application.Current.Clipboard != null && parameter != null && parameter is string text) + { + await Application.Current.Clipboard.SetTextAsync(text); + SendNotification("", $"{text} {LocalizationProvider.Instance.copied}", Avalonia.Controls.Notifications.NotificationType.Success); + } + } + + public async Task RemoveProfileCommand() + { + ConfirmationDialogViewModel confirmation = new ConfirmationDialogViewModel(null, string.Format(LocalizationProvider.Instance.profile_remove_question_format_1, AccountManager.SelectedAccount.username)); + + var result = await ShowDialog(confirmation); + + if (result is bool b && !b) return; + + AccountStatus status = await AccountManager.RemoveAsync(); + + switch(status) + { + case AccountStatus.OK: + { + SendNotification("", LocalizationProvider.Instance.profile_removed); + + LauncherSettingsProvider.Instance.Server.AutoLoginCreds = null; + + LauncherSettingsProvider.Instance.SaveSettings(); + + NavigateTo(new ConnectServerViewModel(HostScreen)); + break; + } + case AccountStatus.UpdateFailed: + { + SendNotification("", LocalizationProvider.Instance.profile_removal_failed); + break; + } + case AccountStatus.NoConnection: + { + SendNotification("", LocalizationProvider.Instance.no_servers_available); + NavigateTo(new ConnectServerViewModel(HostScreen)); + break; + } + } + } + + private void UpdateProfileInfo() + { + AccountManager.UpdateProfileInfo(); + ImageRequest.CacheSideImage(AccountManager.SelectedProfileInfo.Side); + ProfileInfo.UpdateDisplayedProfile(AccountManager.SelectedProfileInfo); + if (ProfileInfo.SideImage != SideImage.Path) + { + SideImage.Path = ProfileInfo.SideImage; + SideImage.Touch(); + } + } + + + //pull profile every x seconds + private int aliveCallBackCountdown = 60; + private void GameAliveCallBack(ProcessMonitor monitor) + { + aliveCallBackCountdown--; + + if (aliveCallBackCountdown <= 0) + { + aliveCallBackCountdown = 60; + UpdateProfileInfo(); + } + } + + private void GameExitCallback(ProcessMonitor monitor) + { + monitor.Stop(); + + LauncherSettingsProvider.Instance.GameRunning = false; + + //Make sure the call to MainWindow happens on the UI thread. + switch (LauncherSettingsProvider.Instance.LauncherStartGameAction) + { + case LauncherAction.MinimizeAction: + { + ChangeWindowState(Avalonia.Controls.WindowState.Normal); + + break; + } + } + + UpdateProfileInfo(); + } + } +} diff --git a/project/Aki.Launcher/ViewModels/SettingsViewModel.cs b/project/Aki.Launcher/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..e23ce4c --- /dev/null +++ b/project/Aki.Launcher/ViewModels/SettingsViewModel.cs @@ -0,0 +1,181 @@ +using Aki.Launcher.Controllers; +using Aki.Launcher.Helpers; +using Aki.Launcher.Models; +using Aki.Launcher.Models.Launcher; +using Aki.Launcher.ViewModels.Dialogs; +using Avalonia; +using Avalonia.Controls; +using ReactiveUI; +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +namespace Aki.Launcher.ViewModels +{ + public class SettingsViewModel : ViewModelBase + { + public LocaleCollection Locales { get; set; } = new LocaleCollection(); + + private GameStarter gameStarter = new GameStarter(new GameStarterFrontend()); + + public SettingsViewModel(IScreen Host) : base(Host) + { + if(Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow.Closing += MainWindow_Closing; + } + } + + private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) + { + LauncherSettingsProvider.Instance.SaveSettings(); + } + + public void GoBackCommand() + { + if (Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow.Closing -= MainWindow_Closing; + } + + LauncherSettingsProvider.Instance.AllowSettings = true; + LauncherSettingsProvider.Instance.SaveSettings(); + + NavigateBack(); + } + + public void CleanTempFilesCommand() + { + bool filesCleared = gameStarter.CleanTempFiles(); + + if (filesCleared) + { + SendNotification("", LocalizationProvider.Instance.clean_temp_files_succeeded, Avalonia.Controls.Notifications.NotificationType.Success); + } + else + { + SendNotification("", LocalizationProvider.Instance.clean_temp_files_failed, Avalonia.Controls.Notifications.NotificationType.Error); + } + } + + public void RemoveRegistryKeysCommand() + { + bool regKeysRemoved = gameStarter.RemoveRegistryKeys(); + + if (regKeysRemoved) + { + SendNotification("", LocalizationProvider.Instance.remove_registry_keys_succeeded, Avalonia.Controls.Notifications.NotificationType.Success); + } + else + { + SendNotification("", LocalizationProvider.Instance.remove_registry_keys_failed, Avalonia.Controls.Notifications.NotificationType.Error); + } + } + + public async Task ClearGameSettingsCommand() + { + + bool BackupAndRemove(string backupFolderPath, FileInfo file) + { + try + { + file.Refresh(); + + //if for some reason the file no longer exists /shrug + if (!file.Exists) + { + return false; + } + + //create backup dir and copy file + Directory.CreateDirectory(backupFolderPath); + + string newFilePath = Path.Combine(backupFolderPath, $"{file.Name}_{DateTime.Now.ToString("MM-dd-yyyy_hh-mm-ss-tt")}.bak"); + + File.Copy(file.FullName, newFilePath); + + //copy check + if (!File.Exists(newFilePath)) + { + return false; + } + + //delete old file + file.Delete(); + } + catch (Exception ex) + { + LogManager.Instance.Exception(ex); + } + + //delete check + if (file.Exists) + { + return false; + } + + return true; + } + + string EFTSettingsFolder = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Escape from Tarkov"); + string backupFolderPath = Path.Combine(EFTSettingsFolder, "Backups"); + + if (Directory.Exists(EFTSettingsFolder)) + { + FileInfo local_ini = new FileInfo(Path.Combine(EFTSettingsFolder, "local.ini")); + FileInfo shared_ini = new FileInfo(Path.Combine(EFTSettingsFolder, "shared.ini")); + + string Message = string.Format(LocalizationProvider.Instance.clear_game_settings_warning, backupFolderPath); + ConfirmationDialogViewModel confirmDelete = new ConfirmationDialogViewModel(null, Message, LocalizationProvider.Instance.clear_game_settings, LocalizationProvider.Instance.cancel); + + var confirmation = await ShowDialog(confirmDelete); + + if (confirmation is bool proceed && !proceed) + { + return; + } + + bool localSucceeded = BackupAndRemove(backupFolderPath, local_ini); + bool sharedSucceeded = BackupAndRemove(backupFolderPath, shared_ini); + + //if one fails, I'm considering it bad. Send failed notification. + if (!localSucceeded || !sharedSucceeded) + { + SendNotification("", LocalizationProvider.Instance.clear_game_settings_failed, Avalonia.Controls.Notifications.NotificationType.Error); + return; + } + } + + SendNotification("", LocalizationProvider.Instance.clear_game_settings_succeeded, Avalonia.Controls.Notifications.NotificationType.Success); + } + + public void OpenGameFolderCommand() + { + Process.Start(new ProcessStartInfo + { + FileName = Path.EndsInDirectorySeparator(LauncherSettingsProvider.Instance.GamePath) ? LauncherSettingsProvider.Instance.GamePath : LauncherSettingsProvider.Instance.GamePath + Path.DirectorySeparatorChar, + UseShellExecute = true, + Verb = "open" + }); + } + + public async Task SelectGameFolderCommand() + { + OpenFolderDialog dialog = new OpenFolderDialog(); + + dialog.Directory = Assembly.GetExecutingAssembly().Location; + + if (Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) + { + string? result = await dialog.ShowAsync(desktop.MainWindow); + + if (result != null) + { + LauncherSettingsProvider.Instance.GamePath = result; + } + } + } + } +} diff --git a/project/Aki.Launcher/ViewModels/ViewModelBase.cs b/project/Aki.Launcher/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..786e8ad --- /dev/null +++ b/project/Aki.Launcher/ViewModels/ViewModelBase.cs @@ -0,0 +1,173 @@ +using Aki.Launcher.Attributes; +using Aki.Launcher.Controllers; +using Aki.Launcher.Models; +using Aki.Launcher.ViewModels.Notifications; +using Avalonia.Controls.Notifications; +using Avalonia.Threading; +using ReactiveUI; +using Splat; +using System; +using System.Threading.Tasks; +using dialogHost = DialogHost.DialogHost; + +namespace Aki.Launcher.ViewModels +{ + public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableViewModel + { + public ViewModelActivator Activator { get; } = new ViewModelActivator(); + + protected WindowNotificationManager NotificationManager => Locator.Current.GetService(); + + public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 7); + + public IScreen HostScreen { get; } + + /// + /// Delay the return of the viewmodel + /// + /// The amount of time in milliseconds to delay + /// The viewmodel after the delay time + /// Useful to delay the navigation to another view. For instance, to allow an animation to complete. + private async Task WithDelay(int Milliseconds) + { + await Task.Delay(Milliseconds); + + return this; + } + + /// + /// Tests all preconditions of a viewmodel + /// + /// + /// The first failed precondition or a successful precondition if all tests pass + /// Execution of preconditions stops at the first failed condition + private NavigationPreConditionResult TestPreConditions(ViewModelBase ViewModel) + { + var attribs = ViewModel.GetType().GetCustomAttributes(typeof(NavigationPreCondition), true); + + foreach(var attrib in attribs) + { + if(attrib is NavigationPreCondition condition) + { + NavigationPreConditionResult result = condition.TestPreCondition(HostScreen); + + if(!result.Succeeded) + { + var vmTypeName = ViewModel.GetType().Name; + + LogManager.Instance.Warning($"[{vmTypeName}] Failed pre-condition check: {attrib.GetType().Name}"); + return result; + } + } + } + + return NavigationPreConditionResult.FromSuccess(); + } + + /// + /// Process the results of the precondition tests + /// + /// + /// The viewmodel that should be loaded + private ViewModelBase ProcessViewModelResults(ViewModelBase ViewModel) + { + NavigationPreConditionResult result = TestPreConditions(ViewModel); + + if (!result.Succeeded) + { + ViewModel = result.ViewModel; + } + + return ViewModel; + } + + /// + /// Navigate to another viewmodel after a delay + /// + /// + /// + /// + public async Task NavigateToWithDelay(ViewModelBase ViewModel, int Milliseconds) + { + ViewModel = ProcessViewModelResults(ViewModel); + + if (ViewModel == null) return; + + await Dispatcher.UIThread.InvokeAsync(async () => + { + HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds)); + }); + } + + /// + /// Navigate to another viewmodel + /// + /// + public void NavigateTo(ViewModelBase ViewModel) + { + ViewModel = ProcessViewModelResults(ViewModel); + + if (ViewModel == null) return; + + Dispatcher.UIThread.InvokeAsync(() => + { + HostScreen.Router.Navigate.Execute(ViewModel); + }); + } + + /// + /// Navigate to the previous viewmodel + /// + public void NavigateBack() + { + var ViewModel = HostScreen.Router.NavigationStack[HostScreen.Router.NavigationStack.Count - 2]; + + if(ViewModel is ViewModelBase vmBase) + { + var result = TestPreConditions(vmBase); + + if (!result.Succeeded) + { + Dispatcher.UIThread.InvokeAsync(() => + { + if (result.ViewModel == null) return; + + HostScreen.Router.Navigate.Execute(result.ViewModel); + return; + }); + } + } + + Dispatcher.UIThread.InvokeAsync(() => + { + HostScreen.Router.NavigateBack.Execute(); + }); + } + + /// + /// A convenience method for sending notifications + /// + /// + /// + /// + public void SendNotification(string Title, string Message, NotificationType Type = NotificationType.Information) + { + NotificationManager.Show(new AkiNotificationViewModel(HostScreen, Title, Message, Type)); + } + + /// + /// A convenience method for showing dialogs + /// + /// + /// + public async Task ShowDialog(object ViewModel) + { + return await dialogHost.Show(ViewModel); + } + + public ViewModelBase(IScreen Host) + { + HostScreen = Host; + } + } +} diff --git a/project/Aki.Launcher/Views/ConnectServerView.axaml b/project/Aki.Launcher/Views/ConnectServerView.axaml new file mode 100644 index 0000000..9028526 --- /dev/null +++ b/project/Aki.Launcher/Views/ConnectServerView.axaml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/project/Aki.Launcher/Views/Dialogs/ConfirmationDialogView.axaml.cs b/project/Aki.Launcher/Views/Dialogs/ConfirmationDialogView.axaml.cs new file mode 100644 index 0000000..2429d0f --- /dev/null +++ b/project/Aki.Launcher/Views/Dialogs/ConfirmationDialogView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Aki.Launcher.Views.Dialogs +{ + public partial class ConfirmationDialogView : UserControl + { + public ConfirmationDialogView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/project/Aki.Launcher/Views/Dialogs/RegisterDialogView.axaml b/project/Aki.Launcher/Views/Dialogs/RegisterDialogView.axaml new file mode 100644 index 0000000..d4d999c --- /dev/null +++ b/project/Aki.Launcher/Views/Dialogs/RegisterDialogView.axaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/Aki.Launcher/Views/SettingsView.axaml.cs b/project/Aki.Launcher/Views/SettingsView.axaml.cs new file mode 100644 index 0000000..e441d19 --- /dev/null +++ b/project/Aki.Launcher/Views/SettingsView.axaml.cs @@ -0,0 +1,21 @@ +using Aki.Launcher.ViewModels; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Aki.Launcher.Views +{ + public partial class SettingsView : ReactiveUserControl + { + public SettingsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + this.WhenActivated(disposables => { }); + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/project/Launcher.code-workspace b/project/Launcher.code-workspace new file mode 100644 index 0000000..a6ead35 --- /dev/null +++ b/project/Launcher.code-workspace @@ -0,0 +1,29 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "window.title": "SPT-AKI Launcher" + }, + "extensions": { + "recommendations": [ + "ms-dotnettools.csharp" + ] + }, + "tasks":{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "dotnet cake", + "group": { + "kind": "build", + "isDefault": true + } + } + ] + } +} \ No newline at end of file diff --git a/project/Launcher.sln b/project/Launcher.sln new file mode 100644 index 0000000..930a5d0 --- /dev/null +++ b/project/Launcher.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aki.ByteBanger", "Aki.ByteBanger\Aki.ByteBanger.csproj", "{4B1F5C39-92C9-41DC-820C-E0EC3500E400}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aki.Build", "Aki.Build\Aki.Build.csproj", "{A8A96141-291E-44B7-A074-4B240274083C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aki.Launcher.Base", "Aki.Launcher.Base\Aki.Launcher.Base.csproj", "{1DD556B6-45FF-43B6-A1CA-2590F270304C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aki.Launcher", "Aki.Launcher\Aki.Launcher.csproj", "{C566CA5A-0B68-4F76-A980-8BC0FE701491}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4B1F5C39-92C9-41DC-820C-E0EC3500E400}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B1F5C39-92C9-41DC-820C-E0EC3500E400}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B1F5C39-92C9-41DC-820C-E0EC3500E400}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B1F5C39-92C9-41DC-820C-E0EC3500E400}.Release|Any CPU.Build.0 = Release|Any CPU + {A8A96141-291E-44B7-A074-4B240274083C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8A96141-291E-44B7-A074-4B240274083C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8A96141-291E-44B7-A074-4B240274083C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8A96141-291E-44B7-A074-4B240274083C}.Release|Any CPU.Build.0 = Release|Any CPU + {1DD556B6-45FF-43B6-A1CA-2590F270304C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DD556B6-45FF-43B6-A1CA-2590F270304C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DD556B6-45FF-43B6-A1CA-2590F270304C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DD556B6-45FF-43B6-A1CA-2590F270304C}.Release|Any CPU.Build.0 = Release|Any CPU + {C566CA5A-0B68-4F76-A980-8BC0FE701491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C566CA5A-0B68-4F76-A980-8BC0FE701491}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C566CA5A-0B68-4F76-A980-8BC0FE701491}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C566CA5A-0B68-4F76-A980-8BC0FE701491}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6D41F10D-B323-4A4A-917A-B0C063846413} + EndGlobalSection +EndGlobal diff --git a/project/build.cake b/project/build.cake new file mode 100644 index 0000000..5961304 --- /dev/null +++ b/project/build.cake @@ -0,0 +1,82 @@ +string target = Argument("target", "ExecuteBuild"); +string config = Argument("config", "Release"); +bool VSBuilt = Argument("vsbuilt", false); + +// Cake API Reference: https://cakebuild.net/dsl/ +// setup variables +var buildDir = "./Build"; +var csprojPaths = GetFiles("./**/Aki.*(Launcher).csproj"); +var delPaths = GetDirectories("./**/*(obj|bin)"); +var akiData = "./Aki.Launcher/Aki_Data"; +var licenseFile = "../LICENSE.md"; +var publishRuntime = "win10-x64"; +var launcherDebugFolder = "./Aki.Launcher/bin/Debug/net6.0/win10-x64"; + +// Clean build directory and remove obj / bin folder from projects +Task("Clean") + .WithCriteria(!VSBuilt) //building from VS will lock the files and fail to clean the project directories. Post-Build event on Aki.Build sets this switch to true to avoid this. + .Does(() => + { + CleanDirectory(buildDir); + }) + .DoesForEach(delPaths, (directoryPath) => + { + DeleteDirectory(directoryPath, new DeleteDirectorySettings + { + Recursive = true, + Force = true + }); + }); + +// Restore, build, and publish selected csproj files +Task("Publish") + .IsDependentOn("Clean") + .DoesForEach(csprojPaths, (csprojFile) => + { + DotNetPublish(csprojFile.FullPath, new DotNetPublishSettings + { + NoLogo = true, + Configuration = config, + Runtime = publishRuntime, + PublishSingleFile = true, + SelfContained = false, + OutputDirectory = buildDir + }); + }); + +// Copy Aki_Data folder and license to build directory +Task("CopyBuildData") + .IsDependentOn("Publish") + .Does(() => + { + CopyDirectory(akiData, $"{buildDir}/Aki_Data"); + CopyFile(licenseFile, $"{buildDir}/LICENSE-Launcher.txt"); + }); + +// Copy Aki_Data to the launcher's debug directory so you can run the launcher with debugging from VS +Task("CopyDebugData") + .WithCriteria(config == "Debug") + .Does(() => + { + EnsureDirectoryDoesNotExist($"{launcherDebugFolder}/Aki_Data"); + + CopyDirectory(akiData, $"{launcherDebugFolder}/Aki_Data"); + }); + +// Remove pdb files from build if running in release configuration +Task("RemovePDBs") + .WithCriteria(config == "Release") + .IsDependentOn("CopyBuildData") + .Does(() => + { + DeleteFiles($"{buildDir}/*.pdb"); + }); + +// Runs all build tasks based on dependency and configuration +Task("ExecuteBuild") + .IsDependentOn("CopyBuildData") + .IsDependentOn("RemovePDBs") + .IsDependentOn("CopyDebugData"); + +// Runs target task +RunTarget(target); \ No newline at end of file