0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 09:50:43 -05:00

Merge tag '3.8.0'

# Conflicts:
#	project/Aki.Core/AkiCorePlugin.cs
#	project/Aki.Core/Utils/ValidationUtil.cs
#	project/Aki.Custom/Aki.Custom.csproj
#	project/Aki.Custom/Patches/CustomAiPatch.cs
#	project/Aki.Debugging/AkiDebuggingPlugin.cs
#	project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs
#	project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs
#	project/Aki.SinglePlayer/Patches/RaidFix/LabsKeycardRemovalPatch.cs
#	project/Aki.SinglePlayer/Patches/ScavMode/ScavLateStartPatch.cs
This commit is contained in:
Refringe 2024-04-05 00:14:52 -04:00
commit 688ff6f334
Signed by: Refringe
GPG Key ID: 7715B85B4A6306ED
172 changed files with 4918 additions and 1120 deletions

View File

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

View File

@ -22,26 +22,31 @@ git config --local user.email "USERNAME@SOMETHING.com"
``` ```
## Requirements ## Requirements
- Escape From Tarkov 29197
- Escape From Tarkov 26535 - Visual Studio Code -OR- Visual Studio 2022
- BepInEx 5.4.21
- Visual Studio Code
- .NET 6 SDK - .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 (VS Code)
## Build (vscode)
1. File > Open Workspace > Modules.code-workspace 1. File > Open Workspace > Modules.code-workspace
2. Terminal > Run Build Task... 2. Terminal > Run Build Task...
3. Copy contents of `/Build` into SPT game folder and overwrite 3. Copy contents of `/Build` into SPT game folder and overwrite
## Build (VS) ## Build (VS 2022)
1. Open solution 1. Open solution
2. Restore nuget packages 2. Restore nuget packages
3. Run `dotnet new tool-manifest` 3. Build solution
4. Sometimes you need to run `dotnet tool restore` 4. Copy contents of `/Build` into SPT game folder and overwrite
5. Run `dotnet tool install Cake.Tool`
6. Build solution ## Game Setup
7. Copy contents of `/Build` into SPT game folder and overwrite 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`

View File

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

View File

@ -1,12 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net472</TargetFramework> <TargetFramework>net471</TargetFramework>
<Configuration>Release</Configuration>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Company>SPT Aki</Company> <Company>SPT Aki</Company>
<Copyright>Copyright @ SPT Aki 2023</Copyright> <Copyright>Copyright @ SPT Aki 2024</Copyright>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -19,11 +20,14 @@
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" /> <ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Core\Aki.Core.csproj" /> <ProjectReference Include="..\Aki.Core\Aki.Core.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" /> <ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
<ProjectReference Include="..\Aki.PrePatch\Aki.PrePatch.csproj" />
<ProjectReference Include="..\Aki.Debugging\Aki.Debugging.csproj" />
<ProjectReference Include="..\Aki.SinglePlayer\Aki.SinglePlayer.csproj" /> <ProjectReference Include="..\Aki.SinglePlayer\Aki.SinglePlayer.csproj" />
<ProjectReference Include="..\Aki.Custom\Aki.Custom.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet cake &quot;../build.cake&quot; --vsbuilt=true" /> <Exec Command="pwsh -NoProfile -ExecutionPolicy Bypass ../build.ps1" WorkingDirectory="$(ProjectDir)" />
</Target> </Target>
</Project> </Project>

View File

@ -2,15 +2,17 @@
<PropertyGroup> <PropertyGroup>
<Version>1.0.0.0</Version> <Version>1.0.0.0</Version>
<TargetFramework>net472</TargetFramework> <TargetFramework>net471</TargetFramework>
<Configuration>Release</Configuration>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Company>Aki</Company> <Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright> <Copyright>Copyright @ Aki 2024</Copyright>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System.Net.http" />
<Reference Include="bsg.componentace.compression.libs.zlib" HintPath="..\Shared\Managed\bsg.componentace.compression.libs.zlib.dll" Private="False" /> <Reference Include="bsg.componentace.compression.libs.zlib" HintPath="..\Shared\Managed\bsg.componentace.compression.libs.zlib.dll" Private="False" />
</ItemGroup> </ItemGroup>

View File

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

View File

@ -1,3 +1,5 @@
#region DEPRECATED, REMOVE IN 3.8.1
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -6,12 +8,10 @@ using Aki.Common.Utils;
namespace Aki.Common.Http namespace Aki.Common.Http
{ {
[Obsolete("Request is deprecated, please use Aki.Common.Http.Client instead.")]
public class Request public class Request
{ {
/// <summary> [Obsolete("Request.Send() is deprecated, please use Aki.Common.Http.Client instead.")]
/// Send a request to remote endpoint and optionally receive a response body.
/// Deflate is the accepted compression format.
/// </summary>
public byte[] Send(string url, string method, byte[] data = null, bool compress = true, string mime = null, Dictionary<string, string> headers = null) public byte[] Send(string url, string method, byte[] data = null, bool compress = true, string mime = null, Dictionary<string, string> headers = null)
{ {
if (!WebConstants.IsValidMethod(method)) if (!WebConstants.IsValidMethod(method))
@ -81,3 +81,5 @@ namespace Aki.Common.Http
} }
} }
} }
#endregion

View File

@ -1,49 +1,46 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Aki.Common.Utils; using System.Threading.Tasks;
using BepInEx.Logging; using BepInEx.Logging;
using Aki.Common.Utils;
namespace Aki.Common.Http namespace Aki.Common.Http
{ {
public static class RequestHandler public static class RequestHandler
{ {
private static string _host;
private static string _session;
private static Request _request;
private static Dictionary<string, string> _headers;
private static ManualLogSource _logger; 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() static RequestHandler()
{ {
_logger = Logger.CreateLogSource(nameof(RequestHandler)); _logger = Logger.CreateLogSource(nameof(RequestHandler));
Initialize();
}
private static void Initialize() // grab required info from command args
{ var args = Environment.GetCommandLineArgs();
_request = new Request();
string[] args = Environment.GetCommandLineArgs(); foreach (var arg in args)
foreach (string arg in args)
{ {
if (arg.Contains("BackendUrl")) if (arg.Contains("BackendUrl"))
{ {
string json = arg.Replace("-config=", string.Empty); var json = arg.Replace("-config=", string.Empty);
_host = Json.Deserialize<ServerConfig>(json).BackendUrl; Host = Json.Deserialize<ServerConfig>(json).BackendUrl;
} }
if (arg.Contains("-token=")) if (arg.Contains("-token="))
{ {
_session = arg.Replace("-token=", string.Empty); SessionId = arg.Replace("-token=", string.Empty);
_headers = new Dictionary<string, string>()
{
{ "Cookie", $"PHPSESSID={_session}" },
{ "SessionId", _session }
};
} }
} }
IsLocal = Host.Contains("127.0.0.1")
|| Host.Contains("localhost");
// initialize http client
HttpClient = new Client(Host, SessionId);
} }
private static void ValidateData(byte[] data) private static void ValidateData(byte[] data)
@ -66,46 +63,129 @@ namespace Aki.Common.Http
_logger.LogInfo($"Request was successful"); _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}");
_logger.LogInfo($"Request GET data: {_session}:{url}"); var data = HttpClient.Get(path);
byte[] result = _request.Send(url, "GET", null, headers: _headers);
ValidateData(result); ValidateData(data);
return result; 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}");
_logger.LogInfo($"Request GET json: {_session}:{url}"); var payload = HttpClient.Get(path);
byte[] data = _request.Send(url, "GET", headers: _headers); var body = Encoding.UTF8.GetString(payload);
string result = Encoding.UTF8.GetString(data);
ValidateJson(result); ValidateJson(body);
return result; 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}");
_logger.LogInfo($"Request POST json: {_session}:{url}"); var payload = Encoding.UTF8.GetBytes(json);
byte[] data = _request.Send(url, "POST", Encoding.UTF8.GetBytes(json), true, "application/json", _headers); var data = HttpClient.Post(path, payload);
string result = Encoding.UTF8.GetString(data); var body = Encoding.UTF8.GetString(data);
ValidateJson(result); ValidateJson(body);
return result; 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: {SessionId}:{path}");
_logger.LogInfo($"Request PUT json: {_session}:{url}");
_request.Send(url, "PUT", Encoding.UTF8.GetBytes(json), true, "application/json", _headers); 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<string, string>()
{
{ "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<string, string>()
{
{ "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<string, string>()
{
{ "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<string, string>()
{
{ "Cookie", $"PHPSESSID={SessionId}" },
{ "SessionId", SessionId }
};
var request = new Request();
request.Send(url, "PUT", payload, true, mime, headers);
}
#endregion
} }
} }

View File

@ -1,53 +1,39 @@
using System.Collections.Generic; #region DEPRECATED, REMOVE IN 3.8.1
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Aki.Common.Http namespace Aki.Common.Http
{ {
[Obsolete("WebConstants is deprecated, please use System.Net.Http functionality instead.")]
public static class WebConstants public static class WebConstants
{ {
/// <summary> [Obsolete("Get is deprecated, please use HttpMethod.Get instead.")]
/// HTML GET method.
/// </summary>
public const string Get = "GET"; public const string Get = "GET";
/// <summary> [Obsolete("Head is deprecated, please use HttpMethod.Head instead.")]
/// HTML HEAD method.
/// </summary>
public const string Head = "HEAD"; public const string Head = "HEAD";
/// <summary> [Obsolete("Post is deprecated, please use HttpMethod.Post instead.")]
/// HTML POST method.
/// </summary>
public const string Post = "POST"; public const string Post = "POST";
/// <summary> [Obsolete("Put is deprecated, please use HttpMethod.Put instead.")]
/// HTML PUT method.
/// </summary>
public const string Put = "PUT"; public const string Put = "PUT";
/// <summary> [Obsolete("Delete is deprecated, please use HttpMethod.Delete instead.")]
/// HTML DELETE method.
/// </summary>
public const string Delete = "DELETE"; public const string Delete = "DELETE";
/// <summary> [Obsolete("Connect is deprecated, please use HttpMethod.Connect instead.")]
/// HTML CONNECT method.
/// </summary>
public const string Connect = "CONNECT"; public const string Connect = "CONNECT";
/// <summary> [Obsolete("Options is deprecated, please use HttpMethod.Options instead.")]
/// HTML OPTIONS method.
/// </summary>
public const string Options = "OPTIONS"; public const string Options = "OPTIONS";
/// <summary> [Obsolete("Trace is deprecated, please use HttpMethod.Trace instead.")]
/// HTML TRACE method.
/// </summary>
public const string Trace = "TRACE"; public const string Trace = "TRACE";
/// <summary> [Obsolete("Mime is deprecated, there is sadly no replacement.")]
/// HTML MIME types.
/// </summary>
public static Dictionary<string, string> Mime { get; private set; } public static Dictionary<string, string> Mime { get; private set; }
static WebConstants() static WebConstants()
@ -68,9 +54,7 @@ namespace Aki.Common.Http
}; };
} }
/// <summary> [Obsolete("IsValidMethod is deprecated, please check against HttpMethod entries instead.")]
/// Is HTML method valid?
/// </summary>
public static bool IsValidMethod(string method) public static bool IsValidMethod(string method)
{ {
return method == Get return method == Get
@ -83,12 +67,12 @@ namespace Aki.Common.Http
|| method == Trace; || method == Trace;
} }
/// <summary> [Obsolete("isValidMime is deprecated, there is sadly no replacement available.")]
/// Is MIME type valid?
/// </summary>
public static bool IsValidMime(string mime) public static bool IsValidMime(string mime)
{ {
return Mime.Any(x => x.Value == mime); return Mime.Any(x => x.Value == mime);
} }
} }
} }
#endregion

View File

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

View File

@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net472</TargetFramework> <TargetFramework>net471</TargetFramework>
<AssemblyName>aki-core</AssemblyName> <AssemblyName>aki-core</AssemblyName>
<Configuration>Release</Configuration>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Company>Aki</Company> <Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright> <Copyright>Copyright @ Aki 2024</Copyright>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -8,14 +8,20 @@ namespace Aki.Core
[BepInPlugin("com.spt-aki.core", "AKI.Core", AkiPluginInfo.PLUGIN_VERSION)] [BepInPlugin("com.spt-aki.core", "AKI.Core", AkiPluginInfo.PLUGIN_VERSION)]
class AkiCorePlugin : BaseUnityPlugin class AkiCorePlugin : BaseUnityPlugin
{ {
// Temp static logger field, remove along with plugin whitelisting before release
internal static BepInEx.Logging.ManualLogSource _logger;
public void Awake() public void Awake()
{ {
_logger = Logger;
Logger.LogInfo("Loading: Aki.Core"); Logger.LogInfo("Loading: Aki.Core");
try try
{ {
new ConsistencySinglePatch().Enable(); new ConsistencySinglePatch().Enable();
new ConsistencyMultiPatch().Enable(); new ConsistencyMultiPatch().Enable();
new GameValidationPatch().Enable();
new BattlEyePatch().Enable(); new BattlEyePatch().Enable();
new SslCertificatePatch().Enable(); new SslCertificatePatch().Enable();
new UnityWebRequestPatch().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($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED");
Logger.LogError($"{GetType().Name}: {ex}"); Logger.LogError($"{GetType().Name}: {ex}");
throw; throw;
} }

View File

@ -5,9 +5,6 @@ namespace Aki.Core.Models
{ {
public class FakeCertificateHandler : CertificateHandler public class FakeCertificateHandler : CertificateHandler
{ {
protected override bool ValidateCertificate(byte[] certificateData) protected override bool ValidateCertificate(byte[] certificateData) => true;
{
return ValidationUtil.Validate();
}
} }
} }

View File

@ -1,9 +1,8 @@
using Aki.Core.Utils; using Aki.Core.Utils;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using HarmonyLib;
namespace Aki.Core.Patches namespace Aki.Core.Patches
{ {
@ -11,11 +10,7 @@ namespace Aki.Core.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var methodName = "RunValidation"; return AccessTools.Method(typeof(BattleeyePatchClass), nameof(BattleeyePatchClass.RunValidation));
var flags = BindingFlags.Public | BindingFlags.Instance;
return PatchConstants.EftTypes.Single(x => x.GetMethod(methodName, flags) != null)
.GetMethod(methodName, flags);
} }
[PatchPrefix] [PatchPrefix]

View File

@ -12,8 +12,8 @@ namespace Aki.Core.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return PatchConstants.FilesCheckerTypes.Single(x => x.Name == "ConsistencyController") return PatchConstants.FilesCheckerTypes.SingleCustom(x => x.Name == "ConsistencyController")
.GetMethods().Single(x => x.Name == "EnsureConsistency" && x.ReturnType == typeof(Task<ICheckResult>)); .GetMethods().SingleCustom(x => x.Name == "EnsureConsistency" && x.ReturnType == typeof(Task<ICheckResult>));
} }
[PatchPrefix] [PatchPrefix]

View File

@ -12,8 +12,8 @@ namespace Aki.Core.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return PatchConstants.FilesCheckerTypes.Single(x => x.Name == "ConsistencyController") return PatchConstants.FilesCheckerTypes.SingleCustom(x => x.Name == "ConsistencyController")
.GetMethods().Single(x => x.Name == "EnsureConsistencySingle" && x.ReturnType == typeof(Task<ICheckResult>)); .GetMethods().SingleCustom(x => x.Name == "EnsureConsistencySingle" && x.ReturnType == typeof(Task<ICheckResult>));
} }
[PatchPrefix] [PatchPrefix]

View File

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

View File

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

View File

@ -1,9 +1,8 @@
using System.Linq;
using System.Reflection; using System.Reflection;
using UnityEngine.Networking; using System.Security.Cryptography.X509Certificates;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Aki.Core.Utils; using Aki.Core.Utils;
using HarmonyLib;
namespace Aki.Core.Patches namespace Aki.Core.Patches
{ {
@ -11,15 +10,14 @@ namespace Aki.Core.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return PatchConstants.EftTypes.Single(x => x.BaseType == typeof(CertificateHandler)) return AccessTools.Method(typeof(SslCertPatchClass), nameof(SslCertPatchClass.ValidateCertificate), new[] { typeof(X509Certificate) });
.GetMethod("ValidateCertificate", PatchConstants.PrivateFlags);
} }
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(ref bool __result) private static bool PatchPrefix(ref bool __result)
{ {
__result = ValidationUtil.Validate(); __result = true;
return false; // Skip origial return false; // Skip original
} }
} }
} }

View File

@ -15,8 +15,7 @@ namespace Aki.Core.Patches
{ {
try try
{ {
_ = GClass239.DEBUG_LOGIC; // UPDATE BELOW LINE TOO var type = PatchConstants.EftTypes.SingleOrDefault(t => t.GetField("TransportPrefixes") != null);
var type = PatchConstants.EftTypes.Single(t => t.Name == "Class239");
if (type == null) if (type == null)
{ {
@ -36,18 +35,17 @@ namespace Aki.Core.Patches
protected override MethodBase GetTargetMethod() 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); .GetMethod("CreateFromLegacyParams", BindingFlags.Static | BindingFlags.Public);
} }
[PatchPrefix] [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 legacyParams.Url = legacyParams.Url
.Replace("https://", "") .Replace("https://", "")
.Replace("http://", ""); .Replace("http://", "");
//Console.WriteLine($"Edited url {legacyParams.Url}");
return true; // do original method after return true; // do original method after
} }

View File

@ -1,7 +1,6 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using System; using System;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Aki.Core.Patches namespace Aki.Core.Patches
@ -10,15 +9,18 @@ namespace Aki.Core.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var targetInterface = PatchConstants.EftTypes.Single(x => x == typeof(IConnectionHandler) && x.IsInterface); var targetInterface = PatchConstants.EftTypes.SingleCustom(x => x == typeof(IConnectionHandler) && x.IsInterface);
var typeThatMatches = PatchConstants.EftTypes.Single(x => targetInterface.IsAssignableFrom(x) && x.IsAbstract && !x.IsInterface); var typeThatMatches = PatchConstants.EftTypes.SingleCustom(x => targetInterface.IsAssignableFrom(x) && x.IsAbstract && !x.IsInterface);
return typeThatMatches.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Single(x => x.ReturnType == typeof(Uri));
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] [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:"));
} }
} }
} }

View File

@ -21,7 +21,7 @@ namespace Aki.Core.Utils
{ {
v3, v3,
new FileInfo(Path.Combine(v2, @"BattlEye\BEClient_x64.dll")), 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, "ConsistencyInfo")),
new FileInfo(Path.Combine(v2, "Uninstall.exe")), new FileInfo(Path.Combine(v2, "Uninstall.exe")),
new FileInfo(Path.Combine(v2, "UnityCrashHandler64.exe")) new FileInfo(Path.Combine(v2, "UnityCrashHandler64.exe"))

View File

@ -86,7 +86,7 @@ namespace Aki.Custom.Airdrops.Utils
{ {
var serverConfig = GetConfigFromServer(); var serverConfig = GetConfigFromServer();
var allAirdropPoints = LocationScene.GetAll<AirdropPoint>().ToList(); var allAirdropPoints = LocationScene.GetAll<AirdropPoint>().ToList();
var playerPosition = gameWorld.MainPlayer.Position; var playerPosition = ((IPlayer)gameWorld.MainPlayer).Position;
var flareAirdropPoints = new List<AirdropPoint>(); var flareAirdropPoints = new List<AirdropPoint>();
var dropChance = ChanceToSpawn(gameWorld, serverConfig, isFlare); var dropChance = ChanceToSpawn(gameWorld, serverConfig, isFlare);
var flareSpawnRadiusDistance = 100f; var flareSpawnRadiusDistance = 100f;

View File

@ -1,19 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net472</TargetFramework> <TargetFramework>net471</TargetFramework>
<AssemblyName>aki-custom</AssemblyName> <AssemblyName>aki-custom</AssemblyName>
<Configuration>Release</Configuration>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Company>Aki</Company> <Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright> <Copyright>Copyright @ Aki 2024</Copyright>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="AnimationSystem.Types" HintPath="..\Shared\Managed\AnimationSystem.Types.dll" />
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\hollowed.dll" Private="False" /> <Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\hollowed.dll" Private="False" />
<Reference Include="Comfort" HintPath="..\Shared\Managed\Comfort.dll" Private="False" /> <Reference Include="Comfort" HintPath="..\Shared\Managed\Comfort.dll" Private="False" />
<Reference Include="DissonanceVoip" HintPath="..\Shared\Managed\DissonanceVoip.dll" Private="False" />
<Reference Include="Sirenix.Serialization" HintPath="..\Shared\Managed\Sirenix.Serialization.dll" Private="False" /> <Reference Include="Sirenix.Serialization" HintPath="..\Shared\Managed\Sirenix.Serialization.dll" Private="False" />
<Reference Include="Unity.TextMeshPro" HintPath="..\Shared\Managed\Unity.TextMeshPro.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" /> <Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
<Reference Include="UnityEngine.AIModule" HintPath="..\Shared\Managed\UnityEngine.AIModule.dll" Private="False" /> <Reference Include="UnityEngine.AIModule" HintPath="..\Shared\Managed\UnityEngine.AIModule.dll" Private="False" />
<Reference Include="UnityEngine.AnimationModule" HintPath="..\Shared\Managed\UnityEngine.AnimationModule.dll" Private="False" /> <Reference Include="UnityEngine.AnimationModule" HintPath="..\Shared\Managed\UnityEngine.AnimationModule.dll" Private="False" />
@ -38,6 +42,7 @@
<ProjectReference Include="..\Aki.PrePatch\Aki.PrePatch.csproj" /> <ProjectReference Include="..\Aki.PrePatch\Aki.PrePatch.csproj" />
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" /> <ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" /> <ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
<ProjectReference Include="..\Aki.SinglePlayer\Aki.SinglePlayer.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,10 +1,13 @@
using System; using System;
using Aki.Common; using Aki.Common;
using Aki.Custom.Airdrops.Patches; using Aki.Custom.Airdrops.Patches;
using Aki.Custom.BTR.Patches;
using Aki.Custom.Patches; using Aki.Custom.Patches;
using Aki.Custom.Utils; using Aki.Custom.Utils;
using Aki.SinglePlayer.Patches.ScavMode; using Aki.Reflection.Utils;
using Aki.SinglePlayer.Utils.MainMenu;
using BepInEx; using BepInEx;
using UnityEngine;
namespace Aki.Custom namespace Aki.Custom
{ {
@ -29,9 +32,12 @@ namespace Aki.Custom
// Fixed in live, no need for patch // Fixed in live, no need for patch
//new RaidSettingsWindowPatch().Enable(); //new RaidSettingsWindowPatch().Enable();
new OfflineRaidSettingsMenuPatch().Enable(); new OfflineRaidSettingsMenuPatch().Enable();
new SessionIdPatch().Enable(); // new SessionIdPatch().Enable();
new VersionLabelPatch().Enable(); new VersionLabelPatch().Enable();
new IsEnemyPatch().Enable(); new IsEnemyPatch().Enable();
new BotCalledDataTryCallPatch().Enable();
new BotCallForHelpCallBotPatch().Enable();
new BotOwnerDisposePatch().Enable();
new LocationLootCacheBustingPatch().Enable(); new LocationLootCacheBustingPatch().Enable();
//new AddSelfAsEnemyPatch().Enable(); //new AddSelfAsEnemyPatch().Enable();
new CheckAndAddEnemyPatch().Enable(); new CheckAndAddEnemyPatch().Enable();
@ -41,18 +47,45 @@ namespace Aki.Custom
new AirdropFlarePatch().Enable(); new AirdropFlarePatch().Enable();
new AddSptBotSettingsPatch().Enable(); new AddSptBotSettingsPatch().Enable();
new CustomAiPatch().Enable(); new CustomAiPatch().Enable();
new AddTraitorScavsPatch().Enable();
new ExitWhileLootingPatch().Enable(); new ExitWhileLootingPatch().Enable();
new QTEPatch().Enable(); new QTEPatch().Enable();
new PmcFirstAidPatch().Enable(); new PmcFirstAidPatch().Enable();
new SettingsLocationPatch().Enable(); new SettingsLocationPatch().Enable();
new SetLocationIdOnRaidStartPatch().Enable();
//new RankPanelPatch().Enable(); //new RankPanelPatch().Enable();
new RagfairFeePatch().Enable(); new RagfairFeePatch().Enable();
new ScavQuestPatch().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<MenuNotificationManager>();
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED"); Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED");
Logger.LogError($"{GetType().Name}: {ex}"); 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; throw;
} }

View File

@ -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<GameWorld>.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<BotEventHandler>.Instance;
var botsController = Singleton<IBotGame>.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<BTRRoadKillTrigger>();
// 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<Player.FirearmController>();
var weaponPrefab = (WeaponPrefab)AccessTools.Field(firearmController.GetType(), "weaponPrefab_0").GetValue(firearmController);
weaponSoundPlayer = weaponPrefab.GetComponent<WeaponSoundPlayer>();
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<Player> passengers = gameWorld.AllAlivePlayersList.Where(x => x.BtrState == EPlayerBtrState.Inside).ToList();
List<int> 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<MeshRenderer>();
foreach (var renderer in meshRenderers)
{
renderer.enabled = false;
}
btrServerSide.turnCheckerObject.GetComponent<Renderer>().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());
}
/// <summary>
/// Custom method to make the BTR coaxial machine gun shoot.
/// </summary>
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);
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<Renderer> rendererList = new List<Renderer>(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<GameWorld>.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<ObservedPlayerView, bool>)_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<ObservedPlayerView, bool>(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<Player.FirearmController>();
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<PlayerPoolObject>();
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);
}
}
}

View File

@ -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<GamePlayerOwner>), nameof(BaseLocalGame<GamePlayerOwner>.Stop));
}
[PatchPrefix]
private static void PatchPrefix()
{
var gameWorld = Singleton<GameWorld>.Instance;
if (gameWorld == null)
{
return;
}
var btrManager = gameWorld.GetComponent<BTRManager>();
if (btrManager != null)
{
Logger.LogWarning("[AKI-BTR] BTRDestroyAtRaidEndPatch - Raid Ended: Destroying BTRManager");
Object.Destroy(btrManager);
}
}
}
}

View File

@ -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<JsonConverter[]>("Converters").Value;
Type baseLocalGameType = PatchConstants.LocalGameType.BaseType;
return AccessTools.Method(baseLocalGameType, nameof(LocalGame.Stop));
}
[PatchPrefix]
public static void PatchPrefix()
{
GameWorld gameWorld = Singleton<GameWorld>.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<ItemFactory>.Instance.TreeToFlatItems(btrStash.Grid.Items);
RequestHandler.PutJson("/singleplayer/traderServices/itemDelivery", new
{
items = flatItems,
traderId = BTRUtil.BTRTraderId
}.ToJson(_defaultJsonConverters));
}
}
}

View File

@ -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<GameWorld>.Instance;
var player = gameWorld.MainPlayer;
var btrManager = gameWorld.GetComponent<BTRManager>();
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);
}
}
}
}
}

View File

@ -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<GameWorld>.Instance;
var btrManager = gameWorld.GetComponent<BTRManager>();
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<BtrNotificationInteractionMessageEvent>().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);
}
}
}
}

View File

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

View File

@ -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
{
/// <summary>
/// Adds a BTRManager component to the GameWorld game object when raid starts.
/// </summary>
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<BackendConfigSettingsClass>.Instance.BTRSettings;
var gameWorld = Singleton<GameWorld>.Instance;
// Only run on maps that have the BTR enabled
string location = gameWorld.MainPlayer.Location;
if (!btrSettings.LocationsWithBTR.Contains(location))
{
return;
}
gameWorld.gameObject.AddComponent<BTRManager>();
}
catch (System.Exception)
{
ConsoleScreen.LogError("[AKI-BTR] Exception thrown, check logs.");
throw;
}
}
}
}

View File

@ -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<AbstractGame>.Instance.Status;
_statusProperty.SetValue(Singleton<AbstractGame>.Instance, GameStatus.Starting);
}
[PatchPostfix]
private static void PatchPostfix()
{
_statusProperty.SetValue(Singleton<AbstractGame>.Instance, originalStatus);
}
}
}

View File

@ -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
{
/// <summary>
/// Patches an empty method in <see cref="BTRView"/> to handle updating the BTR bot's Neutrals and Enemies lists in response to taking damage.
/// </summary>
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<BotEventHandler>.Instance;
if (botEventHandler == null)
{
Logger.LogError($"[AKI-BTR] BTRReceiveDamageInfoPatch - BotEventHandler is null");
return;
}
var shotBy = (Player)damageInfo.Player.iPlayer;
botEventHandler.InterruptTraderServiceBtrSupportByBetrayer(shotBy);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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
/// <summary>
/// Used to create an instance of the item in-raid.
/// </summary>
public static Item CreateItem(string tplId)
{
var id = Guid.NewGuid().ToString("N").Substring(0, 24);
return Singleton<ItemFactory>.Instance.CreateItem(id, tplId, null);
}
public static BTRConfigModel GetConfigFromServer()
{
string json = RequestHandler.GetJson("/singleplayer/btr/config");
return JsonConvert.DeserializeObject<BTRConfigModel>(json);
}
}
}

View File

@ -22,9 +22,9 @@ namespace Aki.Custom.CustomAI
return (int)botRoleToCheck == AkiBotsPrePatcher.sptBearValue || (int)botRoleToCheck == AkiBotsPrePatcher.sptUsecValue; 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) // Check bot is pscav by looking for the opening parentheses of their nickname e.g. scavname (pmc name)
return true; return true;

View File

@ -16,7 +16,9 @@ namespace Aki.Custom.CustomAI
private static readonly string throwableItemId = "543be6564bdc2df4348b4568"; private static readonly string throwableItemId = "543be6564bdc2df4348b4568";
private static readonly string ammoItemId = "5485a8684bdc2da71d8b4567"; private static readonly string ammoItemId = "5485a8684bdc2da71d8b4567";
private static readonly string weaponId = "5422acb9af1c889c16000029"; private static readonly string weaponId = "5422acb9af1c889c16000029";
private static readonly List<string> nonFiRItems = new List<string>() { magazineId, drugId, mediKitItem, medicalItemId, injectorItemId, throwableItemId, ammoItemId }; private static readonly string armorPlate = "644120aa86ffbe10ee032b6f";
private static readonly string builtInInserts = "65649eb40bf0ed77b8044453";
private static readonly List<string> nonFiRItems = new List<string>() { magazineId, drugId, mediKitItem, medicalItemId, injectorItemId, throwableItemId, ammoItemId, armorPlate, builtInInserts };
private static readonly string pistolId = "5447b5cf4bdc2d65278b4567"; private static readonly string pistolId = "5447b5cf4bdc2d65278b4567";
private static readonly string smgId = "5447b5e04bdc2d62278b4567"; private static readonly string smgId = "5447b5e04bdc2d62278b4567";
@ -30,6 +32,7 @@ namespace Aki.Custom.CustomAI
private static readonly string knifeId = "5447e1d04bdc2dff2f8b4567"; private static readonly string knifeId = "5447e1d04bdc2dff2f8b4567";
private static readonly List<string> weaponTypeIds = new List<string>() { pistolId, smgId, assaultRifleId, assaultCarbineId, shotgunId, marksmanRifleId, sniperRifleId, machinegunId, grenadeLauncherId, knifeId }; private static readonly List<string> weaponTypeIds = new List<string>() { pistolId, smgId, assaultRifleId, assaultCarbineId, shotgunId, marksmanRifleId, sniperRifleId, machinegunId, grenadeLauncherId, knifeId };
private static readonly List<string> nonFiRPocketLoot = new List<string>{ throwableItemId, ammoItemId, magazineId, medicalItemId, mediKitItem, injectorItemId, drugId };
private readonly ManualLogSource logger; private readonly ManualLogSource logger;
public PmcFoundInRaidEquipment(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 // Must run before the container loot code, otherwise backpack loot is not FiR
MakeEquipmentNotFiR(___botOwner_0); MakeEquipmentNotFiR(___botOwner_0);
// Get inventory items that hold other items (backpack/rig/pockets) // Get inventory items that hold other items (backpack/rig/pockets/armor)
List<Slot> containerGear = ___botOwner_0.Profile.Inventory.Equipment.GetContainerSlots(); IReadOnlyList<Slot> containerGear = ___botOwner_0.Profile.Inventory.Equipment.ContainerSlots;
var nonFiRRootItems = new List<Item>();
foreach (var container in containerGear) foreach (var container in containerGear)
{ {
var firstItem = container.Items.FirstOrDefault();
foreach (var item in container.ContainedItem.GetAllItems()) foreach (var item in container.ContainedItem.GetAllItems())
{ {
// Skip items that match container (array has itself as an item) // 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}"); //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its same as container {container.FullId}");
continue; 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)) 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"); //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) // 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)) 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"); //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist");
continue; continue;
} }
// Don't add FiR to grenades/mags/ammo in pockets // Don't add FiR to grenades/mags/ammo/meds in pockets
if (container.Name == "Pockets" && new List<string> { throwableItemId, ammoItemId, magazineId, medicalItemId }.Any(item.Template._parent.Contains)) 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"); //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist");
continue; 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}"); //Logger.LogError($"flagging item FiR: {item.Id} {item.Name} _parent: {item.Template._parent}");
item.SpawnedInSession = true; item.SpawnedInSession = true;
} }
} }
// Set dogtag as FiR // Set dogtag as FiR
var dogtag = ___botOwner_0.Profile.Inventory.GetItemsInSlots(new EquipmentSlot[] { EquipmentSlot.Dogtag }); var dogtag = ___botOwner_0.Profile.Inventory.GetItemsInSlots(new EquipmentSlot[] { EquipmentSlot.Dogtag }).FirstOrDefault();
dogtag.FirstOrDefault().SpawnedInSession = true; if (dogtag != null)
{
dogtag.SpawnedInSession = true;
}
} }

View File

@ -0,0 +1,5 @@
public struct LoggingLevelResponse
{
public int verbosity { get; set; }
public bool sendToServer {get; set; }
}

View File

@ -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 class BundleInfo
{ {
public string Key { get; } public string Key { get; }
@ -14,3 +19,5 @@
} }
} }
} }
#endregion

View File

@ -1,9 +1,29 @@
namespace Aki.Custom.Models using System.Runtime.Serialization;
namespace Aki.Custom.Models
{ {
[DataContract]
public struct BundleItem public struct BundleItem
{ {
[DataMember(Name = "filename")]
public string FileName; public string FileName;
[DataMember(Name = "crc")]
public uint Crc; public uint Crc;
[DataMember(Name = "dependencies")]
public string[] 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;
}
} }
} }

View File

@ -10,8 +10,10 @@ namespace Aki.Custom.Models
public bool ScavWars; public bool ScavWars;
public bool TaggedAndCursed; public bool TaggedAndCursed;
public bool EnablePve; 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; AiAmount = aiAmount;
AiDifficulty = aiDifficulty; AiDifficulty = aiDifficulty;
@ -19,6 +21,8 @@ namespace Aki.Custom.Models
ScavWars = scavWars; ScavWars = scavWars;
TaggedAndCursed = taggedAndCursed; TaggedAndCursed = taggedAndCursed;
EnablePve = enablePve; EnablePve = enablePve;
RandomWeather = randomWeather;
RandomTime = randomTime;
} }
} }
} }

View File

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

View File

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

View File

@ -1,38 +1,15 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT; using EFT;
using System;
using System.Linq;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class AddEnemyToAllGroupsInBotZonePatch : ModulePatch 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() protected override MethodBase GetTargetMethod()
{ {
Logger.LogDebug($"{this.GetType().Name} Type: {_targetType.Name}"); return AccessTools.Method(typeof(BotsController), nameof(BotsController.AddEnemyToAllGroupsInBotZone));
Logger.LogDebug($"{this.GetType().Name} Method: {methodName}");
return _targetType.GetMethod(methodName);
} }
/// <summary> /// <summary>
@ -44,6 +21,17 @@ namespace Aki.Custom.Patches
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(BotsController __instance, IPlayer aggressor, IPlayer groupOwner, IPlayer target) 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; BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone;
foreach (var item in __instance.Groups()) foreach (var item in __instance.Groups())
{ {
@ -68,7 +56,7 @@ namespace Aki.Custom.Patches
} }
} }
return false; return false; // Skip original
} }
} }
} }

View File

@ -3,6 +3,7 @@ using System.Reflection;
using Aki.PrePatch; using Aki.PrePatch;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using EFT; using EFT;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -10,7 +11,7 @@ namespace Aki.Custom.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(BotSettingsRepoClass).GetMethod("Init"); return AccessTools.Method(typeof(BotSettingsRepoClass), nameof(BotSettingsRepoClass.Init));
} }
[PatchPrefix] [PatchPrefix]

View File

@ -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<int>(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<GameWorld>.Instance.MainPlayer;
var enemies = new List<BotOwner>();
var players = new List<Player>() { 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<IBotGame>.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;
}
}
}

View File

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

View File

@ -16,11 +16,11 @@ namespace Aki.Custom.Patches
{ {
var desiredType = PatchConstants.LocalGameType; var desiredType = PatchConstants.LocalGameType;
var desiredMethod = desiredType var desiredMethod = desiredType
.GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly) .GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.SingleOrDefault(m => IsTargetMethod(m)); .SingleOrDefault(IsTargetMethod);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType.Name}"); 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; return desiredMethod;
} }

View File

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

View File

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

View File

@ -3,7 +3,6 @@ using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using EFT; using EFT;
using EFT.UI; using EFT.UI;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
@ -15,7 +14,7 @@ namespace Aki.Custom.Patches
var methodName = "LoadDifficultyStringInternal"; var methodName = "LoadDifficultyStringInternal";
var flags = BindingFlags.Public | BindingFlags.Static; 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); .GetMethod(methodName, flags);
} }

View File

@ -1,36 +1,15 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT; using EFT;
using System;
using System.Linq;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class BotEnemyTargetPatch : ModulePatch 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() protected override MethodBase GetTargetMethod()
{ {
return _targetType.GetMethod(methodName); return AccessTools.Method(typeof(BotsController), nameof(BotsController.AddEnemyToAllGroupsInBotZone));
} }
/// <summary> /// <summary>

View File

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

View File

@ -1,6 +1,7 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using EFT; using EFT;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -9,11 +10,9 @@ namespace Aki.Custom.Patches
/// </summary> /// </summary>
internal class BotSelfEnemyPatch : ModulePatch internal class BotSelfEnemyPatch : ModulePatch
{ {
private static readonly string methodName = "PreActivate";
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(BotOwner).GetMethod(methodName); return AccessTools.Method(typeof(BotOwner), nameof(BotOwner.PreActivate));
} }
[PatchPrefix] [PatchPrefix]

View File

@ -1,65 +1,29 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT; using EFT;
using System;
using System.Linq;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class CheckAndAddEnemyPatch : ModulePatch public class CheckAndAddEnemyPatch : ModulePatch
{ {
private static Type _targetType;
private readonly string _targetMethodName = "CheckAndAddEnemy";
/// <summary>
/// BotGroupClass.CheckAndAddEnemy()
/// </summary>
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() protected override MethodBase GetTargetMethod()
{ {
return _targetType.GetMethod(_targetMethodName); return AccessTools.Method(typeof(BotsGroup), nameof(BotsGroup.CheckAndAddEnemy));
} }
/// <summary> /// <summary>
/// CheckAndAddEnemy() /// CheckAndAddEnemy()
/// Goal: This patch lets bosses shoot back once a PMC has shot them /// 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
/// </summary> /// </summary>
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(BotsGroup __instance, IPlayer player, ref bool ignoreAI) private static bool PatchPrefix(BotsGroup __instance, IPlayer player, ref bool __result)
{ {
// Z already has player as enemy BUT Enemies dict is empty, adding them again causes 'existing key' errors // Set result to not include !player.AIData.IsAI checks
if (__instance.InitialBotType == WildSpawnType.bossZryachiy || __instance.InitialBotType == WildSpawnType.followerZryachiy) __result = player.HealthController.IsAlive && !__instance.Enemies.ContainsKey(player) && __instance.AddEnemy(player, EBotEnemyCause.checkAddTODO);
{ return false; // Skip Original
return false;
}
if (!player.HealthController.IsAlive)
{
return false; // Skip original
}
if (!__instance.Enemies.ContainsKey(player))
{
__instance.AddEnemy(player, EBotEnemyCause.checkAddTODO);
}
return false; // Skip original
} }
} }
} }

View File

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

View File

@ -1,7 +1,6 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using Aki.Common.Http; using Aki.Common.Http;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
@ -13,7 +12,7 @@ namespace Aki.Custom.Patches
var methodName = "LoadCoreByString"; var methodName = "LoadCoreByString";
var flags = BindingFlags.Public | BindingFlags.Static; 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); .GetMethod(methodName, flags);
} }

View File

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

View File

@ -5,6 +5,7 @@ using Comfort.Common;
using System.Reflection; using System.Reflection;
using Aki.PrePatch; using Aki.PrePatch;
using Aki.Custom.CustomAI; using Aki.Custom.CustomAI;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -15,7 +16,7 @@ namespace Aki.Custom.Patches
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(StandartBotBrain).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance); return AccessTools.Method(typeof(StandartBotBrain), nameof(StandartBotBrain.Activate));
} }
/// <summary> /// <summary>
@ -28,12 +29,13 @@ namespace Aki.Custom.Patches
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(out WildSpawnType __state, StandartBotBrain __instance, BotOwner ___botOwner_0) private static bool PatchPrefix(out WildSpawnType __state, StandartBotBrain __instance, BotOwner ___botOwner_0)
{ {
var player = Singleton<GameWorld>.Instance.MainPlayer;
___botOwner_0.Profile.Info.Settings.Role = FixAssaultGroupPmcsRole(___botOwner_0); ___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() __state = ___botOwner_0.Profile.Info.Settings.Role; // Store original type in state param to allow access in PatchPostFix()
try try
{ {
string currentMapName = GetCurrentMap(); 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); ___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 // Set spt bot bot back to original type
___botOwner_0.Profile.Info.Settings.Role = __state; ___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 // Set pscav back to original type
___botOwner_0.Profile.Info.Settings.Role = __state; ___botOwner_0.Profile.Info.Settings.Role = __state;

View File

@ -1,5 +1,4 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Diz.Jobs; using Diz.Jobs;
using Diz.Resources; using Diz.Resources;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -12,27 +11,21 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Aki.Common.Utils;
using Aki.Custom.Models; using Aki.Custom.Models;
using Aki.Custom.Utils; using Aki.Custom.Utils;
using DependencyGraph = DependencyGraph<IEasyBundle>; using DependencyGraph = DependencyGraph<IEasyBundle>;
using Aki.Reflection.Utils;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class EasyAssetsPatch : ModulePatch public class EasyAssetsPatch : ModulePatch
{ {
private static readonly FieldInfo _manifestField;
private static readonly FieldInfo _bundlesField; private static readonly FieldInfo _bundlesField;
private static readonly PropertyInfo _systemProperty;
static EasyAssetsPatch() static EasyAssetsPatch()
{ {
var type = typeof(EasyAssets); _bundlesField = typeof(EasyAssets).GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags);
_manifestField = type.GetField(nameof(EasyAssets.Manifest));
_bundlesField = type.GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags);
// DependencyGraph<IEasyBundle>
_systemProperty = type.GetProperty("System");
} }
public EasyAssetsPatch() public EasyAssetsPatch()
@ -45,7 +38,7 @@ namespace Aki.Custom.Patches
protected override MethodBase GetTargetMethod() 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) private static bool IsTargetMethod(MethodInfo mi)
@ -65,23 +58,30 @@ namespace Aki.Custom.Patches
return false; return false;
} }
private static async Task Init(EasyAssets instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath, private static async Task Init(EasyAssets instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath, string platformName, [CanBeNull] Func<string, bool> shouldExclude, Func<string, Task> bundleCheck)
string platformName, [CanBeNull] Func<string, bool> shouldExclude, Func<string, Task> bundleCheck)
{ {
// platform manifest // platform manifest
var path = $"{rootPath.Replace("file:///", string.Empty).Replace("file://", string.Empty)}/{platformName}/"; var path = $"{rootPath.Replace("file:///", string.Empty).Replace("file://", string.Empty)}/{platformName}/";
var filepath = path + 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 // create bundles array from obfuscated type
var bundleNames = manifest.GetAllAssetBundles().Union(BundleManager.Bundles.Keys).ToArray(); var bundleNames = manifest.GetAllAssetBundles()
var bundles = (IEasyBundle[])Array.CreateInstance(EasyBundleHelper.Type, bundleNames.Length); .Union(BundleManager.Bundles.Keys)
.ToArray();
// create bundle lock
if (bundleLock == null) if (bundleLock == null)
{ {
bundleLock = new BundleLock(int.MaxValue); 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++) for (var i = 0; i < bundleNames.Length; i++)
{ {
bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[] bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[]
@ -96,11 +96,18 @@ namespace Aki.Custom.Patches
await JobScheduler.Yield(EJobPriority.Immediate); await JobScheduler.Yield(EJobPriority.Immediate);
} }
_manifestField.SetValue(instance, manifest); // create dependency graph
instance.Manifest = manifest;
_bundlesField.SetValue(instance, bundles); _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<CompatibilityAssetBundleManifest> GetManifestBundle(string filepath) private static async Task<CompatibilityAssetBundleManifest> GetManifestBundle(string filepath)
{ {
var manifestLoading = AssetBundle.LoadFromFileAsync(filepath); var manifestLoading = AssetBundle.LoadFromFileAsync(filepath);
@ -115,16 +122,18 @@ namespace Aki.Custom.Patches
private static async Task<CompatibilityAssetBundleManifest> GetManifestJson(string filepath) private static async Task<CompatibilityAssetBundleManifest> GetManifestJson(string filepath)
{ {
var text = string.Empty; var text = VFS.ReadTextFile(filepath);
using (var reader = File.OpenText($"{filepath}.json")) /* we cannot parse directly as <string, BundleDetails>, 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.
text = await reader.ReadToEndAsync(); ...so we need to first convert it to a slimmed-down type (BundleItem), then convert back to BundleDetails.
} */
var raw = JsonConvert.DeserializeObject<Dictionary<string, BundleItem>>(text);
var converted = raw.ToDictionary(GetPairKey, GetPairValue);
var data = JsonConvert.DeserializeObject<Dictionary<string, BundleItem>>(text).ToDictionary(GetPairKey, GetPairValue); // initialize manifest
var manifest = ScriptableObject.CreateInstance<CompatibilityAssetBundleManifest>(); var manifest = ScriptableObject.CreateInstance<CompatibilityAssetBundleManifest>();
manifest.SetResults(data); manifest.SetResults(converted);
return manifest; return manifest;
} }

View File

@ -1,4 +1,5 @@
using Aki.Reflection.Patching; using System;
using Aki.Reflection.Patching;
using Diz.DependencyManager; using Diz.DependencyManager;
using UnityEngine.Build.Pipeline; using UnityEngine.Build.Pipeline;
using System.IO; using System.IO;
@ -26,21 +27,26 @@ namespace Aki.Custom.Patches
[PatchPostfix] [PatchPostfix]
private static void PatchPostfix(object __instance, string key, string rootPath, CompatibilityAssetBundleManifest manifest, IBundleLock bundleLock) private static void PatchPostfix(object __instance, string key, string rootPath, CompatibilityAssetBundleManifest manifest, IBundleLock bundleLock)
{ {
var path = rootPath + key; var filepath = rootPath + key;
var dependencyKeys = manifest.GetDirectDependencies(key) ?? new string[0]; var dependencies = manifest.GetDirectDependencies(key) ?? Array.Empty<string>();
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; // server bundle
path = bundle.Path; 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) _ = new EasyBundleHelper(__instance)
{ {
Key = key, Key = key,
Path = path, Path = filepath,
KeyWithoutExtension = Path.GetFileNameWithoutExtension(key), KeyWithoutExtension = Path.GetFileNameWithoutExtension(key),
DependencyKeys = dependencyKeys, DependencyKeys = dependencies,
LoadState = new BindableState<ELoadState>(ELoadState.Unloaded, null), LoadState = new BindableState<ELoadState>(ELoadState.Unloaded, null),
BundleLock = bundleLock BundleLock = bundleLock
}; };

View File

@ -1,9 +1,8 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Comfort.Common; using Comfort.Common;
using EFT; using EFT;
using System.Linq;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -14,8 +13,7 @@ namespace Aki.Custom.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return PatchConstants.EftTypes.Single(x => x.Name == "LocalGame").BaseType // BaseLocalGame return AccessTools.Method(typeof(BaseLocalGame<GamePlayerOwner>), nameof(BaseLocalGame<GamePlayerOwner>.Stop));
.GetMethod("Stop", BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance);
} }
// Look at BaseLocalGame<TPlayerOwner> and find a method named "Stop" // Look at BaseLocalGame<TPlayerOwner> and find a method named "Stop"
@ -33,7 +31,7 @@ namespace Aki.Custom.Patches
var player = Singleton<GameWorld>.Instance.MainPlayer; var player = Singleton<GameWorld>.Instance.MainPlayer;
if (profileId == player?.Profile.Id) if (profileId == player?.Profile.Id)
{ {
GClass2898.Instance.CloseAllScreensForced(); GClass3107.Instance.CloseAllScreensForced();
} }
return true; return true;

View File

@ -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
{
/// <summary>
/// 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
/// </summary>
public class FixBrokenSpawnOnSandboxPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return AccessTools.Method(typeof(GameWorld), nameof(GameWorld.OnGameStarted));
}
[PatchPrefix]
private static void PatchPrefix()
{
var gameWorld = Singleton<GameWorld>.Instance;
if (gameWorld == null)
{
return;
}
var playerLocation = gameWorld.MainPlayer.Location;
if (playerLocation == "Sandbox")
{
LocationScene.GetAll<BotZone>().ToList().First(x => x.name == "ZoneSandbox").MaxPersonsOnPatrol = GetMaxPatrolValueFromServer();
}
}
public static int GetMaxPatrolValueFromServer()
{
string json = RequestHandler.GetJson("/singleplayer/sandbox/maxpatrol");
return JsonConvert.DeserializeObject<int>(json);
}
}
}

View File

@ -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<GameWorld>.Instance;
Random random = new Random();
// Get all extracts the player has
List<ExfiltrationPoint> EligiblePoints = ExfiltrationControllerClass.Instance.EligiblePoints(gameWorld.MainPlayer.Profile).ToList();
List<ExfiltrationPoint> PointsToPickFrom = new List<ExfiltrationPoint>();
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);
}
}
}

View File

@ -1,35 +1,16 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT; using EFT;
using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class IsEnemyPatch : ModulePatch 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() protected override MethodBase GetTargetMethod()
{ {
return _targetType.GetMethod(_targetMethodName); return AccessTools.Method(typeof(BotsGroup), nameof(BotsGroup.IsEnemy));
} }
/// <summary> /// <summary>
@ -41,6 +22,17 @@ namespace Aki.Custom.Patches
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(ref bool __result, BotsGroup __instance, IPlayer requester) 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 var isEnemy = false; // default not an enemy
if (requester == null) if (requester == null)
{ {

View File

@ -1,8 +1,7 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using System;
using System.Linq;
using System.Reflection; using System.Reflection;
using EFT;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -13,8 +12,8 @@ namespace Aki.Custom.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "LocalGame").BaseType; // BaseLocalGame var desiredType = typeof(BaseLocalGame<GamePlayerOwner>);
var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).Single(x => IsTargetMethod(x)); // method_6 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} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");

View File

@ -7,6 +7,7 @@ using EFT.UI.Matchmaker;
using System.Reflection; using System.Reflection;
using EFT; using EFT;
using HarmonyLib; using HarmonyLib;
using Aki.Reflection.Utils;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -14,13 +15,8 @@ namespace Aki.Custom.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var desiredType = typeof(MatchmakerOfflineRaidScreen); return AccessTools.GetDeclaredMethods(typeof(MatchmakerOfflineRaidScreen))
var desiredMethod = desiredType.GetMethod(nameof(MatchmakerOfflineRaidScreen.Show)); .SingleCustom(m => m.Name == nameof(MatchmakerOfflineRaidScreen.Show) && m.GetParameters().Length == 1);
Logger.LogDebug($"{GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
} }
[PatchPrefix] [PatchPrefix]
@ -49,6 +45,8 @@ namespace Aki.Custom.Patches
raidSettings.WavesSettings.IsBosses = settings.BossEnabled; raidSettings.WavesSettings.IsBosses = settings.BossEnabled;
raidSettings.BotSettings.IsScavWars = false; raidSettings.BotSettings.IsScavWars = false;
raidSettings.WavesSettings.IsTaggedAndCursed = settings.TaggedAndCursed; raidSettings.WavesSettings.IsTaggedAndCursed = settings.TaggedAndCursed;
raidSettings.TimeAndWeatherSettings.IsRandomWeather = settings.RandomWeather;
raidSettings.TimeAndWeatherSettings.IsRandomTime = settings.RandomTime;
} }
[PatchPostfix] [PatchPostfix]

View File

@ -2,23 +2,15 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using EFT.UI; using EFT.UI;
using EFT.UI.Matchmaker; using EFT.UI.Matchmaker;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class OfflineRaidSettingsMenuPatch : ModulePatch public class OfflineRaidSettingsMenuPatch : ModulePatch
{ {
/// <summary>
/// RaidSettingsWindow.Show()
/// </summary>
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var desiredType = typeof(RaidSettingsWindow); return AccessTools.Method(typeof(RaidSettingsWindow), nameof(RaidSettingsWindow.Show));
var desiredMethod = desiredType.GetMethod(nameof(RaidSettingsWindow.Show));
Logger.LogDebug($"{GetType().Name} Type: {desiredType.Name}");
Logger.LogDebug($"{GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
} }
[PatchPostfix] [PatchPostfix]

View File

@ -1,4 +1,5 @@
using Aki.Reflection.Patching; using Aki.PrePatch;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using EFT; using EFT;
using System; using System;
@ -23,7 +24,7 @@ namespace Aki.Custom.Patches
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return _targetType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); return _targetType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public);
} }
/// <summary> /// <summary>
@ -42,7 +43,7 @@ namespace Aki.Custom.Patches
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(BotOwner ___botOwner_0) 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; var healthController = ___botOwner_0.GetPlayer.ActiveHealthController;

View File

@ -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
{
/// <summary>
/// Prevents loading of non-whitelisted client mods to minimize the amount of false issue reports being made during the public BE phase
/// </summary>
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);
}
}
}
}

View File

@ -2,14 +2,16 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using System.Reflection; using System.Reflection;
using EFT; using EFT;
using Aki.Reflection.Utils; using HarmonyLib;
using System.Linq;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class QTEPatch : ModulePatch 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] [PatchPostfix]
private static void PatchPostfix(HideoutPlayerOwner __instance) private static void PatchPostfix(HideoutPlayerOwner __instance)

View File

@ -1,9 +1,9 @@
using Aki.Common.Http; using Aki.Common.Http;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT.InventoryLogic; using EFT.InventoryLogic;
using EFT.UI.Ragfair; using EFT.UI.Ragfair;
using System.Reflection; using System.Reflection;
using HarmonyLib;
using UnityEngine; using UnityEngine;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
@ -17,33 +17,32 @@ namespace Aki.Custom.Patches
public RagfairFeePatch() public RagfairFeePatch()
{ {
// Remember to update prefix parameter if below lines are broken // Remember to update prefix parameter if below lines are broken
_ = nameof(GClass2860.IsAllSelectedItemSame); _ = nameof(GClass3069.IsAllSelectedItemSame);
_ = nameof(GClass2860.AutoSelectSimilar); _ = nameof(GClass3069.AutoSelectSimilar);
} }
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(AddOfferWindow).GetMethod("method_1", PatchConstants.PrivateFlags); return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.method_1));
} }
/// <summary> /// <summary>
/// Calculate tax to charge player and send to server before the offer is sent /// Calculate tax to charge player and send to server before the offer is sent
/// </summary> /// </summary>
/// <param name="___item_0">Item sold</param> /// <param name="___item_0">Item sold</param>
/// <param name="___gclass2859_0">OfferItemCount</param> /// <param name="___gclass3069_0">OfferItemCount</param>
/// <param name="___double_0">RequirementsPrice</param> /// <param name="___double_0">RequirementsPrice</param>
/// <param name="___bool_0">SellInOnePiece</param> /// <param name="___bool_0">SellInOnePiece</param>
[PatchPrefix] [PatchPrefix]
private static void PatchPrefix(ref Item ___item_0, ref GClass2860 ___gclass2860_0, ref double ___double_0, ref bool ___bool_0) 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 RequestHandler.PutJson("/client/ragfair/offerfees", new
{ {
id = ___item_0.Id, id = ___item_0.Id,
tpl = ___item_0.TemplateId, tpl = ___item_0.TemplateId,
count = ___gclass2860_0.OfferItemCount, count = ___gclass3069_0.OfferItemCount,
fee = Mathf.CeilToInt((float)GClass1941.CalculateTaxPrice(___item_0, ___gclass2860_0.OfferItemCount, ___double_0, ___bool_0)) fee = Mathf.CeilToInt((float)GClass2089.CalculateTaxPrice(___item_0, ___gclass3069_0.OfferItemCount, ___double_0, ___bool_0))
} }.ToJson());
.ToJson());
} }
} }
} }

View File

@ -5,6 +5,7 @@ using Aki.Custom.Models;
using EFT.UI; using EFT.UI;
using EFT.UI.Matchmaker; using EFT.UI.Matchmaker;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -14,19 +15,23 @@ namespace Aki.Custom.Patches
/// </summary> /// </summary>
public class RaidSettingsWindowPatch : ModulePatch public class RaidSettingsWindowPatch : ModulePatch
{ {
/// <summary>
/// Target method should have ~20 .UpdateValue() calls in it
/// </summary>
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var desiredType = typeof(RaidSettingsWindow); return AccessTools.Method(typeof(RaidSettingsWindow), nameof(RaidSettingsWindow.method_8));
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;
} }
[PatchPrefix] [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 json = RequestHandler.GetJson("/singleplayer/settings/raid/menu");
var settings = Json.Deserialize<DefaultRaidSettings>(json); var settings = Json.Deserialize<DefaultRaidSettings>(json);
@ -35,7 +40,10 @@ namespace Aki.Custom.Patches
____scavWars.UpdateValue(false); ____scavWars.UpdateValue(false);
____taggedAndCursed.UpdateValue(settings.TaggedAndCursed); ____taggedAndCursed.UpdateValue(settings.TaggedAndCursed);
____aiDifficultyDropdown.UpdateValue((int)settings.AiDifficulty); ____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; return false;
} }

View File

@ -1,9 +1,9 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using Comfort.Common; using Comfort.Common;
using EFT; using EFT;
using EFT.UI; using EFT.UI;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -11,13 +11,7 @@ namespace Aki.Custom.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var desiredType = typeof(RankPanel); return AccessTools.Method(typeof(RankPanel), nameof(RankPanel.Show));
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;
} }
[PatchPrefix] [PatchPrefix]

View File

@ -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<GamePlayerOwner>), nameof(BaseLocalGame<GamePlayerOwner>.Stop));
}
[PatchPrefix]
private static void PatchPrefix()
{
TraderServicesManager.Instance.Clear();
}
}
}

View File

@ -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
{
/// <summary>
/// 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
/// </summary>
protected override MethodBase GetTargetMethod()
{
return AccessTools.Method(typeof(QuestItemViewPanel), nameof(QuestItemViewPanel.smethod_0));
}
[PatchPrefix]
private static void PatchPreFix(ref IEnumerable<QuestDataClass> quests)
{
var gameWorld = Singleton<GameWorld>.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);
}
}
}
}
}

View File

@ -1,10 +1,11 @@
using Aki.Reflection.Patching; using System.Linq;
using System.Reflection;
using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using EFT.UI.Matchmaker; using EFT.UI.Matchmaker;
using System.Linq; using HarmonyLib;
using System.Reflection;
namespace Aki.SinglePlayer.Patches.ScavMode namespace Aki.Custom.Patches
{ {
/// <summary> /// <summary>
/// Copy over scav-only quests from PMC profile to scav profile on pre-raid screen /// 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() protected override MethodBase GetTargetMethod()
{ {
var desiredType = typeof(MatchmakerOfflineRaidScreen); return AccessTools.GetDeclaredMethods(typeof(MatchmakerOfflineRaidScreen))
var desiredMethod = desiredType.GetMethod(nameof(MatchmakerOfflineRaidScreen.Show)); .SingleCustom(m => m.Name == nameof(MatchmakerOfflineRaidScreen.Show) && m.GetParameters().Length == 1);
Logger.LogDebug($"{GetType().Name} Type: {desiredType?.Name}");
Logger.LogDebug($"{GetType().Name} Method: {desiredMethod?.Name}");
return desiredMethod;
} }
[PatchPostfix] [PatchPostfix]

View File

@ -1,8 +1,9 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT.UI; using EFT.UI;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using EFT;
using HarmonyLib;
using UnityEngine; using UnityEngine;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
@ -11,14 +12,9 @@ namespace Aki.Custom.Patches
{ {
private static PreloaderUI _preloader; private static PreloaderUI _preloader;
static SessionIdPatch()
{
_preloader = null;
}
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return PatchConstants.LocalGameType.BaseType.GetMethod("method_5", PatchConstants.PrivateFlags); return AccessTools.Method(typeof(BaseLocalGame<GamePlayerOwner>), nameof(BaseLocalGame<GamePlayerOwner>.method_5));
} }
[PatchPostfix] [PatchPostfix]

View File

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

View File

@ -6,9 +6,12 @@ using System.Reflection;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
/// <summary>
/// Redirect the settings data to save into the SPT folder, not app data
/// </summary>
public class SettingsLocationPatch : ModulePatch 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() protected override MethodBase GetTargetMethod()
{ {

View File

@ -5,7 +5,6 @@ using Aki.Reflection.Utils;
using Aki.Custom.Models; using Aki.Custom.Models;
using EFT.UI; using EFT.UI;
using HarmonyLib; using HarmonyLib;
using System.Linq;
using System.Reflection; using System.Reflection;
using Comfort.Common; using Comfort.Common;
@ -16,20 +15,11 @@ namespace Aki.Custom.Patches
private static string _versionLabel; private static string _versionLabel;
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{
try
{ {
return PatchConstants.EftTypes return PatchConstants.EftTypes
.Single(x => x.GetField("Taxonomy", BindingFlags.Public | BindingFlags.Instance) != null) .SingleCustom(x => x.GetField("Taxonomy", BindingFlags.Public | BindingFlags.Instance) != null)
.GetMethod("Create", BindingFlags.Public | BindingFlags.Static); .GetMethod("Create", BindingFlags.Public | BindingFlags.Static);
} }
catch (System.Exception e)
{
Logger.LogInfo($"VersionLabelPatch failed {e.Message} {e.StackTrace} {e.InnerException.StackTrace}");
throw;
}
}
[PatchPostfix] [PatchPostfix]
internal static void PatchPostfix(object __result) internal static void PatchPostfix(object __result)
@ -43,7 +33,9 @@ namespace Aki.Custom.Patches
Traverse.Create(Singleton<PreloaderUI>.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}"); Traverse.Create(Singleton<PreloaderUI>.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}");
Traverse.Create(Singleton<PreloaderUI>.Instance).Field("string_2").SetValue(_versionLabel); Traverse.Create(Singleton<PreloaderUI>.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}");
} }
} }
} }

View File

@ -1,50 +1,109 @@
using System.Collections.Generic; using System.Collections.Concurrent;
using System.Text.RegularExpressions; using System.Threading.Tasks;
using BepInEx.Logging;
using Newtonsoft.Json;
using Aki.Common.Http; using Aki.Common.Http;
using Aki.Common.Utils; using Aki.Common.Utils;
using Aki.Custom.Models; using Aki.Custom.Models;
using Newtonsoft.Json.Linq;
namespace Aki.Custom.Utils namespace Aki.Custom.Utils
{ {
public static class BundleManager public static class BundleManager
{ {
public const string CachePath = "user/cache/bundles/"; private static ManualLogSource _logger;
public static Dictionary<string, BundleInfo> Bundles { get; private set; } public static readonly ConcurrentDictionary<string, BundleItem> Bundles;
public static string CachePath;
static BundleManager() static BundleManager()
{ {
Bundles = new Dictionary<string, BundleInfo>(); _logger = Logger.CreateLogSource(nameof(BundleManager));
Bundles = new ConcurrentDictionary<string, BundleItem>();
if (VFS.Exists(CachePath)) CachePath = "user/cache/bundles/";
{
VFS.DeleteDirectory(CachePath);
} }
public static string GetBundlePath(BundleItem bundle)
{
return RequestHandler.IsLocal
? $"{bundle.ModPath}/bundles/{bundle.FileName}"
: CachePath + bundle.FileName;
} }
public static void GetBundles() public static void GetBundles()
{ {
// get bundles
var json = RequestHandler.GetJson("/singleplayer/bundles"); var json = RequestHandler.GetJson("/singleplayer/bundles");
var jArray = JArray.Parse(json); var bundles = JsonConvert.DeserializeObject<BundleItem[]>(json);
foreach (var jObj in jArray) // register bundles
{ var toDownload = new ConcurrentBag<BundleItem>();
var key = jObj["key"].ToString();
var path = jObj["path"].ToString();
var bundle = new BundleInfo(key, path, jObj["dependencyKeys"].ToObject<string[]>());
if (path.Contains("http")) Parallel.ForEach(bundles, (bundle) =>
{ {
var filepath = CachePath + Regex.Split(path, "bundle/", RegexOptions.IgnoreCase)[1]; Bundles.TryAdd(bundle.FileName, bundle);
var data = RequestHandler.GetData(path, true);
if (ShouldReaquire(bundle))
{
// mark for download
toDownload.Add(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); VFS.WriteFile(filepath, data);
bundle.Path = filepath; });
}
} }
Bundles.Add(key, bundle); private static bool ShouldReaquire(BundleItem bundle)
{
if (RequestHandler.IsLocal)
{
// only handle remote bundles
return false;
} }
VFS.WriteTextFile(CachePath + "bundles.json", Json.Serialize<Dictionary<string, BundleInfo>>(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;
}
} }
} }
} }

View File

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

View File

@ -11,7 +11,7 @@ namespace Aki.Custom.Utils
{ {
public class EasyBundleHelper 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 _pathField;
private static readonly FieldInfo _keyWithoutExtensionField; private static readonly FieldInfo _keyWithoutExtensionField;
private static readonly FieldInfo _bundleLockField; private static readonly FieldInfo _bundleLockField;
@ -27,24 +27,28 @@ namespace Aki.Custom.Utils
_ = nameof(IBundleLock.IsLocked); _ = nameof(IBundleLock.IsLocked);
_ = nameof(BindableState.Bind); _ = nameof(BindableState.Bind);
// Class can be found as a private array inside EasyAssets.cs, next to DependencyGraph<IEasyBundle> Type = PatchConstants.EftTypes.SingleCustom(x => !x.IsInterface && x.GetProperty("SameNameAsset", PatchConstants.PublicDeclaredFlags) != null);
Type = PatchConstants.EftTypes.Single(x => x.GetMethod("set_SameNameAsset", _NonPublicInstanceflags) != null);
_pathField = Type.GetField("string_1", _NonPublicInstanceflags); _pathField = Type.GetField("string_1", NonPublicInstanceFlags);
_keyWithoutExtensionField = Type.GetField("string_0", _NonPublicInstanceflags); _keyWithoutExtensionField = Type.GetField("string_0", NonPublicInstanceFlags);
_bundleLockField = Type.GetFields(_NonPublicInstanceflags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock)); _bundleLockField = Type.GetFields(NonPublicInstanceFlags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock));
_dependencyKeysProperty = Type.GetProperty("DependencyKeys"); _dependencyKeysProperty = Type.GetProperty("DependencyKeys");
_keyProperty = Type.GetProperty("Key"); _keyProperty = Type.GetProperty("Key");
_loadStateProperty = Type.GetProperty("LoadState"); _loadStateProperty = Type.GetProperty("LoadState");
// Function with 0 params and returns task (usually method_0()) // 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)); var possibleMethods = Type.GetMethods(PatchConstants.PublicDeclaredFlags).Where(x => x.GetParameters().Length == 0 && x.ReturnType == typeof(Task)).ToArray();
if (possibleMethods.Count() > 1) 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) public EasyBundleHelper(object easyBundle)

View File

@ -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<string> whitelistedPlugins = new HashSet<string>
{
"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<VersionResponse>(versionJson).Version;
commitHash = sptVersion?.Trim()?.Split(' ')?.Last() ?? "";
var releaseJson = RequestHandler.GetJson("/singleplayer/release");
release = Json.Deserialize<ReleaseResponse>(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<PreloaderUI>.Instantiated && ShouldShowBetaMessage())
{
Singleton<PreloaderUI>.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<PreloaderUI>.Instantiated && ShouldShowReleaseNotes())
{
Singleton<PreloaderUI>.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;
}
}
}

View File

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

View File

@ -1,17 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net472</TargetFramework> <TargetFramework>net471</TargetFramework>
<AssemblyName>aki-debugging</AssemblyName> <AssemblyName>aki-debugging</AssemblyName>
<Configuration>Release</Configuration>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Company>Aki</Company> <Company>Aki</Company>
<Copyright>Copyright @ Aki 2022</Copyright> <Copyright>Copyright @ Aki 2024</Copyright>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\hollowed.dll" Private="False" /> <Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\hollowed.dll" Private="False" />
<Reference Include="bsg.console.core" HintPath="..\Shared\Managed\bsg.console.core.dll" Private="False" />
<Reference Include="Sirenix.Serialization" HintPath="..\Shared\Managed\Sirenix.Serialization.dll" Private="False" /> <Reference Include="Sirenix.Serialization" HintPath="..\Shared\Managed\Sirenix.Serialization.dll" Private="False" />
<Reference Include="Unity.TextMeshPro" HintPath="..\Shared\Managed\Unity.TextMeshPro.dll" Private="False" /> <Reference Include="Unity.TextMeshPro" HintPath="..\Shared\Managed\Unity.TextMeshPro.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" /> <Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
@ -20,10 +22,15 @@
<Reference Include="UnityEngine.PhysicsModule" HintPath="..\Shared\Managed\UnityEngine.PhysicsModule.dll" Private="False" /> <Reference Include="UnityEngine.PhysicsModule" HintPath="..\Shared\Managed\UnityEngine.PhysicsModule.dll" Private="False" />
<Reference Include="UnityEngine.UI" HintPath="..\Shared\Managed\UnityEngine.UI.dll" Private="False" /> <Reference Include="UnityEngine.UI" HintPath="..\Shared\Managed\UnityEngine.UI.dll" Private="False" />
<Reference Include="UnityEngine.UIModule" HintPath="..\Shared\Managed\UnityEngine.UIModule.dll" Private="False" /> <Reference Include="UnityEngine.UIModule" HintPath="..\Shared\Managed\UnityEngine.UIModule.dll" Private="False" />
<Reference Include="Comfort.Common" HintPath="..\Shared\Managed\Comfort.dll" Private="False" />
<Reference Include="DissonanceVoip" HintPath="..\Shared\Managed\DissonanceVoip.dll" Private="False" />
<Reference Include="Nlog" HintPath="..\Shared\Managed\NLog.dll" Private="False" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Aki.Common\Aki.Common.csproj" /> <ProjectReference Include="..\Aki.Common\Aki.Common.csproj" />
<ProjectReference Include="..\Aki.Custom\Aki.Custom.csproj" />
<ProjectReference Include="..\Aki.PrePatch\Aki.PrePatch.csproj" />
<ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" /> <ProjectReference Include="..\Aki.Reflection\Aki.Reflection.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,5 +1,7 @@
using System; using System;
using Aki.Common; using Aki.Common;
using Aki.Common.Http;
using Aki.Common.Utils;
using Aki.Debugging.Patches; using Aki.Debugging.Patches;
using BepInEx; using BepInEx;
@ -8,14 +10,26 @@ namespace Aki.Debugging
[BepInPlugin("com.spt-aki.debugging", "AKI.Debugging", AkiPluginInfo.PLUGIN_VERSION)] [BepInPlugin("com.spt-aki.debugging", "AKI.Debugging", AkiPluginInfo.PLUGIN_VERSION)]
public class AkiDebuggingPlugin : BaseUnityPlugin public class AkiDebuggingPlugin : BaseUnityPlugin
{ {
public static LoggingLevelResponse logLevel;
public void Awake() public void Awake()
{ {
Logger.LogInfo("Loading: Aki.Debugging"); Logger.LogInfo("Loading: Aki.Debugging");
try try
{ {
// new CoordinatesPatch().Enable();
new EndRaidDebug().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) catch (Exception ex)
{ {
@ -25,5 +39,11 @@ namespace Aki.Debugging
Logger.LogInfo("Completed: Aki.Debugging"); Logger.LogInfo("Completed: Aki.Debugging");
} }
public void Start()
{
var loggingJson = RequestHandler.GetJson("/singleplayer/enableBSGlogging");
logLevel = Json.Deserialize<LoggingLevelResponse>(loggingJson);
}
} }
} }

View File

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

View File

@ -1,30 +1,27 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT; using EFT;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using System; using System;
using System.Reflection; using System.Reflection;
using HarmonyLib;
namespace Aki.Debugging.Patches namespace Aki.Debugging.Patches
{ {
public class CoordinatesPatch : ModulePatch public class CoordinatesPatch : ModulePatch
{ {
private static TextMeshProUGUI _alphaLabel; private static TextMeshProUGUI _alphaLabel;
private static PropertyInfo _playerProperty;
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var localGameBaseType = PatchConstants.LocalGameType.BaseType; return AccessTools.Method(typeof(BaseLocalGame<GamePlayerOwner>), nameof(BaseLocalGame<GamePlayerOwner>.Update));
_playerProperty = localGameBaseType.GetProperty("PlayerOwner", BindingFlags.Public | BindingFlags.Instance);
return localGameBaseType.GetMethod("Update", PatchConstants.PrivateFlags);
} }
[PatchPrefix] [PatchPrefix]
private static void PatchPrefix(object __instance) private static void PatchPrefix(BaseLocalGame<GamePlayerOwner> __instance)
{
if (Input.GetKeyDown(KeyCode.LeftControl))
{ {
if (!Input.GetKeyDown(KeyCode.LeftControl)) return;
if (_alphaLabel == null) if (_alphaLabel == null)
{ {
_alphaLabel = GameObject.Find("AlphaLabel").GetComponent<TextMeshProUGUI>(); _alphaLabel = GameObject.Find("AlphaLabel").GetComponent<TextMeshProUGUI>();
@ -33,7 +30,7 @@ namespace Aki.Debugging.Patches
_alphaLabel.font = Resources.Load<TMP_FontAsset>("Fonts & Materials/ARIAL SDF"); _alphaLabel.font = Resources.Load<TMP_FontAsset>("Fonts & Materials/ARIAL SDF");
} }
var playerOwner = (GamePlayerOwner)_playerProperty.GetValue(__instance); var playerOwner = __instance.PlayerOwner;
var aiming = LookingRaycast(playerOwner.Player); var aiming = LookingRaycast(playerOwner.Player);
if (_alphaLabel != null) if (_alphaLabel != null)
@ -46,7 +43,6 @@ namespace Aki.Debugging.Patches
var rotation = playerOwner.transform.rotation.eulerAngles; var rotation = playerOwner.transform.rotation.eulerAngles;
Logger.LogInfo($"Character position: [{position.x},{position.y},{position.z}] | Rotation: [{rotation.x},{rotation.y},{rotation.z}]"); Logger.LogInfo($"Character position: [{position.x},{position.y},{position.z}] | Rotation: [{rotation.x},{rotation.y},{rotation.z}]");
} }
}
public static Vector3 LookingRaycast(Player player) public static Vector3 LookingRaycast(Player player)
{ {

View File

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

View File

@ -1,9 +1,9 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using System.Reflection; using System.Reflection;
using Aki.Reflection.Utils;
using BepInEx.Logging; using BepInEx.Logging;
using EFT; using EFT;
using EFT.UI; using EFT.UI;
using HarmonyLib;
using TMPro; using TMPro;
namespace Aki.Debugging.Patches namespace Aki.Debugging.Patches
@ -12,12 +12,12 @@ namespace Aki.Debugging.Patches
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(TraderCard).GetMethod("method_0", PatchConstants.PrivateFlags); return AccessTools.Method(typeof(TraderCard), nameof(TraderCard.method_0));
} }
[PatchPrefix] [PatchPrefix]
private static bool PatchPreFix(ref LocalizedText ____nickName, ref TMP_Text ____standing, 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) if (____nickName.LocalizationKey == null)
{ {
@ -37,16 +37,16 @@ namespace Aki.Debugging.Patches
return false; // skip original 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"); 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"); 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; return true;

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