diff --git a/ExampleMods/ExampleMods.csproj b/ExampleMods/ExampleMods.csproj index d2c59028..4bee12a5 100644 --- a/ExampleMods/ExampleMods.csproj +++ b/ExampleMods/ExampleMods.csproj @@ -26,4 +26,8 @@ + + + + diff --git a/ExampleMods/Mods/11RegisterClassesInDI/RegisterClassesInDI.cs b/ExampleMods/Mods/11RegisterClassesInDI/RegisterClassesInDI.cs index 245687b3..029d87d7 100644 --- a/ExampleMods/Mods/11RegisterClassesInDI/RegisterClassesInDI.cs +++ b/ExampleMods/Mods/11RegisterClassesInDI/RegisterClassesInDI.cs @@ -5,13 +5,13 @@ using SptCommon.Annotations; namespace ExampleMods.Mods._11RegisterClassesInDI; [Injectable] -public class RegisterClassesInDI : IPostDBLoadMod // Run after db has loaded +public class Bundle : IPostDBLoadMod // Run after db has loaded { private readonly SingletonClassExample _singletonClassExample; private readonly TransientClassExample _transientClassExample; // We inject 2 classes (singleton and transient) we've made below - public RegisterClassesInDI( + public Bundle( SingletonClassExample singletonClassExample, TransientClassExample transientClassExample) { diff --git a/ExampleMods/Mods/12Bundle/Bundle.cs b/ExampleMods/Mods/12Bundle/Bundle.cs new file mode 100644 index 00000000..ea971e1e --- /dev/null +++ b/ExampleMods/Mods/12Bundle/Bundle.cs @@ -0,0 +1,19 @@ +using Core.Models.External; +using Core.Models.Utils; +using SptCommon.Annotations; + +namespace ExampleMods.Mods._12Bundle; + +[Injectable] +public class Bundle : IPostDBLoadMod // Run after db has loaded +{ + + + public Bundle() + { + } + + public void PostDBLoad() + { + } +} diff --git a/ExampleMods/Mods/12Bundle/bundles.json b/ExampleMods/Mods/12Bundle/bundles.json new file mode 100644 index 00000000..536c4046 --- /dev/null +++ b/ExampleMods/Mods/12Bundle/bundles.json @@ -0,0 +1,8 @@ +{ + "manifest": [ + { + "key": "assets/content/weapons/usable_items/item_bottle/textures/client_assets.bundle", + "dependencyKeys": [] + } + ] +} diff --git a/ExampleMods/Mods/12Bundle/bundles/assets/content/patron_1143x33mmr_lead.bundle b/ExampleMods/Mods/12Bundle/bundles/assets/content/patron_1143x33mmr_lead.bundle new file mode 100644 index 00000000..ef204b85 Binary files /dev/null and b/ExampleMods/Mods/12Bundle/bundles/assets/content/patron_1143x33mmr_lead.bundle differ diff --git a/ExampleMods/Mods/12Bundle/package.json b/ExampleMods/Mods/12Bundle/package.json new file mode 100644 index 00000000..32a6db4c --- /dev/null +++ b/ExampleMods/Mods/12Bundle/package.json @@ -0,0 +1,13 @@ +{ + "Name": "12Bundle", + "Version": "1.0.0", + "SptVersion": "~4.0", + "LoadBefore": [], + "LoadAfter": [], + "IncompatibileMods": [], + "Url": "https://github.com/sp-tarkov/server-csharp/tree/develop/ExampleMods/Mods", + "IsBundleMod": false, + "Author": "SPT", + "Contributors": [], + "Licence": "MIT" +} diff --git a/Libraries/Core/Callbacks/BundleCallbacks.cs b/Libraries/Core/Callbacks/BundleCallbacks.cs index 2affebda..05092129 100644 --- a/Libraries/Core/Callbacks/BundleCallbacks.cs +++ b/Libraries/Core/Callbacks/BundleCallbacks.cs @@ -1,4 +1,5 @@ -using Core.Models.Eft.Common; +using Core.Loaders; +using Core.Models.Eft.Common; using Core.Utils; using SptCommon.Annotations; @@ -6,9 +7,8 @@ namespace Core.Callbacks; [Injectable(InjectableTypeOverride = typeof(BundleCallbacks))] public class BundleCallbacks( - HttpResponseUtil _httpResponseUtil - // BundleLoader _bundleLoader, -) + HttpResponseUtil _httpResponseUtil, + BundleLoader _bundleLoader) { /// /// Handle singleplayer/bundles @@ -19,8 +19,7 @@ public class BundleCallbacks( /// public string GetBundles(string url, EmptyRequestData info, string sessionID) { - // return _httpResponseUtil.NoBody(_bundleLoader.GetBundles()); - return _httpResponseUtil.NoBody(new List()); + return _httpResponseUtil.NoBody(_bundleLoader.GetBundles()); } public string GetBundle(string url, object info, string sessionID) diff --git a/Libraries/Core/Loaders/BundleLoader.cs b/Libraries/Core/Loaders/BundleLoader.cs new file mode 100644 index 00000000..1b1be090 --- /dev/null +++ b/Libraries/Core/Loaders/BundleLoader.cs @@ -0,0 +1,145 @@ +using System.Text.Json.Serialization; +using Core.Models.Utils; +using Core.Services; +using Core.Utils; +using Core.Utils.Cloners; +using SptCommon.Annotations; + +namespace Core.Loaders +{ + public class BundleInfo + { + public string ModPath + { + get; + } + + public IBundleManifestEntry Bundle + { + get; + } + + public long Crc + { + get; + } + + public List Dependencies + { + get; + } + + public BundleInfo( + string modPath, + IBundleManifestEntry bundle, + long bundleHash) + { + ModPath = modPath; + Bundle = bundle; + Crc = bundleHash; + Dependencies = bundle?.DependencyKeys ?? []; + } + } + + [Injectable(InjectionType.Singleton)] + public class BundleLoader + { + private readonly ISptLogger _logger; + private readonly HashUtil _hashUtil; + private readonly JsonUtil _jsonUtil; + private readonly FileUtil _fileUtil; + private readonly BundleHashCacheService _bundleHashCacheService; + private readonly InMemoryCacheService _inMemoryCacheService; + private readonly ICloner _cloner; + private readonly Dictionary _bundles; + + public BundleLoader( + ISptLogger logger, + HashUtil hashUtil, + JsonUtil jsonUtil, + FileUtil fileUtil, + BundleHashCacheService bundleHashCacheService, + InMemoryCacheService inMemoryCacheService, + ICloner cloner) + { + _logger = logger; + _hashUtil = hashUtil; + _jsonUtil = jsonUtil; + _fileUtil = fileUtil; + _bundleHashCacheService = bundleHashCacheService; + _inMemoryCacheService = inMemoryCacheService; + _cloner = cloner; + } + + public List GetBundles() + { + var result = new List(); + + foreach (var bundle in _bundles) { + result.Add(bundle.Value); + } + + return result; + } + + public BundleInfo GetBundle(string bundleKey) + { + return _cloner.Clone(_bundles[bundleKey]); + } + + public void AddBundles(string modPath) + { + //TODO: Implement + + var modBundlesJson = _fileUtil.ReadFile(Path.Combine(modPath, "bundles.json")); + var modBundles = _jsonUtil.Deserialize(modBundlesJson); + var bundleManifestArr = modBundles?.Manifest; + + foreach (var bundleManifest in bundleManifestArr) + { + // modpath.slice(0, -1).replace(/\\/ g, "/"); + // var bundleLocalPath = $"{modpath}bundles/${bundleManifest.key}".replace(/\\/g, "/"); + + // if (!_bundleHashCacheService.CalculateAndMatchHash(bundleLocalPath)) + // { + // _bundleHashCacheService.CalculateAndStoreHash(bundleLocalPath); + // } + + // var bundleHash = _bundleHashCacheService.GetStoredValue(bundleLocalPath); + + // AddBundle(bundleManifest.key, new BundleInfo(relativeModPath, bundleManifest, bundleHash)); + } + } + + public void AddBundle(string key, BundleInfo bundle) + { + var success = _bundles.TryAdd(key, bundle); + if (!success) + { + _logger.Error($"Unable to add bundle: {key}"); + } + } + } +} + +public interface IBundleManifest +{ + [JsonPropertyName("manifest")] + public List Manifest { get; set; } +} + +public interface IBundleManifestEntry +{ + [JsonPropertyName("key")] + public string Key { + get; + set; + } + + [JsonPropertyName("dependencyKeys")] + public List? DependencyKeys + { + get; + set; + } +} diff --git a/Libraries/Core/Properties/launchSettings.json b/Libraries/Core/Properties/launchSettings.json new file mode 100644 index 00000000..829a5441 --- /dev/null +++ b/Libraries/Core/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Core": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:64951;http://localhost:64952" + } + } +} \ No newline at end of file diff --git a/Libraries/Core/Services/BundleHashCacheService.cs b/Libraries/Core/Services/BundleHashCacheService.cs new file mode 100644 index 00000000..d9730312 --- /dev/null +++ b/Libraries/Core/Services/BundleHashCacheService.cs @@ -0,0 +1,68 @@ +using Core.Models.Utils; +using Core.Utils; +using SptCommon.Annotations; + +namespace Core.Services +{ + [Injectable(InjectionType.Singleton)] + public class BundleHashCacheService + { + private readonly ISptLogger _logger; + private readonly JsonUtil _jsonUtil; + private readonly HashUtil _hashUtil; + private readonly FileUtil _fileUtil; + protected readonly Dictionary _bundleHashes; + protected const string _bundleHashCachePath = "./user/cache/bundleHashCache.json"; + + public BundleHashCacheService( + ISptLogger logger, + JsonUtil jsonUtil, + HashUtil hashUtil, + FileUtil fileUtil) + { + _logger = logger; + _jsonUtil = jsonUtil; + _hashUtil = hashUtil; + _fileUtil = fileUtil; + } + public string GetStoredValue(string key) + { + if (!_bundleHashes.TryGetValue(key, out var value)) + { + return string.Empty; + } + + return value; + } + + public void StoreValue(string bundlePath, string hash) + { + _bundleHashes.Add(bundlePath, hash); + + _fileUtil.WriteFile(_bundleHashCachePath, _jsonUtil.Serialize(_bundleHashes)); + + _logger.Debug($"Bundle: {bundlePath} hash stored in: ${_bundleHashCachePath}"); + } + + public bool CalculateAndMatchHash(string bundlePath) + { + return MatchWithStoredHash(bundlePath, CalculateHash(bundlePath)); + } + + public void CalculateAndStoreHash(string bundlePath) + { + StoreValue(bundlePath, CalculateHash(bundlePath)); + } + + public string CalculateHash(string bundlePath) + { + var fileData = _fileUtil.ReadFile(bundlePath); + return _hashUtil.GenerateMd5ForData(fileData); + } + + public bool MatchWithStoredHash(string bundlePath, string hash) + { + return GetStoredValue(bundlePath) == hash; + } + } +}