using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Events; using Barotrauma.Steam; using FluentResults; using OneOf; namespace Barotrauma.LuaCs.Services; /// /// Provides resolution for dynamically locating the best matching package at the time of consumption. /// public sealed class ContentPackageInfoLookup : IPackageInfoLookupService, IEventEnabledPackageListChanged, IEventAllPackageListChanged { #region INTERNAL // packageinfo query data private readonly ConcurrentDictionary, IPackageInfo> _packageInfoMap = new(); // package query data private readonly ConcurrentDictionary> _packageIdGroups = new(); private readonly ConcurrentDictionary> _reversePackageIdGroups = new(); private readonly HashSet _enabledPackages; private readonly HashSet _allPackages; // threading private readonly AsyncReaderWriterLock _packageIdGroupsLock = new(); private readonly AsyncReaderWriterLock _packageSetsLock = new(); // services private readonly IEventService _eventService; private readonly IPackageListRetrievalService _packageListRetrievalService; private int _isDisposed = 0; private uint _idCounter = 0; // returns ++_idCounter; private uint GetNextId() => Interlocked.Increment(ref _idCounter); private ContentPackage GetBestMatchPackage(IPackageInfo packageInfo) { if (packageInfo is null) return null; if (!_packageIdGroups.TryGetValue(packageInfo.Id, out var packageGroup) || packageGroup.IsDefaultOrEmpty) return null; if (packageGroup.Length == 1) return packageGroup[0]; bool nameGood = !packageInfo.Name.IsNullOrWhiteSpace(); // try by enabled var prev = packageGroup; var packList = packageGroup; using (_packageSetsLock.AcquireReaderLock().GetAwaiter().GetResult()) { packList = packList .Where(p => p is not null && _enabledPackages.Contains(p)) .ToImmutableArray(); } if (ReturnValue()) return packList[0]; // try by steam id if (packageInfo.SteamWorkshopId != 0) { packList = packList .Where(p => p.TryExtractSteamWorkshopId(out var sId) && sId.Value == packageInfo.SteamWorkshopId) .ToImmutableArray(); if (ReturnValue()) return packList[0]; } // try by name if (nameGood) { packList = packList .Where(p => p.Name == packageInfo.Name) .ToImmutableArray(); if (ReturnValue()) return packList[0]; } // try by localmods packList = packList.Where(p => p.Path.ToLowerInvariant().Contains("localmods")) .ToImmutableArray(); if (ReturnValue()) return packList[0]; // get the first in the list return packList.First(); bool ReturnValue() { if (packList.IsDefaultOrEmpty) packList = prev; else if (packList.Length == 1) return true; else prev = packList; return false; } } private async Task SyncPackagesLists(IReadOnlyList enabledPackages, IReadOnlyList allPackages) { if (enabledPackages is null || allPackages is null) return; // take all locks using var l1 = await _packageIdGroupsLock.AcquireWriterLock(); using var l2 = await _packageSetsLock.AcquireWriterLock(); // calc diffs var toAddAll = allPackages.Except(_allPackages).ToHashSet(); var toAddEnabled = enabledPackages.Except(_enabledPackages).ToHashSet(); var toRemoveAll = _allPackages.Except(allPackages).ToHashSet(); var toRemoveEnabled = _enabledPackages.Except(enabledPackages).ToHashSet(); // remove old if (toRemoveAll.Any()) { foreach (var package in toRemoveAll) { if (package is null) continue; _allPackages.Remove(package); // try to find id lookup if (!_reversePackageIdGroups.TryGetValue(package, out var idGroup)) continue; // found packs if (!idGroup.IsDefaultOrEmpty) { foreach (var id in idGroup) { if (!_packageIdGroups.TryGetValue(id, out var packageGroup) || packageGroup.IsDefaultOrEmpty) continue; _packageIdGroups[id] = packageGroup.RemoveAll(p => toRemoveAll.Contains(p)); } } // remove ref _reversePackageIdGroups.Remove(package, out _); } } if (toRemoveEnabled.Any()) { foreach (var package in toRemoveEnabled) { if (package is null) continue; _enabledPackages.Remove(package); } } // add new if (toAddAll.Any()) { foreach (var package in toAddAll) { if (package is null) continue; _allPackages.Add(package); var steamId = package.TryExtractSteamWorkshopId(out var id) ? id.Value : 0; IPackageInfo packageInfo; Queue idListsToAdd = new(); if (!package.Name.IsNullOrWhiteSpace() && steamId > 0) { // combined key packageInfo = GetOrCreateInfoForMap(package, (package.Name, steamId)); AddToPackageIdGroups(packageInfo.Id, package); // string key packageInfo = GetOrCreateInfoForMap(package, package.Name); AddToPackageIdGroups(packageInfo.Id, package); // steamId key packageInfo = GetOrCreateInfoForMap(package, steamId); AddToPackageIdGroups(packageInfo.Id, package); } // try find in the existing list, or make a new one IPackageInfo GetOrCreateInfoForMap(ContentPackage package, OneOf.OneOf infoKey) { return _packageInfoMap.TryGetValue(infoKey, out var pInfo) ? pInfo : new PackageInfo(package, GetNextId(), GetBestMatchPackage); } // add to package lookups void AddToPackageIdGroups(uint id, ContentPackage package) { if (_packageIdGroups.TryGetValue(id, out var packageGroup)) { if (!packageGroup.Contains(package)) _packageIdGroups[id] = packageGroup.Add(package); } else _packageIdGroups[id] = new[] { package }.ToImmutableArray(); if (_reversePackageIdGroups.TryGetValue(package, out var idGroup)) { if (!idGroup.Contains(id)) _reversePackageIdGroups[package] = idGroup.Add(id); } else _reversePackageIdGroups[package] = new[] { id }.ToImmutableArray(); } } } if (toAddEnabled.Any()) { foreach (var package in toAddEnabled) { if (package is null) continue; _enabledPackages.Add(package); } } } private async Task> LookupInternal(OneOf.OneOf infoKey) { using (await _packageIdGroupsLock.AcquireReaderLock()) { if (_packageInfoMap.TryGetValue(infoKey, out var packageInfo)) return FluentResults.Result.Ok(packageInfo); } // change to write lock using (await _packageIdGroupsLock.AcquireWriterLock()) { // create one var packageInfo = infoKey.Match( sPackName => new PackageInfo(sPackName, GetNextId(), GetBestMatchPackage), uSteamId => new PackageInfo(uSteamId, GetNextId(), GetBestMatchPackage), cKey => new PackageInfo(cKey.Item1, cKey.Item2, GetNextId(), GetBestMatchPackage) ); _packageInfoMap[infoKey] = packageInfo; // empty array _packageIdGroups[packageInfo.Id] = ImmutableArray.Empty; return FluentResults.Result.Ok(packageInfo); } } #endregion public ContentPackageInfoLookup(IEventService eventService, IPackageListRetrievalService packageListRetrievalService) { _eventService = eventService ?? throw new ArgumentNullException( $"{nameof(ContentPackageInfoLookup)}: {nameof(eventService)} cannot be null."); _packageListRetrievalService = packageListRetrievalService ?? throw new ArgumentNullException(nameof(packageListRetrievalService)); this._enabledPackages = new HashSet(); this._allPackages = new HashSet(); } public void Dispose() { IsDisposed = true; // locks using var l1 = _packageIdGroupsLock.AcquireWriterLock().GetAwaiter().GetResult(); using var l2 = _packageSetsLock.AcquireWriterLock().GetAwaiter().GetResult(); _eventService.Unsubscribe(this); _eventService.Unsubscribe(this); _packageIdGroups.Clear(); _packageInfoMap.Clear(); _reversePackageIdGroups.Clear(); } public bool IsDisposed { get => ModUtils.Threading.GetBool(ref _isDisposed); private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } public FluentResults.Result Reset() { if (IsDisposed) return FluentResults.Result.Fail($"Service is disposed."); using var l1 = _packageIdGroupsLock.AcquireWriterLock().GetAwaiter().GetResult(); using var l2 = _packageSetsLock.AcquireWriterLock().GetAwaiter().GetResult(); _packageIdGroups.Clear(); _packageInfoMap.Clear(); _reversePackageIdGroups.Clear(); RefreshPackageLists(); return FluentResults.Result.Ok(); } public void OnEnabledPackageListChanged(CorePackage package, IEnumerable regularPackages) { ((IService)this).CheckDisposed(); SyncPackagesLists( regularPackages.Select(p => (ContentPackage)p).ToImmutableArray().Add(package), _allPackages.ToImmutableArray()) .GetAwaiter().GetResult(); } public void OnAllPackageListChanged(IEnumerable corePackages, IEnumerable regularPackages) { ((IService)this).CheckDisposed(); SyncPackagesLists( _enabledPackages.ToImmutableArray(), regularPackages.Select(p => p as ContentPackage) .Union(corePackages.Select(p => p as ContentPackage)) .ToImmutableArray() ).GetAwaiter().GetResult(); } public bool IsPackageEnabled(ContentPackage package) { if (package is null) return false; using (_packageSetsLock.AcquireReaderLock().GetAwaiter().GetResult()) { return _enabledPackages.Contains(package); } } public async Task> Lookup(string packageName) { ((IService)this).CheckDisposed(); if(packageName.IsNullOrWhiteSpace()) return FluentResults.Result.Fail($"Name is null or empty."); return await LookupInternal(packageName); } public async Task> Lookup(string packageName, ulong steamWorkshopId) { ((IService)this).CheckDisposed(); if (packageName.IsNullOrWhiteSpace() || steamWorkshopId == 0) return FluentResults.Result.Fail($"Name or steam id is null or empty."); return await LookupInternal((packageName, steamWorkshopId)); } public async Task> Lookup(ulong steamWorkshopId) { ((IService)this).CheckDisposed(); if (steamWorkshopId is 0) return FluentResults.Result.Fail($"SteamId is 0."); return await LookupInternal(steamWorkshopId); } public async Task> Lookup(ContentPackage package) { ((IService)this).CheckDisposed(); if (package is null) return FluentResults.Result.Fail($"Package is null."); if (package.TryExtractSteamWorkshopId(out var steamWorkshopId) && steamWorkshopId.Value != 0) { if (!package.Name.IsNullOrWhiteSpace()) return await LookupInternal((package.Name, steamWorkshopId.Value)); else return await LookupInternal(steamWorkshopId.Value); } if (!package.Name.IsNullOrWhiteSpace()) return await LookupInternal(package.Name); return FluentResults.Result.Fail($"Package name is null and steamid is 0."); } public void RefreshPackageLists() { ((IService)this).CheckDisposed(); if (Thread.CurrentThread != GameMain.MainThread) throw new InvalidOperationException($"{nameof(ContentPackageInfoLookup)}: {nameof(RefreshPackageLists)} must be run on the main thread."); var enabledPackages = _packageListRetrievalService.GetEnabledContentPackages().ToImmutableArray(); var allPackages = _packageListRetrievalService.GetAllContentPackages().ToImmutableArray(); SyncPackagesLists(enabledPackages, allPackages).GetAwaiter().GetResult(); } }