commit b3ce0ec36f11f91f1c120bc9c393e8b212b641fd Author: Dev Date: Fri Mar 3 18:52:31 2023 +0000 Add repo diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c66d895 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5e79ced --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbe9f94 --- /dev/null +++ b/README.md @@ -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. diff --git a/docs/packetsniffer.md b/docs/packetsniffer.md new file mode 100644 index 0000000..7d6b61f --- /dev/null +++ b/docs/packetsniffer.md @@ -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(ConsistencyController.CheckResult.Succeed(new TimeSpan())); +} +``` + +#### Consistency single + +```cs +// Token: 0x06000053 RID: 83 RVA: 0x000028D4 File Offset: 0x00000AD4 +[prefix] +ConsistencyController.EnsureConsistencySingle() +{ + return Task.FromResult(ConsistencyController.CheckResult.Succeed(new TimeSpan())); +} +``` diff --git a/project/Aki.Build/Aki.Build.csproj b/project/Aki.Build/Aki.Build.csproj new file mode 100644 index 0000000..a754a8f --- /dev/null +++ b/project/Aki.Build/Aki.Build.csproj @@ -0,0 +1,33 @@ + + + + net472 + + + + Aki + Copyright @ Aki 2022 + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/project/Aki.Common/Aki.Common.csproj b/project/Aki.Common/Aki.Common.csproj new file mode 100644 index 0000000..c32e131 --- /dev/null +++ b/project/Aki.Common/Aki.Common.csproj @@ -0,0 +1,24 @@ + + + + net472 + + + + Aki + Copyright @ Aki 2022 + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/project/Aki.Common/Http/Request.cs b/project/Aki.Common/Http/Request.cs new file mode 100644 index 0000000..0b9414b --- /dev/null +++ b/project/Aki.Common/Http/Request.cs @@ -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 + { + /// + /// Send a request to remote endpoint and optionally receive a response body. + /// Deflate is the accepted compression format. + /// + public byte[] Send(string url, string method, byte[] data = null, bool compress = true, string mime = null, Dictionary 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 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; + } + } + } + } +} diff --git a/project/Aki.Common/Http/RequestHandler.cs b/project/Aki.Common/Http/RequestHandler.cs new file mode 100644 index 0000000..1b73438 --- /dev/null +++ b/project/Aki.Common/Http/RequestHandler.cs @@ -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 _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(json).BackendUrl; + } + + if (arg.Contains("-token=")) + { + _session = arg.Replace("-token=", string.Empty); + _headers = new Dictionary() + { + { "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); + } + } +} diff --git a/project/Aki.Common/Http/ServerConfig.cs b/project/Aki.Common/Http/ServerConfig.cs new file mode 100644 index 0000000..8ad8c9f --- /dev/null +++ b/project/Aki.Common/Http/ServerConfig.cs @@ -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; + } + } +} diff --git a/project/Aki.Common/Http/WebConstants.cs b/project/Aki.Common/Http/WebConstants.cs new file mode 100644 index 0000000..cdd0873 --- /dev/null +++ b/project/Aki.Common/Http/WebConstants.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Aki.Common.Http +{ + public static class WebConstants + { + /// + /// HTML GET method. + /// + public const string Get = "GET"; + + /// + /// HTML HEAD method. + /// + public const string Head = "HEAD"; + + /// + /// HTML POST method. + /// + public const string Post = "POST"; + + /// + /// HTML PUT method. + /// + public const string Put = "PUT"; + + /// + /// HTML DELETE method. + /// + public const string Delete = "DELETE"; + + /// + /// HTML CONNECT method. + /// + public const string Connect = "CONNECT"; + + /// + /// HTML OPTIONS method. + /// + public const string Options = "OPTIONS"; + + /// + /// HTML TRACE method. + /// + public const string Trace = "TRACE"; + + /// + /// HTML MIME types. + /// + public static Dictionary Mime { get; private set; } + + static WebConstants() + { + Mime = new Dictionary() + { + { ".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" } + }; + } + + /// + /// Is HTML method valid? + /// + public static bool IsValidMethod(string method) + { + return method == Get + || method == Head + || method == Post + || method == Put + || method == Delete + || method == Connect + || method == Options + || method == Trace; + } + + /// + /// Is MIME type valid? + /// + public static bool IsValidMime(string mime) + { + return Mime.Any(x => x.Value == mime); + } + } +} diff --git a/project/Aki.Common/Utils/Json.cs b/project/Aki.Common/Utils/Json.cs new file mode 100644 index 0000000..6fc6f4c --- /dev/null +++ b/project/Aki.Common/Utils/Json.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Aki.Common.Utils +{ + public static class Json + { + public static string Serialize(T data) + { + return JsonConvert.SerializeObject(data); + } + + public static T Deserialize(string json) + { + return JsonConvert.DeserializeObject(json); + } + } +} diff --git a/project/Aki.Common/Utils/VFS.cs b/project/Aki.Common/Utils/VFS.cs new file mode 100644 index 0000000..b241c00 --- /dev/null +++ b/project/Aki.Common/Utils/VFS.cs @@ -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; + } + + /// + /// Combine two filepaths. + /// + public static string Combine(string path1, string path2) + { + return Path.Combine(path1, path2); + } + + /// + /// Combines the filepath with the current working directory. + /// + public static string FromCwd(this string filepath) + { + return Combine(Cwd, filepath); + } + + /// + /// Get directory path of a filepath. + /// + public static string GetDirectory(this string filepath) + { + string value = Path.GetDirectoryName(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : string.Empty; + } + + /// + /// Get file of a filepath + /// + public static string GetFile(this string filepath) + { + string value = Path.GetFileName(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : string.Empty; + } + + /// + /// Get file name of a filepath + /// + public static string GetFileName(this string filepath) + { + string value = Path.GetFileNameWithoutExtension(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : string.Empty; + } + + /// + /// Get file extension of a filepath. + /// + public static string GetFileExtension(this string filepath) + { + string value = Path.GetExtension(filepath); + return (!string.IsNullOrWhiteSpace(value)) ? value : string.Empty; + } + + /// + /// Move file from one place to another + /// + public static void MoveFile(string a, string b) + { + new FileInfo(a).MoveTo(b); + } + + /// + /// Does the filepath exist? + /// + public static bool Exists(string filepath) + { + return Directory.Exists(filepath) || File.Exists(filepath); + } + + /// + /// Create directory (recursive). + /// + public static void CreateDirectory(string filepath) + { + Directory.CreateDirectory(filepath); + } + + /// + /// Get file content as bytes. + /// + public static byte[] ReadFile(string filepath) + { + return File.ReadAllBytes(filepath); + } + + /// + /// Get file content as string. + /// + public static string ReadTextFile(string filepath) + { + return File.ReadAllText(filepath); + } + + /// + /// Write data to file. + /// + public static void WriteFile(string filepath, byte[] data) + { + if (!Exists(filepath)) + { + CreateDirectory(filepath.GetDirectory()); + } + + File.WriteAllBytes(filepath, data); + } + + /// + /// Write string to file. + /// + 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); + } + } + + /// + /// Get directories in directory by full path. + /// + public static string[] GetDirectories(string filepath) + { + DirectoryInfo di = new DirectoryInfo(filepath); + List paths = new List(); + + foreach (DirectoryInfo directory in di.GetDirectories()) + { + paths.Add(directory.FullName); + } + + return paths.ToArray(); + } + + /// + /// Get files in directory by full path. + /// + public static string[] GetFiles(string filepath) + { + DirectoryInfo di = new DirectoryInfo(filepath); + List paths = new List(); + + foreach (FileInfo file in di.GetFiles()) + { + paths.Add(file.FullName); + } + + return paths.ToArray(); + } + + /// + /// Delete directory. + /// + public static void DeleteDirectory(string filepath) + { + DirectoryInfo di = new DirectoryInfo(filepath); + + foreach (FileInfo file in di.GetFiles()) + { + file.IsReadOnly = false; + file.Delete(); + } + + foreach (DirectoryInfo directory in di.GetDirectories()) + { + DeleteDirectory(directory.FullName); + } + + di.Delete(); + } + + /// + /// Delete file. + /// + public static void DeleteFile(string filepath) + { + FileInfo file = new FileInfo(filepath); + file.IsReadOnly = false; + file.Delete(); + } + + /// + /// Get files count inside directory recursively + /// + public static int GetFilesCount(string filepath) + { + DirectoryInfo di = new DirectoryInfo(filepath); + return di.Exists ? di.GetFiles("*.*", SearchOption.AllDirectories).Length : -1; + } + } +} diff --git a/project/Aki.Common/Utils/Zlib.cs b/project/Aki.Common/Utils/Zlib.cs new file mode 100644 index 0000000..c48c21d --- /dev/null +++ b/project/Aki.Common/Utils/Zlib.cs @@ -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 + + /// + /// Check if the file is ZLib compressed + /// + /// Data + /// If the file is Zlib compressed + 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; + } + + /// + /// Deflate data. + /// + 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; + } + + /// + /// Inflate data. + /// + 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(); + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Core/Aki.Core.csproj b/project/Aki.Core/Aki.Core.csproj new file mode 100644 index 0000000..6c687bd --- /dev/null +++ b/project/Aki.Core/Aki.Core.csproj @@ -0,0 +1,35 @@ + + + + net472 + aki-core + + + + Aki + Copyright @ Aki 2022 + + + + + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/project/Aki.Core/AkiCorePlugin.cs b/project/Aki.Core/AkiCorePlugin.cs new file mode 100644 index 0000000..a4029d9 --- /dev/null +++ b/project/Aki.Core/AkiCorePlugin.cs @@ -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"); + } + } +} diff --git a/project/Aki.Core/Models/FakeCertificateHandler.cs b/project/Aki.Core/Models/FakeCertificateHandler.cs new file mode 100644 index 0000000..2780b81 --- /dev/null +++ b/project/Aki.Core/Models/FakeCertificateHandler.cs @@ -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(); + } + } +} diff --git a/project/Aki.Core/Models/FakeFileCheckerResult.cs b/project/Aki.Core/Models/FakeFileCheckerResult.cs new file mode 100644 index 0000000..2e2cd7e --- /dev/null +++ b/project/Aki.Core/Models/FakeFileCheckerResult.cs @@ -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; + } + } +} diff --git a/project/Aki.Core/Patches/BattlEyePatch.cs b/project/Aki.Core/Patches/BattlEyePatch.cs new file mode 100644 index 0000000..3fc27b9 --- /dev/null +++ b/project/Aki.Core/Patches/BattlEyePatch.cs @@ -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; + } + } +} diff --git a/project/Aki.Core/Patches/ConsistencyMultiPatch.cs b/project/Aki.Core/Patches/ConsistencyMultiPatch.cs new file mode 100644 index 0000000..7a71d8b --- /dev/null +++ b/project/Aki.Core/Patches/ConsistencyMultiPatch.cs @@ -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)); + } + + [PatchPrefix] + private static bool PatchPrefix(ref object __result) + { + __result = Task.FromResult(new FakeFileCheckerResult()); + return false; + } + } +} diff --git a/project/Aki.Core/Patches/ConsistencySinglePatch.cs b/project/Aki.Core/Patches/ConsistencySinglePatch.cs new file mode 100644 index 0000000..96447b9 --- /dev/null +++ b/project/Aki.Core/Patches/ConsistencySinglePatch.cs @@ -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)); + } + + [PatchPrefix] + private static bool PatchPrefix(ref object __result) + { + __result = Task.FromResult(new FakeFileCheckerResult()); + return false; + } + } +} diff --git a/project/Aki.Core/Patches/DataHandlerDebugPatch.cs b/project/Aki.Core/Patches/DataHandlerDebugPatch.cs new file mode 100644 index 0000000..d23c2a3 --- /dev/null +++ b/project/Aki.Core/Patches/DataHandlerDebugPatch.cs @@ -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}"); + } + } +} \ No newline at end of file diff --git a/project/Aki.Core/Patches/SslCertificatePatch.cs b/project/Aki.Core/Patches/SslCertificatePatch.cs new file mode 100644 index 0000000..972ca66 --- /dev/null +++ b/project/Aki.Core/Patches/SslCertificatePatch.cs @@ -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 + } + } +} diff --git a/project/Aki.Core/Patches/TransportPrefixPatch.cs b/project/Aki.Core/Patches/TransportPrefixPatch.cs new file mode 100644 index 0000000..c197286 --- /dev/null +++ b/project/Aki.Core/Patches/TransportPrefixPatch.cs @@ -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>(); + 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 PatchTranspile(ILGenerator generator, IEnumerable instructions) + { + var codes = new List(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(); + } + } +} \ No newline at end of file diff --git a/project/Aki.Core/Patches/UnityWebRequestPatch.cs b/project/Aki.Core/Patches/UnityWebRequestPatch.cs new file mode 100644 index 0000000..b5ef767 --- /dev/null +++ b/project/Aki.Core/Patches/UnityWebRequestPatch.cs @@ -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; + } + } +} diff --git a/project/Aki.Core/Patches/WebSocketPatch.cs b/project/Aki.Core/Patches/WebSocketPatch.cs new file mode 100644 index 0000000..856d6f9 --- /dev/null +++ b/project/Aki.Core/Patches/WebSocketPatch.cs @@ -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:")); + } + } +} diff --git a/project/Aki.Core/Utils/ValidationUtil.cs b/project/Aki.Core/Utils/ValidationUtil.cs new file mode 100644 index 0000000..7c98fc2 --- /dev/null +++ b/project/Aki.Core/Utils/ValidationUtil.cs @@ -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; + } + } +} diff --git a/project/Aki.Custom/Airdrops/AirdropBox.cs b/project/Aki.Custom/Airdrops/AirdropBox.cs new file mode 100644 index 0000000..306c611 --- /dev/null +++ b/project/Aki.Custom/Airdrops/AirdropBox.cs @@ -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 soundsDictionary; + private BetterSource audioSource; + + private BetterSource AudioSource + { + get + { + if (audioSource != null) return audioSource; + + audioSource = Singleton.Instance.GetSource(BetterAudio.AudioSourceGroupType.Environment, false); + audioSource.transform.parent = transform; + audioSource.transform.localPosition = Vector3.up; + + return audioSource; + } + } + + public static async Task Init(float crateFallSpeed) + { + var instance = (await LoadCrate()).AddComponent(); + instance.soundsDictionary = await LoadSounds(); + + instance.container = instance.GetComponentInChildren(); + + instance.boxSync = instance.GetComponent(); + instance.boxLogic = new AirdropLogicClass(); + instance.boxSync.SetLogic(instance.boxLogic); + + instance.paraAnimator = instance.boxSync.Parachute.GetComponent(); + instance.paraMaterial = instance.boxSync.Parachute.GetComponentInChildren().material; + instance.fallSpeed = crateFallSpeed; + return instance; + } + + private static async Task LoadCrate() + { + var easyAssets = Singleton.Instance.EasyAssets; + await easyAssets.Retain(CRATE_PATH, null, null).LoadingJob; + + var crate = Instantiate(easyAssets.GetAsset(CRATE_PATH)); + crate.SetActive(false); + return crate; + } + + private static async Task> LoadSounds() + { + var easyAssets = Singleton.Instance.EasyAssets; + await easyAssets.Retain(AIRDROP_SOUNDS_PATH, null, null).LoadingJob; + + var soundsDictionary = new Dictionary(); + var sets = easyAssets.GetAsset(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().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.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; + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/AirdropPlane.cs b/project/Aki.Custom/Airdrops/AirdropPlane.cs new file mode 100644 index 0000000..a4d7c8b --- /dev/null +++ b/project/Aki.Custom/Airdrops/AirdropPlane.cs @@ -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 Init(Vector3 airdropPoint, int dropHeight, float planeVolume, float speed) + { + var instance = (await LoadPlane()).AddComponent(); + + instance.airplaneSync = instance.GetComponent(); + 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 LoadPlane() + { + var easyAssets = Singleton.Instance.EasyAssets; + await easyAssets.Retain(PLANE_PATH, null, null).LoadingJob; + var plane = Instantiate(easyAssets.GetAsset(PLANE_PATH)); + return plane; + } + + private void SetAudio(float planeVolume) + { + var airplaneAudio = gameObject.AddComponent(); + airplaneAudio.clip = airplaneSync.soundClip.Clip; + + airplaneAudio.dopplerLevel = 1f; + airplaneAudio.outputAudioMixerGroup = Singleton.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(); + var endTime = Time.unscaledTime + emissionTime; + Singleton.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); + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/AirdropsManager.cs b/project/Aki.Custom/Airdrops/AirdropsManager.cs new file mode 100644 index 0000000..d28226c --- /dev/null +++ b/project/Aki.Custom/Airdrops/AirdropsManager.cs @@ -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.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); + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/Models/AirdropConfigModel.cs b/project/Aki.Custom/Airdrops/Models/AirdropConfigModel.cs new file mode 100644 index 0000000..b035f1e --- /dev/null +++ b/project/Aki.Custom/Airdrops/Models/AirdropConfigModel.cs @@ -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; } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/Models/AirdropLootModel.cs b/project/Aki.Custom/Airdrops/Models/AirdropLootModel.cs new file mode 100644 index 0000000..b7ed34d --- /dev/null +++ b/project/Aki.Custom/Airdrops/Models/AirdropLootModel.cs @@ -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; } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/Models/AirdropParametersModel.cs b/project/Aki.Custom/Airdrops/Models/AirdropParametersModel.cs new file mode 100644 index 0000000..946dfe4 --- /dev/null +++ b/project/Aki.Custom/Airdrops/Models/AirdropParametersModel.cs @@ -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; + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/Patches/AirdropFlarePatch.cs b/project/Aki.Custom/Airdrops/Patches/AirdropFlarePatch.cs new file mode 100644 index 0000000..4e03a3c --- /dev/null +++ b/project/Aki.Custom/Airdrops/Patches/AirdropFlarePatch.cs @@ -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.Instance; + var points = LocationScene.GetAll().Any(); + + if (gameWorld != null && points && _usableFlares.Any(x => x == flareCartridge.Template._id)) + { + gameWorld.gameObject.AddComponent().isFlareDrop = true; + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/Patches/AirdropPatch.cs b/project/Aki.Custom/Airdrops/Patches/AirdropPatch.cs new file mode 100644 index 0000000..c473a01 --- /dev/null +++ b/project/Aki.Custom/Airdrops/Patches/AirdropPatch.cs @@ -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.Instance; + var points = LocationScene.GetAll().Any(); + + if (gameWorld != null && points) + { + gameWorld.gameObject.AddComponent(); + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Airdrops/Utils/AirdropUtil.cs b/project/Aki.Custom/Airdrops/Utils/AirdropUtil.cs new file mode 100644 index 0000000..fbe3e98 --- /dev/null +++ b/project/Aki.Custom/Airdrops/Utils/AirdropUtil.cs @@ -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(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 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().ToList(); + var playerPosition = gameWorld.RegisteredPlayers[0].Position; + var flareAirdropPoints = new List(); + 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 + }; + } + } +} diff --git a/project/Aki.Custom/Airdrops/Utils/ItemFactoryUtil.cs b/project/Aki.Custom/Airdrops/Utils/ItemFactoryUtil.cs new file mode 100644 index 0000000..1601127 --- /dev/null +++ b/project/Aki.Custom/Airdrops/Utils/ItemFactoryUtil.cs @@ -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.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.Instance); + } + else + { + Debug.LogError($"[AKI-AIRDROPS]: unable to find template: {DropContainer}"); + } + } + + public async void AddLoot(LootableContainer container) + { + List 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.Instance.LoadBundlesAndCreatePools(PoolManager.PoolsCategory.Raid, PoolManager.AssemblyType.Local, resources, JobPriority.Immediate, null, PoolManager.DefaultCancellationToken); + } + } + + private List GetLoot() + { + var json = RequestHandler.GetJson("/client/location/getAirdropLoot"); + var loot = JsonConvert.DeserializeObject>(json); + + return loot; + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Aki.Custom.csproj b/project/Aki.Custom/Aki.Custom.csproj new file mode 100644 index 0000000..1a0adfb --- /dev/null +++ b/project/Aki.Custom/Aki.Custom.csproj @@ -0,0 +1,42 @@ + + + + net472 + aki-custom + + + + Aki + Copyright @ Aki 2022 + + + + + + + + + + + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/project/Aki.Custom/AkiCustomPlugin.cs b/project/Aki.Custom/AkiCustomPlugin.cs new file mode 100644 index 0000000..c389ca5 --- /dev/null +++ b/project/Aki.Custom/AkiCustomPlugin.cs @@ -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"); + } + } +} diff --git a/project/Aki.Custom/Models/BundleInfo.cs b/project/Aki.Custom/Models/BundleInfo.cs new file mode 100644 index 0000000..b3026e5 --- /dev/null +++ b/project/Aki.Custom/Models/BundleInfo.cs @@ -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; + } + } +} diff --git a/project/Aki.Custom/Models/BundleItem.cs b/project/Aki.Custom/Models/BundleItem.cs new file mode 100644 index 0000000..0b5b963 --- /dev/null +++ b/project/Aki.Custom/Models/BundleItem.cs @@ -0,0 +1,9 @@ +namespace Aki.Custom.Models +{ + public struct BundleItem + { + public string FileName; + public uint Crc; + public string[] Dependencies; + } +} diff --git a/project/Aki.Custom/Models/DefaultRaidSettings.cs b/project/Aki.Custom/Models/DefaultRaidSettings.cs new file mode 100644 index 0000000..4a8e77e --- /dev/null +++ b/project/Aki.Custom/Models/DefaultRaidSettings.cs @@ -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; + } + } +} diff --git a/project/Aki.Custom/Models/VersionResponse.cs b/project/Aki.Custom/Models/VersionResponse.cs new file mode 100644 index 0000000..4ce51d2 --- /dev/null +++ b/project/Aki.Custom/Models/VersionResponse.cs @@ -0,0 +1,8 @@ +namespace Aki.Custom.Models +{ + public struct VersionResponse + { + public string Version { get; set; } + } +} + diff --git a/project/Aki.Custom/Patches/AddEnemyPatch.cs b/project/Aki.Custom/Patches/AddEnemyPatch.cs new file mode 100644 index 0000000..4fad0dd --- /dev/null +++ b/project/Aki.Custom/Patches/AddEnemyPatch.cs @@ -0,0 +1,34 @@ +using Aki.Reflection.Patching; +using EFT; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + /// + /// 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 + /// + 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)__instance.GetType().GetField("list_1", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); + if (botOwners.Any(x => x.Id == person.Id)) + { + return false; + } + + return true; + } + } +} diff --git a/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs b/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs new file mode 100644 index 0000000..6705d36 --- /dev/null +++ b/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs @@ -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); + } + + /// + /// 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. + /// + [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; + } + } +} diff --git a/project/Aki.Custom/Patches/AddSptBotSettingsPatch.cs b/project/Aki.Custom/Patches/AddSptBotSettingsPatch.cs new file mode 100644 index 0000000..d5b4669 --- /dev/null +++ b/project/Aki.Custom/Patches/AddSptBotSettingsPatch.cs @@ -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 ___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())); + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/BossSpawnChancePatch.cs b/project/Aki.Custom/Patches/BossSpawnChancePatch.cs new file mode 100644 index 0000000..3fb2658 --- /dev/null +++ b/project/Aki.Custom/Patches/BossSpawnChancePatch.cs @@ -0,0 +1,56 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using System.Linq; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + /// + /// Boss spawn chance is 100%, all the time, this patch adjusts the chance to the maps boss wave value + /// + 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]; + } + } + } +} diff --git a/project/Aki.Custom/Patches/BotDifficultyPatch.cs b/project/Aki.Custom/Patches/BotDifficultyPatch.cs new file mode 100644 index 0000000..d1cb06f --- /dev/null +++ b/project/Aki.Custom/Patches/BotDifficultyPatch.cs @@ -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); + } + } +} diff --git a/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs b/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs new file mode 100644 index 0000000..7245cb5 --- /dev/null +++ b/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs @@ -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); + } + + /// + /// 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. + /// + [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; + } + } +} diff --git a/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs b/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs new file mode 100644 index 0000000..50481da --- /dev/null +++ b/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs @@ -0,0 +1,41 @@ +using Aki.Reflection.Patching; +using EFT; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + /// + /// Goal: patch removes the current bot from its own enemy list - occurs when adding bots type to its enemy array in difficulty settings + /// + 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; + } + } +} diff --git a/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs b/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs new file mode 100644 index 0000000..ffe4fea --- /dev/null +++ b/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs @@ -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); + } + + /// + /// CheckAndAddEnemy() + /// Goal: This patch lets bosses shoot back once a PMC has shot them + /// removes the !player.AIData.IsAI check + /// + [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)_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; + } + } +} diff --git a/project/Aki.Custom/Patches/CoreDifficultyPatch.cs b/project/Aki.Custom/Patches/CoreDifficultyPatch.cs new file mode 100644 index 0000000..379c6d4 --- /dev/null +++ b/project/Aki.Custom/Patches/CoreDifficultyPatch.cs @@ -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); + } + } +} diff --git a/project/Aki.Custom/Patches/CustomAiPatch.cs b/project/Aki.Custom/Patches/CustomAiPatch.cs new file mode 100644 index 0000000..491f02a --- /dev/null +++ b/project/Aki.Custom/Patches/CustomAiPatch.cs @@ -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>> botTypeCache = new Dictionary>>(); + private static DateTime cacheDate = new DateTime(); + + protected override MethodBase GetTargetMethod() + { + return typeof(BotBrainClass).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance); + } + + /// + /// 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 + /// + /// state to save for postfix to use later + /// + /// botOwner_0 property + [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 + } + + /// + /// Revert prefix change, get bots type back to what it was before changes + /// + /// Saved state from prefix patch + /// botOwner_0 property + [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.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>>>(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"; + } + } +} diff --git a/project/Aki.Custom/Patches/EasyAssetsPatch.cs b/project/Aki.Custom/Patches/EasyAssetsPatch.cs new file mode 100644 index 0000000..0597b36 --- /dev/null +++ b/project/Aki.Custom/Patches/EasyAssetsPatch.cs @@ -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; + +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 shouldExclude, [CanBeNull] Func bundleCheck) + { + __result = Init(__instance, bundleLock, defaultKey, rootPath, platformName, shouldExclude, bundleCheck); + return false; + } + + public static string GetPairKey(KeyValuePair x) + { + return x.Key; + } + + public static BundleDetails GetPairValue(KeyValuePair x) + { + return new BundleDetails + { + FileName = x.Value.FileName, + Crc = x.Value.Crc, + Dependencies = x.Value.Dependencies + }; + } + + private static async Task 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 GetManifestJson(string filepath) + { + var text = string.Empty; + + using (var reader = File.OpenText($"{filepath}.json")) + { + text = await reader.ReadToEndAsync(); + } + + var data = JsonConvert.DeserializeObject>(text).ToDictionary(GetPairKey, GetPairValue); + var manifest = ScriptableObject.CreateInstance(); + manifest.SetResults(data); + + return manifest; + } + + private static async Task Init(EasyAssets instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath, + string platformName, [CanBeNull] Func shouldExclude, Func 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)); + } + } +} diff --git a/project/Aki.Custom/Patches/EasyBundlePatch.cs b/project/Aki.Custom/Patches/EasyBundlePatch.cs new file mode 100644 index 0000000..998aff7 --- /dev/null +++ b/project/Aki.Custom/Patches/EasyBundlePatch.cs @@ -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.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.Unloaded, null), + BundleLock = bundleLock + }; + } + } +} diff --git a/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs b/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs new file mode 100644 index 0000000..33bb8ba --- /dev/null +++ b/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs @@ -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 +{ + /// + /// Fix a bsg bug that causes the game to soft-lock when you have a container opened when extracting + /// + 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 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.Instance.MainPlayer; + if (profileId == player?.Profile.Id) + { + GClass2727.Instance.CloseAllScreensForced(); + } + + return true; + } + } +} diff --git a/project/Aki.Custom/Patches/IsEnemyPatch.cs b/project/Aki.Custom/Patches/IsEnemyPatch.cs new file mode 100644 index 0000000..9804fc4 --- /dev/null +++ b/project/Aki.Custom/Patches/IsEnemyPatch.cs @@ -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); + } + + /// + /// 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 + /// + [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 + } + + /// + /// Return True when usec default behavior is attack + bot is usec + /// + /// + /// bool + 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; + } + + /// + /// Return True when bear default behavior is attack + bot is bear + /// + /// + /// + 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; + } + } +} diff --git a/project/Aki.Custom/Patches/OfflineRaidMenuPatch.cs b/project/Aki.Custom/Patches/OfflineRaidMenuPatch.cs new file mode 100644 index 0000000..d5bb7ea --- /dev/null +++ b/project/Aki.Custom/Patches/OfflineRaidMenuPatch.cs @@ -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(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); + } + } +} diff --git a/project/Aki.Custom/Patches/QTEPatch.cs b/project/Aki.Custom/Patches/QTEPatch.cs new file mode 100644 index 0000000..3fff3c4 --- /dev/null +++ b/project/Aki.Custom/Patches/QTEPatch.cs @@ -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()); + } + } +} diff --git a/project/Aki.Custom/Patches/SessionIdPatch.cs b/project/Aki.Custom/Patches/SessionIdPatch.cs new file mode 100644 index 0000000..dbf33cc --- /dev/null +++ b/project/Aki.Custom/Patches/SessionIdPatch.cs @@ -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(); + } + + if (_preloader != null) + { + var raidID = Path.GetRandomFileName().Replace(".", string.Empty).Substring(0, 6).ToUpperInvariant(); + _preloader.SetSessionId(raidID); + } + } + } +} diff --git a/project/Aki.Custom/Patches/VersionLabelPatch.cs b/project/Aki.Custom/Patches/VersionLabelPatch.cs new file mode 100644 index 0000000..169275d --- /dev/null +++ b/project/Aki.Custom/Patches/VersionLabelPatch.cs @@ -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(json).Version; + Logger.LogInfo($"Server version: {_versionLabel}"); + } + + Traverse.Create(MonoBehaviourSingleton.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}"); + Traverse.Create(MonoBehaviourSingleton.Instance).Field("string_2").SetValue(_versionLabel); + Traverse.Create(__result).Field("Major").SetValue(_versionLabel); + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Utils/BundleManager.cs b/project/Aki.Custom/Utils/BundleManager.cs new file mode 100644 index 0000000..95e0d83 --- /dev/null +++ b/project/Aki.Custom/Utils/BundleManager.cs @@ -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 Bundles { get; private set; } + + static BundleManager() + { + Bundles = new Dictionary(); + + 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()); + + 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>(Bundles)); + } + } +} diff --git a/project/Aki.Custom/Utils/EasyBundleHelper.cs b/project/Aki.Custom/Utils/EasyBundleHelper.cs new file mode 100644 index 0000000..7a8b0b6 --- /dev/null +++ b/project/Aki.Custom/Utils/EasyBundleHelper.cs @@ -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; + +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 DependencyKeys + { + get + { + return (IEnumerable)_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[] { }); + } + } +} diff --git a/project/Aki.Debugging/Aki.Debugging.csproj b/project/Aki.Debugging/Aki.Debugging.csproj new file mode 100644 index 0000000..ec2de3e --- /dev/null +++ b/project/Aki.Debugging/Aki.Debugging.csproj @@ -0,0 +1,35 @@ + + + + net472 + aki-debugging + + + + Aki + Copyright @ Aki 2022 + + + + + + + + + + + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/project/Aki.Debugging/AkiDebuggingPlugin.cs b/project/Aki.Debugging/AkiDebuggingPlugin.cs new file mode 100644 index 0000000..3569cff --- /dev/null +++ b/project/Aki.Debugging/AkiDebuggingPlugin.cs @@ -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"); + } + } +} diff --git a/project/Aki.Debugging/Patches/CoordinatesPatch.cs b/project/Aki.Debugging/Patches/CoordinatesPatch.cs new file mode 100644 index 0000000..0be13a0 --- /dev/null +++ b/project/Aki.Debugging/Patches/CoordinatesPatch.cs @@ -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(); + _alphaLabel.color = Color.green; + _alphaLabel.fontSize = 22; + _alphaLabel.font = Resources.Load("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; + } + } + } +} diff --git a/project/Aki.PrePatch/Aki.PrePatch.csproj b/project/Aki.PrePatch/Aki.PrePatch.csproj new file mode 100644 index 0000000..910da62 --- /dev/null +++ b/project/Aki.PrePatch/Aki.PrePatch.csproj @@ -0,0 +1,30 @@ + + + + + net472 + aki_PrePatch + + + + Aki + Copyright @ Aki 2022 + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/project/Aki.PrePatch/AkiBotsPrePatcher.cs b/project/Aki.PrePatch/AkiBotsPrePatcher.cs new file mode 100644 index 0000000..2450cfc --- /dev/null +++ b/project/Aki.PrePatch/AkiBotsPrePatcher.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Mono.Cecil; + +namespace Aki.PrePatch +{ + public static class AkiBotsPrePatcher + { + public static IEnumerable 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); + } + } +} \ No newline at end of file diff --git a/project/Aki.Reflection/Aki.Reflection.csproj b/project/Aki.Reflection/Aki.Reflection.csproj new file mode 100644 index 0000000..d93915e --- /dev/null +++ b/project/Aki.Reflection/Aki.Reflection.csproj @@ -0,0 +1,35 @@ + + + + net472 + + + + Aki + Copyright @ Aki 2022 + + + + + + + + + + + + + + + + + compile; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/project/Aki.Reflection/CodeWrapper/Code.cs b/project/Aki.Reflection/CodeWrapper/Code.cs new file mode 100644 index 0000000..14925a0 --- /dev/null +++ b/project/Aki.Reflection/CodeWrapper/Code.cs @@ -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; + } + } +} diff --git a/project/Aki.Reflection/CodeWrapper/CodeGenerator.cs b/project/Aki.Reflection/CodeWrapper/CodeGenerator.cs new file mode 100644 index 0000000..2e6959e --- /dev/null +++ b/project/Aki.Reflection/CodeWrapper/CodeGenerator.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; + +namespace Aki.Reflection.CodeWrapper +{ + /// + /// Helper class to generate IL code for transpilers + /// + public class CodeGenerator + { + public static List GenerateInstructions(List codes) + { + var list = new List(); + + 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