0
0
mirror of https://github.com/sp-tarkov/launcher.git synced 2025-02-12 17:30:42 -05:00
This commit is contained in:
Dev 2023-03-03 19:25:33 +00:00
commit dda5edfbc6
124 changed files with 10146 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -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

357
.gitignore vendored Normal file
View File

@ -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/

31
LICENSE.md Normal file
View File

@ -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.

27
README.md Normal file
View File

@ -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.

View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "2.0.0",
"commands": [
"dotnet-cake"
]
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.ByteBanger\Aki.ByteBanger.csproj" />
<ProjectReference Include="..\Aki.Launcher.Base\Aki.Launcher.Base.csproj" />
<ProjectReference Include="..\Aki.Launcher\Aki.Launcher.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(BuildingInsideVisualStudio)' == 'true'">
<Exec Command="dotnet cake &quot;../build.cake&quot; --config=&quot;$(ConfigurationName)&quot; --vsbuilt=true" />
</Target>
</Project>

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -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
}
}

View File

@ -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
```

View File

@ -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<PatchItem> items = new List<PatchItem>();
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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -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<PatchItem> items = new List<PatchItem>();
List<byte> 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<byte>();
}
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;
}
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Aki.Launch</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="zlib.net, Version=1.0.3.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\Aki.Launcher\References\zlib.net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.ByteBanger\Aki.ByteBanger.csproj" />
</ItemGroup>
</Project>

View File

@ -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<AccountStatus> LoginAsync(LoginModel Creds)
{
return await Task.Run(() =>
{
return Login(Creds.Username, Creds.Password);
});
}
public static async Task<AccountStatus> 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<AccountInfo>(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<ServerProfileInfo>(profileInfoJson);
SelectedProfileInfo = new ProfileInfo(serverProfileInfo);
}
}
public static ServerProfileInfo[] GetExistingProfiles()
{
string profileJsonArray = RequestHandler.RequestExistingProfiles();
if(profileJsonArray != null)
{
ServerProfileInfo[] miniProfiles = Json.Deserialize<ServerProfileInfo[]>(profileJsonArray);
if (miniProfiles != null && miniProfiles.Length > 0)
{
return miniProfiles;
}
}
return new ServerProfileInfo[0];
}
public static async Task<AccountStatus> 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<AccountStatus> 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<bool>(json))
{
SelectedAccount = null;
return AccountStatus.OK;
}
else
{
return AccountStatus.UpdateFailed;
}
}
catch
{
return AccountStatus.NoConnection;
}
}
public static async Task<AccountStatus> 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<AccountStatus> 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<AccountStatus> 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;
}
}
}

View File

@ -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<GameStarterResult> 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);
}
/// <summary>
/// Remove the registry keys
/// </summary>
/// <returns>returns true if the keys were removed. returns false if an exception occured</returns>
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;
}
/// <summary>
/// Clean the temp folder
/// </summary>
/// <returns>returns true if the temp folder was cleaned succefully or doesn't exist. returns false if something went wrong.</returns>
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;
}
}
}

View File

@ -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
{
/// <summary>
/// LogManager
/// </summary>
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}");
}
}

View File

@ -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");
}
}
}

View File

@ -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<string>(json);
}
catch
{
return "";
}
}
public static string GetCompatibleGameVersion()
{
try
{
string json = RequestHandler.RequestCompatibleGameVersion();
return Json.Deserialize<string>(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<ServerInfo>(json);
}
public static async Task LoadDefaultServerAsync(string server)
{
await Task.Run(() =>
{
LoadServer(server);
});
}
}
}

View File

@ -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<TKey, TValue>(this Dictionary<TKey, TValue> Dic, TValue value)
{
List<TKey> 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;
}
}
}

View File

@ -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<ProgressInfo> 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();
}
}
}
}
}

View File

@ -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<Settings>(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<string>();
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));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<PatchResultInfo> 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<PatchResultInfo> 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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<PatchResultInfo> task);
}
}

View File

@ -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
{
/// <summary>
/// The task that will report progress to the <see cref="Custom_Controls.Dialogs.ProgressDialog"/>
/// </summary>
public Action ProgressableTask { get; }
/// <summary>
/// Cancel the ProgressableTask with a reason.
/// </summary>
public event EventHandler<object> TaskCancelled;
/// <summary>
/// The <see cref="Custom_Controls.Dialogs.ProgressDialog"/> will subscribe to this event to update its main progress bar (top bar)
/// </summary>
public event EventHandler<ProgressInfo> ProgressChanged;
}
}

View File

@ -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<string> CachedRoutes = new List<string>();
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);
}
}
}
}

View File

@ -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>(T data)
{
return JsonConvert.SerializeObject(data);
}
public static T Deserialize<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json);
}
public static void Save<T>(string filepath, T data)
{
string json = Serialize<T>(data);
File.WriteAllText(filepath, json);
}
/// <summary>
/// Save an object as json with formatting
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="filepath">Full path to file</param>
/// <param name="data">Object to save to json file</param>
/// <param name="format">NewtonSoft.Json Formatting</param>
public static void SaveWithFormatting<T>(string filepath, T data, Formatting format)
{
if (!File.Exists(filepath))
{
Directory.CreateDirectory(Path.GetDirectoryName(filepath));
}
File.WriteAllText(filepath, JsonConvert.SerializeObject(data, format));
}
/// <summary>
/// Load a class from file and don't save it if it doesn't exist.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="filepath">Full path to the file to load</param>
/// <param name="AllowNullValues">Allow null class property values to be returned. Default is false</param>
/// <returns>Returns a class object or null</returns>
public static T LoadClassWithoutSaving<T>(string filepath, bool AllowNullValues = false) where T : class
{
if (File.Exists(filepath))
{
string json = File.ReadAllText(filepath);
T classObject = JsonConvert.DeserializeObject<T>(json);
if (!AllowNullValues)
{
if (classObject.GetType().GetProperties().Any(x => x.GetValue(classObject) == null))
{
return null;
}
}
return classObject;
}
return null;
}
public static T Load<T>(string filepath) where T : new()
{
if (!File.Exists(filepath))
{
Save(filepath, new T());
return Load<T>(filepath);
}
string json = File.ReadAllText(filepath);
return Deserialize<T>(json);
}
/// <summary>
/// Get a single property back from a json file.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="FilePath">Full Path to json file</param>
/// <param name="PropertyName">Name of property to return</param>
/// <returns></returns>
public static T GetPropertyByName<T>(string FilePath, string PropertyName)
{
using (StreamReader sr = new StreamReader(FilePath))
{
var tempData = JObject.Parse(sr.ReadToEnd());
if (tempData != null)
{
if (tempData[PropertyName].Value<T>() is T requestedData)
{
return requestedData;
}
}
}
return default;
}
}
}

View File

@ -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<ProcessMonitor> aliveCallback;
private readonly Action<ProcessMonitor> exitCallback;
public ProcessMonitor(string processName, double interval, Action<ProcessMonitor> aliveCallback, Action<ProcessMonitor> 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);
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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();
}
/// <summary>
/// Combine two filepaths.
/// </summary>
public static string Combine(string path1, string path2)
{
return Path.Combine(path1, path2);
}
/// <summary>
/// Combines the filepath with the current working directory.
/// </summary>
public static string FromCwd(this string filepath)
{
return Combine(Cwd, filepath);
}
/// <summary>
/// Get directory path of a filepath.
/// </summary>
public static string GetDirectory(this string filepath)
{
string value = Path.GetDirectoryName(filepath);
return (!string.IsNullOrWhiteSpace(value)) ? value : "";
}
/// <summary>
/// Get file of a filepath
/// </summary>
public static string GetFile(this string filepath)
{
string value = Path.GetFileName(filepath);
return (!string.IsNullOrWhiteSpace(value)) ? value : "";
}
/// <summary>
/// Get file name of a filepath
/// </summary>
public static string GetFileName(this string filepath)
{
string value = Path.GetFileNameWithoutExtension(filepath);
return (!string.IsNullOrWhiteSpace(value)) ? value : "";
}
/// <summary>
/// Get file extension of a filepath.
/// </summary>
public static string GetFileExtension(this string filepath)
{
string value = Path.GetExtension(filepath);
return (!string.IsNullOrWhiteSpace(value)) ? value : "";
}
/// <summary>
/// Move file from one place to another
/// </summary>
public static void MoveFile(string a, string b)
{
lock (mutex)
{
new FileInfo(a).MoveTo(b);
}
}
/// <summary>
/// Does the filepath exist?
/// </summary>
public static bool Exists(string filepath)
{
lock (mutex)
{
return Directory.Exists(filepath) || File.Exists(filepath);
}
}
/// <summary>
/// Create directory (recursive).
/// </summary>
public static void CreateDirectory(string filepath)
{
lock (mutex)
{
Directory.CreateDirectory(filepath);
}
}
/// <summary>
/// Get file content as bytes.
/// </summary>
public static byte[] ReadFile(string filepath)
{
lock (mutex)
{
return File.ReadAllBytes(filepath);
}
}
/// <summary>
/// Get file content as string.
/// </summary>
public static string ReadFile(string filepath, Encoding encoding = null)
{
return (encoding ?? Encoding.UTF8).GetString(ReadFile(filepath));
}
/// <summary>
/// Write data to file.
/// </summary>
public static void WriteFile(string filepath, byte[] data, bool append = false)
{
lock (mutex)
{
if (!Exists(filepath))
{
CreateDirectory(filepath.GetDirectory());
}
File.WriteAllBytes(filepath, data);
}
}
/// <summary>
/// Write string to file.
/// </summary>
public static void WriteFile(string filepath, string data, bool append = false, Encoding encoding = null)
{
WriteFile(filepath, (encoding ?? Encoding.UTF8).GetBytes(data), append);
}
/// <summary>
/// Get directories in directory by full path.
/// </summary>
public static string[] GetDirectories(string filepath)
{
lock (mutex)
{
DirectoryInfo di = new DirectoryInfo(filepath);
List<string> paths = new List<string>();
foreach (DirectoryInfo directory in di.GetDirectories())
{
paths.Add(directory.FullName);
}
return paths.ToArray();
}
}
/// <summary>
/// Get files in directory by full path.
/// </summary>
public static string[] GetFiles(string filepath)
{
lock (mutex)
{
DirectoryInfo di = new DirectoryInfo(filepath);
List<string> paths = new List<string>();
foreach (FileInfo file in di.GetFiles())
{
paths.Add(file.FullName);
}
return paths.ToArray();
}
}
/// <summary>
/// Delete directory.
/// </summary>
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();
}
}
/// <summary>
/// Delete file.
/// </summary>
public static void DeleteFile(string filepath)
{
lock (mutex)
{
FileInfo file = new FileInfo(filepath);
file.IsReadOnly = false;
file.Delete();
}
}
/// <summary>
/// Get files count inside directory recusively
/// </summary>
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;
}
}
}
}

View File

@ -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 = "";
}
}
}

View File

@ -0,0 +1,7 @@
namespace Aki.Launcher.Models.Aki
{
public class AkiData
{
public string version { get; set; }
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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];
}
}
}

View File

@ -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; }
}
}

View File

@ -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";
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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<string> AvailableEditions { get; private set; } = new ObservableCollection<string>(ServerManager.SelectedServer.editions);
public EditionCollection()
{
SelectedEditionIndex = 0;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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<string> 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));
}
}
}

View File

@ -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, "(?<!^)[A-Z]", "_$0").ToLower();
var locale = LocalizationProvider.Instance.GetType().GetProperty(localePropertyName).GetValue(LocalizationProvider.Instance, null) ?? value;
if (locale is string localizedName)
{
Name = localizedName;
}
}
public LocalizedLauncherAction(LauncherAction action)
{
string value = action.ToString();
Action = action;
Name = value;
UpdateLocaleName();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
}

View File

@ -0,0 +1,50 @@
/* LoginModel.cs
* License: NCSA Open Source License
*
* Copyright: Merijn Hendriks
* AUTHORS:
* Merijn Hendriks
*/
using System.ComponentModel;
namespace Aki.Launcher.Models.Launcher
{
public class LoginModel : 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 event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
}

View File

@ -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<Task<bool>> 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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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<NotificationItem> queue { get; set; } = new ObservableCollection<NotificationItem>();
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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
/// <summary>
/// Checks if the aki versions are compatible (non-major changes)
/// </summary>
/// <param name="CurrentVersion"></param>
/// <param name="ExpectedVersion"></param>
/// <returns></returns>
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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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();
}
}

454
project/Aki.Launcher/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Nullable>enable</Nullable>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<AvaloniaXaml Remove="Properties\**" />
<Compile Remove="Properties\**" />
<EmbeddedResource Remove="Properties\**" />
<None Remove="Properties\**" />
<AvaloniaResource Remove="Assets\Styles.axaml" />
<None Remove=".gitignore" />
<None Remove="Assets\aki-logo.png" />
<None Remove="Assets\icon.ico" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\icon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.15" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.15" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.12" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.15" />
<PackageReference Include="DialogHost.Avalonia" Version="0.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.ByteBanger\Aki.ByteBanger.csproj" />
<ProjectReference Include="..\Aki.Launcher.Base\Aki.Launcher.Base.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json">
<HintPath>References\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="zlib.net">
<HintPath>References\zlib.net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Update="Assets\Styles.axaml">
<SubType>Designer</SubType>
</AvaloniaXaml>
</ItemGroup>
</Project>

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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. Sil 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 lapplication",
"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"
}

View File

@ -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"
}

View File

@ -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 をダウンロードしたページ"
}

View File

@ -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게임 실행에 문제가 발생하거나 되지 않을 수 있습니다."
}

View File

@ -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Игра может работать некорректно или не работать вообще."
}

View File

@ -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"
}

View File

@ -0,0 +1,52 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Aki.Launcher"
x:Class="Aki.Launcher.App">
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<StyleInclude Source="avares://DialogHost.Avalonia/Styles.xaml"/>
<FluentTheme Mode="Light"/>
</Application.Styles>
<Application.Resources>
<!-- Colors -->
<Color x:Key="AKI_DarkGray">#121212</Color>
<Color x:Key="AKI_Yellow">#FFC107</Color>
<Color x:Key="AKI_White">#FFFFFF</Color>
<Color x:Key="AKI_Gray">#282828</Color>
<Color x:Key="AKI_DarkGrayBlue">#323947</Color>
<!-- Brushes -->
<SolidColorBrush x:Key="AKI_Foreground_Light" Color="{StaticResource AKI_White}"/>
<SolidColorBrush x:Key="AKI_Background_Light" Color="{StaticResource AKI_Gray}"/>
<SolidColorBrush x:Key="AKI_Background_Dark" Color="{StaticResource AKI_DarkGray}"/>
<SolidColorBrush x:Key="AKI_Brush_Yellow" Color="{StaticResource AKI_Yellow}"/>
<SolidColorBrush x:Key="AKI_Brush_DarkGrayBlue" Color="{StaticResource AKI_DarkGrayBlue}"/>
<SolidColorBrush x:Key="AKI_Brush_Lighter" Color="Gainsboro"/>
<!-- Path Geometry for re-usable icons -->
<PathGeometry x:Key="FolderWithPlus" Figures="M20 6h-8l-2-2H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-1 8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z" FillRule="NonZero"
/>
<PathGeometry x:Key="OpenFolder" Figures="M 2.2731724 14.474999 C 2.5381753 14.186249 3.2824783 12.195001 3.9271792 10.05 5.6676413 4.2592679 4.7621113 4.8000009 12.719033 4.8000009 c 5.6684 0 6.78597
0.072438 7.12511 0.4618343 0.332844 0.3821726 0.17704 1.1971998 -0.903259 4.7250006 -0.763041 2.4917722 -1.52781 4.4189802 -1.840552 4.6381652 C 16.708149 14.899859 14.592619 15 9.1783054
15 2.1694393 15 1.8160107 14.973129 2.2731724 14.474999 Z M 0.36305228 14.025959 C 0.11166709 13.786409 0 11.721164 0 7.3114288 0 1.9218189 0.0760474 0.8703905 0.49472143 0.47142828 0.8806724
0.10364926 1.7051307 0 4.2446088 0 7.4749739 0 7.5058294 0.00685701 8.2944922 0.89999983 L 9.0892098 1.8 h 3.6407872 c 3.221023 0 3.71338 0.069177 4.270431 0.5999996 0.346306 0.3300009 0.629646
0.802501 0.629646 1.0500009 0 0.3838238 -0.858607 0.4500002 -5.83853 0.4500002 -5.6986082 0 -5.856156 0.016794 -6.5739181 0.7007613 C 4.8131633 4.9861817 4.2426547 6.0999322 3.9498292 7.0757619
2.3566037 12.385128 1.8127023 13.81777 1.2887903 14.084957 c -0.37832867 0.192941 -0.68163535 0.173611 -0.92573802 -0.059 z" FillRule="NonZero"
/>
<PathGeometry x:Key="Alert" Figures="M 1.1531774 13.277025 C 1.7874263 12.329396 3.9770847 8.9543883 6.0190869 5.7770253 8.0610888 2.599662 9.8524992 0 10 0 c 0.147501 0 1.938911 2.599662 3.980913 5.7770253 2.042002 3.177363 4.23166 6.5523707 4.86591 7.4999997 L 20 15 H 10 0 Z M 10.904431 11.75675 c 0 -0.540527 -0.301477 -0.810801 -0.904431 -0.810801 -0.6029544 0 -0.9044312 0.270274 -0.9044312 0.810801 0 0.540545 0.3014768 0.810818 0.9044312 0.810818 0.602954 0 0.904431 -0.270273 0.904431 -0.810818 z m 0 -4.4594502 C 10.904431 5.540539 10.783833 5.270267 10 5.270267 c -0.78384 0 -0.9044312 0.270272 -0.9044312 2.0270328 0 1.7567452 0.1205987 2.0270177 0.9044312 2.0270177 0.78384 0 0.904431 -0.2702725 0.904431 -2.0270177 z" FillRule="NonZero"
/>
<PathGeometry x:Key="Delete" Figures="M 1.371429 14.489266 C 1.0250013 14.152485 0.85714332 12.248674 0.85714332 8.6563122 V 3.3233314 H 6 11.142857 v 5.3329808 c 0 3.5923618 -0.167858 5.4961728 -0.514286 5.8329538 -0.7004895 0.680979 -8.5566525 0.680979 -9.257142 0 z M 0 1.6567735 C 0 0.96445539 0.28571444 0.82349613 1.6889935 0.82349613 c 0.9289457 0 1.8081847 -0.18748843 1.9538644 -0.41664027 0.3448694 -0.54247448 4.3694148 -0.54247448 4.7142842 0 0.1456846 0.22915184 1.0249187 0.41664027 1.9538639 0.41664027 1.40328 0 1.688994 0.14096714 1.688994 0.83327737 0 0.7935997 -0.285714 0.8332789 -6 0.8332789 -5.71428556 0 -6 -0.039683 -6 -0.8332789 z" FillRule="NonZero"
/>
<PathGeometry x:Key="Gear" Figures="m 4.4615102 11.271429 c 0 -0.992273 -0.845292 -1.7685001 -1.6612536 -1.5255145 C 1.5450127 10.119702 1.3309542 10.039187 0.67223375 8.9455133 L 0.00700434 7.8410343 0.69579591 7.1261827 C 1.5704786 6.2184053 1.5693913 5.4953105 0.69230711 4.7875001 L 0 4.228826 0.66874495 3.0894129 C 1.3304739 1.9619619 1.5415251 1.879247 2.8002845 2.2540889 3.6162462 2.4970745 4.4615381 1.7208472 4.4615381 0.7285713 4.4615381 0.05428908 4.5761682 0 6 0 c 1.4238306 0 1.5384619 0.05431176 1.5384619 0.7285713 0 0.9922759 0.8452919 1.7685032 1.6612536 1.5255176 1.2587595 -0.3748419 1.4698105 -0.292127 2.1315395 0.835324 L 12 4.228826 11.307692 4.7875001 c -0.877111 0.7078104 -0.878199 1.4309052 -0.0035 2.3386826 l 0.688796 0.7148516 -0.665233 1.104479 C 10.669034 10.039187 10.454975 10.119702 9.1997324 9.7459145 8.3837708 9.5029289 7.5384788 10.279156 7.5384788 11.271429 7.5384788 11.945711 7.4238487 12 6.0000169 12 4.5761863 12 4.461555 11.945688 4.461555 11.271429 Z M 7.4416793 7.4477106 C 8.3356062 6.628907 8.3527385 5.4951178 7.4848013 4.5943362 6.6450019 3.7227628 5.4821482 3.7060611 4.5582677 4.5522701 3.6643409 5.3710738 3.6472085 6.504863 4.5151457 7.4056445 5.3549451 8.2772179 6.5177988 8.2939196 7.4416793 7.4477106 Z" FillRule="NonZero"
/>
</Application.Resources>
</Application>

View File

@ -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>((exception) =>
{
LogManager.Instance.Exception(exception);
});
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

View File

@ -0,0 +1,476 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="using:Aki.Launcher.CustomControls"
xmlns:rxui="using:Avalonia.ReactiveUI"
>
<Design.PreviewWith>
<StackPanel Spacing="5" Background="{StaticResource AKI_Background_Dark}">
<Button Content="Blah"/>
<TextBox Text="Some cool text here" Margin="5"/>
<TextBox Watermark="This is a watermark" Margin="5"/>
</StackPanel>
</Design.PreviewWith>
<!-- Add Styles Here -->
<!-- Notification Manager Styles -->
<Style Selector="WindowNotificationManager">
<Setter Property="Margin" Value="0 35 0 0"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="MaxItems" Value="2"/>
</Style>
<!-- NotificationCard Styles -->
<Style Selector="NotificationCard">
<Setter Property="Template">
<ControlTemplate>
<LayoutTransformControl Name="PART_LayoutTransformControl" UseRenderTransform="True">
<Border CornerRadius="{DynamicResource ControlCornerRadius}" BoxShadow="0 6 8 0 #4F000000" Margin="5 5 5 10">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}" ClipToBounds="True">
<DockPanel>
<ContentControl Name="PART_Content" Content="{TemplateBinding Content}" />
</DockPanel>
</Border>
</Border>
</LayoutTransformControl>
</ControlTemplate>
</Setter>
<Style.Animations>
<Animation Duration="0:0:0.45" Easing="SineEaseIn" FillMode="Forward">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0"/>
<Setter Property="ZIndex" Value="0"/>
<Setter Property="TranslateTransform.Y" Value="-100"/>
<Setter Property="ScaleTransform.ScaleX" Value="1"/>
<Setter Property="ScaleTransform.ScaleY" Value="1"/>
</KeyFrame>
<KeyFrame Cue="30%">
<Setter Property="Opacity" Value="0"/>
</KeyFrame>
<KeyFrame Cue="99%">
<Setter Property="ZIndex" Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1"/>
<Setter Property="ZIndex" Value="1"/>
<Setter Property="TranslateTransform.Y" Value="0"/>
<Setter Property="ScaleTransform.ScaleX" Value="1"/>
<Setter Property="ScaleTransform.ScaleY" Value="1"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="NotificationCard[IsClosing=true] /template/ LayoutTransformControl#PART_LayoutTransformControl">
<Style.Animations>
<Animation Duration="0:0:0.3" Easing="SineEaseOut" FillMode="Backward">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1"/>
<Setter Property="TranslateTransform.X" Value="0"/>
<Setter Property="TranslateTransform.Y" Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0"/>
<Setter Property="TranslateTransform.X" Value="0"/>
<Setter Property="TranslateTransform.Y" Value="-100"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- TitleBar Styles -->
<Style Selector="cc|TitleBar">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
<Setter Property="ButtonForeground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<Style Selector="cc|TitleBar.versiontag">
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="0 0 0 2"/>
</Style>
<!-- TextBox Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml -->
<Style Selector="TextBox">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:focus">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:pointerover">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="DimGray"/>
</Style>
<Style Selector="TextBox:pointerover /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style Selector="TextBox:focus /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style Selector="TextBox /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="White"/>
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<!-- Label Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Label.xaml -->
<Style Selector="Label">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
</Style>
<Style Selector="Label.yellow">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="Label.dark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style Selector="Label.versionMismatch">
<Setter Property="Foreground" Value="OrangeRed"/>
</Style>
<!-- ProgressBar Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml -->
<Style Selector="ProgressBar">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<Style Selector="ProgressBar.error">
<Setter Property="Foreground" Value="Red"/>
<Style.Animations>
<Animation Duration="0:0:0.5" FillMode="Forward">
<KeyFrame Cue="0%">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Value" Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="Value" Value="100"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- Seperator Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Separator.xaml -->
<Style Selector="Separator">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
</Style>
<!-- Button Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Button.xaml -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
</Style>
<Style Selector="Button:pointerover">
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<!-- Button yellow -->
<Style Selector="Button.yellow">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.yellow:pointerover">
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Gold"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="Button.yellow:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<!-- Button Link Style -->
<Style Selector="Button.link">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button.link:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button.link:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<!-- Button Bordered Link Style -->
<Style Selector="Button.borderedlink">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button.borderedlink:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="Button.borderedlink:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<!-- Button Profile Info Style -->
<Style Selector="Button.profileinfo">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Background_Dark}"/>
</Style>
<Style Selector="Button.icon">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
</Style>
<Style Selector="Button.icon:pointerover">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<!-- Checkbox styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml -->
<Style Selector="CheckBox">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
</Style>
<Style Selector="CheckBox:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Foreground_Light}" />
</Style>
<Style Selector="CheckBox:checked">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
</Style>
<Style Selector="CheckBox:checked /template/ Border#NormalRectangle">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="CheckBox:checked /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<!-- ComboBox Styles -->
<!-- Source Ref: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml -->
<Style Selector="ComboBox">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
<Setter Property="PlaceholderForeground" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
<Grid RowDefinitions="Auto, *, Auto"
ColumnDefinitions="*,32">
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
IsVisible="False"
TextBlock.FontWeight="{DynamicResource ComboBoxHeaderThemeFontWeight}"
Margin="{DynamicResource ComboBoxTopHeaderMargin}"
VerticalAlignment="Top" />
<Border x:Name="Background"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
MinWidth="{DynamicResource ComboBoxThemeMinWidth}" />
<Border x:Name="HighlightBackground"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="{DynamicResource ComboBoxBackgroundUnfocused}"
BorderBrush="{DynamicResource ComboBoxBackgroundBorderBrushUnfocused}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<TextBlock x:Name="PlaceholderTextBlock"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
Text="{TemplateBinding PlaceholderText}"
Foreground="{TemplateBinding PlaceholderForeground}"
IsVisible="{TemplateBinding SelectionBoxItem, Converter={x:Static ObjectConverters.IsNull}}" />
<ContentControl x:Name="ContentPresenter"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding ItemTemplate}"
Grid.Row="1"
Grid.Column="0"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
<Border x:Name="DropDownOverlay"
Grid.Row="1"
Grid.Column="1"
Background="Transparent"
Margin="0,1,1,1"
Width="30"
IsVisible="False"
HorizontalAlignment="Right" />
<Viewbox UseLayoutRounding="False"
MinHeight="{DynamicResource ComboBoxMinHeight}"
Grid.Row="1"
Grid.Column="1"
IsHitTestVisible="False"
Margin="0,0,10,0"
Height="12"
Width="12"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<Panel>
<Panel Height="12"
Width="12" />
<Path x:Name="DropDownGlyph"
VerticalAlignment="Center" />
</Panel>
</Viewbox>
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
IsOpen="{TemplateBinding IsDropDownOpen, Mode=TwoWay}"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="Background"
IsLightDismissEnabled="True">
<Border x:Name="PopupBorder"
Background="{StaticResource AKI_Background_Dark}"
BorderBrush="{StaticResource AKI_Background_Dark}"
BorderThickness="{DynamicResource ComboBoxDropdownBorderThickness}"
Margin="0,-1,0,-1"
Padding="{DynamicResource ComboBoxDropdownBorderPadding}"
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
Margin="{DynamicResource ComboBoxDropdownContentMargin}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
VirtualizationMode="{TemplateBinding VirtualizationMode}" />
</ScrollViewer>
</Border>
</Popup>
</Grid>
</DataValidationErrors>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ComboBox /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{StaticResource AKI_Foreground_Light}" />
</Style>
<Style Selector="ComboBox:pointerover /template/ Path#DropDownGlyph">
<Setter Property="Fill" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="ComboBox:pointerover">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="ComboBox:pointerover /template/ Border#Background">
<Setter Property="Background" Value="Black"/>
<Setter Property="BorderBrush" Value="Black" />
</Style>
<Style Selector="ComboBox /template/ Border#PopupBorder">
<Setter Property="Background" Value="Black"/>
<Setter Property="BorderBrush" Value="Black"/>
</Style>
<!-- ComboBoxItem Styles -->
<!-- Source Ref: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml-->
<Style Selector="ComboBoxItem /template/ ContentPresenter">
<Setter Property="Background" Value="Black"/>
</Style>
<Style Selector="ComboBoxItem">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
</Style>
<Style Selector="ComboBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Background_Light}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Foreground_Light}" />
</Style>
<Style Selector="ComboBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Background_Dark}" />
</Style>
</Styles>

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

View File

@ -0,0 +1,13 @@
using Aki.Launcher.Models;
using Aki.Launcher.ViewModels;
using ReactiveUI;
using System;
namespace Aki.Launcher.Attributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public abstract class NavigationPreCondition : Attribute
{
public abstract NavigationPreConditionResult TestPreCondition(IScreen Host);
}
}

View File

@ -0,0 +1,19 @@
using Aki.Launcher.Helpers;
using Aki.Launcher.Models;
using Aki.Launcher.ViewModels;
using ReactiveUI;
namespace Aki.Launcher.Attributes
{
public class RequireLoggedIn : NavigationPreCondition
{
public override NavigationPreConditionResult TestPreCondition(IScreen Host)
{
AccountStatus status = AccountManager.Login(AccountManager.SelectedAccount.username, AccountManager.SelectedAccount.password);
if (status == AccountStatus.OK) return NavigationPreConditionResult.FromSuccess();
return NavigationPreConditionResult.FromError(LocalizationProvider.Instance.login_failed, new ConnectServerViewModel(Host));
}
}
}

View File

@ -0,0 +1,19 @@
using Aki.Launcher.Helpers;
using Aki.Launcher.Models;
using Aki.Launcher.ViewModels;
using ReactiveUI;
namespace Aki.Launcher.Attributes
{
public class RequireServerConnected : NavigationPreCondition
{
public override NavigationPreConditionResult TestPreCondition(IScreen Host)
{
if (ServerManager.PingServer()) return NavigationPreConditionResult.FromSuccess();
string error = string.Format(LocalizationProvider.Instance.server_unavailable_format_1, LauncherSettingsProvider.Instance.Server.Name);
return NavigationPreConditionResult.FromError(error, new ConnectServerViewModel(Host));
}
}
}

View File

@ -0,0 +1,45 @@
/* ImageSourceConverter.cs
* License: NCSA Open Source License
*
* Copyright: Merijn Hendriks
* AUTHORS:
* waffle.lord
*/
using Aki.Launcher.Controllers;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
using System;
using System.Globalization;
namespace Aki.Launcher.Converters
{
public class ImageSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is string valueString))
{
return null;
}
try
{
if (value is string rawUri && targetType.IsAssignableFrom(typeof(Bitmap)))
{
return new Bitmap(rawUri);
}
return null;
}
catch
{
return null;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="using:Aki.Launcher.Models"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Aki.Launcher.CustomControls.LocalizedLauncherActionSelector">
<ComboBox x:Name="combobox"
SelectionChanged="combobox_SelectionChanged"
>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type model:LocalizedLauncherAction}">
<Label Content="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>

View File

@ -0,0 +1,83 @@
using Aki.Launcher.Helpers;
using Aki.Launcher.Models;
using Aki.Launcher.Models.Launcher;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System;
using System.Collections.Generic;
namespace Aki.Launcher.CustomControls
{
public partial class LocalizedLauncherActionSelector : UserControl
{
public LocalizedLauncherActionSelector()
{
InitializeComponent();
this.DetachedFromVisualTree += LocalizedLauncherActionSelector_DetachedFromVisualTree;
LocalizationProvider.LocaleChanged += LocalizationProvider_LocaleChanged;
}
private void LocalizationProvider_LocaleChanged(object? sender, EventArgs e)
{
UpdateLocales();
}
private void LocalizedLauncherActionSelector_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
//make sure static event handler is released
LocalizationProvider.LocaleChanged -= LocalizationProvider_LocaleChanged;
}
public void UpdateLocales()
{
var comboBox = this.FindControl<ComboBox>("combobox");
foreach (var item in comboBox.Items)
{
if(item is LocalizedLauncherAction localizedAction)
{
localizedAction.UpdateLocaleName();
}
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
var comboBox = this.FindControl<ComboBox>("combobox");
Array actions = Enum.GetValues(typeof(LauncherAction));
List<LocalizedLauncherAction> actionsList = new List<LocalizedLauncherAction>();
foreach (var action in actions)
{
if (action is LauncherAction launcherAction)
{
actionsList.Add(new LocalizedLauncherAction(launcherAction));
}
}
comboBox.Items = actionsList;
foreach(var item in comboBox.Items)
{
if(item is LocalizedLauncherAction actionItem && actionItem.Action == LauncherSettingsProvider.Instance.LauncherStartGameAction)
{
comboBox.SelectedItem = item;
}
}
}
public void combobox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if(e.AddedItems.Count == 1 && e.AddedItems[0] is LocalizedLauncherAction localizedAction)
{
LauncherSettingsProvider.Instance.LauncherStartGameAction = localizedAction.Action;
}
}
}
}

View File

@ -0,0 +1,94 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="using:Aki.Launcher.Helpers"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Aki.Launcher.CustomControls.TitleBar">
<Grid ColumnDefinitions="AUTO,*,AUTO,10,AUTO,AUTO">
<Rectangle Grid.ColumnSpan="6" IsHitTestVisible="False"
Fill="{Binding Background, RelativeSource={
RelativeSource AncestorType=UserControl}}"
/>
<Label Content="{Binding Title, RelativeSource={
RelativeSource AncestorType=UserControl}}"
IsHitTestVisible="False"
Foreground="{Binding Foreground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
VerticalContentAlignment="Center"
/>
<!-- Setting button -->
<Button x:Name="stb"
Grid.Column="2"
FontSize="12"
Command="{Binding SettingsButtonCommand, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Classes="link"
IsEnabled="{Binding Source={x:Static helpers:LauncherSettingsProvider.Instance}, Path=AllowSettings}"
IsVisible="{Binding Source={x:Static helpers:LauncherSettingsProvider.Instance}, Path=AllowSettings}"
>
<StackPanel Orientation="Horizontal">
<Path Data="{StaticResource Gear}" Fill="{Binding ElementName=stb, Path=Foreground}"
Margin="2"/>
<TextBlock Text="{Binding Source={x:Static helpers:LocalizationProvider.Instance}, Path=settings_menu}"/>
</StackPanel>
</Button>
<!-- Minimize (-) Button -->
<Button Content="&#xE949;" Grid.Column="4"
Foreground="{Binding ButtonForeground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Command="{Binding MinButtonCommand, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35"
>
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
</Style>
</Button.Styles>
</Button>
<!-- Close (X) Button -->
<Button Content="&#xE106;" Grid.Column="5"
Foreground="{Binding ButtonForeground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Command="{Binding XButtonCommand, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35"
>
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="IndianRed"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="Crimson"/>
</Style>
</Button.Styles>
</Button>
</Grid>
</UserControl>

View File

@ -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<string> TitleProperty =
AvaloniaProperty.Register<TitleBar, string>(nameof(Title));
public string Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly StyledProperty<IBrush> ButtonForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(ButtonForeground));
public IBrush ButtonForeground
{
get => GetValue(ButtonForegroundProperty);
set => SetValue(ButtonForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Foreground));
public new IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Background));
public new IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
//Close Button Command (X Button) Property
public static readonly StyledProperty<ICommand> XButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(XButtonCommand));
public ICommand XButtonCommand
{
get => GetValue(XButtonCommandProperty);
set => SetValue(XButtonCommandProperty, value);
}
//Minimize Button Command (- Button) Property
public static readonly StyledProperty<ICommand> MinButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(MinButtonCommand));
public ICommand MinButtonCommand
{
get => GetValue(MinButtonCommandProperty);
set => SetValue(MinButtonCommandProperty, value);
}
//Setting Button Command Property
public static readonly StyledProperty<ICommand> SettingsButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(SettingsButtonCommand));
public ICommand SettingsButtonCommand
{
get => GetValue(SettingsButtonCommandProperty);
set => SetValue(SettingsButtonCommandProperty, value);
}
}
}

View File

@ -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<WindowNotificationManager>();
public async Task CompletePatchTask(IAsyncEnumerable<PatchResultInfo> 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();
}
}
}
}
}
}

View File

@ -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);
}
/// <summary>
/// Force property changed by touching the image path.
/// </summary>
/// <remarks>Can be used to force image re-loading</remarks>
public void Touch()
{
string tmp = Path;
Path = "";
Path = tmp;
}
}
}

View File

@ -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);
}
}

View File

@ -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<App>()
.UseReactiveUI()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -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;
}
}
}

View File

@ -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>("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();
});
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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; }
/// <summary>
/// A confirmation dialog
/// </summary>
/// <param name="Host">Set to null when <see cref="ViewModelBase.ShowDialog(object)"/> is used, since the dialog host is handling routing</param>
/// <param name="Question"></param>
/// <param name="ConfirmButtonText"></param>
/// <param name="DenyButtonText"></param>
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;
}
}
}

View File

@ -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();
/// <summary>
/// A registration dialog
/// </summary>
/// <param name="Host">Set to null when <see cref="ViewModelBase.ShowDialog(object)"/> is used, since the dialog host is handling routing</param>
/// <param name="Username"></param>
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;
}
}
}

View File

@ -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; }
/// <summary>
/// A warning dialog
/// </summary>
/// <param name="Host">Set to null when <see cref="ViewModelBase.ShowDialog(object)"/> is used, since the dialog host is handling routing</param>
/// <param name="ButtonText"></param>
/// <param name="WarningMessage"></param>
public WarningDialogViewModel(IScreen Host, string WarningMessage, string? ButtonText = null) : base(Host)
{
this.WarningMessage = WarningMessage;
this.ButtonText = ButtonText ?? LocalizationProvider.Instance.ok;
}
}
}

View File

@ -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<ProfileInfo> ExistingProfiles { get; set; } = new ObservableCollection<ProfileInfo>();
public LoginModel Login { get; set; } = new LoginModel();
public ReactiveCommand<Unit, Unit> 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<ImageHelper>("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();
}
}
}
}
}

View File

@ -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<ImageHelper>(Background, "bgimage");
Locator.CurrentMutable.RegisterConstant<AkiVersion>(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));
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More