Merge branch 'heads/upstream' into OBT/1.2.0(SpringUpdate)

This commit is contained in:
NotAlwaysTrue
2026-04-25 13:08:16 +08:00
420 changed files with 24089 additions and 11191 deletions
@@ -0,0 +1,682 @@
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;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs;
public sealed partial class ConfigService : IConfigService
{
#region Disposal_Locks_Reset
private readonly AsyncReaderWriterLock _operationLock = new ();
private readonly AsyncReaderWriterLock _settingsByPackageLock = new ();
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public void Dispose()
{
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_logger.LogDebug($"{nameof(ConfigService)}: Disposing.");
_configInfoParserService.Dispose();
_configProfileInfoParserService.Dispose();
if (!_settingsInstances.IsEmpty)
{
foreach (var instance in _settingsInstances)
{
try
{
if (instance.Value is null)
{
continue;
}
_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
// ReSharper disable once AccessToDisposedClosure
sub.OnSettingInstanceDisposed(instance.Value));
instance.Value.Dispose();
}
catch
{
// ignored
continue;
}
}
}
_settingsInstances.Clear();
_instanceFactory.Clear();
_settingsInstancesByPackage.Clear();
_commandsService.Dispose();
_storageService = null;
_logger = null;
_eventService = null;
_configInfoParserService = null;
_configProfileInfoParserService = null;
_commandsService = null;
_infoProvider = null;
}
public FluentResults.Result Reset()
{
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = new FluentResults.Result();
if (!_settingsInstances.IsEmpty)
{
foreach (var instance in _settingsInstances)
{
try
{
if (instance.Value is null)
{
continue;
}
_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
// ReSharper disable once AccessToDisposedClosure
sub.OnSettingInstanceDisposed(instance.Value));
instance.Value.Dispose();
}
catch (Exception e)
{
result.WithError(new ExceptionalError(e));
}
}
}
_settingsInstances.Clear();
_instanceFactory.Clear();
_settingsInstancesByPackage.Clear();
_storageService.PurgeCache();
return result;
}
#endregion
private const string SaveDataFileName = "SettingsData.xml";
// --- Settings
private readonly ConcurrentDictionary<(ContentPackage OwnerPackage, string InternalName), ISettingBase>
_settingsInstances = new();
private readonly ConcurrentDictionary<string, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase>>
_instanceFactory = new();
private readonly ConcurrentDictionary<ContentPackage, ConcurrentBag<ISettingBase>>
_settingsInstancesByPackage = new();
// --- Profiles
private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), IConfigProfileInfo>
_settingsProfiles = new();
private IStorageService _storageService;
private ILoggerService _logger;
private IEventService _eventService;
private IConsoleCommandsService _commandsService;
private ILuaCsInfoProvider _infoProvider;
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> _configInfoParserService;
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> _configProfileInfoParserService;
public ConfigService(ILoggerService logger,
IStorageService storageService,
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> configInfoParserService,
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> configProfileInfoParserService,
IEventService eventService,
IConsoleCommandsService commandsService,
ILuaCsInfoProvider infoProvider)
{
_logger = logger;
_storageService = storageService;
_configInfoParserService = configInfoParserService;
_configProfileInfoParserService = configProfileInfoParserService;
_eventService = eventService;
_commandsService = commandsService;
_infoProvider = infoProvider;
_storageService.UseCaching = false;
InjectCommands(commandsService);
}
private void InjectCommands(IConsoleCommandsService commandsService)
{
commandsService.RegisterCommand("cfg_getvalue", "cfg_getvalue [Content Package] [InternalName] [ValueString]: gets a config value.", (string[] args) =>
{
if (args.Length < 1)
{
_logger.LogError("Please specify the name of the package to set the config.");
return;
}
if (args.Length < 2)
{
_logger.LogError("Please specify the name of the config.");
return;
}
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
if (package == null)
{
_logger.LogError($"Could not find the package {args[0]}!");
return;
}
string internalName = args[1];
if (!TryGetConfig(package, internalName, out ISettingBase setting))
{
_logger.LogError($"Could not get config with name {internalName}");
return;
}
_logger.LogMessage($"config {internalName} value is {setting.GetStringValue()}", Color.Green);
}, getValidArgs: () => new[]
{
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
});
commandsService.RegisterCommand("cfg_setvalue", "cfg_setvalue [Content Package] [InternalName] [ValueString]: sets a config.", (string[] args) =>
{
if (args.Length < 1)
{
_logger.LogError("Please specify the name of the package to set the config.");
return;
}
if (args.Length < 2)
{
_logger.LogError("Please specify the name of the config.");
return;
}
if (args.Length < 3)
{
_logger.LogError("Please specify the value to set the config to.");
return;
}
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
if (package == null)
{
_logger.LogError($"Could not find the package {args[0]}!");
return;
}
string internalName = args[1];
string valueString = args[2];
if (!TryGetConfig(package, internalName, out ISettingBase setting))
{
_logger.LogError($"Could not get config with name {internalName}");
return;
}
if (setting.TrySetSerializedValue(valueString))
{
_logger.LogMessage($"Set config {internalName} value to {valueString}", Color.Green);
if (SaveConfigValue(setting) is { IsFailed: true } res)
{
_logger.LogMessage($"Failed to save new config data to disk. Reasons: {res.ToString()}");
}
}
else
{
_logger.LogError($"Failed to set config value");
}
}, getValidArgs: () => new[]
{
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
});
commandsService.RegisterCommand("cfg_setprofile", "cfg_setprofile [ContentPackage] [InternalProfileName]",
(string[] args) =>
{
if (args.Length < 1 || args[0].IsNullOrWhiteSpace())
{
_logger.LogError("Please specify the name of the package of the profile.");
return;
}
if (args.Length < 2 || args[1].IsNullOrWhiteSpace())
{
_logger.LogError("Please specify the name of the profile.");
return;
}
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0], null);
if (package == null)
{
_logger.LogError($"Could not find the package {args[0]}!");
return;
}
var res = ApplyConfigProfile(package, args[1]);
if (res.IsFailed)
{
_logger.LogError($"Errors while applying profile {args[1]}!");
_logger.LogResults(res);
return;
}
_logger.Log($"Profile {args[1]} applied successfully!", Color.Green);
}, getValidArgs: () => new[]
{
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
}, false);
}
public void RegisterSettingTypeInitializer<T>(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory) where T : class, ISettingBase
{
Guard.IsNotNullOrWhiteSpace(typeIdentifier, nameof(typeIdentifier));
Guard.IsNotNull(settingFactory, nameof(settingFactory));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_instanceFactory.ContainsKey(typeIdentifier))
{
ThrowHelper.ThrowArgumentException($"{nameof(RegisterSettingTypeInitializer)}: The type identifier {typeIdentifier} is already registered.");
}
_instanceFactory[typeIdentifier] = settingFactory;
}
private static ImmutableArray<T> SelectCompatible<T>(ImmutableArray<T> resources) where T : IBaseResourceInfo
{
return resources
.Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform))
.Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget))
.OrderBy(r => r.Optional ? 1 : 0) // optional content last
.ThenBy(r => r.LoadPriority)
.ToImmutableArray();
}
public async Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources)
{
using var lck = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (configResources.IsDefaultOrEmpty)
{
return FluentResults.Result.Ok();
}
var result = new FluentResults.Result();
var taskBuilder = ImmutableArray.CreateBuilder<Task<ImmutableArray<IConfigInfo>>>();
var toProcessErrors = new ConcurrentStack<IError>();
foreach (var resource in SelectCompatible(configResources))
{
taskBuilder.Add(await Task.Factory.StartNew<Task<ImmutableArray<IConfigInfo>>>(async Task<ImmutableArray<IConfigInfo>> () =>
{
var r = await _configInfoParserService.TryParseResourcesAsync(resource);
if (r.IsFailed)
{
toProcessErrors.PushRange(r.Errors.ToArray());
return ImmutableArray<IConfigInfo>.Empty;
}
return r.Value;
}));
}
var taskResults = await Task.WhenAll(taskBuilder.ToImmutable());
if (toProcessErrors.Count > 0)
{
return FluentResults.Result.Fail($"{nameof(LoadConfigsAsync)}: Errors while loading configuration info: ").WithErrors(toProcessErrors.ToArray());
}
var toProcessDocs = taskResults
.Where(tr => !tr.IsDefaultOrEmpty)
.SelectMany(tr => tr)
.Where(icf => icf is not null)
.ToImmutableArray();
var instanceQueue = new Queue<(IConfigInfo configInfo, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase> factory)>();
foreach (var info in toProcessDocs)
{
if (!_instanceFactory.TryGetValue(info.DataType, out var factory))
{
result.WithError(
$"{nameof(LoadConfigsAsync)}: Could not retrieve the instance factory for the data type of '{info.DataType}'!");
continue;
}
if (_settingsInstances.ContainsKey((info.OwnerPackage, info.InternalName)))
{
// duplicate for some reason (ie. double loading). This should never happen.
ThrowHelper.ThrowInvalidOperationException($"{nameof(LoadConfigsAsync)}: A setting for the [ContentPackage].[InternalName] of '[{info.OwnerPackage.Name}].[{info.InternalName}]' already exists!");
}
instanceQueue.Enqueue((info, factory));
}
var toProcessInstanceQueue = new Queue<(IConfigInfo info, ISettingBase instance)>();
while (instanceQueue.TryDequeue(out var instanceFactoryInfo))
{
try
{
toProcessInstanceQueue.Enqueue((instanceFactoryInfo.configInfo, instanceFactoryInfo.factory((this, instanceFactoryInfo.configInfo))));
}
catch (Exception e)
{
result.WithError(
$"{nameof(LoadConfigsAsync)}: Error while instancing setting for '{instanceFactoryInfo.configInfo.OwnerPackage}.{instanceFactoryInfo.configInfo.InternalName}': {e.Message}!");
continue;
}
}
using var settingsLck = await _settingsByPackageLock.AcquireWriterLock(); // block to protect new bag instance creation
while (toProcessInstanceQueue.TryDequeue(out var newInstanceData))
{
_settingsInstances[(newInstanceData.info.OwnerPackage, newInstanceData.info.InternalName)] = newInstanceData.instance;
if (!_settingsInstancesByPackage.TryGetValue(newInstanceData.info.OwnerPackage, out _))
{
_settingsInstancesByPackage[newInstanceData.info.OwnerPackage] = new ConcurrentBag<ISettingBase>();
}
_settingsInstancesByPackage[newInstanceData.info.OwnerPackage].Add(newInstanceData.instance);
result.WithReasons(_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
sub.OnSettingInstanceCreated(newInstanceData.instance)).Reasons);
}
return result;
}
public async Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigResourceInfo> configProfileResources)
{
using var _ = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (configProfileResources.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadConfigsProfilesAsync)}: {nameof(configProfileResources)} is empty.");
}
var result = new FluentResults.Result();
foreach (var resource in SelectCompatible(configProfileResources))
{
var r = await _configProfileInfoParserService.TryParseResourcesAsync(resource);
if (r.IsFailed)
{
result.WithErrors(r.Errors);
continue;
}
foreach (var info in r.Value)
{
if (!_settingsProfiles.TryAdd((info.OwnerPackage, info.InternalName), info))
{
result.WithErrors(r.Errors);
continue;
}
if (info.InternalName.Equals("default", StringComparison.InvariantCultureIgnoreCase))
{
//apply it
foreach (var value in info.ProfileValues)
{
if (_settingsInstances.TryGetValue((info.OwnerPackage, value.SettingName), out var instance))
{
instance.TrySetSerializedValue(value.Element);
}
}
}
}
}
return result;
}
public FluentResults.Result LoadSavedValueForConfig(ISettingBase setting)
{
Guard.IsNotNull(setting, nameof(setting));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not { } saveFileResult)
{
#if DEBUG
return FluentResults.Result.Fail(
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
#endif
return FluentResults.Result.Ok();
}
if (saveFileResult is { IsFailed: true })
{
#if DEBUG
_logger.LogResults(saveFileResult.ToResult());
return FluentResults.Result.Fail(
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
#endif
return FluentResults.Result.Ok();
}
if (saveFileResult.Value.Root is not {} rootElement
|| !string.Equals(rootElement.Name.LocalName, "Configuration", StringComparison.InvariantCultureIgnoreCase))
{
return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Root invalid for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
}
if (rootElement.GetChildElement(XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), StringComparison.InvariantCultureIgnoreCase)
?.GetChildElement(setting.InternalName, StringComparison.InvariantCultureIgnoreCase) is not {} cfgValueElement)
{
#if DEBUG
return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Could not find saved value for setting:[{setting.OwnerPackage.Name}.{setting.InternalName}]");
#endif
return FluentResults.Result.Ok();
}
return FluentResults.Result.OkIf(setting.TrySetSerializedValue(cfgValueElement), new Error($"Failed to set value for [{setting.OwnerPackage.Name}.{setting.InternalName}]"));
}
public FluentResults.Result LoadSavedConfigsValues()
{
ImmutableArray<ISettingBase> cfgValues;
using (var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult())
{
IService.CheckDisposed(this);
cfgValues = _settingsInstances.Select(kvp => kvp.Value).ToImmutableArray();
}
var ret = new FluentResults.Result();
foreach (var settingBase in cfgValues)
{
#if DEBUG
// log in debug only.
ret.WithReasons(LoadSavedValueForConfig(settingBase).Reasons);
#else
LoadSavedValueForConfig(settingBase);
#endif
}
return ret;
}
public FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName));
using var _ = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_settingsProfiles.TryGetValue((package, internalName), out var setting))
{
return FluentResults.Result.Fail($"{nameof(ApplyConfigProfile)}: Could not find profile [{package.Name}.{internalName}]");
}
var result = new FluentResults.Result();
foreach (var profileValue in setting.ProfileValues)
{
if (!_settingsInstances.TryGetValue((package, profileValue.SettingName), out var instance))
{
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Could not find setting [{profileValue.SettingName}]."));
continue;
}
if (!instance.TrySetSerializedValue(profileValue.Element))
{
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Failed to set value for [{profileValue.SettingName}]."));
}
}
return result;
}
public FluentResults.Result SaveConfigValue(ISettingBase setting)
{
XDocument cpCfgValues;
if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not {} saveFileResult)
{
return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Storage Service Failure while trying to load file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
}
// get Configuration
if (saveFileResult.IsFailed)
{
cpCfgValues = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement("Configuration"));
}
else
{
cpCfgValues = saveFileResult.Value;
}
if (cpCfgValues.Root is null || cpCfgValues.Root.Name != "Configuration")
{
return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Bad save file format for setting: [{setting.OwnerPackage.Name}.{setting.InternalName}]");
}
XElement currentTarget = GetOrAddElement(cpCfgValues.Root, XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), name => new XElement(name));
currentTarget = GetOrAddElement(currentTarget, setting.InternalName, name => new XElement(name));
var ret = setting.GetSerializableValue().Match(str =>
{
var tgt = currentTarget.Attribute("Value");
if (tgt is null)
{
var attr = new XAttribute("Value", str);
currentTarget.Add(attr);
}
else
{
tgt.Value = str;
}
return FluentResults.Result.Ok();
},
elem =>
{
currentTarget.ReplaceNodes(new XElement("Value", elem));
return FluentResults.Result.Ok();
});
ret.WithReasons(_storageService.SaveLocalXml(setting.OwnerPackage, SaveDataFileName, cpCfgValues).Reasons);
return ret;
XElement GetOrAddElement(XElement containerElement, string elementName, Func<string, XElement> factory)
{
var element = containerElement.Element(elementName);
if (element is null)
{
element = factory(elementName);
containerElement.Add(element);
}
return element;
}
}
public FluentResults.Result DisposePackageData(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
ConcurrentBag<ISettingBase> toDispose;
using (var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult())
{
if (!_settingsInstancesByPackage.TryRemove(package, out toDispose) || toDispose is null)
{
return FluentResults.Result.Ok();
}
}
var result = new FluentResults.Result();
foreach (var setting in toDispose)
{
result.WithReasons(_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub => sub.OnSettingInstanceDisposed(setting)).Reasons);
try
{
_settingsInstances.TryRemove((setting.OwnerPackage, setting.InternalName), out _);
setting.Dispose();
}
catch (Exception e)
{
result.WithError(new ExceptionalError(e));
}
}
return result;
}
public FluentResults.Result DisposeAllPackageData()
{
return this.Reset();
}
public bool TryGetConfig<T>(ContentPackage package, string internalName, out T instance) where T : ISettingBase
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var settingsLck =
_settingsByPackageLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
instance = default;
if(!_settingsInstances.TryGetValue((package, internalName), out var inst))
{
return false;
}
if (inst is not T instanceT)
{
return false;
}
instance = instanceT;
return true;
}
}
@@ -0,0 +1,97 @@
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.LuaCs;
internal class ConsoleCommandsService : IConsoleCommandsService
{
private readonly List<DebugConsole.Command> _registeredCommands = new();
public void Dispose()
{
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
foreach (var cmd in _registeredCommands.ToImmutableArray())
{
DebugConsole.Commands.Remove(cmd);
}
_registeredCommands.Clear();
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public void RegisterCommand(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false)
{
IService.CheckDisposed(this);
if (DebugConsole.Commands.Any(cmd => cmd.Names.Contains(name)))
{
LuaCsSetup.Instance.Logger.LogWarning($"Registering console command {name} more than once!");
}
var cmd = new DebugConsole.Command(name, help, onExecute, getValidArgs, isCheat);
_registeredCommands.Add(cmd);
DebugConsole.Commands.Add(cmd);
}
public void AssignOnExecute(string names, Action<string[]> onExecute)
{
var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
if (matchingCommand == null)
{
throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found.");
}
else
{
matchingCommand.OnExecute = onExecute;
}
}
#if SERVER
public void AssignOnClientRequestExecute(string names, Action<Client, Vector2, string[]> onClientRequestExecute)
{
var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
if (matchingCommand == null)
{
throw new Exception("AssignOnClientRequestExecute failed. Command matching the name(s) \"" + names + "\" not found.");
}
else
{
matchingCommand.OnClientRequestExecute = onClientRequestExecute;
}
}
#endif
public void RemoveCommand(string name)
{
IService.CheckDisposed(this);
_registeredCommands.RemoveAll(cmd => cmd.Names.Contains(name));
DebugConsole.Commands.RemoveAll(cmd => cmd.Names.Contains(name));
}
public void RemoveRegisteredCommands()
{
IService.CheckDisposed(this);
foreach (var cmd in _registeredCommands.ToImmutableArray())
{
DebugConsole.Commands.Remove(cmd);
}
_registeredCommands.Clear();
}
}
@@ -0,0 +1,426 @@
using Barotrauma.LuaCs.Events;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.Interpreter;
using OneOf;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Barotrauma.LuaCs;
public partial class EventService : IEventService
{
private readonly record struct TypeStringKey : IEqualityComparer<TypeStringKey>, IEquatable<TypeStringKey>
{
public Type Type { get; init; }
public string TypeName { get; init; }
public readonly int HashCode;
public TypeStringKey(Type type)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
TypeName = type.Name.ToLowerInvariant();
HashCode = TypeName.GetHashCode();
}
public TypeStringKey(string typeName)
{
Type = null;
TypeName = typeName?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(typeName));
HashCode = TypeName.GetHashCode();
}
public bool Equals(TypeStringKey x, TypeStringKey y)
{
if (x.Type is not null && y.Type is not null)
return x.Type == y.Type;
return x.TypeName == y.TypeName;
}
public int GetHashCode(TypeStringKey obj)
{
return obj.HashCode;
}
public static implicit operator TypeStringKey(Type type) => new(type);
public static implicit operator TypeStringKey(string typeName) => new(typeName);
}
private readonly ILoggerService _loggerService;
private readonly ILuaPatcher _luaPatcher;
private readonly AsyncReaderWriterLock _operationsLock = new();
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<OneOf<IEvent, string>, IEvent>> _subscribers = new();
private readonly ConcurrentDictionary<TypeStringKey, (TypeStringKey Event, Func<LuaCsFunc, IEvent> RunnerFactory)> _luaAliasEventFactory = new();
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<TypeStringKey, LuaCsFunc>> _luaLegacyEventsSubscribers = new();
private readonly ConcurrentDictionary<IEventService, IEventService> _subscribedEventDispatchers = new();
#region LifeCycle
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_luaLegacyEventsSubscribers.Clear();
_luaAliasEventFactory.Clear();
_subscribers.Clear();
_luaPatcher.Dispose();
}
private int _isDisposed;
public EventService(ILoggerService loggerService, ILuaPatcher luaPatcher)
{
_loggerService = loggerService;
_luaPatcher = luaPatcher;
}
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public FluentResults.Result Reset()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_luaLegacyEventsSubscribers.Clear();
_luaAliasEventFactory.Clear();
_subscribers.Clear();
_luaPatcher.Reset();
return FluentResults.Result.Ok();
}
#endregion
#region LuaEventSystem
public void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
Guard.IsNotNull(callback, nameof(callback));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
{
var eventSubs = _subscribers.GetOrAdd(eventFunc.Event, key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
eventSubs[identifier] = eventFunc.RunnerFactory(callback);
}
else
{
var eventSubs = _luaLegacyEventsSubscribers.GetOrAdd(eventName, key => new ConcurrentDictionary<TypeStringKey, LuaCsFunc>());
eventSubs[identifier] = callback;
}
}
public void Add(string eventName, LuaCsFunc callback, object owner = null)
{
// random ident, we hope for no conflicts :barodev:.
Add(eventName, Random.Shared.NextInt64().ToString() ,callback);
}
public object Call(string eventName, params object[] args)
{
return Call<object>(eventName, args);
}
[MoonSharpHidden] // Needs to be hidden so Lua doesn't accidentally use this instead of the above
public T Call<T>(string eventName, params object[] args)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubscribers)
|| eventSubscribers.IsEmpty)
{
return default;
}
T returnValue = default;
foreach (var subscriber in eventSubscribers)
{
try
{
object result = subscriber.Value.Invoke(args);
if (result is DynValue luaResult)
{
if (luaResult.Type == DataType.Tuple)
{
bool replaceNil = luaResult.Tuple.Length > 1 && luaResult.Tuple[1].CastToBool();
if (!luaResult.Tuple[0].IsNil() || replaceNil)
{
returnValue = luaResult.ToObject<T>();
}
}
else if (!luaResult.IsNil())
{
returnValue = luaResult.ToObject<T>();
}
}
else
{
returnValue = (T)result;
}
}
catch (Exception e)
{
_loggerService.LogError(e.Message);
#if DEBUG
throw;
#endif
}
}
return returnValue;
}
public void Subscribe<T>(string identifier, IDictionary<string, LuaCsFunc> callbacks) where T : class, IEvent<T>
{
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
Guard.IsNotNull(callbacks, nameof(callbacks));
Guard.IsNotEmpty(callbacks, nameof(callbacks));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var eventSubs = _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
eventSubs[identifier] = T.GetLuaRunner(callbacks);
}
public void Remove(string eventName, string identifier)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
{
if (_subscribers.TryGetValue(eventFunc.Event, out var eventSubs))
{
eventSubs.TryRemove(identifier, out _);
}
}
else
{
if (_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubs))
{
eventSubs.TryRemove(identifier, out _);
}
}
}
public void Unsubscribe(string eventName, string identifier)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!_subscribers.TryGetValue(eventName, out var evtSubscribers))
{
return;
}
evtSubscribers.TryRemove(identifier, out _);
}
public void PublishLuaEvent<T>(LuaCsFunc subscriberRunner) where T : class, IEvent<T>
{
this.PublishEvent<T>(sub => subscriberRunner(sub));
}
public FluentResults.Result RegisterLuaEventAlias<T>(string luaEventName, string targetMethod) where T : class, IEvent<T>
{
Guard.IsNotNullOrWhiteSpace(luaEventName, nameof(luaEventName));
Guard.IsNotNullOrWhiteSpace(targetMethod, nameof(targetMethod));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.ContainsKey(luaEventName))
{
#if DEBUG
ThrowHelper.ThrowInvalidOperationException($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
#endif
return FluentResults.Result.Fail($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
}
var eventRunnerFactory = (LuaCsFunc function) => (IEvent)T.GetLuaRunner(new Dictionary<string, LuaCsFunc>
{
{ targetMethod, function }
});
_luaAliasEventFactory[luaEventName] = (Event: typeof(T), RunnerFactory: eventRunnerFactory);
// create the group
_subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
return FluentResults.Result.Ok();
}
#endregion
public FluentResults.Result Subscribe<T>(T subscriber) where T : class, IEvent<T>
{
Guard.IsNotNull(subscriber, nameof(subscriber));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var eventSubs =
_subscribers.GetOrAdd(typeof(T), (type) => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
if (eventSubs.ContainsKey(subscriber))
{
ThrowHelper.ThrowInvalidOperationException($"{nameof(Subscribe)}: The instance is already registered!");
}
return eventSubs.TryAdd(subscriber, subscriber)
? FluentResults.Result.Ok()
: FluentResults.Result.Fail($"{nameof(Subscribe)}: Failed to add subscriber.");
}
public void Unsubscribe<T>(T subscriber) where T : class, IEvent
{
Guard.IsNotNull(subscriber, nameof(subscriber));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_subscribers.TryGetValue(typeof(T), out var evtSubscribers))
{
return;
}
evtSubscribers.TryRemove(subscriber, out _);
}
public void ClearAllEventSubscribers<T>() where T : class, IEvent
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribers.TryRemove(typeof(T), out _);
}
public void ClearAllSubscribers()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribers.Clear();
}
public FluentResults.Result PublishEvent<T>(Action<T> action) where T : class, IEvent<T>
{
Guard.IsNotNull(action, nameof(action));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_subscribers.TryGetValue(typeof(T), out var subs) || subs.IsEmpty)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
foreach (var sub in subs)
{
try
{
action.Invoke(Unsafe.As<T>(sub.Value));
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
_loggerService.LogError(e.Message);
continue;
}
}
foreach (var dispatchers in _subscribedEventDispatchers.ToImmutableArray())
{
dispatchers.Value.PublishEvent(action);
}
return results;
}
public void AddDispatcherEventService(IEventService eventService)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribedEventDispatchers.TryAdd(eventService, eventService);
}
public void RemoveDispatcherEventService(IEventService eventService)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribedEventDispatchers.TryRemove(eventService, out _);
}
#region LuaPatcherAdapter
public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(identifier, className, methodName, parameterTypes, patch, hookType);
}
public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(identifier, className, methodName, patch, hookType);
}
public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(className, methodName, parameterTypes, patch, hookType);
}
public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(className, methodName, patch, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType)
{
return _luaPatcher.RemovePatch(className, className, methodName, parameterTypes, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType)
{
return _luaPatcher.RemovePatch(className, className, methodName, hookType);
}
public void HookMethod(string identifier, MethodBase method, LuaCsPatch patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null)
{
_luaPatcher.HookMethod(identifier, method, patch, hookType, owner);
}
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(identifier, className, methodName, parameterNames, patch, hookMethodType);
}
public void HookMethod(string identifier, string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(identifier, className, methodName, patch, hookMethodType);
}
public void HookMethod(string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(className, methodName, patch, hookMethodType);
}
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(className, methodName, parameterNames, patch, hookMethodType);
}
#endregion
}
@@ -0,0 +1,416 @@
using Barotrauma.Items.Components;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using Barotrauma.Steam;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using static Barotrauma.ContentPackageManager;
namespace Barotrauma.LuaCs;
[HarmonyPatch]
internal class HarmonyEventPatchesService : ISystem
{
public bool IsDisposed { get; private set; }
public FluentResults.Result Reset()
{
Unpatch();
Patch();
return FluentResults.Result.Ok();
}
private static IEventService _eventService;
private static ILoggerService _loggerService;
private readonly Harmony Harmony;
public HarmonyEventPatchesService(IEventService eventService, ILoggerService loggerService)
{
_eventService = eventService;
_loggerService = loggerService;
Harmony = new Harmony("LuaCsForBarotrauma.Events");
Patch();
}
private void Patch()
{
this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService));
#if SERVER
this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService.Patch_StartGame_End));
#endif
}
private void Unpatch()
{
this.Harmony?.UnpatchSelf();
}
[HarmonyPatch(typeof(CoroutineManager), nameof(CoroutineManager.Update)), HarmonyPostfix]
public static void CoroutineManager_Update_Post()
{
_eventService.PublishEvent<IEventUpdate>(x => x.OnUpdate(CoroutineManager.DeltaTime));
_loggerService.ProcessLogs();
}
#if CLIENT
[HarmonyPatch(typeof(GameSession), nameof(GameSession.StartRound), new Type[]
{
typeof(LevelData), typeof(bool), typeof(SubmarineInfo), typeof(SubmarineInfo)
}), HarmonyPostfix]
public static void GameSession_StartRound_Post()
{
_eventService.PublishEvent<IEventRoundStarted>(x => x.OnRoundStart());
}
#endif
[HarmonyPatch(typeof(GameSession), nameof(GameSession.EndRound)), HarmonyPrefix]
public static void GameSession_EndRound_Pre()
{
_eventService.PublishEvent<IEventRoundEnded>(x => x.OnRoundEnd());
}
[HarmonyPatch(typeof(GameSession), nameof(GameSession.LoadPreviousSave)), HarmonyPrefix]
public static void GameSession_LoadPreviousSave_Pre()
{
_eventService.PublishEvent<IEventRoundEnded>(x => x.OnRoundEnd());
}
[HarmonyPatch(typeof(GameSession), nameof(GameSession.EndMissions)), HarmonyPostfix]
public static void GameSession_EndMission_Post(GameSession __instance)
{
_eventService.PublishEvent<IEventMissionsEnded>(x => x.OnMissionsEnded(__instance.Missions.ToList()));
}
[HarmonyPatch(typeof(Screen), nameof(Screen.Select)), HarmonyPostfix]
public static void Screen_Selected_Post(Screen __instance)
{
_eventService.PublishEvent<IEventScreenSelected>(x => x.OnScreenSelected(__instance));
}
#if CLIENT
[HarmonyPatch(typeof(MainMenuScreen), "StartGame"), HarmonyPostfix]
public static void MainMenuScreen_StartGame_Pre(Screen __instance)
{
LuaCsSetup.Instance.SetRunState(RunState.Running);
}
[HarmonyPatch(typeof(MainMenuScreen), "LoadGame"), HarmonyPostfix]
public static void MainMenuScreen_LoadGame_Pre(Screen __instance)
{
LuaCsSetup.Instance.SetRunState(RunState.Running);
}
[HarmonyPatch(typeof(MutableWorkshopMenu), nameof(MutableWorkshopMenu.Apply)), HarmonyPostfix]
public static void MutableWorkshopMenu_Apply_Post(Screen __instance)
{
LuaCsSetup.Instance.PromptCSharpMods(selection => { }, joiningServer: false);
}
#endif
[HarmonyPatch(typeof(ContentPackageManager.PackageSource), nameof(ContentPackageManager.PackageSource.Refresh)), HarmonyPostfix]
public static void PackageSource_Refresh_Post()
{
_eventService.PublishEvent<IEventAllPackageListChanged>(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages));
}
[HarmonyPatch(typeof(ContentPackageManager), nameof(ContentPackageManager.Init)), HarmonyPostfix]
public static void ContentPackageManager_Init_Post()
{
_eventService.PublishEvent<IEventAllPackageListChanged>(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages));
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
}
[HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetCore)), HarmonyPostfix]
public static void EnabledPackages_SetCore_Post()
{
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
}
[HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetRegular)), HarmonyPostfix]
public static void EnabledPackages_SetRegular_Post()
{
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
}
#if CLIENT
[HarmonyPatch(typeof(GameClient), "ReadDataMessage"), HarmonyPrefix]
public static bool GameClient_ReadDataMessage_Pre(IReadMessage inc)
{
int prevBitPosition = inc.BitPosition;
ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte();
bool? skip = null;
_eventService.PublishEvent<IEventServerRawNetMessageReceived>(x => skip = x.OnReceivedServerNetMessage(inc, header) ?? skip);
if (skip == true)
{
return false;
}
inc.BitPosition = prevBitPosition; // rewind so the game can read the message
return true;
}
[HarmonyPatch(typeof(SubEditorScreen), nameof(SubEditorScreen.Select), new Type[] { }), HarmonyPostfix]
public static void SubEditorScreen_Selected_Post(Screen __instance)
{
_eventService.PublishEvent<IEventScreenSelected>(x => x.OnScreenSelected(__instance));
}
[HarmonyPatch(typeof(PlayerInput), nameof(PlayerInput.Update)), HarmonyPrefix]
public static void PlayerInput_Update_Pre(double deltaTime)
{
_eventService.PublishEvent<IEventKeyUpdate>(x => x.OnKeyUpdate(deltaTime));
}
[HarmonyPatch(typeof(DebugConsole), "IsCommandPermitted"), HarmonyPrefix]
public static bool DebugConsole_IsCommandPermitted(Identifier command, ref bool __result)
{
DebugConsole.Command c = DebugConsole.FindCommand(command.Value);
if (DebugConsole.Commands.IndexOf(c) >= LuaCsSetup.DebugConsoleCommandVanillaIndex)
{
__result = true;
return false;
}
return true;
}
#elif SERVER
[HarmonyPatch(typeof(GameServer), "ReadDataMessage"), HarmonyPrefix]
public static bool GameServer_ReadDataMessage_Pre(NetworkConnection sender, IReadMessage inc)
{
int prevBitPosition = inc.BitPosition;
ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte();
bool? skip = null;
_eventService.PublishEvent<IEventClientRawNetMessageReceived>(x => skip = x.OnReceivedClientNetMessage(inc, header, sender) ?? skip);
if (skip == true)
{
return false;
}
inc.BitPosition = prevBitPosition; // rewind so the game can read the message
return true;
}
[HarmonyPatch(typeof(GameServer), "OnInitializationComplete"), HarmonyPostfix]
public static void GameServer_OnInitializationComplete_Post(GameServer __instance)
{
Client client = __instance.ConnectedClients.LastOrDefault();
if (client == null) { return; }
_eventService.PublishEvent<IEventClientConnected>(x => x.OnClientConnected(client));
}
[HarmonyPatch(typeof(GameServer), nameof(GameServer.DisconnectClient), new Type[] { typeof(Client), typeof(PeerDisconnectPacket) }), HarmonyPrefix]
public static void GameServer_DisconnectClient_Pre(Client client, PeerDisconnectPacket peerDisconnectPacket)
{
if (client == null) { return; }
_eventService.PublishEvent<IEventClientDisconnected>(x => x.OnClientDisconnected(client));
}
[HarmonyPatch(typeof(GameServer), nameof(GameServer.AssignJobs)), HarmonyPostfix]
public static void GameServer_AssignJobs_Post(List<Client> unassigned)
{
_eventService.PublishEvent<IEventJobsAssigned>(x => x.OnJobsAssigned(unassigned));
}
#endif
[HarmonyPatch(typeof(Character), nameof(Character.Create), new[] {
typeof(CharacterPrefab),
typeof(Vector2),
typeof(string),
typeof(CharacterInfo),
typeof(ushort),
typeof(bool),
typeof(bool),
typeof(bool),
typeof(RagdollParams),
typeof(bool)
}), HarmonyPostfix]
public static void Character_Create_Post(Character __result)
{
_eventService.PublishEvent<IEventCharacterCreated>(x => x.OnCharacterCreated(__result));
}
[HarmonyPatch(typeof(Character), "KillProjSpecific"), HarmonyPostfix]
public static void Character_Kill_Post(Character __instance, Affliction causeOfDeathAffliction, CauseOfDeathType causeOfDeath)
{
_eventService.PublishEvent<IEventCharacterDeath>(x => x.OnCharacterDeath(__instance, causeOfDeathAffliction, causeOfDeath));
}
[HarmonyPatch(typeof(Character), nameof(Character.GiveJobItems)), HarmonyPostfix]
public static void Character_GiveJobItems_Post(Character __instance, WayPoint spawnPoint, bool isPvPMode)
{
_eventService.PublishEvent<IEventGiveCharacterJobItems>(x => x.OnGiveCharacterJobItems(__instance, spawnPoint, isPvPMode));
}
[HarmonyPatch(typeof(Character), nameof(Character.DamageLimb)), HarmonyPrefix]
public static bool Character_DamageLimb_Pre(AttackResult __result, Character __instance, Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker, float damageMultiplier, bool allowStacking, float penetration, bool shouldImplode, bool ignoreDamageOverlay, bool recalculateVitality)
{
AttackResult? result = null;
_eventService.PublishEvent<IEventCharacterDamageLimb>(x => result = x.OnCharacterDamageLimb(__instance, worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier, allowStacking, penetration, shouldImplode));
if (result != null)
{
__result = (AttackResult)result;
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Affliction), nameof(Affliction.Update)), HarmonyPostfix]
public static void Affliction_Update_Post(Affliction __instance, CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
_eventService.PublishEvent<IEventAfflictionUpdate>(x => x.OnAfflictionUpdate(__instance, characterHealth, targetLimb, deltaTime));
}
[HarmonyPatch(typeof(Connection), nameof(Connection.SendSignal)), HarmonyPostfix]
public static void Connection_SendSignal_Post(Connection __instance, Signal signal)
{
foreach (var wire in __instance.Wires)
{
Connection recipient = wire.OtherConnection(__instance);
if (recipient == null) { continue; }
_eventService.PublishEvent<IEventSignalReceived>(x => x.OnSignalReceived(signal, recipient));
_eventService.Call("signalReceived." + recipient.Item.Prefab.Identifier, signal, recipient);
}
foreach (CircuitBoxConnection connection in __instance.CircuitBoxConnections)
{
_eventService.PublishEvent<IEventSignalReceived>(x => x.OnSignalReceived(signal, connection.Connection));
_eventService.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection.Connection);
}
}
[HarmonyPatch(typeof(Item), MethodType.Constructor, new Type[] { typeof(Rectangle), typeof(ItemPrefab), typeof(Submarine), typeof(bool), typeof(ushort) }), HarmonyPostfix]
public static void Item_Ctor_Post(Item __instance)
{
_eventService.PublishEvent<IEventItemCreated>(x => x.OnItemCreated(__instance));
}
[HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix]
public static void Item_Remove_Post(Item __instance)
{
_eventService.PublishEvent<IEventItemRemoved>(x => x.OnItemRemoved(__instance));
}
[HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix]
public static void Item_ShallowRemove_Post(Item __instance)
{
_eventService.PublishEvent<IEventItemRemoved>(x => x.OnItemRemoved(__instance));
}
[HarmonyPatch(typeof(Item), nameof(Item.Use)), HarmonyPrefix]
public static bool Item_Use_Pre(Item __instance, Character user, Limb targetLimb, Entity useTarget)
{
if (__instance.RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
{
return true;
}
if (__instance.Condition <= 0.0f) { return true; }
bool? result = null;
_eventService.PublishEvent<IEventItemUse>(x => result = x.OnItemUsed(__instance, user, targetLimb, useTarget));
if (result == true)
{
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Item), nameof(Item.SecondaryUse)), HarmonyPrefix]
public static bool Item_SecondaryUse_Pre(Item __instance, Character character)
{
if (__instance.Condition <= 0.0f) { return true; }
bool? result = null;
_eventService.PublishEvent<IEventItemSecondaryUse>(x => result = x.OnItemSecondaryUsed(__instance, character));
if (result == true)
{
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Inventory), "PutItem"), HarmonyPrefix]
public static bool Inventory_PutItem_Prefix(Inventory __instance, Item item, int i, Character user, bool removeItem)
{
bool? result = null;
_eventService.PublishEvent<IEventInventoryPutItem>(x => result = x.OnInventoryPutItem(__instance, item, user, i, removeItem));
if (result == true)
{
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Inventory), "TrySwapping"), HarmonyPrefix]
public static bool Inventory_TrySwapping_Prefix(Inventory __instance, Item item, int index, Character user, bool swapWholeStack, ref bool __result)
{
// uncomment when we are plugin
// if (item?.ParentInventory == null || !__instance.slots[index].Any()) { return false; }
// if (__instance.slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; }
if (!__instance.AllowSwappingContainedItems) { return false; }
bool? result = null;
_eventService.PublishEvent<IEventInventoryItemSwap>(x => result = x.OnInventoryItemSwap(__instance, item, user, index, swapWholeStack));
if (result != null)
{
__result = (bool)result;
return false; // skip
}
return true;
}
public void Dispose()
{
IsDisposed = true;
this.Harmony?.UnpatchSelf();
}
#if SERVER
[HarmonyPatch]
class Patch_StartGame_End
{
static MethodBase TargetMethod()
{
var original = AccessTools.Method(
typeof(GameServer),
"StartGame"
);
return AccessTools.EnumeratorMoveNext(original);
}
[HarmonyPostfix]
static void Postfix(object __instance, bool __result)
{
if (!__result) { return; }
var enumerator = __instance as IEnumerator<CoroutineStatus>;
if (enumerator == null) { return; }
if (enumerator.Current == CoroutineStatus.Success)
{
_eventService.PublishEvent<IEventRoundStarted>(x => x.OnRoundStart());
}
}
}
#endif
}
@@ -0,0 +1,218 @@
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using FluentResults;
using HarmonyLib;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Barotrauma.LuaCs;
public partial class LoggerService : ILoggerService
{
private List<ILoggerSubscriber> logSubscribers = [];
private ConcurrentQueue<PendingLog> logQueue = [];
#if SERVER
private const string TargetPrefix = "[SV]";
private const int NetMaxLength = 1024; // character limit of vanilla Barotrauma's chat system.
private const int NetMaxMessages = 60;
// This is used so it's possible to call logging functions inside the serverLog
// hook without creating an infinite loop
private bool _isInsideLogCall = false;
#else
private const string TargetPrefix = "[CL]";
#endif
public LoggerService() { }
public void Subscribe(ILoggerSubscriber subscriber)
{
logSubscribers.Add(subscriber);
}
public void Unsubscribe(ILoggerSubscriber subscriber)
{
logSubscribers.Remove(subscriber);
}
public void ProcessLogs()
{
while (logQueue.TryDequeue(out PendingLog log))
{
logSubscribers.ForEach(s => s.OnLog(log));
DebugConsole.NewMessage(log.Message, log.Color);
#if SERVER
if (GameMain.Server != null)
{
if (GameMain.Server.ServerSettings.SaveServerLogs)
{
string logMessage = "[LuaCs] " + log.Message;
GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, log.MessageType, false);
if (!_isInsideLogCall)
{
_isInsideLogCall = true;
LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(logMessage, log.MessageType));
_isInsideLogCall = false;
}
}
for (int i = 0; i < log.Message.Length; i += NetMaxLength)
{
string subStr = log.Message.Substring(i, Math.Min(1024, log.Message.Length - i));
BroadcastMessage(subStr);
}
}
void BroadcastMessage(string m)
{
foreach (var client in GameMain.Server.ConnectedClients)
{
ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: log.Color);
GameMain.Server.SendDirectChatMessage(consoleMessage, client);
if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog))
{
continue;
}
ChatMessage logMessage = ChatMessage.Create(log.MessageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null);
GameMain.Server.SendDirectChatMessage(logMessage, client);
}
}
#endif
}
}
public void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
{
if (LuaCsSetup.Instance.HideUserNamesInLogs && !Environment.UserName.IsNullOrEmpty())
{
message = message.Replace(Environment.UserName, "USERNAME");
}
message = $"{TargetPrefix} {message}";
logQueue.Enqueue(new PendingLog(message, color, messageType));
}
public void LogError(string message)
{
Log($"{message}", Color.Red, ServerLog.MessageType.Error);
}
public void LogWarning(string message)
{
Log($"{message}", Color.Yellow, ServerLog.MessageType.ServerMessage);
}
public void LogMessage(string message, Color? serverColor = null, Color? clientColor = null)
{
serverColor ??= Color.MediumPurple;
clientColor ??= Color.Purple;
#if SERVER
Log(message, serverColor);
#else
Log(message, clientColor);
#endif
}
public void HandleException(Exception exception, string prefix = null)
{
string errorString = "";
switch (exception)
{
case NetRuntimeException netRuntimeException:
if (netRuntimeException.DecoratedMessage == null)
{
errorString = $"{prefix ?? ""}{netRuntimeException.ToString()}";
}
else
{
// FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace...
errorString = $"{prefix ?? ""}{netRuntimeException.DecoratedMessage}: {netRuntimeException}";
}
break;
case InterpreterException interpreterException:
if (interpreterException.DecoratedMessage == null)
{
errorString = $"{prefix ?? ""}{interpreterException.ToString()}";
}
else
{
errorString = $"{prefix ?? ""}{interpreterException.DecoratedMessage}";
}
break;
default:
string s = exception.StackTrace != null ? exception.ToString() : $"{exception}\n{Environment.StackTrace}";
errorString = $"{prefix ?? ""}{s}";
break;
}
LogError(prefix + Environment.UserName + " " + errorString);
}
public void LogResults(FluentResults.Result result)
{
if (result == null)
{
LogError("Result is null");
return;
}
if (result.IsSuccess)
{
return;
}
if (result.IsFailed)
{
foreach (var error in result.Errors)
{
if (error is ExceptionalError exceptionalError)
{
HandleException(exceptionalError.Exception);
}
else
{
LogError($"FluentResults::IError: {error.Message}");
/*if (error.Reasons != null)
{
foreach (var reason in error.Reasons)
{
LogError($" - {reason.Message}");
}
}*/
}
}
}
}
public void LogDebug(string message, Color? color = null)
{
Log(message, color ?? Color.Purple);
}
public void LogDebugWarning(string message)
{
Log(message, Color.Yellow);
}
public void LogDebugError(string message)
{
Log(message, Color.Red);
}
public void Dispose() { }
public FluentResults.Result Reset() => FluentResults.Result.Ok();
public bool IsDisposed { get; }
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.LuaCs;
public sealed class LuaCsInfoProvider : ILuaCsInfoProvider
{
public void Dispose()
{
// stateless service
}
public bool IsDisposed => false;
public bool IsCsEnabled => LuaCsSetup.Instance.IsCsEnabled;
public bool HideUserNamesInLogs => LuaCsSetup.Instance.HideUserNamesInLogs;
public bool UseCaching => LuaCsSetup.Instance.UseCaching;
public RunState CurrentRunState => LuaCsSetup.Instance.CurrentRunState;
public ContentPackage LuaCsForBarotraumaPackage
{
get
{
return ContentPackageManager.EnabledPackages.Regular.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName), null)
?? ContentPackageManager.LocalPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName))
?? ContentPackageManager.WorkshopPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName));
}
}
}
@@ -0,0 +1,670 @@
#nullable enable
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using FluentResults;
using Microsoft.CodeAnalysis;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Barotrauma.LuaCs;
class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService, IEventAssemblyUnloading
{
public Script? InternalScript => _script;
private Script? _script;
private bool _isRunning;
[MemberNotNullWhen(true, nameof(_script))]
public bool IsRunning => _isRunning;
private List<ILuaScriptResourceInfo> _resourcesInfo = new List<ILuaScriptResourceInfo>();
private readonly AsyncReaderWriterLock _operationsLock = new ();
private readonly ILuaUserDataService _userDataService;
private readonly ISafeLuaUserDataService _safeUserDataService;
private readonly ILuaScriptLoader _luaScriptLoader;
private readonly ILuaScriptServicesConfig _luaScriptServicesConfig;
private readonly ILoggerService _loggerService;
private readonly LuaGame _luaGame;
private readonly IEventService _eventService;
private readonly ILuaCsTimer _luaCsTimer;
private readonly IDefaultLuaRegistrar _defaultLuaRegistrar;
private readonly IPluginManagementService _pluginManagementService;
private readonly INetworkingService _networkingService;
private readonly IConsoleCommandsService _commandsService;
private readonly ILuaConfigService _configService;
private readonly ILuaCsInfoProvider _luaCsInfoProvider;
private readonly Lazy<IPackageManagementService> _packageManagementService;
//private readonly ILuaCsUtility _luaCsUtility;
public LuaScriptManagementService(
ILoggerService loggerService,
ILuaScriptLoader loader,
ILuaUserDataService userDataService,
ISafeLuaUserDataService safeUserDataService,
IDefaultLuaRegistrar defaultLuaRegistrar,
ILuaScriptServicesConfig luaScriptServicesConfig,
IPluginManagementService pluginManagementService,
INetworkingService networkingService,
LuaGame luaGame,
IEventService eventService,
//ILuaCsUtility luaCsUtility,
ILuaCsTimer luaCsTimer,
IConsoleCommandsService commandsService,
ILuaCsInfoProvider luaCsInfoProvider,
ILuaConfigService configService,
Lazy<IPackageManagementService> packageManagementService)
{
_luaScriptLoader = loader;
_userDataService = userDataService;
_safeUserDataService = safeUserDataService;
_defaultLuaRegistrar = defaultLuaRegistrar;
_luaScriptServicesConfig = luaScriptServicesConfig;
_loggerService = loggerService;
_pluginManagementService = pluginManagementService;
_networkingService = networkingService;
_luaGame = luaGame;
_eventService = eventService;
_commandsService = commandsService;
_luaCsInfoProvider = luaCsInfoProvider;
_configService = configService;
_packageManagementService = packageManagementService;
_luaCsTimer = luaCsTimer;
RegisterLuaEvents();
RegisterConsoleCommands(_commandsService);
}
private void RegisterConsoleCommands(IConsoleCommandsService commands)
{
#if CLIENT
commands.RegisterCommand("cl_reloadlua|cl_reloadcs|cl_reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) =>
{
LuaCsSetup.Instance.EventService.PublishEvent<IEventReloadAllPackages>(sub => sub.OnReloadAllPackages());
});
commands.RegisterCommand("cl_lua", $"cl_lua: Runs a string on the client.", (string[] args) =>
{
if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands))
{
DebugConsole.ThrowError("Command not permitted.");
return;
}
if (LuaCsSetup.Instance.CurrentRunState != RunState.Running)
{
DebugConsole.ThrowError("LuaCs not initialized, use the console command cl_reloadluacs to force initialization.");
return;
}
var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args));
LuaCsSetup.Instance.Logger.LogResults(result.ToResult());
});
commands.RegisterCommand("cl_toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) =>
{
DebugConsole.Log($"This command is currently not implemented. Please open a github issue if you need this feature.");
/*int port = 41912;
if (args.Length > 0)
{
int.TryParse(args[0], out port);
}
throw new NotImplementedException();
//GameMain.LuaCs.ToggleDebugger(port);*/
});
#elif SERVER
commands.RegisterCommand("lua", "lua: Runs a string.", (string[] args) =>
{
var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args));
LuaCsSetup.Instance.Logger.LogResults(result.ToResult());
});
commands.RegisterCommand("reloadlua|reloadcs|reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) =>
{
LuaCsSetup.Instance.EventService.PublishEvent<IEventReloadAllPackages>(sub => sub.OnReloadAllPackages());
});
commands.RegisterCommand("toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) =>
{
int port = 41912;
if (args.Length > 0)
{
int.TryParse(args[0], out port);
}
throw new NotImplementedException();
//GameMain.LuaCs.ToggleDebugger(port);
});
#endif
#if SERVER
commands.RegisterCommand("install_cl_lua|install_cl|install_cl_cs|install_cl_luacs", "Installs Client-Side LuaCs into your client.", (string[] args) =>
{
LuaCsInstaller.Install();
});
#endif
}
public bool IsDisposed { get; private set; }
public void SetCachingPolicy(bool useCaching)
{
_luaScriptLoader?.SetCachingPolicy(useCaching);
}
public async Task<FluentResults.Result> LoadScriptResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourcesInfo)
{
if (!_luaCsInfoProvider.UseCaching)
{
return FluentResults.Result.Ok();
}
// Do any exception checks you can before acquiring a lock to avoid needlessly holding up resources.
if (resourcesInfo.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadScriptResourcesAsync)}: The parameter is empty!");
}
// Acquire a lock:
// Reader = Allow parallel operations (try to avoid nesting acquiring the lock when possible)
// Writer = Exclusive use (ie. executing scripts or Dispose())
using var lck = await _operationsLock.AcquireWriterLock(); // IDisposable using with generate a try-finally and release for you.
IService.CheckDisposed(this); // Check disposed after you have the lock
// If you use a ConcurrentDictionary instead of a List, it will handle threading issues for you.
_resourcesInfo.AddRange(resourcesInfo.OrderBy(static r => r.LoadPriority));
// Use the StorageService's caching function by just loading the file with caching turned on.
// Right now the LuaScriptLoader has this on by default.
var cacheRes = await _luaScriptLoader.CacheResourcesAsync(resourcesInfo);
// Aggregate and return results to the caller to deal with. Optionally, log here if you want.
// Automatically converted to a Task<T> when 'async' is in the method declaration.
if (cacheRes.IsFailed)
{
return cacheRes.ToResult();
}
return new FluentResults.Result().WithReasons(cacheRes.Value.SelectMany(cr => cr.Item2.Reasons));
}
public FluentResults.Result<DynValue> DoString(string code)
{
IService.CheckDisposed(this);
if (_script == null || !IsRunning) { throw new Exception("Disposed"); }
try
{
var result = _script.DoString(code);
return FluentResults.Result.Ok(result);
}
catch (Exception ex)
{
return FluentResults.Result.Fail(new ExceptionalError(ex));
}
}
private DynValue DoFile(string file, Table? globalContext = null, string? codeStringFriendly = null)
{
if (_script == null)
{
throw new Exception("Not running");
}
if (!LuaCsFile.CanReadFromPath(file))
{
// TODO: Replace with LuaScriptLoader IsFileAccessible.
throw new ScriptRuntimeException($"dofile: File access to {file} not allowed.");
}
if (!LuaCsFile.Exists(file))
{
// TODO: Replace with LuaScriptLoader IsFileAccessible.
throw new ScriptRuntimeException($"dofile: File {file} not found.");
}
return _script.DoFile(file, globalContext, codeStringFriendly);
}
private DynValue LoadFile(string file, Table? globalContext = null, string? codeStringFriendly = null)
{
if (_script == null)
{
throw new Exception("Not running");
}
if (!LuaCsFile.CanReadFromPath(file))
{
throw new ScriptRuntimeException($"loadfile: File access to {file} not allowed.");
}
if (!LuaCsFile.Exists(file))
{
throw new ScriptRuntimeException($"loadfile: File {file} not found.");
}
return _script.LoadFile(file, globalContext, codeStringFriendly);
}
private void RegisterLuaEvents()
{
_eventService.Subscribe<IEventAssemblyUnloading>(this);
_eventService.RegisterLuaEventAlias<IEventUpdate>("think", nameof(IEventUpdate.OnUpdate));
_eventService.RegisterLuaEventAlias<IEventKeyUpdate>("keyUpdate", nameof(IEventKeyUpdate.OnKeyUpdate));
_eventService.RegisterLuaEventAlias<IEventAfflictionUpdate>("afflictionUpdate", nameof(IEventAfflictionUpdate.OnAfflictionUpdate));
_eventService.RegisterLuaEventAlias<IEventCharacterCreated>("character.created", nameof(IEventCharacterCreated.OnCharacterCreated));
_eventService.RegisterLuaEventAlias<IEventCharacterDeath>("character.death", nameof(IEventCharacterDeath.OnCharacterDeath));
_eventService.RegisterLuaEventAlias<IEventCharacterDamageLimb>("character.damageLimb", nameof(IEventCharacterDamageLimb.OnCharacterDamageLimb));
_eventService.RegisterLuaEventAlias<IEventGiveCharacterJobItems>("character.giveJobItems", nameof(IEventGiveCharacterJobItems.OnGiveCharacterJobItems));
_eventService.RegisterLuaEventAlias<IEventHumanCPRSuccess>("character.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess));
_eventService.RegisterLuaEventAlias<IEventHumanCPRFailed>("character.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed));
_eventService.RegisterLuaEventAlias<IEventHumanCPRSuccess>("human.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess));
_eventService.RegisterLuaEventAlias<IEventHumanCPRFailed>("human.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed));
_eventService.RegisterLuaEventAlias<IEventCharacterApplyDamage>("character.applyDamage", nameof(IEventCharacterApplyDamage.OnCharacterApplyDamage));
_eventService.RegisterLuaEventAlias<IEventCharacterApplyAffliction>("character.applyAffliction", nameof(IEventCharacterApplyAffliction.OnCharacterApplyAffliction));
_eventService.RegisterLuaEventAlias<IEventGapOxygenUpdate>("gapOxygenUpdate", nameof(IEventGapOxygenUpdate.OnGapOxygenUpdate));
_eventService.RegisterLuaEventAlias<IEventClientControlHusk>("husk.clientControlHusk", nameof(IEventClientControlHusk.OnClientControlHusk));
_eventService.RegisterLuaEventAlias<IEventMeleeWeaponHandleImpact>("meleeWeapon.handleImpact", nameof(IEventMeleeWeaponHandleImpact.OnMeleeWeaponHandleImpact));
_eventService.RegisterLuaEventAlias<IEventServerLog>("serverLog", nameof(IEventServerLog.OnServerLog));
_eventService.RegisterLuaEventAlias<IEventTryClientChangeName>("tryChangeClientName", nameof(IEventTryClientChangeName.OnTryClienChangeName));
_eventService.RegisterLuaEventAlias<IEventChangeFallDamage>("changeFallDamage", nameof(IEventChangeFallDamage.OnChangeFallDamage));
_eventService.RegisterLuaEventAlias<IEventChatMessage>("chatMessage", nameof(IEventChatMessage.OnChatMessage));
_eventService.RegisterLuaEventAlias<IEventCanUseVoiceRadio>("canUseVoiceRadio", nameof(IEventCanUseVoiceRadio.OnCanUseVoiceRadio));
_eventService.RegisterLuaEventAlias<IEventChangeLocalVoiceRange>("changeLocalVoiceRange", nameof(IEventChangeLocalVoiceRange.OnChangeLocalVoiceRange));
_eventService.RegisterLuaEventAlias<IEventRoundStarted>("roundStart", nameof(IEventRoundStarted.OnRoundStart));
_eventService.RegisterLuaEventAlias<IEventRoundEnded>("roundEnd", nameof(IEventRoundEnded.OnRoundEnd));
_eventService.RegisterLuaEventAlias<IEventMissionsEnded>("missionsEnded", nameof(IEventMissionsEnded.OnMissionsEnded));
_eventService.RegisterLuaEventAlias<IEventSignalReceived>("signalReceived", nameof(IEventSignalReceived.OnSignalReceived));
_eventService.RegisterLuaEventAlias<IEventItemCreated>("item.created", nameof(IEventItemCreated.OnItemCreated));
_eventService.RegisterLuaEventAlias<IEventItemRemoved>("item.removed", nameof(IEventItemRemoved.OnItemRemoved));
_eventService.RegisterLuaEventAlias<IEventItemUse>("item.use", nameof(IEventItemUse.OnItemUsed));
_eventService.RegisterLuaEventAlias<IEventItemSecondaryUse>("item.secondaryUse", nameof(IEventItemSecondaryUse.OnItemSecondaryUsed));
_eventService.RegisterLuaEventAlias<IEventItemReadPropertyChange>("item.readPropertyChange", nameof(IEventItemReadPropertyChange.OnItemReadPropertyChange));
_eventService.RegisterLuaEventAlias<IEventItemDeconstructed>("item.deconstructed", nameof(IEventItemDeconstructed.OnItemDeconstructed));
_eventService.RegisterLuaEventAlias<IEventInventoryPutItem>("inventoryPutItem", nameof(IEventInventoryPutItem.OnInventoryPutItem));
_eventService.RegisterLuaEventAlias<IEventInventoryItemSwap>("inventoryItemSwap", nameof(IEventInventoryItemSwap.OnInventoryItemSwap));
// Compatibility
_eventService.RegisterLuaEventAlias<IEventCharacterCreated>("characterCreated", nameof(IEventCharacterCreated.OnCharacterCreated));
_eventService.RegisterLuaEventAlias<IEventCharacterDeath>("characterDeath", nameof(IEventCharacterDeath.OnCharacterDeath));
#if SERVER
_eventService.RegisterLuaEventAlias<IEventClientConnected>("client.connected", nameof(IEventClientConnected.OnClientConnected));
_eventService.RegisterLuaEventAlias<IEventClientDisconnected>("client.disconnected", nameof(IEventClientDisconnected.OnClientDisconnected));
_eventService.RegisterLuaEventAlias<IEventJobsAssigned>("jobsAssigned", nameof(IEventJobsAssigned.OnJobsAssigned));
_eventService.RegisterLuaEventAlias<IEventClientRawNetMessageReceived>("netMessageReceived", nameof(IEventClientRawNetMessageReceived.OnReceivedClientNetMessage));
// Compatibility
_eventService.RegisterLuaEventAlias<IEventClientConnected>("clientConnected", nameof(IEventClientConnected.OnClientConnected));
_eventService.RegisterLuaEventAlias<IEventClientDisconnected>("clientDisconnected", nameof(IEventClientDisconnected.OnClientDisconnected));
_eventService.RegisterLuaEventAlias<IEventModifyChatMessage>("modifyChatMessage", nameof(IEventModifyChatMessage.OnModifyMessagePredicate));
#elif CLIENT
_eventService.RegisterLuaEventAlias<IEventServerRawNetMessageReceived>("netMessageReceived", nameof(IEventServerRawNetMessageReceived.OnReceivedServerNetMessage));
#endif
}
private void SetupEnvironment(bool enableSandbox)
{
_script = new Script(CoreModules.Preset_SoftSandbox | CoreModules.Debug | CoreModules.IO | CoreModules.OS_System);
_script.Options.DebugPrint = (string msg) =>
{
_loggerService.LogMessage($"[Lua] {msg}");
};
SetCachingPolicy(_luaCsInfoProvider.UseCaching);
_script.Options.ScriptLoader = _luaScriptLoader;
_script.Options.CheckThreadAccess = false;
Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; };
UserData.RegisterType<ILuaCsHook.HookMethodType>();
UserData.RegisterType(typeof(LuaGame));
StandardUserDataDescriptor descriptor = (StandardUserDataDescriptor)UserData.RegisterType(typeof(EventService));
descriptor.AddDynValue("HookMethodType", UserData.CreateStatic<ILuaCsHook.HookMethodType>());
UserData.RegisterType(typeof(ILuaCsNetworking));
UserData.RegisterType(typeof(ILuaCsUtility));
UserData.RegisterType(typeof(ILuaCsTimer));
UserData.RegisterType(typeof(LuaCsFile));
UserData.RegisterType(typeof(ILuaScriptResourceInfo));
UserData.RegisterType(typeof(IResourceInfo));
UserData.RegisterType(typeof(IUserDataDescriptor));
UserData.RegisterType(typeof(INetworkingService));
UserData.RegisterType(typeof(ILuaConfigService));
UserData.RegisterType(typeof(ILoggerService));
UserData.RegisterType(typeof(ISettingBase));
UserData.RegisterType(typeof(IDataInfo));
Type[] settingBaseTypes = [
typeof(ISettingBase<bool>),
typeof(ISettingBase<string>),
typeof(ISettingBase<byte>),
typeof(ISettingBase<sbyte>),
typeof(ISettingBase<ushort>),
typeof(ISettingBase<short>),
typeof(ISettingBase<char>),
typeof(ISettingBase<uint>),
typeof(ISettingBase<int>),
typeof(ISettingBase<ulong>),
typeof(ISettingBase<long>),
typeof(ISettingBase<float>),
typeof(ISettingBase<double>),
typeof(ISettingRangeBase<float>),
typeof(ISettingRangeBase<int>),
typeof(ISettingList<string>),
typeof(ISettingList<byte>),
typeof(ISettingList<sbyte>),
typeof(ISettingList<ushort>),
typeof(ISettingList<short>),
typeof(ISettingList<char>),
typeof(ISettingList<uint>),
typeof(ISettingList<int>),
typeof(ISettingList<ulong>),
typeof(ISettingList<long>),
typeof(ISettingList<float>),
typeof(ISettingList<double>),
];
Dictionary<string, Dictionary<string, object>> settingsTable = [];
foreach (Type type in settingBaseTypes)
{
UserData.RegisterType(type);
string baseName = type.Name.RemoveFromEnd("`1").Substring(1);
if (!settingsTable.ContainsKey(baseName))
{
settingsTable[baseName] = new Dictionary<string, object>();
}
settingsTable[baseName][type.GetGenericArguments()[0].Name] = UserData.CreateStatic(type);
}
foreach (var keyPair in settingsTable)
{
_script.Globals[keyPair.Key] = keyPair.Value;
}
UserData.RegisterType(typeof(ISettingRangeBase<int>));
#if CLIENT
UserData.RegisterType(typeof(ISettingControl));
#endif
new LuaConverters(this).RegisterLuaConverters();
var luaRequire = new LuaRequire(_script);
_script.Globals["setmodulepaths"] = (string[] str) => ((LuaScriptLoader)_luaScriptLoader).ModulePaths = str;
_script.Globals["dofile"] = (Func<string, Table, string, DynValue>)DoFile;
_script.Globals["loadfile"] = (Func<string, Table, string, DynValue>)LoadFile;
_script.Globals["require"] = (Func<string, Table, DynValue>)luaRequire.Require;
_script.Globals["printerror"] = (DynValue o) => { _loggerService.LogError($"[Lua] {o.ToString()}"); };
_script.Globals["dostring"] = (Func<string, Table, string, DynValue>)_script.DoString;
_script.Globals["load"] = (Func<string, Table, string, DynValue>)_script.LoadString;
_script.Globals["Game"] = _luaGame;
_script.Globals["Hook"] = _eventService;
_script.Globals["Timer"] = _luaCsTimer;
_script.Globals["File"] = UserData.CreateStatic<LuaCsFile>();
_script.Globals["ConfigService"] = _configService;
_script.Globals["Networking"] = _networkingService;
_script.Globals["trygetpackage"] = (string name, out ContentPackage package) =>
_packageManagementService.Value.TryGetLoadedPackageByName(name, out package);
_script.Globals["Logger"] = _loggerService;
//_script.Globals["Steam"] = Steam;
if (enableSandbox)
{
UserData.RegisterType(typeof(SafeLuaUserDataService));
_script.Globals["LuaUserData"] = _safeUserDataService;
}
else
{
UserData.RegisterType(typeof(LuaUserDataService));
_script.Globals["LuaUserData"] = _userDataService;
}
Table eventsTable = new Table(_script);
var typesValue = _pluginManagementService.GetImplementingTypes<IEvent>(includeInterfaces: true, includeAbstractTypes: true);
if (typesValue.IsSuccess)
{
foreach (var eventType in typesValue.Value)
{
if (eventType.IsGenericType) { continue; }
if (!eventType.IsInterface) { continue; }
UserData.RegisterType(eventType);
eventsTable[eventType.Name] = UserData.CreateStatic(eventType);
}
}
_script.Globals["Events"] = eventsTable;
_script.Globals["ExecutionNumber"] = 0;
_script.Globals["CSActive"] = !enableSandbox;
((Table)_script.Globals["debug"])["breakpoint"] = () => { Debugger.Break(); };
_script.Globals["SERVER"] = LuaCsSetup.IsServer;
_script.Globals["CLIENT"] = LuaCsSetup.IsClient;
_defaultLuaRegistrar.RegisterAll();
}
public FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaScriptResourceInfo> executionOrder, bool enableSandbox)
{
if (_isRunning)
{
return FluentResults.Result.Fail("Tried to execute Lua scripts without unloading first.");
}
_loggerService.LogMessage("[Lua] Executing scripts");
SetupEnvironment(enableSandbox);
if (_script == null) { return FluentResults.Result.Ok(); } // never happens
var result = FluentResults.Result.Ok();
_isRunning = true;
var packages = executionOrder.Select(r => r.OwnerPackage)
.Distinct()
.Select(p => $"{p.Dir}/Lua/?.lua")
.ToArray();
((LuaScriptLoader)_luaScriptLoader).ModulePaths = packages;
Table package = (Table)_script.Globals["package"];
package.Set("path", DynValue.FromObject(_script, packages));
#if CLIENT
if (GameMain.NetworkMember is { IsClient: true })
{
var startMessage = _networkingService.Start("_luastart");
var packagesToReport = ContentPackageManager.EnabledPackages.All
.Where(p => _packageManagementService.Value.PackageContainsAnyRunnableResource(p))
.Where(p => !p.NameMatches(LuaCsSetup.PackageName))
.ToList();
startMessage.WriteUInt16((UInt16)packagesToReport.Count());
foreach (var enabledPackage in packagesToReport)
{
var id = enabledPackage.UgcId;
string hash = enabledPackage.Hash.StringRepresentation ?? "";
startMessage.WriteString(enabledPackage.Name);
startMessage.WriteString(enabledPackage.ModVersion);
if (id.TryUnwrap(out ContentPackageId? packageId) && packageId is SteamWorkshopId steamId)
{
startMessage.WriteUInt64(steamId.Value);
}
else
{
startMessage.WriteUInt64(0);
}
startMessage.WriteString(hash);
}
_networkingService.Send(startMessage);
}
#elif SERVER
_networkingService.Receive("_luastart", (message, client) =>
{
var num = message.ReadUInt16();
List<Table> packages = new List<Table>();
for (int i = 0; i < num; i++)
{
Table table = new Table(_script);
table.Set("Name", DynValue.NewString(message.ReadString()));
table.Set("Version", DynValue.NewString(message.ReadString()));
table.Set("Id", DynValue.NewString(message.ReadUInt64().ToString()));
table.Set("Hash", DynValue.NewString(message.ReadString()));
packages.Add(table);
}
_eventService.Call("client.packages", client, packages);
});
#endif
foreach (ILuaScriptResourceInfo resource in executionOrder.Where(l => l.IsAutorun))
{
foreach (ContentPath filePath in resource.FilePaths)
{
try
{
_loggerService.LogMessage($"[Lua] - Run {filePath.Value}");
_script.Call(_script.LoadFile(filePath.FullPath), resource.OwnerPackage.Dir);
}
catch(Exception e)
{
result = result.WithError(new ExceptionalError(e));
}
}
}
_eventService.Call("loaded");
return result;
}
public DynValue? CallFunctionSafe(object luaFunction, params object[] args)
{
if (!IsRunning) { return null; }
lock (_script)
{
try
{
return _script.Call(luaFunction, args);
}
catch (Exception e)
{
_loggerService.HandleException(e);
}
return null;
}
}
public FluentResults.Result UnloadActiveScripts()
{
_isRunning = false;
_script = null;
return FluentResults.Result.Ok();
}
public FluentResults.Result DisposePackageResources(ContentPackage package)
{
return FluentResults.Result.Ok();
}
public FluentResults.Result DisposeAllPackageResources()
{
if (IsRunning)
{
UnloadActiveScripts();
}
_resourcesInfo.Clear();
_luaScriptLoader.ClearCaches();
return FluentResults.Result.Ok();
}
public FluentResults.Result Reset()
{
IService.CheckDisposed(this);
_luaScriptLoader.ClearCaches();
_userDataService.Reset();
_luaCsTimer.Reset();
RegisterLuaEvents();
return DisposeAllPackageResources();
}
public void Dispose()
{
IsDisposed = true;
_userDataService.Dispose();
_luaScriptLoader.Dispose();
_commandsService.Dispose();
}
public object? GetGlobalTableValue(string tableName)
{
if (!IsRunning) { return null; }
return _script.Globals[tableName];
}
public void OnAssemblyUnloading(Assembly assembly)
{
foreach (Type type in assembly.SafeGetTypes())
{
UserData.UnregisterType(type, deleteHistory: true);
}
}
}
@@ -0,0 +1,94 @@
using Barotrauma;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Events;
using FluentResults;
using HarmonyLib;
using Microsoft.Xna.Framework;
[HarmonyPatch]
internal class MainMenuPatch : ISystem, IEventScreenSelected
{
public bool IsDisposed { get; private set; }
private readonly IEventService _eventService;
private bool mainMenuUIAdded = false;
public MainMenuPatch(IEventService eventService)
{
_eventService = eventService;
RegisterEvents();
#if CLIENT
if (Screen.Selected is MainMenuScreen mainMenuScreen)
{
AddToMainMenu(mainMenuScreen);
}
#endif
}
public void OnScreenSelected(Screen screen)
{
#if CLIENT
if (screen is MainMenuScreen mainMenuScreen)
{
AddToMainMenu(mainMenuScreen);
}
#endif
}
#if CLIENT
private void AddToMainMenu(MainMenuScreen screen)
{
if (mainMenuUIAdded) { return; }
var textBlock = new GUITextBlock(new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, "", Color.Red)
{
IgnoreLayoutGroups = false
};
textBlock.OnAddedToGUIUpdateList = (GUIComponent component) =>
{
string mode = LuaCsSetup.Instance.CsRunPolicyValue;
if (mode is "Prompt")
{
string sessionState = LuaCsSetup.Instance.IsCsEnabledForSession ? "yes" : "no";
mode = $"enabled (prompt mode, allowed for this session: {sessionState})";
}
else if (mode is "Enabled")
{
mode = "always enabled";
}
else
{
mode = "disabled";
}
textBlock.Text = $"LuaCsForBarotrauma active (revision {AssemblyInfo.GitRevision}), C# is currently {mode}\nNew settings available in the game settings menu.";
};
mainMenuUIAdded = true;
}
#endif
private void RegisterEvents()
{
_eventService.Subscribe<IEventScreenSelected>(this);
}
public void Dispose()
{
_eventService.Unsubscribe<IEventScreenSelected>(this);
IsDisposed = true;
}
public FluentResults.Result Reset()
{
RegisterEvents();
return FluentResults.Result.Ok();
}
}
@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FarseerPhysics.Common;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public sealed partial class ModConfigFileParserService :
IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>,
IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>,
IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>
{
private IStorageService _storageService;
private readonly AsyncReaderWriterLock _operationsLock = new();
public ModConfigFileParserService(IStorageService storageService)
{
_storageService = storageService;
}
#region Dispose
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
try
{
_storageService.Dispose();
this._storageService = null;
}
catch
{
// ignored
}
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#endregion
// --- Assemblies
async Task<Result<IAssemblyResourceInfo>> IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
{
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (CheckThrowNullRefs(src, "Assembly") is { IsFailed: true } fail)
return fail;
var isScript = src.Element.GetAttributeBool("IsScript", false);
var runtimeEnv = GetRuntimeEnvironment(src.Element);
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, isScript ? ".cs" : ".dll");
if (fileResults.IsFailed)
return FluentResults.Result.Fail(fileResults.Errors);
return new AssemblyResourceInfo()
{
SupportedPlatforms = runtimeEnv.Platform,
SupportedTargets = runtimeEnv.Target,
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
FilePaths = fileResults.Value,
Optional = src.Element.GetAttributeBool("Optional", false),
InternalName = src.Element.GetAttributeString("Name", string.Empty),
OwnerPackage = src.Owner,
RequiredPackages = src.Required,
IncompatiblePackages = src.Incompatible,
// Type Specific
FriendlyName = src.Element.GetAttributeString("FriendlyName", GetFallbackCompliantAssemblyName(src.Owner)),
IsScript = isScript,
UseInternalAccessName = src.Element.GetAttributeBool("UseInternalAccessName", false),
IsReferenceModeOnly = src.Element.GetAttributeBool("IsReferenceModeOnly", false)
};
// helper methods
string GetFallbackCompliantAssemblyName(ContentPackage package)
{
if (package.Name.IsNullOrWhiteSpace())
{
return "FallbackAssemblyName";
}
// replace non az chars with '_'
var sanitizedPackageName = Regex.Replace(package.Name, @"[^a-zA-Z0-9_]", "_");
if (char.IsDigit(sanitizedPackageName[0]))
{
sanitizedPackageName = "ASM" + sanitizedPackageName;
}
// replace consecutive '_'
return Regex.Replace(sanitizedPackageName, @"[_.]{2,}", "_");
}
}
async Task<ImmutableArray<Result<IAssemblyResourceInfo>>> IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
{
return await this.TryParseGenericResourcesAsync<IAssemblyResourceInfo>(sources);
}
// --- Config
async Task<Result<IConfigResourceInfo>> IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
{
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (CheckThrowNullRefs(src, "Config") is { IsFailed: true } fail)
return fail;
var runtimeEnv = GetRuntimeEnvironment(src.Element);
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".xml");
if (fileResults.IsFailed)
return FluentResults.Result.Fail(fileResults.Errors);
return new ConfigResourceInfo()
{
SupportedPlatforms = runtimeEnv.Platform,
SupportedTargets = runtimeEnv.Target,
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
FilePaths = fileResults.Value,
Optional = src.Element.GetAttributeBool("Optional", false),
InternalName = src.Element.GetAttributeString("Name", string.Empty),
OwnerPackage = src.Owner,
RequiredPackages = src.Required,
IncompatiblePackages = src.Incompatible
};
}
async Task<ImmutableArray<Result<IConfigResourceInfo>>> IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
{
return await this.TryParseGenericResourcesAsync<IConfigResourceInfo>(sources);
}
// --- Lua Scripts
async Task<Result<ILuaScriptResourceInfo>> IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
{
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (CheckThrowNullRefs(src, "Lua") is { IsFailed: true } fail)
return fail;
var runtimeEnv = GetRuntimeEnvironment(src.Element);
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".lua");
if (fileResults.IsFailed)
return FluentResults.Result.Fail(fileResults.Errors);
return new LuaScriptsResourceInfo()
{
SupportedPlatforms = runtimeEnv.Platform,
SupportedTargets = runtimeEnv.Target,
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
FilePaths = fileResults.Value,
Optional = src.Element.GetAttributeBool("Optional", false),
InternalName = src.Element.GetAttributeString("Name", string.Empty),
OwnerPackage = src.Owner,
RequiredPackages = src.Required,
IncompatiblePackages = src.Incompatible,
// Type Specific
IsAutorun = src.Element.GetAttributeBool("IsAutorun", false),
RunUnrestricted = src.Element.GetAttributeBool("RunUnrestricted", false)
};
}
private FluentResults.Result CheckThrowNullRefs(ResourceParserInfo src, string elementName)
{
Guard.IsNotNull(src, nameof(src));
Guard.IsNotNull(src.Owner, nameof(src.Owner));
Guard.IsNotNull(src.Element, nameof(src.Element));
if (src.Element.Name != elementName)
{
return FluentResults.Result.Fail($"Element name '{elementName}' is incorrect");
}
return FluentResults.Result.Ok();
}
async Task<ImmutableArray<Result<ILuaScriptResourceInfo>>> IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
{
return await this.TryParseGenericResourcesAsync<ILuaScriptResourceInfo>(sources);
}
// --- Helpers
private async Task<Result<ImmutableArray<ContentPath>>> UnsafeGetCheckedFiles(XElement srcElement, ContentPackage srcOwner, string fileExtension)
{
var builder = ImmutableArray.CreateBuilder<ContentPath>();
var filePath = srcElement.GetAttributeContentPath("File", srcOwner);
var folderPath = srcElement.GetAttributeContentPath("Folder", srcOwner);
var res = new FluentResults.Result<ImmutableArray<ContentPath>>();
if ((!filePath?.Value.IsNullOrWhiteSpace()) ?? false)
{
if (_storageService.FileExists(filePath.FullPath) is { IsSuccess: true, Value: true })
{
builder.Add(filePath);
}
else
{
if (srcElement.GetAttributeBool("IsFileRequired", true))
{
res.WithError($"{srcOwner.Name}: The file '{filePath}' is missing!");
}
else
{
res.WithSuccess($"Skipped missing not-required file: '{filePath}'");
}
}
}
if ((!folderPath?.Value.IsNullOrWhiteSpace()) ?? false)
{
if (_storageService.DirectoryExists(folderPath.FullPath) is { IsSuccess: true, Value: true })
{
var searchLocation = System.IO.Path.GetRelativePath(srcOwner.Dir, folderPath.Value);
var files = _storageService.FindFilesInPackage(srcOwner, searchLocation, "*"+fileExtension, true);
if (files.IsFailed)
{
res.WithError($"{srcOwner.Name}: Failed to load files from {folderPath}!");
}
else
{
foreach (var file in files.Value)
{
builder.Add(ContentPath.FromRaw(srcOwner, $"%ModDir%/{System.IO.Path.GetRelativePath(System.IO.Path.GetFullPath(srcOwner.Dir), file)}"));
}
}
}
else
{
if (srcElement.GetAttributeBool("IsFileRequired", true))
{
res.WithError($"{srcOwner.Name}: The file '{folderPath}' is missing!");
}
else
{
res.WithSuccess($"Skipped missing not-required folder: '{folderPath}'");
}
}
}
return res.WithValue(builder.ToImmutable());
}
private (Platform Platform, Target Target) GetRuntimeEnvironment(XElement element)
{
return (
Platform: element.GetAttributeEnum("Platform", Platform.Any),
Target: element.GetAttributeEnum("Target", Target.Any));
}
private async Task<ImmutableArray<Result<T>>> TryParseGenericResourcesAsync<T>(IEnumerable<ResourceParserInfo> sources)
{
// ReSharper disable once PossibleMultipleEnumeration
Guard.IsNotNull(sources, nameof(IParserServiceAsync<ResourceParserInfo, T>.TryParseResourcesAsync));
var builder = ImmutableArray.CreateBuilder<Result<T>>();
foreach (var info in sources)
{
builder.Add(await Unsafe.As<IParserServiceAsync<ResourceParserInfo, T>>(this).TryParseResourceAsync(info));
}
return builder.ToImmutable();
}
}
@@ -0,0 +1,430 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.VsCodeDebugger.SDK;
namespace Barotrauma.LuaCs;
public sealed class ModConfigService : IModConfigService
{
private IStorageService _storageService;
private ILoggerService _logger;
private IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo> _assemblyParserService;
private IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo> _luaScriptParserService;
private IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo> _configParserService;
#if CLIENT
private IParserServiceAsync<ResourceParserInfo, IStylesResourceInfo> _stylesParserService;
#endif
private readonly AsyncReaderWriterLock _operationsLock = new();
public ModConfigService(IStorageService storageService,
IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo> assemblyParserService,
IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo> luaScriptParserService,
IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo> configParserService,
#if CLIENT
IParserServiceAsync<ResourceParserInfo, IStylesResourceInfo> stylesParserService,
#endif
ILoggerService logger)
{
_storageService = storageService;
_assemblyParserService = assemblyParserService;
_luaScriptParserService = luaScriptParserService;
_configParserService = configParserService;
_logger = logger;
#if CLIENT
_stylesParserService = stylesParserService;
#endif
}
#region Dispose
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
try
{
_storageService.Dispose();
_logger.Dispose();
_assemblyParserService.Dispose();
_luaScriptParserService.Dispose();
_configParserService.Dispose();
_storageService = null;
_logger = null;
_assemblyParserService = null;
_luaScriptParserService = null;
_configParserService = null;
#if CLIENT
_stylesParserService.Dispose();
_stylesParserService = null;
#endif
}
catch
{
// ignored
}
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#endregion
public async Task<Result<IModConfigInfo>> CreateConfigAsync(ContentPackage src)
{
Guard.IsNotNull(src, nameof(src));
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config })
{
return await CreateFromConfigXmlAsync(src, config);
}
return await CreateFromLegacyAsync(src);
}
public async Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src)
{
if (src.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException($"{nameof(CreateConfigsAsync)}: The supplied array is default or empty!");
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
var builder = ImmutableArray.CreateBuilder<Task<Task<Result<IModConfigInfo>>>>(src.Length);
foreach (var srcItem in src)
{
builder.Add(Task.Factory.StartNew(async Task<Result<IModConfigInfo>> () => await CreateConfigAsync(srcItem)));
}
var taskResults = await Task.WhenAll(builder.ToImmutable());
var returnResults = ImmutableArray.CreateBuilder<(ContentPackage Source, Result<IModConfigInfo> Config)>();
foreach (var taskResult in taskResults)
{
if (taskResult.IsFaulted)
{
ThrowHelper.ThrowInvalidOperationException($"{nameof(CreateConfigsAsync)}: Task failed: {taskResult.Exception?.Message}");
}
var r = await taskResult;
returnResults.Add((r.Value.Package, r));
}
return returnResults.ToImmutable();
}
//--- Helpers
private async Task<Result<XElement>> TryGetModConfigXmlAsync(ContentPackage src)
{
return await _storageService.LoadPackageXmlAsync(ContentPath.FromRaw(src, "%ModDir%/ModConfig.xml")) is { IsSuccess: true, Value: { Root: {} config} }
? FluentResults.Result.Ok(config)
: FluentResults.Result.Fail<XElement>("ModConfig.xml not found");
}
private async Task<Result<IModConfigInfo>> CreateFromConfigXmlAsync(ContentPackage owner, XElement src)
{
var asmTask = Task.Factory.StartNew(async () => await GetAssembliesFromXml(owner, src));
var cfgTask = Task.Factory.StartNew(async () => await GetConfigsFromXml(owner, src));
var luaTask = Task.Factory.StartNew(async () => await GetLuaScriptsFromXml(owner, src));
#if CLIENT
var styleTask = Task.Factory.StartNew(async () => await GetStylesFromXml(owner, src));
#endif
await Task.WhenAll(
asmTask,
cfgTask,
#if CLIENT
styleTask,
#endif
luaTask);
return FluentResults.Result.Ok<IModConfigInfo>(new ModConfigInfo()
{
Package = owner,
Assemblies = await await asmTask,
Configs = await await cfgTask,
#if CLIENT
Styles = await await styleTask,
#endif
LuaScripts = await await luaTask
});
async Task<ImmutableArray<ILuaScriptResourceInfo>> GetLuaScriptsFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<ILuaScriptResourceInfo>(contentPackage, cfgElement, "Lua", "FileGroup", _luaScriptParserService);
}
async Task<ImmutableArray<IConfigResourceInfo>> GetConfigsFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<IConfigResourceInfo>(contentPackage, cfgElement, "Config", "FileGroup", _configParserService);
}
async Task<ImmutableArray<IAssemblyResourceInfo>> GetAssembliesFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<IAssemblyResourceInfo>(contentPackage, cfgElement, "Assembly", "FileGroup", _assemblyParserService);
}
#if CLIENT
async Task<ImmutableArray<IStylesResourceInfo>> GetStylesFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<IStylesResourceInfo>(contentPackage, cfgElement, "Style", "FileGroup", _stylesParserService);
}
#endif
async Task<ImmutableArray<T>> GetResourceFromXml<T>(ContentPackage contentPackage, XElement cfgElement, string elemName, string fileGroupName, IParserServiceAsync<ResourceParserInfo, T> resourceService)
{
var elems = GetResourceElementsWithName(owner, cfgElement, elemName, fileGroupName);
if (elems.IsDefaultOrEmpty)
return ImmutableArray<T>.Empty;
var results = await resourceService.TryParseResourcesAsync(elems);
Guard.IsNotEmpty((IReadOnlyCollection<Result<T>>)results, nameof(results));
var resources = ImmutableArray.CreateBuilder<T>();
foreach (var result in results)
{
if (result.Errors.Count > 0)
{
_logger.LogResults(result.ToResult());
continue;
}
resources.Add(result.Value);
}
return resources.ToImmutable();
}
ImmutableArray<ResourceParserInfo> GetResourceElementsWithName(ContentPackage package, XElement root, string elemName, string groupName)
{
var elems = ImmutableArray.CreateBuilder<ResourceParserInfo>();
elems.AddRange(root.GetChildElements(elemName)
.Select(e => new ResourceParserInfo(package, e, ImmutableArray<Identifier>.Empty, ImmutableArray<Identifier>.Empty))
.ToImmutableArray());
if (root.GetChildElements(groupName).ToImmutableArray() is { IsDefaultOrEmpty: false } fileGroups)
{
foreach (var fileGroup in fileGroups)
{
if (fileGroup.GetChildElements(elemName).ToImmutableArray() is { IsDefaultOrEmpty: false } subLuaElems)
{
var cond = GetDependencyIdentifiers(fileGroup, true);
var negCond = GetDependencyIdentifiers(fileGroup, false);
foreach (var element in subLuaElems)
{
elems.Add(new ResourceParserInfo(package, element, cond, negCond));
}
}
}
}
return elems.ToImmutable();
}
ImmutableArray<Identifier> GetDependencyIdentifiers(XElement fg, bool depsLoadedSetting)
{
return fg.GetChildElements("Conditional")
.Where(cElem => bool.TryParse(cElem.GetAttribute("IsLoaded").Value, out bool isLoaded) && isLoaded == depsLoadedSetting)
.SelectMany(cElem2 => cElem2.GetAttributeString("Dependencies", String.Empty)
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.Select(ident => new Identifier(ident)))
.ToImmutableArray();
}
}
private async Task<Result<IModConfigInfo>> CreateFromLegacyAsync(ContentPackage src)
{
return new ModConfigInfo()
{
Package = src,
Assemblies = GetAssembliesLegacy(src),
Configs = GetConfigsLegacy(src),
LuaScripts = GetLuaScriptsLegacy(src)
};
ImmutableArray<IAssemblyResourceInfo> GetAssembliesLegacy(ContentPackage srcPackage)
{
var binSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[]
{
("bin/Client/Windows", Target.Client, Platform.Windows),
("bin/Client/Linux", Target.Client, Platform.Linux),
("bin/Client/OSX", Target.Client, Platform.OSX),
("bin/Server/Windows", Target.Server, Platform.Windows),
("bin/Server/Linux", Target.Server, Platform.Linux),
("bin/Server/OSX", Target.Server, Platform.OSX)
};
var builder = ImmutableArray.CreateBuilder<IAssemblyResourceInfo>();
foreach (var searchPathways in binSearchInd)
{
if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.dll",
true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
{
builder.Add(new AssemblyResourceInfo()
{
OwnerPackage = srcPackage,
InternalName = searchPathways.SubFolder,
SupportedPlatforms = searchPathways.Platforms,
SupportedTargets = searchPathways.Targets,
LoadPriority = 0,
FilePaths = result.Value.Select(fp => ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray(),
FriendlyName = $"{srcPackage.Name}.{searchPathways.SubFolder.Replace('/','.')}",
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
IsScript = false,
IsReferenceModeOnly = false
});
}
}
var sharedResult = _storageService.FindFilesInPackage(srcPackage,
Path.Combine("CSharp/Shared"),
"*.cs", true);
var sharedFiles = sharedResult.IsSuccess && !sharedResult.Value.IsDefaultOrEmpty
? sharedResult.Value.Select(fp =>
ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray()
: ImmutableArray<ContentPath>.Empty;
var srcSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[]
{
("CSharp/Client", Target.Client, Platform.Any),
("CSharp/Server", Target.Server, Platform.Any)
};
foreach (var searchPathways in srcSearchInd)
{
// we have architecture dependent files as well
if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.cs",
true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
{
builder.Add(new AssemblyResourceInfo()
{
OwnerPackage = srcPackage,
InternalName = searchPathways.SubFolder,
SupportedPlatforms = searchPathways.Platforms,
SupportedTargets = searchPathways.Targets,
LoadPriority = 0,
FilePaths = result.Value
.Select(fp => ContentPath.FromRaw(srcPackage,
$"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
.Concat(sharedFiles).ToImmutableArray(),
FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName, // give the best chance of success (InternalsAware + Publicizer)
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
UseInternalAccessName = false, //compile as public and then fallback to internals
IsScript = true,
IsReferenceModeOnly = false
});
}
// add the shared files by themselves
else if (!sharedFiles.IsDefaultOrEmpty)
{
builder.Add(new AssemblyResourceInfo()
{
OwnerPackage = srcPackage,
InternalName = searchPathways.SubFolder,
SupportedPlatforms = searchPathways.Platforms,
SupportedTargets = searchPathways.Targets,
LoadPriority = 0,
FilePaths = sharedFiles,
FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName,
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
UseInternalAccessName = false,
IsScript = true,
IsReferenceModeOnly = false
});
}
}
return builder.ToImmutable();
}
ImmutableArray<IConfigResourceInfo> GetConfigsLegacy(ContentPackage src)
{
return ImmutableArray<IConfigResourceInfo>.Empty;
}
ImmutableArray<ILuaScriptResourceInfo> GetLuaScriptsLegacy(ContentPackage src)
{
var builder = ImmutableArray.CreateBuilder<ILuaScriptResourceInfo>();
if (_storageService.FindFilesInPackage(src, "Lua", "*.lua", true)
is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
{
ImmutableArray<string> cleanedResult = result.Value.Select(fp => fp.CleanUpPathCrossPlatform()).ToImmutableArray();
ImmutableArray<string> autorun = cleanedResult
.Where(fp => fp.Contains("Lua/ForcedAutorun/") || fp.Contains("Lua/Autorun/"))
.ToImmutableArray();
ImmutableArray<ContentPath> autorunFP = autorun.Select(fp => ContentPath.FromRaw(src,
$"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray();
ImmutableArray<ContentPath> reg = cleanedResult.Except(autorun)
.Select(fp => ContentPath.FromRaw(src,
$"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray();
builder.Add(new LuaScriptsResourceInfo()
{
OwnerPackage = src,
InternalName = "LegacyAutorun",
SupportedPlatforms = Platform.Any,
SupportedTargets = Target.Any,
LoadPriority = 1, // autorun should be last to ensure that dependent code in other files are loaded first
FilePaths = autorunFP,
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
IsAutorun = true,
RunUnrestricted = false
});
builder.Add(new LuaScriptsResourceInfo()
{
OwnerPackage = src,
InternalName = "Legacy",
SupportedPlatforms = Platform.Any,
SupportedTargets = Target.Any,
LoadPriority = 0, // should be included first to ensure that dependent code in these files are available
FilePaths = reg,
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
IsAutorun = false,
RunUnrestricted = false
});
}
return builder.ToImmutable();
}
}
}
@@ -0,0 +1,421 @@
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using FluentResults;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs;
internal partial class NetworkingService : INetworkingService, IEventSettingInstanceLifetime
{
public readonly record struct NetId
{
private readonly string _value;
public NetId(string netId)
{
_value = netId;
}
public static void Write(IWriteMessage message, NetId netId)
{
message.WriteString(netId._value);
}
public static NetId Read(IReadMessage message)
{
return new NetId(message.ReadString());
}
}
private enum ClientToServer
{
NetMessageInternalId,
NetMessageNetId,
RequestSingleNetId,
RequestSync,
}
private enum ServerToClient
{
NetMessageInternalId,
NetMessageNetId,
ReceiveNetIds
}
private ClientPacketHeader? clientHeader = null;
public ClientPacketHeader ClientHeader
{
get
{
if (clientHeader == null)
{
byte lastHeader = (byte)Enum.GetValues(typeof(ClientPacketHeader)).Cast<ClientPacketHeader>().Last();
clientHeader = (ClientPacketHeader)(lastHeader + 1);
}
return (ClientPacketHeader)clientHeader;
}
}
private ServerPacketHeader? serverHeader = null;
public ServerPacketHeader ServerHeader
{
get
{
if (serverHeader == null)
{
byte lastHeader = (byte)Enum.GetValues(typeof(ServerPacketHeader)).Cast<ServerPacketHeader>().Last();
serverHeader = (ServerPacketHeader)(lastHeader + 1);
}
return (ServerPacketHeader)serverHeader;
}
}
private ConcurrentDictionary<INetworkSyncVar, NetId> netVars = [];
private ConcurrentDictionary<NetId, NetMessageReceived> netReceives = [];
private ConcurrentDictionary<ushort, NetId> packetToId = [];
private ConcurrentDictionary<NetId, ushort> idToPacket = [];
public bool IsActive
{
get
{
return GameMain.NetworkMember != null;
}
}
public bool IsSynchronized { get; private set; }
public bool IsDisposed { get; private set; }
private readonly IEventService _eventService;
private readonly ILoggerService _loggerService;
private readonly INetworkIdProvider _networkIdProvider;
public NetworkingService(IEventService eventService, INetworkIdProvider networkIdProvider, ILoggerService loggerService)
{
_eventService = eventService;
_networkIdProvider = networkIdProvider;
_loggerService = loggerService;
#if SERVER
IsSynchronized = true;
#endif
SubscribeToEvents();
}
public void Receive(string netIdString, LuaCsAction callback)
{
#if SERVER
Receive(new NetId(netIdString), (IReadMessage message, Client client) => callback(message, client));
#elif CLIENT
Receive(new NetId(netIdString), (IReadMessage message) => callback(message, null));
#endif
}
public void Receive(string netIdString, NetMessageReceived callback) => Receive(new NetId(netIdString), callback);
public void Receive(Guid netIdGuid, NetMessageReceived callback) => Receive(new NetId(netIdGuid.ToString()), callback);
public IWriteMessage Start(string netIdString)
{
if (netIdString == null)
{
// idk why but Lua calls this method with null instead of the Start method with no arguments
return new WriteOnlyMessage();
}
return Start(new NetId(netIdString));
}
public IWriteMessage Start(Guid netIdGuid) => Start(new NetId(netIdGuid.ToString()));
public IWriteMessage Start() => new WriteOnlyMessage();
internal void Receive(NetId netId, NetMessageReceived callback)
{
#if SERVER
RegisterId(netId);
#elif CLIENT
RequestId(netId);
#endif
netReceives[netId] = callback;
}
private void HandleNetMessage(IReadMessage netMessage, NetId netId, Client client = null)
{
if (netReceives.ContainsKey(netId))
{
try
{
#if CLIENT
netReceives[netId](netMessage);
#elif SERVER
netReceives[netId](netMessage, client);
#endif
}
catch (Exception e)
{
_loggerService.LogResults(new ExceptionalError("Exception thrown inside NetMessageReceive({netId})", e));
}
}
else
{
if (GameSettings.CurrentConfig.VerboseLogging)
{
#if SERVER
_loggerService.LogError($"Received NetMessage for unknown netid {netId} from {GameServer.ClientLogName(client)}.");
#else
_loggerService.LogError($"Received NetMessage for unknown netid {netId} from server.");
#endif
}
}
}
private void HandleNetMessageString(IReadMessage netMessage, Client client = null)
{
NetId netId = NetId.Read(netMessage);
HandleNetMessage(netMessage, netId, client);
}
private void SubscribeToEvents()
{
_eventService.Subscribe<IEventSettingInstanceLifetime>(this);
#if CLIENT
_eventService.Subscribe<IEventServerConnected>(this);
_eventService.Subscribe<IEventServerRawNetMessageReceived>(this);
#elif SERVER
_eventService.Subscribe<IEventClientRawNetMessageReceived>(this);
#endif
}
public Guid GetNetworkIdForInstance(INetworkSyncVar var)
{
return _networkIdProvider.GetNetworkIdForInstance(var);
}
public void RegisterNetVar(INetworkSyncVar netVar)
{
netVar.SetNetworkOwner(this);
NetId netId = new NetId(netVar.InstanceId.ToString());
netVars[netVar] = netId;
#if CLIENT
Receive(netId, (IReadMessage message) =>
{
if (netVar.SyncType == NetSync.None)
{
_loggerService.LogWarning($"Received net var from server but {nameof(NetSync)} is {netVar.SyncType.ToString()}");
return;
}
netVar.ReadNetMessage(message);
});
#elif SERVER
Receive(netId, (IReadMessage message, Client client) =>
{
if (netVar.SyncType == NetSync.None || netVar.SyncType == NetSync.ServerAuthority)
{
_loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but {nameof(NetSync)} is {netVar.SyncType.ToString()}");
return;
}
if (!client.HasPermission(netVar.WritePermissions))
{
_loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but the client lacks permissions to modify it");
return;
}
netVar.ReadNetMessage(message);
// Sync back to all clients
if (netVar.SyncType != NetSync.ClientOneWay)
{
SendNetVar(netVar);
}
});
#endif
}
public void DeregisterNetVar(INetworkSyncVar netVar)
{
if (netVar is null)
{
return;
}
netVar.SetNetworkOwner(null);
netVars.TryRemove(netVar, out _);
}
public void SendNetVar(INetworkSyncVar netVar) => SendNetVar(netVar, null);
public void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection = null)
{
if (!netVars.TryGetValue(netVar, out NetId netId))
{
throw new InvalidOperationException("Tried to send net var across network without registering first");
}
if (netVar.SyncType == NetSync.None) { return; }
#if CLIENT
if (netVar.SyncType == NetSync.ServerAuthority) { return; }
#elif SERVER
if (netVar.SyncType == NetSync.ClientOneWay) { return; }
#endif
IWriteMessage message = Start(netId);
netVar.WriteNetMessage(message);
#if CLIENT
SendToServer(message);
#elif SERVER
SendToClient(message, connection);
#endif
}
public FluentResults.Result Reset()
{
IsSynchronized = false;
netReceives = new ConcurrentDictionary<NetId, NetMessageReceived>();
packetToId = new ConcurrentDictionary<ushort, NetId>();
idToPacket = new ConcurrentDictionary<NetId, ushort>();
netVars = new ConcurrentDictionary<INetworkSyncVar, NetId>();
SubscribeToEvents();
return FluentResults.Result.Ok();
}
public void Dispose()
{
IsDisposed = true;
}
#region Compatiblity
private static readonly HttpClient client = new HttpClient();
public async void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
{
try
{
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
if (data != null)
{
request.Content = new StringContent(data, Encoding.UTF8, contentType);
}
HttpResponseMessage response = await client.SendAsync(request);
if (savePath != null)
{
if (LuaCsFile.IsPathAllowedException(savePath))
{
byte[] responseData = await response.Content.ReadAsByteArrayAsync();
using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write))
{
fileStream.Write(responseData, 0, responseData.Length);
}
}
}
string responseBody = await response.Content.ReadAsStringAsync();
CrossThread.RequestExecutionOnMainThread(() =>
{
callback(responseBody, (int)response.StatusCode, response.Headers);
});
}
catch (HttpRequestException e)
{
CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, e.StatusCode, null); });
}
catch (Exception e)
{
CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, null, null); });
}
}
public void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, data, "POST", contentType, headers, savePath);
}
public void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, data, "POST", contentType, headers, savePath);
}
public void HttpGet(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, null, "GET", null, headers, savePath);
}
public void RequestGetHTTP(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, null, "GET", null, headers, savePath);
}
public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData)
{
GameMain.NetworkMember.CreateEntityEvent(entity, extraData);
}
public ushort LastClientListUpdateID
{
get { return GameMain.NetworkMember.LastClientListUpdateID; }
set { GameMain.NetworkMember.LastClientListUpdateID = value; }
}
#if SERVER
public void ClientWriteLobby(Client client) => GameMain.Server.ClientWriteLobby(client);
public void UpdateClientPermissions(Client client)
{
GameMain.Server.UpdateClientPermissions(client);
}
public int FileSenderMaxPacketsPerUpdate
{
get { return FileSender.FileTransferOut.MaxPacketsPerUpdate; }
set { FileSender.FileTransferOut.MaxPacketsPerUpdate = value; }
}
#endif
#endregion
public void OnSettingInstanceCreated<T>(T configInstance) where T : ISettingBase
{
if (configInstance is INetworkSyncVar syncVar)
{
RegisterNetVar(syncVar);
}
}
public void OnSettingInstanceDisposed<T>(T configInstance) where T : ISettingBase
{
if (configInstance is INetworkSyncVar syncVar)
{
DeregisterNetVar(syncVar);
}
}
}
@@ -0,0 +1,558 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Xml;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public sealed class PackageManagementService : IPackageManagementService
{
// svc
private ILoggerService _logger;
private IModConfigService _modConfigService;
private IConfigService _configService;
private ILuaScriptManagementService _luaScriptManagementService;
private IPluginManagementService _pluginManagementService;
private IConsoleCommandsService _commandsService;
#if CLIENT
private IUIStylesService _uiStylesService;
#endif
private IPackageManagementServiceConfig _runConfig;
// state
private readonly ConcurrentDictionary<ContentPackage, IModConfigInfo> _loadedPackages = new();
private readonly ConcurrentDictionary<ContentPackage, IModConfigInfo> _runningPackages = new();
private readonly ConcurrentDictionary<string, ContentPackage> _packageNameCache = new();
// control
/// <summary>
/// Service Disposal Lock.
/// </summary>
private readonly AsyncReaderWriterLock _operationsLock = new();
/// <summary>
/// Execution of packages lock.
/// <br/> Read: Package loading/unloading (Multi-operation mode).
/// <br/> Write: Package execution (exclusive mode).
/// </summary>
private readonly AsyncReaderWriterLock _executionLock = new();
public PackageManagementService(ILoggerService logger,
IModConfigService modConfigService,
ILuaScriptManagementService luaScriptManagementService,
IPluginManagementService pluginManagementService,
IConfigService configService,
IConsoleCommandsService commandsService,
#if CLIENT
IUIStylesService uiStylesService,
#endif
IPackageManagementServiceConfig runConfig)
{
_logger = logger;
_modConfigService = modConfigService;
_luaScriptManagementService = luaScriptManagementService;
_pluginManagementService = pluginManagementService;
_configService = configService;
_runConfig = runConfig;
#if CLIENT
_uiStylesService = uiStylesService;
#endif
_commandsService = commandsService;
commandsService.RegisterCommand("pms_getxmlname",
"Gets the XML encoded name for the given package, as used in localization.",
onExecute: args =>
{
if (args.Length < 1)
{
_logger.LogError("Please specify the name of the package.");
return;
}
if (ContentPackageManager.AllPackages.FirstOrDefault(p => p.Name == args[0]) is { } pkg)
{
_logger.Log($"Package Xml Name: '{XmlConvert.EncodeLocalName(pkg.Name)}'");
return;
}
_logger.Log($"Could not find package with the name '{args[0]}'");
},
getValidArgs: () =>
{
return new[]
{
this._loadedPackages.Keys.Select(p => p.Name).ToArray()
};
});
}
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
_logger.LogMessage($"{nameof(PackageManagementService)} is disposing.");
_luaScriptManagementService.Dispose();
_pluginManagementService.Dispose();
_modConfigService.Dispose();
_logger.Dispose();
#if CLIENT
_uiStylesService.Dispose();
#endif
_logger = null;
_luaScriptManagementService = null;
_pluginManagementService = null;
_modConfigService = null;
#if CLIENT
_uiStylesService = null;
#endif
_loadedPackages.Clear();
_runningPackages.Clear();
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public FluentResults.Result Reset()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (IsDisposed)
return FluentResults.Result.Fail($"{nameof(PackageManagementService)}failed to reset. Has already been disposed.");
try
{
var operationResult = new FluentResults.Result();
operationResult.WithReasons(_luaScriptManagementService.Reset().Reasons);
operationResult.WithReasons(_pluginManagementService.Reset().Reasons);
operationResult.WithReasons(_configService.Reset().Reasons);
#if CLIENT
operationResult.WithReasons(_uiStylesService.Reset().Reasons);
#endif
_runningPackages.Clear();
_loadedPackages.Clear();
_packageNameCache.Clear();
return operationResult;
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
public bool TryGetLoadedPackageByName(string name, out ContentPackage package)
{
package = null;
if (name.IsNullOrWhiteSpace())
{
return false;
}
using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
return _packageNameCache.TryGetValue(name, out package);
}
public FluentResults.Result LoadPackageInfo(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.TryGetValue(package, out var result))
{
_logger.LogWarning($"{nameof(LoadPackageInfo)}: Tried to load already-loaded package {package.Name}.");
return FluentResults.Result.Ok();
}
var pkgCfgInfo = _modConfigService.CreateConfigAsync(package).ConfigureAwait(false).GetAwaiter().GetResult();
if (pkgCfgInfo.IsFailed)
{
_logger.LogResults(pkgCfgInfo.ToResult());
return pkgCfgInfo.ToResult();
}
return UnsafeAddPackageInternal(package, pkgCfgInfo.Value);
}
public FluentResults.Result LoadPackagesInfo(ImmutableArray<ContentPackage> packages)
{
if (packages.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentException($"{nameof(LoadPackagesInfo)}: packages list is empty.");
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = new FluentResults.Result();
var packages2 = packages.OrderBy(pkg => pkg.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
.ThenBy(packages.IndexOf)
.ToImmutableArray();
var pkgConfigs = _modConfigService.CreateConfigsAsync([..packages2]).ConfigureAwait(false).GetAwaiter().GetResult();
foreach (var pkgConfig in pkgConfigs)
{
result.WithReasons(pkgConfig.Config.Reasons);
if (pkgConfig.Config.IsSuccess)
{
result.WithReasons(UnsafeAddPackageInternal(pkgConfig.Source, pkgConfig.Config.Value).Reasons);
}
}
return result;
}
private FluentResults.Result UnsafeAddPackageInternal(ContentPackage package, IModConfigInfo config)
{
if (_loadedPackages.TryGetValue(package, out _))
{
_logger.LogWarning($"Tried to load already-loaded package {package.Name}.");
return FluentResults.Result.Ok();
}
// We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe.
foreach (var info in config.Assemblies)
{
TouchMeFullPaths(info);
}
foreach (var info in config.Configs)
{
TouchMeFullPaths(info);
}
foreach (var info in config.LuaScripts)
{
TouchMeFullPaths(info);
}
// We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe.
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.PreserveSig)]
void TouchMeFullPaths(IBaseResourceInfo info)
{
foreach (var contentPath in info.FilePaths)
{
var s = contentPath.FullPath;
}
}
_loadedPackages[package] = config;
_packageNameCache[package.Name] = package;
try
{
var res = new FluentResults.Result();
var tasks = ImmutableArray.CreateBuilder<Task<Task<FluentResults.Result>>>();
if (!config.Configs.IsDefaultOrEmpty)
{
tasks.Add(Task.Factory.StartNew(async Task<FluentResults.Result> () =>
new FluentResults.Result()
.WithReasons((await _configService.LoadConfigsAsync(config.Configs)).Reasons)
.WithReasons((await _configService.LoadConfigsProfilesAsync(config.Configs)).Reasons)));
}
if (!config.LuaScripts.IsDefaultOrEmpty)
{
tasks.Add(Task.Factory.StartNew(async () =>
await _luaScriptManagementService.LoadScriptResourcesAsync(config.LuaScripts)));
}
if (tasks.Count == 0)
{
return FluentResults.Result.Ok();
}
#if CLIENT
if (!config.Styles.IsDefaultOrEmpty)
{
res.WithReasons(_uiStylesService.LoadAssets(config.Styles).Reasons);
}
#endif
var r = Task.WhenAll(tasks.ToArray()).ConfigureAwait(false).GetAwaiter().GetResult();
foreach (var task in r)
{
res.WithReasons(task.ConfigureAwait(false).GetAwaiter().GetResult().Reasons);
}
return res;
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
public FluentResults.Result ExecuteLoadedPackages(ImmutableArray<ContentPackage> executionOrder, bool executeCsAssemblies)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (executionOrder.IsDefaultOrEmpty)
{
return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages in the execution order list.");
}
if (!_runningPackages.IsEmpty)
{
return FluentResults.Result.Fail(
$"{nameof(ExecuteLoadedPackages)}: There are already packages running! List: {
_runningPackages.Aggregate(string.Empty, (acc, kvp) => "-" + kvp + "\n" + kvp.Key.Name)}");
}
if (_loadedPackages.IsEmpty)
{
return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages loaded. Nothing to run!)");
}
var result = new FluentResults.Result();
// get loading order. Note: packages not in the execution order list will load first.
var loadingOrderedPackages = _loadedPackages
.OrderBy(pkg => pkg.Key.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
.ThenBy(pkg => executionOrder.IndexOf(pkg.Key))
.ToImmutableArray();
var loadOrderByPackage = loadingOrderedPackages.Select(p => p.Key).ToImmutableArray();
var toLoadPackagesIndents = loadingOrderedPackages
.SelectMany(p => p.Key.AltNames.Union(new []{ p.Key.Name }).ToIdentifiers())
.ToImmutableHashSet();
// NOTE: Config/Settings are instanced in LoadPackages()
if (executeCsAssemblies)
{
var plugins = SelectCompatible(loadingOrderedPackages
.SelectMany(pkg => pkg.Value.Assemblies)
.ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage);
if (!plugins.IsDefaultOrEmpty)
{
result.WithReasons(_pluginManagementService.LoadAssemblyResources(plugins).Reasons);
result.WithReasons(_pluginManagementService.ActivatePluginInstances(
plugins.Select(p => p.OwnerPackage).ToImmutableArray(), false).Reasons);
}
}
//lua scripts
var luaScripts = SelectCompatible(loadingOrderedPackages
.Where(pkg => executeCsAssemblies
|| !pkg.Value.LuaScripts.Any(scr => scr.RunUnrestricted))
.SelectMany(pkg => pkg.Value.LuaScripts)
.ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage);
if (!luaScripts.IsDefaultOrEmpty)
{
result.WithReasons(_luaScriptManagementService.ExecuteLoadedScripts(luaScripts, enableSandbox: !executeCsAssemblies).Reasons);
}
foreach (var package in loadingOrderedPackages)
{
_runningPackages[package.Key] = package.Value;
}
return result;
}
private static ImmutableArray<T> SelectCompatible<T>(ImmutableArray<T> resources,
ImmutableHashSet<Identifier> enabledPackagesIdents,
ImmutableArray<ContentPackage> loadingOrder)
where T : IBaseResourceInfo
{
return resources
.Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform))
.Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget))
.Where(r => !r.Optional || (
(r.RequiredPackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.RequiredPackages).Any())
&& (r.IncompatiblePackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.IncompatiblePackages).None())))
.OrderBy(r => r.Optional ? 1 : 0) // optional content last
.ThenBy(r => loadingOrder.IndexOf(r.OwnerPackage))
.ThenBy(r => r.LoadPriority)
.ToImmutableArray();
}
public FluentResults.Result SyncLoadedPackagesList(ImmutableArray<ContentPackage> packages)
{
if (packages.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException(nameof(packages));
if (!_runningPackages.IsEmpty)
ThrowHelper.ThrowInvalidOperationException($"{nameof(SyncLoadedPackagesList)}: There are packages running!");
var toRemove = _loadedPackages.Keys.Except(packages).ToImmutableArray();
var toAdd = packages.Except(_loadedPackages.Keys)
.OrderBy(pack => packages.IndexOf(pack)).ToImmutableArray();
var result = new FluentResults.Result();
if (!toRemove.IsDefaultOrEmpty)
{
result.WithReasons(UnloadPackages(toRemove).Reasons);
}
if (!toAdd.IsDefaultOrEmpty)
{
result.WithReasons(LoadPackagesInfo(toAdd).Reasons);
}
return result;
}
public FluentResults.Result StopRunningPackages()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.IsEmpty || _runningPackages.IsEmpty)
{
_logger.LogWarning($"{nameof(StopRunningPackages)}: No packages are currently executing.");
return FluentResults.Result.Ok();
}
var res = new FluentResults.Result();
res.WithReasons(_luaScriptManagementService.UnloadActiveScripts().Reasons);
res.WithReasons(_pluginManagementService.UnloadManagedAssemblies().Reasons);
_runningPackages.Clear();
return res;
}
public FluentResults.Result UnloadPackage(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_loadedPackages.ContainsKey(package))
{
return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: The package is not loaded.");
}
if (!_runningPackages.IsEmpty)
{
return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: Packages are currently executing.");
}
var result = new FluentResults.Result();
result.WithReasons(_luaScriptManagementService.DisposePackageResources(package).Reasons);
result.WithReasons(_configService.DisposePackageData(package).Reasons);
#if CLIENT
result.WithReasons(_uiStylesService.UnloadPackage(package).Reasons);
#endif
_loadedPackages.TryRemove(package, out _);
_packageNameCache.TryRemove(package.Name, out _);
return result;
}
public FluentResults.Result UnloadPackages(ImmutableArray<ContentPackage> packages)
{
if (packages.IsDefaultOrEmpty)
return FluentResults.Result.Fail($"{nameof(UnloadPackages)}: Package list is empty.");
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = new FluentResults.Result();
foreach (var package in packages)
{
result.WithReasons(UnloadPackage(package).Reasons);
}
return result;
}
public FluentResults.Result UnloadAllPackages()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.IsEmpty)
return FluentResults.Result.Ok();
if (!_runningPackages.IsEmpty)
return FluentResults.Result.Fail($"{nameof(UnloadAllPackages)}: Packages are currently executing.");
var result = new FluentResults.Result();
result.WithReasons(_luaScriptManagementService.DisposeAllPackageResources().Reasons);
result.WithReasons(_configService.DisposeAllPackageData().Reasons);
_loadedPackages.Clear();
return result;
}
public ImmutableArray<ContentPackage> GetAllLoadedPackages()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return [.._loadedPackages.Keys];
}
public bool IsPackageRunning(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return _runningPackages.ContainsKey(package);
}
public bool IsAnyPackageLoaded()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return !_loadedPackages.IsEmpty;
}
public bool IsAnyPackageRunning()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return !_runningPackages.IsEmpty;
}
public ImmutableArray<ContentPackage> GetLoadedUnrestrictedPackages()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.IsEmpty)
return ImmutableArray<ContentPackage>.Empty;
return [.._loadedPackages.Values
.Where(cfg => !cfg.Assemblies.IsDefaultOrEmpty || cfg.LuaScripts.Any(scr => scr.RunUnrestricted))
.Select(cfg => cfg.Package)];
}
public bool PackageContainsAnyRunnableResource(ContentPackage package)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = GetModConfigForPackage(package);
if (result.IsSuccess)
{
return result.Value.Assemblies.Any() || result.Value.LuaScripts.Any();
}
else
{
return false;
}
}
public Result<IModConfigInfo> GetModConfigForPackage(ContentPackage package)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_loadedPackages.TryGetValue(package, out var modConfig))
{
return FluentResults.Result.Fail($"Failed to find mod config for package {package.Name}");
}
return new FluentResults.Result<IModConfigInfo>().WithValue(modConfig);
}
}
@@ -0,0 +1,962 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using System.Xml.Serialization;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events;
using FluentResults;
using FluentResults.LuaCs;
using LightInject;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Diagnostics;
using OneOf;
namespace Barotrauma.LuaCs;
public class PluginManagementService : IAssemblyManagementService
{
#region CSHARP_COMPILATION_OPTIONS
private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default
.WithPreprocessorSymbols(new[]
{
#if SERVER
"SERVER"
#elif CLIENT
"CLIENT"
#else
"UNDEFINED"
#endif
#if DEBUG
,"DEBUG"
#endif
});
#if WINDOWS
private const string PLATFORM_TARGET = "Windows";
#elif OSX
private const string PLATFORM_TARGET = "OSX";
#elif LINUX
private const string PLATFORM_TARGET = "Linux";
#endif
#if CLIENT
private const string ARCHITECTURE_TARGET = "Client";
#elif SERVER
private const string ARCHITECTURE_TARGET = "Server";
#endif
private static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithMetadataImportOptions(MetadataImportOptions.All)
#if DEBUG
.WithOptimizationLevel(OptimizationLevel.Debug)
#else
.WithOptimizationLevel(OptimizationLevel.Release)
#endif
.WithAllowUnsafe(true);
private static readonly SyntaxTree BaseAssemblyImports = CSharpSyntaxTree.ParseText(
new StringBuilder()
.AppendLine("global using LuaCsHook = Barotrauma.LuaCs.Compatibility.ILuaCsHook;")
.AppendLine("global using System.Reflection;")
.AppendLine("global using Barotrauma;")
.AppendLine("global using Barotrauma.LuaCs;")
.AppendLine("global using Barotrauma.LuaCs.Compatibility;")
.AppendLine("using System.Runtime.CompilerServices;")
.AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]")
#if CLIENT
.AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]")
#elif SERVER
.AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]")
#endif
.ToString(),
ScriptParseOptions);
private ImmutableArray<MetadataReference> _baseMetadataReferences = ImmutableArray<MetadataReference>.Empty;
private ImmutableArray<MetadataReference> _baseMetadataReferencesNonPublicized = ImmutableArray<MetadataReference>.Empty;
private IEnumerable<MetadataReference> BaseMetadataReferences
{
get
{
if (_baseMetadataReferences.IsDefaultOrEmpty)
{
_baseMetadataReferences = Basic.Reference.Assemblies.Net80.References.All
.Union(AssemblyLoadContext.Default.Assemblies
.Where(ass =>
!ass.IsDynamic &&
!ass.GetName().FullName.StartsWith("BarotraumaCore") &&
!ass.GetName().FullName.StartsWith("Barotrauma") &&
!ass.GetName().FullName.StartsWith("DedicatedServer"))
.Where(ass => !ass.Location.IsNullOrWhiteSpace())
.Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location)))
.Where(ar => ar is not null)
.ToImmutableArray();
}
return _baseMetadataReferences;
}
}
private IEnumerable<MetadataReference> BaseMetadataReferencesWithBarotrauma
{
get
{
if (_baseMetadataReferencesNonPublicized.IsDefaultOrEmpty)
{
_baseMetadataReferencesNonPublicized = Basic.Reference.Assemblies.Net80.References.All
.Union(AssemblyLoadContext.Default.Assemblies
.Where(ass => !ass.IsDynamic)
.Where(ass => !ass.Location.IsNullOrWhiteSpace())
.Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location)))
.Where(ar => ar is not null)
.ToImmutableArray();
}
return _baseMetadataReferencesNonPublicized;
}
}
#endregion
#region Disposal
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
UnsafeDisposeResourcesInternal();
_assemblyLoaderFactory = null;
_storageService = null;
_eventService = null;
_logger = null;
_configService = null;
_luaScriptManagementService = null;
_luaCsInfoProvider = null;
GC.SuppressFinalize(this);
}
private void UnsafeDisposeResourcesInternal()
{
foreach (var packPlugin in _pluginInstances.SelectMany(kvp => kvp.Value.Select(pluginInst => (kvp.Key, pluginInst))))
{
try
{
packPlugin.pluginInst.Dispose();
}
catch (Exception e)
{
_logger.LogError($"Error while disposing plugin for ContentPackage {packPlugin.Key.Name}: \n{e.Message}");
}
}
_pluginInstances.Clear();
_pluginPackageLookup.Clear();
_pluginInjectorContainer?.Dispose();
_pluginInjectorContainer = null;
foreach (var loader in _assemblyLoaders)
{
try
{
loader.Value.Dispose();
_unloadingAssemblyLoaders.Add(loader.Value, loader.Key);
}
catch (Exception e)
{
_logger?.LogError($"Failed to dispose of {nameof(IAssemblyLoaderService)} for ContentPackage {loader.Key.Name}: \n{e.Message}");
if (loader.Value.Assemblies.Any())
{
foreach (var ass in loader.Value.Assemblies)
{
_logger?.LogWarning($"{nameof(PluginManagementService)}: Fallback manual unsubscription of assemblies: {ass.GetName()}");
ReflectionUtils.RemoveAssemblyFromCache(ass);
}
}
}
}
_assemblyLoaders.Clear();
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public FluentResults.Result Reset()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
UnsafeDisposeResourcesInternal();
return FluentResults.Result.Ok();
}
#endregion
private IAssemblyLoaderService.IFactory _assemblyLoaderFactory;
private IStorageService _storageService;
private ILoggerService _logger;
private Lazy<IEventService> _eventService;
private Lazy<IConfigService> _configService;
private Lazy<ILuaScriptManagementService> _luaScriptManagementService;
private IEventService _pluginEventService;
private Lazy<ILuaPatcher> _pluginLuaPatcherService;
private Func<IConsoleCommandsService> _consoleCommandServiceFactory;
private ILuaCsInfoProvider _luaCsInfoProvider;
private readonly ConcurrentDictionary<ContentPackage, IAssemblyLoaderService> _assemblyLoaders = new();
private readonly ConcurrentDictionary<Type, ContentPackage> _pluginPackageLookup = new();
private readonly ConcurrentDictionary<ContentPackage, ImmutableArray<IAssemblyPlugin>> _pluginInstances = new();
private readonly ConditionalWeakTable<IAssemblyLoaderService, ContentPackage> _unloadingAssemblyLoaders = new();
private readonly ConcurrentBag<IntPtr> _loadedNativeLibraries = new();
private readonly AsyncReaderWriterLock _operationsLock = new();
private ServiceContainer _pluginInjectorContainer;
public PluginManagementService(
IAssemblyLoaderService.IFactory assemblyLoaderFactory,
IStorageService storageService,
ILoggerService logger,
Lazy<IEventService> eventService,
Lazy<ILuaScriptManagementService> luaScriptManagementService,
Lazy<IConfigService> configService,
Lazy<ILuaPatcher> pluginLuaPatcherService,
Func<IConsoleCommandsService> consoleCommandServiceFactory,
ILuaCsInfoProvider luaCsInfoProvider)
{
_assemblyLoaderFactory = assemblyLoaderFactory;
_storageService = storageService;
_logger = logger;
_eventService = eventService;
_luaScriptManagementService = luaScriptManagementService;
_configService = configService;
_pluginLuaPatcherService = pluginLuaPatcherService;
_consoleCommandServiceFactory = consoleCommandServiceFactory;
_luaCsInfoProvider = luaCsInfoProvider;
}
private ServiceContainer CreatePluginServiceContainer()
{
var container = new ServiceContainer(new ContainerOptions()
{
EnablePropertyInjection = true
});
_pluginEventService ??= new EventService(_logger, _pluginLuaPatcherService.Value);
_eventService.Value.AddDispatcherEventService(_pluginEventService);
container.Register<ILoggerService>(fac => _logger);
container.Register<IStorageService>(fac => _storageService);
container.Register<IEventService>(fac => _pluginEventService);
container.Register<IPluginManagementService>(fac => this);
container.Register<ILuaScriptManagementService>(fac => _luaScriptManagementService.Value);
container.Register<IConfigService>(fac => _configService.Value);
container.Register<IConsoleCommandsService>(fac => _consoleCommandServiceFactory?.Invoke());
return container;
}
public Result<ImmutableArray<Type>> GetImplementingTypes<T>(bool includeInterfaces = false, bool includeAbstractTypes = false,
bool includeDefaultContext = true)
{
if (includeInterfaces)
{
includeAbstractTypes = true;
}
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var builder = ImmutableArray.CreateBuilder<Type>();
if (includeDefaultContext)
{
foreach (var ass in AssemblyLoadContext.Default.Assemblies)
{
AddTypesFromAssembly(ass);
}
}
foreach (var ass in _assemblyLoaders.Values.Where(al => !al.IsReferenceOnlyMode).SelectMany(al => al.Assemblies))
{
AddTypesFromAssembly(ass);
}
return builder.ToImmutable();
void AddTypesFromAssembly(Assembly assembly)
{
foreach (var type in assembly.GetSafeTypes())
{
if ((includeInterfaces || !type.IsInterface)
&& (includeAbstractTypes || !type.IsAbstract)
&& type.IsAssignableTo(typeof(T)))
{
builder.Add(type);
}
}
}
}
public bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage)
{
return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage);
}
public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false,
bool includeDefaultContext = true)
{
if (typeName.StartsWith("out ") || typeName.StartsWith("ref "))
{
typeName = typeName.Remove(0, 4);
isByRefType = true;
}
if (includeDefaultContext)
{
var type = Type.GetType(typeName, false, false);
if (type is not null && (includeInterfaces || !type.IsInterface))
{
if (isByRefType)
{
return type.MakeByRefType();
}
return type;
}
foreach (var ass in AssemblyLoadContext.Default.Assemblies)
{
if (ass.GetType(typeName, false, false) is not {} type2 || (!includeInterfaces && type2.IsInterface))
{
continue;
}
return isByRefType ? type2.MakeByRefType() : type2;
}
}
foreach (var ass in AssemblyLoadContext.All
.Where(alc => alc != AssemblyLoadContext.Default)
.SelectMany(alc => alc.Assemblies))
{
if (ass.GetType(typeName, false, false) is not {} type || (!includeInterfaces && type.IsInterface))
{
continue;
}
return isByRefType ? type.MakeByRefType() : type;
}
return null;
}
public FluentResults.Result ActivatePluginInstances(ImmutableArray<ContentPackage> executionOrder, bool excludeAlreadyRunningPackages = true)
{
if (executionOrder.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(ActivatePluginInstances)}: The ececution list provided is empty.");
}
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_assemblyLoaders.IsEmpty)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
var toLoad = _assemblyLoaders
.Where(al => executionOrder.Contains(al.Key))
.Where(al => !excludeAlreadyRunningPackages || !_pluginInstances.ContainsKey(al.Key))
.SelectMany(al => al.Value.Assemblies.Select(ass => (al.Key, ass)))
.SelectMany<(ContentPackage Key, Assembly ass), (ContentPackage Key, Type type)>(kvp =>
{
try
{
return kvp.ass.GetTypes()
.Where(type =>
type is { IsInterface: false, IsAbstract: false, IsGenericType: false }
&& type.IsAssignableTo(typeof(IAssemblyPlugin)))
.Select(type => (kvp.Key, type));
}
catch (ReflectionTypeLoadException re)
{
results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'"));
results.WithError(new ExceptionalError(re));
}
catch (Exception e)
{
results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'"));
results.WithError(new ExceptionalError(e));
}
return new List<(ContentPackage Key, Type type)>();
})
.GroupBy(kvp => kvp.Key, kvp => kvp.type)
.OrderBy(exeGrp => executionOrder.IndexOf(exeGrp.Key))
.ToImmutableArray();
if (toLoad.Length == 0)
{
return results;
}
_logger.LogMessage($"Activating {nameof(IAssemblyPlugin)} instances");
var loadedPackagePlugins =
ImmutableArray.CreateBuilder<(ContentPackage Package, ImmutableArray<IAssemblyPlugin> Plugins)>();
_pluginInjectorContainer ??= CreatePluginServiceContainer();
foreach (var packageTypes in toLoad)
{
var loadedTypes = ImmutableArray.CreateBuilder<IAssemblyPlugin>();
foreach (var pluginType in packageTypes)
{
try
{
_logger.LogMessage($"- Instantiating {pluginType.Name}");
var plugin = (IAssemblyPlugin)Activator.CreateInstance(pluginType);
_pluginInjectorContainer.InjectProperties(plugin);
_pluginInjectorContainer.Register(pluginType, fac => plugin);
loadedTypes.Add(plugin);
_pluginPackageLookup.TryAdd(pluginType, packageTypes.Key);
}
catch (Exception e)
{
results.WithError(new ExceptionalError($"Failed to instantiate mod: {packageTypes.Key.Name}", e));
continue;
}
}
loadedPackagePlugins.Add((packageTypes.Key, loadedTypes.ToImmutable()));
}
var packPluginGroups = loadedPackagePlugins.ToImmutable();
foreach (var packagePluginGrp in packPluginGroups)
{
if (_pluginInstances.TryGetValue(packagePluginGrp.Package, out var plugins))
{
_pluginInstances[packagePluginGrp.Package] = plugins.Concat(packagePluginGrp.Plugins).ToImmutableArray();
continue;
}
_pluginInstances[packagePluginGrp.Package] = packagePluginGrp.Plugins;
}
var pluginsToInit = packPluginGroups.SelectMany(ppg => ppg.Plugins).ToImmutableArray();
foreach (var plugin in pluginsToInit)
{
results.WithReasons(PluginInitRunner(plugin, p => p.PreInitPatching()).Reasons);
}
_eventService.Value.PublishEvent<IEventPluginPreInitialize>(sub => sub.PreInitPatching());
foreach (var plugin in pluginsToInit)
{
results.WithReasons(PluginInitRunner(plugin, p => p.Initialize()).Reasons);
}
_eventService.Value.PublishEvent<IEventPluginInitialize>(sub => sub.Initialize());
foreach (var plugin in pluginsToInit)
{
results.WithReasons(PluginInitRunner(plugin, p => p.OnLoadCompleted()).Reasons);
}
_eventService.Value.PublishEvent<IEventPluginLoadCompleted>(sub => sub.OnLoadCompleted());
return results;
// helper
FluentResults.Result PluginInitRunner(IAssemblyPlugin plugin, Action<IAssemblyPlugin> action)
{
try
{
action(plugin);
return FluentResults.Result.Ok();
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
}
public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources)
{
if (resources.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAssemblyResources)} The resource list is empty.)");
}
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_storageService.UseCaching = _luaCsInfoProvider.UseCaching;
if (!_luaCsInfoProvider.UseCaching)
{
_storageService.PurgeCache();
}
var orderedContentPacks = resources.GroupBy(res => res.OwnerPackage)
.OrderBy(res => resources.FindIndex(r2 => r2.OwnerPackage == res.Key))
.ToImmutableArray();
var result = new FluentResults.Result();
foreach (var contentPack in orderedContentPacks)
{
LoadBinaries(contentPack);
LoadAndCompileScriptAssemblies(contentPack);
foreach (var ass in _assemblyLoaders[contentPack.Key].Assemblies)
{
ReflectionUtils.AddNonAbstractAssemblyTypes(ass);
}
}
return result;
// --- helper methods
void LoadBinaries(IGrouping<ContentPackage,IAssemblyResourceInfo> contentPackRes)
{
var binaries = contentPackRes.Where(cRes => !cRes.IsScript)
.OrderBy(bin => bin.LoadPriority)
.SelectMany(bin => bin.FilePaths)
.ToImmutableArray();
if (binaries.IsDefaultOrEmpty)
{
return;
}
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
new IAssemblyLoaderService.LoaderInitData(
InstanceId: Guid.NewGuid(),
contentPackRes.Key.Name,
IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly),
OwnerPackage: contentPackRes.Key,
OnUnload: OnAssemblyLoaderUnloading,
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
)));
var dependencyPaths = binaries
.Select(bin => System.IO.Path.GetDirectoryName(bin.FullPath))
.Distinct()
.ToImmutableArray();
foreach (var binResource in binaries)
{
var res = assemblyLoader.LoadAssemblyFromFile(binResource.FullPath, dependencyPaths);
result.WithReasons(res.Reasons);
_logger.LogResults(res.ToResult());
}
}
void LoadAndCompileScriptAssemblies(IGrouping<ContentPackage, IAssemblyResourceInfo> contentPackRes)
{
var scriptsGrp = contentPackRes.Where(cRes => cRes.IsScript)
.Select(scr => (scr.OwnerPackage, scr.FriendlyName, scr.FilePaths, scr.UseInternalAccessName, scr.LoadPriority))
.OrderBy(scr => scr.LoadPriority)
.GroupBy(scr => scr.FriendlyName)
.ToImmutableArray();
if (scriptsGrp.IsDefaultOrEmpty)
{
return;
}
var metadataReferences = GetMetadataReferences(false).ToImmutableArray();
var metadataReferencesNonPublicized = GetMetadataReferences(true).ToImmutableArray();
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
new IAssemblyLoaderService.LoaderInitData(
InstanceId: Guid.NewGuid(),
contentPackRes.Key.Name,
IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly),
OwnerPackage: contentPackRes.Key,
OnUnload: OnAssemblyLoaderUnloading,
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
)));
// create syntax trees
foreach (var scripts in scriptsGrp)
{
var syntaxTreesBuilder = ImmutableArray.CreateBuilder<SyntaxTree>();
bool hasInternalsAwareBeenAdded = false;
bool compileWithInternalName = true;
foreach (var resourceInfo in scripts)
{
// this should be the same for the entire collection of src files so we just grab it from the collection
compileWithInternalName = resourceInfo.UseInternalAccessName;
if (!hasInternalsAwareBeenAdded)
{
hasInternalsAwareBeenAdded = true;
syntaxTreesBuilder.Add(BaseAssemblyImports);
}
if (resourceInfo.FilePaths.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAndCompileScriptAssemblies)} The resource list is empty for package {resourceInfo.OwnerPackage}.");
}
foreach (var resourcePath in resourceInfo.FilePaths)
{
var loadRes = GetSourceFilesText(resourcePath);
if (loadRes.IsFailed)
{
_logger.LogResults(loadRes.ToResult());
continue;
}
CancellationToken token = CancellationToken.None;
string sourceCode = loadRes.Value;
sourceCode = DoSourceCodeTextCompatibilityPass(sourceCode);
syntaxTreesBuilder.Add(SyntaxFactory.ParseSyntaxTree(
text: sourceCode,
options: ScriptParseOptions,
path: resourcePath.FullPath,
encoding: Encoding.Default,
cancellationToken: token
));
}
}
if (syntaxTreesBuilder.Count < 1)
{
continue;
}
_logger.LogMessage($"Compiling assembly for {scripts.Key}, in ContentPackage {contentPackRes.Key.Name}");
var res = assemblyLoader.CompileScriptAssembly(
assemblyName: scripts.Key,
compileWithInternalAccess: compileWithInternalName,
syntaxTrees: syntaxTreesBuilder.ToImmutable(),
metadataReferences: compileWithInternalName ? metadataReferencesNonPublicized : metadataReferences,
compilationOptions: CompilationOptions);
// try with internal access instead for legacy mods
if (!compileWithInternalName && res.IsFailed)
{
_logger.LogMessage($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name}. Trying fallback method.");
var res2 = assemblyLoader.CompileScriptAssembly(
assemblyName: scripts.Key,
compileWithInternalAccess: true,
syntaxTrees: syntaxTreesBuilder.ToImmutable(),
metadataReferences: metadataReferencesNonPublicized,
compilationOptions: CompilationOptions);
// overwrite result with good compilation
if (res2.IsSuccess)
{
var reasonsStr = res.Reasons.Aggregate("", (accum, reason) => accum + "\n" + reason.Message);
_logger.LogWarning($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name} succeeded. Original errors were: \n {reasonsStr}");
res = res2;
}
}
result.WithReasons(res.Reasons);
}
}
Result<string> GetSourceFilesText(ContentPath resourceInfoFilePath)
{
if (_storageService.LoadPackageText(resourceInfoFilePath) is not { IsFailed: false } res)
{
_logger.LogError($"{nameof(GetSourceFilesText)}: Failed to load source file for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}.");
return FluentResults.Result.Fail($"{nameof(GetSourceFilesText)}: Failed to load source files for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}.");
}
return res;
}
IEnumerable<MetadataReference> GetMetadataReferences(bool useNonPublicizedAssemblies)
{
var builder = ImmutableArray.CreateBuilder<MetadataReference>();
if (useNonPublicizedAssemblies)
{
builder.AddRange(BaseMetadataReferencesWithBarotrauma);
foreach (var loaderService in _assemblyLoaders
.Where(asl => !asl.Key.Name.Equals("LuaCsForBarotrauma", StringComparison.InvariantCultureIgnoreCase))
.ToImmutableArray())
{
builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null));
}
}
else
{
builder.AddRange(BaseMetadataReferences);
foreach (var loaderService in _assemblyLoaders)
{
builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null));
}
}
return builder.ToImmutable();
}
}
private string DoSourceCodeTextCompatibilityPass(string sourceCode)
{
return sourceCode
.Replace("GameMain.LuaCs", "LuaCsSetup.Instance")
.Replace(" Client.ClientList", " ModUtils.Client.ClientList")
.Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList")
.Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab");
}
private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName)
{
Guard.IsNull(callerAssembly, nameof(callerAssembly));
Guard.IsNullOrWhiteSpace(targetAssemblyName, nameof(targetAssemblyName));
if (AssemblyLoadContext.GetLoadContext(callerAssembly) is not IAssemblyLoaderService loaderService)
{
return IntPtr.Zero;
}
var targetDirectory = Path.GetFullPath(loaderService.OwnerPackage.Dir);
if (!targetAssemblyName.TrimEnd().EndsWith(".dll"))
{
targetAssemblyName += ".dll";
}
var res = _storageService.FindFilesInPackage(loaderService.OwnerPackage, string.Empty, targetAssemblyName, true);
if (res.IsFailed || !res.Value.Any())
{
return IntPtr.Zero;
}
foreach (var path in res.Value)
{
if (System.Runtime.InteropServices.NativeLibrary.TryLoad(path, out IntPtr asmPtr))
{
_loadedNativeLibraries.Add(asmPtr);
return asmPtr;
}
}
return IntPtr.Zero;
}
private Assembly OnAssemblyLoaderResolvingManaged(IAssemblyLoaderService requestingLoader, AssemblyName searchName)
{
// This method is used during assembly instantiation, we cannot put a lock here.
//using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
foreach (var loader in _assemblyLoaders.Where(kvp => kvp.Value != requestingLoader)
.Select(kvp => kvp.Value).ToImmutableArray())
{
if (loader.IsReferenceOnlyMode || !loader.Assemblies.Any())
{
continue;
}
foreach (var assembly in loader.Assemblies)
{
if (assembly.GetName().FullName == searchName.FullName)
{
return assembly;
}
}
}
return null;
}
private void OnAssemblyLoaderUnloading(IAssemblyLoaderService loader)
{
if (!loader.Assemblies.Any())
{
return;
}
foreach (var assembly in loader.Assemblies)
{
_eventService?.Value?.PublishEvent<IEventAssemblyUnloading>(sub => sub.OnAssemblyUnloading(assembly));
}
}
public FluentResults.Result UnloadManagedAssemblies()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_assemblyLoaders.Count == 0)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons);
ReflectionUtils.ResetCache();
foreach (var loaderService in _assemblyLoaders)
{
try
{
loaderService.Value.Dispose();
_unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key);
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
}
}
_assemblyLoaders.Clear();
_storageService.PurgeCache();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
#if DEBUG
// Print still loaded assembly load ctx after giving some time
CoroutineManager.Invoke(() =>
{
if (!_unloadingAssemblyLoaders.Any())
{
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
{
sb.AppendLine($"- '{kvp.Value.Name}'");
}
// Use DebugConsole in case logger is null by the time this executes.
if (_logger is null)
{
DebugConsole.LogError(sb.ToString());
}
else
{
_logger.LogWarning(sb.ToString());
}
}, 3.0f);
#endif
// clear native libraries
if (_loadedNativeLibraries.Any())
{
foreach (var ptr in _loadedNativeLibraries)
{
try
{
System.Runtime.InteropServices.NativeLibrary.Free(ptr);
}
catch
{
// ignored
continue;
}
}
_loadedNativeLibraries.Clear();
}
return results;
}
private FluentResults.Result UnsafeDisposeManagedTypeInstances()
{
var results = new FluentResults.Result();
if (!_pluginInstances.IsEmpty)
{
foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value))
{
try
{
instance.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
continue;
}
}
}
if (_pluginEventService is not null)
{
_eventService.Value.RemoveDispatcherEventService(_pluginEventService);
_pluginEventService = null;
}
_pluginInjectorContainer = null;
_pluginInstances.Clear();
_pluginPackageLookup.Clear();
return results;
}
public Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts)
{
using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var guids = excludedContexts;
return assemblyName.Match<Assembly>((AssemblyName asm) =>
{
foreach (var ass in _assemblyLoaders.Values
.Where(al => guids.Length == 0 || !guids.Contains(al.Id))
.SelectMany(al => al.Assemblies)
.ToImmutableArray())
{
if (ass.GetName() == asm)
{
return ass;
}
}
return null;
},
(string asmName) =>
{
foreach (var ass in _assemblyLoaders.Values.SelectMany(al => al.Assemblies))
{
if (ass.GetName().Name?.Equals(asmName) ?? ass.GetName().FullName.Equals(asmName))
{
return ass;
}
}
return null;
});
}
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public class PluginService
{
}
@@ -0,0 +1,230 @@
using Barotrauma.IO;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using FarseerPhysics.Common;
using FluentResults;
using FluentResults.LuaCs;
using Microsoft.Toolkit.Diagnostics;
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Path = System.IO.Path;
namespace Barotrauma.LuaCs;
public class SafeStorageService : StorageService, ISafeStorageService
{
private ConcurrentDictionary<string, byte>
_fileListRead = new (),
_fileListWrite = new();
private readonly AsyncReaderWriterLock _higherOperationsLock = new();
public SafeStorageService(IStorageServiceConfig configData) : base(configData)
{
IsReadOperationAllowedEval = (fp) => IsFileAccessible(fp, true, true);
IsWriteOperationAllowedEval = (fp) => IsFileAccessible(fp, false, true);
}
private string GetFullPath(string path) => System.IO.Path.GetFullPath(path).CleanUpPathCrossPlatform();
public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
try
{
path = GetFullPath(path);
if (path.StartsWith(ConfigData.WorkshopModsDirectory)
|| path.StartsWith(ConfigData.LocalModsDirectory)
#if CLIENT
|| path.StartsWith(ConfigData.TempDownloadsDirectory)
#endif
)
{
return true;
}
if (!_fileListRead.ContainsKey(path))
{
return false;
}
if (!readOnly && !_fileListWrite.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 readOnly ? fs.CanRead : fs.CanWrite;
}
catch
{
return false;
}
}
public void AddFileToWhitelist(string path, bool readOnly = true)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
try
{
path = GetFullPath(path);
_fileListRead.AddOrUpdate(path, s => 0, (s, b) => 0);
if (!readOnly)
{
_fileListWrite.AddOrUpdate(path, s => 0, (s, b) => 0);
}
}
catch
{
return;
}
}
public void AddFilesToWhitelist(ImmutableArray<string> paths, bool readOnly = true)
{
if (paths.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException(nameof(paths));
foreach (var path in paths)
{
AddFileToWhitelist(path, readOnly);
}
}
public void RemoveFileFromAllWhitelists(string path)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
try
{
path = GetFullPath(path);
_fileListRead.TryRemove(path, out _);
_fileListWrite.TryRemove(path, out _);
}
catch
{
return;
}
}
public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths)
{
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (filePaths.IsDefaultOrEmpty)
{
return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty.");
}
_fileListRead.Clear();
var res = new FluentResults.Result();
foreach (var path in filePaths)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
try
{
var p = Path.GetFullPath(path.CleanUpPathCrossPlatform());
if (_fileListRead.ContainsKey(p))
{
res = res.WithReason(new Success($"Path already in whitelist: {p}"));
continue;
}
if (_fileListRead.TryAdd(p, 0))
{
res = res.WithSuccess($"Added path successfully: {p}");
continue;
}
res = res.WithError(new Error($"Failed to add path to list: {p}"));
}
catch (Exception e)
{
res = res.WithError(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.ExceptionDetails, e.Message)
.WithMetadata(MetadataType.RootObject, path)
);
continue;
}
}
return res;
}
public FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths)
{
if (filePaths.IsDefaultOrEmpty)
{
return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty.");
}
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_fileListRead.Clear();
_fileListWrite.Clear();
var res = new FluentResults.Result();
foreach (var path in filePaths)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
try
{
var p = Path.GetFullPath(path.CleanUpPathCrossPlatform());
TryAddToList(_fileListRead, p);
TryAddToList(_fileListWrite, p);
res = res.WithError(new Error($"Failed to add path to list: {p}"));
}
catch (Exception e)
{
res = res.WithError(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.ExceptionDetails, e.Message)
.WithMetadata(MetadataType.RootObject, path)
);
continue;
}
}
void TryAddToList(ConcurrentDictionary<string, byte> dict, string p)
{
if (dict.ContainsKey(p))
{
res = res.WithReason(new Success($"Path already in whitelist: {p}"));
return;
}
if (dict.TryAdd(p, 0))
{
res = res.WithSuccess($"Added path successfully: {p}");
return;
}
}
return res;
}
public void ClearAllWhitelists()
{
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_fileListRead.Clear();
_fileListWrite.Clear();
}
}
@@ -0,0 +1,295 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using LightInject;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public class ServicesProvider : IServicesProvider
{
private ServiceContainer _serviceContainerInst;
private ServiceContainer ServiceContainer => _serviceContainerInst;
/// <summary>
/// Definition: [Key: ConcreteType, Value: TypeInstance]
/// </summary>
private ImmutableArray<ISystem> _systemInstances = ImmutableArray<ISystem>.Empty;
private readonly ReaderWriterLockSlim _serviceLock = new();
public ServicesProvider()
{
_serviceContainerInst = new ServiceContainer(new ContainerOptions()
{
EnablePropertyInjection = false
});
//_serviceContainerInst.Register<IServicesProvider>((f) => this);
}
public void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
{
// ISystem services must run as a lifetime singleton
if (typeof(TSvcInterface).IsAssignableTo(typeof(ISystem)))
{
lifetimeInstance = new PerContainerLifetime();
}
if (lifetimeInstance is null)
{
switch (lifetime)
{
case ServiceLifetime.Singleton:
lifetimeInstance = new PerContainerLifetime();
break;
case ServiceLifetime.PerThread:
lifetimeInstance = new PerThreadLifetime();
break;
// treat these as transient
case ServiceLifetime.Transient:
case ServiceLifetime.Invalid:
case ServiceLifetime.Custom:
default:
lifetimeInstance = null;
break;
}
}
try
{
_serviceLock.EnterReadLock();
if (lifetimeInstance is not null)
ServiceContainer.Register<TSvcInterface, TService>(lifetimeInstance);
else
ServiceContainer.Register<TSvcInterface, TService>();
}
finally
{
_serviceLock.ExitReadLock();
}
}
public void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime,
ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
{
if (name.IsNullOrWhiteSpace())
{
throw new ArgumentNullException($"Tried to register a service of type {typeof(TService).Name} but the name provided is null or empty." );
}
// ISystem services must run as a lifetime singleton
if (typeof(TService).IsAssignableTo(typeof(ISystem)))
{
lifetimeInstance = new PerContainerLifetime();
}
if (lifetimeInstance is null)
{
switch (lifetime)
{
case ServiceLifetime.Singleton:
lifetimeInstance = new PerContainerLifetime();
break;
case ServiceLifetime.PerThread:
lifetimeInstance = new PerThreadLifetime();
break;
// treat these as transient
case ServiceLifetime.Transient:
case ServiceLifetime.Invalid:
case ServiceLifetime.Custom: // lifetime should not be null here
default:
lifetimeInstance = new PerRequestLifeTime();
break;
}
}
try
{
_serviceLock.EnterReadLock();
ServiceContainer.Register<TSvcInterface, TService>(name, lifetimeInstance);
}
finally
{
_serviceLock.ExitReadLock();
}
}
public void RegisterServiceResolver<TSvcInterface>(Func<ServiceContainer, TSvcInterface> factory) where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
ServiceContainer.Register<TSvcInterface>(f => factory(ServiceContainer));
}
finally
{
_serviceLock.ExitReadLock();
}
}
public void CompileAndRun()
{
try
{
_serviceLock.EnterWriteLock();
ServiceContainer!.Compile();
if (!_systemInstances.IsDefaultOrEmpty)
{
ThrowHelper.ThrowInvalidOperationException($"Systems are already instanced!");
}
_systemInstances = ServiceContainer.GetAllInstances(typeof(ISystem))
.Select(obj => (ISystem)obj)
.ToImmutableArray();
}
finally
{
_serviceLock.ExitWriteLock();
}
}
public void InjectServices<T>(T inst) where T : class
{
try
{
_serviceLock.EnterReadLock();
ServiceContainer.InjectProperties(inst);
}
finally
{
_serviceLock.ExitReadLock();
}
}
public bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
service = ServiceContainer.TryGetInstance<TSvcInterface>();
return service is not null;
}
catch
{
service = null;
return false;
}
finally
{
_serviceLock.ExitReadLock();
}
}
public TSvcInterface GetService<TSvcInterface>() where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
return ServiceContainer.GetInstance<TSvcInterface>();
}
finally
{
_serviceLock.ExitReadLock();
}
}
public bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
service = ServiceContainer.TryGetInstance<TSvcInterface>(name);
return service is not null;
}
catch
{
service = null;
return false;
}
finally
{
_serviceLock.ExitReadLock();
}
}
public event Action<Type, IService> OnServiceInstanced;
public ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService
{
try
{
_serviceLock.EnterReadLock();
return ServiceContainer.GetAllInstances<TSvc>().ToImmutableArray();
}
finally
{
_serviceLock.ExitReadLock();
}
}
[MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.Synchronized)]
public void DisposeAndReset()
{
// Plugins should never be allowed to execute this.
if (Assembly.GetCallingAssembly() != Assembly.GetExecutingAssembly())
{
throw new MethodAccessException(
$"Assembly {Assembly.GetCallingAssembly().FullName} attempted to call {nameof(DisposeAndReset)}().");
}
try
{
_serviceLock.EnterWriteLock();
foreach (var system in _systemInstances)
{
try
{
system.Dispose();
}
catch (Exception e)
{
// ignored, no logging services available.
}
}
_systemInstances = ImmutableArray<ISystem>.Empty;
_serviceContainerInst?.Dispose();
_serviceContainerInst = new ServiceContainer();
}
finally
{
_serviceLock.ExitWriteLock();
}
}
}
public class PerThreadLifetime : ILifetime
{
private readonly ThreadLocal<object> _instance = new();
public object GetInstance(Func<object> createInstance, Scope scope)
{
if (_instance.Value is null)
{
var inst = createInstance.Invoke();
// IDisposable dispatch
if (inst is IDisposable disposable)
{
if (scope is null)
{
throw new InvalidOperationException("Attempt disposable object without a valid scope.");
}
scope.TrackInstance(disposable);
}
_instance.Value = inst;
}
return _instance.Value;
}
}
@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using OneOf;
namespace Barotrauma.LuaCs;
public sealed class SettingsFileParserService :
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>,
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>
{
#region DisposalControl
private AsyncReaderWriterLock _operationLock = new();
public void Dispose()
{
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_storageService.Dispose();
_storageService = null;
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#endregion
private IStorageService _storageService;
public SettingsFileParserService(IStorageService storageService)
{
_storageService = storageService;
}
async Task<Result<ImmutableArray<IConfigInfo>>> IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>
.TryParseResourcesAsync(IConfigResourceInfo src)
{
Guard.IsNotNull(src, nameof(src));
Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage));
using var lck = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (src.FilePaths.IsDefaultOrEmpty)
{
return ReturnFail($"The config file list is empty.");
}
var parsedInfo = ImmutableArray.CreateBuilder<IConfigInfo>();
foreach ((ContentPath path, Result<XDocument> docLoadResult) res in await _storageService.LoadPackageXmlFilesAsync(src.FilePaths))
{
if (res.docLoadResult.IsFailed)
{
return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}").WithErrors(res.docLoadResult.Errors);
}
var settingElements = res.docLoadResult.Value.GetChildElement("Configuration")
.GetChildElements("Settings").SelectMany(e => e.GetChildElements("Setting")).ToImmutableArray();
if (settingElements.IsDefaultOrEmpty)
{
continue;
}
var packageIdent = XmlConvert.EncodeLocalName(res.path.ContentPackage!.Name);
foreach (var element in settingElements)
{
var name = element.GetAttributeString("Name", string.Empty);
if (name.IsNullOrWhiteSpace())
{
return ReturnFail(
$"The internal name for a setting in the config file '{res.path.FullPath}' is empty!");
}
var newSetting = new ConfigInfo()
{
InternalName = name,
OwnerPackage = res.path.ContentPackage,
DataType = element.GetAttributeString("Type", string.Empty),
Element = element,
EditableStates = element.GetAttributeBool("ReadOnly", false) ? RunState.Unloaded :
element.GetAttributeBool("AllowChangesWhileExecuting", true) ? RunState.Running :
RunState.LoadedNoExec,
NetSync = element.GetAttributeEnum("NetSync", NetSync.None),
#if CLIENT
DisplayName = $"{packageIdent}.{name}.DisplayName",
Description = $"{packageIdent}.{name}.Description",
DisplayCategory = $"{packageIdent}.{name}.DisplayCategory",
ShowInMenus = element.GetAttributeBool("ShowInMenus", true),
Tooltip = $"{packageIdent}.{name}.Tooltip",
ImageIconPath = element.GetAttributeString("ImageIcon", string.Empty) is {} val && !val.IsNullOrWhiteSpace() ?
ContentPath.FromRaw(res.path.ContentPackage, val) : ContentPath.Empty
#endif
};
if (!IsInfoValid(newSetting))
{
return ReturnFail($"A setting was invalid. ContentPackage: {res.path.ContentPackage.Name}. Name: {newSetting?.InternalName}");
}
parsedInfo.Add(newSetting);
}
}
return FluentResults.Result.Ok(parsedInfo.ToImmutable());
// Helpers
FluentResults.Result ReturnFail(string msg)
{
return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>.TryParseResourcesAsync)}: {msg}");
}
bool IsInfoValid(ConfigInfo info)
{
return info.OwnerPackage != null
&& !info.InternalName.IsNullOrWhiteSpace()
&& !info.DataType.IsNullOrWhiteSpace()
&& info.Element != null
#if CLIENT
&& !info.DisplayName.IsNullOrWhiteSpace()
&& !info.Description.IsNullOrWhiteSpace()
&& !info.DisplayCategory.IsNullOrWhiteSpace()
&& !info.Tooltip.IsNullOrWhiteSpace()
#endif
;
}
}
async Task<Result<ImmutableArray<IConfigProfileInfo>>>
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>
.TryParseResourcesAsync(IConfigResourceInfo src)
{
Guard.IsNotNull(src, nameof(src));
Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage));
using var lck = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (src.FilePaths.IsDefaultOrEmpty)
{
return ReturnFail($"The config file list is empty.");
}
var parsedInfo = ImmutableArray.CreateBuilder<IConfigProfileInfo>();
foreach ((ContentPath path, Result<XDocument> docLoadResult) res in await _storageService
.LoadPackageXmlFilesAsync(src.FilePaths))
{
if (res.docLoadResult.IsFailed)
{
return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}")
.WithErrors(res.docLoadResult.Errors);
}
var profileCollection = res.docLoadResult.Value.GetChildElement("Configuration")
.GetChildElement("Profiles");
if (profileCollection == null)
{
continue;
}
foreach (var profile in profileCollection.GetChildElements("Profile"))
{
var profileName = profile.GetAttributeString("Name", string.Empty);
Guard.IsNotNullOrWhiteSpace(profileName, nameof(profileName));
var settingValues = profile.GetChildElements("SettingValue").ToImmutableArray();
if (settingValues.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException(nameof(settingValues));
}
var profileValuesBuilder = ImmutableArray.CreateBuilder<(string ConfigName, XElement Value)>();
foreach (var settingValue in settingValues)
{
var cfgName = settingValue.GetAttributeString("Name", string.Empty);
Guard.IsNotNullOrWhiteSpace(cfgName, nameof(cfgName));
profileValuesBuilder.Add((cfgName, settingValue));
}
parsedInfo.Add(new ConfigProfileInfo()
{
InternalName = profileName,
OwnerPackage = res.path.ContentPackage,
ProfileValues = profileValuesBuilder.ToImmutable()
});
}
}
return parsedInfo.ToImmutable();
FluentResults.Result ReturnFail(string msg)
{
return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>.TryParseResourcesAsync)}: {msg}");
}
}
}
@@ -0,0 +1,728 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using FluentResults;
using FluentResults.LuaCs;
using Microsoft.CodeAnalysis;
using Microsoft.Toolkit.Diagnostics;
using Error = FluentResults.Error;
using Path = System.IO.Path;
namespace Barotrauma.LuaCs;
public class StorageService : IStorageService
{
public StorageService(IStorageServiceConfig configData)
{
ConfigData = configData;
IsReadOperationAllowedEval = bool (str) => true;
IsWriteOperationAllowedEval = bool (str) => true;
}
private readonly ConcurrentDictionary<string, OneOf.OneOf<byte[], string, XDocument>> _fsCache = new();
protected readonly IStorageServiceConfig ConfigData;
protected readonly AsyncReaderWriterLock OperationsLock = new();
private Func<string, bool> _isReadOperationAllowedEval;
protected Func<string, bool> IsReadOperationAllowedEval
{
get => _isReadOperationAllowedEval;
set
{
if (value is not null)
_isReadOperationAllowedEval = value;
}
}
private Func<string, bool> _isWriteOperationAllowedEval;
protected Func<string, bool> IsWriteOperationAllowedEval
{
get => _isWriteOperationAllowedEval;
set
{
if (value is not null)
_isWriteOperationAllowedEval = value;
}
}
public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed);
private int _isDisposed = 0;
public virtual void Dispose()
{
using var lck = OperationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
_fsCache.Clear();
}
public void PurgeCache()
{
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_fsCache.Clear();
}
public void PurgeFileFromCache(string absolutePath)
{
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
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)
{
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
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;
}
}
}
// --- Local Game Content
protected Result<string> GetAbsolutePathForLocal(ContentPackage package, string localFilePath)
{
if (Path.IsPathRooted(localFilePath))
ThrowHelper.ThrowArgumentException($"{nameof(GetAbsolutePathForLocal)}: The path {localFilePath} is an absolute path.");
try
{
var path = System.IO.Path.GetFullPath(Path.Combine(
ConfigData.LocalPackageDataPath.Replace(ConfigData.LocalDataPathRegex, XmlConvert.EncodeLocalName(package.Name)).CleanUpPathCrossPlatform(),
localFilePath.CleanUpPathCrossPlatform()));
if (!path.StartsWith(Path.GetFullPath(ConfigData.LocalDataSavePath)))
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(GetAbsolutePathForLocal)}: The local path of '{path}' is not a local path!");
return path;
}
catch (Exception e)
{
if (e is ArgumentNullException or ArgumentException or UnauthorizedAccessException)
throw; // these are dev errors and should be propagated.
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
private Result<T> LoadLocalData<T>(ContentPackage package, string localFilePath, Func<string, Result<T>> dataLoader)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : dataLoader(res.Value);
}
public Result<XDocument> LoadLocalXml(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadXml);
public Result<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadBinary);
public Result<string> LoadLocalText(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadText);
private FluentResults.Result SaveLocalData<T>(ContentPackage package, string localFilePath, in T data, Func<string, T, FluentResults.Result> dataSaver)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : dataSaver(res.Value, data);
}
public FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document)
=> SaveLocalData(package, localFilePath, document, (path, data) => TrySaveXml(path, in data));
public FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes)
=> SaveLocalData(package, localFilePath, bytes, (path, data) => TrySaveBinary(path, in data));
public FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text)
=> SaveLocalData(package, localFilePath, text, (path, data) => TrySaveText(path, in data));
private async Task<Result<T>> LoadLocalDataAsync<T>(ContentPackage package, string localFilePath,
Func<string, Task<Result<T>>> dataLoader)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : await dataLoader(res.Value);
}
public async Task<Result<XDocument>> LoadLocalXmlAsync(ContentPackage package, string localFilePath)
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadXmlAsync(path));
public async Task<Result<byte[]>> LoadLocalBinaryAsync(ContentPackage package, string localFilePath)
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadBinaryAsync(path));
public async Task<Result<string>> LoadLocalTextAsync(ContentPackage package, string localFilePath)
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadTextAsync(path));
private async Task<FluentResults.Result> SaveLocalDataAsync<T>(ContentPackage package, string localFilePath,
T data, Func<string, T, Task<FluentResults.Result>> dataSaver)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
IService.CheckDisposed(this);
using var lck = await OperationsLock.AcquireReaderLock();
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : await dataSaver(res.Value, data);
}
public async Task<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document)
=> await SaveLocalDataAsync(package, localFilePath, document, async (path, doc) => await TrySaveXmlAsync(path, doc));
public async Task<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes)
=> await SaveLocalDataAsync(package, localFilePath, bytes, async (path, bin) => await TrySaveBinaryAsync(path, bin));
public async Task<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text)
=> await SaveLocalDataAsync(package, localFilePath, text, async (path, txt) => await TrySaveTextAsync(path, txt));
private bool IsPackagePathValid(ContentPath contentPath)
{
return contentPath.FullPath.StartsWith(ConfigData.WorkshopModsDirectory)
|| contentPath.FullPath.StartsWith(ConfigData.LocalModsDirectory)
#if CLIENT
|| contentPath.FullPath.StartsWith(ConfigData.TempDownloadsDirectory)
#endif
|| contentPath.FullPath.StartsWith(Path.GetFullPath(ContentPackageManager.VanillaCorePackage!.Dir).CleanUpPathCrossPlatform());
}
// --- Package Content
private Result<T> LoadPackageData<T>(ContentPath contentPath, Func<string, Result<T>> dataLoader)
{
Guard.IsNotNull(contentPath, nameof(contentPath));
Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!IsPackagePathValid(contentPath))
{
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageData)}: The filepath of `{contentPath.FullPath}' is not in a package directory!");
}
return dataLoader(contentPath.FullPath);
}
public Result<XDocument> LoadPackageXml(ContentPath filePath)
=> LoadPackageData(filePath, path => TryLoadXml(filePath.FullPath));
public Result<byte[]> LoadPackageBinary(ContentPath filePath)
=> LoadPackageData(filePath, path => TryLoadBinary(filePath.FullPath));
public Result<string> LoadPackageText(ContentPath filePath)
=> LoadPackageData(filePath, path => TryLoadText(filePath.FullPath));
private ImmutableArray<(ContentPath, Result<T>)> LoadPackageDataFiles<T>(ImmutableArray<ContentPath> filePaths, Func<string, Result<T>> dataLoader)
{
if (filePaths.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!");
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
var builder = ImmutableArray.CreateBuilder<(ContentPath, Result<T>)>();
foreach (var path in filePaths)
{
builder.Add((path, LoadPackageData(path, dataLoader)));
}
return builder.ToImmutable();
}
public ImmutableArray<(ContentPath, Result<XDocument>)> LoadPackageXmlFiles(ImmutableArray<ContentPath> filePaths)
=> LoadPackageDataFiles(filePaths, TryLoadXml);
public ImmutableArray<(ContentPath, Result<byte[]>)> LoadPackageBinaryFiles(ImmutableArray<ContentPath> filePaths)
=> LoadPackageDataFiles(filePaths, TryLoadBinary);
public ImmutableArray<(ContentPath, Result<string>)> LoadPackageTextFiles(ImmutableArray<ContentPath> filePaths)
=> LoadPackageDataFiles(filePaths, TryLoadText);
public Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively)
{
Guard.IsNotNull(package, nameof(package));
try
{
var cp = ContentPath.FromRaw(package, package.Dir);
var fullPath = localSubfolder.IsNullOrWhiteSpace()
? Path.GetFullPath(cp.FullPath)
: Path.GetFullPath(localSubfolder, cp.FullPath);
return System.IO.Directory.GetFiles(fullPath, regexFilter,
searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
.ToImmutableArray();
}
catch (Exception e)
{
if (e is ArgumentNullException or ArgumentException)
throw;
return FluentResults.Result.Fail(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
}
private async Task<Result<T>> LoadPackageDataAsync<T>(ContentPath contentPath, Func<string, Task<Result<T>>> dataLoader)
{
Guard.IsNotNull(contentPath, nameof(contentPath));
Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (!IsPackagePathValid(contentPath))
{
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageDataAsync)}: The filepath of `{contentPath.FullPath}' is not in a package directory!");
}
return await dataLoader(contentPath.FullPath);
}
public async Task<Result<XDocument>> LoadPackageXmlAsync(ContentPath filePath)
=> await LoadPackageDataAsync(filePath, async path => await TryLoadXmlAsync(path));
public async Task<Result<byte[]>> LoadPackageBinaryAsync(ContentPath filePath)
=> await LoadPackageDataAsync(filePath, async path => await TryLoadBinaryAsync(path));
public async Task<Result<string>> LoadPackageTextAsync(ContentPath filePath)
=> await LoadPackageDataAsync(filePath, async path => await TryLoadTextAsync(path));
private async Task<ImmutableArray<(ContentPath, Result<T>)>> LoadPackageDataFilesAsync<T>(
ImmutableArray<ContentPath> filePaths, Func<string, Task<Result<T>>> dataLoader)
{
if (filePaths.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!");
}
using var lck = await OperationsLock.AcquireReaderLock();
var builder = ImmutableArray.CreateBuilder<(ContentPath, Result<T>)>();
foreach (var path in filePaths)
{
builder.Add((path, await LoadPackageDataAsync(path, dataLoader)));
}
return builder.ToImmutable();
}
public async Task<ImmutableArray<(ContentPath, Result<XDocument>)>> LoadPackageXmlFilesAsync(ImmutableArray<ContentPath> filePaths)
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadXmlAsync(path));
public async Task<ImmutableArray<(ContentPath, Result<byte[]>)>> LoadPackageBinaryFilesAsync(ImmutableArray<ContentPath> filePaths)
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadBinaryAsync(path));
public async Task<ImmutableArray<(ContentPath, Result<string>)>> LoadPackageTextFilesAsync(ImmutableArray<ContentPath> filePaths)
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadTextAsync(path));
private int _useCaching;
public bool UseCaching
{
get => ModUtils.Threading.GetBool(ref _useCaching);
set => ModUtils.Threading.SetBool(ref _useCaching, value);
}
// Method group redirect
private FluentResults.Result<XDocument> TryLoadXml(string filePath) => TryLoadXml(filePath, null);
public virtual FluentResults.Result<XDocument> TryLoadXml(string filePath, Encoding encoding)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var r = TryLoadText(filePath, encoding);
if (r is { IsSuccess: true, Value: not null })
return XDocument.Parse(r.Value);
else
{
return r.ToResult<XDocument>(s => null)
.WithError(GetGeneralError(nameof(LoadLocalXml), filePath));
}
}
// Method group redirect
private FluentResults.Result<string> TryLoadText(string filePath) => TryLoadText(filePath, null);
public virtual FluentResults.Result<string> TryLoadText(string filePath, Encoding encoding)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadText)}: File '{filePath}' is not allowed.");
}
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 virtual FluentResults.Result<byte[]> TryLoadBinary(string filePath)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadBinary)}: File '{filePath}' is not allowed.");
}
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 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)
{
Guard.IsNotNullOrWhiteSpace(text, nameof(text));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveText)}: File '{filePath}' is not allowed.");
}
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);
Directory.CreateDirectory(Path.GetDirectoryName(fp)!);
System.IO.File.WriteAllText(fp, t, encoding ?? Encoding.UTF8);
if (UseCaching)
_fsCache[filePath] = t;
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
});
}
public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
Guard.IsNotNull(bytes, nameof(bytes));
Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveBinary)}: File '{filePath}' is not allowed.");
}
byte[] b = new byte[bytes.Length];
System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length);
return IOExceptionsOperationRunner(nameof(TrySaveBinary), filePath, () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
Directory.CreateDirectory(Path.GetDirectoryName(fp)!);
System.IO.File.WriteAllBytes(fp, b);
if (UseCaching)
_fsCache[filePath] = b;
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
});
}
public virtual FluentResults.Result<bool> FileExists(string filePath)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
IService.CheckDisposed(this);
// lock not needed
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(FileExists)}: File '{filePath}' is not allowed.");
}
return IOExceptionsOperationRunner<bool>(nameof(FileExists), filePath, () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
return System.IO.File.Exists(fp);
});
}
public virtual FluentResults.Result<bool> DirectoryExists(string directoryPath)
{
Guard.IsNotNullOrWhiteSpace(directoryPath, nameof(directoryPath));
IService.CheckDisposed(this);
// lock not needed
if (IsReadOperationAllowedEval?.Invoke(directoryPath) is not true)
{
return FluentResults.Result.Fail($"{nameof(DirectoryExists)}: File '{directoryPath}' is not allowed.");
}
try
{
var di = new DirectoryInfo(directoryPath);
return di.Exists;
}
catch (Exception ex)
{
return new FluentResults.Result<bool>().WithError(ex.Message);
}
}
public virtual async Task<FluentResults.Result<XDocument>> TryLoadXmlAsync(string filePath, Encoding encoding = null)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadXmlAsync)}: File '{filePath}' is not allowed.");
}
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);
var doc = await XDocument.LoadAsync(fs, LoadOptions.PreserveWhitespace, CancellationToken.None);
if (UseCaching)
_fsCache[filePath] = doc;
return FluentResults.Result.Ok(doc);
}
catch (Exception e)
{
return FluentResults.Result.Fail<XDocument>(GetGeneralError(nameof(TryLoadXmlAsync), filePath));
}
}
public virtual async Task<FluentResults.Result<string>> TryLoadTextAsync(string filePath, Encoding encoding = null)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadTextAsync)}: File '{filePath}' is not allowed.");
}
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);
var txt = await System.IO.File.ReadAllTextAsync(fp);
if (UseCaching)
_fsCache[filePath] = txt;
return FluentResults.Result.Ok(txt);
});
}
public virtual async Task<FluentResults.Result<byte[]>> TryLoadBinaryAsync(string filePath)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadBinaryAsync)}: File '{filePath}' is not allowed.");
}
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();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
return await System.IO.File.ReadAllBytesAsync(fp);
});
}
// method group overload
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)
{
Guard.IsNotNullOrWhiteSpace(text, nameof(text));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveTextAsync)}: File '{filePath}' is not allowed.");
}
string t = text.ToString(); //copy
return await IOExceptionsOperationRunnerAsync(nameof(TrySaveText), filePath, async () =>
{
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 virtual async Task<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
Guard.IsNotNull(bytes, nameof(bytes));
Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveBinaryAsync)}: File '{filePath}' is not allowed.");
}
byte[] b = new byte[bytes.Length];
System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length);
return await IOExceptionsOperationRunnerAsync(nameof(TrySaveBinary), filePath, async () =>
{
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");
});
}
private async Task<FluentResults.Result<T>> IOExceptionsOperationRunnerAsync<T>(string funcName, string filepath, Func<Task<FluentResults.Result<T>>> operation)
{
try
{
return await operation?.Invoke()!;
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private async Task<FluentResults.Result> IOExceptionsOperationRunnerAsync(string funcName, string filepath, Func<Task<FluentResults.Result>> operation)
{
try
{
return await operation?.Invoke()!;
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private FluentResults.Result<T> IOExceptionsOperationRunner<T>(string funcName, string filepath, Func<FluentResults.Result<T>> operation)
{
try
{
return operation?.Invoke();
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private FluentResults.Result IOExceptionsOperationRunner(string funcName, string filepath, Func<FluentResults.Result> operation)
{
try
{
return operation?.Invoke();
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private Error GetGeneralError(string funcName, string localfp, ContentPackage package) =>
new Error($"{funcName}: Failed to load local file.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, localfp)
.WithMetadata(MetadataType.RootObject, package);
private Error GetGeneralError(string funcName, string localfp) =>
new Error($"{funcName}: Failed to load local file.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, localfp);
private FluentResults.Result<TReturn> ReturnException<TReturn, TException>(TException exception, ContentPackage package) where TException : Exception
{
return new FluentResults.Result<TReturn>().WithError(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
private FluentResults.Result ReturnException<TException>(TException exception, ContentPackage package) where TException : Exception
{
return new FluentResults.Result().WithError(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
private FluentResults.Result ReturnException<TException>(TException exception, string filePath) where TException : Exception
{
return new FluentResults.Result().WithError(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, filePath));
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using OneOf;
// ReSharper disable InconsistentNaming
namespace Barotrauma.LuaCs;
public interface IAssemblyManagementService : IPluginManagementService
{
/// <summary>
/// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied.
/// </summary>
/// <param name="assemblyName">The assembly info.</param>
/// <param name="excludedContexts">Guids of excluded contexts.</param>
/// <returns><b>On Success:</b> The assembly. <br/><b>On Failure:</b> nothing.</returns>
FluentResults.Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts);
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using FluentResults;
namespace Barotrauma.LuaCs;
public partial interface IConfigService : IReusableService, ILuaConfigService
{
void RegisterSettingTypeInitializer<T>(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory)
where T : class, ISettingBase;
Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources);
Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigResourceInfo> configProfileResources);
FluentResults.Result LoadSavedConfigsValues();
FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName);
FluentResults.Result DisposePackageData(ContentPackage package);
FluentResults.Result DisposeAllPackageData();
}
@@ -0,0 +1,16 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma.LuaCs;
public interface IConsoleCommandsService : IService
{
void RegisterCommand(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false);
void AssignOnExecute(string names, Action<string[]> onExecute);
#if SERVER
internal void AssignOnClientRequestExecute(string names, Action<Client, Vector2, string[]> onClientRequestExecute);
#endif
void RemoveCommand(string name);
void RemoveRegisteredCommands();
}
@@ -0,0 +1,52 @@
using System;
using System.Reflection;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.LuaCs;
namespace Barotrauma.LuaCs;
public interface IEventService : IReusableService, ILuaEventService
{
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result Subscribe<T>(T subscriber) where T : class, IEvent<T>;
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <typeparam name="T"></typeparam>
void Unsubscribe<T>(T subscriber) where T : class, IEvent;
/// <summary>
/// Clears all subscribers for a given event type and removes any registration to the type.
/// </summary>
/// <typeparam name="T">The event type.</typeparam>
void ClearAllEventSubscribers<T>() where T : class, IEvent;
/// <summary>
/// Clears all subscribers lists.
/// </summary>
void ClearAllSubscribers();
/// <summary>
/// Invokes all alive subscribers of the given event using the provided invocation factory.
/// </summary>
/// <param name="action"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result PublishEvent<T>(Action<T> action) where T : class, IEvent<T>;
/// <summary>
/// Adds an event service that will receive all published events.
/// </summary>
/// <param name="eventService"></param>
void AddDispatcherEventService(IEventService eventService);
/// <summary>
/// Removes an event service from the dispatcher list.
/// </summary>
/// <param name="eventService"></param>
void RemoveDispatcherEventService(IEventService eventService);
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FluentResults;
namespace Barotrauma.LuaCs;
public interface IParserService<in TSrc, TOut> : IService
{
Result<TOut> TryParseResource(TSrc src);
ImmutableArray<Result<TOut>> TryParseResources(IEnumerable<TSrc> sources);
}
public interface IParserServiceAsync<in TSrc, TOut> : IService
{
Task<Result<TOut>> TryParseResourceAsync(TSrc src);
Task<ImmutableArray<Result<TOut>>> TryParseResourcesAsync(IEnumerable<TSrc> sources);
}
public interface IParserServiceOneToManyAsync<in TSrc, TOut> : IService
{
Task<Result<ImmutableArray<TOut>>> TryParseResourcesAsync(TSrc src);
}
@@ -0,0 +1,59 @@
using System;
using Barotrauma.Networking;
using FluentResults;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs;
public readonly record struct PendingLog(string Message, Color? Color, ServerLog.MessageType MessageType);
public interface ILoggerSubscriber
{
void OnLog(PendingLog pendingLog);
}
/// <summary>
/// Provides console and debug logging services
/// </summary>
public interface ILoggerService : IReusableService
{
void Subscribe(ILoggerSubscriber subscriber);
void Unsubscribe(ILoggerSubscriber subscriber);
void ProcessLogs();
void HandleException(Exception exception, string prefix = null);
void LogError(string message);
void LogWarning(string message);
void LogMessage(string message, Color? serverColor = null, Color? clientColor = null);
void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage);
void LogResults(FluentResults.Result result);
#region DebugBuilds
void LogDebug(string message, Color? color = null);
void LogDebugWarning(string message);
void LogDebugError(string message);
#endregion
#region LegacyCompat_LuaCsLogger
public void HandleException(Exception ex, LuaCsMessageOrigin origin)
{
HandleException(ex, origin.ToString());
}
public void LogError(string message, LuaCsMessageOrigin origin)
{
LogError(message);
}
#endregion
}
public enum LuaCsMessageOrigin
{
LuaCs,
Unknown,
LuaMod,
CSharpMod,
}
@@ -0,0 +1,32 @@
namespace Barotrauma.LuaCs;
/// <summary>
/// Provides access to data from the current <see cref="LuaCsSetup"/>.
/// </summary>
public interface ILuaCsInfoProvider : IService
{
/// <summary>
/// Whether C# plugin code is enabled.
/// </summary>
public bool IsCsEnabled { get; }
/// <summary>
/// Whether usernames are anonymized or show in logs.
/// </summary>
public bool HideUserNamesInLogs { get; }
/// <summary>
/// Whether file system caching is enabled.
/// </summary>
public bool UseCaching { get; }
/// <summary>
/// The current state of the Execution State Machine.
/// </summary>
public RunState CurrentRunState { get; }
/// <summary>
/// Returns the best-matching LuaCsForBarotrauma package (enabled list > localMods > WorkshopMods).
/// </summary>
public ContentPackage LuaCsForBarotraumaPackage { get; }
}
@@ -0,0 +1,66 @@
#nullable enable
using System.Collections.Immutable;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs;
public interface ILuaScriptManagementService : IReusableService
{
/// <summary>
/// The running <see cref="Script"/> instance, if available.
/// </summary>
/// <remarks>
/// It is recommended to avoid using this directly if another API is available for the intended purposes.
/// </remarks>
Script? InternalScript { get; }
object? GetGlobalTableValue(string tableName);
FluentResults.Result<DynValue> DoString(string code);
DynValue? CallFunctionSafe(object luaFunction, params object[] args);
/// <summary>
/// Whether to enable/disable the file system caching for lua.
/// </summary>
/// <param name="useCaching"></param>
void SetCachingPolicy(bool useCaching);
/// <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);
/// <summary>
/// Executes already loaded into memory scripts data, in the supplied order.
/// </summary>
/// <param name="executionOrder"></param>
/// <returns></returns>
// [Required]
FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaScriptResourceInfo> executionOrder, bool enableSandbox);
/// <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();
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using FluentResults;
namespace Barotrauma.LuaCs;
public interface IModConfigService : IService
{
/// <summary>
/// Loads or dynamically generates a <see cref="IModConfigInfo"/> for the given <see cref="ContentPackage"/>.
/// <br/> Throws a <see cref="NullReferenceException"/> if the package is null.
/// </summary>
/// <param name="src"></param>
/// <returns></returns>
Task<Result<IModConfigInfo>> CreateConfigAsync([NotNull]ContentPackage src);
Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src);
}
@@ -0,0 +1,39 @@
using System;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs;
#if CLIENT
public delegate void NetMessageReceived(IReadMessage netMessage);
#elif SERVER
internal delegate void NetMessageReceived(IReadMessage netMessage, Client connection);
#endif
internal interface INetworkingService : IReusableService, ILuaCsNetworking, IEntityNetworkingService
{
bool IsActive { get; }
bool IsSynchronized { get; }
IWriteMessage Start(string netId);
IWriteMessage Start(Guid netId);
void Receive(string netId, NetMessageReceived action);
void Receive(Guid netId, NetMessageReceived action);
#if SERVER
void SendToClient(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#elif CLIENT
void SendToServer(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#endif
}
public interface IEntityNetworkingService
{
Guid GetNetworkIdForInstance(INetworkSyncVar var);
void RegisterNetVar(INetworkSyncVar netVar);
void DeregisterNetVar(INetworkSyncVar netVar);
void SendNetVar(INetworkSyncVar netVar);
void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection);
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading.Tasks;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using FluentResults;
namespace Barotrauma.LuaCs;
public interface IPackageManagementService : IReusableService
{
public bool TryGetLoadedPackageByName(string name, out ContentPackage package);
public FluentResults.Result LoadPackageInfo(ContentPackage package);
public FluentResults.Result LoadPackagesInfo(ImmutableArray<ContentPackage> packages);
public FluentResults.Result ExecuteLoadedPackages(ImmutableArray<ContentPackage> executionOrder, bool executeCsAssemblies);
public FluentResults.Result SyncLoadedPackagesList(ImmutableArray<ContentPackage> packages);
public FluentResults.Result StopRunningPackages();
public FluentResults.Result UnloadPackage(ContentPackage package);
public FluentResults.Result UnloadPackages(ImmutableArray<ContentPackage> packages);
public FluentResults.Result UnloadAllPackages();
public ImmutableArray<ContentPackage> GetAllLoadedPackages();
public ImmutableArray<ContentPackage> GetLoadedUnrestrictedPackages();
public bool IsPackageRunning(ContentPackage package);
public bool IsAnyPackageLoaded();
public bool IsAnyPackageRunning();
public bool PackageContainsAnyRunnableResource(ContentPackage package);
public Result<IModConfigInfo> GetModConfigForPackage(ContentPackage package);
}
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Barotrauma.LuaCs.Data;
using Microsoft.CodeAnalysis;
namespace Barotrauma.LuaCs;
public interface IPluginManagementService : IReusableService
{
/// <summary>
/// Gets all types in searched <see cref="IAssemblyLoaderService"/> that implement the type supplied.
/// </summary>
/// <param name="includeInterfaces"></param>
/// <param name="includeAbstractTypes"></param>
/// <param name="includeDefaultContext"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result<ImmutableArray<Type>> GetImplementingTypes<T>(
bool includeInterfaces = false,
bool includeAbstractTypes = false,
bool includeDefaultContext = true);
/// <summary>
/// Gets the <see cref="ContentPackage"/> that contains the plugin type.
/// </summary>
/// <param name="ownerPackage"></param>
/// <typeparam name="TPlugin"></typeparam>
/// <returns></returns>
bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage);
/// <summary>
/// Tries to find the type given the fully qualified name and filters.
/// </summary>
/// <param name="typeName"></param>
/// <param name="isByRefType"></param>
/// <param name="includeInterfaces"></param>
/// <param name="includeDefaultContext"></param>
/// <returns></returns>
Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, bool includeDefaultContext = true);
/// <summary>
///
/// </summary>
/// <param name="executionOrder"></param>
/// <param name="excludeAlreadyRunningPackages"></param>
/// <returns></returns>
FluentResults.Result ActivatePluginInstances(ImmutableArray<ContentPackage> executionOrder, bool excludeAlreadyRunningPackages = true);
/// <summary>
/// Loads the provided assembly resources in the order of their dependencies and intra-mod priority load order.
/// </summary>
/// <param name="resources"></param>
/// <returns>Success/Failure and list of failed resources, if any.</returns>
FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources);
/// <summary>
/// Unloads all managed <see cref="IAssemblyPlugin"/>, <see cref="Assembly"/>, and <see cref="IAssemblyLoaderService"/>s.
/// </summary>
/// <returns>Success of the operation. <br/><b>Note: does not guarantee .NET runtime assembly unloading success.</b></returns>
FluentResults.Result UnloadManagedAssemblies();
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs;
public interface IPluginService : IReusableService
{
bool IsAssemblyLoaded(string friendlyName);
/// <summary>
/// Loads the assemblies for the given information
/// </summary>
/// <param name="assemblyResourcesInfo"></param>
/// <param name="injectServices"></param>
/// <param name="typeInstances"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result LoadAndInstanceTypes<T>(IEnumerable<IAssemblyResourceInfo> assemblyResourcesInfo, bool injectServices, out ImmutableArray<T> typeInstances) where T : class, IAssemblyPlugin;
FluentResults.Result<ImmutableArray<T>> GetLoadedPluginTypesInPackage<T>() where T : class, IAssemblyPlugin;
/// <summary>
/// Advances the loading/execution state of the plugin. IMPORTANT: You cannot set the execution state of plugins
/// to 'Disposed'. You must instead call the 'DisposePlugins' method.
/// </summary>
/// <param name="newState"></param>
/// <returns></returns>
FluentResults.Result AdvancePluginStates(PluginRunState newState);
/// <summary>
/// Disposes of all running plugins hosted by the service and releases their references to allow unloading.
/// </summary>
/// <returns>Success of the operation. Returns false if any plugin threw errors during disposal.</returns>
FluentResults.Result DisposePlugins();
/// <summary>
/// Gets the current plugin execution state.
/// </summary>
/// <returns></returns>
PluginRunState GetPluginRunState();
}
public enum PluginRunState
{
Instanced=0,
PreInitialization=1,
Initialized=2,
LoadingCompleted=3,
Disposed=4
}
@@ -0,0 +1,54 @@
using System.Collections.Immutable;
namespace Barotrauma.LuaCs;
public interface ISafeStorageService : IStorageService, ISafeStorageValidation { }
public interface ISafeStorageValidation
{
/// <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">The path to the file, exactly as it will be passed to the Try(Load|Save) methods in <see cref="StorageService"/>.</param>
/// <param name="readOnly">Whether to add it to the read whitelist only, or Read+Write whitelists.</param>
void AddFileToWhitelist(string path, bool readOnly = true);
/// <summary>
/// Adds the given collection of file paths to whitelists (Read|+Write)
/// </summary>
/// <param name="paths">The paths to the files, formatted exactly as it will be passed to the Try(Load|Save) methods in <see cref="StorageService"/>.</param>
/// <param name="readOnly">Whether to add it to the read whitelist only, or Read+Write whitelists.</param>
void AddFilesToWhitelist(ImmutableArray<string> paths, 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. Overwrites previous list.
/// </summary>
/// <param name="filePaths">List of file paths allowed, as will be passed to the <see cref="StorageService"/> Try(Load|Save) methods.</param>
FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths);
/// <summary>
/// Sets the whitelist filtering for read & write file permissions for the instance. Overwrites previous lists.
/// </summary>
/// <param name="filePaths">List of file paths allowed, as will be passed to the <see cref="StorageService"/> Try(Load|Save) methods.</param>
FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths);
/// <summary>
/// Deletes all paths from all white lists.
/// </summary>
void ClearAllWhitelists();
}
@@ -0,0 +1,43 @@
using System;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
/// <summary>
/// Represents a <see cref="IReusableService"/> that is automatically instantiated at startup for the lifetime of the
/// <see cref="IServiceProvider"/> instance.
/// </summary>
public interface ISystem : IReusableService { }
/// <summary>
/// Defines a service that can be reset to it's post-constructor state and reused without needing to be disposed.
/// Intended for persistent services.
/// </summary>
public interface IReusableService : IService
{
/// <summary>
/// Returns the service to its original state (post-instantiation).
/// Allows a service instance to be reused without disposing of the instance.
/// </summary>
FluentResults.Result Reset();
}
/// <summary>
/// Base interface inherited by all services.
/// </summary>
/// <exception cref="ObjectDisposedException">Throws exception if `IsDisposed` return true.</exception>
public interface IService : IDisposable
{
bool IsDisposed { get; }
public void CheckDisposed()
{
if (IsDisposed)
ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!");
}
static void CheckDisposed(IService service)
{
if (service.IsDisposed)
ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{service.GetType().Name}'!");
}
}
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using LightInject;
namespace Barotrauma.LuaCs;
/// <summary>
/// Provides instancing and management of <see cref="IService"/>, <see cref="IReusableService"/>, and <see cref="ISystem"/>
/// instances.
/// </summary>
public interface IServicesProvider
{
#region Type_Registration
/// <summary>
/// Registers a type as a service for a given interface.
/// </summary>
/// <remarks>NOTE: <see cref="ISystem"/> services are forced to <see cref="ServiceLifetime.Singleton"/></remarks>
/// <param name="lifetime">The <see cref="ServiceLifetime"/> of the service when requested.</param>
/// <param name="lifetimeInstance">Custom lifetime instance.</param>
/// <typeparam name="TSvcInterface">Service interface.</typeparam>
/// <typeparam name="TService">Implementing service type.</typeparam>
void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
/// <summary>
/// Registers a type as a service for a given interface that can be requested by name.
/// </summary>
/// <remarks>NOTE: <see cref="ISystem"/> services are forced to <see cref="ServiceLifetime.Singleton"/></remarks>
/// <param name="name">Name of the service for lookup.</param>
/// <param name="lifetime">The <see cref="ServiceLifetime"/> of the service when requested.</param>
/// <param name="lifetimeInstance">Custom lifetime instance.</param>
/// <typeparam name="TSvcInterface">Service interface.</typeparam>
/// <typeparam name="TService">Implementing service type.</typeparam>
void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
/// <summary>
/// Registers a factory for resolving the service type.
/// </summary>
/// <param name="factory"></param>
/// <typeparam name="TSvcInterface"></typeparam>
void RegisterServiceResolver<TSvcInterface>(Func<ServiceContainer, TSvcInterface> factory) where TSvcInterface : class, IService;
/// <summary>
/// Compiles/Generates IL for registered services and instantiates all registered <see cref="ISystem"/> types.
/// </summary>
public void CompileAndRun();
#endregion
#region Services_Instancing_Injection
/// <summary>
/// Injects services into the properties of already instanced objects.
/// </summary>
/// <param name="inst"></param>
/// <typeparam name="T"></typeparam>
void InjectServices<T>(T inst) where T : class;
/// <summary>
/// Tries to get a service for the given interface, returns success/failure.
/// </summary>
/// <param name="service"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService;
/// <summary>
/// Tries to get a service for the given interface, throws an exception upon failure.
/// </summary>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
TSvcInterface GetService<TSvcInterface>() where TSvcInterface : class, IService;
/// <summary>
/// Tries to get a service for the given name and interface, returns success/failure.
/// </summary>
/// <param name="name"></param>
/// <param name="service"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService;
/// <summary>
/// Called whenever a new service is created/instanced.
/// Args[0]: The interface type of the service.
/// Args[1]: The instance of the service.
/// </summary>
event System.Action<Type, IService> OnServiceInstanced;
#endregion
#region ActiveServices
/// <summary>
/// Returns all services for the given interface.
/// </summary>
/// <typeparam name="TSvc"></typeparam>
/// <returns></returns>
ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService;
#endregion
// Notes: Left public due to the common use of Publicizers
#region Internal_Use
/// <summary>
/// Notes: Internal use only if hosted by LuaCsForBarotrauma. Disposes of all services and resets DI container. Warning: unable to dispose of services held by other objects.
/// </summary>
void DisposeAndReset();
#endregion
}
public enum ServiceLifetime
{
Transient, Singleton, PerThread, Invalid, Custom
}
@@ -0,0 +1,84 @@
using System;
using System.Collections.Immutable;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using FluentResults;
namespace Barotrauma.LuaCs;
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);
FluentResults.Result<string> LoadLocalText(ContentPackage package, string localFilePath);
FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document);
FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes);
FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text);
// async
Task<FluentResults.Result<XDocument>> LoadLocalXmlAsync(ContentPackage package, string localFilePath);
Task<FluentResults.Result<byte[]>> LoadLocalBinaryAsync(ContentPackage package, string localFilePath);
Task<FluentResults.Result<string>> LoadLocalTextAsync(ContentPackage package, string localFilePath);
Task<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document);
Task<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes);
Task<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text);
// -- package directory
// singles
Result<XDocument> LoadPackageXml(ContentPath filePath);
Result<byte[]> LoadPackageBinary(ContentPath filePath);
Result<string> LoadPackageText(ContentPath filePath);
// collections
ImmutableArray<(ContentPath, Result<XDocument>)> LoadPackageXmlFiles(ImmutableArray<ContentPath> filePaths);
ImmutableArray<(ContentPath, Result<byte[]>)> LoadPackageBinaryFiles(ImmutableArray<ContentPath> filePaths);
ImmutableArray<(ContentPath, Result<string>)> LoadPackageTextFiles(ImmutableArray<ContentPath> filePaths);
FluentResults.Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively);
// async
// singles
Task<Result<XDocument>> LoadPackageXmlAsync(ContentPath filePath);
Task<Result<byte[]>> LoadPackageBinaryAsync(ContentPath filePath);
Task<Result<string>> LoadPackageTextAsync(ContentPath filePath);
// collections
Task<ImmutableArray<(ContentPath, Result<XDocument>)>> LoadPackageXmlFilesAsync(ImmutableArray<ContentPath> filePaths);
Task<ImmutableArray<(ContentPath, Result<byte[]>)>> LoadPackageBinaryFilesAsync(ImmutableArray<ContentPath> filePaths);
Task<ImmutableArray<(ContentPath, Result<string>)>> LoadPackageTextFilesAsync(ImmutableArray<ContentPath> filePaths);
// -- absolute paths
FluentResults.Result<XDocument> TryLoadXml(string filePath, Encoding encoding = null);
FluentResults.Result<string> TryLoadText(string filePath, Encoding encoding = null);
FluentResults.Result<byte[]> TryLoadBinary(string filePath);
FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null);
FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null);
FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes);
FluentResults.Result<bool> FileExists(string filePath);
FluentResults.Result<bool> DirectoryExists(string directoryPath);
//async
Task<FluentResults.Result<XDocument>> TryLoadXmlAsync(string filePath, Encoding encoding = null);
Task<FluentResults.Result<string>> TryLoadTextAsync(string filePath, Encoding encoding = null);
Task<FluentResults.Result<byte[]>> TryLoadBinaryAsync(string filePath);
Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null);
Task<FluentResults.Result> TrySaveTextAsync(string filePath, string text, Encoding encoding = null);
Task<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes);
}
@@ -0,0 +1,240 @@
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using MoonSharp.Interpreter.Interop.BasicDescriptors;
using Sigil;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Barotrauma.LuaCs;
public interface IDefaultLuaRegistrar : IService
{
public void RegisterAll();
}
public class DefaultLuaRegistrar : IDefaultLuaRegistrar
{
public bool IsDisposed { get; private set; }
private readonly ILuaUserDataService _userDataService;
private readonly ISafeLuaUserDataService _safeUserDataService;
private readonly ILoggerService _loggerService;
private class SteamIDMemberDescriptor : IMemberDescriptor
{
public bool IsStatic => false;
public string Name => "SteamID";
public MemberDescriptorAccess MemberAccess => MemberDescriptorAccess.CanRead;
public DynValue GetValue(Script script, object obj)
{
if (obj is Client client)
{
return DynValue.FromObject(script, ModUtils.Client.GetSteamId(client));
}
throw new System.NotImplementedException();
}
public void SetValue(Script script, object obj, DynValue value)
{
throw new System.NotImplementedException();
}
}
public DefaultLuaRegistrar(ILoggerService loggerService, ILuaUserDataService userDataService, ISafeLuaUserDataService safeUserDataService)
{
_userDataService = userDataService;
_safeUserDataService = safeUserDataService;
_loggerService = loggerService;
}
private void RegisterShared()
{
_userDataService.RegisterType("System.TimeSpan");
_userDataService.RegisterType("System.Exception");
_userDataService.RegisterType("System.Console");
_userDataService.RegisterType("System.Exception");
_userDataService.RegisterType("Barotrauma.Success`2");
_userDataService.RegisterType("Barotrauma.Failure`2");
_userDataService.RegisterType("Barotrauma.Range`1");
_userDataService.RegisterType("Barotrauma.ItemPrefab");
_userDataService.RegisterType("Barotrauma.InputType");
List<Assembly> assembliesToScan = [typeof(DefaultLuaRegistrar).Assembly, typeof(Identifier).Assembly, typeof(Microsoft.Xna.Framework.Vector2).Assembly];
foreach (var type in assembliesToScan.SelectMany(a => a.GetTypes()))
{
if (type.IsEnum || type.Name.StartsWith("<") || type.IsDefined(typeof(CompilerGeneratedAttribute)) || !_safeUserDataService.IsAllowed(type.FullName))
{
continue;
}
_userDataService.RegisterType(type.FullName);
}
_userDataService.RegisterType("Barotrauma.LuaSByte");
_userDataService.RegisterType("Barotrauma.LuaByte");
_userDataService.RegisterType("Barotrauma.LuaInt16");
_userDataService.RegisterType("Barotrauma.LuaUInt16");
_userDataService.RegisterType("Barotrauma.LuaInt32");
_userDataService.RegisterType("Barotrauma.LuaUInt32");
_userDataService.RegisterType("Barotrauma.LuaInt64");
_userDataService.RegisterType("Barotrauma.LuaUInt64");
_userDataService.RegisterType("Barotrauma.LuaSingle");
_userDataService.RegisterType("Barotrauma.LuaDouble");
_userDataService.RegisterType("Barotrauma.Level+InterestingPosition");
_userDataService.RegisterType("Barotrauma.Networking.RespawnManager+TeamSpecificState");
_userDataService.RegisterType("Barotrauma.CharacterParams+AIParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+TargetParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+InventoryParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+HealthParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+ParticleParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+SoundParams");
_userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByIdentifier");
_userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByTag");
_userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.StatusEffect"), "user");
_userDataService.RegisterType("Barotrauma.ContentPackageManager+PackageSource");
_userDataService.RegisterType("Barotrauma.ContentPackageManager+EnabledPackages");
_userDataService.RegisterType("System.Xml.Linq.XElement");
_userDataService.RegisterType("System.Xml.Linq.XName");
_userDataService.RegisterType("System.Xml.Linq.XAttribute");
_userDataService.RegisterType("System.Xml.Linq.XContainer");
_userDataService.RegisterType("System.Xml.Linq.XDocument");
_userDataService.RegisterType("System.Xml.Linq.XNode");
_userDataService.RegisterType("Barotrauma.Networking.ServerSettings+SavedClientPermission");
_userDataService.RegisterType("Barotrauma.Inventory+ItemSlot");
_userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface"), "customInterfaceElementList");
_userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface+CustomInterfaceElement");
_userDataService.RegisterType("Barotrauma.DebugConsole+Command");
{
var descriptor = _userDataService.RegisterType("Barotrauma.NetLobbyScreen");
#if SERVER
_userDataService.MakeFieldAccessible(descriptor, "subs");
#endif
}
_userDataService.RegisterType("FarseerPhysics.Dynamics.Body");
_userDataService.RegisterType("FarseerPhysics.Dynamics.World");
_userDataService.RegisterType("FarseerPhysics.Dynamics.Fixture");
_userDataService.RegisterType("FarseerPhysics.ConvertUnits");
_userDataService.RegisterType("FarseerPhysics.Collision.AABB");
_userDataService.RegisterType("FarseerPhysics.Collision.ContactFeature");
_userDataService.RegisterType("FarseerPhysics.Collision.ManifoldPoint");
_userDataService.RegisterType("FarseerPhysics.Collision.ContactID");
_userDataService.RegisterType("FarseerPhysics.Collision.Manifold");
_userDataService.RegisterType("FarseerPhysics.Collision.RayCastInput");
_userDataService.RegisterType("FarseerPhysics.Collision.ClipVertex");
_userDataService.RegisterType("FarseerPhysics.Collision.RayCastOutput");
_userDataService.RegisterType("FarseerPhysics.Collision.EPAxis");
_userDataService.RegisterType("FarseerPhysics.Collision.ReferenceFace");
_userDataService.RegisterType("FarseerPhysics.Collision.Collision");
_userDataService.RegisterType("Voronoi2.DoubleVector2");
_userDataService.RegisterType("Voronoi2.Site");
_userDataService.RegisterType("Voronoi2.Edge");
_userDataService.RegisterType("Voronoi2.Halfedge");
_userDataService.RegisterType("Voronoi2.VoronoiCell");
_userDataService.RegisterType("Voronoi2.GraphEdge");
_userDataService.RegisterType("Barotrauma.PrefabCollection`1");
_userDataService.RegisterType("Barotrauma.PrefabSelector`1");
_userDataService.RegisterType("Barotrauma.Pair`2");
_userDataService.RegisterExtensionType("Barotrauma.MathUtils");
_userDataService.RegisterExtensionType("Barotrauma.XMLExtensions");
var itemPrefabDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.ItemPrefab");
itemPrefabDescriptor.AddMember("GetItemPrefab", new MethodMemberDescriptor(typeof(ModUtils.ItemPrefab).GetMethod(nameof(ModUtils.ItemPrefab.GetItemPrefab), BindingFlags.NonPublic | BindingFlags.Static)));
var clientDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.Networking.Client");
clientDescriptor.AddMember("ClientList", new PropertyMemberDescriptor(typeof(ModUtils.Client).GetProperty(nameof(ModUtils.Client.ClientList), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
clientDescriptor.AddMember("SteamID", new SteamIDMemberDescriptor());
#if SERVER
clientDescriptor.AddMember("UnbanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.UnbanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
clientDescriptor.AddMember("BanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.BanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
#endif
_userDataService.RegisterExtensionType(typeof(ClientExtensions).FullName);
_userDataService.RegisterExtensionType(typeof(ItemExtensions).FullName);
_userDataService.RegisterExtensionType(typeof(MapEntityExtensions).FullName);
_userDataService.RegisterExtensionType(typeof(QualityExtensions).FullName);
var toolBox = UserData.RegisterType(typeof(ToolBox));
#if CLIENT
_userDataService.RemoveMember(toolBox, "OpenFileWithShell");
#endif
}
#if CLIENT
private void RegisterClient()
{
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Effect");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameterCollection");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameter");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.SpriteBatch");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Texture2D");
_userDataService.RegisterType("EventInput.KeyboardDispatcher");
_userDataService.RegisterType("EventInput.KeyEventArgs");
_userDataService.RegisterType("Microsoft.Xna.Framework.Input.Keys");
_userDataService.RegisterType("Microsoft.Xna.Framework.Input.KeyboardState");
_userDataService.RegisterType("Barotrauma.Anchor");
_userDataService.RegisterType("Barotrauma.Alignment");
_userDataService.RegisterType("Barotrauma.Pivot");
_userDataService.RegisterType("Barotrauma.Key");
_userDataService.RegisterType("Barotrauma.PlayerInput");
_userDataService.RegisterType("Barotrauma.Inventory+SlotReference");
}
#elif SERVER
private void RegisterServer()
{
_userDataService.RegisterType("Barotrauma.Character+TeamChangeEventData");
}
#endif
public void RegisterAll()
{
RegisterShared();
#if CLIENT
RegisterClient();
#elif SERVER
RegisterServer();
#endif
}
public void Dispose()
{
IsDisposed = true;
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using Barotrauma.LuaCs.Data;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs;
public interface ILuaConfigService : ILuaService
{
FluentResults.Result LoadSavedValueForConfig(ISettingBase setting);
bool TryGetConfig<T>(ContentPackage package, string internalName, out T instance) where T : ISettingBase;
FluentResults.Result SaveConfigValue(ISettingBase setting);
}
@@ -0,0 +1,11 @@
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs;
/// <summary>
/// Service for providing stateful functions and in-memory storage for lua functions
/// </summary>
public interface ILuaDataService : ILuaService
{
}
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs.Compatibility;
namespace Barotrauma.LuaCs;
public interface ILuaSafeEventService : ILuaService, ILuaCsHook
{
/// <summary>
/// Subscribes lua scripts via <see cref="ImpromptuInterface"/> for the given <see cref="IEvent{T}"/> interface.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="identifier"></param>
/// <param name="callbacks">A 'method name'=='signature action' dictionary matching the interface method list.</param>
void Subscribe<T>(string identifier, IDictionary<string, LuaCsFunc> callbacks) where T : class, IEvent<T>;
/// <summary>
/// Removes a subscriber from an event that subscribed under the given identifier.
/// </summary>
/// <param name="eventName"></param>
/// <param name="identifier"></param>
void Unsubscribe(string eventName, string identifier);
/// <summary>
/// Send an event to all subscribers to an interface.
/// </summary>
/// <typeparam name="T">Interface type.</typeparam>
/// <param name="subscriberRunner">Execution runner, the subscriber is provided as the first argument in the lua runner.</param>
/// <returns></returns>
void PublishLuaEvent<T>(LuaCsFunc subscriberRunner) where T : class, IEvent<T>;
/// <summary>
/// Defines the target method name for legacy <see cref="ILuaCsHook.Add(string, LuaCsFunc)"/> to target on new <see cref="IEvent{T}"/>
/// interfaces.
/// </summary>
/// <param name="luaEventName">The <see cref="ILuaCsHook.Add(string, LuaCsFunc)"/> legacy event name.</param>
/// <param name="targetMethod">.</param>
/// <typeparam name="T">The event interface type.</typeparam>
/// <returns>Operation success.</returns>
/// <exception cref="ArgumentNullException">The <see cref="luaEventName"/> is <b>null or empty.</b></exception>
public FluentResults.Result RegisterLuaEventAlias<T>(string luaEventName, string targetMethod) where T : class, IEvent<T>;
}
public interface ILuaEventService : ILuaSafeEventService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaNetworkingService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaPackageManagementService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaPackageService : ILuaService
{
}
@@ -0,0 +1,23 @@
using Barotrauma.LuaCs.Compatibility;
using System;
using System.Reflection;
using static Barotrauma.LuaCs.Compatibility.ILuaCsHook;
using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch;
namespace Barotrauma.LuaCs;
public interface ILuaPatcher : IReusableService
{
string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
string Patch(string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, HookMethodType hookType);
bool RemovePatch(string identifier, string className, string methodName, HookMethodType hookType);
void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, HookMethodType hookType = HookMethodType.Before, IAssemblyPlugin owner = null);
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
public void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
public void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
}
@@ -0,0 +1,18 @@
using System.Collections.Immutable;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using FluentResults;
using MoonSharp.Interpreter.Loaders;
namespace Barotrauma.LuaCs;
public interface ILuaScriptLoader : IService, IScriptLoader, ISafeStorageValidation
{
void ClearCaches();
/// <summary>
/// Whether caching is enabled/disabled.
/// </summary>
/// <param name="useCaching"></param>
void SetCachingPolicy(bool useCaching);
Task<Result<ImmutableArray<(ContentPath Path, Result<string>)>>> CacheResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourceInfos);
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaService : IService
{
}
@@ -0,0 +1,418 @@
using System;
using MoonSharp.Interpreter;
using Microsoft.Xna.Framework;
using FarseerPhysics.Dynamics;
using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch;
using Barotrauma.Networking;
using System.Collections.Immutable;
using Barotrauma.LuaCs;
namespace Barotrauma
{
public class LuaConverters
{
private readonly ILuaScriptManagementService _luaScriptManagementService;
public LuaConverters(ILuaScriptManagementService luaScriptManagementService)
{
_luaScriptManagementService = luaScriptManagementService;
}
private DynValue Call(object function, params object[] arguments) => _luaScriptManagementService.CallFunctionSafe(function, arguments);
public void RegisterLuaConverters()
{
RegisterAction<Item>();
RegisterAction<Character>();
RegisterAction<Character, Character>();
RegisterAction<Entity>();
RegisterAction<float>();
RegisterAction();
RegisterFunc<Fixture, Vector2, Vector2, float, float>();
RegisterFunc<AIObjective, bool>();
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsAction), v => (LuaCsAction)(args =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
Call(v.Function, args);
}
}));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsFunc), v => (LuaCsFunc)(args =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
return Call(v.Function, args);
}
return default;
}));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsCompatPatchFunc), v => (LuaCsCompatPatchFunc)((self, args) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
return Call(v.Function, self, args);
}
return default;
}));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsPatchFunc), v => (LuaCsPatchFunc)((self, args) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
return Call(v.Function, self, args);
}
return default;
}));
void RegisterHandler<T>(Func<Closure, T> converter) => Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(T), v => converter(v.Function));
RegisterHandler(f => (Character.OnDeathHandler)((a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (Character.OnAttackedHandler)((a1, a2) => Call(f, a1, a2)));
#if CLIENT
RegisterAction<Microsoft.Xna.Framework.Graphics.SpriteBatch, GUICustomComponent>();
RegisterAction<float, Microsoft.Xna.Framework.Graphics.SpriteBatch>();
RegisterAction<Microsoft.Xna.Framework.Graphics.SpriteBatch, float>();
{
RegisterHandler(f => (GUIComponent.SecondaryButtonDownHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIButton.OnClickedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIButton.OnButtonDownHandler)(
() => Call(f)?.CastToBool() ?? default));
RegisterHandler(f => (GUIButton.OnPressedHandler)(
() => Call(f)?.CastToBool() ?? default));
RegisterHandler(f => (GUIColorPicker.OnColorSelectedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIDropDown.OnSelectedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIListBox.OnSelectedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIListBox.OnRearrangedHandler)(
(a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (GUIListBox.CheckSelectedHandler)(
() => Call(f)?.ToObject() ?? default));
RegisterHandler(f => (GUINumberInput.OnValueEnteredHandler)(
(a1) => Call(f, a1)));
RegisterHandler(f => (GUINumberInput.OnValueChangedHandler)(
(a1) => Call(f, a1)));
RegisterHandler(f => (GUIProgressBar.ProgressGetterHandler)(
() => (float)(Call(f)?.CastToNumber() ?? default)));
RegisterHandler(f => (GUIRadioButtonGroup.RadioButtonGroupDelegate)(
(a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (GUIScrollBar.OnMovedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIScrollBar.ScrollConversion)(
(a1, a2) => (float)(Call(f, a1, a2)?.CastToNumber() ?? default)));
RegisterHandler(f => (GUITextBlock.TextGetterHandler)(
() => Call(f, new object[0])?.CastToString() ?? default));
RegisterHandler(f => (GUITextBox.OnEnterHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUITextBox.OnTextChangedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (TextBoxEvent)(
(a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (GUITickBox.OnSelectedHandler)(
(a1) => Call(f, a1)?.CastToBool() ?? default));
RegisterHandler(f => (GUITextBlock.ClickableArea.OnClickDelegate)(
(a1, a2) => Call(f, a1, a2)));
}
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(NetMessageReceived), v => (NetMessageReceived)((arg1) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
Call(v.Function, arg1);
}
}));
#elif SERVER
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(NetMessageReceived), v => (NetMessageReceived)((arg1, arg2) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
Call(v.Function, arg1, arg2);
}
}));
#endif
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(Pair<JobPrefab, int>), v =>
{
return new Pair<JobPrefab, int>((JobPrefab)v.Table.Get(1).ToObject(), (int)v.Table.Get(2).CastToNumber());
});
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<ulong>((Script script, ulong v) =>
{
return DynValue.NewString(v.ToString());
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.String, typeof(ulong), v =>
{
return ulong.Parse(v.String);
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(sbyte),
canConvert: luaValue => luaValue.UserData?.Object is LuaSByte,
converter: luaValue => luaValue.UserData.Object is LuaSByte v
? (sbyte)v
: throw new ScriptRuntimeException("use SByte(value) to pass primitive type 'sbyte' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(byte),
canConvert: luaValue => luaValue.UserData?.Object is LuaByte,
converter: luaValue => luaValue.UserData.Object is LuaByte v
? (byte)v
: throw new ScriptRuntimeException("use Byte(value) to pass primitive type 'byte' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(short),
canConvert: luaValue => luaValue.UserData?.Object is LuaInt16,
converter: luaValue => luaValue.UserData.Object is LuaInt16 v
? (short)v
: throw new ScriptRuntimeException("use Int16(value) to pass primitive type 'short' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(ushort),
canConvert: luaValue => luaValue.UserData?.Object is LuaUInt16,
converter: luaValue => luaValue.UserData.Object is LuaUInt16 v
? (ushort)v
: throw new ScriptRuntimeException("use UInt16(value) to pass primitive type 'ushort' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(int),
canConvert: luaValue => luaValue.UserData?.Object is LuaInt32,
converter: luaValue => luaValue.UserData.Object is LuaInt32 v
? (int)v
: throw new ScriptRuntimeException("use Int32(value) to pass primitive type 'int' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(uint),
canConvert: luaValue => luaValue.UserData?.Object is LuaUInt32,
converter: luaValue => luaValue.UserData.Object is LuaUInt32 v
? (uint)v
: throw new ScriptRuntimeException("use UInt32(value) to pass primitive type 'uint' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(long),
canConvert: luaValue => luaValue.UserData?.Object is LuaInt64,
converter: luaValue => luaValue.UserData.Object is LuaInt64 v
? (long)v
: throw new ScriptRuntimeException("use Int64(value) to pass primitive type 'long' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(ulong),
canConvert: luaValue => luaValue.UserData?.Object is LuaUInt64,
converter: luaValue => luaValue.UserData.Object is LuaUInt64 v
? (ulong)v
: throw new ScriptRuntimeException("use UInt64(value) to pass primitive type 'ulong' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(float),
canConvert: luaValue => luaValue.UserData?.Object is LuaSingle,
converter: luaValue => luaValue.UserData.Object is LuaSingle v
? (float)v
: throw new ScriptRuntimeException("use Single(value) to pass primitive type 'float' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(double),
canConvert: luaValue => luaValue.UserData?.Object is LuaDouble,
converter: luaValue => luaValue.UserData.Object is LuaDouble v
? (double)v
: throw new ScriptRuntimeException("use Double(value) to pass primitive type 'double' to C#"));
RegisterOption<Character>(DataType.UserData);
RegisterOption<AccountId>(DataType.UserData);
RegisterOption<ContentPackageId>(DataType.UserData);
RegisterOption<SteamId>(DataType.UserData);
RegisterOption<DateTime>(DataType.UserData);
RegisterOption<BannedPlayer>(DataType.UserData);
RegisterOption<Address>(DataType.UserData);
RegisterOption<int>(DataType.Number);
RegisterEither<Address, AccountId>();
RegisterImmutableArray<FactionPrefab.HireableCharacter>();
}
private static void RegisterImmutableArray<T>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(ImmutableArray<T>), v =>
{
return v.ToObject<T[]>().ToImmutableArray();
});
}
private static void RegisterEither<T1, T2>()
{
DynValue convertEitherIntoDynValue(Either<T1, T2> either)
{
if (either.TryGet(out T1 value1))
{
return UserData.Create(value1);
}
if (either.TryGet(out T2 value2))
{
return UserData.Create(value2);
}
return null;
}
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(EitherT<T1, T2>), (Script v, object obj) =>
{
if (obj is EitherT<T1, T2> either)
{
return convertEitherIntoDynValue(either);
}
return null;
});
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(EitherU<T1, T2>), (Script v, object obj) =>
{
if (obj is EitherU<T1, T2> either)
{
return convertEitherIntoDynValue(either);
}
return null;
});
}
private static void RegisterOption<T>(DataType dataType)
{
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(Option<T>), (Script v, object obj) =>
{
if (obj is Option<T> option)
{
if (option.TryUnwrap(out T outValue))
{
return UserData.Create(outValue);
}
}
return DynValue.Nil;
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(dataType, typeof(Option<T>), v =>
{
return Option<T>.Some(v.ToObject<T>());
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Nil, typeof(Option<T>), v =>
{
return Option<T>.None();
});
}
private void RegisterAction<T>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action<T>), v =>
{
var function = v.Function;
return (Action<T>)(p => Call(function, p));
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action<T>), v =>
{
var function = v.Function;
return (Action<T>)(p => Call(function, p));
});
}
private void RegisterAction<T1, T2>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action<T1, T2>), v =>
{
var function = v.Function;
return (Action<T1, T2>)((a1, a2) => Call(function, a1, a2));
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action<T1, T2>), v =>
{
var function = v.Function;
return (Action<T1, T2>)((a1, a2) => Call(function, a1, a2));
});
}
private void RegisterAction()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v =>
{
var function = v.Function;
return (Action)(() => Call(function));
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action), v =>
{
var function = v.Function;
return (Action)(() => Call(function));
});
}
private void RegisterFunc<T1>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1>), v =>
{
var function = v.Function;
return () => function.Call().ToObject<T1>();
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Func<T1>), v =>
{
var function = v.Function;
return () => function.Call().ToObject<T1>();
});
}
private void RegisterFunc<T1, T2>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1, T2>), v =>
{
var function = v.Function;
return (T1 a) => function.Call(a).ToObject<T2>();
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Func<T1, T2>), v =>
{
var function = v.Function;
return (T1 a) => function.Call(a).ToObject<T2>();
});
}
private void RegisterFunc<T1, T2, T3, T4, T5>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1, T2, T3, T4, T5>), v =>
{
var function = v.Function;
return (T1 a, T2 b, T3 c, T4 d) => function.Call(a, b, c, d).ToObject<T5>();
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1, T2, T3, T4, T5>), v =>
{
var function = v.Function;
return (T1 a, T2 b, T3 c, T4 d) => function.Call(a, b, c, d).ToObject<T5>();
});
}
}
}
@@ -0,0 +1,51 @@
using System;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using Barotrauma.LuaCs;
namespace Barotrauma
{
public partial class LuaCsLogger
{
public static void HandleException(Exception ex, LuaCsMessageOrigin origin)
{
LuaCsSetup.Instance.Logger.HandleException(ex);
}
public static void LogError(string message, LuaCsMessageOrigin origin)
{
LuaCsSetup.Instance.Logger.LogError(message);
}
public static void LogError(string message)
{
LuaCsSetup.Instance.Logger.LogError(message);
}
public static void LogMessage(string message, Color? serverColor = null, Color? clientColor = null)
{
LuaCsSetup.Instance.Logger.LogMessage(message, serverColor, clientColor);
}
public static void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
{
LuaCsSetup.Instance.Logger.Log(message, color, messageType);
}
}
partial class LuaCsSetup
{
// Compatibility with cs mods that use this method.
public static void PrintLuaError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}");
public static void PrintCsError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}");
public static void PrintGenericError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}");
internal void PrintMessage(object message) => LuaCsSetup.Instance.Logger.LogMessage($"{message}");
public static void PrintCsMessage(object message) => LuaCsSetup.Instance.Logger.LogMessage($"{message}");
internal void HandleException(Exception ex, LuaCsMessageOrigin origin) => LuaCsSetup.Instance.Logger.HandleException(ex);
}
}
@@ -0,0 +1,82 @@
using Barotrauma.LuaCs;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Barotrauma
{
public interface IPerformanceData
{
public string Identifier { get; }
public long ElapsedTicks { get; }
}
public class SimplePerformanceData : IPerformanceData
{
public string Identifier { get; }
public long ElapsedTicks { get; }
public SimplePerformanceData(string identifier, long elapsedTicks)
{
Identifier = identifier;
ElapsedTicks = elapsedTicks;
}
}
public class PerformanceCounterService : IReusableService
{
public bool EnablePerformanceCounter { get; set; } = false;
private Dictionary<string, List<IPerformanceData>> _data = new Dictionary<string, List<IPerformanceData>>();
public void AddElapsedTicks(IPerformanceData data)
{
if (!EnablePerformanceCounter) { return; }
if (!_data.ContainsKey(data.Identifier))
{
_data.Add(data.Identifier, new List<IPerformanceData>());
}
_data[data.Identifier].Add(data);
Trim(data.Identifier, 100);
}
public T GetLatestSnapshot<T>(string identifier) where T : class, IPerformanceData
{
if (!_data.ContainsKey(identifier)) { return default; }
return (T)_data[identifier].Last();
}
public T[] GetSnapshot<T>(string identifier, int length) where T : class, IPerformanceData, new()
{
if (!_data.ContainsKey(identifier)) { return new T[] { }; }
length = Math.Min(length, _data[identifier].Count);
return _data[identifier].GetRange(_data[identifier].Count - length, length).Cast<T>().ToArray();
}
public void Trim(string identifier, int maxSize)
{
if (!_data.ContainsKey(identifier)) { return; }
if (_data[identifier].Count > maxSize)
{
_data[identifier].RemoveRange(0, _data[identifier].Count - maxSize);
}
}
public FluentResults.Result Reset()
{
_data = new Dictionary<string, List<IPerformanceData>>();
return FluentResults.Result.Ok();
}
public void Dispose() { }
public bool IsDisposed { get; }
}
}
@@ -0,0 +1,151 @@
using Steamworks;
using Steamworks.Data;
using Barotrauma.Steam;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using System;
using Steamworks.Ugc;
namespace Barotrauma
{
partial class LuaCsSteam
{
private struct WorkshopItemDownload
{
public Steamworks.Ugc.Item Item;
public string Destination;
public LuaCsAction Callback;
}
double lastTimeChecked = 0;
List<WorkshopItemDownload> itemsBeingDownloaded = new List<WorkshopItemDownload>();
public LuaCsSteam()
{
}
private static void CopyFolder(string sourceDirName, string destDirName, bool copySubDirs, bool overwriteExisting = false)
{
// Get the subdirectories for the specified directory.
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
if (!dir.Exists)
{
throw new System.IO.DirectoryNotFoundException(
"Source directory does not exist or could not be found: "
+ sourceDirName);
}
IEnumerable<DirectoryInfo> dirs = dir.GetDirectories();
// If the destination directory doesn't exist, create it.
if (!Directory.Exists(destDirName))
{
Directory.CreateDirectory(destDirName);
}
// Get the files in the directory and copy them to the new location.
IEnumerable<FileInfo> files = dir.GetFiles();
foreach (FileInfo file in files)
{
string tempPath = Path.Combine(destDirName, file.Name);
if (!overwriteExisting && File.Exists(tempPath)) { continue; }
file.CopyTo(tempPath, true);
}
// If copying subdirectories, copy them and their contents to new location.
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string tempPath = Path.Combine(destDirName, subdir.Name);
CopyFolder(subdir.FullName, tempPath, copySubDirs, overwriteExisting);
}
}
}
private async void DownloadWorkshopItemAsync(WorkshopItemDownload download, bool startDownload = false)
{
if (startDownload)
{
SteamManager.Workshop.NukeDownload(download.Item);
SteamUGC.Download(download.Item.Id, true);
itemsBeingDownloaded.Add(download);
}
if (download.Item.IsInstalled && Directory.Exists(download.Item.Directory))
{
if (download.Callback != null)
{
download.Callback(download.Item);
}
itemsBeingDownloaded.Remove(download);
CopyFolder(download.Item.Directory, download.Destination, true, true);
return;
}
}
public async void DownloadWorkshopItem(ulong id, string destination, LuaCsAction callback)
{
if (!LuaCsFile.IsPathAllowedException(destination)) { return; }
Option<Steamworks.Ugc.Item> itemOption = await SteamManager.Workshop.GetItem(id);
if (itemOption.TryUnwrap(out Steamworks.Ugc.Item item))
{
DownloadWorkshopItemAsync(new WorkshopItemDownload()
{
Item = item,
Destination = destination,
Callback = callback
}, true);
}
else
{
throw new Exception($"Tried to download invalid workshop item {id}.");
}
}
public void DownloadWorkshopItem(Steamworks.Ugc.Item item, string destination, LuaCsAction callback)
{
DownloadWorkshopItemAsync(new WorkshopItemDownload()
{
Item = item,
Destination = destination,
Callback = callback
}, true);
}
public async void GetWorkshopItem(UInt64 id, LuaCsAction callback)
{
Option<Steamworks.Ugc.Item> itemOption = await SteamManager.Workshop.GetItem(id);
if (itemOption.TryUnwrap(out Steamworks.Ugc.Item item))
{
callback(item);
}
else
{
callback(null);
}
}
public void Update()
{
if (itemsBeingDownloaded.Count > 0 && Timing.TotalTime > lastTimeChecked) // SteamUGC.OnDownloadItemResult for some reason doesn't work, so i need to do this stupid thing.
{
foreach (var item in itemsBeingDownloaded.ToArray())
{
DownloadWorkshopItemAsync(item);
}
lastTimeChecked = Timing.TotalTime + 15;
}
}
}
}
@@ -0,0 +1,153 @@
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Compatibility;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Barotrauma
{
public class LuaCsTimer : ILuaCsTimer, IEventUpdate
{
public static double Time => Timing.TotalTime;
public static double GetTime() => Time;
public static double AccumulatorMax
{
get
{
return Timing.AccumulatorMax;
}
set
{
Timing.AccumulatorMax = value;
}
}
private class TimerComparer : IComparer<TimedAction>
{
public int Compare(TimedAction timedAction1, TimedAction timedAction2)
{
if (timedAction1 == null || timedAction2 == null)
return 0;
return -Math.Sign(timedAction2.ExecutionTime - timedAction1.ExecutionTime);
}
}
private class TimedAction
{
public LuaCsAction Action
{
get;
private set;
}
public double ExecutionTime
{
get;
private set;
}
public TimedAction(LuaCsAction action, int delayMs)
{
this.Action = action;
ExecutionTime = Time + (delayMs / 1000f);
}
}
private List<TimedAction> timedActions = new List<TimedAction>();
private readonly IEventService _eventService;
private readonly ILoggerService _loggerService;
public LuaCsTimer(IEventService eventService, ILoggerService loggerService)
{
_eventService = eventService;
_loggerService = loggerService;
SubscribeToEvents();
}
private void AddTimer(TimedAction timedAction)
{
if (timedAction == null)
{
throw new ArgumentNullException(nameof(timedAction));
}
lock (timedActions)
{
int insertionPoint = timedActions.BinarySearch(timedAction, new TimerComparer());
if (insertionPoint < 0)
{
insertionPoint = ~insertionPoint;
}
timedActions.Insert(insertionPoint, timedAction);
}
}
public void Clear()
{
timedActions = new List<TimedAction>();
}
public void Wait(LuaCsAction action, int millisecondDelay)
{
TimedAction timedAction = new TimedAction(action, millisecondDelay);
AddTimer(timedAction);
}
public void NextFrame(LuaCsAction action)
{
TimedAction timedAction = new TimedAction(action, 0);
AddTimer(timedAction);
}
public void OnUpdate(double fixedDeltaTime)
{
lock (timedActions)
{
TimedAction[] timedCopy = timedActions.ToArray();
for (int i = 0; i < timedCopy.Length; i++)
{
TimedAction timedAction = timedCopy[i];
if (Time >= timedAction.ExecutionTime)
{
try
{
timedAction.Action();
}
catch (Exception e)
{
_loggerService.HandleException(e);
}
timedActions.Remove(timedAction);
}
else
{
break;
}
}
}
}
private void SubscribeToEvents()
{
_eventService.Subscribe<IEventUpdate>(this);
}
public FluentResults.Result Reset()
{
SubscribeToEvents();
return FluentResults.Result.Ok();
}
public void Dispose()
{
_eventService.Unsubscribe<IEventUpdate>(this);
}
public bool IsDisposed => false;
}
}
@@ -0,0 +1,247 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using MoonSharp.Interpreter;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.LuaCs;
namespace Barotrauma
{
partial class LuaCsFile
{
public static bool CanReadFromPath(string path)
{
string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath();
path = getFullPath(path);
bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
string localModsDir = getFullPath(ContentPackage.LocalModsDir);
string workshopModsDir = getFullPath(ContentPackage.WorkshopModsDir);
#if CLIENT
string tempDownloadDir = getFullPath(ModReceiver.DownloadFolder);
#endif
if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath)))
return true;
if (pathStartsWith(localModsDir))
return true;
if (pathStartsWith(workshopModsDir))
return true;
#if CLIENT
if (pathStartsWith(tempDownloadDir))
return true;
#endif
if (pathStartsWith(getFullPath(".")))
return true;
return false;
}
public static bool CanWriteToPath(string path)
{
const long LuaCsPackageId = 2559634234;
string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath();
path = getFullPath(path);
bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
if (pathStartsWith(getFullPath(LuaCsSetup.GetLuaCsPackage().Path)))
{
return false;
}
if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath)))
return true;
if (pathStartsWith(getFullPath(ContentPackage.LocalModsDir)))
return true;
if (pathStartsWith(getFullPath(ContentPackage.WorkshopModsDir)))
return true;
#if CLIENT
if (pathStartsWith(getFullPath(ModReceiver.DownloadFolder)))
return true;
#endif
return false;
}
public static bool IsPathAllowedException(string path, bool write = true, LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown)
{
if (write)
{
if (CanWriteToPath(path))
{
return true;
}
else
{
throw new Exception("File access to \"" + path + "\" not allowed.");
}
}
else
{
if (CanReadFromPath(path))
{
return true;
}
else
{
throw new Exception("File access to \"" + path + "\" not allowed.");
}
}
}
public static bool IsPathAllowedLuaException(string path, bool write = true) =>
IsPathAllowedException(path, write, LuaCsMessageOrigin.LuaMod);
public static bool IsPathAllowedCsException(string path, bool write = true) =>
IsPathAllowedException(path, write, LuaCsMessageOrigin.CSharpMod);
public static string Read(string path)
{
if (!IsPathAllowedException(path, false))
return "";
return File.ReadAllText(path);
}
public static void Write(string path, string text)
{
if (!IsPathAllowedException(path))
return;
File.WriteAllText(path, text);
}
public static void Delete(string path)
{
if (!IsPathAllowedException(path))
return;
File.Delete(path);
}
public static void DeleteDirectory(string path)
{
if (!IsPathAllowedException(path))
return;
Directory.Delete(path, true);
}
public static void Move(string path, string destination)
{
if (!IsPathAllowedException(path))
return;
if (!IsPathAllowedException(destination))
return;
File.Move(path, destination, true);
}
public static FileStream OpenRead(string path)
{
if (!IsPathAllowedException(path))
return null;
return File.Open(path, FileMode.Open, FileAccess.Read);
}
public static FileStream OpenWrite(string path)
{
if (!IsPathAllowedException(path))
return null;
if (File.Exists(path)) return File.Open(path, FileMode.Truncate, FileAccess.Write);
else return File.Open(path, FileMode.Create, FileAccess.Write);
}
public static bool Exists(string path)
{
if (!IsPathAllowedException(path, false))
return false;
return File.Exists(path);
}
public static bool CreateDirectory(string path)
{
if (!IsPathAllowedException(path))
return false;
Directory.CreateDirectory(path);
return true;
}
public static bool DirectoryExists(string path)
{
if (!IsPathAllowedException(path, false))
return false;
return Directory.Exists(path);
}
public static string[] GetFiles(string path)
{
if (!IsPathAllowedException(path, false))
return null;
return Directory.GetFiles(path);
}
public static string[] GetDirectories(string path)
{
if (!IsPathAllowedException(path, false))
return new string[] { };
return Directory.GetDirectories(path);
}
public static string[] DirSearch(string sDir)
{
if (!IsPathAllowedException(sDir, false))
return new string[] { };
List<string> files = new List<string>();
try
{
foreach (string f in Directory.GetFiles(sDir))
{
files.Add(f);
}
foreach (string d in Directory.GetDirectories(sDir))
{
foreach (string f in Directory.GetFiles(d))
{
files.Add(f);
}
DirSearch(d);
}
}
catch (System.Exception excpt)
{
Console.WriteLine(excpt.Message);
}
return files.ToArray();
}
}
}
@@ -0,0 +1,560 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs
{
partial class LuaGame : IReusableService
{
public bool IsSingleplayer => GameMain.IsSingleplayer;
public bool IsMultiplayer => GameMain.IsMultiplayer;
public string SaveFolder => string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath;
#if CLIENT
public GameClient Client
{
get
{
return GameMain.Client;
}
}
public bool Paused => GameMain.Instance?.Paused == true;
public byte SessionId => GameMain.Client.SessionId;
public byte MyID => SessionId; // compatibility
public ChatMode ActiveChatMode => GameMain.ActiveChatMode;
public ChatBox ChatBox
{
get
{
if (GameMain.IsSingleplayer)
return GameMain.GameSession.CrewManager.ChatBox;
else
return GameMain.Client.ChatBox;
}
}
public Sounds.SoundManager SoundManager
{
get
{
return GameMain.SoundManager;
}
}
public Lights.LightManager LightManager
{
get
{
return GameMain.LightManager;
}
}
public SubEditorScreen SubEditorScreen
{
get
{
return GameMain.SubEditorScreen;
}
}
public MainMenuScreen MainMenuScreen
{
get
{
return GameMain.MainMenuScreen;
}
}
public Particles.ParticleManager ParticleManager
{
get
{
return GameMain.ParticleManager;
}
}
public bool IsSubEditor
{
get
{
return Screen.Selected is SubEditorScreen;
}
}
#else
public GameServer Server
{
get
{
return GameMain.Server;
}
}
public bool IsDedicated
{
get
{
return GameMain.Server.ServerPeer is LidgrenServerPeer;
}
}
public bool Paused => false;
#endif
public ServerSettings ServerSettings
{
get
{
#if SERVER
return GameMain.Server.ServerSettings;
#else
return GameMain.Client.ServerSettings;
#endif
}
}
public RespawnManager RespawnManager
{
get
{
#if SERVER
return GameMain.Server.RespawnManager;
#else
return GameMain.Client.RespawnManager;
#endif
}
}
public List<DebugConsole.Command> Commands => DebugConsole.Commands;
public bool? ForceVoice = null;
public bool? ForceLocalVoice = null;
public DynValue Settings;
public bool allowWifiChat = false;
public bool overrideTraitors = false;
public bool overrideRespawnSub = false;
public bool overrideSignalRadio = false;
public bool disableSpamFilter = false;
public bool disableDisconnectCharacter = false;
public bool enableControlHusk = false;
public int MapEntityUpdateInterval
{
get { return MapEntity.MapEntityUpdateInterval; }
set { MapEntity.MapEntityUpdateInterval = value; }
}
public int GapUpdateInterval
{
get { return 1; }
set { }
}
public int PoweredUpdateInterval
{
get { return MapEntity.PoweredUpdateInterval; }
set { MapEntity.PoweredUpdateInterval = value; }
}
public int CharacterUpdateInterval
{
get { return Character.CharacterUpdateInterval; }
set { Character.CharacterUpdateInterval = value; }
}
public HashSet<Item> UpdatePriorityItems = new HashSet<Item>();
public HashSet<Character> UpdatePriorityCharacters = new HashSet<Character>();
public void AddPriorityItem(Item item)
{
UpdatePriorityItems.Add(item);
}
public void RemovePriorityItem(Item item)
{
UpdatePriorityItems.Remove(item);
}
public void ClearPriorityItem()
{
UpdatePriorityItems.Clear();
}
public void AddPriorityCharacter(Character character)
{
UpdatePriorityCharacters.Add(character);
}
public void RemovePriorityCharacter(Character character)
{
UpdatePriorityCharacters.Remove(character);
}
public void ClearPriorityCharacter()
{
UpdatePriorityCharacters.Clear();
}
public bool RoundStarted
{
get
{
if (GameMain.IsSingleplayer) { return GameMain.GameSession != null && GameMain.GameSession.IsRunning; }
#if SERVER
return GameMain.Server?.GameStarted == true;
#else
return GameMain.Client?.GameStarted == true;
#endif
}
}
public GameSession GameSession
{
get
{
return GameMain.GameSession;
}
}
public NetLobbyScreen NetLobbyScreen
{
get
{
return GameMain.NetLobbyScreen;
}
}
public GameScreen GameScreen
{
get
{
return GameMain.GameScreen;
}
}
public World World
{
get
{
return GameMain.World;
}
}
#if SERVER
public ServerPeer Peer
{
get
{
return GameMain.Server.ServerPeer;
}
}
#else
public ClientPeer Peer
{
get
{
return GameMain.Client.ClientPeer;
}
}
#endif
private readonly IConsoleCommandsService _consoleCommands;
public LuaGame(IConsoleCommandsService consoleCommands)
{
UserData.RegisterType(typeof(GameSettings));
Settings = UserData.CreateStatic(typeof(GameSettings));
_consoleCommands = consoleCommands;
}
public void OverrideTraitors(bool o)
{
overrideTraitors = o;
}
public void OverrideRespawnSub(bool o)
{
overrideRespawnSub = o;
}
public void AllowWifiChat(bool o)
{
allowWifiChat = o;
}
public void OverrideSignalRadio(bool o)
{
overrideSignalRadio = o;
}
public void DisableSpamFilter(bool o)
{
disableSpamFilter = o;
}
public void DisableDisconnectCharacter(bool o)
{
disableDisconnectCharacter = o;
}
public void EnableControlHusk(bool o)
{
enableControlHusk = o;
}
public static void Explode(Vector2 pos, float range = 100, float force = 30, float damage = 30, float structureDamage = 30, float itemDamage = 30, float empStrength = 0, float ballastFloraStrength = 0)
{
new Explosion(range, force, damage, structureDamage, itemDamage, empStrength, ballastFloraStrength).Explode(pos, null);
}
public static string SpawnItem(string name, Vector2 pos, bool inventory = false, Character character = null)
{
string error;
DebugConsole.SpawnItem(new string[] { name, inventory ? "inventory" : "cursor" }, pos, character, out error);
return error;
}
public static ContentPackage[] GetEnabledContentPackages()
{
return ContentPackageManager.EnabledPackages.All.ToArray();
}
public static ItemPrefab GetItemPrefab(string itemNameOrId)
{
ItemPrefab itemPrefab =
(MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab;
return itemPrefab;
}
public static Submarine GetRespawnSub()
{
#if SERVER
if (GameMain.Server.RespawnManager == null) { return null; }
return GameMain.Server.RespawnManager.GetShuttle(CharacterTeamType.Team1);
#else
if (GameMain.Client.RespawnManager == null) { return null; }
return GameMain.Client.RespawnManager.GetShuttle(CharacterTeamType.Team1);
#endif
}
public static Items.Components.Steering GetSubmarineSteering(Submarine sub)
{
foreach (Item item in Item.ItemList)
{
if (item.Submarine != sub) continue;
var steering = item.GetComponent<Items.Components.Steering>();
if (steering != null)
{
return steering;
}
}
return null;
}
public static WifiComponent GetWifiComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<WifiComponent>();
}
public static LightComponent GetLightComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<LightComponent>();
}
public static CustomInterface GetCustomInterface(Item item)
{
if (item == null) return null;
return item.GetComponent<CustomInterface>();
}
public static Fabricator GetFabricatorComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<Fabricator>();
}
public static Holdable GetHoldableComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<Holdable>();
}
public static void ExecuteCommand(string command)
{
DebugConsole.ExecuteCommand(command);
}
public static Signal CreateSignal(string value, int stepsTaken = 1, Character sender = null, Item source = null, float power = 0, float strength = 1)
{
return new Signal(value, stepsTaken, sender, source, power, strength);
}
public void RemoveCommand(string name)
{
_consoleCommands.RemoveCommand(name);
for (var i = DebugConsole.Commands.Count - 1; i >= 0; i--)
{
foreach (var cmdname in DebugConsole.Commands[i].Names)
{
if (cmdname == name)
{
DebugConsole.Commands.RemoveAt(i);
continue;
}
}
}
}
public void AddCommand(string name, string help, LuaCsAction onExecute, LuaCsFunc getValidArgs = null, bool isCheat = false)
{
_consoleCommands.RegisterCommand(name, help,
(string[] args) =>
{
onExecute(new object[] { args });
},
() =>
{
if (getValidArgs == null) { return null; }
var validArgs = getValidArgs();
if (validArgs is DynValue luaValue)
{
return luaValue.ToObject<string[][]>();
}
return (string[][])validArgs;
}
);
}
public void AddCommand(string name, LuaCsAction onExecute, LuaCsFunc getValidArgs = null, bool isCheat = false)
{
_consoleCommands.RegisterCommand(name, "",
(string[] args) =>
{
onExecute(new object[] { args });
},
() =>
{
if (getValidArgs == null) { return null; }
var validArgs = getValidArgs();
if (validArgs is DynValue luaValue)
{
return luaValue.ToObject<string[][]>();
}
return (string[][])validArgs;
}
);
}
public bool IsDisposed => throw new NotImplementedException();
public void AssignOnExecute(string names, object onExecute) => DebugConsole.AssignOnExecute(names, (string[] args) =>
{
LuaCsSetup.Instance.LuaScriptManagementService.CallFunctionSafe(onExecute, new object[] { args });
});
public void SaveGame(string path)
{
if (!LuaCsFile.CanWriteToPath(path)) { throw new ScriptRuntimeException($"Saving files to {path} is disallowed."); }
SaveUtil.SaveGame(CampaignDataPath.CreateRegular(path));
}
public void LoadGame(string path)
{
SaveUtil.LoadGame(CampaignDataPath.CreateRegular(path));
}
#if SERVER
public void LoadCampaign(string path, Client client = null)
{
MultiPlayerCampaign.LoadCampaign(CampaignDataPath.CreateRegular(path), client);
}
public static void SendMessage(string msg, ChatMessageType? messageType = null, Client sender = null, Character character = null)
{
GameMain.Server.SendChatMessage(msg, messageType, sender, character);
}
public static void SendTraitorMessage(WriteOnlyMessage message, Client client)
{
GameMain.Server.SendTraitorMessage(message, client);
}
public static void SendDirectChatMessage(string sendername, string text, Character sender, ChatMessageType messageType = ChatMessageType.Private, Client client = null, string iconStyle = "")
{
ChatMessage cm = ChatMessage.Create(sendername, text, messageType, sender);
cm.IconStyle = iconStyle;
GameMain.Server.SendDirectChatMessage(cm, client);
}
public static void SendDirectChatMessage(ChatMessage chatMessage, Client client)
{
GameMain.Server.SendDirectChatMessage(chatMessage, client);
}
public static void Log(string message, ServerLog.MessageType type)
{
GameServer.Log(message, type);
}
public static void DispatchRespawnSub()
{
GameMain.Server.RespawnManager.DispatchShuttle(GameMain.Server.RespawnManager.GetTeamSpecificState(CharacterTeamType.Team1));
}
public static GameServer.TryStartGameResult StartGame()
{
return GameMain.Server.TryStartGame();
}
public static void EndGame()
{
GameMain.Server.EndGame();
}
public void AssignOnClientRequestExecute(string names, LuaCsAction onExecute) =>
_consoleCommands.AssignOnClientRequestExecute(names, (Client client, Vector2 position, string[] args) => onExecute(client, position, args));
#endif
public void Stop()
{
MapEntityUpdateInterval = 1;
CharacterUpdateInterval = 1;
_consoleCommands.RemoveRegisteredCommands();
}
public FluentResults.Result Reset()
{
Stop();
return FluentResults.Result.Ok();
}
public void Dispose()
{
Stop();
}
}
}
@@ -0,0 +1,116 @@
using MoonSharp.Interpreter.Platforms;
using MoonSharp.Interpreter;
using System;
using System.IO;
using System.Text;
namespace Barotrauma
{
public class LuaPlatformAccessor : PlatformAccessorBase
{
public static FileMode ParseFileMode(string mode)
{
mode = mode.Replace("b", "");
if (mode == "r")
return FileMode.Open;
else if (mode == "r+")
return FileMode.OpenOrCreate;
else if (mode == "w")
return FileMode.Create;
else if (mode == "w+")
return FileMode.Truncate;
else
return FileMode.Append;
}
public static FileAccess ParseFileAccess(string mode)
{
mode = mode.Replace("b", "");
if (mode == "r")
return FileAccess.Read;
else if (mode == "r+")
return FileAccess.ReadWrite;
else if (mode == "w")
return FileAccess.ReadWrite;
else if (mode == "w+")
return FileAccess.ReadWrite;
else
return FileAccess.Write;
}
public override string GetEnvironmentVariable(string envvarname)
{
return null;
}
public override CoreModules FilterSupportedCoreModules(CoreModules module)
{
return module;
}
public override Stream IO_OpenFile(Script script, string filename, Encoding encoding, string mode)
{
if (!LuaCsFile.IsPathAllowedLuaException(filename)) { return Stream.Null; }
FileStream stream = new FileStream(filename, ParseFileMode(mode), ParseFileAccess(mode), FileShare.ReadWrite | FileShare.Delete);
return stream;
}
public override Stream IO_GetStandardStream(StandardFileType type)
{
switch (type)
{
case StandardFileType.StdIn:
return Console.OpenStandardInput();
case StandardFileType.StdOut:
return Console.OpenStandardOutput();
case StandardFileType.StdErr:
return Console.OpenStandardError();
default:
throw new ArgumentException("type");
}
}
public override string IO_OS_GetTempFilename()
{
return "LocalMods/temp.txt";
}
public override void OS_ExitFast(int exitCode)
{
throw new ScriptRuntimeException("usage of os.exit is not allowed.");
}
public override bool OS_FileExists(string file)
{
return LuaCsFile.Exists(file);
}
public override void OS_FileDelete(string file)
{
LuaCsFile.Delete(file);
}
public override void OS_FileMove(string src, string dst)
{
LuaCsFile.Move(src, dst);
}
public override int OS_Execute(string cmdline)
{
throw new ScriptRuntimeException("usage of os.execute is not allowed.");
}
public override string GetPlatformNamePrefix()
{
return "lua";
}
public override void DefaultPrint(string content)
{
System.Diagnostics.Debug.WriteLine(content);
}
}
}
@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.IO;
using MoonSharp.Interpreter;
namespace Barotrauma
{
class LuaRequire
{
private Script lua { get; set; }
private Dictionary<string, DynValue> loadedModules { get; set; }
private bool GetExistingReturnValue(string moduleName, ref DynValue returnValue)
{
return loadedModules.TryGetValue(
moduleName,
out returnValue
);
}
private string FixContentPackagePath(string contentPackagePath)
{
contentPackagePath = Path.TrimEndingDirectorySeparator(
new FileInfo(contentPackagePath) // filelist.xml
.Directory
.FullName
.CleanUpPathCrossPlatform()
);
return contentPackagePath;
}
private string GetContentPackagePath(string path)
{
IEnumerable<ContentPackage> allContentPackages = ContentPackageManager.AllPackages;
foreach (ContentPackage contentPackage in allContentPackages)
{
string contentPackagePath = FixContentPackagePath(contentPackage.Path);
if (path.StartsWith(contentPackagePath))
{
return contentPackagePath;
}
}
// Return null if we can't find a content package that
// this module belongs to.
return null;
}
private string GetContentPackagePath(string moduleName, Table environment)
{
string filePath = lua.Options
.ScriptLoader
.ResolveModuleName(
moduleName,
environment
);
filePath = Path.TrimEndingDirectorySeparator(
new FileInfo(filePath)
.Directory
.FullName
.CleanUpPathCrossPlatform()
);
return GetContentPackagePath(filePath);
}
private void SaveReturnValue(string moduleName, DynValue returnValue)
{
loadedModules[moduleName] = returnValue;
}
private void ExecuteModule(string moduleName, Table environment, ref DynValue returnValue)
{
DynValue loadFunc = lua.RequireModule(
moduleName,
environment
);
string packagePath = GetContentPackagePath(
moduleName,
environment
);
returnValue = lua.Call(
loadFunc,
packagePath
);
}
// Lua modules that have been previously loaded by require() will
// not be loaded again; instead, their initial return value is
// preserved and returned again on subsequent attempts.
public DynValue Require(string moduleName, Table globalContext)
{
DynValue returnValue = null;
Table environment = globalContext ?? lua.Globals;
if (GetExistingReturnValue(moduleName, ref returnValue))
return returnValue;
ExecuteModule(moduleName, environment, ref returnValue);
if (
returnValue == null
|| returnValue.IsNil()
|| returnValue.IsVoid()
)
returnValue = DynValue.NewBoolean(true);
SaveReturnValue(moduleName, returnValue);
return returnValue;
}
public LuaRequire(Script lua)
{
this.lua = lua;
loadedModules = new Dictionary<string, DynValue>();
}
}
}
@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
namespace Barotrauma
{
partial class LuaSafeUserData
{
public IUserDataDescriptor this[string index]
{
get => LuaUserData.Descriptors.GetValueOrDefault(index);
}
private static bool CanBeRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
if (typeName == "System.Single") { return true; }
if (typeName.StartsWith("System.Collections", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Microsoft.Xna", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Barotrauma.IO", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.ToolBox", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.SaveUtil", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.", StringComparison.Ordinal))
return true;
return false;
}
private static bool CanBeReRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
return true;
}
private static bool IsAllowed(string typeName)
{
if (!CanBeReRegistered(typeName) && LuaUserData.IsRegistered(typeName))
{
return false;
}
if (!CanBeRegistered(typeName) && !LuaUserData.IsRegistered(typeName))
{
return false;
}
return true;
}
private static void CheckAllowed(string typeName)
{
if (!IsAllowed(typeName))
{
throw new ScriptRuntimeException($"Type {typeName} can't be registered");
}
}
public static Type GetType(string typeName)
{
CheckAllowed(typeName);
return LuaUserData.GetType(typeName);
}
public static IUserDataDescriptor RegisterType(string typeName)
{
CheckAllowed(typeName);
return LuaUserData.RegisterType(typeName);
}
public static IUserDataDescriptor RegisterTypeBarotrauma(string typeName)
{
return RegisterType($"Barotrauma.{typeName}");
}
public static void RegisterExtensionType(string typeName)
{
CheckAllowed(typeName);
LuaUserData.RegisterExtensionType(typeName);
}
public static bool IsRegistered(string typeName)
{
return LuaUserData.IsRegistered(typeName);
}
public static void UnregisterType(string typeName, bool deleteHistory = false)
{
LuaUserData.UnregisterType(typeName, deleteHistory);
}
public static IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArguements)
{
CheckAllowed(typeName);
return LuaUserData.RegisterGenericType(typeName, typeNameArguements);
}
public static void UnregisterGenericType(string typeName, params string[] typeNameArguements)
{
LuaUserData.UnregisterGenericType(typeName, typeNameArguements);
}
public static bool IsTargetType(object obj, string typeName)
{
return LuaUserData.IsTargetType(obj, typeName);
}
public static string TypeOf(object obj)
{
return LuaUserData.TypeOf(obj);
}
public static object CreateStatic(string typeName)
{
CheckAllowed(typeName);
return LuaUserData.CreateStatic(typeName);
}
public static object CreateEnumTable(string typeName)
{
return LuaUserData.CreateEnumTable(typeName);
}
public static void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName)
{
LuaUserData.MakeFieldAccessible(IUUD, fieldName);
}
public static void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null)
{
LuaUserData.MakeMethodAccessible(IUUD, methodName, parameters);
}
public static void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName)
{
LuaUserData.MakePropertyAccessible(IUUD, propertyName);
}
public static void AddMethod(IUserDataDescriptor IUUD, string methodName, object function)
{
LuaUserData.AddMethod(IUUD, methodName, function);
}
public static void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value)
{
LuaUserData.AddField(IUUD, fieldName, value);
}
public static void RemoveMember(IUserDataDescriptor IUUD, string memberName)
{
LuaUserData.RemoveMember(IUUD, memberName);
}
public static bool HasMember(object obj, string memberName)
{
return LuaUserData.HasMember(obj, memberName);
}
public static void AddCallMetaTable(object userdata) { }
public static DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
{
return LuaUserData.CreateUserDataFromDescriptor(scriptObject, desiredTypeDescriptor);
}
public static DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
return LuaUserData.CreateUserDataFromType(scriptObject, desiredType);
}
}
}
@@ -0,0 +1,234 @@
using System;
namespace Barotrauma
{
public struct LuaSByte
{
public readonly sbyte Value;
public LuaSByte(double v)
{
Value = (sbyte)v;
}
public LuaSByte(string v, int radix = 10)
{
Value = Convert.ToSByte(v, radix);
}
public static implicit operator sbyte(LuaSByte luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaByte
{
public readonly byte Value;
public LuaByte(double v)
{
Value = (byte)v;
}
public LuaByte(string v, int radix = 10)
{
Value = Convert.ToByte(v, radix);
}
public static implicit operator byte(LuaByte luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaInt16
{
public readonly short Value;
public LuaInt16(double v)
{
Value = (short)v;
}
public LuaInt16(string v, int radix = 10)
{
Value = Convert.ToInt16(v, radix);
}
public static implicit operator short(LuaInt16 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaUInt16
{
public readonly ushort Value;
public LuaUInt16(double v)
{
Value = (ushort)v;
}
public LuaUInt16(string v, int radix = 10)
{
Value = Convert.ToUInt16(v, radix);
}
public static implicit operator ushort(LuaUInt16 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaInt32
{
public readonly int Value;
public LuaInt32(double v)
{
Value = (int)v;
}
public LuaInt32(string v, int radix = 10)
{
Value = Convert.ToInt32(v, radix);
}
public static implicit operator int(LuaInt32 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaUInt32
{
public readonly uint Value;
public LuaUInt32(double v)
{
Value = (uint)v;
}
public LuaUInt32(string v, int radix = 10)
{
Value = Convert.ToUInt32(v, radix);
}
public static implicit operator uint(LuaUInt32 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaInt64
{
public readonly long Value;
public LuaInt64(double v)
{
Value = (long)v;
}
public LuaInt64(double lo, double hi)
{
Value = Convert.ToUInt32(lo) | (long)Convert.ToInt32(hi) << 32;
}
public LuaInt64(string v, int radix = 10)
{
Value = Convert.ToInt64(v, radix);
}
public static implicit operator long(LuaInt64 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaUInt64
{
public readonly ulong Value;
public LuaUInt64(double v)
{
Value = (ulong)v;
}
public LuaUInt64(double lo, double hi)
{
Value = Convert.ToUInt32(lo) | (ulong)Convert.ToUInt32(hi) << 32;
}
public LuaUInt64(string v, int radix = 10)
{
Value = Convert.ToUInt64(v, radix);
}
public static implicit operator ulong(LuaUInt64 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaSingle
{
public readonly float Value;
public LuaSingle(double v)
{
Value = (float)v;
}
public LuaSingle(string v)
{
Value = float.Parse(v);
}
public static implicit operator float(LuaSingle luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaDouble
{
public readonly double Value;
public LuaDouble(double v)
{
Value = v;
}
public LuaDouble(string v)
{
Value = double.Parse(v);
}
public static implicit operator double(LuaDouble luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
}
@@ -0,0 +1,236 @@
global using LuaCsHook = Barotrauma.LuaCs.Compatibility.ILuaCsHook;
using System;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using System.Collections.Generic;
using Barotrauma.LuaCs.Compatibility;
using MoonSharp.Interpreter;
using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch;
namespace Barotrauma
{
// XXX: this can't be renamed because of backward compatibility with C# mods
public delegate object LuaCsPatch(object self, Dictionary<string, object> args);
}
namespace Barotrauma.LuaCs
{
partial class LuaPatcherService
{
private static LuaPatcherService instance;
private Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>> compatHookPrefixMethods = new Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>>();
private Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>> compatHookPostfixMethods = new Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>>();
private static void _hookLuaCsPatch(MethodBase __originalMethod, object[] __args, object __instance, out object result, ILuaCsHook.HookMethodType hookType)
{
result = null;
try
{
var funcAddr = ((long)__originalMethod.MethodHandle.GetFunctionPointer());
HashSet<(string, LuaCsCompatPatchFunc)> methodSet = null;
switch (hookType)
{
case ILuaCsHook.HookMethodType.Before:
instance.compatHookPrefixMethods.TryGetValue(funcAddr, out methodSet);
break;
case ILuaCsHook.HookMethodType.After:
instance.compatHookPostfixMethods.TryGetValue(funcAddr, out methodSet);
break;
default:
throw new ArgumentException($"Invalid {nameof(ILuaCsHook.HookMethodType)} enum value.", nameof(hookType));
}
if (methodSet != null)
{
var @params = __originalMethod.GetParameters();
var args = new Dictionary<string, object>();
for (int i = 0; i < @params.Length; i++)
{
args.Add(@params[i].Name, __args[i]);
}
foreach (var tuple in methodSet)
{
var _result = tuple.Item2(__instance, args);
if (_result != null)
{
if (_result is DynValue res)
{
if (!res.IsNil())
{
if (__originalMethod is MethodInfo mi && mi.ReturnType != typeof(void))
{
result = res.ToObject(mi.ReturnType);
}
else
{
result = res.ToObject();
}
}
}
else
{
result = _result;
}
}
}
}
}
catch (Exception ex)
{
LuaCsLogger.LogError($"Error in {__originalMethod.Name}:", LuaCsMessageOrigin.Unknown);
LuaCsLogger.HandleException(ex, LuaCsMessageOrigin.Unknown);
}
}
private static bool HookLuaCsPatchPrefix(MethodBase __originalMethod, object[] __args, object __instance)
{
_hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.Before);
return result == null;
}
private static void HookLuaCsPatchPostfix(MethodBase __originalMethod, object[] __args, object __instance) =>
_hookLuaCsPatch(__originalMethod, __args, __instance, out object _, ILuaCsHook.HookMethodType.After);
private static bool HookLuaCsPatchRetPrefix(MethodBase __originalMethod, object[] __args, ref object __result, object __instance)
{
_hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.Before);
if (result != null)
{
__result = result;
return false;
}
else return true;
}
private static void HookLuaCsPatchRetPostfix(MethodBase __originalMethod, object[] __args, ref object __result, object __instance)
{
_hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.After);
if (result != null) __result = result;
}
private static MethodInfo _miHookLuaCsPatchPrefix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchPrefix", BindingFlags.NonPublic | BindingFlags.Static);
private static MethodInfo _miHookLuaCsPatchPostfix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchPostfix", BindingFlags.NonPublic | BindingFlags.Static);
private static MethodInfo _miHookLuaCsPatchRetPrefix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchRetPrefix", BindingFlags.NonPublic | BindingFlags.Static);
private static MethodInfo _miHookLuaCsPatchRetPostfix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchRetPostfix", BindingFlags.NonPublic | BindingFlags.Static);
// TODO: deprecate this
public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null)
{
if (identifier == null || method == null || patch == null)
{
LuaCsLogger.HandleException(new ArgumentNullException("Identifier, Method and Patch arguments must not be null."), LuaCsMessageOrigin.Unknown);
return;
}
ValidatePatchTarget(method);
var funcAddr = ((long)method.MethodHandle.GetFunctionPointer());
var patches = Harmony.GetPatchInfo(method);
if (hookType == ILuaCsHook.HookMethodType.Before)
{
if (method is MethodInfo mi && mi.ReturnType != typeof(void))
{
if (patches == null || patches.Prefixes == null || patches.Prefixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchRetPrefix) == null)
{
harmony.Patch(method, prefix: new HarmonyMethod(_miHookLuaCsPatchRetPrefix));
}
}
else
{
if (patches == null || patches.Prefixes == null || patches.Prefixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchPrefix) == null)
{
harmony.Patch(method, prefix: new HarmonyMethod(_miHookLuaCsPatchPrefix));
}
}
if (compatHookPrefixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc)> methodSet))
{
if (identifier != "")
{
methodSet.RemoveWhere(tuple => tuple.Item1 == identifier);
}
methodSet.Add((identifier, patch));
}
else if (patch != null)
{
compatHookPrefixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc)>() { (identifier, patch) });
}
}
else if (hookType == ILuaCsHook.HookMethodType.After)
{
if (method is MethodInfo mi && mi.ReturnType != typeof(void))
{
if (patches == null || patches.Postfixes == null || patches.Postfixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchRetPostfix) == null)
{
harmony.Patch(method, postfix: new HarmonyMethod(_miHookLuaCsPatchRetPostfix));
}
}
else
{
if (patches == null || patches.Postfixes == null || patches.Postfixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchPostfix) == null)
{
harmony.Patch(method, postfix: new HarmonyMethod(_miHookLuaCsPatchPostfix));
}
}
if (compatHookPostfixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc)> methodSet))
{
if (identifier != "")
{
methodSet.RemoveWhere(tuple => tuple.Item1 == identifier);
}
methodSet.Add((identifier, patch));
}
else if (patch != null)
{
compatHookPostfixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc)>() { (identifier, patch) });
}
}
}
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterNames);
if (method == null) return;
if (method.GetParameters().Any(x => x.ParameterType.IsByRef))
{
throw new InvalidOperationException($"{nameof(HookMethod)} doesn't support ByRef parameters; use {nameof(Patch)} instead.");
}
HookMethod(identifier, method, patch, hookMethodType);
}
public void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) =>
HookMethod(identifier, className, methodName, null, patch, hookMethodType);
public void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) =>
HookMethod("", className, methodName, null, patch, hookMethodType);
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) =>
HookMethod("", className, methodName, parameterNames, patch, hookMethodType);
public void UnhookMethod(string identifier, MethodBase method, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before)
{
var funcAddr = (long)method.MethodHandle.GetFunctionPointer();
Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>> methods;
if (hookType == ILuaCsHook.HookMethodType.Before) methods = compatHookPrefixMethods;
else if (hookType == ILuaCsHook.HookMethodType.After) methods = compatHookPostfixMethods;
else throw null;
if (methods.ContainsKey(funcAddr)) methods[funcAddr]?.RemoveWhere(t => t.Item1 == identifier);
}
protected void UnhookMethod(string identifier, string className, string methodName, string[] parameterNames, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterNames);
if (method == null) return;
UnhookMethod(identifier, method, hookType);
}
}
}
@@ -0,0 +1,816 @@
using Barotrauma.LuaCs;
using HarmonyLib;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using Sigil;
using Sigil.NonGeneric;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
namespace Barotrauma
{
public delegate void LuaCsAction(params object[] args);
public delegate object LuaCsFunc(params object[] args);
public delegate DynValue LuaCsPatchFunc(object instance, LuaPatcherService.ParameterTable ptable);
}
namespace Barotrauma.LuaCs
{
public partial class LuaPatcherService : ILuaPatcher
{
private class LuaCsHookCallback
{
public string name;
public string hookName;
public LuaCsFunc func;
public LuaCsHookCallback(string name, string hookName, LuaCsFunc func)
{
this.name = name;
this.hookName = hookName;
this.func = func;
}
}
private class LuaCsPatch
{
public string Identifier { get; set; }
public LuaCsPatchFunc PatchFunc { get; set; }
}
private class PatchedMethod
{
public PatchedMethod(MethodInfo harmonyPrefix, MethodInfo harmonyPostfix)
{
HarmonyPrefixMethod = harmonyPrefix;
HarmonyPostfixMethod = harmonyPostfix;
Prefixes = new Dictionary<string, LuaCsPatch>();
Postfixes = new Dictionary<string, LuaCsPatch>();
}
public MethodInfo HarmonyPrefixMethod { get; }
public MethodInfo HarmonyPostfixMethod { get; }
public IEnumerator<LuaCsPatch> GetPrefixEnumerator() => Prefixes.Values.GetEnumerator();
public IEnumerator<LuaCsPatch> GetPostfixEnumerator() => Postfixes.Values.GetEnumerator();
public Dictionary<string, LuaCsPatch> Prefixes { get; }
public Dictionary<string, LuaCsPatch> Postfixes { get; }
}
public class ParameterTable
{
private readonly Dictionary<string, object> parameters;
private bool returnValueModified;
private object returnValue;
public ParameterTable(Dictionary<string, object> dict)
{
parameters = dict;
}
public object this[string paramName]
{
get
{
if (ModifiedParameters.TryGetValue(paramName, out var value))
{
return value;
}
return OriginalParameters[paramName];
}
set
{
ModifiedParameters[paramName] = value;
}
}
public object OriginalReturnValue { get; private set; }
public object ReturnValue
{
get
{
if (returnValueModified) return returnValue;
return OriginalReturnValue;
}
set
{
returnValueModified = true;
returnValue = value;
}
}
public bool PreventExecution { get; set; }
public Dictionary<string, object> OriginalParameters => parameters;
[MoonSharpHidden]
public Dictionary<string, object> ModifiedParameters { get; } = new Dictionary<string, object>();
}
private struct MethodKey : IEquatable<MethodKey>
{
public ModuleHandle ModuleHandle { get; set; }
public int MetadataToken { get; set; }
public override bool Equals(object obj)
{
return obj is MethodKey key && Equals(key);
}
public bool Equals(MethodKey other)
{
return ModuleHandle.Equals(other.ModuleHandle) && MetadataToken == other.MetadataToken;
}
public override int GetHashCode()
{
return HashCode.Combine(ModuleHandle, MetadataToken);
}
public static bool operator ==(MethodKey left, MethodKey right)
{
return left.Equals(right);
}
public static bool operator !=(MethodKey left, MethodKey right)
{
return !(left == right);
}
public static MethodKey Create(MethodBase method) => new MethodKey
{
ModuleHandle = method.Module.ModuleHandle,
MetadataToken = method.MetadataToken,
};
}
private static readonly string[] prohibitedHooks =
{
"Barotrauma.Lua",
"Barotrauma.Cs",
"Barotrauma.ContentPackageManager",
};
private Harmony harmony;
private Lazy<ModuleBuilder> patchModuleBuilder;
private readonly Dictionary<MethodKey, PatchedMethod> registeredPatches = new Dictionary<MethodKey, PatchedMethod>();
public LuaPatcherService()
{
instance = this;
harmony = new Harmony("LuaCsForBarotrauma");
patchModuleBuilder = new Lazy<ModuleBuilder>(CreateModuleBuilder);
UserData.RegisterType<ParameterTable>();
// whats this for?
/*
var hookType = UserData.RegisterType<EventService>();
var hookDesc = (StandardUserDataDescriptor)hookType;
typeof(EventService).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m => {
if (
m.Name.Contains("HookMethod") ||
m.Name.Contains("UnhookMethod") ||
m.Name.Contains("EnqueueFunction") ||
m.Name.Contains("EnqueueTimedFunction")
)
{
hookDesc.AddMember(m.Name, new MethodMemberDescriptor(m, InteropAccessMode.Default));
}
});
*/
}
private static void ValidatePatchTarget(MethodBase method)
{
if (prohibitedHooks.Any(h => method.DeclaringType.FullName.StartsWith(h)))
{
throw new ArgumentException("Hooks into the modding environment are prohibited.");
}
}
private static string NormalizeIdentifier(string identifier)
{
return identifier?.Trim().ToLowerInvariant();
}
private ModuleBuilder CreateModuleBuilder()
{
var assemblyName = $"LuaCsHookPatch-{Guid.NewGuid():N}";
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.RunAndCollect);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("LuaCsHookPatch");
// This code emits the Roslyn attribute
// "IgnoresAccessChecksToAttribute" so we can freely access
// the Barotrauma assembly from our dynamic patches.
// This is important because the generated IL references
// non-public types/members.
// class IgnoresAccessChecksToAttribute {
var typeBuilder = moduleBuilder.DefineType(
name: "System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute",
attr: TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class,
parent: typeof(Attribute));
// [AttributeUsage(AllowMultiple = true)]
var attributeUsageAttribute = new CustomAttributeBuilder(
con: typeof(AttributeUsageAttribute).GetConstructor(new[] { typeof(AttributeTargets) }),
constructorArgs: new object[] { AttributeTargets.Assembly },
namedProperties: new[] { typeof(AttributeUsageAttribute).GetProperty("AllowMultiple") },
propertyValues: new object[] { true });
typeBuilder.SetCustomAttribute(attributeUsageAttribute);
// private readonly string assemblyName;
var attributeTypeFieldBuilder = typeBuilder.DefineField(
fieldName: "assemblyName",
type: typeof(string),
attributes: FieldAttributes.Private | FieldAttributes.InitOnly);
var ctor = Emit.BuildConstructor(
parameterTypes: new[] { typeof(string) },
type: typeBuilder,
attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
callingConvention: CallingConventions.Standard | CallingConventions.HasThis);
// IL: this.assemblyName = arg;
ctor.LoadArgument(0);
ctor.LoadArgument(1);
ctor.StoreField(attributeTypeFieldBuilder);
ctor.Return();
ctor.CreateConstructor();
// public string AttributeName => this.assemblyName;
var attributeNameGetter = Emit.BuildMethod(
returnType: typeof(string),
parameterTypes: new Type[0],
type: typeBuilder,
name: "get_AttributeName",
attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
callingConvention: CallingConventions.Standard | CallingConventions.HasThis);
attributeNameGetter.LoadArgument(0);
attributeNameGetter.LoadField(attributeTypeFieldBuilder);
attributeNameGetter.Return();
var attributeName = typeBuilder.DefineProperty(
name: "AttributeName",
attributes: PropertyAttributes.None,
returnType: typeof(string),
parameterTypes: null);
attributeName.SetGetMethod(attributeNameGetter.CreateMethod());
// }
var type = typeBuilder.CreateTypeInfo().AsType();
// The assembly names are hardcoded, otherwise it would
// break unit tests.
var assembliesToExpose = new[] { "Barotrauma", "DedicatedServer" };
foreach (var name in assembliesToExpose)
{
var attr = new CustomAttributeBuilder(
con: type.GetConstructor(new[] { typeof(string)}),
constructorArgs: new[] { name });
assemblyBuilder.SetCustomAttribute(attr);
}
return moduleBuilder;
}
private static MethodBase ResolveMethod(string className, string methodName, string[] parameters)
{
var classType = LuaCsSetup.Instance.PluginManagementService.GetType(className);
if (classType == null) throw new ScriptRuntimeException($"invalid class name '{className}'");
const BindingFlags BINDING_FLAGS = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
MethodBase method = null;
try
{
if (parameters != null)
{
Type[] parameterTypes = new Type[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
Type type = LuaCsSetup.Instance.PluginManagementService.GetType(parameters[i]);
if (type == null)
{
throw new ScriptRuntimeException($"invalid parameter type '{parameters[i]}'");
}
parameterTypes[i] = type;
}
method = methodName switch
{
".cctor" => classType.TypeInitializer,
".ctor" => classType.GetConstructors(BINDING_FLAGS)
.Except(new[] { classType.TypeInitializer })
.Where(x => x.GetParameters().Select(x => x.ParameterType).SequenceEqual(parameterTypes))
.SingleOrDefault(),
_ => classType.GetMethod(methodName, BINDING_FLAGS, null, parameterTypes, null),
};
}
else
{
ConstructorInfo GetCtor()
{
var ctors = classType.GetConstructors(BINDING_FLAGS)
.Except(new[] { classType.TypeInitializer })
.GetEnumerator();
if (!ctors.MoveNext()) return null;
var ctor = ctors.Current;
if (ctors.MoveNext()) throw new AmbiguousMatchException();
return ctor;
}
method = methodName switch
{
".cctor" => throw new ScriptRuntimeException("type initializers can't have parameters"),
".ctor" => GetCtor(),
_ => classType.GetMethod(methodName, BINDING_FLAGS),
};
}
}
catch (AmbiguousMatchException)
{
throw new ScriptRuntimeException("ambiguous method signature");
}
if (method == null)
{
var parameterNamesStr = parameters == null ? "" : string.Join(", ", parameters);
throw new ScriptRuntimeException($"method '{methodName}({parameterNamesStr})' not found in class '{className}'");
}
return method;
}
private class DynamicParameterMapping
{
public DynamicParameterMapping(string name, Type originalMethodParamType, Type harmonyPatchParamType)
{
ParameterName = name;
OriginalMethodParamType = originalMethodParamType;
HarmonyPatchParamType = harmonyPatchParamType;
}
public string ParameterName { get; set; }
public Type OriginalMethodParamType { get; set; }
public Type HarmonyPatchParamType { get; set; }
}
private static readonly Regex InvalidIdentifierCharsRegex = new Regex(@"[^\w\d]", RegexOptions.Compiled);
private const string FIELD_LUACS = "LuaCs";
public bool IsDisposed { get; private set; }
// If you need to debug this:
// - use https://sharplab.io ; it's a very useful for resource for writing IL by hand.
// - use il.NewMessage("") or il.WriteLine("") to see where the IL crashes at runtime.
private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase original, LuaCsHook.HookMethodType hookType)
{
var parameters = new List<DynamicParameterMapping>
{
new DynamicParameterMapping("__originalMethod", null, typeof(MethodBase)),
new DynamicParameterMapping("__instance", null, typeof(object)),
};
var hasReturnType = original is MethodInfo mi && mi.ReturnType != typeof(void);
if (hasReturnType)
{
parameters.Add(new DynamicParameterMapping("__result", null, typeof(object).MakeByRefType()));
}
foreach (var parameter in original.GetParameters())
{
var paramName = parameter.Name;
var originalMethodParamType = parameter.ParameterType;
var harmonyPatchParamType = originalMethodParamType.IsByRef
? originalMethodParamType
// Make all parameters modifiable by the harmony patch
: originalMethodParamType.MakeByRefType();
parameters.Add(new DynamicParameterMapping(paramName, originalMethodParamType, harmonyPatchParamType));
}
static string MangleName(object o) => InvalidIdentifierCharsRegex.Replace(o?.ToString(), "_");
var moduleBuilder = patchModuleBuilder.Value;
var mangledName = original.DeclaringType != null
? $"{MangleName(original.DeclaringType)}-{MangleName(original)}"
: MangleName(original);
var typeBuilder = moduleBuilder.DefineType($"Patch_{identifier}_{Guid.NewGuid():N}_{mangledName}", TypeAttributes.Public);
var luaCsField = typeBuilder.DefineField(FIELD_LUACS, typeof(LuaCsSetup), FieldAttributes.Public | FieldAttributes.Static);
var methodName = hookType == LuaCsHook.HookMethodType.Before ? "HarmonyPrefix" : "HarmonyPostfix";
var il = Emit.BuildMethod(
returnType: hookType == LuaCsHook.HookMethodType.Before ? typeof(bool) : typeof(void),
parameterTypes: parameters.Select(x => x.HarmonyPatchParamType).ToArray(),
type: typeBuilder,
name: methodName,
attributes: MethodAttributes.Public | MethodAttributes.Static,
callingConvention: CallingConventions.Standard);
var labelReturn = il.DefineLabel("endOfFunction");
il.BeginExceptionBlock(out var exceptionBlock);
// IL: var harmonyReturnValue = true;
var harmonyReturnValue = il.DeclareLocal<bool>("harmonyReturnValue");
il.LoadConstant(true);
il.StoreLocal(harmonyReturnValue);
// IL: var patchKey = MethodKey.Create(__originalMethod);
var patchKey = il.DeclareLocal<MethodKey>("patchKey");
il.LoadArgument(0); // load __originalMethod
il.CastClass<MethodBase>();
il.Call(typeof(MethodKey).GetMethod(nameof(MethodKey.Create)));
il.StoreLocal(patchKey);
// IL: var patchExists = instance.registeredPatches.TryGetValue(patchKey, out MethodPatches patches)
var patchExists = il.DeclareLocal<bool>("patchExists");
var patches = il.DeclareLocal<PatchedMethod>("patches");
il.LoadField(typeof(LuaPatcherService).GetField(nameof(instance), BindingFlags.NonPublic | BindingFlags.Static));
il.LoadField(typeof(LuaPatcherService).GetField(nameof(registeredPatches), BindingFlags.NonPublic | BindingFlags.Instance));
il.LoadLocal(patchKey);
il.LoadLocalAddress(patches); // out parameter
il.Call(typeof(Dictionary<MethodKey, PatchedMethod>).GetMethod("TryGetValue"));
il.StoreLocal(patchExists);
// IL: if (!patchExists)
il.LoadLocal(patchExists);
il.IfNot((il) =>
{
// XXX: if we get here, it's probably because a patched
// method was running when `reloadlua` was executed.
// This can happen with a postfix on
// `Barotrauma.Networking.GameServer#Update`.
il.Leave(labelReturn);
});
// IL: var parameterDict = new Dictionary<string, object>(<paramCount>);
var parameterDict = il.DeclareLocal<Dictionary<string, object>>("parameterDict");
il.LoadConstant(parameters.Count(x => x.OriginalMethodParamType != null)); // preallocate the dictionary using the # of args
il.NewObject(typeof(Dictionary<string, object>), typeof(int));
il.StoreLocal(parameterDict);
for (ushort i = 0; i < parameters.Count; i++)
{
// Skip parameters that don't exist in the original method
if (parameters[i].OriginalMethodParamType == null) continue;
// IL: parameterDict.Add(<paramName>, <paramValue>);
il.LoadLocal(parameterDict);
il.LoadConstant(parameters[i].ParameterName);
il.LoadArgument(i);
il.ToObject(parameters[i].HarmonyPatchParamType);
il.Call(typeof(Dictionary<string, object>).GetMethod("Add"));
}
// IL: var ptable = new ParameterTable(parameterDict);
var ptable = il.DeclareLocal<ParameterTable>("ptable");
il.LoadLocal(parameterDict);
il.NewObject(typeof(ParameterTable), typeof(Dictionary<string, object>));
il.StoreLocal(ptable);
if (hasReturnType && hookType == LuaCsHook.HookMethodType.After)
{
// IL: ptable.OriginalReturnValue = __result;
il.LoadLocal(ptable);
il.LoadArgument(2); // ref __result
il.ToObject(parameters[2].HarmonyPatchParamType);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.OriginalReturnValue)).GetSetMethod(nonPublic: true));
}
// IL: var enumerator = patches.GetPrefixEnumerator();
var enumerator = il.DeclareLocal<IEnumerator<LuaCsPatch>>("enumerator");
il.LoadLocal(patches);
il.CallVirtual(typeof(PatchedMethod).GetMethod(
name: hookType == LuaCsHook.HookMethodType.Before
? nameof(PatchedMethod.GetPrefixEnumerator)
: nameof(PatchedMethod.GetPostfixEnumerator),
bindingAttr: BindingFlags.Public | BindingFlags.Instance));
il.StoreLocal(enumerator);
var labelUpdateParameters = il.DefineLabel("updateParameters");
// Iterate over prefixes/postfixes
il.ForEachEnumerator<LuaCsPatch>(enumerator, (il, current, labelLeave) =>
{
// IL: var luaReturnValue = current.PatchFunc.Invoke(__instance, ptable);
var luaReturnValue = il.DeclareLocal<DynValue>("luaReturnValue");
il.LoadLocal(current);
il.Call(typeof(LuaCsPatch).GetProperty(nameof(LuaCsPatch.PatchFunc)).GetGetMethod());
il.LoadArgument(1); // __instance
il.LoadLocal(ptable);
il.CallVirtual(typeof(LuaCsPatchFunc).GetMethod("Invoke"));
il.StoreLocal(luaReturnValue);
if (hasReturnType)
{
// IL: var ptableReturnValue = ptable.ReturnValue;
var ptableReturnValue = il.DeclareLocal<object>("ptableReturnValue");
il.LoadLocal(ptable);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.ReturnValue)).GetGetMethod());
il.StoreLocal(ptableReturnValue);
// IL: if (ptableReturnValue != null)
il.LoadLocal(ptableReturnValue);
il.If((il) =>
{
// IL: __result = ptableReturnValue;
il.LoadArgument(2); // ref __result
il.LoadLocal(ptableReturnValue);
il.StoreIndirect(typeof(object));
il.Break();
});
// IL: if (luaReturnValue != null)
il.LoadLocal(luaReturnValue);
il.If((il) =>
{
// IL: if (!luaReturnValue.IsVoid())
il.LoadLocal(luaReturnValue);
il.Call(typeof(DynValue).GetMethod(nameof(DynValue.IsVoid)));
il.IfNot((il) =>
{
// IL: var csReturnType = Type.GetTypeFromHandle(<original.ReturnType>);
var csReturnType = il.DeclareLocal<Type>("csReturnType");
il.LoadType(((MethodInfo)original).ReturnType);
il.StoreLocal(csReturnType);
// IL: var csReturnValue = luaReturnValue.ToObject(csReturnType);
var csReturnValue = il.DeclareLocal<object>("csReturnValue");
il.LoadLocal(luaReturnValue);
il.LoadLocal(csReturnType);
il.Call(typeof(DynValue).GetMethod(
name: nameof(DynValue.ToObject),
bindingAttr: BindingFlags.Public | BindingFlags.Instance,
binder: null,
types: new Type[] { typeof(Type) },
modifiers: null));
il.StoreLocal(csReturnValue);
// IL: __result = csReturnValue;
il.LoadArgument(2); // ref __result
il.LoadLocal(csReturnValue);
il.StoreIndirect(typeof(object));
});
});
}
// IL: if (ptable.PreventExecution)
il.LoadLocal(ptable);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.PreventExecution)).GetGetMethod());
il.If((il) =>
{
// IL: harmonyReturnValue = false;
il.LoadConstant(false);
il.StoreLocal(harmonyReturnValue);
// IL: break;
il.Leave(labelLeave);
});
});
// IL: var modifiedParameters = ptable.ModifiedParameters;
var modifiedParameters = il.DeclareLocal<Dictionary<string, object>>("modifiedParameters");
il.LoadLocal(ptable);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.ModifiedParameters)).GetGetMethod());
il.StoreLocal(modifiedParameters);
// IL: object modifiedValue;
var modifiedValue = il.DeclareLocal<object>("modifiedValue");
// Update the parameters
for (ushort i = 0; i < parameters.Count; i++)
{
// Skip parameters that don't exist in the original method
if (parameters[i].OriginalMethodParamType == null) continue;
// IL: if (modifiedParameters.TryGetValue("parameterName", out modifiedValue))
il.LoadLocal(modifiedParameters);
il.LoadConstant(parameters[i].ParameterName);
il.LoadLocalAddress(modifiedValue); // out parameter
il.Call(typeof(Dictionary<string, object>).GetMethod(nameof(Dictionary<string, object>.TryGetValue)));
il.If((il) =>
{
// XXX: GetElementType() gets the "real" type behind
// the ByRef. This is safe because all the parameters
// are made into ByRef to support modification.
var paramType = parameters[i].HarmonyPatchParamType.GetElementType();
// IL: ref argName = modifiedValue;
il.LoadArgument(i);
il.LoadLocalAndCast(modifiedValue, paramType);
if (paramType.IsValueType)
{
il.StoreObject(paramType);
}
else
{
il.StoreIndirect(paramType);
}
});
}
il.MarkLabel(labelReturn);
// IL: catch (Exception exception)
il.BeginCatchAllBlock(exceptionBlock, out var catchBlock);
var exception = il.DeclareLocal<Exception>("exception");
il.StoreLocal(exception);
// IL: if (LuaCs != null)
il.LoadField(luaCsField);
il.If((il) =>
{
// IL: LuaCs.HandleException(exception, LuaCsMessageOrigin.LuaMod);
il.LoadLocal(exception);
il.LoadConstant((int)LuaCsMessageOrigin.LuaMod); // underlying enum type is int
il.Call(typeof(LuaCsLogger).GetMethod(nameof(LuaCsLogger.HandleException), BindingFlags.Public | BindingFlags.Static));
});
il.EndCatchBlock(catchBlock);
il.EndExceptionBlock(exceptionBlock);
// Only prefixes return a bool
if (hookType == LuaCsHook.HookMethodType.Before)
{
il.LoadLocal(harmonyReturnValue);
}
il.Return();
var method = il.CreateMethod();
for (var i = 0; i < parameters.Count; i++)
{
method.DefineParameter(i + 1, ParameterAttributes.None, parameters[i].ParameterName);
}
var type = typeBuilder.CreateType();
type.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static).SetValue(null, LuaCsSetup.Instance);
return type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static);
}
private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
if (method == null) throw new ArgumentNullException(nameof(method));
if (patch == null) throw new ArgumentNullException(nameof(patch));
ValidatePatchTarget(method);
identifier ??= Guid.NewGuid().ToString("N");
identifier = NormalizeIdentifier(identifier);
var patchKey = MethodKey.Create(method);
if (!registeredPatches.TryGetValue(patchKey, out var methodPatches))
{
var harmonyPrefix = CreateDynamicHarmonyPatch(identifier, method, LuaCsHook.HookMethodType.Before);
var harmonyPostfix = CreateDynamicHarmonyPatch(identifier, method, LuaCsHook.HookMethodType.After);
harmony.Patch(method, prefix: new HarmonyMethod(harmonyPrefix), postfix: new HarmonyMethod(harmonyPostfix));
methodPatches = registeredPatches[patchKey] = new PatchedMethod(harmonyPrefix, harmonyPostfix);
}
if (hookType == LuaCsHook.HookMethodType.Before)
{
if (methodPatches.Prefixes.Remove(identifier))
{
LuaCsLogger.LogMessage($"Replacing existing prefix: {identifier}");
}
methodPatches.Prefixes.Add(identifier, new LuaCsPatch
{
Identifier = identifier,
PatchFunc = patch,
});
}
else if (hookType == LuaCsHook.HookMethodType.After)
{
if (methodPatches.Postfixes.Remove(identifier))
{
LuaCsLogger.LogMessage($"Replacing existing postfix: {identifier}");
}
methodPatches.Postfixes.Add(identifier, new LuaCsPatch
{
Identifier = identifier,
PatchFunc = patch,
});
}
return identifier;
}
public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterTypes);
return Patch(identifier, method, patch, hookType);
}
public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, null);
return Patch(identifier, method, patch, hookType);
}
public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterTypes);
return Patch(null, method, patch, hookType);
}
public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, null);
return Patch(null, method, patch, hookType);
}
private bool RemovePatch(string identifier, MethodBase method, LuaCsHook.HookMethodType hookType)
{
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
identifier = NormalizeIdentifier(identifier);
var patchKey = MethodKey.Create(method);
if (!registeredPatches.TryGetValue(patchKey, out var methodPatches))
{
return false;
}
return hookType switch
{
LuaCsHook.HookMethodType.Before => methodPatches.Prefixes.Remove(identifier),
LuaCsHook.HookMethodType.After => methodPatches.Postfixes.Remove(identifier),
_ => throw new ArgumentException($"Invalid {nameof(LuaCsHook.HookMethodType)} enum value.", nameof(hookType)),
};
}
public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType)
{
var method = ResolveMethod(className, methodName, parameterTypes);
return RemovePatch(identifier, method, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType)
{
var method = ResolveMethod(className, methodName, null);
return RemovePatch(identifier, method, hookType);
}
private void ClearAll()
{
harmony?.UnpatchSelf();
foreach (var (_, patch) in registeredPatches)
{
// Remove references stored in our dynamic types so the generated
// assembly can be garbage-collected.
patch.HarmonyPrefixMethod.DeclaringType
.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static)
.SetValue(null, null);
patch.HarmonyPostfixMethod.DeclaringType
.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static)
.SetValue(null, null);
}
registeredPatches.Clear();
compatHookPrefixMethods.Clear();
compatHookPostfixMethods.Clear();
}
public void Dispose()
{
IsDisposed = true;
ClearAll();
}
public FluentResults.Result Reset()
{
ClearAll();
return FluentResults.Result.Ok();
}
}
}
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
using System.IO;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Loaders;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using FluentResults;
namespace Barotrauma.LuaCs
{
public class LuaScriptLoader : ScriptLoaderBase, ILuaScriptLoader
{
public LuaScriptLoader(ISafeStorageService storageService, Lazy<ILoggerService> loggerService)
{
this._storageService = storageService;
this._loggerService = loggerService;
storageService.UseCaching = true;
}
private readonly ISafeStorageService _storageService;
private readonly Lazy<ILoggerService> _loggerService;
public override object LoadFile(string file, Table globalContext)
{
IService.CheckDisposed(this);
if (file.IsNullOrWhiteSpace())
{
return null;
}
var res = _storageService.TryLoadText(file);
if (res.IsFailed || res is not { Value: { } script})
{
UnsafeLogErrors($"Failed to load file '{file}'.", res.ToResult());
return null;
}
if (script.IsNullOrWhiteSpace())
{
UnsafeLogErrors($"The file '{file}' is empty. ", res.ToResult());
return null;
}
return script;
}
public void ClearCaches()
{
IService.CheckDisposed(this);
_storageService?.PurgeCache();
}
public void SetCachingPolicy(bool useCaching)
{
if (_storageService is null)
{
return;
}
if (!useCaching)
{
_storageService.PurgeCache();
}
_storageService.UseCaching = useCaching;
}
public async Task<Result<ImmutableArray<(ContentPath Path, Result<string>)>>> CacheResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourceInfos)
{
IService.CheckDisposed(this);
if (!_storageService.UseCaching)
{
return FluentResults.Result.Fail($"Caching is not enabled.");
}
return await this._storageService.LoadPackageTextFilesAsync([..resourceInfos.SelectMany(ri => ri.FilePaths)]);
}
public override bool ScriptFileExists(string file)
{
IService.CheckDisposed(this);
var result = _storageService.FileExists(file);
if (result is { IsFailed: true })
{
UnsafeLogErrors($"Unable to find and load file \"{file}\".", result.ToResult());
return false;
}
return result.Value;
}
private void UnsafeLogErrors(string message, FluentResults.Result result = null)
{
_loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: {message}");
if (result is null || result.Errors.Count <= 0)
{
return;
}
foreach (var error in result.Errors)
{
_loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: Error: {error.Message}.");
}
}
public void Dispose()
{
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_storageService?.Dispose();
_loggerService?.Value.Dispose();
}
private int _isDisposed = 0;
public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed);
public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true)
{
IService.CheckDisposed(this);
return _storageService.IsFileAccessible(path, readOnly, checkWhitelistOnly);
}
public void AddFileToWhitelist(string path, bool readOnly = true)
{
IService.CheckDisposed(this);
_storageService.AddFileToWhitelist(path, readOnly);
}
public void AddFilesToWhitelist(ImmutableArray<string> paths, bool readOnly = true)
{
IService.CheckDisposed(this);
_storageService.AddFilesToWhitelist(paths, readOnly);
}
public void RemoveFileFromAllWhitelists(string path)
{
IService.CheckDisposed(this);
_storageService.RemoveFileFromAllWhitelists(path);
}
public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths)
{
IService.CheckDisposed(this);
return _storageService.SetReadOnlyWhitelist(filePaths);
}
public FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths)
{
IService.CheckDisposed(this);
return _storageService.SetReadWriteWhitelist(filePaths);
}
public void ClearAllWhitelists()
{
IService.CheckDisposed(this);
_storageService.ClearAllWhitelists();
}
}
}
@@ -0,0 +1,413 @@
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma.LuaCs;
public interface ILuaUserDataService : IReusableService
{
IReadOnlyDictionary<string, IUserDataDescriptor> Descriptors { get; }
IUserDataDescriptor RegisterType(string typeName);
void RegisterExtensionType(string typeName);
bool IsRegistered(string typeName);
void UnregisterType(string typeName, bool deleteHistory = false);
object CreateStatic(string typeName);
bool IsTargetType(object obj, string typeName);
string TypeOf(object obj);
object CreateEnumTable(string typeName);
void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName);
void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null);
void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName);
void AddMethod(IUserDataDescriptor IUUD, string methodName, object function);
void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value);
void RemoveMember(IUserDataDescriptor IUUD, string memberName);
bool HasMember(object obj, string memberName);
/// <summary>
/// See <see cref="CreateUserDataFromType"/>.
/// </summary>
/// <param name="scriptObject">Lua value to convert and wrap in a userdata.</param>
/// <param name="desiredTypeDescriptor">Descriptor of the type of the object to convert the Lua value to. Uses MoonSharp ScriptToClr converters.</param>
/// <returns>A userdata that wraps the Lua value converted to an object of the desired type as described by <paramref name="desiredTypeDescriptor"/>.</returns>
DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor);
/// <summary>
/// Converts a Lua value to a CLR object of a desired type and wraps it in a userdata.
/// If the type is not registered, then a new <see cref="MoonSharp.Interpreter.Interop.StandardUserDataDescriptor"/> will be created and used.
/// The goal of this method is to allow Lua scripts to create userdata to wrap certain data without having to register types.
/// <remarks>Wrapping the value in a userdata preserves the original type during script-to-CLR conversions.</remarks>
/// <example>A Lua script needs to pass a List`1 to a CLR method expecting System.Object, MoonSharp gets
/// in the way by converting the List`1 to a MoonSharp.Interpreter.Table and breaking everything.
/// Registering the List`1 type can break other scripts relying on default converters, so instead
/// it is better to manually wrap the List`1 object into a userdata.
/// </example>
/// </summary>
/// <param name="scriptObject">Lua value to convert and wrap in a userdata.</param>
/// <param name="desiredType">Type describing the CLR type of the object to convert the Lua value to.</param>
/// <returns>A userdata that wraps the Lua value converted to an object of the desired type.</returns>
DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType);
void AddCallMetaTable(object userdata);
}
public class LuaUserDataService : ILuaUserDataService
{
public bool IsDisposed { get; private set; }
public IReadOnlyDictionary<string, IUserDataDescriptor> Descriptors => descriptors;
private ConcurrentDictionary<string, IUserDataDescriptor> descriptors;
private readonly IPluginManagementService _pluginManagementService;
public LuaUserDataService(IPluginManagementService pluginManagementService)
{
descriptors = new ConcurrentDictionary<string, IUserDataDescriptor>();
_pluginManagementService = pluginManagementService;
}
public IUserDataDescriptor this[string key]
{
get
{
return descriptors.GetValueOrDefault(key);
}
}
private Type GetType(string typeName) => _pluginManagementService.GetType(typeName, includeInterfaces: true);
public IUserDataDescriptor RegisterType(string typeName)
{
Type type = GetType(typeName);
if (type == null)
{
throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}.");
}
var descriptor = UserData.RegisterType(type);
descriptors.TryAdd(typeName, descriptor);
return descriptor;
}
public void RegisterExtensionType(string typeName)
{
Type type = GetType(typeName);
if (type == null)
{
throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}.");
}
UserData.RegisterExtensionType(type);
}
public bool IsRegistered(string typeName)
{
Type type = GetType(typeName);
if (type == null)
{
return false;
}
return UserData.GetDescriptorForType(type, true) != null;
}
public void UnregisterType(string typeName, bool deleteHistory = false)
{
Type type = GetType(typeName);
if (type == null)
{
throw new ScriptRuntimeException($"tried to unregister a type that doesn't exist: {typeName}.");
}
UserData.UnregisterType(type, deleteHistory);
}
public bool IsTargetType(object obj, string typeName)
{
if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); }
Type targetType = GetType(typeName);
if (targetType == null) { throw new ScriptRuntimeException("target type not found"); }
Type type = obj is Type ? (Type)obj : obj.GetType();
return targetType.IsAssignableFrom(type);
}
public string TypeOf(object obj)
{
if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); }
return obj.GetType().FullName;
}
public object CreateEnumTable(string typeName)
{
Type type = GetType(typeName);
if (type == null)
{
throw new ScriptRuntimeException($"tried to create an enum table with a type that doesn't exist:: {typeName}.");
}
Dictionary<string, object> result = new Dictionary<string, object>();
foreach (var value in Enum.GetValues(type))
{
string name = Enum.GetName(type, value);
result[name] = value;
}
return result;
}
public object CreateStatic(string typeName)
{
Type type = GetType(typeName);
if (type == null)
{
throw new ScriptRuntimeException($"tried to create a static userdata of a type that doesn't exist: {typeName}.");
}
MethodInfo method = typeof(UserData).GetMethod(nameof(UserData.CreateStatic), 1, new Type[0]);
MethodInfo generic = method.MakeGenericMethod(type);
return generic.Invoke(null, null);
}
private FieldInfo FindFieldRecursively(Type type, string fieldName)
{
var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (field == null && type.BaseType != null)
{
return FindFieldRecursively(type.BaseType, fieldName);
}
return field;
}
public void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName)
{
if (IUUD == null)
{
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {fieldName} accessible.");
}
var descriptor = (StandardUserDataDescriptor)IUUD;
FieldInfo field = FindFieldRecursively(IUUD.Type, fieldName);
if (field == null)
{
throw new ScriptRuntimeException($"tried to make field '{fieldName}' accessible, but the field doesn't exist.");
}
descriptor.RemoveMember(fieldName);
descriptor.AddMember(fieldName, new FieldMemberDescriptor(field, InteropAccessMode.Default));
}
private MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null)
{
MethodInfo method;
if (types == null)
{
method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
}
else
{
method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, types);
}
if (method == null && type.BaseType != null)
{
return FindMethodRecursively(type.BaseType, methodName, types);
}
return method;
}
public void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null)
{
if (IUUD == null)
{
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {methodName} accessible.");
}
Type[] parameterTypes = null;
if (parameters != null)
{
parameterTypes = new Type[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
Type type = GetType(parameters[i]);
if (type == null)
{
throw new ScriptRuntimeException($"invalid parameter type '{parameters[i]}'");
}
parameterTypes[i] = type;
}
}
var descriptor = (StandardUserDataDescriptor)IUUD;
MethodBase method;
try
{
method = FindMethodRecursively(IUUD.Type, methodName, parameterTypes);
}
catch (AmbiguousMatchException ex)
{
throw new ScriptRuntimeException("ambiguous method signature.");
}
if (method == null)
{
throw new ScriptRuntimeException($"tried to make method '{methodName}' accessible, but the method doesn't exist.");
}
descriptor.AddMember(methodName, new MethodMemberDescriptor(method, InteropAccessMode.Default));
}
private PropertyInfo FindPropertyRecursively(Type type, string propertyName)
{
var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (property == null && type.BaseType != null)
{
return FindPropertyRecursively(type.BaseType, propertyName);
}
return property;
}
public void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName)
{
if (IUUD == null)
{
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {propertyName} accessible.");
}
var descriptor = (StandardUserDataDescriptor)IUUD;
PropertyInfo property = FindPropertyRecursively(IUUD.Type, propertyName);
if (property == null)
{
throw new ScriptRuntimeException($"tried to make property '{propertyName}' accessible, but the property doesn't exist.");
}
descriptor.RemoveMember(propertyName);
descriptor.AddMember(propertyName, new PropertyMemberDescriptor(property, InteropAccessMode.Default, property.GetGetMethod(true), property.GetSetMethod(true)));
}
public void AddMethod(IUserDataDescriptor IUUD, string methodName, object function)
{
if (IUUD == null)
{
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add method {methodName}.");
}
var descriptor = (StandardUserDataDescriptor)IUUD;
descriptor.RemoveMember(methodName);
descriptor.AddMember(methodName, new ObjectCallbackMemberDescriptor(methodName, (object arg1, ScriptExecutionContext arg2, CallbackArguments arg3) =>
{
if (LuaCsSetup.Instance != null)
{
return LuaCsSetup.Instance.CallLuaFunction(function, arg3.GetArray());
}
return null;
}));
}
public void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value)
{
if (IUUD == null)
{
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add field {fieldName}.");
}
var descriptor = (StandardUserDataDescriptor)IUUD;
descriptor.RemoveMember(fieldName);
descriptor.AddMember(fieldName, new DynValueMemberDescriptor(fieldName, value));
}
public void RemoveMember(IUserDataDescriptor IUUD, string memberName)
{
if (IUUD == null)
{
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to remove the member {memberName}.");
}
var descriptor = (StandardUserDataDescriptor)IUUD;
descriptor.RemoveMember(memberName);
}
public bool HasMember(object obj, string memberName)
{
if (obj == null) { throw new ScriptRuntimeException("object is nil"); }
Type type;
if (obj is Type)
{
type = (Type)obj;
}
else if (obj is IUserDataDescriptor descriptor)
{
type = descriptor.Type;
if (((StandardUserDataDescriptor)descriptor).HasMember(memberName))
{
return true;
}
}
else
{
type = obj.GetType();
}
if (type.GetMember(memberName).Length == 0)
{
return false;
}
return true;
}
public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
{
return UserData.Create(scriptObject.ToObject(desiredTypeDescriptor.Type), desiredTypeDescriptor);
}
public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
IUserDataDescriptor descriptor = UserData.GetDescriptorForType(desiredType, true);
descriptor ??= new StandardUserDataDescriptor(desiredType, InteropAccessMode.Default);
return CreateUserDataFromDescriptor(scriptObject, descriptor);
}
public void AddCallMetaTable(object userdata) { }
public void Dispose()
{
IsDisposed = true;
descriptors.Clear();
}
public FluentResults.Result Reset()
{
descriptors.Clear();
return FluentResults.Result.Ok();
}
}
@@ -0,0 +1,236 @@
using Barotrauma;
using Barotrauma.LuaCs;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Barotrauma.LuaCs;
public interface ISafeLuaUserDataService : IService
{
bool IsAllowed(string typeName);
IUserDataDescriptor RegisterType(string typeName);
void RegisterExtensionType(string typeName);
bool IsRegistered(string typeName);
void UnregisterType(string typeName, bool deleteHistory = false);
object CreateStatic(string typeName);
bool IsTargetType(object obj, string typeName);
string TypeOf(object obj);
object CreateEnumTable(string typeName);
void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName);
void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null);
void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName);
void AddMethod(IUserDataDescriptor IUUD, string methodName, object function);
void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value);
void RemoveMember(IUserDataDescriptor IUUD, string memberName);
bool HasMember(object obj, string memberName);
/// <summary>
/// See <see cref="CreateUserDataFromType"/>.
/// </summary>
/// <param name="scriptObject">Lua value to convert and wrap in a userdata.</param>
/// <param name="desiredTypeDescriptor">Descriptor of the type of the object to convert the Lua value to. Uses MoonSharp ScriptToClr converters.</param>
/// <returns>A userdata that wraps the Lua value converted to an object of the desired type as described by <paramref name="desiredTypeDescriptor"/>.</returns>
DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor);
/// <summary>
/// Converts a Lua value to a CLR object of a desired type and wraps it in a userdata.
/// If the type is not registered, then a new <see cref="MoonSharp.Interpreter.Interop.StandardUserDataDescriptor"/> will be created and used.
/// The goal of this method is to allow Lua scripts to create userdata to wrap certain data without having to register types.
/// <remarks>Wrapping the value in a userdata preserves the original type during script-to-CLR conversions.</remarks>
/// <example>A Lua script needs to pass a List`1 to a CLR method expecting System.Object, MoonSharp gets
/// in the way by converting the List`1 to a MoonSharp.Interpreter.Table and breaking everything.
/// Registering the List`1 type can break other scripts relying on default converters, so instead
/// it is better to manually wrap the List`1 object into a userdata.
/// </example>
/// </summary>
/// <param name="scriptObject">Lua value to convert and wrap in a userdata.</param>
/// <param name="desiredType">Type describing the CLR type of the object to convert the Lua value to.</param>
/// <returns>A userdata that wraps the Lua value converted to an object of the desired type.</returns>
DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType);
void AddCallMetaTable(object userdata);
}
public class SafeLuaUserDataService : ISafeLuaUserDataService
{
private readonly ILuaUserDataService _userDataService;
public bool IsDisposed { get; private set; }
public SafeLuaUserDataService(ILuaUserDataService userDataService)
{
_userDataService = userDataService;
}
public IUserDataDescriptor this[string key]
{
get
{
return _userDataService.Descriptors.GetValueOrDefault(key);
}
}
private bool CanBeRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
if (typeName == "System.Single") { return true; }
if (typeName == "System.Console") { return true; }
if (typeName.StartsWith("System.Collections", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Microsoft.Xna", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Barotrauma.IO", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.ToolBox", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.SaveUtil", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.", StringComparison.Ordinal))
return true;
return false;
}
private bool CanBeReRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
return true;
}
public bool IsAllowed(string typeName)
{
if (!CanBeReRegistered(typeName) && IsRegistered(typeName))
{
return false;
}
if (!CanBeRegistered(typeName))
{
return false;
}
return true;
}
private void CheckAllowed(string typeName)
{
if (!IsAllowed(typeName))
{
throw new ScriptRuntimeException($"Type {typeName} can't be registered");
}
}
public IUserDataDescriptor RegisterType(string typeName)
{
CheckAllowed(typeName);
return _userDataService.RegisterType(typeName);
}
public void RegisterExtensionType(string typeName)
{
CheckAllowed(typeName);
_userDataService.RegisterExtensionType(typeName);
}
public bool IsRegistered(string typeName)
{
return _userDataService.IsRegistered(typeName);
}
public void UnregisterType(string typeName, bool deleteHistory = false)
{
IsAllowed(typeName);
_userDataService.UnregisterType(typeName, deleteHistory);
}
public object CreateStatic(string typeName)
{
return _userDataService.CreateStatic(typeName);
}
public bool IsTargetType(object obj, string typeName)
{
return _userDataService.IsTargetType(obj, typeName);
}
public string TypeOf(object obj)
{
return _userDataService.TypeOf(obj);
}
public object CreateEnumTable(string typeName)
{
return _userDataService.CreateEnumTable(typeName);
}
public void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName)
{
_userDataService.MakeFieldAccessible(IUUD, fieldName);
}
public void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null)
{
_userDataService.MakeMethodAccessible(IUUD, methodName, parameters);
}
public void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName)
{
_userDataService.MakePropertyAccessible(IUUD, propertyName);
}
public void AddMethod(IUserDataDescriptor IUUD, string methodName, object function)
{
_userDataService.AddMethod(IUUD, methodName, function);
}
public void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value)
{
_userDataService.AddField(IUUD, fieldName, value);
}
public void RemoveMember(IUserDataDescriptor IUUD, string memberName)
{
_userDataService.RemoveMember(IUUD, memberName);
}
public bool HasMember(object obj, string memberName)
{
return _userDataService.HasMember(obj, memberName);
}
public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
{
return _userDataService.CreateUserDataFromDescriptor(scriptObject, desiredTypeDescriptor);
}
public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
return _userDataService.CreateUserDataFromType(scriptObject, desiredType);
}
public void AddCallMetaTable(object userdata) { }
public void Dispose()
{
IsDisposed = true;
}
}