0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-12 14:10:44 -05:00
This commit is contained in:
Dev 2023-03-03 18:52:31 +00:00
commit b3ce0ec36f
120 changed files with 7323 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

355
.gitignore vendored Normal file
View File

@ -0,0 +1,355 @@
## 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
# Aki
**/Build/
**/Shared/Managed/*
**/tools/
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Bb]in/[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/

32
LICENSE.md Normal file
View File

@ -0,0 +1,32 @@
# NCSA Open Source License
Copyright (c) 2022 Merijn Hendriks. All rights reserved.
Developed by: Merijn Hendriks
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 Merijn Hendriks, 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.

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# Modules
BepInEx plugins to alter Escape From Tarkov's behaviour
**Project** | **Function**
------------------ | --------------------------------------------
Aki.Build | Build script
Aki.Bundles | External bundle loader
Aki.Common | Common utilities used across projects
Aki.Core | Required patches to start the game
Aki.Custom | SPT-AKI enhancements to EFT
Aki.Debugging | Debug utilities (disabled in release builds)
Aki.Reflection | Reflection utilities used across the project
Aki.SinglePlayer | Simulating online game while offline
## Requirements
- Escape From Tarkov 22032
- BepInEx 5.4.19
- Visual Studio Code
- .NET 6 SDK
## Setup
Copy-paste Live EFT's `EscapeFromTarkov_Data/Managed/` folder to into Modules' `Project/Shared/` folder
## Build
1. File > Open Workspace > Modules.code-workspace
2. Terminal > Run Build Task...
3. Copy-paste content inside `Build` into `%gamedir%`, overwrite when prompted.

128
docs/packetsniffer.md Normal file
View File

@ -0,0 +1,128 @@
# Packet Sniffer
References are based on version 0.12.12.15.17566
## Requirements
- de4dot
- dnspy
## Deobfuscation
```cs
// Token: 0x0600D716 RID: 55062 RVA: 0x00127E88 File Offset: 0x00126088
Class2082.smethod_0()
{
return (string)((Hashtable)AppDomain.CurrentDomain.GetData(Class2082.string_0))[int_0];
}
```
```cmd
de4dot-x64.exe Assembly-CSharp.dll
de4dot-x64.exe --un-name "!^<>[a-z0-9]$&!^<>[a-z0-9]__.*$&![A-Z][A-Z]\$<>.*$&^[a-zA-Z_<{$][a-zA-Z_0-9<>{}$.`-]*$" "Assembly-CSharp-cleaned.dll" --strtyp delegate --strtok 0x0600D716
pause
```
### Fix ResolutionScope error
1. DnSpy > File > Open... > `Assembly-CSharp-cleaned-cleaned.dll`
2. DnSpy > File > Save module.. > OK
## Modifications
### Assembly-CSharp.dll
#### Save requests
```cs
// Token: 0x06001CF6 RID: 7414 RVA: 0x0019CAC8 File Offset: 0x0019ACC8
[postfix]
Class182.method_2()
{
var uri = new Uri(url);
var path = (System.IO.Directory.GetCurrentDirectory() + "\\HTTP_DATA\\").Replace("\\\\", "\\");
var file = uri.LocalPath.Replace('/', '.').Remove(0, 1);
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
if (System.IO.Directory.CreateDirectory(path).Exists && obj != null)
{
System.IO.File.WriteAllText($@"{path}req.{file}_{time}.json", text);
}
}
```
#### Save responses
```cs
// Token: 0x06001D01 RID: 7425 RVA: 0x0019D200 File Offset: 0x0019B400
[postfix]
Class182.method_8()
{
// add this at the end, before "return text3;"
// in case you turn this into a harmony patch, text3 = __result
var uri = new Uri(url);
var path = (System.IO.Directory.GetCurrentDirectory() + "\\HTTP_DATA\\").Replace("\\\\", "\\");
var file = uri.LocalPath.Replace('/', '.').Remove(0, 1);
var time = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
if (System.IO.Directory.CreateDirectory(path).Exists)
{
System.IO.File.WriteAllText($@"{path}resp.{file}_{time}.json", text3);
}
}
```
#### Disable SSL certification
```cs
// Token: 0x0600509D RID: 20637 RVA: 0x0027D244 File Offset: 0x0027B444
[prefix]
Class537.ValidateCertificate()
{
return true;
}
```
```cs
// Token: 0x0600509E RID: 20638 RVA: 0x0027D2B4 File Offset: 0x0027B4B4
[prefix]
Class537.ValidateCertificate()
{
return true;
}
```
#### Battleye
```cs
// Token: 0x06006B7A RID: 27514 RVA: 0x002D55B8 File Offset: 0x002D37B8
[prefix]
Class815.RunValidation()
{
this.Succeed = true;
}
```
### FilesChecker.dll
#### Consistency multi
```cs
// Token: 0x06000054 RID: 84 RVA: 0x00002A38 File Offset: 0x00000C38
[prefix]
ConsistencyController.EnsureConsistency()
{
return Task.FromResult<ICheckResult>(ConsistencyController.CheckResult.Succeed(new TimeSpan()));
}
```
#### Consistency single
```cs
// Token: 0x06000053 RID: 83 RVA: 0x000028D4 File Offset: 0x00000AD4
[prefix]
ConsistencyController.EnsureConsistencySingle()
{
return Task.FromResult<ICheckResult>(ConsistencyController.CheckResult.Succeed(new TimeSpan()));
}
```

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<None Include="Build.ps1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Core\Aki.Core.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
<ProjectReference Include="..\Aki.SinglePlayer\Aki.SinglePlayer.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet cake &quot;../build.cake&quot; --vsbuilt=true" />
</Target>
</Project>

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="bsg.componentace.compression.libs.zlib" HintPath="..\Shared\Managed\bsg.componentace.compression.libs.zlib.dll" Private="False" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Core" Version="5.4.21" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Aki.Common.Utils;
namespace Aki.Common.Http
{
public class Request
{
/// <summary>
/// Send a request to remote endpoint and optionally receive a response body.
/// Deflate is the accepted compression format.
/// </summary>
public byte[] Send(string url, string method, byte[] data = null, bool compress = true, string mime = null, Dictionary<string, string> headers = null)
{
if (!WebConstants.IsValidMethod(method))
{
throw new ArgumentException("request method is invalid");
}
Uri uri = new Uri(url);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
if (uri.Scheme == "https")
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
request.ServerCertificateValidationCallback = delegate { return true; };
}
request.Timeout = 15000;
request.Method = method;
request.Headers.Add("Accept-Encoding", "deflate");
if (headers != null)
{
foreach (KeyValuePair<string, string> item in headers)
{
request.Headers.Add(item.Key, item.Value);
}
}
if (method != WebConstants.Get && method != WebConstants.Head && data != null)
{
byte[] body = (compress) ? Zlib.Compress(data, ZlibCompression.Maximum) : data;
request.ContentType = WebConstants.IsValidMime(mime) ? mime : "application/octet-stream";
request.ContentLength = body.Length;
if (compress)
{
request.Headers.Add("Content-Encoding", "deflate");
}
using (Stream stream = request.GetRequestStream())
{
stream.Write(body, 0, body.Length);
}
}
using (WebResponse response = request.GetResponse())
{
using (MemoryStream ms = new MemoryStream())
{
response.GetResponseStream().CopyTo(ms);
byte[] body = ms.ToArray();
if (body.Length == 0)
{
return null;
}
if (Zlib.IsCompressed(body))
{
return Zlib.Decompress(body);
}
return body;
}
}
}
}
}

View File

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Text;
using Aki.Common.Utils;
using BepInEx.Logging;
namespace Aki.Common.Http
{
public static class RequestHandler
{
private static string _host;
private static string _session;
private static Request _request;
private static Dictionary<string, string> _headers;
private static ManualLogSource _logger;
static RequestHandler()
{
_logger = Logger.CreateLogSource(nameof(RequestHandler));
Initialize();
}
private static void Initialize()
{
_request = new Request();
string[] args = Environment.GetCommandLineArgs();
foreach (string arg in args)
{
if (arg.Contains("BackendUrl"))
{
string json = arg.Replace("-config=", string.Empty);
_host = Json.Deserialize<ServerConfig>(json).BackendUrl;
}
if (arg.Contains("-token="))
{
_session = arg.Replace("-token=", string.Empty);
_headers = new Dictionary<string, string>()
{
{ "Cookie", $"PHPSESSID={_session}" },
{ "SessionId", _session }
};
}
}
}
private static void ValidateData(byte[] data)
{
if (data == null)
{
_logger.LogError($"Request failed, body is null");
}
_logger.LogInfo($"Request was successful");
}
private static void ValidateJson(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
_logger.LogError($"Request failed, body is null");
}
_logger.LogInfo($"Request was successful");
}
public static byte[] GetData(string path, bool hasHost = false)
{
string url = (hasHost) ? path : _host + path;
_logger.LogInfo($"Request GET data: {_session}:{url}");
byte[] result = _request.Send(url, "GET", null, headers: _headers);
ValidateData(result);
return result;
}
public static string GetJson(string path, bool hasHost = false)
{
string url = (hasHost) ? path : _host + path;
_logger.LogInfo($"Request GET json: {_session}:{url}");
byte[] data = _request.Send(url, "GET", headers: _headers);
string result = Encoding.UTF8.GetString(data);
ValidateJson(result);
return result;
}
public static string PostJson(string path, string json, bool hasHost = false)
{
string url = (hasHost) ? path : _host + path;
_logger.LogInfo($"Request POST json: {_session}:{url}");
byte[] data = _request.Send(url, "POST", Encoding.UTF8.GetBytes(json), true, "application/json", _headers);
string result = Encoding.UTF8.GetString(data);
ValidateJson(result);
return result;
}
public static void PutJson(string path, string json, bool hasHost = false)
{
string url = (hasHost) ? path : _host + path;
_logger.LogInfo($"Request PUT json: {_session}:{url}");
_request.Send(url, "PUT", Encoding.UTF8.GetBytes(json), true, "application/json", _headers);
}
}
}

View File

@ -0,0 +1,14 @@
namespace Aki.Common.Http
{
public class ServerConfig
{
public string BackendUrl { get; }
public string Version { get; }
public ServerConfig(string backendUrl, string version)
{
BackendUrl = backendUrl;
Version = version;
}
}
}

View File

@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.Linq;
namespace Aki.Common.Http
{
public static class WebConstants
{
/// <summary>
/// HTML GET method.
/// </summary>
public const string Get = "GET";
/// <summary>
/// HTML HEAD method.
/// </summary>
public const string Head = "HEAD";
/// <summary>
/// HTML POST method.
/// </summary>
public const string Post = "POST";
/// <summary>
/// HTML PUT method.
/// </summary>
public const string Put = "PUT";
/// <summary>
/// HTML DELETE method.
/// </summary>
public const string Delete = "DELETE";
/// <summary>
/// HTML CONNECT method.
/// </summary>
public const string Connect = "CONNECT";
/// <summary>
/// HTML OPTIONS method.
/// </summary>
public const string Options = "OPTIONS";
/// <summary>
/// HTML TRACE method.
/// </summary>
public const string Trace = "TRACE";
/// <summary>
/// HTML MIME types.
/// </summary>
public static Dictionary<string, string> Mime { get; private set; }
static WebConstants()
{
Mime = new Dictionary<string, string>()
{
{ ".bin", "application/octet-stream" },
{ ".txt", "text/plain" },
{ ".htm", "text/html" },
{ ".html", "text/html" },
{ ".css", "text/css" },
{ ".js", "text/javascript" },
{ ".jpeg", "image/jpeg" },
{ ".jpg", "image/jpeg" },
{ ".png", "image/png" },
{ ".ico", "image/vnd.microsoft.icon" },
{ ".json", "application/json" }
};
}
/// <summary>
/// Is HTML method valid?
/// </summary>
public static bool IsValidMethod(string method)
{
return method == Get
|| method == Head
|| method == Post
|| method == Put
|| method == Delete
|| method == Connect
|| method == Options
|| method == Trace;
}
/// <summary>
/// Is MIME type valid?
/// </summary>
public static bool IsValidMime(string mime)
{
return Mime.Any(x => x.Value == mime);
}
}
}

View File

@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace Aki.Common.Utils
{
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);
}
}
}

View File

@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Aki.Common.Utils
{
public static class VFS
{
public static string Cwd { get; private set; }
static VFS()
{
Cwd = Environment.CurrentDirectory;
}
/// <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 : string.Empty;
}
/// <summary>
/// Get file of a filepath
/// </summary>
public static string GetFile(this string filepath)
{
string value = Path.GetFileName(filepath);
return (!string.IsNullOrWhiteSpace(value)) ? value : string.Empty;
}
/// <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 : string.Empty;
}
/// <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 : string.Empty;
}
/// <summary>
/// Move file from one place to another
/// </summary>
public static void MoveFile(string a, string b)
{
new FileInfo(a).MoveTo(b);
}
/// <summary>
/// Does the filepath exist?
/// </summary>
public static bool Exists(string filepath)
{
return Directory.Exists(filepath) || File.Exists(filepath);
}
/// <summary>
/// Create directory (recursive).
/// </summary>
public static void CreateDirectory(string filepath)
{
Directory.CreateDirectory(filepath);
}
/// <summary>
/// Get file content as bytes.
/// </summary>
public static byte[] ReadFile(string filepath)
{
return File.ReadAllBytes(filepath);
}
/// <summary>
/// Get file content as string.
/// </summary>
public static string ReadTextFile(string filepath)
{
return File.ReadAllText(filepath);
}
/// <summary>
/// Write data to file.
/// </summary>
public static void WriteFile(string filepath, byte[] data)
{
if (!Exists(filepath))
{
CreateDirectory(filepath.GetDirectory());
}
File.WriteAllBytes(filepath, data);
}
/// <summary>
/// Write string to file.
/// </summary>
public static void WriteTextFile(string filepath, string data, bool append = false)
{
if (!Exists(filepath))
{
CreateDirectory(filepath.GetDirectory());
}
if (append)
{
File.AppendAllText(filepath, data);
}
else
{
File.WriteAllText(filepath, data);
}
}
/// <summary>
/// Get directories in directory by full path.
/// </summary>
public static string[] GetDirectories(string filepath)
{
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)
{
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)
{
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)
{
FileInfo file = new FileInfo(filepath);
file.IsReadOnly = false;
file.Delete();
}
/// <summary>
/// Get files count inside directory recursively
/// </summary>
public static int GetFilesCount(string filepath)
{
DirectoryInfo di = new DirectoryInfo(filepath);
return di.Exists ? di.GetFiles("*.*", SearchOption.AllDirectories).Length : -1;
}
}
}

View File

@ -0,0 +1,127 @@
using System;
using System.IO;
using ComponentAce.Compression.Libs.zlib;
namespace Aki.Common.Utils
{
public enum ZlibCompression
{
Store = 0,
Fastest = 1,
Fast = 3,
Normal = 5,
Ultra = 7,
Maximum = 9
}
public static class Zlib
{
// Level | CM/CI FLG
// ----- | ---------
// 1 | 78 01
// 2 | 78 5E
// 3 | 78 5E
// 4 | 78 5E
// 5 | 78 5E
// 6 | 78 9C
// 7 | 78 DA
// 8 | 78 DA
// 9 | 78 DA
/// <summary>
/// Check if the file is ZLib compressed
/// </summary>
/// <param name="Data">Data</param>
/// <returns>If the file is Zlib compressed</returns>
public static bool IsCompressed(byte[] Data)
{
// We need the first two bytes;
// First byte: Info (CM/CINFO) Header, should always be 0x78
// Second byte: Flags (FLG) Header, should define our compression level.
if (Data == null || Data.Length < 3 || Data[0] != 0x78)
{
return false;
}
switch (Data[1])
{
case 0x01: // fastest
case 0x5E: // low
case 0x9C: // normal
case 0xDA: // max
return true;
}
return false;
}
/// <summary>
/// Deflate data.
/// </summary>
public static byte[] Compress(byte[] data, ZlibCompression level)
{
byte[] buffer = new byte[data.Length + 24];
ZStream zs = new ZStream()
{
avail_in = data.Length,
next_in = data,
next_in_index = 0,
avail_out = buffer.Length,
next_out = buffer,
next_out_index = 0
};
zs.deflateInit((int)level);
zs.deflate(zlibConst.Z_FINISH);
data = new byte[zs.next_out_index];
Array.Copy(zs.next_out, 0, data, 0, zs.next_out_index);
return data;
}
/// <summary>
/// Inflate data.
/// </summary>
public static byte[] Decompress(byte[] data)
{
byte[] buffer = new byte[4096];
ZStream zs = new ZStream()
{
avail_in = data.Length,
next_in = data,
next_in_index = 0,
avail_out = buffer.Length,
next_out = buffer,
next_out_index = 0
};
zs.inflateInit();
using (MemoryStream ms = new MemoryStream())
{
do
{
zs.avail_out = buffer.Length;
zs.next_out = buffer;
zs.next_out_index = 0;
int result = zs.inflate(0);
if (result != 0 && result != 1)
{
break;
}
ms.Write(zs.next_out, 0, zs.next_out_index);
}
while (zs.avail_in > 0 || zs.avail_out == 0);
return ms.ToArray();
}
}
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>aki-core</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\Assembly-CSharp.dll" Private="False" />
<Reference Include="Assembly-CSharp-firstpass" HintPath="..\Shared\Managed\Assembly-CSharp-firstpass.dll" Private="False" />
<Reference Include="FilesChecker" HintPath="..\Shared\Managed\FilesChecker.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
<Reference Include="UnityEngine.CoreModule" HintPath="..\Shared\Managed\UnityEngine.CoreModule.dll" Private="False" />
<Reference Include="UnityEngine.UnityWebRequestModule" HintPath="..\Shared\Managed\UnityEngine.UnityWebRequestModule.dll" Private="False" />
<Reference Include="UnityEngine.UnityWebRequestTextureModule" HintPath="..\Shared\Managed\UnityEngine.UnityWebRequestTextureModule.dll" Private="False" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,33 @@
using System;
using Aki.Core.Patches;
using BepInEx;
namespace Aki.Core
{
[BepInPlugin("com.spt-aki.core", "AKI.Core", "1.0.0")]
class AkiCorePlugin : BaseUnityPlugin
{
public AkiCorePlugin()
{
Logger.LogInfo("Loading: Aki.Core");
try
{
new ConsistencySinglePatch().Enable();
new ConsistencyMultiPatch().Enable();
new BattlEyePatch().Enable();
new SslCertificatePatch().Enable();
new UnityWebRequestPatch().Enable();
new WebSocketPatch().Enable();
new TransportPrefixPatch().Enable();
}
catch (Exception ex)
{
Logger.LogError($"{GetType().Name}: {ex}");
throw;
}
Logger.LogInfo("Completed: Aki.Core");
}
}
}

View File

@ -0,0 +1,13 @@
using UnityEngine.Networking;
using Aki.Core.Utils;
namespace Aki.Core.Models
{
public class FakeCertificateHandler : CertificateHandler
{
protected override bool ValidateCertificate(byte[] certificateData)
{
return ValidationUtil.Validate();
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using FilesChecker;
namespace Aki.Core.Models
{
public class FakeFileCheckerResult : ICheckResult
{
public TimeSpan ElapsedTime { get; private set; }
public Exception Exception { get; private set; }
public FakeFileCheckerResult()
{
ElapsedTime = new TimeSpan();
Exception = null;
}
}
}

View File

@ -0,0 +1,29 @@
using Aki.Core.Utils;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Aki.Core.Patches
{
public class BattlEyePatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var methodName = "RunValidation";
var flags = BindingFlags.Public | BindingFlags.Instance;
return PatchConstants.EftTypes.Single(x => x.GetMethod(methodName, flags) != null)
.GetMethod(methodName, flags);
}
[PatchPrefix]
private static bool PatchPrefix(ref Task __result, ref bool ___bool_0)
{
___bool_0 = ValidationUtil.Validate();
__result = Task.CompletedTask;
return false;
}
}
}

View File

@ -0,0 +1,26 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.Core.Models;
using FilesChecker;
namespace Aki.Core.Patches
{
public class ConsistencyMultiPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return PatchConstants.FilesCheckerTypes.Single(x => x.Name == "ConsistencyController")
.GetMethods().Single(x => x.Name == "EnsureConsistency" && x.ReturnType == typeof(Task<ICheckResult>));
}
[PatchPrefix]
private static bool PatchPrefix(ref object __result)
{
__result = Task.FromResult<ICheckResult>(new FakeFileCheckerResult());
return false;
}
}
}

View File

@ -0,0 +1,26 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.Core.Models;
using FilesChecker;
namespace Aki.Core.Patches
{
public class ConsistencySinglePatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return PatchConstants.FilesCheckerTypes.Single(x => x.Name == "ConsistencyController")
.GetMethods().Single(x => x.Name == "EnsureConsistencySingle" && x.ReturnType == typeof(Task<ICheckResult>));
}
[PatchPrefix]
private static bool PatchPrefix(ref object __result)
{
__result = Task.FromResult<ICheckResult>(new FakeFileCheckerResult());
return false;
}
}
}

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Aki.Core.Models;
using System.Threading.Tasks;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using FilesChecker;
using HarmonyLib;
using System;
namespace Aki.Core.Patches
{
public class DataHandlerDebugPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return PatchConstants.EftTypes
.Single(t => t.Name == "DataHandler")
.GetMethod("method_5", BindingFlags.Instance | BindingFlags.NonPublic);
}
[PatchPostfix]
private static void PatchPrefix(ref string __result)
{
Console.WriteLine($"response json: ${__result}");
}
}
}

View File

@ -0,0 +1,25 @@
using System.Linq;
using System.Reflection;
using UnityEngine.Networking;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.Core.Utils;
namespace Aki.Core.Patches
{
public class SslCertificatePatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return PatchConstants.EftTypes.Single(x => x.BaseType == typeof(CertificateHandler))
.GetMethod("ValidateCertificate", PatchConstants.PrivateFlags);
}
[PatchPrefix]
private static bool PatchPrefix(ref bool __result)
{
__result = ValidationUtil.Validate();
return false; // Skip origial
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using HarmonyLib;
namespace Aki.Core.Patches
{
public class TransportPrefixPatch : ModulePatch
{
public TransportPrefixPatch()
{
try
{
var type = PatchConstants.EftTypes.Single(t => t.Name == "Class228");
var value = Traverse.Create(type).Field("TransportPrefixes").GetValue<Dictionary<ETransportProtocolType, string>>();
value[ETransportProtocolType.HTTPS] = "http://";
value[ETransportProtocolType.WSS] = "ws://";
}
catch (Exception ex)
{
Logger.LogError($"{nameof(TransportPrefixPatch)}: {ex}");
throw;
}
}
protected override MethodBase GetTargetMethod()
{
return PatchConstants.EftTypes.Single(t => t.GetMethods().Any(m => m.Name == "CreateFromLegacyParams"))
.GetMethod("CreateFromLegacyParams", BindingFlags.Static | BindingFlags.Public);
}
[PatchPrefix]
private static bool PatchPrefix(ref GStruct22 legacyParams)
{
//Console.WriteLine($"Original url {legacyParams.Url}");
legacyParams.Url = legacyParams.Url
.Replace("https://", "")
.Replace("http://", "");
//Console.WriteLine($"Edited url {legacyParams.Url}");
return true; // do original method after
}
[PatchTranspiler]
private static IEnumerable<CodeInstruction> PatchTranspile(ILGenerator generator, IEnumerable<CodeInstruction> instructions)
{
var codes = new List<CodeInstruction>(instructions);
var searchCode = new CodeInstruction(OpCodes.Ldstr, "https://");
var searchIndex = -1;
for (var i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == searchCode.opcode && codes[i].operand == searchCode.operand)
{
searchIndex = i;
break;
}
}
if (searchIndex == -1)
{
Logger.LogError($"{nameof(TransportPrefixPatch)} failed: Could not find reference code.");
return instructions;
}
codes[searchIndex] = new CodeInstruction(OpCodes.Ldstr, "http://");
return codes.AsEnumerable();
}
}
}

View File

@ -0,0 +1,23 @@
using System.Reflection;
using UnityEngine.Networking;
using Aki.Reflection.Patching;
using Aki.Core.Models;
namespace Aki.Core.Patches
{
public class UnityWebRequestPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(UnityWebRequestTexture).GetMethod(nameof(UnityWebRequestTexture.GetTexture), new[] { typeof(string) });
}
[PatchPostfix]
private static void PatchPostfix(UnityWebRequest __result)
{
__result.certificateHandler = new FakeCertificateHandler();
__result.disposeCertificateHandlerOnDispose = true;
__result.timeout = 15000;
}
}
}

View File

@ -0,0 +1,24 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.Core.Patches
{
public class WebSocketPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var targetInterface = PatchConstants.EftTypes.Single(x => x == typeof(IConnectionHandler) && x.IsInterface);
var typeThatMatches = PatchConstants.EftTypes.Single(x => targetInterface.IsAssignableFrom(x) && x.IsAbstract && !x.IsInterface);
return typeThatMatches.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Single(x => x.ReturnType == typeof(Uri));
}
[PatchPostfix]
private static Uri PatchPostfix(Uri __instance)
{
return new Uri(__instance.ToString().Replace("wss:", "ws:"));
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.Win32;
using System.IO;
namespace Aki.Core.Utils
{
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,241 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Comfort.Common;
using EFT.Airdrop;
using EFT.Interactive;
using EFT.SynchronizableObjects;
using UnityEngine;
namespace Aki.Custom.Airdrops
{
public class AirdropBox : MonoBehaviour
{
private const string CRATE_PATH = "assets/content/location_objects/lootable/prefab/scontainer_crate.bundle";
private const string AIRDROP_SOUNDS_PATH = "assets/content/audio/prefabs/airdrop/airdropsounds.bundle";
private readonly int CROSSFADE = Shader.PropertyToID("_Crossfade");
private readonly int COLLISION = Animator.StringToHash("collision");
public LootableContainer container;
private float fallSpeed;
private AirdropSynchronizableObject boxSync;
private AirdropLogicClass boxLogic;
private Material paraMaterial;
private Animator paraAnimator;
private AirdropSurfaceSet surfaceSet;
private Dictionary<BaseBallistic.ESurfaceSound, AirdropSurfaceSet> soundsDictionary;
private BetterSource audioSource;
private BetterSource AudioSource
{
get
{
if (audioSource != null) return audioSource;
audioSource = Singleton<BetterAudio>.Instance.GetSource(BetterAudio.AudioSourceGroupType.Environment, false);
audioSource.transform.parent = transform;
audioSource.transform.localPosition = Vector3.up;
return audioSource;
}
}
public static async Task<AirdropBox> Init(float crateFallSpeed)
{
var instance = (await LoadCrate()).AddComponent<AirdropBox>();
instance.soundsDictionary = await LoadSounds();
instance.container = instance.GetComponentInChildren<LootableContainer>();
instance.boxSync = instance.GetComponent<AirdropSynchronizableObject>();
instance.boxLogic = new AirdropLogicClass();
instance.boxSync.SetLogic(instance.boxLogic);
instance.paraAnimator = instance.boxSync.Parachute.GetComponent<Animator>();
instance.paraMaterial = instance.boxSync.Parachute.GetComponentInChildren<Renderer>().material;
instance.fallSpeed = crateFallSpeed;
return instance;
}
private static async Task<GameObject> LoadCrate()
{
var easyAssets = Singleton<PoolManager>.Instance.EasyAssets;
await easyAssets.Retain(CRATE_PATH, null, null).LoadingJob;
var crate = Instantiate(easyAssets.GetAsset<GameObject>(CRATE_PATH));
crate.SetActive(false);
return crate;
}
private static async Task<Dictionary<BaseBallistic.ESurfaceSound, AirdropSurfaceSet>> LoadSounds()
{
var easyAssets = Singleton<PoolManager>.Instance.EasyAssets;
await easyAssets.Retain(AIRDROP_SOUNDS_PATH, null, null).LoadingJob;
var soundsDictionary = new Dictionary<BaseBallistic.ESurfaceSound, AirdropSurfaceSet>();
var sets = easyAssets.GetAsset<AirdropSounds>(AIRDROP_SOUNDS_PATH).Sets;
foreach (var set in sets)
{
if (!soundsDictionary.ContainsKey(set.Surface))
{
soundsDictionary.Add(set.Surface, set);
}
else
{
Debug.LogError(set.Surface + " surface sounds are duplicated");
}
}
return soundsDictionary;
}
public IEnumerator DropCrate(Vector3 position)
{
RaycastBoxDistance(LayerMaskClass.TerrainLowPoly, out var hitInfo, position);
SetLandingSound();
boxSync.Init(1, position, Vector3.zero);
PlayAudioClip(boxSync.SqueakClip, true);
if(hitInfo.distance < 155f)
{
for (float i = 0; i < 1; i += Time.deltaTime / 6f)
{
transform.position = Vector3.Lerp(position, hitInfo.point, i*i);
yield return null;
}
transform.position = hitInfo.point;
}
else
{
var parachuteOpenPos = position + new Vector3(0f, -148.2f, 0f); // (5.5s * -9.8m/s^2) / 2
for (float i = 0; i < 1; i += Time.deltaTime / 5.5f)
{
transform.position = Vector3.Lerp(position, parachuteOpenPos, i * i);
yield return null;
}
OpenParachute();
while (RaycastBoxDistance(LayerMaskClass.TerrainLowPoly, out _))
{
transform.Translate(Vector3.down * (Time.deltaTime * fallSpeed));
transform.Rotate(Vector3.up, Time.deltaTime * 6f);
yield return null;
}
transform.position = hitInfo.point;
CloseParachute();
}
OnBoxLand(out var clipLength);
yield return new WaitForSecondsRealtime(clipLength + 0.5f);
ReleaseAudioSource();
}
private void OnBoxLand(out float clipLength)
{
var landingClip = surfaceSet.LandingSoundBank.PickSingleClip(surfaceSet.LandingSoundBank.GetRandomClipIndex(2));
clipLength = landingClip.length;
boxSync.AirdropDust.SetActive(true);
boxSync.AirdropDust.GetComponent<ParticleSystem>().Play();
AudioSource.source1.Stop();
PlayAudioClip(new TaggedClip
{
Clip = landingClip,
Falloff = (int)surfaceSet.LandingSoundBank.Rolloff,
Volume = surfaceSet.LandingSoundBank.BaseVolume
});
}
private bool RaycastBoxDistance(LayerMask layerMask, out RaycastHit hitInfo)
{
return RaycastBoxDistance(layerMask, out hitInfo, transform.position);
}
private bool RaycastBoxDistance(LayerMask layerMask, out RaycastHit hitInfo, Vector3 origin)
{
var ray = new Ray(origin, Vector3.down);
var raycast = Physics.Raycast(ray, out hitInfo, Mathf.Infinity, layerMask);
if (!raycast) return false;
return hitInfo.distance > 0.05f;
}
private void SetLandingSound()
{
if (!RaycastBoxDistance(LayerMaskClass.AudioControllerStepLayerMask, out var raycast))
{
Debug.LogError("Raycast to ground returns no hit. Choose Concrete sound landing set");
surfaceSet = soundsDictionary[BaseBallistic.ESurfaceSound.Concrete];
}
else
{
if (raycast.collider.TryGetComponent(out BaseBallistic component))
{
var surfaceSound = component.GetSurfaceSound(raycast.point);
if (soundsDictionary.ContainsKey(surfaceSound))
{
surfaceSet = soundsDictionary[surfaceSound];
return;
}
}
surfaceSet = soundsDictionary[BaseBallistic.ESurfaceSound.Concrete];
}
}
private void PlayAudioClip(TaggedClip clip, bool looped = false)
{
var volume = clip.Volume;
var occlusionGroupSimple = Singleton<BetterAudio>.Instance.GetOcclusionGroupSimple(transform.position, ref volume);
AudioSource.gameObject.SetActive(true);
AudioSource.source1.outputAudioMixerGroup = occlusionGroupSimple;
AudioSource.source1.spatialBlend = 1f;
AudioSource.SetRolloff(clip.Falloff);
AudioSource.source1.volume = volume;
if (AudioSource.source1.isPlaying) return;
AudioSource.source1.clip = clip.Clip;
AudioSource.source1.loop = looped;
AudioSource.source1.Play();
}
private void OpenParachute()
{
boxSync.Parachute.SetActive(true);
paraAnimator.SetBool(COLLISION, false);
StartCoroutine(CrossFadeAnimation(1f));
}
private void CloseParachute()
{
paraAnimator.SetBool(COLLISION, true);
StartCoroutine(CrossFadeAnimation(0f));
}
private IEnumerator CrossFadeAnimation(float targetFadeValue)
{
var curFadeValue = paraMaterial.GetFloat(CROSSFADE);
for (float i = 0; i < 1; i += Time.deltaTime / 2f)
{
paraMaterial.SetFloat(CROSSFADE, Mathf.Lerp(curFadeValue, targetFadeValue, i*i));
yield return null;
}
paraMaterial.SetFloat(CROSSFADE, targetFadeValue);
if (targetFadeValue == 0f)
{
boxSync.Parachute.SetActive(false);
}
}
private void ReleaseAudioSource()
{
if (audioSource == null) return;
audioSource.transform.parent = null;
audioSource.Release();
audioSource = null;
}
}
}

View File

@ -0,0 +1,138 @@
using System.Collections;
using System.Threading.Tasks;
using Comfort.Common;
using EFT;
using EFT.Airdrop;
using EFT.SynchronizableObjects;
using UnityEngine;
namespace Aki.Custom.Airdrops
{
public class AirdropPlane : MonoBehaviour
{
private const string PLANE_PATH = "assets/content/location_objects/lootable/prefab/il76md-90.prefab";
private const float RADIUS_TO_PICK_RANDOM_POINT = 3000f;
private AirplaneSynchronizableObject airplaneSync;
private float speed;
private float distanceToDrop;
private float flaresCooldown;
private bool flaresDeployed;
private bool headingChanged;
public static async Task<AirdropPlane> Init(Vector3 airdropPoint, int dropHeight, float planeVolume, float speed)
{
var instance = (await LoadPlane()).AddComponent<AirdropPlane>();
instance.airplaneSync = instance.GetComponent<AirplaneSynchronizableObject>();
instance.airplaneSync.SetLogic(new AirplaneLogicClass());
instance.SetPosition(dropHeight, airdropPoint);
instance.SetAudio(planeVolume);
instance.speed = speed;
instance.gameObject.SetActive(false);
return instance;
}
private static async Task<GameObject> LoadPlane()
{
var easyAssets = Singleton<PoolManager>.Instance.EasyAssets;
await easyAssets.Retain(PLANE_PATH, null, null).LoadingJob;
var plane = Instantiate(easyAssets.GetAsset<GameObject>(PLANE_PATH));
return plane;
}
private void SetAudio(float planeVolume)
{
var airplaneAudio = gameObject.AddComponent<AudioSource>();
airplaneAudio.clip = airplaneSync.soundClip.Clip;
airplaneAudio.dopplerLevel = 1f;
airplaneAudio.outputAudioMixerGroup = Singleton<BetterAudio>.Instance.VeryStandartMixerGroup;
airplaneAudio.loop = true;
airplaneAudio.maxDistance = 2000;
airplaneAudio.minDistance = 1;
airplaneAudio.pitch = 0.5f;
airplaneAudio.priority = 128;
airplaneAudio.reverbZoneMix = 1;
airplaneAudio.rolloffMode = AudioRolloffMode.Custom;
airplaneAudio.spatialBlend = 1;
airplaneAudio.spread = 60;
airplaneAudio.volume = planeVolume;
airplaneAudio.Play();
}
private void SetPosition(int dropHeight, Vector3 airdropPoint)
{
var pointOnCircle = Random.insideUnitCircle.normalized * RADIUS_TO_PICK_RANDOM_POINT;
transform.position = new Vector3(pointOnCircle.x, dropHeight, pointOnCircle.y);
transform.LookAt(new Vector3(airdropPoint.x, dropHeight, airdropPoint.z));
}
public void ManualUpdate(float distance)
{
transform.Translate(Vector3.forward * (Time.deltaTime * speed));
distanceToDrop = distance;
UpdateFlaresLogic();
if (distance - 200f > 0f || headingChanged) return;
StartCoroutine(ChangeHeading());
headingChanged = true;
}
private void UpdateFlaresLogic()
{
if (flaresDeployed) return;
if (distanceToDrop > 0f && flaresCooldown <= Time.unscaledTime)
{
flaresCooldown = Time.unscaledTime + 4f;
StartCoroutine(DeployFlares(Random.Range(0.2f, 0.4f)));
}
if (distanceToDrop > 0f) return;
flaresDeployed = true;
StartCoroutine(DeployFlares(5f));
}
private IEnumerator DeployFlares(float emissionTime)
{
var projectile = Instantiate(airplaneSync.infraredCountermeasureParticles, transform);
projectile.transform.localPosition = new Vector3(0f, -5f, 0f);
var flares = projectile.GetComponentsInChildren<ParticleSystem>();
var endTime = Time.unscaledTime + emissionTime;
Singleton<GameWorld>.Instance.SynchronizableObjectLogicProcessor.AirdropManager.AddProjectile(projectile,
endTime + flares[0].main.duration + flares[0].main.startLifetime.Evaluate(1f));
while (endTime > Time.unscaledTime)
yield return null;
projectile.transform.parent = null;
foreach (var particleSystem in flares)
particleSystem.Stop();
}
private IEnumerator ChangeHeading()
{
var startingRotation = transform.eulerAngles;
var middleRotation = startingRotation + new Vector3(0f, 40f, -200f);
var endRotation = middleRotation + new Vector3(0f, 40f, 200f);
for (float i = 0; i < 1; i += Time.deltaTime / 25f)
{
var finalRotation = Vector3.Lerp(middleRotation, endRotation, EasingSmoothSquared(i));
transform.eulerAngles = Vector3.Lerp(startingRotation, finalRotation, EasingSmoothSquared(i));
yield return null;
}
}
private float EasingSmoothSquared(float x)
{
return x < 0.5 ? x * x * 2 : (1 - (1 - x) * (1 - x) * 2);
}
}
}

View File

@ -0,0 +1,107 @@
using Aki.Custom.Airdrops.Models;
using Aki.Custom.Airdrops.Utils;
using Comfort.Common;
using EFT;
using UnityEngine;
namespace Aki.Custom.Airdrops
{
public class AirdropsManager : MonoBehaviour
{
private AirdropPlane airdropPlane;
private AirdropBox airdropBox;
private ItemFactoryUtil factory;
public bool isFlareDrop;
private AirdropParametersModel airdropParameters;
public async void Start()
{
var gameWorld = Singleton<GameWorld>.Instance;
if (gameWorld == null) Destroy(this);
airdropParameters = AirdropUtil.InitAirdropParams(gameWorld, isFlareDrop);
if (!airdropParameters.AirdropAvailable)
{
Destroy(this);
return;
}
try
{
airdropPlane = await AirdropPlane.Init(airdropParameters.RandomAirdropPoint,
airdropParameters.DropHeight, airdropParameters.Config.PlaneVolume, airdropParameters.Config.PlaneSpeed);
airdropBox = await AirdropBox.Init(airdropParameters.Config.CrateFallSpeed);
factory = new ItemFactoryUtil();
}
catch
{
Debug.LogError($"[AKI-AIRDROPS]: Unable to create plane or crate, airdrop won't occur");
Destroy(this);
throw;
}
SetDistanceToDrop();
}
public void FixedUpdate()
{
airdropParameters.Timer += 0.02f;
if (airdropParameters.Timer >= airdropParameters.TimeToStart && !airdropParameters.PlaneSpawned)
{
StartPlane();
}
if (!airdropParameters.PlaneSpawned) return;
if (airdropParameters.DistanceTraveled >= airdropParameters.DistanceToDrop && !airdropParameters.BoxSpawned)
{
StartBox();
BuildLootContainer();
}
if (airdropParameters.DistanceTraveled < airdropParameters.DistanceToTravel)
{
airdropParameters.DistanceTraveled += Time.deltaTime * airdropParameters.Config.PlaneSpeed;
var distanceToDrop = airdropParameters.DistanceToDrop - airdropParameters.DistanceTraveled;
airdropPlane.ManualUpdate(distanceToDrop);
}
else
{
Destroy(airdropPlane.gameObject);
Destroy(this);
}
}
private void StartPlane()
{
airdropPlane.gameObject.SetActive(true);
airdropParameters.PlaneSpawned = true;
}
private void StartBox()
{
airdropParameters.BoxSpawned = true;
var pointPos = airdropParameters.RandomAirdropPoint;
var dropPos = new Vector3(pointPos.x, airdropParameters.DropHeight, pointPos.z);
airdropBox.gameObject.SetActive(true);
airdropBox.StartCoroutine(airdropBox.DropCrate(dropPos));
}
private void BuildLootContainer()
{
factory.BuildContainer(airdropBox.container);
factory.AddLoot(airdropBox.container);
}
private void SetDistanceToDrop()
{
airdropParameters.DistanceToDrop = Vector3.Distance(
new Vector3(airdropParameters.RandomAirdropPoint.x, airdropParameters.DropHeight, airdropParameters.RandomAirdropPoint.z),
airdropPlane.transform.position);
}
}
}

View File

@ -0,0 +1,55 @@
using Newtonsoft.Json;
namespace Aki.Custom.Airdrops.Models
{
public class AirdropConfigModel
{
[JsonProperty("airdropChancePercent")]
public AirdropChancePercent AirdropChancePercent { get; set; }
[JsonProperty("airdropMinStartTimeSeconds")]
public int AirdropMinStartTimeSeconds { get; set; }
[JsonProperty("airdropMaxStartTimeSeconds")]
public int AirdropMaxStartTimeSeconds { get; set; }
[JsonProperty("planeMinFlyHeight")]
public int PlaneMinFlyHeight { get; set; }
[JsonProperty("planeMaxFlyHeight")]
public int PlaneMaxFlyHeight { get; set; }
[JsonProperty("planeVolume")]
public float PlaneVolume { get; set; }
[JsonProperty("planeSpeed")]
public float PlaneSpeed { get; set; }
[JsonProperty("crateFallSpeed")]
public float CrateFallSpeed { get; set; }
}
public class AirdropChancePercent
{
[JsonProperty("bigmap")]
public int Bigmap { get; set; }
[JsonProperty("woods")]
public int Woods { get; set; }
[JsonProperty("lighthouse")]
public int Lighthouse { get; set; }
[JsonProperty("shoreline")]
public int Shoreline { get; set; }
[JsonProperty("interchange")]
public int Interchange { get; set; }
[JsonProperty("reserve")]
public int Reserve { get; set; }
[JsonProperty("tarkovStreets")]
public int TarkovStreets { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace Aki.Custom.Airdrops.Models
{
public class AirdropLootModel
{
[JsonProperty("tpl")]
public string Tpl { get; set; }
[JsonProperty("isPreset")]
public bool IsPreset { get; set; }
[JsonProperty("stackCount")]
public int StackCount { get; set; }
[JsonProperty("id")]
public string ID { get; set; }
}
}

View File

@ -0,0 +1,20 @@
namespace Aki.Custom.Airdrops.Models
{
public class AirdropParametersModel
{
public AirdropConfigModel Config;
public bool AirdropAvailable;
public float DistanceTraveled;
public float DistanceToTravel;
public float DistanceToDrop;
public float Timer;
public bool PlaneSpawned;
public bool BoxSpawned;
public int DropHeight;
public int TimeToStart;
public UnityEngine.Vector3 RandomAirdropPoint;
}
}

View File

@ -0,0 +1,32 @@
using Aki.Reflection.Patching;
using Comfort.Common;
using EFT;
using EFT.Airdrop;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Airdrops.Patches
{
public class AirdropFlarePatch : ModulePatch
{
private static readonly string[] _usableFlares = { "624c09cfbc2e27219346d955", "62389ba9a63f32501b1b4451" };
protected override MethodBase GetTargetMethod()
{
return typeof(FlareCartridge).GetMethod(nameof(FlareCartridge.Init),
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
}
[PatchPostfix]
private static void PatchPostfix(BulletClass flareCartridge)
{
var gameWorld = Singleton<GameWorld>.Instance;
var points = LocationScene.GetAll<AirdropPoint>().Any();
if (gameWorld != null && points && _usableFlares.Any(x => x == flareCartridge.Template._id))
{
gameWorld.gameObject.AddComponent<AirdropsManager>().isFlareDrop = true;
}
}
}
}

View File

@ -0,0 +1,29 @@
using Aki.Reflection.Patching;
using Comfort.Common;
using EFT;
using EFT.Airdrop;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Airdrops.Patches
{
public class AirdropPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted));
}
[PatchPostfix]
public static void PatchPostFix()
{
var gameWorld = Singleton<GameWorld>.Instance;
var points = LocationScene.GetAll<AirdropPoint>().Any();
if (gameWorld != null && points)
{
gameWorld.gameObject.AddComponent<AirdropsManager>();
}
}
}
}

View File

@ -0,0 +1,127 @@
using Aki.Common.Http;
using Aki.Custom.Airdrops.Models;
using EFT;
using EFT.Airdrop;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Aki.Custom.Airdrops.Utils
{
public static class AirdropUtil
{
public static AirdropConfigModel GetConfigFromServer()
{
string json = RequestHandler.GetJson("/singleplayer/airdrop/config");
return JsonConvert.DeserializeObject<AirdropConfigModel>(json);
}
public static int ChanceToSpawn(GameWorld gameWorld, AirdropConfigModel config, bool isFlare)
{
// Flare summoned airdrops are guaranteed
if (isFlare)
{
return 100;
}
string location = gameWorld.RegisteredPlayers[0].Location;
int result = 25;
switch (location.ToLower())
{
case "bigmap":
{
result = config.AirdropChancePercent.Bigmap;
break;
}
case "interchange":
{
result = config.AirdropChancePercent.Interchange;
break;
}
case "rezervbase":
{
result = config.AirdropChancePercent.Reserve;
break;
}
case "shoreline":
{
result = config.AirdropChancePercent.Shoreline;
break;
}
case "woods":
{
result = config.AirdropChancePercent.Woods;
break;
}
case "lighthouse":
{
result = config.AirdropChancePercent.Lighthouse;
break;
}
case "tarkovstreets":
{
result = config.AirdropChancePercent.TarkovStreets;
break;
}
}
return result;
}
public static bool ShouldAirdropOccur(int dropChance, List<AirdropPoint> airdropPoints)
{
return airdropPoints.Count > 0 && Random.Range(0, 100) <= dropChance;
}
public static AirdropParametersModel InitAirdropParams(GameWorld gameWorld, bool isFlare)
{
var serverConfig = GetConfigFromServer();
var allAirdropPoints = LocationScene.GetAll<AirdropPoint>().ToList();
var playerPosition = gameWorld.RegisteredPlayers[0].Position;
var flareAirdropPoints = new List<AirdropPoint>();
var dropChance = ChanceToSpawn(gameWorld, serverConfig, isFlare);
if (isFlare && allAirdropPoints.Count > 0)
{
foreach (AirdropPoint point in allAirdropPoints)
{
if (Vector3.Distance(playerPosition, point.transform.position) <= 100f)
{
flareAirdropPoints.Add(point);
}
}
}
if (flareAirdropPoints.Count == 0 && isFlare)
{
Debug.LogError($"[AKI-AIRDROPS]: Airdrop called in by flare, Unable to find an airdropPoint within 100m, defaulting to normal drop");
flareAirdropPoints.Add(allAirdropPoints.OrderBy(_ => Guid.NewGuid()).FirstOrDefault());
}
return new AirdropParametersModel()
{
Config = serverConfig,
AirdropAvailable = ShouldAirdropOccur(dropChance, allAirdropPoints),
DistanceTraveled = 0f,
DistanceToTravel = 8000f,
Timer = 0,
PlaneSpawned = false,
BoxSpawned = false,
DropHeight = Random.Range(serverConfig.PlaneMinFlyHeight, serverConfig.PlaneMaxFlyHeight),
TimeToStart = isFlare
? 5
: Random.Range(serverConfig.AirdropMinStartTimeSeconds, serverConfig.AirdropMaxStartTimeSeconds),
RandomAirdropPoint = isFlare && allAirdropPoints.Count > 0
? flareAirdropPoints.OrderBy(_ => Guid.NewGuid()).First().transform.position
: allAirdropPoints.OrderBy(_ => Guid.NewGuid()).First().transform.position
};
}
}
}

View File

@ -0,0 +1,73 @@
using Comfort.Common;
using EFT;
using UnityEngine;
using EFT.Interactive;
using EFT.InventoryLogic;
using Aki.Common.Http;
using Newtonsoft.Json;
using System.Collections.Generic;
using Aki.Custom.Airdrops.Models;
using System.Linq;
using System.Threading.Tasks;
namespace Aki.Custom.Airdrops.Utils
{
public class ItemFactoryUtil
{
private ItemFactory itemFactory;
private static readonly string DropContainer = "6223349b3136504a544d1608";
public ItemFactoryUtil()
{
itemFactory = Singleton<ItemFactory>.Instance;
}
public void BuildContainer(LootableContainer container)
{
if (itemFactory.ItemTemplates.TryGetValue(DropContainer, out var template))
{
Item item = itemFactory.CreateItem(DropContainer, template._id, null);
LootItem.CreateLootContainer(container, item, "CRATE", Singleton<GameWorld>.Instance);
}
else
{
Debug.LogError($"[AKI-AIRDROPS]: unable to find template: {DropContainer}");
}
}
public async void AddLoot(LootableContainer container)
{
List<AirdropLootModel> loot = GetLoot();
Item actualItem;
foreach (var item in loot)
{
ResourceKey[] resources;
if (item.IsPreset)
{
actualItem = itemFactory.GetPresetItem(item.Tpl);
resources = actualItem.GetAllItems().Select(x => x.Template).SelectMany(x => x.AllResources).ToArray();
}
else
{
actualItem = itemFactory.CreateItem(item.ID, item.Tpl, null);
actualItem.StackObjectsCount = item.StackCount;
resources = actualItem.Template.AllResources.ToArray();
}
container.ItemOwner.MainStorage[0].Add(actualItem);
await Singleton<PoolManager>.Instance.LoadBundlesAndCreatePools(PoolManager.PoolsCategory.Raid, PoolManager.AssemblyType.Local, resources, JobPriority.Immediate, null, PoolManager.DefaultCancellationToken);
}
}
private List<AirdropLootModel> GetLoot()
{
var json = RequestHandler.GetJson("/client/location/getAirdropLoot");
var loot = JsonConvert.DeserializeObject<List<AirdropLootModel>>(json);
return loot;
}
}
}

View File

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>aki-custom</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\Assembly-CSharp.dll" Private="False" />
<Reference Include="Comfort" HintPath="..\Shared\Managed\Comfort.dll" Private="False" />
<Reference Include="Sirenix.Serialization" HintPath="..\Shared\Managed\Sirenix.Serialization.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
<Reference Include="UnityEngine.AnimationModule" HintPath="..\Shared\Managed\UnityEngine.AnimationModule.dll" Private="False" />
<Reference Include="UnityEngine.AssetBundleModule" HintPath="..\Shared\Managed\UnityEngine.AssetBundleModule.dll" Private="False" />
<Reference Include="UnityEngine.AudioModule" HintPath="..\Shared\Managed\UnityEngine.AudioModule.dll" Private="False" />
<Reference Include="UnityEngine.CoreModule" HintPath="..\Shared\Managed\UnityEngine.CoreModule.dll" Private="False" />
<Reference Include="UnityEngine.ParticleSystemModule" HintPath="..\Shared\Managed\UnityEngine.ParticleSystemModule.dll" Private="false" />
<Reference Include="UnityEngine.PhysicsModule" HintPath="..\Shared\Managed\UnityEngine.PhysicsModule.dll" Private="False" />
<Reference Include="UnityEngine.UI" HintPath="..\Shared\Managed\UnityEngine.UI.dll" Private="False" />
<Reference Include="UnityEngine.UIModule" HintPath="..\Shared\Managed\UnityEngine.UIModule.dll" Private="False" />
<Reference Include="Unity.ScriptableBuildPipeline" HintPath="..\Shared\Managed\Unity.ScriptableBuildPipeline.dll" Private="False" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.PrePatch\Aki.PrePatch.csproj" />
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,50 @@
using System;
using Aki.Custom.Airdrops.Patches;
using Aki.Custom.Patches;
using Aki.Custom.Utils;
using BepInEx;
namespace Aki.Custom
{
[BepInPlugin("com.spt-aki.custom", "AKI.Custom", "1.0.0")]
class AkiCustomPlugin : BaseUnityPlugin
{
public AkiCustomPlugin()
{
Logger.LogInfo("Loading: Aki.Custom");
try
{
// Bundle patches should always load first
BundleManager.GetBundles();
new EasyAssetsPatch().Enable();
new EasyBundlePatch().Enable();
new BossSpawnChancePatch().Enable();
new BotDifficultyPatch().Enable();
new CoreDifficultyPatch().Enable();
new OfflineRaidMenuPatch().Enable();
new SessionIdPatch().Enable();
new VersionLabelPatch().Enable();
new IsEnemyPatch().Enable();
//new AddSelfAsEnemyPatch().Enable();
new CheckAndAddEnemyPatch().Enable();
new BotSelfEnemyPatch().Enable(); // needed
new AddEnemyToAllGroupsInBotZonePatch().Enable();
new AirdropPatch().Enable();
new AirdropFlarePatch().Enable();
new AddSptBotSettingsPatch().Enable();
new CustomAiPatch().Enable();
new ExitWhileLootingPatch().Enable();
new QTEPatch().Enable();
}
catch (Exception ex)
{
Logger.LogError($"{GetType().Name}: {ex}");
throw;
}
Logger.LogInfo("Completed: Aki.Custom");
}
}
}

View File

@ -0,0 +1,16 @@
namespace Aki.Custom.Models
{
public class BundleInfo
{
public string Key { get; }
public string Path { get; set; }
public string[] DependencyKeys { get; }
public BundleInfo(string key, string path, string[] dependencyKeys)
{
Key = key;
Path = path;
DependencyKeys = dependencyKeys;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Aki.Custom.Models
{
public struct BundleItem
{
public string FileName;
public uint Crc;
public string[] Dependencies;
}
}

View File

@ -0,0 +1,24 @@
using EFT.Bots;
namespace Aki.Custom.Models
{
public class DefaultRaidSettings
{
public EBotAmount AiAmount;
public EBotDifficulty AiDifficulty;
public bool BossEnabled;
public bool ScavWars;
public bool TaggedAndCursed;
public bool EnablePve;
public DefaultRaidSettings(EBotAmount aiAmount, EBotDifficulty aiDifficulty, bool bossEnabled, bool scavWars, bool taggedAndCursed, bool enablePve)
{
AiAmount = aiAmount;
AiDifficulty = aiDifficulty;
BossEnabled = bossEnabled;
ScavWars = scavWars;
TaggedAndCursed = taggedAndCursed;
EnablePve = enablePve;
}
}
}

View File

@ -0,0 +1,8 @@
namespace Aki.Custom.Models
{
public struct VersionResponse
{
public string Version { get; set; }
}
}

View File

@ -0,0 +1,34 @@
using Aki.Reflection.Patching;
using EFT;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
/// <summary>
/// If a bot being added has an ID found in list_1, it means its trying to add itself to its enemy list
/// Dont add bot to enemy list if its in list_1 and skip the rest of the AddEnemy() function
/// </summary>
public class AddSelfAsEnemyPatch : ModulePatch
{
private static readonly string methodName = "AddEnemy";
protected override MethodBase GetTargetMethod()
{
return typeof(BotGroupClass).GetMethod(methodName);
}
[PatchPrefix]
private static bool PatchPrefix(BotGroupClass __instance, IAIDetails person)
{
var botOwners = (List<BotOwner>)__instance.GetType().GetField("list_1", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
if (botOwners.Any(x => x.Id == person.Id))
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,74 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
public class AddEnemyToAllGroupsInBotZonePatch : ModulePatch
{
private static Type _targetType;
private const string methodName = "AddEnemyToAllGroupsInBotZone";
public AddEnemyToAllGroupsInBotZonePatch()
{
_targetType = PatchConstants.EftTypes.Single(IsTargetType);
}
private bool IsTargetType(Type type)
{
if (type.Name == nameof(BotControllerClass) && type.GetMethod(methodName) != null)
{
return true;
}
return false;
}
protected override MethodBase GetTargetMethod()
{
Logger.LogDebug($"{this.GetType().Name} Type: {_targetType.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {methodName}");
return _targetType.GetMethod(methodName);
}
/// <summary>
/// AddEnemyToAllGroupsInBotZone()
/// Goal: by default, AddEnemyToAllGroupsInBotZone doesn't check if the bot group is on the same side as the player.
/// The effect of this is that when you are a Scav and kill a Usec, every bot group in the zone will aggro you including other Scavs.
/// This should fix that.
/// </summary>
[PatchPrefix]
private static bool PatchPrefix(BotControllerClass __instance, IAIDetails aggressor, IAIDetails groupOwner, IAIDetails target)
{
BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone;
foreach (var item in __instance.Groups())
{
if (item.Key != botZone)
{
continue;
}
foreach (var group in item.Value.GetGroups(notNull: true))
{
bool differentSide = aggressor.Side != group.Side;
bool sameSide = aggressor.Side == target.Side;
if (!group.Enemies.ContainsKey(aggressor)
&& (differentSide || !sameSide)
&& !group.HaveMemberWithRole(WildSpawnType.gifter)
&& group.ShallRevengeFor(target)
)
{
group.AddEnemy(aggressor);
}
}
}
return false;
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Reflection;
using Aki.PrePatch;
using Aki.Reflection.Patching;
using EFT;
namespace Aki.Custom.Patches
{
public class AddSptBotSettingsPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(BotSettingsRepoClass).GetMethod("Init");
}
[PatchPrefix]
private static void PatchPrefix(ref Dictionary<WildSpawnType, BotSettingsValuesClass> ___dictionary_0)
{
if (___dictionary_0.ContainsKey((WildSpawnType)AkiBotsPrePatcher.sptUsecValue))
{
return;
}
___dictionary_0.Add((WildSpawnType)AkiBotsPrePatcher.sptUsecValue, new BotSettingsValuesClass(false, false, false, EPlayerSide.Savage.ToStringNoBox()));
___dictionary_0.Add((WildSpawnType)AkiBotsPrePatcher.sptBearValue, new BotSettingsValuesClass(false, false, false, EPlayerSide.Savage.ToStringNoBox()));
}
}
}

View File

@ -0,0 +1,56 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
/// <summary>
/// Boss spawn chance is 100%, all the time, this patch adjusts the chance to the maps boss wave value
/// </summary>
public class BossSpawnChancePatch : ModulePatch
{
private static float[] _bossSpawnPercent;
protected override MethodBase GetTargetMethod()
{
var desiredType = PatchConstants.LocalGameType;
var desiredMethod = desiredType
.GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly)
.SingleOrDefault(m => IsTargetMethod(m));
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod.Name}");
return desiredMethod;
}
private static bool IsTargetMethod(MethodInfo mi)
{
var parameters = mi.GetParameters();
return (parameters.Length == 2
&& parameters[0].Name == "wavesSettings"
&& parameters[1].Name == "bossLocationSpawn");
}
[PatchPrefix]
private static void PatchPrefix(BossLocationSpawn[] bossLocationSpawn)
{
_bossSpawnPercent = bossLocationSpawn.Select(s => s.BossChance).ToArray();
}
[PatchPostfix]
private static void PatchPostfix(ref BossLocationSpawn[] __result)
{
if (__result.Length != _bossSpawnPercent.Length)
{
return;
}
for (var i = 0; i < _bossSpawnPercent.Length; i++)
{
__result[i].BossChance = _bossSpawnPercent[i];
}
}
}
}

View File

@ -0,0 +1,28 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.Common.Http;
using EFT;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
public class BotDifficultyPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var methodName = "LoadDifficultyStringInternal";
var flags = BindingFlags.Public | BindingFlags.Static;
return PatchConstants.EftTypes.Single(x => x.GetMethod(methodName, flags) != null)
.GetMethod(methodName, flags);
}
[PatchPrefix]
private static bool PatchPrefix(ref string __result, BotDifficulty botDifficulty, WildSpawnType role)
{
__result = RequestHandler.GetJson($"/singleplayer/settings/bot/difficulty/{role}/{botDifficulty}");
return string.IsNullOrWhiteSpace(__result);
}
}
}

View File

@ -0,0 +1,73 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
public class BotEnemyTargetPatch : ModulePatch
{
private static Type _targetType;
private static readonly string methodName = "AddEnemyToAllGroupsInBotZone";
public BotEnemyTargetPatch()
{
_targetType = PatchConstants.EftTypes.Single(IsTargetType);
}
private bool IsTargetType(Type type)
{
if (type.Name == nameof(BotControllerClass) && type.GetMethod(methodName) != null)
{
Logger.LogInfo($"{methodName}: {type.FullName}");
return true;
}
return false;
}
protected override MethodBase GetTargetMethod()
{
return _targetType.GetMethod(methodName);
}
/// <summary>
/// AddEnemyToAllGroupsInBotZone()
/// Goal: by default, AddEnemyToAllGroupsInBotZone doesn't check if the bot group is on the same side as the player.
/// The effect of this is that when you are a Scav and kill a Usec, every bot group in the zone will aggro you including other Scavs.
/// This should fix that.
/// </summary>
[PatchPrefix]
private static bool PatchPrefix(BotControllerClass __instance, IAIDetails aggressor, IAIDetails groupOwner, IAIDetails target)
{
BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone;
foreach (var item in __instance.Groups())
{
if (item.Key != botZone)
{
continue;
}
foreach (var group in item.Value.GetGroups(notNull: true))
{
if (!group.Enemies.ContainsKey(aggressor) && ShouldAttack(aggressor, target, group))
{
group.AddEnemy(aggressor);
}
}
}
return false;
}
private static bool ShouldAttack(IAIDetails attacker, IAIDetails victim, BotGroupClass groupToCheck)
{
// Group should target if player attack a victim on the same side or if the group is not on the same side as the player.
bool shouldAttack = attacker.Side != groupToCheck.Side
|| attacker.Side == victim.Side;
return !groupToCheck.HaveMemberWithRole(WildSpawnType.gifter) && groupToCheck.ShallRevengeFor(victim) && shouldAttack;
}
}
}

View File

@ -0,0 +1,41 @@
using Aki.Reflection.Patching;
using EFT;
using System.Reflection;
namespace Aki.Custom.Patches
{
/// <summary>
/// Goal: patch removes the current bot from its own enemy list - occurs when adding bots type to its enemy array in difficulty settings
/// </summary>
internal class BotSelfEnemyPatch : ModulePatch
{
private static readonly string methodName = "PreActivate";
protected override MethodBase GetTargetMethod()
{
return typeof(BotOwner).GetMethod(methodName);
}
[PatchPrefix]
private static bool PatchPrefix(BotOwner __instance, BotGroupClass group)
{
IAIDetails selfToRemove = null;
foreach (var enemy in group.Enemies)
{
if (enemy.Key.Id == __instance.Id)
{
selfToRemove = enemy.Key;
break;
}
}
if (selfToRemove != null)
{
group.Enemies.Remove(selfToRemove);
}
return true;
}
}
}

View File

@ -0,0 +1,73 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
public class CheckAndAddEnemyPatch : ModulePatch
{
private static Type _targetType;
private static FieldInfo _sideField;
private static FieldInfo _enemiesField;
private static FieldInfo _spawnTypeField;
private static MethodInfo _addEnemy;
private readonly string _targetMethodName = "CheckAndAddEnemy";
public CheckAndAddEnemyPatch()
{
_targetType = PatchConstants.EftTypes.Single(IsTargetType);
_sideField = _targetType.GetField("Side");
_enemiesField = _targetType.GetField("Enemies");
_spawnTypeField = _targetType.GetField("wildSpawnType_0", BindingFlags.NonPublic | BindingFlags.Instance);
_addEnemy = _targetType.GetMethod("AddEnemy");
}
private bool IsTargetType(Type type)
{
if (type.GetMethod("AddEnemy") != null && type.GetMethod("AddEnemyGroupIfAllowed") != null)
{
return true;
}
return false;
}
protected override MethodBase GetTargetMethod()
{
return _targetType.GetMethod(_targetMethodName);
}
/// <summary>
/// CheckAndAddEnemy()
/// Goal: This patch lets bosses shoot back once a PMC has shot them
/// removes the !player.AIData.IsAI check
/// </summary>
[PatchPrefix]
private static bool PatchPrefix(object __instance, ref IAIDetails player, ref bool ignoreAI)
{
//var side = (EPlayerSide)_sideField.GetValue(__instance);
//var botType = (WildSpawnType)_spawnTypeField.GetValue(__instance);
if (!player.HealthController.IsAlive)
{
return false; // do nothing and skip
}
var enemies = (Dictionary<IAIDetails, BotSettingsClass>)_enemiesField.GetValue(__instance);
if (enemies.ContainsKey(player))
{
return false;// do nothing and skip
}
// Add enemy to list
//if (!enemies.ContainsKey(player) && (!playerIsAi || ignoreAI))
_addEnemy.Invoke(__instance, new IAIDetails[] { player });
return false;
}
}
}

View File

@ -0,0 +1,27 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.Common.Http;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
public class CoreDifficultyPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var methodName = "LoadCoreByString";
var flags = BindingFlags.Public | BindingFlags.Static;
return PatchConstants.EftTypes.Single(x => x.GetMethod(methodName, flags) != null)
.GetMethod(methodName, flags);
}
[PatchPrefix]
private static bool PatchPrefix(ref string __result)
{
__result = RequestHandler.GetJson("/singleplayer/settings/bot/difficulty/core/core");
return string.IsNullOrWhiteSpace(__result);
}
}
}

View File

@ -0,0 +1,148 @@
using Aki.Common.Http;
using Aki.Reflection.Patching;
using Comfort.Common;
using EFT;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Aki.PrePatch;
using Random = System.Random;
namespace Aki.Custom.Patches
{
public class CustomAiPatch : ModulePatch
{
private static readonly Random random = new Random();
private static Dictionary<WildSpawnType, Dictionary<string, Dictionary<string, int>>> botTypeCache = new Dictionary<WildSpawnType, Dictionary<string, Dictionary<string, int>>>();
private static DateTime cacheDate = new DateTime();
protected override MethodBase GetTargetMethod()
{
return typeof(BotBrainClass).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance);
}
/// <summary>
/// Get a randomly picked wildspawntype from server and change PMC bot to use it, this ensures the bot is generated with that random type altering its behaviour
/// </summary>
/// <param name="__state">state to save for postfix to use later</param>
/// <param name="__instance"></param>
/// <param name="___botOwner_0">botOwner_0 property</param>
[PatchPrefix]
private static bool PatchPrefix(out WildSpawnType __state, object __instance, BotOwner ___botOwner_0)
{
// Store original type in state param
__state = ___botOwner_0.Profile.Info.Settings.Role;
//Console.WriteLine($"Processing bot {___botOwner_0.Profile.Info.Nickname} with role {___botOwner_0.Profile.Info.Settings.Role}");
try
{
if (BotIsSptPmc(___botOwner_0.Profile.Info.Settings.Role))
{
string currentMapName = GetCurrentMap();
if (!botTypeCache.TryGetValue(___botOwner_0.Profile.Info.Settings.Role, out var botSettings) || CacheIsStale())
{
ResetCacheDate();
HydrateCacheWithServerData();
if (!botTypeCache.TryGetValue(___botOwner_0.Profile.Info.Settings.Role, out botSettings))
{
throw new Exception($"Bots were refreshed from the server but the cache still doesnt contain an appropriate bot for type {___botOwner_0.Profile.Info.Settings.Role}");
}
}
var mapSettings = botSettings[currentMapName.ToLower()];
var randomType = WeightedRandom(mapSettings.Keys.ToArray(), mapSettings.Values.ToArray());
if (Enum.TryParse(randomType, out WildSpawnType newAiType))
{
Console.WriteLine($"Updated spt bot {___botOwner_0.Profile.Info.Nickname}: {___botOwner_0.Profile.Info.Settings.Role} to {newAiType}");
___botOwner_0.Profile.Info.Settings.Role = newAiType;
}
else
{
Console.WriteLine($"Couldnt not update spt bot {___botOwner_0.Profile.Info.Nickname} to the new type, random type {randomType} does not exist for WildSpawnType");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error processing log: {ex.Message}");
Console.WriteLine(ex.StackTrace);
}
return true; // Do original
}
/// <summary>
/// Revert prefix change, get bots type back to what it was before changes
/// </summary>
/// <param name="__state">Saved state from prefix patch</param>
/// <param name="___botOwner_0">botOwner_0 property</param>
[PatchPostfix]
private static void PatchPostFix(WildSpawnType __state, BotOwner ___botOwner_0)
{
if (BotIsSptPmc(__state))
{
// Set spt bot bot back to original type
___botOwner_0.Profile.Info.Settings.Role = __state;
}
}
private static bool BotIsSptPmc(WildSpawnType role)
{
return (long)role == -2147483648 || (long)role == 0;
}
private static string GetCurrentMap()
{
var gameWorld = Singleton<GameWorld>.Instance;
return gameWorld.RegisteredPlayers[0].Location;
}
private static bool CacheIsStale()
{
TimeSpan cacheAge = DateTime.Now - cacheDate;
return cacheAge.Minutes > 20;
}
private static void ResetCacheDate()
{
cacheDate = DateTime.Now;
}
private static void HydrateCacheWithServerData()
{
// Get weightings for PMCs from server and store in dict
var result = RequestHandler.GetJson($"/singleplayer/settings/bot/getBotBehaviours/");
botTypeCache = JsonConvert.DeserializeObject<Dictionary<WildSpawnType, Dictionary<string, Dictionary<string, int>>>>(result);
Console.WriteLine($"cached: {botTypeCache.Count} bots");
}
private static string WeightedRandom(string[] botTypes, int[] weights)
{
var cumulativeWeights = new int[botTypes.Length];
for (int i = 0; i < weights.Length; i++)
{
cumulativeWeights[i] = weights[i] + (i == 0 ? 0 : cumulativeWeights[i - 1]);
}
var maxCumulativeWeight = cumulativeWeights[cumulativeWeights.Length - 1];
var randomNumber = maxCumulativeWeight * random.NextDouble();
for (var itemIndex = 0; itemIndex < botTypes.Length; itemIndex++)
{
if (cumulativeWeights[itemIndex] >= randomNumber)
{
return botTypes[itemIndex];
}
}
Console.WriteLine("failed to get random bot weighting, returned assault");
return "assault";
}
}
}

View File

@ -0,0 +1,137 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Diz.Jobs;
using Diz.Resources;
using JetBrains.Annotations;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Build.Pipeline;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Aki.Custom.Models;
using Aki.Custom.Utils;
using DependencyGraph = DependencyGraph<IEasyBundle>;
namespace Aki.Custom.Patches
{
public class EasyAssetsPatch : ModulePatch
{
private static readonly FieldInfo _manifestField;
private static readonly FieldInfo _bundlesField;
private static readonly PropertyInfo _systemProperty;
static EasyAssetsPatch()
{
var type = typeof(EasyAssets);
_manifestField = type.GetField(nameof(EasyAssets.Manifest));
_bundlesField = type.GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags);
_systemProperty = type.GetProperty("System");
}
public EasyAssetsPatch()
{
_ = nameof(IEasyBundle.SameNameAsset);
_ = nameof(IBundleLock.IsLocked);
_ = nameof(BundleLock.MaxConcurrentOperations);
_ = nameof(DependencyGraph.GetDefaultNode);
}
protected override MethodBase GetTargetMethod()
{
return typeof(EasyAssets).GetMethods(PatchConstants.PrivateFlags).Single(IsTargetMethod);
}
private static bool IsTargetMethod(MethodInfo mi)
{
var parameters = mi.GetParameters();
return (parameters.Length == 6
&& parameters[0].Name == "bundleLock"
&& parameters[1].Name == "defaultKey"
&& parameters[4].Name == "shouldExclude");
}
[PatchPrefix]
private static bool PatchPrefix(ref Task __result, EasyAssets __instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath,
string platformName, [CanBeNull] Func<string, bool> shouldExclude, [CanBeNull] Func<string, Task> bundleCheck)
{
__result = Init(__instance, bundleLock, defaultKey, rootPath, platformName, shouldExclude, bundleCheck);
return false;
}
public static string GetPairKey(KeyValuePair<string, BundleItem> x)
{
return x.Key;
}
public static BundleDetails GetPairValue(KeyValuePair<string, BundleItem> x)
{
return new BundleDetails
{
FileName = x.Value.FileName,
Crc = x.Value.Crc,
Dependencies = x.Value.Dependencies
};
}
private static async Task<CompatibilityAssetBundleManifest> GetManifestBundle(string filepath)
{
var manifestLoading = AssetBundle.LoadFromFileAsync(filepath);
await manifestLoading.Await();
var assetBundle = manifestLoading.assetBundle;
var assetLoading = assetBundle.LoadAllAssetsAsync();
await assetLoading.Await();
return (CompatibilityAssetBundleManifest)assetLoading.allAssets[0];
}
private static async Task<CompatibilityAssetBundleManifest> GetManifestJson(string filepath)
{
var text = string.Empty;
using (var reader = File.OpenText($"{filepath}.json"))
{
text = await reader.ReadToEndAsync();
}
var data = JsonConvert.DeserializeObject<Dictionary<string, BundleItem>>(text).ToDictionary(GetPairKey, GetPairValue);
var manifest = ScriptableObject.CreateInstance<CompatibilityAssetBundleManifest>();
manifest.SetResults(data);
return manifest;
}
private static async Task Init(EasyAssets instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath,
string platformName, [CanBeNull] Func<string, bool> shouldExclude, Func<string, Task> bundleCheck)
{
// platform manifest
var path = $"{rootPath.Replace("file:///", string.Empty).Replace("file://", string.Empty)}/{platformName}/";
var filepath = path + platformName;
var manifest = (File.Exists(filepath)) ? await GetManifestBundle(filepath) : await GetManifestJson(filepath);
// load bundles
var bundleNames = manifest.GetAllAssetBundles().Union(BundleManager.Bundles.Keys).ToArray();
var bundles = (IEasyBundle[])Array.CreateInstance(EasyBundleHelper.Type, bundleNames.Length);
if (bundleLock == null)
{
bundleLock = new BundleLock(int.MaxValue);
}
for (var i = 0; i < bundleNames.Length; i++)
{
bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[] { bundleNames[i], path, manifest, bundleLock, bundleCheck });
await JobScheduler.Yield(EJobPriority.Immediate);
}
_manifestField.SetValue(instance, manifest);
_bundlesField.SetValue(instance, bundles);
_systemProperty.SetValue(instance, new DependencyGraph(bundles, defaultKey, shouldExclude));
}
}
}

View File

@ -0,0 +1,49 @@
using Aki.Reflection.Patching;
using Diz.DependencyManager;
using UnityEngine.Build.Pipeline;
using System.IO;
using System.Linq;
using System.Reflection;
using Aki.Custom.Models;
using Aki.Custom.Utils;
namespace Aki.Custom.Patches
{
public class EasyBundlePatch : ModulePatch
{
static EasyBundlePatch()
{
_ = nameof(IEasyBundle.SameNameAsset);
_ = nameof(IBundleLock.IsLocked);
_ = nameof(BindableState<ELoadState>.Bind);
}
protected override MethodBase GetTargetMethod()
{
return EasyBundleHelper.Type.GetConstructors()[0];
}
[PatchPostfix]
private static void PatchPostfix(object __instance, string key, string rootPath, CompatibilityAssetBundleManifest manifest, IBundleLock bundleLock)
{
var path = rootPath + key;
var dependencyKeys = manifest.GetDirectDependencies(key) ?? new string[0];
if (BundleManager.Bundles.TryGetValue(key, out BundleInfo bundle))
{
dependencyKeys = (dependencyKeys.Length > 0) ? dependencyKeys.Union(bundle.DependencyKeys).ToArray() : bundle.DependencyKeys;
path = bundle.Path;
}
_ = new EasyBundleHelper(__instance)
{
Key = key,
Path = path,
KeyWithoutExtension = Path.GetFileNameWithoutExtension(key),
DependencyKeys = dependencyKeys,
LoadState = new BindableState<ELoadState>(ELoadState.Unloaded, null),
BundleLock = bundleLock
};
}
}
}

View File

@ -0,0 +1,42 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Comfort.Common;
using EFT;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
/// <summary>
/// Fix a bsg bug that causes the game to soft-lock when you have a container opened when extracting
/// </summary>
public class ExitWhileLootingPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return PatchConstants.EftTypes.Single(x => x.Name == "LocalGame").BaseType // BaseLocalGame
.GetMethod("Stop", BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance);
}
// Look at BaseLocalGame<TPlayerOwner> and find a method named "Stop"
// once you find it, there should be a StartBlackScreenShow method with
// a callback method (on dnspy will be called @class.method_0)
// Go into that method. This will be part of the code:
// if (GClass2505.CheckCurrentScreen(EScreenType.Reconnect))
// {
// GClass2505.CloseAllScreensForced();
// }
// The code INSIDE the if needs to run
[PatchPrefix]
private static bool PatchPrefix(string profileId)
{
var player = Singleton<GameWorld>.Instance.MainPlayer;
if (profileId == player?.Profile.Id)
{
GClass2727.Instance.CloseAllScreensForced();
}
return true;
}
}
}

View File

@ -0,0 +1,121 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
public class IsEnemyPatch : ModulePatch
{
private static Type _targetType;
private readonly string _targetMethodName = "IsEnemy";
public IsEnemyPatch()
{
_targetType = PatchConstants.EftTypes.Single(IsTargetType);
}
private bool IsTargetType(Type type)
{
if (type.GetMethod("AddEnemy") != null && type.GetMethod("AddEnemyGroupIfAllowed") != null)
{
return true;
}
return false;
}
protected override MethodBase GetTargetMethod()
{
return _targetType.GetMethod(_targetMethodName);
}
/// <summary>
/// IsEnemy()
/// Goal: Make bots take Side into account when deciding if another player/bot is an enemy
/// Check enemy cache list first, if not found, check side, if they differ, add to enemy list and return true
/// Needed to ensure bot checks the enemy side, not just its botType
/// </summary>
[PatchPrefix]
private static bool PatchPrefix(ref bool __result, BotGroupClass __instance, IAIDetails requester)
{
var isEnemy = false; // default not an enemy
// Check existing enemies list
if (__instance.Enemies.Any(x=> x.Value.Player.Id == requester.Id))
{
isEnemy = true;
}
else
{
if (__instance.Side == EPlayerSide.Usec)
{
if (requester.Side == EPlayerSide.Bear || requester.Side == EPlayerSide.Savage ||
ShouldAttackUsec(requester))
{
isEnemy = true;
__instance.AddEnemy(requester);
}
}
else if (__instance.Side == EPlayerSide.Bear)
{
if (requester.Side == EPlayerSide.Usec || requester.Side == EPlayerSide.Savage ||
ShouldAttackBear(requester))
{
isEnemy = true;
__instance.AddEnemy(requester);
}
}
else if (__instance.Side == EPlayerSide.Savage)
{
if (requester.Side != EPlayerSide.Savage)
{
// everyone else is an enemy to savage (scavs)
isEnemy = true;
__instance.AddEnemy(requester);
}
}
}
__result = isEnemy;
return true; // Skip original
}
/// <summary>
/// Return True when usec default behavior is attack + bot is usec
/// </summary>
/// <param name="requester"></param>
/// <returns>bool</returns>
private static bool ShouldAttackUsec(IAIDetails requester)
{
var requesterMind = requester?.AIData?.BotOwner?.Settings?.FileSettings?.Mind;
if (requesterMind == null)
{
return false;
}
return requester.IsAI && requesterMind.DEFAULT_USEC_BEHAVIOUR == EWarnBehaviour.Attack && requester.Side == EPlayerSide.Usec;
}
/// <summary>
/// Return True when bear default behavior is attack + bot is bear
/// </summary>
/// <param name="requester"></param>
/// <returns></returns>
private static bool ShouldAttackBear(IAIDetails requester)
{
var requesterMind = requester.AIData?.BotOwner?.Settings?.FileSettings?.Mind;
if (requesterMind == null)
{
return false;
}
return requester.IsAI && requesterMind.DEFAULT_BEAR_BEHAVIOUR == EWarnBehaviour.Attack && requester.Side == EPlayerSide.Bear;
}
}
}

View File

@ -0,0 +1,69 @@
using Aki.Common.Http;
using Aki.Common.Utils;
using Aki.Reflection.Patching;
using Aki.Custom.Models;
using EFT.UI;
using EFT.UI.Matchmaker;
using System.Reflection;
using UnityEngine;
using EFT;
using static EFT.UI.Matchmaker.MatchmakerOfflineRaidScreen;
namespace Aki.Custom.Patches
{
public class OfflineRaidMenuPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var desiredType = typeof(MatchmakerOfflineRaidScreen);
var desiredMethod = desiredType.GetMethod(nameof(MatchmakerOfflineRaidScreen.Show));
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
[PatchPrefix]
private static void PatchPrefix(GClass2769 controller, UpdatableToggle ____offlineModeToggle)
{
var raidSettings = controller.RaidSettings;
raidSettings.RaidMode = ERaidMode.Local;
raidSettings.BotSettings.IsEnabled = true;
// Default checkbox to be ticked
____offlineModeToggle.isOn = true;
// get settings from server
var json = RequestHandler.GetJson("/singleplayer/settings/raid/menu");
var settings = Json.Deserialize<DefaultRaidSettings>(json);
// TODO: Not all settings are used and they also don't cover all the new settings that are available client-side
if (settings != null)
{
raidSettings.BotSettings.BotAmount = settings.AiAmount;
raidSettings.WavesSettings.BotAmount = settings.AiAmount;
raidSettings.WavesSettings.BotDifficulty = settings.AiDifficulty;
raidSettings.WavesSettings.IsBosses = settings.BossEnabled;
raidSettings.BotSettings.IsScavWars = false;
raidSettings.WavesSettings.IsTaggedAndCursed = settings.TaggedAndCursed;
}
}
[PatchPostfix]
private static void PatchPostfix()
{
// disable "no progression save" panel
var offlineRaidScreenContent = GameObject.Find("Matchmaker Offline Raid Screen").transform.Find("Content").transform;
var warningPanel = offlineRaidScreenContent.Find("WarningPanelHorLayout");
warningPanel.gameObject.SetActive(false);
var spacer = offlineRaidScreenContent.Find("Space (1)");
spacer.gameObject.SetActive(false);
}
}
}

View File

@ -0,0 +1,25 @@
using Aki.Common.Http;
using Aki.Reflection.Patching;
using System.Reflection;
using EFT;
using Aki.Reflection.Utils;
using System.Linq;
namespace Aki.Custom.Patches
{
public class QTEPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() => typeof(HideoutPlayerOwner).GetMethod(nameof(HideoutPlayerOwner.StopWorkout));
[PatchPostfix]
private static void PatchPostfix(HideoutPlayerOwner __instance)
{
RequestHandler.PutJson("/client/hideout/workout", new
{
skills = __instance.HideoutPlayer.Skills,
effects = __instance.HideoutPlayer.HealthController.BodyPartEffects
}
.ToJson());
}
}
}

View File

@ -0,0 +1,39 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT.UI;
using System.IO;
using System.Reflection;
using UnityEngine;
namespace Aki.Custom.Patches
{
public class SessionIdPatch : ModulePatch
{
private static PreloaderUI _preloader;
static SessionIdPatch()
{
_preloader = null;
}
protected override MethodBase GetTargetMethod()
{
return PatchConstants.LocalGameType.BaseType.GetMethod("method_5", PatchConstants.PrivateFlags);
}
[PatchPostfix]
private static void PatchPostfix()
{
if (_preloader == null)
{
_preloader = Object.FindObjectOfType<PreloaderUI>();
}
if (_preloader != null)
{
var raidID = Path.GetRandomFileName().Replace(".", string.Empty).Substring(0, 6).ToUpperInvariant();
_preloader.SetSessionId(raidID);
}
}
}
}

View File

@ -0,0 +1,48 @@
using Aki.Common.Http;
using Aki.Common.Utils;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.Custom.Models;
using EFT.UI;
using HarmonyLib;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
public class VersionLabelPatch : ModulePatch
{
private static string _versionLabel;
protected override MethodBase GetTargetMethod()
{
try
{
return PatchConstants.EftTypes
.Single(x => x.GetField("Taxonomy", BindingFlags.Public | BindingFlags.Instance) != null)
.GetMethod("Create", BindingFlags.Public | BindingFlags.Static);
}
catch (System.Exception e)
{
Logger.LogInfo($"VersionLabelPatch failed {e.Message} {e.StackTrace} {e.InnerException.StackTrace}");
throw;
}
}
[PatchPostfix]
internal static void PatchPostfix(object __result)
{
if (string.IsNullOrEmpty(_versionLabel))
{
var json = RequestHandler.GetJson("/singleplayer/settings/version");
_versionLabel = Json.Deserialize<VersionResponse>(json).Version;
Logger.LogInfo($"Server version: {_versionLabel}");
}
Traverse.Create(MonoBehaviourSingleton<PreloaderUI>.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}");
Traverse.Create(MonoBehaviourSingleton<PreloaderUI>.Instance).Field("string_2").SetValue(_versionLabel);
Traverse.Create(__result).Field("Major").SetValue(_versionLabel);
}
}
}

View File

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Aki.Common.Http;
using Aki.Common.Utils;
using Aki.Custom.Models;
using Newtonsoft.Json.Linq;
namespace Aki.Custom.Utils
{
public static class BundleManager
{
public const string CachePath = "user/cache/bundles/";
public static Dictionary<string, BundleInfo> Bundles { get; private set; }
static BundleManager()
{
Bundles = new Dictionary<string, BundleInfo>();
if (VFS.Exists(CachePath))
{
VFS.DeleteDirectory(CachePath);
}
}
public static void GetBundles()
{
var json = RequestHandler.GetJson("/singleplayer/bundles");
var jArray = JArray.Parse(json);
foreach (var jObj in jArray)
{
var key = jObj["key"].ToString();
var path = jObj["path"].ToString();
var bundle = new BundleInfo(key, path, jObj["dependencyKeys"].ToObject<string[]>());
if (path.Contains("http"))
{
var filepath = CachePath + Regex.Split(path, "bundle/", RegexOptions.IgnoreCase)[1];
var data = RequestHandler.GetData(path, true);
VFS.WriteFile(filepath, data);
bundle.Path = filepath;
}
Bundles.Add(key, bundle);
}
VFS.WriteTextFile(CachePath + "bundles.json", Json.Serialize<Dictionary<string, BundleInfo>>(Bundles));
}
}
}

View File

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Aki.Reflection.Utils;
using BindableState = BindableState<Diz.DependencyManager.ELoadState>;
namespace Aki.Custom.Utils
{
public class EasyBundleHelper
{
private const BindingFlags _flags = BindingFlags.Instance | BindingFlags.NonPublic;
private static readonly FieldInfo _pathField;
private static readonly FieldInfo _keyWithoutExtensionField;
private static readonly FieldInfo _bundleLockField;
private static readonly PropertyInfo _dependencyKeysProperty;
private static readonly PropertyInfo _keyProperty;
private static readonly PropertyInfo _loadStateProperty;
private static readonly MethodInfo _loadingCoroutineMethod;
private readonly object _instance;
public static readonly Type Type;
static EasyBundleHelper()
{
_ = nameof(IBundleLock.IsLocked);
_ = nameof(BindableState.Bind);
Type = PatchConstants.EftTypes.Single(x => x.GetMethod("set_SameNameAsset", _flags) != null);
_pathField = Type.GetField("string_1", _flags);
_keyWithoutExtensionField = Type.GetField("string_0", _flags);
_bundleLockField = Type.GetFields(_flags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock));
_dependencyKeysProperty = Type.GetProperty("DependencyKeys");
_keyProperty = Type.GetProperty("Key");
_loadStateProperty = Type.GetProperty("LoadState");
_loadingCoroutineMethod = Type.GetMethods(_flags).Single(x => x.GetParameters().Length == 0 && x.ReturnType == typeof(Task));
}
public EasyBundleHelper(object easyBundle)
{
_instance = easyBundle;
}
public IEnumerable<string> DependencyKeys
{
get
{
return (IEnumerable<string>)_dependencyKeysProperty.GetValue(_instance);
}
set
{
_dependencyKeysProperty.SetValue(_instance, value);
}
}
public IBundleLock BundleLock
{
get
{
return (IBundleLock)_bundleLockField.GetValue(_instance);
}
set
{
_bundleLockField.SetValue(_instance, value);
}
}
public string Path
{
get
{
return (string)_pathField.GetValue(_instance);
}
set
{
_pathField.SetValue(_instance, value);
}
}
public string Key
{
get
{
return (string)_keyProperty.GetValue(_instance);
}
set
{
_keyProperty.SetValue(_instance, value);
}
}
public BindableState LoadState
{
get
{
return (BindableState)_loadStateProperty.GetValue(_instance);
}
set
{
_loadStateProperty.SetValue(_instance, value);
}
}
public string KeyWithoutExtension
{
get
{
return (string)_keyWithoutExtensionField.GetValue(_instance);
}
set
{
_keyWithoutExtensionField.SetValue(_instance, value);
}
}
public Task LoadingCoroutine()
{
return (Task)_loadingCoroutineMethod.Invoke(_instance, new object[] { });
}
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>aki-debugging</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\Assembly-CSharp.dll" Private="False" />
<Reference Include="Unity.TextMeshPro" HintPath="..\Shared\Managed\Unity.TextMeshPro.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
<Reference Include="UnityEngine.CoreModule" HintPath="..\Shared\Managed\UnityEngine.CoreModule.dll" Private="False" />
<Reference Include="UnityEngine.InputLegacyModule" HintPath="..\Shared\Managed\UnityEngine.InputLegacyModule.dll" Private="False" />
<Reference Include="UnityEngine.PhysicsModule" HintPath="..\Shared\Managed\UnityEngine.PhysicsModule.dll" Private="False" />
<Reference Include="UnityEngine.UI" HintPath="..\Shared\Managed\UnityEngine.UI.dll" Private="False" />
<Reference Include="UnityEngine.UIModule" HintPath="..\Shared\Managed\UnityEngine.UIModule.dll" Private="False" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
using System;
using Aki.Debugging.Patches;
using BepInEx;
namespace Aki.Debugging
{
[BepInPlugin("com.spt-aki.debugging", "AKI.Debugging", "1.0.0")]
public class AkiDebuggingPlugin : BaseUnityPlugin
{
public AkiDebuggingPlugin()
{
Logger.LogInfo("Loading: Aki.Debugging");
try
{
// new CoordinatesPatch().Enable();
}
catch (Exception ex)
{
Logger.LogError($"{GetType().Name}: {ex}");
throw;
}
Logger.LogInfo("Completed: Aki.Debugging");
}
}
}

View File

@ -0,0 +1,70 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using TMPro;
using UnityEngine;
using System;
using System.Reflection;
namespace Aki.Debugging.Patches
{
public class CoordinatesPatch : ModulePatch
{
private static TextMeshProUGUI _alphaLabel;
private static PropertyInfo _playerProperty;
protected override MethodBase GetTargetMethod()
{
var localGameBaseType = PatchConstants.LocalGameType.BaseType;
_playerProperty = localGameBaseType.GetProperty("PlayerOwner", BindingFlags.Public | BindingFlags.Instance);
return localGameBaseType.GetMethod("Update", PatchConstants.PrivateFlags);
}
[PatchPrefix]
private static void PatchPrefix(object __instance)
{
if (Input.GetKeyDown(KeyCode.LeftControl))
{
if (_alphaLabel == null)
{
_alphaLabel = GameObject.Find("AlphaLabel").GetComponent<TextMeshProUGUI>();
_alphaLabel.color = Color.green;
_alphaLabel.fontSize = 22;
_alphaLabel.font = Resources.Load<TMP_FontAsset>("Fonts & Materials/ARIAL SDF");
}
var playerOwner = (GamePlayerOwner)_playerProperty.GetValue(__instance);
var aiming = LookingRaycast(playerOwner.Player);
if (_alphaLabel != null)
{
_alphaLabel.text = $"Looking at: [{aiming.x}, {aiming.y}, {aiming.z}]";
Logger.LogInfo(_alphaLabel.text);
}
var position = playerOwner.transform.position;
var rotation = playerOwner.transform.rotation.eulerAngles;
Logger.LogInfo($"Character position: [{position.x},{position.y},{position.z}] | Rotation: [{rotation.x},{rotation.y},{rotation.z}]");
}
}
public static Vector3 LookingRaycast(Player player)
{
try
{
if (player == null || player.Fireport == null)
{
return Vector3.zero;
}
Physics.Linecast(player.Fireport.position, player.Fireport.position - player.Fireport.up * 1000f, out var raycastHit, 331776);
return raycastHit.point;
}
catch (Exception e)
{
Logger.LogError($"Coordinate Debug raycast failed: {e.Message}");
return Vector3.zero;
}
}
}
}

View File

@ -0,0 +1,30 @@

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>aki_PrePatch</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\Assembly-CSharp.dll" Private="False" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Mono.Cecil;
namespace Aki.PrePatch
{
public static class AkiBotsPrePatcher
{
public static IEnumerable<string> TargetDLLs { get; } = new[] { "Assembly-CSharp.dll" };
public static long sptUsecValue = 0x80000000;
public static long sptBearValue = 0x100000000;
public static void Patch(ref AssemblyDefinition assembly)
{
var botEnums = assembly.MainModule.GetType("EFT.WildSpawnType");
var sptUsec = new FieldDefinition("sptUsec",
FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault,
botEnums)
{ Constant = sptUsecValue };
var sptBear = new FieldDefinition("sptBear",
FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault,
botEnums)
{ Constant = sptBearValue };
botEnums.Fields.Add(sptUsec);
botEnums.Fields.Add(sptBear);
}
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\Assembly-CSharp.dll" Private="False" />
<Reference Include="bsg.componentace.compression.libs.zlib" HintPath="..\Shared\Managed\bsg.componentace.compression.libs.zlib.dll" Private="False" />
<Reference Include="Comfort" HintPath="..\Shared\Managed\Comfort.dll" Private="False" />
<Reference Include="FilesChecker" HintPath="..\Shared\Managed\FilesChecker.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
<Reference Include="UnityEngine.CoreModule" HintPath="..\Shared\Managed\UnityEngine.CoreModule.dll" Private="False" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.0.8" PrivateAssets="All" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
<PackageReference Include="BepInEx.Core" Version="5.4.21" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.1.0" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,48 @@
using System;
using System.Reflection.Emit;
namespace Aki.Reflection.CodeWrapper
{
public class Code
{
public OpCode OpCode { get; }
public Type CallerType { get; }
public object OperandTarget { get; }
public Type[] Parameters { get; }
public bool HasOperand { get; }
public Code(OpCode opCode)
{
OpCode = opCode;
HasOperand = false;
}
public Code(OpCode opCode, object operandTarget)
{
OpCode = opCode;
OperandTarget = operandTarget;
HasOperand = true;
}
public Code(OpCode opCode, Type callerType)
{
OpCode = opCode;
CallerType = callerType;
HasOperand = true;
}
public Code(OpCode opCode, Type callerType, object operandTarget, Type[] parameters = null)
{
OpCode = opCode;
CallerType = callerType;
OperandTarget = operandTarget;
Parameters = parameters;
HasOperand = true;
}
public virtual Label? GetLabel()
{
return null;
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using HarmonyLib;
namespace Aki.Reflection.CodeWrapper
{
/// <summary>
/// Helper class to generate IL code for transpilers
/// </summary>
public class CodeGenerator
{
public static List<CodeInstruction> GenerateInstructions(List<Code> codes)
{
var list = new List<CodeInstruction>();
foreach (Code code in codes)
{
list.Add(ParseCode(code));
}
return list;
}
private static CodeInstruction ParseCode(Code code)
{
if (!code.HasOperand)
{
return new CodeInstruction(code.OpCode) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Ldfld || code.OpCode == OpCodes.Stfld)
{
return new CodeInstruction(code.OpCode, AccessTools.Field(code.CallerType, code.OperandTarget as string)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Call || code.OpCode == OpCodes.Callvirt)
{
return new CodeInstruction(code.OpCode, AccessTools.Method(code.CallerType, code.OperandTarget as string, code.Parameters)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Box)
{
return new CodeInstruction(code.OpCode, code.CallerType) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Br || code.OpCode == OpCodes.Brfalse || code.OpCode == OpCodes.Brtrue || code.OpCode == OpCodes.Brtrue_S
|| code.OpCode == OpCodes.Brfalse_S || code.OpCode == OpCodes.Br_S)
{
return new CodeInstruction(code.OpCode, code.OperandTarget) { labels = GetLabelList(code) };
}
throw new ArgumentException($"Code with OpCode {nameof(code.OpCode)} is not supported.");
}
private static List<Label> GetLabelList(Code code)
{
if (code.GetLabel() == null)
{
return new List<Label>();
}
return new List<Label>() { (Label)code.GetLabel() };
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Reflection.Emit;
namespace Aki.Reflection.CodeWrapper
{
public class CodeWithLabel : Code
{
public Label Label { get; }
public CodeWithLabel(OpCode opCode, Label label) : base(opCode)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, object operandTarget) : base(opCode, operandTarget)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, Type callerType) : base(opCode, callerType)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, Type callerType, object operandTarget, Type[] parameters = null) : base(opCode, callerType, operandTarget, parameters)
{
Label = label;
}
public override Label? GetLabel()
{
return Label;
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using JetBrains.Annotations;
namespace Aki.Reflection.Patching
{
[MeansImplicitUse]
[AttributeUsage(AttributeTargets.Method)]
public class PatchPrefixAttribute : Attribute
{
}
[MeansImplicitUse]
[AttributeUsage(AttributeTargets.Method)]
public class PatchPostfixAttribute : Attribute
{
}
[MeansImplicitUse]
[AttributeUsage(AttributeTargets.Method)]
public class PatchTranspilerAttribute : Attribute
{
}
[MeansImplicitUse]
[AttributeUsage(AttributeTargets.Method)]
public class PatchFinalizerAttribute : Attribute
{
}
[MeansImplicitUse]
[AttributeUsage(AttributeTargets.Method)]
public class PatchILManipulatorAttribute : Attribute
{
}
}

View File

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using BepInEx.Logging;
using HarmonyLib;
namespace Aki.Reflection.Patching
{
public abstract class ModulePatch
{
private readonly Harmony _harmony;
private readonly List<HarmonyMethod> _prefixList;
private readonly List<HarmonyMethod> _postfixList;
private readonly List<HarmonyMethod> _transpilerList;
private readonly List<HarmonyMethod> _finalizerList;
private readonly List<HarmonyMethod> _ilmanipulatorList;
protected static ManualLogSource Logger { get; private set; }
protected ModulePatch() : this(null)
{
if (Logger == null)
{
Logger = BepInEx.Logging.Logger.CreateLogSource(nameof(ModulePatch));
}
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="name">Name</param>
protected ModulePatch(string name = null)
{
_harmony = new Harmony(name ?? GetType().Name);
_prefixList = GetPatchMethods(typeof(PatchPrefixAttribute));
_postfixList = GetPatchMethods(typeof(PatchPostfixAttribute));
_transpilerList = GetPatchMethods(typeof(PatchTranspilerAttribute));
_finalizerList = GetPatchMethods(typeof(PatchFinalizerAttribute));
_ilmanipulatorList = GetPatchMethods(typeof(PatchILManipulatorAttribute));
if (_prefixList.Count == 0
&& _postfixList.Count == 0
&& _transpilerList.Count == 0
&& _finalizerList.Count == 0
&& _ilmanipulatorList.Count == 0)
{
throw new Exception($"{_harmony.Id}: At least one of the patch methods must be specified");
}
}
/// <summary>
/// Get original method
/// </summary>
/// <returns>Method</returns>
protected abstract MethodBase GetTargetMethod();
/// <summary>
/// Get HarmonyMethod from string
/// </summary>
/// <param name="attributeType">Attribute type</param>
/// <returns>Method</returns>
private List<HarmonyMethod> GetPatchMethods(Type attributeType)
{
var T = GetType();
var methods = new List<HarmonyMethod>();
foreach (var method in T.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
if (method.GetCustomAttribute(attributeType) != null)
{
methods.Add(new HarmonyMethod(method));
}
}
return methods;
}
/// <summary>
/// Apply patch to target
/// </summary>
public void Enable()
{
var target = GetTargetMethod();
if (target == null)
{
throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null");
}
try
{
foreach (var prefix in _prefixList)
{
_harmony.Patch(target, prefix: prefix);
}
foreach (var postfix in _postfixList)
{
_harmony.Patch(target, postfix: postfix);
}
foreach (var transpiler in _transpilerList)
{
_harmony.Patch(target, transpiler: transpiler);
}
foreach (var finalizer in _finalizerList)
{
_harmony.Patch(target, finalizer: finalizer);
}
foreach (var ilmanipulator in _ilmanipulatorList)
{
_harmony.Patch(target, ilmanipulator: ilmanipulator);
}
Logger.LogInfo($"Enabled patch {_harmony.Id}");
}
catch (Exception ex)
{
Logger.LogError($"{_harmony.Id}: {ex}");
throw new Exception($"{_harmony.Id}:", ex);
}
}
/// <summary>
/// Remove applied patch from target
/// </summary>
public void Disable()
{
var target = GetTargetMethod();
if (target == null)
{
throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null");
}
try
{
_harmony.Unpatch(target, HarmonyPatchType.All, _harmony.Id);
Logger.LogInfo($"Disabled patch {_harmony.Id}");
}
catch (Exception ex)
{
Logger.LogError($"{_harmony.Id}: {ex}");
throw new Exception($"{_harmony.Id}:", ex);
}
}
}
}

View File

@ -0,0 +1,18 @@
using Comfort.Common;
using EFT;
namespace Aki.Reflection.Utils
{
public static class ClientAppUtils
{
public static ClientApplication<ISession> GetClientApp()
{
return Singleton<ClientApplication<ISession>>.Instance;
}
public static TarkovApplication GetMainApp()
{
return GetClientApp() as TarkovApplication;
}
}
}

View File

@ -0,0 +1,28 @@
using UnityEngine;
namespace Aki.Reflection.Utils
{
public static class HookObject
{
public static GameObject _object
{
get
{
GameObject result = GameObject.Find("Aki.Hook");
if (result == null)
{
result = new GameObject("Aki.Hook");
Object.DontDestroyOnLoad(result);
}
return result;
}
}
public static T AddOrGetComponent<T>() where T : MonoBehaviour
{
return _object.GetOrAddComponent<T>();
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Reflection;
using Comfort.Common;
using EFT;
using FilesChecker;
namespace Aki.Reflection.Utils
{
public static class PatchConstants
{
public static BindingFlags PrivateFlags { get; private set; }
public static BindingFlags PublicFlags { get; private set; }
public static Type[] EftTypes { get; private set; }
public static Type[] FilesCheckerTypes { get; private set; }
public static Type LocalGameType { get; private set; }
public static Type ExfilPointManagerType { get; private set; }
public static Type SessionInterfaceType { get; private set; }
public static Type BackendSessionInterfaceType { get; private set; }
private static ISession _backEndSession;
public static ISession BackEndSession
{
get
{
if (_backEndSession == null)
{
_backEndSession = Singleton<ClientApplication<ISession>>.Instance.GetClientBackEndSession();
}
return _backEndSession;
}
}
static PatchConstants()
{
_ = nameof(ISession.GetPhpSessionId);
PrivateFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
PublicFlags = BindingFlags.Public | BindingFlags.Instance;
EftTypes = typeof(AbstractGame).Assembly.GetTypes();
FilesCheckerTypes = typeof(ICheckResult).Assembly.GetTypes();
LocalGameType = EftTypes.Single(x => x.Name == "LocalGame");
ExfilPointManagerType = EftTypes.Single(x => x.GetMethod("InitAllExfiltrationPoints") != null);
SessionInterfaceType = EftTypes.Single(x => x.GetMethods().Select(y => y.Name).Contains("GetPhpSessionId") && x.IsInterface);
BackendSessionInterfaceType = EftTypes.Single(x => x.GetMethods().Select(y => y.Name).Contains("ChangeProfileStatus") && x.IsInterface);
}
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>aki-singleplayer</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\Assembly-CSharp.dll" Private="False" />
<Reference Include="Comfort.Unity" HintPath="..\Shared\Managed\Comfort.Unity.dll" Private="False" />
<Reference Include="DissonanceVoip" HintPath="..\Shared\Managed\DissonanceVoip.dll" Private="False" />
<Reference Include="ItemComponent.Types" HintPath="..\Shared\Managed\ItemComponent.Types.dll" Private="False" />
<Reference Include="Comfort" HintPath="..\Shared\Managed\Comfort.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
<Reference Include="UnityEngine.CoreModule" HintPath="..\Shared\Managed\UnityEngine.CoreModule.dll" Private="False" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" ExcludeAssets="runtime" PrivateAssets="all">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,59 @@
using System;
using Aki.SinglePlayer.Patches.Healing;
using Aki.SinglePlayer.Patches.MainMenu;
using Aki.SinglePlayer.Patches.Progression;
using Aki.SinglePlayer.Patches.Quests;
using Aki.SinglePlayer.Patches.RaidFix;
using Aki.SinglePlayer.Patches.ScavMode;
using BepInEx;
namespace Aki.SinglePlayer
{
[BepInPlugin("com.spt-aki.singleplayer", "AKI.Singleplayer", "1.0.0")]
class AkiSingleplayerPlugin : BaseUnityPlugin
{
public AkiSingleplayerPlugin()
{
Logger.LogInfo("Loading: Aki.SinglePlayer");
try
{
new OfflineSaveProfilePatch().Enable();
new OfflineSpawnPointPatch().Enable();
new ExperienceGainPatch().Enable();
new MainMenuControllerPatch().Enable();
new PlayerPatch().Enable();
new SelectLocationScreenPatch().Enable();
new InsuranceScreenPatch().Enable();
new BotTemplateLimitPatch().Enable();
new GetNewBotTemplatesPatch().Enable();
new RemoveUsedBotProfilePatch().Enable();
new DogtagPatch().Enable();
new LoadOfflineRaidScreenPatch().Enable();
new ScavPrefabLoadPatch().Enable();
new ScavProfileLoadPatch().Enable();
new ScavExfilPatch().Enable();
new ExfilPointManagerPatch().Enable();
new TinnitusFixPatch().Enable();
new MaxBotPatch().Enable();
new SpawnPmcPatch().Enable();
new PostRaidHealingPricePatch().Enable();
new EndByTimerPatch().Enable();
new PostRaidHealScreenPatch().Enable();
new VoIPTogglerPatch().Enable();
new MidRaidQuestChangePatch().Enable();
new HealthControllerPatch().Enable();
new LighthouseBridgePatch().Enable();
new LighthouseTransmitterPatch().Enable();
new EmptyInfilFixPatch().Enable();
}
catch (Exception ex)
{
Logger.LogError($"{GetType().Name}: {ex}");
throw;
}
Logger.LogInfo("Completed: Aki.SinglePlayer");
}
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
namespace Aki.SinglePlayer.Models.Healing
{
public class BodyPartHealth
{
private readonly Dictionary<EBodyPartEffect, float> _effects = new Dictionary<EBodyPartEffect, float>();
public float Maximum { get; private set; }
public float Current { get; private set; }
public IReadOnlyDictionary<EBodyPartEffect, float> Effects => _effects;
public void Initialize(float current, float maximum)
{
Maximum = maximum;
Current = current;
}
public void ChangeHealth(float diff)
{
Current += diff;
}
public void AddEffect(EBodyPartEffect bodyPartEffect, float time = -1)
{
_effects[bodyPartEffect] = time;
}
public void UpdateEffect(EBodyPartEffect bodyPartEffect, float time)
{
_effects[bodyPartEffect] = time;
}
public void RemoveAllEffects()
{
_effects.Clear();
}
public void RemoveEffect(EBodyPartEffect bodyPartEffect)
{
if (_effects.ContainsKey(bodyPartEffect))
{
_effects.Remove(bodyPartEffect);
}
}
}
}

View File

@ -0,0 +1,12 @@
namespace Aki.SinglePlayer.Models.Healing
{
public enum EBodyPartEffect
{
Fracture,
LightBleeding,
HeavyBleeding,
MildMusclePain,
SevereMusclePain,
Unknown
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Aki.SinglePlayer.Models.Healing
{
public class PlayerHealth
{
private readonly Dictionary<EBodyPart, BodyPartHealth> _health;
public IReadOnlyDictionary<EBodyPart, BodyPartHealth> Health => _health;
public bool IsAlive { get; set; }
public float Hydration { get; set; }
public float Energy { get; set; }
public float Temperature { get; set; }
public PlayerHealth()
{
IsAlive = true;
_health = new Dictionary<EBodyPart, BodyPartHealth>()
{
{ EBodyPart.Head, new BodyPartHealth() },
{ EBodyPart.Chest, new BodyPartHealth() },
{ EBodyPart.Stomach, new BodyPartHealth() },
{ EBodyPart.LeftArm, new BodyPartHealth() },
{ EBodyPart.RightArm, new BodyPartHealth() },
{ EBodyPart.LeftLeg, new BodyPartHealth() },
{ EBodyPart.RightLeg, new BodyPartHealth() }
};
}
}
}

View File

@ -0,0 +1,144 @@
using Comfort.Common;
using EFT;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Aki.SinglePlayer.Models.Progression
{
public class LighthouseProgressionClass : MonoBehaviour
{
private bool _isScav;
private GameWorld _gameWorld;
private float _timer;
private bool _addedToEnemy;
private List<MineDirectionalColliders> _mines;
private RecodableItemClass _transmitter;
private List<Player> _bosses;
private bool _aggressor;
private bool _disabledDoor;
private readonly string _transmitterId = "62e910aaf957f2915e0a5e36";
public void Start()
{
_gameWorld = Singleton<GameWorld>.Instance;
_bosses = new List<Player>();
_mines = GameObject.FindObjectsOfType<MineDirectionalColliders>().ToList();
if (_gameWorld == null || _gameWorld.MainPlayer.Location.ToLower() != "lighthouse") return;
// if player is a scav, there is no need to continue this method.
if (_gameWorld.MainPlayer.Side == EPlayerSide.Savage)
{
_isScav = true;
return;
}
// Get the players Transmitter.
_transmitter = (RecodableItemClass) _gameWorld.MainPlayer.Profile.Inventory.AllRealPlayerItems.FirstOrDefault(x => x.TemplateId == _transmitterId);
if (_transmitter != null)
{
GameObject.Find("Attack").SetActive(false);
// this zone was added in a newer version and the gameObject actually has a \
GameObject.Find("CloseZone\\").SetActive(false);
// Give access to the Lightkeepers door.
_gameWorld.BufferZoneController.SetPlayerAccessStatus(_gameWorld.MainPlayer.ProfileId, true);
}
}
public void Update()
{
if (_gameWorld == null || _addedToEnemy || _disabledDoor || _transmitter == null) return;
_timer += Time.deltaTime;
if (_timer < 10f) return;
if (_bosses.Count == 0)
{
SetupBosses();
}
if (_isScav)
{
PlayerIsScav();
return;
}
if (_gameWorld?.MainPlayer?.HandsController?.Item?.TemplateId == _transmitterId)
{
if (_transmitter?.RecodableComponent?.Status == RadioTransmitterStatus.Green)
{
foreach (var mine in _mines)
{
if (mine.gameObject.activeSelf)
{
mine.gameObject.SetActive(false);
}
}
}
}
else
{
foreach (var mine in _mines)
{
if (!mine.gameObject.activeSelf)
{
mine.gameObject.SetActive(true);
}
}
}
if (_aggressor)
{
PlayerIsAggressor();
}
}
private void SetupBosses()
{
foreach (var player in _gameWorld.AllPlayers)
{
if (!player.IsYourPlayer)
{
if (player.AIData.BotOwner.IsRole(WildSpawnType.bossZryachiy) || player.AIData.BotOwner.IsRole(WildSpawnType.followerZryachiy))
{
// Sub to Bosses OnDeath event, Set mainplayer to aggressor on this script
player.OnPlayerDeadOrUnspawn += player1 =>
{
if (player1.KillerId != null && player1.KillerId == _gameWorld.MainPlayer.ProfileId)
{
_aggressor = true;
}
};
_bosses.Add(player);
}
}
}
}
private void PlayerIsScav()
{
// If player is a scav, they must be added to the bosses enemy list otherwise they wont kill them
foreach (var boss in _bosses)
{
boss.AIData.BotOwner.BotsGroup.AddEnemy(_gameWorld.MainPlayer);
}
_addedToEnemy = true;
}
private void PlayerIsAggressor()
{
// Disable access to Lightkeepers door for the player
_gameWorld.BufferZoneController.SetPlayerAccessStatus(_gameWorld.MainPlayer.ProfileId, false);
_transmitter?.RecodableComponent?.SetStatus(RadioTransmitterStatus.Yellow);
_transmitter?.RecodableComponent?.SetEncoded(false);
_disabledDoor = true;
}
}
}

View File

@ -0,0 +1,26 @@
using Aki.SinglePlayer.Models.Healing;
using Newtonsoft.Json;
using EFT;
namespace Aki.SinglePlayer.Models.Progression
{
public class SaveProfileRequest
{
[JsonProperty("exit")]
public string Exit;
[JsonProperty("profile")]
public Profile Profile;
[JsonProperty("isPlayerScav")]
public bool IsPlayerScav;
[JsonProperty("health")]
public PlayerHealth Health;
public SaveProfileRequest()
{
Exit = "left";
}
}
}

View File

@ -0,0 +1,40 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Comfort.Common;
using EFT;
namespace Aki.SinglePlayer.Models.RaidFix
{
public struct BundleLoader
{
Profile Profile;
TaskScheduler TaskScheduler { get; }
public BundleLoader(TaskScheduler taskScheduler)
{
Profile = null;
TaskScheduler = taskScheduler;
}
public Task<Profile> LoadBundles(Task<Profile> task)
{
Profile = task.Result;
var loadTask = Singleton<PoolManager>.Instance.LoadBundlesAndCreatePools(
PoolManager.PoolsCategory.Raid,
PoolManager.AssemblyType.Local,
Profile.GetAllPrefabPaths(false).ToArray(),
JobPriority.General,
null,
default(CancellationToken));
return loadTask.ContinueWith(GetProfile, TaskScheduler);
}
private Profile GetProfile(Task task)
{
return Profile;
}
}
}

View File

@ -0,0 +1,55 @@
using Aki.Reflection.Patching;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Healing
{
/// <summary>
/// HealthController used by post-raid heal screen and health listenen class are different, this patch
/// ensures effects (fracture/bleeding) on body parts stay in sync
/// </summary>
public class HealthControllerPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(HealthControllerClass).GetMethod("ApplyTreatment", BindingFlags.Public | BindingFlags.Instance);
}
[PatchPrefix]
private static void PatchPrefix(object healthObserver)
{
var property = healthObserver.GetType().GetProperty("Effects");
if (property != null)
{
var effects = property.GetValue(healthObserver);
if (effects != null && effects.GetType().GetGenericTypeDefinition() == typeof(List<>))
{
var parsedEffects = ((IList)effects).Cast<object>().ToList();
parsedEffects.ForEach(effect =>
{
var parsedEffect = effect as IEffect;
try
{
// I tried using reflections to raise the event, I also tried replacing the effect
// health controller using reflections but they are different types than the one the actual
// player class uses so it cant be replaced.
// Unfortunately, the easiest way to deal with this is just manually triggering the HealthListener method
Utils.Healing.HealthListener.Instance.OnEffectRemovedEvent(parsedEffect);
}
catch (Exception ex)
{
Logger.LogError($"Exception!\n{ex.Message}\n{ex.StackTrace}");
}
});
}
}
else
{
Logger.LogDebug("No effects found to heal on the observer!");
}
}
}
}

View File

@ -0,0 +1,49 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Healing
{
public class MainMenuControllerPatch : ModulePatch
{
static MainMenuControllerPatch()
{
_ = nameof(IHealthController.HydrationChangedEvent);
_ = nameof(MainMenuController.HealthController);
}
protected override MethodBase GetTargetMethod()
{
var desiredType = typeof(MainMenuController);
var desiredMethod = desiredType.GetMethod("method_1", PatchConstants.PrivateFlags);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
[PatchPostfix]
private static void PatchPostfix(MainMenuController __instance)
{
var healthController = __instance.HealthController;
var listener = Utils.Healing.HealthListener.Instance;
if (healthController == null)
{
Logger.LogInfo("MainMenuControllerPatch() - healthController is null");
}
if (listener == null)
{
Logger.LogInfo("MainMenuControllerPatch() - listener is null");
}
if (healthController != null && listener != null)
{
listener.Init(healthController, false);
}
}
}
}

View File

@ -0,0 +1,40 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System.Reflection;
using System.Threading.Tasks;
namespace Aki.SinglePlayer.Patches.Healing
{
public class PlayerPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var desiredType = typeof(Player);
var desiredMethod = desiredType.GetMethod("Init", PatchConstants.PrivateFlags);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
[PatchPostfix]
private static async void PatchPostfix(Task __result, Player __instance, Profile profile)
{
await __result;
if (profile?.Id.StartsWith("pmc") == true)
{
Logger.LogDebug($"Hooking up health listener to profile: {profile.Id}");
var listener = Utils.Healing.HealthListener.Instance;
listener.Init(__instance.HealthController, true);
Logger.LogDebug($"HealthController instance: {__instance.HealthController.GetHashCode()}");
}
else
{
Logger.LogDebug($"Skipped on HealthController instance: {__instance.HealthController.GetHashCode()} for profile id: {profile?.Id}");
}
}
}
}

View File

@ -0,0 +1,50 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Healing
{
/// <summary>
/// We need to alter Class1049.smethod_0().
/// Set the passed in ERaidMode to online, this ensures the heal screen shows.
/// It cannot be changed in the calling method as doing so causes the post-raid exp display to remain at 0
/// </summary>
public class PostRaidHealScreenPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
// Class1049.smethod_0 as of 18969
//internal static Class1049 smethod_0(GInterface29 backend, string profileId, Profile savageProfile, LocationSettingsClass.GClass1097 location, ExitStatus exitStatus, TimeSpan exitTime, ERaidMode raidMode)
var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "PostRaidHealthScreenClass");
var desiredMethod = desiredType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Single(IsTargetMethod);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
private static bool IsTargetMethod(MethodInfo mi)
{
var parameters = mi.GetParameters();
return parameters.Length == 7
&& parameters[0].Name == "session"
&& parameters[1].Name == "profileId"
&& parameters[2].Name == "savageProfile"
&& parameters[3].Name == "location"
&& parameters[4].Name == "exitStatus"
&& parameters[5].Name == "exitTime"
&& parameters[6].Name == "raidMode";
}
[PatchPrefix]
private static bool PatchPrefix(TarkovApplication __instance, ref ERaidMode raidMode)
{
raidMode = ERaidMode.Online;
return true; // Perform original method
}
}
}

View File

@ -0,0 +1,40 @@
using Aki.Reflection.Patching;
using EFT;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.MainMenu
{
/// <summary>
/// Force ERaidMode to online to make interface show insurance page
/// </summary>
class InsuranceScreenPatch : ModulePatch
{
static InsuranceScreenPatch()
{
_ = nameof(MainMenuController.InventoryController);
}
protected override MethodBase GetTargetMethod()
{
var desiredType = typeof(MainMenuController);
var desiredMethod = desiredType.GetMethod("method_66", BindingFlags.NonPublic | BindingFlags.Instance);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
[PatchPrefix]
private static void PrefixPatch(RaidSettings ___raidSettings_0)
{
___raidSettings_0.RaidMode = ERaidMode.Online;
}
[PatchPostfix]
private static void PostfixPatch(RaidSettings ___raidSettings_0)
{
___raidSettings_0.RaidMode = ERaidMode.Local;
}
}
}

View File

@ -0,0 +1,31 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT.UI;
using EFT.UI.Matchmaker;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.MainMenu
{
/// <summary>
/// Remove the ready button from select location screen
/// </summary>
public class SelectLocationScreenPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var desiredType = typeof(MatchMakerSelectionLocationScreen);
var desiredMethod = desiredType.GetMethod("Awake", PatchConstants.PrivateFlags);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
[PatchPostfix]
private static void PatchPostfix(DefaultUIButton ____readyButton)
{
____readyButton.Interactable = false;
}
}
}

View File

@ -0,0 +1,44 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT.UI.SessionEnd;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Progression
{
public class ExperienceGainPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var desiredType = typeof(SessionResultExperienceCount);
var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).FirstOrDefault(IsTargetMethod);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
private static bool IsTargetMethod(MethodInfo mi)
{
var parameters = mi.GetParameters();
return (parameters.Length == 3
&& parameters[0].Name == "profile"
&& parameters[1].Name == "isOnline"
&& parameters[2].Name == "exitStatus"
&& parameters[1].ParameterType == typeof(bool));
}
[PatchPrefix]
private static void PatchPrefix(ref bool isOnline)
{
isOnline = false;
}
[PatchPostfix]
private static void PatchPostfix(ref bool isOnline)
{
isOnline = true;
}
}
}

View File

@ -0,0 +1,26 @@
using Aki.Reflection.Patching;
using Aki.SinglePlayer.Models.Progression;
using Comfort.Common;
using EFT;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Progression
{
public class LighthouseBridgePatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted));
}
[PatchPostfix]
private static void PatchPostfix()
{
var gameWorld = Singleton<GameWorld>.Instance;
if (gameWorld == null || gameWorld.MainPlayer.Location.ToLower() != "lighthouse") return;
gameWorld.GetOrAddComponent<LighthouseProgressionClass>();
}
}
}

View File

@ -0,0 +1,41 @@
using Aki.Reflection.Patching;
using Comfort.Common;
using EFT;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Progression
{
public class LighthouseTransmitterPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(RadioTransmitterHandlerClass).GetMethod("method_4", BindingFlags.NonPublic | BindingFlags.Instance);
}
[PatchPrefix]
private static bool PatchPrefix(RadioTransmitterHandlerClass __instance)
{
var gameWorld = Singleton<GameWorld>.Instance;
if (gameWorld == null) return false;
var transmitter = __instance.RecodableComponent;
if (transmitter.IsEncoded)
{
transmitter.SetStatus(RadioTransmitterStatus.Green);
}
else if (gameWorld.MainPlayer.IsAgressorInLighthouseTraderZone)
{
// this might need to be tested and changed as I don't think this currently is affect upon killing bosses
transmitter.SetStatus(RadioTransmitterStatus.Yellow);
}
else
{
transmitter.SetStatus(RadioTransmitterStatus.Red);
}
return false;
}
}
}

View File

@ -0,0 +1,41 @@
using Aki.Reflection.Patching;
using Comfort.Common;
using EFT;
using HarmonyLib;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Progression
{
/// <summary>
/// After picking up a quest item, trigger CheckForStatusChange() from the questController to fully update a quest subtasks to show (e.g. `survive and extract item from raid` task)
/// </summary>
public class MidRaidQuestChangePatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(Profile).GetMethod("AddToCarriedQuestItems", BindingFlags.Public | BindingFlags.Instance);
}
[PatchPostfix]
private static void PatchPostfix()
{
var gameWorld = Singleton<GameWorld>.Instance;
if (gameWorld != null)
{
var player = gameWorld.MainPlayer;
var questController = Traverse.Create(player).Field<QuestControllerClass>("_questController").Value;
if (questController != null)
{
foreach (var quest in questController.Quests.ToList())
{
quest.CheckForStatusChange(true, true);
}
}
}
}
}
}

View File

@ -0,0 +1,72 @@
using Aki.Common.Http;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.SinglePlayer.Models.Progression;
using Aki.SinglePlayer.Utils.Progression;
using Comfort.Common;
using EFT;
using HarmonyLib;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Progression
{
public class OfflineSaveProfilePatch : ModulePatch
{
private static readonly JsonConverter[] _defaultJsonConverters;
static OfflineSaveProfilePatch()
{
_ = nameof(ClientMetrics.Metrics);
var converterClass = typeof(AbstractGame).Assembly.GetTypes()
.First(t => t.GetField("Converters", BindingFlags.Static | BindingFlags.Public) != null);
_defaultJsonConverters = Traverse.Create(converterClass).Field<JsonConverter[]>("Converters").Value;
}
protected override MethodBase GetTargetMethod()
{
// method_45 - as of 16432
// method_43 - as of 18876
var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "TarkovApplication");
var desiredMethod = Array.Find(desiredType.GetMethods(PatchConstants.PrivateFlags), IsTargetMethod);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
private bool IsTargetMethod(MethodInfo arg)
{
var parameters = arg.GetParameters();
return parameters.Length > 4
&& parameters[0]?.Name == "profileId"
&& parameters[1]?.Name == "savageProfile"
&& parameters[2]?.Name == "location"
&& arg.ReturnType == typeof(void);
}
[PatchPrefix]
private static void PatchPrefix(string profileId, RaidSettings ____raidSettings, TarkovApplication __instance, Result<ExitStatus, TimeSpan, ClientMetrics> result)
{
// Get scav or pmc profile based on IsScav value
var profile = (____raidSettings.IsScav)
? PatchConstants.BackEndSession.ProfileOfPet
: PatchConstants.BackEndSession.Profile;
SaveProfileRequest request = new SaveProfileRequest
{
Exit = result.Value0.ToString().ToLowerInvariant(),
Profile = profile,
Health = Utils.Healing.HealthListener.Instance.CurrentHealth,
IsPlayerScav = ____raidSettings.IsScav
};
RequestHandler.PutJson("/raid/profile/save", request.ToJson(_defaultJsonConverters.AddItem(new NotesJsonConverter()).ToArray()));
}
}
}

View File

@ -0,0 +1,88 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using EFT.Game.Spawning;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Progression
{
public class OfflineSpawnPointPatch : ModulePatch
{
static OfflineSpawnPointPatch()
{
_ = nameof(ISpawnPoints.CreateSpawnPoint);
}
protected override MethodBase GetTargetMethod()
{
var desiredType = PatchConstants.EftTypes.First(IsTargetType);
var desiredMethod = desiredType
.GetMethods(PatchConstants.PrivateFlags)
.First(m => m.Name.Contains("SelectSpawnPoint"));
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
private static bool IsTargetType(Type type)
{
// GClass1812 as of 17349
// GClass1886 as of 18876
return (type.GetMethods(PatchConstants.PrivateFlags).Any(x => x.Name.IndexOf("CheckFarthestFromOtherPlayers", StringComparison.OrdinalIgnoreCase) != -1)
&& type.IsClass);
}
[PatchPrefix]
private static bool PatchPrefix(
ref ISpawnPoint __result,
object __instance,
ESpawnCategory category,
EPlayerSide side,
string groupId,
IAIDetails person,
string infiltration)
{
var spawnPointsField = (ISpawnPoints)__instance.GetType().GetFields(PatchConstants.PrivateFlags).SingleOrDefault(f => f.FieldType == typeof(ISpawnPoints))?.GetValue(__instance);
if (spawnPointsField == null)
{
throw new Exception($"OfflineSpawnPointPatch: Failed to locate field of {nameof(ISpawnPoints)} on class instance ({__instance.GetType().Name})");
}
var mapSpawnPoints = spawnPointsField.ToList();
var unfilteredFilteredSpawnPoints = mapSpawnPoints.ToList();
// filter by e.g. 'Boiler Tanks' (always seems to be map name?)
if (!string.IsNullOrEmpty(infiltration))
{
mapSpawnPoints = mapSpawnPoints.Where(sp => sp?.Infiltration != null && (string.IsNullOrEmpty(infiltration) || sp.Infiltration.Equals(infiltration))).ToList();
}
mapSpawnPoints = FilterByPlayerSide(mapSpawnPoints, category, side);
__result = mapSpawnPoints.Count == 0
? GetFallBackSpawnPoint(unfilteredFilteredSpawnPoints, category, side, infiltration)
: mapSpawnPoints.RandomElement();
Logger.LogInfo($"Desired spawnpoint: [{category}] [{side}] [{infiltration}]");
Logger.LogInfo($"PatchPrefix SelectSpawnPoint: [{__result.Id}] [{__result.Name}] [{__result.Categories}] [{__result.Sides}] [{__result.Infiltration}]");
return false;
}
private static List<ISpawnPoint> FilterByPlayerSide(List<ISpawnPoint> mapSpawnPoints, ESpawnCategory category, EPlayerSide side)
{
// Filter by category 'player' and by side ('usec', 'bear')
return mapSpawnPoints.Where(sp => sp.Categories.Contain(category) && sp.Sides.Contain(side)).ToList();
}
private static ISpawnPoint GetFallBackSpawnPoint(List<ISpawnPoint> spawnPoints, ESpawnCategory category, EPlayerSide side, string infiltration)
{
Logger.LogWarning($"PatchPrefix SelectSpawnPoint: Couldn't find any spawn points for: {category} | {side} | {infiltration} using random filtered spawn instead");
return spawnPoints.Where(sp => sp.Categories.Contain(ESpawnCategory.Player)).RandomElement();
}
}
}

View File

@ -0,0 +1,73 @@
using Aki.Reflection.Patching;
using EFT;
using EFT.InventoryLogic;
using System;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Quests
{
public class DogtagPatch : ModulePatch
{
private static BindingFlags _flags;
private static PropertyInfo _getEquipmentProperty;
static DogtagPatch()
{
_ = nameof(EquipmentClass.GetSlot);
_ = nameof(DamageInfo.Weapon);
_flags = BindingFlags.Instance | BindingFlags.NonPublic;
_getEquipmentProperty = typeof(Player).GetProperty("Equipment", _flags);
}
protected override MethodBase GetTargetMethod()
{
return typeof(Player).GetMethod("OnBeenKilledByAggressor", _flags);
}
[PatchPostfix]
private static void PatchPostfix(Player __instance, Player aggressor, DamageInfo damageInfo)
{
if (__instance.Profile.Info.Side == EPlayerSide.Savage)
{
return;
}
var equipment = (EquipmentClass)_getEquipmentProperty.GetValue(__instance);
var dogtagSlot = equipment.GetSlot(EquipmentSlot.Dogtag);
var dogtagItem = dogtagSlot.ContainedItem;
if (dogtagItem == null)
{
Logger.LogError("DogtagPatch error > DogTag slot item is null somehow.");
return;
}
var itemComponent = dogtagItem.GetItemComponent<DogtagComponent>();
if (itemComponent == null)
{
Logger.LogError("DogtagPatch error > DogTagComponent on dog tag slot is null. Something went horrifically wrong!");
return;
}
var victimProfileInfo = __instance.Profile.Info;
itemComponent.AccountId = __instance.Profile.AccountId;
itemComponent.ProfileId = __instance.Profile.Id;
itemComponent.Nickname = victimProfileInfo.Nickname;
itemComponent.Side = victimProfileInfo.Side;
itemComponent.KillerName = aggressor.Profile.Info.Nickname;
itemComponent.Time = DateTime.Now;
itemComponent.Status = "Killed by ";
itemComponent.KillerAccountId = aggressor.Profile.AccountId;
itemComponent.KillerProfileId = aggressor.Profile.Id;
itemComponent.WeaponName = damageInfo.Weapon.Name;
if (__instance.Profile.Info.Experience > 0)
{
itemComponent.Level = victimProfileInfo.Level;
}
}
}
}

View File

@ -0,0 +1,55 @@
using Aki.Common.Http;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Quests
{
/// <summary>
/// Having the raid timer reach zero results in a successful extract,
/// this patch makes it so letting the time reach zero results in a MIA result
/// </summary>
public class EndByTimerPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var desiredType = PatchConstants.LocalGameType.BaseType;
var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).SingleOrDefault(IsStopRaidMethod);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
private static bool IsStopRaidMethod(MethodInfo mi)
{
var parameters = mi.GetParameters();
return (parameters.Length == 4
&& parameters[0].Name == "profileId"
&& parameters[1].Name == "exitStatus"
&& parameters[2].Name == "exitName"
&& parameters[3].Name == "delay"
&& parameters[0].ParameterType == typeof(string)
&& parameters[1].ParameterType == typeof(ExitStatus)
&& parameters[2].ParameterType == typeof(string)
&& parameters[3].ParameterType == typeof(float));
}
[PatchPrefix]
private static bool PrefixPatch(object __instance, ref ExitStatus exitStatus, ref string exitName)
{
// No extract name and successful, its a MIA
if (string.IsNullOrEmpty(exitName?.Trim()) && exitStatus == ExitStatus.Survived)
{
exitStatus = ExitStatus.MissingInAction;
exitName = null;
}
return true; // Do original
}
}
}

View File

@ -0,0 +1,49 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.Quests
{
public class SpawnPmcPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
var desiredType = PatchConstants.EftTypes.Single(IsTargetType);
var desiredMethod = desiredType.GetMethod("method_1", PatchConstants.PrivateFlags);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
}
private static bool IsTargetType(Type type)
{
if (!typeof(IBotData).IsAssignableFrom(type) || type.GetMethod("method_1", PatchConstants.PrivateFlags) == null)
{
return false;
}
var fields = type.GetFields(PatchConstants.PrivateFlags);
return fields.Any(f => f.FieldType != typeof(WildSpawnType)) && fields.Any(f => f.FieldType == typeof(BotDifficulty));
}
[PatchPrefix]
private static bool PatchPrefix(ref bool __result, object __instance, WildSpawnType ___wildSpawnType_0, BotDifficulty ___botDifficulty_0, Profile x)
{
if (x == null)
{
__result = false;
Logger.LogInfo($"profile x was null, ___wildSpawnType_0 = {___wildSpawnType_0}");
return false; // Skip original
}
__result = x.Info.Settings.Role == ___wildSpawnType_0 && x.Info.Settings.BotDifficulty == ___botDifficulty_0;
return false; // Skip original
}
}
}

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