diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/DisplayableData.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/DisplayableData.cs new file mode 100644 index 000000000..64649068e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/DisplayableData.cs @@ -0,0 +1,16 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs.Configuration; + +public class DisplayableData : IDisplayableData +{ + public string Name { get; private set; } + public string ModName { get; private set; } + public string DisplayName { get; private set; } + public string DisplayModName { get; private set; } + public string DisplayCategory { get; private set; } + public string Tooltip { get; private set; } + public string ImageIcon { get; private set; } + public Point IconResolution { get; private set; } + public bool ShowWhenNotLoaded { get; private set; } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IConfigDisplayDefinitions.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IConfigDisplayDefinitions.cs new file mode 100644 index 000000000..3c5fbc5f7 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IConfigDisplayDefinitions.cs @@ -0,0 +1,6 @@ +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Configuration; + +public partial interface IConfigBase : IDisplayableData, IDisplayableInitialize { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs new file mode 100644 index 000000000..69449ed6b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs @@ -0,0 +1,66 @@ +using System.Numerics; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs.Configuration; + +/// +/// Contains the Display Data for use with Menus. +/// +public interface IDisplayableData +{ + /// + /// Internal name of the instance. + /// + string Name { get; } + /// + /// Internal mod name of the instance. ContentPackage name will be used by default. + /// + string ModName { get; } + /// + /// The name to display in GUIs and Menus. + /// + string DisplayName { get; } + /// + /// The mod name to display in GUIs and Menus. + /// + string DisplayModName { get; } + /// + /// Category this instance falls under. Used by menus when filtering by category. + /// + string DisplayCategory { get; } + /// + /// The tooltip shown on hover. + /// + string Tooltip { get; } + /// + /// The fully qualified filepath to the image icon for this config. + /// + string ImageIcon { get; } + /// + /// Required if ImageIcon is set. X,Y resolution of the image. + /// + Point IconResolution { get; } + /// + /// Whether to show the entry in the menu when not loaded. + /// + bool ShowWhenNotLoaded { get; } +} + +public interface IDisplayableInitialize +{ + void Initialize(IDisplayableData values); + + // copy this as needed + /*public void Initialize(IDisplayableData values) + { + this.Name = values.Name; + this.ModName = values.ModName; + this.DisplayName = values.DisplayName; + this.DisplayModName = values.DisplayModName; + this.DisplayCategory = values.DisplayCategory; + this.Tooltip = values.Tooltip; + this.ImageIcon = values.ImageIcon; + this.IconResolution = values.IconResolution; + this.ShowWhenNotLoaded = values.ShowWhenNotLoaded; + }*/ +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs new file mode 100644 index 000000000..d46048703 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs @@ -0,0 +1,21 @@ +using System.Collections.Immutable; +using System.Globalization; + +namespace Barotrauma.LuaCs.Data; + +public partial record ModConfigInfo : IModConfigInfo +{ + public ImmutableArray StylesResourceInfos { get; init; } +} + +public record StylesResourceInfo : IStylesResourceInfo +{ + public Platform SupportedPlatforms { get; init; } + public Target SupportedTargets { get; init; } + public int LoadPriority { get; init; } + public ImmutableArray FilePaths { get; init; } + public bool Optional { get; init; } + public ImmutableArray SupportedCultures { get; init; } + public string InternalName { get; init; } + public ImmutableArray Dependencies { get; init; } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs new file mode 100644 index 000000000..45d563fa7 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs @@ -0,0 +1,5 @@ +using Barotrauma.LuaCs.Configuration; + +namespace Barotrauma.LuaCs.Data; + +public partial interface IConfigInfo : IDisplayableData { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs new file mode 100644 index 000000000..14974ac8c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -0,0 +1,15 @@ +using System.Collections.Immutable; + +namespace Barotrauma.LuaCs.Data; + +public partial interface IModConfigInfo : IStylesResourcesInfo { } + +public interface IStylesResourceInfo : IResourceInfo, IResourceCultureInfo, ILoadableResourceInfo, IPackageDependenciesInfo { } + +public interface IStylesResourcesInfo +{ + /// + /// Collection of loadable styles data. + /// + ImmutableArray StylesResourceInfos { get; } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IStylesInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IStylesInfo.cs new file mode 100644 index 000000000..b3e2a456b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IStylesInfo.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Data; + +public interface IStylesInfo +{ + +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IClientLoggerService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IClientLoggerService.cs new file mode 100644 index 000000000..21cc88352 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IClientLoggerService.cs @@ -0,0 +1,7 @@ +namespace Barotrauma.LuaCs.Services; + +public interface IClientLoggerService : IService +{ + void AddToGUIUpdateList(); + void ShowErrorOverlay(string message, float time = 5f, float duration = 1.5f); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs new file mode 100644 index 000000000..cd0b38829 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs @@ -0,0 +1,51 @@ +namespace Barotrauma.LuaCs.Services; + +// TODO: Rework interface to support resource infos. +/// +/// Loads XML Style assets from the given content package. +/// +public interface IStylesService : IService +{ + /// + /// Tries to load the styles file for the given contentpackage and path into a new UIStylesProcessor instance. + /// + /// + /// + /// + bool TryLoadStylesFile(ContentPackage package, ContentPath path); + /// + /// Unloads all styles assets and UIStyleProcessor instances. + /// + void UnloadAllStyles(); + + /// + /// Tries to the get the font asset by xml asset name, returns null on failure. + /// + /// XML Name of the asset. + /// The asset or null if none are found. + GUIFont GetFont(string fontName); + /// + /// Tries to the get the sprite asset by xml asset name, returns null on failure. + /// + /// XML Name of the asset. + /// The asset or null if none are found. + GUISprite GetSprite(string spriteName); + /// + /// Tries to the get the sprite sheet asset by xml asset name, returns null on failure. + /// + /// XML Name of the asset. + /// The asset or null if none are found. + GUISpriteSheet GetSpriteSheet(string spriteSheetName); + /// + /// Tries to the get the cursor asset by xml asset name, returns null on failure. + /// + /// XML Name of the asset. + /// The asset or null if none are found. + GUICursor GetCursor(string cursorName); + /// + /// Tries to the get the color asset by xml asset name, returns null on failure. + /// + /// XML Name of the asset. + /// The asset or null if none are found. + GUIColor GetColor(string colorName); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/LoggerService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/LoggerService.cs new file mode 100644 index 000000000..aee2efabb --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/LoggerService.cs @@ -0,0 +1,50 @@ +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs.Services; + +public partial class LoggerService : ILoggerService, IClientLoggerService +{ + private GUIFrame _overlayFrame; + private GUITextBlock _textBlock; + private double _showTimer = 0; + + + private void CreateOverlay(string message) + { + _overlayFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.03f), null), null, new Color(50, 50, 50, 100)) + { + CanBeFocused = false + }; + + GUILayoutGroup layout = + new GUILayoutGroup( + new RectTransform(new Vector2(0.8f, 0.8f), _overlayFrame.RectTransform, Anchor.CenterLeft), false, + Anchor.Center); + + _textBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0f), layout.RectTransform), message); + _overlayFrame.RectTransform.MinSize = new Point((int)(_textBlock.TextSize.X * 1.2), 0); + + layout.Recalculate(); + } + + public void AddToGUIUpdateList() + { + if (_overlayFrame != null && Timing.TotalTime <= _showTimer) + { + _overlayFrame.AddToGUIUpdateList(); + } + } + + public void ShowErrorOverlay(string message, float time = 5f, float duration = 1.5f) + { + if (Timing.TotalTime <= _showTimer) + { + return; + } + + CreateOverlay(message); + + _overlayFrame.Flash(Color.Red, duration, true); + _showTimer = Timing.TotalTime + time; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs new file mode 100644 index 000000000..a079ef1ec --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Services.Processing; + +namespace Barotrauma.LuaCs.Services; + +public partial class PackageService : IStylesResourcesInfo +{ + private readonly Lazy _stylesService; + + public PackageService( + Lazy converterService, + Lazy legacyConfigService, + Lazy luaScriptService, + Lazy localizationService, + Lazy pluginService, + Lazy stylesService, + Lazy configService, + IPackageManagementService packageManagementService, + IStorageService storageService, + ILoggerService loggerService) + { + _modConfigConverterService = converterService; + _legacyConfigService = legacyConfigService; + _luaScriptService = luaScriptService; + _localizationService = localizationService; + _pluginService = pluginService; + _stylesService = stylesService; + _configService = configService; + _packageManagementService = packageManagementService; + _storageService = storageService; + _loggerService = loggerService; + } + + public ImmutableArray StylesResourceInfos => ModConfigInfo?.StylesResourceInfos ?? ImmutableArray.Empty; + + public void LoadStyles([NotNull]IStylesResourcesInfo stylesInfo) + { + throw new NotImplementedException(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/IClientParserDefinitions.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/IClientParserDefinitions.cs new file mode 100644 index 000000000..8c81c7fb5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/IClientParserDefinitions.cs @@ -0,0 +1,9 @@ +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services.Processing; + +#region XmlToResourceParsers +public interface IXmlStylesToResConverterService : IXmlResourceConverterService { } + +#endregion diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs new file mode 100644 index 000000000..f859dcd09 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.LuaCs.Services; + +public class StylesService : IStylesService +{ + private readonly Dictionary _loadedProcessors = new(); + private readonly IStorageService _storageService; + private readonly ILoggerService _loggerService; + + public StylesService(IStorageService storageService, ILoggerService loggerService) + { + _storageService = storageService; + _loggerService = loggerService; + } + + public bool TryLoadStylesFile(ContentPackage package, ContentPath path) + { + //check if file already in dict + if (_loadedProcessors.ContainsKey(path.FullPath)) + { + return true; + } + //check if file exists + if (_storageService.FileExists(path.FullPath)) + { + try + { + var styleProcessor = new UIStyleProcessor(package, path); + styleProcessor.LoadFile(); + _loadedProcessors.Add(path.FullPath, styleProcessor); + } + catch (InvalidDataException exception) + { + _loggerService.LogError($"XmlAssetService.TryLoadStylesFile failed for ContentPackage {package.Name}: Exception: {exception.Message}"); + return false; + } + + return true; + } + + return false; + } + + public void UnloadAllStyles() + { + if (NoProcessorsLoaded) + return; + + foreach (var processor in _loadedProcessors) + { + processor.Value.UnloadFile(); + } + _loadedProcessors.Clear(); + } + + public GUIFont GetFont(string fontName) + { + if (NoProcessorsLoaded) + return null; + foreach (var processor in _loadedProcessors.Values) + { + if (processor.Fonts.TryGetValue(fontName, out var asset)) + return asset; + } + + return null; + } + + public GUISprite GetSprite(string spriteName) + { + if (NoProcessorsLoaded) + return null; + foreach (var processor in _loadedProcessors.Values) + { + if (processor.Sprites.TryGetValue(spriteName, out var asset)) + return asset; + } + + return null; + } + + public GUISpriteSheet GetSpriteSheet(string spriteSheetName) + { + if (NoProcessorsLoaded) + return null; + foreach (var processor in _loadedProcessors.Values) + { + if (processor.SpriteSheets.TryGetValue(spriteSheetName, out var asset)) + return asset; + } + + return null; + } + + public GUICursor GetCursor(string cursorName) + { + if (NoProcessorsLoaded) + return null; + foreach (var processor in _loadedProcessors.Values) + { + if (processor.Cursors.TryGetValue(cursorName, out var asset)) + return asset; + } + + return null; + } + + public GUIColor GetColor(string colorName) + { + if (NoProcessorsLoaded) + return null; + foreach (var processor in _loadedProcessors.Values) + { + if (processor.Colors.TryGetValue(colorName, out var asset)) + return asset; + } + + return null; + } + + private bool NoProcessorsLoaded => _loadedProcessors.Count < 1; + + public void Dispose() + { + UnloadAllStyles(); + GC.SuppressFinalize(this); + } + + public void Reset() + { + UnloadAllStyles(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/UIStyleProcessor.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/UIStyleProcessor.cs new file mode 100644 index 000000000..b90276e7a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/UIStyleProcessor.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma.LuaCs.Services; + +public class UIStyleProcessor : HashlessFile +{ + private readonly UIStyleFile _fake; + public readonly Dictionary Fonts = new(); + public readonly Dictionary Sprites = new(); + public readonly Dictionary SpriteSheets = new(); + public readonly Dictionary Cursors = new(); + public readonly Dictionary Colors = new(); + + public UIStyleProcessor(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) + { + _fake = new UIStyleFile(contentPackage, path); + } + + public override void LoadFile() + { + var element = XMLExtensions.TryLoadXml(path: Path)?.Root?.FromPackage(ContentPackage); + if (element is null) + throw new InvalidDataException($"UIStyleProcessor: Failed to load UI style file: {Path}"); + + var styleElement = element.Name.LocalName.ToLowerInvariant() == "style" ? element : element.GetChildElement("style"); + if (styleElement is null) + throw new InvalidDataException($"UIStyleProcessor: no 'style' XmlElement found in file: {Path}"); + + var childElements = styleElement.GetChildElements("Font"); + if (childElements is not null) + AddToList(Fonts, childElements, _fake); + + childElements = styleElement.GetChildElements("Sprite"); + if (childElements is not null) + AddToList(Sprites, childElements, _fake); + + childElements = styleElement.GetChildElements("Spritesheet"); + if (childElements is not null) + AddToList(SpriteSheets, childElements, _fake); + + childElements = styleElement.GetChildElements("Cursor"); + if (childElements is not null) + AddToList(Cursors, childElements, _fake); + + childElements = styleElement.GetChildElements("Color"); + if (childElements is not null) + AddToList(Colors, childElements, _fake); + + + void AddToList(Dictionary dict, IEnumerable ele, UIStyleFile file) where T1 : GUISelector where T2 : GUIPrefab + { + foreach (ContentXElement prefabElement in ele) + { + string name = prefabElement.GetAttributeString("name", string.Empty); + if (name != string.Empty) + { + var prefab = (T2)Activator.CreateInstance(typeof(T2), new object[]{ prefabElement, file })!; + if (!dict.ContainsKey(name)) + dict[name] = (T1)Activator.CreateInstance(typeof(T1), new object[] { name })!; + dict[name].Prefabs.Add(prefab, false); + } + } + } + } + + public override void UnloadFile() + { + Fonts.Values.ForEach(p => p.Prefabs.RemoveByFile(_fake)); + Sprites.Values.ForEach(p => p.Prefabs.RemoveByFile(_fake)); + SpriteSheets.Values.ForEach(p => p.Prefabs.RemoveByFile(_fake)); + Cursors.Values.ForEach(p => p.Prefabs.RemoveByFile(_fake)); + Colors.Values.ForEach(p => p.Prefabs.RemoveByFile(_fake)); + + Fonts.Clear(); + Sprites.Clear(); + SpriteSheets.Clear(); + Cursors.Clear(); + Colors.Clear(); + } + + public override void Sort() + { + Fonts.Values.ForEach(p => p.Prefabs.Sort()); + Sprites.Values.ForEach(p => p.Prefabs.Sort()); + SpriteSheets.Values.ForEach(p => p.Prefabs.Sort()); + Cursors.Values.ForEach(p => p.Prefabs.Sort()); + Colors.Values.ForEach(p => p.Prefabs.Sort()); + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs new file mode 100644 index 000000000..4b97459b1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Barotrauma.LuaCs.Services.Processing; + +// ReSharper disable once CheckNamespace +namespace Barotrauma.LuaCs.Services; + +public partial class PackageService +{ + public PackageService( + Lazy converterService, + Lazy legacyConfigService, + Lazy luaScriptService, + Lazy localizationService, + Lazy pluginService, + Lazy configService, + IPackageManagementService packageManagementService, + IStorageService storageService, + ILoggerService loggerService) + { + _modConfigConverterService = converterService; + _legacyConfigService = legacyConfigService; + _luaScriptService = luaScriptService; + _localizationService = localizationService; + _pluginService = pluginService; + _configService = configService; + _packageManagementService = packageManagementService; + _storageService = storageService; + _loggerService = loggerService; + } +} diff --git a/Barotrauma/BarotraumaShared/Luatrauma.props b/Barotrauma/BarotraumaShared/Luatrauma.props index cf9a48490..62c907748 100644 --- a/Barotrauma/BarotraumaShared/Luatrauma.props +++ b/Barotrauma/BarotraumaShared/Luatrauma.props @@ -5,6 +5,8 @@ + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs new file mode 100644 index 000000000..37e076506 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs @@ -0,0 +1,21 @@ +using System; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Configuration; + +public partial interface IConfigBase : IVarId +{ + bool IsInitialized { get; } + string GetValue(); + bool TrySetValue(string value); + bool IsAssignable(string value); + Type GetValueType(); + void Initialize(IVarId id, string defaultValue); +} + +public interface IVarId +{ + Guid InstanceId { get; } + string InternalName { get; } + ContentPackage OwnerPackage { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs new file mode 100644 index 000000000..30ba129c6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs @@ -0,0 +1,12 @@ +using System; +using Barotrauma.LuaCs.Networking; + +namespace Barotrauma.LuaCs.Configuration; + +public interface IConfigEntry : IConfigBase, INetVar where T : IConvertible, IEquatable +{ + T Value { get; } + bool TrySetValue(T value); + bool IsAssignable(T value); + void Initialize(IVarId id, T defaultValue); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigList.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigList.cs new file mode 100644 index 000000000..226ddbe08 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigList.cs @@ -0,0 +1,8 @@ +using Barotrauma.LuaCs.Networking; + +namespace Barotrauma.LuaCs.Configuration; + +public interface IConfigList : IConfigBase, INetVar +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigRangeEntry.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigRangeEntry.cs new file mode 100644 index 000000000..571bb1d3f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigRangeEntry.cs @@ -0,0 +1,13 @@ +using System; + +namespace Barotrauma.LuaCs.Configuration; + +public interface IConfigRangeEntry : IConfigEntry where T : IConvertible, IEquatable +{ + T MinValue { get; } + T MaxValue { get; } + + int GetStepCount(); + float GetRangeMin(); + float GetRangeMax(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs new file mode 100644 index 000000000..7362f990f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace Barotrauma.LuaCs.Data; + +#region ModConfigurationInfo + +public partial record ModConfigInfo : IModConfigInfo +{ + public ContentPackage Package { get; init; } + public string PackageName { get; init; } + public TargetRunMode RunModes { get; init; } + + public ImmutableArray SupportedCultures { get; init; } + public ImmutableArray Assemblies { get; init; } + public ImmutableArray Localizations { get; init; } + public ImmutableArray LuaScripts { get; init; } + public ImmutableArray Configs { get; init; } + public ImmutableArray ConfigProfiles { get; init; } +} + +#endregion + +#region DataContracts + +public record AssemblyResourceInfo : IAssemblyResourceInfo +{ + public ContentPackage OwnerPackage { get; init; } + public string FriendlyName { get; init; } + public bool IsScript { get; init; } + public string InternalName { get; init; } + public bool LazyLoad { get; init; } + public Platform SupportedPlatforms { get; init; } + public Target SupportedTargets { get; init; } + public int LoadPriority { get; init; } + public ImmutableArray FilePaths { get; init; } + public ImmutableArray SupportedCultures { get; init; } + public ImmutableArray Dependencies { get; init; } + public bool Optional { get; init; } +} + +public record DependencyInfo : IPackageDependencyInfo +{ + public ContentPackage OwnerPackage { get; init; } + public string FolderPath { get; init; } + public string PackageName { get; init; } + public ulong SteamWorkshopId { get; init; } + public ContentPackage DependencyPackage { get; init; } +} + +public record LocalizationResourceInfo : ILocalizationResourceInfo +{ + public ContentPackage OwnerPackage { get; init; } + public CultureInfo TargetCulture { get; init; } + public Platform SupportedPlatforms { get; init; } + public Target SupportedTargets { get; init; } + public int LoadPriority { get; init; } + public ImmutableArray FilePaths { get; init; } + public ImmutableArray SupportedCultures { get; init; } + public ImmutableArray Dependencies { get; init; } + public bool Optional { get; init; } +} + +public readonly struct LuaScriptResourceInfo : ILuaResourceInfo +{ + public ContentPackage OwnerPackage { get; init; } + public Platform SupportedPlatforms { get; init; } + public Target SupportedTargets { get; init; } + public int LoadPriority { get; init; } + public ImmutableArray FilePaths { get; init; } + public ImmutableArray SupportedCultures { get; init; } + public ImmutableArray Dependencies { get; init; } + public bool Optional { get; init; } + public string InternalName { get; init; } + public bool LazyLoad { get; init; } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs new file mode 100644 index 000000000..42d702b7f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs @@ -0,0 +1,27 @@ +using System; + +namespace Barotrauma.LuaCs.Data; + +[Flags] +public enum Platform +{ + Linux=0x1, + OSX=0x2, + Windows=0x4 +} + +[Flags] +public enum Target +{ + Client=0x1, + Server=0x2 +} + +[Flags] +public enum TargetRunMode +{ + ClientEnabled = 0x1, + ClientAlways = 0x2, + ServerEnabled = 0x4, + ServerAlways = 0x8 +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs new file mode 100644 index 000000000..e970aa9ce --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Globalization; + +namespace Barotrauma.LuaCs.Data; + +public interface IPlatformInfo +{ + /// + /// Platforms that these localization files should be loaded for. + /// + [Required] + Platform SupportedPlatforms { get; } + + /// + /// Targets that these localization files should be loaded for. + /// + [Required] + Target SupportedTargets { get; } +} + + +/// +/// Which package does the following data belong to? +/// +public interface IPackageInfo +{ + ContentPackage OwnerPackage { get; } +} + + +/// +/// ResourceInfos contain metadata about a resource. +/// +public interface IResourceInfo : IPlatformInfo +{ + /// + /// [Optional] + /// Allows you to specify the loading order for all assets of the same type (ie. styles, assemblies, etc.). + /// + int LoadPriority { get; } + + /// + /// Resource absolute file paths. + /// + [Required] + ImmutableArray FilePaths { get; } + + /// + /// Marks this resource as optional (ie. Cross-CP content). Setting this to true will allow the dependency system to + /// try and order the loading but not fail if it runs into circular dependency issues. + /// + bool Optional { get; } +} + +/// +/// Information about supported cultures. It is intended to be ignored if the array is ImmutableArray.Empty . +/// +public interface IResourceCultureInfo +{ + /// + /// List of supported cultures by this resource. + /// + ImmutableArray SupportedCultures { get; } +} + + +public interface ILoadableResourceInfo +{ + /// + /// [UNIQUE] The name that will be used when trying to reference this resource for execution or loading. + /// + [Required] + public string InternalName { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs new file mode 100644 index 000000000..c56cb5aa1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs @@ -0,0 +1,26 @@ +using System; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Data; + +// TODO: Finish +public partial interface IConfigInfo +{ + string Name { get; } + string PackageName { get; } + ConfigDataType Type { get; } + string DefaultValue { get; } + ClientPermissions RequiredPermissions { get; } +} + +public enum ConfigDataType +{ + Boolean, Int32, Int64, Single, Double, String, + Color, Vector2, Vector3, List, + RangeInt32, RangeSingle, ControlInput +} + +public enum NetSync +{ + None, TwoWay, ServerAuthority, ClientOneWay +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs new file mode 100644 index 000000000..7be1db56c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Data; + +public interface IConfigProfileInfo +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs new file mode 100644 index 000000000..c05887e5a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Data; + +public interface ILocalizationInfo +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs new file mode 100644 index 000000000..8a324f870 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs @@ -0,0 +1,14 @@ +using System.Collections.Immutable; + +namespace Barotrauma.LuaCs.Data; + +public partial interface IModConfigInfo : IResourceCultureInfo, IAssembliesResourcesInfo, + ILocalizationsResourcesInfo, ILuaScriptsResourcesInfo, IConfigsResourcesInfo, + IConfigProfilesResourcesInfo +{ + // package info + ContentPackage Package { get; } + string PackageName { get; } + // configuration + TargetRunMode RunModes { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IPackageDependencyInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IPackageDependencyInfo.cs new file mode 100644 index 000000000..ef96428f1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IPackageDependencyInfo.cs @@ -0,0 +1,31 @@ +using System.Collections.Immutable; + +namespace Barotrauma.LuaCs.Data; + +public interface IPackageDependencyInfo : IPackageInfo +{ + /// + /// Root folder of the content package. + /// + public string FolderPath { get; } + /// + /// Name of the package. + /// + public string PackageName { get; } + /// + /// Steam ID of the package. + /// + public ulong SteamWorkshopId { get; } + /// + /// The dependency package, if found in the ALL Packages List. + /// + public ContentPackage DependencyPackage { get; } +} + +public interface IPackageDependenciesInfo +{ + /// + /// List of required packages. + /// + ImmutableArray Dependencies { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs new file mode 100644 index 000000000..8b6f28e9b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; + +namespace Barotrauma.LuaCs.Data; + +public interface IConfigResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IPackageInfo { } +public interface IConfigProfileResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IPackageInfo { } +public interface ILocalizationResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IPackageInfo { } +/// +/// Represents loadable Lua files. +/// +public interface ILuaResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, ILoadableResourceInfo, IPackageInfo { } +public interface IAssemblyResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, ILoadableResourceInfo, IPackageInfo +{ + /// + /// The friendly name of the assembly. Script files belonging to the same assembly should all have the same name. + /// Legacy scripts will all be given the sanitized name of the Content Package they belong to. + /// + public string FriendlyName { get; } + /// + /// Is this entry referring to a script file collection. + /// + public bool IsScript { get; } +} + + +#region Collections + +public interface IAssembliesResourcesInfo +{ + ImmutableArray Assemblies { get; } +} + +public interface ILocalizationsResourcesInfo +{ + ImmutableArray Localizations { get; } +} + +public interface ILuaScriptsResourcesInfo +{ + ImmutableArray LuaScripts { get; } +} + +public interface IConfigsResourcesInfo +{ + ImmutableArray Configs { get; } +} + +public interface IConfigProfilesResourcesInfo +{ + ImmutableArray ConfigProfiles { get; } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IRunConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IRunConfig.cs new file mode 100644 index 000000000..a0da6263b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IRunConfig.cs @@ -0,0 +1,18 @@ +namespace Barotrauma.LuaCs.Data; + +/// +/// Legacy data contract for the old run configuration system. Should be deprecated +/// once no longer needed. +/// +public interface IRunConfig +{ + bool UseNonPublicizedAssemblies { get; } + bool AutoGenerated { get; } + bool UseInternalAssemblyName { get; } + string Client { get; } + string Server { get; } + + bool IsForced(); + bool IsStandard(); + bool IsForcedOrStandard(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsLogger.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsLogger.cs index a8816c595..e854a8e04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsLogger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsLogger.cs @@ -19,7 +19,7 @@ namespace Barotrauma #if SERVER private const string LogPrefix = "SV"; - private const int NetMaxLength = 1024; + private const int NetMaxLength = 1024; // character limit of vanilla Barotrauma's chat system. private const int NetMaxMessages = 60; // This is used so its possible to call logging functions inside the serverLog diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index fb2ef00f1..4a479c11f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -11,6 +11,7 @@ using MoonSharp.VsCodeDebugger; using System.Reflection; using System.Runtime.Loader; using System.Xml.Linq; +using Barotrauma.LuaCs.Services; using Barotrauma.Networking; namespace Barotrauma @@ -99,7 +100,7 @@ namespace Barotrauma public CsPackageManager PluginPackageManager => _pluginPackageManager ??= new CsPackageManager(AssemblyManager, this); public LuaCsModStore ModStore { get; private set; } - private LuaRequire require { get; set; } + private LuaRequire Require { get; set; } public LuaCsSetupConfig Config { get; private set; } public MoonSharpVsCodeDebugServer DebugServer { get; private set; } public bool IsInitialized { get; private set; } @@ -320,6 +321,7 @@ namespace Barotrauma #endif } + public void Stop() { PluginPackageManager.UnloadPlugins(); @@ -395,7 +397,7 @@ namespace Barotrauma Lua.Options.CheckThreadAccess = false; Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; }; - require = new LuaRequire(Lua); + Require = new LuaRequire(Lua); Game = new LuaGame(); Networking = new LuaCsNetworking(); @@ -428,7 +430,7 @@ namespace Barotrauma Lua.Globals["dofile"] = (Func)DoFile; Lua.Globals["loadfile"] = (Func)LoadFile; - Lua.Globals["require"] = (Func)require.Require; + Lua.Globals["require"] = (Func)Require.Require; Lua.Globals["dostring"] = (Func)Lua.DoString; Lua.Globals["load"] = (Func)Lua.LoadString; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetCallback.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetCallback.cs new file mode 100644 index 000000000..f498da372 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetCallback.cs @@ -0,0 +1,15 @@ +using System; + +namespace Barotrauma.LuaCs.Networking; + +public partial interface INetCallback +{ + public ushort CallbackId { get; } +} + +#if SERVER +public partial interface INetCallback +{ + +} +#endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs new file mode 100644 index 000000000..b4a7d5885 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs @@ -0,0 +1,25 @@ +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Networking; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Networking; + +public interface INetVar : IVarId +{ + /// + /// Synchronized network id, uninitialized if value is zero/0. Used by Networking service. + /// + ushort NetId { get; } + /// + /// Synchronization type + /// + NetSync SyncType { get; } + /// + /// Permissions needed by clients to send net-events or receive net messages. + /// + ClientPermissions WritePermissions { get; } + void ReadNetMessage(INetReadMessage message); + void WriteNetMessage(INetWriteMessage message); + void Initialize(ushort netId, NetSync syncMode, ClientPermissions writePermissions); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/NetInterfaceCompat.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/NetInterfaceCompat.cs new file mode 100644 index 000000000..c68b318ae --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/NetInterfaceCompat.cs @@ -0,0 +1,151 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs.Networking; + +#region Wrapper-IWriteMessage + +/// +/// Literally just exists because Barotrauma.IWriteMessage is internal only. +/// +public interface INetWriteMessage +{ + internal IWriteMessage Message { get; } + internal void SetMessage(IWriteMessage msg); + + void WriteBoolean(bool val) => Message.WriteBoolean(val); + + void WritePadBits() => Message.WritePadBits(); + + void WriteByte(byte val) => Message.WriteByte(val); + + void WriteInt16(short val) => Message.WriteInt16(val); + + void WriteUInt16(ushort val) => Message.WriteUInt16(val); + + void WriteInt32(int val) => Message.WriteInt32(val); + + void WriteUInt32(uint val) => Message.WriteUInt32(val); + + void WriteInt64(long val) => Message.WriteInt64(val); + + void WriteUInt64(ulong val) => Message.WriteUInt64(val); + + void WriteSingle(float val) => Message.WriteSingle(val); + + void WriteDouble(double val) => Message.WriteDouble(val); + + void WriteColorR8G8B8(Color val) => Message.WriteColorR8G8B8(val); + + void WriteColorR8G8B8A8(Color val) => Message.WriteColorR8G8B8A8(val); + + void WriteVariableUInt32(uint val) => Message.WriteVariableUInt32(val); + + void WriteString(string val) => Message.WriteString(val); + + void WriteIdentifier(Identifier val) => Message.WriteIdentifier(val); + + void WriteRangedInteger(int val, int min, int max) => Message.WriteRangedInteger(val, min, max); + + void WriteRangedSingle(float val, float min, float max, int bitCount) => + Message.WriteRangedSingle(val, min, max, bitCount); + + void WriteBytes(byte[] val, int startIndex, int length) => Message.WriteBytes(val, startIndex, length); + + byte[] PrepareForSending(bool compressPastThreshold, out bool isCompressed, out int outLength) => + Message.PrepareForSending(compressPastThreshold, out isCompressed, out outLength); + + int BitPosition + { + get => Message.BitPosition; + set => Message.BitPosition = value; + } + + int BytePosition => Message.BytePosition; + + byte[] Buffer => Message.Buffer; + + int LengthBits + { + get => Message.LengthBits; + set => Message.LengthBits = value; + } + + int LengthBytes => Message.LengthBytes; +} + +#endregion + +#region Wrapper-IReadMessage + +/// +/// Literally just exists because Barotrauma.IReadMessage is internal only. +/// +public interface INetReadMessage +{ + internal IReadMessage Message { get; } + internal void SetMessage(IReadMessage msg); + + bool ReadBoolean() => Message.ReadBoolean(); + void ReadPadBits() => Message.ReadPadBits(); + byte ReadByte() => Message.ReadByte(); + byte PeekByte() => Message.PeekByte(); + ushort ReadUInt16() => Message.ReadUInt16(); + short ReadInt16() => Message.ReadInt16(); + uint ReadUInt32() => Message.ReadUInt32(); + int ReadInt32() => Message.ReadInt32(); + ulong ReadUInt64() => Message.ReadUInt64(); + long ReadInt64() => Message.ReadInt64(); + float ReadSingle() => Message.ReadSingle(); + double ReadDouble() => Message.ReadDouble(); + uint ReadVariableUInt32() => Message.ReadVariableUInt32(); + string ReadString() => Message.ReadString(); + Identifier ReadIdentifier() => Message.ReadIdentifier(); + Color ReadColorR8G8B8() => Message.ReadColorR8G8B8(); + Color ReadColorR8G8B8A8() => Message.ReadColorR8G8B8A8(); + int ReadRangedInteger(int min, int max) => Message.ReadRangedInteger(min, max); + float ReadRangedSingle(float min, float max, int bitCount) => Message.ReadRangedSingle(min, max, bitCount); + byte[] ReadBytes(int numberOfBytes) => Message.ReadBytes(numberOfBytes); + int BitPosition + { + get => Message.BitPosition; + set => Message.BitPosition = value; + } + int BytePosition => Message.BytePosition; + byte[] Buffer => Message.Buffer; + int LengthBits + { + get => Message.LengthBits; + set => Message.LengthBits = value; + } + int LengthBytes => Message.LengthBytes; +} + +#endregion + +#region HelperImplementations + +public class NetWriteMessage : INetWriteMessage +{ + private IWriteMessage Message { get; set; } + + IWriteMessage INetWriteMessage.Message => Message; + + void INetWriteMessage.SetMessage(IWriteMessage msg) + { + Message = msg; + } +} + +public class NetReadMessage : INetReadMessage +{ + private IReadMessage Message { get; set; } + IReadMessage INetReadMessage.Message => Message; + + void INetReadMessage.SetMessage(IReadMessage msg) + { + Message = msg; + } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs index 8ba1f8921..a23d70f7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using Barotrauma.LuaCs.Services; using Barotrauma.Steam; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -149,23 +150,23 @@ public sealed class CsPackageManager : IDisposable #endregion /// - /// Whether or not assemblies have been loaded. + /// Whether assemblies have been loaded. /// public bool AssembliesLoaded { get; private set; } /// - /// Whether or not loaded plugins had their preloader run. + /// Whether loaded plugins had their preloader run. /// public bool PluginsPreInit { get; private set; } /// - /// Whether or not plugins' types have been instantiated. + /// Whether plugins' types have been instantiated. /// public bool PluginsInitialized { get; private set; } /// - /// Whether or not plugins are fully loaded. + /// Whether plugins are fully loaded. /// public bool PluginsLoaded { get; private set; } @@ -966,7 +967,7 @@ public sealed class CsPackageManager : IDisposable /// Packages with errors or cyclic dependencies. Element is error message. Null if empty. /// Optional: Allows for a custom checks to be performed on each package. /// Returns a bool indicating if the package is ready to load. - /// Whether or not the process produces a usable list. + /// Whether the process produces a usable list. private static bool OrderAndFilterPackagesByDependencies( Dictionary> packages, out IEnumerable readyToLoad, diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs index dd61c0108..e01db5f4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs @@ -7,13 +7,14 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; +using Barotrauma.LuaCs.Services; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; // ReSharper disable ConditionIsAlwaysTrueOrFalse [assembly: InternalsVisibleTo("CompiledAssembly")] -namespace Barotrauma; +namespace Barotrauma.LuaCs; /// /// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution. @@ -31,11 +32,11 @@ public class MemoryFileAssemblyContextLoader : AssemblyLoadContext // internal private readonly Dictionary _dependencyResolvers = new(); // path-folder, resolver protected bool IsResolving; //this is to avoid circular dependency lookup. - private AssemblyManager _assemblyManager; + private IAssemblyManagementService _assemblyManager; public bool IsTemplateMode { get; set; } public bool IsDisposed { get; private set; } - public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true) + public MemoryFileAssemblyContextLoader(IAssemblyManagementService assemblyManager) : base(isCollectible: true) { this._assemblyManager = assemblyManager; this.IsDisposed = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs index 64bc66006..0e46032c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs @@ -1,25 +1,26 @@ using System; using System.ComponentModel; using System.Xml.Serialization; +using Barotrauma.LuaCs.Data; namespace Barotrauma; [Serializable] -public sealed class RunConfig +public sealed class RunConfig : IRunConfig { /// /// How should scripts be run on the server. /// [XmlElement(ElementName = "Server")] [DefaultValue("Standard")] - public string Server; + public string Server { get; set; } /// /// How should scripts be run on the client. /// [XmlElement(ElementName = "Client")] [DefaultValue("Standard")] - public string Client; + public string Client { get; set; } /// /// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency"). diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs similarity index 80% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs index c7f582395..9d1b7c38f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs @@ -14,7 +14,7 @@ using Microsoft.CodeAnalysis.CSharp; // ReSharper disable EventNeverSubscribedTo.Global // ReSharper disable InconsistentNaming -namespace Barotrauma; +namespace Barotrauma.LuaCs.Services; /*** * Note: This class was written to be thread-safe in order to allow parallelization in loading in the future if the need @@ -25,36 +25,14 @@ namespace Barotrauma; /// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin. /// All plugins are loaded into their own AssemblyLoadContext along with their dependencies. /// -public class AssemblyManager +public class AssemblyManager : IAssemblyManagementService { #region ExternalAPI - - /// - /// Called when an assembly is loaded. - /// + public event Action OnAssemblyLoaded; - - /// - /// Called when an assembly is marked for unloading, before unloading begins. You should use this to cleanup - /// any references that you have to this assembly. - /// public event Action OnAssemblyUnloading; - - /// - /// Called whenever an exception is thrown. First arg is a formatted message, Second arg is the Exception. - /// public event Action OnException; - - /// - /// For unloading issue debugging. Called whenever MemoryFileAssemblyContextLoader [load context] is unloaded. - /// public event Action OnACLUnload; - - - /// - /// [DEBUG ONLY] - /// Returns a list of the current unloading ACLs. - /// public ImmutableList> StillUnloadingACLs { get @@ -70,12 +48,6 @@ public class AssemblyManager } } } - - - // ReSharper disable once MemberCanBePrivate.Global - /// - /// Checks if there are any AssemblyLoadContexts still in the process of unloading. - /// public bool IsCurrentlyUnloading { get @@ -95,21 +67,6 @@ public class AssemblyManager } } } - - // Old API compatibility - public IEnumerable GetSubTypesInLoadedAssemblies() - { - return GetSubTypesInLoadedAssemblies(false); - } - - - /// - /// Allows iteration over all non-interface types in all loaded assemblies in the AsmMgr that are assignable to the given type (IsAssignableFrom). - /// Warning: care should be used when using this method in hot paths as performance may be affected. - /// - /// The type to compare against - /// Forces caches to clear and for the lists of types to be rebuilt. - /// An Enumerator for matching types. public IEnumerable GetSubTypesInLoadedAssemblies(bool rebuildList) { Type targetType = typeof(T); @@ -165,14 +122,6 @@ public class AssemblyManager OpsLockLoaded.ExitReadLock(); } } - - /// - /// Tries to get types assignable to type from the ACL given the Guid. - /// - /// - /// - /// - /// public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types) { Type targetType = typeof(T); @@ -188,13 +137,6 @@ public class AssemblyManager types = null; return false; } - - /// - /// Tries to get types from the ACL given the Guid. - /// - /// - /// - /// public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types) { if (TryGetACL(id, out var acl)) @@ -206,14 +148,6 @@ public class AssemblyManager types = null; return false; } - - - /// - /// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string. - /// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ". - /// - /// The string name of the type to search for. - /// An Enumerator for matching types. List will be empty if bad params are supplied. public IEnumerable GetTypesByName(string typeName) { List types = new(); @@ -299,12 +233,6 @@ public class AssemblyManager } } } - - /// - /// Allows iteration over all types (including interfaces) in all loaded assemblies managed by the AsmMgr. - /// Warning: High usage may result in performance issues. - /// - /// An Enumerator for iteration. public IEnumerable GetAllTypesInLoadedAssemblies() { OpsLockLoaded.EnterReadLock(); @@ -325,13 +253,6 @@ public class AssemblyManager OpsLockLoaded.ExitReadLock(); } } - - /// - /// Returns a list of all loaded ACLs. - /// WARNING: References to these ACLs outside of the AssemblyManager should be kept in a WeakReference in order - /// to avoid causing issues with unloading/disposal. - /// - /// public IEnumerable GetAllLoadedACLs() { OpsLockLoaded.EnterReadLock(); @@ -358,36 +279,14 @@ public class AssemblyManager #region InternalAPI - /// - /// [Unsafe] Warning: only for use in nested threading functions. Requires care to manage access. - /// Does not make any guarantees about the state of the ACL after the list has been returned. - /// - /// [MethodImpl(MethodImplOptions.Synchronized | MethodImplOptions.NoInlining)] - internal ImmutableList UnsafeGetAllLoadedACLs() + ImmutableList IAssemblyManagementService.UnsafeGetAllLoadedACLs() { if (LoadedACLs.IsEmpty) return ImmutableList.Empty; return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList(); } - - /// - /// Used by content package and plugin management to stop unloading of a given ACL until all plugins have gracefully closed. - /// public event System.Func IsReadyToUnloadACL; - - /// - /// Compiles an assembly from supplied references and syntax trees into the specified AssemblyContextLoader. - /// A new ACL will be created if the Guid supplied is Guid.Empty. - /// - /// - /// - /// - /// - /// A non-unique name for later reference. Optional, set to null if unused. - /// The guid of the assembly - /// - /// public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName, [NotNull] IEnumerable syntaxTree, IEnumerable externalMetadataReferences, @@ -440,14 +339,6 @@ public class AssemblyManager return state; } - - /// - /// Switches the ACL with the given Guid to Template Mode, which disables assembly name resolution for any assemblies loaded in it. - /// These ACLs are intended to be used to host Assemblies for information only and not for code execution. - /// WARNING: This process is irreversible. - /// - /// Guid of the ACL. - /// Whether or not an ACL was found with the given ID. public bool SetACLToTemplateMode(Guid guid) { if (!TryGetACL(guid, out var acl)) @@ -455,16 +346,6 @@ public class AssemblyManager acl.Acl.IsTemplateMode = true; return true; } - - /// - /// Tries to load all assemblies at the supplied file paths list into the ACl with the given Guid. - /// If the supplied Guid is Empty, then a new ACl will be created and the Guid will be assigned to it. - /// - /// List of assemblies to try and load. - /// A non-unique name for later reference. Optional. - /// Guid of the ACL or Empty if none specified. Guid of ACL will be assigned to this var. - /// Operation success messages. - /// public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable filePaths, string friendlyName, ref Guid id) { @@ -507,9 +388,7 @@ public class AssemblyManager return AssemblyLoadingSuccessState.ACLLoadFailure; } - - - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Synchronized)] + [MethodImpl(MethodImplOptions.NoInlining)] public bool TryBeginDispose() { OpsLockLoaded.EnterWriteLock(); @@ -563,8 +442,6 @@ public class AssemblyManager OpsLockLoaded.ExitWriteLock(); } } - - [MethodImpl(MethodImplOptions.NoInlining)] public bool FinalizeDispose() { @@ -606,15 +483,7 @@ public class AssemblyManager return isUnloaded; } - - /// - /// Tries to retrieve the LoadedACL with the given ID or null if none is found. - /// WARNING: External references to this ACL with long lifespans should be kept in a WeakReference - /// to avoid causing unloading/disposal issues. - /// - /// GUID of the ACL. - /// The found ACL or null if none was found. - /// Whether or not an ACL was found. + [MethodImpl(MethodImplOptions.NoInlining)] public bool TryGetACL(Guid id, out LoadedACL acl) { @@ -865,6 +734,16 @@ public class AssemblyManager } #endregion + + public void Dispose() + { + TryBeginDispose(); + } + + public void Reset() + { + TryBeginDispose(); + } } public static class AssemblyExtensions diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IAssemblyManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IAssemblyManagementService.cs new file mode 100644 index 000000000..1ba46fa9a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IAssemblyManagementService.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +// ReSharper disable InconsistentNaming + +namespace Barotrauma.LuaCs.Services; + + +public interface IAssemblyManagementService : IService +{ + #region Public API + + /// + /// Called when an assembly is loaded. + /// + public event Action OnAssemblyLoaded; + + /// + /// Called when an assembly is marked for unloading, before unloading begins. You should use this to cleanup + /// any references that you have to this assembly. + /// + public event Action OnAssemblyUnloading; + + /// + /// Called whenever an exception is thrown. First arg is a formatted message, Second arg is the Exception. + /// + public event Action OnException; + + /// + /// For unloading issue debugging. Called whenever MemoryFileAssemblyContextLoader [load context] is unloaded. + /// + // ReSharper disable once InconsistentNaming + public event Action OnACLUnload; + + + /// + /// [DEBUG ONLY] + /// Returns a list of the current unloading ACLs. + /// + // ReSharper disable once InconsistentNaming + public ImmutableList> StillUnloadingACLs { get; } + + // ReSharper disable once MemberCanBePrivate.Global + /// + /// Checks if there are any AssemblyLoadContexts still in the process of unloading. + /// + public bool IsCurrentlyUnloading { get; } + + /// + /// Allows iteration over all non-interface types in all loaded assemblies in the AsmMgr that are assignable to the given type (IsAssignableFrom). + /// Warning: care should be used when using this method in hot paths as performance may be affected. + /// + /// The type to compare against + /// Forces caches to clear and for the lists of types to be rebuilt. + /// An Enumerator for matching types. + public IEnumerable GetSubTypesInLoadedAssemblies(bool rebuildList); + + /// + /// Tries to get types assignable to type from the ACL given the Guid. + /// + /// + /// + /// + /// Operation success. + public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types); + + /// + /// Tries to get types from the ACL given the Guid. + /// + /// + /// + /// + public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types); + + /// + /// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string. + /// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ". + /// + /// The string name of the type to search for. + /// An Enumerator for matching types. List will be empty if bad params are supplied. + public IEnumerable GetTypesByName(string typeName); + + /// + /// Allows iteration over all types (including interfaces) in all loaded assemblies managed by the AsmMgr. + /// Warning: High usage may result in performance issues. + /// + /// An Enumerator for iteration. + public IEnumerable GetAllTypesInLoadedAssemblies(); + + /// + /// Returns a list of all loaded ACLs. + /// WARNING: References to these ACLs outside the AssemblyManager should be kept in a WeakReference in order + /// to avoid causing issues with unloading/disposal. + /// + /// + public IEnumerable GetAllLoadedACLs(); + + #endregion + + #region InternalAPI + /*** Notes: Internal API uses the 'public' modifier because of the common and recommended use of publicized APIs + * by third-party add-ins. + */ + + /// + /// [Unsafe] Warning: only for use in nested threading functions. Requires care to manage access. + /// Does not make any guarantees about the state of the ACL after the list has been returned. + /// + /// + public ImmutableList UnsafeGetAllLoadedACLs(); + + /// + /// Used by content package and plugin management to stop unloading of a given ACL until all plugins have gracefully closed. + /// + public event System.Func IsReadyToUnloadACL; + + /// + /// Compiles an assembly from supplied references and syntax trees into the specified AssemblyContextLoader. + /// A new ACL will be created if the Guid supplied is Guid.Empty. + /// + /// + /// + /// + /// + /// A non-unique name for later reference. Optional, set to null if unused. + /// The guid of the assembly + /// + /// + public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName, + [NotNull] IEnumerable syntaxTree, + IEnumerable externalMetadataReferences, + [NotNull] CSharpCompilationOptions compilationOptions, + string friendlyName, + ref Guid id, + IEnumerable externFileAssemblyRefs = null); + + /// + /// Switches the ACL with the given Guid to Template Mode, which disables assembly name resolution for any assemblies loaded in it. + /// These ACLs are intended to be used to host Assemblies for information only and not for code execution. + /// WARNING: This process is irreversible. + /// + /// Guid of the ACL. + /// Whether an ACL was found with the given ID. + public bool SetACLToTemplateMode(Guid guid); + + + /// + /// Tries to load all assemblies at the supplied file paths list into the ACl with the given Guid. + /// If the supplied Guid is Empty, then a new ACl will be created and the Guid will be assigned to it. + /// + /// List of assemblies to try and load. + /// A non-unique name for later reference. Optional. + /// Guid of the ACL or Empty if none specified. Guid of ACL will be assigned to this var. + /// Operation success messages. + /// + public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable filePaths, + string friendlyName, ref Guid id); + + + /// + /// Tries to begin the disposal process of ACLs. + /// + /// Returns whether the unloading process could be initiated. + public bool TryBeginDispose(); + + + /// + /// Returns whether unloading is completed and updates the styate of the unloading cache. + /// + /// + public bool FinalizeDispose(); + + /// + /// Tries to retrieve the LoadedACL with the given ID or null if none is found. + /// WARNING: External references to this ACL with long lifespans should be kept in a WeakReference + /// to avoid causing unloading/disposal issues. + /// + /// GUID of the ACL. + /// The found ACL or null if none was found. + /// Whether an ACL was found. + public bool TryGetACL(Guid id, out AssemblyManager.LoadedACL acl); + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IConfigService.cs new file mode 100644 index 000000000..ac6deedd5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IConfigService.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Services; + +public interface IConfigService : IService +{ + /* + * Resource Files. + */ + bool TryAddConfigs(ImmutableArray configResources); + bool TryAddConfigsProfiles(ImmutableArray configProfileResources); + void RemoveConfigs(ImmutableArray configResources); + void RemoveConfigsProfiles(ImmutableArray configProfilesResources); + + + /* + * Already processed + */ + bool TryAddConfigs(ImmutableArray configs); + bool TryAddConfigsProfiles(ImmutableArray configProfiles); + void RemoveConfigs(ImmutableArray configs); + void RemoveConfigsProfiles(ImmutableArray configProfiles); + + /* + * Immediate mode, does not have displayable functionality + */ + IConfigEntry AddConfigEntry(ContentPackage package, string name, + T defaultValue, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action> onValueChanged = null) where T : IConvertible, IEquatable; + + IConfigList AddConfigList(ContentPackage package, string name, + int defaultIndex, IReadOnlyList values, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action onValueChanged = null); + + IReadOnlyDictionary GetConfigsForPackage(ContentPackage package); + IReadOnlyDictionary GetConfigsForPackage(string packageName); + IReadOnlyDictionary<(ContentPackage, string), IConfigBase> GetAllConfigs(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IEventService.cs new file mode 100644 index 000000000..3c0caef4e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IEventService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services; + +public interface IEventService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IHookManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IHookManagementService.cs new file mode 100644 index 000000000..5428ed704 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IHookManagementService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services; + +public interface IHookManagementService : IService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILegacyConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILegacyConfigService.cs new file mode 100644 index 000000000..a93559084 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILegacyConfigService.cs @@ -0,0 +1,8 @@ +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public interface ILegacyConfigService : IService +{ + bool TryBuildModConfigFromLegacy(ContentPackage package, out IModConfigInfo configInfo); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILocalizationService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILocalizationService.cs new file mode 100644 index 000000000..045d3e986 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILocalizationService.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public interface ILocalizationService : IService +{ + IReadOnlyCollection GetLoadedLocales(); + void Remove(ImmutableArray localizations); + bool TrySetCurrentCulture(CultureInfo culture); + bool TrySetCurrentCulture(string cultureName); + bool TryLoadLocalizations(ImmutableArray localizationResources); + string GetLocalizedString(string key, string fallback); + string GetLocalizedString(string key, CultureInfo targetCulture); + bool TryRegisterLocalizationResolver(CultureInfo targetCulture, Func factoryResolver); + bool ReplaceSymbols(string text, string symbolExpr); + bool IsCurrentCultureSupported(IResourceCultureInfo culturesInfo); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILoggerService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILoggerService.cs new file mode 100644 index 000000000..eb457b7d3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILoggerService.cs @@ -0,0 +1,25 @@ +using System; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.LuaCs.Services; + +/// +/// Provides console and debug logging services +/// +public interface ILoggerService : IService +{ + void HandleException(Exception exception, string prefix = null); + void LogError(string message); + void LogWarning(string message); + void LogMessage(string message, Color? serverColor = null, Color? clientColor = null); + void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage); + + #region DebugBuilds + + void LogDebug(string message, Color? color = null); + void LogDebugWarning(string message); + void LogDebugError(string message); + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILuaScriptService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILuaScriptService.cs new file mode 100644 index 000000000..6d4a8e673 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILuaScriptService.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using Barotrauma.LuaCs.Data; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; + +namespace Barotrauma.LuaCs.Services; + +public interface ILuaScriptService : IService +{ + #region Script_File_Collector + + /// + /// Adds the script files to the runner but does not execute them. + /// + /// + /// + bool TryAddScriptFiles(ImmutableArray luaResource); + /// + /// Removes the specific resources from the script runner. Important: Does not stop the + /// execution of any code related to the files nor guarantee cleanup of resources! + /// + /// + void RemoveScriptFiles(ImmutableArray luaResource); + + /// + /// Executes loaded script files on the management service. + /// + /// + /// + /// + bool TryExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false); + ImmutableArray GetScriptResources(); + + #endregion +} + +public interface ILuaScriptManagementService : IService +{ + #region Script_File_Execution + + bool TryExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false); + bool TryExecuteLoadedScripts(ImmutableArray scripts, bool pauseExecutionOnError = false, bool verboseLogging = false); + bool TryExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false); + + #endregion + + #region Type_Registration + + IUserDataDescriptor RegisterType(Type type); + IUserDataDescriptor RegisterType(string typeName); + IUserDataDescriptor RegisterGenericType(Type type); + IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArgs); + void UnregisterType(Type type); + void UnregisterType(string typeName); + void UnregisterAllTypes(); + + #endregion + + #region Type_Checks_&Utilities + + bool IsRegistered(Type type); + bool IsTargetType(object obj, string typeName); + string TypeOf(object obj); + object CreateStatic(string typeName); + object CreateEnumTable(string typeName); + FieldInfo FindFieldRecursively(Type type, string fieldName); + void MakeFieldAccessible(IUserDataDescriptor descriptor, string fieldName); + MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null); + void MakeMethodAccessible(IUserDataDescriptor descriptor, string methodName, string[] parameters = null); + PropertyInfo FindPropertyRecursively(Type type, string propertyName); + void MakePropertyAccessible(IUserDataDescriptor descriptor, string propertyName); + void AddMethod(IUserDataDescriptor descriptor, string methodName, object function); + void AddField(IUserDataDescriptor descriptor, string fieldName, DynValue value); + void RemoveMember(IUserDataDescriptor descriptor, string memberName); + bool HasMember(object obj, string memberName); + DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor descriptor); + DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType); + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/INetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/INetworkingService.cs new file mode 100644 index 000000000..3b2c7269b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/INetworkingService.cs @@ -0,0 +1,23 @@ +using System; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Networking; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Services; + +public interface INetworkingService : IService +{ + bool IsActive { get; } + bool IsSynchronized { get; } + bool TryRegisterVar(INetVar var, NetSync mode, ClientPermissions permissions); + void UnregisterVar(Guid varId); + bool SendEvent(Guid varId); + void SendMessageGlobal(string id, string message); + void Synchronize(); + + #region LegacyAPI + + bool RestrictMessageSize { get; set; } + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageManagementService.cs new file mode 100644 index 000000000..3ef92394a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageManagementService.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public interface IPackageManagementService : IService +{ + void AddPackages(ref ReadOnlySpan<(ContentPackage, bool)> packages, + bool executeImmediately = false, + bool errorOnFailures = false, + bool errorOnExistingPackageFound = false); + void LoadPackages(bool onlyUnloadedPackages = true, bool rescanPackages = false); + void UnloadPackages(bool errorOnFailures = true); + bool IsPackageLoaded(ContentPackage package); + bool CheckDependencyLoaded(IPackageDependencyInfo info); + bool CheckDependenciesLoaded(IEnumerable infos, out IReadOnlyList missingPackages); + bool CheckEnvironmentSupported(IPlatformInfo platform); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageService.cs new file mode 100644 index 000000000..1411cb313 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageService.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public interface IPackageService : IService, + // These allow us the pass the IContentPackageService to anything that needs the data without having to directly reference the member + IResourceCultureInfo, IAssembliesResourcesInfo, ILocalizationsResourcesInfo, ILuaScriptsResourcesInfo +{ + ContentPackage Package { get; } + IModConfigInfo ModConfigInfo { get; } + /// + /// Try to load the XML config and resources information from the given package. + /// + /// + /// Whether the package was parsed without errors and any information was found. Will return false for purely vanilla packages. + bool TryLoadResourcesInfo([NotNull]ContentPackage package); + /// + /// Tries to load all assemblies and instance plugins for the given resources list, regardless whether they're marked as optional and/or lazy load. + /// Will sort by load priority unless overriden/bypassed. + /// + /// + /// + /// Whether loading is successful. Returns true on an empty list. + void LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false); + void LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo); + void AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo); +#if CLIENT + void LoadStyles([NotNull]IStylesResourcesInfo stylesInfo); +#endif + void LoadConfig([NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo); +} + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginManagementService.cs new file mode 100644 index 000000000..b5c505c83 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginManagementService.cs @@ -0,0 +1,7 @@ +namespace Barotrauma.LuaCs.Services; + +public interface IPluginManagementService : IService +{ + bool IsAssemblyLoadedGlobal(string friendlyName); + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginService.cs new file mode 100644 index 000000000..cbdd9ba74 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginService.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public interface IPluginService : IService +{ + bool IsAssemblyLoaded(string friendlyName); + /// + /// Loads the assemblies for the given information + /// + /// + /// + /// + /// + /// + bool TryLoadAndInstanceTypes(IEnumerable assemblyResourcesInfo, bool injectServices, out ImmutableArray typeInstances) where T : class, IAssemblyPlugin; + ImmutableArray GetLoadedPluginTypesInPackage() where T : class, IAssemblyPlugin; + /// + /// Advances the loading/execution state of the plugin. IMPORTANT: You cannot set the execution state of plugins + /// to 'Disposed'. You must instead call the 'DisposePlugins' method. + /// + /// + /// + bool AdvancePluginStates(PluginRunState newState); + + /// + /// Disposes of all running plugins hosted by the service and releases their references to allow unloading. + /// + /// Success of the operation. Returns false if any plugin threw errors during disposal. + bool DisposePlugins(); + + /// + /// Gets the current plugin execution state. + /// + /// + PluginRunState GetPluginRunState(); +} + +public enum PluginRunState +{ + Instanced=0, + PreInitialization=1, + Initialized=2, + LoadingCompleted=3, + Disposed=4 +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IService.cs new file mode 100644 index 000000000..bd7595143 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IService.cs @@ -0,0 +1,15 @@ +using System; + +namespace Barotrauma.LuaCs.Services; + +/// +/// Base interface inherited by all services +/// +public interface IService : IDisposable +{ + /// + /// Returns the service to its original state (post-instantiation). + /// Allows a service instance to be reused without disposing of the instance. + /// + void Reset(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IServicesProvider.cs new file mode 100644 index 000000000..0304b7da4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IServicesProvider.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using LightInject; + +namespace Barotrauma.LuaCs.Services; + +/// +/// Provides instancing and management of IServices. +/// +public interface IServicesProvider +{ + #region Type_Registration + + /// + /// Registers a type as a service for a given interface. + /// + /// + /// + /// + /// + void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new(); + + /// + /// Registers a type as a service for a given interface that can be requested by name. + /// + /// + /// + /// + /// + /// + void RegisterServiceType(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new(); + + /// + /// Called whenever a new service type for a given interface is implemented. + /// Args[0]: Interface type + /// Args[1]: Implementing type + /// + event System.Action OnServiceRegistered; + + /// + /// Runs compilation of registered services. + /// + public void Compile(); + + #endregion + + #region Services_Instancing_Injection + + /// + /// Injects services into the properties of already instanced objects. + /// + /// + /// + void InjectServices(T inst) where T : class; + + /// + /// Tries to get a service for the given interface, returns success/failure. + /// + /// + /// + /// + /// + bool TryGetService(out IService service) where TSvcInterface : class, IService; + + /// + /// Tries to get a service for the given name and interface, returns success/failure. + /// + /// + /// + /// + /// + /// + bool TryGetService(string name, out IService service) where TSvcInterface : class, IService; + + /// + /// Called whenever a new service is created/instanced. + /// Args[0]: The interface type of the service. + /// Args[1]: The instance of the service. + /// + event System.Action OnServiceInstanced; + + #endregion + + #region ActiveServices + + /// + /// Returns all services for the given interface. + /// + /// + /// + ImmutableArray GetAllServices() where TSvc : class, IService; + + #endregion + + // Notes: Left public due to the common use of Publicizers + #region Internal_Use + + /// + /// Notes: Internal use only if hosted by LuaCsForBarotrauma. Disposes of all services and resets DI container. Warning: unable to dispose of services held by other objects. + /// + void DisposeAndReset(); + + #endregion +} + +public enum ServiceLifetime +{ + Transient, Singleton, PerThread, Invalid, Custom +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IStorageService.cs new file mode 100644 index 000000000..608a5417c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IStorageService.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; +using System.Xml.Linq; + +namespace Barotrauma.LuaCs.Services; + +public interface IStorageService : IService +{ + #region LocalGameData + + bool TryLoadLocalXml(ContentPackage package, string localFilePath, out XDocument document); + bool TryLoadLocalBinary(ContentPackage package, string localFilePath, out byte[] bytes); + bool TryLoadLocalText(ContentPackage package, string localFilePath, out string text); + bool FileExistsInLocalData(ContentPackage package, string localFilePath); + + #endregion + + #region ContentPackageData + bool TryLoadPackageXml(ContentPackage package, string localFilePath, out XDocument document); + bool TryLoadPackageBinary(ContentPackage package, string localFilePath, out byte[] bytes); + bool TryLoadPackageText(ContentPackage package, string localFilePath, out string text); + + ImmutableArray TryLoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePath, out ImmutableArray document); + ImmutableArray TryLoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePath, out ImmutableArray bytes); + ImmutableArray TryLoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePath, out ImmutableArray text); + + bool FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively, out ImmutableArray localFilePaths); + bool FileExistsInPackage(ContentPackage package, string localFilePath); + + #endregion + + #region AbsolutePaths + + bool TryLoadXml(string filePath, out XDocument document); + bool TrySaveXml(string filePath, in XDocument document); + bool TryLoadBinary(string filePath, out byte[] bytes); + bool TrySaveBinary(string filePath, in byte[] bytes); + bool TryLoadText(string filePath, out string text); + bool TrySaveText(string filePath, string text); + bool FileExists(string filePath); + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LoggerService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LoggerService.cs new file mode 100644 index 000000000..25fb27542 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LoggerService.cs @@ -0,0 +1,149 @@ +using System; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using MoonSharp.Interpreter; + +namespace Barotrauma.LuaCs.Services; + +public partial class LoggerService : ILoggerService +{ + public bool HideUserNames = true; + +#if SERVER + private const string LogPrefix = "SV"; + private const int NetMaxLength = 1024; // character limit of vanilla Barotrauma's chat system. + private const int NetMaxMessages = 60; + + // This is used so it's possible to call logging functions inside the serverLog + // hook without creating an infinite loop + private bool _lockLog = false; +#else + private const string LogPrefix = "CL"; +#endif + + public void HandleException(Exception exception, string prefix = null) + { + string errorString = ""; + switch (exception) + { + case NetRuntimeException netRuntimeException: + if (netRuntimeException.DecoratedMessage == null) + { + errorString = $"{prefix ?? ""}{netRuntimeException.ToString()}"; + } + else + { + // FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace... + errorString = $"{prefix ?? ""}{netRuntimeException.DecoratedMessage}: {netRuntimeException}"; + } + break; + case InterpreterException interpreterException: + if (interpreterException.DecoratedMessage == null) + { + errorString = $"{prefix ?? ""}{interpreterException.ToString()}"; + } + else + { + errorString = $"{prefix ?? ""}{interpreterException.DecoratedMessage}"; + } + break; + default: + string s = exception.StackTrace != null ? exception.ToString() : $"{exception}\n{Environment.StackTrace}"; + errorString = $"{prefix ?? ""}{s}"; + break; + } + + LogError(prefix + Environment.UserName + " " + errorString); + } + + public void LogError(string message) + { + if (HideUserNames && !Environment.UserName.IsNullOrEmpty()) + { + message = message.Replace(Environment.UserName, "USERNAME"); + } + + Log($"{message}", Color.Red, ServerLog.MessageType.Error); + } + + public void LogWarning(string message) + { + throw new NotImplementedException(); + } + + public void LogMessage(string message, Color? serverColor = null, Color? clientColor = null) + { + serverColor ??= Color.MediumPurple; + clientColor ??= Color.Purple; + +#if SERVER + Log(message, serverColor); +#else + Log(message, clientColor); +#endif + } + + public void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage) + { + DebugConsole.NewMessage(message, color); + +#if SERVER + void BroadcastMessage(string m) + { + foreach (var client in GameMain.Server.ConnectedClients) + { + ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: color); + GameMain.Server.SendDirectChatMessage(consoleMessage, client); + + if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog)) + { + continue; + } + + ChatMessage logMessage = ChatMessage.Create(messageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null); + GameMain.Server.SendDirectChatMessage(logMessage, client); + } + } + + if (GameMain.Server != null) + { + if (GameMain.Server.ServerSettings.SaveServerLogs) + { + string logMessage = "[LuaCs] " + message; + GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, messageType, false); + + if (!_lockLog) + { + _lockLog = true; + GameMain.LuaCs?.Hook?.Call("serverLog", logMessage, messageType); + _lockLog = false; + } + } + + for (int i = 0; i < message.Length; i += NetMaxLength) + { + string subStr = message.Substring(i, Math.Min(1024, message.Length - i)); + BroadcastMessage(subStr); + } + } +#endif + } + + public void LogDebug(string message, Color? color = null) + { + throw new NotImplementedException(); + } + + public void LogDebugWarning(string message) + { + throw new NotImplementedException(); + } + + public void LogDebugError(string message) + { + throw new NotImplementedException(); + } + + public void Dispose() { } + public void Reset() { } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptService.cs new file mode 100644 index 000000000..b1c30f23e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services; + +public class LuaScriptService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs new file mode 100644 index 000000000..2f5d5eb6f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public class PackageManagementService : IPackageManagementService, IPluginManagementService +{ + private readonly Func _contentPackageServiceFactory; + private readonly Lazy _assemblyManagementService; + + public PackageManagementService( + Func getPackageService, + Lazy assemblyManagementService) + { + this._contentPackageServiceFactory = getPackageService; + this._assemblyManagementService = assemblyManagementService; + } + + + public void Dispose() + { + // TODO release managed resources here + } + + public void Reset() + { + throw new NotImplementedException(); + } + + public bool IsAssemblyLoadedGlobal(string friendlyName) + { + throw new NotImplementedException(); + } + + public void AddPackages(ref ReadOnlySpan<(ContentPackage, bool)> packages, bool executeImmediately = false, bool errorOnFailures = false, + bool errorOnExistingPackageFound = false) + { + throw new NotImplementedException(); + } + + public void LoadPackages(bool onlyUnloadedPackages = true, bool rescanPackages = false) + { + throw new NotImplementedException(); + } + + public void UnloadPackages(bool errorOnFailures = true) + { + throw new NotImplementedException(); + } + + public bool IsPackageLoaded(ContentPackage package) + { + throw new NotImplementedException(); + } + + public bool CheckDependencyLoaded(IPackageDependencyInfo info) + { + throw new NotImplementedException(); + } + + public bool CheckDependenciesLoaded(IEnumerable infos, out IReadOnlyList missingPackages) + { + throw new NotImplementedException(); + } + + public bool CheckEnvironmentSupported(IPlatformInfo platform) + { + throw new NotImplementedException(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs new file mode 100644 index 000000000..97e1f0cfe --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs @@ -0,0 +1,627 @@ +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 +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConverterServiceDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConverterServiceDefinitions.cs new file mode 100644 index 000000000..dbb150ff0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConverterServiceDefinitions.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services.Processing; + +#region TypeDef + +// ReSharper disable once TypeParameterCanBeVariant +public interface IConverterService : IService +{ + bool TryParseResource(TSrc src, out TOut resources); + bool TryParseResources(IEnumerable sources, out List resources); +} + +public interface IXmlResourceConverterService : IConverterService { } +public interface IResourceToXmlConverterService : IConverterService { } + +#endregion + +/// +/// Parses Xml to produce loading metadata info for linked loadable files. +/// +#region XmlToResourceInfoParsers + +public interface IXmlAssemblyResConverter : IXmlResourceConverterService { } +public interface IXmlConfigResConverterService : IXmlResourceConverterService { } +public interface IXmlLocalizationResConverterService : IXmlResourceConverterService { } + +#endregion + +/// +/// Parses Xml to produce ready-to-use info/data without any additional file/data loading. +/// +#region XmlToInfoParsers +public interface IXmlDependencyConverterService : IXmlResourceConverterService { } +public interface IXmlModConfigConverterService : IXmlResourceConverterService { } +/// +/// Parses legacy packages that make use of the RunConfig.xml structure to produce a ModConfig. +/// +public interface IXmlLegacyModConfigConverterService : IXmlResourceConverterService { } + +#endregion + + +#region ResToInfoParsers +public interface ILocalizationResToInfoParser : IConverterService { } +public interface IConfigResConverterService : IConverterService { } +public interface IConfigProfileResConverterService : IConverterService { } + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs new file mode 100644 index 000000000..2f4004e3b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaConfigService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaDataService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaDataService.cs new file mode 100644 index 000000000..04f4893bc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaDataService.cs @@ -0,0 +1,39 @@ +using MoonSharp.Interpreter; + +namespace Barotrauma.LuaCs.Services.Safe; + +/// +/// Service for providing stateful functions and in-memory storage for lua functions +/// +public interface ILuaDataService : ILuaService +{ + /// + /// Returns stored table for the given object if it exists. + /// + /// + /// + /// The table data or null if none exists. + Table GetObjectTable(object obj, string tableName); + + /// + /// Returns stored table data under the given name if it exists. + /// + /// + /// The table data or null if none exists. + Table GetTable(string tableName); + + /// + /// Returns stored table data for the given object or creates a new table if one doesn't exist. + /// + /// + /// + /// + Table GetOrCreateObjectTable(object obj, string tableName); + + /// + /// Returns stored table data or creates a new table if one doesn't exist. + /// + /// + /// + Table GetOrCreateTable(string tableName); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs new file mode 100644 index 000000000..07f38202c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaEventService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaNetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaNetworkingService.cs new file mode 100644 index 000000000..5df591472 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaNetworkingService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaNetworkingService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaPackageManagementService.cs new file mode 100644 index 000000000..391d680a5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaPackageManagementService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaPackageManagementService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaPackageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaPackageService.cs new file mode 100644 index 000000000..891224015 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaPackageService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaPackageService : ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaService.cs new file mode 100644 index 000000000..0c6093319 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaService.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaService +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs new file mode 100644 index 000000000..b06b74940 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Immutable; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using LightInject; + +namespace Barotrauma.LuaCs.Services; + + +public class ServicesProvider : IServicesProvider +{ + private ServiceContainer _serviceContainerInst; + private ServiceContainer ServiceContainer + { + get + { + // ReSharper disable once ConvertIfStatementToNullCoalescingExpression + if (_serviceContainerInst is null) + _serviceContainerInst = new ServiceContainer(); + return _serviceContainerInst; + } + } + + private readonly ReaderWriterLockSlim _serviceLock = new(); + + public void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new() + { + if (lifetimeInstance is null) + { + switch (lifetime) + { + case ServiceLifetime.Singleton: + lifetimeInstance = new PerContainerLifetime(); + break; + case ServiceLifetime.PerThread: + lifetimeInstance = new PerThreadLifetime(); + break; + // treat these as transient + case ServiceLifetime.Transient: + case ServiceLifetime.Invalid: + case ServiceLifetime.Custom: // lifetime should not be null here + default: + lifetimeInstance = new PerRequestLifeTime(); + break; + } + } + + try + { + _serviceLock.EnterReadLock(); + ServiceContainer.Register(lifetimeInstance); + ServiceContainer.Compile(); + OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(TService)); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public void RegisterServiceType(string name, ServiceLifetime lifetime, + ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new() + { + if (name.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException($"Tried to register a service of type {typeof(TService).Name} but the name provided is null or empty." ); + } + + if (lifetimeInstance is null) + { + switch (lifetime) + { + case ServiceLifetime.Singleton: + lifetimeInstance = new PerContainerLifetime(); + break; + case ServiceLifetime.PerThread: + lifetimeInstance = new PerThreadLifetime(); + break; + // treat these as transient + case ServiceLifetime.Transient: + case ServiceLifetime.Invalid: + case ServiceLifetime.Custom: // lifetime should not be null here + default: + lifetimeInstance = new PerRequestLifeTime(); + break; + } + } + + try + { + _serviceLock.EnterReadLock(); + ServiceContainer.Register(name, lifetimeInstance); + ServiceContainer.Compile(); + OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(TService)); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public void Compile() + { + try + { + _serviceLock.EnterReadLock(); + ServiceContainer?.Compile(); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public event Action OnServiceRegistered; + + public void InjectServices(T inst) where T : class + { + try + { + _serviceLock.EnterReadLock(); + ServiceContainer.InjectProperties(inst); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public bool TryGetService(out IService service) where TSvcInterface : class, IService + { + try + { + _serviceLock.EnterReadLock(); + service = ServiceContainer.TryGetInstance(); + return service is not null; + } + catch + { + service = null; + return false; + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public bool TryGetService(string name, out IService service) where TSvcInterface : class, IService + { + try + { + _serviceLock.EnterReadLock(); + service = ServiceContainer.TryGetInstance(name); + return service is not null; + } + catch + { + service = null; + return false; + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + public event Action OnServiceInstanced; + + public ImmutableArray GetAllServices() where TSvc : class, IService + { + try + { + _serviceLock.EnterReadLock(); + return ServiceContainer.GetAllInstances().ToImmutableArray(); + } + finally + { + _serviceLock.ExitReadLock(); + } + } + + [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.NoInlining)] + public void DisposeAndReset() + { + // Plugins should never be allowed to execute this. + if (Assembly.GetCallingAssembly() != Assembly.GetExecutingAssembly()) + { + throw new MethodAccessException( + $"Assembly {Assembly.GetCallingAssembly().FullName} attempted to call DisposeAllServices()."); + } + + try + { + _serviceLock.EnterWriteLock(); + _serviceContainerInst?.Dispose(); + _serviceContainerInst = new ServiceContainer(); + } + finally + { + _serviceLock.ExitWriteLock(); + } + } +} + +public class PerThreadLifetime : ILifetime +{ + private readonly ThreadLocal _instance = new(); + + public object GetInstance(Func createInstance, Scope scope) + { + if (_instance.Value is null) + { + var inst = createInstance.Invoke(); + // IDisposable dispatch + if (inst is IDisposable disposable) + { + if (scope is null) + { + throw new InvalidOperationException("Attempt disposable object without a valid scope."); + } + scope.TrackInstance(disposable); + } + + _instance.Value = inst; + } + + return _instance.Value; + } +}