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

Merge 382 into master (!125)

Reviewed-on: SPT-AKI/Modules#125
This commit is contained in:
chomp 2024-05-13 10:25:35 +00:00
commit b02b8e7349
7 changed files with 240 additions and 180 deletions

View File

@ -1,14 +1,14 @@
using System; using System;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using Aki.Common.Http; using System.Text;
using System.Threading.Tasks;
using Aki.Common.Utils; using Aki.Common.Utils;
namespace Aki.Common.Http namespace Aki.Common.Http
{ {
// NOTE: you do not want to dispose this, keep a reference for the lifetime // NOTE: Don't dispose this, keep a reference for the lifetime of the
// of the application. // application.
// NOTE: cannot be made async due to Unity's limitations.
public class Client : IDisposable public class Client : IDisposable
{ {
protected readonly HttpClient _httpv; protected readonly HttpClient _httpv;
@ -22,9 +22,9 @@ namespace Aki.Common.Http
_accountId = accountId; _accountId = accountId;
_retries = retries; _retries = retries;
var handler = new HttpClientHandler() var handler = new HttpClientHandler
{ {
// force setting cookies in header instead of CookieContainer // set cookies in header instead
UseCookies = false UseCookies = false
}; };
@ -43,7 +43,7 @@ namespace Aki.Common.Http
}; };
} }
protected byte[] Send(HttpMethod method, string path, byte[] data, bool compress = true) protected async Task<byte[]> SendAsync(HttpMethod method, string path, byte[] data, bool zipped = true)
{ {
HttpResponseMessage response = null; HttpResponseMessage response = null;
@ -51,17 +51,17 @@ namespace Aki.Common.Http
{ {
if (data != null) if (data != null)
{ {
// if there is data, convert to payload
byte[] payload = (compress)
? Zlib.Compress(data, ZlibCompression.Maximum)
: data;
// add payload to request // add payload to request
request.Content = new ByteArrayContent(payload); if (zipped)
{
data = Zlib.Compress(data, ZlibCompression.Maximum);
}
request.Content = new ByteArrayContent(data);
} }
// send request // send request
response = _httpv.SendAsync(request).Result; response = await _httpv.SendAsync(request);
} }
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
@ -72,85 +72,79 @@ namespace Aki.Common.Http
using (var ms = new MemoryStream()) using (var ms = new MemoryStream())
{ {
using (var stream = response.Content.ReadAsStreamAsync().Result) using (var stream = await response.Content.ReadAsStreamAsync())
{ {
// grap response payload // grap response payload
stream.CopyTo(ms); await stream.CopyToAsync(ms);
var bytes = ms.ToArray(); var body = ms.ToArray();
if (bytes != null) if (Zlib.IsCompressed(body))
{ {
// payload contains data body = Zlib.Decompress(body);
return Zlib.IsCompressed(bytes)
? Zlib.Decompress(bytes)
: bytes;
} }
if (body == null)
{
// payload doesn't contains data
var code = response.StatusCode.ToString();
body = Encoding.UTF8.GetBytes(code);
}
return body;
}
}
}
protected async Task<byte[]> SendWithRetriesAsync(HttpMethod method, string path, byte[] data, bool compress = 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 await SendAsync(method, path, data, compress);
}
catch (Exception ex)
{
error = ex;
} }
} }
// response returned no data throw error;
return null; }
public async Task<byte[]> GetAsync(string path)
{
return await SendWithRetriesAsync(HttpMethod.Get, path, null);
} }
public byte[] Get(string path) public byte[] Get(string path)
{ {
var error = new Exception("Internal error"); return Task.Run(() => GetAsync(path)).Result;
// 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) public async Task<byte[]> PostAsync(string path, byte[] data, bool compress = true)
{ {
var error = new Exception("Internal error"); return await SendWithRetriesAsync(HttpMethod.Post, path, data, compress);
// 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) public byte[] Post(string path, byte[] data, bool compress = true)
{ {
var error = new Exception("Internal error"); return Task.Run(() => PostAsync(path, data, compress)).Result;
}
// NOTE: <= is intentional, 0 is send, 1,2,3 is retry // NOTE: returns status code as bytes
for (var i = 0; i <= _retries; ++i) public async Task<byte[]> PutAsync(string path, byte[] data, bool compress = true)
{ {
try return await SendWithRetriesAsync(HttpMethod.Post, path, data, compress);
{ }
Send(HttpMethod.Put, path, data, compressed);
return;
}
catch (Exception ex)
{
error = ex;
}
}
throw error; // NOTE: returns status code as bytes
public byte[] Put(string path, byte[] data, bool compress = true)
{
return Task.Run(() => PutAsync(path, data, compress)).Result;
} }
public void Dispose() public void Dispose()

View File

@ -43,65 +43,91 @@ namespace Aki.Common.Http
HttpClient = new Client(Host, SessionId); HttpClient = new Client(Host, SessionId);
} }
private static void ValidateData(byte[] data) private static void ValidateData(string path, byte[] data)
{ {
if (data == null) if (data == null)
{ {
_logger.LogError($"Request failed, body is null"); _logger.LogError($"[REQUEST FAILED] {path}");
} }
_logger.LogInfo($"Request was successful"); _logger.LogInfo($"[REQUEST SUCCESSFUL] {path}");
} }
private static void ValidateJson(string json) private static void ValidateJson(string path, string json)
{ {
if (string.IsNullOrWhiteSpace(json)) if (string.IsNullOrWhiteSpace(json))
{ {
_logger.LogError($"Request failed, body is null"); _logger.LogError($"[REQUEST FAILED] {path}");
} }
_logger.LogInfo($"Request was successful"); _logger.LogInfo($"[REQUEST SUCCESSFUL] {path}");
}
public static async Task<byte[]> GetDataAsync(string path)
{
_logger.LogInfo($"[REQUEST]: {path}");
var data = await HttpClient.GetAsync(path);
ValidateData(path, data);
return data;
} }
public static byte[] GetData(string path) public static byte[] GetData(string path)
{ {
_logger.LogInfo($"Request GET data: {SessionId}:{path}"); return Task.Run(() => GetData(path)).Result;
}
var data = HttpClient.Get(path);
ValidateData(data); public static async Task<string> GetJsonAsync(string path)
return data; {
_logger.LogInfo($"[REQUEST]: {path}");
var payload = await HttpClient.GetAsync(path);
var body = Encoding.UTF8.GetString(payload);
ValidateJson(path, body);
return body;
} }
public static string GetJson(string path) public static string GetJson(string path)
{ {
_logger.LogInfo($"Request GET json: {SessionId}:{path}"); return Task.Run(() => GetJsonAsync(path)).Result;
var payload = HttpClient.Get(path);
var body = Encoding.UTF8.GetString(payload);
ValidateJson(body);
return body;
} }
public static string PostJson(string path, string json) public static string PostJsonAsync(string path, string json)
{ {
_logger.LogInfo($"Request POST json: {SessionId}:{path}"); _logger.LogInfo($"[REQUEST]: {path}");
var payload = Encoding.UTF8.GetBytes(json); var payload = Encoding.UTF8.GetBytes(json);
var data = HttpClient.Post(path, payload); var data = HttpClient.Post(path, payload);
var body = Encoding.UTF8.GetString(data); var body = Encoding.UTF8.GetString(data);
ValidateJson(body); ValidateJson(path, body);
return body; return body;
} }
public static void PutJson(string path, string json) public static string PostJson(string path, string json)
{ {
_logger.LogInfo($"Request PUT json: {SessionId}:{path}"); return Task.Run(() => PostJsonAsync(path, json)).Result;
}
// NOTE: returns status code
public static async Task<string> PutJsonAsync(string path, string json)
{
_logger.LogInfo($"[REQUEST]: {path}");
var payload = Encoding.UTF8.GetBytes(json); var payload = Encoding.UTF8.GetBytes(json);
HttpClient.Put(path, payload); var data = await HttpClient.PutAsync(path, payload);
var body = Encoding.UTF8.GetString(data);
ValidateJson(path, body);
return body;
}
// NOTE: returns status code
public static string PutJson(string path, string json)
{
return Task.Run(() => PutJsonAsync(path, json)).Result;
} }
#region DEPRECATED, REMOVE IN 3.8.1 #region DEPRECATED, REMOVE IN 3.8.1
@ -120,7 +146,7 @@ namespace Aki.Common.Http
var request = new Request(); var request = new Request();
var data = request.Send(url, "GET", null, headers: headers); var data = request.Send(url, "GET", null, headers: headers);
ValidateData(data); ValidateData(url, data);
return data; return data;
} }
@ -141,7 +167,7 @@ namespace Aki.Common.Http
var data = request.Send(url, "GET", headers: headers); var data = request.Send(url, "GET", headers: headers);
var body = Encoding.UTF8.GetString(data); var body = Encoding.UTF8.GetString(data);
ValidateJson(body); ValidateJson(url, body);
return body; return body;
} }
@ -164,7 +190,7 @@ namespace Aki.Common.Http
var data = request.Send(url, "POST", payload, true, mime, headers); var data = request.Send(url, "POST", payload, true, mime, headers);
var body = Encoding.UTF8.GetString(data); var body = Encoding.UTF8.GetString(data);
ValidateJson(body); ValidateJson(url, body);
return body; return body;
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks;
namespace Aki.Common.Utils namespace Aki.Common.Utils
{ {
@ -97,6 +98,22 @@ namespace Aki.Common.Utils
return File.ReadAllBytes(filepath); return File.ReadAllBytes(filepath);
} }
/// <summary>
/// Get file content as bytes.
/// </summary>
public static async Task<byte[]> ReadFileAsync(string filepath)
{
byte[] result;
using (var fs = File.Open(filepath, FileMode.Open))
{
result = new byte[fs.Length];
await fs.ReadAsync(result, 0, (int)fs.Length);
}
return result;
}
/// <summary> /// <summary>
/// Get file content as string. /// Get file content as string.
/// </summary> /// </summary>
@ -105,6 +122,20 @@ namespace Aki.Common.Utils
return File.ReadAllText(filepath); return File.ReadAllText(filepath);
} }
/// <summary>
/// Get file content as string.
/// </summary>
public static async Task<string> ReadTextFileAsync(string filepath)
{
using (var fs = File.Open(filepath, FileMode.Open))
{
using (var sr = new StreamReader(fs))
{
return await sr.ReadToEndAsync();
}
}
}
/// <summary> /// <summary>
/// Write data to file. /// Write data to file.
/// </summary> /// </summary>
@ -118,6 +149,22 @@ namespace Aki.Common.Utils
File.WriteAllBytes(filepath, data); File.WriteAllBytes(filepath, data);
} }
/// <summary>
/// Write data to file.
/// </summary>
public static async Task WriteFileAsync(string filepath, byte[] data)
{
if (!Exists(filepath))
{
CreateDirectory(filepath.GetDirectory());
}
using (FileStream stream = File.Open(filepath, FileMode.OpenOrCreate, FileAccess.Write))
{
await stream.WriteAsync(data, 0, data.Length);
}
}
/// <summary> /// <summary>
/// Write string to file. /// Write string to file.
/// </summary> /// </summary>

View File

@ -21,7 +21,6 @@ namespace Aki.Custom
try try
{ {
// Bundle patches should always load first // Bundle patches should always load first
BundleManager.GetBundles();
new EasyAssetsPatch().Enable(); new EasyAssetsPatch().Enable();
new EasyBundlePatch().Enable(); new EasyBundlePatch().Enable();

View File

@ -1,21 +1,19 @@
using Aki.Reflection.Patching; using System;
using Diz.Jobs; using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Diz.Resources; using Diz.Resources;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using UnityEngine; using UnityEngine;
using UnityEngine.Build.Pipeline; using UnityEngine.Build.Pipeline;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Aki.Common.Utils; 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 Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using DependencyGraph = DependencyGraph<IEasyBundle>;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -38,36 +36,35 @@ namespace Aki.Custom.Patches
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(EasyAssets).GetMethods(PatchConstants.PublicDeclaredFlags).SingleCustom(IsTargetMethod); return typeof(EasyAssets).GetMethod(nameof(EasyAssets.Create));
}
private static bool IsTargetMethod(MethodInfo mi)
{
var parameters = mi.GetParameters();
return (parameters.Length == 6
&& parameters[0].Name == "bundleLock"
&& parameters[1].Name == "defaultKey"
&& parameters[4].Name == "shouldExclude");
} }
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(ref Task __result, EasyAssets __instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath, private static bool PatchPrefix(ref Task<EasyAssets> __result, GameObject gameObject, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath,
string platformName, [CanBeNull] Func<string, bool> shouldExclude, [CanBeNull] Func<string, Task> bundleCheck) string platformName, [CanBeNull] Func<string, bool> shouldExclude, [CanBeNull] Func<string, Task> bundleCheck)
{ {
__result = Init(__instance, bundleLock, defaultKey, rootPath, platformName, shouldExclude, bundleCheck); var easyAsset = gameObject.AddComponent<EasyAssets>();
__result = Init(easyAsset, bundleLock, defaultKey, rootPath, platformName, shouldExclude, bundleCheck);
return false; return false;
} }
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) private static async Task<EasyAssets> Init(EasyAssets instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath, 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 eftBundlesPath = $"{rootPath.Replace("file:///", string.Empty).Replace("file://", string.Empty)}/{platformName}/";
var filepath = path + platformName; var filepath = eftBundlesPath + platformName;
var jsonfile = filepath + ".json"; var jsonfile = filepath + ".json";
var manifest = File.Exists(jsonfile) var manifest = VFS.Exists(jsonfile)
? await GetManifestJson(jsonfile) ? await GetManifestJson(jsonfile)
: await GetManifestBundle(filepath); : await GetManifestBundle(filepath);
// lazy-initialize aki bundles
if (BundleManager.Bundles.Keys.Count == 0)
{
await BundleManager.DownloadManifest();
}
// create bundles array from obfuscated type // create bundles array from obfuscated type
var bundleNames = manifest.GetAllAssetBundles() var bundleNames = manifest.GetAllAssetBundles()
.Union(BundleManager.Bundles.Keys) .Union(BundleManager.Bundles.Keys)
@ -79,27 +76,43 @@ namespace Aki.Custom.Patches
bundleLock = new BundleLock(int.MaxValue); bundleLock = new BundleLock(int.MaxValue);
} }
// create bundle of obfuscated type
var bundles = (IEasyBundle[])Array.CreateInstance(EasyBundleHelper.Type, bundleNames.Length); 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++)
{ {
var key = bundleNames[i];
var path = eftBundlesPath;
// acquire external bundle
if (BundleManager.Bundles.TryGetValue(key, out var bundleInfo))
{
// we need base path without file extension
path = BundleManager.GetBundlePath(bundleInfo);
// only download when connected externally
if (await BundleManager.ShouldReaquire(bundleInfo))
{
await BundleManager.DownloadBundle(bundleInfo);
}
}
// create bundle of obfuscated type
bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[] bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[]
{ {
bundleNames[i], key,
path, path,
manifest, manifest,
bundleLock, bundleLock,
bundleCheck bundleCheck
}); });
await JobScheduler.Yield(EJobPriority.Immediate);
} }
// create dependency graph // create dependency graph
instance.Manifest = manifest; instance.Manifest = manifest;
_bundlesField.SetValue(instance, bundles); _bundlesField.SetValue(instance, bundles);
instance.System = new DependencyGraph(bundles, defaultKey, shouldExclude); instance.System = new DependencyGraph(bundles, defaultKey, shouldExclude);
return instance;
} }
// NOTE: used by: // NOTE: used by:
@ -122,7 +135,7 @@ namespace Aki.Custom.Patches
private static async Task<CompatibilityAssetBundleManifest> GetManifestJson(string filepath) private static async Task<CompatibilityAssetBundleManifest> GetManifestJson(string filepath)
{ {
var text = VFS.ReadTextFile(filepath); var text = await VFS.ReadTextFileAsync(filepath);
/* we cannot parse directly as <string, BundleDetails>, because... /* 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. [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.

View File

@ -1,12 +1,12 @@
using System; using System;
using Aki.Reflection.Patching;
using Diz.DependencyManager;
using UnityEngine.Build.Pipeline;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Diz.DependencyManager;
using UnityEngine.Build.Pipeline;
using Aki.Custom.Models; using Aki.Custom.Models;
using Aki.Custom.Utils; using Aki.Custom.Utils;
using Aki.Reflection.Patching;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -38,7 +38,7 @@ namespace Aki.Custom.Patches
: bundle.Dependencies; : bundle.Dependencies;
// set path to either cache (HTTP) or mod (local) // set path to either cache (HTTP) or mod (local)
filepath = BundleManager.GetBundlePath(bundle); filepath = BundleManager.GetBundleFilePath(bundle);
} }
_ = new EasyBundleHelper(__instance) _ = new EasyBundleHelper(__instance)

View File

@ -10,85 +10,66 @@ namespace Aki.Custom.Utils
{ {
public static class BundleManager public static class BundleManager
{ {
private static ManualLogSource _logger; private const string CachePath = "user/cache/bundles/";
private static readonly ManualLogSource _logger;
public static readonly ConcurrentDictionary<string, BundleItem> Bundles; public static readonly ConcurrentDictionary<string, BundleItem> Bundles;
public static string CachePath;
static BundleManager() static BundleManager()
{ {
_logger = Logger.CreateLogSource(nameof(BundleManager)); _logger = Logger.CreateLogSource(nameof(BundleManager));
Bundles = new ConcurrentDictionary<string, BundleItem>(); Bundles = new ConcurrentDictionary<string, BundleItem>();
CachePath = "user/cache/bundles/";
} }
public static string GetBundlePath(BundleItem bundle) public static string GetBundlePath(BundleItem bundle)
{ {
return RequestHandler.IsLocal return RequestHandler.IsLocal
? $"{bundle.ModPath}/bundles/{bundle.FileName}" ? $"{bundle.ModPath}/bundles/"
: CachePath + bundle.FileName; : CachePath;
} }
public static void GetBundles() public static string GetBundleFilePath(BundleItem bundle)
{
return GetBundlePath(bundle) + bundle.FileName;
}
public static async Task DownloadManifest()
{ {
// get bundles // get bundles
var json = RequestHandler.GetJson("/singleplayer/bundles"); var json = await RequestHandler.GetJsonAsync("/singleplayer/bundles");
var bundles = JsonConvert.DeserializeObject<BundleItem[]>(json); var bundles = JsonConvert.DeserializeObject<BundleItem[]>(json);
// register bundles foreach (var bundle in bundles)
var toDownload = new ConcurrentBag<BundleItem>();
Parallel.ForEach(bundles, (bundle) =>
{ {
Bundles.TryAdd(bundle.FileName, bundle); Bundles.TryAdd(bundle.FileName, bundle);
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);
});
} }
} }
private static bool ShouldReaquire(BundleItem bundle) public static async Task DownloadBundle(BundleItem bundle)
{ {
if (RequestHandler.IsLocal) var filepath = GetBundleFilePath(bundle);
{ var data = await RequestHandler.GetDataAsync($"/files/bundle/{bundle.FileName}");
// only handle remote bundles await VFS.WriteFileAsync(filepath, data);
return false; }
}
public static async Task<bool> ShouldReaquire(BundleItem bundle)
{
// read cache // read cache
var filepath = CachePath + bundle.FileName; var filepath = GetBundleFilePath(bundle);
if (VFS.Exists(filepath)) if (VFS.Exists(filepath))
{ {
// calculate hash // calculate hash
var data = VFS.ReadFile(filepath); var data = await VFS.ReadFileAsync(filepath);
var crc = Crc32.Compute(data); var crc = Crc32.Compute(data);
if (crc == bundle.Crc) if (crc == bundle.Crc)
{ {
// file is up-to-date // file is up-to-date
_logger.LogInfo($"CACHE: Loading locally {bundle.FileName}"); var location = RequestHandler.IsLocal
? "MOD"
: "CACHE";
_logger.LogInfo($"{location}: Loading locally {bundle.FileName}");
return false; return false;
} }
else else