using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Barotrauma.LuaCs.Configuration; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Events; using Barotrauma.LuaCs.Services; using Barotrauma.LuaCs.Services.Compatibility; using Barotrauma.LuaCs.Services.Processing; using Barotrauma.Networking; using FluentResults; using ImpromptuInterface; namespace Barotrauma { internal delegate void LuaCsMessageLogger(string message); internal delegate void LuaCsErrorHandler(Exception ex, LuaCsMessageOrigin origin); internal delegate void LuaCsExceptionHandler(Exception ex, LuaCsMessageOrigin origin); partial class LuaCsSetup : IDisposable, IEventScreenSelected, IEventAllPackageListChanged, IEventEnabledPackageListChanged { public LuaCsSetup() { // == startup _servicesProvider = new ServicesProvider(); RegisterServices(); ValidateLuaCsContent(); SubscribeToLuaCsEvents(); return; // == end // == helpers void RegisterServices() { _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); #if CLIENT _servicesProvider.RegisterServiceType(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); #endif _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); // TODO: ILocalizationService // TODO: IConfigService // TODO: INetworkingService // TODO: [Resource Converter/Parser Services] _servicesProvider.Compile(); } // Validates LuaCs assets in /Content are valid and ready to use. void ValidateLuaCsContent() { throw new NotImplementedException(); } } void SubscribeToLuaCsEvents() { EventService.Subscribe(this); // game state hook in EventService.Subscribe(this); EventService.Subscribe(this); } #region CONST_DEF #if SERVER public const bool IsServer = true; public const bool IsClient = false; #else public const bool IsServer = false; public const bool IsClient = true; #endif #endregion #region Services_ConfigVars /* * === Singleton Services */ private readonly IServicesProvider _servicesProvider; public PerformanceCounterService PerformanceCounter => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Performance counter service not found!"); public ILoggerService Logger => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Logger service not found!"); public IConfigService ConfigService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Config Manager service not found!"); public IPackageManagementService PackageManagementService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Package Manager service not found!"); public IPluginManagementService PluginManagementService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Plugin Manager service not found!"); public ILuaScriptManagementService LuaScriptManagementService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Lua Script Manager service not found!"); public ILocalizationService LocalizationService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Localization Manager service not found!"); public INetworkingService NetworkingService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Networking Manager service not found!"); public IEventService EventService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Networking Manager service not found!"); public LuaGame Game => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("LuaGame service not found!"); /* * === Config Vars */ /// /// Whether C# plugin code is enabled. /// public IConfigEntry IsCsEnabled { get; private set; } /// /// Whether mods marked as 'forced' or 'always load' should only be loaded if they're in the enabled mods list. /// public IConfigEntry TreatForcedModsAsNormal { get; private set; } /// /// Whether the lua script runner from Workshop package should be used over the in-built version. /// public IConfigEntry PreferToUseWorkshopLuaSetup { get; private set; } /// /// Whether the popup error GUI should be hidden/suppressed. /// public IConfigEntry DisableErrorGUIOverlay { get; private set; } /// /// Whether usernames are anonymized or show in logs. /// public IConfigEntry HideUserNamesInLogs { get; private set; } /// /// The SteamId of the Workshop LuaCs CPackage in use, if available. /// public IConfigEntry LuaForBarotraumaSteamId { get; private set; } /// /// The SteamId of the Workshop LuaCs CsForBarotrauma add-on, if available. /// public IConfigEntry CsForBarotraumaSteamId { get; private set; } /// /// Whether to (re)load all package assets when a lobby starts/code session begins. /// Intended for development use, or when packages are expected to change outside of External Updates (ie. Steam Workshop). /// public IConfigEntry ReloadPackagesOnLobbyStart { get; private set; } /** * == Ops Vars */ private RunState _runState; /// /// The current run state of all services managed by LuaCs. /// public RunState CurrentRunState => _runState; private bool CPacksParsed => CurrentRunState >= RunState.Parsed; private bool IsStaticAssetsLoaded => CurrentRunState >= RunState.Configuration; private bool IsCodeRunning => CurrentRunState >= RunState.Running; private readonly ConcurrentQueue _toLoad = new(); private readonly ConcurrentQueue _toUnload = new(); #endregion #region LegacyRedirects public ILuaCsHook Hook => this.EventService; #endregion public static bool IsRunningInsideWorkshop { get { #if SERVER return Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) != Directory.GetCurrentDirectory(); #else return false; // unnecessary but just keeps things clear that this is NOT for client stuff #endif } } private partial bool ShouldRunCs(); // TODO: Rework [Obsolete("Use IPluginManagementService::GetTypesByName()")] public static Type GetType(string typeName, bool throwOnError = false, bool ignoreCase = false) { throw new NotImplementedException(); //return AssemblyManager.GetTypesByName(typeName).FirstOrDefault((Type)null); } public static ContentPackage GetPackage(ContentPackageId id, bool fallbackToAll = true, bool useBackup = false) { foreach (ContentPackage package in ContentPackageManager.EnabledPackages.All) { if (package.UgcId.ValueEquals(id)) { return package; } } if (fallbackToAll) { foreach (ContentPackage package in ContentPackageManager.LocalPackages) { if (package.UgcId.ValueEquals(id)) { return package; } } foreach (ContentPackage package in ContentPackageManager.AllPackages) { if (package.UgcId.ValueEquals(id)) { return package; } } } if (useBackup && ContentPackageManager.EnabledPackages.BackupPackages.Regular != null) { foreach (ContentPackage package in ContentPackageManager.EnabledPackages.BackupPackages.Regular.Value) { if (package.UgcId.ValueEquals(id)) { return package; } } } return null; } public void Dispose() { try { SetRunState(RunState.Unloaded); } catch (Exception e) { Logger.LogError(e.Message); } try { DisposeLuaCsConfig(); PluginManagementService.Dispose(); LuaScriptManagementService.Dispose(); #if CLIENT StylesManagementService.Dispose(); #endif ConfigService.Dispose(); LocalizationService.Dispose(); PackageManagementService.Dispose(); // TODO: Add all missing services. //NetworkingService.Dispose(); EventService.Dispose(); _servicesProvider.DisposeAndReset(); } catch (Exception e) { Console.WriteLine(e); throw; } GC.SuppressFinalize(this); } /// /// Handles changes in game states tracked by screen changes. /// /// The new game screen. public partial void OnScreenSelected(Screen screen); public void OnAllPackageListChanged(IEnumerable corePackages, IEnumerable regularPackages) { UpdateLoadedPackagesList(); } public void OnEnabledPackageListChanged(CorePackage corePackage, IEnumerable regularPackages) { UpdateLoadedPackagesList(); } private void UpdateLoadedPackagesList() { var newPackSet = ContentPackageManager.AllPackages .Union(ContentPackageManager.EnabledPackages.All) .ToHashSet(); var currPackSet = PackageManagementService.GetAllLoadedPackages().ToHashSet(); var toAdd = newPackSet.Except(currPackSet); var toRemove = currPackSet.Except(newPackSet); foreach (var package in toAdd) _toLoad.Enqueue(package); foreach (var package in toRemove) _toUnload.Enqueue(package); ProcessPackagesListDifferences(); } void ProcessPackagesListDifferences() { if (IsCodeRunning) return; // no reason to do anything if we're fully unloaded. if (!CPacksParsed) { _toLoad.Clear(); _toUnload.Clear(); } while (_toUnload.TryDequeue(out var cp)) { LuaScriptManagementService.DisposePackageResources(cp); ConfigService.DisposeConfigsProfiles(cp); ConfigService.DisposeConfigs(cp); #if CLIENT StylesManagementService.DisposeStylesForPackage(cp); #endif LocalizationService.DisposePackage(cp); PackageManagementService.DisposePackageInfos(cp); } var ls = new List(); while (_toLoad.TryDequeue(out var cp)) { if (PackageManagementService.LoadPackageInfosAsync(cp).GetAwaiter().GetResult() is { IsFailed: true } failure) { Logger.LogError($"Failed to load package infos for {cp.Name}"); Logger.LogResults(failure); continue; } ls.Add(cp); } if (ls.Any()) { LoadStaticAssetsAsync(ls).GetAwaiter().GetResult(); } } void SetRunState(RunState runState) { if (CurrentRunState == runState) return; if (runState > CurrentRunState) { if (CurrentRunState < RunState.Parsed) LoadCurrentContentPackageInfos(); if (runState <= CurrentRunState) return; if (CurrentRunState < RunState.Configuration) LoadStaticAssets(); if (runState <= CurrentRunState) return; if (CurrentRunState < RunState.Running) RunScripts(); } else if (runState < CurrentRunState) { if (CurrentRunState >= RunState.Running) { StopScripts(); ProcessPackagesListDifferences(); _runState = RunState.Configuration; } if (runState >= CurrentRunState) return; if (CurrentRunState == RunState.Configuration) { UnloadStaticAssets(); _runState = RunState.Parsed; } if (runState >= CurrentRunState) return; if (CurrentRunState == RunState.Parsed) { UnloadContentPackageInfos(); _runState = RunState.Unloaded; } // we should be unloaded completely now | RunState.Unloaded } } void LoadCurrentContentPackageInfos() { if (CurrentRunState >= RunState.Parsed) return; // load core var result1 = PackageManagementService.LoadPackageInfosAsync(ContentPackageManager.VanillaCorePackage) .GetAwaiter().GetResult(); if (result1.IsFailed) { Logger.LogError($"Unable to load LuaCs CorePackage resources! Running in degraded mode."); Logger.LogResults(result1); } // load regular var list = ContentPackageManager.RegularPackages .Union(ContentPackageManager.EnabledPackages.All) .ToImmutableList(); LoadContentPackagesInfos(list); if (CurrentRunState < RunState.Parsed) _runState = RunState.Parsed; } void LoadContentPackagesInfos(IReadOnlyList packages) { var result2 = PackageManagementService.LoadPackagesInfosAsync(packages) .GetAwaiter().GetResult(); foreach (var entry in result2) { if (entry.Item2.IsSuccess) Logger.LogMessage($"Successfully parsed package: {entry.Item1.Name}"); else if (entry.Item2.IsFailed) Logger.LogResults(entry.Item2); } } void LoadStaticAssets() { if (CurrentRunState < RunState.Parsed) { throw new InvalidOperationException($"{nameof(LoadStaticAssets)} cannot load assets in the '{CurrentRunState}' state."); } if (CurrentRunState >= RunState.Configuration) return; while (_toUnload.TryDequeue(out var cp)) PackageManagementService.DisposePackageInfos(cp); LoadStaticAssetsAsync(PackageManagementService.GetAllLoadedPackages()).GetAwaiter().GetResult(); LoadLuaCsConfig(); if (CurrentRunState < RunState.Configuration) _runState = RunState.Configuration; } void LoadLuaCsConfig() { IsCsEnabled = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "IsCsEnabled") ?? throw new NullReferenceException($"{nameof(IsCsEnabled)} cannot be loaded."); TreatForcedModsAsNormal = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "TreatForcedModsAsNormal") ?? throw new NullReferenceException($"{nameof(TreatForcedModsAsNormal)} cannot be loaded."); DisableErrorGUIOverlay = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "DisableErrorGUIOverlay") ?? throw new NullReferenceException($"{nameof(DisableErrorGUIOverlay)} cannot be loaded."); HideUserNamesInLogs = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "HideUserNamesInLogs") ?? throw new NullReferenceException($"{nameof(HideUserNamesInLogs)} cannot be loaded."); LuaForBarotraumaSteamId = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "LuaForBarotraumaSteamId") ?? throw new NullReferenceException($"{nameof(LuaForBarotraumaSteamId)} cannot be loaded."); CsForBarotraumaSteamId = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "CsForBarotraumaSteamId") ?? throw new NullReferenceException($"{nameof(CsForBarotraumaSteamId)} cannot be loaded."); } void DisposeLuaCsConfig() { IsCsEnabled = null; TreatForcedModsAsNormal = null; DisableErrorGUIOverlay = null; HideUserNamesInLogs = null; LuaForBarotraumaSteamId = null; CsForBarotraumaSteamId = null; } async Task LoadStaticAssetsAsync(IReadOnlyList packages) { var locRes = ImmutableArray.Empty; var cfgRes = ImmutableArray.Empty; var cfpRes = ImmutableArray.Empty; var luaRes = ImmutableArray.Empty; #if CLIENT var styleRes = ImmutableArray.Empty; #endif var tasksBuilder = ImmutableArray.CreateBuilder(); //---- get resource infos tasksBuilder.AddRange(new Func(async () => { var res = await PackageManagementService.GetLocalizationsInfosAsync(packages); if (res.IsSuccess) locRes = res.Value.Localizations; if (res.Errors.Any()) ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), res.ToResult()); })(), new Func(async () => { var res = await PackageManagementService.GetConfigsInfosAsync(packages); if (res.IsSuccess) cfgRes = res.Value.Configs; if (res.Errors.Any()) ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), res.ToResult()); })(), new Func(async () => { var res = await PackageManagementService.GetConfigProfilesInfosAsync(packages); if (res.IsSuccess) cfpRes = res.Value.ConfigProfiles; if (res.Errors.Any()) ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), res.ToResult()); })(), new Func(async () => { var res = await PackageManagementService.GetLuaScriptsInfosAsync(packages); if (res.IsSuccess) luaRes = res.Value.LuaScripts; if (res.Errors.Any()) ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), res.ToResult()); })()); #if CLIENT tasksBuilder.Add(new Func(async () => { var res = await PackageManagementService.GetStylesInfosAsync(packages); if (res.IsSuccess) styleRes = res.Value.StylesResourceInfos; if (res.Errors.Any()) ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), res.ToResult()); })()); #endif await Task.WhenAll(tasksBuilder.MoveToImmutable()); tasksBuilder.Clear(); //---- load resources tasksBuilder.AddRange(new Func(async () => { var res = await ConfigService.LoadConfigsAsync(cfgRes); if (res.Errors.Any()) Logger.LogResults(res); res = await ConfigService.LoadConfigsProfilesAsync(cfpRes); if (res.Errors.Any()) Logger.LogResults(res); })(), new Func(async () => { var res = await LuaScriptManagementService.LoadScriptResourcesAsync(luaRes); if (res.Errors.Any()) Logger.LogResults(res); })()); #if CLIENT tasksBuilder.Add(new Func(async () => { var res = await StylesManagementService.LoadStylesAsync(styleRes); if (res.Errors.Any()) Logger.LogResults(res); })()); #endif // load localizations first if (!locRes.IsDefaultOrEmpty) { var res = await LocalizationService.LoadLocalizations(locRes); if (res.Errors.Any()) Logger.LogResults(res); } await Task.WhenAll(tasksBuilder.MoveToImmutable()); } void RunScripts() { if (!IsStaticAssetsLoaded) { throw new InvalidOperationException($"{nameof(RunScripts)} cannot load assets in the '{CurrentRunState}' state."); } if (CurrentRunState >= RunState.Running) return; if (ShouldRunCs()) { var asmRes = PackageManagementService.GetAssembliesInfos(PackageManagementService .GetAllLoadedPackages() .Where(ContentPackageManager.EnabledPackages.All.Contains) .ToList()); if (asmRes.IsFailed) { Logger.LogError($"{nameof(RunScripts)}: Errors will retrieving assembly resources, cannot load scripts!"); Logger.LogResults(asmRes.ToResult()); return; } var res = PluginManagementService.LoadAssemblyResources(asmRes.Value.Assemblies); if (res.IsFailed) { Logger.LogError($"{nameof(RunScripts)}: Failed to initialize scripts!"); Logger.LogResults(res.ToResult()); } else { if (res.Errors.Any()) Logger.LogResults(res.ToResult()); if (PluginManagementService.GetImplementingTypes() is {IsSuccess: true} types) { var typeInst = PluginManagementService.ActivateTypeInstances(types.Value, true, true); foreach (var loadRes in typeInst) { if (loadRes is { IsSuccess: true, Value: { Item2: { } pluginInstance } }) { EventService.Subscribe(pluginInstance); EventService.Subscribe(pluginInstance); EventService.Subscribe(pluginInstance); } else { Logger.LogResults(loadRes.ToResult()); } } EventService.PublishEvent(sub => sub.PreInitPatching()); EventService.PublishEvent(sub => sub.Initialize()); EventService.PublishEvent(sub => sub.OnLoadCompleted()); } } } //lua var luaRes = PackageManagementService.GetLuaScriptsInfos(PackageManagementService .GetAllLoadedPackages() .Where(ContentPackageManager.EnabledPackages.All.Contains) .ToList()); if (luaRes.IsFailed) { Logger.LogError($"{nameof(RunScripts)}: Failed to get enabled lua script resources!"); Logger.LogResults(luaRes.ToResult()); return; } if (luaRes.Errors.Any()) Logger.LogResults(luaRes.ToResult()); LuaScriptManagementService.ExecuteLoadedScripts(luaRes.Value.LuaScripts); if (CurrentRunState < RunState.Running) _runState = RunState.Running; } void UnloadContentPackageInfos() { if (IsStaticAssetsLoaded) { throw new InvalidOperationException($"{nameof(UnloadStaticAssets)}: Cannot unload static assets when the current run state is {CurrentRunState}."); } PackageManagementService.Reset(); _toUnload.Clear(); } void UnloadStaticAssets() { if (IsCodeRunning) { throw new InvalidOperationException($"{nameof(UnloadStaticAssets)}: Cannot unload static assets when the current run state is {CurrentRunState}."); } PluginManagementService.Reset(); LuaScriptManagementService.Reset(); ConfigService.Reset(); #if CLIENT StylesManagementService.Reset(); #endif LocalizationService.Reset(); if (CurrentRunState >= RunState.Configuration) { _runState = RunState.Parsed; } } void StopScripts() { EventService.ClearAllSubscribers(); LuaScriptManagementService.UnloadActiveScripts(); PluginManagementService.UnloadAllAssemblyResources(); SubscribeToLuaCsEvents(); if (IsCodeRunning) { _runState = RunState.Configuration; } } } /// /// Specifies the current run state of the LuaCs Modding System. /// [Important]Enum State values ordering must be in the form of (lower state) === (higher state) /// public enum RunState : byte { Unloaded = 0, // No asset data loaded. Parsed, // CPacks' ResourceInfos are parsed. Configuration, // localization and configuration assets loaded. Running // all assets loaded, code running. } }