using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Barotrauma.Extensions; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Services.Processing; namespace Barotrauma.LuaCs.Services; public partial class PackageService : IPackageService { private readonly ReaderWriterLockSlim _operationsUsageLock = new(); // only stops race conditions for pointer access // mod config / package scanners/parsers private readonly Lazy _modConfigConverterService; private readonly Lazy _legacyConfigService; private readonly Lazy _luaScriptService; private readonly Lazy _localizationService; private readonly Lazy _pluginService; private readonly Lazy _configService; private readonly IPackageManagementService _packageManagementService; private readonly IStorageService _storageService; private readonly ILoggerService _loggerService; // .ctor in server source and client source // state monitors private int _configsLoaded, _localizationsLoaded, _luaScriptsLoaded, _pluginsLoaded, _isDisposed; private int _loadingOperationsRunning; public bool ConfigsLoaded { get => GetThreadSafeBool(ref _configsLoaded); private set => SetThreadSafeBool(ref _configsLoaded, value); } public bool LocalizationsLoaded { get => GetThreadSafeBool(ref _localizationsLoaded); private set => SetThreadSafeBool(ref _localizationsLoaded, value); } public bool LuaScriptsLoaded { get => GetThreadSafeBool(ref _luaScriptsLoaded); private set => SetThreadSafeBool(ref _luaScriptsLoaded, value); } public bool PluginsLoaded { get => GetThreadSafeBool(ref _pluginsLoaded); private set => SetThreadSafeBool(ref _pluginsLoaded, value); } public bool IsDisposed { get => GetThreadSafeBool(ref _isDisposed); private set => SetThreadSafeBool(ref _isDisposed, value); } private bool LoadingOperationsRunning { get => Interlocked.CompareExchange(ref _loadingOperationsRunning, 0, 0) > 0; set // we use the set as our inc/decr { if (value) { Interlocked.Add(ref _loadingOperationsRunning, 1); } else { Interlocked.Add(ref _loadingOperationsRunning, -1); } } } #region Member: ContentPackage private readonly ReaderWriterLockSlim _packageAccessLock = new(); private ContentPackage _package; public ContentPackage Package { get { _packageAccessLock.EnterReadLock(); try { return _package; } finally { _packageAccessLock.ExitReadLock(); } } private set { _packageAccessLock.EnterWriteLock(); try { _package = value; } finally { _packageAccessLock.ExitWriteLock(); } } } #endregion #region DataContracts #region Member: ModConfigInfo private readonly ReaderWriterLockSlim _modConfigUsageLock = new(); private IModConfigInfo _modConfigInfo; public IModConfigInfo ModConfigInfo { get { _modConfigUsageLock.EnterReadLock(); try { return _modConfigInfo; } finally { _modConfigUsageLock.ExitReadLock(); } } private set { _modConfigUsageLock.EnterWriteLock(); try { _modConfigInfo = value; } finally { _modConfigUsageLock.ExitWriteLock(); } } } #endregion public ImmutableArray SupportedCultures => ModConfigInfo?.SupportedCultures ?? ImmutableArray.Empty; public ImmutableArray Assemblies => ModConfigInfo?.Assemblies ?? ImmutableArray.Empty; public ImmutableArray Localizations => ModConfigInfo?.Localizations ?? ImmutableArray.Empty; public ImmutableArray LuaScripts => ModConfigInfo?.LuaScripts ?? ImmutableArray.Empty; public ImmutableArray Configs => ModConfigInfo?.Configs ?? ImmutableArray.Empty; public ImmutableArray ConfigProfiles => ModConfigInfo?.ConfigProfiles ?? ImmutableArray.Empty; #endregion #region PublicAPI public bool TryLoadResourcesInfo(ContentPackage package) { _operationsUsageLock.EnterWriteLock(); LoadingOperationsRunning = true; try { if (IsDisposed) { throw new ObjectDisposedException($"This package service instance is disposed!"); } // try loading the ModConfig.xml. If it fails, use the Legacy loader to try and construct one from the package structure. if (_storageService.TryLoadPackageXml(package, "ModConfig.xml", out var configXml) && configXml.Root is not null) { if (_modConfigConverterService.Value.TryParseResource(configXml.Root, out IModConfigInfo configInfo)) { ModConfigInfo = configInfo; } else { _loggerService.LogError( $"Failed to parse ModConfig.xml for package {package.Name}, package mod content not loaded."); return false; } } else if (_legacyConfigService.Value.TryBuildModConfigFromLegacy(package, out var legacyConfig)) { ModConfigInfo = legacyConfig; } else { // vanilla mod or broken return false; } return true; } finally { LoadingOperationsRunning = false; _operationsUsageLock.ExitWriteLock(); } } public void LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false) { _operationsUsageLock.EnterReadLock(); LoadingOperationsRunning = true; try { if (IsDisposed) { throw new ObjectDisposedException($"This package service instance is disposed!"); } SanitationChecksCore(assembliesInfo, "assemblies", nameof(LoadPlugins)); SanitationChecksEnumerable(assembliesInfo.Assemblies, "assemblies", nameof(LoadPlugins)); #if DEBUG assembliesInfo.Assemblies.ForEach(ari => { if (!this.Assemblies.Contains(ari)) { throw new ArgumentException( $"Package Service: tried to load the assembly resource {ari.InternalName} for package {this.Package.Name} but it is not in the list for this package."); } }); #endif // Order these assemblies by internal dependencies ImmutableArray resources; if (ignoreDependencySorting) { resources = assembliesInfo.Assemblies; } else // sort by load order { resources = assembliesInfo.Assemblies .OrderByDescending(a => a.LoadPriority) .ToImmutableArray(); } // Try loading them, throw on failure. if (!_pluginService.Value.TryLoadAndInstanceTypes(resources, true, out var instancedTypes)) { throw new TypeLoadException($"PackageService: unable to load assemblies for package {this.Package.Name}! Aborting loading!"); } PluginsLoaded = true; } finally { LoadingOperationsRunning = false; _operationsUsageLock.ExitReadLock(); } } public void LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo) { _operationsUsageLock.EnterReadLock(); LoadingOperationsRunning = true; try { if (IsDisposed) { throw new ObjectDisposedException($"This package service instance is disposed!"); } SanitationChecksCore(localizationsInfo, "localizations", nameof(LoadLocalizations)); SanitationChecksEnumerable(localizationsInfo.Localizations, "localizations", nameof(LoadLocalizations)); #if DEBUG localizationsInfo.Localizations.ForEach(ri => { if (!this.Localizations.Contains(ri)) { throw new ArgumentException( $"Package Service: tried to load the localization resource for package {this.Package.Name} but it is not in the list for this package."); } }); #endif if (!_localizationService.Value.TryLoadLocalizations(localizationsInfo.Localizations)) { throw new FileLoadException($"Package Service: unable to load localizations for package {this.Package.Name}! Aborting!"); } LocalizationsLoaded = true; } finally { LoadingOperationsRunning = false; _operationsUsageLock.ExitReadLock(); } } public void AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo) { _operationsUsageLock.EnterReadLock(); LoadingOperationsRunning = true; try { if (IsDisposed) { throw new ObjectDisposedException($"This package service instance is disposed!"); } SanitationChecksCore(luaScriptsInfo, "luaScripts", nameof(AddLuaScripts)); SanitationChecksEnumerable(luaScriptsInfo.LuaScripts, "luaScripts", nameof(AddLuaScripts)); #if DEBUG luaScriptsInfo.LuaScripts.ForEach(ri => { if (!this.LuaScripts.Contains(ri)) { throw new ArgumentException( $"Package Service: tried to load the lua script resource for package {this.Package.Name} but it is not in the list for this package."); } }); #endif if (!_luaScriptService.Value.TryAddScriptFiles(luaScriptsInfo.LuaScripts)) { throw new ArgumentException( $"Package Service: unable to add lua files for package {this.Package.Name}! Aborting!"); } LuaScriptsLoaded = true; } finally { LoadingOperationsRunning = false; _operationsUsageLock.ExitReadLock(); } } public void LoadConfig( [NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo) { _operationsUsageLock.EnterReadLock(); LoadingOperationsRunning = true; try { if (IsDisposed) { throw new ObjectDisposedException($"This package service instance is disposed!"); } SanitationChecksCore(configsResourcesInfo, "config", nameof(LoadConfig)); SanitationChecksCore(configProfilesResourcesInfo, "config profiles", nameof(LoadConfig)); SanitationChecksEnumerable(configsResourcesInfo.Configs, "config", nameof(LoadConfig)); SanitationChecksEnumerable(configProfilesResourcesInfo.ConfigProfiles, "config profiles", nameof(LoadConfig)); #if DEBUG configsResourcesInfo.Configs.ForEach(ri => { if (!this.Configs.Contains(ri)) { throw new ArgumentException( $"Package Service: tried to load the configs resource for package {this.Package.Name} but it is not in the list for this package."); } }); configProfilesResourcesInfo.ConfigProfiles.ForEach(ri => { if (!this.ConfigProfiles.Contains(ri)) { throw new ArgumentException( $"Package Service: tried to load the localization resource for package {this.Package.Name} but it is not in the list for this package."); } }); #endif if (!_configService.Value.TryAddConfigs(configsResourcesInfo.Configs)) { throw new ArgumentException( $"Package Service: unable to add configs for package {this.Package.Name}! Aborting!"); } if (!_configService.Value.TryAddConfigsProfiles(configProfilesResourcesInfo.ConfigProfiles)) { throw new ArgumentException( $"Package Service: unable to add configs profiles for package {this.Package.Name}! Aborting!"); } ConfigsLoaded = true; } finally { LoadingOperationsRunning = false; _operationsUsageLock.ExitReadLock(); } } public void Dispose() { /* * Notes: we need to unload this package from services in the order that the services are dependent on each other. * Unloading Order: Lua Scripts > Assemblies > Config Profiles > Configs > Styles > Localizations */ _operationsUsageLock.EnterWriteLock(); try { if (this.Package is null) { _loggerService.LogError( $"Package Service: cannot Dispose of service as ContentPackage and info is not set!"); return; } if (this.ModConfigInfo is null) { _loggerService.LogError($"Package Service: cannot Dispose of service as ModConfigInfo is not loaded!"); return; } /* * To be graceful, we want to ensure that any async calls and other threads are allowed to be processed before we begin * disposal to reduce friction with other thread operations, so we release the lock and periodically check it * to see of other threads have finished operations before cleaning everything up. */ IsDisposed = true; // set stop flag, callers should handle exception cases Interlocked.MemoryBarrier(); //ensure cache states DateTime timeoutLimit = DateTime.Now.AddSeconds(10); while (LoadingOperationsRunning) { _operationsUsageLock.ExitWriteLock(); Thread.Sleep(1); _operationsUsageLock.EnterWriteLock(); if (timeoutLimit < DateTime.Now) { _loggerService.LogError($"Package Service: Dispose() time out reached while waiting for other operations. Continuing."); break; } } GC.SuppressFinalize(this); _luaScriptService.Value.RemoveScriptFiles(this.LuaScripts); _pluginService.Value.DisposePlugins(); _configService.Value.RemoveConfigsProfiles(this.ConfigProfiles); _configService.Value.RemoveConfigs(this.Configs); #if CLIENT _stylesService.Value.UnloadAllStyles(); #endif _localizationService.Value.Remove(this.Localizations); ModConfigInfo = null; Package = null; } catch { _loggerService.LogError($"Package Service: exception while running Dispose()."); throw; } finally { _operationsUsageLock.ExitWriteLock(); } } public void Reset() { _operationsUsageLock.EnterWriteLock(); try { if (this.Package is null) { _loggerService.LogError( $"Package Service: cannot Dispose of service as ContentPackage and info is not set!"); return; } if (this.ModConfigInfo is null) { _loggerService.LogError($"Package Service: cannot Dispose of service as ModConfigInfo is not loaded!"); return; } Interlocked.MemoryBarrier(); //ensure cache states DateTime timeoutLimit = DateTime.Now.AddSeconds(10); while (LoadingOperationsRunning) { _operationsUsageLock.ExitWriteLock(); Thread.Sleep(1); _operationsUsageLock.EnterWriteLock(); if (timeoutLimit < DateTime.Now) { _loggerService.LogError($"Package Service: Dispose() time out reached while waiting for other operations. Continuing."); break; } } if (LuaScriptsLoaded) { _luaScriptService.Value.RemoveScriptFiles(this.LuaScripts); LuaScriptsLoaded = false; } if (PluginsLoaded) { _pluginService.Value.DisposePlugins(); PluginsLoaded = false; } if (ConfigsLoaded) { _configService.Value.RemoveConfigsProfiles(this.ConfigProfiles); _configService.Value.RemoveConfigs(this.Configs); ConfigsLoaded = false; } if (LocalizationsLoaded) { _localizationService.Value.Remove(this.Localizations); LocalizationsLoaded = false; } } finally { _operationsUsageLock.ExitWriteLock(); } } #endregion #region INTERNAL private void SanitationChecksCore(object o, string resTypeInfoName, string callerName) { if (o is null) { _loggerService.LogError($"Package Service: {resTypeInfoName} resources list is null!"); throw new NullReferenceException($"Package Service: {resTypeInfoName} resources list is null!"); } if (this.Package is null) { _loggerService.LogError($"Package Service: package not set at {callerName}()!"); throw new NullReferenceException($"Package Service: package not set at {callerName}()!"); } } private void SanitationChecksEnumerable(ImmutableArray resourceInfos, string resTypeInfoName, string callerName) where T : IResourceInfo, IResourceCultureInfo, IPackageInfo, IPackageDependenciesInfo { // Check if list is empty. Nothing more to do. if (resourceInfos.IsDefaultOrEmpty) return; // Check if all resources in the list are registered to this package, throw if not. foreach (var resourceInfo in resourceInfos) { // ownership checks if (resourceInfo.OwnerPackage is null) { throw new ArgumentException($"Package Service: {resTypeInfoName} info for resource does not have a package name set! Run by {this.Package.Name}."); } if (resourceInfo.OwnerPackage != this.Package) { throw new ArgumentException( $"Package Service: {resTypeInfoName} info does not belong to this package! Owned by {resourceInfo.OwnerPackage.Name} but is run by {this.Package.Name}."); } // Check if external dependencies are loaded and if current environment is supported, throw if not if (resourceInfo.Dependencies.IsDefaultOrEmpty) continue; bool resourceMissing = false; resourceInfo.Dependencies.ForEach(pdi => { // for clarification: assemblies passed to the function should always be loaded. // optional assemblies should be filtered out before the list is sent. // left this as a reminder :) /*if (pdi.Optional) return;*/ if (!_packageManagementService.CheckDependencyLoaded(pdi)) { resourceMissing = true; _loggerService.LogError( $"Package Service: the following dependency for package {resourceInfo.OwnerPackage.Name} is not loaded: {pdi.DependencyPackage?.Name ?? (pdi.PackageName.IsNullOrWhiteSpace() ? pdi.SteamWorkshopId.ToString() : pdi.PackageName)}"); } }); if (!resourceMissing) { throw new FileLoadException($"Package Service: dependencies for package {resourceInfo.OwnerPackage.Name} are not loaded."); } // check runtime platform if (!_packageManagementService.CheckEnvironmentSupported(resourceInfo)) { throw new PlatformNotSupportedException($"Package service: the {resTypeInfoName} from {resourceInfo.OwnerPackage.Name} is not supported on this platform."); } // check local culture if (!_localizationService.Value.IsCurrentCultureSupported(resourceInfo)) { throw new PlatformNotSupportedException($"Package service: the {resTypeInfoName} from {resourceInfo.OwnerPackage.Name} is not supported in this culture."); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool GetThreadSafeBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) == 1; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetThreadSafeBool(ref int var, bool value) { if (value) { Interlocked.CompareExchange(ref var, 1, 0); } else { Interlocked.CompareExchange(ref var, 0, 1); } } #endregion }