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