diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs new file mode 100644 index 000000000..789c00ac0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Configuration; + + +/// +/// Base type of all menu displayable types. +/// +public interface IDisplayableConfigBase : IDataInfo, IConfigDisplayInfo +{ + /// + /// Whether the current config is editable. + /// + bool IsEditable { get; } + /// + /// Used to indicate the implemented interface and targeted display logic. + /// + static virtual DisplayType DisplayOption => DisplayType.Undefined; +} + +public interface IDisplayableConfigBase : IDisplayableConfigBase +{ + void SetValue(TValue value); + TDisplay GetDisplayValue(); +} + +public interface IDisplayableConfigBool : IDisplayableConfigBase +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Boolean; +} + +public interface IDisplayableConfigText : IDisplayableConfigBase +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Text; +} + +public interface IDisplayableConfigInt : IDisplayableConfigBase +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Integer; +} + +public interface IDisplayableConfigFloat : IDisplayableConfigBase +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Float; +} + +public interface IDisplayableConfigSliderInt : IDisplayableConfigBase<(int Min, int Max, int Value, int Steps), int> +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.SliderInt; +} + +public interface IDisplayableConfigSliderFloat : IDisplayableConfigBase<(float Min, float Max, float Value, int Steps), float> +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.SliderFloat; +} + +public interface IDisplayableConfigDropdown : IDisplayableConfigBase, string> +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Dropdown; +} + +/// +/// Allows completely custom-designed UI for this configuration component. +/// +public interface IDisplayableConfigCustom : IDisplayableConfigBase +{ + static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Custom; + /// + /// Draw your menu settings option. + /// + /// Parent layout component. + void DrawComponent(GUILayoutGroup layoutGroup); + /// + /// Called when the config element is set to be disposed to allow for cleanup. + /// + void DisposeGUI(); + /// + /// Called when the UI indicates to save the current value as permanent. + /// + void OnValueSaved(); + /// + /// Called when the UI indicates to discard the currently displayed value and revert to the last saved value. + /// + void OnValueDiscarded(); +} + + + +/// +/// Indicates the intended display and feedback logic to be used by the . +///
[Important] +///
The type must implement the indicated interface for the selected option, or it will not be displayed. +///
+public enum DisplayType +{ + /// + /// Will not be displayed in menus. + /// + Undefined, + /// + /// Will be shown as a checkbox. + ///
[Requires()] + ///
+ Boolean, + /// + /// Shown as an editable text input. + ///
[Requires()] + ///
+ Text, + /// + /// Shown as number input (no decimal input). + ///
[Requires()] + ///
+ Integer, + /// + /// Shown as a number input. + ///
[Requires()] + ///
+ Float, + /// + /// Shown as a slider, values parsed as integers. + ///
[Requires()] + ///
+ SliderInt, + /// + /// Shown as a slider, values parsed as single-precision decimal numbers. + ///
[Requires()] + ///
+ SliderFloat, + /// + /// Shown as a menu, values parsed as strings. + ///
[Requires()] + ///
+ Dropdown, + /// + /// UI Display is implemented by inheritor and actioned by a call to . + ///
[Requires()] + ///
+ Custom +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs deleted file mode 100644 index 570f48f9b..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Immutable; -using System.Globalization; - -namespace Barotrauma.LuaCs.Data; - -public partial record ModConfigInfo : IModConfigInfo -{ - public ImmutableArray Styles { 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 ContentPackage OwnerPackage { get; init; } - public string FallbackPackageName { 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 index 576d18118..801bc5ac4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IConfigInfo.cs @@ -2,8 +2,22 @@ using Barotrauma.LuaCs.Configuration; namespace Barotrauma.LuaCs.Data; -public partial interface IConfigInfo +public partial interface IConfigInfo : IConfigDisplayInfo { } + +public interface IConfigDisplayInfo { + /// + /// User-friendly name or Localization Token. + /// + string DisplayName { get; } + /// + /// User-friendly description or Localization Token. + /// + string Description { get; } + /// + /// The menu category to display under. Used for filtering. + /// + string DisplayCategory { get; } /// /// Should this config be displayed in end-user menus. /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs deleted file mode 100644 index de47ef225..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Immutable; - -namespace Barotrauma.LuaCs.Data; - -public partial interface IModConfigInfo : IStylesResourcesInfo { } - -public interface IStylesResourceInfo : IResourceInfo, IResourceCultureInfo, IDataInfo, IPackageDependenciesInfo { } - -public interface IStylesResourcesInfo -{ - /// - /// Collection of loadable styles data. - /// - ImmutableArray Styles { get; } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IStylesInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IStylesInfo.cs deleted file mode 100644 index b3e2a456b..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IStylesInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Barotrauma.LuaCs.Data; - -public interface IStylesInfo -{ - -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs index 19649aea8..894641c17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/LuaCsSetup.cs @@ -22,9 +22,6 @@ namespace Barotrauma } private partial bool ShouldRunCs() => IsCsEnabled.Value; - - public IStylesManagementService StylesManagementService => _servicesProvider.TryGetService(out var svc) - ? svc : throw new NullReferenceException("Networking Manager service not found!"); public void CheckCsEnabled() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/ConfigService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/ConfigService.cs new file mode 100644 index 000000000..a628969b2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/ConfigService.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Immutable; +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; +using Barotrauma.Networking; +using FluentResults; + +namespace Barotrauma.LuaCs.Services; + +public partial class ConfigService +{ + public ImmutableArray GetDisplayableConfigs() + { + throw new NotImplementedException(); + } + + public ImmutableArray GetDisplayableConfigsForPackage(ContentPackage package) + { + throw new NotImplementedException(); + } + + public Result AddConfigControl(IConfigInfo configInfo) + { + throw new NotImplementedException(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IConfigService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IConfigService.cs index 89b2316a3..cbe7bf94d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IConfigService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IConfigService.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Services; using Barotrauma.Networking; @@ -8,28 +10,8 @@ namespace Barotrauma.LuaCs.Services; public partial interface IConfigService { - /* - * Immediate mode - */ - FluentResults.Result> AddConfigEntry(IDisplayableData data, - T defaultValue, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action> onValueChanged = null) where T : IConvertible, IEquatable; - - FluentResults.Result AddConfigList(IDisplayableData data, - int defaultIndex, IReadOnlyList values, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action onValueChanged = null); + ImmutableArray GetDisplayableConfigs(); + ImmutableArray GetDisplayableConfigsForPackage(ContentPackage package); - FluentResults.Result> AddConfigRangeEntry(IDisplayableData data, - T defaultValue, T minValue, T maxValue, - Func, int> getStepCount, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action> onValueChanged = null) where T : IConvertible, IEquatable; + FluentResults.Result AddConfigControl(IConfigInfo configInfo); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesManagementService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesManagementService.cs deleted file mode 100644 index dc5dfc69a..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesManagementService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Immutable; -using System.Threading.Tasks; -using Barotrauma.LuaCs.Data; - -namespace Barotrauma.LuaCs.Services; - -public interface IStylesManagementService : IReusableService -{ - Task LoadStylesAsync(ImmutableArray styles); - FluentResults.Result GetStylesService(ContentPackage package); - Task DisposeAllStyles(); - Task DisposeStylesForPackage(ContentPackage package); -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs deleted file mode 100644 index 26c8b2404..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Threading.Tasks; - -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 and path into a new instance. - /// - /// - /// - /// - Task LoadStylesFileAsync(ContentPackage package, ContentPath path); - - /// - /// Unloads all styles assets and instances. - /// - FluentResults.Result UnloadAllStyles(); - - /// - /// Tries to the get the 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 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 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 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 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/NetworkingService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/NetworkingService.cs index 597709cc5..654e2c817 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/NetworkingService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/NetworkingService.cs @@ -1,13 +1,14 @@ using Barotrauma.LuaCs.Services; using Barotrauma.Networking; using System; +using System.Collections.Concurrent; using System.Collections.Generic; namespace Barotrauma.LuaCs.Services; partial class NetworkingService : INetworkingService { - private Dictionary> receiveQueue = new Dictionary>(); + private ConcurrentDictionary> receiveQueue = new(); public void SendSyncMessage() { @@ -99,7 +100,7 @@ partial class NetworkingService : INetworkingService } else { - if (!receiveQueue.ContainsKey(id)) { receiveQueue[id] = new Queue(); } + if (!receiveQueue.ContainsKey(id)) { receiveQueue[id] = new ConcurrentQueue(); } receiveQueue[id].Enqueue(netMessage); if (GameSettings.CurrentConfig.VerboseLogging) diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageManagementService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageManagementService.cs deleted file mode 100644 index 7b722751a..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageManagementService.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using Barotrauma.LuaCs.Data; -using Barotrauma.LuaCs.Services.Processing; -using FluentResults; -// ReSharper disable UseCollectionExpression - -namespace Barotrauma.LuaCs.Services; - -public partial class PackageManagementService : IPackageManagementService -{ - public PackageManagementService( - IConverterServiceAsync modConfigParserService, - IProcessorService, IAssembliesResourcesInfo> assemblyInfoConverter, - IProcessorService, IConfigsResourcesInfo> configsInfoConverter, - IProcessorService, IConfigProfilesResourcesInfo> configProfilesConverter, - IProcessorService, ILocalizationsResourcesInfo> localizationsConverter, - IProcessorService, ILuaScriptsResourcesInfo> luaScriptsConverter, - IPackageInfoLookupService packageInfoLookupService, Func, IStylesResourcesInfo> stylesInfoConverter) - { - _stylesInfoConverter = stylesInfoConverter; - _modConfigParserService = modConfigParserService; - _assemblyInfoConverter = assemblyInfoConverter; - _configsInfoConverter = configsInfoConverter; - _configProfilesConverter = configProfilesConverter; - _localizationsConverter = localizationsConverter; - _luaScriptsConverter = luaScriptsConverter; - _packageInfoLookupService = packageInfoLookupService; - } - - private readonly Func, IStylesResourcesInfo> _stylesInfoConverter; - - public ImmutableArray Styles => _modInfos.IsEmpty ? ImmutableArray.Empty - : _modInfos.SelectMany(kvp => kvp.Value.Styles).ToImmutableArray(); - - public Result GetStylesInfos(ContentPackage package, bool onlySupportedResources = true) - { - ((IService)this).CheckDisposed(); - if (package is null) - return FluentResults.Result.Fail($"{nameof(GetStylesInfos)}: ContentPackage is null."); - if (_modInfos.TryGetValue(package, out var result)) - return FluentResults.Result.Ok(_stylesInfoConverter(onlySupportedResources? - result.Styles.Where(r => - (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 - && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() - : result.Styles - )); - return FluentResults.Result.Fail( - $"{nameof(GetStylesInfos)}: ContentPackage {package.Name} is not registered."); - } - - public Result GetStylesInfos(IReadOnlyList packages, bool onlySupportedResources = true) - { - ((IService)this).CheckDisposed(); - if (packages is null || packages.Count == 0) - return FluentResults.Result.Fail($"{nameof(GetStylesInfos)}: ContentPackage list is null or empty."); - var builder = ImmutableArray.CreateBuilder(); - foreach (var package in packages) - { - if (_modInfos.TryGetValue(package, out var result) && result.Styles is { IsEmpty: false }) - { - builder.AddRange(onlySupportedResources? - result.Styles.Where(r => - (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 - && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() - : result.Styles); - } - } - - return FluentResults.Result.Ok(_stylesInfoConverter(builder.MoveToImmutable())); - } - - public async Task> GetStylesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) - { - return await Task.Run(() => GetStylesInfos(packages, onlySupportedResources)); - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/ModConfigService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/ModConfigService.cs index 386a66940..0e46339e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/ModConfigService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/ModConfigService.cs @@ -14,26 +14,17 @@ public partial class ModConfigService private partial async Task> GetModConfigInfoAsync(ContentPackage package, XElement root) { var asm = root.GetChildElements("Assembly").ToImmutableArray(); - var loc = root.GetChildElements("Localization").ToImmutableArray(); var cfg = root.GetChildElements("Config").ToImmutableArray(); var lua = root.GetChildElements("Lua").ToImmutableArray(); - var stl = root.GetChildElements("Style").ToImmutableArray(); return FluentResults.Result.Ok(new ModConfigInfo() { Package = package, PackageName = package.Name, Assemblies = asm.Any() ? GetAssemblies(package, asm) : ImmutableArray.Empty, - Localizations = loc.Any() ? GetLocalizations(package, loc) : ImmutableArray.Empty, Configs = cfg.Any() ? GetConfigs(package, cfg) : ImmutableArray.Empty, ConfigProfiles = cfg.Any() ? GetConfigProfiles(package, cfg) : ImmutableArray.Empty, - LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray.Empty, - Styles = stl.Any() ? GetStyles(package, stl) : ImmutableArray.Empty + LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray.Empty }); } - - private ImmutableArray GetStyles(ContentPackage src, IEnumerable elements) - { - throw new NotImplementedException(); - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/StylesManagementService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/StylesManagementService.cs deleted file mode 100644 index 49bd12eb2..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Processing/StylesManagementService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Barotrauma.LuaCs.Services.Processing; - -public class StylesManagementService : IStylesManagementService -{ - -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs deleted file mode 100644 index 6e608a5fc..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Linq; -using FluentResults; -using FluentResults.LuaCs; - -namespace Barotrauma.LuaCs.Services; - -// TODO: Complete rewrite -public class StylesService : IStylesService -{ - private readonly ConcurrentDictionary _loadedProcessors = new(); - private readonly IStorageService _storageService; - private readonly ILoggerService _loggerService; - - public StylesService(IStorageService storageService, ILoggerService loggerService) - { - _storageService = storageService; - _loggerService = loggerService; - } - - - public async Task LoadStylesFileAsync(ContentPackage package, ContentPath path) - { - throw new NotImplementedException(); - } - - public FluentResults.Result UnloadAllStyles() - { - if (NoProcessorsLoaded) - return FluentResults.Result.Fail(new Error($"{nameof(StylesService)}.{nameof(UnloadAllStyles)}: No processors have been loaded.") - .WithMetadata(MetadataType.ExceptionObject, this)); - - foreach (var processor in _loadedProcessors) - { - processor.Value.UnloadFile(); - } - _loadedProcessors.Clear(); - return FluentResults.Result.Ok(); - } - - 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.IsEmpty; - - public void Dispose() - { - UnloadAllStyles(); - GC.SuppressFinalize(this); - } - - public bool IsDisposed { get; private set; } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/UIStyleProcessor.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/UIStyleProcessor.cs deleted file mode 100644 index b90276e7a..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/UIStyleProcessor.cs +++ /dev/null @@ -1,93 +0,0 @@ -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/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 37b5d6e5c..d7b368980 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -12,6 +12,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Xml.Linq; +using Barotrauma.LuaCs.Events; using Barotrauma.Sounds; namespace Barotrauma @@ -1532,7 +1533,7 @@ namespace Barotrauma { Select(enableAutoSave: true); - GameMain.LuaCs.CheckInitialize(); + GameMain.LuaCs.EventService.PublishEvent(sub => sub.OnScreenSelected(this)); } public void Select(bool enableAutoSave = true) diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageManagementService.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageManagementService.cs index 435facc4d..99e66743e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageManagementService.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageManagementService.cs @@ -11,7 +11,6 @@ public partial class PackageManagementService IProcessorService, IAssembliesResourcesInfo> assemblyInfoConverter, IProcessorService, IConfigsResourcesInfo> configsInfoConverter, IProcessorService, IConfigProfilesResourcesInfo> configProfilesConverter, - IProcessorService, ILocalizationsResourcesInfo> localizationsConverter, IProcessorService, ILuaScriptsResourcesInfo> luaScriptsConverter, IPackageInfoLookupService packageInfoLookupService) { @@ -19,7 +18,6 @@ public partial class PackageManagementService _assemblyInfoConverter = assemblyInfoConverter; _configsInfoConverter = configsInfoConverter; _configProfilesConverter = configProfilesConverter; - _localizationsConverter = localizationsConverter; _luaScriptsConverter = luaScriptsConverter; _packageInfoLookupService = packageInfoLookupService; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/Processing/ModConfigService.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/Processing/ModConfigService.cs index ee93eccf5..3c59ac10e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/Processing/ModConfigService.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/Processing/ModConfigService.cs @@ -15,7 +15,6 @@ public partial class ModConfigService private partial async Task> GetModConfigInfoAsync(ContentPackage package, XElement root) { var asm = root.GetChildElements("Assembly").ToImmutableArray(); - var loc = root.GetChildElements("Localization").ToImmutableArray(); var cfg = root.GetChildElements("Config").ToImmutableArray(); var lua = root.GetChildElements("Lua").ToImmutableArray(); @@ -24,7 +23,6 @@ public partial class ModConfigService Package = package, PackageName = package.Name, Assemblies = asm.Any() ? GetAssemblies(package, asm) : ImmutableArray.Empty, - Localizations = loc.Any() ? GetLocalizations(package, loc) : ImmutableArray.Empty, Configs = cfg.Any() ? GetConfigs(package, cfg) : ImmutableArray.Empty, ConfigProfiles = cfg.Any() ? GetConfigProfiles(package, cfg) : ImmutableArray.Empty, LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray.Empty diff --git a/Barotrauma/BarotraumaShared/Lua/Content/ModConfig.xml b/Barotrauma/BarotraumaShared/Lua/Content/ModConfig.xml new file mode 100644 index 000000000..e69de29bb diff --git a/Barotrauma/BarotraumaShared/Lua/Content/Schemas/IModConfig.xsd b/Barotrauma/BarotraumaShared/Lua/Content/Schemas/IModConfig.xsd new file mode 100644 index 000000000..1908c9960 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Lua/Content/Schemas/IModConfig.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/ConfigEntry.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/ConfigEntry.cs new file mode 100644 index 000000000..4fd22a1cb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/ConfigEntry.cs @@ -0,0 +1,103 @@ +using System; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Services; +using Barotrauma.Networking; +using OneOf; + +namespace Barotrauma.LuaCs.Configuration; + +public class ConfigEntry : IConfigEntry where T : IEquatable +{ + + private readonly Action, INetReadMessage> _readMessageHandler; + private readonly Action, INetWriteMessage> _writeMessageHandler; + + public ConfigEntry(IConfigInfo configInfo, Action, INetReadMessage> readMessageHandler, + Action, INetWriteMessage> writeMessageHandler) + { + _readMessageHandler = readMessageHandler; + _writeMessageHandler = writeMessageHandler; + } + + public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } + + public bool Equals(IConfigBase other) + { + if (ReferenceEquals(this, other)) + return true; + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public Type GetValueType() + { + throw new NotImplementedException(); + } + + public string GetValue() + { + throw new NotImplementedException(); + } + + public bool TrySetValue(OneOf value) + { + throw new NotImplementedException(); + } + + public bool IsAssignable(OneOf value) + { + throw new NotImplementedException(); + } + + private event Action> _onValueChanged; + public event Action> OnValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + + event Action IConfigBase.OnValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + + public OneOf GetSerializableValue() + { + throw new NotImplementedException(); + } + + public Guid InstanceId => throw new NotImplementedException(); + + public NetSync SyncType => throw new NotImplementedException(); + + public ClientPermissions WritePermissions => throw new NotImplementedException(); + + public void ReadNetMessage(INetReadMessage message) + { + throw new NotImplementedException(); + } + + public void WriteNetMessage(INetWriteMessage message) + { + throw new NotImplementedException(); + } + + public T Value => throw new NotImplementedException(); + + public bool TrySetValue(T value) + { + throw new NotImplementedException(); + } + + public bool IsAssignable(T value) + { + throw new NotImplementedException(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/ConfigList.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/ConfigList.cs new file mode 100644 index 000000000..d67d09e7d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/ConfigList.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Services; +using Barotrauma.Networking; +using OneOf; + +namespace Barotrauma.LuaCs.Configuration; + +public class ConfigList : IConfigList where T : IEquatable +{ + private readonly Action, INetReadMessage> _readMessageHandler; + private readonly Action, INetWriteMessage> _writeMessageHandler; + + public ConfigList(IConfigInfo configInfo, Action, INetReadMessage> readMessageHandler, + Action, INetWriteMessage> writeMessageHandler) + { + _readMessageHandler = readMessageHandler; + _writeMessageHandler = writeMessageHandler; + } + + public string InternalName => throw new NotImplementedException(); + + public ContentPackage OwnerPackage => throw new NotImplementedException(); + + public bool Equals(IConfigBase other) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public Type GetValueType() + { + throw new NotImplementedException(); + } + + public string GetValue() + { + throw new NotImplementedException(); + } + + public bool TrySetValue(OneOf value) + { + throw new NotImplementedException(); + } + + public bool IsAssignable(OneOf value) + { + throw new NotImplementedException(); + } + + private event Action> _onValueChanged; + + public event Action> OnValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + + event Action> IConfigEntry.OnValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + + event Action IConfigBase.OnValueChanged + { + add => _onValueChanged += value; + remove => _onValueChanged -= value; + } + + public T Value => throw new NotImplementedException(); + + public bool TrySetValue(T value) + { + throw new NotImplementedException(); + } + + public bool IsAssignable(T value) + { + throw new NotImplementedException(); + } + + public OneOf GetSerializableValue() + { + throw new NotImplementedException(); + } + + public Guid InstanceId => throw new NotImplementedException(); + + public NetSync SyncType => throw new NotImplementedException(); + + public ClientPermissions WritePermissions => throw new NotImplementedException(); + + public void ReadNetMessage(INetReadMessage message) + { + throw new NotImplementedException(); + } + + public void WriteNetMessage(INetWriteMessage message) + { + throw new NotImplementedException(); + } + + public IReadOnlyList Options => throw new NotImplementedException(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs index 87334e592..8e65f68b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs @@ -1,20 +1,17 @@ using System; +using System.Xml.Linq; using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Services; using Barotrauma.Networking; namespace Barotrauma.LuaCs.Configuration; -public partial interface IConfigBase : IVarId +public partial interface IConfigBase : IDataInfo, IEquatable, IDisposable { - bool IsInitialized { get; } - string GetValue(); - bool TrySetValue(string value); - bool IsAssignable(string value); Type GetValueType(); - void Initialize(IVarId id, string defaultValue); -} - -public interface IVarId : IDataInfo -{ - Guid InstanceId { get; } + string GetValue(); + bool TrySetValue(OneOf.OneOf value); + bool IsAssignable(OneOf.OneOf value); + event Action OnValueChanged; + OneOf.OneOf GetSerializableValue(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs index a39032258..9b173233a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs @@ -3,10 +3,10 @@ using Barotrauma.LuaCs.Services; namespace Barotrauma.LuaCs.Configuration; -public interface IConfigEntry : IConfigBase, INetVar where T : IConvertible, IEquatable +public interface IConfigEntry : IConfigBase, INetworkSyncEntity where T : IEquatable { T Value { get; } bool TrySetValue(T value); bool IsAssignable(T value); - void Initialize(IVarId id, T defaultValue); + new event Action> OnValueChanged; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEnum.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEnum.cs new file mode 100644 index 000000000..742f80a46 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEnum.cs @@ -0,0 +1,9 @@ +using System; +using Barotrauma.LuaCs.Services; + +namespace Barotrauma.LuaCs.Configuration; + +public interface IConfigEnum : IConfigBase, INetworkSyncEntity +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigList.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigList.cs index 0d291f215..d78b5779d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigList.cs @@ -1,8 +1,11 @@ +using System; +using System.Collections.Generic; using Barotrauma.LuaCs.Services; namespace Barotrauma.LuaCs.Configuration; -public interface IConfigList : IConfigBase, INetVar +public interface IConfigList : IConfigEntry, INetworkSyncEntity where T : IEquatable { - + IReadOnlyList Options { get; } + new event Action> OnValueChanged; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs similarity index 86% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs index 6dd98c625..790c7c9cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs @@ -5,7 +5,10 @@ using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Xml.Linq; +using Barotrauma.LuaCs.Services; using Barotrauma.Steam; +using OneOf; namespace Barotrauma.LuaCs.Data; @@ -16,7 +19,6 @@ public partial record ModConfigInfo : IModConfigInfo public ContentPackage Package { get; init; } public string PackageName { 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; } @@ -24,10 +26,9 @@ public partial record ModConfigInfo : IModConfigInfo #endregion -#region DataContracts +#region DataContracts_Resources public record AssemblyResourcesInfo(ImmutableArray Assemblies) : IAssembliesResourcesInfo; -public record LocalizationResourcesInfo(ImmutableArray Localizations) : ILocalizationsResourcesInfo; public record LuaScriptsResourcesInfo(ImmutableArray LuaScripts) : ILuaScriptsResourcesInfo; public record ConfigResourcesInfo(ImmutableArray Configs) : IConfigsResourcesInfo; public record ConfigProfilesResourcesInfo(ImmutableArray ConfigProfiles) : IConfigProfilesResourcesInfo; @@ -161,20 +162,7 @@ public record ConfigProfileResourceInfo : IConfigProfileResourceInfo public ContentPackage OwnerPackage { get; init; } } -public record LocalizationResourceInfo : ILocalizationResourceInfo -{ - public string InternalName { get; init; } - 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 readonly struct LuaScriptScriptResourceInfo : ILuaScriptResourceInfo +public readonly struct LuaScriptsResourceInfo : ILuaScriptResourceInfo { public ContentPackage OwnerPackage { get; init; } public Platform SupportedPlatforms { get; init; } @@ -189,3 +177,34 @@ public readonly struct LuaScriptScriptResourceInfo : ILuaScriptResourceInfo } #endregion + +#region DataContracts_ParsedInfo + +public record ConfigInfo : IConfigInfo +{ + public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } + public Type DataType { get; init; } + public OneOf DefaultValue { get; init; } + public OneOf Value { get; init; } + public RunState EditableStates { get; init; } + public NetSync NetSync { get; init; } + +#if CLIENT // IConfigDisplayInfo + public string DisplayName { get; init; } + public string Description { get; init; } + public string DisplayCategory { get; init; } + public bool ShowInMenus { get; init; } + public string Tooltip { get; init; } + public string ImageIconPath { get; init; } +#endif +} + +public record ConfigProfileInfo : IConfigProfileInfo +{ + public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } + public IReadOnlyList<(string ConfigName, OneOf Value)> ProfileValues { get; init; } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs index cfa46a629..4d8a5e5ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs @@ -1,51 +1,43 @@ using System; +using System.Xml.Linq; using Barotrauma.LuaCs.Services; using Barotrauma.Networking; namespace Barotrauma.LuaCs.Data; -// TODO: Finish +/// +/// Parsed data from a configuration xml. +/// public partial interface IConfigInfo : IDataInfo { /// - /// Specifies the data type this should be initialized to (ie. string, int, vector, etc.) - /// Custom types can be registered by mods. + /// Specifies the type initializer that will be used to instantiate the config var. /// Type DataType { get; } /// - /// String version of the default value. + /// The default value. /// - string DefaultValue { get; } + OneOf.OneOf DefaultValue { get; } /// - /// The value the last time this config was saved, if found in /data/. + /// The value the last time this config was saved. If not found, returns the default value. + ///
[If(Type='')]
+ /// The value is from the 'Value' Attribute. Typically used for types with single/simple values, such as primitives. + ///
[If(Type='')]
+ /// The value is from the first 'Value' child element. Typically used with complex config types, such as range and list. ///
- string StoredValue { get; } + OneOf.OneOf Value { get; } /// - /// Custom data storage for other type-specific information needed. IE. Used to store the min, - /// max and step values for the IConfigRangeEntry(T). + /// In what (s) is this config editable. Will be editable in the selected state, and lower value states. + ///

+ /// [Important]
Setting this to value lower than 'Configuration` will render this config read-only. + ///

Expected Behaviour: + ///
[|]: Read-Only. + ///
[]: Can only be changed at the Main Menu (not in a lobby). + ///
[]: Can be changed at the Main Menu and while a lobby is active. ///
- string CustomParameters { get; } - /// - /// [Multiplayer]
- /// What permissions do clients require to change this setting. - ///
- ClientPermissions RequiredPermissions { get; } - /// - /// In what s is this config editable. - ///
- /// Note: Setting this to value lower than 'Configuration` will render this config read-only. - ///
- RunState CanEditStates { get; } + RunState EditableStates { get; } /// /// Network synchronization rules for this config. /// NetSync NetSync { get; } - /// - /// User friendly name or Localization Token. - /// - string DisplayName { get; } - /// - /// User friendly description or Localization Token. - /// - string Description { get; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs index 7be1db56c..d60fb2772 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigProfileInfo.cs @@ -1,6 +1,9 @@ -namespace Barotrauma.LuaCs.Data; +using System.Collections.Generic; +using System.Xml.Linq; -public interface IConfigProfileInfo +namespace Barotrauma.LuaCs.Data; + +public interface IConfigProfileInfo : IDataInfo { - + IReadOnlyList<(string ConfigName, OneOf.OneOf Value)> ProfileValues { get; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs deleted file mode 100644 index b74b1f275..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; - -namespace Barotrauma.LuaCs.Data; - -public interface ILocalizationInfo : IDataInfo -{ - string Symbol { get; } - IReadOnlyDictionary LocalizedValues { get; } - RawLString GetLocalizedString(CultureInfo locale); - RawLString GetLocalizedString(string cultureCode); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs index 7d60bf562..cdecd9037 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs @@ -3,7 +3,7 @@ namespace Barotrauma.LuaCs.Data; public partial interface IModConfigInfo : IAssembliesResourcesInfo, - ILocalizationsResourcesInfo, ILuaScriptsResourcesInfo, IConfigsResourcesInfo, + ILuaScriptsResourcesInfo, IConfigsResourcesInfo, IConfigProfilesResourcesInfo { // package info diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs index ad6bb45d3..777bd24d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -6,7 +6,6 @@ namespace Barotrauma.LuaCs.Data; public interface IConfigResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IDataInfo { } public interface IConfigProfileResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IDataInfo { } -public interface ILocalizationResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IDataInfo { } /// /// Represents loadable Lua files. @@ -40,11 +39,6 @@ public interface IAssembliesResourcesInfo ImmutableArray Assemblies { get; } } -public interface ILocalizationsResourcesInfo -{ - ImmutableArray Localizations { get; } -} - public interface ILuaScriptsResourcesInfo { ImmutableArray LuaScripts { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs new file mode 100644 index 000000000..f3e3bab2d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.AccessControl; +using Barotrauma.Networking; +using FluentResults; + +namespace Barotrauma.LuaCs.Data; + + +// --- Storage Service +public interface IStorageServiceConfig +{ + string LocalModsDirectory { get; } + string WorkshopModsDirectory { get; } + string GameSettingsConfigPath { get; } +#if CLIENT + string TempDownloadsDirectory { get; } +#endif + + //ReadOnlyCollection SafeIOReadDirectories { get; } + //ReadOnlyCollection SafeIOWriteDirectories { get; } + IEnumerable GlobalIOReadWhitelist(); + IEnumerable GlobalIOWriteWhitelist(); + + bool IOReadWhiteListContains(string filePath); + bool IOWriteWhiteListContains(string filePath); + + string LocalDataSavePath { get; } + string LocalDataPathRegex { get; } + string LocalPackageDataPath { get; } + public string RunLocation { get; } + bool GlobalSafeIOEnabled { get; } +} + +internal interface IStorageServiceConfigUpdate +{ + public FluentResults.Result SetSafeReadFilePaths(string[] filePaths); + public FluentResults.Result SetSafeWriteFilePaths(string[] filePaths); +} + +public record StorageServiceConfig : IStorageServiceConfig, IStorageServiceConfigUpdate +{ + private static readonly string ExecutionLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location.CleanUpPath()); + + public string LocalModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath(); + public string WorkshopModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath(); + public string GameSettingsConfigPath { get; init; } = System.IO.Path.GetFullPath( + string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) + ? SaveUtil.DefaultSaveFolder + : GameSettings.CurrentConfig.SavePath).CleanUpPath(); +#if CLIENT + public string TempDownloadsDirectory { get; init; } = System.IO.Path.GetFullPath(ModReceiver.DownloadFolder).CleanUpPath(); +#endif + + private readonly AsyncReaderWriterLock _safeIOReadLock = new(); + private readonly AsyncReaderWriterLock _safeIOWriteLock = new(); + private readonly ConcurrentDictionary _safeIOReadFilePaths = new(); + + private readonly ConcurrentDictionary _safeIOWriteFilePaths = new(); + + public IEnumerable GlobalIOReadWhitelist() + { + using var lck = _safeIOReadLock.AcquireReaderLock().GetAwaiter().GetResult(); + + if (_safeIOReadFilePaths.Count == 0) + { + yield break; + } + + foreach (var path in _safeIOReadFilePaths) + { + yield return path.Key; + } + } + + public IEnumerable GlobalIOWriteWhitelist() + { + using var lck = _safeIOWriteLock.AcquireReaderLock().GetAwaiter().GetResult(); + + if (_safeIOWriteFilePaths.Count == 0) + { + yield break; + } + + foreach (var path in _safeIOWriteFilePaths) + { + yield return path.Key; + } + } + + public bool IOReadWhiteListContains(string filePath) + { + if (filePath.IsNullOrWhiteSpace()) + return false; + return _safeIOReadFilePaths.ContainsKey(filePath); + } + + public bool IOWriteWhiteListContains(string filePath) + { + if (filePath.IsNullOrWhiteSpace()) + return false; + return _safeIOWriteFilePaths.ContainsKey(filePath); + } + + public string LocalDataSavePath => Path.Combine(ExecutionLocation, "/Data/Mods/"); + + public string LocalDataPathRegex => ""; + + public string RunLocation => ExecutionLocation; + public bool GlobalSafeIOEnabled => false; + + public string LocalPackageDataPath + { + get + { + return ContainsIllegalPaths(LocalDataSavePath) ? $"/Data/Mods/{LocalDataPathRegex}" + : Path.Combine(LocalDataSavePath, LocalDataPathRegex); + + bool ContainsIllegalPaths(string path) + { + throw new NotImplementedException(); + } + } + } + + + public FluentResults.Result SetSafeReadFilePaths(string[] filePaths) + { + using var lck = _safeIOReadLock.AcquireWriterLock().GetAwaiter().GetResult(); + return SetSafeDirectory(_safeIOReadFilePaths, filePaths); + } + + public FluentResults.Result SetSafeWriteFilePaths(string[] filePaths) + { + using var lck = _safeIOWriteLock.AcquireWriterLock().GetAwaiter().GetResult(); + return SetSafeDirectory(_safeIOWriteFilePaths, filePaths); + } + + private FluentResults.Result SetSafeDirectory(ConcurrentDictionary target, string[] filePaths) + { + if (filePaths is null || filePaths.Length < 1) + { + target.Clear(); + return FluentResults.Result.Ok(); + } + + FluentResults.Result result = new(); + + target.Clear(); + foreach (string path in filePaths) + { + if (path.IsNullOrWhiteSpace()) + { + result = result.WithError($"ServicesConfigData: A supplied whitelist path was null."); + continue; + } + + try + { + var path2 = Path.GetFullPath(path); + target.TryAdd(path2, 0); + } + catch (Exception e) + { + result = result.WithError( + new ExceptionalError(e).WithMetadata(FluentResults.LuaCs.MetadataType.ExceptionObject, this)); + continue; + } + } + + return result.WithSuccess($"Whitelist updated."); + } +} + +// --- Config Service +public interface IConfigServiceConfig +{ + string LocalConfigPathPartial { get; } + string FileNamePattern { get; } +} + +public record ConfigServiceConfig : IConfigServiceConfig +{ + public string LocalConfigPathPartial => $"/Config/{FileNamePattern}.xml"; + public string FileNamePattern => ""; +} + + +// --- Lua Scripts Service +public interface ILuaScriptServicesConfig +{ + bool SafeLuaIOEnabled { get; } + bool UseCaching { get; } +} + +public record LuaScriptServicesConfig : ILuaScriptServicesConfig +{ + public bool SafeLuaIOEnabled => true; + public bool UseCaching => true; +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs index 36d8d9b82..ffefc003c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Barotrauma.LuaCs.Configuration; using Barotrauma.LuaCs.Services; using Barotrauma.Networking; using Dynamitey; @@ -60,6 +61,11 @@ internal interface IEventReloadAllPackages : IEvent void OnReloadAllPackages(); } +internal interface IEventConfigVarInstanced : IEvent +{ + void OnConfigCreated(IConfigBase config); +} + #endregion #region GameEvents diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs deleted file mode 100644 index 212c70ae5..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaScriptLoader.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.IO; -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Loaders; -using System.Linq; - -namespace Barotrauma -{ - class LuaScriptLoader : ScriptLoaderBase - { - - public override object LoadFile(string file, Table globalContext) - { - if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return null; - - return File.ReadAllText(file); - } - - public override bool ScriptFileExists(string file) - { - if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return false; - - return File.Exists(file); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index c409ac4d3..ae24a2efc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -44,15 +44,11 @@ namespace Barotrauma _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); -#if CLIENT - _servicesProvider.RegisterServiceType(ServiceLifetime.Transient); - _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); -#endif _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); - // TODO: ILocalizationService + // TODO: IConfigService // TODO: INetworkingService // TODO: [Resource Converter/Parser Services] @@ -61,13 +57,12 @@ namespace Barotrauma _servicesProvider.RegisterServiceType, IAssembliesResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType, IConfigsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType, IConfigProfilesResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient); - _servicesProvider.RegisterServiceType, ILocalizationsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType, ILuaScriptsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient); + // Loaders and Processors (yes the naming is reversed, oops). _servicesProvider.RegisterServiceType, ModConfigService>(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType, ModConfigService>(ServiceLifetime.Transient); - - + _servicesProvider.RegisterServiceType(ServiceLifetime.Transient); _servicesProvider.Compile(); } @@ -121,8 +116,6 @@ namespace Barotrauma ? svc : throw new NullReferenceException("Plugin Manager service not found!"); public ILuaScriptManagementService LuaScriptManagementService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Lua Script Manager service not found!"); - public ILocalizationService LocalizationService => _servicesProvider.TryGetService(out var svc) - ? svc : throw new NullReferenceException("Localization Manager service not found!"); public INetworkingService NetworkingService => _servicesProvider.TryGetService(out var svc) ? svc : throw new NullReferenceException("Networking Manager service not found!"); public IEventService EventService => _servicesProvider.TryGetService(out var svc) @@ -179,6 +172,11 @@ namespace Barotrauma /// TODO: @evilfactory@users.noreply.github.com /// public IConfigEntry RestrictMessageSize { get; private set; } + + /// + /// The local save path for all local data storage for mods. + /// + public IConfigEntry LocalDataSavePath { get; private set; } /** * == Ops Vars @@ -287,11 +285,7 @@ namespace Barotrauma PluginManagementService.Dispose(); LuaScriptManagementService.Dispose(); -#if CLIENT - StylesManagementService.Dispose(); -#endif ConfigService.Dispose(); - LocalizationService.Dispose(); PackageManagementService.Dispose(); // TODO: Add all missing services. //NetworkingService.Dispose(); @@ -372,12 +366,7 @@ namespace Barotrauma while (_toUnload.TryDequeue(out var cp)) { LuaScriptManagementService.DisposePackageResources(cp); - ConfigService.DisposeConfigsProfiles(cp); - ConfigService.DisposeConfigs(cp); -#if CLIENT - StylesManagementService.DisposeStylesForPackage(cp); -#endif - LocalizationService.DisposePackage(cp); + ConfigService.DisposePackageData(cp); PackageManagementService.DisposePackageInfos(cp); } @@ -515,23 +504,22 @@ namespace Barotrauma void LoadLuaCsConfig() { - IsCsEnabled = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "IsCsEnabled") - ?? throw new NullReferenceException($"{nameof(IsCsEnabled)} cannot be loaded."); - TreatForcedModsAsNormal = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "TreatForcedModsAsNormal") - ?? throw new NullReferenceException($"{nameof(TreatForcedModsAsNormal)} cannot be loaded."); - DisableErrorGUIOverlay = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "DisableErrorGUIOverlay") - ?? throw new NullReferenceException($"{nameof(DisableErrorGUIOverlay)} cannot be loaded."); - HideUserNamesInLogs = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "HideUserNamesInLogs") - ?? throw new NullReferenceException($"{nameof(HideUserNamesInLogs)} cannot be loaded."); - LuaForBarotraumaSteamId = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "LuaForBarotraumaSteamId") - ?? throw new NullReferenceException($"{nameof(LuaForBarotraumaSteamId)} cannot be loaded."); - CsForBarotraumaSteamId = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "CsForBarotraumaSteamId") - ?? throw new NullReferenceException($"{nameof(CsForBarotraumaSteamId)} cannot be loaded."); - RestrictMessageSize = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "RestrictMessageSize") - ?? throw new NullReferenceException($"{nameof(RestrictMessageSize)} cannot be loaded."); - ReloadPackagesOnLobbyStart = ConfigService.GetConfig>(ContentPackageManager.VanillaCorePackage, "ReloadPackagesOnLobbyStart") - ?? throw new NullReferenceException($"{nameof(ReloadPackagesOnLobbyStart)} cannot be loaded."); - + IsCsEnabled = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "IsCsEnabled", out var val1) ? val1 + : throw new NullReferenceException($"{nameof(IsCsEnabled)} cannot be loaded."); + TreatForcedModsAsNormal = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "TreatForcedModsAsNormal", out var val2) ? val2 + : throw new NullReferenceException($"{nameof(TreatForcedModsAsNormal)} cannot be loaded."); + DisableErrorGUIOverlay = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "DisableErrorGUIOverlay", out var val3) ? val3 + : throw new NullReferenceException($"{nameof(DisableErrorGUIOverlay)} cannot be loaded."); + HideUserNamesInLogs = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "HideUserNamesInLogs", out var val4) ? val4 + : throw new NullReferenceException($"{nameof(HideUserNamesInLogs)} cannot be loaded."); + LuaForBarotraumaSteamId = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "LuaForBarotraumaSteamId", out var val5) ? val5 + : throw new NullReferenceException($"{nameof(LuaForBarotraumaSteamId)} cannot be loaded."); + CsForBarotraumaSteamId = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "CsForBarotraumaSteamId", out var val6) ? val6 + : throw new NullReferenceException($"{nameof(CsForBarotraumaSteamId)} cannot be loaded."); + RestrictMessageSize = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "RestrictMessageSize", out var val7) ? val7 + : throw new NullReferenceException($"{nameof(RestrictMessageSize)} cannot be loaded."); + ReloadPackagesOnLobbyStart = ConfigService.TryGetConfig>(ContentPackageManager.VanillaCorePackage, "ReloadPackagesOnLobbyStart", out var val8) ? val8 + : throw new NullReferenceException($"{nameof(ReloadPackagesOnLobbyStart)} cannot be loaded."); } void DisposeLuaCsConfig() @@ -548,27 +536,14 @@ namespace Barotrauma async Task LoadStaticAssetsAsync(IReadOnlyList packages) { - var locRes = ImmutableArray.Empty; var cfgRes = ImmutableArray.Empty; var cfpRes = ImmutableArray.Empty; var luaRes = ImmutableArray.Empty; - -#if CLIENT - var styleRes = ImmutableArray.Empty; -#endif var tasksBuilder = ImmutableArray.CreateBuilder(); //---- get resource infos - tasksBuilder.AddRange(new Func(async () => - { - var res = await PackageManagementService.GetLocalizationsInfosAsync(packages); - if (res.IsSuccess) - locRes = res.Value.Localizations; - if (res.Errors.Any()) - ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), - res.ToResult()); - })(), + tasksBuilder.AddRange( new Func(async () => { var res = await PackageManagementService.GetConfigsInfosAsync(packages); @@ -596,18 +571,7 @@ namespace Barotrauma ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), res.ToResult()); })()); - -#if CLIENT - tasksBuilder.Add(new Func(async () => - { - var res = await PackageManagementService.GetStylesInfosAsync(packages); - if (res.IsSuccess) - styleRes = res.Value.Styles; - if (res.Errors.Any()) - ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), - res.ToResult()); - })()); -#endif + await Task.WhenAll(tasksBuilder.MoveToImmutable()); tasksBuilder.Clear(); @@ -628,23 +592,6 @@ namespace Barotrauma Logger.LogResults(res); })()); -#if CLIENT - tasksBuilder.Add(new Func(async () => - { - var res = await StylesManagementService.LoadStylesAsync(styleRes); - if (res.Errors.Any()) - Logger.LogResults(res); - })()); -#endif - - // load localizations first - if (!locRes.IsDefaultOrEmpty) - { - var res = await LocalizationService.LoadLocalizations(locRes); - if (res.Errors.Any()) - Logger.LogResults(res); - } - await Task.WhenAll(tasksBuilder.MoveToImmutable()); } @@ -706,22 +653,18 @@ namespace Barotrauma } //lua - var luaRes = PackageManagementService.GetLuaScriptsInfos(PackageManagementService - .GetAllLoadedPackages() + var luaRes = PackageManagementService.LuaScripts + .Select(ls => ls.OwnerPackage) + .Where(p => p is not null) .Where(ContentPackageManager.EnabledPackages.All.Contains) - .ToList()); - if (luaRes.IsFailed) + .ToImmutableArray(); + if (luaRes.IsDefaultOrEmpty) { Logger.LogError($"{nameof(RunScripts)}: Failed to get enabled lua script resources!"); - Logger.LogResults(luaRes.ToResult()); return; } - if (luaRes.Errors.Any()) - Logger.LogResults(luaRes.ToResult()); - - - LuaScriptManagementService.ExecuteLoadedScripts(luaRes.Value.LuaScripts); + LuaScriptManagementService.ExecuteLoadedScriptsForPackages(luaRes); if (CurrentRunState < RunState.Running) _runState = RunState.Running; @@ -748,10 +691,6 @@ namespace Barotrauma PluginManagementService.Reset(); LuaScriptManagementService.Reset(); ConfigService.Reset(); -#if CLIENT - StylesManagementService.Reset(); -#endif - LocalizationService.Reset(); if (CurrentRunState >= RunState.Configuration) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs index ad34c723d..fc30ce5db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs @@ -567,8 +567,17 @@ namespace FluentResults.LuaCs public static class MetadataType { public static string ExceptionDetails = nameof(ExceptionDetails); + /// + /// The object that threw the exception. + /// public static string ExceptionObject = nameof(ExceptionObject); + /// + /// The parameter-object responsible for the exception thrown (not the exception thrower). + /// public static string RootObject = nameof(RootObject); + /// + /// Additional exception sources. + /// public static string Sources = nameof(Sources); public static string StackTrace = nameof(StackTrace); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs deleted file mode 100644 index a907b18fa..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Barotrauma.LuaCs.Configuration; -using Barotrauma.LuaCs.Data; -using Barotrauma.LuaCs.Services; -using Barotrauma.Networking; - -namespace Barotrauma.LuaCs.Services; - -public interface INetVar : IVarId -{ - /// - /// 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); -} - -public enum NetSync -{ - None, TwoWay, ServerAuthority, ClientOneWay -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetworkSyncEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetworkSyncEntity.cs new file mode 100644 index 000000000..e0cb5713d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetworkSyncEntity.cs @@ -0,0 +1,66 @@ +using System; +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Services; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Services; + +public interface INetworkSyncEntity +{ + /// + /// Network-synchronized object ID. Used for networking send/receive message events. + /// + Guid InstanceId { get; } + + /// + /// Synchronization type. See for more information. + /// + NetSync SyncType { get; } + + /// + /// Permissions needed by clients to send net-events and/or receive net messages. + /// + ClientPermissions WritePermissions { get; } + + /// + /// Called when an incoming net message has data for this network object, typically from the same entity on another + /// machine. + /// + /// Wrapper for the internal type: + void ReadNetMessage(INetReadMessage message); + + /// + /// Called when a network send-event involving this entity is triggered. Any data expected to be read by the recipient + /// network object on the other instance(s) should be written to the packet. + /// + /// Wrapper for the internal type: + void WriteNetMessage(INetWriteMessage message); +} + +/// +/// Specifies the networking send/receive relationship for network object. Objects implementing this interface are +/// expected to adhere to the contract or de-sync may occur. +/// +public enum NetSync +{ + /// + /// No network synchronization. + /// + None, + /// + /// Both the client and the server have 'send' and 'receive' permissions (limited by ). Can also be used to allow two-way communication + /// with the server. + /// + TwoWay, + /// + /// Only the host/server has the authority to change this value. + /// + ServerAuthority, + /// + /// Only clients (with the required by ) may change the value and all value changes are communicated to the server/host. + ///

[Important] The host/server will not send the value to other connected clients.
+ /// Intended to allow clients to send one-way messages to the server. + ///
+ ClientOneWay +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsUtility.cs index b5db0066c..4e1bde042 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsUtility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsUtility.cs @@ -2,5 +2,9 @@ public interface ILuaCsUtility : ILuaCsShim { + public bool CanReadFromPath(string file); + public bool CanWriteToPath(string file); + internal bool IsPathAllowedException(string path, bool write = true, + LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigInitializers.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigInitializers.cs new file mode 100644 index 000000000..d3bad9e9d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigInitializers.cs @@ -0,0 +1,340 @@ +using System; +using System.Numerics; +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.Xna.Framework; +using Vector2 = Microsoft.Xna.Framework.Vector2; +using Vector3 = Microsoft.Xna.Framework.Vector3; +using Vector4 = Microsoft.Xna.Framework.Vector4; + +namespace Barotrauma.LuaCs.Services; + +public class ConfigInitializers : IService +{ + // parameterless .ctor + public ConfigInitializers() + { + } + + public void Dispose() + { + // stateless service + return; + } + + // stateless service + public bool IsDisposed => false; + + private Result> CreateConfigEntry(IConfigInfo configInfo, + Action, INetReadMessage> readHandler, + Action, INetWriteMessage> writeHandler) + where T : IEquatable + { + try + { + var ice = new ConfigEntry(configInfo, readHandler, writeHandler); + return FluentResults.Result.Ok>(ice); + } + catch (Exception e) + { + return FluentResults.Result.Fail($"Error while initializing config var: {configInfo?.OwnerPackage} - {configInfo?.InternalName}") + .WithError(new ExceptionalError(e)); + } + } + + private Result> CreateConfigList(IConfigInfo configInfo, + Action, INetReadMessage> readHandler, Action, INetWriteMessage> writeHandler) + where T : IEquatable + { + try + { + var icl = new ConfigList(configInfo, readHandler, writeHandler); + return FluentResults.Result.Ok>(icl); + } + catch (Exception e) + { + return FluentResults.Result.Fail($"Error while initializing config var: {configInfo?.OwnerPackage} - {configInfo?.InternalName}") + .WithError(new ExceptionalError(e)); + } + } + + public void RegisterTypeInitializers(IConfigService configService) + { + if (configService == null) + throw new ArgumentNullException($"{nameof(RegisterTypeInitializers)}: {nameof(IConfigService)} is null."); + + configService.RegisterTypeInitializer>(this.CreateConfigBool); + configService.RegisterTypeInitializer>(this.CreateConfigSbyte); + configService.RegisterTypeInitializer>(this.CreateConfigByte); + configService.RegisterTypeInitializer>(this.CreateConfigShort); + configService.RegisterTypeInitializer>(this.CreateConfigUShort); + configService.RegisterTypeInitializer>(this.CreateConfigInt32); + configService.RegisterTypeInitializer>(this.CreateConfigUInt32); + configService.RegisterTypeInitializer>(this.CreateConfigInt64); + configService.RegisterTypeInitializer>(this.CreateConfigUInt64); + configService.RegisterTypeInitializer>(this.CreateConfigFloat32); + configService.RegisterTypeInitializer>(this.CreateConfigFloat64); + configService.RegisterTypeInitializer>(this.CreateConfigFloat128); + configService.RegisterTypeInitializer>(this.CreateConfigChar); + configService.RegisterTypeInitializer>(this.CreateConfigString); + configService.RegisterTypeInitializer>(this.CreateConfigColor); + configService.RegisterTypeInitializer>(this.CreateConfigVector2); + configService.RegisterTypeInitializer>(this.CreateConfigVector3); + configService.RegisterTypeInitializer>(this.CreateConfigVector4); + } + + + #region InitializerWrappers_NetworkInjected + + private void AssignValueConditional(T val, IConfigEntry inst) where T : IEquatable + { +#if SERVER + if (inst.SyncType is NetSync.None or NetSync.ServerAuthority) + throw new InvalidOperationException($"[Server] Tried to assign a net value to a type that does not support sync: {inst.SyncType}. Name: {inst.InternalName}, Package: {inst.OwnerPackage.Name}"); + inst.TrySetValue(val); +#else + if (inst.SyncType is NetSync.None or NetSync.ClientOneWay) + throw new InvalidOperationException($"[Client] Tried to assign a net value to a type that does not support sync: {inst.SyncType}. Name: {inst.InternalName}, Package: {inst.OwnerPackage.Name}"); + inst.TrySetValue(val); +#endif + } + + private Result> CreateConfigBool(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadBoolean(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteBoolean(inst.Value); + }); + } + + private Result> CreateConfigSbyte(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional((sbyte)readMsg.ReadInt16(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteInt16((short)inst.Value); + }); + } + + private Result> CreateConfigByte(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional((byte)readMsg.ReadUInt16(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteUInt16((byte)inst.Value); + }); + } + + private Result> CreateConfigShort(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadInt16(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteInt16(inst.Value); + }); + } + + private Result> CreateConfigUShort(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadUInt16(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteUInt16(inst.Value); + }); + } + + private Result> CreateConfigInt32(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadInt32(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteInt32(inst.Value); + }); + } + + private Result> CreateConfigUInt32(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadUInt32(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteUInt32(inst.Value); + }); + } + + private Result> CreateConfigInt64(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadInt64(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteInt64(inst.Value); + }); + } + + private Result> CreateConfigUInt64(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadUInt64(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteUInt64(inst.Value); + }); + } + + private Result> CreateConfigFloat32(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadSingle(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteSingle(inst.Value); + }); + } + + private Result> CreateConfigFloat64(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadDouble(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteDouble(inst.Value); + }); + } + + private Result> CreateConfigFloat128(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + var decimalArr = new int[4]; + decimalArr[0] = readMsg.ReadInt32(); + decimalArr[1] = readMsg.ReadInt32(); + decimalArr[2] = readMsg.ReadInt32(); + decimalArr[3] = readMsg.ReadInt32(); + AssignValueConditional(new decimal(decimalArr), inst); + + }, (inst, writeMsg) => + { + var decimalArr = Decimal.GetBits(inst.Value); + writeMsg.WriteInt32(decimalArr[0]); + writeMsg.WriteInt32(decimalArr[1]); + writeMsg.WriteInt32(decimalArr[2]); + writeMsg.WriteInt32(decimalArr[3]); + }); + } + + private Result> CreateConfigChar(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional((char)readMsg.ReadUInt16(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteUInt16(inst.Value); + }); + } + + private Result> CreateConfigString(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadString(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteString(inst.Value); + }); + } + + private Result> CreateConfigColor(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(readMsg.ReadColorR8G8B8A8(), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteColorR8G8B8A8(inst.Value); + }); + } + + private Result> CreateConfigVector2(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(new Vector2(readMsg.ReadSingle(), readMsg.ReadSingle()), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteSingle(inst.Value.X); + writeMsg.WriteSingle(inst.Value.Y); + }); + } + + private Result> CreateConfigVector3(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(new Vector3(readMsg.ReadSingle(), readMsg.ReadSingle(), readMsg.ReadSingle()), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteSingle(inst.Value.X); + writeMsg.WriteSingle(inst.Value.Y); + writeMsg.WriteSingle(inst.Value.Z); + }); + } + + private Result> CreateConfigVector4(IConfigInfo configInfo) + { + return CreateConfigEntry(configInfo, (inst, readMsg) => + { + AssignValueConditional(new Vector4( + readMsg.ReadSingle(), + readMsg.ReadSingle(), + readMsg.ReadSingle(), + readMsg.ReadSingle()), inst); + + }, (inst, writeMsg) => + { + writeMsg.WriteSingle(inst.Value.X); + writeMsg.WriteSingle(inst.Value.Y); + writeMsg.WriteSingle(inst.Value.Z); + writeMsg.WriteSingle(inst.Value.W); + }); + } + + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs index f6962300d..2b31d3da4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs @@ -1,6 +1,690 @@ -namespace Barotrauma.LuaCs.Services; +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs.Services.Processing; +using Barotrauma.Networking; +using Dynamitey.DynamicObjects; +using FluentResults; +using Microsoft.Xna.Framework; +using OneOf; +using Path = Barotrauma.IO.Path; -public class ConfigService : IConfigService +namespace Barotrauma.LuaCs.Services; + +public partial class ConfigService : IConfigService { + //--- Internals + public ConfigService(IConverterServiceAsync> configProfileResourceConverter, + IConverterServiceAsync> configResourceConverter, IEventService eventService, System.Lazy storageService) + { + _configProfileResourceConverter = configProfileResourceConverter; + _configResourceConverter = configResourceConverter; + _eventService = eventService; + _storageService = storageService; + this._base = this; + } + + // data, states + private readonly IService _base; + private int _isDisposed = 0; + private readonly ConcurrentDictionary>> _configTypeInitializers = new(); + private readonly ConcurrentDictionary<(ContentPackage Package, string ConfigName), IConfigBase> _configs = new(); + private readonly ConcurrentDictionary> _packageConfigReverseLookup = new(); + private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), ImmutableArray<(string ConfigName, OneOf.OneOf Value)>> _configProfiles = new(); + private readonly ConcurrentDictionary> _packageProfilesReverseLookup = new(); + private readonly ConcurrentDictionary _packageNameMap= new(); + + private readonly AsyncReaderWriterLock _disposeOpsLock = new(); + + // extern services + private readonly IConverterServiceAsync> _configResourceConverter; + private readonly IConverterServiceAsync> _configProfileResourceConverter; + private readonly IEventService _eventService; + private readonly System.Lazy _storageService; + + //--- GC + public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed); + + public void Dispose() + { + // stop all ops + using var lck = _disposeOpsLock.AcquireWriterLock().GetAwaiter().GetResult(); + // set flag + ModUtils.Threading.SetBool(ref _isDisposed, true); + + _configTypeInitializers.Clear(); + if (!_configs.IsEmpty) + { + foreach (var config in _configs) + { + if (config.Value is IDisposable disposable) + disposable.Dispose(); + config.Value.OnValueChanged -= this.SaveConfigEvent; + } + _configs.Clear(); + } + + _configProfiles.Clear(); + _packageConfigReverseLookup.Clear(); + _packageNameMap.Clear(); + _packageProfilesReverseLookup.Clear(); + + GC.SuppressFinalize(this); + } + + public FluentResults.Result Reset() + { + using var lck = _disposeOpsLock.AcquireWriterLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + _configTypeInitializers.Clear(); + _configs.Clear(); + _configProfiles.Clear(); + _packageConfigReverseLookup.Clear(); + _packageNameMap.Clear(); + _packageProfilesReverseLookup.Clear(); + + return FluentResults.Result.Ok(); + } + + //--- API contracts + // Notes: + // -- Lua Interface uses strong types due to lua limitations. May be required to move API to an adapter class + // to allow testing abstraction. + // -- Lua interface should not propagate errors. + #region LuaInterface + + private bool TryGetConfigValue(string packageName, string configName, out T value) where T : IEquatable + { + value = default; + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + if (ModUtils.Threading.GetBool(ref _isDisposed)) + return false; + if (!_packageNameMap.TryGetValue(packageName, out var package)) + return false; + if (!_configs.TryGetValue((package, configName), out var config)) + return false; + if (config is not IConfigEntry entry) + return false; + value = entry.Value; + return true; + } + + public bool TryGetConfigBool(string packageName, string configName, out bool value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigInt(string packageName, string configName, out int value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigFloat(string packageName, string configName, out float value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigNumber(string packageName, string configName, out double value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigString(string packageName, string configName, out string value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigVector2(string packageName, string configName, out Vector2 value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigVector3(string packageName, string configName, out Vector3 value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigColor(string packageName, string configName, out Color value) + { + return TryGetConfigValue(packageName, configName, out value); + } + + public bool TryGetConfigList(string packageName, string configName, out IReadOnlyList value) + { + value = null; + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + if (ModUtils.Threading.GetBool(ref _isDisposed)) + return false; + if (!_packageNameMap.TryGetValue(packageName, out var package)) + return false; + if (!_configs.TryGetValue((package, configName), out var config)) + return false; + if (config is not IConfigList entry) + return false; + value = entry.Options; + return value is not null && value.Count > 0; + } + + private void SetConfigValue(string packageName, string configName, T value) where T : IEquatable + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + if (ModUtils.Threading.GetBool(ref _isDisposed)) + return; + if (!_packageNameMap.TryGetValue(packageName, out var package)) + return; + if (!_configs.TryGetValue((package, configName), out var config)) + return; + if (config is not IConfigEntry entry) + return; + entry.TrySetValue(value); + } + + public void SetConfigBool(string packageName, string configName, bool value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigInt(string packageName, string configName, int value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigFloat(string packageName, string configName, float value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigNumber(string packageName, string configName, double value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigString(string packageName, string configName, string value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigVector2(string packageName, string configName, Vector2 value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigVector3(string packageName, string configName, Vector3 value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigColor(string packageName, string configName, Color value) + { + SetConfigValue(packageName, configName, value); + } + + public void SetConfigList(string packageName, string configName, string value) + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + if (ModUtils.Threading.GetBool(ref _isDisposed)) + return; + if (!_packageNameMap.TryGetValue(packageName, out var package)) + return; + if (!_configs.TryGetValue((package, configName), out var config)) + return; + if (config is not IConfigList entry) + return; + entry.TrySetValue(value); + } + + public bool TryApplyProfileSettings(string packageName, string profileName) + { + + if (ModUtils.Threading.GetBool(ref _isDisposed)) + return false; + if (packageName.IsNullOrWhiteSpace() || profileName.IsNullOrWhiteSpace()) + return false; + ContentPackage package = null; + using (var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult()) + { + if (!_packageNameMap.TryGetValue(packageName, out package) || package == null) + return false; + } + // exit semaphore before invocation. Note: Race condition, may require copy implementation. + return this.ApplyProfileSettings(package, profileName).IsSuccess; + } + + #endregion + + public void RegisterTypeInitializer(Func> initializer, bool replaceIfExists = false) + where TData : IEquatable where TConfig : IConfigBase + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + Type dataType = typeof(TData); + if (_configTypeInitializers.ContainsKey(dataType) && !replaceIfExists) + return; + _configTypeInitializers[dataType] = (info => + { + var res = initializer(info); + if (res.IsFailed) + return FluentResults.Result.Fail($"Failed to initialize config type {dataType.Name}").WithErrors(res.Errors); + return res.Value; + }); + } + + private void AddConfigInstance((ContentPackage Package, string ConfigName) key, IConfigBase instance) + { + _configs[key] = instance; + if (!_packageNameMap.ContainsKey(key.Package.Name)) + _packageNameMap[key.Package.Name] = key.Package; + if (!_packageConfigReverseLookup.TryGetValue(key.Package, out var list)) + { + list = new ConcurrentBag<(ContentPackage Package, string ConfigName)>(); + _packageConfigReverseLookup[key.Package] = list; + } + list.Add(key); + // save hook + instance.OnValueChanged += this.SaveConfigEvent; + _eventService.PublishEvent(sub => sub.OnConfigCreated(instance)); + } + + private void AddProfileInstance((ContentPackage Package, string ProfileName) key, IConfigProfileInfo profile) + { + _configProfiles[key] = profile.ProfileValues.ToImmutableArray(); + if (!_packageNameMap.ContainsKey(key.Package.Name)) + _packageNameMap[key.Package.Name] = key.Package; + if (!_packageProfilesReverseLookup.TryGetValue(key.Package, out var list)) + { + list = new ConcurrentBag<(ContentPackage Package, string ProfileName)>(); + _packageProfilesReverseLookup[key.Package] = list; + } + list.Add(key); + } + + public async Task LoadConfigsAsync(ImmutableArray configResources) + { + using var lck = await _disposeOpsLock.AcquireReaderLock(); + _base.CheckDisposed(); + + if (configResources.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(LoadConfigsAsync)}: Array is empty."); + + var results = await _configResourceConverter.TryParseResourcesAsync(configResources); + var ret = new FluentResults.Result(); + + foreach (var result in results) + { + if (result.Errors.Any()) + ret.Errors.AddRange(result.Errors); + if (result.IsFailed || result.Value is not { Count: > 0 } res) + continue; + + foreach (var configInfo in res) + { + if (_configs.ContainsKey((configInfo.OwnerPackage, configInfo.InternalName))) + { + ret.Errors.Add(new Error($"{nameof(LoadConfigsAsync)}: Config already exists for the compound key {configInfo.OwnerPackage.Name} | {configInfo.InternalName}")); + continue; + } + + if (!_configTypeInitializers.TryGetValue(configInfo.DataType, out var initializer)) + { + ret.Errors.Add(new Error($"{nameof(LoadConfigsAsync)} No type initializer for {configInfo.DataType}")); + continue; + } + + + + var cfg = initializer(configInfo); + if (cfg.Errors.Any()) + ret.Errors.AddRange(cfg.Errors); + if (cfg.IsFailed || cfg.Value is not {} val) + continue; + + AddConfigInstance((configInfo.OwnerPackage, configInfo.InternalName), val); + } + } + + return ret; + } + + public async Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources) + { + using var lck = await _disposeOpsLock.AcquireReaderLock(); + _base.CheckDisposed(); + + if (configProfileResources.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(LoadConfigsProfilesAsync)}: Array is empty."); + + var results = await _configProfileResourceConverter.TryParseResourcesAsync(configProfileResources); + var ret = new FluentResults.Result(); + + foreach (var result in results) + { + if (result.Errors.Any()) + ret.Errors.AddRange(result.Errors); + if (result.IsFailed || result.Value is not { Count: > 0 } res) + continue; + + foreach (var profileInfo in res) + { + if (_configProfiles.ContainsKey((profileInfo.OwnerPackage, profileInfo.InternalName))) + { + ret.Errors.Add(new Error($"{nameof(LoadConfigsProfilesAsync)}: Config already exists for the compound key {profileInfo.OwnerPackage.Name} | {profileInfo.InternalName}")); + continue; + } + + AddProfileInstance((profileInfo.OwnerPackage, profileInfo.InternalName), profileInfo); + } + } + + return ret; + } + + public FluentResults.Result AddConfig(IConfigInfo configInfo) where TConfig : IConfigBase + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + if (configInfo is null) + return FluentResults.Result.Fail($"{nameof(AddConfig)}: Config is null."); + + if (!_configTypeInitializers.TryGetValue(configInfo.DataType, out var initializer)) + return FluentResults.Result.Fail($"{nameof(AddConfig)}: No type initializer for {configInfo.DataType}"); + + var errList = new List(); + + try + { + var cfg = initializer(configInfo); + if (cfg.Errors.Any()) + errList.AddRange(cfg.Errors); + if (cfg.IsFailed || cfg.Value is null) + return FluentResults.Result.Fail($"Failed to initialize {configInfo.DataType}").WithErrors(errList); + AddConfigInstance((configInfo.OwnerPackage, configInfo.InternalName), cfg.Value); + return (TConfig)cfg.Value; + } + catch(Exception ex) + { + return FluentResults.Result.Fail($"Failed to initialize {configInfo.DataType}").WithError(new ExceptionalError(ex)); + } + } + + public FluentResults.Result ApplyProfileSettings(ContentPackage package, string profileName) + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + if (package == null || string.IsNullOrEmpty(profileName)) + return FluentResults.Result.Fail($"{nameof(ApplyProfileSettings)}: ContentPackage and/or name were null or empty."); + + if (!_configProfiles.TryGetValue((package, profileName), out var list)) + return FluentResults.Result.Fail($"No profiles found for package {package.Name} with name {profileName}"); + + if (list.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(ApplyProfileSettings)}: No stored values for profile {profileName}."); + + var errList = new List(); + + foreach (var profileVal in list) + { + if (!_configs.TryGetValue((package, profileVal.ConfigName), out var val)) + continue; + + if (!val.TrySetValue(profileVal.Value)) + errList.Add(new Error($"Failed to apply value from profile named {profileName} to {val.InternalName}")); + // continue + } + + return FluentResults.Result.Ok().WithErrors(errList); + } + + public FluentResults.Result DisposePackageData(ContentPackage package) + { + // stop regular ops during deletion ops + using var lck = _disposeOpsLock.AcquireWriterLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + if (package is null) + return FluentResults.Result.Fail($"{nameof(DisposePackageData)}: Package was null."); + + var errList = new List(); + + if (_packageConfigReverseLookup.Remove(package, out var cfgKeys)) + { + if (cfgKeys.Any()) + { + foreach (var key in cfgKeys) + { + try + { + _configs.Remove(key, out var cfg); + cfg?.Dispose(); + } + catch (Exception e) + { + errList.Add(new ExceptionalError(e)); + } + } + } + } + + if (_packageProfilesReverseLookup.Remove(package, out var profileKeys)) + { + if (profileKeys.Any()) + { + foreach (var key in profileKeys) + { + _configProfiles.Remove(key, out _); + } + } + } + + _packageNameMap.Remove(package.Name, out _); + + return FluentResults.Result.Ok().WithErrors(errList); + } + + public Result> GetConfigsForPackage(ContentPackage package) + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + if (!_packageConfigReverseLookup.TryGetValue(package, out var keys) || keys.IsEmpty) + return FluentResults.Result.Fail($"No configs found for package {package.Name}"); + + return _configs.Where(kvp => keys.Contains(kvp.Key)).ToFrozenDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + + public Result Value)>>> GetProfilesForPackage(ContentPackage package) + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + if (!_packageProfilesReverseLookup.TryGetValue(package, out var keys) || keys.IsEmpty) + return FluentResults.Result.Fail($"No profiles found for package {package.Name}"); + + return _configProfiles.Where(kvp => keys.Contains(kvp.Key)).ToFrozenDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + + public IReadOnlyDictionary<(ContentPackage Package, string Name), IConfigBase> GetAllConfigs() + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + return _configs.ToFrozenDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + + public bool TryGetConfig(ContentPackage package, string name, out T config) where T : IConfigBase + { + using var lck = _disposeOpsLock.AcquireReaderLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + + config = default; + if (!_configs.TryGetValue((package, name), out var value)) + return false; + try + { + config = (T)value; + return true; + } + catch + { + return false; + } + } + + public async Task SaveAllConfigs() + { + using var lck = await _disposeOpsLock.AcquireReaderLock(); + _base.CheckDisposed(); + if (_configs.IsEmpty) + return FluentResults.Result.Ok(); + var toSave = _configs.Where(kvp => kvp.Value is not null).Select(kvp => kvp.Value) + .ToImmutableArray(); + var errList = ImmutableArray.CreateBuilder(); + foreach (var config in toSave) + { + var res = await SaveConfigInternal(config); + if (res.Errors.Any()) + errList.AddRange(res.Errors); + } + return FluentResults.Result.Ok().WithErrors(errList.MoveToImmutable()); + } + + public async Task SaveConfigsForPackage(ContentPackage package) + { + if (package is null) + return FluentResults.Result.Fail($"{nameof(SaveConfigsForPackage)}: Package was null."); + using var lck = await _disposeOpsLock.AcquireReaderLock(); + _base.CheckDisposed(); + if (!_packageConfigReverseLookup.TryGetValue(package, out var keys) || keys.IsEmpty) + return FluentResults.Result.Fail($"No configs found for package {package.Name}"); + ConcurrentQueue toSave = new(); + foreach (var key in keys) + { + if (_configs.TryGetValue(key, out var config)) + toSave.Enqueue(config); + } + if (toSave.IsEmpty) + return FluentResults.Result.Fail($"No configs found for package {package.Name}"); + var errList = ImmutableArray.CreateBuilder(); + while (toSave.TryDequeue(out var config)) + { + var res = await SaveConfigInternal(config); + if (res.Errors.Any()) + errList.AddRange(res.Errors); + } + return FluentResults.Result.Ok().WithErrors(errList.MoveToImmutable()); + } + + public async Task SaveConfig((ContentPackage Package, string ConfigName) config) + { + if (config.Package is null || config.ConfigName.IsNullOrWhiteSpace()) + return FluentResults.Result.Fail($"{nameof(SaveConfig)}: Config properties were null or empty."); + using var lck = await _disposeOpsLock.AcquireReaderLock(); + _base.CheckDisposed(); + if (!_configs.TryGetValue(config, out var instance)) + return FluentResults.Result.Fail($"{nameof(SaveConfig)}: No config found for package {config.Package.Name} and name {config.ConfigName}"); + return await SaveConfigInternal(instance); + } + + private void SaveConfigEvent(IConfigBase instance) + { + using var lck = _disposeOpsLock.AcquireWriterLock().GetAwaiter().GetResult(); + _base.CheckDisposed(); + SaveConfigInternal(instance).GetAwaiter().GetResult(); + } + + private async Task SaveConfigInternal(IConfigBase instance) + { + var localStorePath = Path.Combine("Config", SanitizedFileName($"{instance.OwnerPackage.Name}.xml)")); + // Locking and checks must be handled by the caller. + var val = instance.GetSerializableValue(); + var docRes = await _storageService.Value.LoadLocalXmlAsync(instance.OwnerPackage, localStorePath); + XDocument doc; + XElement cfgElement; + XElement valueElement; + + // structure is + /* + * + * <[instance.InternalName]> + * + * <--Contents Here-> + * + * + * + */ + + if (docRes.IsFailed || docRes.Value is null) + { + doc = new XDocument( + new XElement("Config", new XAttribute("ContentPackage", instance.OwnerPackage.Name), + cfgElement = new XElement(instance.InternalName, valueElement = new XElement("Value")))); + } + else + { + doc = docRes.Value; + var e1 = doc.GetChildElement("Config"); + if (e1 is null) + { + e1 = new XElement("Config"); + doc.Add(e1); + } + + cfgElement = e1.GetChildElement(instance.InternalName); + if (cfgElement is null) + { + cfgElement = new XElement(instance.InternalName); + e1.Add(cfgElement); + } + + valueElement = cfgElement.GetChildElement("Value"); + if (valueElement is null) + { + valueElement = new XElement("Value"); + cfgElement.Add(valueElement); + } + } + + valueElement.Remove(); // remove from cfg + + // get potential updated element + var updatedElement = val.Match(str => + { + valueElement.RemoveAll(); + valueElement.Value = str; + return valueElement; + }, element => + { + valueElement.RemoveAll(); + valueElement.Add(element); + return valueElement; + }); + + // (re) add updated element. + cfgElement.Add(updatedElement); + + return await _storageService.Value.SaveLocalXmlAsync(instance.OwnerPackage, localStorePath, doc); + } + + private static readonly Regex RemoveInvalidChars = new Regex($"[{Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()))}]", + RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private string SanitizedFileName(string fileName, string replacement = "_") + { + return RemoveInvalidChars.Replace(fileName, replacement); + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ContentPackageInfoLookup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ContentPackageInfoLookup.cs index 4a2f8e594..e8f906079 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ContentPackageInfoLookup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ContentPackageInfoLookup.cs @@ -326,7 +326,17 @@ public sealed class ContentPackageInfoLookup : IPackageInfoLookupService, IEvent .ToImmutableArray() ).GetAwaiter().GetResult(); } - + + public bool IsPackageEnabled(ContentPackage package) + { + if (package is null) + return false; + using (_packageSetsLock.AcquireReaderLock().GetAwaiter().GetResult()) + { + return _enabledPackages.Contains(package); + } + } + public async Task> Lookup(string packageName) { ((IService)this).CheckDisposed(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LocalizationService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LocalizationService.cs deleted file mode 100644 index e3a51d638..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LocalizationService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Barotrauma.LuaCs.Services; - -public interface LocalizationService -{ - -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptManagementService.cs index aa0c74610..f8531acef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptManagementService.cs @@ -2,13 +2,216 @@ using MoonSharp.Interpreter; using MoonSharp.Interpreter.Interop; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; +using System.Linq; using System.Reflection; +using System.Text; +using System.Threading; using System.Threading.Tasks; +using Barotrauma.LuaCs.Services.Safe; +using FluentResults; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using MoonSharp.Interpreter.Loaders; namespace Barotrauma.LuaCs.Services; -public class LuaScriptManagementService : ILuaScriptManagementService +public class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService { + public LuaScriptManagementService(ILuaScriptLoader loader, ILuaScriptServicesConfig luaScriptServicesConfig) + { + _luaScriptLoader = loader; + _luaScriptServicesConfig = luaScriptServicesConfig; + } + private readonly ILuaScriptLoader _luaScriptLoader; + private readonly ILuaScriptServicesConfig _luaScriptServicesConfig; + + public void Dispose() + { + _luaScriptLoader.Dispose(); + } + + public bool IsDisposed + { + get => throw new NotImplementedException(); + } + + public FluentResults.Result Reset() + { + throw new NotImplementedException(); + } + + public Result GetGlobalTableValue(string tableName) + { + throw new NotImplementedException(); + } + + public async Task LoadScriptResourcesAsync(ImmutableArray resourcesInfo) + { + throw new NotImplementedException(); + } + + public FluentResults.Result ExecuteLoadedScriptsForPackage(ContentPackage package) + { + throw new NotImplementedException(); + } + + public FluentResults.Result ExecuteLoadedScriptsForPackages(IEnumerable packages) + { + throw new NotImplementedException(); + } + + public FluentResults.Result ExecuteLoadedScripts() + { + throw new NotImplementedException(); + } + + public FluentResults.Result DisposePackageResources(ContentPackage package) + { + throw new NotImplementedException(); + } + + public FluentResults.Result UnloadActiveScripts() + { + throw new NotImplementedException(); + } + + public FluentResults.Result DisposeAllPackageResources() + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor RegisterType(Type type) + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor RegisterGenericType(Type type) + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor GetTypeInfo(string typeName) + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor GetGenericTypeInfo(string typeName, params string[] typeNameArgs) + { + throw new NotImplementedException(); + } + + public void UnregisterType(Type type) + { + throw new NotImplementedException(); + } + + public bool IsRegistered(Type type) + { + throw new NotImplementedException(); + } + + public bool IsTargetType(object obj, string typeName) + { + throw new NotImplementedException(); + } + + public string TypeOf(object obj) + { + throw new NotImplementedException(); + } + + public object CreateStatic(string typeName) + { + throw new NotImplementedException(); + } + + public object CreateEnumTable(string typeName) + { + throw new NotImplementedException(); + } + + public FieldInfo FindFieldRecursively(Type type, string fieldName) + { + throw new NotImplementedException(); + } + + public void MakeFieldAccessible(IUserDataDescriptor descriptor, string fieldName) + { + throw new NotImplementedException(); + } + + public MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null) + { + throw new NotImplementedException(); + } + + public void MakeMethodAccessible(IUserDataDescriptor descriptor, string methodName, string[] parameters = null) + { + throw new NotImplementedException(); + } + + public PropertyInfo FindPropertyRecursively(Type type, string propertyName) + { + throw new NotImplementedException(); + } + + public void MakePropertyAccessible(IUserDataDescriptor descriptor, string propertyName) + { + throw new NotImplementedException(); + } + + public void AddMethod(IUserDataDescriptor descriptor, string methodName, object function) + { + throw new NotImplementedException(); + } + + public void AddField(IUserDataDescriptor descriptor, string fieldName, DynValue value) + { + throw new NotImplementedException(); + } + + public void RemoveMember(IUserDataDescriptor descriptor, string memberName) + { + throw new NotImplementedException(); + } + + public bool HasMember(object obj, string memberName) + { + throw new NotImplementedException(); + } + + public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor descriptor) + { + throw new NotImplementedException(); + } + + public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType) + { + throw new NotImplementedException(); + } + + public Table GetObjectTable(object obj, string tableName) + { + throw new NotImplementedException(); + } + + public Table GetTable(string tableName) + { + throw new NotImplementedException(); + } + + public Table GetOrCreateObjectTable(object obj, string tableName) + { + throw new NotImplementedException(); + } + + public Table GetOrCreateTable(string tableName) + { + throw new NotImplementedException(); + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/NetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/NetworkingService.cs index 97d0baed2..f4abb97d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/NetworkingService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/NetworkingService.cs @@ -22,7 +22,7 @@ internal partial class NetworkingService : INetworkingService ReceiveIds } - private Dictionary netVars = new Dictionary(); + private Dictionary netVars = new Dictionary(); private Dictionary netReceives = new Dictionary(); private Dictionary packetToId = new Dictionary(); private Dictionary idToPacket = new Dictionary(); @@ -46,7 +46,7 @@ internal partial class NetworkingService : INetworkingService #endif } - public void RegisterNetVar(INetVar netVar) + public void RegisterNetVar(INetworkSyncEntity netVar) { netVars[netVar.InstanceId] = netVar; @@ -58,7 +58,7 @@ internal partial class NetworkingService : INetworkingService }; } - public void SendNetVar(INetVar netVar) + public void SendNetVar(INetworkSyncEntity netVar) { if (netVars.ContainsKey(netVar.InstanceId)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs index 23f646bb6..318da7814 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs @@ -27,7 +27,6 @@ public partial class PackageManagementService : IPackageManagementService private readonly IProcessorService, IAssembliesResourcesInfo> _assemblyInfoConverter; private readonly IProcessorService, IConfigsResourcesInfo> _configsInfoConverter; private readonly IProcessorService, IConfigProfilesResourcesInfo> _configProfilesConverter; - private readonly IProcessorService, ILocalizationsResourcesInfo> _localizationsConverter; private readonly IProcessorService, ILuaScriptsResourcesInfo> _luaScriptsConverter; @@ -56,9 +55,7 @@ public partial class PackageManagementService : IPackageManagementService } return FluentResults.Result.Ok(); } - - public ImmutableArray Localizations => _modInfos.IsEmpty ? ImmutableArray.Empty - : _modInfos.SelectMany(kvp => kvp.Value.Localizations).ToImmutableArray(); + public ImmutableArray Configs => _modInfos.IsEmpty ? ImmutableArray.Empty : _modInfos.SelectMany(kvp => kvp.Value.Configs).ToImmutableArray(); public ImmutableArray ConfigProfiles => _modInfos.IsEmpty ? ImmutableArray.Empty @@ -103,6 +100,25 @@ public partial class PackageManagementService : IPackageManagementService : _modInfos.Select(kvp => kvp.Key).ToImmutableArray(); } + public bool IsPackageLoaded(ContentPackage package) + { + return package is not null && _modInfos.ContainsKey(package); + } + + public ImmutableArray FilterUnloadableResources(IReadOnlyList resources, bool enabledPackagesOnly = false) + where T : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo + { + return resources + .Where(r => r is not null) + .Where(r => (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0) + .Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0) + .Where(r => !r.Dependencies.Any() || r.Dependencies.All(d => + d.Dependency.GetPackage() is {} p // cp is valid + && _modInfos.ContainsKey(p) // cp is parsed + && (!enabledPackagesOnly || _packageInfoLookupService.IsPackageEnabled(p)))) // cp is enabled + .ToImmutableArray(); + } + public void DisposePackageInfos(ContentPackage package) { _modInfos.TryRemove(package, out _); @@ -217,26 +233,6 @@ public partial class PackageManagementService : IPackageManagementService $"{nameof(GetConfigProfilesInfos)}: ContentPackage {package.Name} is not registered."); } - public Result GetLocalizationsInfos(ContentPackage package, bool onlySupportedResources = true) - { - ((IService)this).CheckDisposed(); - if (package is null) - return FluentResults.Result.Fail($"{nameof(GetLocalizationsInfos)}: ContentPackage is null."); - - if (_modInfos.TryGetValue(package, out var result)) - { - return FluentResults.Result.Ok(_localizationsConverter.Process(onlySupportedResources? - result.Localizations.Where(r => - (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 - && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() - : result.Localizations - )); - } - - return FluentResults.Result.Fail( - $"{nameof(GetLocalizationsInfos)}: ContentPackage {package.Name} is not registered."); - } - public Result GetLuaScriptsInfos(ContentPackage package, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); @@ -320,27 +316,6 @@ public partial class PackageManagementService : IPackageManagementService return FluentResults.Result.Ok(_configProfilesConverter.Process(builder.MoveToImmutable())); } - public Result GetLocalizationsInfos(IReadOnlyList packages, bool onlySupportedResources = true) - { - ((IService)this).CheckDisposed(); - if (packages is null || packages.Count == 0) - return FluentResults.Result.Fail($"{nameof(GetLocalizationsInfos)}: ContentPackage list is null or empty."); - var builder = ImmutableArray.CreateBuilder(); - foreach (var package in packages) - { - if (_modInfos.TryGetValue(package, out var result) && result.Localizations is { IsEmpty: false }) - { - builder.AddRange(onlySupportedResources? - result.Localizations.Where(r => - (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 - && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() - : result.Localizations); - } - } - - return FluentResults.Result.Ok(_localizationsConverter.Process(builder.MoveToImmutable())); - } - public Result GetLuaScriptsInfos(IReadOnlyList packages, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); @@ -377,11 +352,6 @@ public partial class PackageManagementService : IPackageManagementService return await Task.Run(() => GetConfigProfilesInfos(packages, onlySupportedResources)); } - public async Task> GetLocalizationsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) - { - return await Task.Run(() => GetLocalizationsInfos(packages, onlySupportedResources)); - } - public async Task> GetLuaScriptsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) { return await Task.Run(() => GetLuaScriptsInfos(packages, onlySupportedResources)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ConfigIOService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ConfigIOService.cs new file mode 100644 index 000000000..f07a01e8c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ConfigIOService.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using FarseerPhysics.Common; +using FluentResults; +using OneOf; + +namespace Barotrauma.LuaCs.Services.Processing; + +public class ConfigIOService : IConfigIOService +{ + private readonly IStorageService _storageService; + private readonly IConfigServiceConfig _configServiceConfig; + + public ConfigIOService(IStorageService storageService, IConfigServiceConfig configServiceConfig) + { + this._storageService = storageService; + storageService.UseCaching = true; + _configServiceConfig = configServiceConfig; + } + + public void Dispose() + { + // stateless service + return; + } + + // stateless service + public bool IsDisposed => false; + public FluentResults.Result Reset() + { + _storageService.PurgeCache(); + return FluentResults.Result.Ok(); + } + + public async Task>> TryParseResourceAsync(IConfigResourceInfo src) + { + if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Config resource and/or components were null."); + + try + { + var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, src.FilePaths); + if (infos.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found."); + + var errList = new List(); + + var resList = infos.Select(info => + { + if (info.Item2.Errors.Any()) + errList.AddRange(info.Item2.Errors); + if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc) + { + errList.Add(new Error($"Unable to parse file: {info.Item1}")); + return default; + } + + return (info.Item1, configXDoc); + }) + .Where(doc => !doc.Item1.IsNullOrWhiteSpace() && doc.configXDoc != null) + .SelectMany(doc => doc.configXDoc.Root.GetChildElements("Configuration")) + .SelectMany(cfgContainer => cfgContainer.GetChildElements("Configs")) + .SelectMany(cfgContainer => cfgContainer.GetChildElements("Config")) + .Select(async cfgElement => + { + try + { + OneOf.OneOf defaultValue = cfgElement.GetChildElement("Value"); + if (defaultValue.AsT1 is null) + defaultValue = cfgElement.GetAttributeString("Value", string.Empty); + + var internalName = cfgElement.GetAttributeString("Name", string.Empty); + if (internalName.IsNullOrWhiteSpace()) + return null; + + return new ConfigInfo() + { + DataType = Type.GetType(cfgElement.GetAttributeString("Type", "string")), + OwnerPackage = src.OwnerPackage, + DefaultValue = defaultValue, + Value = await LoadConfigDataFromLocal(src.OwnerPackage, internalName) is { IsSuccess: true } res + ? res.Value : defaultValue, + EditableStates = cfgElement.GetAttributeBool("ReadOnly", false) + ? RunState.Unloaded // read-only + : RunState.Running, // editable at runtime + InternalName = internalName, + NetSync = Enum.Parse( + cfgElement.GetAttributeString("NetSync", nameof(NetSync.None))), +#if CLIENT + DisplayName = cfgElement.GetAttributeString("DisplayName", null), + Description = cfgElement.GetAttributeString("Description", null), + DisplayCategory = cfgElement.GetAttributeString("Category", null), + ShowInMenus = cfgElement.GetAttributeBool("ShowInMenus", true), + Tooltip = cfgElement.GetAttributeString("Tooltip", null), + ImageIconPath = cfgElement.GetAttributeString("Image", null) +#endif + }; + } + catch (Exception e) + { + errList.Add(new Error($"Failed to parse config var for package {src.OwnerPackage}")); + errList.Add(new ExceptionalError(e)); + return null; + } + }) + .Where(task => task is not null) + .ToImmutableArray(); + + var result = (await Task.WhenAll(resList)).ToImmutableArray(); + + var ret = FluentResults.Result.Ok((IReadOnlyList)result); + if (errList.Any()) + ret.Errors.AddRange(errList); + return ret; + } + catch(Exception e) + { + return FluentResults.Result.Fail($"Failed to parse config resource for package {src.OwnerPackage}"); + } + } + + public async Task>>> TryParseResourcesAsync(IEnumerable sources) + { + var results = new ConcurrentQueue>>(); + + var src = sources.ToImmutableArray(); + if (!src.Any()) + return ImmutableArray>>.Empty; + + await src.ParallelForEachAsync(async cfg => + { + var res = await TryParseResourceAsync(cfg); + results.Enqueue(res); + }, 2); // we only need 2 parallels to buffer against disk loading. + + return results.ToImmutableArray(); + } + + public async Task>> TryParseResourceAsync(IConfigProfileResourceInfo src) + { + if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Profile resource and/or components were null."); + + try + { + var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, src.FilePaths); + if (infos.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found."); + + var errList = new List(); + + var resList = infos.Select(info => + { + if (info.Item2.Errors.Any()) + errList.AddRange(info.Item2.Errors); + if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc) + { + errList.Add(new Error($"Unable to parse file: {info.Item1}")); + return null; + } + + return configXDoc; + }) + .Where(doc => doc is not null) + .SelectMany(doc => doc.Root.GetChildElements("Configuration")) + .SelectMany(cfgContainer => cfgContainer.GetChildElements("Profiles")) + .SelectMany(cfgContainer => cfgContainer.GetChildElements("Profile")) + .Select(cfgElement => + { + try + { + return new ConfigProfileInfo() + { + OwnerPackage = src.OwnerPackage, + InternalName = cfgElement.GetAttributeString("Name", null), + ProfileValues = cfgElement.GetChildElements("ConfigValue") + .Select Value)>(element => + { + if (element.GetAttributeString("Name", null) is not { } name) + return default; + if (element.GetAttributeString("Value", null) is { } value) + return (name, value); + if (element.GetChildElement("Value") is { } xValue) + return (name, xValue); + return default; + }) + .Where(val => val.ConfigName is not null && val.Value.Match( + s => !s.IsNullOrWhiteSpace(), + element => element is not null)) + .ToList() + }; + } + catch (Exception e) + { + errList.Add(new Error($"Failed to parse profile var for package {src.OwnerPackage}")); + errList.Add(new ExceptionalError(e)); + return null; + } + }) + .Where(cfgInfo => cfgInfo != null && !cfgInfo.InternalName.IsNullOrWhiteSpace()) + .ToImmutableArray(); + + var ret = FluentResults.Result.Ok((IReadOnlyList)resList); + if (errList.Any()) + ret.Errors.AddRange(errList); + return ret; + } + catch(Exception e) + { + return FluentResults.Result.Fail($"Failed to parse profile resource for package {src.OwnerPackage}"); + } + } + + public async Task>>> TryParseResourcesAsync(IEnumerable sources) + { + var results = new ConcurrentQueue>>(); + + var src = sources.ToImmutableArray(); + if (!src.Any()) + return ImmutableArray>>.Empty; + + await src.ParallelForEachAsync(async cfg => + { + var res = await TryParseResourceAsync(cfg); + results.Enqueue(res); + }, 2); // we only need 2 parallels to buffer against disk loading. + + return results.ToImmutableArray(); + } + + private static readonly Regex RemoveInvalidChars = new Regex($"[{Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()))}]", + RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private string SanitizedFileName(string fileName, string replacement = "_") + { + return RemoveInvalidChars.Replace(fileName, replacement); + } + + public async Task SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue) + { + if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace() || serializedValue is null) + return FluentResults.Result.Fail($"{nameof(SaveConfigDataLocal)}: Argument(s) were null"); + + var res = await LoadPackageConfigDocInternal(package); + + throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized. + } + + public async Task>> LoadConfigDataFromLocal(ContentPackage package, string configName) + { + if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace()) + return FluentResults.Result.Fail($"{nameof(LoadConfigDataFromLocal)}: Argument(s) were null"); + + var filePath = _configServiceConfig.LocalConfigPathPartial.Replace( + _configServiceConfig.FileNamePattern, + $"{SanitizedFileName(package.Name)}.xml"); + + var res = await _storageService.LoadLocalXmlAsync(package, filePath); + + throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized. + } + + private async Task> LoadPackageConfigDocInternal(ContentPackage package) + { + var filePath = _configServiceConfig.LocalConfigPathPartial.Replace( + _configServiceConfig.FileNamePattern, + $"{SanitizedFileName(package.Name)}.xml"); + + throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized. + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConfigIOService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConfigIOService.cs new file mode 100644 index 000000000..7361b6575 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConfigIOService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Services.Processing; + +namespace Barotrauma.LuaCs.Services.Processing; + +public interface IConfigIOService : IReusableService, + IConverterServiceAsync>, + IConverterServiceAsync> +{ + Task SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue); + Task>> LoadConfigDataFromLocal(ContentPackage package, string configName); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs index b4f39989a..afb5c3ec1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs @@ -69,12 +69,8 @@ public partial class ModConfigService : IConverterServiceAsync.Empty, ConfigProfiles = ImmutableArray.Empty, - Localizations = ImmutableArray.Empty, Package = src, PackageName = src.Name -#if CLIENT - ,Styles = ImmutableArray.Empty -#endif }; } catch (Exception e) @@ -84,40 +80,6 @@ public partial class ModConfigService : IConverterServiceAsync> GetModConfigInfoAsync(ContentPackage package, XElement root); - - private ImmutableArray GetLocalizations(ContentPackage src, IEnumerable elements) - { - var builder = ImmutableArray.CreateBuilder(); - - if (GetXmlFilesList(src, elements, "Localizations") - is not { IsSuccess: true, Value: { } xmlFiles }) - return ImmutableArray.Empty; - - foreach (var file in xmlFiles) - { - // get dependencies - var deps = GetElementsDependenciesData(file.Item1, src); - // get platform, culture and target architecture - var info = GetElementsAttributesData(file.Item1, file.Item2.First()); - - builder.Add(new LocalizationResourceInfo() - { - Dependencies = deps, - Optional = info.IsOptional, - FilePaths = file.Item2, - InternalName = info.Name, - LoadPriority = info.LoadPriority, - OwnerPackage = src, - SupportedCultures = info.SupportedCultures, - SupportedPlatforms = info.SupportedPlatforms, - SupportedTargets = info.SupportedTargets - }); - } - - return builder.Count > 0 - ? builder.ToImmutable() - : ImmutableArray.Empty; - } private ImmutableArray GetAssemblies(ContentPackage src, IEnumerable elements) { @@ -265,7 +227,7 @@ public partial class ModConfigService : IConverterServiceAsync(); + if (_storageService.FindFilesInPackage(src, "CSharp/Shared", "*.cs", true) + is { IsSuccess: true, Value: { IsDefaultOrEmpty: false } files }) + { + sharedCsBuilder.AddRange(files); + } + + var filesCssShared = sharedCsBuilder.MoveToImmutable(); + var sharedFound = !filesCssShared.IsDefaultOrEmpty; // source files legacy: server if (_storageService.FindFilesInPackage(src, "CSharp/Server", "*.cs", true) @@ -550,7 +519,7 @@ public partial class ModConfigService : IConverterServiceAsync.Empty, - FilePaths = sharedFound ? filesCssServer.Concat(filesCssShared).ToImmutableArray() : filesCssServer, + FilePaths = sharedFound ? filesCssServer.Concat(filesCssShared).ToImmutableArray() : filesCssServer, FriendlyName = "CssServer", InternalName = "CssServer", IsScript = true, @@ -594,7 +563,7 @@ public partial class ModConfigService : IConverterServiceAsync.Empty, FilePaths = fileAll.Where(path => !path.Contains("Autorun")).ToImmutableArray(), @@ -607,7 +576,7 @@ public partial class ModConfigService : IConverterServiceAsync.Empty, FilePaths = fileAll.Where(path => path.Contains("Autorun")).ToImmutableArray(), diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ResourceInfoProcessors.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ResourceInfoProcessors.cs index c1151a649..d6fb94b32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ResourceInfoProcessors.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ResourceInfoProcessors.cs @@ -9,7 +9,6 @@ public partial class ResourceInfoArrayPacker : IProcessorService, IAssembliesResourcesInfo>, IProcessorService, IConfigsResourcesInfo>, IProcessorService, IConfigProfilesResourcesInfo>, - IProcessorService, ILocalizationsResourcesInfo>, IProcessorService, ILuaScriptsResourcesInfo> { private bool _isDisposed; @@ -28,11 +27,6 @@ public partial class ResourceInfoArrayPacker : return new ConfigProfilesResourcesInfo(src.ToImmutableArray()); } - public ILocalizationsResourcesInfo Process(IReadOnlyList src) - { - return new LocalizationResourcesInfo(src.ToImmutableArray()); - } - public ILuaScriptsResourcesInfo Process(IReadOnlyList src) { return new LuaScriptsResourcesInfo(src.ToImmutableArray()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs index 781827ced..182f4fc4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs @@ -1,8 +1,32 @@ -using Barotrauma.LuaCs.Configuration; +using System.Collections.Generic; +using Barotrauma.LuaCs.Configuration; +using Microsoft.Xna.Framework; namespace Barotrauma.LuaCs.Services.Safe; public interface ILuaConfigService : ILuaService { - + // get values + bool TryGetConfigBool(string packageName, string configName, out bool value); + bool TryGetConfigInt(string packageName, string configName, out int value); + bool TryGetConfigFloat(string packageName, string configName, out float value); + bool TryGetConfigNumber(string packageName, string configName, out double value); + bool TryGetConfigString(string packageName, string configName, out string value); + bool TryGetConfigVector2(string packageName, string configName, out Vector2 value); + bool TryGetConfigVector3(string packageName, string configName, out Vector3 value); + bool TryGetConfigColor(string packageName, string configName, out Color value); + bool TryGetConfigList(string packageName, string configName, out IReadOnlyList value); + // set values + void SetConfigBool(string packageName, string configName, bool value); + void SetConfigInt(string packageName, string configName, int value); + void SetConfigFloat(string packageName, string configName, float value); + void SetConfigNumber(string packageName, string configName, double value); + void SetConfigString(string packageName, string configName, string value); + void SetConfigVector2(string packageName, string configName, Vector2 value); + void SetConfigVector3(string packageName, string configName, Vector3 value); + void SetConfigColor(string packageName, string configName, Color value); + void SetConfigList(string packageName, string configName, string value); + // profiles + bool TryApplyProfileSettings(string packageName, string profileName); + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaDataService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaDataService.cs index 04f4893bc..88cc45bdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaDataService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaDataService.cs @@ -25,6 +25,8 @@ public interface ILuaDataService : ILuaService /// /// Returns stored table data for the given object or creates a new table if one doesn't exist. /// + /// Note: tables are stored using weak references and will be automatically deleted when the object is + /// garbage collected. /// /// /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaScriptLoader.cs new file mode 100644 index 000000000..4f6475d06 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaScriptLoader.cs @@ -0,0 +1,8 @@ +using MoonSharp.Interpreter.Loaders; + +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaScriptLoader : IService, IScriptLoader +{ + void ClearCaches(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ISafeStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ISafeStorageService.cs new file mode 100644 index 000000000..bad6520eb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ISafeStorageService.cs @@ -0,0 +1,56 @@ +using System.Collections.Immutable; + +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ISafeStorageService : IStorageService +{ + /// + /// Checks the given file path to see if it can be read. This includes any permissions, whitelists and OS checks. + /// + /// The absolute path to the file. + /// Whether to only check for read permissions only, or full RWM if false. + /// Whether to only check if the file is safe to access, without checking accessibility at the OS level. + /// Whether the file is accessible. + bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true); + + /// + /// Adds the given path to the specified whitelists. + /// + /// Either the fully-qualified or local reference path to the given file. + /// + void AddFileToWhitelist(string path, bool readOnly = true); + + /// + /// Removes the given path from all whitelists (Read|Write). + /// + /// + void RemoveFileFromAllWhitelists(string path); + + /// + /// Sets the whitelist filtering for read-only file permissions for the instance. + /// + /// List of absolute file paths allowed. + FluentResults.Result SetReadOnlyWhitelist(ImmutableArray filePaths); + + /// + /// Sets the whitelist filtering for read & write file permissions for the instance. + /// + /// List of absolute file paths allowed. + FluentResults.Result SetReadWriteWhitelist(ImmutableArray filePaths); + + /// + /// Deletes all paths from all white lists. + /// + void ClearAllWhitelists(); + + /// + /// Whether the service instance is in file read-only mode. + /// + bool IsReadOnlyMode { get; } + + /// + /// Sets the service into file read-only mode. Cannot be undone. + /// + /// + bool EnableReadOnlyMode(); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/LuaScriptLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/LuaScriptLoader.cs new file mode 100644 index 000000000..ab1e19553 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/LuaScriptLoader.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Loaders; +using System.Linq; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Services.Safe; + +namespace Barotrauma.LuaCs.Services.Safe +{ + public class LuaScriptLoader : ScriptLoaderBase, ILuaScriptLoader + { + public LuaScriptLoader(IStorageService storageService, Lazy loggerService, ILuaScriptServicesConfig luaScriptServicesConfig) + { + this._storageService = storageService; + this._loggerService = loggerService; + this._luaScriptServicesConfig = luaScriptServicesConfig; + _storageService.UseCaching = _luaScriptServicesConfig.UseCaching; + if (_luaScriptServicesConfig.SafeLuaIOEnabled) + { + //_storageService.EnableWhitelistOnly(); + } + } + + private readonly IStorageService _storageService; + private readonly Lazy _loggerService; + private readonly ILuaScriptServicesConfig _luaScriptServicesConfig; + + public override object LoadFile(string file, Table globalContext) + { + ((IService)this).CheckDisposed(); + + if (!CanReadFromPath(file)) + { + LogErrors($"File access to \"{file}\" is not allowed."); + return null; + } + + if (_storageService.TryLoadText(file) is not { IsSuccess: true, Value: not null } script) + { + LogErrors($"Failed to load file \"{file}\"."); + return null; + } + + if (script.Value.IsNullOrWhiteSpace()) + { + LogErrors($"The file \"{file}\" was empty."); + return null; + } + + return script.Value; + } + + public void ClearCaches() + { + ((IService)this).CheckDisposed(); + _storageService?.PurgeCache(); + } + + public override bool ScriptFileExists(string file) + { + ((IService)this).CheckDisposed(); + + if (!CanReadFromPath(file)) + { + LogErrors($"File access to \"{file}\" is not allowed."); + return false; + } + + var result = _storageService.FileExists(file); + + if (result is { IsFailed: true }) + { + LogErrors($"Unable to find and load file \"{file}\"."); + return false; + } + + return result.IsSuccess; + } + + private bool CanReadFromPath(string file) + { + throw new NotImplementedException(); + } + + private bool CanWriteToPath(string file) + { + throw new NotImplementedException(); + } + + private void LogErrors(string message, FluentResults.Result result = null) + { + _loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: {message}"); + + if (result is null || result.Errors.Count <= 0) + return; + + foreach (var error in result.Errors) + { + _loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: Error: {error.Message}."); + } + } + + public void Dispose() + { + if (IsDisposed) + return; + IsDisposed = true; + + _storageService?.Dispose(); + _loggerService?.Value.Dispose(); + } + + public bool IsDisposed { get; private set; } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/SafeStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/SafeStorageService.cs new file mode 100644 index 000000000..328705184 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/SafeStorageService.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using Barotrauma.IO; +using Barotrauma.LuaCs.Data; +using FarseerPhysics.Common; +using FluentResults; + +namespace Barotrauma.LuaCs.Services.Safe; + +public class SafeStorageService : StorageService, ISafeStorageService +{ + private ConcurrentDictionary _fileListRead = new (), _fileListReadWrite = new(); + + public SafeStorageService(IStorageServiceConfig configData) : base(configData) + { + + } + + private string GetFullPath(string path) => System.IO.Path.GetFullPath(path).CleanUpPathCrossPlatform(); + + public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true) + { + ((IService)this).CheckDisposed(); + + try + { + path = GetFullPath(path); + if (!readOnly && IsReadOnlyMode) + return false; + if (readOnly) + { + if (!_fileListRead.ContainsKey(path)) + return false; + } + else + { + if (!_fileListReadWrite.ContainsKey(path)) + return false; + } + if (checkWhitelistOnly) + return true; + + using var fs = System.IO.File.Open( + path, FileMode.Open, readOnly ? FileAccess.Read : FileAccess.ReadWrite, FileShare.ReadWrite); + + return true; + } + catch + { + return false; + } + } + + public void AddFileToWhitelist(string path, bool readOnly = true) + { + ((IService)this).CheckDisposed(); + try + { + path = GetFullPath(path); + _fileListRead.AddOrUpdate(path, s => 0, (s, b) => 0); + if (!readOnly && !IsReadOnlyMode) + _fileListRead.AddOrUpdate(path, s => 0, (s, b) => 0); + } + catch + { + return; + } + } + + public void RemoveFileFromAllWhitelists(string path) + { + ((IService)this).CheckDisposed(); + try + { + path = GetFullPath(path); + _fileListRead.TryRemove(path, out _); + _fileListReadWrite.TryRemove(path, out _); + } + catch + { + return; + } + } + + public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray filePaths) + { + ((IService)this).CheckDisposed(); + if (filePaths.IsDefaultOrEmpty) + return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty."); + var res = new FluentResults.Result(); + foreach (var path in filePaths) + { + // TODO: Cleanup path and add it. + } + + throw new NotImplementedException(); + } + + public FluentResults.Result SetReadWriteWhitelist(ImmutableArray filePaths) + { + ((IService)this).CheckDisposed(); + throw new System.NotImplementedException(); + } + + public void ClearAllWhitelists() + { + throw new System.NotImplementedException(); + } + + private int _isReadOnlyMode = 0; + public bool IsReadOnlyMode => ModUtils.Threading.GetBool(ref _isReadOnlyMode); + + public bool EnableReadOnlyMode() + { + ModUtils.Threading.SetBool(ref _isReadOnlyMode, true); + return ModUtils.Threading.GetBool(ref _isReadOnlyMode); + } + + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs index bc7bd0bad..54730328f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs @@ -1,117 +1,151 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Collections.ObjectModel; using System.IO; +using System.Linq; using System.Reflection; using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; -using Barotrauma.LuaCs.Configuration; -using Barotrauma.LuaCs.Services; +using Barotrauma.LuaCs.Data; using Barotrauma.Steam; using FluentResults; using FluentResults.LuaCs; -using Microsoft.CodeAnalysis; -using OneOf.Types; using Error = FluentResults.Error; -using File = Barotrauma.IO.File; using Path = Barotrauma.IO.Path; -using Success = OneOf.Types.Success; namespace Barotrauma.LuaCs.Services; public class StorageService : IStorageService { - public StorageService(Lazy configService) + public StorageService(IStorageServiceConfig configData) { - _configService = configService; + _configData = configData; } - private readonly Lazy _configService; - private IConfigEntry _kLocalStoragePath = null; - private IConfigEntry _kLocalFilePathRules = null; - private const string _packagePathKeyword = ""; - private readonly string _runLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location.CleanUpPath()); - - // TODO: Rewrite the config to get info from .ctor. - private IConfigEntry LocalStoragePath => _kLocalStoragePath ??= GetOrCreateConfig(nameof(LocalStoragePath), "/Data/Mods"); - private IConfigEntry LocalFilePathRule => _kLocalFilePathRules ??= GetOrCreateConfig(nameof(LocalFilePathRule), _packagePathKeyword); - private IConfigEntry GetOrCreateConfig(string name, string defaultValue) - { - var c = _configService.Value - .GetConfig>(ModUtils.Definitions.LuaCsForBarotrauma, name); - if (c is not null) - return c; - - var c1 = _configService.Value.AddConfigEntry( - ModUtils.Definitions.LuaCsForBarotrauma, - name, defaultValue, NetSync.None, valueChangePredicate: (value) => false); - if (c1.IsSuccess) - return c1.Value; - - throw new KeyNotFoundException("Cannot find storage value for key: " + name); - - } - public bool IsDisposed { get; private set; } + private readonly ConcurrentDictionary> _fsCache = new(); + protected readonly IStorageServiceConfig _configData; + + public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed); + private int _isDisposed = 0; public void Dispose() { - if (IsDisposed) - return; - IsDisposed = true; - _kLocalStoragePath = null; - _kLocalFilePathRules = null; + ModUtils.Threading.SetBool(ref _isDisposed, true); } - public FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath) => + public void PurgeCache() + { + ((IService)this).CheckDisposed(); + _fsCache.Clear(); + } + + public void PurgeFileFromCache(string absolutePath) + { + ((IService)this).CheckDisposed(); + + if (absolutePath.IsNullOrWhiteSpace()) + return; + + try + { + //sanitation pass + absolutePath = System.IO.Path.GetFullPath(absolutePath).CleanUpPath(); + _fsCache.Remove(absolutePath, out _); + } + catch + { + // ignored + return; + } + } + + public void PurgeFilesFromCache(params string[] absolutePaths) + { + ((IService)this).CheckDisposed(); + + if (absolutePaths.Length < 1) + return; + + foreach (var path in absolutePaths) + { + try + { + if (path.IsNullOrWhiteSpace()) + continue; + + //sanitation pass + var path2 = System.IO.Path.GetFullPath(path).CleanUpPath(); + _fsCache.Remove(path2, out _); + } + catch + { + // ignored + continue; + } + } + } + + private int _useCaching; + public bool UseCaching + { + get => ModUtils.Threading.GetBool(ref _useCaching); + set => ModUtils.Threading.SetBool(ref _useCaching, value); + } + + public virtual FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadXml(r.Value) : r.ToResult(); - public FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath) => + public virtual FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadBinary(r.Value) : r.ToResult(); - public FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath) => + public virtual FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadText(r.Value) : r.ToResult(); - public FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document) => + public virtual FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TrySaveXml(r.Value, document) : r.ToResult(); - public FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes) => + public virtual FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TrySaveBinary(r.Value, bytes) : r.ToResult(); - public FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text) => + public virtual FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TrySaveText(r.Value, text) : r.ToResult(); - public async Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath) => + public virtual async Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadXmlAsync(r.Value) : r.ToResult(); - public async Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) => + public virtual async Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadBinaryAsync(r.Value) : r.ToResult(); - public async Task> LoadLocalTextAsync(ContentPackage package, string localFilePath) => + public virtual async Task> LoadLocalTextAsync(ContentPackage package, string localFilePath) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadTextAsync(r.Value) : r.ToResult(); - public async Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) => + public virtual async Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TrySaveXmlAsync(r.Value, document) : r.ToResult(); - public async Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) => + public virtual async Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TrySaveBinaryAsync(r.Value, bytes) : r.ToResult(); - public async Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) => + public virtual async Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) => GetAbsFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TrySaveTextAsync(r.Value, text) : r.ToResult(); - public FluentResults.Result LoadPackageXml(ContentPackage package, string localFilePath) => + public virtual FluentResults.Result LoadPackageXml(ContentPackage package, string localFilePath) => GetAbsFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadXml(r.Value) : r.ToResult(); - public FluentResults.Result LoadPackageBinary(ContentPackage package, string localFilePath) => + public virtual FluentResults.Result LoadPackageBinary(ContentPackage package, string localFilePath) => GetAbsFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadBinary(r.Value) : r.ToResult(); - public FluentResults.Result LoadPackageText(ContentPackage package, string localFilePath) => + public virtual FluentResults.Result LoadPackageText(ContentPackage package, string localFilePath) => GetAbsFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadText(r.Value) : r.ToResult(); - public ImmutableArray<(string, FluentResults.Result)> LoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePaths) + + + public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) @@ -122,7 +156,7 @@ public class StorageService : IStorageService return builder.MoveToImmutable(); } - public ImmutableArray<(string, FluentResults.Result)> LoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePaths) + public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) @@ -133,7 +167,7 @@ public class StorageService : IStorageService return builder.MoveToImmutable(); } - public ImmutableArray<(string, FluentResults.Result)> LoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePaths) + public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) @@ -144,7 +178,7 @@ public class StorageService : IStorageService return builder.MoveToImmutable(); } - public FluentResults.Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively) + public virtual FluentResults.Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively) { ((IService)this).CheckDisposed(); var r = GetAbsFromPackage(package, localSubfolder); @@ -157,53 +191,60 @@ public class StorageService : IStorageService .WithValue(arr.ToImmutableArray()); } - public async Task> LoadPackageXmlAsync(ContentPackage package, string localFilePath) => + public virtual async Task> LoadPackageXmlAsync(ContentPackage package, string localFilePath) => GetAbsFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadXmlAsync(r.Value) : r.ToResult(); - public async Task> LoadPackageBinaryAsync(ContentPackage package, string localFilePath) => + public virtual async Task> LoadPackageBinaryAsync(ContentPackage package, string localFilePath) => GetAbsFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadBinaryAsync(r.Value) : r.ToResult(); - public async Task> LoadPackageTextAsync(ContentPackage package, string localFilePath) => + public virtual async Task> LoadPackageTextAsync(ContentPackage package, string localFilePath) => GetAbsFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadTextAsync(r.Value) : r.ToResult(); - public async Task)>> LoadPackageXmlFilesAsync(ContentPackage package, ImmutableArray localFilePaths) + public virtual async Task)>> LoadPackageXmlFilesAsync(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - foreach (var path in localFilePaths) + + await localFilePaths.ParallelForEachAsync(async path => + { builder.Add((path, await LoadPackageXmlAsync(package, path))); + }, maxDegreeOfParallelism: 2); return builder.MoveToImmutable(); } - public async Task)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray localFilePaths) + public virtual async Task)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - foreach (var path in localFilePaths) + await localFilePaths.ParallelForEachAsync(async path => + { builder.Add((path, await LoadPackageBinaryAsync(package, path))); + }, maxDegreeOfParallelism: 2); return builder.MoveToImmutable(); } - public async Task)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray localFilePaths) + public virtual async Task)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - foreach (var path in localFilePaths) + await localFilePaths.ParallelForEachAsync(async path => + { builder.Add((path, await LoadPackageTextAsync(package, path))); + }, maxDegreeOfParallelism: 2); return builder.MoveToImmutable(); } - public FluentResults.Result TryLoadXml(string filePath, Encoding encoding = null) + public virtual FluentResults.Result TryLoadXml(string filePath, Encoding encoding = null) { ((IService)this).CheckDisposed(); var r = TryLoadText(filePath, encoding); @@ -216,32 +257,48 @@ public class StorageService : IStorageService } } - public FluentResults.Result TryLoadText(string filePath, Encoding encoding = null) + public virtual FluentResults.Result TryLoadText(string filePath, Encoding encoding = null) { ((IService)this).CheckDisposed(); + if (UseCaching && _fsCache.TryGetValue(filePath, out var result) + && result.TryPickT1(out var cachedVal, out _)) + { + return FluentResults.Result.Ok(cachedVal); + } + return IOExceptionsOperationRunner(nameof(TryLoadText), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); var fileText = encoding is null ? System.IO.File.ReadAllText(fp) : System.IO.File.ReadAllText(fp, encoding); + if (UseCaching) + _fsCache[filePath] = fileText; return new FluentResults.Result().WithSuccess($"Loaded file successfully").WithValue(fileText); }); } - public FluentResults.Result TryLoadBinary(string filePath) + public virtual FluentResults.Result TryLoadBinary(string filePath) { ((IService)this).CheckDisposed(); + if (UseCaching && _fsCache.TryGetValue(filePath, out var result) + && result.TryPickT0(out var cachedVal, out _)) + { + return FluentResults.Result.Ok(cachedVal); + } + return IOExceptionsOperationRunner(nameof(TryLoadBinary), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); var fileData = System.IO.File.ReadAllBytes(fp); + if (UseCaching) + _fsCache[filePath] = fileData; return new FluentResults.Result().WithSuccess($"Loaded file successfully").WithValue(fileData); }); } - public FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null) => TrySaveText(filePath, document.ToString(), encoding); - public FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null) + public virtual FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null) => TrySaveText(filePath, document.ToString(), encoding); + public virtual FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null) { ((IService)this).CheckDisposed(); if (text.IsNullOrWhiteSpace()) @@ -251,18 +308,19 @@ public class StorageService : IStorageService .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, filePath)); } - string t = text; //copy return IOExceptionsOperationRunner(nameof(TrySaveText), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); System.IO.File.WriteAllText(fp, t, encoding); + if (UseCaching) + _fsCache[filePath] = t; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } - public FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes) + public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes) { ((IService)this).CheckDisposed(); if (bytes is null || bytes.Length == 0) @@ -279,11 +337,13 @@ public class StorageService : IStorageService var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); System.IO.File.WriteAllBytes(fp, b); + if (UseCaching) + _fsCache[filePath] = b; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } - public FluentResults.Result FileExists(string filePath) + public virtual FluentResults.Result FileExists(string filePath) { ((IService)this).CheckDisposed(); return IOExceptionsOperationRunner(nameof(FileExists), filePath, () => @@ -294,7 +354,7 @@ public class StorageService : IStorageService }); } - public FluentResults.Result DirectoryExists(string directoryPath) + public virtual FluentResults.Result DirectoryExists(string directoryPath) { ((IService)this).CheckDisposed(); try @@ -308,12 +368,19 @@ public class StorageService : IStorageService } } - public async Task> TryLoadXmlAsync(string filePath, Encoding encoding = null) + public virtual async Task> TryLoadXmlAsync(string filePath, Encoding encoding = null) { + ((IService)this).CheckDisposed(); + if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) + && cachedVal.TryPickT2(out var cachedDoc, out _)) + return FluentResults.Result.Ok(cachedDoc); try { await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); - return await XDocument.LoadAsync(fs, LoadOptions.PreserveWhitespace, CancellationToken.None); + var doc = await XDocument.LoadAsync(fs, LoadOptions.PreserveWhitespace, CancellationToken.None); + if (UseCaching) + _fsCache[filePath] = doc; + return FluentResults.Result.Ok(doc); } catch (Exception e) { @@ -321,20 +388,33 @@ public class StorageService : IStorageService } } - public async Task> TryLoadTextAsync(string filePath, Encoding encoding = null) + public virtual async Task> TryLoadTextAsync(string filePath, Encoding encoding = null) { ((IService)this).CheckDisposed(); + if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) + && cachedVal.TryPickT1(out var cachedTxt, out _)) + return FluentResults.Result.Ok(cachedTxt); + return await IOExceptionsOperationRunnerAsync(nameof(TryLoadTextAsync), filePath, async () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); - return await System.IO.File.ReadAllTextAsync(fp); + var txt = await System.IO.File.ReadAllTextAsync(fp); + if (UseCaching) + _fsCache[filePath] = txt; + return FluentResults.Result.Ok(txt); }); } - public async Task> TryLoadBinaryAsync(string filePath) + public virtual async Task> TryLoadBinaryAsync(string filePath) { ((IService)this).CheckDisposed(); + if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) + && cachedVal.TryPickT0(out var cachedBin, out _)) + { + return cachedBin; + } + return await IOExceptionsOperationRunnerAsync(nameof(TryLoadTextAsync), filePath, async () => { var fp = filePath.CleanUpPath(); @@ -343,8 +423,8 @@ public class StorageService : IStorageService }); } - public async Task TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding); - public async Task TrySaveTextAsync(string filePath, string text, Encoding encoding = null) + public virtual async Task TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding); + public virtual async Task TrySaveTextAsync(string filePath, string text, Encoding encoding = null) { ((IService)this).CheckDisposed(); if (text.IsNullOrWhiteSpace()) @@ -361,11 +441,13 @@ public class StorageService : IStorageService var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); await System.IO.File.WriteAllTextAsync(fp, t, encoding); + if (UseCaching) + _fsCache[filePath] = t; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } - public async Task TrySaveBinaryAsync(string filePath, byte[] bytes) + public virtual async Task TrySaveBinaryAsync(string filePath, byte[] bytes) { ((IService)this).CheckDisposed(); if (bytes is null || bytes.Length == 0) @@ -382,6 +464,8 @@ public class StorageService : IStorageService var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); await System.IO.File.WriteAllBytesAsync(fp, b); + if (UseCaching) + _fsCache[filePath] = b; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } @@ -392,41 +476,9 @@ public class StorageService : IStorageService { return await operation?.Invoke()!; } - catch (ArgumentNullException ane) + catch (Exception e) { - return ReturnException(ane, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (ArgumentException ae) - { - return ReturnException(ae, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (PathTooLongException ptle) - { - return ReturnException(ptle, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (NotSupportedException nse) - { - return ReturnException(nse, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (UnauthorizedAccessException uae) - { - return ReturnException(uae, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (DirectoryNotFoundException dnfe) - { - return ReturnException(dnfe, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (FileNotFoundException fnfe) - { - return ReturnException(fnfe, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (SecurityException se) - { - return ReturnException(se, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (IOException ioe) - { - return ReturnException(ioe, filepath).WithError(GetGeneralError(nameof(SaveLocalXml), filepath)); + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -436,41 +488,9 @@ public class StorageService : IStorageService { return await operation?.Invoke()!; } - catch (ArgumentNullException ane) + catch (Exception e) { - return ReturnException(ane, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (ArgumentException ae) - { - return ReturnException(ae, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (PathTooLongException ptle) - { - return ReturnException(ptle, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (NotSupportedException nse) - { - return ReturnException(nse, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (UnauthorizedAccessException uae) - { - return ReturnException(uae, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (DirectoryNotFoundException dnfe) - { - return ReturnException(dnfe, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (FileNotFoundException fnfe) - { - return ReturnException(fnfe, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (SecurityException se) - { - return ReturnException(se, filepath).WithError(GetGeneralError(funcName, filepath)); - } - catch (IOException ioe) - { - return ReturnException(ioe, filepath).WithError(GetGeneralError(nameof(SaveLocalXml), filepath)); + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -480,50 +500,9 @@ public class StorageService : IStorageService { return operation?.Invoke(); } - catch (ArgumentNullException ane) + catch (Exception e) { - return ReturnException(ane, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (ArgumentException ae) - { - return ReturnException(ae, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (PathTooLongException ptle) - { - return ReturnException(ptle, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (NotSupportedException nse) - { - return ReturnException(nse, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (UnauthorizedAccessException uae) - { - return ReturnException(uae, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (DirectoryNotFoundException dnfe) - { - return ReturnException(dnfe, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (FileNotFoundException fnfe) - { - return ReturnException(fnfe, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (SecurityException se) - { - return ReturnException(se, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (IOException ioe) - { - return ReturnException(ioe, filepath) - .WithError(GetGeneralError(nameof(SaveLocalXml), filepath)); + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -533,50 +512,9 @@ public class StorageService : IStorageService { return operation?.Invoke(); } - catch (ArgumentNullException ane) + catch (Exception e) { - return ReturnException(ane, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (ArgumentException ae) - { - return ReturnException(ae, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (PathTooLongException ptle) - { - return ReturnException(ptle, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (NotSupportedException nse) - { - return ReturnException(nse, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (UnauthorizedAccessException uae) - { - return ReturnException(uae, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (DirectoryNotFoundException dnfe) - { - return ReturnException(dnfe, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (FileNotFoundException fnfe) - { - return ReturnException(fnfe, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (SecurityException se) - { - return ReturnException(se, filepath) - .WithError(GetGeneralError(funcName, filepath)); - } - catch (IOException ioe) - { - return ReturnException(ioe, filepath) - .WithError(GetGeneralError(nameof(SaveLocalXml), filepath)); + return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -610,14 +548,11 @@ public class StorageService : IStorageService } return new FluentResults.Result().WithSuccess($"Path constructed") - .WithValue( System.IO.Path.GetFullPath(System.IO.Path.Combine( - _runLocation, - LocalStoragePath.Value, - LocalFilePathRule.Value.Replace(_packagePathKeyword, package.Name.IsNullOrWhiteSpace() - ? package.TryExtractSteamWorkshopId(out var id) - ? id.Value.ToString() - : "_fallbackFolder" - : package.Name), + .WithValue(System.IO.Path.GetFullPath(System.IO.Path.Combine( + _configData.RunLocation, + _configData.LocalPackageDataPath.Replace( + _configData.LocalDataPathRegex, + package.TryExtractSteamWorkshopId(out var id) ? id.Value.ToString() : package.Name), localFilePath))); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs index 5e5a19118..b26cc724b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs @@ -3,77 +3,44 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using System.Xml.Linq; using Barotrauma.LuaCs.Configuration; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Services; using Barotrauma.LuaCs.Services.Safe; using Barotrauma.Networking; +using FluentResults; namespace Barotrauma.LuaCs.Services; public partial interface IConfigService : IReusableService, ILuaConfigService { - /* - * Resource Files. - */ + /// + /// Registers a type initializer from instancing config types by indicated type from config. + /// + /// + /// + /// The as parsed from the configuration info. + /// The resulting configuration instance. + void RegisterTypeInitializer(Func> initializer, bool replaceIfExists = false) + where TData : IEquatable where TConfig : IConfigBase; + + // Config Files/Resources Task LoadConfigsAsync(ImmutableArray configResources); Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources); - FluentResults.Result DisposeConfigs(ImmutableArray configResources); - FluentResults.Result DisposeConfigsProfiles(ImmutableArray configProfilesResources); - FluentResults.Result DisposeConfigs(ContentPackage package); - FluentResults.Result DisposeConfigsProfiles(ContentPackage package); + // Immediate Mode + FluentResults.Result AddConfig(IConfigInfo configInfo) where TConfig : IConfigBase; - /* - * Immediate mode - */ - FluentResults.Result> 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; - - FluentResults.Result AddConfigList(ContentPackage package, string name, - int defaultIndex, IReadOnlyList values, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action onValueChanged = null); - - FluentResults.Result> AddConfigRangeEntry(ContentPackage package, string name, - T defaultValue, T minValue, T maxValue, - Func, int> getStepCount, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action> onValueChanged = null) where T : IConvertible, IEquatable; - - FluentResults.Result> AddConfigEntry(string packageName, string name, - T defaultValue, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action> onValueChanged = null) where T : IConvertible, IEquatable; - - FluentResults.Result AddConfigList(string packageName, string name, - int defaultIndex, IReadOnlyList values, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action onValueChanged = null); - - FluentResults.Result> AddConfigRangeEntry(string packageName, string name, - T defaultValue, T minValue, T maxValue, - Func, int> getStepCount, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action> onValueChanged = null) where T : IConvertible, IEquatable; - - FluentResults.Result> GetConfigsForPackage(ContentPackage package); - FluentResults.Result> GetConfigsForPackage(string packageName); - IReadOnlyDictionary<(ContentPackage, string), IConfigBase> GetAllConfigs(); - T GetConfig(ContentPackage package, string name) where T : IConfigBase; - T GetConfig(string packageName, string name) where T : IConfigBase; + // Utility + FluentResults.Result ApplyProfileSettings(ContentPackage package, string profileName); + FluentResults.Result DisposePackageData(ContentPackage package); + FluentResults.Result> GetConfigsForPackage(ContentPackage package); + FluentResults.Result Value)>>> + GetProfilesForPackage(ContentPackage package); + IReadOnlyDictionary<(ContentPackage Package, string Name), IConfigBase> GetAllConfigs(); + bool TryGetConfig(ContentPackage package, string name, out T config) where T : IConfigBase; + Task SaveAllConfigs(); + Task SaveConfigsForPackage(ContentPackage package); + Task SaveConfig((ContentPackage Package, string ConfigName) config); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILocalizationService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILocalizationService.cs deleted file mode 100644 index 59abcfe90..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILocalizationService.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Globalization; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading.Tasks; -using Barotrauma.LuaCs.Data; - -namespace Barotrauma.LuaCs.Services; - -public interface ILocalizationService : IReusableService -{ - IReadOnlyCollection GetLoadedLocales(); - void Remove(ImmutableArray localizations); - void DisposePackage(ContentPackage package); - FluentResults.Result SetCurrentCulture(CultureInfo culture); - FluentResults.Result SetCurrentCulture(string cultureName); - Task LoadLocalizations(ImmutableArray localizationResources); - - /// - /// Tries to get a localized string without a fallback. Returns success/failure and associated data. - /// - /// Neutral localization key. - /// - FluentResults.Result GetLocalizedString(string key); - FluentResults.Result GetLocalizedString(string key, CultureInfo targetCulture); - string GetLocalizedString(string key, string fallback); - string GetLocalizedString(string key, string fallback, CultureInfo targetCulture); - FluentResults.Result GetLocalizedStringForPackage(ContentPackage package, string key); - FluentResults.Result GetLocalizedStringForPackage(ContentPackage package, string key, CultureInfo targetCulture); - string GetLocalizedStringForPackage(ContentPackage package, string key, string fallback); - string GetLocalizedStringForPackage(ContentPackage package, string key, string fallback, CultureInfo targetCulture); - FluentResults.Result RegisterLocalizationResolver(CultureInfo targetCulture, Func factoryResolver); - bool IsCurrentCultureSupported(IResourceCultureInfo culturesInfo); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILuaScriptManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILuaScriptManagementService.cs index 88e432400..d8acb5544 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILuaScriptManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILuaScriptManagementService.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Reflection; using System.Threading.Tasks; using Barotrauma.LuaCs.Data; +using FluentResults; using MoonSharp.Interpreter; using MoonSharp.Interpreter.Interop; @@ -13,13 +14,58 @@ public interface ILuaScriptManagementService : IReusableService { #region Script_Ops + Result GetGlobalTableValue(string tableName); + + /// + /// Parses and loads script sources (code) into a memory cache without executing it. + /// + /// + /// + // [Required] Task LoadScriptResourcesAsync(ImmutableArray resourcesInfo); - FluentResults.Result ExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false); - FluentResults.Result ExecuteLoadedScripts(ImmutableArray scripts, bool pauseExecutionOnError = false, bool verboseLogging = false); - FluentResults.Result ExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false); + /// + /// Executes cached scripts (code) for the given . + /// + /// + /// + // [Required] + FluentResults.Result ExecuteLoadedScriptsForPackage(ContentPackage package); + + /// + /// Executes cached scripts (code) for the given collection . + /// + /// + /// + // [Required] + FluentResults.Result ExecuteLoadedScriptsForPackages(IEnumerable packages); + + /// + /// + /// + /// + // [Required] + FluentResults.Result ExecuteLoadedScripts(); + + /// + /// + /// + /// + /// + // [Required] FluentResults.Result DisposePackageResources(ContentPackage package); + + /// + /// Calls dispose on, and clears active refs for, currently running scripts. Does not clear caches. + /// + /// FluentResults.Result UnloadActiveScripts(); + + /// + /// Unloads all scripts and clears all caches/references. + /// + /// + /// May be functionally equivalent to FluentResults.Result DisposeAllPackageResources(); #endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/INetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/INetworkingService.cs index 835176d95..c89b5e63e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/INetworkingService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/INetworkingService.cs @@ -8,7 +8,7 @@ namespace Barotrauma.LuaCs.Services; internal delegate void NetMessageReceived(IReadMessage netMessage); -internal partial interface INetworkingService : IReusableService, ILuaCsNetworking +internal partial interface INetworkingService : IReusableService, ILuaCsNetworking, IEntityNetworkingService { bool IsActive { get; } bool IsSynchronized { get; } @@ -20,6 +20,11 @@ internal partial interface INetworkingService : IReusableService, ILuaCsNetworki #elif CLIENT public void Send(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable); #endif - public void RegisterNetVar(INetVar netVar); - public void SendNetVar(INetVar netVar); + +} + +public interface IEntityNetworkingService +{ + public void RegisterNetVar(INetworkSyncEntity netVar); + public void SendNetVar(INetworkSyncEntity netVar); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageInfoLookupService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageInfoLookupService.cs index 36dabf384..836103c25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageInfoLookupService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageInfoLookupService.cs @@ -7,6 +7,7 @@ namespace Barotrauma.LuaCs.Services; public interface IPackageInfoLookupService : IReusableService { + bool IsPackageEnabled(ContentPackage package); Task> Lookup(string packageName); Task> Lookup(string packageName, ulong steamWorkshopId); Task> Lookup(ulong steamWorkshopId); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs index 72bf528d0..c342ff9f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs @@ -9,11 +9,7 @@ using Barotrauma.LuaCs.Data; namespace Barotrauma.LuaCs.Services; -public interface IPackageManagementService : IReusableService, ILocalizationsResourcesInfo, IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo, IAssembliesResourcesInfo -#if CLIENT - ,IStylesResourcesInfo -#endif - +public interface IPackageManagementService : IReusableService, IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo, IAssembliesResourcesInfo { /// /// Loads and parses the provided for supported by the current runtime environment. @@ -30,6 +26,20 @@ public interface IPackageManagementService : IReusableService, ILocalizationsRes /// Task> LoadPackagesInfosAsync(IReadOnlyList packages); IReadOnlyList GetAllLoadedPackages(); + bool IsPackageLoaded(ContentPackage package); + + /// + /// Filters out resources not suitable for the current environment using the following criteria:
+ /// - Platform (Operating System)
+ /// - Target (Client|Server)
+ /// - Null/Invalid
+ /// - Dependency Package Registered in PMS
+ ///
+ /// + /// + /// + ImmutableArray FilterUnloadableResources(IReadOnlyList resources, bool enabledPackagesOnly = false) + where T : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo; void DisposePackageInfos(ContentPackage package); void DisposePackagesInfos(IReadOnlyList packages); FluentResults.Result GetPackageDependencyInfo(ContentPackage ownerPackage, string packageName, ulong steamWorkshopId); @@ -38,28 +48,15 @@ public interface IPackageManagementService : IReusableService, ILocalizationsRes FluentResults.Result GetAssembliesInfos(ContentPackage package, bool onlySupportedResources = true); FluentResults.Result GetConfigsInfos(ContentPackage package, bool onlySupportedResources = true); FluentResults.Result GetConfigProfilesInfos(ContentPackage package, bool onlySupportedResources = true); - FluentResults.Result GetLocalizationsInfos(ContentPackage package, bool onlySupportedResources = true); FluentResults.Result GetLuaScriptsInfos(ContentPackage package, bool onlySupportedResources = true); -#if CLIENT - FluentResults.Result GetStylesInfos(ContentPackage package, bool onlySupportedResources = true); -#endif // collection FluentResults.Result GetAssembliesInfos(IReadOnlyList packages, bool onlySupportedResources = true); FluentResults.Result GetConfigsInfos(IReadOnlyList packages, bool onlySupportedResources = true); FluentResults.Result GetConfigProfilesInfos(IReadOnlyList packages, bool onlySupportedResources = true); - FluentResults.Result GetLocalizationsInfos(IReadOnlyList packages, bool onlySupportedResources = true); - FluentResults.Result GetLuaScriptsInfos(IReadOnlyList packages, bool onlySupportedResources = true); -#if CLIENT - FluentResults.Result GetStylesInfos(IReadOnlyList packages, bool onlySupportedResources = true); -#endif + FluentResults.Result GetLuaScriptsInfos(IReadOnlyList packages, bool onlySupportedResources = true); Task> GetAssembliesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); Task> GetConfigsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); Task> GetConfigProfilesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); - Task> GetLocalizationsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); - Task> GetLuaScriptsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); -#if CLIENT - Task> GetStylesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); -#endif - + Task> GetLuaScriptsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs index 281c39a9b..860a8eb6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System; +using System.Collections.Immutable; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; @@ -7,6 +8,26 @@ namespace Barotrauma.LuaCs.Services; public interface IStorageService : IService { + + bool UseCaching { get; set; } + + /// + /// Deletes all cached file data. + /// + void PurgeCache(); + + /// + /// Deletes the data for the supplied file path from the data cache. + /// + /// + void PurgeFileFromCache(string absolutePath); + + /// + /// Deletes the data from the supplied file paths from the data cache. + /// + /// + void PurgeFilesFromCache(params string[] absolutePaths); + // -- local game folder storage FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath); FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs index ebfc6269e..ce6334379 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Loader; using System.Threading; using Barotrauma.Extensions; @@ -55,22 +56,131 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService //internal private readonly IAssemblyManagementService _assemblyManagementService; private readonly Action _onUnload; + private readonly Func _onResolvingManaged; + private readonly Func _onResolvingUnmanagedDll; private readonly ConcurrentDictionary _dependencyResolvers = new(); private readonly ConcurrentDictionary _loadedAssemblyData = new(); private readonly ThreadLocal _isResolving = new(static()=>false); // cyclic resolution exit + private readonly ThreadLocal _isResolvingNative = new(static () => false); - public AssemblyLoader(IAssemblyManagementService assemblyManagementService, + public AssemblyLoader( + IAssemblyManagementService assemblyManagementService, Guid id, string name, - bool isReferenceOnlyMode, Action onUnload) + bool isReferenceOnlyMode, + Action onUnload = null) : base(isCollectible: true, name: name) { _assemblyManagementService = assemblyManagementService; Id = id; IsReferenceOnlyMode = isReferenceOnlyMode; - _onUnload = onUnload; - if (_onUnload is not null) - base.Unloading += OnUnload; + base.Unloading += OnUnload; + base.Resolving += OnResolvingManagedAssembly; + base.ResolvingUnmanagedDll += OnResolvingUnmanagedDll; + } + + private IntPtr OnResolvingUnmanagedDll(Assembly invokingAssembly, string assemblyName) + { + if (IsDisposed) + return 0; + + if (_isResolvingNative.Value) + return 0; + + AreOperationRunning = true; + _isResolvingNative.Value = true; + try + { + if (!_dependencyResolvers.IsEmpty) + { + foreach (var resolver in _dependencyResolvers) + { + try + { + var path = resolver.Value.ResolveUnmanagedDllToPath(assemblyName); + if (path.IsNullOrWhiteSpace()) + continue; + return base.LoadUnmanagedDllFromPath(path); + } + catch + { + // ignored + continue; + } + } + } + + if (_onResolvingUnmanagedDll is not null) + { + try + { + return _onResolvingUnmanagedDll(invokingAssembly, assemblyName); + } + catch + { + // ignored + } + } + + return 0; + } + finally + { + AreOperationRunning = false; + _isResolvingNative.Value = false; + } + } + + private Assembly OnResolvingManagedAssembly(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName) + { + if (IsDisposed) + return null; + + if (_isResolving.Value) + return null; + + AreOperationRunning = true; + _isResolving.Value = true; + try + { + if (!_dependencyResolvers.IsEmpty) + { + foreach (var resolver in _dependencyResolvers) + { + try + { + var path = resolver.Value.ResolveAssemblyToPath(assemblyName); + if (path.IsNullOrWhiteSpace()) + continue; + return assemblyLoadContext.LoadFromAssemblyPath(path); + } + catch + { + // ignored + continue; + } + } + } + + if (_onResolvingManaged is not null) + { + try + { + return _onResolvingManaged(assemblyLoadContext, assemblyName); + } + catch + { + // ignored + } + } + + return null; + } + finally + { + AreOperationRunning = false; + _isResolving.Value = false; + } } public IEnumerable AssemblyReferences @@ -107,10 +217,13 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } catch (Exception ex) { - return res.WithError(new ExceptionalError(ex) + res = res.WithError(new ExceptionalError(ex) .WithMetadata(MetadataType.Sources, path)); } } + + if (res.Errors.Any()) + return FluentResults.Result.Fail(res.Errors); return FluentResults.Result.Ok(); } finally @@ -140,13 +253,16 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService if (_loadedAssemblyData.ContainsKey(assemblyName)) { - return new Result().WithError(new Error($"The name provided is already assigned to an assembly!") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, syntaxTrees)); + return new Result().WithError( + new Error($"The name provided is already assigned to an assembly!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); } - - var compilationAssemblyName = compileWithInternalAccess ? IAssemblyLoaderService.InternalsAwareAssemblyName : assemblyName; - + + var compilationAssemblyName = compileWithInternalAccess + ? IAssemblyLoaderService.InternalsAwareAssemblyName + : assemblyName; + compilationOptions ??= new CSharpCompilationOptions( outputKind: OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release, @@ -158,7 +274,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService { typeof(CSharpCompilationOptions) .GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic) - ?.SetValue(compilationOptions, + ?.SetValue(compilationOptions, (uint)1 << 25 // CSharp.BinderFlags.AllowAwaitInUnsafeContext | (uint)1 << 22 // CSharp.BinderFlags.IgnoreAccessibility | (uint)1 << 1 // CSharp.BinderFlags.SuppressObsoleteChecks @@ -166,34 +282,39 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService } using var asmMemoryStream = new MemoryStream(); - var result = CSharpCompilation.Create(compilationAssemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(asmMemoryStream); + var result = CSharpCompilation + .Create(compilationAssemblyName, syntaxTrees, metadataReferences, compilationOptions) + .Emit(asmMemoryStream); if (!result.Success) { var res = new FluentResults.Result().WithError( new Error($"Compilation failed for assembly {assemblyName}!") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, syntaxTrees)); - var failuresDiag = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error); + var failuresDiag = + result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error); foreach (var diag in failuresDiag) { res = res.WithError(new Error(diag.GetMessage()) .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.ExceptionDetails, diag.Descriptor.Description)); - } + } + return res; } - + asmMemoryStream.Seek(0, SeekOrigin.Begin); - try - { - var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray()); - _loadedAssemblyData[data.Assembly] = data; - return new Result().WithSuccess($"Compiled assembly {assemblyName} successful.").WithValue(data.Assembly); - } - catch (Exception ex) - { - return new FluentResults.Result().WithError(new ExceptionalError(ex)); - } + var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray()); + _loadedAssemblyData[data.Assembly] = data; + return new Result().WithSuccess($"Compiled assembly {assemblyName} successful.") + .WithValue(data.Assembly); + } + catch (Exception ex) + { + return new FluentResults.Result().WithError(new ExceptionalError(ex) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyName) + .WithMetadata(MetadataType.Sources, syntaxTrees)); } finally { @@ -211,7 +332,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService try { if (assemblyFilePath.IsNullOrWhiteSpace()) - return new Result().WithError(new Error($"The path provided is null!")); + return new Result().WithError(new Error($"The path provided is empty.")); if (additionalDependencyPaths.Any()) { @@ -219,7 +340,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService if (!r.IsFailed) { // we have errors, loading may not work. - return FluentResults.Result.Fail(new Error($"Failed to load dependency paths") + return FluentResults.Result.Fail(new Error($"Failed to load dependency paths.") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, assemblyFilePath)) .WithErrors(r.Errors); @@ -240,61 +361,51 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService { var assembly = LoadFromAssemblyPath(sanitizedFilePath); _loadedAssemblyData[assembly] = new AssemblyData(assembly, sanitizedFilePath); - return new Result().WithSuccess($"Loaded assembly'{assembly.GetName()}'").WithValue(assembly); - } - catch (ArgumentNullException ane) - { - return FluentResults.Result.Fail(new ExceptionalError(ane) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, ane.Message) - .WithMetadata(MetadataType.StackTrace, ane.StackTrace)); - } - catch (ArgumentException ae) - { - return FluentResults.Result.Fail(new ExceptionalError(ae) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, ae.Message) - .WithMetadata(MetadataType.StackTrace, ae.StackTrace)); - } - catch (FileLoadException fle) - { - return FluentResults.Result.Fail(new ExceptionalError(fle) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, fle.Message) - .WithMetadata(MetadataType.StackTrace, fle.StackTrace)); + return new Result().WithSuccess($"Loaded assembly '{assembly.GetName()}'").WithValue(assembly); } catch (FileNotFoundException fnfe) { - return FluentResults.Result.Fail(new ExceptionalError(fnfe) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, fnfe.Message) - .WithMetadata(MetadataType.StackTrace, fnfe.StackTrace)); - } - catch (BadImageFormatException bife) - { - return FluentResults.Result.Fail(new ExceptionalError(bife) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, bife.Message) - .WithMetadata(MetadataType.StackTrace, bife.StackTrace)); + // last attempt + try + { + var assemblyName = new AssemblyName(System.IO.Path.GetFileName(sanitizedFilePath)); + foreach (var resolver in _dependencyResolvers) + { + try + { + var path = resolver.Value.ResolveAssemblyToPath(assemblyName); + return base.LoadFromAssemblyPath(path); + } + catch + { + continue; + } + } + return GenerateExceptionReturn(fnfe); + } + catch (Exception e) + { + return GenerateExceptionReturn(fnfe); + } } catch (Exception e) { - return FluentResults.Result.Fail(new ExceptionalError(e) - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, assemblyFilePath) - .WithMetadata(MetadataType.ExceptionDetails, e.Message) - .WithMetadata(MetadataType.StackTrace, e.StackTrace)); + return GenerateExceptionReturn(e); } } finally { AreOperationRunning = false; } + + FluentResults.Result GenerateExceptionReturn(T exception) where T : Exception + { + return FluentResults.Result.Fail(new ExceptionalError(exception) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, exception.Message) + .WithMetadata(MetadataType.StackTrace, exception.StackTrace)); + } } public FluentResults.Result GetAssemblyByName(string assemblyName) @@ -303,7 +414,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService return FluentResults.Result.Fail(new Error($"Loader is disposed!")); if (assemblyName.IsNullOrWhiteSpace()) { - return FluentResults.Result.Fail(new Error($"Assembly name is null") + return FluentResults.Result.Fail(new Error($"Assembly name is empty.") .WithMetadata(MetadataType.ExceptionObject, this)); } AreOperationRunning = true; @@ -311,7 +422,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService { if (_loadedAssemblyData.TryGetValue(assemblyName, out var data)) { - return new Result().WithSuccess(new Success($"Assembly found")).WithValue(data.Assembly); + return new Result().WithSuccess(new Success($"Assembly found.")).WithValue(data.Assembly); } // search any assemblies that were background loaded and we're unaware of. @@ -332,11 +443,11 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService // ignored } - return new Result().WithSuccess(new Success($"Assembly found")).WithValue(assembly1); + return new Result().WithSuccess(new Success($"Assembly found.")).WithValue(assembly1); } } - return FluentResults.Result.Fail(new Error($"Assembly named { assemblyName } not found!")); + return FluentResults.Result.Fail(new Error($"Assembly named '{ assemblyName }' not found!")); } finally { @@ -420,60 +531,97 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService return; // we don't want to invoke events twice nor cause strong GC handles. IsDisposed = true; this.Unload(); + this.DisposeInternal(); + } + + ~AssemblyLoader() + { + this.DisposeInternal(); + } + + private void OnUnload(AssemblyLoadContext context) + { + // Try to wait for loading ops on other threads if they happen to occur with a timeout. + // This should be an edge, should it even occur. + DateTime timeout = DateTime.Now.AddSeconds(2); + while (timeout > DateTime.Now) + { + if (!AreOperationRunning) + break; + Thread.Sleep(1000/Timing.FixedUpdateRate-1); + } + + var wf = new WeakReference(this); + _onUnload?.Invoke(this); + } + + private void DisposeInternal() + { + IsDisposed = true; + base.Resolving -= OnResolvingManagedAssembly; + base.ResolvingUnmanagedDll -= OnResolvingUnmanagedDll; + base.Unloading -= OnUnload; + this._dependencyResolvers.Clear(); + this._loadedAssemblyData.Clear(); GC.SuppressFinalize(this); } protected override Assembly Load(AssemblyName assemblyName) { - if (_isResolving.Value) + if (IsDisposed) return null; - - _isResolving.Value = true; + AreOperationRunning = true; try { - if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var data)) - return data.Assembly; - var ids = new[] { this.Id }; - if (_assemblyManagementService.GetLoadedAssembly(assemblyName, in ids) is { IsSuccess: true } ret) - return ret.Value; + if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var assembly)) + return assembly.Assembly; return null; } - catch (ArgumentNullException _) + catch { return null; } finally { - _isResolving.Value = false; + AreOperationRunning = false; } } - // Use the default import resolver since native libraries are niche and not blocking for unloading. - // Implement if conflicts become an issue. - /*protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { - // Implement NativeLibrary::InternalLoadUnmanagedDll() - throw new NotImplementedException(); - }*/ - - private void OnUnload(AssemblyLoadContext context) - { - IsDisposed = true; - - // Try to wait for loading ops on other threads if they happen to occur. - // Minor race condition on the loop exit but this loader is not intended to be thread-safe by design, this is just to cover edge cases. - DateTime timeout = DateTime.Now.AddSeconds(5); - while (timeout > DateTime.Now) + if (IsDisposed) + return 0; + + GCHandle? handle = null; + AreOperationRunning = true; + try { - if (!AreOperationRunning) - break; + if (_loadedAssemblyData.TryGetValue(unmanagedDllName, out var assemblyData)) + { + handle = GCHandle.Alloc(assemblyData.Assembly, GCHandleType.Pinned); + nint asmPtr = GCHandle.ToIntPtr(handle.Value); + return asmPtr; + } } - - base.Unloading -= OnUnload; - var wf = new WeakReference(this); - _onUnload?.Invoke(this); - this._dependencyResolvers.Clear(); - this._loadedAssemblyData.Clear(); + catch + { + return 0; + } + finally + { + AreOperationRunning = false; + try + { + if (handle.HasValue) + handle.Value.Free(); + } + catch + { + // ignored. We just want to ensure that free is called. + } + } + + return 0; } private readonly record struct AssemblyData @@ -538,7 +686,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService public int GetHashCode(AssemblyOrStringKey obj) { - return obj.HashCode; + return this.HashCode; } public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly);