0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 09:50:43 -05:00
modules/project/Aki.Custom/Utils/BundleManager.cs
Merijn Hendriks 4b401e7449 async-bundles (!117)
This patch contains the following:

- Initial async VFS code (for reading / writing files)
- Simplified Http Client code
- Added async support to Http Client, RequestHandler
- Improved RequestHandler logging
- Deferred bundle loading to EasyAssetPatch
- Make GetManifestJson run async

This comes with a number of benefits:

- When downloading bundles, it will mention which files succeeded or failed to download
- Bundle loading happens in the initial screen, not the white screen
- Fixed the issue where bundle loading could break bepinex loading (too long load time)
- Modders can now make async http request and read/write files async

I removed logging of sessionid inside the RequestHandler for each request, sessionid is already visible from bepinex log startup parameters.

At last, sorry for the amount of commits it took. I initially wanted to target the 3.9.0 branch, but decided to use 3.8.1 instead as async request can really help out some mods.

Reviewed-on: SPT-AKI/Modules#117
Co-authored-by: Merijn Hendriks <merijn.d.hendriks@gmail.com>
Co-committed-by: Merijn Hendriks <merijn.d.hendriks@gmail.com>
2024-05-06 10:28:51 +00:00

110 lines
3.4 KiB
C#

using System.Collections.Concurrent;
using System.Threading.Tasks;
using BepInEx.Logging;
using Newtonsoft.Json;
using Aki.Common.Http;
using Aki.Common.Utils;
using Aki.Custom.Models;
namespace Aki.Custom.Utils
{
public static class BundleManager
{
private static readonly ManualLogSource _logger;
public static readonly ConcurrentDictionary<string, BundleItem> Bundles;
public static string CachePath;
static BundleManager()
{
_logger = Logger.CreateLogSource(nameof(BundleManager));
Bundles = new ConcurrentDictionary<string, BundleItem>();
CachePath = "user/cache/bundles/";
}
public static string GetBundlePath(BundleItem bundle)
{
return RequestHandler.IsLocal
? $"{bundle.ModPath}/bundles/{bundle.FileName}"
: CachePath + bundle.FileName;
}
public static async Task GetBundles()
{
// get bundles
var json = RequestHandler.GetJson("/singleplayer/bundles");
var bundles = JsonConvert.DeserializeObject<BundleItem[]>(json);
// register bundles
var toDownload = new ConcurrentBag<BundleItem>();
foreach (var bundle in bundles)
{
Bundles.TryAdd(bundle.FileName, bundle);
if (await 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
foreach (var bundle in toDownload)
{
// download bundle
var filepath = GetBundlePath(bundle);
var data = await RequestHandler.GetDataAsync($"/files/bundle/{bundle.FileName}");
await VFS.WriteFileAsync(filepath, data);
}
}
}
private static async Task<bool> ShouldReaquire(BundleItem bundle)
{
if (RequestHandler.IsLocal)
{
// only handle remote bundles
return false;
}
// read cache
var filepath = CachePath + bundle.FileName;
if (VFS.Exists(filepath))
{
// calculate hash
var data = await VFS.ReadFileAsync(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;
}
}
}
}