IT BUILDS!!!

- Removed LocalizationServices and other sus things.
- Rewrote AssemblyLoader
[In Progress] SafeStorageService
[In Progress] LuaScriptLoader
This commit is contained in:
MapleWheels
2025-03-30 06:20:45 -04:00
committed by Maplewheels
parent 52d920d969
commit c6713f37bb
67 changed files with 3336 additions and 1283 deletions

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Configuration;
/// <summary>
/// Base type of all menu displayable types.
/// </summary>
public interface IDisplayableConfigBase : IDataInfo, IConfigDisplayInfo
{
/// <summary>
/// Whether the current config is editable.
/// </summary>
bool IsEditable { get; }
/// <summary>
/// Used to indicate the implemented interface and targeted display logic.
/// </summary>
static virtual DisplayType DisplayOption => DisplayType.Undefined;
}
public interface IDisplayableConfigBase<out TDisplay, in TValue> : IDisplayableConfigBase
{
void SetValue(TValue value);
TDisplay GetDisplayValue();
}
public interface IDisplayableConfigBool : IDisplayableConfigBase<bool, bool>
{
static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Boolean;
}
public interface IDisplayableConfigText : IDisplayableConfigBase<string, string>
{
static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Text;
}
public interface IDisplayableConfigInt : IDisplayableConfigBase<int, int>
{
static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Integer;
}
public interface IDisplayableConfigFloat : IDisplayableConfigBase<float, float>
{
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<List<string>, string>
{
static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Dropdown;
}
/// <summary>
/// Allows completely custom-designed UI for this configuration component.
/// </summary>
public interface IDisplayableConfigCustom : IDisplayableConfigBase
{
static DisplayType IDisplayableConfigBase.DisplayOption => DisplayType.Custom;
/// <summary>
/// Draw your menu settings option.
/// </summary>
/// <param name="layoutGroup">Parent layout component.</param>
void DrawComponent(GUILayoutGroup layoutGroup);
/// <summary>
/// Called when the config element is set to be disposed to allow for cleanup.
/// </summary>
void DisposeGUI();
/// <summary>
/// Called when the UI indicates to save the current value as permanent.
/// </summary>
void OnValueSaved();
/// <summary>
/// Called when the UI indicates to discard the currently displayed value and revert to the last saved value.
/// </summary>
void OnValueDiscarded();
}
/// <summary>
/// Indicates the intended display and feedback logic to be used by the <see cref="SettingsMenu"/>.
/// <br/><b>[Important]</b>
/// <br/>The type must implement the indicated interface for the selected option, or it will not be displayed.
/// </summary>
public enum DisplayType
{
/// <summary>
/// Will not be displayed in menus.
/// </summary>
Undefined,
/// <summary>
/// Will be shown as a checkbox.
/// <br/><b>[Requires(<see cref="IDisplayableConfigBool"/>)]</b>
/// </summary>
Boolean,
/// <summary>
/// Shown as an editable text input.
/// <br/><b>[Requires(<see cref="IDisplayableConfigText"/>)]</b>
/// </summary>
Text,
/// <summary>
/// Shown as number input (no decimal input).
/// <br/><b>[Requires(<see cref="IDisplayableConfigInt"/>)]</b>
/// </summary>
Integer,
/// <summary>
/// Shown as a number input.
/// <br/><b>[Requires(<see cref="IDisplayableConfigFloat"/>)]</b>
/// </summary>
Float,
/// <summary>
/// Shown as a slider, values parsed as integers.
/// <br/><b>[Requires(<see cref="IDisplayableConfigSliderInt"/>)]</b>
/// </summary>
SliderInt,
/// <summary>
/// Shown as a slider, values parsed as single-precision decimal numbers.
/// <br/><b>[Requires(<see cref="IDisplayableConfigSliderFloat"/>)]</b>
/// </summary>
SliderFloat,
/// <summary>
/// Shown as a <see cref="GUIDropDown"/> menu, values parsed as strings.
/// <br/><b>[Requires(<see cref="IDisplayableConfigDropdown"/>)]</b>
/// </summary>
Dropdown,
/// <summary>
/// UI Display is implemented by inheritor and actioned by a call to <see cref="IDisplayableConfigCustom.DrawComponent"/>.
/// <br/><b>[Requires(<see cref="IDisplayableConfigCustom"/>)]</b>
/// </summary>
Custom
}

View File

@@ -1,23 +0,0 @@
using System.Collections.Immutable;
using System.Globalization;
namespace Barotrauma.LuaCs.Data;
public partial record ModConfigInfo : IModConfigInfo
{
public ImmutableArray<IStylesResourceInfo> Styles { get; init; }
}
public record StylesResourceInfo : IStylesResourceInfo
{
public Platform SupportedPlatforms { get; init; }
public Target SupportedTargets { get; init; }
public int LoadPriority { get; init; }
public ImmutableArray<string> FilePaths { get; init; }
public bool Optional { get; init; }
public ImmutableArray<CultureInfo> SupportedCultures { get; init; }
public string InternalName { get; init; }
public ContentPackage OwnerPackage { get; init; }
public string FallbackPackageName { get; init; }
public ImmutableArray<IPackageDependency> Dependencies { get; init; }
}

View File

@@ -2,8 +2,22 @@ using Barotrauma.LuaCs.Configuration;
namespace Barotrauma.LuaCs.Data;
public partial interface IConfigInfo
public partial interface IConfigInfo : IConfigDisplayInfo { }
public interface IConfigDisplayInfo
{
/// <summary>
/// User-friendly name or Localization Token.
/// </summary>
string DisplayName { get; }
/// <summary>
/// User-friendly description or Localization Token.
/// </summary>
string Description { get; }
/// <summary>
/// The menu category to display under. Used for filtering.
/// </summary>
string DisplayCategory { get; }
/// <summary>
/// Should this config be displayed in end-user menus.
/// </summary>

View File

@@ -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
{
/// <summary>
/// Collection of loadable styles data.
/// </summary>
ImmutableArray<IStylesResourceInfo> Styles { get; }
}

View File

@@ -1,6 +0,0 @@
namespace Barotrauma.LuaCs.Data;
public interface IStylesInfo
{
}

View File

@@ -22,9 +22,6 @@ namespace Barotrauma
}
private partial bool ShouldRunCs() => IsCsEnabled.Value;
public IStylesManagementService StylesManagementService => _servicesProvider.TryGetService<IStylesManagementService>(out var svc)
? svc : throw new NullReferenceException("Networking Manager service not found!");
public void CheckCsEnabled()
{

View File

@@ -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<IDisplayableConfigBase> GetDisplayableConfigs()
{
throw new NotImplementedException();
}
public ImmutableArray<IDisplayableConfigBase> GetDisplayableConfigsForPackage(ContentPackage package)
{
throw new NotImplementedException();
}
public Result<IConfigControl> AddConfigControl(IConfigInfo configInfo)
{
throw new NotImplementedException();
}
}

View File

@@ -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<IConfigEntry<T>> AddConfigEntry<T>(IDisplayableData data,
T defaultValue,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigList> AddConfigList(IDisplayableData data,
int defaultIndex, IReadOnlyList<string> values,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<IConfigList, int, bool> valueChangePredicate = null,
Action<IConfigList, int> onValueChanged = null);
ImmutableArray<IDisplayableConfigBase> GetDisplayableConfigs();
ImmutableArray<IDisplayableConfigBase> GetDisplayableConfigsForPackage(ContentPackage package);
FluentResults.Result<IConfigRangeEntry<T>> AddConfigRangeEntry<T>(IDisplayableData data,
T defaultValue, T minValue, T maxValue,
Func<IConfigRangeEntry<T>, int> getStepCount,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigControl> AddConfigControl(IConfigInfo configInfo);
}

View File

@@ -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<FluentResults.Result> LoadStylesAsync(ImmutableArray<IStylesResourceInfo> styles);
FluentResults.Result<IStylesService> GetStylesService(ContentPackage package);
Task<FluentResults.Result> DisposeAllStyles();
Task<FluentResults.Result> DisposeStylesForPackage(ContentPackage package);
}

View File

@@ -1,58 +0,0 @@
using System.Threading.Tasks;
namespace Barotrauma.LuaCs.Services;
// TODO: Rework interface to support resource infos.
/// <summary>
/// Loads XML Style assets from the given content package.
/// </summary>
public interface IStylesService : IService
{
/// <summary>
/// Tries to load the styles file for the given <see cref="ContentPackage"/> and path into a new <see cref="UIStyleProcessor"/> instance.
/// </summary>
/// <param name="package"></param>
/// <param name="path"></param>
/// <returns></returns>
Task<FluentResults.Result> LoadStylesFileAsync(ContentPackage package, ContentPath path);
/// <summary>
/// Unloads all styles assets and <see cref="UIStyleProcessor"/> instances.
/// </summary>
FluentResults.Result UnloadAllStyles();
/// <summary>
/// Tries to the get the <see cref="GUIFont"/> asset by xml asset name, returns null on failure.
/// </summary>
/// <param name="fontName">XML Name of the asset.</param>
/// <returns>The asset or null if none are found.</returns>
GUIFont GetFont(string fontName);
/// <summary>
/// Tries to the get the <see cref="GUISprite"/> asset by xml asset name, returns null on failure.
/// </summary>
/// <param name="spriteName">XML Name of the asset.</param>
/// <returns>The asset or null if none are found.</returns>
GUISprite GetSprite(string spriteName);
/// <summary>
/// Tries to the get the <see cref="GUISpriteSheet"/> asset by xml asset name, returns null on failure.
/// </summary>
/// <param name="spriteSheetName">XML Name of the asset.</param>
/// <returns>The asset or null if none are found.</returns>
GUISpriteSheet GetSpriteSheet(string spriteSheetName);
/// <summary>
/// Tries to the get the <see cref="GUICursor"/> asset by xml asset name, returns null on failure.
/// </summary>
/// <param name="cursorName">XML Name of the asset.</param>
/// <returns>The asset or null if none are found.</returns>
GUICursor GetCursor(string cursorName);
/// <summary>
/// Tries to the get the <see cref="GUIColor"/> asset by xml asset name, returns null on failure.
/// </summary>
/// <param name="colorName">XML Name of the asset.</param>
/// <returns>The asset or null if none are found.</returns>
GUIColor GetColor(string colorName);
}

View File

@@ -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<ushort, Queue<IReadMessage>> receiveQueue = new Dictionary<ushort, Queue<IReadMessage>>();
private ConcurrentDictionary<ushort, ConcurrentQueue<IReadMessage>> receiveQueue = new();
public void SendSyncMessage()
{
@@ -99,7 +100,7 @@ partial class NetworkingService : INetworkingService
}
else
{
if (!receiveQueue.ContainsKey(id)) { receiveQueue[id] = new Queue<IReadMessage>(); }
if (!receiveQueue.ContainsKey(id)) { receiveQueue[id] = new ConcurrentQueue<IReadMessage>(); }
receiveQueue[id].Enqueue(netMessage);
if (GameSettings.CurrentConfig.VerboseLogging)

View File

@@ -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<ContentPackage, IModConfigInfo> modConfigParserService,
IProcessorService<IReadOnlyList<IAssemblyResourceInfo>, IAssembliesResourcesInfo> assemblyInfoConverter,
IProcessorService<IReadOnlyList<IConfigResourceInfo>, IConfigsResourcesInfo> configsInfoConverter,
IProcessorService<IReadOnlyList<IConfigProfileResourceInfo>, IConfigProfilesResourcesInfo> configProfilesConverter,
IProcessorService<IReadOnlyList<ILocalizationResourceInfo>, ILocalizationsResourcesInfo> localizationsConverter,
IProcessorService<IReadOnlyList<ILuaScriptResourceInfo>, ILuaScriptsResourcesInfo> luaScriptsConverter,
IPackageInfoLookupService packageInfoLookupService, Func<IReadOnlyList<IStylesResourceInfo>, IStylesResourcesInfo> stylesInfoConverter)
{
_stylesInfoConverter = stylesInfoConverter;
_modConfigParserService = modConfigParserService;
_assemblyInfoConverter = assemblyInfoConverter;
_configsInfoConverter = configsInfoConverter;
_configProfilesConverter = configProfilesConverter;
_localizationsConverter = localizationsConverter;
_luaScriptsConverter = luaScriptsConverter;
_packageInfoLookupService = packageInfoLookupService;
}
private readonly Func<IReadOnlyList<IStylesResourceInfo>, IStylesResourcesInfo> _stylesInfoConverter;
public ImmutableArray<IStylesResourceInfo> Styles => _modInfos.IsEmpty ? ImmutableArray<IStylesResourceInfo>.Empty
: _modInfos.SelectMany(kvp => kvp.Value.Styles).ToImmutableArray();
public Result<IStylesResourcesInfo> 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<IStylesResourcesInfo>(_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<IStylesResourcesInfo> GetStylesInfos(IReadOnlyList<ContentPackage> 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<IStylesResourceInfo>();
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<Result<IStylesResourcesInfo>> GetStylesInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true)
{
return await Task.Run(() => GetStylesInfos(packages, onlySupportedResources));
}
}

View File

@@ -14,26 +14,17 @@ public partial class ModConfigService
private partial async Task<Result<IModConfigInfo>> 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<IModConfigInfo>(new ModConfigInfo()
{
Package = package,
PackageName = package.Name,
Assemblies = asm.Any() ? GetAssemblies(package, asm) : ImmutableArray<IAssemblyResourceInfo>.Empty,
Localizations = loc.Any() ? GetLocalizations(package, loc) : ImmutableArray<ILocalizationResourceInfo>.Empty,
Configs = cfg.Any() ? GetConfigs(package, cfg) : ImmutableArray<IConfigResourceInfo>.Empty,
ConfigProfiles = cfg.Any() ? GetConfigProfiles(package, cfg) : ImmutableArray<IConfigProfileResourceInfo>.Empty,
LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray<ILuaScriptResourceInfo>.Empty,
Styles = stl.Any() ? GetStyles(package, stl) : ImmutableArray<IStylesResourceInfo>.Empty
LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray<ILuaScriptResourceInfo>.Empty
});
}
private ImmutableArray<IStylesResourceInfo> GetStyles(ContentPackage src, IEnumerable<XElement> elements)
{
throw new NotImplementedException();
}
}

View File

@@ -1,6 +0,0 @@
namespace Barotrauma.LuaCs.Services.Processing;
public class StylesManagementService : IStylesManagementService
{
}

View File

@@ -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<string, UIStyleProcessor> _loadedProcessors = new();
private readonly IStorageService _storageService;
private readonly ILoggerService _loggerService;
public StylesService(IStorageService storageService, ILoggerService loggerService)
{
_storageService = storageService;
_loggerService = loggerService;
}
public async Task<FluentResults.Result> 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; }
}

View File

@@ -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<string, GUIFont> Fonts = new();
public readonly Dictionary<string, GUISprite> Sprites = new();
public readonly Dictionary<string, GUISpriteSheet> SpriteSheets = new();
public readonly Dictionary<string, GUICursor> Cursors = new();
public readonly Dictionary<string, GUIColor> 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<GUIFont, GUIFontPrefab>(Fonts, childElements, _fake);
childElements = styleElement.GetChildElements("Sprite");
if (childElements is not null)
AddToList<GUISprite, GUISpritePrefab>(Sprites, childElements, _fake);
childElements = styleElement.GetChildElements("Spritesheet");
if (childElements is not null)
AddToList<GUISpriteSheet, GUISpriteSheetPrefab>(SpriteSheets, childElements, _fake);
childElements = styleElement.GetChildElements("Cursor");
if (childElements is not null)
AddToList<GUICursor, GUICursorPrefab>(Cursors, childElements, _fake);
childElements = styleElement.GetChildElements("Color");
if (childElements is not null)
AddToList<GUIColor, GUIColorPrefab>(Colors, childElements, _fake);
void AddToList<T1, T2>(Dictionary<string, T1> dict, IEnumerable<ContentXElement> ele, UIStyleFile file) where T1 : GUISelector<T2> 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());
}
}

View File

@@ -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<IEventScreenSelected>(sub => sub.OnScreenSelected(this));
}
public void Select(bool enableAutoSave = true)

View File

@@ -11,7 +11,6 @@ public partial class PackageManagementService
IProcessorService<IReadOnlyList<IAssemblyResourceInfo>, IAssembliesResourcesInfo> assemblyInfoConverter,
IProcessorService<IReadOnlyList<IConfigResourceInfo>, IConfigsResourcesInfo> configsInfoConverter,
IProcessorService<IReadOnlyList<IConfigProfileResourceInfo>, IConfigProfilesResourcesInfo> configProfilesConverter,
IProcessorService<IReadOnlyList<ILocalizationResourceInfo>, ILocalizationsResourcesInfo> localizationsConverter,
IProcessorService<IReadOnlyList<ILuaScriptResourceInfo>, ILuaScriptsResourcesInfo> luaScriptsConverter,
IPackageInfoLookupService packageInfoLookupService)
{
@@ -19,7 +18,6 @@ public partial class PackageManagementService
_assemblyInfoConverter = assemblyInfoConverter;
_configsInfoConverter = configsInfoConverter;
_configProfilesConverter = configProfilesConverter;
_localizationsConverter = localizationsConverter;
_luaScriptsConverter = luaScriptsConverter;
_packageInfoLookupService = packageInfoLookupService;
}

View File

@@ -15,7 +15,6 @@ public partial class ModConfigService
private partial async Task<Result<IModConfigInfo>> 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<IAssemblyResourceInfo>.Empty,
Localizations = loc.Any() ? GetLocalizations(package, loc) : ImmutableArray<ILocalizationResourceInfo>.Empty,
Configs = cfg.Any() ? GetConfigs(package, cfg) : ImmutableArray<IConfigResourceInfo>.Empty,
ConfigProfiles = cfg.Any() ? GetConfigProfiles(package, cfg) : ImmutableArray<IConfigProfileResourceInfo>.Empty,
LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray<ILuaScriptResourceInfo>.Empty

View File

@@ -0,0 +1,50 @@
<?xml version="1.0"?>
<!-- For: DataType="typeof(Barotrauma.LuaCs.Data.IModConfig)"-->
<schema
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:xs="http://www.w3.org/2001/XMLSchema-datatypes"
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
targetNamespace="Barotrauma.LuaCs.Data"
vc:minVersion="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- ATTRIBUTES -->
<!--Attribute "Folder" restrictions-->
<attribute name="Folder">
<simpleType>
<restriction base="string"/>
</simpleType>
</attribute>
<!--Attribute "File" restrictions-->
<attribute name="File">
<simpleType>
<restriction base="string"/>
</simpleType>
</attribute>
<!--Attribute "Platform" restrictions-->
<attribute name="Platform">
<simpleType>
<restriction base="string">
<pattern value="(Windows|Linux|OSX)"/>
</restriction>
</simpleType>
</attribute>
<!--Attribute "Target" restrictions-->
<attribute name="Target">
<simpleType>
<restriction base="string">
<pattern value="(Client|Server|Any)"/>
</restriction>
</simpleType>
</attribute>
<!--Attribute "Culture" restrictions-->
<attribute name="Culture">
<simpleType>
<restriction base="string">
<pattern value="^[a-z]{2,3}(?:-[A-Z]{2,3}(?:-[a-zA-Z]{4})?)?$"/>
</restriction>
</simpleType>
</attribute>
</schema>

View File

@@ -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<T> : IConfigEntry<T> where T : IEquatable<T>
{
private readonly Action<ConfigEntry<T>, INetReadMessage> _readMessageHandler;
private readonly Action<ConfigEntry<T>, INetWriteMessage> _writeMessageHandler;
public ConfigEntry(IConfigInfo configInfo, Action<ConfigEntry<T>, INetReadMessage> readMessageHandler,
Action<ConfigEntry<T>, 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<string, XElement> value)
{
throw new NotImplementedException();
}
public bool IsAssignable(OneOf<string, XElement> value)
{
throw new NotImplementedException();
}
private event Action<IConfigEntry<T>> _onValueChanged;
public event Action<IConfigEntry<T>> OnValueChanged
{
add => _onValueChanged += value;
remove => _onValueChanged -= value;
}
event Action<IConfigBase> IConfigBase.OnValueChanged
{
add => _onValueChanged += value;
remove => _onValueChanged -= value;
}
public OneOf<string, XElement> 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();
}
}

View File

@@ -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<T> : IConfigList<T> where T : IEquatable<T>
{
private readonly Action<ConfigList<T>, INetReadMessage> _readMessageHandler;
private readonly Action<ConfigList<T>, INetWriteMessage> _writeMessageHandler;
public ConfigList(IConfigInfo configInfo, Action<ConfigList<T>, INetReadMessage> readMessageHandler,
Action<ConfigList<T>, 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<string, XElement> value)
{
throw new NotImplementedException();
}
public bool IsAssignable(OneOf<string, XElement> value)
{
throw new NotImplementedException();
}
private event Action<IConfigList<T>> _onValueChanged;
public event Action<IConfigList<T>> OnValueChanged
{
add => _onValueChanged += value;
remove => _onValueChanged -= value;
}
event Action<IConfigEntry<T>> IConfigEntry<T>.OnValueChanged
{
add => _onValueChanged += value;
remove => _onValueChanged -= value;
}
event Action<IConfigBase> 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<string, XElement> 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<T> Options => throw new NotImplementedException();
}

View File

@@ -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<IConfigBase>, 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<string, XElement> value);
bool IsAssignable(OneOf.OneOf<string, XElement> value);
event Action<IConfigBase> OnValueChanged;
OneOf.OneOf<string, XElement> GetSerializableValue();
}

View File

@@ -3,10 +3,10 @@ using Barotrauma.LuaCs.Services;
namespace Barotrauma.LuaCs.Configuration;
public interface IConfigEntry<T> : IConfigBase, INetVar where T : IConvertible, IEquatable<T>
public interface IConfigEntry<T> : IConfigBase, INetworkSyncEntity where T : IEquatable<T>
{
T Value { get; }
bool TrySetValue(T value);
bool IsAssignable(T value);
void Initialize(IVarId id, T defaultValue);
new event Action<IConfigEntry<T>> OnValueChanged;
}

View File

@@ -0,0 +1,9 @@
using System;
using Barotrauma.LuaCs.Services;
namespace Barotrauma.LuaCs.Configuration;
public interface IConfigEnum : IConfigBase, INetworkSyncEntity
{
}

View File

@@ -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<T> : IConfigEntry<T>, INetworkSyncEntity where T : IEquatable<T>
{
IReadOnlyList<T> Options { get; }
new event Action<IConfigList<T>> OnValueChanged;
}

View File

@@ -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<IAssemblyResourceInfo> Assemblies { get; init; }
public ImmutableArray<ILocalizationResourceInfo> Localizations { get; init; }
public ImmutableArray<ILuaScriptResourceInfo> LuaScripts { get; init; }
public ImmutableArray<IConfigResourceInfo> Configs { get; init; }
public ImmutableArray<IConfigProfileResourceInfo> ConfigProfiles { get; init; }
@@ -24,10 +26,9 @@ public partial record ModConfigInfo : IModConfigInfo
#endregion
#region DataContracts
#region DataContracts_Resources
public record AssemblyResourcesInfo(ImmutableArray<IAssemblyResourceInfo> Assemblies) : IAssembliesResourcesInfo;
public record LocalizationResourcesInfo(ImmutableArray<ILocalizationResourceInfo> Localizations) : ILocalizationsResourcesInfo;
public record LuaScriptsResourcesInfo(ImmutableArray<ILuaScriptResourceInfo> LuaScripts) : ILuaScriptsResourcesInfo;
public record ConfigResourcesInfo(ImmutableArray<IConfigResourceInfo> Configs) : IConfigsResourcesInfo;
public record ConfigProfilesResourcesInfo(ImmutableArray<IConfigProfileResourceInfo> 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<string> FilePaths { get; init; }
public ImmutableArray<CultureInfo> SupportedCultures { get; init; }
public ImmutableArray<IPackageDependency> 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<string, XElement> DefaultValue { get; init; }
public OneOf<string, XElement> 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<string, XElement> Value)> ProfileValues { get; init; }
}
#endregion

View File

@@ -1,51 +1,43 @@
using System;
using System.Xml.Linq;
using Barotrauma.LuaCs.Services;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs.Data;
// TODO: Finish
/// <summary>
/// Parsed data from a configuration xml.
/// </summary>
public partial interface IConfigInfo : IDataInfo
{
/// <summary>
/// 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.
/// </summary>
Type DataType { get; }
/// <summary>
/// String version of the default value.
/// The default value.
/// </summary>
string DefaultValue { get; }
OneOf.OneOf<string, XElement> DefaultValue { get; }
/// <summary>
/// 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.
/// <br/><b>[If(Type='<see cref="string"/>')]</b><br/>
/// The value is from the 'Value' Attribute. Typically used for types with single/simple values, such as primitives.
/// <br/><b>[If(Type='<see cref="XElement"/>')]</b><br/>
/// The value is from the first 'Value' child element. Typically used with complex config types, such as range and list.
/// </summary>
string StoredValue { get; }
OneOf.OneOf<string, XElement> Value { get; }
/// <summary>
/// Custom data storage for other type-specific information needed. IE. Used to store the min,
/// max and step values for the <b>IConfigRangeEntry(T)</b>.
/// In what <see cref="RunState"/>(s) is this config editable. Will be editable in the selected state, and lower value states.
/// <br/><br/>
/// <b>[Important]</b><br/> Setting this to value lower than 'Configuration` will render this config read-only.
/// <br/><br/><b>Expected Behaviour</b>:
/// <br/><b>[<see cref="RunState.Unloaded"/>|<see cref="RunState.Parsed"/>]</b>: Read-Only.
/// <br/><b>[<see cref="RunState.Configuration"/>]</b>: Can only be changed at the Main Menu (not in a lobby).
/// <br/><b>[<see cref="RunState.Running"/>]</b>: Can be changed at the Main Menu and while a lobby is active.
/// </summary>
string CustomParameters { get; }
/// <summary>
/// <b>[Multiplayer]</b><br/>
/// What permissions do clients require to change this setting.
/// </summary>
ClientPermissions RequiredPermissions { get; }
/// <summary>
/// In what <see cref="RunState"/>s is this config editable.
/// <br/>
/// Note: Setting this to value lower than 'Configuration` will render this config read-only.
/// </summary>
RunState CanEditStates { get; }
RunState EditableStates { get; }
/// <summary>
/// Network synchronization rules for this config.
/// </summary>
NetSync NetSync { get; }
/// <summary>
/// User friendly name or Localization Token.
/// </summary>
string DisplayName { get; }
/// <summary>
/// User friendly description or Localization Token.
/// </summary>
string Description { get; }
}

View File

@@ -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<string, XElement> Value)> ProfileValues { get; }
}

View File

@@ -1,12 +0,0 @@
using System.Collections.Generic;
using System.Globalization;
namespace Barotrauma.LuaCs.Data;
public interface ILocalizationInfo : IDataInfo
{
string Symbol { get; }
IReadOnlyDictionary<CultureInfo, RawLString> LocalizedValues { get; }
RawLString GetLocalizedString(CultureInfo locale);
RawLString GetLocalizedString(string cultureCode);
}

View File

@@ -3,7 +3,7 @@
namespace Barotrauma.LuaCs.Data;
public partial interface IModConfigInfo : IAssembliesResourcesInfo,
ILocalizationsResourcesInfo, ILuaScriptsResourcesInfo, IConfigsResourcesInfo,
ILuaScriptsResourcesInfo, IConfigsResourcesInfo,
IConfigProfilesResourcesInfo
{
// package info

View File

@@ -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 { }
/// <summary>
/// Represents loadable Lua files.
@@ -40,11 +39,6 @@ public interface IAssembliesResourcesInfo
ImmutableArray<IAssemblyResourceInfo> Assemblies { get; }
}
public interface ILocalizationsResourcesInfo
{
ImmutableArray<ILocalizationResourceInfo> Localizations { get; }
}
public interface ILuaScriptsResourcesInfo
{
ImmutableArray<ILuaScriptResourceInfo> LuaScripts { get; }

View File

@@ -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<string> SafeIOReadDirectories { get; }
//ReadOnlyCollection<string> SafeIOWriteDirectories { get; }
IEnumerable<string> GlobalIOReadWhitelist();
IEnumerable<string> 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<string,byte> _safeIOReadFilePaths = new();
private readonly ConcurrentDictionary<string,byte> _safeIOWriteFilePaths = new();
public IEnumerable<string> 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<string> 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 => "<PACKAGENAME>";
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<string,byte> 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 => "<ConfigName>";
}
// --- Lua Scripts Service
public interface ILuaScriptServicesConfig
{
bool SafeLuaIOEnabled { get; }
bool UseCaching { get; }
}
public record LuaScriptServicesConfig : ILuaScriptServicesConfig
{
public bool SafeLuaIOEnabled => true;
public bool UseCaching => true;
}

View File

@@ -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<IEventReloadAllPackages>
void OnReloadAllPackages();
}
internal interface IEventConfigVarInstanced : IEvent<IEventConfigVarInstanced>
{
void OnConfigCreated(IConfigBase config);
}
#endregion
#region GameEvents

View File

@@ -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);
}
}
}

View File

@@ -44,15 +44,11 @@ namespace Barotrauma
_servicesProvider.RegisterServiceType<PerformanceCounterService, PerformanceCounterService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<IStorageService, StorageService>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IEventService, EventService>(ServiceLifetime.Singleton);
#if CLIENT
_servicesProvider.RegisterServiceType<IStylesService, StylesService>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IStylesManagementService, StylesManagementService>(ServiceLifetime.Singleton);
#endif
_servicesProvider.RegisterServiceType<IPackageManagementService, PackageManagementService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<IPluginManagementService, PluginManagementService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<ILuaScriptManagementService, LuaScriptManagementService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<LuaGame, LuaGame>(ServiceLifetime.Singleton);
// TODO: ILocalizationService
// TODO: IConfigService
// TODO: INetworkingService
// TODO: [Resource Converter/Parser Services]
@@ -61,13 +57,12 @@ namespace Barotrauma
_servicesProvider.RegisterServiceType<IProcessorService<IReadOnlyList<IAssemblyResourceInfo>, IAssembliesResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IProcessorService<IReadOnlyList<IConfigResourceInfo>, IConfigsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IProcessorService<IReadOnlyList<IConfigProfileResourceInfo>, IConfigProfilesResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IProcessorService<IReadOnlyList<ILocalizationResourceInfo>, ILocalizationsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IProcessorService<IReadOnlyList<ILuaScriptResourceInfo>, ILuaScriptsResourcesInfo>, ResourceInfoArrayPacker>(ServiceLifetime.Transient);
// Loaders and Processors (yes the naming is reversed, oops).
_servicesProvider.RegisterServiceType<IConverterService<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IConverterServiceAsync<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<IConfigIOService, ConfigIOService>(ServiceLifetime.Transient);
_servicesProvider.Compile();
}
@@ -121,8 +116,6 @@ namespace Barotrauma
? svc : throw new NullReferenceException("Plugin Manager service not found!");
public ILuaScriptManagementService LuaScriptManagementService => _servicesProvider.TryGetService<ILuaScriptManagementService>(out var svc)
? svc : throw new NullReferenceException("Lua Script Manager service not found!");
public ILocalizationService LocalizationService => _servicesProvider.TryGetService<ILocalizationService>(out var svc)
? svc : throw new NullReferenceException("Localization Manager service not found!");
public INetworkingService NetworkingService => _servicesProvider.TryGetService<INetworkingService>(out var svc)
? svc : throw new NullReferenceException("Networking Manager service not found!");
public IEventService EventService => _servicesProvider.TryGetService<IEventService>(out var svc)
@@ -179,6 +172,11 @@ namespace Barotrauma
/// TODO: @evilfactory@users.noreply.github.com
/// </summary>
public IConfigEntry<bool> RestrictMessageSize { get; private set; }
/// <summary>
/// The local save path for all local data storage for mods.
/// </summary>
public IConfigEntry<string> 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<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "IsCsEnabled")
?? throw new NullReferenceException($"{nameof(IsCsEnabled)} cannot be loaded.");
TreatForcedModsAsNormal = ConfigService.GetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "TreatForcedModsAsNormal")
?? throw new NullReferenceException($"{nameof(TreatForcedModsAsNormal)} cannot be loaded.");
DisableErrorGUIOverlay = ConfigService.GetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "DisableErrorGUIOverlay")
?? throw new NullReferenceException($"{nameof(DisableErrorGUIOverlay)} cannot be loaded.");
HideUserNamesInLogs = ConfigService.GetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "HideUserNamesInLogs")
?? throw new NullReferenceException($"{nameof(HideUserNamesInLogs)} cannot be loaded.");
LuaForBarotraumaSteamId = ConfigService.GetConfig<IConfigEntry<ulong>>(ContentPackageManager.VanillaCorePackage, "LuaForBarotraumaSteamId")
?? throw new NullReferenceException($"{nameof(LuaForBarotraumaSteamId)} cannot be loaded.");
CsForBarotraumaSteamId = ConfigService.GetConfig<IConfigEntry<ulong>>(ContentPackageManager.VanillaCorePackage, "CsForBarotraumaSteamId")
?? throw new NullReferenceException($"{nameof(CsForBarotraumaSteamId)} cannot be loaded.");
RestrictMessageSize = ConfigService.GetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "RestrictMessageSize")
?? throw new NullReferenceException($"{nameof(RestrictMessageSize)} cannot be loaded.");
ReloadPackagesOnLobbyStart = ConfigService.GetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "ReloadPackagesOnLobbyStart")
?? throw new NullReferenceException($"{nameof(ReloadPackagesOnLobbyStart)} cannot be loaded.");
IsCsEnabled = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "IsCsEnabled", out var val1) ? val1
: throw new NullReferenceException($"{nameof(IsCsEnabled)} cannot be loaded.");
TreatForcedModsAsNormal = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "TreatForcedModsAsNormal", out var val2) ? val2
: throw new NullReferenceException($"{nameof(TreatForcedModsAsNormal)} cannot be loaded.");
DisableErrorGUIOverlay = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "DisableErrorGUIOverlay", out var val3) ? val3
: throw new NullReferenceException($"{nameof(DisableErrorGUIOverlay)} cannot be loaded.");
HideUserNamesInLogs = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "HideUserNamesInLogs", out var val4) ? val4
: throw new NullReferenceException($"{nameof(HideUserNamesInLogs)} cannot be loaded.");
LuaForBarotraumaSteamId = ConfigService.TryGetConfig<IConfigEntry<ulong>>(ContentPackageManager.VanillaCorePackage, "LuaForBarotraumaSteamId", out var val5) ? val5
: throw new NullReferenceException($"{nameof(LuaForBarotraumaSteamId)} cannot be loaded.");
CsForBarotraumaSteamId = ConfigService.TryGetConfig<IConfigEntry<ulong>>(ContentPackageManager.VanillaCorePackage, "CsForBarotraumaSteamId", out var val6) ? val6
: throw new NullReferenceException($"{nameof(CsForBarotraumaSteamId)} cannot be loaded.");
RestrictMessageSize = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "RestrictMessageSize", out var val7) ? val7
: throw new NullReferenceException($"{nameof(RestrictMessageSize)} cannot be loaded.");
ReloadPackagesOnLobbyStart = ConfigService.TryGetConfig<IConfigEntry<bool>>(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<ContentPackage> packages)
{
var locRes = ImmutableArray<ILocalizationResourceInfo>.Empty;
var cfgRes = ImmutableArray<IConfigResourceInfo>.Empty;
var cfpRes = ImmutableArray<IConfigProfileResourceInfo>.Empty;
var luaRes = ImmutableArray<ILuaScriptResourceInfo>.Empty;
#if CLIENT
var styleRes = ImmutableArray<IStylesResourceInfo>.Empty;
#endif
var tasksBuilder = ImmutableArray.CreateBuilder<Task>();
//---- get resource infos
tasksBuilder.AddRange(new Func<Task>(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<Task>(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<Task>(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<Task>(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)
{

View File

@@ -567,8 +567,17 @@ namespace FluentResults.LuaCs
public static class MetadataType
{
public static string ExceptionDetails = nameof(ExceptionDetails);
/// <summary>
/// The object that threw the exception.
/// </summary>
public static string ExceptionObject = nameof(ExceptionObject);
/// <summary>
/// The parameter-object responsible for the exception thrown (not the exception thrower).
/// </summary>
public static string RootObject = nameof(RootObject);
/// <summary>
/// Additional exception sources.
/// </summary>
public static string Sources = nameof(Sources);
public static string StackTrace = nameof(StackTrace);
}

View File

@@ -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
{
/// <summary>
/// Synchronization type
/// </summary>
NetSync SyncType { get; }
/// <summary>
/// Permissions needed by clients to send net-events or receive net messages.
/// </summary>
ClientPermissions WritePermissions { get; }
void ReadNetMessage(INetReadMessage message);
void WriteNetMessage(INetWriteMessage message);
}
public enum NetSync
{
None, TwoWay, ServerAuthority, ClientOneWay
}

View File

@@ -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
{
/// <summary>
/// Network-synchronized object ID. Used for networking send/receive message events.
/// </summary>
Guid InstanceId { get; }
/// <summary>
/// Synchronization type. See <see cref="NetSync"/> for more information.
/// </summary>
NetSync SyncType { get; }
/// <summary>
/// Permissions needed by clients to send net-events and/or receive net messages.
/// </summary>
ClientPermissions WritePermissions { get; }
/// <summary>
/// Called when an incoming net message has data for this network object, typically from the same entity on another
/// machine.
/// </summary>
/// <param name="message">Wrapper for the internal type: <see cref="IReadMessage"/></param>
void ReadNetMessage(INetReadMessage message);
/// <summary>
/// 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.
/// </summary>
/// <param name="message">Wrapper for the internal type: <see cref="IWriteMessage"/></param>
void WriteNetMessage(INetWriteMessage message);
}
/// <summary>
/// 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.
/// </summary>
public enum NetSync
{
/// <summary>
/// No network synchronization.
/// </summary>
None,
/// <summary>
/// Both the client and the server have 'send' and 'receive' permissions (limited by <see cref="ClientPermissions"/>). Can also be used to allow two-way communication
/// with the server.
/// </summary>
TwoWay,
/// <summary>
/// Only the host/server has the authority to change this value.
/// </summary>
ServerAuthority,
/// <summary>
/// Only clients (with the required by <see cref="ClientPermissions"/>) may change the value and all value changes are communicated to the server/host.
/// <br/><br/><b>[Important] The host/server will not send the value to other connected clients.</b><br/>
/// Intended to allow clients to send one-way messages to the server.
/// </summary>
ClientOneWay
}

View File

@@ -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);
}

View File

@@ -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<IConfigEntry<T>> CreateConfigEntry<T>(IConfigInfo configInfo,
Action<ConfigEntry<T>, INetReadMessage> readHandler,
Action<ConfigEntry<T>, INetWriteMessage> writeHandler)
where T : IEquatable<T>
{
try
{
var ice = new ConfigEntry<T>(configInfo, readHandler, writeHandler);
return FluentResults.Result.Ok<IConfigEntry<T>>(ice);
}
catch (Exception e)
{
return FluentResults.Result.Fail($"Error while initializing config var: {configInfo?.OwnerPackage} - {configInfo?.InternalName}")
.WithError(new ExceptionalError(e));
}
}
private Result<IConfigList<T>> CreateConfigList<T>(IConfigInfo configInfo,
Action<IConfigList<T>, INetReadMessage> readHandler, Action<IConfigList<T>, INetWriteMessage> writeHandler)
where T : IEquatable<T>
{
try
{
var icl = new ConfigList<T>(configInfo, readHandler, writeHandler);
return FluentResults.Result.Ok<IConfigList<T>>(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<bool, IConfigEntry<bool>>(this.CreateConfigBool);
configService.RegisterTypeInitializer<sbyte, IConfigEntry<sbyte>>(this.CreateConfigSbyte);
configService.RegisterTypeInitializer<byte, IConfigEntry<byte>>(this.CreateConfigByte);
configService.RegisterTypeInitializer<short, IConfigEntry<short>>(this.CreateConfigShort);
configService.RegisterTypeInitializer<ushort, IConfigEntry<ushort>>(this.CreateConfigUShort);
configService.RegisterTypeInitializer<int, IConfigEntry<int>>(this.CreateConfigInt32);
configService.RegisterTypeInitializer<uint, IConfigEntry<uint>>(this.CreateConfigUInt32);
configService.RegisterTypeInitializer<long, IConfigEntry<long>>(this.CreateConfigInt64);
configService.RegisterTypeInitializer<ulong, IConfigEntry<ulong>>(this.CreateConfigUInt64);
configService.RegisterTypeInitializer<float, IConfigEntry<float>>(this.CreateConfigFloat32);
configService.RegisterTypeInitializer<double, IConfigEntry<double>>(this.CreateConfigFloat64);
configService.RegisterTypeInitializer<decimal, IConfigEntry<decimal>>(this.CreateConfigFloat128);
configService.RegisterTypeInitializer<char, IConfigEntry<char>>(this.CreateConfigChar);
configService.RegisterTypeInitializer<string, IConfigEntry<string>>(this.CreateConfigString);
configService.RegisterTypeInitializer<Color, IConfigEntry<Color>>(this.CreateConfigColor);
configService.RegisterTypeInitializer<Vector2, IConfigEntry<Vector2>>(this.CreateConfigVector2);
configService.RegisterTypeInitializer<Vector3, IConfigEntry<Vector3>>(this.CreateConfigVector3);
configService.RegisterTypeInitializer<Vector4, IConfigEntry<Vector4>>(this.CreateConfigVector4);
}
#region InitializerWrappers_NetworkInjected
private void AssignValueConditional<T>(T val, IConfigEntry<T> inst) where T : IEquatable<T>
{
#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<IConfigEntry<bool>> CreateConfigBool(IConfigInfo configInfo)
{
return CreateConfigEntry<bool>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadBoolean(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteBoolean(inst.Value);
});
}
private Result<IConfigEntry<sbyte>> CreateConfigSbyte(IConfigInfo configInfo)
{
return CreateConfigEntry<sbyte>(configInfo, (inst, readMsg) =>
{
AssignValueConditional((sbyte)readMsg.ReadInt16(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteInt16((short)inst.Value);
});
}
private Result<IConfigEntry<byte>> CreateConfigByte(IConfigInfo configInfo)
{
return CreateConfigEntry<byte>(configInfo, (inst, readMsg) =>
{
AssignValueConditional((byte)readMsg.ReadUInt16(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteUInt16((byte)inst.Value);
});
}
private Result<IConfigEntry<short>> CreateConfigShort(IConfigInfo configInfo)
{
return CreateConfigEntry<short>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadInt16(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteInt16(inst.Value);
});
}
private Result<IConfigEntry<ushort>> CreateConfigUShort(IConfigInfo configInfo)
{
return CreateConfigEntry<ushort>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadUInt16(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteUInt16(inst.Value);
});
}
private Result<IConfigEntry<int>> CreateConfigInt32(IConfigInfo configInfo)
{
return CreateConfigEntry<int>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadInt32(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteInt32(inst.Value);
});
}
private Result<IConfigEntry<uint>> CreateConfigUInt32(IConfigInfo configInfo)
{
return CreateConfigEntry<uint>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadUInt32(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteUInt32(inst.Value);
});
}
private Result<IConfigEntry<long>> CreateConfigInt64(IConfigInfo configInfo)
{
return CreateConfigEntry<long>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadInt64(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteInt64(inst.Value);
});
}
private Result<IConfigEntry<ulong>> CreateConfigUInt64(IConfigInfo configInfo)
{
return CreateConfigEntry<ulong>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadUInt64(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteUInt64(inst.Value);
});
}
private Result<IConfigEntry<float>> CreateConfigFloat32(IConfigInfo configInfo)
{
return CreateConfigEntry<float>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadSingle(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteSingle(inst.Value);
});
}
private Result<IConfigEntry<double>> CreateConfigFloat64(IConfigInfo configInfo)
{
return CreateConfigEntry<double>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadDouble(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteDouble(inst.Value);
});
}
private Result<IConfigEntry<decimal>> CreateConfigFloat128(IConfigInfo configInfo)
{
return CreateConfigEntry<decimal>(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<IConfigEntry<char>> CreateConfigChar(IConfigInfo configInfo)
{
return CreateConfigEntry<char>(configInfo, (inst, readMsg) =>
{
AssignValueConditional((char)readMsg.ReadUInt16(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteUInt16(inst.Value);
});
}
private Result<IConfigEntry<string>> CreateConfigString(IConfigInfo configInfo)
{
return CreateConfigEntry<string>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadString(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteString(inst.Value);
});
}
private Result<IConfigEntry<Color>> CreateConfigColor(IConfigInfo configInfo)
{
return CreateConfigEntry<Color>(configInfo, (inst, readMsg) =>
{
AssignValueConditional(readMsg.ReadColorR8G8B8A8(), inst);
}, (inst, writeMsg) =>
{
writeMsg.WriteColorR8G8B8A8(inst.Value);
});
}
private Result<IConfigEntry<Vector2>> CreateConfigVector2(IConfigInfo configInfo)
{
return CreateConfigEntry<Vector2>(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<IConfigEntry<Vector3>> CreateConfigVector3(IConfigInfo configInfo)
{
return CreateConfigEntry<Vector3>(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<IConfigEntry<Vector4>> CreateConfigVector4(IConfigInfo configInfo)
{
return CreateConfigEntry<Vector4>(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
}

View File

@@ -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<IConfigProfileResourceInfo, IReadOnlyList<IConfigProfileInfo>> configProfileResourceConverter,
IConverterServiceAsync<IConfigResourceInfo, IReadOnlyList<IConfigInfo>> configResourceConverter, IEventService eventService, System.Lazy<IStorageService> 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<Type, Func<IConfigInfo, FluentResults.Result<IConfigBase>>> _configTypeInitializers = new();
private readonly ConcurrentDictionary<(ContentPackage Package, string ConfigName), IConfigBase> _configs = new();
private readonly ConcurrentDictionary<ContentPackage, ConcurrentBag<(ContentPackage Package, string ConfigName)>> _packageConfigReverseLookup = new();
private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), ImmutableArray<(string ConfigName, OneOf.OneOf<string, XElement> Value)>> _configProfiles = new();
private readonly ConcurrentDictionary<ContentPackage, ConcurrentBag<(ContentPackage Package, string ProfileName)>> _packageProfilesReverseLookup = new();
private readonly ConcurrentDictionary<string, ContentPackage> _packageNameMap= new();
private readonly AsyncReaderWriterLock _disposeOpsLock = new();
// extern services
private readonly IConverterServiceAsync<IConfigResourceInfo, IReadOnlyList<IConfigInfo>> _configResourceConverter;
private readonly IConverterServiceAsync<IConfigProfileResourceInfo, IReadOnlyList<IConfigProfileInfo>> _configProfileResourceConverter;
private readonly IEventService _eventService;
private readonly System.Lazy<IStorageService> _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<T>(string packageName, string configName, out T value) where T : IEquatable<T>
{
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<T> 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<string> 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<string> entry)
return false;
value = entry.Options;
return value is not null && value.Count > 0;
}
private void SetConfigValue<T>(string packageName, string configName, T value) where T : IEquatable<T>
{
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<T> 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<string> 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<TData, TConfig>(Func<IConfigInfo, FluentResults.Result<TConfig>> initializer, bool replaceIfExists = false)
where TData : IEquatable<TData> 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<IEventConfigVarInstanced>(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<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> 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<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigProfileResourceInfo> 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<TConfig> AddConfig<TConfig>(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<IError>();
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<IError>();
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<IError>();
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<IReadOnlyDictionary<(ContentPackage Package, string ConfigName), IConfigBase>> 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<IReadOnlyDictionary<(ContentPackage Package, string ConfigName), ImmutableArray<(string ConfigName, OneOf<string, XElement> 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<T>(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<FluentResults.Result> 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<IError>();
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<FluentResults.Result> 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<IConfigBase> 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<IError>();
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<FluentResults.Result> 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<FluentResults.Result> 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
/*
* <Config ContentPackage="[PackageName]">
* <[instance.InternalName]>
* <Value>
* <--Contents Here->
* </Value>
* </[instance.InternalName]>
* </Config>
*/
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<XElement>(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);
}
}

View File

@@ -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<Result<IPackageInfo>> Lookup(string packageName)
{
((IService)this).CheckDisposed();

View File

@@ -1,6 +0,0 @@
namespace Barotrauma.LuaCs.Services;
public interface LocalizationService
{
}

View File

@@ -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<object> GetGlobalTableValue(string tableName)
{
throw new NotImplementedException();
}
public async Task<FluentResults.Result> LoadScriptResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourcesInfo)
{
throw new NotImplementedException();
}
public FluentResults.Result ExecuteLoadedScriptsForPackage(ContentPackage package)
{
throw new NotImplementedException();
}
public FluentResults.Result ExecuteLoadedScriptsForPackages(IEnumerable<ContentPackage> 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();
}
}

View File

@@ -22,7 +22,7 @@ internal partial class NetworkingService : INetworkingService
ReceiveIds
}
private Dictionary<Guid, INetVar> netVars = new Dictionary<Guid, INetVar>();
private Dictionary<Guid, INetworkSyncEntity> netVars = new Dictionary<Guid, INetworkSyncEntity>();
private Dictionary<Guid, NetMessageReceived> netReceives = new Dictionary<Guid, NetMessageReceived>();
private Dictionary<ushort, Guid> packetToId = new Dictionary<ushort, Guid>();
private Dictionary<Guid, ushort> idToPacket = new Dictionary<Guid, ushort>();
@@ -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))
{

View File

@@ -27,7 +27,6 @@ public partial class PackageManagementService : IPackageManagementService
private readonly IProcessorService<IReadOnlyList<IAssemblyResourceInfo>, IAssembliesResourcesInfo> _assemblyInfoConverter;
private readonly IProcessorService<IReadOnlyList<IConfigResourceInfo>, IConfigsResourcesInfo> _configsInfoConverter;
private readonly IProcessorService<IReadOnlyList<IConfigProfileResourceInfo>, IConfigProfilesResourcesInfo> _configProfilesConverter;
private readonly IProcessorService<IReadOnlyList<ILocalizationResourceInfo>, ILocalizationsResourcesInfo> _localizationsConverter;
private readonly IProcessorService<IReadOnlyList<ILuaScriptResourceInfo>, ILuaScriptsResourcesInfo> _luaScriptsConverter;
@@ -56,9 +55,7 @@ public partial class PackageManagementService : IPackageManagementService
}
return FluentResults.Result.Ok();
}
public ImmutableArray<ILocalizationResourceInfo> Localizations => _modInfos.IsEmpty ? ImmutableArray<ILocalizationResourceInfo>.Empty
: _modInfos.SelectMany(kvp => kvp.Value.Localizations).ToImmutableArray();
public ImmutableArray<IConfigResourceInfo> Configs => _modInfos.IsEmpty ? ImmutableArray<IConfigResourceInfo>.Empty
: _modInfos.SelectMany(kvp => kvp.Value.Configs).ToImmutableArray();
public ImmutableArray<IConfigProfileResourceInfo> ConfigProfiles => _modInfos.IsEmpty ? ImmutableArray<IConfigProfileResourceInfo>.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<T> FilterUnloadableResources<T>(IReadOnlyList<T> 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<ILocalizationsResourcesInfo> 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<ILocalizationsResourcesInfo>(_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<ILuaScriptsResourcesInfo> 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<ILocalizationsResourcesInfo> GetLocalizationsInfos(IReadOnlyList<ContentPackage> 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<ILocalizationResourceInfo>();
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<ILuaScriptsResourcesInfo> GetLuaScriptsInfos(IReadOnlyList<ContentPackage> 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<Result<ILocalizationsResourcesInfo>> GetLocalizationsInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true)
{
return await Task.Run(() => GetLocalizationsInfos(packages, onlySupportedResources));
}
public async Task<Result<ILuaScriptsResourcesInfo>> GetLuaScriptsInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true)
{
return await Task.Run(() => GetLuaScriptsInfos(packages, onlySupportedResources));

View File

@@ -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<Result<IReadOnlyList<IConfigInfo>>> 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<IError>();
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<string, XElement> 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<NetSync>(
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<IConfigInfo>)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<ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigResourceInfo> sources)
{
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigInfo>>>();
var src = sources.ToImmutableArray();
if (!src.Any())
return ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>.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<Result<IReadOnlyList<IConfigProfileInfo>>> 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<IError>();
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<XElement, (string ConfigName, OneOf.OneOf<string, XElement> 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<bool>(
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<IConfigProfileInfo>)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<ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigProfileResourceInfo> sources)
{
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigProfileInfo>>>();
var src = sources.ToImmutableArray();
if (!src.Any())
return ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>.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<FluentResults.Result> 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<Result<OneOf<string, XElement>>> 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<FluentResults.Result<XDocument>> 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.
}
}

View File

@@ -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<IConfigResourceInfo, IReadOnlyList<IConfigInfo>>,
IConverterServiceAsync<IConfigProfileResourceInfo, IReadOnlyList<IConfigProfileInfo>>
{
Task<FluentResults.Result> SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue);
Task<FluentResults.Result<OneOf.OneOf<string, XElement>>> LoadConfigDataFromLocal(ContentPackage package, string configName);
}

View File

@@ -69,12 +69,8 @@ public partial class ModConfigService : IConverterServiceAsync<ContentPackage, I
LuaScripts = lua,
Configs = ImmutableArray<IConfigResourceInfo>.Empty,
ConfigProfiles = ImmutableArray<IConfigProfileResourceInfo>.Empty,
Localizations = ImmutableArray<ILocalizationResourceInfo>.Empty,
Package = src,
PackageName = src.Name
#if CLIENT
,Styles = ImmutableArray<IStylesResourceInfo>.Empty
#endif
};
}
catch (Exception e)
@@ -84,40 +80,6 @@ public partial class ModConfigService : IConverterServiceAsync<ContentPackage, I
}
private partial Task<Result<IModConfigInfo>> GetModConfigInfoAsync(ContentPackage package, XElement root);
private ImmutableArray<ILocalizationResourceInfo> GetLocalizations(ContentPackage src, IEnumerable<XElement> elements)
{
var builder = ImmutableArray.CreateBuilder<ILocalizationResourceInfo>();
if (GetXmlFilesList(src, elements, "Localizations")
is not { IsSuccess: true, Value: { } xmlFiles })
return ImmutableArray<ILocalizationResourceInfo>.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<ILocalizationResourceInfo>.Empty;
}
private ImmutableArray<IAssemblyResourceInfo> GetAssemblies(ContentPackage src, IEnumerable<XElement> elements)
{
@@ -265,7 +227,7 @@ public partial class ModConfigService : IConverterServiceAsync<ContentPackage, I
// get platform, culture and target architecture
var info = GetElementsAttributesData(file.Item1, file.Item2.First());
builder.Add(new LuaScriptScriptResourceInfo()
builder.Add(new LuaScriptsResourceInfo()
{
Dependencies = deps,
Optional = info.IsOptional,
@@ -539,9 +501,16 @@ public partial class ModConfigService : IConverterServiceAsync<ContentPackage, I
SupportedTargets = Target.Client
});
}
var sharedFound = _storageService.FindFilesInPackage(src, "CSharp/Shared", "*.cs", true)
is { IsSuccess: true, Value: { IsDefaultOrEmpty: false } filesCssShared };
var sharedCsBuilder = ImmutableArray.CreateBuilder<string>();
if (_storageService.FindFilesInPackage(src, "CSharp/Shared", "*.cs", true)
is { IsSuccess: true, Value: { IsDefaultOrEmpty: false } files })
{
sharedCsBuilder.AddRange<string>(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<ContentPackage, I
builder.Add(new AssemblyResourceInfo()
{
Dependencies = ImmutableArray<IPackageDependency>.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<ContentPackage, I
if (_storageService.FindFilesInPackage(src, "Lua", "*.lua", true)
is { IsSuccess: true, Value: { IsDefaultOrEmpty: false } fileAll })
{
builder.Add(new LuaScriptScriptResourceInfo()
builder.Add(new LuaScriptsResourceInfo()
{
Dependencies = ImmutableArray<IPackageDependency>.Empty,
FilePaths = fileAll.Where(path => !path.Contains("Autorun")).ToImmutableArray(),
@@ -607,7 +576,7 @@ public partial class ModConfigService : IConverterServiceAsync<ContentPackage, I
SupportedTargets = Target.Client | Target.Server
});
builder.Add(new LuaScriptScriptResourceInfo()
builder.Add(new LuaScriptsResourceInfo()
{
Dependencies = ImmutableArray<IPackageDependency>.Empty,
FilePaths = fileAll.Where(path => path.Contains("Autorun")).ToImmutableArray(),

View File

@@ -9,7 +9,6 @@ public partial class ResourceInfoArrayPacker :
IProcessorService<IReadOnlyList<IAssemblyResourceInfo>, IAssembliesResourcesInfo>,
IProcessorService<IReadOnlyList<IConfigResourceInfo>, IConfigsResourcesInfo>,
IProcessorService<IReadOnlyList<IConfigProfileResourceInfo>, IConfigProfilesResourcesInfo>,
IProcessorService<IReadOnlyList<ILocalizationResourceInfo>, ILocalizationsResourcesInfo>,
IProcessorService<IReadOnlyList<ILuaScriptResourceInfo>, ILuaScriptsResourcesInfo>
{
private bool _isDisposed;
@@ -28,11 +27,6 @@ public partial class ResourceInfoArrayPacker :
return new ConfigProfilesResourcesInfo(src.ToImmutableArray());
}
public ILocalizationsResourcesInfo Process(IReadOnlyList<ILocalizationResourceInfo> src)
{
return new LocalizationResourcesInfo(src.ToImmutableArray());
}
public ILuaScriptsResourcesInfo Process(IReadOnlyList<ILuaScriptResourceInfo> src)
{
return new LuaScriptsResourcesInfo(src.ToImmutableArray());

View File

@@ -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<string> 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);
}

View File

@@ -25,6 +25,8 @@ public interface ILuaDataService : ILuaService
/// <summary>
/// Returns stored table data for the given object or creates a new table if one doesn't exist.
/// </summary>
/// <remarks>Note: tables are stored using weak references and will be automatically deleted when the object is
/// garbage collected.</remarks>
/// <param name="obj"></param>
/// <param name="tableName"></param>
/// <returns></returns>

View File

@@ -0,0 +1,8 @@
using MoonSharp.Interpreter.Loaders;
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaScriptLoader : IService, IScriptLoader
{
void ClearCaches();
}

View File

@@ -0,0 +1,56 @@
using System.Collections.Immutable;
namespace Barotrauma.LuaCs.Services.Safe;
public interface ISafeStorageService : IStorageService
{
/// <summary>
/// Checks the given file path to see if it can be read. This includes any permissions, whitelists and OS checks.
/// </summary>
/// <param name="path">The absolute path to the file.</param>
/// <param name="readOnly">Whether to only check for read permissions only, or full RWM if false.</param>
/// <param name="checkWhitelistOnly">Whether to only check if the file is safe to access, without checking accessibility at the OS level.</param>
/// <returns>Whether the file is accessible.</returns>
bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true);
/// <summary>
/// Adds the given path to the specified whitelists.
/// </summary>
/// <param name="path">Either the fully-qualified or local reference path to the given file.</param>
/// <param name="readOnly"></param>
void AddFileToWhitelist(string path, bool readOnly = true);
/// <summary>
/// Removes the given path from all whitelists (Read|Write).
/// </summary>
/// <param name="path"></param>
void RemoveFileFromAllWhitelists(string path);
/// <summary>
/// Sets the whitelist filtering for read-only file permissions for the instance.
/// </summary>
/// <param name="filePaths">List of absolute file paths allowed.</param>
FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths);
/// <summary>
/// Sets the whitelist filtering for read & write file permissions for the instance.
/// </summary>
/// <param name="filePaths">List of absolute file paths allowed.</param>
FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths);
/// <summary>
/// Deletes all paths from all white lists.
/// </summary>
void ClearAllWhitelists();
/// <summary>
/// Whether the service instance is in file read-only mode.
/// </summary>
bool IsReadOnlyMode { get; }
/// <summary>
/// Sets the service into file read-only mode. Cannot be undone.
/// </summary>
/// <returns></returns>
bool EnableReadOnlyMode();
}

View File

@@ -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<ILoggerService> 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<ILoggerService> _loggerService;
private readonly ILuaScriptServicesConfig _luaScriptServicesConfig;
public override object LoadFile(string file, Table globalContext)
{
((IService)this).CheckDisposed();
if (!CanReadFromPath(file))
{
LogErrors<string>($"File access to \"{file}\" is not allowed.");
return null;
}
if (_storageService.TryLoadText(file) is not { IsSuccess: true, Value: not null } script)
{
LogErrors<string>($"Failed to load file \"{file}\".");
return null;
}
if (script.Value.IsNullOrWhiteSpace())
{
LogErrors<string>($"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<string>($"File access to \"{file}\" is not allowed.");
return false;
}
var result = _storageService.FileExists(file);
if (result is { IsFailed: true })
{
LogErrors<string>($"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<T>(string message, FluentResults.Result<T> 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; }
}
}

View File

@@ -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<string, byte> _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<string> 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<string> 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);
}
}

View File

@@ -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<IConfigService> configService)
public StorageService(IStorageServiceConfig configData)
{
_configService = configService;
_configData = configData;
}
private readonly Lazy<IConfigService> _configService;
private IConfigEntry<string> _kLocalStoragePath = null;
private IConfigEntry<string> _kLocalFilePathRules = null;
private const string _packagePathKeyword = "<PACKNAME>";
private readonly string _runLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location.CleanUpPath());
// TODO: Rewrite the config to get info from .ctor.
private IConfigEntry<string> LocalStoragePath => _kLocalStoragePath ??= GetOrCreateConfig(nameof(LocalStoragePath), "/Data/Mods");
private IConfigEntry<string> LocalFilePathRule => _kLocalFilePathRules ??= GetOrCreateConfig(nameof(LocalFilePathRule), _packagePathKeyword);
private IConfigEntry<string> GetOrCreateConfig(string name, string defaultValue)
{
var c = _configService.Value
.GetConfig<IConfigEntry<string>>(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<string, OneOf.OneOf<byte[], string, XDocument>> _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<XDocument> 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<XDocument> 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<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath) =>
public virtual FluentResults.Result<byte[]> 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<string> LoadLocalText(ContentPackage package, string localFilePath) =>
public virtual FluentResults.Result<string> 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<FluentResults.Result<XDocument>> LoadLocalXmlAsync(ContentPackage package, string localFilePath) =>
public virtual async Task<FluentResults.Result<XDocument>> 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<FluentResults.Result<byte[]>> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) =>
public virtual async Task<FluentResults.Result<byte[]>> 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<FluentResults.Result<string>> LoadLocalTextAsync(ContentPackage package, string localFilePath) =>
public virtual async Task<FluentResults.Result<string>> 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<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) =>
public virtual async Task<FluentResults.Result> 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<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) =>
public virtual async Task<FluentResults.Result> 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<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) =>
public virtual async Task<FluentResults.Result> 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<XDocument> LoadPackageXml(ContentPackage package, string localFilePath) =>
public virtual FluentResults.Result<XDocument> 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<byte[]> LoadPackageBinary(ContentPackage package, string localFilePath) =>
public virtual FluentResults.Result<byte[]> 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<string> LoadPackageText(ContentPackage package, string localFilePath) =>
public virtual FluentResults.Result<string> 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<XDocument>)> LoadPackageXmlFiles(ContentPackage package, ImmutableArray<string> localFilePaths)
public virtual ImmutableArray<(string, FluentResults.Result<XDocument>)> LoadPackageXmlFiles(ContentPackage package, ImmutableArray<string> localFilePaths)
{
((IService)this).CheckDisposed();
if (localFilePaths.IsDefaultOrEmpty)
@@ -122,7 +156,7 @@ public class StorageService : IStorageService
return builder.MoveToImmutable();
}
public ImmutableArray<(string, FluentResults.Result<byte[]>)> LoadPackageBinaryFiles(ContentPackage package, ImmutableArray<string> localFilePaths)
public virtual ImmutableArray<(string, FluentResults.Result<byte[]>)> LoadPackageBinaryFiles(ContentPackage package, ImmutableArray<string> localFilePaths)
{
((IService)this).CheckDisposed();
if (localFilePaths.IsDefaultOrEmpty)
@@ -133,7 +167,7 @@ public class StorageService : IStorageService
return builder.MoveToImmutable();
}
public ImmutableArray<(string, FluentResults.Result<string>)> LoadPackageTextFiles(ContentPackage package, ImmutableArray<string> localFilePaths)
public virtual ImmutableArray<(string, FluentResults.Result<string>)> LoadPackageTextFiles(ContentPackage package, ImmutableArray<string> localFilePaths)
{
((IService)this).CheckDisposed();
if (localFilePaths.IsDefaultOrEmpty)
@@ -144,7 +178,7 @@ public class StorageService : IStorageService
return builder.MoveToImmutable();
}
public FluentResults.Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively)
public virtual FluentResults.Result<ImmutableArray<string>> 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<FluentResults.Result<XDocument>> LoadPackageXmlAsync(ContentPackage package, string localFilePath) =>
public virtual async Task<FluentResults.Result<XDocument>> 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<FluentResults.Result<byte[]>> LoadPackageBinaryAsync(ContentPackage package, string localFilePath) =>
public virtual async Task<FluentResults.Result<byte[]>> 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<FluentResults.Result<string>> LoadPackageTextAsync(ContentPackage package, string localFilePath) =>
public virtual async Task<FluentResults.Result<string>> 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<ImmutableArray<(string, FluentResults.Result<XDocument>)>> LoadPackageXmlFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths)
public virtual async Task<ImmutableArray<(string, FluentResults.Result<XDocument>)>> LoadPackageXmlFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths)
{
((IService)this).CheckDisposed();
if (localFilePaths.IsDefaultOrEmpty)
return ImmutableArray<(string, FluentResults.Result<XDocument>)>.Empty;
var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result<XDocument>)>(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<ImmutableArray<(string, FluentResults.Result<byte[]>)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths)
public virtual async Task<ImmutableArray<(string, FluentResults.Result<byte[]>)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths)
{
((IService)this).CheckDisposed();
if (localFilePaths.IsDefaultOrEmpty)
return ImmutableArray<(string, FluentResults.Result<byte[]>)>.Empty;
var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result<byte[]>)>(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<ImmutableArray<(string, FluentResults.Result<string>)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths)
public virtual async Task<ImmutableArray<(string, FluentResults.Result<string>)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray<string> localFilePaths)
{
((IService)this).CheckDisposed();
if (localFilePaths.IsDefaultOrEmpty)
return ImmutableArray<(string, FluentResults.Result<string>)>.Empty;
var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result<string>)>(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<XDocument> TryLoadXml(string filePath, Encoding encoding = null)
public virtual FluentResults.Result<XDocument> 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<string> TryLoadText(string filePath, Encoding encoding = null)
public virtual FluentResults.Result<string> 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<string>().WithSuccess($"Loaded file successfully").WithValue(fileText);
});
}
public FluentResults.Result<byte[]> TryLoadBinary(string filePath)
public virtual FluentResults.Result<byte[]> 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<byte[]>().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<bool> FileExists(string filePath)
public virtual FluentResults.Result<bool> FileExists(string filePath)
{
((IService)this).CheckDisposed();
return IOExceptionsOperationRunner<bool>(nameof(FileExists), filePath, () =>
@@ -294,7 +354,7 @@ public class StorageService : IStorageService
});
}
public FluentResults.Result<bool> DirectoryExists(string directoryPath)
public virtual FluentResults.Result<bool> DirectoryExists(string directoryPath)
{
((IService)this).CheckDisposed();
try
@@ -308,12 +368,19 @@ public class StorageService : IStorageService
}
}
public async Task<FluentResults.Result<XDocument>> TryLoadXmlAsync(string filePath, Encoding encoding = null)
public virtual async Task<FluentResults.Result<XDocument>> 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<FluentResults.Result<string>> TryLoadTextAsync(string filePath, Encoding encoding = null)
public virtual async Task<FluentResults.Result<string>> 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<string>(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<FluentResults.Result<byte[]>> TryLoadBinaryAsync(string filePath)
public virtual async Task<FluentResults.Result<byte[]>> 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<byte[]>(nameof(TryLoadTextAsync), filePath, async () =>
{
var fp = filePath.CleanUpPath();
@@ -343,8 +423,8 @@ public class StorageService : IStorageService
});
}
public async Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding);
public async Task<FluentResults.Result> TrySaveTextAsync(string filePath, string text, Encoding encoding = null)
public virtual async Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding);
public virtual async Task<FluentResults.Result> 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<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes)
public virtual async Task<FluentResults.Result> 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<string>().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)));
}

View File

@@ -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.
*/
/// <summary>
/// Registers a type initializer from instancing config types by indicated type from config.
/// </summary>
/// <param name="initializer"></param>
/// <param name="replaceIfExists"></param>
/// <typeparam name="TData">The <see cref="Type"/> as parsed from the configuration info.</typeparam>
/// <typeparam name="TConfig">The resulting configuration instance.</typeparam>
void RegisterTypeInitializer<TData, TConfig>(Func<IConfigInfo, FluentResults.Result<TConfig>> initializer, bool replaceIfExists = false)
where TData : IEquatable<TData> where TConfig : IConfigBase;
// Config Files/Resources
Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources);
Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigProfileResourceInfo> configProfileResources);
FluentResults.Result DisposeConfigs(ImmutableArray<IConfigResourceInfo> configResources);
FluentResults.Result DisposeConfigsProfiles(ImmutableArray<IConfigProfileResourceInfo> configProfilesResources);
FluentResults.Result DisposeConfigs(ContentPackage package);
FluentResults.Result DisposeConfigsProfiles(ContentPackage package);
// Immediate Mode
FluentResults.Result<TConfig> AddConfig<TConfig>(IConfigInfo configInfo) where TConfig : IConfigBase;
/*
* Immediate mode
*/
FluentResults.Result<IConfigEntry<T>> AddConfigEntry<T>(ContentPackage package, string name,
T defaultValue,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigList> AddConfigList(ContentPackage package, string name,
int defaultIndex, IReadOnlyList<string> values,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<IConfigList, int, bool> valueChangePredicate = null,
Action<IConfigList, int> onValueChanged = null);
FluentResults.Result<IConfigRangeEntry<T>> AddConfigRangeEntry<T>(ContentPackage package, string name,
T defaultValue, T minValue, T maxValue,
Func<IConfigRangeEntry<T>, int> getStepCount,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigEntry<T>> AddConfigEntry<T>(string packageName, string name,
T defaultValue,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigList> AddConfigList(string packageName, string name,
int defaultIndex, IReadOnlyList<string> values,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<IConfigList, int, bool> valueChangePredicate = null,
Action<IConfigList, int> onValueChanged = null);
FluentResults.Result<IConfigRangeEntry<T>> AddConfigRangeEntry<T>(string packageName, string name,
T defaultValue, T minValue, T maxValue,
Func<IConfigRangeEntry<T>, int> getStepCount,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IReadOnlyDictionary<string, IConfigBase>> GetConfigsForPackage(ContentPackage package);
FluentResults.Result<IReadOnlyDictionary<string, IConfigBase>> GetConfigsForPackage(string packageName);
IReadOnlyDictionary<(ContentPackage, string), IConfigBase> GetAllConfigs();
T GetConfig<T>(ContentPackage package, string name) where T : IConfigBase;
T GetConfig<T>(string packageName, string name) where T : IConfigBase;
// Utility
FluentResults.Result ApplyProfileSettings(ContentPackage package, string profileName);
FluentResults.Result DisposePackageData(ContentPackage package);
FluentResults.Result<IReadOnlyDictionary<(ContentPackage Package, string ConfigName), IConfigBase>> GetConfigsForPackage(ContentPackage package);
FluentResults.Result<IReadOnlyDictionary<(ContentPackage Package, string ConfigName), ImmutableArray<(string ConfigName, OneOf.OneOf<string, XElement> Value)>>>
GetProfilesForPackage(ContentPackage package);
IReadOnlyDictionary<(ContentPackage Package, string Name), IConfigBase> GetAllConfigs();
bool TryGetConfig<T>(ContentPackage package, string name, out T config) where T : IConfigBase;
Task<FluentResults.Result> SaveAllConfigs();
Task<FluentResults.Result> SaveConfigsForPackage(ContentPackage package);
Task<FluentResults.Result> SaveConfig((ContentPackage Package, string ConfigName) config);
}

View File

@@ -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<CultureInfo> GetLoadedLocales();
void Remove(ImmutableArray<ILocalizationResourceInfo> localizations);
void DisposePackage(ContentPackage package);
FluentResults.Result SetCurrentCulture(CultureInfo culture);
FluentResults.Result SetCurrentCulture(string cultureName);
Task<FluentResults.Result> LoadLocalizations(ImmutableArray<ILocalizationResourceInfo> localizationResources);
/// <summary>
/// Tries to get a localized string without a fallback. Returns success/failure and associated data.
/// </summary>
/// <param name="key">Neutral localization key.</param>
/// <returns></returns>
FluentResults.Result<string> GetLocalizedString(string key);
FluentResults.Result<string> GetLocalizedString(string key, CultureInfo targetCulture);
string GetLocalizedString(string key, string fallback);
string GetLocalizedString(string key, string fallback, CultureInfo targetCulture);
FluentResults.Result<string> GetLocalizedStringForPackage(ContentPackage package, string key);
FluentResults.Result<string> 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<string, CultureInfo, string> factoryResolver);
bool IsCurrentCultureSupported(IResourceCultureInfo culturesInfo);
}

View File

@@ -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<object> GetGlobalTableValue(string tableName);
/// <summary>
/// Parses and loads script sources (code) into a memory cache without executing it.
/// </summary>
/// <param name="resourcesInfo"></param>
/// <returns></returns>
// [Required]
Task<FluentResults.Result> LoadScriptResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourcesInfo);
FluentResults.Result ExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false);
FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaScriptResourceInfo> scripts, bool pauseExecutionOnError = false, bool verboseLogging = false);
FluentResults.Result ExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false);
/// <summary>
/// Executes cached scripts (code) for the given <see cref="ContentPackage"/>.
/// </summary>
/// <param name="package"></param>
/// <returns></returns>
// [Required]
FluentResults.Result ExecuteLoadedScriptsForPackage(ContentPackage package);
/// <summary>
/// Executes cached scripts (code) for the given collection <see cref="ContentPackage"/>.
/// </summary>
/// <param name="packages"></param>
/// <returns></returns>
// [Required]
FluentResults.Result ExecuteLoadedScriptsForPackages(IEnumerable<ContentPackage> packages);
/// <summary>
///
/// </summary>
/// <returns></returns>
// [Required]
FluentResults.Result ExecuteLoadedScripts();
/// <summary>
///
/// </summary>
/// <param name="package"></param>
/// <returns></returns>
// [Required]
FluentResults.Result DisposePackageResources(ContentPackage package);
/// <summary>
/// Calls dispose on, and clears active refs for, currently running scripts. Does not clear caches.
/// </summary>
/// <returns></returns>
FluentResults.Result UnloadActiveScripts();
/// <summary>
/// Unloads all scripts and clears all caches/references.
/// </summary>
/// <returns></returns>
/// <remarks>May be functionally equivalent to <see cref="IReusableService.Reset"/></remarks>
FluentResults.Result DisposeAllPackageResources();
#endregion

View File

@@ -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);
}

View File

@@ -7,6 +7,7 @@ namespace Barotrauma.LuaCs.Services;
public interface IPackageInfoLookupService : IReusableService
{
bool IsPackageEnabled(ContentPackage package);
Task<FluentResults.Result<IPackageInfo>> Lookup(string packageName);
Task<FluentResults.Result<IPackageInfo>> Lookup(string packageName, ulong steamWorkshopId);
Task<FluentResults.Result<IPackageInfo>> Lookup(ulong steamWorkshopId);

View File

@@ -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
{
/// <summary>
/// Loads and parses the provided <see cref="ContentPackage"/> for <see cref="IResourceInfo"/> supported by the current runtime environment.
@@ -30,6 +26,20 @@ public interface IPackageManagementService : IReusableService, ILocalizationsRes
/// <returns></returns>
Task<IReadOnlyList<(ContentPackage, FluentResults.Result)>> LoadPackagesInfosAsync(IReadOnlyList<ContentPackage> packages);
IReadOnlyList<ContentPackage> GetAllLoadedPackages();
bool IsPackageLoaded(ContentPackage package);
/// <summary>
/// Filters out resources not suitable for the current environment using the following criteria: <br/>
/// - Platform (Operating System)<br/>
/// - Target (Client|Server)<br/>
/// - Null/Invalid<br/>
/// - Dependency Package Registered in PMS<br/>
/// </summary>
/// <param name="resources"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
ImmutableArray<T> FilterUnloadableResources<T>(IReadOnlyList<T> resources, bool enabledPackagesOnly = false)
where T : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo;
void DisposePackageInfos(ContentPackage package);
void DisposePackagesInfos(IReadOnlyList<ContentPackage> packages);
FluentResults.Result<IPackageDependency> GetPackageDependencyInfo(ContentPackage ownerPackage, string packageName, ulong steamWorkshopId);
@@ -38,28 +48,15 @@ public interface IPackageManagementService : IReusableService, ILocalizationsRes
FluentResults.Result<IAssembliesResourcesInfo> GetAssembliesInfos(ContentPackage package, bool onlySupportedResources = true);
FluentResults.Result<IConfigsResourcesInfo> GetConfigsInfos(ContentPackage package, bool onlySupportedResources = true);
FluentResults.Result<IConfigProfilesResourcesInfo> GetConfigProfilesInfos(ContentPackage package, bool onlySupportedResources = true);
FluentResults.Result<ILocalizationsResourcesInfo> GetLocalizationsInfos(ContentPackage package, bool onlySupportedResources = true);
FluentResults.Result<ILuaScriptsResourcesInfo> GetLuaScriptsInfos(ContentPackage package, bool onlySupportedResources = true);
#if CLIENT
FluentResults.Result<IStylesResourcesInfo> GetStylesInfos(ContentPackage package, bool onlySupportedResources = true);
#endif
// collection
FluentResults.Result<IAssembliesResourcesInfo> GetAssembliesInfos(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
FluentResults.Result<IConfigsResourcesInfo> GetConfigsInfos(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
FluentResults.Result<IConfigProfilesResourcesInfo> GetConfigProfilesInfos(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
FluentResults.Result<ILocalizationsResourcesInfo> GetLocalizationsInfos(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
FluentResults.Result<ILuaScriptsResourcesInfo> GetLuaScriptsInfos(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
#if CLIENT
FluentResults.Result<IStylesResourcesInfo> GetStylesInfos(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
#endif
FluentResults.Result<ILuaScriptsResourcesInfo> GetLuaScriptsInfos(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
Task<FluentResults.Result<IAssembliesResourcesInfo>> GetAssembliesInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
Task<FluentResults.Result<IConfigsResourcesInfo>> GetConfigsInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
Task<FluentResults.Result<IConfigProfilesResourcesInfo>> GetConfigProfilesInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
Task<FluentResults.Result<ILocalizationsResourcesInfo>> GetLocalizationsInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
Task<FluentResults.Result<ILuaScriptsResourcesInfo>> GetLuaScriptsInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
#if CLIENT
Task<FluentResults.Result<IStylesResourcesInfo>> GetStylesInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
#endif
Task<FluentResults.Result<ILuaScriptsResourcesInfo>> GetLuaScriptsInfosAsync(IReadOnlyList<ContentPackage> packages, bool onlySupportedResources = true);
}

View File

@@ -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; }
/// <summary>
/// Deletes all cached file data.
/// </summary>
void PurgeCache();
/// <summary>
/// Deletes the data for the supplied file path from the data cache.
/// </summary>
/// <param name="absolutePath"></param>
void PurgeFileFromCache(string absolutePath);
/// <summary>
/// Deletes the data from the supplied file paths from the data cache.
/// </summary>
/// <param name="absolutePaths"></param>
void PurgeFilesFromCache(params string[] absolutePaths);
// -- local game folder storage
FluentResults.Result<XDocument> LoadLocalXml(ContentPackage package, string localFilePath);
FluentResults.Result<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath);

View File

@@ -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<AssemblyLoader> _onUnload;
private readonly Func<AssemblyLoadContext, AssemblyName, Assembly> _onResolvingManaged;
private readonly Func<Assembly, string, IntPtr> _onResolvingUnmanagedDll;
private readonly ConcurrentDictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new();
private readonly ConcurrentDictionary<AssemblyOrStringKey, AssemblyData> _loadedAssemblyData = new();
private readonly ThreadLocal<bool> _isResolving = new(static()=>false); // cyclic resolution exit
private readonly ThreadLocal<bool> _isResolvingNative = new(static () => false);
public AssemblyLoader(IAssemblyManagementService assemblyManagementService,
public AssemblyLoader(
IAssemblyManagementService assemblyManagementService,
Guid id, string name,
bool isReferenceOnlyMode, Action<AssemblyLoader> onUnload)
bool isReferenceOnlyMode,
Action<AssemblyLoader> 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<MetadataReference> 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<Assembly>().WithError(new Error($"The name provided is already assigned to an assembly!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, syntaxTrees));
return new Result<Assembly>().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<Assembly>().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<Assembly>().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<Assembly>().WithError(new Error($"The path provided is null!"));
return new Result<Assembly>().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<Assembly>().WithSuccess($"Loaded assembly'{assembly.GetName()}'").WithValue(assembly);
}
catch (ArgumentNullException ane)
{
return FluentResults.Result.Fail<Assembly>(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<Assembly>(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<Assembly>(new ExceptionalError(fle)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, assemblyFilePath)
.WithMetadata(MetadataType.ExceptionDetails, fle.Message)
.WithMetadata(MetadataType.StackTrace, fle.StackTrace));
return new Result<Assembly>().WithSuccess($"Loaded assembly '{assembly.GetName()}'").WithValue(assembly);
}
catch (FileNotFoundException fnfe)
{
return FluentResults.Result.Fail<Assembly>(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<Assembly>(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<Assembly>(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<Assembly> GenerateExceptionReturn<T>(T exception) where T : Exception
{
return FluentResults.Result.Fail<Assembly>(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, assemblyFilePath)
.WithMetadata(MetadataType.ExceptionDetails, exception.Message)
.WithMetadata(MetadataType.StackTrace, exception.StackTrace));
}
}
public FluentResults.Result<Assembly> 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<Assembly>().WithSuccess(new Success($"Assembly found")).WithValue(data.Assembly);
return new Result<Assembly>().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<Assembly>().WithSuccess(new Success($"Assembly found")).WithValue(assembly1);
return new Result<Assembly>().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<IAssemblyLoaderService>(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<IAssemblyLoaderService>(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);