Merge branch 'heads/upstream' into OBT/1.2.0(SpringUpdate)
This commit is contained in:
@@ -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
|
||||
}
|
||||
+416
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+670
@@ -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();
|
||||
}
|
||||
}
|
||||
+281
@@ -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;
|
||||
}
|
||||
}
|
||||
+212
@@ -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));
|
||||
}
|
||||
}
|
||||
+25
@@ -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);
|
||||
}
|
||||
+24
@@ -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();
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
+25
@@ -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);
|
||||
}
|
||||
+59
@@ -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,
|
||||
}
|
||||
+32
@@ -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; }
|
||||
}
|
||||
+66
@@ -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();
|
||||
}
|
||||
+22
@@ -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);
|
||||
}
|
||||
+39
@@ -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);
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
+63
@@ -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();
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
+54
@@ -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}'!");
|
||||
}
|
||||
}
|
||||
+118
@@ -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
|
||||
}
|
||||
+84
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
+6
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
+418
@@ -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>();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+82
@@ -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; }
|
||||
}
|
||||
}
|
||||
+151
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+153
@@ -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;
|
||||
}
|
||||
}
|
||||
+247
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+116
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -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();
|
||||
}
|
||||
}
|
||||
+236
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user