using System.Collections.Concurrent; using System.Text.Json.Serialization; using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Models.Spt.Mod; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Loaders; /* { "ModPath" : "/user/mods/Mod3", "FileName" : "assets/content/weapons/usable_items/item_bottle/textures/client_assets.bundle", "Bundle" : { "key" : "assets/content/weapons/usable_items/item_bottle/textures/client_assets.bundle", "dependencyKeys" : [ ] }, "Crc" : 1030040371, "Dependencies" : [ ] } */ public class BundleInfo(string modPath, BundleManifestEntry bundle, uint bundleHash) { public string ModPath { get; private set; } = modPath; public string FileName { get; private set; } = bundle.Key; public BundleManifestEntry Bundle { get; private set; } = bundle; public uint Crc { get; private set; } = bundleHash; public List Dependencies { get; private set; } = bundle?.DependencyKeys ?? []; } [Injectable(InjectionType.Singleton)] public class BundleLoader(ISptLogger logger, JsonUtil jsonUtil, BundleHashCacheService bundleHashCacheService) { private readonly ConcurrentDictionary _bundles = []; public async Task LoadBundlesAsync(SptMod mod) { await bundleHashCacheService.HydrateCache(); var modPath = mod.GetModPath(); var modBundles = await jsonUtil.DeserializeFromFileAsync( Path.Join(Directory.GetCurrentDirectory(), modPath, "bundles.json") ); var relativeModPath = modPath.Replace('\\', '/'); var bundlesPath = Path.Join(relativeModPath, "bundles"); if (modBundles?.Manifest is null) { logger.Warning($"Could not find manifest for mod {mod.ModMetadata.Name}, skipping!"); return; } await Parallel.ForEachAsync( modBundles.Manifest, async (bundleManifest, ct) => { var bundleLocalPath = Path.Join(bundlesPath, bundleManifest.Key).Replace('\\', '/'); if (!File.Exists(bundleLocalPath)) { logger.Warning($"Could not find bundle {bundleManifest.Key} for mod {mod.ModMetadata.Name}"); return; } var bundleHash = await bundleHashCacheService.CalculateMatchAndStoreHash(bundleLocalPath); AddBundle(bundleManifest.Key, new BundleInfo(relativeModPath, bundleManifest, bundleHash)); } ); await bundleHashCacheService.WriteCache(); } /// /// Handle singleplayer/bundles /// /// List of loaded bundles. public List GetBundles() { var result = new List(); foreach (var bundle in _bundles) { result.Add(bundle.Value); } return result; } public BundleInfo? GetBundle(string bundleKey) { return _bundles.GetValueOrDefault(bundleKey); } public void AddBundle(string key, BundleInfo bundle) { var success = _bundles.TryAdd(key, bundle); if (!success) { logger.Error($"Unable to add bundle: {key}"); } } } public record BundleManifest { [JsonPropertyName("manifest")] public List? Manifest { get; set; } } public record BundleManifestEntry { [JsonPropertyName("key")] public required string Key { get; set; } [JsonPropertyName("dependencyKeys")] public List? DependencyKeys { get; set; } }