diff --git a/.gitea/workflows/build-trigger.yaml b/.gitea/workflows/build-trigger.yaml new file mode 100644 index 0000000..bc76ecc --- /dev/null +++ b/.gitea/workflows/build-trigger.yaml @@ -0,0 +1,35 @@ +name: Trigger Main Build Pipeline + +on: + push: + tags: + - '*' + +jobs: + trigger-main-build: + runs-on: ubuntu-latest + steps: + - name: Setup Git Config + run: | + git config --global user.email "noreply@sp-tarkov.com" + git config --global user.name "TriggerBot" + + - name: Clone Build Repository + run: | + rm -rf ../Build + git clone https://${{ secrets.BUILD_USERNAME }}:${{ secrets.BUILD_ACCESS_TOKEN }}@dev.sp-tarkov.com/SPT-AKI/Build.git ../Build + + - name: Trigger Branch + working-directory: ../Build + run: git checkout -b trigger || git checkout trigger + + - name: Create Trigger File + working-directory: ../Build + run: | + echo "${GITHUB_REF_NAME}" > .gitea/trigger + git add .gitea/trigger + git commit -m "Modules triggered build with tag '${GITHUB_REF_NAME}'" + + - name: Force Push + working-directory: ../Build + run: git push --force origin trigger diff --git a/README.md b/README.md index 3540bc3..6e8a369 100644 --- a/README.md +++ b/README.md @@ -22,26 +22,31 @@ git config --local user.email "USERNAME@SOMETHING.com" ``` ## Requirements - -- Escape From Tarkov 26535 -- BepInEx 5.4.21 -- Visual Studio Code +- Escape From Tarkov 29197 +- Visual Studio Code -OR- Visual Studio 2022 - .NET 6 SDK +- [PowerShell v7](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows) + - Can also be installed via: `dotnet tool update --global PowerShell` -## Setup +## Project Setup +Copy-paste Live EFT's `EscapeFromTarkov_Data/Managed/` folder to into this project's `Project/Shared/Managed/` folder -Copy-paste Live EFT's `EscapeFromTarkov_Data/Managed/` folder to into Modules' `Project/Shared/` folder - -## Build (vscode) +## Build (VS Code) 1. File > Open Workspace > Modules.code-workspace 2. Terminal > Run Build Task... 3. Copy contents of `/Build` into SPT game folder and overwrite -## Build (VS) +## Build (VS 2022) 1. Open solution 2. Restore nuget packages -3. Run `dotnet new tool-manifest` -4. Sometimes you need to run `dotnet tool restore` -5. Run `dotnet tool install Cake.Tool` -6. Build solution -7. Copy contents of `/Build` into SPT game folder and overwrite +3. Build solution +4. Copy contents of `/Build` into SPT game folder and overwrite + +## Game Setup +1. Copy Live EFT files into a separate directory (from now on this will be referred to as the "SPT directory") +2. Download BepInEx 5.4.22 x64 ([BepInEx Releases - GitHub](https://github.com/BepInEx/BepInEx/releases/tag/v5.4.22)) +3. Extract contents of the BepInEx zip into the root SPT directory +4. Build Modules, Server and Launcher +5. Copy the contents of each project's `Build` folder into the root SPT directory +6. (Optional, but recommended) Download the BepInEx5 version of ConfigurationManager ([ConfigurationManager Releases - GitHub](https://github.com/BepInEx/BepInEx.ConfigurationManager/releases)) and extract the contents of the zip into the root SPT directory. The default keybind for opening the menu will be `F1` +7. (Optional) Edit the BepInEx config (`\BepInEx\config\BepInEx.cfg`) and append `Debug` to the `LogLevels` setting. Example: `LogLevels = Fatal, Error, Warning, Message, Info, Debug` diff --git a/project/.config/dotnet-tools.json b/project/.config/dotnet-tools.json deleted file mode 100644 index 831d039..0000000 --- a/project/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "cake.tool": { - "version": "3.0.0", - "commands": [ - "dotnet-cake" - ] - } - } -} \ No newline at end of file diff --git a/project/Aki.Build/Aki.Build.csproj b/project/Aki.Build/Aki.Build.csproj index 079b874..c4e1d46 100644 --- a/project/Aki.Build/Aki.Build.csproj +++ b/project/Aki.Build/Aki.Build.csproj @@ -1,12 +1,13 @@  - net472 + net471 + Release SPT Aki - Copyright @ SPT Aki 2023 + Copyright @ SPT Aki 2024 @@ -19,11 +20,14 @@ + + + - + diff --git a/project/Aki.Common/Aki.Common.csproj b/project/Aki.Common/Aki.Common.csproj index 690ee64..137a581 100644 --- a/project/Aki.Common/Aki.Common.csproj +++ b/project/Aki.Common/Aki.Common.csproj @@ -2,15 +2,17 @@ 1.0.0.0 - net472 + net471 + Release Aki - Copyright @ Aki 2022 + Copyright @ Aki 2024 + diff --git a/project/Aki.Common/Http/Client.cs b/project/Aki.Common/Http/Client.cs new file mode 100644 index 0000000..993f1c1 --- /dev/null +++ b/project/Aki.Common/Http/Client.cs @@ -0,0 +1,161 @@ +using System; +using System.IO; +using System.Net.Http; +using Aki.Common.Http; +using Aki.Common.Utils; + +namespace Aki.Common.Http +{ + // NOTE: you do not want to dispose this, keep a reference for the lifetime + // of the application. + // NOTE: cannot be made async due to Unity's limitations. + public class Client : IDisposable + { + protected readonly HttpClient _httpv; + protected readonly string _address; + protected readonly string _accountId; + protected readonly int _retries; + + public Client(string address, string accountId, int retries = 3) + { + _address = address; + _accountId = accountId; + _retries = retries; + + var handler = new HttpClientHandler() + { + // force setting cookies in header instead of CookieContainer + UseCookies = false + }; + + _httpv = new HttpClient(handler); + } + + private HttpRequestMessage GetNewRequest(HttpMethod method, string path) + { + return new HttpRequestMessage() + { + Method = method, + RequestUri = new Uri(_address + path), + Headers = { + { "Cookie", $"PHPSESSID={_accountId}" } + } + }; + } + + protected byte[] Send(HttpMethod method, string path, byte[] data, bool compress = true) + { + HttpResponseMessage response = null; + + using (var request = GetNewRequest(method, path)) + { + if (data != null) + { + // if there is data, convert to payload + byte[] payload = (compress) + ? Zlib.Compress(data, ZlibCompression.Maximum) + : data; + + // add payload to request + request.Content = new ByteArrayContent(payload); + } + + // send request + response = _httpv.SendAsync(request).Result; + } + + if (!response.IsSuccessStatusCode) + { + // response error + throw new Exception($"Code {response.StatusCode}"); + } + + using (var ms = new MemoryStream()) + { + using (var stream = response.Content.ReadAsStreamAsync().Result) + { + // grap response payload + stream.CopyTo(ms); + var bytes = ms.ToArray(); + + if (bytes != null) + { + // payload contains data + return Zlib.IsCompressed(bytes) + ? Zlib.Decompress(bytes) + : bytes; + } + } + } + + // response returned no data + return null; + } + + public byte[] Get(string path) + { + var error = new Exception("Internal error"); + + // NOTE: <= is intentional, 0 is send, 1,2,3 is retry + for (var i = 0; i <= _retries; ++i) + { + try + { + return Send(HttpMethod.Get, path, null, false); + } + catch (Exception ex) + { + error = ex; + } + } + + throw error; + } + + public byte[] Post(string path, byte[] data, bool compressed = true) + { + var error = new Exception("Internal error"); + + // NOTE: <= is intentional, 0 is send, 1,2,3 is retry + for (var i = 0; i <= _retries; ++i) + { + try + { + return Send(HttpMethod.Post, path, data, compressed); + } + catch (Exception ex) + { + error = ex; + } + } + + throw error; + } + + public void Put(string path, byte[] data, bool compressed = true) + { + var error = new Exception("Internal error"); + + // NOTE: <= is intentional, 0 is send, 1,2,3 is retry + for (var i = 0; i <= _retries; ++i) + { + try + { + Send(HttpMethod.Put, path, data, compressed); + return; + } + catch (Exception ex) + { + error = ex; + } + } + + throw error; + } + + public void Dispose() + { + _httpv.Dispose(); + } + } +} diff --git a/project/Aki.Common/Http/Request.cs b/project/Aki.Common/Http/Request.cs index 0b9414b..0b92c5a 100644 --- a/project/Aki.Common/Http/Request.cs +++ b/project/Aki.Common/Http/Request.cs @@ -1,3 +1,5 @@ +#region DEPRECATED, REMOVE IN 3.8.1 + using System; using System.Collections.Generic; using System.IO; @@ -6,12 +8,10 @@ using Aki.Common.Utils; namespace Aki.Common.Http { + [Obsolete("Request is deprecated, please use Aki.Common.Http.Client instead.")] public class Request { - /// - /// Send a request to remote endpoint and optionally receive a response body. - /// Deflate is the accepted compression format. - /// + [Obsolete("Request.Send() is deprecated, please use Aki.Common.Http.Client instead.")] public byte[] Send(string url, string method, byte[] data = null, bool compress = true, string mime = null, Dictionary headers = null) { if (!WebConstants.IsValidMethod(method)) @@ -81,3 +81,5 @@ namespace Aki.Common.Http } } } + +#endregion \ No newline at end of file diff --git a/project/Aki.Common/Http/RequestHandler.cs b/project/Aki.Common/Http/RequestHandler.cs index 1b73438..22f5d65 100644 --- a/project/Aki.Common/Http/RequestHandler.cs +++ b/project/Aki.Common/Http/RequestHandler.cs @@ -1,49 +1,46 @@ using System; using System.Collections.Generic; using System.Text; -using Aki.Common.Utils; +using System.Threading.Tasks; using BepInEx.Logging; +using Aki.Common.Utils; 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; + public static readonly Client HttpClient; + public static readonly string Host; + public static readonly string SessionId; + public static readonly bool IsLocal; static RequestHandler() { _logger = Logger.CreateLogSource(nameof(RequestHandler)); - Initialize(); - } + + // grab required info from command args + var args = Environment.GetCommandLineArgs(); - private static void Initialize() - { - _request = new Request(); - - string[] args = Environment.GetCommandLineArgs(); - - foreach (string arg in args) + foreach (var arg in args) { if (arg.Contains("BackendUrl")) { - string json = arg.Replace("-config=", string.Empty); - _host = Json.Deserialize(json).BackendUrl; + var 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 } - }; + SessionId = arg.Replace("-token=", string.Empty); } } + + IsLocal = Host.Contains("127.0.0.1") + || Host.Contains("localhost"); + + // initialize http client + HttpClient = new Client(Host, SessionId); } private static void ValidateData(byte[] data) @@ -66,46 +63,129 @@ namespace Aki.Common.Http _logger.LogInfo($"Request was successful"); } - public static byte[] GetData(string path, bool hasHost = false) + public static byte[] GetData(string path) { - string url = (hasHost) ? path : _host + path; + _logger.LogInfo($"Request GET data: {SessionId}:{path}"); + + var data = HttpClient.Get(path); - _logger.LogInfo($"Request GET data: {_session}:{url}"); - byte[] result = _request.Send(url, "GET", null, headers: _headers); - - ValidateData(result); - return result; + ValidateData(data); + return data; } - public static string GetJson(string path, bool hasHost = false) + public static string GetJson(string path) { - string url = (hasHost) ? path : _host + path; + _logger.LogInfo($"Request GET json: {SessionId}:{path}"); + + var payload = HttpClient.Get(path); + var body = Encoding.UTF8.GetString(payload); - _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; + ValidateJson(body); + return body; } - public static string PostJson(string path, string json, bool hasHost = false) + public static string PostJson(string path, string json) { - string url = (hasHost) ? path : _host + path; + _logger.LogInfo($"Request POST json: {SessionId}:{path}"); + + var payload = Encoding.UTF8.GetBytes(json); + var data = HttpClient.Post(path, payload); + var body = Encoding.UTF8.GetString(data); - _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; + ValidateJson(body); + return body; } - public static void PutJson(string path, string json, bool hasHost = false) + public static void PutJson(string path, string json) { - 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); + _logger.LogInfo($"Request PUT json: {SessionId}:{path}"); + + var payload = Encoding.UTF8.GetBytes(json); + HttpClient.Put(path, payload); } + +#region DEPRECATED, REMOVE IN 3.8.1 + [Obsolete("GetData(path, isHost) is deprecated, please use GetData(path) instead.")] + public static byte[] GetData(string path, bool hasHost) + { + var url = (hasHost) ? path : Host + path; + _logger.LogInfo($"Request GET data: {SessionId}:{url}"); + + var headers = new Dictionary() + { + { "Cookie", $"PHPSESSID={SessionId}" }, + { "SessionId", SessionId } + }; + + var request = new Request(); + var data = request.Send(url, "GET", null, headers: headers); + + ValidateData(data); + return data; + + } + + [Obsolete("GetJson(path, isHost) is deprecated, please use GetJson(path) instead.")] + public static string GetJson(string path, bool hasHost) + { + var url = (hasHost) ? path : Host + path; + _logger.LogInfo($"Request GET json: {SessionId}:{url}"); + + var headers = new Dictionary() + { + { "Cookie", $"PHPSESSID={SessionId}" }, + { "SessionId", SessionId } + }; + + var request = new Request(); + var data = request.Send(url, "GET", headers: headers); + var body = Encoding.UTF8.GetString(data); + + ValidateJson(body); + return body; + + } + + [Obsolete("PostJson(path, json, isHost) is deprecated, please use PostJson(path, json) instead.")] + public static string PostJson(string path, string json, bool hasHost) + { + var url = (hasHost) ? path : Host + path; + _logger.LogInfo($"Request POST json: {SessionId}:{url}"); + + var payload = Encoding.UTF8.GetBytes(json); + var mime = WebConstants.Mime[".json"]; + var headers = new Dictionary() + { + { "Cookie", $"PHPSESSID={SessionId}" }, + { "SessionId", SessionId } + }; + + var request = new Request(); + var data = request.Send(url, "POST", payload, true, mime, headers); + var body = Encoding.UTF8.GetString(data); + + ValidateJson(body); + return body; + + } + + [Obsolete("PutJson(path, json, isHost) is deprecated, please use PutJson(path, json) instead.")] + public static void PutJson(string path, string json, bool hasHost) + { + var url = (hasHost) ? path : Host + path; + _logger.LogInfo($"Request PUT json: {SessionId}:{url}"); + + var payload = Encoding.UTF8.GetBytes(json); + var mime = WebConstants.Mime[".json"]; + var headers = new Dictionary() + { + { "Cookie", $"PHPSESSID={SessionId}" }, + { "SessionId", SessionId } + }; + + var request = new Request(); + request.Send(url, "PUT", payload, true, mime, headers); + } +#endregion } } diff --git a/project/Aki.Common/Http/WebConstants.cs b/project/Aki.Common/Http/WebConstants.cs index cdd0873..57b797e 100644 --- a/project/Aki.Common/Http/WebConstants.cs +++ b/project/Aki.Common/Http/WebConstants.cs @@ -1,53 +1,39 @@ -using System.Collections.Generic; +#region DEPRECATED, REMOVE IN 3.8.1 + +using System; +using System.Collections.Generic; using System.Linq; namespace Aki.Common.Http { + [Obsolete("WebConstants is deprecated, please use System.Net.Http functionality instead.")] public static class WebConstants { - /// - /// HTML GET method. - /// + [Obsolete("Get is deprecated, please use HttpMethod.Get instead.")] public const string Get = "GET"; - /// - /// HTML HEAD method. - /// + [Obsolete("Head is deprecated, please use HttpMethod.Head instead.")] public const string Head = "HEAD"; - /// - /// HTML POST method. - /// + [Obsolete("Post is deprecated, please use HttpMethod.Post instead.")] public const string Post = "POST"; - /// - /// HTML PUT method. - /// + [Obsolete("Put is deprecated, please use HttpMethod.Put instead.")] public const string Put = "PUT"; - /// - /// HTML DELETE method. - /// + [Obsolete("Delete is deprecated, please use HttpMethod.Delete instead.")] public const string Delete = "DELETE"; - /// - /// HTML CONNECT method. - /// + [Obsolete("Connect is deprecated, please use HttpMethod.Connect instead.")] public const string Connect = "CONNECT"; - /// - /// HTML OPTIONS method. - /// + [Obsolete("Options is deprecated, please use HttpMethod.Options instead.")] public const string Options = "OPTIONS"; - /// - /// HTML TRACE method. - /// + [Obsolete("Trace is deprecated, please use HttpMethod.Trace instead.")] public const string Trace = "TRACE"; - /// - /// HTML MIME types. - /// + [Obsolete("Mime is deprecated, there is sadly no replacement.")] public static Dictionary Mime { get; private set; } static WebConstants() @@ -68,9 +54,7 @@ namespace Aki.Common.Http }; } - /// - /// Is HTML method valid? - /// + [Obsolete("IsValidMethod is deprecated, please check against HttpMethod entries instead.")] public static bool IsValidMethod(string method) { return method == Get @@ -83,12 +67,12 @@ namespace Aki.Common.Http || method == Trace; } - /// - /// Is MIME type valid? - /// + [Obsolete("isValidMime is deprecated, there is sadly no replacement available.")] public static bool IsValidMime(string mime) { return Mime.Any(x => x.Value == mime); } } } + +#endregion \ No newline at end of file diff --git a/project/Aki.Common/Utils/Zlib.cs b/project/Aki.Common/Utils/Zlib.cs index c48c21d..1f3dcd2 100644 --- a/project/Aki.Common/Utils/Zlib.cs +++ b/project/Aki.Common/Utils/Zlib.cs @@ -1,127 +1,86 @@ -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 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 + public static class Zlib + { + /// + /// Check if the file is ZLib compressed + /// + /// Data + /// If the file is Zlib compressed + public static bool IsCompressed(byte[] data) + { + if (data == null || data.Length < 3) + { + return false; + } - /// - /// 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. + // data[0]: Info (CM/CINFO) Header; must be 0x78 + if (data[0] != 0x78) + { + return false; + } - if (Data == null || Data.Length < 3 || Data[0] != 0x78) - { - return false; - } + // data[1]: Flags (FLG) Header; compression level. + switch (data[1]) + { + case 0x01: // [0x78 0x01] level 0-2: fastest + case 0x5E: // [0x78 0x5E] level 3-4: low + case 0x9C: // [0x78 0x9C] level 5-6: normal + case 0xDA: // [0x78 0xDA] level 7-9: max + return true; + } - switch (Data[1]) - { - case 0x01: // fastest - case 0x5E: // low - case 0x9C: // normal - case 0xDA: // max - return true; - } + return false; + } - return false; - } + private static byte[] Run(byte[] data, ZlibCompression level) + { + // ZOutputStream.Close() flushes itself. + // ZOutputStream.Flush() flushes the target stream. + // It's fucking stupid, but whatever. + // -- Waffle.Lord, 2022-12-01 - /// - /// Deflate data. - /// - public static byte[] Compress(byte[] data, ZlibCompression level) - { - byte[] buffer = new byte[data.Length + 24]; + using (var ms = new MemoryStream()) + { + using (var zs = (level > ZlibCompression.Store) + ? new ZOutputStream(ms, (int)level) + : new ZOutputStream(ms)) + { + zs.Write(data, 0, data.Length); + } + // <-- zs flushes everything here - 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 - }; + return ms.ToArray(); + } + } - 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; - } + /// + /// Deflate data. + /// + public static byte[] Compress(byte[] data, ZlibCompression level) + { + return Run(data, level); + } /// /// 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(); - } - } - } + { + return Run(data, ZlibCompression.Store); + } + } } \ No newline at end of file diff --git a/project/Aki.Core/Aki.Core.csproj b/project/Aki.Core/Aki.Core.csproj index 6448701..f762b57 100644 --- a/project/Aki.Core/Aki.Core.csproj +++ b/project/Aki.Core/Aki.Core.csproj @@ -1,13 +1,14 @@  - net472 + net471 aki-core + Release Aki - Copyright @ Aki 2022 + Copyright @ Aki 2024 diff --git a/project/Aki.Core/AkiCorePlugin.cs b/project/Aki.Core/AkiCorePlugin.cs index eaacee0..db5436d 100644 --- a/project/Aki.Core/AkiCorePlugin.cs +++ b/project/Aki.Core/AkiCorePlugin.cs @@ -8,14 +8,20 @@ namespace Aki.Core [BepInPlugin("com.spt-aki.core", "AKI.Core", AkiPluginInfo.PLUGIN_VERSION)] class AkiCorePlugin : BaseUnityPlugin { + // Temp static logger field, remove along with plugin whitelisting before release + internal static BepInEx.Logging.ManualLogSource _logger; + public void Awake() { + _logger = Logger; + Logger.LogInfo("Loading: Aki.Core"); try { new ConsistencySinglePatch().Enable(); new ConsistencyMultiPatch().Enable(); + new GameValidationPatch().Enable(); new BattlEyePatch().Enable(); new SslCertificatePatch().Enable(); new UnityWebRequestPatch().Enable(); @@ -26,6 +32,7 @@ namespace Aki.Core { Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED"); Logger.LogError($"{GetType().Name}: {ex}"); + throw; } diff --git a/project/Aki.Core/Models/FakeCertificateHandler.cs b/project/Aki.Core/Models/FakeCertificateHandler.cs index 2780b81..3e38a56 100644 --- a/project/Aki.Core/Models/FakeCertificateHandler.cs +++ b/project/Aki.Core/Models/FakeCertificateHandler.cs @@ -5,9 +5,6 @@ namespace Aki.Core.Models { public class FakeCertificateHandler : CertificateHandler { - protected override bool ValidateCertificate(byte[] certificateData) - { - return ValidationUtil.Validate(); - } + protected override bool ValidateCertificate(byte[] certificateData) => true; } } diff --git a/project/Aki.Core/Patches/BattlEyePatch.cs b/project/Aki.Core/Patches/BattlEyePatch.cs index 3fc27b9..8be7e26 100644 --- a/project/Aki.Core/Patches/BattlEyePatch.cs +++ b/project/Aki.Core/Patches/BattlEyePatch.cs @@ -1,9 +1,8 @@ using Aki.Core.Utils; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; -using System.Linq; using System.Reflection; using System.Threading.Tasks; +using HarmonyLib; namespace Aki.Core.Patches { @@ -11,11 +10,7 @@ namespace Aki.Core.Patches { 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); + return AccessTools.Method(typeof(BattleeyePatchClass), nameof(BattleeyePatchClass.RunValidation)); } [PatchPrefix] diff --git a/project/Aki.Core/Patches/ConsistencyMultiPatch.cs b/project/Aki.Core/Patches/ConsistencyMultiPatch.cs index 7a71d8b..ad24902 100644 --- a/project/Aki.Core/Patches/ConsistencyMultiPatch.cs +++ b/project/Aki.Core/Patches/ConsistencyMultiPatch.cs @@ -12,8 +12,8 @@ namespace Aki.Core.Patches { protected override MethodBase GetTargetMethod() { - return PatchConstants.FilesCheckerTypes.Single(x => x.Name == "ConsistencyController") - .GetMethods().Single(x => x.Name == "EnsureConsistency" && x.ReturnType == typeof(Task)); + return PatchConstants.FilesCheckerTypes.SingleCustom(x => x.Name == "ConsistencyController") + .GetMethods().SingleCustom(x => x.Name == "EnsureConsistency" && x.ReturnType == typeof(Task)); } [PatchPrefix] diff --git a/project/Aki.Core/Patches/ConsistencySinglePatch.cs b/project/Aki.Core/Patches/ConsistencySinglePatch.cs index 96447b9..fb0172b 100644 --- a/project/Aki.Core/Patches/ConsistencySinglePatch.cs +++ b/project/Aki.Core/Patches/ConsistencySinglePatch.cs @@ -12,8 +12,8 @@ namespace Aki.Core.Patches { protected override MethodBase GetTargetMethod() { - return PatchConstants.FilesCheckerTypes.Single(x => x.Name == "ConsistencyController") - .GetMethods().Single(x => x.Name == "EnsureConsistencySingle" && x.ReturnType == typeof(Task)); + return PatchConstants.FilesCheckerTypes.SingleCustom(x => x.Name == "ConsistencyController") + .GetMethods().SingleCustom(x => x.Name == "EnsureConsistencySingle" && x.ReturnType == typeof(Task)); } [PatchPrefix] diff --git a/project/Aki.Core/Patches/DataHandlerDebugPatch.cs b/project/Aki.Core/Patches/DataHandlerDebugPatch.cs deleted file mode 100644 index d23c2a3..0000000 --- a/project/Aki.Core/Patches/DataHandlerDebugPatch.cs +++ /dev/null @@ -1,30 +0,0 @@ -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/GameValidationPatch.cs b/project/Aki.Core/Patches/GameValidationPatch.cs new file mode 100644 index 0000000..0311cb4 --- /dev/null +++ b/project/Aki.Core/Patches/GameValidationPatch.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using Aki.Common.Utils; +using Aki.Core.Utils; +using Aki.Reflection.Patching; +using HarmonyLib; + +namespace Aki.Core.Patches +{ + public class GameValidationPatch : ModulePatch + { + private const string PluginName = "Aki.Core"; + private const string ErrorMessage = "Validation failed"; + private static BepInEx.Logging.ManualLogSource _logger = null; + private static bool _hasRun = false; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BattleeyePatchClass), nameof(BattleeyePatchClass.RunValidation)); + } + + [PatchPostfix] + private static void PatchPostfix() + { + if (ValidationUtil.Validate() || _hasRun) + return; + + if (_logger == null) + _logger = BepInEx.Logging.Logger.CreateLogSource(PluginName); + + _hasRun = true; + ServerLog.Warn($"Warning: {PluginName}", ErrorMessage); + _logger?.LogWarning(ErrorMessage); + } + } +} \ No newline at end of file diff --git a/project/Aki.Core/Patches/SslCertificatePatch.cs b/project/Aki.Core/Patches/SslCertificatePatch.cs index 972ca66..f2c7f5b 100644 --- a/project/Aki.Core/Patches/SslCertificatePatch.cs +++ b/project/Aki.Core/Patches/SslCertificatePatch.cs @@ -1,25 +1,23 @@ -using System.Linq; using System.Reflection; -using UnityEngine.Networking; +using System.Security.Cryptography.X509Certificates; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using Aki.Core.Utils; +using HarmonyLib; 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); + { + return AccessTools.Method(typeof(SslCertPatchClass), nameof(SslCertPatchClass.ValidateCertificate), new[] { typeof(X509Certificate) }); } [PatchPrefix] private static bool PatchPrefix(ref bool __result) { - __result = ValidationUtil.Validate(); - return false; // Skip origial + __result = true; + return false; // Skip original } } } diff --git a/project/Aki.Core/Patches/TransportPrefixPatch.cs b/project/Aki.Core/Patches/TransportPrefixPatch.cs index 208b0c6..fc0e19c 100644 --- a/project/Aki.Core/Patches/TransportPrefixPatch.cs +++ b/project/Aki.Core/Patches/TransportPrefixPatch.cs @@ -15,8 +15,7 @@ namespace Aki.Core.Patches { try { - _ = GClass239.DEBUG_LOGIC; // UPDATE BELOW LINE TOO - var type = PatchConstants.EftTypes.Single(t => t.Name == "Class239"); + var type = PatchConstants.EftTypes.SingleOrDefault(t => t.GetField("TransportPrefixes") != null); if (type == null) { @@ -36,18 +35,17 @@ namespace Aki.Core.Patches protected override MethodBase GetTargetMethod() { - return PatchConstants.EftTypes.Single(t => t.GetMethods().Any(m => m.Name == "CreateFromLegacyParams")) + return PatchConstants.EftTypes.SingleCustom(t => t.GetMethods().Any(m => m.Name == "CreateFromLegacyParams")) .GetMethod("CreateFromLegacyParams", BindingFlags.Static | BindingFlags.Public); } [PatchPrefix] - private static bool PatchPrefix(ref GStruct21 legacyParams) + private static bool PatchPrefix(ref LegacyParamsStruct 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 } diff --git a/project/Aki.Core/Patches/WebSocketPatch.cs b/project/Aki.Core/Patches/WebSocketPatch.cs index 856d6f9..aac0b1e 100644 --- a/project/Aki.Core/Patches/WebSocketPatch.cs +++ b/project/Aki.Core/Patches/WebSocketPatch.cs @@ -1,7 +1,6 @@ using Aki.Reflection.Patching; using Aki.Reflection.Utils; using System; -using System.Linq; using System.Reflection; namespace Aki.Core.Patches @@ -10,15 +9,18 @@ namespace Aki.Core.Patches { 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)); + var targetInterface = PatchConstants.EftTypes.SingleCustom(x => x == typeof(IConnectionHandler) && x.IsInterface); + var typeThatMatches = PatchConstants.EftTypes.SingleCustom(x => targetInterface.IsAssignableFrom(x) && x.IsAbstract && !x.IsInterface); + + return typeThatMatches.GetMethods(BindingFlags.Public | BindingFlags.Instance).SingleCustom(x => x.ReturnType == typeof(Uri)); } + // This is a pass through postfix and behaves a little differently than usual + // https://harmony.pardeike.net/articles/patching-postfix.html#pass-through-postfixes [PatchPostfix] - private static Uri PatchPostfix(Uri __instance) + private static Uri PatchPostfix(Uri __result) { - return new Uri(__instance.ToString().Replace("wss:", "ws:")); + return new Uri(__result.ToString().Replace("wss:", "ws:")); } } } diff --git a/project/Aki.Core/Utils/ValidationUtil.cs b/project/Aki.Core/Utils/ValidationUtil.cs index aafa1a3..ab0b24b 100644 --- a/project/Aki.Core/Utils/ValidationUtil.cs +++ b/project/Aki.Core/Utils/ValidationUtil.cs @@ -16,12 +16,12 @@ namespace Aki.Core.Utils var v1 = Registry.LocalMachine.OpenSubKey(c0, false).GetValue("InstallLocation"); var v2 = (v1 != null) ? v1.ToString() : string.Empty; var v3 = new DirectoryInfo(v2); - + var v4 = new FileSystemInfo[] { v3, new FileInfo(Path.Combine(v2, @"BattlEye\BEClient_x64.dll")), - new FileInfo(Path.Combine(v2, @"BattlEye\BEService_x64.dll")), + new FileInfo(Path.Combine(v2, @"BattlEye\BEService_x64.exe")), new FileInfo(Path.Combine(v2, "ConsistencyInfo")), new FileInfo(Path.Combine(v2, "Uninstall.exe")), new FileInfo(Path.Combine(v2, "UnityCrashHandler64.exe")) @@ -55,4 +55,4 @@ namespace Aki.Core.Utils return File.Exists(a) ? new FileInfo(a) : null; } } -} \ No newline at end of file +} diff --git a/project/Aki.Custom/Airdrops/Utils/AirdropUtil.cs b/project/Aki.Custom/Airdrops/Utils/AirdropUtil.cs index 4efc73b..f102606 100644 --- a/project/Aki.Custom/Airdrops/Utils/AirdropUtil.cs +++ b/project/Aki.Custom/Airdrops/Utils/AirdropUtil.cs @@ -86,7 +86,7 @@ namespace Aki.Custom.Airdrops.Utils { var serverConfig = GetConfigFromServer(); var allAirdropPoints = LocationScene.GetAll().ToList(); - var playerPosition = gameWorld.MainPlayer.Position; + var playerPosition = ((IPlayer)gameWorld.MainPlayer).Position; var flareAirdropPoints = new List(); var dropChance = ChanceToSpawn(gameWorld, serverConfig, isFlare); var flareSpawnRadiusDistance = 100f; diff --git a/project/Aki.Custom/Aki.Custom.csproj b/project/Aki.Custom/Aki.Custom.csproj index 284c0f6..e5d4afe 100644 --- a/project/Aki.Custom/Aki.Custom.csproj +++ b/project/Aki.Custom/Aki.Custom.csproj @@ -1,22 +1,26 @@  - net472 + net471 aki-custom + Release Aki - Copyright @ Aki 2022 + Copyright @ Aki 2024 + + + - + @@ -38,6 +42,7 @@ + diff --git a/project/Aki.Custom/AkiCustomPlugin.cs b/project/Aki.Custom/AkiCustomPlugin.cs index 41b7e45..e3d39f8 100644 --- a/project/Aki.Custom/AkiCustomPlugin.cs +++ b/project/Aki.Custom/AkiCustomPlugin.cs @@ -1,10 +1,13 @@ using System; using Aki.Common; using Aki.Custom.Airdrops.Patches; +using Aki.Custom.BTR.Patches; using Aki.Custom.Patches; using Aki.Custom.Utils; -using Aki.SinglePlayer.Patches.ScavMode; +using Aki.Reflection.Utils; +using Aki.SinglePlayer.Utils.MainMenu; using BepInEx; +using UnityEngine; namespace Aki.Custom { @@ -29,9 +32,12 @@ namespace Aki.Custom // Fixed in live, no need for patch //new RaidSettingsWindowPatch().Enable(); new OfflineRaidSettingsMenuPatch().Enable(); - new SessionIdPatch().Enable(); + // new SessionIdPatch().Enable(); new VersionLabelPatch().Enable(); new IsEnemyPatch().Enable(); + new BotCalledDataTryCallPatch().Enable(); + new BotCallForHelpCallBotPatch().Enable(); + new BotOwnerDisposePatch().Enable(); new LocationLootCacheBustingPatch().Enable(); //new AddSelfAsEnemyPatch().Enable(); new CheckAndAddEnemyPatch().Enable(); @@ -41,18 +47,45 @@ namespace Aki.Custom new AirdropFlarePatch().Enable(); new AddSptBotSettingsPatch().Enable(); new CustomAiPatch().Enable(); + new AddTraitorScavsPatch().Enable(); new ExitWhileLootingPatch().Enable(); new QTEPatch().Enable(); new PmcFirstAidPatch().Enable(); new SettingsLocationPatch().Enable(); + new SetLocationIdOnRaidStartPatch().Enable(); //new RankPanelPatch().Enable(); new RagfairFeePatch().Enable(); new ScavQuestPatch().Enable(); + new FixBrokenSpawnOnSandboxPatch().Enable(); + new BTRPathLoadPatch().Enable(); + new BTRActivateTraderDialogPatch().Enable(); + new BTRInteractionPatch().Enable(); + new BTRExtractPassengersPatch().Enable(); + new BTRBotAttachPatch().Enable(); + new BTRReceiveDamageInfoPatch().Enable(); + new BTRTurretCanShootPatch().Enable(); + new BTRTurretDefaultAimingPositionPatch().Enable(); + new BTRIsDoorsClosedPath().Enable(); + new BTRPatch().Enable(); + new BTRTransferItemsPatch().Enable(); + new BTREndRaidItemDeliveryPatch().Enable(); + new BTRDestroyAtRaidEndPatch().Enable(); + new BTRVehicleMovementSpeedPatch().Enable(); + new ScavItemCheckmarkPatch().Enable(); + new ResetTraderServicesPatch().Enable(); + new CultistAmuletRemovalPatch().Enable(); + new HalloweenExtractPatch().Enable(); + new ClampRagdollPatch().Enable(); + + HookObject.AddOrGetComponent(); } catch (Exception ex) { Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED"); Logger.LogError($"{GetType().Name}: {ex}"); + MessageBoxHelper.Show($"A patch in {GetType().Name} FAILED. {ex.Message}. SUBSEQUENT PATCHES HAVE NOT LOADED, CHECK LOG FOR MORE DETAILS", "ERROR", MessageBoxHelper.MessageBoxType.OK); + Application.Quit(); + throw; } diff --git a/project/Aki.Custom/BTR/BTRManager.cs b/project/Aki.Custom/BTR/BTRManager.cs new file mode 100644 index 0000000..36a7d80 --- /dev/null +++ b/project/Aki.Custom/BTR/BTRManager.cs @@ -0,0 +1,518 @@ +using Aki.Custom.BTR.Utils; +using Aki.SinglePlayer.Utils.TraderServices; +using Comfort.Common; +using EFT; +using EFT.InventoryLogic; +using EFT.UI; +using EFT.Vehicle; +using HarmonyLib; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +using Random = UnityEngine.Random; + +namespace Aki.Custom.BTR +{ + public class BTRManager : MonoBehaviour + { + private GameWorld gameWorld; + private BotEventHandler botEventHandler; + + private BotBTRService btrBotService; + private BTRControllerClass btrController; + private BTRVehicle btrServerSide; + private BTRView btrClientSide; + private BotOwner btrBotShooter; + private BTRDataPacket btrDataPacket = default; + private bool btrBotShooterInitialized = false; + + private float coverFireTime = 90f; + private Coroutine _coverFireTimerCoroutine; + + private BTRSide lastInteractedBtrSide; + public BTRSide LastInteractedBtrSide => lastInteractedBtrSide; + + private Coroutine _shootingTargetCoroutine; + private BTRTurretServer btrTurretServer; + private bool isTurretInDefaultRotation; + private EnemyInfo currentTarget = null; + private bool isShooting = false; + private float machineGunAimDelay = 0.4f; + private Vector2 machineGunBurstCount; + private Vector2 machineGunRecoveryTime; + private BulletClass btrMachineGunAmmo; + private Item btrMachineGunWeapon; + private Player.FirearmController firearmController; + private WeaponSoundPlayer weaponSoundPlayer; + + private MethodInfo _updateTaxiPriceMethod; + + private float originalDamageCoeff; + + BTRManager() + { + Type btrControllerType = typeof(BTRControllerClass); + _updateTaxiPriceMethod = AccessTools.GetDeclaredMethods(btrControllerType).Single(IsUpdateTaxiPriceMethod); + } + + private void Awake() + { + try + { + gameWorld = Singleton.Instance; + if (gameWorld == null) + { + Destroy(this); + return; + } + + if (gameWorld.BtrController == null) + { + gameWorld.BtrController = new BTRControllerClass(); + } + + btrController = gameWorld.BtrController; + + InitBtr(); + } + catch + { + ConsoleScreen.LogError("[AKI-BTR] Unable to spawn BTR. Check logs."); + Destroy(this); + throw; + } + } + + public void OnPlayerInteractDoor(PlayerInteractPacket interactPacket) + { + btrServerSide.LeftSlot0State = 0; + btrServerSide.LeftSlot1State = 0; + btrServerSide.RightSlot0State = 0; + btrServerSide.RightSlot1State = 0; + + bool playerGoIn = interactPacket.InteractionType == EInteractionType.GoIn; + + if (interactPacket.SideId == 0 && playerGoIn) + { + if (interactPacket.SlotId == 0) + { + btrServerSide.LeftSlot0State = 1; + } + else if (interactPacket.SlotId == 1) + { + btrServerSide.LeftSlot1State = 1; + } + } + else if (interactPacket.SideId == 1 && playerGoIn) + { + if (interactPacket.SlotId == 0) + { + btrServerSide.RightSlot0State = 1; + } + else if (interactPacket.SlotId == 1) + { + btrServerSide.RightSlot1State = 1; + } + } + + // If the player is going into the BTR, store their damage coefficient + // and set it to 0, so they don't die while inside the BTR + if (interactPacket.InteractionType == EInteractionType.GoIn) + { + originalDamageCoeff = gameWorld.MainPlayer.ActiveHealthController.DamageCoeff; + gameWorld.MainPlayer.ActiveHealthController.SetDamageCoeff(0f); + + } + // Otherwise restore the damage coefficient + else if (interactPacket.InteractionType == EInteractionType.GoOut) + { + gameWorld.MainPlayer.ActiveHealthController.SetDamageCoeff(originalDamageCoeff); + } + } + + // Find `BTRControllerClass.method_9(PathDestination currentDestinationPoint, bool lastRoutePoint)` + private bool IsUpdateTaxiPriceMethod(MethodInfo method) + { + return (method.GetParameters().Length == 2 && method.GetParameters()[0].ParameterType == typeof(PathDestination)); + } + + private void Update() + { + btrController.SyncBTRVehicleFromServer(UpdateDataPacket()); + + if (btrController.BotShooterBtr == null) return; + + // BotShooterBtr doesn't get assigned to BtrController immediately so we check this in Update + if (!btrBotShooterInitialized) + { + InitBtrBotService(); + btrBotShooterInitialized = true; + } + + UpdateTarget(); + + if (HasTarget()) + { + SetAim(); + + if (!isShooting && CanShoot()) + { + StartShooting(); + } + } + else if (!isTurretInDefaultRotation) + { + btrTurretServer.DisableAiming(); + } + } + + private void InitBtr() + { + // Initial setup + botEventHandler = Singleton.Instance; + var botsController = Singleton.Instance.BotsController; + btrBotService = botsController.BotTradersServices.BTRServices; + btrController.method_3(); // spawns server-side BTR game object + botsController.BotSpawner.SpawnBotBTR(); // spawns the scav bot which controls the BTR's turret + + // Initial BTR configuration + btrServerSide = btrController.BtrVehicle; + btrClientSide = btrController.BtrView; + btrServerSide.transform.Find("KillBox").gameObject.AddComponent(); + + // Get config from server and initialise respective settings + ConfigureSettingsFromServer(); + + var btrMapConfig = btrController.MapPathsConfiguration; + btrServerSide.CurrentPathConfig = btrMapConfig.PathsConfiguration.pathsConfigurations.RandomElement(); + btrServerSide.Initialization(btrMapConfig); + btrController.method_14(); // creates and assigns the BTR a fake stash + + DisableServerSideRenderers(); + + gameWorld.MainPlayer.OnBtrStateChanged += HandleBtrDoorState; + + btrServerSide.MoveEnable(); + btrServerSide.IncomingToDestinationEvent += ToDestinationEvent; + + // Sync initial position and rotation + UpdateDataPacket(); + btrClientSide.transform.position = btrDataPacket.position; + btrClientSide.transform.rotation = btrDataPacket.rotation; + + // Initialise turret variables + btrTurretServer = btrServerSide.BTRTurret; + var btrTurretDefaultTargetTransform = (Transform)AccessTools.Field(btrTurretServer.GetType(), "defaultTargetTransform").GetValue(btrTurretServer); + isTurretInDefaultRotation = btrTurretServer.targetTransform == btrTurretDefaultTargetTransform + && btrTurretServer.targetPosition == btrTurretServer.defaultAimingPosition; + btrMachineGunAmmo = (BulletClass)BTRUtil.CreateItem(BTRUtil.BTRMachineGunAmmoTplId); + btrMachineGunWeapon = BTRUtil.CreateItem(BTRUtil.BTRMachineGunWeaponTplId); + + // Pull services data for the BTR from the server + TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId); + } + + private void ConfigureSettingsFromServer() + { + var serverConfig = BTRUtil.GetConfigFromServer(); + + btrServerSide.moveSpeed = serverConfig.MoveSpeed; + btrServerSide.pauseDurationRange.x = serverConfig.PointWaitTime.Min; + btrServerSide.pauseDurationRange.y = serverConfig.PointWaitTime.Max; + btrServerSide.readyToDeparture = serverConfig.TaxiWaitTime; + coverFireTime = serverConfig.CoverFireTime; + machineGunAimDelay = serverConfig.MachineGunAimDelay; + machineGunBurstCount = new Vector2(serverConfig.MachineGunBurstCount.Min, serverConfig.MachineGunBurstCount.Max); + machineGunRecoveryTime = new Vector2(serverConfig.MachineGunRecoveryTime.Min, serverConfig.MachineGunRecoveryTime.Max); + } + + private void InitBtrBotService() + { + btrBotShooter = btrController.BotShooterBtr; + firearmController = btrBotShooter.GetComponent(); + var weaponPrefab = (WeaponPrefab)AccessTools.Field(firearmController.GetType(), "weaponPrefab_0").GetValue(firearmController); + weaponSoundPlayer = weaponPrefab.GetComponent(); + + btrBotService.Reset(); // Player will be added to Neutrals list and removed from Enemies list + TraderServicesManager.Instance.OnTraderServicePurchased += BtrTraderServicePurchased; + } + + /** + * BTR has arrived at a destination, re-calculate taxi prices and remove purchased taxi service + */ + private void ToDestinationEvent(PathDestination destinationPoint, bool isFirst, bool isFinal, bool isLastRoutePoint) + { + // Remove purchased taxi service + TraderServicesManager.Instance.RemovePurchasedService(ETraderServiceType.PlayerTaxi, BTRUtil.BTRTraderId); + + // Update the prices for the taxi service + _updateTaxiPriceMethod.Invoke(btrController, new object[] { destinationPoint, isFinal }); + + // Update the UI + TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId); + } + + private bool IsBtrService(ETraderServiceType serviceType) + { + if (serviceType == ETraderServiceType.BtrItemsDelivery + || serviceType == ETraderServiceType.PlayerTaxi + || serviceType == ETraderServiceType.BtrBotCover) + { + return true; + } + + return false; + } + + private void BtrTraderServicePurchased(ETraderServiceType serviceType, string subserviceId) + { + if (!IsBtrService(serviceType)) + { + return; + } + + List passengers = gameWorld.AllAlivePlayersList.Where(x => x.BtrState == EPlayerBtrState.Inside).ToList(); + List playersToNotify = passengers.Select(x => x.Id).ToList(); + btrController.method_6(playersToNotify, serviceType); // notify BTR passengers that a service has been purchased + + switch (serviceType) + { + case ETraderServiceType.BtrBotCover: + botEventHandler.ApplyTraderServiceBtrSupport(passengers); + StartCoverFireTimer(coverFireTime); + break; + case ETraderServiceType.PlayerTaxi: + btrController.BtrVehicle.IsPaid = true; + btrController.BtrVehicle.MoveToDestination(subserviceId); + break; + } + } + + private void StartCoverFireTimer(float time) + { + _coverFireTimerCoroutine = StaticManager.BeginCoroutine(CoverFireTimer(time)); + } + + private IEnumerator CoverFireTimer(float time) + { + yield return new WaitForSecondsRealtime(time); + botEventHandler.StopTraderServiceBtrSupport(); + } + + private void HandleBtrDoorState(EPlayerBtrState playerBtrState) + { + if (playerBtrState == EPlayerBtrState.GoIn || playerBtrState == EPlayerBtrState.GoOut) + { + // Open Door + UpdateBTRSideDoorState(1); + } + else if (playerBtrState == EPlayerBtrState.Inside || playerBtrState == EPlayerBtrState.Outside) + { + // Close Door + UpdateBTRSideDoorState(0); + } + } + + private void UpdateBTRSideDoorState(byte state) + { + try + { + var player = gameWorld.MainPlayer; + + BTRSide btrSide = player.BtrInteractionSide != null ? player.BtrInteractionSide : lastInteractedBtrSide; + byte sideId = btrClientSide.GetSideId(btrSide); + switch (sideId) + { + case 0: + btrServerSide.LeftSideState = state; + break; + case 1: + btrServerSide.RightSideState = state; + break; + } + + lastInteractedBtrSide = player.BtrInteractionSide; + } + catch + { + ConsoleScreen.LogError("[AKI-BTR] lastInteractedBtrSide is null when it shouldn't be. Check logs."); + throw; + } + } + + private BTRDataPacket UpdateDataPacket() + { + btrDataPacket.position = btrServerSide.transform.position; + btrDataPacket.rotation = btrServerSide.transform.rotation; + if (btrTurretServer != null && btrTurretServer.gunsBlockRoot != null) + { + btrDataPacket.turretRotation = btrTurretServer.transform.rotation; + btrDataPacket.gunsBlockRotation = btrTurretServer.gunsBlockRoot.rotation; + } + btrDataPacket.State = (byte)btrServerSide.BtrState; + btrDataPacket.RouteState = (byte)btrServerSide.VehicleRouteState; + btrDataPacket.LeftSideState = btrServerSide.LeftSideState; + btrDataPacket.LeftSlot0State = btrServerSide.LeftSlot0State; + btrDataPacket.LeftSlot1State = btrServerSide.LeftSlot1State; + btrDataPacket.RightSideState = btrServerSide.RightSideState; + btrDataPacket.RightSlot0State = btrServerSide.RightSlot0State; + btrDataPacket.RightSlot1State = btrServerSide.RightSlot1State; + btrDataPacket.currentSpeed = btrServerSide.currentSpeed; + btrDataPacket.timeToEndPause = btrServerSide.timeToEndPause; + btrDataPacket.moveDirection = (byte)btrServerSide.VehicleMoveDirection; + btrDataPacket.MoveSpeed = btrServerSide.moveSpeed; + if (btrController != null && btrController.BotShooterBtr != null) + { + btrDataPacket.BtrBotId = btrController.BotShooterBtr.Id; + } + + return btrDataPacket; + } + + private void DisableServerSideRenderers() + { + var meshRenderers = btrServerSide.transform.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) + { + renderer.enabled = false; + } + + btrServerSide.turnCheckerObject.GetComponent().enabled = false; // Disables the red debug sphere + } + + private void UpdateTarget() + { + currentTarget = btrBotShooter.Memory.GoalEnemy; + } + + private bool HasTarget() + { + if (currentTarget != null) + { + return true; + } + + return false; + } + + private void SetAim() + { + if (currentTarget.IsVisible) + { + Vector3 targetPos = currentTarget.CurrPosition; + Transform targetTransform = currentTarget.Person.Transform.Original; + if (btrTurretServer.CheckPositionInAimingZone(targetPos) && btrTurretServer.targetTransform != targetTransform) + { + btrTurretServer.EnableAimingObject(targetTransform); + } + } + else + { + Vector3 targetLastPos = currentTarget.EnemyLastPositionReal; + if (btrTurretServer.CheckPositionInAimingZone(targetLastPos) + && Time.time - currentTarget.PersonalLastSeenTime < 3f + && btrTurretServer.targetPosition != targetLastPos) + { + btrTurretServer.EnableAimingPosition(targetLastPos); + + } + else if (Time.time - currentTarget.PersonalLastSeenTime >= 3f && !isTurretInDefaultRotation) + { + btrTurretServer.DisableAiming(); + } + } + } + + private bool CanShoot() + { + if (currentTarget.IsVisible && btrBotShooter.BotBtrData.CanShoot()) + { + return true; + } + + return false; + } + + private void StartShooting() + { + _shootingTargetCoroutine = StaticManager.BeginCoroutine(ShootMachineGun()); + } + + /// + /// Custom method to make the BTR coaxial machine gun shoot. + /// + private IEnumerator ShootMachineGun() + { + isShooting = true; + + yield return new WaitForSecondsRealtime(machineGunAimDelay); + if (currentTarget?.Person == null || currentTarget?.IsVisible == false || !btrBotShooter.BotBtrData.CanShoot()) + { + isShooting = false; + yield break; + } + + Transform machineGunMuzzle = btrTurretServer.machineGunLaunchPoint; + var ballisticCalculator = gameWorld.SharedBallisticsCalculator; + + int burstMin = Mathf.FloorToInt(machineGunBurstCount.x); + int burstMax = Mathf.FloorToInt(machineGunBurstCount.y); + int burstCount = Random.Range(burstMin, burstMax + 1); + Vector3 targetHeadPos = currentTarget.Person.PlayerBones.Head.position; + while (burstCount > 0) + { + // Only update shooting position if the target isn't null + if (currentTarget?.Person != null) + { + targetHeadPos = currentTarget.Person.PlayerBones.Head.position; + } + Vector3 aimDirection = Vector3.Normalize(targetHeadPos - machineGunMuzzle.position); + ballisticCalculator.Shoot(btrMachineGunAmmo, machineGunMuzzle.position, aimDirection, btrBotShooter.ProfileId, btrMachineGunWeapon, 1f, 0); + firearmController.method_54(weaponSoundPlayer, btrMachineGunAmmo, machineGunMuzzle.position, aimDirection, false); + burstCount--; + yield return new WaitForSecondsRealtime(0.092308f); // 650 RPM + } + + float waitTime = Random.Range(machineGunRecoveryTime.x, machineGunRecoveryTime.y); + yield return new WaitForSecondsRealtime(waitTime); + + isShooting = false; + } + + private void OnDestroy() + { + if (gameWorld == null) + { + return; + } + + StaticManager.KillCoroutine(ref _shootingTargetCoroutine); + StaticManager.KillCoroutine(ref _coverFireTimerCoroutine); + + if (TraderServicesManager.Instance != null) + { + TraderServicesManager.Instance.OnTraderServicePurchased -= BtrTraderServicePurchased; + } + + if (gameWorld.MainPlayer != null) + { + gameWorld.MainPlayer.OnBtrStateChanged -= HandleBtrDoorState; + } + + if (btrClientSide != null) + { + Debug.LogWarning("[AKI-BTR] BTRManager - Destroying btrClientSide"); + Destroy(btrClientSide.gameObject); + } + + if (btrServerSide != null) + { + Debug.LogWarning("[AKI-BTR] BTRManager - Destroying btrServerSide"); + Destroy(btrServerSide.gameObject); + } + } + } +} diff --git a/project/Aki.Custom/BTR/BTRRoadKillTrigger.cs b/project/Aki.Custom/BTR/BTRRoadKillTrigger.cs new file mode 100644 index 0000000..8b819eb --- /dev/null +++ b/project/Aki.Custom/BTR/BTRRoadKillTrigger.cs @@ -0,0 +1,38 @@ +using EFT; +using EFT.Interactive; +using UnityEngine; + +namespace Aki.Custom.BTR +{ + public class BTRRoadKillTrigger : DamageTrigger + { + public override bool IsStatic => false; + + public override void AddPenalty(GInterface94 player) + { + } + + public override void PlaySound() + { + } + + public override void ProceedDamage(GInterface94 player, BodyPartCollider bodyPart) + { + bodyPart.ApplyInstantKill(new DamageInfo() + { + Damage = 9999f, + Direction = Vector3.zero, + HitCollider = bodyPart.Collider, + HitNormal = Vector3.zero, + HitPoint = Vector3.zero, + DamageType = EDamageType.Btr, + HittedBallisticCollider = bodyPart, + Player = null + }); + } + + public override void RemovePenalty(GInterface94 player) + { + } + } +} diff --git a/project/Aki.Custom/BTR/Models/BtrConfigModel.cs b/project/Aki.Custom/BTR/Models/BtrConfigModel.cs new file mode 100644 index 0000000..ec48fba --- /dev/null +++ b/project/Aki.Custom/BTR/Models/BtrConfigModel.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Aki.Custom.BTR.Models +{ + public class BTRConfigModel + { + [JsonProperty("moveSpeed")] + public float MoveSpeed { get; set; } + + [JsonProperty("coverFireTime")] + public float CoverFireTime { get; set; } + + [JsonProperty("pointWaitTime")] + public BtrMinMaxValue PointWaitTime { get; set; } + + [JsonProperty("taxiWaitTime")] + public float TaxiWaitTime { get; set; } + + [JsonProperty("machineGunAimDelay")] + public float MachineGunAimDelay { get; set; } + + [JsonProperty("machineGunBurstCount")] + public BtrMinMaxValue MachineGunBurstCount { get; set; } + + [JsonProperty("machineGunRecoveryTime")] + public BtrMinMaxValue MachineGunRecoveryTime { get; set; } + } + + public class BtrMinMaxValue + { + [JsonProperty("min")] + public float Min { get; set; } + + [JsonProperty("max")] + public float Max { get; set; } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRActivateTraderDialogPatch.cs b/project/Aki.Custom/BTR/Patches/BTRActivateTraderDialogPatch.cs new file mode 100644 index 0000000..d0c44a1 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRActivateTraderDialogPatch.cs @@ -0,0 +1,55 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.UI.Screens; +using EFT.Vehicle; +using HarmonyLib; +using System; +using System.Reflection; +using BTRDialog = EFT.UI.TraderDialogScreen.GClass3132; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRActivateTraderDialogPatch : ModulePatch + { + private static FieldInfo _playerInventoryControllerField; + private static FieldInfo _playerQuestControllerField; + + protected override MethodBase GetTargetMethod() + { + _playerInventoryControllerField = AccessTools.Field(typeof(Player), "_inventoryController"); + _playerQuestControllerField = AccessTools.Field(typeof(Player), "_questController"); + + var targetType = AccessTools.FirstInner(typeof(GetActionsClass), IsTargetType); + return AccessTools.Method(targetType, "method_2"); + } + + private bool IsTargetType(Type type) + { + FieldInfo btrField = type.GetField("btr"); + + if (btrField != null && btrField.FieldType == typeof(BTRSide)) + { + return true; + } + + return false; + } + + [PatchPrefix] + private static bool PatchPrefix() + { + var gameWorld = Singleton.Instance; + var player = gameWorld.MainPlayer; + + InventoryControllerClass inventoryController = _playerInventoryControllerField.GetValue(player) as InventoryControllerClass; + AbstractQuestControllerClass questController = _playerQuestControllerField.GetValue(player) as AbstractQuestControllerClass; + + BTRDialog btrDialog = new BTRDialog(player.Profile, Profile.TraderInfo.TraderServiceToId[Profile.ETraderServiceSource.Btr], questController, inventoryController, null); + btrDialog.OnClose += player.UpdateInteractionCast; + btrDialog.ShowScreen(EScreenState.Queued); + + return false; + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRBotAttachPatch.cs b/project/Aki.Custom/BTR/Patches/BTRBotAttachPatch.cs new file mode 100644 index 0000000..7c8312f --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRBotAttachPatch.cs @@ -0,0 +1,167 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.AssetsManager; +using EFT.NextObservedPlayer; +using EFT.Vehicle; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace Aki.Custom.BTR.Patches +{ + // Fixes the BTR Bot initialization in AttachBot() of BTRTurretView + // + // Context: + // ClientGameWorld in LiveEFT will register the server-side BTR Bot as type ObservedPlayerView and is stored in GameWorld's allObservedPlayersByID dictionary. + // In SPT, GameWorld.allObservedPlayersByID is empty which results in the game never finishing the initialization of the BTR Bot which includes disabling its gun, voice and mesh renderers. + // + // This is essentially a full reimplementation of the BTRTurretView class, but using Player instead of ObservedPlayerView. + // + public class BTRBotAttachPatch : ModulePatch + { + private static FieldInfo _valueTuple0Field; + private static FieldInfo _gunModsToDisableField; + private static FieldInfo _weaponPrefab0Field; + private static readonly List rendererList = new List(256); + + protected override MethodBase GetTargetMethod() + { + var targetType = typeof(BTRTurretView); + + _valueTuple0Field = AccessTools.Field(targetType, "valueTuple_0"); + _gunModsToDisableField = AccessTools.Field(targetType, "_gunModsToDisable"); + _weaponPrefab0Field = AccessTools.Field(typeof(Player.FirearmController), "weaponPrefab_0"); + + return AccessTools.Method(targetType, nameof(BTRTurretView.AttachBot)); + } + + [PatchPrefix] + private static bool PatchPrefix(BTRTurretView __instance, int btrBotId) + { + var gameWorld = Singleton.Instance; + if (gameWorld == null) + { + return false; + } + + // Find the BTR turret + var alivePlayersList = gameWorld.AllAlivePlayersList; + Player turretPlayer = alivePlayersList.FirstOrDefault(x => x.Id == btrBotId); + if (turretPlayer == null) + { + return false; + } + + // Init the turret view + var valueTuple = (ValueTuple)_valueTuple0Field.GetValue(__instance); + if (!valueTuple.Item2 && !InitTurretView(__instance, turretPlayer)) + { + Logger.LogError("[AKI-BTR] BTRBotAttachPatch - BtrBot initialization failed"); + return false; + } + + WeaponPrefab weaponPrefab; + Transform transform; + if (FindTurretObjects(turretPlayer, out weaponPrefab, out transform)) + { + weaponPrefab.transform.SetPositionAndRotation(__instance.GunRoot.position, __instance.GunRoot.rotation); + transform.SetPositionAndRotation(__instance.GunRoot.position, __instance.GunRoot.rotation); + } + + return false; + } + + private static bool InitTurretView(BTRTurretView btrTurretView, Player turretPlayer) + { + EnableTurretObjects(btrTurretView, turretPlayer, false); + + // We only use this for tracking whether the turret is initialized, so we don't need to set the ObservedPlayerView + _valueTuple0Field.SetValue(btrTurretView, new ValueTuple(null, true)); + return true; + } + + private static void EnableTurretObjects(BTRTurretView btrTurretView, Player player, bool enable) + { + // Find the turret weapon transform + WeaponPrefab weaponPrefab; + Transform weaponTransform; + if (!FindTurretObjects(player, out weaponPrefab, out weaponTransform)) + { + return; + } + + // Hide the turret bot + SetVisible(player, weaponPrefab, false); + + // Disable the components we need to disaable + var _gunModsToDisable = (string[])_gunModsToDisableField.GetValue(btrTurretView); + foreach (Transform child in weaponTransform) + { + if (_gunModsToDisable.Contains(child.name)) + { + child.gameObject.SetActive(enable); + } + } + } + + private static bool FindTurretObjects(Player player, out WeaponPrefab weaponPrefab, out Transform weapon) + { + // Find the WeaponPrefab and Transform of the turret weapon + var aiFirearmController = player.gameObject.GetComponent(); + weaponPrefab = (WeaponPrefab)_weaponPrefab0Field.GetValue(aiFirearmController); + + if (weaponPrefab == null) + { + weapon = null; + return false; + } + + weapon = weaponPrefab.Hierarchy.GetTransform(ECharacterWeaponBones.weapon); + return weapon != null; + } + + /** + * A re-implementation of the ObservedPlayerController.Culling.Mode setter that works for a Player object + */ + private static void SetVisible(Player player, WeaponPrefab weaponPrefab, bool isVisible) + { + // Toggle any animators and colliders + if (player.HealthController.IsAlive) + { + IAnimator bodyAnimatorCommon = player.GetBodyAnimatorCommon(); + if (bodyAnimatorCommon.enabled != isVisible) + { + bool flag = !bodyAnimatorCommon.enabled; + bodyAnimatorCommon.enabled = isVisible; + FirearmsAnimator firearmsAnimator = player.HandsController.FirearmsAnimator; + if (firearmsAnimator != null && firearmsAnimator.Animator.enabled != isVisible) + { + firearmsAnimator.Animator.enabled = isVisible; + } + } + + PlayerPoolObject component = player.gameObject.GetComponent(); + foreach (Collider collider in component.Colliders) + { + if (collider.enabled != isVisible) + { + collider.enabled = isVisible; + } + } + } + + // Build a list of renderers for this player object and set their rendering state + rendererList.Clear(); + player.PlayerBody.GetRenderersNonAlloc(rendererList); + if (weaponPrefab != null) + { + rendererList.AddRange(weaponPrefab.Renderers); + } + rendererList.ForEach(renderer => renderer.forceRenderingOff = !isVisible); + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRDestroyAtRaidEndPatch.cs b/project/Aki.Custom/BTR/Patches/BTRDestroyAtRaidEndPatch.cs new file mode 100644 index 0000000..ab399eb --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRDestroyAtRaidEndPatch.cs @@ -0,0 +1,34 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using HarmonyLib; +using System.Reflection; +using Object = UnityEngine.Object; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRDestroyAtRaidEndPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BaseLocalGame), nameof(BaseLocalGame.Stop)); + } + + [PatchPrefix] + private static void PatchPrefix() + { + var gameWorld = Singleton.Instance; + if (gameWorld == null) + { + return; + } + + var btrManager = gameWorld.GetComponent(); + if (btrManager != null) + { + Logger.LogWarning("[AKI-BTR] BTRDestroyAtRaidEndPatch - Raid Ended: Destroying BTRManager"); + Object.Destroy(btrManager); + } + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTREndRaidItemDeliveryPatch.cs b/project/Aki.Custom/BTR/Patches/BTREndRaidItemDeliveryPatch.cs new file mode 100644 index 0000000..8eec3b3 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTREndRaidItemDeliveryPatch.cs @@ -0,0 +1,67 @@ +using Aki.Common.Http; +using Aki.Custom.BTR.Utils; +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using Comfort.Common; +using EFT; +using HarmonyLib; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Reflection; + +namespace Aki.Custom.BTR.Patches +{ + public class BTREndRaidItemDeliveryPatch : ModulePatch + { + private static JsonConverter[] _defaultJsonConverters; + + protected override MethodBase GetTargetMethod() + { + var converterClass = typeof(AbstractGame).Assembly.GetTypes() + .First(t => t.GetField("Converters", BindingFlags.Static | BindingFlags.Public) != null); + _defaultJsonConverters = Traverse.Create(converterClass).Field("Converters").Value; + + Type baseLocalGameType = PatchConstants.LocalGameType.BaseType; + return AccessTools.Method(baseLocalGameType, nameof(LocalGame.Stop)); + } + + [PatchPrefix] + public static void PatchPrefix() + { + GameWorld gameWorld = Singleton.Instance; + if (gameWorld == null) + { + Logger.LogError("[AKI-BTR] BTREndRaidItemDeliveryPatch - GameWorld is null"); + return; + } + var player = gameWorld.MainPlayer; + if (player == null) + { + Logger.LogError("[AKI-BTR] BTREndRaidItemDeliveryPatch - Player is null"); + return; + } + + // Match doesn't have a BTR + if (gameWorld.BtrController == null) + { + return; + } + + if (!gameWorld.BtrController.HasNonEmptyTransferContainer(player.Profile.Id)) + { + Logger.LogDebug("[AKI-BTR] BTREndRaidItemDeliveryPatch - No items in transfer container"); + return; + } + + var btrStash = gameWorld.BtrController.GetOrAddTransferContainer(player.Profile.Id); + var flatItems = Singleton.Instance.TreeToFlatItems(btrStash.Grid.Items); + + RequestHandler.PutJson("/singleplayer/traderServices/itemDelivery", new + { + items = flatItems, + traderId = BTRUtil.BTRTraderId + }.ToJson(_defaultJsonConverters)); + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRExtractPassengersPatch.cs b/project/Aki.Custom/BTR/Patches/BTRExtractPassengersPatch.cs new file mode 100644 index 0000000..68e5744 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRExtractPassengersPatch.cs @@ -0,0 +1,48 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.Vehicle; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRExtractPassengersPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(VehicleBase), nameof(VehicleBase.ExtractPassengers)); + } + + [PatchPrefix] + private static void PatchPrefix() + { + var gameWorld = Singleton.Instance; + var player = gameWorld.MainPlayer; + var btrManager = gameWorld.GetComponent(); + + var btrSide = btrManager.LastInteractedBtrSide; + if (btrSide == null) + { + return; + } + + if (btrSide.TryGetCachedPlace(out byte b)) + { + var interactionBtrPacket = btrSide.GetInteractWithBtrPacket(b, EInteractionType.GoOut); + if (interactionBtrPacket.HasInteraction) + { + BTRView btrView = gameWorld.BtrController.BtrView; + if (btrView == null) + { + Logger.LogError($"[AKI-BTR] BTRExtractPassengersPatch - btrView is null"); + return; + } + + btrView.Interaction(player, interactionBtrPacket); + btrManager.OnPlayerInteractDoor(interactionBtrPacket); + } + } + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRInteractionPatch.cs b/project/Aki.Custom/BTR/Patches/BTRInteractionPatch.cs new file mode 100644 index 0000000..82727d2 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRInteractionPatch.cs @@ -0,0 +1,58 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.GlobalEvents; +using EFT.Vehicle; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRInteractionPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.FirstMethod(typeof(Player), IsTargetMethod); + } + + /** + * Find the "BtrInteraction" method that takes parameters + */ + private bool IsTargetMethod(MethodBase method) + { + return method.Name == nameof(Player.BtrInteraction) && method.GetParameters().Length > 0; + } + + [PatchPostfix] + private static void PatchPostfix(Player __instance, BTRSide btr, byte placeId, EInteractionType interaction) + { + var gameWorld = Singleton.Instance; + var btrManager = gameWorld.GetComponent(); + + var interactionBtrPacket = btr.GetInteractWithBtrPacket(placeId, interaction); + __instance.UpdateInteractionCast(); + + // Prevent player from entering BTR when blacklisted + var btrBot = gameWorld.BtrController.BotShooterBtr; + if (btrBot.BotsGroup.Enemies.ContainsKey(__instance)) + { + // Notify player they are blacklisted from entering BTR + GlobalEventHandlerClass.CreateEvent().Invoke(__instance.Id, EBtrInteractionStatus.Blacklisted); + return; + } + + if (interactionBtrPacket.HasInteraction) + { + BTRView btrView = gameWorld.BtrController.BtrView; + if (btrView == null) + { + Logger.LogError("[AKI-BTR] BTRInteractionPatch - btrView is null"); + return; + } + + btrView.Interaction(__instance, interactionBtrPacket); + btrManager.OnPlayerInteractDoor(interactionBtrPacket); + } + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRIsDoorsClosedPatch.cs b/project/Aki.Custom/BTR/Patches/BTRIsDoorsClosedPatch.cs new file mode 100644 index 0000000..379b893 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRIsDoorsClosedPatch.cs @@ -0,0 +1,42 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRIsDoorsClosedPath : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(VehicleBase), nameof(VehicleBase.IsDoorsClosed)); + } + + [PatchPrefix] + private static bool PatchPrefix(ref bool __result) + { + var gameWorld = Singleton.Instance; + if (gameWorld == null) + { + Logger.LogError("[AKI-BTR] BTRIsDoorsClosedPatch - GameWorld is null"); + return true; + } + + var serverSideBTR = gameWorld.BtrController.BtrVehicle; + if (serverSideBTR == null) + { + Logger.LogError("[AKI-BTR] BTRIsDoorsClosedPatch - serverSideBTR is null"); + return true; + } + + if (serverSideBTR.LeftSideState == 0 && serverSideBTR.RightSideState == 0) + { + __result = true; + return false; + } + + return true; + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRPatch.cs b/project/Aki.Custom/BTR/Patches/BTRPatch.cs new file mode 100644 index 0000000..8a5d782 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRPatch.cs @@ -0,0 +1,47 @@ +using System.Linq; +using System.Reflection; +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.UI; +using HarmonyLib; + +namespace Aki.Custom.BTR.Patches +{ + /// + /// Adds a BTRManager component to the GameWorld game object when raid starts. + /// + public class BTRPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + // Note: This may seem like a weird place to hook, but `SetTime` requires that the BtrController + // exist and be setup, so we'll use this as the entry point + return AccessTools.Method(typeof(ExtractionTimersPanel), nameof(ExtractionTimersPanel.SetTime)); + } + + [PatchPrefix] + private static void PatchPrefix() + { + try + { + var btrSettings = Singleton.Instance.BTRSettings; + var gameWorld = Singleton.Instance; + + // Only run on maps that have the BTR enabled + string location = gameWorld.MainPlayer.Location; + if (!btrSettings.LocationsWithBTR.Contains(location)) + { + return; + } + + gameWorld.gameObject.AddComponent(); + } + catch (System.Exception) + { + ConsoleScreen.LogError("[AKI-BTR] Exception thrown, check logs."); + throw; + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/BTR/Patches/BTRPathLoadPatch.cs b/project/Aki.Custom/BTR/Patches/BTRPathLoadPatch.cs new file mode 100644 index 0000000..455fffe --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRPathLoadPatch.cs @@ -0,0 +1,35 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.BTR.Patches +{ + // The BTRManager MapPathsConfiguration loading depends on the game state being set to Starting + // so set it to Starting while the method is running, then reset it afterwards + public class BTRPathLoadPatch : ModulePatch + { + private static PropertyInfo _statusProperty; + private static GameStatus originalStatus; + protected override MethodBase GetTargetMethod() + { + _statusProperty = AccessTools.Property(typeof(AbstractGame), nameof(AbstractGame.Status)); + + return AccessTools.Method(typeof(BTRControllerClass), nameof(BTRControllerClass.method_1)); + } + + [PatchPrefix] + private static void PatchPrefix() + { + originalStatus = Singleton.Instance.Status; + _statusProperty.SetValue(Singleton.Instance, GameStatus.Starting); + } + + [PatchPostfix] + private static void PatchPostfix() + { + _statusProperty.SetValue(Singleton.Instance, originalStatus); + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRReceiveDamageInfoPatch.cs b/project/Aki.Custom/BTR/Patches/BTRReceiveDamageInfoPatch.cs new file mode 100644 index 0000000..427a064 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRReceiveDamageInfoPatch.cs @@ -0,0 +1,34 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.Vehicle; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.BTR.Patches +{ + /// + /// Patches an empty method in to handle updating the BTR bot's Neutrals and Enemies lists in response to taking damage. + /// + public class BTRReceiveDamageInfoPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BTRView), nameof(BTRView.method_1)); + } + + [PatchPrefix] + private static void PatchPrefix(DamageInfo damageInfo) + { + var botEventHandler = Singleton.Instance; + if (botEventHandler == null) + { + Logger.LogError($"[AKI-BTR] BTRReceiveDamageInfoPatch - BotEventHandler is null"); + return; + } + + var shotBy = (Player)damageInfo.Player.iPlayer; + botEventHandler.InterruptTraderServiceBtrSupportByBetrayer(shotBy); + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRTransferItemsPatch.cs b/project/Aki.Custom/BTR/Patches/BTRTransferItemsPatch.cs new file mode 100644 index 0000000..f641174 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRTransferItemsPatch.cs @@ -0,0 +1,31 @@ +using Aki.Custom.BTR.Utils; +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using EFT.UI; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRTransferItemsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + + return AccessTools.Method(typeof(TransferItemsInRaidScreen), nameof(TransferItemsInRaidScreen.Close)); + } + + [PatchPostfix] + private static void PatchPostfix(bool ___bool_1) + { + // Didn't extract items + if (!___bool_1) + { + return; + } + + // Update the trader services information now that we've used this service + TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId); + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRTurretCanShootPatch.cs b/project/Aki.Custom/BTR/Patches/BTRTurretCanShootPatch.cs new file mode 100644 index 0000000..12c8580 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRTurretCanShootPatch.cs @@ -0,0 +1,29 @@ +using Aki.Reflection.Patching; +using EFT.Vehicle; +using HarmonyLib; +using System.Reflection; +using UnityEngine; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRTurretCanShootPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BTRTurretServer), nameof(BTRTurretServer.method_1)); + } + + [PatchPrefix] + private static bool PatchPrefix(BTRTurretServer __instance, Transform ___defaultTargetTransform) + { + bool flag = __instance.targetTransform != null && __instance.targetTransform != ___defaultTargetTransform; + bool flag2 = __instance.method_2(); + bool flag3 = __instance.targetPosition != __instance.defaultAimingPosition; + + var isCanShootProperty = AccessTools.DeclaredProperty(__instance.GetType(), nameof(__instance.IsCanShoot)); + isCanShootProperty.SetValue(__instance, (flag || flag3) && flag2); + + return false; + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRTurretDefaultAimingPositionPatch.cs b/project/Aki.Custom/BTR/Patches/BTRTurretDefaultAimingPositionPatch.cs new file mode 100644 index 0000000..3123530 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRTurretDefaultAimingPositionPatch.cs @@ -0,0 +1,24 @@ +using Aki.Reflection.Patching; +using EFT.Vehicle; +using HarmonyLib; +using System.Reflection; +using UnityEngine; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRTurretDefaultAimingPositionPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BTRTurretServer), nameof(BTRTurretServer.Start)); + } + + [PatchPrefix] + private static bool PatchPrefix(BTRTurretServer __instance) + { + __instance.defaultAimingPosition = Vector3.zero; + + return false; + } + } +} diff --git a/project/Aki.Custom/BTR/Patches/BTRVehicleMovementSpeedPatch.cs b/project/Aki.Custom/BTR/Patches/BTRVehicleMovementSpeedPatch.cs new file mode 100644 index 0000000..0bf2b59 --- /dev/null +++ b/project/Aki.Custom/BTR/Patches/BTRVehicleMovementSpeedPatch.cs @@ -0,0 +1,22 @@ +using Aki.Reflection.Patching; +using EFT.Vehicle; +using HarmonyLib; +using System.Reflection; +using UnityEngine; + +namespace Aki.Custom.BTR.Patches +{ + public class BTRVehicleMovementSpeedPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BTRVehicle), nameof(BTRVehicle.Update)); + } + + [PatchPrefix] + private static void PatchPrefix(ref float ___float_10, float ___moveSpeed) + { + ___float_10 = ___moveSpeed * Time.deltaTime; + } + } +} diff --git a/project/Aki.Custom/BTR/Utils/BTRUtil.cs b/project/Aki.Custom/BTR/Utils/BTRUtil.cs new file mode 100644 index 0000000..26f0b4f --- /dev/null +++ b/project/Aki.Custom/BTR/Utils/BTRUtil.cs @@ -0,0 +1,32 @@ +using Aki.Common.Http; +using Aki.Custom.BTR.Models; +using Comfort.Common; +using EFT; +using EFT.InventoryLogic; +using Newtonsoft.Json; +using System; + +namespace Aki.Custom.BTR.Utils +{ + public static class BTRUtil + { + public static readonly string BTRTraderId = Profile.TraderInfo.BTR_TRADER_ID; + public static readonly string BTRMachineGunWeaponTplId = "657857faeff4c850222dff1b"; // BTR PKTM machine gun + public static readonly string BTRMachineGunAmmoTplId = "5e023d34e8a400319a28ed44"; // 7.62x54mmR BT + + /// + /// Used to create an instance of the item in-raid. + /// + public static Item CreateItem(string tplId) + { + var id = Guid.NewGuid().ToString("N").Substring(0, 24); + return Singleton.Instance.CreateItem(id, tplId, null); + } + + public static BTRConfigModel GetConfigFromServer() + { + string json = RequestHandler.GetJson("/singleplayer/btr/config"); + return JsonConvert.DeserializeObject(json); + } + } +} diff --git a/project/Aki.Custom/CustomAI/AiHelpers.cs b/project/Aki.Custom/CustomAI/AiHelpers.cs index 98484fe..53ff9c3 100644 --- a/project/Aki.Custom/CustomAI/AiHelpers.cs +++ b/project/Aki.Custom/CustomAI/AiHelpers.cs @@ -22,9 +22,9 @@ namespace Aki.Custom.CustomAI return (int)botRoleToCheck == AkiBotsPrePatcher.sptBearValue || (int)botRoleToCheck == AkiBotsPrePatcher.sptUsecValue; } - public static bool BotIsPlayerScav(WildSpawnType role, BotOwner ___botOwner_0) + public static bool BotIsPlayerScav(WildSpawnType role, string nickname) { - if (___botOwner_0.Profile.Info.Nickname.Contains("(") && role == WildSpawnType.assault) + if (role == WildSpawnType.assault && nickname.Contains("(")) { // Check bot is pscav by looking for the opening parentheses of their nickname e.g. scavname (pmc name) return true; diff --git a/project/Aki.Custom/CustomAI/PmcFoundInRaidEquipment.cs b/project/Aki.Custom/CustomAI/PmcFoundInRaidEquipment.cs index 65b6f10..cdf7df4 100644 --- a/project/Aki.Custom/CustomAI/PmcFoundInRaidEquipment.cs +++ b/project/Aki.Custom/CustomAI/PmcFoundInRaidEquipment.cs @@ -16,7 +16,9 @@ namespace Aki.Custom.CustomAI private static readonly string throwableItemId = "543be6564bdc2df4348b4568"; private static readonly string ammoItemId = "5485a8684bdc2da71d8b4567"; private static readonly string weaponId = "5422acb9af1c889c16000029"; - private static readonly List nonFiRItems = new List() { magazineId, drugId, mediKitItem, medicalItemId, injectorItemId, throwableItemId, ammoItemId }; + private static readonly string armorPlate = "644120aa86ffbe10ee032b6f"; + private static readonly string builtInInserts = "65649eb40bf0ed77b8044453"; + private static readonly List nonFiRItems = new List() { magazineId, drugId, mediKitItem, medicalItemId, injectorItemId, throwableItemId, ammoItemId, armorPlate, builtInInserts }; private static readonly string pistolId = "5447b5cf4bdc2d65278b4567"; private static readonly string smgId = "5447b5e04bdc2d62278b4567"; @@ -30,6 +32,7 @@ namespace Aki.Custom.CustomAI private static readonly string knifeId = "5447e1d04bdc2dff2f8b4567"; private static readonly List weaponTypeIds = new List() { pistolId, smgId, assaultRifleId, assaultCarbineId, shotgunId, marksmanRifleId, sniperRifleId, machinegunId, grenadeLauncherId, knifeId }; + private static readonly List nonFiRPocketLoot = new List{ throwableItemId, ammoItemId, magazineId, medicalItemId, mediKitItem, injectorItemId, drugId }; private readonly ManualLogSource logger; public PmcFoundInRaidEquipment(ManualLogSource logger) @@ -42,20 +45,34 @@ namespace Aki.Custom.CustomAI // Must run before the container loot code, otherwise backpack loot is not FiR MakeEquipmentNotFiR(___botOwner_0); - // Get inventory items that hold other items (backpack/rig/pockets) - List containerGear = ___botOwner_0.Profile.Inventory.Equipment.GetContainerSlots(); + // Get inventory items that hold other items (backpack/rig/pockets/armor) + IReadOnlyList containerGear = ___botOwner_0.Profile.Inventory.Equipment.ContainerSlots; + var nonFiRRootItems = new List(); foreach (var container in containerGear) { + var firstItem = container.Items.FirstOrDefault(); foreach (var item in container.ContainedItem.GetAllItems()) { // Skip items that match container (array has itself as an item) - if (item.Id == container.Items.FirstOrDefault().Id) + if (item.Id == firstItem?.Id) { //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its same as container {container.FullId}"); continue; } - // Dont add FiR to tacvest items PMC usually brings into raid (meds/mags etc) + // Don't add FiR to any item inside armor vest, e.g. plates/soft inserts + if (container.Name == "ArmorVest") + { + continue; + } + + // Don't add FiR to any item inside head gear, e.g. soft inserts/nvg/lights + if (container.Name == "Headwear") + { + continue; + } + + // Don't add FiR to tacvest items PMC usually brings into raid (meds/mags etc) if (container.Name == "TacticalVest" && nonFiRItems.Any(item.Template._parent.Contains)) { //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist"); @@ -65,25 +82,37 @@ namespace Aki.Custom.CustomAI // Don't add FiR to weapons in backpack (server sometimes adds pre-made weapons to backpack to simulate PMCs looting bodies) if (container.Name == "Backpack" && weaponTypeIds.Any(item.Template._parent.Contains)) { + // Add weapon root to list for later use + nonFiRRootItems.Add(item); //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist"); continue; } - // Don't add FiR to grenades/mags/ammo in pockets - if (container.Name == "Pockets" && new List { throwableItemId, ammoItemId, magazineId, medicalItemId }.Any(item.Template._parent.Contains)) + // Don't add FiR to grenades/mags/ammo/meds in pockets + if (container.Name == "Pockets" && nonFiRPocketLoot.Exists(item.Template._parent.Contains)) { //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist"); continue; } + // Check for mods of weapons in backpacks + var isChildOfEquippedNonFiRItem = nonFiRRootItems.Any(nonFiRRootItem => item.IsChildOf(nonFiRRootItem)); + if (isChildOfEquippedNonFiRItem) + { + continue; + } + //Logger.LogError($"flagging item FiR: {item.Id} {item.Name} _parent: {item.Template._parent}"); item.SpawnedInSession = true; } } // Set dogtag as FiR - var dogtag = ___botOwner_0.Profile.Inventory.GetItemsInSlots(new EquipmentSlot[] { EquipmentSlot.Dogtag }); - dogtag.FirstOrDefault().SpawnedInSession = true; + var dogtag = ___botOwner_0.Profile.Inventory.GetItemsInSlots(new EquipmentSlot[] { EquipmentSlot.Dogtag }).FirstOrDefault(); + if (dogtag != null) + { + dogtag.SpawnedInSession = true; + } } diff --git a/project/Aki.Custom/Models/BsgLoggingResponse.cs b/project/Aki.Custom/Models/BsgLoggingResponse.cs new file mode 100644 index 0000000..5c3fab9 --- /dev/null +++ b/project/Aki.Custom/Models/BsgLoggingResponse.cs @@ -0,0 +1,5 @@ +public struct LoggingLevelResponse +{ + public int verbosity { get; set; } + public bool sendToServer {get; set; } +} \ No newline at end of file diff --git a/project/Aki.Custom/Models/BundleInfo.cs b/project/Aki.Custom/Models/BundleInfo.cs index b3026e5..b393746 100644 --- a/project/Aki.Custom/Models/BundleInfo.cs +++ b/project/Aki.Custom/Models/BundleInfo.cs @@ -1,5 +1,10 @@ -namespace Aki.Custom.Models +#region DEPRECATED, REMOVE IN 3.8.1 + +using System; + +namespace Aki.Custom.Models { + [Obsolete("BundleInfo is deprecated, please use BundleItem instead.")] public class BundleInfo { public string Key { get; } @@ -14,3 +19,5 @@ } } } + +#endregion \ No newline at end of file diff --git a/project/Aki.Custom/Models/BundleItem.cs b/project/Aki.Custom/Models/BundleItem.cs index 0b5b963..62ccd26 100644 --- a/project/Aki.Custom/Models/BundleItem.cs +++ b/project/Aki.Custom/Models/BundleItem.cs @@ -1,9 +1,29 @@ -namespace Aki.Custom.Models +using System.Runtime.Serialization; + +namespace Aki.Custom.Models { + [DataContract] public struct BundleItem { + [DataMember(Name = "filename")] public string FileName; + + [DataMember(Name = "crc")] public uint Crc; + + [DataMember(Name = "dependencies")] public string[] Dependencies; + + // exclusive to aki, ignored in EscapeFromTarkov_Data/StreamingAssets/Windows/Windows.json + [DataMember(Name = "modpath")] + public string ModPath; + + public BundleItem(string filename, uint crc, string[] dependencies, string modpath = "") + { + FileName = filename; + Crc = crc; + Dependencies = dependencies; + ModPath = modpath; + } } } diff --git a/project/Aki.Custom/Models/DefaultRaidSettings.cs b/project/Aki.Custom/Models/DefaultRaidSettings.cs index 4a8e77e..f26ab83 100644 --- a/project/Aki.Custom/Models/DefaultRaidSettings.cs +++ b/project/Aki.Custom/Models/DefaultRaidSettings.cs @@ -10,8 +10,10 @@ namespace Aki.Custom.Models public bool ScavWars; public bool TaggedAndCursed; public bool EnablePve; + public bool RandomWeather; + public bool RandomTime; - public DefaultRaidSettings(EBotAmount aiAmount, EBotDifficulty aiDifficulty, bool bossEnabled, bool scavWars, bool taggedAndCursed, bool enablePve) + public DefaultRaidSettings(EBotAmount aiAmount, EBotDifficulty aiDifficulty, bool bossEnabled, bool scavWars, bool taggedAndCursed, bool enablePve, bool randomWeather, bool randomTime) { AiAmount = aiAmount; AiDifficulty = aiDifficulty; @@ -19,6 +21,8 @@ namespace Aki.Custom.Models ScavWars = scavWars; TaggedAndCursed = taggedAndCursed; EnablePve = enablePve; + RandomWeather = randomWeather; + RandomTime = randomTime; } } } diff --git a/project/Aki.Custom/Models/ReleaseResponse.cs b/project/Aki.Custom/Models/ReleaseResponse.cs new file mode 100644 index 0000000..9eb319c --- /dev/null +++ b/project/Aki.Custom/Models/ReleaseResponse.cs @@ -0,0 +1,19 @@ +namespace Aki.Custom.Models +{ + public struct ReleaseResponse + { + public bool isBeta { get; set; } + public bool isModdable { get; set; } + public bool isModded { get; set; } + public float betaDisclaimerTimeoutDelay { get; set; } + public string betaDisclaimerText { get; set; } + public string betaDisclaimerAcceptText { get; set; } + public string serverModsLoadedText { get; set; } + public string serverModsLoadedDebugText { get; set; } + public string clientModsLoadedText { get; set; } + public string clientModsLoadedDebugText { get; set; } + public string illegalPluginsLoadedText { get; set; } + public string illegalPluginsExceptionText { get; set; } + public string releaseSummaryText { get; set; } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/AddEnemyPatch.cs b/project/Aki.Custom/Patches/AddEnemyPatch.cs deleted file mode 100644 index 7fbd8e9..0000000 --- a/project/Aki.Custom/Patches/AddEnemyPatch.cs +++ /dev/null @@ -1,34 +0,0 @@ -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(BotZoneGroupsDictionary).GetMethod(methodName); - } - - [PatchPrefix] - private static bool PatchPrefix(BotZoneGroupsDictionary __instance, IPlayer 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 index 304d18f..85f4ffd 100644 --- a/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs +++ b/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs @@ -1,38 +1,15 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; -using System; -using System.Linq; using System.Reflection; +using HarmonyLib; 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(BotsController) && 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); + return AccessTools.Method(typeof(BotsController), nameof(BotsController.AddEnemyToAllGroupsInBotZone)); } /// @@ -44,6 +21,17 @@ namespace Aki.Custom.Patches [PatchPrefix] private static bool PatchPrefix(BotsController __instance, IPlayer aggressor, IPlayer groupOwner, IPlayer target) { + if (!groupOwner.IsAI) + { + return false; // Skip original + } + + // If you damage yourself exit early as we dont want to try add ourself to our own enemy list + if (aggressor.IsYourPlayer && target.IsYourPlayer) + { + return false; // Skip original + } + BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone; foreach (var item in __instance.Groups()) { @@ -68,7 +56,7 @@ namespace Aki.Custom.Patches } } - return false; + return false; // Skip original } } } diff --git a/project/Aki.Custom/Patches/AddSptBotSettingsPatch.cs b/project/Aki.Custom/Patches/AddSptBotSettingsPatch.cs index d5b4669..a261a48 100644 --- a/project/Aki.Custom/Patches/AddSptBotSettingsPatch.cs +++ b/project/Aki.Custom/Patches/AddSptBotSettingsPatch.cs @@ -3,6 +3,7 @@ using System.Reflection; using Aki.PrePatch; using Aki.Reflection.Patching; using EFT; +using HarmonyLib; namespace Aki.Custom.Patches { @@ -10,7 +11,7 @@ namespace Aki.Custom.Patches { protected override MethodBase GetTargetMethod() { - return typeof(BotSettingsRepoClass).GetMethod("Init"); + return AccessTools.Method(typeof(BotSettingsRepoClass), nameof(BotSettingsRepoClass.Init)); } [PatchPrefix] diff --git a/project/Aki.Custom/Patches/AddTraitorScavsPatch.cs b/project/Aki.Custom/Patches/AddTraitorScavsPatch.cs new file mode 100644 index 0000000..6e5b820 --- /dev/null +++ b/project/Aki.Custom/Patches/AddTraitorScavsPatch.cs @@ -0,0 +1,64 @@ +using Aki.Common.Http; +using Aki.Custom.Airdrops.Models; +using Aki.Custom.CustomAI; +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using HarmonyLib; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + public class AddTraitorScavsPatch : ModulePatch + { + private static int? TraitorChancePercent; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BotSpawner), nameof(BotSpawner.GetGroupAndSetEnemies)); + } + + [PatchPrefix] + private static bool PatchPrefix(ref BotsGroup __result, IBotGame ____game, DeadBodiesController ____deadBodiesController, BotOwner bot, BotZone zone) + { + if (!TraitorChancePercent.HasValue) + { + string json = RequestHandler.GetJson("/singleplayer/scav/traitorscavhostile"); + TraitorChancePercent = JsonConvert.DeserializeObject(json); + } + + WildSpawnType role = bot.Profile.Info.Settings.Role; + if (AiHelpers.BotIsPlayerScav(role, bot.Profile.Info.Nickname) && new Random().Next(1, 100) < TraitorChancePercent) + { + Logger.LogInfo($"Making {bot.name} ({bot.Profile.Nickname}) hostile to player"); + + // Create a new group for this scav itself to belong to + var player = Singleton.Instance.MainPlayer; + var enemies = new List(); + var players = new List() { player }; + var botsGroup = new BotsGroup(zone, ____game, bot, enemies, ____deadBodiesController, players, false); + + // Because we don't want to use the zone-specific group, we add the new group with no key. This is similar to free for all + Singleton.Instance.BotsController.BotSpawner.Groups.AddNoKey(botsGroup, zone); + botsGroup.AddEnemy(player, EBotEnemyCause.checkAddTODO); + + // Make it so the player can kill the scav without aggroing the rest of the scavs + bot.Loyalty.CanBeFreeKilled = true; + + // Traitors dont talk + bot.BotTalk.SetSilence(9999); + + // Return our new botgroup + __result = botsGroup; + + // Skip original + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/BetaLogoPatch.cs b/project/Aki.Custom/Patches/BetaLogoPatch.cs new file mode 100644 index 0000000..c978d90 --- /dev/null +++ b/project/Aki.Custom/Patches/BetaLogoPatch.cs @@ -0,0 +1,64 @@ +using Aki.Reflection.Patching; +using EFT.UI; +using EFT; +using HarmonyLib; +using System.Reflection; +using Aki.SinglePlayer.Utils.MainMenu; +using TMPro; +using UnityEngine; + +namespace Aki.SinglePlayer.Patches.MainMenu +{ + public class BetaLogoPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(TarkovApplication), nameof(TarkovApplication.method_27)); + } + + [PatchPrefix] + private static void PatchPrefix(Profile profile) + { + MonoBehaviourSingleton.Instance.SetWatermarkStatus(profile, true); + } + } + + public class BetaLogoPatch2 : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ClientWatermark), nameof(ClientWatermark.method_0)); + } + + [PatchPostfix] + private static void PatchPostfix(ref TextMeshProUGUI ____label, Profile ___profile_0) + { + ____label.text = $"{MenuNotificationManager.commitHash}"; + } + } + + public class BetaLogoPatch3 : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ClientWatermark), nameof(ClientWatermark.smethod_0)); + } + + // Prefix so the logic isn't being duplicated. + [PatchPrefix] + private static bool PatchPrefix(int screenHeight, int screenWidth, int rectHeight, int rectWidth, ref Vector2 __result) + { + System.Random random = new System.Random(); + + int maxX = (screenWidth / 4) - (rectWidth / 2); + int maxY = (screenHeight / 4) - (rectHeight / 2); + int newX = random.Next(-maxX, maxX); + int newY = random.Next(-maxY, maxY); + + __result = new Vector2(newX, newY); + + // Skip original + return false; + } + } +} diff --git a/project/Aki.Custom/Patches/BossSpawnChancePatch.cs b/project/Aki.Custom/Patches/BossSpawnChancePatch.cs index 3fb2658..14b64b4 100644 --- a/project/Aki.Custom/Patches/BossSpawnChancePatch.cs +++ b/project/Aki.Custom/Patches/BossSpawnChancePatch.cs @@ -16,11 +16,11 @@ namespace Aki.Custom.Patches { var desiredType = PatchConstants.LocalGameType; var desiredMethod = desiredType - .GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly) - .SingleOrDefault(m => IsTargetMethod(m)); + .GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .SingleOrDefault(IsTargetMethod); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod.Name}"); + Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name ?? "NOT FOUND"}"); return desiredMethod; } diff --git a/project/Aki.Custom/Patches/BotCallForHelpCallBotPatch.cs b/project/Aki.Custom/Patches/BotCallForHelpCallBotPatch.cs new file mode 100644 index 0000000..97938be --- /dev/null +++ b/project/Aki.Custom/Patches/BotCallForHelpCallBotPatch.cs @@ -0,0 +1,54 @@ +using Aki.Reflection.Patching; +using EFT; +using HarmonyLib; +using System.Reflection; +using UnityEngine; + +namespace Aki.Custom.Patches +{ + /** + * BSG passes the wrong target location into the TryCall method for BotCallForHelp, it's passing in + * the bots current target instead of the target of the bot calling for help + * + * This results in both an NRE, and the called bots target location being wrong + */ + internal class BotCallForHelpCallBotPatch : ModulePatch + { + private static FieldInfo _originalPanicTypeField; + + protected override MethodBase GetTargetMethod() + { + _originalPanicTypeField = AccessTools.Field(typeof(BotCallForHelp), "_originalPanicType"); + + return AccessTools.FirstMethod(typeof(BotCallForHelp), IsTargetMethod); + } + + protected bool IsTargetMethod(MethodBase method) + { + var parameters = method.GetParameters(); + return (parameters.Length == 1 + && parameters[0].Name == "calledBot"); + } + + [PatchPrefix] + private static bool PatchPrefix(ref bool __result, BotCallForHelp __instance, BotOwner calledBot, BotOwner ___botOwner_0) + { + if (__instance.method_2(calledBot) && ___botOwner_0.Memory.GoalEnemy != null) + { + _originalPanicTypeField.SetValue(calledBot.CallForHelp, calledBot.DangerPointsData.PanicType); + calledBot.DangerPointsData.PanicType = PanicType.none; + calledBot.Brain.BaseBrain.CalcActionNextFrame(); + // Note: This differs from BSG's implementation in that we pass in botOwner_0's enemy pos instead of calledBot's enemy pos + calledBot.CalledData.TryCall(new Vector3?(___botOwner_0.Memory.GoalEnemy.Person.Position), ___botOwner_0, true); + __result = true; + } + else + { + __result = false; + } + + // Skip original + return false; + } + } +} diff --git a/project/Aki.Custom/Patches/BotCalledDataTryCallPatch.cs b/project/Aki.Custom/Patches/BotCalledDataTryCallPatch.cs new file mode 100644 index 0000000..e8c0646 --- /dev/null +++ b/project/Aki.Custom/Patches/BotCalledDataTryCallPatch.cs @@ -0,0 +1,49 @@ +using Aki.Reflection.Patching; +using EFT; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + /** + * It's possible for `AddEnemy` to return false, in that case, further code in TryCall will fail, + * so we do the first bit of `TryCall` ourselves, and skip the original function if AddEnemy fails + */ + internal class BotCalledDataTryCallPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BotCalledData), nameof(BotCalledData.TryCall)); + } + + [PatchPrefix] + private static bool PatchPrefix(ref bool __result, BotOwner caller, BotOwner ___botOwner_0, BotOwner ____caller) + { + if (___botOwner_0.EnemiesController.IsEnemy(caller.AIData.Player) || ____caller != null) + { + __result = false; + + // Skip original + return false; + } + + if (caller.Memory.GoalEnemy != null) + { + IPlayer person = caller.Memory.GoalEnemy.Person; + if (!___botOwner_0.BotsGroup.Enemies.ContainsKey(person)) + { + if (!___botOwner_0.BotsGroup.AddEnemy(person, EBotEnemyCause.callBot)) + { + __result = false; + + // Skip original + return false; + } + } + } + + // Allow original + return true; + } + } +} diff --git a/project/Aki.Custom/Patches/BotDifficultyPatch.cs b/project/Aki.Custom/Patches/BotDifficultyPatch.cs index a684ed6..2f5ebe3 100644 --- a/project/Aki.Custom/Patches/BotDifficultyPatch.cs +++ b/project/Aki.Custom/Patches/BotDifficultyPatch.cs @@ -3,7 +3,6 @@ using Aki.Reflection.Patching; using Aki.Reflection.Utils; using EFT; using EFT.UI; -using System.Linq; using System.Reflection; namespace Aki.Custom.Patches @@ -15,7 +14,7 @@ namespace Aki.Custom.Patches var methodName = "LoadDifficultyStringInternal"; var flags = BindingFlags.Public | BindingFlags.Static; - return PatchConstants.EftTypes.Single(x => x.GetMethod(methodName, flags) != null) + return PatchConstants.EftTypes.SingleCustom(x => x.GetMethod(methodName, flags) != null) .GetMethod(methodName, flags); } diff --git a/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs b/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs index 6524d70..2d02a66 100644 --- a/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs +++ b/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs @@ -1,36 +1,15 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; -using System; -using System.Linq; using System.Reflection; +using HarmonyLib; 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(BotsController) && type.GetMethod(methodName) != null) - { - Logger.LogInfo($"{methodName}: {type.FullName}"); - return true; - } - - return false; - } - protected override MethodBase GetTargetMethod() { - return _targetType.GetMethod(methodName); + return AccessTools.Method(typeof(BotsController), nameof(BotsController.AddEnemyToAllGroupsInBotZone)); } /// diff --git a/project/Aki.Custom/Patches/BotOwnerDisposePatch.cs b/project/Aki.Custom/Patches/BotOwnerDisposePatch.cs new file mode 100644 index 0000000..9266b91 --- /dev/null +++ b/project/Aki.Custom/Patches/BotOwnerDisposePatch.cs @@ -0,0 +1,28 @@ +using Aki.Reflection.Patching; +using EFT; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + /** + * BotOwner doesn't call SetOff on the CalledData object when a bot is disposed, this can result + * in bots that are no longer alive having their `OnEnemyAdd` method called + */ + internal class BotOwnerDisposePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BotOwner), nameof(BotOwner.Dispose)); + } + + [PatchPrefix] + private static void PatchPrefix(BotOwner __instance) + { + if (__instance.CalledData != null) + { + __instance.CalledData.SetOff(); + } + } + } +} diff --git a/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs b/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs index cc7ca9e..179f24c 100644 --- a/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs +++ b/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs @@ -1,6 +1,7 @@ using Aki.Reflection.Patching; using EFT; using System.Reflection; +using HarmonyLib; namespace Aki.Custom.Patches { @@ -9,11 +10,9 @@ namespace Aki.Custom.Patches /// internal class BotSelfEnemyPatch : ModulePatch { - private static readonly string methodName = "PreActivate"; - protected override MethodBase GetTargetMethod() { - return typeof(BotOwner).GetMethod(methodName); + return AccessTools.Method(typeof(BotOwner), nameof(BotOwner.PreActivate)); } [PatchPrefix] diff --git a/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs b/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs index 54dd730..d53f1c3 100644 --- a/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs +++ b/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs @@ -1,65 +1,29 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; -using System; -using System.Linq; using System.Reflection; +using HarmonyLib; namespace Aki.Custom.Patches { public class CheckAndAddEnemyPatch : ModulePatch { - private static Type _targetType; - private readonly string _targetMethodName = "CheckAndAddEnemy"; - - /// - /// BotGroupClass.CheckAndAddEnemy() - /// - public CheckAndAddEnemyPatch() - { - _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); + return AccessTools.Method(typeof(BotsGroup), nameof(BotsGroup.CheckAndAddEnemy)); } /// /// CheckAndAddEnemy() /// Goal: This patch lets bosses shoot back once a PMC has shot them - /// removes the !player.AIData.IsAI check + /// Removes the !player.AIData.IsAI check + /// BSG changed the way CheckAndAddEnemy Works in 14.0 Returns a bool now /// [PatchPrefix] - private static bool PatchPrefix(BotsGroup __instance, IPlayer player, ref bool ignoreAI) - { - // Z already has player as enemy BUT Enemies dict is empty, adding them again causes 'existing key' errors - if (__instance.InitialBotType == WildSpawnType.bossZryachiy || __instance.InitialBotType == WildSpawnType.followerZryachiy) - { - return false; - } - - if (!player.HealthController.IsAlive) - { - return false; // Skip original - } - - if (!__instance.Enemies.ContainsKey(player)) - { - __instance.AddEnemy(player, EBotEnemyCause.checkAddTODO); - } - - return false; // Skip original + private static bool PatchPrefix(BotsGroup __instance, IPlayer player, ref bool __result) + { + // Set result to not include !player.AIData.IsAI checks + __result = player.HealthController.IsAlive && !__instance.Enemies.ContainsKey(player) && __instance.AddEnemy(player, EBotEnemyCause.checkAddTODO); + return false; // Skip Original } } } diff --git a/project/Aki.Custom/Patches/ClampRagdollPatch.cs b/project/Aki.Custom/Patches/ClampRagdollPatch.cs new file mode 100644 index 0000000..266eb40 --- /dev/null +++ b/project/Aki.Custom/Patches/ClampRagdollPatch.cs @@ -0,0 +1,22 @@ +using Aki.Reflection.Patching; +using EFT.Interactive; +using HarmonyLib; +using System.Reflection; +using UnityEngine; + +namespace Aki.Custom.Patches +{ + public class ClampRagdollPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(Corpse), nameof(Corpse.method_16)); + } + + [PatchPrefix] + private static void PatchPreFix(ref Vector3 velocity) + { + velocity.y = Mathf.Clamp(velocity.y, -1f, 1f); + } + } +} diff --git a/project/Aki.Custom/Patches/CoreDifficultyPatch.cs b/project/Aki.Custom/Patches/CoreDifficultyPatch.cs index 379c6d4..4dc2d70 100644 --- a/project/Aki.Custom/Patches/CoreDifficultyPatch.cs +++ b/project/Aki.Custom/Patches/CoreDifficultyPatch.cs @@ -1,7 +1,6 @@ using Aki.Reflection.Patching; using Aki.Reflection.Utils; using Aki.Common.Http; -using System.Linq; using System.Reflection; namespace Aki.Custom.Patches @@ -13,7 +12,7 @@ namespace Aki.Custom.Patches var methodName = "LoadCoreByString"; var flags = BindingFlags.Public | BindingFlags.Static; - return PatchConstants.EftTypes.Single(x => x.GetMethod(methodName, flags) != null) + return PatchConstants.EftTypes.SingleCustom(x => x.GetMethod(methodName, flags) != null) .GetMethod(methodName, flags); } diff --git a/project/Aki.Custom/Patches/CultistAmuletRemovalPatch.cs b/project/Aki.Custom/Patches/CultistAmuletRemovalPatch.cs new file mode 100644 index 0000000..766dd4a --- /dev/null +++ b/project/Aki.Custom/Patches/CultistAmuletRemovalPatch.cs @@ -0,0 +1,36 @@ +using Aki.Reflection.Patching; +using EFT; +using EFT.InventoryLogic; +using EFT.UI.DragAndDrop; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Aki.Custom.Patches +{ + public class CultistAmuletRemovalPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(CultistEventsClass), nameof(CultistEventsClass.method_4)); + } + + [PatchPostfix] + private static void PatchPostfix(DamageInfo damageInfo, Player victim) + { + var player = damageInfo.Player.iPlayer; + var amulet = player.FindCultistAmulet(); + if (victim.Profile.Info.Settings.Role.IsSectant() && amulet != null) + { + var list = (player.Profile.Inventory.Equipment.GetSlot(EquipmentSlot.Pockets).ContainedItem as SearchableItemClass).Slots; + var amuletslot = list.Single(x => x.ContainedItem == amulet); + amuletslot.RemoveItem(); + } + } + + } +} diff --git a/project/Aki.Custom/Patches/CustomAiPatch.cs b/project/Aki.Custom/Patches/CustomAiPatch.cs index 8683ca3..d14ee6f 100644 --- a/project/Aki.Custom/Patches/CustomAiPatch.cs +++ b/project/Aki.Custom/Patches/CustomAiPatch.cs @@ -5,6 +5,7 @@ using Comfort.Common; using System.Reflection; using Aki.PrePatch; using Aki.Custom.CustomAI; +using HarmonyLib; namespace Aki.Custom.Patches { @@ -15,7 +16,7 @@ namespace Aki.Custom.Patches protected override MethodBase GetTargetMethod() { - return typeof(StandartBotBrain).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance); + return AccessTools.Method(typeof(StandartBotBrain), nameof(StandartBotBrain.Activate)); } /// @@ -28,12 +29,13 @@ namespace Aki.Custom.Patches [PatchPrefix] private static bool PatchPrefix(out WildSpawnType __state, StandartBotBrain __instance, BotOwner ___botOwner_0) { + var player = Singleton.Instance.MainPlayer; ___botOwner_0.Profile.Info.Settings.Role = FixAssaultGroupPmcsRole(___botOwner_0); __state = ___botOwner_0.Profile.Info.Settings.Role; // Store original type in state param to allow access in PatchPostFix() try { string currentMapName = GetCurrentMap(); - if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0)) + if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0.Profile.Info.Nickname)) { ___botOwner_0.Profile.Info.Settings.Role = aIBrainSpawnWeightAdjustment.GetRandomisedPlayerScavType(___botOwner_0, currentMapName); @@ -100,7 +102,7 @@ namespace Aki.Custom.Patches // Set spt bot bot back to original type ___botOwner_0.Profile.Info.Settings.Role = __state; } - else if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0)) + else if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0.Profile.Info.Nickname)) { // Set pscav back to original type ___botOwner_0.Profile.Info.Settings.Role = __state; diff --git a/project/Aki.Custom/Patches/EasyAssetsPatch.cs b/project/Aki.Custom/Patches/EasyAssetsPatch.cs index d77d9c3..7f4fa16 100644 --- a/project/Aki.Custom/Patches/EasyAssetsPatch.cs +++ b/project/Aki.Custom/Patches/EasyAssetsPatch.cs @@ -1,5 +1,4 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using Diz.Jobs; using Diz.Resources; using JetBrains.Annotations; @@ -12,27 +11,21 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Aki.Common.Utils; using Aki.Custom.Models; using Aki.Custom.Utils; using DependencyGraph = DependencyGraph; +using Aki.Reflection.Utils; 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); - - // DependencyGraph - _systemProperty = type.GetProperty("System"); + _bundlesField = typeof(EasyAssets).GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags); } public EasyAssetsPatch() @@ -45,7 +38,7 @@ namespace Aki.Custom.Patches protected override MethodBase GetTargetMethod() { - return typeof(EasyAssets).GetMethods(PatchConstants.PrivateFlags).Single(IsTargetMethod); + return typeof(EasyAssets).GetMethods(PatchConstants.PublicDeclaredFlags).SingleCustom(IsTargetMethod); } private static bool IsTargetMethod(MethodInfo mi) @@ -65,42 +58,56 @@ namespace Aki.Custom.Patches return false; } - private static async Task Init(EasyAssets instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath, - string platformName, [CanBeNull] Func shouldExclude, Func bundleCheck) + 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); + var jsonfile = filepath + ".json"; + var manifest = File.Exists(jsonfile) + ? await GetManifestJson(jsonfile) + : await GetManifestBundle(filepath); - // load bundles - var bundleNames = manifest.GetAllAssetBundles().Union(BundleManager.Bundles.Keys).ToArray(); - var bundles = (IEasyBundle[])Array.CreateInstance(EasyBundleHelper.Type, bundleNames.Length); + // create bundles array from obfuscated type + var bundleNames = manifest.GetAllAssetBundles() + .Union(BundleManager.Bundles.Keys) + .ToArray(); + // create bundle lock if (bundleLock == null) { bundleLock = new BundleLock(int.MaxValue); } + // create bundle of obfuscated type + var bundles = (IEasyBundle[])Array.CreateInstance(EasyBundleHelper.Type, bundleNames.Length); + for (var i = 0; i < bundleNames.Length; i++) { bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[] - { - bundleNames[i], - path, - manifest, - bundleLock, - bundleCheck - }); + { + bundleNames[i], + path, + manifest, + bundleLock, + bundleCheck + }); await JobScheduler.Yield(EJobPriority.Immediate); } - _manifestField.SetValue(instance, manifest); + // create dependency graph + instance.Manifest = manifest; _bundlesField.SetValue(instance, bundles); - _systemProperty.SetValue(instance, new DependencyGraph(bundles, defaultKey, shouldExclude)); + instance.System = new DependencyGraph(bundles, defaultKey, shouldExclude); } + // NOTE: used by: + // - EscapeFromTarkov_Data/StreamingAssets/Windows/cubemaps + // - EscapeFromTarkov_Data/StreamingAssets/Windows/defaultmaterial + // - EscapeFromTarkov_Data/StreamingAssets/Windows/dissonancesetup + // - EscapeFromTarkov_Data/StreamingAssets/Windows/Doge + // - EscapeFromTarkov_Data/StreamingAssets/Windows/shaders private static async Task GetManifestBundle(string filepath) { var manifestLoading = AssetBundle.LoadFromFileAsync(filepath); @@ -115,16 +122,18 @@ namespace Aki.Custom.Patches private static async Task GetManifestJson(string filepath) { - var text = string.Empty; + var text = VFS.ReadTextFile(filepath); - using (var reader = File.OpenText($"{filepath}.json")) - { - text = await reader.ReadToEndAsync(); - } + /* we cannot parse directly as , because... + [Error : Unity Log] JsonSerializationException: Expected string when reading UnityEngine.Hash128 type, got 'StartObject' <>. Path '['assets/content/weapons/animations/simple_animations.bundle'].Hash', line 1, position 176. + ...so we need to first convert it to a slimmed-down type (BundleItem), then convert back to BundleDetails. + */ + var raw = JsonConvert.DeserializeObject>(text); + var converted = raw.ToDictionary(GetPairKey, GetPairValue); - var data = JsonConvert.DeserializeObject>(text).ToDictionary(GetPairKey, GetPairValue); + // initialize manifest var manifest = ScriptableObject.CreateInstance(); - manifest.SetResults(data); + manifest.SetResults(converted); return manifest; } diff --git a/project/Aki.Custom/Patches/EasyBundlePatch.cs b/project/Aki.Custom/Patches/EasyBundlePatch.cs index 998aff7..f5e727c 100644 --- a/project/Aki.Custom/Patches/EasyBundlePatch.cs +++ b/project/Aki.Custom/Patches/EasyBundlePatch.cs @@ -1,4 +1,5 @@ -using Aki.Reflection.Patching; +using System; +using Aki.Reflection.Patching; using Diz.DependencyManager; using UnityEngine.Build.Pipeline; using System.IO; @@ -26,21 +27,26 @@ namespace Aki.Custom.Patches [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]; + var filepath = rootPath + key; + var dependencies = manifest.GetDirectDependencies(key) ?? Array.Empty(); - if (BundleManager.Bundles.TryGetValue(key, out BundleInfo bundle)) + if (BundleManager.Bundles.TryGetValue(key, out BundleItem bundle)) { - dependencyKeys = (dependencyKeys.Length > 0) ? dependencyKeys.Union(bundle.DependencyKeys).ToArray() : bundle.DependencyKeys; - path = bundle.Path; + // server bundle + dependencies = (dependencies.Length > 0) + ? dependencies.Union(bundle.Dependencies).ToArray() + : bundle.Dependencies; + + // set path to either cache (HTTP) or mod (local) + filepath = BundleManager.GetBundlePath(bundle); } _ = new EasyBundleHelper(__instance) { Key = key, - Path = path, + Path = filepath, KeyWithoutExtension = Path.GetFileNameWithoutExtension(key), - DependencyKeys = dependencyKeys, + DependencyKeys = dependencies, LoadState = new BindableState(ELoadState.Unloaded, null), BundleLock = bundleLock }; diff --git a/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs b/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs index b960590..c4ecb70 100644 --- a/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs +++ b/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs @@ -1,9 +1,8 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using Comfort.Common; using EFT; -using System.Linq; using System.Reflection; +using HarmonyLib; namespace Aki.Custom.Patches { @@ -14,8 +13,7 @@ namespace Aki.Custom.Patches { protected override MethodBase GetTargetMethod() { - return PatchConstants.EftTypes.Single(x => x.Name == "LocalGame").BaseType // BaseLocalGame - .GetMethod("Stop", BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance); + return AccessTools.Method(typeof(BaseLocalGame), nameof(BaseLocalGame.Stop)); } // Look at BaseLocalGame and find a method named "Stop" @@ -33,7 +31,7 @@ namespace Aki.Custom.Patches var player = Singleton.Instance.MainPlayer; if (profileId == player?.Profile.Id) { - GClass2898.Instance.CloseAllScreensForced(); + GClass3107.Instance.CloseAllScreensForced(); } return true; diff --git a/project/Aki.Custom/Patches/FixBrokenSpawnOnSandboxPatch.cs b/project/Aki.Custom/Patches/FixBrokenSpawnOnSandboxPatch.cs new file mode 100644 index 0000000..7edf2d4 --- /dev/null +++ b/project/Aki.Custom/Patches/FixBrokenSpawnOnSandboxPatch.cs @@ -0,0 +1,46 @@ +using Aki.Common.Http; +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using HarmonyLib; +using Newtonsoft.Json; +using System.Linq; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + /// + /// Fixes the map sandbox from only spawning 1 bot at start of game as well as fixing no spawns till all bots are dead. + /// Remove once BSG decides to fix their map + /// + public class FixBrokenSpawnOnSandboxPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GameWorld), nameof(GameWorld.OnGameStarted)); + } + + [PatchPrefix] + private static void PatchPrefix() + { + var gameWorld = Singleton.Instance; + if (gameWorld == null) + { + return; + } + + var playerLocation = gameWorld.MainPlayer.Location; + + if (playerLocation == "Sandbox") + { + LocationScene.GetAll().ToList().First(x => x.name == "ZoneSandbox").MaxPersonsOnPatrol = GetMaxPatrolValueFromServer(); + } + } + + public static int GetMaxPatrolValueFromServer() + { + string json = RequestHandler.GetJson("/singleplayer/sandbox/maxpatrol"); + return JsonConvert.DeserializeObject(json); + } + } +} diff --git a/project/Aki.Custom/Patches/HalloweenExtractPatch.cs b/project/Aki.Custom/Patches/HalloweenExtractPatch.cs new file mode 100644 index 0000000..6212345 --- /dev/null +++ b/project/Aki.Custom/Patches/HalloweenExtractPatch.cs @@ -0,0 +1,47 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.Interactive; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + public class HalloweenExtractPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BotHalloweenEvent), nameof(BotHalloweenEvent.RitualCompleted)); + } + + [PatchPostfix] + private static void PatchPostfix() + { + GameWorld gameWorld = Singleton.Instance; + Random random = new Random(); + // Get all extracts the player has + List EligiblePoints = ExfiltrationControllerClass.Instance.EligiblePoints(gameWorld.MainPlayer.Profile).ToList(); + List PointsToPickFrom = new List(); + + + foreach (var ExfilPoint in EligiblePoints) + { + if (ExfilPoint.Status == EExfiltrationStatus.RegularMode) + { + // Only add extracts that we want exludes car and timed extracts i think? + PointsToPickFrom.Add(ExfilPoint); + //ConsoleScreen.Log(ExfilPoint.Settings.Name + " Added to pool"); + } + } + // Randomly pick a extract from the list + int index = random.Next(PointsToPickFrom.Count); + string selectedExtract = PointsToPickFrom[index].Settings.Name; + //ConsoleScreen.Log(selectedExtract + " Picked for Extract"); + + ExfiltrationControllerClass.Instance.EventDisableAllExitsExceptOne(selectedExtract); + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/IsEnemyPatch.cs b/project/Aki.Custom/Patches/IsEnemyPatch.cs index 584d469..8f0f37e 100644 --- a/project/Aki.Custom/Patches/IsEnemyPatch.cs +++ b/project/Aki.Custom/Patches/IsEnemyPatch.cs @@ -1,35 +1,16 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; -using System; using System.Linq; using System.Reflection; +using HarmonyLib; 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); + return AccessTools.Method(typeof(BotsGroup), nameof(BotsGroup.IsEnemy)); } /// @@ -41,6 +22,17 @@ namespace Aki.Custom.Patches [PatchPrefix] private static bool PatchPrefix(ref bool __result, BotsGroup __instance, IPlayer requester) { + if (__instance.InitialBotType == WildSpawnType.peacefullZryachiyEvent + || __instance.InitialBotType == WildSpawnType.shooterBTR + || __instance.InitialBotType == WildSpawnType.gifter + || __instance.InitialBotType == WildSpawnType.sectantWarrior + || __instance.InitialBotType == WildSpawnType.sectantPriest + || __instance.InitialBotType == WildSpawnType.sectactPriestEvent + || __instance.InitialBotType == WildSpawnType.ravangeZryachiyEvent) + { + return true; // Do original code + } + var isEnemy = false; // default not an enemy if (requester == null) { diff --git a/project/Aki.Custom/Patches/LocationLootCacheBustingPatch.cs b/project/Aki.Custom/Patches/LocationLootCacheBustingPatch.cs index 4c68fd0..ed4e38d 100644 --- a/project/Aki.Custom/Patches/LocationLootCacheBustingPatch.cs +++ b/project/Aki.Custom/Patches/LocationLootCacheBustingPatch.cs @@ -1,8 +1,7 @@ using Aki.Reflection.Patching; using Aki.Reflection.Utils; -using System; -using System.Linq; using System.Reflection; +using EFT; namespace Aki.Custom.Patches { @@ -13,8 +12,8 @@ namespace Aki.Custom.Patches { protected override MethodBase GetTargetMethod() { - var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "LocalGame").BaseType; // BaseLocalGame - var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).Single(x => IsTargetMethod(x)); // method_6 + var desiredType = typeof(BaseLocalGame); + var desiredMethod = desiredType.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public).SingleCustom(IsTargetMethod); // method_6 Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); diff --git a/project/Aki.Custom/Patches/OfflineRaidMenuPatch.cs b/project/Aki.Custom/Patches/OfflineRaidMenuPatch.cs index 047a692..16b3879 100644 --- a/project/Aki.Custom/Patches/OfflineRaidMenuPatch.cs +++ b/project/Aki.Custom/Patches/OfflineRaidMenuPatch.cs @@ -7,6 +7,7 @@ using EFT.UI.Matchmaker; using System.Reflection; using EFT; using HarmonyLib; +using Aki.Reflection.Utils; namespace Aki.Custom.Patches { @@ -14,13 +15,8 @@ namespace Aki.Custom.Patches { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(MatchmakerOfflineRaidScreen); - var desiredMethod = desiredType.GetMethod(nameof(MatchmakerOfflineRaidScreen.Show)); - - Logger.LogDebug($"{GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.GetDeclaredMethods(typeof(MatchmakerOfflineRaidScreen)) + .SingleCustom(m => m.Name == nameof(MatchmakerOfflineRaidScreen.Show) && m.GetParameters().Length == 1); } [PatchPrefix] @@ -49,6 +45,8 @@ namespace Aki.Custom.Patches raidSettings.WavesSettings.IsBosses = settings.BossEnabled; raidSettings.BotSettings.IsScavWars = false; raidSettings.WavesSettings.IsTaggedAndCursed = settings.TaggedAndCursed; + raidSettings.TimeAndWeatherSettings.IsRandomWeather = settings.RandomWeather; + raidSettings.TimeAndWeatherSettings.IsRandomTime = settings.RandomTime; } [PatchPostfix] diff --git a/project/Aki.Custom/Patches/OfflineRaidSettingsMenuPatch.cs b/project/Aki.Custom/Patches/OfflineRaidSettingsMenuPatch.cs index ff1f43f..e93cc0f 100644 --- a/project/Aki.Custom/Patches/OfflineRaidSettingsMenuPatch.cs +++ b/project/Aki.Custom/Patches/OfflineRaidSettingsMenuPatch.cs @@ -2,23 +2,15 @@ using Aki.Reflection.Patching; using EFT.UI; using EFT.UI.Matchmaker; +using HarmonyLib; namespace Aki.Custom.Patches { public class OfflineRaidSettingsMenuPatch : ModulePatch { - /// - /// RaidSettingsWindow.Show() - /// protected override MethodBase GetTargetMethod() { - var desiredType = typeof(RaidSettingsWindow); - var desiredMethod = desiredType.GetMethod(nameof(RaidSettingsWindow.Show)); - - Logger.LogDebug($"{GetType().Name} Type: {desiredType.Name}"); - Logger.LogDebug($"{GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(RaidSettingsWindow), nameof(RaidSettingsWindow.Show)); } [PatchPostfix] diff --git a/project/Aki.Custom/Patches/PmcFirstAidPatch.cs b/project/Aki.Custom/Patches/PmcFirstAidPatch.cs index 0dde0c8..11d9715 100644 --- a/project/Aki.Custom/Patches/PmcFirstAidPatch.cs +++ b/project/Aki.Custom/Patches/PmcFirstAidPatch.cs @@ -1,4 +1,5 @@ -using Aki.Reflection.Patching; +using Aki.PrePatch; +using Aki.Reflection.Patching; using Aki.Reflection.Utils; using EFT; using System; @@ -23,7 +24,7 @@ namespace Aki.Custom.Patches protected override MethodBase GetTargetMethod() { - return _targetType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + return _targetType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public); } /// @@ -42,7 +43,7 @@ namespace Aki.Custom.Patches [PatchPrefix] private static bool PatchPrefix(BotOwner ___botOwner_0) { - if (___botOwner_0.IsRole((WildSpawnType)33) || ___botOwner_0.IsRole((WildSpawnType)34)) + if (___botOwner_0.IsRole((WildSpawnType)AkiBotsPrePatcher.sptUsecValue) || ___botOwner_0.IsRole((WildSpawnType)AkiBotsPrePatcher.sptBearValue)) { var healthController = ___botOwner_0.GetPlayer.ActiveHealthController; diff --git a/project/Aki.Custom/Patches/PreventClientModsPatch.cs b/project/Aki.Custom/Patches/PreventClientModsPatch.cs new file mode 100644 index 0000000..8a3d04e --- /dev/null +++ b/project/Aki.Custom/Patches/PreventClientModsPatch.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.MainMenu; +using BepInEx.Bootstrap; +using BepInEx.Logging; +using EFT; +using HarmonyLib; + +namespace Aki.SinglePlayer.Patches.MainMenu +{ + /// + /// Prevents loading of non-whitelisted client mods to minimize the amount of false issue reports being made during the public BE phase + /// + public class PreventClientModsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(TarkovApplication), nameof(TarkovApplication.method_20)); + } + + [PatchPrefix] + private static void Prefix() + { + CheckForNonWhitelistedPlugins(Logger); + } + + private static void CheckForNonWhitelistedPlugins(ManualLogSource logger) + { + if (MenuNotificationManager.disallowedPlugins.Any()) + { + logger.LogError($"{MenuNotificationManager.release.illegalPluginsLoadedText}\n{string.Join("\n", MenuNotificationManager.disallowedPlugins)}"); + throw new Exception(MenuNotificationManager.release.illegalPluginsExceptionText); + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/QTEPatch.cs b/project/Aki.Custom/Patches/QTEPatch.cs index 3fff3c4..ed66a04 100644 --- a/project/Aki.Custom/Patches/QTEPatch.cs +++ b/project/Aki.Custom/Patches/QTEPatch.cs @@ -2,14 +2,16 @@ using Aki.Reflection.Patching; using System.Reflection; using EFT; -using Aki.Reflection.Utils; -using System.Linq; +using HarmonyLib; namespace Aki.Custom.Patches { public class QTEPatch : ModulePatch { - protected override MethodBase GetTargetMethod() => typeof(HideoutPlayerOwner).GetMethod(nameof(HideoutPlayerOwner.StopWorkout)); + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(HideoutPlayerOwner), nameof(HideoutPlayerOwner.StopWorkout)); + } [PatchPostfix] private static void PatchPostfix(HideoutPlayerOwner __instance) diff --git a/project/Aki.Custom/Patches/RagfairFeePatch.cs b/project/Aki.Custom/Patches/RagfairFeePatch.cs index fddcf52..188ee5e 100644 --- a/project/Aki.Custom/Patches/RagfairFeePatch.cs +++ b/project/Aki.Custom/Patches/RagfairFeePatch.cs @@ -1,9 +1,9 @@ using Aki.Common.Http; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT.InventoryLogic; using EFT.UI.Ragfair; using System.Reflection; +using HarmonyLib; using UnityEngine; namespace Aki.Custom.Patches @@ -17,33 +17,32 @@ namespace Aki.Custom.Patches public RagfairFeePatch() { // Remember to update prefix parameter if below lines are broken - _ = nameof(GClass2860.IsAllSelectedItemSame); - _ = nameof(GClass2860.AutoSelectSimilar); + _ = nameof(GClass3069.IsAllSelectedItemSame); + _ = nameof(GClass3069.AutoSelectSimilar); } protected override MethodBase GetTargetMethod() { - return typeof(AddOfferWindow).GetMethod("method_1", PatchConstants.PrivateFlags); + return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.method_1)); } - /// - /// Calculate tax to charge player and send to server before the offer is sent - /// - /// Item sold - /// OfferItemCount - /// RequirementsPrice - /// SellInOnePiece - [PatchPrefix] - private static void PatchPrefix(ref Item ___item_0, ref GClass2860 ___gclass2860_0, ref double ___double_0, ref bool ___bool_0) + /// + /// Calculate tax to charge player and send to server before the offer is sent + /// + /// Item sold + /// OfferItemCount + /// RequirementsPrice + /// SellInOnePiece + [PatchPrefix] + private static void PatchPrefix(ref Item ___item_0, ref GClass3069 ___gclass3069_0, ref double ___double_0, ref bool ___bool_0) { RequestHandler.PutJson("/client/ragfair/offerfees", new { id = ___item_0.Id, tpl = ___item_0.TemplateId, - count = ___gclass2860_0.OfferItemCount, - fee = Mathf.CeilToInt((float)GClass1941.CalculateTaxPrice(___item_0, ___gclass2860_0.OfferItemCount, ___double_0, ___bool_0)) - } - .ToJson()); + count = ___gclass3069_0.OfferItemCount, + fee = Mathf.CeilToInt((float)GClass2089.CalculateTaxPrice(___item_0, ___gclass3069_0.OfferItemCount, ___double_0, ___bool_0)) + }.ToJson()); } } } \ No newline at end of file diff --git a/project/Aki.Custom/Patches/RaidSettingsWindowPatch.cs b/project/Aki.Custom/Patches/RaidSettingsWindowPatch.cs index be71c06..dcfd061 100644 --- a/project/Aki.Custom/Patches/RaidSettingsWindowPatch.cs +++ b/project/Aki.Custom/Patches/RaidSettingsWindowPatch.cs @@ -5,6 +5,7 @@ using Aki.Custom.Models; using EFT.UI; using EFT.UI.Matchmaker; using System.Reflection; +using HarmonyLib; namespace Aki.Custom.Patches { @@ -14,19 +15,23 @@ namespace Aki.Custom.Patches /// public class RaidSettingsWindowPatch : ModulePatch { + /// + /// Target method should have ~20 .UpdateValue() calls in it + /// protected override MethodBase GetTargetMethod() { - var desiredType = typeof(RaidSettingsWindow); - var desiredMethod = desiredType.GetMethod("method_8", BindingFlags.NonPublic | BindingFlags.Instance); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(RaidSettingsWindow), nameof(RaidSettingsWindow.method_8)); } [PatchPrefix] - private static bool PatchPreFix(UpdatableToggle ____enableBosses, UpdatableToggle ____scavWars, UpdatableToggle ____taggedAndCursed, DropDownBox ____aiDifficultyDropdown, DropDownBox ____aiAmountDropdown) + private static bool PatchPreFix( + UpdatableToggle ____enableBosses, + UpdatableToggle ____scavWars, + UpdatableToggle ____taggedAndCursed, + DropDownBox ____aiDifficultyDropdown, + DropDownBox ____aiAmountDropdown, + UpdatableToggle ____randomWeatherToggle, + UpdatableToggle ____randomTimeToggle) { var json = RequestHandler.GetJson("/singleplayer/settings/raid/menu"); var settings = Json.Deserialize(json); @@ -35,7 +40,10 @@ namespace Aki.Custom.Patches ____scavWars.UpdateValue(false); ____taggedAndCursed.UpdateValue(settings.TaggedAndCursed); ____aiDifficultyDropdown.UpdateValue((int)settings.AiDifficulty); - ____aiAmountDropdown.UpdateValue((int)(settings.AiAmount)); + ____aiAmountDropdown.UpdateValue((int)settings.AiAmount); + + ____randomWeatherToggle.UpdateValue(settings.RandomWeather); + ____randomTimeToggle.UpdateValue(settings.RandomTime); return false; } diff --git a/project/Aki.Custom/Patches/RankPanelPatch.cs b/project/Aki.Custom/Patches/RankPanelPatch.cs index 2a445e1..e62c9fb 100644 --- a/project/Aki.Custom/Patches/RankPanelPatch.cs +++ b/project/Aki.Custom/Patches/RankPanelPatch.cs @@ -1,9 +1,9 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using Comfort.Common; using EFT; using EFT.UI; using System.Reflection; +using HarmonyLib; namespace Aki.Custom.Patches { @@ -11,13 +11,7 @@ namespace Aki.Custom.Patches { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(RankPanel); - var desiredMethod = desiredType.GetMethod("Show", PatchConstants.PublicFlags); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(RankPanel), nameof(RankPanel.Show)); } [PatchPrefix] diff --git a/project/Aki.Custom/Patches/ResetTraderServicesPatch.cs b/project/Aki.Custom/Patches/ResetTraderServicesPatch.cs new file mode 100644 index 0000000..ff8731d --- /dev/null +++ b/project/Aki.Custom/Patches/ResetTraderServicesPatch.cs @@ -0,0 +1,22 @@ +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using EFT; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + public class ResetTraderServicesPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BaseLocalGame), nameof(BaseLocalGame.Stop)); + } + + [PatchPrefix] + private static void PatchPrefix() + { + TraderServicesManager.Instance.Clear(); + } + } +} diff --git a/project/Aki.Custom/Patches/ScavItemCheckmarkPatch.cs b/project/Aki.Custom/Patches/ScavItemCheckmarkPatch.cs new file mode 100644 index 0000000..0c85112 --- /dev/null +++ b/project/Aki.Custom/Patches/ScavItemCheckmarkPatch.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using Comfort.Common; +using EFT; +using EFT.UI.DragAndDrop; +using HarmonyLib; + +namespace Aki.Custom.Patches +{ + + public class ScavItemCheckmarkPatch : ModulePatch + { + /// + /// This patch runs both inraid and on main Menu everytime the inventory is loaded + /// Aim is to let Scavs see what required items your PMC needs for quests like Live using the FiR status + /// + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(QuestItemViewPanel), nameof(QuestItemViewPanel.smethod_0)); + } + + [PatchPrefix] + private static void PatchPreFix(ref IEnumerable quests) + { + var gameWorld = Singleton.Instance; + + if (gameWorld != null) + { + if (gameWorld.MainPlayer.Location != "hideout" && gameWorld.MainPlayer.Fraction == ETagStatus.Scav) + { + var pmcQuests = PatchConstants.BackEndSession.Profile.QuestsData; + var scavQuests = PatchConstants.BackEndSession.ProfileOfPet.QuestsData; + quests = pmcQuests.Concat(scavQuests); + } + } + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/ScavQuestPatch.cs b/project/Aki.Custom/Patches/ScavQuestPatch.cs index 3959cd6..7007bf6 100644 --- a/project/Aki.Custom/Patches/ScavQuestPatch.cs +++ b/project/Aki.Custom/Patches/ScavQuestPatch.cs @@ -1,10 +1,11 @@ -using Aki.Reflection.Patching; +using System.Linq; +using System.Reflection; +using Aki.Reflection.Patching; using Aki.Reflection.Utils; using EFT.UI.Matchmaker; -using System.Linq; -using System.Reflection; +using HarmonyLib; -namespace Aki.SinglePlayer.Patches.ScavMode +namespace Aki.Custom.Patches { /// /// Copy over scav-only quests from PMC profile to scav profile on pre-raid screen @@ -14,13 +15,8 @@ namespace Aki.SinglePlayer.Patches.ScavMode { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(MatchmakerOfflineRaidScreen); - var desiredMethod = desiredType.GetMethod(nameof(MatchmakerOfflineRaidScreen.Show)); - - Logger.LogDebug($"{GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.GetDeclaredMethods(typeof(MatchmakerOfflineRaidScreen)) + .SingleCustom(m => m.Name == nameof(MatchmakerOfflineRaidScreen.Show) && m.GetParameters().Length == 1); } [PatchPostfix] diff --git a/project/Aki.Custom/Patches/SessionIdPatch.cs b/project/Aki.Custom/Patches/SessionIdPatch.cs index dbf33cc..ef2190e 100644 --- a/project/Aki.Custom/Patches/SessionIdPatch.cs +++ b/project/Aki.Custom/Patches/SessionIdPatch.cs @@ -1,8 +1,9 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT.UI; using System.IO; using System.Reflection; +using EFT; +using HarmonyLib; using UnityEngine; namespace Aki.Custom.Patches @@ -11,15 +12,10 @@ namespace Aki.Custom.Patches { private static PreloaderUI _preloader; - static SessionIdPatch() - { - _preloader = null; - } - protected override MethodBase GetTargetMethod() { - return PatchConstants.LocalGameType.BaseType.GetMethod("method_5", PatchConstants.PrivateFlags); - } + return AccessTools.Method(typeof(BaseLocalGame), nameof(BaseLocalGame.method_5)); + } [PatchPostfix] private static void PatchPostfix() diff --git a/project/Aki.Custom/Patches/SetLocationIdOnRaidStartPatch.cs b/project/Aki.Custom/Patches/SetLocationIdOnRaidStartPatch.cs new file mode 100644 index 0000000..e1b5ae7 --- /dev/null +++ b/project/Aki.Custom/Patches/SetLocationIdOnRaidStartPatch.cs @@ -0,0 +1,68 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using EFT; +using System.Linq; +using System.Reflection; +using Comfort.Common; +using System; +using static LocationSettingsClass; + +namespace Aki.Custom.Patches +{ + /// + /// Local games do not set the locationId property like a network game does, `LocationId` is used by various bsg systems + /// e.g. btr/lightkeeper services + /// + public class SetLocationIdOnRaidStartPatch : ModulePatch + { + private static PropertyInfo _locationProperty; + + protected override MethodBase GetTargetMethod() + { + Type localGameBaseType = PatchConstants.LocalGameType.BaseType; + + // At this point, gameWorld.MainPlayer isn't set, so we need to use the LocalGame's `Location_0` property + _locationProperty = localGameBaseType.GetProperties(PatchConstants.PublicDeclaredFlags) + .SingleCustom(x => x.PropertyType == typeof(Location)); + + // Find the TimeAndWeatherSettings handling method + var desiredMethod = localGameBaseType.GetMethods(PatchConstants.PublicDeclaredFlags).SingleOrDefault(IsTargetMethod); + + Logger.LogDebug($"{GetType().Name} Type: {localGameBaseType?.Name}"); + Logger.LogDebug($"{GetType().Name} Method: {desiredMethod?.Name}"); + + return desiredMethod; + } + + private static bool IsTargetMethod(MethodInfo mi) + { + // Find method_3(TimeAndWeatherSettings timeAndWeather) + var parameters = mi.GetParameters(); + return (parameters.Length == 1 && parameters[0].ParameterType == typeof(TimeAndWeatherSettings)); + } + + [PatchPostfix] + private static void PatchPostfix(AbstractGame __instance) + { + var gameWorld = Singleton.Instance; + + // EFT.HideoutGame is an internal class, so we can't do static type checking :( + if (__instance.GetType().Name.Contains("HideoutGame")) + { + return; + } + + Location location = _locationProperty.GetValue(__instance) as Location; + + if (location == null) + { + Logger.LogError($"[SetLocationId] Failed to get location data"); + return; + } + + gameWorld.LocationId = location.Id; + + Logger.LogDebug($"[SetLocationId] Set locationId to: {location.Id}"); + } + } +} diff --git a/project/Aki.Custom/Patches/SettingsLocationPatch.cs b/project/Aki.Custom/Patches/SettingsLocationPatch.cs index 1d9f08f..895021a 100644 --- a/project/Aki.Custom/Patches/SettingsLocationPatch.cs +++ b/project/Aki.Custom/Patches/SettingsLocationPatch.cs @@ -6,9 +6,12 @@ using System.Reflection; namespace Aki.Custom.Patches { + /// + /// Redirect the settings data to save into the SPT folder, not app data + /// public class SettingsLocationPatch : ModulePatch { - private static string _sptPath = Path.Combine(Environment.CurrentDirectory, "user", "sptSettings"); + private static readonly string _sptPath = Path.Combine(Environment.CurrentDirectory, "user", "sptSettings"); protected override MethodBase GetTargetMethod() { diff --git a/project/Aki.Custom/Patches/VersionLabelPatch.cs b/project/Aki.Custom/Patches/VersionLabelPatch.cs index 9feaba3..7ba51e5 100644 --- a/project/Aki.Custom/Patches/VersionLabelPatch.cs +++ b/project/Aki.Custom/Patches/VersionLabelPatch.cs @@ -5,7 +5,6 @@ using Aki.Reflection.Utils; using Aki.Custom.Models; using EFT.UI; using HarmonyLib; -using System.Linq; using System.Reflection; using Comfort.Common; @@ -17,18 +16,9 @@ namespace Aki.Custom.Patches protected override MethodBase GetTargetMethod() { - try - { - return PatchConstants.EftTypes - .Single(x => x.GetField("Taxonomy", BindingFlags.Public | BindingFlags.Instance) != null) + return PatchConstants.EftTypes + .SingleCustom(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] @@ -43,7 +33,9 @@ namespace Aki.Custom.Patches Traverse.Create(Singleton.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}"); Traverse.Create(Singleton.Instance).Field("string_2").SetValue(_versionLabel); - Traverse.Create(__result).Field("Major").SetValue(_versionLabel); + var major = Traverse.Create(__result).Field("Major"); + var existingValue = major.GetValue(); + major.SetValue($"{existingValue} {_versionLabel}"); } } } \ No newline at end of file diff --git a/project/Aki.Custom/Utils/BundleManager.cs b/project/Aki.Custom/Utils/BundleManager.cs index 95e0d83..caf11ac 100644 --- a/project/Aki.Custom/Utils/BundleManager.cs +++ b/project/Aki.Custom/Utils/BundleManager.cs @@ -1,50 +1,109 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using BepInEx.Logging; +using Newtonsoft.Json; 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; } + private static ManualLogSource _logger; + public static readonly ConcurrentDictionary Bundles; + public static string CachePath; static BundleManager() { - Bundles = new Dictionary(); + _logger = Logger.CreateLogSource(nameof(BundleManager)); + Bundles = new ConcurrentDictionary(); + CachePath = "user/cache/bundles/"; + } - if (VFS.Exists(CachePath)) - { - VFS.DeleteDirectory(CachePath); - } + public static string GetBundlePath(BundleItem bundle) + { + return RequestHandler.IsLocal + ? $"{bundle.ModPath}/bundles/{bundle.FileName}" + : CachePath + bundle.FileName; } public static void GetBundles() { + // get bundles var json = RequestHandler.GetJson("/singleplayer/bundles"); - var jArray = JArray.Parse(json); + var bundles = JsonConvert.DeserializeObject(json); - foreach (var jObj in jArray) + // register bundles + var toDownload = new ConcurrentBag(); + + Parallel.ForEach(bundles, (bundle) => { - var key = jObj["key"].ToString(); - var path = jObj["path"].ToString(); - var bundle = new BundleInfo(key, path, jObj["dependencyKeys"].ToObject()); + Bundles.TryAdd(bundle.FileName, bundle); - if (path.Contains("http")) + if (ShouldReaquire(bundle)) { - var filepath = CachePath + Regex.Split(path, "bundle/", RegexOptions.IgnoreCase)[1]; - var data = RequestHandler.GetData(path, true); - VFS.WriteFile(filepath, data); - bundle.Path = filepath; + // mark for download + toDownload.Add(bundle); } + }); - Bundles.Add(key, bundle); + if (RequestHandler.IsLocal) + { + // loading from local mods + _logger.LogInfo("CACHE: Loading all bundles from mods on disk."); + return; + } + else + { + // download bundles + // NOTE: assumes bundle keys to be unique + Parallel.ForEach(toDownload, (bundle) => + { + // download bundle + var filepath = GetBundlePath(bundle); + var data = RequestHandler.GetData($"/files/bundle/{bundle.FileName}"); + VFS.WriteFile(filepath, data); + }); + } + } + + private static bool ShouldReaquire(BundleItem bundle) + { + if (RequestHandler.IsLocal) + { + // only handle remote bundles + return false; } - VFS.WriteTextFile(CachePath + "bundles.json", Json.Serialize>(Bundles)); + // read cache + var filepath = CachePath + bundle.FileName; + + if (VFS.Exists(filepath)) + { + // calculate hash + var data = VFS.ReadFile(filepath); + var crc = Crc32.Compute(data); + + if (crc == bundle.Crc) + { + // file is up-to-date + _logger.LogInfo($"CACHE: Loading locally {bundle.FileName}"); + return false; + } + else + { + // crc doesn't match, reaquire the file + _logger.LogInfo($"CACHE: Bundle is invalid, (re-)acquiring {bundle.FileName}"); + return true; + } + } + else + { + // file doesn't exist in cache + _logger.LogInfo($"CACHE: Bundle is missing, (re-)acquiring {bundle.FileName}"); + return true; + } } } } diff --git a/project/Aki.Custom/Utils/Crc32.cs b/project/Aki.Custom/Utils/Crc32.cs new file mode 100644 index 0000000..afbc858 --- /dev/null +++ b/project/Aki.Custom/Utils/Crc32.cs @@ -0,0 +1,80 @@ +/* + By Senko-san (Merijn Hendriks) + Taken from https://github.com/spt-haru/haru +*/ + +using System; + +namespace Aki.Custom.Utils +{ + public class Crc32 + { + private static readonly uint[] _lookupTable = new uint[] + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, + 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, + 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, + 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, + 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, + 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, + 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, + 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, + 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, + 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, + 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, + 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, + 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, + 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, + 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, + 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, + 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, + 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, + 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, + 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, + 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, + 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, + 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, + 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, + 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, + 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, + 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, + 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, + 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, + 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, + 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, + 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, + 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, + 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, + 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, + 0x2D02EF8D + }; + + public static uint Compute(byte[] bytes) + { + var crc = uint.MaxValue; + + foreach (var octet in bytes) + { + crc = _lookupTable[(crc ^ octet) & byte.MaxValue] ^ crc >> 8; + } + + return ~crc; + } + } +} diff --git a/project/Aki.Custom/Utils/EasyBundleHelper.cs b/project/Aki.Custom/Utils/EasyBundleHelper.cs index f3fdf40..84f09e6 100644 --- a/project/Aki.Custom/Utils/EasyBundleHelper.cs +++ b/project/Aki.Custom/Utils/EasyBundleHelper.cs @@ -11,7 +11,7 @@ namespace Aki.Custom.Utils { public class EasyBundleHelper { - private const BindingFlags _NonPublicInstanceflags = BindingFlags.Instance | BindingFlags.NonPublic; + private const BindingFlags NonPublicInstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic; private static readonly FieldInfo _pathField; private static readonly FieldInfo _keyWithoutExtensionField; private static readonly FieldInfo _bundleLockField; @@ -27,24 +27,28 @@ namespace Aki.Custom.Utils _ = nameof(IBundleLock.IsLocked); _ = nameof(BindableState.Bind); - // Class can be found as a private array inside EasyAssets.cs, next to DependencyGraph - Type = PatchConstants.EftTypes.Single(x => x.GetMethod("set_SameNameAsset", _NonPublicInstanceflags) != null); + Type = PatchConstants.EftTypes.SingleCustom(x => !x.IsInterface && x.GetProperty("SameNameAsset", PatchConstants.PublicDeclaredFlags) != null); - _pathField = Type.GetField("string_1", _NonPublicInstanceflags); - _keyWithoutExtensionField = Type.GetField("string_0", _NonPublicInstanceflags); - _bundleLockField = Type.GetFields(_NonPublicInstanceflags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock)); + _pathField = Type.GetField("string_1", NonPublicInstanceFlags); + _keyWithoutExtensionField = Type.GetField("string_0", NonPublicInstanceFlags); + _bundleLockField = Type.GetFields(NonPublicInstanceFlags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock)); _dependencyKeysProperty = Type.GetProperty("DependencyKeys"); _keyProperty = Type.GetProperty("Key"); _loadStateProperty = Type.GetProperty("LoadState"); // Function with 0 params and returns task (usually method_0()) - var possibleMethods = Type.GetMethods(_NonPublicInstanceflags).Where(x => x.GetParameters().Length == 0 && x.ReturnType == typeof(Task)); - if (possibleMethods.Count() > 1) + var possibleMethods = Type.GetMethods(PatchConstants.PublicDeclaredFlags).Where(x => x.GetParameters().Length == 0 && x.ReturnType == typeof(Task)).ToArray(); + if (possibleMethods.Length > 1) { - Console.WriteLine($"Unable to find desired method as there are multiple possible matches: {string.Join(",", possibleMethods.Select(x => x.Name))}"); + throw new Exception($"Unable to find the Loading Coroutine method as there are multiple possible matches: {string.Join(",", possibleMethods.Select(x => x.Name))}"); } - _loadingCoroutineMethod = possibleMethods.SingleOrDefault(); + if (possibleMethods.Length == 0) + { + throw new Exception("Unable to find the Loading Coroutine method as there are no matches"); + } + + _loadingCoroutineMethod = possibleMethods.Single(); } public EasyBundleHelper(object easyBundle) diff --git a/project/Aki.Custom/Utils/MenuNotificationManager.cs b/project/Aki.Custom/Utils/MenuNotificationManager.cs new file mode 100644 index 0000000..573738b --- /dev/null +++ b/project/Aki.Custom/Utils/MenuNotificationManager.cs @@ -0,0 +1,169 @@ +using Aki.Common.Http; +using Aki.Common.Utils; +using Aki.Custom.Models; +using Aki.SinglePlayer.Patches.MainMenu; +using BepInEx.Bootstrap; +using BepInEx.Logging; +using Comfort.Common; +using EFT.UI; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Aki.SinglePlayer.Utils.MainMenu +{ + public class MenuNotificationManager : MonoBehaviour + { + public static string sptVersion; + public static string commitHash; + internal static HashSet whitelistedPlugins = new HashSet + { + "com.spt-aki.core", + "com.spt-aki.custom", + "com.spt-aki.debugging", + "com.spt-aki.singleplayer", + "com.bepis.bepinex.configurationmanager", + "com.terkoiz.freecam", + "com.sinai.unityexplorer", + "com.cwx.debuggingtool-dxyz", + "com.cwx.debuggingtool", + "xyz.drakia.botdebug", + "com.kobrakon.camunsnap", + "RuntimeUnityEditor" + }; + + public static string[] disallowedPlugins; + internal static ReleaseResponse release; + private bool _isBetaDisclaimerOpen = false; + private ManualLogSource Logger; + + public void Start() + { + Logger = BepInEx.Logging.Logger.CreateLogSource(nameof(MenuNotificationManager)); + + var versionJson = RequestHandler.GetJson("/singleplayer/settings/version"); + sptVersion = Json.Deserialize(versionJson).Version; + commitHash = sptVersion?.Trim()?.Split(' ')?.Last() ?? ""; + + var releaseJson = RequestHandler.GetJson("/singleplayer/release"); + release = Json.Deserialize(releaseJson); + + SetVersionPref(); + + // Enable the watermark if this is a bleeding edge build. + // Enabled in start to allow time for the request containing the bool to process. + if (release.isBeta) + { + new BetaLogoPatch().Enable(); + new BetaLogoPatch2().Enable(); + new BetaLogoPatch3().Enable(); + } + + disallowedPlugins = Chainloader.PluginInfos.Values + .Select(pi => pi.Metadata.GUID).Except(whitelistedPlugins).ToArray(); + + // Prevent client mods if the server is built with mods disabled + if (!release.isModdable) + { + new PreventClientModsPatch().Enable(); + } + + if (release.isBeta && PlayerPrefs.GetInt("SPT_AcceptedBETerms") == 1) + { + Logger.LogInfo(release.betaDisclaimerAcceptText); + ServerLog.Info("Aki.Custom", release.betaDisclaimerAcceptText); + } + + if (release.isModded && release.isBeta && release.isModdable) + { + commitHash += $"\n {release.serverModsLoadedDebugText}"; + ServerLog.Warn("Aki.Custom", release.serverModsLoadedText); + } + + if (disallowedPlugins.Any() && release.isBeta && release.isModdable) + { + commitHash += $"\n {release.clientModsLoadedDebugText}"; + ServerLog.Warn("Aki.Custom", $"{release.clientModsLoadedText}\n{string.Join("\n", disallowedPlugins)}"); + } + } + public void Update() + { + if (sptVersion == null) + { + return; + } + + ShowBetaMessage(); + ShowReleaseNotes(); + } + + // Show the beta message + // if mods are enabled show that mods are loaded in the message. + private void ShowBetaMessage() + { + if (Singleton.Instantiated && ShouldShowBetaMessage()) + { + Singleton.Instance.ShowCriticalErrorScreen(sptVersion, release.betaDisclaimerText, ErrorScreen.EButtonType.OkButton, release.betaDisclaimerTimeoutDelay, new Action(OnMessageAccepted), new Action(OnTimeOut)); + _isBetaDisclaimerOpen = true; + } + } + + // Show the release notes. + private void ShowReleaseNotes() + { + if (Singleton.Instantiated && ShouldShowReleaseNotes()) + { + Singleton.Instance.ShowCriticalErrorScreen(sptVersion, release.releaseSummaryText, ErrorScreen.EButtonType.OkButton, 36000, null, null); + PlayerPrefs.SetInt("SPT_ShownReleaseNotes", 1); + } + } + + // User accepted the terms, allow to continue. + private void OnMessageAccepted() + { + Logger.LogInfo(release.betaDisclaimerAcceptText); + PlayerPrefs.SetInt("SPT_AcceptedBETerms", 1); + _isBetaDisclaimerOpen = false; + } + + // If the user doesnt accept the message "Ok" then the game will close. + private void OnTimeOut() + { + Application.Quit(); + } + + // Stores the current build in the registry to check later + // Return true if changed, false if not + private void SetVersionPref() + { + if (GetVersionPref() == string.Empty || GetVersionPref() != sptVersion) + { + PlayerPrefs.SetString("SPT_Version", sptVersion); + + // 0 val used to indicate false, 1 val used to indicate true + PlayerPrefs.SetInt("SPT_AcceptedBETerms", 0); + PlayerPrefs.SetInt("SPT_ShownReleaseNotes", 0); + } + } + + // Retrieves the current build from the registry to check against the current build + // If this is the first run and no entry exists returns an empty string + private string GetVersionPref() + { + return PlayerPrefs.GetString("SPT_Version", string.Empty); + } + + // Should we show the message, only show if first run or if build has changed + private bool ShouldShowBetaMessage() + { + return PlayerPrefs.GetInt("SPT_AcceptedBETerms") == 0 && release.isBeta && !_isBetaDisclaimerOpen ? true : false; + } + + // Should we show the release notes, only show on first run or if build has changed + private bool ShouldShowReleaseNotes() + { + return PlayerPrefs.GetInt("SPT_ShownReleaseNotes") == 0 && !_isBetaDisclaimerOpen && release.releaseSummaryText != string.Empty ? true : false; + } + } +} diff --git a/project/Aki.Custom/Utils/MessageBoxHelper.cs b/project/Aki.Custom/Utils/MessageBoxHelper.cs new file mode 100644 index 0000000..d3fe762 --- /dev/null +++ b/project/Aki.Custom/Utils/MessageBoxHelper.cs @@ -0,0 +1,56 @@ +using System; +using System.Runtime.InteropServices; + +namespace Aki.Custom.Utils +{ + public class MessageBoxHelper + { + public enum MessageBoxType : uint + { + ABORTRETRYIGNORE = (uint)(0x00000002L | 0x00000010L), + CANCELTRYCONTINUE = (uint)(0x00000006L | 0x00000030L), + HELP = (uint)(0x00004000L | 0x00000040L), + OK = (uint)(0x00000000L | 0x00000040L), + OKCANCEL = (uint)(0x00000001L | 0x00000040L), + RETRYCANCEL = (uint)0x00000005L, + YESNO = (uint)(0x00000004L | 0x00000040L), + YESNOCANCEL = (uint)(0x00000003L | 0x00000040L), + DEFAULT = (uint)(0x00000000L | 0x00000010L) + } + + public enum MessageBoxResult + { + ERROR = -1, + OK = 1, + CANCEL = 2, + ABORT = 3, + RETRY = 4, + IGNORE = 5, + YES = 6, + NO = 7, + TRY_AGAIN = 10 + } + + [DllImport("user32.dll")] + private static extern IntPtr GetActiveWindow(); + [DllImport("user32.dll", SetLastError = true)] + static extern int MessageBox(IntPtr hwnd, String lpText, String lpCaption, uint uType); + + public static IntPtr GetWindowHandle() + { + return GetActiveWindow(); + } + + public static MessageBoxResult Show(string text, string caption, MessageBoxType type = MessageBoxType.DEFAULT) + { + try + { + return (MessageBoxResult)MessageBox(GetWindowHandle(), text, caption, (uint)type); ; + } + catch (Exception) + { + return MessageBoxResult.ERROR; + } + } + } +} diff --git a/project/Aki.Debugging/Aki.Debugging.csproj b/project/Aki.Debugging/Aki.Debugging.csproj index 3958538..e6b446e 100644 --- a/project/Aki.Debugging/Aki.Debugging.csproj +++ b/project/Aki.Debugging/Aki.Debugging.csproj @@ -1,17 +1,19 @@  - net472 + net471 aki-debugging + Release Aki - Copyright @ Aki 2022 + Copyright @ Aki 2024 + @@ -20,10 +22,15 @@ + + + + + diff --git a/project/Aki.Debugging/AkiDebuggingPlugin.cs b/project/Aki.Debugging/AkiDebuggingPlugin.cs index ead4d08..68834ea 100644 --- a/project/Aki.Debugging/AkiDebuggingPlugin.cs +++ b/project/Aki.Debugging/AkiDebuggingPlugin.cs @@ -1,5 +1,7 @@ using System; using Aki.Common; +using Aki.Common.Http; +using Aki.Common.Utils; using Aki.Debugging.Patches; using BepInEx; @@ -8,15 +10,27 @@ namespace Aki.Debugging [BepInPlugin("com.spt-aki.debugging", "AKI.Debugging", AkiPluginInfo.PLUGIN_VERSION)] public class AkiDebuggingPlugin : BaseUnityPlugin { + public static LoggingLevelResponse logLevel; + public void Awake() { Logger.LogInfo("Loading: Aki.Debugging"); try { - // new CoordinatesPatch().Enable(); new EndRaidDebug().Enable(); - } + new LoggerClassLogPatch().Enable(); + // new CoordinatesPatch().Enable(); + // new StaticLootDumper().Enable(); + // new ExfilDumper().Enable(); + + // BTR debug command patches, can be disabled later + //new BTRDebugCommandPatch().Enable(); + //new BTRDebugDataPatch().Enable(); + + //new PMCBotSpawnLocationPatch().Enable(); + //new ReloadClientPatch().Enable(); + } catch (Exception ex) { Logger.LogError($"{GetType().Name}: {ex}"); @@ -25,5 +39,11 @@ namespace Aki.Debugging Logger.LogInfo("Completed: Aki.Debugging"); } + + public void Start() + { + var loggingJson = RequestHandler.GetJson("/singleplayer/enableBSGlogging"); + logLevel = Json.Deserialize(loggingJson); + } } } diff --git a/project/Aki.Debugging/Patches/BTRDebugCommandPatch.cs b/project/Aki.Debugging/Patches/BTRDebugCommandPatch.cs new file mode 100644 index 0000000..a9b7a37 --- /dev/null +++ b/project/Aki.Debugging/Patches/BTRDebugCommandPatch.cs @@ -0,0 +1,49 @@ +using Aki.Custom.BTR.Patches; +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using EFT; +using EFT.UI; +using HarmonyLib; +using System.Reflection; +using DialogControlClass = GClass1957; + +namespace Aki.Debugging.Patches +{ + // Enable the `debug_show_dialog_screen` command, and custom `btr_deliver_items` command + internal class BTRDebugCommandPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ConsoleScreen), nameof(ConsoleScreen.InitConsole)); + } + + [PatchPostfix] + internal static void PatchPostfix() + { + ConsoleScreen.Processor.RegisterCommandGroup(); + ConsoleScreen.Processor.RegisterCommand("btr_deliver_items", new System.Action(BtrDeliverItemsCommand)); + } + + // Custom command to force item extraction sending + public static void BtrDeliverItemsCommand() + { + BTREndRaidItemDeliveryPatch.PatchPrefix(); + } + } + + // When running the `debug_show_dialog_screen` command, fetch the service data first, and force debug off + public class BTRDebugDataPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(DialogControlClass), nameof(DialogControlClass.ShowDialogScreen)); + } + + [PatchPrefix] + internal static void PatchPrefix(Profile.ETraderServiceSource traderServiceSourceType, ref bool useDebugData) + { + useDebugData = false; + TraderServicesManager.Instance.GetTraderServicesDataFromServer(Profile.TraderInfo.TraderServiceToId[traderServiceSourceType]); + } + } +} diff --git a/project/Aki.Debugging/Patches/CoordinatesPatch.cs b/project/Aki.Debugging/Patches/CoordinatesPatch.cs index 0be13a0..70927d5 100644 --- a/project/Aki.Debugging/Patches/CoordinatesPatch.cs +++ b/project/Aki.Debugging/Patches/CoordinatesPatch.cs @@ -1,51 +1,47 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; using TMPro; using UnityEngine; using System; using System.Reflection; +using HarmonyLib; 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); + return AccessTools.Method(typeof(BaseLocalGame), nameof(BaseLocalGame.Update)); } [PatchPrefix] - private static void PatchPrefix(object __instance) + private static void PatchPrefix(BaseLocalGame __instance) { - if (Input.GetKeyDown(KeyCode.LeftControl)) + if (!Input.GetKeyDown(KeyCode.LeftControl)) return; + + if (_alphaLabel == null) { - 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}]"); + _alphaLabel = GameObject.Find("AlphaLabel").GetComponent(); + _alphaLabel.color = Color.green; + _alphaLabel.fontSize = 22; + _alphaLabel.font = Resources.Load("Fonts & Materials/ARIAL SDF"); } + + var playerOwner = __instance.PlayerOwner; + 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) diff --git a/project/Aki.Debugging/Patches/DataHandlerDebugPatch.cs b/project/Aki.Debugging/Patches/DataHandlerDebugPatch.cs new file mode 100644 index 0000000..30e7cd7 --- /dev/null +++ b/project/Aki.Debugging/Patches/DataHandlerDebugPatch.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; +using Aki.Reflection.Patching; +using HarmonyLib; + +namespace Aki.Debugging.Patches +{ + public class DataHandlerDebugPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(DataHandler), nameof(DataHandler.method_5)); + } + + [PatchPostfix] + private static void PatchPrefix(ref string __result) + { + Console.WriteLine($"response json: ${__result}"); + } + } +} \ No newline at end of file diff --git a/project/Aki.Debugging/Patches/EndRaidDebug.cs b/project/Aki.Debugging/Patches/EndRaidDebug.cs index 869c411..3adc994 100644 --- a/project/Aki.Debugging/Patches/EndRaidDebug.cs +++ b/project/Aki.Debugging/Patches/EndRaidDebug.cs @@ -1,9 +1,9 @@ using Aki.Reflection.Patching; using System.Reflection; -using Aki.Reflection.Utils; using BepInEx.Logging; using EFT; using EFT.UI; +using HarmonyLib; using TMPro; namespace Aki.Debugging.Patches @@ -12,12 +12,12 @@ namespace Aki.Debugging.Patches { protected override MethodBase GetTargetMethod() { - return typeof(TraderCard).GetMethod("method_0", PatchConstants.PrivateFlags); + return AccessTools.Method(typeof(TraderCard), nameof(TraderCard.method_0)); } [PatchPrefix] private static bool PatchPreFix(ref LocalizedText ____nickName, ref TMP_Text ____standing, - ref RankPanel ____rankPanel, ref Profile.GClass1625 ___gclass1625_0) + ref RankPanel ____rankPanel, ref Profile.TraderInfo ___traderInfo_0) { if (____nickName.LocalizationKey == null) { @@ -37,16 +37,16 @@ namespace Aki.Debugging.Patches return false; // skip original } - if (___gclass1625_0?.LoyaltyLevel == null) + if (___traderInfo_0?.LoyaltyLevel == null) { ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); - Logger.Log(LogLevel.Error, "[AKI] _gclass1618_0 or _gclass1618_0.LoyaltyLevel was null"); + Logger.Log(LogLevel.Error, "[AKI] ___traderInfo_0 or ___traderInfo_0.LoyaltyLevel was null"); } - if (___gclass1625_0?.MaxLoyaltyLevel == null) + if (___traderInfo_0?.MaxLoyaltyLevel == null) { ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); - Logger.Log(LogLevel.Error, "[AKI] _gclass1618_0 or _gclass1618_0.MaxLoyaltyLevel was null"); + Logger.Log(LogLevel.Error, "[AKI] ___traderInfo_0 or ___traderInfo_0.MaxLoyaltyLevel was null"); } return true; diff --git a/project/Aki.Debugging/Patches/ExfilDumper.cs b/project/Aki.Debugging/Patches/ExfilDumper.cs new file mode 100644 index 0000000..d665687 --- /dev/null +++ b/project/Aki.Debugging/Patches/ExfilDumper.cs @@ -0,0 +1,140 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.Interactive; +using EFT.InventoryLogic; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using ExitSettingsClass = GClass1225; + +namespace Aki.Debugging.Patches +{ + internal class ExfilDumper : ModulePatch + { + public static string DumpFolder = Path.Combine(Environment.CurrentDirectory, "ExfilDumps"); + + protected override MethodBase GetTargetMethod() + { + return typeof(ExfiltrationControllerClass).GetMethod(nameof(ExfiltrationControllerClass.InitAllExfiltrationPoints)); + } + + [PatchPostfix] + public static void PatchPreFix(ExitSettingsClass[] settings) + { + var gameWorld = Singleton.Instance; + string mapName = gameWorld.MainPlayer.Location.ToLower(); + + var pmcExfilPoints = ExfiltrationControllerClass.Instance.ExfiltrationPoints; + + // Both scav and PMC lists include shared, so remove them from the scav list + var scavExfilPoints = ExfiltrationControllerClass.Instance.ScavExfiltrationPoints.Where(x => !(x is SharedExfiltrationPoint)); + + var exfils = new List(); + + foreach (var exfil in pmcExfilPoints.Concat(scavExfilPoints)) + { + ExitSettingsClass exitSettings = settings.FirstOrDefault(x => x.Name == exfil.Settings.Name); + exfils.Add(new SPTExfilData(exfil, exitSettings)); + } + + string jsonString = JsonConvert.SerializeObject(exfils, Formatting.Indented); + string outputFile = Path.Combine(DumpFolder, mapName, "allExtracts.json"); + Directory.CreateDirectory(Path.GetDirectoryName(outputFile)); + if (File.Exists(outputFile)) + { + File.Delete(outputFile); + } + File.Create(outputFile).Dispose(); + StreamWriter streamWriter = new StreamWriter(outputFile); + streamWriter.Write(jsonString); + streamWriter.Flush(); + streamWriter.Close(); + } + + public class SPTExfilData + { + public float Chance = 0; + public int Count = 0; + public string EntryPoints = ""; + public bool EventAvailable = false; + public float ExfiltrationTime = 0; + public EExfiltrationType ExfiltrationType; + public string Id = ""; + public float MinTime = 0; + public float MaxTime = 0; + public string Name = ""; + public ERequirementState PassageRequirement; + public int PlayersCount = 0; + public EquipmentSlot RequiredSlot; + public string RequirementTip = ""; + public string Side = ""; + + public SPTExfilData(ExfiltrationPoint point, ExitSettingsClass settings) + { + // PMC and shared extracts, prioritize settings over the map data to match base behaviour + if (settings != null && (!(point is ScavExfiltrationPoint) || point is SharedExfiltrationPoint)) + { + if (settings != null) + { + EntryPoints = settings.EntryPoints; + Chance = settings.Chance; + EventAvailable = settings.EventAvailable; + ExfiltrationTime = settings.ExfiltrationTime; + ExfiltrationType = settings.ExfiltrationType; + MaxTime = settings.MaxTime; + MinTime = settings.MinTime; + Name = settings.Name; + PlayersCount = settings.PlayersCount; + } + } + // Scav extracts, and those without settings use the point settings + else + { + EntryPoints = String.Join(",", point.EligibleEntryPoints); + Chance = point.Settings.Chance; + EventAvailable = point.Settings.EventAvailable; + ExfiltrationTime = point.Settings.ExfiltrationTime; + ExfiltrationType = point.Settings.ExfiltrationType; + MaxTime = point.Settings.MaxTime; + MinTime = point.Settings.MinTime; + Name = point.Settings.Name; + PlayersCount = point.Settings.PlayersCount; + } + + // If there's settings, and the requirement is a reference, use that + if (settings?.PassageRequirement == ERequirementState.Reference) + { + PassageRequirement = ERequirementState.Reference; + Id = settings.Id; + } + // Otherwise use the point requirements + else if (point.HasRequirements) + { + Count = point.Requirements[0].Count; + Id = point.Requirements[0].Id; + PassageRequirement = point.Requirements[0].Requirement; + RequiredSlot = point.Requirements[0].RequiredSlot; + RequirementTip = point.Requirements[0].RequirementTip; + } + + // Store the side + if (point is SharedExfiltrationPoint) + { + Side = "Coop"; + } + else if (point is ScavExfiltrationPoint) + { + Side = "Scav"; + } + else + { + Side = "Pmc"; + } + } + } + } +} diff --git a/project/Aki.Debugging/Patches/LoggerClassPatch.cs b/project/Aki.Debugging/Patches/LoggerClassPatch.cs new file mode 100644 index 0000000..dd3c4be --- /dev/null +++ b/project/Aki.Debugging/Patches/LoggerClassPatch.cs @@ -0,0 +1,71 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using Aki.Common.Utils; +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using HarmonyLib; +using NLog; + +namespace Aki.Debugging.Patches +{ + public class LoggerClassLogPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.GetDeclaredMethods(typeof(LoggerClass)) + .SingleCustom(m => m.Name == nameof(LoggerClass.Log) && m.GetParameters().Length == 4); + } + + [PatchPostfix] + private static void PatchPostfix(string nlogFormat, string unityFormat, LogLevel logLevel, object[] args) + { + var bsgLevel = LogLevel.FromOrdinal(logLevel.Ordinal); + var sptLevel = LogLevel.FromOrdinal(AkiDebuggingPlugin.logLevel.verbosity); + + // See Nlog docs for information on ordinal levels + // Ordinal works from low to high 0 - trace, 1 - debug, 3 - info ... + if (bsgLevel >= sptLevel) + { + // First replace all { and } with {{ and }} + nlogFormat = nlogFormat.Replace("{", "{{"); + nlogFormat = nlogFormat.Replace("}", "}}"); + + // Then find any instance of "{{\d}}" and unescape its brackets + nlogFormat = Regex.Replace(nlogFormat, @"{{(\d+)}}", "{$1}"); + + try + { + nlogFormat = string.Format(nlogFormat, args); + } + catch (Exception) + { + Logger.LogError($"Error formatting string: {nlogFormat}"); + for (int i = 0; i < args.Length; i++) + { + Logger.LogError($" args[{i}] = {args[i]}"); + } + return; + } + + Logger.LogDebug($"output Nlog: {logLevel} : {nlogFormat}"); + + if (AkiDebuggingPlugin.logLevel.sendToServer) + { + ServerLog.Info("EFT Logging:", $"{logLevel} : {nlogFormat}"); + } + } + + // I've opted to leave this disabled for now, it doesn't add much in + // terms of value, its mostly the same stuff as the nlogFormat + // Deciced to keep it here incase we decide we want it later. + // After a 5 minute factory run at full verbosity, i ended up with a 20K + // line long player.log file. + + //unityFormat = Regex.Replace(unityFormat, @"\{[^{}]*[^\d{}][^{}]*\}", ""); + //unityFormat = string.Format(unityFormat, args); + //Logger.LogDebug($"Verbosity: {logLevel}"); + //Logger.LogDebug($"output unity: {unityFormat}"); + } + } +} \ No newline at end of file diff --git a/project/Aki.Debugging/Patches/PMCBotSpawnLocationPatch.cs b/project/Aki.Debugging/Patches/PMCBotSpawnLocationPatch.cs new file mode 100644 index 0000000..68edd96 --- /dev/null +++ b/project/Aki.Debugging/Patches/PMCBotSpawnLocationPatch.cs @@ -0,0 +1,101 @@ +using System.Linq; +using System.Reflection; +using Aki.Reflection.Patching; +using EFT; +using EFT.UI; +using HarmonyLib; +using System.Collections.Generic; +using EFT.Game.Spawning; +using System; +using Aki.PrePatch; + +namespace Aki.Debugging.Patches +{ + + // TODO: Instantiation of this is fairly slow, need to find best way to cache it + public class SptSpawnHelper + { + private readonly List playerSpawnPoints; + private readonly Random _rnd = new Random(); + private readonly GStruct380 _spawnSettings = new GStruct380(); + + public SptSpawnHelper() + { + IEnumerable locationSpawnPoints = GClass2928.CreateFromScene(); + + var playerSpawns = locationSpawnPoints.Where(x => x.Categories.HasFlag(ESpawnCategoryMask.Player)).ToList(); + this.playerSpawnPoints = locationSpawnPoints.Where(x => x.Categories.HasFlag(ESpawnCategoryMask.Player)).ToList(); + } + + public void PrintSpawnPoints() + { + foreach (var spawnPoint in playerSpawnPoints) + { + ConsoleScreen.Log("[AKI PMC Bot spawn] Spawn point " + spawnPoint.Id + " location is " + spawnPoint.Position.ToString()); + } + } + + public ISpawnPoint SelectSpawnPoint() + { + // TODO: Select spawn points more intelligently + return this.playerSpawnPoints[_rnd.Next(this.playerSpawnPoints.Count)]; + } + + public List SelectSpawnPoints(int count) + { + // TODO: Fine-grained spawn selection + if (count > this.playerSpawnPoints.Count()) + { + ConsoleScreen.Log($"[AKI PMC Bot spawn] Wanted ${count} but only {this.playerSpawnPoints.Count()} found, returning all"); + return this.playerSpawnPoints; + } + return this.playerSpawnPoints.OrderBy(x => _rnd.Next()).Take(count).ToList(); + } + } + + + public class PMCBotSpawnLocationPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BotSpawner), "TryToSpawnInZoneInner"); + } + + [PatchPrefix] + public static bool PatchPrefix(GClass1472 __instance, GClass591 data) + { + + var firstBotRole = data.Profiles[0].Info.Settings.Role; + if ((int)firstBotRole != AkiBotsPrePatcher.sptBearValue || (int)firstBotRole != AkiBotsPrePatcher.sptUsecValue) + { + ConsoleScreen.Log("[AKI PMC Bot spawn] Spawning a set of Scavs. Skipping..."); + return true; + } + + var helper = new SptSpawnHelper(); + var newSpawns = helper.SelectSpawnPoints(data.Count); + + for (int i = 0; i < data.Count; i++) + { + ConsoleScreen.Log($"[AKI PMC Bot spawn] Trying to spawn bot {i}"); + var currentSpawnData = data.Separate(1); + + // Unset group settings + // TODO: Allow for PMC bot groups? + currentSpawnData.SpawnParams.ShallBeGroup = null; + var spawnPointDetails = newSpawns[i]; + var currentZone = __instance.GetClosestZone(spawnPointDetails.Position, out float _); + + // CorePointId of player spawns seems to always be 0. Bots will not activate properly if this ID is used + // TODO: Verify if CorePointId of 1 is acceptable in all cases + + ConsoleScreen.Log($"[AKI PMC Bot spawn] spawn point chosen: {spawnPointDetails.Name} Core point id was: {spawnPointDetails.CorePointId}"); + currentSpawnData.AddPosition(spawnPointDetails.Position, spawnPointDetails.CorePointId); + + __instance.SpawnBotsInZoneOnPositions(newSpawns.GetRange(i, 1), currentZone, currentSpawnData); + } + + return false; + } + } +} \ No newline at end of file diff --git a/project/Aki.Debugging/Patches/ReloadClientPatch.cs b/project/Aki.Debugging/Patches/ReloadClientPatch.cs new file mode 100644 index 0000000..4904417 --- /dev/null +++ b/project/Aki.Debugging/Patches/ReloadClientPatch.cs @@ -0,0 +1,44 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.Console.Core; +using EFT.UI; +using HarmonyLib; +using System.Reflection; + +namespace Aki.Debugging.Patches +{ + public class ReloadClientPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(PreloaderUI), nameof(PreloaderUI.Awake)); + } + + [PatchPostfix] + private static void PatchPostfix() + { + ConsoleScreen.Processor.RegisterCommandGroup(); + } + + [ConsoleCommand("reload", "", null, "Reloads currently loaded profile.\nOnly use while in Main Menu" + + "\nRunning command while in hideout will cause graphical glitches and NRE to do with Nightvision. Pretty sure wont cause anything bad" + + "\nMay Cause Unexpected Behaviors inraid")] + public static void Reload() + { + + var tarkovapp = Reflection.Utils.ClientAppUtils.GetMainApp(); + GameWorld gameWorld = Singleton.Instance; + if (gameWorld != null && gameWorld.MainPlayer.Location != "hideout") + { + ConsoleScreen.LogError("You are in raid. Please only use in Mainmenu"); + return; // return early we dont want to cause errors because we are inraid + } + else if (gameWorld != null) + { + tarkovapp.HideoutControllerAccess.UnloadHideout(); + } + tarkovapp.method_48(); + } + } +} diff --git a/project/Aki.Debugging/Patches/StaticLootDumper.cs b/project/Aki.Debugging/Patches/StaticLootDumper.cs new file mode 100644 index 0000000..8474e63 --- /dev/null +++ b/project/Aki.Debugging/Patches/StaticLootDumper.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Aki.Reflection.Patching; +using EFT; +using Comfort.Common; +using EFT.Interactive; +using Newtonsoft.Json; +using UnityEngine; + +namespace Aki.Debugging.Patches +{ + public class StaticLootDumper : ModulePatch + { + public static string DumpFolder = Path.Combine(Environment.CurrentDirectory, "StaticDumps"); + + protected override MethodBase GetTargetMethod() + { + return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted)); + } + + [PatchPrefix] + public static void PatchPreFix() + { + InitDirectory(); + + var gameWorld = Singleton.Instance; + string mapName = gameWorld.MainPlayer.Location.ToLower(); + + var containersData = new SPTContainersData(); + + Resources.FindObjectsOfTypeAll(typeof(LootableContainersGroup)).ExecuteForEach(obj => + { + var containersGroup = (LootableContainersGroup)obj; + var sptContainersGroup = new SPTContainersGroup { minContainers = containersGroup.Min, maxContainers = containersGroup.Max }; + if (containersData.containersGroups.ContainsKey(containersGroup.Id)) + { + Logger.LogError($"Container group ID {containersGroup.Id} already exists in dictionary!"); + } + else + { + containersData.containersGroups.Add(containersGroup.Id, sptContainersGroup); + } + }); + + Resources.FindObjectsOfTypeAll(typeof(LootableContainer)).ExecuteForEach(obj => + { + var container = (LootableContainer)obj; + + // Skip empty ID containers + if (container.Id.Length == 0) + { + return; + } + + if (containersData.containers.ContainsKey(container.Id)) + { + Logger.LogError($"Container {container.Id} already exists in dictionary!"); + } + else + { + containersData.containers.Add(container.Id, new SPTContainer { groupId = container.LootableContainersGroupId }); + } + }); + + string jsonString = JsonConvert.SerializeObject(containersData, Formatting.Indented); + + string outputFile = Path.Combine(DumpFolder, $"{mapName}.json"); + if (File.Exists(outputFile)) + { + File.Delete(outputFile); + } + File.Create(outputFile).Dispose(); + StreamWriter streamWriter = new StreamWriter(outputFile); + streamWriter.Write(jsonString); + streamWriter.Flush(); + streamWriter.Close(); + } + + public static void InitDirectory() + { + Directory.CreateDirectory(DumpFolder); + } + } + + public class SPTContainer + { + public string groupId; + } + + public class SPTContainersGroup + { + public int minContainers; + public int maxContainers; + } + + public class SPTContainersData + { + public Dictionary containersGroups = new Dictionary(); + public Dictionary containers = new Dictionary(); + } + +} \ No newline at end of file diff --git a/project/Aki.PrePatch/Aki.PrePatch.csproj b/project/Aki.PrePatch/Aki.PrePatch.csproj index 242f07a..6d84740 100644 --- a/project/Aki.PrePatch/Aki.PrePatch.csproj +++ b/project/Aki.PrePatch/Aki.PrePatch.csproj @@ -2,13 +2,14 @@ - net472 + net471 aki_PrePatch + Release Aki - Copyright @ Aki 2022 + Copyright @ Aki 2024 diff --git a/project/Aki.PrePatch/AkiBotsPrePatcher.cs b/project/Aki.PrePatch/AkiBotsPrePatcher.cs index d2b6d71..d96d32a 100644 --- a/project/Aki.PrePatch/AkiBotsPrePatcher.cs +++ b/project/Aki.PrePatch/AkiBotsPrePatcher.cs @@ -7,8 +7,8 @@ namespace Aki.PrePatch { public static IEnumerable TargetDLLs { get; } = new[] { "Assembly-CSharp.dll" }; - public static int sptUsecValue = 38; - public static int sptBearValue = 39; + public static int sptUsecValue = 47; + public static int sptBearValue = 48; public static void Patch(ref AssemblyDefinition assembly) { diff --git a/project/Aki.Reflection/Aki.Reflection.csproj b/project/Aki.Reflection/Aki.Reflection.csproj index e7dd073..053968c 100644 --- a/project/Aki.Reflection/Aki.Reflection.csproj +++ b/project/Aki.Reflection/Aki.Reflection.csproj @@ -1,16 +1,17 @@  - net472 + net471 + Release Aki - Copyright @ Aki 2022 + Copyright @ Aki 2024 - + diff --git a/project/Aki.Reflection/Utils/PatchConstants.cs b/project/Aki.Reflection/Utils/PatchConstants.cs index 1f25785..39a1f07 100644 --- a/project/Aki.Reflection/Utils/PatchConstants.cs +++ b/project/Aki.Reflection/Utils/PatchConstants.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Comfort.Common; @@ -11,12 +12,14 @@ namespace Aki.Reflection.Utils { public static BindingFlags PrivateFlags { get; private set; } public static BindingFlags PublicFlags { get; private set; } + public static BindingFlags PublicDeclaredFlags { get; private set; } public static Type[] EftTypes { get; private set; } public static Type[] FilesCheckerTypes { get; private set; } public static Type LocalGameType { get; private set; } public static Type ExfilPointManagerType { get; private set; } public static Type SessionInterfaceType { get; private set; } public static Type BackendSessionInterfaceType { get; private set; } + public static Type BackendProfileInterfaceType { get; private set; } private static ISession _backEndSession; public static ISession BackEndSession @@ -36,14 +39,49 @@ namespace Aki.Reflection.Utils { _ = nameof(ISession.GetPhpSessionId); - PrivateFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + PrivateFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; PublicFlags = BindingFlags.Public | BindingFlags.Instance; + PublicDeclaredFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; EftTypes = typeof(AbstractGame).Assembly.GetTypes(); FilesCheckerTypes = typeof(ICheckResult).Assembly.GetTypes(); - LocalGameType = EftTypes.Single(x => x.Name == "LocalGame"); - ExfilPointManagerType = EftTypes.Single(x => x.GetMethod("InitAllExfiltrationPoints") != null); - SessionInterfaceType = EftTypes.Single(x => x.GetMethods().Select(y => y.Name).Contains("GetPhpSessionId") && x.IsInterface); - BackendSessionInterfaceType = EftTypes.Single(x => x.GetMethods().Select(y => y.Name).Contains("ChangeProfileStatus") && x.IsInterface); + LocalGameType = EftTypes.SingleCustom(x => x.Name == "LocalGame"); + ExfilPointManagerType = EftTypes.SingleCustom(x => x.GetMethod("InitAllExfiltrationPoints") != null); + SessionInterfaceType = EftTypes.SingleCustom(x => x.GetMethods().Select(y => y.Name).Contains("GetPhpSessionId") && x.IsInterface); + BackendSessionInterfaceType = EftTypes.SingleCustom(x => x.GetMethods().Select(y => y.Name).Contains("ChangeProfileStatus") && x.IsInterface); + BackendProfileInterfaceType = EftTypes.SingleCustom(x => x.GetMethods().Length == 2 && x.GetMethods().Select(y => y.Name).Contains("get_Profile") && x.IsInterface); + } + + /// + /// A custom LINQ .Single() implementation with improved logging for easier patch debugging + /// + /// A single member of the input sequence that matches the given search pattern + /// + /// + public static T SingleCustom(this IEnumerable types, Func predicate) where T : MemberInfo + { + if (types == null) + { + throw new ArgumentNullException(nameof(types)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + var matchingTypes = types.Where(predicate).ToArray(); + + if (matchingTypes.Length > 1) + { + throw new InvalidOperationException($"More than one member matches the specified search pattern: {string.Join(", ", matchingTypes.Select(t => t.Name))}"); + } + + if (matchingTypes.Length == 0) + { + throw new InvalidOperationException("No members match the specified search pattern"); + } + + return matchingTypes[0]; } } } diff --git a/project/Aki.SinglePlayer/Aki.SinglePlayer.csproj b/project/Aki.SinglePlayer/Aki.SinglePlayer.csproj index d1c4584..a43eb3f 100644 --- a/project/Aki.SinglePlayer/Aki.SinglePlayer.csproj +++ b/project/Aki.SinglePlayer/Aki.SinglePlayer.csproj @@ -1,25 +1,24 @@  - net472 + net471 aki-singleplayer + Release Aki - Copyright @ Aki 2023 + Copyright @ Aki 2024 - - + + - - ..\Shared\Managed\UnityEngine.AudioModule.dll - + diff --git a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs index f3cdf07..86cdb5b 100644 --- a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs +++ b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs @@ -6,6 +6,7 @@ using Aki.SinglePlayer.Patches.Progression; using Aki.SinglePlayer.Patches.Quests; using Aki.SinglePlayer.Patches.RaidFix; using Aki.SinglePlayer.Patches.ScavMode; +using Aki.SinglePlayer.Patches.TraderServices; using BepInEx; namespace Aki.SinglePlayer @@ -20,7 +21,7 @@ namespace Aki.SinglePlayer try { new OfflineSaveProfilePatch().Enable(); - new OfflineSpawnPointPatch().Enable(); + //new OfflineSpawnPointPatch().Enable(); // Spawns are properly randomised and patch is likely no longer needed new ExperienceGainPatch().Enable(); new ScavExperienceGainPatch().Enable(); new MainMenuControllerPatch().Enable(); @@ -41,6 +42,7 @@ namespace Aki.SinglePlayer new SpawnPmcPatch().Enable(); new PostRaidHealingPricePatch().Enable(); new EndByTimerPatch().Enable(); + new InRaidQuestAvailablePatch().Enable(); new PostRaidHealScreenPatch().Enable(); new VoIPTogglerPatch().Enable(); new MidRaidQuestChangePatch().Enable(); @@ -56,6 +58,17 @@ namespace Aki.SinglePlayer new MapReadyButtonPatch().Enable(); new LabsKeycardRemovalPatch().Enable(); new ScavLateStartPatch().Enable(); + new MidRaidAchievementChangePatch().Enable(); + new GetTraderServicesPatch().Enable(); + new PurchaseTraderServicePatch().Enable(); + new ScavSellAllPriceStorePatch().Enable(); + new ScavSellAllRequestPatch().Enable(); + new HideoutQuestIgnorePatch().Enable(); + new LightKeeperServicesPatch().Enable(); + new ScavEncyclopediaPatch().Enable(); + new ScavRepAdjustmentPatch().Enable(); + new AmmoUsedCounterPatch().Enable(); + new ArmorDamageCounterPatch().Enable(); } catch (Exception ex) { diff --git a/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs b/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs index 32811c1..3dfc122 100644 --- a/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs +++ b/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs @@ -11,8 +11,7 @@ namespace Aki.SinglePlayer.Models.Progression private GameWorld _gameWorld; private Player _player; private float _timer; - private bool _playerFlaggedAsEnemyToBosses; - private List _bridgeMines; + private List _bridgeMines; private RecodableItemClass _transmitter; private readonly List _zryachiyAndFollowers = new List(); private bool _aggressor; @@ -32,6 +31,7 @@ namespace Aki.SinglePlayer.Models.Progression return; } + // Get transmitter from players inventory _transmitter = GetTransmitterFromInventory(); @@ -43,19 +43,20 @@ namespace Aki.SinglePlayer.Models.Progression return; } - GameObject.Find("Attack").SetActive(false); + var places = Singleton.Instance.BotsController.CoversData.AIPlaceInfoHolder.Places; - // Zone was added in a newer version and the gameObject actually has a \ - GameObject.Find("CloseZone\\").SetActive(false); + places.First(x => x.name == "Attack").gameObject.SetActive(false); - // Give access to Lightkeepers door - _gameWorld.BufferZoneController.SetPlayerAccessStatus(_player.ProfileId, true); + // Zone was added in a newer version and the gameObject actually has a \ + places.First(y => y.name == "CloseZone\\").gameObject.SetActive(false); - // Expensive, run after gameworld / lighthouse checks above - _bridgeMines = FindObjectsOfType().ToList(); + // Give access to Lightkeepers door + _gameWorld.BufferZoneController.SetPlayerAccessStatus(_player.ProfileId, true); - // Set mines to be non-active - SetBridgeMinesStatus(false); + _bridgeMines = _gameWorld.MineManager.Mines; + + // Set mines to be non-active + SetBridgeMinesStatus(false); } public void Update() @@ -73,7 +74,7 @@ namespace Aki.SinglePlayer.Models.Progression // Player not an enemy to Zryachiy // Lk door not accessible // Player has no transmitter on thier person - if (_gameWorld == null || _playerFlaggedAsEnemyToBosses || _isDoorDisabled || _transmitter == null) + if (_gameWorld == null || _isDoorDisabled || _transmitter == null) { return; } @@ -122,12 +123,13 @@ namespace Aki.SinglePlayer.Models.Progression /// What state mines should be private void SetBridgeMinesStatus(bool active) { - // Find mines with opposite state of what we want - foreach (var mine in _bridgeMines.Where(mine => mine.gameObject.activeSelf == !active)) + + // Find mines with opposite state of what we want + foreach (var mine in _bridgeMines.Where(mine => mine.gameObject.activeSelf == !active && mine.transform.parent.gameObject.name == "Directional_mines_LHZONE")) { - mine.gameObject.SetActive(active); + mine.gameObject.SetActive(active); } - } + } /// /// Put Zryachiy and followers into a list and sub to their death event diff --git a/project/Aki.SinglePlayer/Models/RaidFix/BundleLoader.cs b/project/Aki.SinglePlayer/Models/RaidFix/BundleLoader.cs index 64ad160..35d1e2d 100644 --- a/project/Aki.SinglePlayer/Models/RaidFix/BundleLoader.cs +++ b/project/Aki.SinglePlayer/Models/RaidFix/BundleLoader.cs @@ -24,7 +24,7 @@ namespace Aki.SinglePlayer.Models.RaidFix var loadTask = Singleton.Instance.LoadBundlesAndCreatePools( PoolManager.PoolsCategory.Raid, PoolManager.AssemblyType.Local, - Profile.GetAllPrefabPaths(false).ToArray(), + Profile.GetAllPrefabPaths(false).Where(x => !x.IsNullOrEmpty()).ToArray(), JobPriority.General, null, default(CancellationToken)); diff --git a/project/Aki.SinglePlayer/Models/ScavMode/SellAllRequest.cs b/project/Aki.SinglePlayer/Models/ScavMode/SellAllRequest.cs new file mode 100644 index 0000000..206c0c3 --- /dev/null +++ b/project/Aki.SinglePlayer/Models/ScavMode/SellAllRequest.cs @@ -0,0 +1,20 @@ +using EFT.InventoryLogic.BackendInventoryInteraction; +using Newtonsoft.Json; + +namespace Aki.SinglePlayer.Models.ScavMode +{ + public class SellAllRequest + { + [JsonProperty("Action")] + public string Action; + + [JsonProperty("totalValue")] + public int TotalValue; + + [JsonProperty("fromOwner")] + public OwnerInfo FromOwner; + + [JsonProperty("toOwner")] + public OwnerInfo ToOwner; + } +} diff --git a/project/Aki.SinglePlayer/Patches/Healing/HealthControllerPatch.cs b/project/Aki.SinglePlayer/Patches/Healing/HealthControllerPatch.cs index 77aec85..537318d 100644 --- a/project/Aki.SinglePlayer/Patches/Healing/HealthControllerPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Healing/HealthControllerPatch.cs @@ -4,24 +4,26 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Healing { /// - /// HealthController used by post-raid heal screen and health listenen class are different, this patch + /// HealthController used by post-raid heal screen and health listener class are different, this patch /// ensures effects (fracture/bleeding) on body parts stay in sync /// public class HealthControllerPatch : ModulePatch { protected override MethodBase GetTargetMethod() { - return typeof(HealthControllerClass).GetMethod("ApplyTreatment", BindingFlags.Public | BindingFlags.Instance); + return AccessTools.Method(typeof(HealthControllerClass), nameof(HealthControllerClass.ApplyTreatment)); } [PatchPrefix] private static void PatchPrefix(object healthObserver) { - var property = healthObserver.GetType().GetProperty("Effects"); + // Accesstools will throw warnings if player doesnt have any Effects while using Treatment at end of raid + var property = AccessTools.Property(healthObserver.GetType(), "Effects"); if (property != null) { var effects = property.GetValue(healthObserver); diff --git a/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs b/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs index 1e801d3..d7b99a0 100644 --- a/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs @@ -1,7 +1,7 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT.HealthSystem; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Healing { @@ -15,13 +15,7 @@ namespace Aki.SinglePlayer.Patches.Healing protected override MethodBase GetTargetMethod() { - var desiredType = typeof(MainMenuController); - var desiredMethod = desiredType.GetMethod("method_1", PatchConstants.PrivateFlags); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(MainMenuController), nameof(MainMenuController.method_1)); } [PatchPostfix] diff --git a/project/Aki.SinglePlayer/Patches/Healing/PlayerPatch.cs b/project/Aki.SinglePlayer/Patches/Healing/PlayerPatch.cs index a5828b4..ca6da82 100644 --- a/project/Aki.SinglePlayer/Patches/Healing/PlayerPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Healing/PlayerPatch.cs @@ -1,8 +1,9 @@ +using System; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; using System.Reflection; using System.Threading.Tasks; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Healing { @@ -10,13 +11,7 @@ namespace Aki.SinglePlayer.Patches.Healing { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(Player); - var desiredMethod = desiredType.GetMethod("Init", PatchConstants.PrivateFlags); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(Player), nameof(Player.Init)); } [PatchPostfix] @@ -24,7 +19,7 @@ namespace Aki.SinglePlayer.Patches.Healing { await __result; - if (profile?.Id.StartsWith("pmc") == true) + if (profile?.Id.Equals(Common.Http.RequestHandler.SessionId, StringComparison.InvariantCultureIgnoreCase) ?? false) { Logger.LogDebug($"Hooking up health listener to profile: {profile.Id}"); var listener = Utils.Healing.HealthListener.Instance; diff --git a/project/Aki.SinglePlayer/Patches/Healing/PostRaidHealScreenPatch.cs b/project/Aki.SinglePlayer/Patches/Healing/PostRaidHealScreenPatch.cs index c43b6a7..996f06c 100644 --- a/project/Aki.SinglePlayer/Patches/Healing/PostRaidHealScreenPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Healing/PostRaidHealScreenPatch.cs @@ -17,10 +17,8 @@ namespace Aki.SinglePlayer.Patches.Healing { // Class1049.smethod_0 as of 18969 //internal static Class1049 smethod_0(GInterface29 backend, string profileId, Profile savageProfile, LocationSettingsClass.GClass1097 location, ExitStatus exitStatus, TimeSpan exitTime, ERaidMode raidMode) - var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "PostRaidHealthScreenClass"); - var desiredMethod = desiredType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic).Single(IsTargetMethod); + var desiredMethod = typeof(PostRaidHealthScreenClass).GetMethods(BindingFlags.Static | BindingFlags.Public).SingleCustom(IsTargetMethod); - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); return desiredMethod; @@ -40,10 +38,9 @@ namespace Aki.SinglePlayer.Patches.Healing } [PatchPrefix] - private static bool PatchPrefix(TarkovApplication __instance, ref ERaidMode raidMode) + private static bool PatchPrefix(ref ERaidMode raidMode) { raidMode = ERaidMode.Online; - return true; // Perform original method } } diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/AmmoUsedCounterPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/AmmoUsedCounterPatch.cs new file mode 100644 index 0000000..8b17ab5 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/MainMenu/AmmoUsedCounterPatch.cs @@ -0,0 +1,26 @@ +using Aki.Reflection.Patching; +using EFT; +using HarmonyLib; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.MainMenu +{ + public class AmmoUsedCounterPatch : ModulePatch + { + private static Player player; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(Player), nameof(Player.OnMakingShot)); + } + + [PatchPostfix] + private static void PatchPostfix(Player __instance) + { + if (__instance.IsYourPlayer) + { + __instance.Profile.EftStats.SessionCounters.AddLong(1L, SessionCounterTypesAbstractClass.AmmoUsed); + } + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/ArmorDamageCounterPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/ArmorDamageCounterPatch.cs new file mode 100644 index 0000000..886f486 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/MainMenu/ArmorDamageCounterPatch.cs @@ -0,0 +1,41 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using EFT.InventoryLogic; +using HarmonyLib; +using System; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.MainMenu +{ + public class ArmorDamageCounterPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(Player), nameof(Player.ApplyDamageInfo)); + } + + [PatchPostfix] + private static void PatchPostfix(DamageInfo damageInfo) + { + if (damageInfo.Player == null || damageInfo.Player.iPlayer == null || !damageInfo.Player.iPlayer.IsYourPlayer) + { + return; + } + + if (damageInfo.Weapon is Weapon) + { + if (!Singleton.Instance.ItemTemplates.TryGetValue(damageInfo.SourceId, out var template)) + { + return; + } + + if (template is AmmoTemplate bulletTemplate) + { + float absorbedDamage = (float)Math.Round(bulletTemplate.Damage - damageInfo.Damage); + damageInfo.Player.iPlayer.Profile.EftStats.SessionCounters.AddFloat(absorbedDamage, SessionCounterTypesAbstractClass.CauseArmorDamage); + } + } + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs index 84bb6dd..7486144 100644 --- a/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs +++ b/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs @@ -1,13 +1,14 @@ using Aki.Reflection.Patching; using EFT; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.MainMenu { /// /// Force ERaidMode to online to make interface show insurance page /// - class InsuranceScreenPatch : ModulePatch + public class InsuranceScreenPatch : ModulePatch { static InsuranceScreenPatch() { @@ -17,7 +18,7 @@ namespace Aki.SinglePlayer.Patches.MainMenu protected override MethodBase GetTargetMethod() { //[CompilerGenerated] - //private void method_67() + //private void method_XX() //{ // if (this.raidSettings_0.SelectedLocation.Id == "laboratory") // { @@ -31,13 +32,7 @@ namespace Aki.SinglePlayer.Patches.MainMenu // this.method_41(); //} - var desiredType = typeof(MainMenuController); - var desiredMethod = desiredType.GetMethod("method_69", BindingFlags.NonPublic | BindingFlags.Instance); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(MainMenuController), nameof(MainMenuController.method_72)); } [PatchPrefix] diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/MapReadyButtonPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/MapReadyButtonPatch.cs index de2094f..102631c 100644 --- a/project/Aki.SinglePlayer/Patches/MainMenu/MapReadyButtonPatch.cs +++ b/project/Aki.SinglePlayer/Patches/MainMenu/MapReadyButtonPatch.cs @@ -1,20 +1,21 @@ -using System.Reflection; +using System.Linq; +using System.Reflection; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT.UI; using EFT.UI.Matchmaker; namespace Aki.SinglePlayer.Patches.MainMenu { /// - /// Removes the 'ready' button from the map preview screen - accessible by choosing map to deply to > clicking 'map' bottom left of screen - /// Clicking the ready button makes a call to client/match/available, something we dont want + /// Removes the 'ready' button from the map preview screen - accessible by choosing map to deploy to > clicking 'map' bottom left of screen + /// Clicking the ready button makes a call to client/match/available, something we don't want that /// public class MapReadyButtonPatch : ModulePatch { protected override MethodBase GetTargetMethod() { - return typeof(MatchmakerMapPointsScreen).GetMethod("Show", PatchConstants.PrivateFlags); + // We don't really care which "Show" method is returned - either will do + return typeof(MatchmakerMapPointsScreen).GetMethods().First(m => m.Name == nameof(MatchmakerMapPointsScreen.Show)); } [PatchPostfix] diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs index 20c81a5..2b38a5a 100644 --- a/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs +++ b/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs @@ -1,10 +1,8 @@ using Aki.Common.Utils; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using BepInEx.Bootstrap; using EFT.Communications; using EFT.UI; -using HarmonyLib; using System.Linq; using System.Reflection; using System.Text; @@ -19,16 +17,12 @@ namespace Aki.SinglePlayer.Patches.MainMenu **/ internal class PluginErrorNotifierPatch : ModulePatch { - private static MethodInfo _displayMessageNotificationMethod; private static bool _messageShown = false; protected override MethodBase GetTargetMethod() { - _displayMessageNotificationMethod = AccessTools.Method(typeof(NotificationManagerClass), "DisplayMessageNotification"); - - var desiredType = typeof(MenuScreen); - var desiredMethod = desiredType.GetMethod("Show", PatchConstants.PrivateFlags); - return desiredMethod; + // We don't really care which "Show" method is returned - either will do + return typeof(MenuScreen).GetMethods().First(m => m.Name == nameof(MenuScreen.Show)); } [PatchPostfix] @@ -45,7 +39,7 @@ namespace Aki.SinglePlayer.Patches.MainMenu // Show a toast in the bottom right of the screen indicating how many plugins failed to load var consoleHeaderMessage = $"{failedPluginCount} plugin{(failedPluginCount > 1 ? "s" : "")} failed to load due to errors"; var toastMessage = $"{consoleHeaderMessage}. Please check the console for details."; - _displayMessageNotificationMethod.Invoke(null, new object[] { toastMessage, ENotificationDurationType.Infinite, ENotificationIconType.Alert, Color.red }); + NotificationManagerClass.DisplayMessageNotification(toastMessage, ENotificationDurationType.Infinite, ENotificationIconType.Alert, Color.red); // Build the error message we'll put in the BepInEx and in-game consoles var stringBuilder = new StringBuilder(); diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/SelectLocationScreenPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/SelectLocationScreenPatch.cs index 6415054..a1fa83b 100644 --- a/project/Aki.SinglePlayer/Patches/MainMenu/SelectLocationScreenPatch.cs +++ b/project/Aki.SinglePlayer/Patches/MainMenu/SelectLocationScreenPatch.cs @@ -1,8 +1,8 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT.UI; using EFT.UI.Matchmaker; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.MainMenu { @@ -13,13 +13,7 @@ namespace Aki.SinglePlayer.Patches.MainMenu { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(MatchMakerSelectionLocationScreen); - var desiredMethod = desiredType.GetMethod("Awake", PatchConstants.PrivateFlags); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(MatchMakerSelectionLocationScreen), nameof(MatchMakerSelectionLocationScreen.Awake)); } [PatchPostfix] diff --git a/project/Aki.SinglePlayer/Patches/Progression/ExperienceGainPatch.cs b/project/Aki.SinglePlayer/Patches/Progression/ExperienceGainPatch.cs index c53ee9f..95deae2 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/ExperienceGainPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/ExperienceGainPatch.cs @@ -1,8 +1,8 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT.UI.SessionEnd; -using System.Linq; using System.Reflection; +using EFT; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Progression { @@ -10,25 +10,9 @@ namespace Aki.SinglePlayer.Patches.Progression { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(SessionResultExperienceCount); - var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).FirstOrDefault(IsTargetMethod); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(SessionResultExperienceCount), nameof(SessionResultExperienceCount.Show), new []{ typeof(Profile), typeof(bool), typeof(ExitStatus) }); } - - private static bool IsTargetMethod(MethodInfo mi) - { - var parameters = mi.GetParameters(); - return (parameters.Length == 3 - && parameters[0].Name == "profile" - && parameters[1].Name == "isOnline" - && parameters[2].Name == "exitStatus" - && parameters[1].ParameterType == typeof(bool)); - } - + [PatchPrefix] private static void PatchPrefix(ref bool isOnline) { diff --git a/project/Aki.SinglePlayer/Patches/Progression/HideoutQuestIgnorePatch.cs b/project/Aki.SinglePlayer/Patches/Progression/HideoutQuestIgnorePatch.cs new file mode 100644 index 0000000..aba740f --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/Progression/HideoutQuestIgnorePatch.cs @@ -0,0 +1,35 @@ +using Aki.Reflection.Patching; +using EFT; +using HarmonyLib; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.Progression +{ + /** + * There is no reason to update quest counters when exiting the hideout, so set the + * player's QuestController to null while calling HideoutPlayer.OnGameSessionEnd to + * avoid the quest controller counters from being triggered + * + * Note: Player.OnGameSessionEnd handles the player's quest controller not being set gracefully + */ + public class HideoutQuestIgnorePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(HideoutPlayer), nameof(HideoutPlayer.OnGameSessionEnd)); + } + + [PatchPrefix] + private static void PatchPrefix(ref AbstractQuestControllerClass __state, ref AbstractQuestControllerClass ____questController) + { + __state = ____questController; + ____questController = null; + } + + [PatchPostfix] + private static void PatchPostfix(AbstractQuestControllerClass __state, ref AbstractQuestControllerClass ____questController) + { + ____questController = __state; + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs b/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs index 157a145..1bf0475 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs @@ -3,6 +3,7 @@ using Aki.SinglePlayer.Models.Progression; using Comfort.Common; using EFT; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Progression { @@ -10,7 +11,7 @@ namespace Aki.SinglePlayer.Patches.Progression { protected override MethodBase GetTargetMethod() { - return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted)); + return AccessTools.Method(typeof(GameWorld), nameof(GameWorld.OnGameStarted)); } [PatchPostfix] diff --git a/project/Aki.SinglePlayer/Patches/Progression/LighthouseTransmitterPatch.cs b/project/Aki.SinglePlayer/Patches/Progression/LighthouseTransmitterPatch.cs index 4160d5f..a7caeb6 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/LighthouseTransmitterPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/LighthouseTransmitterPatch.cs @@ -2,6 +2,7 @@ using Comfort.Common; using EFT; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Progression { @@ -9,7 +10,7 @@ namespace Aki.SinglePlayer.Patches.Progression { protected override MethodBase GetTargetMethod() { - return typeof(RadioTransmitterHandlerClass).GetMethod("method_4", BindingFlags.NonPublic | BindingFlags.Instance); + return AccessTools.Method(typeof(RadioTransmitterHandlerClass), nameof(RadioTransmitterHandlerClass.method_4)); } [PatchPrefix] diff --git a/project/Aki.SinglePlayer/Patches/Progression/MidRaidAchievementChangePatch.cs b/project/Aki.SinglePlayer/Patches/Progression/MidRaidAchievementChangePatch.cs new file mode 100644 index 0000000..27914b3 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/Progression/MidRaidAchievementChangePatch.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using Aki.Reflection.Patching; + +namespace Aki.SinglePlayer.Patches.Progression +{ + /// + /// BSG have disabled notifications for local raids, set updateAchievements in the achievement controller to always be true + /// This enables the achievement notifications and the client to save completed achievement data into profile.Achievements + /// + public class MidRaidAchievementChangePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(AchievementControllerClass).GetConstructors()[0]; + } + + [PatchPrefix] + private static bool PatchPrefix(ref bool updateAchievements) + { + updateAchievements = true; + + return true; // Do original method + } + } +} \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Patches/Progression/MidRaidQuestChangePatch.cs b/project/Aki.SinglePlayer/Patches/Progression/MidRaidQuestChangePatch.cs index 603832a..f4d85ae 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/MidRaidQuestChangePatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/MidRaidQuestChangePatch.cs @@ -8,34 +8,35 @@ using System.Reflection; namespace Aki.SinglePlayer.Patches.Progression { /// - /// After picking up a quest item, trigger CheckForStatusChange() from the questController to fully update a quest subtasks to show (e.g. `survive and extract item from raid` task) + /// After picking up a quest item, trigger CheckForStatusChange() from the questController to fully update a quest sub-tasks to show (e.g. `survive and extract item from raid` task) /// public class MidRaidQuestChangePatch : ModulePatch { protected override MethodBase GetTargetMethod() { - return typeof(Profile).GetMethod("AddToCarriedQuestItems", BindingFlags.Public | BindingFlags.Instance); + return AccessTools.Method(typeof(Profile), nameof(Profile.AddToCarriedQuestItems)); } [PatchPostfix] private static void PatchPostfix() { var gameWorld = Singleton.Instance; - - if (gameWorld != null) + if (gameWorld == null) { - var player = gameWorld.MainPlayer; + Logger.LogDebug("[MidRaidQuestChangePatch] gameWorld instance was null"); - var questController = Traverse.Create(player).Field("_questController").Value; - if (questController != null) + return; + } + + var player = gameWorld.MainPlayer; + var questController = Traverse.Create(player).Field("_questController").Value; + if (questController != null) + { + foreach (var quest in questController.Quests.ToList()) { - foreach (var quest in questController.Quests.ToList()) - { - quest.CheckForStatusChange(true, true); - } + quest.CheckForStatusChange(true, true); } } - } } } \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs b/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs index ea93885..0409f2a 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs @@ -14,7 +14,7 @@ using System.Reflection; namespace Aki.SinglePlayer.Patches.Progression { /// - /// After a raid, the client doesnt update the server on what occurred during the raid. To persist loot/quest changes etc we + /// After a raid, the client doesn't update the server on what occurred during the raid. To persist loot/quest changes etc we /// make the client send the active profile to a spt-specific endpoint `/raid/profile/save` where we can update the players profile json /// public class OfflineSaveProfilePatch : ModulePatch @@ -35,8 +35,8 @@ namespace Aki.SinglePlayer.Patches.Progression { // method_45 - as of 16432 // method_43 - as of 18876 - var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "TarkovApplication"); - var desiredMethod = Array.Find(desiredType.GetMethods(PatchConstants.PrivateFlags), IsTargetMethod); + var desiredType = typeof(TarkovApplication); + var desiredMethod = Array.Find(desiredType.GetMethods(PatchConstants.PublicDeclaredFlags), IsTargetMethod); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); @@ -66,7 +66,7 @@ namespace Aki.SinglePlayer.Patches.Progression { Exit = result.Value0.ToString().ToLowerInvariant(), // Exit player used to leave raid Profile = profile, // players scav or pmc profile, depending on type of raid they did - Health = Utils.Healing.HealthListener.Instance.CurrentHealth, // Speicifc limb/effect damage data the player has at end of raid + Health = Utils.Healing.HealthListener.Instance.CurrentHealth, // Specific limb/effect damage data the player has at end of raid Insurance = Utils.Insurance.InsuredItemManager.Instance.GetTrackedItems(), // A copy of items insured by player with durability values as of raid end (if item is returned, send it back with correct durability values) IsPlayerScav = ____raidSettings.IsScav }; diff --git a/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs b/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs index 32d75e4..6c9c7d3 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs @@ -18,9 +18,9 @@ namespace Aki.SinglePlayer.Patches.Progression protected override MethodBase GetTargetMethod() { - var desiredType = PatchConstants.EftTypes.First(IsTargetType); + var desiredType = typeof(SpawnSystemClass); var desiredMethod = desiredType - .GetMethods(PatchConstants.PrivateFlags) + .GetMethods(PatchConstants.PublicDeclaredFlags) .First(m => m.Name.Contains("SelectSpawnPoint")); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); @@ -34,7 +34,7 @@ namespace Aki.SinglePlayer.Patches.Progression // GClass1812 as of 17349 // GClass1886 as of 18876 // Remapped to SpawnSystemClass - return (type.GetMethods(PatchConstants.PrivateFlags).Any(x => x.Name.IndexOf("CheckFarthestFromOtherPlayers", StringComparison.OrdinalIgnoreCase) != -1) + return (type.GetMethods(PatchConstants.PublicDeclaredFlags).Any(x => x.Name.IndexOf("CheckFarthestFromOtherPlayers", StringComparison.OrdinalIgnoreCase) != -1) && type.IsClass); } @@ -48,10 +48,10 @@ namespace Aki.SinglePlayer.Patches.Progression IPlayer person, string infiltration) { - var spawnPointsField = (ISpawnPoints)__instance.GetType().GetFields(PatchConstants.PrivateFlags).SingleOrDefault(f => f.FieldType == typeof(ISpawnPoints))?.GetValue(__instance); + var spawnPointsField = (ISpawnPoints)__instance.GetType().GetFields(PatchConstants.PublicDeclaredFlags).SingleOrDefault(f => f.FieldType == typeof(ISpawnPoints))?.GetValue(__instance); if (spawnPointsField == null) { - throw new Exception($"OfflineSpawnPointPatch: Failed to locate field of {nameof(ISpawnPoints)} on class instance ({__instance.GetType().Name})"); + throw new Exception($"OfflineSpawnPointPatch: Failed to locate field: {nameof(ISpawnPoints)} on class instance: {__instance.GetType().Name}"); } var mapSpawnPoints = spawnPointsField.ToList(); @@ -72,7 +72,7 @@ namespace Aki.SinglePlayer.Patches.Progression Logger.LogInfo($"Desired spawnpoint: [category:{category}] [side:{side}] [infil:{infiltration}] [{mapSpawnPoints.Count} total spawn points]"); Logger.LogInfo($"Selected SpawnPoint: [id:{__result.Id}] [name:{__result.Name}] [category:{__result.Categories}] [side:{__result.Sides}] [infil:{__result.Infiltration}]"); - return false; + return false; // skip original method } private static List FilterByPlayerSide(List mapSpawnPoints, ESpawnCategory category, EPlayerSide side) diff --git a/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs b/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs index 8c1f2ff..5d57b48 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs @@ -1,10 +1,10 @@ +using System; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; using EFT.Counters; using EFT.UI.SessionEnd; -using System.Linq; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Progression { @@ -19,15 +19,13 @@ namespace Aki.SinglePlayer.Patches.Progression /// protected override MethodBase GetTargetMethod() { - var desiredType = typeof(SessionResultExitStatus); - var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).FirstOrDefault(IsTargetMethod); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method( + typeof(SessionResultExitStatus), + nameof(SessionResultExitStatus.Show), + new []{ typeof(Profile), typeof(PlayerVisualRepresentation), typeof(ESideType), typeof(ExitStatus), typeof(TimeSpan), typeof(ISession), typeof(bool) }); } + // Unused, but left here in case patch breaks and finding the intended method is difficult private static bool IsTargetMethod(MethodInfo mi) { var parameters = mi.GetParameters(); diff --git a/project/Aki.SinglePlayer/Patches/Quests/DogtagPatch.cs b/project/Aki.SinglePlayer/Patches/Quests/DogtagPatch.cs index 4b8cf49..db19a70 100644 --- a/project/Aki.SinglePlayer/Patches/Quests/DogtagPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Quests/DogtagPatch.cs @@ -3,26 +3,21 @@ using EFT; using EFT.InventoryLogic; using System; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Quests { public class DogtagPatch : ModulePatch { - private static BindingFlags _flags; - private static PropertyInfo _getEquipmentProperty; - static DogtagPatch() { _ = nameof(EquipmentClass.GetSlot); _ = nameof(DamageInfo.Weapon); - - _flags = BindingFlags.Instance | BindingFlags.NonPublic; - _getEquipmentProperty = typeof(Player).GetProperty("Equipment", _flags); } protected override MethodBase GetTargetMethod() { - return typeof(Player).GetMethod("OnBeenKilledByAggressor", _flags); + return AccessTools.Method(typeof(Player), nameof(Player.OnBeenKilledByAggressor)); } /// @@ -65,7 +60,7 @@ namespace Aki.SinglePlayer.Patches.Quests private static Item GetDogTagItemFromPlayerWhoDied(Player __instance) { - var equipment = (EquipmentClass)_getEquipmentProperty.GetValue(__instance); + var equipment = __instance.Equipment; if (equipment == null) { Logger.LogError("DogtagPatch error > Player has no equipment"); diff --git a/project/Aki.SinglePlayer/Patches/Quests/EndByTimerPatch.cs b/project/Aki.SinglePlayer/Patches/Quests/EndByTimerPatch.cs index 10586bc..1c1184d 100644 --- a/project/Aki.SinglePlayer/Patches/Quests/EndByTimerPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Quests/EndByTimerPatch.cs @@ -1,9 +1,8 @@ using Aki.Common.Http; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; -using System.Linq; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.Quests { @@ -15,15 +14,10 @@ namespace Aki.SinglePlayer.Patches.Quests { protected override MethodBase GetTargetMethod() { - var desiredType = PatchConstants.LocalGameType.BaseType; - var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).SingleOrDefault(IsStopRaidMethod); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(BaseLocalGame), nameof(BaseLocalGame.Stop)); } + // Unused, but left here in case patch breaks and finding the intended method is difficult private static bool IsStopRaidMethod(MethodInfo mi) { var parameters = mi.GetParameters(); @@ -39,13 +33,13 @@ namespace Aki.SinglePlayer.Patches.Quests } [PatchPrefix] - private static bool PrefixPatch(object __instance, ref ExitStatus exitStatus, ref string exitName) + private static bool PrefixPatch(ref ExitStatus exitStatus, ref string exitName) { var isParsed = bool.TryParse(RequestHandler.GetJson("/singleplayer/settings/raid/endstate"), out bool MIAOnRaidEnd); if (isParsed) { // No extract name and successful, its a MIA - if (MIAOnRaidEnd == true && string.IsNullOrEmpty(exitName?.Trim()) && exitStatus == ExitStatus.Survived) + if (MIAOnRaidEnd && string.IsNullOrEmpty(exitName?.Trim()) && exitStatus == ExitStatus.Survived) { exitStatus = ExitStatus.MissingInAction; exitName = null; diff --git a/project/Aki.SinglePlayer/Patches/Quests/InRaidQuestAvailablePatch.cs b/project/Aki.SinglePlayer/Patches/Quests/InRaidQuestAvailablePatch.cs new file mode 100644 index 0000000..1a7c67e --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/Quests/InRaidQuestAvailablePatch.cs @@ -0,0 +1,58 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using EFT.Quests; +using HarmonyLib; +using System; +using System.Linq; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.Quests +{ + /** + * Lightkeeper quests change their state in-raid, and will change to the `AppearStatus` of the quest once + * the AvailableAfter time has been hit. This defaults to `Locked`, but should actually be `AvailableForStart` + * + * So if we get a quest state change from `AvailableAfter` to `Locked`, we should actually change to `AvailableForStart` + */ + public class InRaidQuestAvailablePatch : ModulePatch + { + private static PropertyInfo _questStatusProperty; + + protected override MethodBase GetTargetMethod() + { + var targetType = PatchConstants.EftTypes.FirstOrDefault(IsTargetType); + var targetMethod = AccessTools.Method(targetType, "SetStatus"); + + _questStatusProperty = AccessTools.Property(targetType, "QuestStatus"); + + Logger.LogDebug($"{this.GetType().Name} Type: {targetType?.Name}"); + Logger.LogDebug($"{this.GetType().Name} Method: {targetMethod?.Name}"); + Logger.LogDebug($"{this.GetType().Name} QuestStatus: {_questStatusProperty?.Name}"); + + return targetMethod; + } + + private bool IsTargetType(Type type) + { + if (type.GetProperty("StatusTransition") != null && + type.GetProperty("IsChangeAllowed") != null && + type.GetProperty("NeedCountdown") == null) + { + return true; + } + + return false; + } + + [PatchPrefix] + private static void PatchPrefix(object __instance, ref EQuestStatus status) + { + var currentStatus = (EQuestStatus)_questStatusProperty.GetValue(__instance); + + if (currentStatus == EQuestStatus.AvailableAfter && status == EQuestStatus.Locked) + { + status = EQuestStatus.AvailableForStart; + } + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs b/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs index f425067..459e227 100644 --- a/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs @@ -11,8 +11,8 @@ namespace Aki.SinglePlayer.Patches.Quests { protected override MethodBase GetTargetMethod() { - var desiredType = PatchConstants.EftTypes.Single(IsTargetType); - var desiredMethod = desiredType.GetMethod("method_1", PatchConstants.PrivateFlags); + var desiredType = PatchConstants.EftTypes.SingleCustom(IsTargetType); + var desiredMethod = desiredType.GetMethod("method_1", PatchConstants.PublicDeclaredFlags); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); @@ -22,7 +22,7 @@ namespace Aki.SinglePlayer.Patches.Quests private static bool IsTargetType(Type type) { - if (!typeof(IGetProfileData).IsAssignableFrom(type) || type.GetMethod("method_1", PatchConstants.PrivateFlags) == null) + if (!typeof(IGetProfileData).IsAssignableFrom(type) || type.GetMethod("method_1", PatchConstants.PublicDeclaredFlags) == null) { return false; } @@ -32,7 +32,7 @@ namespace Aki.SinglePlayer.Patches.Quests } [PatchPrefix] - private static bool PatchPrefix(ref bool __result, object __instance, WildSpawnType ___wildSpawnType_0, BotDifficulty ___botDifficulty_0, Profile x) + private static bool PatchPrefix(ref bool __result, WildSpawnType ___wildSpawnType_0, BotDifficulty ___botDifficulty_0, Profile x) { if (x == null) { diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/BotTemplateLimitPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/BotTemplateLimitPatch.cs index 4768120..b09a0f4 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/BotTemplateLimitPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/BotTemplateLimitPatch.cs @@ -1,9 +1,9 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using Aki.Common.Http; using System; using System.Collections.Generic; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -17,13 +17,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix protected override MethodBase GetTargetMethod() { - var desiredType = typeof(BotsPresets); - var desiredMethod = desiredType.GetMethod("method_1", PatchConstants.PrivateFlags); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(BotsPresets), nameof(BotsPresets.method_1)); } [PatchPostfix] diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/EmptyInfilFixPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/EmptyInfilFixPatch.cs index 3a1de3f..7b1db4b 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/EmptyInfilFixPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/EmptyInfilFixPatch.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Aki.Reflection.Patching; @@ -7,9 +6,7 @@ using Aki.Reflection.Utils; using Comfort.Common; using EFT; using EFT.Game.Spawning; -using EFT.UI; using UnityEngine; -using Object = UnityEngine.Object; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -23,8 +20,8 @@ namespace Aki.SinglePlayer.Patches.RaidFix { var desiredType = PatchConstants.LocalGameType.BaseType; var desiredMethod = desiredType - .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.CreateInstance) - .Single(IsTargetMethod); + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.CreateInstance) + .SingleCustom(IsTargetMethod); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs index 5b798d9..c330e25 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs @@ -7,13 +7,12 @@ using Aki.Reflection.Utils; using Aki.SinglePlayer.Models.RaidFix; using System; using System.Collections.Generic; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.RaidFix { public class GetNewBotTemplatesPatch : ModulePatch { - private static MethodInfo _getNewProfileMethod; - static GetNewBotTemplatesPatch() { _ = nameof(IGetProfileData.PrepareToLoadBackend); @@ -22,29 +21,16 @@ namespace Aki.SinglePlayer.Patches.RaidFix _ = nameof(JobPriority.General); } - /// - /// BotsPresets.GetNewProfile() - /// - public GetNewBotTemplatesPatch() - { - var desiredType = typeof(BotsPresets); - _getNewProfileMethod = desiredType - .GetMethod(nameof(BotsPresets.GetNewProfile), BindingFlags.Instance | BindingFlags.NonPublic); // want the func with 2 params (protected + inherited from base) - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {_getNewProfileMethod?.Name}"); - } - /// /// Looking for CreateProfile() /// /// protected override MethodBase GetTargetMethod() { - return typeof(BotsPresets).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - .Single(x => IsTargetMethod(x)); + return AccessTools.DeclaredMethod(typeof(BotsPresets), nameof(BotsPresets.CreateProfile)); } + // Unused, but left here in case patch breaks and finding the intended method is difficult private bool IsTargetMethod(MethodInfo mi) { var parameters = mi.GetParameters(); @@ -58,7 +44,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix /// BotsPresets.GetNewProfile() /// [PatchPrefix] - private static bool PatchPrefix(ref Task __result, BotsPresets __instance, List ___list_0, GClass513 data, ref bool withDelete) + private static bool PatchPrefix(ref Task __result, BotsPresets __instance, List ___list_0, GClass591 data, ref bool withDelete) { /* When client wants new bot and GetNewProfile() return null (if not more available templates or they don't satisfy by Role and Difficulty condition) @@ -72,7 +58,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix try { // Force true to ensure bot profile is deleted after use - _getNewProfileMethod.Invoke(__instance, new object[] { data, true }); + __instance.GetNewProfile(data, true); } catch (Exception e) { diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs index 2e1c8c0..823295c 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs @@ -3,6 +3,7 @@ using EFT; using System.Reflection; using Aki.SinglePlayer.Utils.Insurance; using Comfort.Common; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -10,7 +11,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix { protected override MethodBase GetTargetMethod() { - return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted)); + return AccessTools.Method(typeof(GameWorld), nameof(GameWorld.OnGameStarted)); } [PatchPostfix] diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/LabsKeycardRemovalPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/LabsKeycardRemovalPatch.cs index 18bfb4e..2b31a97 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/LabsKeycardRemovalPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/LabsKeycardRemovalPatch.cs @@ -16,7 +16,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix protected override MethodBase GetTargetMethod() { - return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted)); + return AccessTools.Method(typeof(GameWorld), nameof(GameWorld.OnGameStarted)); } [PatchPostfix] @@ -43,7 +43,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix } var inventoryController = Traverse.Create(player).Field("_inventoryController").Value; - GClass2585.Remove(accessCardItem, inventoryController, false, true); + InteractionsHandlerClass.Remove(accessCardItem, inventoryController, false, true); } } } \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/MaxBotPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/MaxBotPatch.cs index 18b728b..3709371 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/MaxBotPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/MaxBotPatch.cs @@ -1,7 +1,6 @@ using Aki.Common.Http; using Aki.Reflection.Patching; using Aki.Reflection.Utils; -using System.Linq; using System.Reflection; namespace Aki.SinglePlayer.Patches.RaidFix @@ -9,13 +8,13 @@ namespace Aki.SinglePlayer.Patches.RaidFix /// /// Alter the max bot cap with value stored in server, if value is -1, use existing value /// - class MaxBotPatch : ModulePatch + public class MaxBotPatch : ModulePatch { protected override MethodBase GetTargetMethod() { const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; const string methodName = "SetSettings"; - var desiredType = PatchConstants.EftTypes.Single(x => x.GetMethod(methodName, flags) != null && IsTargetMethod(x.GetMethod(methodName, flags))); + var desiredType = PatchConstants.EftTypes.SingleCustom(x => x.GetMethod(methodName, flags) != null && IsTargetMethod(x.GetMethod(methodName, flags))); var desiredMethod = desiredType.GetMethod(methodName, flags); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/PlayerToggleSoundFixPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/PlayerToggleSoundFixPatch.cs index 26e8b09..d2f0749 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/PlayerToggleSoundFixPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/PlayerToggleSoundFixPatch.cs @@ -2,6 +2,7 @@ using System.Reflection; using Aki.Reflection.Patching; using Comfort.Common; using EFT; +using HarmonyLib; using UnityEngine; namespace Aki.SinglePlayer.Patches.RaidFix @@ -13,7 +14,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix { protected override MethodBase GetTargetMethod() { - return typeof(Player).GetMethod("PlayToggleSound", BindingFlags.Instance | BindingFlags.NonPublic); + return AccessTools.Method(typeof(Player), nameof(Player.PlayToggleSound)); } [PatchPrefix] diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs index 53fc6dc..b900f6a 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs @@ -2,7 +2,7 @@ using HarmonyLib; using System; using System.Reflection; -using TraderInfo = EFT.Profile.GClass1625; +using EFT; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -10,17 +10,11 @@ namespace Aki.SinglePlayer.Patches.RaidFix { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(TraderInfo); - var desiredMethod = desiredType.GetMethod("UpdateLevel", BindingFlags.NonPublic | BindingFlags.Instance); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(Profile.TraderInfo), nameof(Profile.TraderInfo.UpdateLevel)); } [PatchPrefix] - protected static void PatchPrefix(TraderInfo __instance) + protected static void PatchPrefix(Profile.TraderInfo __instance) { if (__instance.Settings == null) { @@ -36,7 +30,8 @@ namespace Aki.SinglePlayer.Patches.RaidFix } // Backing field of the "CurrentLoyalty" property - Traverse.Create(__instance).Field("traderLoyaltyLevel_0").SetValue(loyaltyLevelSettings.Value); + // Traverse.Create(__instance).Field("k__BackingField").SetValue(loyaltyLevelSettings.Value); + __instance.CurrentLoyalty = loyaltyLevelSettings.Value; } } } \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs index 57396a8..1e4cb90 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs @@ -1,48 +1,25 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; -using EFT; -using System; -using System.Linq; using System.Reflection; +using Aki.Reflection.Utils; namespace Aki.SinglePlayer.Patches.RaidFix { public class RemoveUsedBotProfilePatch : ModulePatch { - private static readonly BindingFlags _flags; - private static readonly Type _targetInterface; - private static readonly Type _targetType; - private static readonly FieldInfo _profilesField; - static RemoveUsedBotProfilePatch() { _ = nameof(IGetProfileData.ChooseProfile); - - _flags = BindingFlags.Instance | BindingFlags.NonPublic; - _targetInterface = PatchConstants.EftTypes.Single(IsTargetInterface); - _targetType = typeof(BotsPresets); - _profilesField = _targetType.GetField("list_0", _flags); } protected override MethodBase GetTargetMethod() { - return _targetType.GetMethod("GetNewProfile", _flags); + return typeof(BotsPresets).BaseType.GetMethods().SingleCustom(m => m.Name == nameof(BotsPresets.GetNewProfile) && m.IsVirtual); } - - private static bool IsTargetInterface(Type type) - { - return type.IsInterface && type.GetProperty("StartProfilesLoaded") != null && type.GetMethod("CreateProfile") != null; - } - - private static bool IsTargetType(Type type) - { - return _targetInterface.IsAssignableFrom(type) && _targetInterface.IsAssignableFrom(type.BaseType); - } - + /// /// BotsPresets.GetNewProfile() [PatchPrefix] - private static bool PatchPrefix(ref Profile __result, object __instance, GClass513 data, ref bool withDelete) + private static bool PatchPrefix(ref bool withDelete) { withDelete = true; diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/SmokeGrenadeFuseSoundFixPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/SmokeGrenadeFuseSoundFixPatch.cs index ad95d89..edc5b01 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/SmokeGrenadeFuseSoundFixPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/SmokeGrenadeFuseSoundFixPatch.cs @@ -16,7 +16,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix { protected override MethodBase GetTargetMethod() { - return typeof(GrenadeEmission).GetMethod(nameof(GrenadeEmission.StartEmission)); + return AccessTools.Method(typeof(GrenadeEmission), nameof(GrenadeEmission.StartEmission)); } [PatchTranspiler] diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs index 33d5b32..0ede67e 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs @@ -3,6 +3,7 @@ using Aki.Reflection.Utils; using EFT; using System; using System.Reflection; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -15,23 +16,16 @@ namespace Aki.SinglePlayer.Patches.RaidFix /// int_3 = spawn process? - current guess is open spawn positions - bsg doesnt seem to handle negative vaues well /// int_4 = max bots /// - class SpawnProcessNegativeValuePatch : ModulePatch + public class SpawnProcessNegativeValuePatch : ModulePatch { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(BotSpawner); - var desiredMethod = desiredType.GetMethod("CheckOnMax", PatchConstants.PublicFlags); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(BotSpawner), nameof(BotSpawner.CheckOnMax)); } [PatchPrefix] private static bool PatchPreFix(int wantSpawn, ref int toDelay, ref int toSpawn, ref int ____maxBots, int ____allBotsCount, int ____inSpawnProcess) { - // Set bots to delay if alive bots + spawning bots count > maxbots // ____inSpawnProcess can be negative, don't go below 0 when calculating if ((____allBotsCount + Math.Max(____inSpawnProcess, 0)) > ____maxBots) diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs index 240dd2b..d2f54ff 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs @@ -4,6 +4,7 @@ using System.Reflection; using Aki.Reflection.Patching; using System.Collections; using EFT.HealthSystem; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -11,15 +12,16 @@ namespace Aki.SinglePlayer.Patches.RaidFix { protected override MethodBase GetTargetMethod() { - return typeof(BetterAudio).GetMethod("StartTinnitusEffect", BindingFlags.Instance | BindingFlags.Public); + return AccessTools.Method(typeof(BetterAudio), nameof(BetterAudio.StartTinnitusEffect)); } // checks on invoke whether the player is stunned before allowing tinnitus [PatchPrefix] static bool PatchPrefix() { - bool shouldInvoke = typeof(ActiveHealthController) - .GetMethod("FindActiveEffect", BindingFlags.Instance | BindingFlags.Public) + var baseMethod = AccessTools.Method(typeof(ActiveHealthController), nameof(ActiveHealthController.FindActiveEffect)); + + bool shouldInvoke = baseMethod .MakeGenericMethod(typeof(ActiveHealthController) .GetNestedType("Stun", BindingFlags.Instance | BindingFlags.NonPublic)) .Invoke(Singleton.Instance.MainPlayer.ActiveHealthController, new object[] { EBodyPart.Common }) != null; diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/VoIPTogglerPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/VoIPTogglerPatch.cs index 0fbf443..aa16439 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/VoIPTogglerPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/VoIPTogglerPatch.cs @@ -1,7 +1,7 @@ using System.Reflection; using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -9,7 +9,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix { protected override MethodBase GetTargetMethod() { - return typeof(ForceMuteVoIPToggler).GetMethod("Awake", PatchConstants.PrivateFlags); + return AccessTools.Method(typeof(ForceMuteVoIPToggler), nameof(ForceMuteVoIPToggler.Awake)); } [PatchPrefix] diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ExfilPointManagerPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ExfilPointManagerPatch.cs index 4dc694c..caad4ed 100644 --- a/project/Aki.SinglePlayer/Patches/ScavMode/ExfilPointManagerPatch.cs +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ExfilPointManagerPatch.cs @@ -2,6 +2,9 @@ using Aki.Reflection.Patching; using Comfort.Common; using EFT; using System.Reflection; +using HarmonyLib; +using EFT.Interactive; +using System.Linq; namespace Aki.SinglePlayer.Patches.ScavMode { @@ -12,13 +15,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(GameWorld); - var desiredMethod = desiredType.GetMethod("OnGameStarted", BindingFlags.Public | BindingFlags.Instance); - - Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); - Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); - - return desiredMethod; + return AccessTools.Method(typeof(GameWorld), nameof(GameWorld.OnGameStarted)); } [PatchPostfix] @@ -26,23 +23,34 @@ namespace Aki.SinglePlayer.Patches.ScavMode { var gameWorld = Singleton.Instance; - // checks nothing is null otherwise woopsies happen. + // checks nothing is null otherwise bad things happen if (gameWorld == null || gameWorld.RegisteredPlayers == null || gameWorld.ExfiltrationController == null) { - Logger.LogError("Unable to Find Gameworld or RegisterPlayers... Unable to Disable Extracts for Scav raid"); + Logger.LogError("Could not find GameWorld or RegisterPlayers... Unable to disable extracts for Scav raid"); } Player player = gameWorld.MainPlayer; - var exfilController = gameWorld.ExfiltrationController; - - // Only disable PMC extracts if current player is a scav. + // Only disable PMC extracts if current player is a scav if (player.Fraction == ETagStatus.Scav && player.Location != "hideout") { - // these are PMC extracts only, scav extracts are under a different field called ScavExfiltrationPoints. - foreach (var exfil in exfilController.ExfiltrationPoints) + foreach (var exfil in gameWorld.ExfiltrationController.ExfiltrationPoints) { - exfil.Disable(); + if (exfil is ScavExfiltrationPoint scavExfil) + { + // We are checking if player exists in list so we dont disable the wrong extract + if(!scavExfil.EligibleIds.Contains(player.ProfileId)) + { + exfil.Disable(); + } + } + else + { + // Disabling extracts that aren't scav extracts + exfil.Disable(); + // _authorityToChangeStatusExternally Changing this to false stop buttons from re-enabling extracts (d-2 extract, zb-013) + exfil.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).First(x => x.Name == "_authorityToChangeStatusExternally").SetValue(exfil, false); + } } } } diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs index 7730bc1..28ff7ea 100644 --- a/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs +++ b/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs @@ -29,14 +29,12 @@ namespace Aki.SinglePlayer.Patches.ScavMode _ = nameof(TimeAndWeatherSettings.IsRandomWeather); _ = nameof(BotControllerSettings.IsScavWars); _ = nameof(WavesSettings.IsBosses); - _ = GClass2953.MAX_SCAV_COUNT; // UPDATE REFS TO THIS CLASS BELOW !!! - - var menuControllerType = typeof(MainMenuController); + _ = MatchmakerPlayerControllerClass.MAX_SCAV_COUNT; // UPDATE REFS TO THIS CLASS BELOW !!! // `MatchmakerInsuranceScreen` OnShowNextScreen - _onReadyScreenMethod = menuControllerType.GetMethod("method_42", PatchConstants.PrivateFlags); + _onReadyScreenMethod = AccessTools.Method(typeof(MainMenuController), nameof(MainMenuController.method_43)); - _isLocalField = menuControllerType.GetField("bool_0", PatchConstants.PrivateFlags); + _isLocalField = AccessTools.Field(typeof(MainMenuController), "bool_0"); _menuControllerField = typeof(TarkovApplication).GetFields(PatchConstants.PrivateFlags).FirstOrDefault(x => x.FieldType == typeof(MainMenuController)); if (_menuControllerField == null) @@ -48,7 +46,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode protected override MethodBase GetTargetMethod() { // `MatchMakerSelectionLocationScreen` OnShowNextScreen - return typeof(MainMenuController).GetMethod("method_66", PatchConstants.PrivateFlags); + return AccessTools.Method(typeof(MainMenuController), nameof(MainMenuController.method_69)); } [PatchTranspiler] @@ -118,14 +116,19 @@ namespace Aki.SinglePlayer.Patches.ScavMode // Get fields from MainMenuController.cs var raidSettings = Traverse.Create(menuController).Field("raidSettings_0").GetValue(); - var matchmakerPlayersController = Traverse.Create(menuController).Field($"{nameof(GClass2953).ToLowerInvariant()}_0").GetValue(); - var gclass = new MatchmakerOfflineRaidScreen.GClass2942(profile?.Info, ref raidSettings, matchmakerPlayersController); + // Find the private field of type `MatchmakerPlayerControllerClass` + var matchmakerPlayersController = menuController.GetType() + .GetFields(AccessTools.all) + .Single(field => field.FieldType == typeof(MatchmakerPlayerControllerClass)) + ?.GetValue(menuController) as MatchmakerPlayerControllerClass; + + var gclass = new MatchmakerOfflineRaidScreen.GClass3155(profile?.Info, ref raidSettings, matchmakerPlayersController); gclass.OnShowNextScreen += LoadOfflineRaidNextScreen; // `MatchmakerOfflineRaidScreen` OnShowReadyScreen - gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, "method_70"); + gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, nameof(MainMenuController.method_73)); gclass.ShowScreen(EScreenState.Queued); } diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavEncyclopediaPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavEncyclopediaPatch.cs new file mode 100644 index 0000000..e0694ca --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavEncyclopediaPatch.cs @@ -0,0 +1,47 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using Aki.SinglePlayer.Utils.InRaid; +using EFT; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.ScavMode +{ + /** + * At the start of a scav raid, copy the PMC encyclopedia to the scav profile, and + * make sure the scav knows all of the items it has in its inventory + */ + internal class ScavEncyclopediaPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted)); + } + + [PatchPostfix] + public static void PatchPostFix() + { + if (RaidChangesUtil.IsScavRaid) + { + var scavProfile = PatchConstants.BackEndSession.ProfileOfPet; + var pmcProfile = PatchConstants.BackEndSession.Profile; + + // Handle old profiles where the scav doesn't have an encyclopedia + if (scavProfile.Encyclopedia == null) + { + scavProfile.Encyclopedia = new Dictionary(); + } + + // Sync the PMC encyclopedia to the scav profile + foreach (var item in pmcProfile.Encyclopedia.Where(item => !scavProfile.Encyclopedia.ContainsKey(item.Key))) + { + scavProfile.Encyclopedia.Add(item.Key, item.Value); + } + + // Auto examine any items the scav doesn't know that are in their inventory + scavProfile.LearnAll(); + } + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavExfilPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavExfilPatch.cs index ec2f498..58a64d6 100644 --- a/project/Aki.SinglePlayer/Patches/ScavMode/ScavExfilPatch.cs +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavExfilPatch.cs @@ -3,6 +3,7 @@ using Aki.Reflection.Patching; using Comfort.Common; using EFT; using EFT.Interactive; +using HarmonyLib; namespace Aki.SinglePlayer.Patches.ScavMode { @@ -10,7 +11,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode { protected override MethodBase GetTargetMethod() { - return typeof(ExfiltrationControllerClass).GetMethod("EligiblePoints", new []{ typeof(Profile) }); + return AccessTools.Method(typeof(ExfiltrationControllerClass), nameof(ExfiltrationControllerClass.EligiblePoints), new[] { typeof(Profile) }); } [PatchPrefix] @@ -29,7 +30,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode } // Running this prepares all the data for getting scav exfil points - __instance.ScavExfiltrationClaim(Singleton.Instance.MainPlayer.Position, profile.Id, profile.FenceInfo.AvailableExitsCount); + __instance.ScavExfiltrationClaim(((IPlayer)Singleton.Instance.MainPlayer).Position, profile.Id, profile.FenceInfo.AvailableExitsCount); // Get the required mask value and retrieve a list of exfil points, setting it as the result var mask = __instance.GetScavExfiltrationMask(profile.Id); diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavLateStartPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavLateStartPatch.cs index 9092270..bfefce1 100644 --- a/project/Aki.SinglePlayer/Patches/ScavMode/ScavLateStartPatch.cs +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavLateStartPatch.cs @@ -25,8 +25,8 @@ namespace Aki.SinglePlayer.Patches.ScavMode protected override MethodBase GetTargetMethod() { - var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "TarkovApplication"); - var desiredMethod = Array.Find(desiredType.GetMethods(PatchConstants.PrivateFlags), IsTargetMethod); + var desiredType = typeof(TarkovApplication); + var desiredMethod = Array.Find(desiredType.GetMethods(PatchConstants.PublicDeclaredFlags), IsTargetMethod); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); @@ -95,11 +95,10 @@ namespace Aki.SinglePlayer.Patches.ScavMode foreach (var exitChange in exitChangesToApply) { // Find the client exit we want to make changes to - var exitToChange = location.exits.First(x => x.Name == exitChange.Name); + var exitToChange = location.exits.FirstOrDefault(x => x.Name == exitChange.Name); if (exitToChange == null) { Logger.LogDebug($"Exit with Id: {exitChange.Name} not found, skipping"); - continue; } @@ -111,6 +110,10 @@ namespace Aki.SinglePlayer.Patches.ScavMode if (exitChange.MinTime.HasValue) { exitToChange.MinTime = exitChange.MinTime.Value; + } + + if (exitChange.MaxTime.HasValue) + { exitToChange.MaxTime = exitChange.MaxTime.Value; } } @@ -131,19 +134,9 @@ namespace Aki.SinglePlayer.Patches.ScavMode } // Reset values to those from cache - if (clientLocationExit.Chance != cachedExit.Chance) - { - clientLocationExit.Chance = cachedExit.Chance; - } - if (clientLocationExit.MinTime != cachedExit.MinTime) - { - clientLocationExit.MinTime = cachedExit.MinTime; - } - - if (clientLocationExit.MaxTime != cachedExit.MaxTime) - { - clientLocationExit.MaxTime = cachedExit.MaxTime; - } + clientLocationExit.Chance = cachedExit.Chance; + clientLocationExit.MinTime = cachedExit.MinTime; + clientLocationExit.MaxTime = cachedExit.MaxTime; } } diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavPrefabLoadPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavPrefabLoadPatch.cs index 564fd90..6a586d8 100644 --- a/project/Aki.SinglePlayer/Patches/ScavMode/ScavPrefabLoadPatch.cs +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavPrefabLoadPatch.cs @@ -15,13 +15,13 @@ namespace Aki.SinglePlayer.Patches.ScavMode protected override MethodBase GetTargetMethod() { var desiredType = typeof(TarkovApplication) - .GetNestedTypes(PatchConstants.PrivateFlags) - .Single(x => x.GetField("timeAndWeather") != null - && x.GetField("tarkovApplication_0") != null - && x.GetField("timeHasComeScreenController") == null - && x.Name.Contains("Struct")); + .GetNestedTypes(PatchConstants.PublicDeclaredFlags) + .SingleCustom(x => x.GetField("timeAndWeather") != null + && x.GetField("tarkovApplication_0") != null + && x.GetField("timeHasComeScreenController") == null + && x.Name.Contains("Struct")); - var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags) + var desiredMethod = desiredType.GetMethods(PatchConstants.PublicDeclaredFlags) .FirstOrDefault(x => x.Name == "MoveNext"); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); @@ -36,7 +36,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode var codes = new List(instructions); // Search for code where backend.Session.getProfile() is called. - var searchCode = new CodeInstruction(OpCodes.Callvirt, AccessTools.Method(PatchConstants.BackendSessionInterfaceType, "get_Profile")); + var searchCode = new CodeInstruction(OpCodes.Callvirt, AccessTools.Method(PatchConstants.BackendProfileInterfaceType, "get_Profile")); var searchIndex = -1; for (var i = 0; i < codes.Count; i++) @@ -69,9 +69,9 @@ namespace Aki.SinglePlayer.Patches.ScavMode new Code(OpCodes.Ldfld, typeof(TarkovApplication), "_raidSettings"), new Code(OpCodes.Callvirt, typeof(RaidSettings), "get_IsPmc"), new Code(OpCodes.Brfalse, brFalseLabel), - new Code(OpCodes.Callvirt, PatchConstants.BackendSessionInterfaceType, "get_Profile"), + new Code(OpCodes.Callvirt, PatchConstants.BackendProfileInterfaceType, "get_Profile"), new Code(OpCodes.Br, brLabel), - new CodeWithLabel(OpCodes.Callvirt, brFalseLabel, PatchConstants.SessionInterfaceType, "get_ProfileOfPet"), + new CodeWithLabel(OpCodes.Callvirt, brFalseLabel, PatchConstants.BackendProfileInterfaceType, "get_ProfileOfPet"), new CodeWithLabel(OpCodes.Ldc_I4_1, brLabel) }); diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavProfileLoadPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavProfileLoadPatch.cs index 2395d84..5e7556e 100644 --- a/project/Aki.SinglePlayer/Patches/ScavMode/ScavProfileLoadPatch.cs +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavProfileLoadPatch.cs @@ -17,12 +17,12 @@ namespace Aki.SinglePlayer.Patches.ScavMode { // Struct225 - 20575 var desiredType = typeof(TarkovApplication) - .GetNestedTypes(PatchConstants.PrivateFlags) - .Single(x => x.GetField("timeAndWeather") != null + .GetNestedTypes(PatchConstants.PublicDeclaredFlags) + .SingleCustom(x => x.GetField("timeAndWeather") != null && x.GetField("timeHasComeScreenController") != null && x.Name.Contains("Struct")); - var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags) + var desiredMethod = desiredType.GetMethods(PatchConstants.PublicDeclaredFlags) .FirstOrDefault(x => x.Name == "MoveNext"); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); @@ -37,7 +37,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode var codes = new List(instructions); // Search for code where backend.Session.getProfile() is called. - var searchCode = new CodeInstruction(OpCodes.Callvirt, AccessTools.Method(PatchConstants.BackendSessionInterfaceType, "get_Profile")); + var searchCode = new CodeInstruction(OpCodes.Callvirt, AccessTools.Method(PatchConstants.BackendProfileInterfaceType, "get_Profile")); var searchIndex = -1; for (var i = 0; i < codes.Count; i++) @@ -69,10 +69,10 @@ namespace Aki.SinglePlayer.Patches.ScavMode new Code(OpCodes.Ldfld, typeof(TarkovApplication), "_raidSettings"), new Code(OpCodes.Callvirt, typeof(RaidSettings), "get_IsPmc"), new Code(OpCodes.Brfalse, brFalseLabel), - new Code(OpCodes.Callvirt, PatchConstants.BackendSessionInterfaceType, "get_Profile"), + new Code(OpCodes.Callvirt, PatchConstants.BackendProfileInterfaceType, "get_Profile"), new Code(OpCodes.Br, brLabel), - new CodeWithLabel(OpCodes.Callvirt, brFalseLabel, PatchConstants.SessionInterfaceType, "get_ProfileOfPet"), - new CodeWithLabel(OpCodes.Stfld, brLabel, typeof(TarkovApplication).GetNestedTypes(BindingFlags.NonPublic).Single(IsTargetNestedType), "profile") + new CodeWithLabel(OpCodes.Callvirt, brFalseLabel, PatchConstants.BackendProfileInterfaceType, "get_ProfileOfPet"), + new CodeWithLabel(OpCodes.Stfld, brLabel, typeof(TarkovApplication).GetNestedTypes(BindingFlags.Public).SingleCustom(IsTargetNestedType), "profile") }); codes.RemoveRange(searchIndex, 4); @@ -83,7 +83,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode private static bool IsTargetNestedType(System.Type nestedType) { - return nestedType.GetMethods(PatchConstants.PrivateFlags) + return nestedType.GetMethods(PatchConstants.PublicDeclaredFlags) .Count(x => x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == typeof(IResult)) > 0 && nestedType.GetField("savageProfile") != null; } } diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavRepAdjustmentPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavRepAdjustmentPatch.cs new file mode 100644 index 0000000..2543b21 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavRepAdjustmentPatch.cs @@ -0,0 +1,50 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT; +using HarmonyLib; +using System; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.ScavMode +{ + public class ScavRepAdjustmentPatch : ModulePatch + { + // TODO: REMAP/UPDATE GCLASS REF + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GClass1790), nameof(GClass1790.OnEnemyKill)); + } + + [PatchPrefix] + private static void PatchPrefix(string playerProfileId, out Tuple __state) + { + var player = Singleton.Instance.MainPlayer; + __state = new Tuple(null, false); + + if (player.Profile.Side != EPlayerSide.Savage) + { + return; + } + + if (Singleton.Instance.GetEverExistedPlayerByID(playerProfileId) is Player killedPlayer) + { + __state = new Tuple(killedPlayer, killedPlayer.AIData.IsAI); + // Extra check to ensure we only set playerscavs to IsAI = false + if (killedPlayer.Profile.Info.Settings.Role == WildSpawnType.assault && killedPlayer.Profile.Nickname.Contains("(")) + { + killedPlayer.AIData.IsAI = false; + } + + player.Loyalty.method_1(killedPlayer); + } + } + [PatchPostfix] + private static void PatchPostfix(Tuple __state) + { + if(__state.Item1 != null) + { + __state.Item1.AIData.IsAI = __state.Item2; + } + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavSellAllPriceStorePatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavSellAllPriceStorePatch.cs new file mode 100644 index 0000000..9dae79c --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavSellAllPriceStorePatch.cs @@ -0,0 +1,67 @@ +using Aki.Reflection.Patching; +using EFT.InventoryLogic; +using EFT.UI; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.ScavMode +{ + /** + * After fetching the list of items for the post-raid scav inventory screen, calculate + * the total "Sell All" value, and store it for retrieval if the user hits "Sell All" + */ + public class ScavSellAllPriceStorePatch : ModulePatch + { + private static string FENCE_ID = "579dc571d53a0658a154fbec"; + private static string ROUBLE_TID = "5449016a4bdc2d6f028b456f"; + + private static FieldInfo _sessionField; + + public static int StoredPrice; + + protected override MethodBase GetTargetMethod() + { + Type scavInventoryScreenType = typeof(ScavengerInventoryScreen); + _sessionField = AccessTools.GetDeclaredFields(scavInventoryScreenType).FirstOrDefault(f => f.FieldType == typeof(ISession)); + + return AccessTools.FirstMethod(scavInventoryScreenType, IsTargetMethod); + } + + private bool IsTargetMethod(MethodBase method) + { + // Look for a method with one parameter named `items` + // method_3(out IEnumerable items) + if (method.GetParameters().Length == 1 && method.GetParameters()[0].Name == "items") + { + return true; + } + + return false; + } + + [PatchPostfix] + private static void PatchPostfix(ScavengerInventoryScreen __instance, IEnumerable items) + { + ISession session = _sessionField.GetValue(__instance) as ISession; + TraderClass traderClass = session.Traders.FirstOrDefault(x => x.Id == FENCE_ID); + + int totalPrice = 0; + foreach (Item item in items) + { + if (item.TemplateId == ROUBLE_TID) + { + totalPrice += item.StackObjectsCount; + } + else + { + totalPrice += traderClass.GetItemPriceOnScavSell(item, true); + } + } + + StoredPrice = totalPrice; + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/ScavSellAllRequestPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/ScavSellAllRequestPatch.cs new file mode 100644 index 0000000..59dc409 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/ScavMode/ScavSellAllRequestPatch.cs @@ -0,0 +1,81 @@ +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Models.ScavMode; +using Comfort.Common; +using EFT.InventoryLogic.BackendInventoryInteraction; +using EFT.InventoryLogic; +using HarmonyLib; +using System.Reflection; +using System.Threading.Tasks; +using System; +using Aki.Reflection.Utils; + +namespace Aki.SinglePlayer.Patches.ScavMode +{ + /** + * When the user clicks "Sell All" after a scav raid, create a custom request object + * that includes the calculated sell price + */ + public class ScavSellAllRequestPatch : ModulePatch + { + private static MethodInfo _sendOperationMethod; + private string TargetMethodName = "SellAllFromSavage"; + + protected override MethodBase GetTargetMethod() + { + // We want to find a type that contains `SellAllFromSavage` but doesn't extend from `IBackendStatus` + Type targetType = PatchConstants.EftTypes.SingleCustom(IsTargetType); + + Logger.LogDebug($"{this.GetType().Name} Type: {targetType?.Name}"); + + // So we can call "SendOperationRightNow" without directly referencing a GClass + _sendOperationMethod = AccessTools.Method(targetType, "SendOperationRightNow"); + + return AccessTools.Method(targetType, TargetMethodName); + } + + private bool IsTargetType(Type type) + { + // Isn't an interface, isn't part of the dummy class, and contains our target method + if (!type.IsInterface + && type.DeclaringType != typeof(BackendDummyClass) + && type.GetMethod(TargetMethodName) != null) + { + return true; + } + + return false; + } + + [PatchPrefix] + private static bool PatchPrefix(object __instance, ref Task __result, string playerId, string petId) + { + // Build request with additional information + OwnerInfo fromOwner = new OwnerInfo + { + Id = petId, + Type = EOwnerType.Profile + }; + OwnerInfo toOwner = new OwnerInfo + { + Id = playerId, + Type = EOwnerType.Profile + }; + + SellAllRequest request = new SellAllRequest + { + Action = "SellAllFromSavage", + TotalValue = ScavSellAllPriceStorePatch.StoredPrice, // Retrieve value stored in earlier patch + FromOwner = fromOwner, // Scav + ToOwner = toOwner // PMC + }; + + // We'll re-use the same logic/methods that the base code used + TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + _sendOperationMethod.Invoke(__instance, new object[] { request, new Callback(taskCompletionSource.SetResult) }); + __result = taskCompletionSource.Task; + + // Skip original + return false; + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/TraderServices/GetTraderServicesPatch.cs b/project/Aki.SinglePlayer/Patches/TraderServices/GetTraderServicesPatch.cs new file mode 100644 index 0000000..ed8c6db --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/TraderServices/GetTraderServicesPatch.cs @@ -0,0 +1,25 @@ +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using HarmonyLib; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.TraderServices +{ + public class GetTraderServicesPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InventoryControllerClass), nameof(InventoryControllerClass.GetTraderServicesDataFromServer)); + } + + [PatchPrefix] + public static bool PatchPrefix(string traderId) + { + Logger.LogInfo($"Loading {traderId} services from servers"); + TraderServicesManager.Instance.GetTraderServicesDataFromServer(traderId); + + // Skip original + return false; + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/TraderServices/LightKeeperServicesPatch.cs b/project/Aki.SinglePlayer/Patches/TraderServices/LightKeeperServicesPatch.cs new file mode 100644 index 0000000..71a370c --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/TraderServices/LightKeeperServicesPatch.cs @@ -0,0 +1,27 @@ +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using Comfort.Common; +using EFT; +using System.Reflection; + +namespace Aki.SinglePlayer.Patches.TraderServices +{ + public class LightKeeperServicesPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted)); + } + + [PatchPostfix] + public static void PatchPostFix() + { + var gameWorld = Singleton.Instance; + + if (gameWorld != null) + { + gameWorld.gameObject.AddComponent(); + } + } + } +} diff --git a/project/Aki.SinglePlayer/Patches/TraderServices/PurchaseTraderServicePatch.cs b/project/Aki.SinglePlayer/Patches/TraderServices/PurchaseTraderServicePatch.cs new file mode 100644 index 0000000..8ad20c7 --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/TraderServices/PurchaseTraderServicePatch.cs @@ -0,0 +1,36 @@ +using Aki.Reflection.Patching; +using Aki.SinglePlayer.Utils.TraderServices; +using EFT; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Aki.SinglePlayer.Patches.TraderServices +{ + public class PurchaseTraderServicePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InventoryControllerClass), nameof(InventoryControllerClass.TryPurchaseTraderService)); + } + + [PatchPostfix] + public static async void PatchPostFix(Task __result, ETraderServiceType serviceType, AbstractQuestControllerClass questController, string subServiceId) + { + bool purchased = await __result; + if (purchased) + { + Logger.LogInfo($"Player purchased service {serviceType}"); + TraderServicesManager.Instance.AfterPurchaseTraderService(serviceType, questController, subServiceId); + } + else + { + Logger.LogInfo($"Player failed to purchase service {serviceType}"); + } + } + } +} diff --git a/project/Aki.SinglePlayer/Utils/Insurance/InsuredItemManager.cs b/project/Aki.SinglePlayer/Utils/Insurance/InsuredItemManager.cs index ad1980d..b173a64 100644 --- a/project/Aki.SinglePlayer/Utils/Insurance/InsuredItemManager.cs +++ b/project/Aki.SinglePlayer/Utils/Insurance/InsuredItemManager.cs @@ -33,7 +33,7 @@ namespace Aki.SinglePlayer.Utils.Insurance public List GetTrackedItems() { var itemsToSend = new List(); - if (_items == null || _items.Count() == 0) + if (_items == null || !_items.Any()) { return itemsToSend; } diff --git a/project/Aki.SinglePlayer/Utils/TraderServices/LightKeeperServicesManager.cs b/project/Aki.SinglePlayer/Utils/TraderServices/LightKeeperServicesManager.cs new file mode 100644 index 0000000..b13f584 --- /dev/null +++ b/project/Aki.SinglePlayer/Utils/TraderServices/LightKeeperServicesManager.cs @@ -0,0 +1,64 @@ +using BepInEx.Logging; +using Comfort.Common; +using EFT; +using HarmonyLib.Tools; +using UnityEngine; + +namespace Aki.SinglePlayer.Utils.TraderServices +{ + internal class LightKeeperServicesManager : MonoBehaviour + { + private static ManualLogSource logger; + GameWorld gameWorld; + BotsController botsController; + + private void Awake() + { + logger = BepInEx.Logging.Logger.CreateLogSource(nameof(LightKeeperServicesManager)); + + gameWorld = Singleton.Instance; + if (gameWorld == null || TraderServicesManager.Instance == null) + { + logger.LogError("[AKI-LKS] GameWorld or TraderServices null"); + Destroy(this); + return; + } + + botsController = Singleton.Instance.BotsController; + if (botsController == null) + { + logger.LogError("[AKI-LKS] BotsController null"); + Destroy(this); + return; + } + + TraderServicesManager.Instance.OnTraderServicePurchased += OnTraderServicePurchased; + } + + private void OnTraderServicePurchased(ETraderServiceType serviceType, string subserviceId) + { + switch (serviceType) + { + case ETraderServiceType.ExUsecLoyalty: + botsController.BotTradersServices.LighthouseKeeperServices.OnFriendlyExUsecPurchased(gameWorld.MainPlayer); + break; + case ETraderServiceType.ZryachiyAid: + botsController.BotTradersServices.LighthouseKeeperServices.OnFriendlyZryachiyPurchased(gameWorld.MainPlayer); + break; + } + } + + private void OnDestroy() + { + if (gameWorld == null || botsController == null) + { + return; + } + + if (TraderServicesManager.Instance != null) + { + TraderServicesManager.Instance.OnTraderServicePurchased -= OnTraderServicePurchased; + } + } + } +} diff --git a/project/Aki.SinglePlayer/Utils/TraderServices/TraderServiceModel.cs b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServiceModel.cs new file mode 100644 index 0000000..65d5115 --- /dev/null +++ b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServiceModel.cs @@ -0,0 +1,36 @@ +using EFT; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Aki.SinglePlayer.Utils.TraderServices +{ + public class TraderServiceModel + { + [JsonProperty("serviceType")] + public ETraderServiceType ServiceType { get; set; } + + [JsonProperty("itemsToPay")] + public Dictionary ItemsToPay { get; set; } + + [JsonProperty("subServices")] + public Dictionary SubServices { get; set; } + + [JsonProperty("itemsToReceive")] + public MongoID[] ItemsToReceive { get; set; } + + [JsonProperty("requirements")] + public TraderServiceRequirementsModel Requirements { get; set; } + } + + public class TraderServiceRequirementsModel + { + [JsonProperty("completedQuests")] + public string[] CompletedQuests { get; set; } + + [JsonProperty("standings")] + public Dictionary Standings { get; set; } + + [JsonProperty("side")] + public ESideType Side { get; set; } + } +} \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs new file mode 100644 index 0000000..b1758e2 --- /dev/null +++ b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs @@ -0,0 +1,239 @@ +using Aki.Common.Http; +using Comfort.Common; +using EFT; +using EFT.Quests; +using HarmonyLib; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +using static BackendConfigSettingsClass; +using TraderServiceClass = GClass1794; +using QuestDictClass = GClass2133; +using StandingListClass = GClass2135; + +namespace Aki.SinglePlayer.Utils.TraderServices +{ + public class TraderServicesManager + { + /// + /// Subscribe to this event to trigger trader service logic. + /// + public event Action OnTraderServicePurchased; + + private static TraderServicesManager _instance; + + public static TraderServicesManager Instance + { + get + { + if (_instance == null) + { + _instance = new TraderServicesManager(); + } + + return _instance; + } + } + + private Dictionary> _servicePurchased { get; set; } + private HashSet _cachedTraders = new HashSet(); + private FieldInfo _playerQuestControllerField; + + public TraderServicesManager() + { + _servicePurchased = new Dictionary>(); + _playerQuestControllerField = AccessTools.Field(typeof(Player), "_questController"); + } + + public void Clear() + { + _servicePurchased.Clear(); + _cachedTraders.Clear(); + } + + public void GetTraderServicesDataFromServer(string traderId) + { + Dictionary servicesData = Singleton.Instance.ServicesData; + var gameWorld = Singleton.Instance; + var player = gameWorld?.MainPlayer; + + if (gameWorld == null || player == null) + { + Debug.LogError("GetTraderServicesDataFromServer - Error fetching game objects"); + return; + } + + if (!player.Profile.TradersInfo.TryGetValue(traderId, out Profile.TraderInfo traderInfo)) + { + Debug.LogError("GetTraderServicesDataFromServer - Error fetching profile trader info"); + return; + } + + // Only request data from the server if it's not already cached + if (!_cachedTraders.Contains(traderId)) + { + var json = RequestHandler.GetJson($"/singleplayer/traderServices/getTraderServices/{traderId}"); + var traderServiceModels = JsonConvert.DeserializeObject>(json); + + foreach (var traderServiceModel in traderServiceModels) + { + ETraderServiceType serviceType = traderServiceModel.ServiceType; + ServiceData serviceData; + + // Only populate trader services that don't exist yet + if (!servicesData.ContainsKey(traderServiceModel.ServiceType)) + { + TraderServiceClass traderService = new TraderServiceClass + { + TraderId = traderId, + ServiceType = serviceType, + UniqueItems = traderServiceModel.ItemsToReceive ?? new MongoID[0], + ItemsToPay = traderServiceModel.ItemsToPay ?? new Dictionary(), + + // SubServices seem to be populated dynamically in the client (For BTR taxi atleast), so we can just ignore it + // NOTE: For future reference, this is a dict of `point id` to `price` for the BTR taxi + SubServices = new Dictionary() + }; + + // Convert our format to the backend settings format + serviceData = new ServiceData(traderService); + + // Populate requirements if provided + if (traderServiceModel.Requirements != null) + { + if (traderServiceModel.Requirements.Standings != null) + { + serviceData.TraderServiceRequirements.Standings = new StandingListClass(); + serviceData.TraderServiceRequirements.Standings.AddRange(traderServiceModel.Requirements.Standings); + + // BSG has a bug in their code, we _need_ to initialize this if Standings isn't null + serviceData.TraderServiceRequirements.CompletedQuests = new QuestDictClass(); + } + + if (traderServiceModel.Requirements.CompletedQuests != null) + { + serviceData.TraderServiceRequirements.CompletedQuests = new QuestDictClass(); + serviceData.TraderServiceRequirements.CompletedQuests.Concat(traderServiceModel.Requirements.CompletedQuests); + } + } + + servicesData[serviceData.ServiceType] = serviceData; + } + } + + _cachedTraders.Add(traderId); + } + + // Update service availability + foreach (var servicesDataPair in servicesData) + { + // Only update this trader's services + if (servicesDataPair.Value.TraderId != traderId) + { + continue; + } + + var IsServiceAvailable = this.IsServiceAvailable(player, servicesDataPair.Value.TraderServiceRequirements); + + // Check whether we've purchased this service yet + var traderService = servicesDataPair.Key; + var WasPurchasedInThisRaid = IsServicePurchased(traderService, traderId); + traderInfo.SetServiceAvailability(traderService, IsServiceAvailable, WasPurchasedInThisRaid); + } + } + + private bool IsServiceAvailable(Player player, ServiceRequirements requirements) + { + // Handle standing requirements + if (requirements.Standings != null) + { + foreach (var entry in requirements.Standings) + { + if (!player.Profile.TradersInfo.ContainsKey(entry.Key) || + player.Profile.TradersInfo[entry.Key].Standing < entry.Value) + { + return false; + } + } + } + + // Handle quest requirements + if (requirements.CompletedQuests != null) + { + AbstractQuestControllerClass questController = _playerQuestControllerField.GetValue(player) as AbstractQuestControllerClass; + foreach (string questId in requirements.CompletedQuests) + { + var conditional = questController.Quests.GetConditional(questId); + if (conditional == null || conditional.QuestStatus != EQuestStatus.Success) + { + return false; + } + } + } + + return true; + } + + public void AfterPurchaseTraderService(ETraderServiceType serviceType, AbstractQuestControllerClass questController, string subServiceId = null) + { + GameWorld gameWorld = Singleton.Instance; + Player player = gameWorld?.MainPlayer; + + if (gameWorld == null || player == null) + { + Debug.LogError("TryPurchaseTraderService - Error fetching game objects"); + return; + } + + // Service doesn't exist + if (!Singleton.Instance.ServicesData.TryGetValue(serviceType, out var serviceData)) + { + return; + } + + SetServicePurchased(serviceType, subServiceId, serviceData.TraderId); + } + + public void SetServicePurchased(ETraderServiceType serviceType, string subserviceId, string traderId) + { + if (_servicePurchased.TryGetValue(serviceType, out var traderDict)) + { + traderDict[traderId] = true; + } + else + { + _servicePurchased[serviceType] = new Dictionary(); + _servicePurchased[serviceType][traderId] = true; + } + + if (OnTraderServicePurchased != null) + { + OnTraderServicePurchased.Invoke(serviceType, subserviceId); + } + } + + public void RemovePurchasedService(ETraderServiceType serviceType, string traderId) + { + if (_servicePurchased.TryGetValue(serviceType, out var traderDict)) + { + traderDict[traderId] = false; + } + } + + public bool IsServicePurchased(ETraderServiceType serviceType, string traderId) + { + if (_servicePurchased.TryGetValue(serviceType, out var traderDict)) + { + if (traderDict.TryGetValue(traderId, out var result)) + { + return result; + } + } + + return false; + } + } +} diff --git a/project/Modules.code-workspace b/project/Modules.code-workspace index c2b54f9..de30cc3 100644 --- a/project/Modules.code-workspace +++ b/project/Modules.code-workspace @@ -18,7 +18,7 @@ { "label": "build", "type": "shell", - "command": "dotnet cake", + "command": "dotnet build --configuration Release", "group": { "kind": "build", "isDefault": true diff --git a/project/Modules.sln b/project/Modules.sln index dfd1b52..257495d 100644 --- a/project/Modules.sln +++ b/project/Modules.sln @@ -21,7 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aki.PrePatch", "Aki.PrePatc EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution diff --git a/project/Shared/Hollowed/hollowed.dll b/project/Shared/Hollowed/hollowed.dll index 2e413d8..ae4d932 100644 Binary files a/project/Shared/Hollowed/hollowed.dll and b/project/Shared/Hollowed/hollowed.dll differ diff --git a/project/build.cake b/project/build.cake deleted file mode 100644 index 09b4e3f..0000000 --- a/project/build.cake +++ /dev/null @@ -1,107 +0,0 @@ -string target = Argument("target", "ExecuteBuild"); -bool VSBuilt = Argument("vsbuilt", false); - -#addin nuget:?package=Cake.FileHelpers&version=5.0.0 - -// Cake API Reference: https://cakebuild.net/dsl/ -// setup variables -var buildDir = "./Build"; -var delPaths = GetDirectories("./**/*(obj|bin)"); -var licenseFile = "../LICENSE.md"; -var managedFolder = string.Format("{0}/{1}/{2}", buildDir, "EscapeFromTarkov_Data", "Managed"); -var bepInExPluginsFolder = string.Format("{0}/{1}/{2}", buildDir, "BepInEx", "plugins"); -var bepInExPluginsSptFolder = string.Format("{0}/{1}", bepInExPluginsFolder, "spt"); -var bepInExPatchersFolder = string.Format("{0}/{1}/{2}", buildDir, "BepInEx", "patchers"); -var solutionPath = "./Modules.sln"; - -Setup(context => -{ - //building from VS will lock the files and fail to clean the project directories. Post-Build event on Aki.Build sets this switch to true to avoid this. - FileWriteText("./vslock", "lock"); -}); - -Teardown(context => -{ - if(FileExists("./vslock")) - { - DeleteFile("./vslock"); //remove vslock file - } -}); - -// Clean build directory and remove obj / bin folder from projects -Task("Clean") - .WithCriteria(!VSBuilt) - .Does(() => - { - CleanDirectory(buildDir); - }) - .DoesForEach(delPaths, (directoryPath) => - { - DeleteDirectory(directoryPath, new DeleteDirectorySettings - { - Recursive = true, - Force = true - }); - }); - -// Build solution -Task("Build") - .IsDependentOn("Clean") - .WithCriteria(!FileExists("./vslock")) // check for lock file if running from VS - .Does(() => - { - DotNetBuild(solutionPath, new DotNetBuildSettings - { - Configuration = "Release" - }); - }); - -// Copy modules, managed dlls, and license to the build folder -Task("CopyBuildData") - .IsDependentOn("Build") - .Does(() => - { - CleanDirectory(buildDir); - CreateDirectory(managedFolder); - CreateDirectory(bepInExPluginsFolder); - CreateDirectory(bepInExPluginsSptFolder); - CreateDirectory(bepInExPatchersFolder); - CopyFile(licenseFile, string.Format("{0}/LICENSE-Modules.txt", buildDir)); - }) - .DoesForEach(GetFiles("./Aki.*/bin/Release/net472/*.dll"), (dllPath) => //copy modules - { - if(dllPath.GetFilename().ToString().StartsWith("aki_")) - { - //Incase you want to see what is being copied for debuging - //Spectre.Console.AnsiConsole.WriteLine(string.Format("Adding Module: {0}", dllPath.GetFilename())); - - string patcherTransferPath = string.Format("{0}/{1}", bepInExPatchersFolder, dllPath.GetFilename()); - - CopyFile(dllPath, patcherTransferPath); - } - if(dllPath.GetFilename().ToString().StartsWith("aki-")) - { - //Incase you want to see what is being copied for debuging - //Spectre.Console.AnsiConsole.WriteLine(string.Format("Adding Module: {0}", dllPath.GetFilename())); - - string moduleTransferPath = string.Format("{0}/{1}", bepInExPluginsSptFolder, dllPath.GetFilename()); - - CopyFile(dllPath, moduleTransferPath); - } - else if (dllPath.GetFilename().ToString().StartsWith("Aki.")) // Only copy the custom-built dll's to Managed - { - //Incase you want to see what is being copied for debuging - //Spectre.Console.AnsiConsole.WriteLine(string.Format("Adding managed dll: {0}", dllPath.GetFilename())); - - string fileTransferPath = string.Format("{0}/{1}", managedFolder, dllPath.GetFilename()); - - CopyFile(dllPath, fileTransferPath); - } - }); - -// Runs all build tasks based on dependency and configuration -Task("ExecuteBuild") - .IsDependentOn("CopyBuildData"); - -// Runs target task -RunTarget(target); \ No newline at end of file diff --git a/project/build.ps1 b/project/build.ps1 new file mode 100644 index 0000000..e741fd3 --- /dev/null +++ b/project/build.ps1 @@ -0,0 +1,31 @@ +$buildFolder = "..\Build" +$bepinexFolder = "..\Build\BepInEx" +$bepinexPatchFolder = "..\Build\BepInEx\patchers" +$bepinexPluginFolder = "..\Build\BepInEx\plugins" +$bepinexSptFolder = "..\Build\BepInEx\plugins\spt" +$eftDataFolder = "..\Build\EscapeFromTarkov_Data" +$managedFolder = "..\Build\EscapeFromTarkov_Data\Managed" +$projReleaseFolder = ".\bin\Release\net471" +$licenseFile = "..\..\LICENSE.md" + +# Delete build folder and contents to make sure it's clean +if (Test-Path "$buildFolder") { Remove-Item -Path "$buildFolder" -Recurse -Force } + +# Create build folder and subfolders if they don't exist +$foldersToCreate = @("$buildFolder", "$bepinexFolder", "$bepinexPatchFolder", "$bepinexPluginFolder", "$bepinexSptFolder", "$eftDataFolder", "$managedFolder") +foreach ($folder in $foldersToCreate) { + if (-not (Test-Path "$folder")) { New-Item -Path "$folder" -ItemType Directory } +} + +# Move DLLs from project's bin-release folder to the build folder +Copy-Item "$projReleaseFolder\Aki.Common.dll" -Destination "$managedFolder" +Copy-Item "$projReleaseFolder\Aki.Reflection.dll" -Destination "$managedFolder" +Copy-Item "$projReleaseFolder\aki_PrePatch.dll" -Destination "$bepinexPatchFolder" +Copy-Item "$projReleaseFolder\aki-core.dll" -Destination "$bepinexSptFolder" +Copy-Item "$projReleaseFolder\aki-custom.dll" -Destination "$bepinexSptFolder" +Copy-Item "$projReleaseFolder\aki-debugging.dll" -Destination "$bepinexSptFolder" +Copy-Item "$projReleaseFolder\aki-singleplayer.dll" -Destination "$bepinexSptFolder" +# If any new DLLs need to be copied, add here + +# Write the contents of the license file to a txt +Get-Content "$licenseFile" | Out-File "$buildFolder\LICENSE-Modules.txt" -Encoding UTF8