0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 09:50:43 -05:00
modules/project/Aki.Custom/Patches/EasyAssetsPatch.cs
Merijn Hendriks 9c89c31c68 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 (SPT-AKI/Modules#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 SPT-AKI/Server#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: SPT-AKI/Modules#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

157 lines
6.1 KiB
C#

using Aki.Reflection.Patching;
using Diz.Jobs;
using Diz.Resources;
using JetBrains.Annotations;
using Newtonsoft.Json;
using UnityEngine;
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.Custom.Models;
using Aki.Custom.Utils;
using DependencyGraph = DependencyGraph<IEasyBundle>;
using Aki.Reflection.Utils;
namespace Aki.Custom.Patches
{
public class EasyAssetsPatch : ModulePatch
{
private static readonly FieldInfo _bundlesField;
static EasyAssetsPatch()
{
_bundlesField = typeof(EasyAssets).GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags);
}
public EasyAssetsPatch()
{
_ = nameof(IEasyBundle.SameNameAsset);
_ = nameof(IBundleLock.IsLocked);
_ = nameof(BundleLock.MaxConcurrentOperations);
_ = nameof(DependencyGraph.GetDefaultNode);
}
protected override MethodBase GetTargetMethod()
{
return typeof(EasyAssets).GetMethods(PatchConstants.PublicDeclaredFlags).SingleCustom(IsTargetMethod);
}
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]
private static bool PatchPrefix(ref Task __result, EasyAssets __instance, [CanBeNull] IBundleLock bundleLock, string defaultKey, string rootPath,
string platformName, [CanBeNull] Func<string, bool> shouldExclude, [CanBeNull] Func<string, Task> bundleCheck)
{
__result = Init(__instance, bundleLock, defaultKey, rootPath, platformName, shouldExclude, bundleCheck);
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)
{
// platform manifest
var path = $"{rootPath.Replace("file:///", string.Empty).Replace("file://", string.Empty)}/{platformName}/";
var filepath = path + platformName;
var jsonfile = filepath + ".json";
var manifest = File.Exists(jsonfile)
? await GetManifestJson(jsonfile)
: await GetManifestBundle(filepath);
// create bundles array from obfuscated type
var bundleNames = manifest.GetAllAssetBundles()
.Union(BundleManager.Bundles.Keys)
.ToArray();
// create bundle lock
if (bundleLock == null)
{
bundleLock = new BundleLock(int.MaxValue);
}
// create bundle of obfuscated type
var bundles = (IEasyBundle[])Array.CreateInstance(EasyBundleHelper.Type, bundleNames.Length);
for (var i = 0; i < bundleNames.Length; i++)
{
bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[]
{
bundleNames[i],
path,
manifest,
bundleLock,
bundleCheck
});
await JobScheduler.Yield(EJobPriority.Immediate);
}
// create dependency graph
instance.Manifest = manifest;
_bundlesField.SetValue(instance, bundles);
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)
{
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)
{
var text = VFS.ReadTextFile(filepath);
/* 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);
// initialize manifest
var manifest = ScriptableObject.CreateInstance<CompatibilityAssetBundleManifest>();
manifest.SetResults(converted);
return manifest;
}
public static string GetPairKey(KeyValuePair<string, BundleItem> x)
{
return x.Key;
}
public static BundleDetails GetPairValue(KeyValuePair<string, BundleItem> x)
{
return new BundleDetails
{
FileName = x.Value.FileName,
Crc = x.Value.Crc,
Dependencies = x.Value.Dependencies
};
}
}
}