2024-05-06 19:44:28 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
using System.Threading.Tasks;
|
2023-03-03 18:52:31 +00:00
|
|
|
|
using Diz.Resources;
|
|
|
|
|
using JetBrains.Annotations;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.Build.Pipeline;
|
2024-05-21 19:10:17 +01:00
|
|
|
|
using SPT.Common.Utils;
|
|
|
|
|
using SPT.Custom.Models;
|
|
|
|
|
using SPT.Custom.Utils;
|
|
|
|
|
using SPT.Reflection.Patching;
|
2024-05-06 19:44:28 +00:00
|
|
|
|
using DependencyGraph = DependencyGraph<IEasyBundle>;
|
2024-05-21 19:10:17 +01:00
|
|
|
|
using SPT.Reflection.Utils;
|
2023-03-03 18:52:31 +00:00
|
|
|
|
|
2024-05-21 19:10:17 +01:00
|
|
|
|
namespace SPT.Custom.Patches
|
2023-03-03 18:52:31 +00:00
|
|
|
|
{
|
|
|
|
|
public class EasyAssetsPatch : ModulePatch
|
|
|
|
|
{
|
|
|
|
|
private static readonly FieldInfo _bundlesField;
|
|
|
|
|
|
|
|
|
|
static EasyAssetsPatch()
|
|
|
|
|
{
|
2024-05-16 11:09:27 +01:00
|
|
|
|
_bundlesField = typeof(EasyAssets).GetFields(PatchConstants.PrivateFlags).FirstOrDefault(field => field.FieldType == typeof(EasyAssetHelperClass[]));
|
2023-03-03 18:52:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public EasyAssetsPatch()
|
|
|
|
|
{
|
|
|
|
|
_ = nameof(IEasyBundle.SameNameAsset);
|
|
|
|
|
_ = nameof(IBundleLock.IsLocked);
|
|
|
|
|
_ = nameof(BundleLock.MaxConcurrentOperations);
|
|
|
|
|
_ = nameof(DependencyGraph.GetDefaultNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override MethodBase GetTargetMethod()
|
|
|
|
|
{
|
2024-05-06 19:44:28 +00:00
|
|
|
|
return typeof(EasyAssets).GetMethod(nameof(EasyAssets.Create));
|
2023-03-03 18:52:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[PatchPrefix]
|
2024-08-02 16:57:59 +01:00
|
|
|
|
public static bool PatchPrefix(ref Task<EasyAssets> __result, GameObject gameObject, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath,
|
2023-03-03 18:52:31 +00:00
|
|
|
|
string platformName, [CanBeNull] Func<string, bool> shouldExclude, [CanBeNull] Func<string, Task> bundleCheck)
|
|
|
|
|
{
|
2024-05-06 19:44:28 +00:00
|
|
|
|
var easyAsset = gameObject.AddComponent<EasyAssets>();
|
|
|
|
|
__result = Init(easyAsset, bundleLock, defaultKey, rootPath, platformName, shouldExclude, bundleCheck);
|
|
|
|
|
|
2023-03-03 18:52:31 +00:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 19:44:28 +00:00
|
|
|
|
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)
|
2023-03-03 18:52:31 +00:00
|
|
|
|
{
|
2023-07-20 13:11:09 +01:00
|
|
|
|
// platform manifest
|
2024-05-06 19:44:28 +00:00
|
|
|
|
var eftBundlesPath = $"{rootPath.Replace("file:///", string.Empty).Replace("file://", string.Empty)}/{platformName}/";
|
|
|
|
|
var filepath = eftBundlesPath + platformName;
|
2024-03-25 09:53:20 +00:00
|
|
|
|
var jsonfile = filepath + ".json";
|
2024-05-06 19:44:28 +00:00
|
|
|
|
var manifest = VFS.Exists(jsonfile)
|
2024-03-25 09:53:20 +00:00
|
|
|
|
? await GetManifestJson(jsonfile)
|
|
|
|
|
: await GetManifestBundle(filepath);
|
|
|
|
|
|
2024-05-21 19:18:57 +01:00
|
|
|
|
// lazy-initialize SPT bundles
|
2024-05-06 10:28:51 +00:00
|
|
|
|
if (BundleManager.Bundles.Keys.Count == 0)
|
|
|
|
|
{
|
2024-05-06 19:44:28 +00:00
|
|
|
|
await BundleManager.DownloadManifest();
|
2024-05-06 10:28:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-25 09:53:20 +00:00
|
|
|
|
// create bundles array from obfuscated type
|
|
|
|
|
var bundleNames = manifest.GetAllAssetBundles()
|
|
|
|
|
.Union(BundleManager.Bundles.Keys)
|
|
|
|
|
.ToArray();
|
2023-07-20 13:11:09 +01:00
|
|
|
|
|
2024-03-25 09:53:20 +00:00
|
|
|
|
// create bundle lock
|
2023-07-20 13:11:09 +01:00
|
|
|
|
if (bundleLock == null)
|
2023-03-03 18:52:31 +00:00
|
|
|
|
{
|
2023-07-20 13:11:09 +01:00
|
|
|
|
bundleLock = new BundleLock(int.MaxValue);
|
|
|
|
|
}
|
|
|
|
|
|
Modernize HTTP v2 (!104)
This PR won't affect modders who have already updated their mods to 3.8.0. They just need to re-compile.
This is a resubmission of my previous PR (https://dev.sp-tarkov.com/SPT-AKI/Modules/pulls/99) with additional code cleanup and fixes.
Instead of outright removing the functionality, this time I deprecate it instead (marked for removal in next release).
Requires https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/274 to function.
## Overview
- HTTP modernization
- Adds `Aki.Common.Http.Client`, a replacement for `Aki.Common.Http.Request` and builds on top of `System.Net.Http.HttpClient`
- Implements failsafe retries when requesting during busy connections
- Improved debugging
- Improved performance
- Deprecades old request code
- Bundle system
- Fixes remote downloaded bundles using external IP
- Implements functional bundle caching from remote sources
- Implements multi-threaded bundle downloads
- Implements Unity-compatible bundle format support
- Extensive cleanup
- Deprecated unneccecary models
## Why?
In it's current state, the bundle system is ducktaped together in 2021, fundumentally broken and in desperate need of a cleanup and fixes.
The HTTP code hasn't been updated since 2021, and `HttpWebRequest` has been deprecated by Microsoft for a while now.
There was also a lot of opportunity left for simple performance gains that even reduces the complexity of the code.
As for why not two separate PRs (HTTP modernization, bundle rework): both were deeply interconnected. A change in one requires modification in the other. Hence the current approach.
## Testing
The code has been validated and tested by @TheSparta and me.
A large section of the code has been implemented and tested extensively by modders.
### Local
1. Start the game from 127.0.0.1
2. The game starts loading bundles from the mods path
### Remote, full re-aquire
1. Start the server from LAN IP (http.json, set host to `cmd > ipconfig` address, example: `192.168.178.32`)
2. Start the game from LAN IP
3. A folder named `user/cache/bundles` is created
### Remote, partial-aquire (deleted)
1. Ensure all bundles are cached
2. Delete one of the aquired bundles from cache
3. Start the server from LAN IP (http.json, set host to `cmd > ipconfig` address, example: `192.168.178.32`)
4. Start the game from LAN IP
5. The bundle is redownloaded
### Remote, partial-aquire (invalid crc)
1. Ensure all bundles are cached
2. Update a bundle mod with a new bundle on the same path
3. Start the server from LAN IP (http.json, set host to `cmd > ipconfig` address, example: `192.168.178.32`)
4. Start the game from LAN IP
5. The bundle is redownloaded
### Remote, use cache
1. Ensure all bundles are cached
2. Start the server from LAN IP (http.json, set host to `cmd > ipconfig` address, example: `192.168.178.32`)
3. Start the game from LAN IP
4. The game starts loading bundles from the cache path
## Risk assessment
In order to reduce friction between releases, this PR introduces a deprecation system.
Obsolete classes and methods have been marked deprecated.
These will remain available for the current release and continue to function as normal for this release.
A warning will be displayed during build when a modder relies on the deprecated functionality.
The marked classes and methods are to be removed in the next release.
The server-side changes have no impact on modders.
## Deprecation
The following classes are affected:
- `Aki.Common.Http.Request`: Replaced by `Aki.Common.Http.Request`
- `Aki.Common.Http.WebConstants`: Replaced by functionality from `System.Net.Http`
- `Aki.Custom.Models.BundleInfo`: Replaced by `Aki.Custom.Models.BundleItem`
The following methods are affected:
- `Aki.Common.Http.RequestHandler.GetData(path, hasHost)`: `hasHost` enables connection outside intended host.
- `Aki.Common.Http.RequestHandler.GetJson(path, hasHost)`: `hasHost` enables connection outside intended host.
- `Aki.Common.Http.RequestHandler.PostJson(path, json, hasHost)`: `hasHost` enables connection outside intended host.
- `Aki.Common.Http.RequestHandler.PutJson(path, json, hasHost)`: `hasHost` enables connection outside intended host.
The deprecated methods and `Aki.Custom.Models.BundleInfo` are self-contained and can be removed independently.
The deprecated classes require removal of all deprecated code at once.
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Modules/pulls/104
Reviewed-by: TheSparta <thesparta@noreply.dev.sp-tarkov.com>
Co-authored-by: Merijn Hendriks <merijn.d.hendriks@gmail.com>
Co-committed-by: Merijn Hendriks <merijn.d.hendriks@gmail.com>
2024-03-29 18:43:46 +00:00
|
|
|
|
var bundles = (IEasyBundle[])Array.CreateInstance(EasyBundleHelper.Type, bundleNames.Length);
|
|
|
|
|
|
2024-06-18 19:03:36 +00:00
|
|
|
|
var bundleUtils = BundleUtils.Create();
|
|
|
|
|
bundleUtils.Init(bundleNames.Length);
|
|
|
|
|
|
2023-07-20 13:11:09 +01:00
|
|
|
|
for (var i = 0; i < bundleNames.Length; i++)
|
|
|
|
|
{
|
2024-05-06 19:44:28 +00:00
|
|
|
|
var key = bundleNames[i];
|
|
|
|
|
var path = eftBundlesPath;
|
|
|
|
|
|
|
|
|
|
// acquire external bundle
|
|
|
|
|
if (BundleManager.Bundles.TryGetValue(key, out var bundleInfo))
|
|
|
|
|
{
|
2024-06-18 19:03:36 +00:00
|
|
|
|
bundleUtils.SetProgress(i, bundleInfo.FileName);
|
|
|
|
|
|
2024-05-06 19:44:28 +00:00
|
|
|
|
// we need base path without file extension
|
|
|
|
|
path = BundleManager.GetBundlePath(bundleInfo);
|
|
|
|
|
|
|
|
|
|
// only download when connected externally
|
|
|
|
|
if (await BundleManager.ShouldReaquire(bundleInfo))
|
|
|
|
|
{
|
2024-08-14 08:25:59 +00:00
|
|
|
|
VFS.DeleteFile(BundleManager.GetBundleFilePath(bundleInfo));
|
|
|
|
|
|
2024-05-06 19:44:28 +00:00
|
|
|
|
await BundleManager.DownloadBundle(bundleInfo);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// create bundle of obfuscated type
|
2023-10-10 10:58:33 +00:00
|
|
|
|
bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[]
|
2024-03-25 09:53:20 +00:00
|
|
|
|
{
|
2024-05-06 19:44:28 +00:00
|
|
|
|
key,
|
2024-03-25 09:53:20 +00:00
|
|
|
|
path,
|
|
|
|
|
manifest,
|
|
|
|
|
bundleLock,
|
|
|
|
|
bundleCheck
|
|
|
|
|
});
|
2023-07-20 13:11:09 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-18 19:03:36 +00:00
|
|
|
|
bundleUtils.Dispose();
|
|
|
|
|
|
2024-03-25 09:53:20 +00:00
|
|
|
|
// create dependency graph
|
2024-01-13 22:08:29 +00:00
|
|
|
|
instance.Manifest = manifest;
|
2023-07-20 13:11:09 +01:00
|
|
|
|
_bundlesField.SetValue(instance, bundles);
|
2024-01-13 22:08:29 +00:00
|
|
|
|
instance.System = new DependencyGraph(bundles, defaultKey, shouldExclude);
|
2024-05-06 19:44:28 +00:00
|
|
|
|
|
|
|
|
|
return instance;
|
2023-03-03 18:52:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-25 09:53:20 +00:00
|
|
|
|
// 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
|
2023-03-03 18:52:31 +00:00
|
|
|
|
private static async Task<CompatibilityAssetBundleManifest> GetManifestBundle(string filepath)
|
|
|
|
|
{
|
|
|
|
|
var manifestLoading = AssetBundle.LoadFromFileAsync(filepath);
|
|
|
|
|
await manifestLoading.Await();
|
|
|
|
|
|
|
|
|
|
var assetBundle = manifestLoading.assetBundle;
|
|
|
|
|
var assetLoading = assetBundle.LoadAllAssetsAsync();
|
|
|
|
|
await assetLoading.Await();
|
|
|
|
|
|
|
|
|
|
return (CompatibilityAssetBundleManifest)assetLoading.allAssets[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<CompatibilityAssetBundleManifest> GetManifestJson(string filepath)
|
|
|
|
|
{
|
2024-05-06 10:28:51 +00:00
|
|
|
|
var text = await VFS.ReadTextFileAsync(filepath);
|
2023-03-03 18:52:31 +00:00
|
|
|
|
|
2024-03-25 09:53:20 +00:00
|
|
|
|
/* 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.
|
|
|
|
|
...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);
|
2023-03-03 18:52:31 +00:00
|
|
|
|
|
2024-03-25 09:53:20 +00:00
|
|
|
|
// initialize manifest
|
2023-03-03 18:52:31 +00:00
|
|
|
|
var manifest = ScriptableObject.CreateInstance<CompatibilityAssetBundleManifest>();
|
2024-03-25 09:53:20 +00:00
|
|
|
|
manifest.SetResults(converted);
|
2023-03-03 18:52:31 +00:00
|
|
|
|
|
|
|
|
|
return manifest;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-20 13:11:09 +01:00
|
|
|
|
public static string GetPairKey(KeyValuePair<string, BundleItem> x)
|
2023-03-03 18:52:31 +00:00
|
|
|
|
{
|
2023-07-20 13:11:09 +01:00
|
|
|
|
return x.Key;
|
|
|
|
|
}
|
2023-03-03 18:52:31 +00:00
|
|
|
|
|
2023-07-20 13:11:09 +01:00
|
|
|
|
public static BundleDetails GetPairValue(KeyValuePair<string, BundleItem> x)
|
|
|
|
|
{
|
|
|
|
|
return new BundleDetails
|
2023-03-03 18:52:31 +00:00
|
|
|
|
{
|
2023-07-20 13:11:09 +01:00
|
|
|
|
FileName = x.Value.FileName,
|
|
|
|
|
Crc = x.Value.Crc,
|
|
|
|
|
Dependencies = x.Value.Dependencies
|
|
|
|
|
};
|
2023-03-03 18:52:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|