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

This commit is contained in:
NotAlwaysTrue
2026-04-25 13:08:16 +08:00
420 changed files with 24089 additions and 11191 deletions
@@ -0,0 +1,76 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Barotrauma.LuaCs;
// taken from: <https://gist.github.com/cajuncoding/a88f0d00847dcfc241ae80d1c7bafb1e?permalink_comment_id=4498792>
public sealed class AsyncReaderWriterLock : IDisposable
{
readonly SemaphoreSlim _readSemaphore = new SemaphoreSlim(1, 1);
readonly SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1);
int _readerCount;
public async Task<IDisposable> AcquireWriterLock(CancellationToken token = default)
{
await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
try
{
await _readSemaphore.WaitAsync(token).ConfigureAwait(false);
}
catch
{
_writeSemaphore.Release();
throw;
}
return new LockToken(ReleaseWriterLock);
}
private void ReleaseWriterLock()
{
_readSemaphore.Release();
_writeSemaphore.Release();
}
public async Task<IDisposable> AcquireReaderLock(CancellationToken token = default)
{
await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
if (Interlocked.Increment(ref _readerCount) == 1)
{
try
{
await _readSemaphore.WaitAsync(token).ConfigureAwait(false);
}
catch
{
Interlocked.Decrement(ref _readerCount);
_writeSemaphore.Release();
throw;
}
}
_writeSemaphore.Release();
return new LockToken(ReleaseReaderLock);
}
private void ReleaseReaderLock()
{
if (Interlocked.Decrement(ref _readerCount) == 0)
_readSemaphore.Release();
}
public void Dispose()
{
_writeSemaphore.Dispose();
_readSemaphore.Dispose();
}
private sealed class LockToken : IDisposable
{
private readonly Action _action;
public LockToken(Action action) => _action = action;
public void Dispose() => _action?.Invoke();
}
}
@@ -0,0 +1,107 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System;
using System.Reflection;
using static Barotrauma.Items.Components.Quality;
namespace Barotrauma;
static class MapEntityExtensions
{
public static void AddLinked(this MapEntity entity, MapEntity other)
{
entity.linkedTo.Add(other);
}
}
static class ClientExtensions
{
#if SERVER
public static void SetClientCharacter(this Client client, Character character)
{
GameMain.Server.SetClientCharacter(client, character);
}
public static void Kick(this Client client, string reason = "")
{
GameMain.Server.KickClient(client.Connection, reason);
}
public static void Ban(this Client client, string reason = "", float seconds = -1)
{
if (seconds == -1)
{
GameMain.Server.BanClient(client, reason, null);
}
else
{
GameMain.Server.BanClient(client, reason, TimeSpan.FromSeconds(seconds));
}
}
public static bool CheckPermission(this Client client, ClientPermissions permissions)
{
return client.Permissions.HasFlag(permissions);
}
#endif
}
static class ItemExtensions
{
public static object GetComponentString(this Item item, string component)
{
Type type = LuaCsSetup.Instance.PluginManagementService
.GetType("Barotrauma.Items.Components." + component);
if (type == null)
{
return null;
}
MethodInfo method = typeof(Item).GetMethod(nameof(Item.GetComponent));
MethodInfo generic = method.MakeGenericMethod(type);
return generic.Invoke(item, null);
}
#if SERVER
public static object CreateServerEventString(this Item item, string component)
{
var comp = item.GetComponentString(component);
if (comp == null)
return null;
MethodInfo method = typeof(Item).GetMethod(
nameof(Item.CreateServerEvent),
new Type[] { Type.MakeGenericMethodParameter(0) });
MethodInfo generic = method.MakeGenericMethod(comp.GetType());
return generic.Invoke(item, new object[] { comp });
}
public static object CreateServerEventString(this Item item, string component, object[] extraData)
{
var comp = item.GetComponentString(component);
if (comp == null)
return null;
MethodInfo method = typeof(Item).GetMethod(
nameof(Item.CreateServerEvent),
new Type[] { Type.MakeGenericMethodParameter(0), typeof(object[]) });
MethodInfo generic = method.MakeGenericMethod(comp.GetType());
return generic.Invoke(item, new object[] { comp, extraData });
}
#endif
}
static class QualityExtensions
{
public static void SetValue(this Quality quality, StatType statType, float value)
{
quality.statValues[statType] = value;
}
}
@@ -0,0 +1,25 @@
using System;
using System.Reflection;
using Barotrauma.LuaCs;
namespace Barotrauma.LuaCs.Compatibility;
public interface ILuaCsHook : ILuaPatcher, ILuaCsShim
{
// Event Services
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null);
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
void Add(string eventName, LuaCsFunc callback, object owner = null);
void Remove(string eventName, string identifier);
// Does anyone use this? TODO: Analyze old Lua mods for usage scenarios.
//bool Exists(string eventName, string identifier);
object Call(string eventName, params object[] args);
T Call<T>(string eventName, params object[] args);
// Needs to be here instead of ILuaPatcher for compatiility purposes
public enum HookMethodType
{
Before, After
}
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Compatibility;
public interface ILuaCsLogger : ILuaCsShim
{
}
@@ -0,0 +1,26 @@
using Barotrauma.Networking;
using System.Collections.Generic;
namespace Barotrauma.LuaCs.Compatibility;
internal interface ILuaCsNetworking : ILuaCsShim
{
void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData);
ushort LastClientListUpdateID { get; set; }
void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
void HttpGet(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null);
void RequestGetHTTP(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null);
void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
void Receive(string netId, LuaCsAction action);
#if SERVER
int FileSenderMaxPacketsPerUpdate { get; set; }
void ClientWriteLobby(Client client);
void UpdateClientPermissions(Client client);
IWriteMessage Start();
void Send(IWriteMessage mesage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#elif CLIENT
void Send(IWriteMessage mesage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#endif
}
@@ -0,0 +1,8 @@
using Barotrauma.LuaCs;
namespace Barotrauma.LuaCs.Compatibility;
public interface ILuaCsShim : IService
{
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Barotrauma.LuaCs.Compatibility;
internal partial interface ILuaCsTimer : IReusableService, ILuaCsShim
{
public static double Time => Timing.TotalTime;
public static double GetTime() => Time;
public static double AccumulatorMax { get; set; }
public void Clear();
public void Wait(LuaCsAction action, int millisecondDelay);
public void NextFrame(LuaCsAction action);
}
@@ -0,0 +1,10 @@
namespace Barotrauma.LuaCs.Compatibility;
public interface ILuaCsUtility : ILuaCsShim
{
public bool CanReadFromPath(string file);
public bool CanWriteToPath(string file);
internal bool IsPathAllowedException(string path, bool write = true,
LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown);
}
@@ -0,0 +1,305 @@
using Barotrauma;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Xml.Linq;
namespace Barotrauma;
class LuaCsConfig
{
private enum ValueType
{
None,
Text,
Integer,
Decimal,
Boolean,
Collection,
Object,
Enum
}
private static Type[] LoadDocTypes(XElement typesElem)
{
var result = new List<Type>();
var loadedTypes = AssemblyLoadContext.All
.Where(alc => alc != AssemblyLoadContext.Default)
.SelectMany(alc => alc.Assemblies)
.SelectMany(asm => asm.GetTypes())
.ToImmutableArray();
foreach (var elem in typesElem.Elements())
{
var typesFound = loadedTypes.Where(t => t.FullName?.EndsWith(elem.Value) ?? false).ToImmutableList();
if (!typesFound.Any())
{
ModUtils.Logging.PrintError(
$"{nameof(LuaCsConfig)}::{nameof(LoadDocTypes)}() | Unable to find a matching type for {elem.Value}");
continue;
}
result.AddRange(typesFound);
}
return result.ToArray();
}
private static IEnumerable<XElement> SaveDocTypes(IEnumerable<Type> types)
{
return types.Select(t => new XElement("Type", t.ToString()));
}
private static Type GetTypeAttr(Type[] types, XElement elem)
{
var idx = elem.GetAttributeInt("Type", -1);
if (idx < 0 || idx >= types.Length) throw new Exception($"Type index '{idx}' is outside of saved types bounds");
return types[idx];
}
private static ValueType GetValueType(XElement elem)
{
Enum.TryParse(typeof(ValueType), elem.Attribute("Value")?.Value, out object result);
if (result != null) return (ValueType)result;
else return ValueType.None;
}
private static object ParseValue(Type[] types, XElement elem)
{
var type = GetValueType(elem);
if (elem.IsEmpty) return null;
if (type == ValueType.Enum)
{
var tType = GetTypeAttr(types, elem);
if (tType == null || !tType.IsSubclassOf(typeof(Enum))) return null;
if (Enum.TryParse(tType, elem.Value, out object result)) return result;
else return null;
}
if (type == ValueType.Collection)
{
var tType = GetTypeAttr(types, elem);
var tInt = tType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
var gArg = tInt.GetGenericArguments()[0];
if (tType == null || !tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) return null;
object result = null;
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var param = c.GetParameters();
return param.Count() == 1 && param.Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
});
if (ctor != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
var castElems = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(gArg).Invoke(elements, new object[] { elements });
result = ctor.Invoke(new object[] { castElems });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
var addMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Add") return false;
var param = m.GetParameters();
return param.Count() == 1 && param[0].ParameterType == gArg;
});
if (ctor != null && addMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(null);
foreach (var el in elements) addMethod.Invoke(result, new object[] { el });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault();
var setMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Set") return false;
var param = m.GetParameters();
return param.Count() == 2 && param[0].ParameterType == typeof(int) && param[1].ParameterType == gArg;
});
if (ctor != null || setMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(new object[] { elements.Count() });
int i = 0;
foreach (var el in elements)
{
setMethod.Invoke(result, new object[] { i, el });
i++;
}
}
}
return result;
}
else if (type == ValueType.Text) return elem.Value;
else if (type == ValueType.Integer)
{
int.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Decimal)
{
float.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Boolean)
{
bool.TryParse(elem.Value, out var boolean);
return boolean;
}
else if (type == ValueType.Object)
{
var tType = GetTypeAttr(types, elem);
if (tType == null) return null;
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
object result = null;
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
if (ctor == null)
{
if (!tType.IsValueType) return null;
result = Activator.CreateInstance(tType);
}
else result = ctor.Invoke(null);
foreach (var el in elem.Elements())
{
var value = ParseValue(types, el);
var field = fields.FirstOrDefault(f => f.Name == el.Name.LocalName);
if (field != null) field.SetValue(result, value);
var property = properties.FirstOrDefault(p => p.Name == el.Name.LocalName);
if (property != null) property.SetValue(result, value);
}
return result;
}
else return elem.Value;
}
private static void AddTypeAttr(List<Type> types, Type type, XElement elem)
{
if (!types.Contains(type)) types.Add(type);
elem.SetAttributeValue("Type", types.IndexOf(type));
}
private static XElement ParseObject(List<Type> types, string name, object value)
{
XElement result = new XElement(name);
if (value != null)
{
var tType = value.GetType();
if (tType.IsEnum)
{
result.SetAttributeValue("Value", ValueType.Enum);
AddTypeAttr(types, tType, result);
result.Value = Enum.GetName(tType, value) ?? "";
}
else if (value is string str)
{
result.SetAttributeValue("Value", ValueType.Text);
result.Value = str;
}
else if (value is int integer)
{
result.SetAttributeValue("Value", ValueType.Integer);
result.Value = integer.ToString();
}
else if (value is float || value is double)
{
result.SetAttributeValue("Value", ValueType.Decimal);
result.Value = value.ToString();
}
else if (value is bool boolean)
{
result.SetAttributeValue("Value", ValueType.Boolean);
result.Value = boolean.ToString();
}
else if (tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
result.SetAttributeValue("Value", ValueType.Collection);
AddTypeAttr(types, tType, result);
var enumerator = (IEnumerator)tType.GetMethod("GetEnumerator").Invoke(value, null);
while (enumerator.MoveNext())
{
var elVal = ParseObject(types, "Item", enumerator.Current);
result.Add(elVal);
}
}
else if (tType.IsClass || tType.IsValueType)
{
result.SetAttributeValue("Value", ValueType.Object);
AddTypeAttr(types, tType, result);
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
foreach (var field in fields) result.Add(ParseObject(types, field.Name, field.GetValue(value)));
foreach (var property in properties) result.Add(ParseObject(types, property.Name, property.GetValue(value)));
}
else
{
result.SetAttributeValue("Value", ValueType.None);
result.Value = value.ToString();
}
}
return result;
}
public static T Load<T>(FileStream file)
{
var doc = XDocument.Load(file);
var rootElems = doc.Root.Elements().ToArray();
var types = rootElems[0];
var elem = rootElems[1];
var dict = ParseValue(LoadDocTypes(types), elem);
if (dict.GetType() == typeof(T)) return (T)dict;
else throw new Exception($"Loaded configuration is not of the type '{typeof(T).Name}'");
}
public static void Save(FileStream file, object obj)
{
var types = new List<Type>();
var elem = ParseObject(types, "Root", obj);
var root = new XElement("Configuration", new XElement("Types", SaveDocTypes(types)), elem);
var doc = new XDocument(root);
doc.Save(file);
}
public static T Load<T>(string path)
{
using (var file = LuaCsFile.OpenRead(path)) return Load<T>(file);
}
public static void Save(string path, object obj)
{
using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj);
}
}
@@ -1,9 +1,14 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Barotrauma
{
/// <summary>
/// <b>[Obsolete]</b> Legacy compatibility only.
/// </summary>
[Obsolete("Deprecated.")]
public class LuaCsPerformanceCounter
{
public bool EnablePerformanceCounter = false;
@@ -33,4 +38,4 @@ namespace Barotrauma
HookElapsedTime[eventName][hookName] = (double)ticks / Stopwatch.Frequency;
}
}
}
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml.Linq;
using System.Xml.Serialization;
using Barotrauma.LuaCs;
using Barotrauma.Steam;
using OneOf;
namespace Barotrauma.LuaCs.Data;
#region ModConfigurationInfo
public partial record ModConfigInfo : IModConfigInfo
{
public ContentPackage Package { get; init; }
public ImmutableArray<IAssemblyResourceInfo> Assemblies { get; init; }
public ImmutableArray<ILuaScriptResourceInfo> LuaScripts { get; init; }
public ImmutableArray<IConfigResourceInfo> Configs { get; init; }
}
#endregion
#region DataContracts_Resources
public record BaseResourceInfo : IBaseResourceInfo
{
public Platform SupportedPlatforms { get; init; }
public Target SupportedTargets { get; init; }
public int LoadPriority { get; init; }
public ImmutableArray<ContentPath> FilePaths { get; init; }
public bool Optional { get; init; }
public string InternalName { get; init; }
public ContentPackage OwnerPackage { get; init; }
public ImmutableArray<Identifier> RequiredPackages { get; init; }
public ImmutableArray<Identifier> IncompatiblePackages { get; init; }
}
public record AssemblyResourceInfo : BaseResourceInfo, IAssemblyResourceInfo
{
public string FriendlyName { get; init; }
public bool IsScript { get; init; }
public bool UseInternalAccessName { get; init; }
public bool IsReferenceModeOnly { get; init; }
}
/// <summary>
/// Note: Config settings and settings-profiles are stored in the same files.
/// </summary>
public record ConfigResourceInfo : BaseResourceInfo, IConfigResourceInfo {}
public record LuaScriptsResourceInfo : BaseResourceInfo, ILuaScriptResourceInfo
{
public bool IsAutorun { get; init; }
public bool RunUnrestricted { get; init; }
}
#endregion
#region DataContracts_ParsedInfo
public record ConfigInfo : IConfigInfo
{
public string InternalName { get; init; }
public ContentPackage OwnerPackage { get; init; }
public string DataType { get; init; }
public XElement Element { get; init; }
public RunState EditableStates { get; init; }
public NetSync NetSync { get; init; }
#if CLIENT // IConfigDisplayInfo
public string DisplayName { get; init; }
public string Description { get; init; }
public string DisplayCategory { get; init; }
public bool ShowInMenus { get; init; }
public string Tooltip { get; init; }
public ContentPath ImageIconPath { get; init; }
#endif
}
public record ConfigProfileInfo : IConfigProfileInfo
{
/// <summary>
/// Profile name.
/// </summary>
public string InternalName { get; init; }
public ContentPackage OwnerPackage { get; init; }
public IReadOnlyList<(string SettingName, XElement Element)> ProfileValues { get; init; }
}
#endregion
@@ -0,0 +1,21 @@
using System;
// ReSharper disable InconsistentNaming
namespace Barotrauma.LuaCs.Data;
[Flags]
public enum Platform
{
Linux = 0x1,
OSX = 0x2,
Windows = 0x4,
Any = Linux | OSX | Windows
}
[Flags]
public enum Target
{
Client = 0x1,
Server = 0x2,
Any = Client | Server
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Xml.Serialization;
namespace Barotrauma.LuaCs.Data;
public interface IDependencyInfo
{
/// <summary>
/// List of dependency packages required by this resource.
/// </summary>
ImmutableArray<Identifier> RequiredPackages { get; }
/// <summary>
/// List of packages incompatible with this resource.
/// </summary>
ImmutableArray<Identifier> IncompatiblePackages { get; }
}
public interface IPlatformInfo
{
/// <summary>
/// Platforms that these localization files should be loaded for.
/// </summary>
[Required]
[XmlAttribute("Platform")]
Platform SupportedPlatforms { get; }
/// <summary>
/// Targets that these localization files should be loaded for.
/// </summary>
[Required]
[XmlAttribute("Target")]
Target SupportedTargets { get; }
}
/// <summary>
/// ResourceInfos contain metadata about a resource.
/// </summary>
public interface IResourceInfo : IPlatformInfo
{
/// <summary>
/// [Optional]
/// Specifies the loading order for all assets of the same type (ie. styles, assemblies, etc.) from
/// the same <see cref="ContentPackage"/>. Lower number is higher priority, see <see cref="System.Linq.Enumerable.OrderBy{TSource,TKey}(IEnumerable{TSource}, Func{TSource,TKey})"/>
/// </summary>
[XmlAttribute("LoadPriority")]
int LoadPriority { get; }
/// <summary>
/// Resource absolute file paths.
/// </summary>
[Required]
ImmutableArray<ContentPath> FilePaths { get; }
/// <summary>
/// Marks this resource as optional (ie. Cross-CP content). Setting this to true will allow the dependency system to
/// try and order the loading but not fail if it runs into circular dependency issues.
/// </summary>
[XmlAttribute("Optional")]
bool Optional { get; }
}
@@ -0,0 +1,35 @@
using System;
using System.Xml.Linq;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs.Data;
/// <summary>
/// Parsed data from a configuration xml.
/// </summary>
public partial interface IConfigInfo : IDataInfo
{
/// <summary>
/// Specifies the type initializer that will be used to instantiate the config var.
/// </summary>
string DataType { get; }
/// <summary>
/// The 'Setting' XML element.
/// </summary>
XElement Element { get; }
/// <summary>
/// In what <see cref="RunState"/>(s) is this config editable. Will be editable in the selected state, and lower value states.
/// <br/><br/>
/// <b>[Important]</b><br/> Setting this to value lower than 'Configuration` will render this config read-only.
/// <br/><br/><b>Expected Behaviour</b>:
/// <br/><b>[<see cref="RunState.Unloaded"/>|<see cref="RunState.Unloaded"/>]</b>: Read-Only.
/// <br/><b>[<see cref="RunState.LoadedNoExec"/>]</b>: Can only be changed at the Main Menu (not in a lobby).
/// <br/><b>[<see cref="RunState.Running"/>]</b>: Can be changed at the Main Menu and while a lobby is active.
/// </summary>
RunState EditableStates { get; }
/// <summary>
/// Network synchronization rules for this config.
/// </summary>
NetSync NetSync { get; }
}
@@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.LuaCs.Data;
public interface IConfigProfileInfo : IDataInfo
{
IReadOnlyList<(string SettingName, XElement Element)> ProfileValues { get; }
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace Barotrauma.LuaCs.Data;
/// <summary>
/// Serves as a compound-key to refer to all resources and information that comes from a specific source.
/// </summary>
public interface IDataInfo : IEqualityComparer<IDataInfo>, IEquatable<IDataInfo>
{
/// <summary>
/// Internal name unique within the resources inside a package.
/// </summary>
[XmlAttribute("Name")]
string InternalName { get; }
/// <summary>
/// The package this information belongs to.
/// </summary>
ContentPackage OwnerPackage { get; }
bool IEqualityComparer<IDataInfo>.Equals(IDataInfo x, IDataInfo y)
{
if (x is null || y is null)
return false;
if (x.OwnerPackage is null)
throw new NullReferenceException($"ContentPackage not set for resource {x}!");
if (y.OwnerPackage is null)
throw new NullReferenceException($"ContentPackage not set for resource {y}!");
if (x.InternalName.IsNullOrWhiteSpace())
throw new NullReferenceException($"InternalName not set for resource {x}!");
if (y.InternalName.IsNullOrWhiteSpace())
throw new NullReferenceException($"InternalName not set for resource {y}!");
return x.OwnerPackage == y.OwnerPackage && x.InternalName == y.InternalName;
}
bool IEquatable<IDataInfo>.Equals(IDataInfo other)
{
return Equals(this, other);
}
int IEqualityComparer<IDataInfo>.GetHashCode(IDataInfo obj)
{
if (obj.OwnerPackage is null)
throw new NullReferenceException($"ContentPackage not set for resource {obj}!");
if (obj.InternalName.IsNullOrWhiteSpace())
throw new NullReferenceException($"InternalName is null for object {obj}!");
return obj.InternalName.GetHashCode() + obj.OwnerPackage.GetHashCode();
}
}
@@ -0,0 +1,18 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Xml.Linq;
namespace Barotrauma.LuaCs.Data;
public partial interface IModConfigInfo : IAssembliesResourcesInfo,
ILuaScriptsResourcesInfo, IConfigsResourcesInfo
{
// package info
ContentPackage Package { get; }
}
public record ResourceParserInfo(
[NotNull] ContentPackage Owner,
[NotNull] XElement Element,
ImmutableArray<Identifier> Required,
ImmutableArray<Identifier> Incompatible);
@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Xml.Serialization;
namespace Barotrauma.LuaCs.Data;
public interface IBaseResourceInfo : IResourceInfo, IDataInfo, IDependencyInfo {}
public interface IConfigResourceInfo : IBaseResourceInfo {}
/// <summary>
/// Represents loadable Lua files.
/// </summary>
public interface ILuaScriptResourceInfo : IBaseResourceInfo
{
/// <summary>
/// Should this script be run automatically.
/// </summary>
[XmlAttribute("IsAutorun")]
public bool IsAutorun { get; }
/// <summary>
/// Indicates that this lua resources needs to run outside sandbox/requires unrestricted access.
/// </summary>
[XmlAttribute("RunUnrestricted")]
public bool RunUnrestricted { get; }
}
public interface IAssemblyResourceInfo : IBaseResourceInfo
{
/// <summary>
/// The friendly name of the assembly. Script files belonging to the same assembly should all have the same name.
/// Legacy scripts will all be given the sanitized name of the Content Package they belong to.
/// </summary>
[XmlAttribute("FriendlyName")]
public string FriendlyName { get; }
/// <summary>
/// Is this entry referring to a script file collection.
/// </summary>
[XmlAttribute("IsScript")]
public bool IsScript { get; }
/// <summary>
/// <b>[Required(IsScript: true)] Whether the internal compiled assembly name should be named to enabled use of the
/// <see cref="InternalsVisibleToAttribute"/> attribute.</b>
/// </summary>
[XmlAttribute("UseInternalAccessName")]
public bool UseInternalAccessName { get; }
/// <summary>
/// Should the following resources only be used for Compilation MetadataReference.
/// NOTE: Affects the entire package's assembly resources, meant for internal use only.
/// </summary>
[XmlAttribute("IsReferenceModeOnly")]
public bool IsReferenceModeOnly { get; }
}
#region Collections
public interface IAssembliesResourcesInfo
{
ImmutableArray<IAssemblyResourceInfo> Assemblies { get; }
}
public interface ILuaScriptsResourcesInfo
{
ImmutableArray<ILuaScriptResourceInfo> LuaScripts { get; }
}
public interface IConfigsResourcesInfo
{
ImmutableArray<IConfigResourceInfo> Configs { get; }
}
#endregion
@@ -0,0 +1,18 @@
namespace Barotrauma.LuaCs.Data;
/// <summary>
/// Legacy data contract for the old run configuration system. Should be deprecated
/// once no longer needed.
/// </summary>
public interface IRunConfig
{
bool UseNonPublicizedAssemblies { get; }
bool AutoGenerated { get; }
bool UseInternalAssemblyName { get; }
string Client { get; }
string Server { get; }
bool IsForced();
bool IsStandard();
bool IsForcedOrStandard();
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using OneOf;
namespace Barotrauma.LuaCs.Data;
public partial interface ISettingBase : IDataInfo, IEquatable<ISettingBase>, IDisposable
{
/// <summary>
/// Settings production factory. Should be implemented by all types and registered with the Dependency Injector.
/// </summary>
/// <typeparam name="T">An interface type derived from <see cref="ISettingBase"/>.</typeparam>
public interface IFactory<out T> where T : ISettingBase
{
/// <summary>
/// Creates an instance of the given <see cref="ISettingBase"/> type.
/// </summary>
/// <param name="configInfo">Configuration information.</param>
/// <param name="valueChangePredicate">Called before a new value is assigned. Returns a boolean whether to allow
/// the value to be changed to the one given.</param>
/// <returns></returns>
T CreateInstance([NotNull]IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate);
}
IConfigInfo GetConfigInfo();
#if CLIENT
IConfigDisplayInfo GetDisplayInfo();
#endif
bool IsDisposed { get; }
Type GetValueType();
string GetStringValue();
string GetDefaultStringValue();
bool TrySetSerializedValue(OneOf<string, XElement> value);
event Action<ISettingBase> OnValueChanged;
OneOf.OneOf<string, XElement> GetSerializableValue();
}
/// <summary>
/// Creates a setting representing a value of the given <see cref="Type"/>. Must be a compatible listed type. <br/>
/// </summary>
/// <typeparam name="T">
/// <b>Compatible Types:</b><br/>
/// Any primitive type:<br/>
/// - <see cref="byte"/><br/>
/// - <see cref="sbyte"/><br/>
/// - <see cref="ushort"/><br/>
/// - <see cref="short"/><br/>
/// - <see cref="int"/><br/>
/// - <see cref="uint"/><br/>
/// - <see cref="long"/><br/>
/// - <see cref="ulong"/><br/>
/// - <see cref="float"/><br/>
/// - <see cref="double"/><br/>
/// Extension types and Enums: <br/>
/// - <see cref="string"/><br/>
/// - <see cref="Enum"/><br/>
/// </typeparam>
public interface ISettingBase<T> : ISettingBase where T : IEquatable<T>, IConvertible
{
[NotNull]
T Value { get; }
[NotNull]
T DefaultValue { get; }
bool TrySetValue(T value);
}
/// <summary>
/// Creates a setting representing a value of the given <see cref="Type"/> with a minimum and maximum value.
/// Can only be either an <see cref="int"/> or a <see cref="float"/>.
/// </summary>
/// <remarks>The type selection is limited by the Undertow implementation of the GUI Slider.</remarks>
/// <typeparam name="T">The value type, either <see cref="int"/> or <see cref="float"/></typeparam>
public interface ISettingRangeBase<T> : ISettingBase<T> where T : IEquatable<T>, IConvertible
{
T MinValue { get; }
T MaxValue { get; }
int IncrementalSteps { get; }
}
/// <summary>
/// Creates a setting representing a value of the given <see cref="Type"/> with a distinct list of selectable values.
/// Must be a type compatible with <see cref="ISettingBase{T}"/>.
/// </summary>
/// <typeparam name="T">The value type. See <see cref="ISettingBase{T}"/></typeparam>
public interface ISettingList<T> : ISettingBase<T> where T : IEquatable<T>, IConvertible
{
bool TrySetValueByIndex(int index);
IReadOnlyList<T> Options { get; }
IReadOnlyList<string> StringOptions { get; }
}
@@ -0,0 +1,113 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using FluentResults;
using OneOf.Types;
namespace Barotrauma.LuaCs.Data;
// --- Storage Service
// TODO: Configs should not be services, add new registration path for them.
public interface IStorageServiceConfig : IService
{
string LocalModsDirectory { get; }
string WorkshopModsDirectory { get; }
string GameSettingsConfigPath { get; }
#if CLIENT
string TempDownloadsDirectory { get; }
#endif
string LocalDataSavePath { get; }
string LocalDataPathRegex { get; }
string LocalPackageDataPath { get; }
}
public record StorageServiceConfig : IStorageServiceConfig
{
private static readonly string ExecutionLocation = Directory.GetCurrentDirectory().CleanUpPathCrossPlatform();
public string LocalModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath();
public string WorkshopModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath();
public string GameSettingsConfigPath { get; init; } = System.IO.Path.GetFullPath(
string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath)
? SaveUtil.DefaultSaveFolder
: GameSettings.CurrentConfig.SavePath).CleanUpPath();
#if CLIENT
public string TempDownloadsDirectory { get; init; } = System.IO.Path.GetFullPath(ModReceiver.DownloadFolder).CleanUpPath();
#endif
public string LocalDataSavePath => Path.Combine(ExecutionLocation, "Data/Mods").CleanUpPathCrossPlatform();
public string LocalDataPathRegex => "%ModDir%";
public string RunLocation => ExecutionLocation;
public string LocalPackageDataPath => Path.Combine(LocalDataSavePath, LocalDataPathRegex);
public void Dispose()
{
// cannot be disposed.
}
public bool IsDisposed => false;
}
// --- Config Service
public interface IConfigServiceConfig : IService
{
string LocalConfigPathPartial { get; }
string FileNamePattern { get; }
}
public record ConfigServiceConfig : IConfigServiceConfig
{
public string LocalConfigPathPartial => $"/Config/{FileNamePattern}.xml";
public string FileNamePattern => "<ConfigName>";
public void Dispose()
{
// ignored
}
public bool IsDisposed => false;
}
// --- Lua Scripts Service
public interface ILuaScriptServicesConfig : IService
{
bool SafeLuaIOEnabled { get; }
bool UseCaching { get; }
}
public record LuaScriptServicesConfig : ILuaScriptServicesConfig
{
public bool SafeLuaIOEnabled => true;
public bool UseCaching => true;
public void Dispose()
{
// ignored
}
public bool IsDisposed => false;
}
// --- Package Management Service
public interface IPackageManagementServiceConfig : IService
{
bool IsCsEnabled { get; }
}
public class PackageManagementServiceConfig : IPackageManagementServiceConfig
{
public void Dispose()
{
// ignored
}
public bool IsDisposed => false;
public bool IsCsEnabled => true;
}
@@ -0,0 +1,81 @@
using System;
using System.Collections.Concurrent;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Microsoft.Toolkit.Diagnostics;
using Microsoft.Xna.Framework;
using OneOf;
namespace Barotrauma.LuaCs.Data;
public abstract class SettingBase : ISettingBase
{
protected SettingBase(IConfigInfo configInfo)
{
Guard.IsNotNull(configInfo, nameof(configInfo));
ConfigInfo = configInfo;
}
protected IConfigInfo ConfigInfo { get; private set; }
public string InternalName => ConfigInfo.InternalName;
public ContentPackage OwnerPackage => ConfigInfo.OwnerPackage;
public IConfigInfo GetConfigInfo() => ConfigInfo;
#if CLIENT
public IConfigDisplayInfo GetDisplayInfo() => ConfigInfo;
#endif
public virtual bool Equals(ISettingBase other)
{
return other is not null && (
ReferenceEquals(this, other) || !IsDisposed &&
OwnerPackage == other.OwnerPackage &&
InternalName.Equals(other.InternalName));
}
private int _isDisposed = 0;
public virtual bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
protected abstract void OnDispose();
public virtual void Dispose()
{
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
OnDispose();
ConfigInfo = null;
GC.SuppressFinalize(this);
}
// -- Must be implemented
public abstract Type GetValueType();
public abstract string GetStringValue();
public abstract string GetDefaultStringValue();
public abstract bool TrySetSerializedValue(OneOf<string, XElement> value);
public abstract event Action<ISettingBase> OnValueChanged;
public abstract OneOf<string, XElement> GetSerializableValue();
#if CLIENT
public virtual void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
{
new GUITextBox(new RectTransform(relativeSize, layoutGroup.RectTransform), font: GUIStyle.SmallFont)
{
Text = GetStringValue(),
OnTextChangedDelegate = (box, txt) =>
{
onSerializedValue?.Invoke(txt);
return true;
}
};
}
#endif
}
@@ -0,0 +1,395 @@
using System;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using Microsoft.Toolkit.Diagnostics;
using Microsoft.Xna.Framework;
using OneOf;
namespace Barotrauma.LuaCs.Data;
public partial class SettingEntry<T> : SettingBase, ISettingBase<T>, INetworkSyncVar where T : IEquatable<T>, IConvertible
{
public class Factory : ISettingBase.IFactory<ISettingBase<T>>
{
public ISettingBase<T> CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
{
Guard.IsNotNull(configInfo, nameof(configInfo));
return new SettingEntry<T>(configInfo, valueChangePredicate);
}
}
public SettingEntry(IConfigInfo configInfo,
Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
: base(configInfo)
{
if (!(
typeof(T).IsEnum ||
typeof(T).IsPrimitive ||
typeof(T) == typeof(string)))
{
ThrowHelper.ThrowArgumentException($"{nameof(ISettingBase<T>)}: The type of {nameof(T)} is not an allowed type.");
}
ValueChangePredicate = valueChangePredicate;
try
{
Value = (T)Convert.ChangeType(ConfigInfo.Element.GetAttributeString("Value", null), typeof(T));
DefaultValue = Value;
}
catch (Exception e) when (e is InvalidCastException or ArgumentNullException)
{
Value = default(T);
DefaultValue = default(T);
}
}
protected Func<OneOf<string, XElement, object>, bool> ValueChangePredicate;
public T Value { get; protected set; }
public T DefaultValue { get; protected set; }
public virtual bool TrySetValue(T value)
{
if (value is null || value.Equals(Value))
{
return false;
}
#if CLIENT
if (SyncType is NetSync.ServerAuthority && NetworkingService is not null
&& GameMain.IsMultiplayer
&& GameMain.Client is not null
&& !GameMain.Client.HasPermission(this.WritePermissions))
{
return false;
}
#endif
if (!TrySetValueInternal(value))
{
return false;
}
OnValueChanged?.Invoke(this);
#if CLIENT
if (GameMain.IsMultiplayer && SyncType is NetSync.ClientOneWay or NetSync.TwoWay)
{
NetworkingService?.SendNetVar(this);
}
#elif SERVER
if (SyncType is NetSync.TwoWay or NetSync.ServerAuthority)
{
NetworkingService?.SendNetVar(this);
}
#endif
return true;
}
private bool TrySetValueInternal(T value)
{
if (value is null)
{
return false;
}
if (ValueChangePredicate != null && !ValueChangePredicate(value))
{
return false;
}
Value = value;
return true;
}
/// <summary>
/// handles internal networking rules after reading the net message (to avoid synchro issues).
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private bool TrySetValueNetwork(T value)
{
if (NetworkingService is null)
{
return false;
}
#if CLIENT
if (SyncType is NetSync.None or NetSync.ClientOneWay)
{
return false;
}
#else
if (SyncType is NetSync.None or NetSync.ServerAuthority)
{
return false;
}
#endif
if (!TrySetValueInternal(value))
{
return false;
}
#if SERVER
if (SyncType is NetSync.TwoWay)
{
NetworkingService?.SendNetVar(this);
}
#endif
OnValueChanged?.Invoke(this);
return true;
}
protected override void OnDispose()
{
ValueChangePredicate = null;
NetworkingService?.DeregisterNetVar(this);
}
public override Type GetValueType() => typeof(T);
public override string GetStringValue() => Value?.ToString() ?? string.Empty;
public override string GetDefaultStringValue() => DefaultValue?.ToString() ?? string.Empty;
public override bool TrySetSerializedValue(OneOf<string, XElement> value)
{
bool isFailed = false;
var typeConvertedValue = value.Match<T>(
(string val) =>
{
try
{
return (T)Convert.ChangeType(val, typeof(T));
}
catch (Exception e)
{
// ignored
isFailed = true;
return default(T);
}
},
(XElement val) =>
{
try
{
return (T)Convert.ChangeType(val.GetAttributeString("Value", null), typeof(T));
}
catch (Exception e)
{
isFailed = true;
return default(T);
}
});
return !isFailed && TrySetValue(typeConvertedValue);
}
public override event Action<ISettingBase> OnValueChanged;
public override OneOf<string, XElement> GetSerializableValue() => Value.ToString();
// -- Networking
protected IEntityNetworkingService NetworkingService;
public Guid InstanceId => NetworkingService?.GetNetworkIdForInstance(this) ?? Guid.Empty;
public void SetNetworkOwner(IEntityNetworkingService networkingService)
{
NetworkingService = networkingService;
}
public NetSync SyncType => ConfigInfo?.NetSync ?? NetSync.None;
// needs to be added IConfigInfo
public ClientPermissions WritePermissions => ClientPermissions.ManageSettings;
public void ReadNetMessage(IReadMessage message)
{
if (SyncType == NetSync.None || NetworkingService is null)
{
return;
}
try
{
if (typeof(T).IsEnum)
{
TrySetValueInternal((T)(object)message.ReadInt32());
}
// No...there's no better way to do this...
var typeCode = Type.GetTypeCode(typeof(T));
switch (typeCode)
{
case TypeCode.Boolean:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadBoolean(), typeCode));
return;
case TypeCode.Byte:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadByte(), typeCode));
return;
// SByte not supported by interface
case TypeCode.SByte:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt16(), typeCode));
return;
case TypeCode.Int16:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt16(), typeCode));
return;
case TypeCode.Char:
case TypeCode.UInt16:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt16(), typeCode));
return;
case TypeCode.Int32:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt32(), typeCode));
return;
case TypeCode.UInt32:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt32(), typeCode));
return;
case TypeCode.Int64:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt64(), typeCode));
return;
case TypeCode.UInt64:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt64(), typeCode));
return;
case TypeCode.Single:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadSingle(), typeCode));
return;
case TypeCode.Double:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadDouble(), typeCode));
return;
case TypeCode.String:
TrySetValueNetwork((T)Convert.ChangeType(message.ReadString(), typeCode));
return;
case TypeCode.Decimal:
default:
ThrowHelper.ThrowNotSupportedException($"{nameof(SettingEntry<T>)}: The type {typeof(T).Name} is not supported.");
break;
}
}
catch (Exception e)
{
// Suppress unless we're testing.
#if DEBUG
throw;
#endif
}
}
public void WriteNetMessage(IWriteMessage message)
{
if (SyncType == NetSync.None || NetworkingService is null)
{
return;
}
try
{
if (typeof(T).IsEnum)
{
message.WriteInt32((int)((IConvertible)Value));
}
// No...there's no better way to do this...
var typeCode = Type.GetTypeCode(typeof(T));
switch (typeCode)
{
case TypeCode.Boolean:
message.WriteBoolean((bool)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Byte:
message.WriteByte((byte)Convert.ChangeType(Value, typeCode)!);
return;
// SByte not supported by interface
case TypeCode.SByte:
message.WriteInt16((short)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Int16:
message.WriteInt16((short)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Char:
case TypeCode.UInt16:
message.WriteUInt16((ushort)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Int32:
message.WriteInt32((int)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.UInt32:
message.WriteUInt32((uint)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Int64:
message.WriteInt64((long)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.UInt64:
message.WriteUInt64((ulong)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Single:
message.WriteSingle((float)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Double:
message.WriteDouble((double)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.String:
message.WriteString((string)Convert.ChangeType(Value, typeCode)!);
return;
case TypeCode.Decimal:
default:
ThrowHelper.ThrowNotSupportedException($"{nameof(SettingEntry<T>)}: The type {typeof(T).Name} is not supported.");
break;
}
}
catch (Exception e)
{
// Suppress unless we're testing.
#if DEBUG
throw;
#endif
}
}
#if CLIENT
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
{
switch (Type.GetTypeCode(typeof(T)))
{
case TypeCode.Boolean:
new GUITickBox(new RectTransform(relativeSize, layoutGroup.RectTransform), "")
{
Selected = (bool)Convert.ChangeType(this.Value, TypeCode.Boolean),
OnSelected = (box) =>
{
onSerializedValue?.Invoke(box.Selected.ToString());
return true;
}
};
break;
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.Int16:
case TypeCode.Char:
case TypeCode.UInt16:
case TypeCode.Int32:
case TypeCode.UInt32:
case TypeCode.Int64:
case TypeCode.UInt64:
new GUINumberInput(new RectTransform(relativeSize, layoutGroup.RectTransform), NumberType.Int)
{
IntValue = (int)Convert.ChangeType(this.Value, TypeCode.Int32)!,
OnValueChanged = (num) =>
{
onSerializedValue?.Invoke(num.IntValue.ToString());
}
};
break;
case TypeCode.Single:
case TypeCode.Double:
new GUINumberInput(new RectTransform(relativeSize, layoutGroup.RectTransform), NumberType.Float)
{
FloatValue = (float)Convert.ChangeType(this.Value, TypeCode.Single)!,
OnValueChanged = (num) =>
{
onSerializedValue?.Invoke(num.FloatValue.ToString());
}
};
break;
case TypeCode.String:
default:
base.AddDisplayComponent(layoutGroup, relativeSize, onSerializedValue);
break;
}
}
#endif
}
@@ -0,0 +1,121 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Toolkit.Diagnostics;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs.Data;
public class SettingList<T> : SettingEntry<T>, ISettingList<T> where T : IEquatable<T>, IConvertible
{
public class LFactory : ISettingBase.IFactory<ISettingList<T>>
{
public ISettingList<T> CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
{
Guard.IsNotNull(configInfo, nameof(configInfo));
return new SettingList<T>(configInfo, valueChangePredicate);
}
}
public SettingList(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
{
if (!(
typeof(T).IsEnum ||
typeof(T).IsPrimitive ||
typeof(T) == typeof(string)))
{
ThrowHelper.ThrowArgumentException($"{nameof(ISettingBase<T>)}: The type of {nameof(T)} is not an allowed type.");
}
ValueChangePredicate = valueChangePredicate;
var valuesElements = ConfigInfo.Element.GetChildElement("Values")?.GetChildElements("Value")?.ToImmutableArray();
Guard.IsNotNull(valuesElements, this.InternalName);
if (valuesElements.Value.IsEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{this.InternalName}: Could not find any values in list!");
}
foreach (var element in valuesElements.Value)
{
if (!TryConvert(element, out var v1))
{
ThrowHelper.ThrowArgumentException($"{this.InternalName}: Error while parsing list values");
}
_valuesList.Add(v1);
}
if (TryConvert(ConfigInfo.Element, out var v) && _valuesList.Contains(v))
{
Value = v;
DefaultValue = v;
}
else
{
Value = _valuesList[0];
DefaultValue = _valuesList[0];
}
bool TryConvert(XElement element, out T value)
{
try
{
value = (T)Convert.ChangeType(element.GetAttributeString("Value", null), typeof(T));
return true;
}
catch (Exception e) when (e is InvalidCastException or ArgumentNullException)
{
value = default(T);
return false;
}
}
}
private readonly List<T> _valuesList = new();
public override bool TrySetValue(T value)
{
if (!_valuesList.Contains(value))
{
return false;
}
return base.TrySetValue(value);
}
public bool TrySetValueByIndex(int index)
{
if (_valuesList.Count <= index)
{
return false;
}
return base.TrySetValue(_valuesList[index]);
}
public IReadOnlyList<T> Options => _valuesList.AsReadOnly();
public IReadOnlyList<string> StringOptions => _valuesList.Select(e => e.ToString()).ToImmutableArray();
#if CLIENT
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
{
GUIUtil.Dropdown(layoutGroup, (T val) => GetLocalizedString(val.ToString(), val.ToString()), null, Options, Value, (T val) =>
{
onSerializedValue?.Invoke(val.ToString());
}, new Vector2(relativeSize.X, 1f));
string GetLocalizedString(string identifier, string defaultValue)
{
var lstr = TextManager.Get($"{XmlConvert.EncodeLocalName(OwnerPackage.Name)}.{InternalName}.{identifier}.DisplayName");
return lstr.IsNullOrWhiteSpace() ? defaultValue : lstr.Value;
}
}
#endif
}
@@ -0,0 +1,104 @@
using System;
using System.Globalization;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Microsoft.Toolkit.Diagnostics;
using Microsoft.Xna.Framework;
using OneOf;
namespace Barotrauma.LuaCs.Data;
public abstract class SettingRangeBase<T> : SettingEntry<T>, ISettingRangeBase<T> where T : IEquatable<T>, IConvertible
{
public SettingRangeBase(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
{
}
public T MinValue { get; protected init; }
public T MaxValue { get; protected init; }
public int IncrementalSteps { get; protected init; }
}
public class SettingRangeFloat : SettingRangeBase<float>
{
public class RangeFactory : ISettingBase.IFactory<SettingRangeFloat>
{
public SettingRangeFloat CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
{
Guard.IsNotNull(configInfo, nameof(configInfo));
return new SettingRangeFloat(configInfo, valueChangePredicate);
}
}
public SettingRangeFloat(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
{
// funny values in case they forget to set them in the config.
MinValue = configInfo.Element.GetAttributeFloat("Min", float.MinValue);
MaxValue = configInfo.Element.GetAttributeFloat("Max", float.MaxValue);
IncrementalSteps = configInfo.Element.GetAttributeInt("Steps", 3);
}
public override bool TrySetValue(float value)
{
if (value > MaxValue || value < MinValue)
{
return false;
}
return base.TrySetValue(value);
}
#if CLIENT
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
{
GUIUtil.Slider(layoutGroup, new Vector2(MinValue, MaxValue), IncrementalSteps, labelFunc: val =>
{
return val.ToString("G4", CultureInfo.InvariantCulture);
}, Value, setter: val =>
{
onSerializedValue?.Invoke(val.ToString());
}, TextManager.Get(this.GetDisplayInfo().Tooltip), relativeSize);
}
#endif
}
public class SettingRangeInt : SettingRangeBase<int>
{
public class RangeFactory : ISettingBase.IFactory<SettingRangeInt>
{
public SettingRangeInt CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
{
Guard.IsNotNull(configInfo, nameof(configInfo));
return new SettingRangeInt(configInfo, valueChangePredicate);
}
}
public SettingRangeInt(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
{
// funny values in case they forget to set them in the config.
MinValue = configInfo.Element.GetAttributeInt("Min", int.MinValue);
MaxValue = configInfo.Element.GetAttributeInt("Max", int.MaxValue);
IncrementalSteps = configInfo.Element.GetAttributeInt("Steps", 3);
}
public override bool TrySetValue(int value)
{
if (value > MaxValue || value < MinValue)
{
return false;
}
return base.TrySetValue(value);
}
#if CLIENT
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
{
GUIUtil.Slider(layoutGroup, new Vector2(MinValue, MaxValue), IncrementalSteps, labelFunc: val =>
{
return ((int)val).ToString();
}, Value, setter: val =>
{
onSerializedValue?.Invoke(((int)val).ToString());
}, TextManager.Get(this.GetDisplayInfo().Tooltip), relativeSize);
}
#endif
}
@@ -0,0 +1,123 @@
using System;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using OneOf;
namespace Barotrauma.LuaCs.Data;
public interface ISettingsRegistrationProvider : IService
{
void RegisterTypeProviders(IConfigService configService, Func<OneOf<string, XElement, object>, bool> valueChangePredicate);
}
public class SettingsEntryRegistrar : ISettingsRegistrationProvider
{
private ILuaCsInfoProvider _infoProvider;
public SettingsEntryRegistrar(ILuaCsInfoProvider infoProvider)
{
_infoProvider = infoProvider;
}
public void RegisterTypeProviders(IConfigService configService, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
{
RegisterSettingEntry<bool>(configService, "bool", valueChangePredicate);
RegisterSettingEntry<byte>(configService, "byte", valueChangePredicate);
RegisterSettingEntry<sbyte>(configService, "sbyte", valueChangePredicate);
RegisterSettingEntry<short>(configService, "short", valueChangePredicate);
RegisterSettingEntry<ushort>(configService, "ushort", valueChangePredicate);
RegisterSettingEntry<int>(configService, "int", valueChangePredicate);
RegisterSettingEntry<uint>(configService, "uint", valueChangePredicate);
RegisterSettingEntry<long>(configService, "long", valueChangePredicate);
RegisterSettingEntry<ulong>(configService, "ulong", valueChangePredicate);
RegisterSettingEntry<string>(configService, "string", valueChangePredicate);
RegisterSettingEntry<float>(configService, "float", valueChangePredicate);
RegisterSettingEntry<float>(configService, "single", valueChangePredicate);
RegisterSettingEntry<double>(configService, "double", valueChangePredicate);
// ISettingRangeBase<T>
configService.RegisterSettingTypeInitializer("rangeInt", cfgInfo =>
{
return new SettingRangeInt.RangeFactory().CreateInstance(cfgInfo.Info, (val) =>
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
});
configService.RegisterSettingTypeInitializer("rangeFloat", cfgInfo =>
{
return new SettingRangeFloat.RangeFactory().CreateInstance(cfgInfo.Info, (val) =>
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
});
#if CLIENT
configService.RegisterSettingTypeInitializer("control" , cfgInfo =>
{
return new SettingControl.Factory().CreateInstance(cfgInfo.Info, val =>
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
});
#endif
RegisterSettingList<bool>(configService, "listBool", valueChangePredicate);
RegisterSettingList<byte>(configService, "listByte", valueChangePredicate);
RegisterSettingList<sbyte>(configService, "listSbyte", valueChangePredicate);
RegisterSettingList<short>(configService, "listShort", valueChangePredicate);
RegisterSettingList<ushort>(configService, "listUshort", valueChangePredicate);
RegisterSettingList<int>(configService, "listInt", valueChangePredicate);
RegisterSettingList<uint>(configService, "listUint", valueChangePredicate);
RegisterSettingList<long>(configService, "listLong", valueChangePredicate);
RegisterSettingList<ulong>(configService, "listUlong", valueChangePredicate);
RegisterSettingList<string>(configService, "listString", valueChangePredicate);
RegisterSettingList<float>(configService, "listFloat", valueChangePredicate);
RegisterSettingList<float>(configService, "listSingle", valueChangePredicate);
RegisterSettingList<double>(configService, "listDouble", valueChangePredicate);
}
private void RegisterSettingList<T>(IConfigService configService, string typeName, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) where T : IEquatable<T>, IConvertible
{
configService.RegisterSettingTypeInitializer(typeName, cfgInfo =>
{
return new SettingList<T>.LFactory().CreateInstance(cfgInfo.Info, (val) =>
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
});
}
private void RegisterSettingEntry<T>(IConfigService configService, string typeName, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) where T : IEquatable<T>, IConvertible
{
configService.RegisterSettingTypeInitializer(typeName, cfgInfo =>
{
return new SettingEntry<T>.Factory().CreateInstance(cfgInfo.Info, (val) =>
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
});
}
private bool IsValueChangeAllowed(IConfigInfo info, OneOf<string, XElement, object> newValue,
Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
{
#if CLIENT
return !info.Element.GetAttributeBool("ReadOnly", false)
|| info.EditableStates < _infoProvider.CurrentRunState
|| valueChangePredicate is null
|| valueChangePredicate.Invoke(newValue);
#else
// Server has absolute authority.
return !info.Element.GetAttributeBool("ReadOnly", false);
#endif
}
public void Dispose()
{
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_infoProvider.Dispose();
_infoProvider = null;
}
private int _isDisposed;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,110 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using MoonSharp.Interpreter;
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
namespace Barotrauma.Networking
{
partial class Client
{
public static IReadOnlyList<Client> ClientList
{
get
{
if (GameMain.IsSingleplayer) { return new List<Client>(); }
#if SERVER
return GameMain.Server.ConnectedClients;
#else
return GameMain.Client.ConnectedClients;
#endif
}
}
public ulong SteamID
{
get
{
if (AccountId.TryUnwrap(out AccountId outValue) && outValue is SteamId steamId)
{
return steamId.Value;
}
else
{
return 0;
}
}
}
}
}
namespace Barotrauma
{
using Barotrauma.Networking;
using System.Linq;
using System.Reflection;
partial class Character
{
}
partial class Item
{
public object GetComponentString(string component)
{
Type type = LuaUserData.GetType("Barotrauma.Items.Components." + component);
if (type == null)
{
return null;
}
MethodInfo method = typeof(Item).GetMethod(nameof(Item.GetComponent));
MethodInfo generic = method.MakeGenericMethod(type);
return generic.Invoke(this, null);
}
}
partial class ItemPrefab
{
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;
}
}
abstract partial class MapEntity
{
public void AddLinked(MapEntity entity)
{
linkedTo.Add(entity);
}
}
}
namespace Barotrauma.Items.Components
{
using Barotrauma.Networking;
partial class CustomInterface
{
}
partial struct Signal
{
}
}
@@ -1,391 +0,0 @@
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
partial class LuaUserData
{
public static ReadOnlyDictionary<string, IUserDataDescriptor> Descriptors => new ReadOnlyDictionary<string, IUserDataDescriptor>(descriptors);
private static ConcurrentDictionary<string, IUserDataDescriptor> descriptors = new ConcurrentDictionary<string, IUserDataDescriptor>();
public IUserDataDescriptor this[string index]
{
get => Descriptors.GetValueOrDefault(index);
}
public static Type GetType(string typeName) => LuaCsSetup.GetType(typeName);
public static 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 static IUserDataDescriptor RegisterTypeBarotrauma(string typeName)
{
return RegisterType($"Barotrauma.{typeName}");
}
public static 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 static bool IsRegistered(string typeName)
{
Type type = GetType(typeName);
if (type == null)
{
return false;
}
return UserData.GetDescriptorForType(type, true) != null;
}
public static 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 static IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArguements)
{
Type type = GetType(typeName);
Type[] typeArguements = typeNameArguements.Select(x => GetType(x)).ToArray();
Type genericType = type.MakeGenericType(typeArguements);
return UserData.RegisterType(genericType);
}
public static void UnregisterGenericType(string typeName, params string[] typeNameArguements)
{
Type type = GetType(typeName);
Type[] typeArguements = typeNameArguements.Select(x => GetType(x)).ToArray();
Type genericType = type.MakeGenericType(typeArguements);
UserData.UnregisterType(genericType);
}
public static 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 static string TypeOf(object obj)
{
if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); }
return obj.GetType().FullName;
}
public static 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);
var result = generic.Invoke(null, null);
return result;
}
public static 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;
}
private static 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 static 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 static 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 static 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 = LuaUserData.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 static 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 static 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 static 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 (GameMain.LuaCs != null)
return GameMain.LuaCs.CallLuaFunction(function, arg3.GetArray());
return null;
}));
}
public static 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 static 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 static 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;
}
/// <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>
public static DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
{
return UserData.Create(scriptObject.ToObject(desiredTypeDescriptor.Type), 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>
public static DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
IUserDataDescriptor descriptor = UserData.GetDescriptorForType(desiredType, true);
descriptor ??= new StandardUserDataDescriptor(desiredType, InteropAccessMode.Default);
return CreateUserDataFromDescriptor(scriptObject, descriptor);
}
public static void AddCallMetaTable(object userdata) { }
public static void Clear()
{
descriptors.Clear();
}
}
}
@@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Loaders;
using System.Linq;
namespace Barotrauma
{
class LuaScriptLoader : ScriptLoaderBase
{
public override object LoadFile(string file, Table globalContext)
{
if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return null;
return File.ReadAllText(file);
}
public override bool ScriptFileExists(string file)
{
if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return false;
return File.Exists(file);
}
}
}
@@ -9,10 +9,19 @@ namespace Barotrauma
{
private static string[] trackingFiles = new string[]
{
"Barotrauma.dll", "Barotrauma.deps.json", "Barotrauma.pdb", "BarotraumaCore.dll", "BarotraumaCore.pdb",
"0Harmony.dll", "Mono.Cecil.dll",
/* Barotrauma */
"Barotrauma.dll",
"Barotrauma.deps.json",
"Barotrauma.pdb",
"BarotraumaCore.dll",
"BarotraumaCore.pdb",
/* HarmonyX Package */
"0Harmony.dll",
"Mono.Cecil.dll",
"Sigil.dll",
"Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll",
"Mono.Cecil.Mdb.dll",
"Mono.Cecil.Pdb.dll",
"Mono.Cecil.Rocks.dll",
"MonoMod.Backports.dll",
"MonoMod.Core.dll",
@@ -20,15 +29,32 @@ namespace Barotrauma
"MonoMod.RuntimeDetour.dll",
"MonoMod.Utils.dll",
"MonoMod.Iced.dll",
"MoonSharp.Interpreter.dll", "MoonSharp.VsCodeDebugger.dll",
/* MoonSharp */
"MoonSharp.Interpreter.dll",
"MoonSharp.VsCodeDebugger.dll",
"Microsoft.CodeAnalysis.dll", "Microsoft.CodeAnalysis.CSharp.dll",
"Microsoft.CodeAnalysis.CSharp.Scripting.dll", "Microsoft.CodeAnalysis.Scripting.dll",
"System.Reflection.Metadata.dll", "System.Collections.Immutable.dll",
/* Microsoft SDKs */
"Microsoft.CodeAnalysis.dll",
"Microsoft.CodeAnalysis.CSharp.dll",
"Microsoft.CodeAnalysis.CSharp.Scripting.dll",
"Microsoft.CodeAnalysis.Scripting.dll",
"Microsoft.Toolkit.Diagnostics.dll",
"Microsoft.Extensions.Logging.Abstractions.dll",
"System.Reflection.Metadata.dll",
"System.Collections.Immutable.dll",
"System.Runtime.CompilerServices.Unsafe.dll",
"Publicized/DedicatedServer.dll", "Publicized/Barotrauma.dll"
/* Assembly Script Dependencies */
"Publicized/DedicatedServer.dll",
"Publicized/Barotrauma.dll",
"Publicized/BarotraumaCore.dll",
/* Other NuGet Packages */
"Basic.Reference.Assemblies.Net80.dll",
"FluentResults.dll",
"LightInject.dll",
"OneOf.dll"
};
private static void CreateMissingDirectory()
@@ -1,181 +0,0 @@
using System;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
namespace Barotrauma
{
internal enum LuaCsMessageOrigin
{
LuaCs,
Unknown,
LuaMod,
CSharpMod,
}
partial class LuaCsLogger
{
public static bool HideUserNames = true;
#if SERVER
private const string LogPrefix = "SV";
private const int NetMaxLength = 1024;
private const int NetMaxMessages = 60;
// This is used so its possible to call logging functions inside the serverLog
// hook without creating an infinite loop
private static bool lockLog = false;
#else
private const string LogPrefix = "CL";
#endif
public static LuaCsMessageLogger MessageLogger;
public static LuaCsExceptionHandler ExceptionHandler;
public static void HandleException(Exception ex, LuaCsMessageOrigin origin)
{
string errorString = "";
switch (ex)
{
case NetRuntimeException netRuntimeException:
if (netRuntimeException.DecoratedMessage == null)
{
errorString = netRuntimeException.ToString();
}
else
{
// FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace...
errorString = $"{netRuntimeException.DecoratedMessage}: {netRuntimeException}";
}
break;
case InterpreterException interpreterException:
if (interpreterException.DecoratedMessage == null)
{
errorString = interpreterException.ToString();
}
else
{
errorString = interpreterException.DecoratedMessage;
}
break;
default:
errorString = ex.StackTrace != null
? ex.ToString()
: $"{ex}\n{Environment.StackTrace}";
break;
}
LogError(Environment.UserName + " " + errorString, origin);
}
public static void LogError(string message, LuaCsMessageOrigin origin)
{
if (HideUserNames && !Environment.UserName.IsNullOrEmpty())
{
message = message.Replace(Environment.UserName, "USERNAME");
}
switch (origin)
{
case LuaCsMessageOrigin.LuaCs:
case LuaCsMessageOrigin.Unknown:
LogError($"[{LogPrefix} ERROR] {message}");
break;
case LuaCsMessageOrigin.LuaMod:
LogError($"[{LogPrefix} LUA ERROR] {message}");
break;
case LuaCsMessageOrigin.CSharpMod:
LogError($"[{LogPrefix} CS ERROR] {message}");
break;
}
}
public static void LogError(string message)
{
Log($"{message}", Color.Red, ServerLog.MessageType.Error);
}
public static void LogMessage(string message, Color? serverColor = null, Color? clientColor = null)
{
if (serverColor == null) { serverColor = Color.MediumPurple; }
if (clientColor == null) { clientColor = Color.Purple; }
#if SERVER
Log(message, serverColor);
#else
Log(message, clientColor);
#endif
}
public static void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
{
MessageLogger?.Invoke(message);
DebugConsole.NewMessage(message, color);
#if SERVER
void broadcastMessage(string m)
{
foreach (var client in GameMain.Server.ConnectedClients)
{
//if (client.ChatMsgQueue.Count > NetMaxMessages)
//{
// If there's an error or message happening many times per second (inside Update loop for example)
// we will need to discart some messages so the client doesn't get overloaded by all
// those net messages.
// continue;
//}
ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: color);
GameMain.Server.SendDirectChatMessage(consoleMessage, client);
if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog))
{
continue;
}
ChatMessage logMessage = ChatMessage.Create(messageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null);
GameMain.Server.SendDirectChatMessage(logMessage, client);
}
}
if (GameMain.Server != null)
{
if (GameMain.Server.ServerSettings.SaveServerLogs)
{
string logMessage = "[LuaCs] " + message;
GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, messageType, false);
if (!lockLog)
{
lockLog = true;
GameMain.LuaCs?.Hook?.Call("serverLog", logMessage, messageType);
lockLog = false;
}
}
for (int i = 0; i < message.Length; i += NetMaxLength)
{
string subStr = message.Substring(i, Math.Min(1024, message.Length - i));
broadcastMessage(subStr);
}
}
#endif
}
}
partial class LuaCsSetup
{
// Compatibility with cs mods that use this method.
public static void PrintLuaError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.LuaMod);
public static void PrintCsError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.CSharpMod);
public static void PrintGenericError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.LuaCs);
internal void PrintMessage(object message) => LuaCsLogger.LogMessage($"{message}");
public static void PrintCsMessage(object message) => LuaCsLogger.LogMessage($"{message}");
internal void HandleException(Exception ex, LuaCsMessageOrigin origin) => LuaCsLogger.HandleException(ex, origin);
}
}
@@ -1,114 +0,0 @@
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
partial class LuaCsSetup
{
public class LuaCsModStore
{
public abstract class ModStore<T, TStore>
{
protected Dictionary<string, TStore> store;
public TStore Set(string name, TStore value) => store[name] = value;
public TStore Get(string name) => store[name];
public ModStore(Dictionary<string, TStore> store) => this.store = store;
public abstract bool Equals(T value);
}
public class LuaModStore : ModStore<string, DynValue>
{
public string Name;
public LuaModStore(Dictionary<string, DynValue> store) : base(store) { }
public override bool Equals(string value) => Name == value;
}
public class CsModStore : ModStore<ACsMod, object>
{
public ACsMod Mod;
public CsModStore(Dictionary<string, object> store) : base(store) { }
public override bool Equals(ACsMod value) => Mod == value;
}
private HashSet<LuaModStore> luaModInterface;
private HashSet<CsModStore> csModInterface;
public LuaCsModStore()
{
luaModInterface = new HashSet<LuaModStore>();
csModInterface = new HashSet<CsModStore>();
}
public void Initialize()
{
UserData.RegisterType<LuaModStore>();
UserData.RegisterType<CsModStore>();
var msType = UserData.RegisterType<LuaCsModStore>();
var msDesc = (StandardUserDataDescriptor)msType;
typeof(StandardUserDataDescriptor).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m =>
{
if (
m.Name.Contains("Register")
)
{
msDesc.AddMember(m.Name, new MethodMemberDescriptor(m, InteropAccessMode.Default));
}
});
}
public void Clear()
{
luaModInterface.Clear();
csModInterface.Clear();
}
protected LuaModStore Register(string modName)
{
if (luaModInterface.Any(i => i.Equals(modName)))
{
LuaCsLogger.HandleException(new ArgumentException($"'{modName}' entry already registered"), LuaCsMessageOrigin.LuaMod);
return null;
}
var newHandle = new LuaModStore(new Dictionary<string, DynValue>());
if (luaModInterface.Add(newHandle)) return newHandle;
else return null;
}
[MoonSharpHidden]
public CsModStore Register(ACsMod mod)
{
if (csModInterface.Any(i => i.Equals(mod)))
{
LuaCsLogger.HandleException(new ArgumentException($"'{mod.GetType().FullName}' entry already registered"), LuaCsMessageOrigin.CSharpMod);
return null;
}
var newHandle = new CsModStore(new Dictionary<string, object>());
if (csModInterface.Add(newHandle)) return newHandle;
else return null;
}
public CsModStore GetCsStore(string modName) {
var result = csModInterface.Where(i => i.Mod.GetType().FullName == modName).FirstOrDefault();
if (result != null)
{
if (!result.Mod.IsDisposed) return result;
else
{
csModInterface.Remove(result);
return null;
}
}
else return null;
}
protected LuaModStore GetLuaStore(string modName) => luaModInterface.Where(i => i.Name == modName).FirstOrDefault();
}
}
}
@@ -1,173 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using Barotrauma.Networking;
namespace Barotrauma
{
partial class LuaCsNetworking
{
private static readonly HttpClient client = new HttpClient();
private enum LuaCsClientToServer
{
NetMessageId,
NetMessageString,
RequestSingleId,
RequestAllIds,
}
private enum LuaCsServerToClient
{
NetMessageId,
NetMessageString,
ReceiveIds
}
public bool RestrictMessageSize = true;
private Dictionary<string, LuaCsAction> netReceives = new Dictionary<string, LuaCsAction>();
private Dictionary<ushort, string> idToString = new Dictionary<ushort, string>();
private Dictionary<string, ushort> stringToId = new Dictionary<string, ushort>();
public void Initialize()
{
#if CLIENT
SendSyncMessage();
#endif
}
public void Remove(string netMessageName)
{
netReceives.Remove(netMessageName);
}
public IWriteMessage Start()
{
return new WriteOnlyMessage();
}
public string IdToString(ushort id)
{
if (idToString.ContainsKey(id)) { return idToString[id]; }
return null;
}
public ushort StringToId(string name)
{
if (stringToId.ContainsKey(name)) { return stringToId[name]; }
return 0;
}
private void HandleNetMessage(IReadMessage netMessage, string name, Client client = null)
{
if (netReceives.ContainsKey(name))
{
try
{
netReceives[name](netMessage, client);
}
catch (Exception e)
{
LuaCsLogger.LogError($"Exception thrown inside NetMessageReceive({name})", LuaCsMessageOrigin.CSharpMod);
LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod);
}
}
else
{
if (GameSettings.CurrentConfig.VerboseLogging)
{
#if SERVER
LuaCsLogger.LogError($"Received NetMessage for unknown name {name} from {GameServer.ClientLogName(client)}.");
#else
LuaCsLogger.LogError($"Received NetMessage for unknown name {name} from server.");
#endif
}
}
}
private void HandleNetMessageString(IReadMessage netMessage, Client client = null)
{
string name = netMessage.ReadString();
HandleNetMessage(netMessage, name, client);
}
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();
GameMain.LuaCs.Timer.Wait((object[] par) =>
{
callback(responseBody, (int)response.StatusCode, response.Headers);
}, 0);
}
catch (HttpRequestException e)
{
GameMain.LuaCs.Timer.Wait((object[] par) => { callback(e.Message, e.StatusCode, null); }, 0);
}
catch (Exception e)
{
GameMain.LuaCs.Timer.Wait((object[] par) => { callback(e.Message, null, null); }, 0);
}
}
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 HttpGet(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; }
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,538 +0,0 @@
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;
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)
{
string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath();
path = getFullPath(path);
bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
foreach (var package in ContentPackageManager.AllPackages)
{
if (package.UgcId.ValueEquals(LuaCsSetup.LuaForBarotraumaId) && pathStartsWith(getFullPath(package.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();
}
}
class LuaCsConfig
{
private enum ValueType
{
None,
Text,
Integer,
Decimal,
Boolean,
Collection,
Object,
Enum
}
private static Type[] LoadDocTypes(XElement typesElem)
{
var result = new List<Type>();
var loadedTypes = LuaCsSetup.AssemblyManager
.GetAllTypesInLoadedAssemblies()
.ToImmutableHashSet();
foreach (var elem in typesElem.Elements())
{
var typesFound = loadedTypes.Where(t => t.FullName?.EndsWith(elem.Value) ?? false).ToImmutableList();
if (!typesFound.Any())
{
ModUtils.Logging.PrintError(
$"{nameof(LuaCsConfig)}::{nameof(LoadDocTypes)}() | Unable to find a matching type for {elem.Value}");
continue;
}
result.AddRange(typesFound);
}
return result.ToArray();
}
private static IEnumerable<XElement> SaveDocTypes(IEnumerable<Type> types)
{
return types.Select(t => new XElement("Type", t.ToString()));
}
private static Type GetTypeAttr(Type[] types, XElement elem)
{
var idx = elem.GetAttributeInt("Type", -1);
if (idx < 0 || idx >= types.Length) throw new Exception($"Type index '{idx}' is outside of saved types bounds");
return types[idx];
}
private static ValueType GetValueType(XElement elem)
{
Enum.TryParse(typeof(ValueType), elem.Attribute("Value")?.Value, out object result);
if (result != null) return (ValueType)result;
else return ValueType.None;
}
private static object ParseValue(Type[] types, XElement elem)
{
var type = GetValueType(elem);
if (elem.IsEmpty) return null;
if (type == ValueType.Enum)
{
var tType = GetTypeAttr(types, elem);
if (tType == null || !tType.IsSubclassOf(typeof(Enum))) return null;
if (Enum.TryParse(tType, elem.Value, out object result)) return result;
else return null;
}
if (type == ValueType.Collection)
{
var tType = GetTypeAttr(types, elem);
var tInt = tType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
var gArg = tInt.GetGenericArguments()[0];
if (tType == null || !tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) return null;
object result = null;
if (result == null) {
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var param = c.GetParameters();
return param.Count() == 1 && param.Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
});
if (ctor != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
var castElems = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(gArg).Invoke(elements, new object[] { elements });
result = ctor.Invoke(new object[] { castElems });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
var addMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Add") return false;
var param = m.GetParameters();
return param.Count() == 1 && param[0].ParameterType == gArg;
});
if (ctor != null && addMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(null);
foreach (var el in elements) addMethod.Invoke(result, new object[] { el });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault();
var setMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Set") return false;
var param = m.GetParameters();
return param.Count() == 2 && param[0].ParameterType == typeof(int) && param[1].ParameterType == gArg;
});
if (ctor != null || setMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(new object[] { elements.Count() });
int i = 0;
foreach (var el in elements)
{
setMethod.Invoke(result, new object[] { i, el });
i++;
}
}
}
return result;
}
else if (type == ValueType.Text) return elem.Value;
else if (type == ValueType.Integer)
{
int.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Decimal)
{
float.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Boolean)
{
bool.TryParse(elem.Value, out var boolean);
return boolean;
}
else if (type == ValueType.Object)
{
var tType = GetTypeAttr(types, elem);
if (tType == null) return null;
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
object result = null;
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
if (ctor == null)
{
if (!tType.IsValueType) return null;
result = Activator.CreateInstance(tType);
}
else result = ctor.Invoke(null);
foreach(var el in elem.Elements())
{
var value = ParseValue(types, el);
var field = fields.FirstOrDefault(f => f.Name == el.Name.LocalName);
if (field != null) field.SetValue(result, value);
var property = properties.FirstOrDefault(p => p.Name == el.Name.LocalName);
if (property != null) property.SetValue(result, value);
}
return result;
}
else return elem.Value;
}
private static void AddTypeAttr(List<Type> types, Type type, XElement elem)
{
if (!types.Contains(type)) types.Add(type);
elem.SetAttributeValue("Type", types.IndexOf(type));
}
private static XElement ParseObject(List<Type> types, string name, object value)
{
XElement result = new XElement(name);
if (value != null)
{
var tType = value.GetType();
if (tType.IsEnum)
{
result.SetAttributeValue("Value", ValueType.Enum);
AddTypeAttr(types, tType, result);
result.Value = Enum.GetName(tType, value) ?? "";
}
else if (value is string str)
{
result.SetAttributeValue("Value", ValueType.Text);
result.Value = str;
}
else if (value is int integer)
{
result.SetAttributeValue("Value", ValueType.Integer);
result.Value = integer.ToString();
}
else if (value is float || value is double)
{
result.SetAttributeValue("Value", ValueType.Decimal);
result.Value = value.ToString();
}
else if (value is bool boolean)
{
result.SetAttributeValue("Value", ValueType.Boolean);
result.Value = boolean.ToString();
}
else if (tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
result.SetAttributeValue("Value", ValueType.Collection);
AddTypeAttr(types, tType, result);
var enumerator = (IEnumerator)tType.GetMethod("GetEnumerator").Invoke(value, null);
while (enumerator.MoveNext())
{
var elVal = ParseObject(types, "Item", enumerator.Current);
result.Add(elVal);
}
}
else if (tType.IsClass || tType.IsValueType)
{
result.SetAttributeValue("Value", ValueType.Object);
AddTypeAttr(types, tType, result);
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
foreach(var field in fields) result.Add(ParseObject(types, field.Name, field.GetValue(value)));
foreach (var property in properties) result.Add(ParseObject(types, property.Name, property.GetValue(value)));
}
else
{
result.SetAttributeValue("Value", ValueType.None);
result.Value = value.ToString();
}
}
return result;
}
public static T Load<T>(FileStream file)
{
var doc = XDocument.Load(file);
var rootElems = doc.Root.Elements().ToArray();
var types = rootElems[0];
var elem = rootElems[1];
var dict = ParseValue(LoadDocTypes(types), elem);
if (dict.GetType() == typeof(T)) return (T)dict;
else throw new Exception($"Loaded configuration is not of the type '{typeof(T).Name}'");
}
public static void Save(FileStream file, object obj)
{
var types = new List<Type>();
var elem = ParseObject(types, "Root", obj);
var root = new XElement("Configuration", new XElement("Types", SaveDocTypes(types)), elem);
var doc = new XDocument(root);
doc.Save(file);
}
public static T Load<T>(string path)
{
using (var file = LuaCsFile.OpenRead(path)) return Load<T>(file);
}
public static void Save(string path, object obj)
{
using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj);
}
}
}
@@ -1,341 +1,646 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using Microsoft.CodeAnalysis;
using Microsoft.Xna.Framework;
using OneOf;
using Platform = Barotrauma.LuaCs.Data.Platform;
// ReSharper disable ConvertClosureToMethodGroup
namespace Barotrauma;
public static class ModUtils
// This file is cursed, we put everything in it, and I'm not sorry about it.
namespace Barotrauma
{
#region LOGGING
public static class Logging
public static class ModUtils
{
public static void PrintMessage(string s)
public static class ItemPrefab
{
#if SERVER
LuaCsLogger.LogMessage($"[Server] {s}");
#else
LuaCsLogger.LogMessage($"[Client] {s}");
#endif
internal static Barotrauma.ItemPrefab GetItemPrefab(string itemNameOrId)
{
Barotrauma.ItemPrefab itemPrefab =
(Barotrauma.MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
Barotrauma.MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as Barotrauma.ItemPrefab;
return itemPrefab;
}
}
public static void PrintWarning(string s)
public static class Client
{
internal static ulong GetSteamId(Barotrauma.Networking.Client client)
{
if (client.AccountId.TryUnwrap(out AccountId outValue) && outValue is SteamId steamId)
{
return steamId.Value;
}
else
{
return 0;
}
}
#if SERVER
LuaCsLogger.Log($"[Server] {s}", Color.Yellow);
#else
LuaCsLogger.Log($"[Client] {s}", Color.Yellow);
internal static void UnbanPlayer(string playerName)
{
GameMain.Server.UnbanPlayer(playerName);
}
internal static void BanPlayer(string player, string reason, bool range = false, float seconds = -1)
{
if (seconds == -1)
{
GameMain.Server.BanPlayer(player, reason, null);
}
else
{
GameMain.Server.BanPlayer(player, reason, TimeSpan.FromSeconds(seconds));
}
}
#endif
internal static IReadOnlyList<Barotrauma.Networking.Client> ClientList
{
get
{
if (GameMain.IsSingleplayer) { return new List<Barotrauma.Networking.Client>(); }
#if SERVER
return GameMain.Server.ConnectedClients;
#else
return GameMain.Client.ConnectedClients;
#endif
}
}
}
public static class Definitions
{
public const string LuaCsForBarotrauma = nameof(LuaCsForBarotrauma);
}
public static void PrintError(string s)
public static class Environment
{
#if SERVER
LuaCsLogger.LogError($"[Server] {s}");
internal static void SetCurrentThreadAsMain() => MainThreadId = Thread.CurrentThread.ManagedThreadId;
public static int MainThreadId { get; private set; } = Int32.MinValue;
public static bool IsMainThread
{
get
{
if (MainThreadId == Int32.MinValue)
throw new ArgumentNullException("MainThread ID not set.");
return Thread.CurrentThread.ManagedThreadId == MainThreadId;
}
}
public static readonly Platform CurrentPlatform =
#if WINDOWS
Platform.Windows;
#elif MACOS
Platform.MacOS;
#elif LINUX
Platform.Linux;
#else
LuaCsLogger.LogError($"[Client] {s}");
Platform.Linux;
#endif
public static readonly Target CurrentTarget =
#if CLIENT
Target.Client;
#elif SERVER
Target.Server;
#else
Target.Server;
#endif
}
#region LOGGING
public static class Logging
{
public static void PrintMessage(string s)
{
#if SERVER
LuaCsSetup.Instance.Logger.LogMessage($"{s}");
#else
LuaCsSetup.Instance.Logger.LogMessage($"{s}");
#endif
}
public static void PrintWarning(string s)
{
#if SERVER
LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow);
#else
LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow);
#endif
}
public static void PrintError(string s)
{
#if SERVER
LuaCsSetup.Instance.Logger.LogError($"{s}");
#else
LuaCsSetup.Instance.Logger.LogError($"{s}");
#endif
}
}
#endregion
#region FILE_IO
// ReSharper disable once InconsistentNaming
public static class IO
{
public static IEnumerable<string> FindAllFilesInDirectory(string folder, string pattern,
SearchOption option)
{
try
{
return Directory.GetFiles(folder, pattern, option);
}
catch (DirectoryNotFoundException e)
{
return new string[] { };
}
}
public static string PrepareFilePathString(string filePath) =>
PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath));
public static string PrepareFilePathString(string path, string fileName) =>
Path.Combine(SanitizePath(path), SanitizeFileName(fileName));
public static string SanitizeFileName(string fileName)
{
foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform())
fileName = fileName.Replace(c, '_');
return fileName;
}
/// <summary>
/// Gets the sanitized path for the top-level directory for a given content package.
/// </summary>
/// <param name="package"></param>
/// <returns></returns>
public static string GetContentPackageDir(ContentPackage package)
{
return SanitizePath(Path.GetFullPath(package.Dir));
}
public static string SanitizePath(string path)
{
foreach (char c in Path.GetInvalidPathChars())
path = path.Replace(c.ToString(), "_");
return path.CleanUpPath();
}
public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText,
Func<string> fileDataFactory = null, bool createFile = true)
{
fileText = null;
string fp = Path.GetFullPath(SanitizePath(filePath));
IOActionResultState ioActionResultState = IOActionResultState.Success;
if (createFile)
{
ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory);
}
else if (!File.Exists(fp))
{
return IOActionResultState.FileNotFound;
}
if (ioActionResultState == IOActionResultState.Success)
{
try
{
fileText = File.ReadAllText(fp!);
return IOActionResultState.Success;
}
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
return IOActionResultState.FilePathNull;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
return IOActionResultState.FilePathInvalid;
}
catch (DirectoryNotFoundException dnfe)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
return IOActionResultState.DirectoryMissing;
}
catch (PathTooLongException ptle)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
return IOActionResultState.PathTooLong;
}
catch (NotSupportedException nse)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
return IOActionResultState.InvalidOperation;
}
catch (IOException ioe)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
return IOActionResultState.IOFailure;
}
catch (Exception e)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
return IOActionResultState.UnknownError;
}
}
return ioActionResultState;
}
public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath,
Func<string> fileDataFactory = null)
{
string file = Path.GetFileName(filePath);
string path = Path.GetDirectoryName(filePath)!;
formattedFilePath = IO.PrepareFilePathString(path, file);
try
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
if (!File.Exists(formattedFilePath))
File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke());
return IOActionResultState.Success;
}
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}");
return IOActionResultState.FilePathNull;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}");
return IOActionResultState.FilePathInvalid;
}
catch (DirectoryNotFoundException dnfe)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}");
return IOActionResultState.DirectoryMissing;
}
catch (PathTooLongException ptle)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}");
return IOActionResultState.PathTooLong;
}
catch (NotSupportedException nse)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}");
return IOActionResultState.InvalidOperation;
}
catch (IOException ioe)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}");
return IOActionResultState.IOFailure;
}
catch (Exception e)
{
ModUtils.Logging.PrintError(
$"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}");
return IOActionResultState.UnknownError;
}
}
public static IOActionResultState WriteFileText(string filePath, string fileText)
{
IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp);
if (ioActionResultState == IOActionResultState.Success)
{
try
{
File.WriteAllText(fp!, fileText);
return IOActionResultState.Success;
}
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError(
$"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
return IOActionResultState.FilePathNull;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError(
$"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
return IOActionResultState.FilePathInvalid;
}
catch (DirectoryNotFoundException dnfe)
{
ModUtils.Logging.PrintError(
$"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
return IOActionResultState.DirectoryMissing;
}
catch (PathTooLongException ptle)
{
ModUtils.Logging.PrintError(
$"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
return IOActionResultState.PathTooLong;
}
catch (NotSupportedException nse)
{
ModUtils.Logging.PrintError(
$"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
return IOActionResultState.InvalidOperation;
}
catch (IOException ioe)
{
ModUtils.Logging.PrintError(
$"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
return IOActionResultState.IOFailure;
}
catch (Exception e)
{
ModUtils.Logging.PrintError(
$"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
return IOActionResultState.UnknownError;
}
}
return ioActionResultState;
}
/// <summary>
///
/// </summary>
/// <param name="instance"></param>
/// <param name="filepath"></param>
/// <param name="typeFactory"></param>
/// <param name="createFile"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static bool LoadOrCreateTypeXml<T>(out T instance,
string filepath, Func<T> typeFactory = null, bool createFile = true) where T : class, new()
{
instance = null;
filepath = filepath.CleanUpPath();
if (IOActionResultState.Success == GetOrCreateFileText(
filepath, out string fileText, typeFactory is not null
? () =>
{
using StringWriter sw = new StringWriter();
T t = typeFactory?.Invoke();
if (t is not null)
{
XmlSerializer s = new XmlSerializer(typeof(T));
s.Serialize(sw, t);
return sw.ToString();
}
return "";
}
: null, createFile))
{
XmlSerializer s = new XmlSerializer(typeof(T));
try
{
using TextReader tr = new StringReader(fileText);
instance = (T)s.Deserialize(tr);
return true;
}
catch (InvalidOperationException ioe)
{
ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}.");
#if DEBUG
ModUtils.Logging.PrintError(
$"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}");
#endif
instance = null;
return false;
}
}
return false;
}
public enum IOActionResultState
{
Success,
FileNotFound,
FilePathNull,
FilePathInvalid,
DirectoryMissing,
PathTooLong,
InvalidOperation,
IOFailure,
UnknownError
}
}
#endregion
#region GAME
public static class Game
{
/// <summary>
/// Returns whether or not there is a round running.
/// </summary>
/// <returns></returns>
public static bool IsRoundInProgress()
{
#if CLIENT
if (Screen.Selected is not null
&& Screen.Selected.IsEditor)
return false;
#endif
return GameMain.GameSession is not null && Level.Loaded is not null;
}
}
#endregion
#region THREADING
public static class Threading
{
/// <summary>
/// Gets the boolean value of an integer with thread-safety via <code>Interlocked</code>.
/// </summary>
/// <param name="var"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool GetBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) > 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SetBool(ref int var, bool value)
{
if (value)
{
Interlocked.CompareExchange(ref var, 1, 0);
}
else
{
Interlocked.CompareExchange(ref var, 0, 1);
}
}
/// <summary>
/// Gets if the integer is under 1 (is zero/false) and, if so, sets the value to one/true.
/// </summary>
/// <param name="var"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CheckIfClearAndSetBool(ref int var)
{
return Interlocked.CompareExchange(ref var, 1, 0) < 1;
}
/// <summary>
/// Gets if the integer is over 0 (is one/true) and, if so, sets the value to zero/false.
/// </summary>
/// <param name="var"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CheckIfSetAndClearBool(ref int var)
{
return Interlocked.CompareExchange(ref var, 0, 1) > 0;
}
}
#endregion
#region UTILITIES_CORE
public static V TryGetOrSet<K, V>(this IDictionary<K,V> dict, K key, Func<V> valueFactory) where K : IEquatable<K>
{
if (dict.TryGetValue(key, out var dictValue)) return dictValue;
if (valueFactory is not null)
dict.Add(key, valueFactory());
else
return default;
return dict[key];
}
#endregion
}
public static class AssemblyExtensions
{
/// <summary>
/// Gets all types in the given assembly. Handles invalid type scenarios.
/// </summary>
/// <param name="assembly">The assembly to scan</param>
/// <returns>An enumerable collection of types.</returns>
public static IEnumerable<Type> GetSafeTypes(this Assembly assembly)
{
// Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException re)
{
try
{
return re.Types.Where(x => x != null)!;
}
catch (InvalidOperationException)
{
return new List<Type>();
}
}
catch (Exception)
{
return new List<Type>();
}
}
}
#endregion
#region FILE_IO
// ReSharper disable once InconsistentNaming
public static class IO
public static class CollectionExtensions
{
public static IEnumerable<string> FindAllFilesInDirectory(string folder, string pattern,
SearchOption option)
{
try
{
return Directory.GetFiles(folder, pattern, option);
}
catch (DirectoryNotFoundException e)
{
return new string[] { };
}
}
public static string PrepareFilePathString(string filePath) =>
PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath));
public static string PrepareFilePathString(string path, string fileName) =>
Path.Combine(SanitizePath(path), SanitizeFileName(fileName));
public static string SanitizeFileName(string fileName)
{
foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform())
fileName = fileName.Replace(c, '_');
return fileName;
}
/// <summary>
/// Gets the sanitized path for the top-level directory for a given content package.
/// Executes a series of asynchronous tasks with limited parallelism to maintain execution efficiency.
/// </summary>
/// <param name="package"></param>
/// <returns></returns>
public static string GetContentPackageDir(ContentPackage package)
{
return SanitizePath(Path.GetFullPath(package.Dir));
}
public static string SanitizePath(string path)
{
foreach (char c in Path.GetInvalidPathChars())
path = path.Replace(c.ToString(), "_");
return path.CleanUpPath();
}
public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, Func<string> fileDataFactory = null, bool createFile = true)
{
fileText = null;
string fp = Path.GetFullPath(SanitizePath(filePath));
IOActionResultState ioActionResultState = IOActionResultState.Success;
if (createFile)
{
ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory);
}
else if (!File.Exists(fp))
{
return IOActionResultState.FileNotFound;
}
if (ioActionResultState == IOActionResultState.Success)
{
try
{
fileText = File.ReadAllText(fp!);
return IOActionResultState.Success;
}
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
return IOActionResultState.FilePathNull;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
return IOActionResultState.FilePathInvalid;
}
catch (DirectoryNotFoundException dnfe)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
return IOActionResultState.DirectoryMissing;
}
catch (PathTooLongException ptle)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
return IOActionResultState.PathTooLong;
}
catch (NotSupportedException nse)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
return IOActionResultState.InvalidOperation;
}
catch (IOException ioe)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
return IOActionResultState.IOFailure;
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
return IOActionResultState.UnknownError;
}
}
return ioActionResultState;
}
public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath, Func<string> fileDataFactory = null)
{
string file = Path.GetFileName(filePath);
string path = Path.GetDirectoryName(filePath)!;
formattedFilePath = IO.PrepareFilePathString(path, file);
try
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
if (!File.Exists(formattedFilePath))
File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke());
return IOActionResultState.Success;
}
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}");
return IOActionResultState.FilePathNull;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}");
return IOActionResultState.FilePathInvalid;
}
catch (DirectoryNotFoundException dnfe)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}");
return IOActionResultState.DirectoryMissing;
}
catch (PathTooLongException ptle)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}");
return IOActionResultState.PathTooLong;
}
catch (NotSupportedException nse)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}");
return IOActionResultState.InvalidOperation;
}
catch (IOException ioe)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}");
return IOActionResultState.IOFailure;
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}");
return IOActionResultState.UnknownError;
}
}
public static IOActionResultState WriteFileText(string filePath, string fileText)
{
IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp);
if (ioActionResultState == IOActionResultState.Success)
{
try
{
File.WriteAllText(fp!, fileText);
return IOActionResultState.Success;
}
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
return IOActionResultState.FilePathNull;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
return IOActionResultState.FilePathInvalid;
}
catch (DirectoryNotFoundException dnfe)
{
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
return IOActionResultState.DirectoryMissing;
}
catch (PathTooLongException ptle)
{
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
return IOActionResultState.PathTooLong;
}
catch (NotSupportedException nse)
{
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
return IOActionResultState.InvalidOperation;
}
catch (IOException ioe)
{
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
return IOActionResultState.IOFailure;
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
return IOActionResultState.UnknownError;
}
}
return ioActionResultState;
}
/// <summary>
///
/// </summary>
/// <param name="instance"></param>
/// <param name="filepath"></param>
/// <param name="typeFactory"></param>
/// <param name="createFile"></param>
/// <param name="source"></param>
/// <param name="funcBody"></param>
/// <param name="maxDegreeOfParallelism"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static bool LoadOrCreateTypeXml<T>(out T instance,
string filepath, Func<T> typeFactory = null, bool createFile = true) where T : class, new()
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> funcBody, int maxDegreeOfParallelism = 4)
{
instance = null;
filepath = filepath.CleanUpPath();
if (IOActionResultState.Success == GetOrCreateFileText(
filepath, out string fileText, typeFactory is not null ? () =>
{
using StringWriter sw = new StringWriter();
T t = typeFactory?.Invoke();
if (t is not null)
{
XmlSerializer s = new XmlSerializer(typeof(T));
s.Serialize(sw, t);
return sw.ToString();
}
return "";
} : null, createFile))
async Task AwaitParallelLimit(IEnumerator<T> partition)
{
XmlSerializer s = new XmlSerializer(typeof(T));
try
using (partition)
{
using TextReader tr = new StringReader(fileText);
instance = (T)s.Deserialize(tr);
return true;
}
catch(InvalidOperationException ioe)
{
ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}.");
#if DEBUG
ModUtils.Logging.PrintError($"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}");
#endif
instance = null;
return false;
while (partition.MoveNext())
{
await Task.Yield(); // prevents a sync/hot thread hangup
await funcBody(partition.Current);
}
}
}
return false;
}
public enum IOActionResultState
{
Success, FileNotFound, FilePathNull, FilePathInvalid, DirectoryMissing, PathTooLong, InvalidOperation, IOFailure, UnknownError
return Task.WhenAll(
Partitioner
.Create(source)
.GetPartitions(maxDegreeOfParallelism)
.AsParallel()
.Select(p => AwaitParallelLimit(p)));
}
}
#endregion
#region GAME
public static class Game
{
/// <summary>
/// Returns whether or not there is a round running.
/// </summary>
/// <returns></returns>
public static bool IsRoundInProgress()
{
#if CLIENT
if (Screen.Selected is not null
&& Screen.Selected.IsEditor)
return false;
#endif
return GameMain.GameSession is not null && Level.Loaded is not null;
}
}
#endregion
}
#region ExceptionData
namespace FluentResults.LuaCs
{
public static class MetadataType
{
public static string ExceptionDetails = nameof(ExceptionDetails);
/// <summary>
/// The object that threw the exception.
/// </summary>
public static string ExceptionObject = nameof(ExceptionObject);
/// <summary>
/// The parameter-object responsible for the exception thrown (not the exception thrower).
/// </summary>
public static string RootObject = nameof(RootObject);
/// <summary>
/// Additional exception sources.
/// </summary>
public static string Sources = nameof(Sources);
public static string StackTrace = nameof(StackTrace);
}
}
#endregion
@@ -1,6 +0,0 @@
namespace Barotrauma;
public enum ApplicationMode
{
Client, Server
}
@@ -1,15 +0,0 @@
namespace Barotrauma;
public enum AssemblyLoadingSuccessState
{
ACLLoadFailure,
AlreadyLoaded,
BadFilePath,
CannotLoadFile,
InvalidAssembly,
NoAssemblyFound,
PluginInstanceFailure,
BadName,
CannotLoadFromStream,
Success
}
@@ -1,901 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
// ReSharper disable EventNeverSubscribedTo.Global
// ReSharper disable InconsistentNaming
namespace Barotrauma;
/***
* Note: This class was written to be thread-safe in order to allow parallelization in loading in the future if the need
* becomes necessary as there is almost no serial performance overhead for adding threading protection.
*/
/// <summary>
/// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin.
/// All plugins are loaded into their own AssemblyLoadContext along with their dependencies.
/// </summary>
public class AssemblyManager
{
#region ExternalAPI
/// <summary>
/// Called when an assembly is loaded.
/// </summary>
public event Action<Assembly> OnAssemblyLoaded;
/// <summary>
/// Called when an assembly is marked for unloading, before unloading begins. You should use this to cleanup
/// any references that you have to this assembly.
/// </summary>
public event Action<Assembly> OnAssemblyUnloading;
/// <summary>
/// Called whenever an exception is thrown. First arg is a formatted message, Second arg is the Exception.
/// </summary>
public event Action<string, Exception> OnException;
/// <summary>
/// For unloading issue debugging. Called whenever MemoryFileAssemblyContextLoader [load context] is unloaded.
/// </summary>
public event Action<Guid> OnACLUnload;
/// <summary>
/// [DEBUG ONLY]
/// Returns a list of the current unloading ACLs.
/// </summary>
public ImmutableList<WeakReference<MemoryFileAssemblyContextLoader>> StillUnloadingACLs
{
get
{
OpsLockUnloaded.EnterReadLock();
try
{
return UnloadingACLs.ToImmutableList();
}
finally
{
OpsLockUnloaded.ExitReadLock();
}
}
}
// ReSharper disable once MemberCanBePrivate.Global
/// <summary>
/// Checks if there are any AssemblyLoadContexts still in the process of unloading.
/// </summary>
public bool IsCurrentlyUnloading
{
get
{
OpsLockUnloaded.EnterReadLock();
try
{
return UnloadingACLs.Any();
}
catch (Exception)
{
return false;
}
finally
{
OpsLockUnloaded.ExitReadLock();
}
}
}
// Old API compatibility
public IEnumerable<Type> GetSubTypesInLoadedAssemblies<T>()
{
return GetSubTypesInLoadedAssemblies<T>(false);
}
/// <summary>
/// Allows iteration over all non-interface types in all loaded assemblies in the AsmMgr that are assignable to the given type (IsAssignableFrom).
/// Warning: care should be used when using this method in hot paths as performance may be affected.
/// </summary>
/// <typeparam name="T">The type to compare against</typeparam>
/// <param name="rebuildList">Forces caches to clear and for the lists of types to be rebuilt.</param>
/// <returns>An Enumerator for matching types.</returns>
public IEnumerable<Type> GetSubTypesInLoadedAssemblies<T>(bool rebuildList)
{
Type targetType = typeof(T);
string typeName = targetType.FullName ?? targetType.Name;
// rebuild
if (rebuildList)
RebuildTypesList();
// check cache
if (_subTypesLookupCache.TryGetValue(typeName, out var subTypeList))
{
return subTypeList;
}
// build from scratch
OpsLockLoaded.EnterReadLock();
try
{
// build list
var list1 = _defaultContextTypes
.Where(kvp1 => targetType.IsAssignableFrom(kvp1.Value) && !kvp1.Value.IsInterface)
.Concat(LoadedACLs
.SelectMany(kvp => kvp.Value.AssembliesTypes)
.Where(kvp2 => targetType.IsAssignableFrom(kvp2.Value) && !kvp2.Value.IsInterface))
.Select(kvp3 => kvp3.Value)
.ToImmutableList();
// only add if we find something
if (list1.Count > 0)
{
if (!_subTypesLookupCache.TryAdd(typeName, list1))
{
ModUtils.Logging.PrintError(
$"{nameof(AssemblyManager)}: Unable to add subtypes to cache of type {typeName}!");
}
}
else
{
ModUtils.Logging.PrintMessage(
$"{nameof(AssemblyManager)}: Warning: No types found during search for subtypes of {typeName}");
}
return list1;
}
catch (Exception e)
{
this.OnException?.Invoke($"{nameof(AssemblyManager)}::{nameof(GetSubTypesInLoadedAssemblies)}() | Error: {e.Message}", e);
return ImmutableList<Type>.Empty;
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
/// <summary>
/// Tries to get types assignable to type from the ACL given the Guid.
/// </summary>
/// <param name="id"></param>
/// <param name="types"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public bool TryGetSubTypesFromACL<T>(Guid id, out IEnumerable<Type> types)
{
Type targetType = typeof(T);
if (TryGetACL(id, out var acl))
{
types = acl.AssembliesTypes
.Where(kvp => targetType.IsAssignableFrom(kvp.Value) && !kvp.Value.IsInterface)
.Select(kvp => kvp.Value);
return true;
}
types = null;
return false;
}
/// <summary>
/// Tries to get types from the ACL given the Guid.
/// </summary>
/// <param name="id"></param>
/// <param name="types"></param>
/// <returns></returns>
public bool TryGetSubTypesFromACL(Guid id, out IEnumerable<Type> types)
{
if (TryGetACL(id, out var acl))
{
types = acl.AssembliesTypes.Select(kvp => kvp.Value);
return true;
}
types = null;
return false;
}
/// <summary>
/// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string.
/// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ".
/// </summary>
/// <param name="typeName">The string name of the type to search for.</param>
/// <returns>An Enumerator for matching types. List will be empty if bad params are supplied.</returns>
public IEnumerable<Type> GetTypesByName(string typeName)
{
List<Type> types = new();
if (typeName.IsNullOrWhiteSpace())
return types;
bool byRef = false;
if (typeName.StartsWith("out ") || typeName.StartsWith("ref "))
{
typeName = typeName.Remove(0, 4);
byRef = true;
}
TypesListHelper();
if (types.Count > 0)
return types;
// we couldn't find it, rebuild and try one more time
RebuildTypesList();
TypesListHelper();
if (types.Count > 0)
return types;
OpsLockLoaded.EnterReadLock();
try
{
// fallback to Type.GetType
Type t = Type.GetType(typeName, false, false);
if (t is not null)
{
types.Add(byRef ? t.MakeByRefType() : t);
return types;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
t = assembly.GetType(typeName, false, false);
if (t is not null)
types.Add(byRef ? t.MakeByRefType() : t);
}
catch (Exception e)
{
this.OnException?.Invoke(
$"{nameof(AssemblyManager)}::{nameof(GetTypesByName)}() | Error: {e.Message}", e);
}
}
}
finally
{
OpsLockLoaded.ExitReadLock();
}
return types;
void TypesListHelper()
{
if (_defaultContextTypes.TryGetValue(typeName, out var type1))
{
if (type1 is not null)
types.Add(byRef ? type1.MakeByRefType() : type1);
}
OpsLockLoaded.EnterReadLock();
try
{
foreach (KeyValuePair<Guid,LoadedACL> loadedAcl in LoadedACLs)
{
var at = loadedAcl.Value.AssembliesTypes;
if (at.TryGetValue(typeName, out var type2))
{
if (type2 is not null)
types.Add(byRef ? type2.MakeByRefType() : type2);
}
}
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
}
/// <summary>
/// Allows iteration over all types (including interfaces) in all loaded assemblies managed by the AsmMgr.
/// Warning: High usage may result in performance issues.
/// </summary>
/// <returns>An Enumerator for iteration.</returns>
public IEnumerable<Type> GetAllTypesInLoadedAssemblies()
{
OpsLockLoaded.EnterReadLock();
try
{
return _defaultContextTypes
.Select(kvp => kvp.Value)
.Concat(LoadedACLs
.SelectMany(kvp => kvp.Value?.AssembliesTypes.Select(kv => kv.Value)))
.ToImmutableList();
}
catch
{
return ImmutableList<Type>.Empty;
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
/// <summary>
/// Returns a list of all loaded ACLs.
/// WARNING: References to these ACLs outside of the AssemblyManager should be kept in a WeakReference in order
/// to avoid causing issues with unloading/disposal.
/// </summary>
/// <returns></returns>
public IEnumerable<LoadedACL> GetAllLoadedACLs()
{
OpsLockLoaded.EnterReadLock();
try
{
if (!LoadedACLs.Any())
{
return ImmutableList<LoadedACL>.Empty;
}
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
}
catch
{
return ImmutableList<LoadedACL>.Empty;
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
#endregion
#region InternalAPI
/// <summary>
/// [Unsafe] Warning: only for use in nested threading functions. Requires care to manage access.
/// Does not make any guarantees about the state of the ACL after the list has been returned.
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.Synchronized | MethodImplOptions.NoInlining)]
internal ImmutableList<LoadedACL> UnsafeGetAllLoadedACLs()
{
if (LoadedACLs.IsEmpty)
return ImmutableList<LoadedACL>.Empty;
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
}
/// <summary>
/// Used by content package and plugin management to stop unloading of a given ACL until all plugins have gracefully closed.
/// </summary>
public event System.Func<LoadedACL, bool> IsReadyToUnloadACL;
/// <summary>
/// Compiles an assembly from supplied references and syntax trees into the specified AssemblyContextLoader.
/// A new ACL will be created if the Guid supplied is Guid.Empty.
/// </summary>
/// <param name="compiledAssemblyName"></param>
/// <param name="syntaxTree"></param>
/// <param name="externalMetadataReferences"></param>
/// <param name="compilationOptions"></param>
/// <param name="friendlyName">A non-unique name for later reference. Optional, set to null if unused.</param>
/// <param name="id">The guid of the assembly </param>
/// <param name="externFileAssemblyRefs"></param>
/// <returns></returns>
public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName,
[NotNull] IEnumerable<SyntaxTree> syntaxTree,
IEnumerable<MetadataReference> externalMetadataReferences,
[NotNull] CSharpCompilationOptions compilationOptions,
string friendlyName,
ref Guid id,
IEnumerable<Assembly> externFileAssemblyRefs = null)
{
// validation
if (compiledAssemblyName.IsNullOrWhiteSpace())
return AssemblyLoadingSuccessState.BadName;
if (syntaxTree is null)
return AssemblyLoadingSuccessState.InvalidAssembly;
if (!GetOrCreateACL(id, friendlyName, out var acl))
return AssemblyLoadingSuccessState.ACLLoadFailure;
id = acl.Id; // pass on true id returned
// this acl is already hosting an in-memory assembly
if (acl.Acl.CompiledAssembly is not null)
return AssemblyLoadingSuccessState.AlreadyLoaded;
// compile
AssemblyLoadingSuccessState state;
string messages;
try
{
state = acl.Acl.CompileAndLoadScriptAssembly(compiledAssemblyName, syntaxTree, externalMetadataReferences,
compilationOptions, out messages, externFileAssemblyRefs);
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}::{nameof(LoadAssemblyFromMemory)}() | Failed to compile and load assemblies for [ {compiledAssemblyName} / {friendlyName} ]! Details: {e.Message} | {e.StackTrace}");
return AssemblyLoadingSuccessState.InvalidAssembly;
}
// get types
if (state is AssemblyLoadingSuccessState.Success)
{
_subTypesLookupCache.Clear();
acl.RebuildTypesList();
OnAssemblyLoaded?.Invoke(acl.Acl.CompiledAssembly);
}
else
{
ModUtils.Logging.PrintError($"Unable to compile assembly '{compiledAssemblyName}' due to errors: {messages}");
}
return state;
}
/// <summary>
/// Switches the ACL with the given Guid to Template Mode, which disables assembly name resolution for any assemblies loaded in it.
/// These ACLs are intended to be used to host Assemblies for information only and not for code execution.
/// WARNING: This process is irreversible.
/// </summary>
/// <param name="guid">Guid of the ACL.</param>
/// <returns>Whether or not an ACL was found with the given ID.</returns>
public bool SetACLToTemplateMode(Guid guid)
{
if (!TryGetACL(guid, out var acl))
return false;
acl.Acl.IsTemplateMode = true;
return true;
}
/// <summary>
/// Tries to load all assemblies at the supplied file paths list into the ACl with the given Guid.
/// If the supplied Guid is Empty, then a new ACl will be created and the Guid will be assigned to it.
/// </summary>
/// <param name="filePaths">List of assemblies to try and load.</param>
/// <param name="friendlyName">A non-unique name for later reference. Optional.</param>
/// <param name="id">Guid of the ACL or Empty if none specified. Guid of ACL will be assigned to this var.</param>
/// <returns>Operation success messages.</returns>
/// <exception cref="ArgumentNullException"></exception>
public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable<string> filePaths,
string friendlyName, ref Guid id)
{
if (filePaths is null)
{
var exception = new ArgumentNullException(
$"{nameof(AssemblyManager)}::{nameof(LoadAssembliesFromLocations)}() | file paths supplied is null!");
this.OnException?.Invoke($"Error: {exception.Message}", exception);
throw exception;
}
ImmutableList<string> assemblyFilePaths = filePaths.ToImmutableList(); // copy the list before loading
if (!assemblyFilePaths.Any())
{
return AssemblyLoadingSuccessState.NoAssemblyFound;
}
if (GetOrCreateACL(id, friendlyName, out var loadedAcl))
{
var state = loadedAcl.Acl.LoadFromFiles(assemblyFilePaths);
// if failure, we dispose of the acl
if (state != AssemblyLoadingSuccessState.Success)
{
DisposeACL(loadedAcl.Id);
ModUtils.Logging.PrintError($"ACL {friendlyName} failed, unloading...");
return state;
}
// build types list
_subTypesLookupCache.Clear();
loadedAcl.RebuildTypesList();
id = loadedAcl.Id;
foreach (Assembly assembly in loadedAcl.Acl.Assemblies)
{
OnAssemblyLoaded?.Invoke(assembly);
}
return state;
}
return AssemblyLoadingSuccessState.ACLLoadFailure;
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Synchronized)]
public bool TryBeginDispose()
{
OpsLockLoaded.EnterWriteLock();
OpsLockUnloaded.EnterWriteLock();
try
{
_subTypesLookupCache.Clear();
_defaultContextTypes = _defaultContextTypes.Clear();
foreach (KeyValuePair<Guid, LoadedACL> loadedAcl in LoadedACLs)
{
if (loadedAcl.Value.Acl is not null)
{
if (IsReadyToUnloadACL is not null)
{
foreach (Delegate del in IsReadyToUnloadACL.GetInvocationList())
{
if (del is System.Func<LoadedACL, bool> { } func)
{
if (!func.Invoke(loadedAcl.Value))
return false; // Not ready, exit
}
}
}
foreach (Assembly assembly in loadedAcl.Value.Acl.Assemblies)
{
OnAssemblyUnloading?.Invoke(assembly);
}
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(loadedAcl.Value.Acl, true));
loadedAcl.Value.ClearTypesList();
loadedAcl.Value.Acl.Unload();
loadedAcl.Value.ClearACLRef();
OnACLUnload?.Invoke(loadedAcl.Value.Id);
}
}
LoadedACLs.Clear();
return true;
}
catch(Exception e)
{
// should never happen
this.OnException?.Invoke($"{nameof(TryBeginDispose)}() | Error: {e.Message}", e);
return false;
}
finally
{
OpsLockUnloaded.ExitWriteLock();
OpsLockLoaded.ExitWriteLock();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public bool FinalizeDispose()
{
bool isUnloaded;
OpsLockUnloaded.EnterUpgradeableReadLock();
try
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // force the gc to collect unloaded acls.
List<WeakReference<MemoryFileAssemblyContextLoader>> toRemove = new();
foreach (WeakReference<MemoryFileAssemblyContextLoader> weakReference in UnloadingACLs)
{
if (!weakReference.TryGetTarget(out _))
{
toRemove.Add(weakReference);
}
}
if (toRemove.Any())
{
OpsLockUnloaded.EnterWriteLock();
try
{
foreach (WeakReference<MemoryFileAssemblyContextLoader> reference in toRemove)
{
UnloadingACLs.Remove(reference);
}
}
finally
{
OpsLockUnloaded.ExitWriteLock();
}
}
isUnloaded = !UnloadingACLs.Any();
}
finally
{
OpsLockUnloaded.ExitUpgradeableReadLock();
}
return isUnloaded;
}
/// <summary>
/// Tries to retrieve the LoadedACL with the given ID or null if none is found.
/// WARNING: External references to this ACL with long lifespans should be kept in a WeakReference
/// to avoid causing unloading/disposal issues.
/// </summary>
/// <param name="id">GUID of the ACL.</param>
/// <param name="acl">The found ACL or null if none was found.</param>
/// <returns>Whether or not an ACL was found.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public bool TryGetACL(Guid id, out LoadedACL acl)
{
acl = null;
OpsLockLoaded.EnterReadLock();
try
{
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id))
return false;
acl = LoadedACLs[id];
return true;
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
/// <summary>
/// Gets or creates an AssemblyCtxLoader for the given ID. Creates if the ID is empty or no ACL can be found.
/// [IMPORTANT] After calling this method, the id you use should be taken from the acl container (acl.Id).
/// </summary>
/// <param name="id"></param>
/// <param name="friendlyName">A non-unique name for later reference. Optional.</param>
/// <param name="acl"></param>
/// <returns>Should only return false if an error occurs.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private bool GetOrCreateACL(Guid id, string friendlyName, out LoadedACL acl)
{
OpsLockLoaded.EnterUpgradeableReadLock();
try
{
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id) || LoadedACLs[id] is null)
{
OpsLockLoaded.EnterWriteLock();
try
{
id = Guid.NewGuid();
acl = new LoadedACL(id, this, friendlyName);
LoadedACLs[id] = acl;
return true;
}
finally
{
OpsLockLoaded.ExitWriteLock();
}
}
else
{
acl = LoadedACLs[id];
return true;
}
}
catch(Exception e)
{
this.OnException?.Invoke($"{nameof(GetOrCreateACL)}Error: {e.Message}", e);
acl = null;
return false;
}
finally
{
OpsLockLoaded.ExitUpgradeableReadLock();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private bool DisposeACL(Guid id)
{
OpsLockLoaded.EnterWriteLock();
OpsLockUnloaded.EnterWriteLock();
try
{
if (LoadedACLs.ContainsKey(id) && LoadedACLs[id] == null)
{
if (!LoadedACLs.TryRemove(id, out _))
{
ModUtils.Logging.PrintWarning($"An ACL with the GUID {id.ToString()} was found as null. Unable to remove null ACL entry.");
}
}
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id))
{
return false; // nothing to dispose of
}
var acl = LoadedACLs[id];
foreach (Assembly assembly in acl.Acl.Assemblies)
{
OnAssemblyUnloading?.Invoke(assembly);
}
_subTypesLookupCache.Clear();
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(acl.Acl, true));
acl.Acl.Unload();
acl.ClearACLRef();
OnACLUnload?.Invoke(acl.Id);
return true;
}
catch (Exception e)
{
this.OnException?.Invoke($"{nameof(DisposeACL)}() | Error: {e.Message}", e);
return false;
}
finally
{
OpsLockLoaded.ExitWriteLock();
OpsLockUnloaded.ExitWriteLock();
}
}
internal AssemblyManager()
{
RebuildTypesList();
}
/// <summary>
/// Rebuilds the list of types in the default assembly load context.
/// </summary>
private void RebuildTypesList()
{
try
{
_defaultContextTypes = AssemblyLoadContext.Default.Assemblies
.SelectMany(a => a.GetSafeTypes())
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
_subTypesLookupCache.Clear();
}
catch(ArgumentException ae)
{
this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {ae.Message}", ae);
try
{
// some types must've had duplicate type names, build the list while filtering
Dictionary<string, Type> types = new();
foreach (var type in AssemblyLoadContext.Default.Assemblies.SelectMany(a => a.GetSafeTypes()))
{
try
{
types.TryAdd(type.FullName ?? type.Name, type);
}
catch
{
// ignore, null key exception
}
}
_defaultContextTypes = types.ToImmutableDictionary();
}
catch (Exception e)
{
this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {e.Message}", e);
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to create list of default assembly types! Default AssemblyLoadContext types searching not available.");
#if DEBUG
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Exception Details :{e.Message} | {e.InnerException}");
#endif
_defaultContextTypes = ImmutableDictionary<string, Type>.Empty;
}
}
}
#endregion
#region Data
private readonly ConcurrentDictionary<string, ImmutableList<Type>> _subTypesLookupCache = new();
private ImmutableDictionary<string, Type> _defaultContextTypes;
private readonly ConcurrentDictionary<Guid, LoadedACL> LoadedACLs = new();
private readonly List<WeakReference<MemoryFileAssemblyContextLoader>> UnloadingACLs= new();
private readonly ReaderWriterLockSlim OpsLockLoaded = new ();
private readonly ReaderWriterLockSlim OpsLockUnloaded = new ();
#endregion
#region TypeDefs
public sealed class LoadedACL
{
public readonly Guid Id;
private ImmutableDictionary<string, Type> _assembliesTypes = ImmutableDictionary<string, Type>.Empty;
public MemoryFileAssemblyContextLoader Acl { get; private set; }
internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName)
{
this.Id = id;
this.Acl = new(manager)
{
FriendlyName = friendlyName
};
}
public ref readonly ImmutableDictionary<string, Type> AssembliesTypes => ref _assembliesTypes;
/// <summary>
/// Warning: For use by the Assembly Manager only! Do not call this method otherwise.
/// </summary>
internal void ClearACLRef()
{
Acl = null;
}
/// <summary>
/// Rebuild the list of types from assemblies loaded in the AsmCtxLoader.
/// </summary>
internal void RebuildTypesList()
{
if (this.Acl is null)
{
ModUtils.Logging.PrintWarning($"{nameof(RebuildTypesList)}() | ACL with GUID {Id.ToString()} is null, cannot rebuild.");
return;
}
ClearTypesList();
try
{
_assembliesTypes = this.Acl.Assemblies
.SelectMany(a => a.GetSafeTypes())
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
}
catch(ArgumentException)
{
// some types must've had duplicate type names, build the list while filtering
Dictionary<string, Type> types = new();
foreach (var type in this.Acl.Assemblies.SelectMany(a => a.GetSafeTypes()))
{
try
{
types.TryAdd(type.FullName ?? type.Name, type);
}
catch
{
// ignore, null key exception
}
}
_assembliesTypes = types.ToImmutableDictionary();
}
}
internal void ClearTypesList()
{
_assembliesTypes = ImmutableDictionary<string, Type>.Empty;
}
}
#endregion
}
public static class AssemblyExtensions
{
/// <summary>
/// Gets all types in the given assembly. Handles invalid type scenarios.
/// </summary>
/// <param name="assembly">The assembly to scan</param>
/// <returns>An enumerable collection of types.</returns>
public static IEnumerable<Type> GetSafeTypes(this Assembly assembly)
{
// Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException re)
{
try
{
return re.Types.Where(x => x != null)!;
}
catch (InvalidOperationException)
{
return new List<Type>();
}
}
catch (Exception)
{
return new List<Type>();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,22 +0,0 @@
using System;
namespace Barotrauma;
public interface IAssemblyPlugin : IDisposable
{
/// <summary>
/// Called on plugin normal, use this for basic/core loading that does not rely on any other modded content.
/// </summary>
void Initialize();
/// <summary>
/// Called once all plugins have been loaded. if you have integrations with any other mod, put that code here.
/// </summary>
void OnLoadCompleted();
/// <summary>
/// Called before Barotrauma initializes vanilla content. WARNING: This method may be called before Initialize()!
/// </summary>
void PreInitPatching();
}
@@ -1,340 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
// ReSharper disable ConditionIsAlwaysTrueOrFalse
[assembly: InternalsVisibleTo("CompiledAssembly")]
namespace Barotrauma;
/// <summary>
/// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution.
/// [IMPORTANT] Only supports 1 in-memory compiled assembly at a time. Use more instances if you need more.
/// [IMPORTANT] All file assemblies required for the compilation of syntax trees should be loaded first.
/// </summary>
public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
{
// public
public string FriendlyName { get; set; }
// ReSharper disable MemberCanBePrivate.Global
public Assembly CompiledAssembly { get; private set; }
public byte[] CompiledAssemblyImage { get; private set; }
// ReSharper restore MemberCanBePrivate.Global
// internal
private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new(); // path-folder, resolver
protected bool IsResolving; //this is to avoid circular dependency lookup.
private AssemblyManager _assemblyManager;
public bool IsTemplateMode { get; set; }
public bool IsDisposed { get; private set; }
public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true)
{
this._assemblyManager = assemblyManager;
this.IsDisposed = false;
base.Unloading += OnUnload;
}
/// <summary>
/// Try to load the list of disk-file assemblies.
/// </summary>
/// <param name="assemblyFilePaths">Operation success or failure reason.</param>
public AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable<string> assemblyFilePaths)
{
if (assemblyFilePaths is null)
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(LoadFromFiles)}() | The supplied filepath list is null.");
foreach (string filepath in assemblyFilePaths)
{
// path verification
if (filepath.IsNullOrWhiteSpace())
continue;
string sanitizedFilePath = System.IO.Path.GetFullPath(filepath.CleanUpPath());
string directoryKey = System.IO.Path.GetDirectoryName(sanitizedFilePath);
if (directoryKey is null)
return AssemblyLoadingSuccessState.BadFilePath;
// setup dep resolver if not available
if (!_dependencyResolvers.ContainsKey(directoryKey) || _dependencyResolvers[directoryKey] is null)
{
_dependencyResolvers[directoryKey] = new AssemblyDependencyResolver(sanitizedFilePath); // supply the first assembly to be loaded
}
// try loading the assemblies
try
{
LoadFromAssemblyPath(sanitizedFilePath);
}
// on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected.
catch (ArgumentNullException ane)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ane.Message} | {ane.StackTrace}");
return AssemblyLoadingSuccessState.BadFilePath;
}
catch (ArgumentException ae)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ae.Message} | {ae.StackTrace}");
return AssemblyLoadingSuccessState.BadFilePath;
}
catch (FileLoadException fle)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fle.Message} | {fle.StackTrace}");
return AssemblyLoadingSuccessState.CannotLoadFile;
}
catch (FileNotFoundException fnfe)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fnfe.Message} | {fnfe.StackTrace}");
return AssemblyLoadingSuccessState.NoAssemblyFound;
}
catch (BadImageFormatException bife)
{
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {bife.Message} | {bife.StackTrace}");
return AssemblyLoadingSuccessState.InvalidAssembly;
}
catch (Exception e)
{
#if SERVER
LuaCsLogger.LogError($"Unable to load dependency assembly file at {filepath.CleanUpPath()} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
LuaCsLogger.ShowErrorOverlay($"Unable to load dependency assembly file at {filepath} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
return AssemblyLoadingSuccessState.ACLLoadFailure;
}
}
return AssemblyLoadingSuccessState.Success;
}
/// <summary>
/// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
/// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
/// AssemblyManager class.
/// </summary>
/// <param name="assemblyName">Name of the assembly. Must be supplied for in-memory assemblies.</param>
/// <param name="syntaxTrees">Syntax trees to compile into the assembly.</param>
/// <param name="externMetadataReferences">Metadata to be used for compilation.
/// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
/// images not managed by the AssemblyManager class.</param>
/// <param name="compilationOptions">CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
/// <param name="compilationMessages">Will contain any diagnostic messages for compilation failure.</param>
/// <param name="externFileAssemblyReferences">Additional assemblies located in the FileSystem to build metadata references from.
/// Assemblies here will have duplicates by the same name that are currently loaded filtered out.</param>
/// <returns>Success state of the operation.</returns>
/// <exception cref="ArgumentNullException">Throws exception if any of the required arguments are null.</exception>
public AssemblyLoadingSuccessState CompileAndLoadScriptAssembly(
[NotNull] string assemblyName,
[NotNull] IEnumerable<SyntaxTree> syntaxTrees,
IEnumerable<MetadataReference> externMetadataReferences,
[NotNull] CSharpCompilationOptions compilationOptions,
out string compilationMessages,
IEnumerable<Assembly> externFileAssemblyReferences = null)
{
compilationMessages = "";
if (this.CompiledAssembly is not null)
{
return AssemblyLoadingSuccessState.AlreadyLoaded;
}
var externAssemblyRefs = externFileAssemblyReferences is not null ? externFileAssemblyReferences.ToImmutableList() : ImmutableList<Assembly>.Empty;
var externAssemblyNames = externAssemblyRefs.Any() ? externAssemblyRefs
.Where(a => a.FullName is not null)
.Select(a => a.FullName).ToImmutableHashSet()
: ImmutableHashSet<string>.Empty;
// verifications
if (assemblyName.IsNullOrWhiteSpace())
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied assembly name is null!");
if (syntaxTrees is null)
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied syntax tree is null!");
// add external references
List<MetadataReference> metadataReferences = new();
if (externMetadataReferences is not null)
metadataReferences.AddRange(externMetadataReferences);
// build metadata refs from default where not an in-memory compiled assembly and not the same assembly as supplied.
metadataReferences.AddRange(AssemblyLoadContext.Default.Assemblies
.Where(a =>
{
if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
return false;
if (a.FullName is null)
return true;
return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
})
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
.Union(externAssemblyRefs // add custom supplied assemblies
.Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
).ToList());
ImmutableList<AssemblyManager.LoadedACL> loadedAcls = _assemblyManager.GetAllLoadedACLs().ToImmutableList();
if (loadedAcls.Any())
{
// build metadata refs from ACL assemblies from files/disk.
foreach (AssemblyManager.LoadedACL loadedAcl in loadedAcls)
{
if(loadedAcl?.Acl is null || loadedAcl.Acl.IsTemplateMode || loadedAcl.Acl.IsDisposed)
continue;
metadataReferences.AddRange(loadedAcl.Acl.Assemblies
.Where(a =>
{
if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
return false;
if (a.FullName is null)
return true;
return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
})
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
.Union(externAssemblyRefs // add custom supplied assemblies
.Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
).ToList());
}
// build metadata refs from in-memory images
foreach (var loadedAcl in loadedAcls)
{
if (loadedAcl?.Acl?.CompiledAssemblyImage is null || loadedAcl.Acl.CompiledAssemblyImage.Length == 0)
continue;
metadataReferences.Add(MetadataReference.CreateFromImage(loadedAcl.Acl.CompiledAssemblyImage));
}
}
// Change inaccessible options to allow public access to restricted members
var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
topLevelBinderFlagsProperty?.SetValue(compilationOptions, (uint)1 << 22);
// begin compilation
using var memoryCompilation = new MemoryStream();
// compile, emit
var result = CSharpCompilation.Create(assemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(memoryCompilation);
// check for errors
if (!result.Success)
{
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error);
foreach (Diagnostic diagnostic in failures)
{
compilationMessages += $"\n{diagnostic}";
}
return AssemblyLoadingSuccessState.InvalidAssembly;
}
// read compiled assembly from memory stream into an in-memory assembly & image
memoryCompilation.Seek(0, SeekOrigin.Begin); // reset
try
{
CompiledAssembly = LoadFromStream(memoryCompilation);
CompiledAssemblyImage = memoryCompilation.ToArray();
}
catch (Exception e)
{
#if SERVER
LuaCsLogger.LogError($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
LuaCsLogger.ShowErrorOverlay($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
return AssemblyLoadingSuccessState.CannotLoadFromStream;
}
return AssemblyLoadingSuccessState.Success;
}
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")]
protected override Assembly Load(AssemblyName assemblyName)
{
if (IsResolving)
return null; //circular resolution fast exit.
try
{
IsResolving = true;
// resolve self collection
Assembly ass = this.Assemblies.FirstOrDefault(a =>
a.FullName is not null && a.FullName.Equals(assemblyName.FullName), null);
if (ass is not null)
return ass;
// resolve to local folders
foreach (KeyValuePair<string,AssemblyDependencyResolver> pair in _dependencyResolvers)
{
var asspath = pair.Value.ResolveAssemblyToPath(assemblyName);
if (asspath is null)
continue;
ass = LoadFromAssemblyPath(asspath);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (ass is not null)
return ass;
}
//try resolve against other loaded alcs
ImmutableList<AssemblyManager.LoadedACL> list;
try
{
list = _assemblyManager.UnsafeGetAllLoadedACLs();
}
catch
{
list = ImmutableList<AssemblyManager.LoadedACL>.Empty;
}
if (!list.IsEmpty)
{
foreach (var loadedAcL in list)
{
if (loadedAcL.Acl is null || loadedAcL.Acl.IsTemplateMode || loadedAcL.Acl.IsDisposed)
continue;
try
{
ass = loadedAcL.Acl.LoadFromAssemblyName(assemblyName);
if (ass is not null)
return ass;
}
catch
{
// LoadFromAssemblyName throws, no need to propagate
}
}
}
ass = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
if (ass is not null)
return ass;
}
finally
{
IsResolving = false;
}
return null;
}
private void OnUnload(AssemblyLoadContext alc)
{
CompiledAssembly = null;
CompiledAssemblyImage = null;
_dependencyResolvers.Clear();
_assemblyManager = null;
base.Unloading -= OnUnload;
this.IsDisposed = true;
}
}
@@ -0,0 +1,399 @@
using Microsoft.Xna.Framework;
using Sigil;
using Sigil.NonGeneric;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Barotrauma.LuaCs;
internal static class SigilExtensions
{
/// <summary>
/// Puts a type on the stack, as a <see cref="Type" /> object instead of a
/// runtime type token.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="type">The type to put on the stack.</param>
public static void LoadType(this Emit il, Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
il.LoadConstant(type); // ldtoken
// This converts the type token into a Type object
il.Call(typeof(Type).GetMethod(
name: nameof(Type.GetTypeFromHandle),
bindingAttr: BindingFlags.Public | BindingFlags.Static,
binder: null,
types: new Type[] { typeof(RuntimeTypeHandle) },
modifiers: null));
}
/// <summary>
/// Converts the value on the stack to <see cref="object" />.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="type">The type of the value on the stack.</param>
public static void ToObject(this Emit il, Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
il.DerefIfByRef(ref type);
if (type.IsValueType)
{
il.Box(type);
}
else if (type != typeof(object))
{
il.CastClass<object>();
}
}
/// <summary>
/// Deferences the value on stack if the provided type is ByRef.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="type">The type to check if ByRef.</param>
public static void DerefIfByRef(this Emit il, Type type) => il.DerefIfByRef(ref type);
/// <summary>
/// Deferences the value on stack if the provided type is ByRef.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="type">The type to check if ByRef.</param>
public static void DerefIfByRef(this Emit il, ref Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (type.IsByRef)
{
type = type.GetElementType();
if (type.IsValueType)
{
il.LoadObject(type);
}
else
{
il.LoadIndirect(type);
}
}
}
// Copied from https://github.com/evilfactory/moonsharp/blob/5264656c6442e783f3c75082cce69a93d66d4cc0/src/MoonSharp.Interpreter/Interop/Converters/ScriptToClrConversions.cs#L79-L99
private static MethodInfo GetImplicitOperatorMethod(Type baseType, Type targetType)
{
try
{
return Expression.Convert(Expression.Parameter(baseType, null), targetType).Method;
}
catch
{
if (baseType.BaseType != null)
{
return GetImplicitOperatorMethod(baseType.BaseType, targetType);
}
if (targetType.BaseType != null)
{
return GetImplicitOperatorMethod(baseType, targetType.BaseType);
}
return null;
}
}
/// <summary>
/// Loads a local variable and casts it to the target type.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="value">The value to cast. Must be of type <see cref="object" />.</param>
/// <param name="targetType">The type to cast into.</param>
public static void LoadLocalAndCast(this Emit il, Local value, Type targetType)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (targetType == null) throw new ArgumentNullException(nameof(targetType));
if (value.LocalType != typeof(object))
{
throw new ArgumentException($"Expected local type {typeof(object)}; got {value.LocalType}.", nameof(value));
}
var guid = Guid.NewGuid().ToString("N");
if (targetType.IsByRef)
{
targetType = targetType.GetElementType();
}
// IL: var baseType = value.GetType();
var baseType = il.DeclareLocal(typeof(Type), $"cast_baseType_{guid}");
il.LoadLocal(value);
il.Call(typeof(object).GetMethod("GetType"));
il.StoreLocal(baseType);
// IL: var implicitOperatorMethod = SigilExtensions.GetImplicitOperatorMethod(baseType, <targetType>);
var implicitOperatorMethod = il.DeclareLocal(typeof(MethodInfo), $"cast_implicitOperatorMethod_{guid}");
il.LoadLocal(baseType);
il.LoadType(targetType);
il.Call(typeof(SigilExtensions).GetMethod(nameof(GetImplicitOperatorMethod), BindingFlags.NonPublic | BindingFlags.Static));
il.StoreLocal(implicitOperatorMethod);
// IL: <TargetType> castValue;
var castValue = il.DeclareLocal(targetType, $"cast_castValue_{guid}");
// IL: if (implicitConversionMethod != null)
il.LoadLocal(implicitOperatorMethod);
il.Branch((il) =>
{
// IL: var methodInvokeParams = new object[1];
var methodInvokeParams = il.DeclareLocal(typeof(object[]), $"cast_methodInvokeParams_{guid}");
il.LoadConstant(1);
il.NewArray(typeof(object));
il.StoreLocal(methodInvokeParams);
// IL: methodInvokeParams[0] = value;
il.LoadLocal(methodInvokeParams);
il.LoadConstant(0);
il.LoadLocal(value);
il.StoreElement<object>();
// IL: castValue = (<TargetType>)implicitConversionMethod.Invoke(null, methodInvokeParams);
il.LoadLocal(implicitOperatorMethod);
il.LoadNull(); // first parameter is null because implicit cast operators are static
il.LoadLocal(methodInvokeParams);
il.Call(typeof(MethodInfo).GetMethod("Invoke", new[] { typeof(object), typeof(object[]) }));
if (targetType.IsValueType)
{
il.UnboxAny(targetType);
}
else
{
il.CastClass(targetType);
}
il.StoreLocal(castValue);
},
(il) =>
{
// IL: castValue = (<TargetType>)value;
il.LoadLocal(value);
if (targetType.IsValueType)
{
il.UnboxAny(targetType);
}
else
{
il.CastClass(targetType);
}
il.StoreLocal(castValue);
});
il.LoadLocal(castValue);
}
/// <summary>
/// Emits a call to <see cref="string.Format(string, object[])"/>.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="format">The string format.</param>
/// <param name="args">The local variables passed to string.Format.</param>
public static void FormatString(this Emit il, string format, params Local[] args)
{
if (format == null) throw new ArgumentNullException(nameof(format));
if (args == null) throw new ArgumentNullException(nameof(args));
var guid = Guid.NewGuid().ToString("N");
var listType = typeof(List<>).MakeGenericType(typeof(object));
var list = il.DeclareLocal(listType, $"formatString_list_{guid}");
il.NewObject(listType);
il.StoreLocal(list);
foreach (var arg in args)
{
il.LoadLocal(list);
il.LoadLocal(arg);
il.ToObject(arg.LocalType);
il.CallVirtual(listType.GetMethod("Add", new[] { typeof(object) }));
}
var arr = il.DeclareLocal<object[]>($"formatString_arr_{guid}");
il.LoadLocal(list);
il.CallVirtual(listType.GetMethod("ToArray", new Type[0]));
il.StoreLocal(arr);
il.LoadConstant(format);
il.LoadLocal(arr);
il.Call(typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object[]) }));
}
/// <summary>
/// Emits a call to <see cref="DebugConsole.NewMessage(string, Color?, bool)" />.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="message">The message to print.</param>
public static void NewMessage(this Emit il, string message)
{
var newMessage = typeof(DebugConsole).GetMethod(
name: nameof(DebugConsole.NewMessage),
bindingAttr: BindingFlags.Public | BindingFlags.Static,
binder: null,
types: new Type[] { typeof(string), typeof(Color?), typeof(bool) },
modifiers: null);
il.LoadConstant(message);
il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod());
il.LoadConstant(false);
il.Call(newMessage);
}
/// <summary>
/// Emits a call to <see cref="DebugConsole.NewMessage(string, Color?, bool)" />,
/// using the string on the stack.
/// </summary>
/// <param name="il">The IL emitter.</param>
public static void NewMessage(this Emit il)
{
var newMessage = typeof(DebugConsole).GetMethod(
name: nameof(DebugConsole.NewMessage),
bindingAttr: BindingFlags.Public | BindingFlags.Static,
binder: null,
types: new Type[] { typeof(string), typeof(Color?), typeof(bool) },
modifiers: null);
il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod());
il.LoadConstant(false);
il.Call(newMessage);
}
/// <summary>
/// Emits a <c>foreach</c> loop that iterates over an <see cref="IEnumerable{T}"/> local variable.
/// </summary>
/// <typeparam name="T">The type of elements in the enumerable.</typeparam>
/// <param name="il">The IL emitter.</param>
/// <param name="enumerable">The enumerable.</param>
/// <param name="action">The body of code to run on each iteration.</param>
public static void ForEachEnumerable<T>(this Emit il, Local enumerable, Action<Emit, Local, Sigil.Label> action)
{
if (enumerable == null) throw new ArgumentNullException(nameof(enumerable));
if (action == null) throw new ArgumentNullException(nameof(action));
if (!typeof(IEnumerable<T>).IsAssignableFrom(enumerable.LocalType))
{
throw new ArgumentException($"Expected local type {typeof(IEnumerator<T>)}; got {enumerable.LocalType}.", nameof(enumerable));
}
var guid = Guid.NewGuid().ToString("N");
var enumerator = il.DeclareLocal<IEnumerator<T>>($"forEachEnumerable_enumerator_{guid}");
il.LoadLocal(enumerable);
il.CallVirtual(typeof(IEnumerable<T>).GetMethod("GetEnumerator"));
il.StoreLocal(enumerator);
ForEachEnumerator<T>(il, enumerator, action);
}
/// <summary>
/// Emits a <c>foreach</c> loop that iterates over an <see cref="IEnumerator{T}"/> local variable.
/// </summary>
/// <typeparam name="T">The type of elements in the enumerable.</typeparam>
/// <param name="il">The IL emitter.</param>
/// <param name="enumerator">The enumerator.</param>
/// <param name="action">The body of code to run on each iteration.</param>
public static void ForEachEnumerator<T>(this Emit il, Local enumerator, Action<Emit, Local, Sigil.Label> action)
{
if (enumerator == null) throw new ArgumentNullException(nameof(enumerator));
if (action == null) throw new ArgumentNullException(nameof(action));
if (!typeof(IEnumerator<T>).IsAssignableFrom(enumerator.LocalType))
{
throw new ArgumentException($"Expected local type {typeof(IEnumerator<T>)}; got {enumerator.LocalType}.", nameof(enumerator));
}
var guid = Guid.NewGuid().ToString("N");
var labelLoopStart = il.DefineLabel($"forEach_loopStart_{guid}");
var labelMoveNext = il.DefineLabel($"forEach_moveNext_{guid}");
var labelLeave = il.DefineLabel($"forEach_leave_{guid}");
il.BeginExceptionBlock(out var exceptionBlock);
il.Branch(labelMoveNext); // MoveNext() needs to be called at least once before iterating
il.MarkLabel(labelLoopStart);
// IL: var current = enumerator.Current;
var current = il.DeclareLocal<T>($"forEachEnumerator_current_{guid}");
il.LoadLocal(enumerator);
il.CallVirtual(enumerator.LocalType.GetProperty("Current").GetGetMethod());
il.StoreLocal(current);
action(il, current, labelLeave);
il.MarkLabel(labelMoveNext);
il.LoadLocal(enumerator);
il.CallVirtual(typeof(IEnumerator).GetMethod("MoveNext"));
il.BranchIfTrue(labelLoopStart); // loop if MoveNext() returns true
// IL: finally { enumerator.Dispose(); }
il.BeginFinallyBlock(exceptionBlock, out var finallyBlock);
il.LoadLocal(enumerator);
il.CallVirtual(typeof(IDisposable).GetMethod("Dispose"));
il.EndFinallyBlock(finallyBlock);
il.EndExceptionBlock(exceptionBlock);
il.MarkLabel(labelLeave);
}
/// <summary>
/// Emits a branch that only executes if the last value on the stack
/// is truthy (e.g. non-null references, 1, etc).
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="action">The body of code to run if the value is truthy.</param>
public static void If(this Emit il, Action<Emit> action)
{
if (action == null) throw new ArgumentNullException(nameof(action));
il.Branch(@if: action);
}
/// <summary>
/// Emits a branch that only executes if the last value on the stack
/// is falsy (e.g. null references, 0, etc).
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="action">The body of code to run if the value is falsy.</param>
public static void IfNot(this Emit il, Action<Emit> action)
{
if (action == null) throw new ArgumentNullException(nameof(action));
il.Branch(@else: action);
}
/// <summary>
/// Emits two branches that diverge based on a condition -- analogous
/// to an if-else statement. If either <paramref name="if"/>
/// or <paramref name="else"/> are omitted, it behaves the same as
/// <see cref="If(Emit, Action{Emit})"/>
/// and <see cref="IfNot(Emit, Action{Emit})"/>.
/// </summary>
/// <param name="il">The IL emitter.</param>
/// <param name="if">The body of code to run if the value is truthy.</param>
/// <param name="else">The body of code to run if the value is falsy.</param>
public static void Branch(this Emit il, Action<Emit> @if = null, Action<Emit> @else = null)
{
if (@if == null && @else == null) throw new ArgumentException("At least one of the two branches must be defined.");
var guid = Guid.NewGuid().ToString("N");
var labelEnd = il.DefineLabel($"branch_end_{guid}");
if (@if != null && @else != null)
{
var labelElse = il.DefineLabel($"branch_else_{guid}");
il.BranchIfFalse(labelElse);
@if(il);
il.Branch(labelEnd);
il.MarkLabel(labelElse);
@else(il);
}
else if (@if != null)
{
il.BranchIfFalse(labelEnd);
@if(il);
}
else
{
il.BranchIfTrue(labelEnd);
@else(il);
}
il.MarkLabel(labelEnd);
}
}
@@ -0,0 +1,107 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public class StateMachine<T> where T : Enum
{
private readonly ConcurrentDictionary<T, State<T>> _states;
private State<T> _currentState;
public T CurrentState => _currentState.StateId;
private bool _errorOnSameStateSelected;
private readonly AsyncReaderWriterLock _operationsLock = new();
public StateMachine(bool errorOnSameState, T defaultState, Action<State<T>> onEnter, Action<State<T>> onExit)
{
_errorOnSameStateSelected = errorOnSameState;
_states = new ConcurrentDictionary<T, State<T>>();
var defState = new State<T>(defaultState, onEnter, onExit);
_currentState = defState;
_states[defaultState] = defState;
}
public StateMachine<T> AddState(T stateId, Action<State<T>> onEnter, Action<State<T>> onExit)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (_states.TryGetValue(stateId, out _))
{
ThrowHelper.ThrowArgumentException($"State with id {stateId} already exists.");
}
_states[stateId] = new State<T>(stateId, onEnterState: onEnter, onExitState: onExit);
return this;
}
public StateMachine<T> RemoveState(T stateId)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (EqualityComparer<T>.Default.Equals(stateId, CurrentState))
{
ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot remove.");
}
_states.TryRemove(stateId, out _);
return this;
}
public StateMachine<T> AddOrReplaceState(T oldStateId, T newStateId, Action<State<T>> onEnter, Action<State<T>> onExit)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (EqualityComparer<T>.Default.Equals(oldStateId, CurrentState))
{
ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot replace.");
}
_states[oldStateId] = new State<T>(newStateId, onEnter, onExit);
return this;
}
public StateMachine<T> GotoState(T stateId)
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (EqualityComparer<T>.Default.Equals(stateId, CurrentState))
{
if (_errorOnSameStateSelected)
{
ThrowHelper.ThrowInvalidOperationException($"State with id {stateId} is already selected.");
}
return this;
}
if (!_states.TryGetValue(stateId, out var newState))
{
ThrowHelper.ThrowArgumentNullException($"Target state with id {stateId} does not exist.");
}
_currentState.OnExit();
_currentState = newState;
_currentState.OnEnter();
return this;
}
}
public class State<T> where T : Enum
{
public T StateId;
private Action<State<T>> _onEnter, _onExit;
public State(T stateId, Action<State<T>> onEnterState, Action<State<T>> onExitState)
{
StateId = stateId;
_onEnter = onEnterState;
_onExit = onExitState;
}
public virtual void OnEnter()
{
_onEnter?.Invoke(this);
}
public virtual void OnExit()
{
_onExit?.Invoke(this);
}
}
@@ -0,0 +1,15 @@
using System;
namespace Barotrauma.LuaCs;
public partial interface INetCallback
{
public ushort CallbackId { get; }
}
#if SERVER
public partial interface INetCallback
{
}
#endif
@@ -0,0 +1,37 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.Items.Components;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs;
/// <summary>
/// Provides a deterministic ID for a given <see cref="IDataInfo"/> instance under multiple circumstances, for use with
/// network synchronization.
/// </summary>
internal interface INetworkIdProvider : IService
{
/// <summary>
/// Deterministically generates a GUID for the given parameters.
/// </summary>
/// <param name="instance">The instance.</param>
/// <returns>The GUID for the entity.</returns>
Guid GetNetworkIdForInstance([NotNull] IDataInfo instance);
/// <summary>
/// Deterministically generates a GUID for the given parameters.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="attachedEntity">The <see cref="Entity"/> that this instance is attached to, if any.</param>
/// <typeparam name="TEntity">The entity type, if any.</typeparam>
/// <returns>The GUID for the entity.</returns>
Guid GetNetworkIdForInstance<TEntity>([NotNull] IDataInfo instance, TEntity attachedEntity) where TEntity : Entity;
/// <summary>
/// Deterministically generates a GUID for the given parameters.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="attachedItemComponent">The <see cref="ItemComponent"/> that this instance is attached to, if any.</param>
/// <returns>The GUID for the entity.</returns>
Guid GetNetworkIdForInstance([NotNull] IDataInfo instance, [MaybeNull] ItemComponent attachedItemComponent);
}
@@ -0,0 +1,72 @@
using System;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs;
public interface INetworkSyncVar : IDataInfo
{
/// <summary>
/// Network-synchronized object ID. Used for networking send/receive message events.
/// </summary>
Guid InstanceId { get; }
/// <summary>
/// Sets the <see cref="IEntityNetworkingService"/> that is currently managing this instance. The <see cref="InstanceId"/>
/// is retrieved from here.
/// </summary>
/// <param name="networkingService">The networking service managing this instance or null to deregister.</param>
void SetNetworkOwner(IEntityNetworkingService networkingService);
/// <summary>
/// Synchronization type. See <see cref="NetSync"/> for more information.
/// </summary>
NetSync SyncType { get; }
/// <summary>
/// Permissions needed by clients to send net-events and/or receive net messages.
/// </summary>
ClientPermissions WritePermissions { get; }
/// <summary>
/// Called when an incoming net message has data for this network object, typically from the same entity on another
/// machine.
/// </summary>
/// <param name="message">Wrapper for the internal type: <see cref="IReadMessage"/></param>
void ReadNetMessage(IReadMessage message);
/// <summary>
/// Called when a network send-event involving this entity is triggered. Any data expected to be read by the recipient
/// network object on the other instance(s) should be written to the packet.
/// </summary>
/// <param name="message">Wrapper for the internal type: <see cref="IWriteMessage"/></param>
void WriteNetMessage(IWriteMessage message);
}
/// <summary>
/// Specifies the networking send/receive relationship for network object. Objects implementing this interface are
/// expected to adhere to the contract or de-sync may occur.
/// </summary>
public enum NetSync
{
/// <summary>
/// No network synchronization.
/// </summary>
None,
/// <summary>
/// Both the client and the server have 'send' and 'receive' permissions (limited by <see cref="ClientPermissions"/>). Can also be used to allow two-way communication
/// with the server.
/// </summary>
TwoWay,
/// <summary>
/// Only the host/server has the authority to change this value.
/// </summary>
ServerAuthority,
/// <summary>
/// Only clients (with the required by <see cref="ClientPermissions"/>) may change the value and all value changes are communicated to the server/host.
/// <br/><br/><b>[Important] The host/server will not send the value to other connected clients.</b><br/>
/// Intended to allow clients to send one-way messages to the server.
/// </summary>
ClientOneWay
}
@@ -0,0 +1,41 @@
using System;
using Barotrauma.Items.Components;
using Barotrauma.LuaCs.Data;
using System.Security.Cryptography;
using System.Text;
namespace Barotrauma.LuaCs;
internal class NetworkingIdProvider : INetworkIdProvider
{
public void Dispose()
{
//stateless service
}
public bool IsDisposed => false;
private Guid GetNetworkIdFromStringMd5(string id)
{
return new Guid(MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(id)));
}
public Guid GetNetworkIdForInstance(IDataInfo instance)
{
var str = $"{instance.OwnerPackage.Name}.{instance.InternalName}";
return GetNetworkIdFromStringMd5(str);
}
public Guid GetNetworkIdForInstance<TEntity>(IDataInfo instance, TEntity attachedEntity) where TEntity : Entity
{
var str = $"{nameof(TEntity)}({attachedEntity.ID}).{instance.OwnerPackage.Name}.{instance.InternalName}";
return GetNetworkIdFromStringMd5(str);
}
public Guid GetNetworkIdForInstance(IDataInfo instance, ItemComponent attachedItemComponent)
{
var attachedEntity = attachedItemComponent.Item;
var str = $"{attachedEntity.GetType().Name}({attachedEntity.ID}).ComponentId({attachedEntity.Components.IndexOf(attachedItemComponent)}).{instance.OwnerPackage.Name}.{instance.InternalName}";
return GetNetworkIdFromStringMd5(str);
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.LuaCs;
namespace Barotrauma
{
@@ -8,9 +9,11 @@ namespace Barotrauma
public abstract class ACsMod : IAssemblyPlugin
{
private static List<ACsMod> mods = new List<ACsMod>();
[Obsolete("$This does nothing. Stop using it!")]
public static List<ACsMod> LoadedMods { get => mods; }
private const string MOD_STORE = "LocalMods/.modstore";
[Obsolete("$This does nothing. Stop using it!")]
public static string GetStoreFolder<T>() where T : ACsMod
{
if (!Directory.Exists(MOD_STORE)) Directory.CreateDirectory(MOD_STORE);
@@ -19,14 +22,7 @@ namespace Barotrauma
return modFolder;
}
public bool IsDisposed { get; private set; }
/// Mod initialization
public ACsMod()
{
IsDisposed = false;
LoadedMods.Add(this);
}
public bool IsDisposed { get; private set; } = false;
/// <summary>
/// Called as soon as plugin loading begins, use this for internal setup only.
@@ -52,10 +48,8 @@ namespace Barotrauma
}
catch (Exception e)
{
LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod);
LuaCsSetup.Instance.Logger.HandleException(e);
}
LoadedMods.Remove(this);
IsDisposed = true;
}
@@ -0,0 +1,721 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using Barotrauma.Extensions;
using Barotrauma.LuaCs;
using Microsoft.CodeAnalysis;
using FluentResults;
using FluentResults.LuaCs;
using Microsoft.CodeAnalysis.CSharp;
using OneOf;
using Path = System.IO.Path;
[assembly: InternalsVisibleTo(IAssemblyLoaderService.InternalsAwareAssemblyName)]
namespace Barotrauma.LuaCs;
public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
{
public class Factory : IAssemblyLoaderService.IFactory
{
public IAssemblyLoaderService CreateInstance(IAssemblyLoaderService.LoaderInitData initData)
{
return new AssemblyLoader(initData);
}
public void Dispose()
{
//stateless service
}
public bool IsDisposed => false;
}
public Guid Id { get; init; }
public ContentPackage OwnerPackage { get; private set; }
public bool IsReferenceOnlyMode { get; init; }
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
private int _isDisposed;
/// <summary>
/// This bool-int wrapper increments/decrements when set as true/false respectively and return true if the value > 0.
/// </summary>
private bool AreOperationRunning
{
get => Interlocked.CompareExchange(ref _operationsRunning, 0, 0) > 0;
set // we use the set as our inc/decr
{
if (value)
{
Interlocked.Add(ref _operationsRunning, 1);
}
else
{
Interlocked.Add(ref _operationsRunning, -1);
}
}
}
private int _operationsRunning;
//internal
private readonly Action<IAssemblyLoaderService> _onUnload;
private readonly Func<IAssemblyLoaderService, AssemblyName, Assembly> _onResolvingManaged;
private readonly Func<Assembly, string, IntPtr> _onResolvingUnmanagedDll;
private readonly ConcurrentDictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new();
private readonly ConcurrentDictionary<AssemblyOrStringKey, AssemblyData> _loadedAssemblyData = new();
private readonly ThreadLocal<bool> _isResolving = new(static()=>false); // cyclic resolution exit
private readonly ThreadLocal<bool> _isResolvingNative = new(static () => false);
public AssemblyLoader(IAssemblyLoaderService.LoaderInitData initData)
: base(isCollectible: true, name: initData.Name)
{
Id = initData.InstanceId;
IsReferenceOnlyMode = initData.IsReferenceMode;
this._onUnload = initData.OnUnload;
this._onResolvingManaged = initData.OnResolvingManaged;
this._onResolvingUnmanagedDll = initData.OnResolvingUnmanagedDll;
this.OwnerPackage = initData.OwnerPackage;
base.Unloading += OnUnload;
base.Resolving += OnResolvingManagedAssembly;
base.ResolvingUnmanagedDll += OnResolvingUnmanagedDll;
}
private IntPtr OnResolvingUnmanagedDll(Assembly invokingAssembly, string assemblyName)
{
if (IsDisposed)
return 0;
if (_isResolvingNative.Value)
return 0;
AreOperationRunning = true;
_isResolvingNative.Value = true;
try
{
if (!_dependencyResolvers.IsEmpty)
{
foreach (var resolver in _dependencyResolvers)
{
try
{
var path = resolver.Value.ResolveUnmanagedDllToPath(assemblyName);
if (path.IsNullOrWhiteSpace())
continue;
return base.LoadUnmanagedDllFromPath(path);
}
catch
{
// ignored
continue;
}
}
}
if (_onResolvingUnmanagedDll is not null)
{
try
{
return _onResolvingUnmanagedDll(invokingAssembly, assemblyName);
}
catch
{
// ignored
}
}
return 0;
}
finally
{
AreOperationRunning = false;
_isResolvingNative.Value = false;
}
}
private Assembly OnResolvingManagedAssembly(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
{
if (IsDisposed)
return null;
if (_isResolving.Value)
return null;
if (assemblyLoadContext != this)
return null;
AreOperationRunning = true;
_isResolving.Value = true;
try
{
if (!_dependencyResolvers.IsEmpty)
{
foreach (var resolver in _dependencyResolvers)
{
try
{
var path = resolver.Value.ResolveAssemblyToPath(assemblyName);
if (path.IsNullOrWhiteSpace())
continue;
return assemblyLoadContext.LoadFromAssemblyPath(path);
}
catch
{
// ignored
continue;
}
}
}
if (_onResolvingManaged is not null)
{
try
{
return _onResolvingManaged(this, assemblyName);
}
catch
{
// ignored
}
}
return null;
}
finally
{
AreOperationRunning = false;
_isResolving.Value = false;
}
}
public IEnumerable<MetadataReference> AssemblyReferences
{
get
{
if (IsDisposed || _loadedAssemblyData.IsEmpty)
yield return null;
AreOperationRunning = true;
foreach (var data in _loadedAssemblyData.Values)
{
if (data.AssemblyReference is null)
{
continue;
}
yield return data.AssemblyReference;
}
AreOperationRunning = false;
}
}
public FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths)
{
if (IsDisposed)
return FluentResults.Result.Fail($"Loader is disposed!");
AreOperationRunning = true;
try
{
if (paths.Length == 0)
return FluentResults.Result.Ok();
var res = new FluentResults.Result();
foreach (var path in paths)
{
try
{
var p = Path.GetFullPath(path.CleanUpPath());
if (!_dependencyResolvers.ContainsKey(p))
{
_dependencyResolvers[p] = new AssemblyDependencyResolver(p);
}
}
catch (Exception ex)
{
res = res.WithError(new ExceptionalError(ex)
.WithMetadata(MetadataType.Sources, path));
}
}
if (res.Errors.Any())
return FluentResults.Result.Fail(res.Errors);
return FluentResults.Result.Ok();
}
finally
{
AreOperationRunning = false;
}
}
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
bool compileWithInternalAccess,
ImmutableArray<SyntaxTree> syntaxTrees,
ImmutableArray<MetadataReference> metadataReferences,
CSharpCompilationOptions compilationOptions = null)
{
if (IsDisposed)
return FluentResults.Result.Fail($"Loader is disposed!");
AreOperationRunning = true;
try
{
if (assemblyName.IsNullOrWhiteSpace())
{
return new Result<Assembly>().WithError(new Error($"The name provided is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, syntaxTrees));
}
if (_loadedAssemblyData.ContainsKey(assemblyName))
{
return new Result<Assembly>().WithError(
new Error($"The name provided is already assigned to an assembly!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, syntaxTrees));
}
var compilationAssemblyName = compileWithInternalAccess
? IAssemblyLoaderService.InternalsAwareAssemblyName
: assemblyName;
compilationOptions ??= new CSharpCompilationOptions(
outputKind: OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Release,
concurrentBuild: true,
reportSuppressedDiagnostics: false,
warningLevel: 0,
allowUnsafe: true);
if (!compileWithInternalAccess)
{
typeof(CSharpCompilationOptions)
.GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic)
?.SetValue(compilationOptions,
(uint)1 << 25 // CSharp.BinderFlags.AllowAwaitInUnsafeContext
| (uint)1 << 22 // CSharp.BinderFlags.IgnoreAccessibility
| (uint)1 << 1 // CSharp.BinderFlags.SuppressObsoleteChecks
);
}
using var asmMemoryStream = new MemoryStream();
var result = CSharpCompilation
.Create(compilationAssemblyName, syntaxTrees,
metadataReferences, compilationOptions)
.Emit(asmMemoryStream);
if (!result.Success)
{
StringBuilder sb = new StringBuilder();
foreach (var resultDiagnostic in result.Diagnostics)
{
if (resultDiagnostic.IsWarningAsError || resultDiagnostic.Severity == DiagnosticSeverity.Error)
{
//sb.AppendLine($">>> {resultDiagnostic.GetMessage()} | Location: {resultDiagnostic.Location.SourceTree?.GetLineSpan(resultDiagnostic.Location.SourceSpan)} ");
sb.AppendLine($"\n{resultDiagnostic}");
}
}
var res = new FluentResults.Result().WithError(
new Error($"Package Error: {OwnerPackage.Name}: Compilation failed for assembly {assemblyName}!\n {sb.ToString()}\n")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, syntaxTrees));
return res;
}
asmMemoryStream.Seek(0, SeekOrigin.Begin);
var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray());
_loadedAssemblyData[data.Assembly] = data;
return new Result<Assembly>().WithSuccess($"Compiled assembly {assemblyName} successful.")
.WithValue(data.Assembly);
}
catch (Exception ex)
{
return new FluentResults.Result().WithError(new ExceptionalError(ex)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, assemblyName)
.WithMetadata(MetadataType.Sources, syntaxTrees));
}
finally
{
AreOperationRunning = false;
}
}
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
ImmutableArray<string> additionalDependencyPaths)
{
if (IsDisposed)
return FluentResults.Result.Fail($"Loader is disposed!");
AreOperationRunning = true;
try
{
if (assemblyFilePath.IsNullOrWhiteSpace())
return new Result<Assembly>().WithError(new Error($"The path provided is empty."));
if (additionalDependencyPaths.Any())
{
var r = AddDependencyPaths(additionalDependencyPaths);
if (r.IsFailed)
{
// we have errors, loading may not work.
return FluentResults.Result.Fail(new Error($"Failed to load dependency paths for '{assemblyFilePath}' with paths: {additionalDependencyPaths.Aggregate((s, ac) => $"{ac}| P={s}")}.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, assemblyFilePath))
.WithErrors(r.Errors);
}
}
string sanitizedFilePath = Path.GetFullPath(assemblyFilePath.CleanUpPath());
string directoryKey = Path.GetDirectoryName(sanitizedFilePath);
if (directoryKey is null)
{
return FluentResults.Result.Fail(new Error($"Unable to load assembly: bath file path: {assemblyFilePath}")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, sanitizedFilePath));
}
try
{
var assembly = LoadFromAssemblyPath(sanitizedFilePath);
_loadedAssemblyData[assembly] = new AssemblyData(assembly, assembly.Location);
return new Result<Assembly>().WithSuccess($"Loaded assembly '{assembly.GetName()}'").WithValue(assembly);
}
catch (FileNotFoundException fnfe)
{
// last attempt
try
{
var assemblyName = new AssemblyName(System.IO.Path.GetFileName(sanitizedFilePath));
foreach (var resolver in _dependencyResolvers)
{
try
{
var path = resolver.Value.ResolveAssemblyToPath(assemblyName);
return base.LoadFromAssemblyPath(path);
}
catch
{
continue;
}
}
return GenerateExceptionReturn(fnfe);
}
catch (Exception e)
{
return GenerateExceptionReturn(fnfe);
}
}
catch (Exception e)
{
return GenerateExceptionReturn(e);
}
}
finally
{
AreOperationRunning = false;
}
FluentResults.Result<Assembly> GenerateExceptionReturn<T>(T exception) where T : Exception
{
return FluentResults.Result.Fail<Assembly>(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, assemblyFilePath)
.WithMetadata(MetadataType.ExceptionDetails, exception.Message)
.WithMetadata(MetadataType.StackTrace, exception.StackTrace));
}
}
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName)
{
if (IsDisposed)
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
if (assemblyName.IsNullOrWhiteSpace())
{
return FluentResults.Result.Fail(new Error($"Assembly name is empty.")
.WithMetadata(MetadataType.ExceptionObject, this));
}
AreOperationRunning = true;
try
{
if (_loadedAssemblyData.TryGetValue(assemblyName, out var data))
{
return new Result<Assembly>().WithSuccess(new Success($"Assembly found.")).WithValue(data.Assembly);
}
// search any assemblies that were background loaded and we're unaware of.
foreach (var assembly1 in this.Assemblies.Where(a => !_loadedAssemblyData.ContainsKey(a)))
{
if (assembly1.GetName().FullName == assemblyName)
{
try
{
if (!assembly1.Location.IsNullOrWhiteSpace())
{
_loadedAssemblyData[assembly1] = new AssemblyData(assembly1, assembly1.Location);
}
// we don't have the original byte array so we can't store it.
}
catch (NotSupportedException nse) // dynamic assembly or location property threw
{
// ignored
}
return new Result<Assembly>().WithSuccess(new Success($"Assembly found.")).WithValue(assembly1);
}
}
return FluentResults.Result.Fail(new Error($"Assembly named '{ assemblyName }' not found!"));
}
finally
{
AreOperationRunning = false;
}
}
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies()
{
if (IsDisposed)
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
AreOperationRunning = true;
try
{
return new FluentResults.Result<ImmutableArray<Type>>().WithValue(_loadedAssemblyData
.SelectMany(kvp => kvp.Value.Types).ToImmutableArray());
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
finally
{
AreOperationRunning = false;
}
}
public IEnumerable<Type> UnsafeGetTypesInAssemblies()
{
if (IsDisposed)
yield return null;
AreOperationRunning = true;
try
{
if (_loadedAssemblyData.None())
{
yield return null;
}
else
{
foreach (var assemblyData in _loadedAssemblyData.Values)
{
foreach (var type in assemblyData.Types)
{
yield return type;
}
}
}
}
finally
{
AreOperationRunning = false;
}
}
public Result<Type> GetTypeInAssemblies(string typeName)
{
if (IsDisposed)
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
AreOperationRunning = true;
try
{
if (_loadedAssemblyData.IsEmpty)
return FluentResults.Result.Fail(new Error($"No assemblies loaded!"));
foreach (var assemblyData in _loadedAssemblyData)
{
if (assemblyData.Value.TypesByName.TryGetValue(typeName, out var type))
return new FluentResults.Result<Type>().WithSuccess($"Found type.").WithValue(type);
}
return FluentResults.Result.Fail(new Error($"No matching types found for { typeName }!"));
}
finally
{
AreOperationRunning = false;
}
}
public void Dispose()
{
if (IsDisposed)
return; // we don't want to invoke events twice nor cause strong GC handles.
IsDisposed = true;
this.Unload();
this.DisposeInternal();
GC.SuppressFinalize(this);
}
~AssemblyLoader()
{
this.DisposeInternal();
}
private void OnUnload(AssemblyLoadContext context)
{
// Try to wait for loading ops on other threads if they happen to occur with a timeout.
// This should be an edge, should it even occur.
DateTime timeout = DateTime.Now.AddSeconds(2);
while (timeout > DateTime.Now)
{
if (!AreOperationRunning)
break;
Thread.Sleep(1000/Timing.FixedUpdateRate-1);
}
var wf = new WeakReference<IAssemblyLoaderService>(this);
_onUnload?.Invoke(this);
}
private void DisposeInternal()
{
IsDisposed = true;
base.Resolving -= OnResolvingManagedAssembly;
base.ResolvingUnmanagedDll -= OnResolvingUnmanagedDll;
base.Unloading -= OnUnload;
this._dependencyResolvers.Clear();
this._loadedAssemblyData.Clear();
}
protected override Assembly Load(AssemblyName assemblyName)
{
if (IsDisposed)
return null;
AreOperationRunning = true;
try
{
if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var assembly))
return assembly.Assembly;
return null;
}
catch
{
return null;
}
finally
{
AreOperationRunning = false;
}
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
if (IsDisposed)
return 0;
GCHandle? handle = null;
AreOperationRunning = true;
try
{
if (_loadedAssemblyData.TryGetValue(unmanagedDllName, out var assemblyData))
{
handle = GCHandle.Alloc(assemblyData.Assembly, GCHandleType.Pinned);
nint asmPtr = GCHandle.ToIntPtr(handle.Value);
return asmPtr;
}
}
catch
{
return 0;
}
finally
{
AreOperationRunning = false;
try
{
if (handle.HasValue)
handle.Value.Free();
}
catch
{
// ignored. We just want to ensure that free is called.
}
}
return 0;
}
private readonly record struct AssemblyData
{
public readonly Assembly Assembly;
public readonly OneOf<byte[], string> AssemblyImageOrPath;
public readonly MetadataReference AssemblyReference;
public readonly ImmutableArray<Type> Types;
public readonly ImmutableDictionary<string, Type> TypesByName;
public AssemblyData(Assembly assembly, byte[] assemblyImage)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
AssemblyImageOrPath = assemblyImage ?? throw new ArgumentNullException(nameof(assemblyImage));
AssemblyReference = MetadataReference.CreateFromImage(assemblyImage);
Types = assembly.GetSafeTypes().ToImmutableArray();
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
}
public AssemblyData(Assembly assembly, string path)
{
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
AssemblyImageOrPath = path ?? throw new ArgumentNullException(nameof(path));
AssemblyReference = MetadataReference.CreateFromFile(path);
Types = assembly.GetSafeTypes().ToImmutableArray();
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
}
}
private readonly record struct AssemblyOrStringKey : IEquatable<AssemblyOrStringKey>, IEqualityComparer<AssemblyOrStringKey>
{
public Assembly Assembly { get; init; }
public string AssemblyName { get; init; }
public readonly int HashCode;
public AssemblyOrStringKey(Assembly assembly)
{
if(assembly == null)
throw new ArgumentNullException(nameof(assembly));
Assembly = assembly;
AssemblyName = assembly.GetName().FullName;
if (AssemblyName == null)
throw new ArgumentNullException(nameof(AssemblyName));
HashCode = AssemblyName.GetHashCode();
}
public AssemblyOrStringKey(string assemblyName)
{
if (assemblyName.IsNullOrWhiteSpace())
throw new ArgumentNullException(nameof(assemblyName));
Assembly = null;
AssemblyName = assemblyName;
HashCode = AssemblyName.GetHashCode();
}
public bool Equals(AssemblyOrStringKey x, AssemblyOrStringKey y)
{
if (x.Assembly is not null && y.Assembly is not null)
return x.Assembly == y.Assembly;
return x.AssemblyName == y.AssemblyName;
}
public int GetHashCode(AssemblyOrStringKey obj)
{
return this.HashCode;
}
public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly);
public static implicit operator AssemblyOrStringKey(string name) => new AssemblyOrStringKey(name);
}
}
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using Barotrauma.LuaCs;
using FluentResults;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Barotrauma.LuaCs;
public interface IAssemblyLoaderService : IService
{
public interface IFactory : IService
{
IAssemblyLoaderService CreateInstance(LoaderInitData initData);
}
/// <summary>
/// Constructor record for instancing.
/// </summary>
/// <param name="AssemblyManagementService"></param>
/// <param name="InstanceId"></param>
/// <param name="Name"></param>
/// <param name="IsReferenceMode">Assemblies and Types in this context are for <see cref="MetadataReference"/> only.
/// Execution of assembly data is forbidden.</param>
/// <param name="OwnerPackage"></param>
/// <param name="OnUnload"></param>
/// <param name="OnResolvingManaged"></param>
/// <param name="OnResolvingUnmanagedDll"></param>
public record LoaderInitData(
[Required] Guid InstanceId,
[Required][NotNull] string Name,
[Required] bool IsReferenceMode,
ContentPackage OwnerPackage,
Action<IAssemblyLoaderService> OnUnload,
Func<IAssemblyLoaderService, AssemblyName, Assembly> OnResolvingManaged,
Func<Assembly, string, IntPtr> OnResolvingUnmanagedDll);
/// <summary>
/// ID for this instance.
/// </summary>
Guid Id { get; }
/// <summary>
/// The owner content package.
/// </summary>
ContentPackage OwnerPackage { get; }
/// <summary>
/// Indicates that the assemblies in this load context are metadata references only and not
/// intended for execution.
/// </summary>
bool IsReferenceOnlyMode { get; }
/// <summary>
/// Runtime value of constant <see cref="InternalsAwareAssemblyName"/> for extensibility use.
/// </summary>
public static readonly string InternalsAccessAssemblyName = InternalsAwareAssemblyName;
/// <summary>
/// Name for all runtime-compiled assemblies requiring access to <c>internal</c> assembly components. <seealso cref="InternalsVisibleToAttribute"/>
/// </summary>
public const string InternalsAwareAssemblyName = "InternalsAwareAssembly";
/// <summary>
/// Add additional locations for dependency resolution to use.
/// </summary>
/// <param name="paths"></param>
/// <returns></returns>
public FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths);
/// <summary>
/// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
/// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
/// AssemblyManager class.
/// </summary>
/// <param name="assemblyName"><c>[NotNull]</c>Name reference of the assembly.
/// <para><b>[IMPORTANT]</b> This is used to reference this assembly as the true name will be forced if
/// publicized assemblies are not used (InternalsVisibleTo Attrib).</para>
/// Must be supplied for in-memory assemblies.
/// <para>Must be unique to all other assemblies explicitly loaded using this context.</para></param>
/// <param name="compileWithInternalAccess">Forces the assembly name to <see cref="InternalsAccessAssemblyName"/> and grants access to <c>internal</c>.</param>
/// <param name="syntaxTrees"><c>[NotNull]</c>Syntax trees to compile into the assembly.</param>
/// <param name="metadataReferences">All <c>MetadataReference<c/>s to be used for compilation.
/// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
/// images not managed by the AssemblyManager class.</param>
/// <param name="compilationOptions"><c>[NotNull]</c>CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
/// <para><b>[IMPORTANT]</b>Cannot be null or empty if <see cref="compileWithInternalAccess"/> is false.</para></param>
/// <returns>Success state of the operation.</returns>
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
bool compileWithInternalAccess,
ImmutableArray<SyntaxTree> syntaxTrees,
ImmutableArray<MetadataReference> metadataReferences,
CSharpCompilationOptions compilationOptions = null);
/// <summary>
/// Loads the assembly from the provided location and registers all new paths provided with dependency resolution.
/// </summary>
/// <param name="assemblyFilePath">Absolute path to the managed assembly.</param>
/// <param name="additionalDependencyPaths">Additional paths for dependency resolution.</param>
/// <returns>Success and reference to the assembly if successful.</returns>
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
ImmutableArray<string> additionalDependencyPaths);
/// <summary>
/// Returns the already loaded assembly with the same name.
/// </summary>
/// <param name="assemblyName">Name of the assembly.</param>
/// <returns>Operation success on assembly found and assembly.</returns>
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName);
/// <summary>
/// Gets the list of <c>Type</c>s from loaded assemblies.
/// </summary>
/// <returns></returns>
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies();
/// <summary>
/// Gets the list of <c>Type</c>s from loaded assemblies. Does not create a defensive copy and blocks loading/unloading.
/// </summary>
/// <returns></returns>
public IEnumerable<Type> UnsafeGetTypesInAssemblies();
/// <summary>
/// Returns the first found type given it's fully qualified name.
/// </summary>
/// <param name="typeName"></param>
/// <returns></returns>
public FluentResults.Result<Type> GetTypeInAssemblies(string typeName);
/// <summary>
/// List of loaded assemblies.
/// </summary>
public IEnumerable<Assembly> Assemblies { get; }
public IEnumerable<MetadataReference> AssemblyReferences { get; }
}
@@ -0,0 +1,6 @@
using System;
using Barotrauma.LuaCs.Events;
namespace Barotrauma.LuaCs;
public interface IAssemblyPlugin : IDisposable, IEventPluginPreInitialize, IEventPluginInitialize, IEventPluginLoadCompleted { }
@@ -1,25 +1,27 @@
using System;
using System.ComponentModel;
using System.Xml.Serialization;
using Barotrauma.LuaCs.Data;
namespace Barotrauma;
[Serializable]
public sealed class RunConfig
[Obsolete($"Use {nameof(IModConfigInfo)} instead. This class exists for legacy compatibility only.")]
public sealed class RunConfig : IRunConfig
{
/// <summary>
/// How should scripts be run on the server.
/// </summary>
[XmlElement(ElementName = "Server")]
[DefaultValue("Standard")]
public string Server;
public string Server { get; set; }
/// <summary>
/// How should scripts be run on the client.
/// </summary>
[XmlElement(ElementName = "Client")]
[DefaultValue("Standard")]
public string Client;
public string Client { get; set; }
/// <summary>
/// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency").
@@ -0,0 +1,682 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs;
public sealed partial class ConfigService : IConfigService
{
#region Disposal_Locks_Reset
private readonly AsyncReaderWriterLock _operationLock = new ();
private readonly AsyncReaderWriterLock _settingsByPackageLock = new ();
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public void Dispose()
{
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_logger.LogDebug($"{nameof(ConfigService)}: Disposing.");
_configInfoParserService.Dispose();
_configProfileInfoParserService.Dispose();
if (!_settingsInstances.IsEmpty)
{
foreach (var instance in _settingsInstances)
{
try
{
if (instance.Value is null)
{
continue;
}
_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
// ReSharper disable once AccessToDisposedClosure
sub.OnSettingInstanceDisposed(instance.Value));
instance.Value.Dispose();
}
catch
{
// ignored
continue;
}
}
}
_settingsInstances.Clear();
_instanceFactory.Clear();
_settingsInstancesByPackage.Clear();
_commandsService.Dispose();
_storageService = null;
_logger = null;
_eventService = null;
_configInfoParserService = null;
_configProfileInfoParserService = null;
_commandsService = null;
_infoProvider = null;
}
public FluentResults.Result Reset()
{
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = new FluentResults.Result();
if (!_settingsInstances.IsEmpty)
{
foreach (var instance in _settingsInstances)
{
try
{
if (instance.Value is null)
{
continue;
}
_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
// ReSharper disable once AccessToDisposedClosure
sub.OnSettingInstanceDisposed(instance.Value));
instance.Value.Dispose();
}
catch (Exception e)
{
result.WithError(new ExceptionalError(e));
}
}
}
_settingsInstances.Clear();
_instanceFactory.Clear();
_settingsInstancesByPackage.Clear();
_storageService.PurgeCache();
return result;
}
#endregion
private const string SaveDataFileName = "SettingsData.xml";
// --- Settings
private readonly ConcurrentDictionary<(ContentPackage OwnerPackage, string InternalName), ISettingBase>
_settingsInstances = new();
private readonly ConcurrentDictionary<string, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase>>
_instanceFactory = new();
private readonly ConcurrentDictionary<ContentPackage, ConcurrentBag<ISettingBase>>
_settingsInstancesByPackage = new();
// --- Profiles
private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), IConfigProfileInfo>
_settingsProfiles = new();
private IStorageService _storageService;
private ILoggerService _logger;
private IEventService _eventService;
private IConsoleCommandsService _commandsService;
private ILuaCsInfoProvider _infoProvider;
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> _configInfoParserService;
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> _configProfileInfoParserService;
public ConfigService(ILoggerService logger,
IStorageService storageService,
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> configInfoParserService,
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> configProfileInfoParserService,
IEventService eventService,
IConsoleCommandsService commandsService,
ILuaCsInfoProvider infoProvider)
{
_logger = logger;
_storageService = storageService;
_configInfoParserService = configInfoParserService;
_configProfileInfoParserService = configProfileInfoParserService;
_eventService = eventService;
_commandsService = commandsService;
_infoProvider = infoProvider;
_storageService.UseCaching = false;
InjectCommands(commandsService);
}
private void InjectCommands(IConsoleCommandsService commandsService)
{
commandsService.RegisterCommand("cfg_getvalue", "cfg_getvalue [Content Package] [InternalName] [ValueString]: gets a config value.", (string[] args) =>
{
if (args.Length < 1)
{
_logger.LogError("Please specify the name of the package to set the config.");
return;
}
if (args.Length < 2)
{
_logger.LogError("Please specify the name of the config.");
return;
}
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
if (package == null)
{
_logger.LogError($"Could not find the package {args[0]}!");
return;
}
string internalName = args[1];
if (!TryGetConfig(package, internalName, out ISettingBase setting))
{
_logger.LogError($"Could not get config with name {internalName}");
return;
}
_logger.LogMessage($"config {internalName} value is {setting.GetStringValue()}", Color.Green);
}, getValidArgs: () => new[]
{
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
});
commandsService.RegisterCommand("cfg_setvalue", "cfg_setvalue [Content Package] [InternalName] [ValueString]: sets a config.", (string[] args) =>
{
if (args.Length < 1)
{
_logger.LogError("Please specify the name of the package to set the config.");
return;
}
if (args.Length < 2)
{
_logger.LogError("Please specify the name of the config.");
return;
}
if (args.Length < 3)
{
_logger.LogError("Please specify the value to set the config to.");
return;
}
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
if (package == null)
{
_logger.LogError($"Could not find the package {args[0]}!");
return;
}
string internalName = args[1];
string valueString = args[2];
if (!TryGetConfig(package, internalName, out ISettingBase setting))
{
_logger.LogError($"Could not get config with name {internalName}");
return;
}
if (setting.TrySetSerializedValue(valueString))
{
_logger.LogMessage($"Set config {internalName} value to {valueString}", Color.Green);
if (SaveConfigValue(setting) is { IsFailed: true } res)
{
_logger.LogMessage($"Failed to save new config data to disk. Reasons: {res.ToString()}");
}
}
else
{
_logger.LogError($"Failed to set config value");
}
}, getValidArgs: () => new[]
{
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
});
commandsService.RegisterCommand("cfg_setprofile", "cfg_setprofile [ContentPackage] [InternalProfileName]",
(string[] args) =>
{
if (args.Length < 1 || args[0].IsNullOrWhiteSpace())
{
_logger.LogError("Please specify the name of the package of the profile.");
return;
}
if (args.Length < 2 || args[1].IsNullOrWhiteSpace())
{
_logger.LogError("Please specify the name of the profile.");
return;
}
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0], null);
if (package == null)
{
_logger.LogError($"Could not find the package {args[0]}!");
return;
}
var res = ApplyConfigProfile(package, args[1]);
if (res.IsFailed)
{
_logger.LogError($"Errors while applying profile {args[1]}!");
_logger.LogResults(res);
return;
}
_logger.Log($"Profile {args[1]} applied successfully!", Color.Green);
}, getValidArgs: () => new[]
{
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
}, false);
}
public void RegisterSettingTypeInitializer<T>(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory) where T : class, ISettingBase
{
Guard.IsNotNullOrWhiteSpace(typeIdentifier, nameof(typeIdentifier));
Guard.IsNotNull(settingFactory, nameof(settingFactory));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_instanceFactory.ContainsKey(typeIdentifier))
{
ThrowHelper.ThrowArgumentException($"{nameof(RegisterSettingTypeInitializer)}: The type identifier {typeIdentifier} is already registered.");
}
_instanceFactory[typeIdentifier] = settingFactory;
}
private static ImmutableArray<T> SelectCompatible<T>(ImmutableArray<T> resources) where T : IBaseResourceInfo
{
return resources
.Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform))
.Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget))
.OrderBy(r => r.Optional ? 1 : 0) // optional content last
.ThenBy(r => r.LoadPriority)
.ToImmutableArray();
}
public async Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources)
{
using var lck = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (configResources.IsDefaultOrEmpty)
{
return FluentResults.Result.Ok();
}
var result = new FluentResults.Result();
var taskBuilder = ImmutableArray.CreateBuilder<Task<ImmutableArray<IConfigInfo>>>();
var toProcessErrors = new ConcurrentStack<IError>();
foreach (var resource in SelectCompatible(configResources))
{
taskBuilder.Add(await Task.Factory.StartNew<Task<ImmutableArray<IConfigInfo>>>(async Task<ImmutableArray<IConfigInfo>> () =>
{
var r = await _configInfoParserService.TryParseResourcesAsync(resource);
if (r.IsFailed)
{
toProcessErrors.PushRange(r.Errors.ToArray());
return ImmutableArray<IConfigInfo>.Empty;
}
return r.Value;
}));
}
var taskResults = await Task.WhenAll(taskBuilder.ToImmutable());
if (toProcessErrors.Count > 0)
{
return FluentResults.Result.Fail($"{nameof(LoadConfigsAsync)}: Errors while loading configuration info: ").WithErrors(toProcessErrors.ToArray());
}
var toProcessDocs = taskResults
.Where(tr => !tr.IsDefaultOrEmpty)
.SelectMany(tr => tr)
.Where(icf => icf is not null)
.ToImmutableArray();
var instanceQueue = new Queue<(IConfigInfo configInfo, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase> factory)>();
foreach (var info in toProcessDocs)
{
if (!_instanceFactory.TryGetValue(info.DataType, out var factory))
{
result.WithError(
$"{nameof(LoadConfigsAsync)}: Could not retrieve the instance factory for the data type of '{info.DataType}'!");
continue;
}
if (_settingsInstances.ContainsKey((info.OwnerPackage, info.InternalName)))
{
// duplicate for some reason (ie. double loading). This should never happen.
ThrowHelper.ThrowInvalidOperationException($"{nameof(LoadConfigsAsync)}: A setting for the [ContentPackage].[InternalName] of '[{info.OwnerPackage.Name}].[{info.InternalName}]' already exists!");
}
instanceQueue.Enqueue((info, factory));
}
var toProcessInstanceQueue = new Queue<(IConfigInfo info, ISettingBase instance)>();
while (instanceQueue.TryDequeue(out var instanceFactoryInfo))
{
try
{
toProcessInstanceQueue.Enqueue((instanceFactoryInfo.configInfo, instanceFactoryInfo.factory((this, instanceFactoryInfo.configInfo))));
}
catch (Exception e)
{
result.WithError(
$"{nameof(LoadConfigsAsync)}: Error while instancing setting for '{instanceFactoryInfo.configInfo.OwnerPackage}.{instanceFactoryInfo.configInfo.InternalName}': {e.Message}!");
continue;
}
}
using var settingsLck = await _settingsByPackageLock.AcquireWriterLock(); // block to protect new bag instance creation
while (toProcessInstanceQueue.TryDequeue(out var newInstanceData))
{
_settingsInstances[(newInstanceData.info.OwnerPackage, newInstanceData.info.InternalName)] = newInstanceData.instance;
if (!_settingsInstancesByPackage.TryGetValue(newInstanceData.info.OwnerPackage, out _))
{
_settingsInstancesByPackage[newInstanceData.info.OwnerPackage] = new ConcurrentBag<ISettingBase>();
}
_settingsInstancesByPackage[newInstanceData.info.OwnerPackage].Add(newInstanceData.instance);
result.WithReasons(_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
sub.OnSettingInstanceCreated(newInstanceData.instance)).Reasons);
}
return result;
}
public async Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigResourceInfo> configProfileResources)
{
using var _ = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (configProfileResources.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadConfigsProfilesAsync)}: {nameof(configProfileResources)} is empty.");
}
var result = new FluentResults.Result();
foreach (var resource in SelectCompatible(configProfileResources))
{
var r = await _configProfileInfoParserService.TryParseResourcesAsync(resource);
if (r.IsFailed)
{
result.WithErrors(r.Errors);
continue;
}
foreach (var info in r.Value)
{
if (!_settingsProfiles.TryAdd((info.OwnerPackage, info.InternalName), info))
{
result.WithErrors(r.Errors);
continue;
}
if (info.InternalName.Equals("default", StringComparison.InvariantCultureIgnoreCase))
{
//apply it
foreach (var value in info.ProfileValues)
{
if (_settingsInstances.TryGetValue((info.OwnerPackage, value.SettingName), out var instance))
{
instance.TrySetSerializedValue(value.Element);
}
}
}
}
}
return result;
}
public FluentResults.Result LoadSavedValueForConfig(ISettingBase setting)
{
Guard.IsNotNull(setting, nameof(setting));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not { } saveFileResult)
{
#if DEBUG
return FluentResults.Result.Fail(
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
#endif
return FluentResults.Result.Ok();
}
if (saveFileResult is { IsFailed: true })
{
#if DEBUG
_logger.LogResults(saveFileResult.ToResult());
return FluentResults.Result.Fail(
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
#endif
return FluentResults.Result.Ok();
}
if (saveFileResult.Value.Root is not {} rootElement
|| !string.Equals(rootElement.Name.LocalName, "Configuration", StringComparison.InvariantCultureIgnoreCase))
{
return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Root invalid for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
}
if (rootElement.GetChildElement(XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), StringComparison.InvariantCultureIgnoreCase)
?.GetChildElement(setting.InternalName, StringComparison.InvariantCultureIgnoreCase) is not {} cfgValueElement)
{
#if DEBUG
return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Could not find saved value for setting:[{setting.OwnerPackage.Name}.{setting.InternalName}]");
#endif
return FluentResults.Result.Ok();
}
return FluentResults.Result.OkIf(setting.TrySetSerializedValue(cfgValueElement), new Error($"Failed to set value for [{setting.OwnerPackage.Name}.{setting.InternalName}]"));
}
public FluentResults.Result LoadSavedConfigsValues()
{
ImmutableArray<ISettingBase> cfgValues;
using (var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult())
{
IService.CheckDisposed(this);
cfgValues = _settingsInstances.Select(kvp => kvp.Value).ToImmutableArray();
}
var ret = new FluentResults.Result();
foreach (var settingBase in cfgValues)
{
#if DEBUG
// log in debug only.
ret.WithReasons(LoadSavedValueForConfig(settingBase).Reasons);
#else
LoadSavedValueForConfig(settingBase);
#endif
}
return ret;
}
public FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName));
using var _ = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_settingsProfiles.TryGetValue((package, internalName), out var setting))
{
return FluentResults.Result.Fail($"{nameof(ApplyConfigProfile)}: Could not find profile [{package.Name}.{internalName}]");
}
var result = new FluentResults.Result();
foreach (var profileValue in setting.ProfileValues)
{
if (!_settingsInstances.TryGetValue((package, profileValue.SettingName), out var instance))
{
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Could not find setting [{profileValue.SettingName}]."));
continue;
}
if (!instance.TrySetSerializedValue(profileValue.Element))
{
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Failed to set value for [{profileValue.SettingName}]."));
}
}
return result;
}
public FluentResults.Result SaveConfigValue(ISettingBase setting)
{
XDocument cpCfgValues;
if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not {} saveFileResult)
{
return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Storage Service Failure while trying to load file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
}
// get Configuration
if (saveFileResult.IsFailed)
{
cpCfgValues = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement("Configuration"));
}
else
{
cpCfgValues = saveFileResult.Value;
}
if (cpCfgValues.Root is null || cpCfgValues.Root.Name != "Configuration")
{
return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Bad save file format for setting: [{setting.OwnerPackage.Name}.{setting.InternalName}]");
}
XElement currentTarget = GetOrAddElement(cpCfgValues.Root, XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), name => new XElement(name));
currentTarget = GetOrAddElement(currentTarget, setting.InternalName, name => new XElement(name));
var ret = setting.GetSerializableValue().Match(str =>
{
var tgt = currentTarget.Attribute("Value");
if (tgt is null)
{
var attr = new XAttribute("Value", str);
currentTarget.Add(attr);
}
else
{
tgt.Value = str;
}
return FluentResults.Result.Ok();
},
elem =>
{
currentTarget.ReplaceNodes(new XElement("Value", elem));
return FluentResults.Result.Ok();
});
ret.WithReasons(_storageService.SaveLocalXml(setting.OwnerPackage, SaveDataFileName, cpCfgValues).Reasons);
return ret;
XElement GetOrAddElement(XElement containerElement, string elementName, Func<string, XElement> factory)
{
var element = containerElement.Element(elementName);
if (element is null)
{
element = factory(elementName);
containerElement.Add(element);
}
return element;
}
}
public FluentResults.Result DisposePackageData(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
ConcurrentBag<ISettingBase> toDispose;
using (var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult())
{
if (!_settingsInstancesByPackage.TryRemove(package, out toDispose) || toDispose is null)
{
return FluentResults.Result.Ok();
}
}
var result = new FluentResults.Result();
foreach (var setting in toDispose)
{
result.WithReasons(_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub => sub.OnSettingInstanceDisposed(setting)).Reasons);
try
{
_settingsInstances.TryRemove((setting.OwnerPackage, setting.InternalName), out _);
setting.Dispose();
}
catch (Exception e)
{
result.WithError(new ExceptionalError(e));
}
}
return result;
}
public FluentResults.Result DisposeAllPackageData()
{
return this.Reset();
}
public bool TryGetConfig<T>(ContentPackage package, string internalName, out T instance) where T : ISettingBase
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName));
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var settingsLck =
_settingsByPackageLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
instance = default;
if(!_settingsInstances.TryGetValue((package, internalName), out var inst))
{
return false;
}
if (inst is not T instanceT)
{
return false;
}
instance = instanceT;
return true;
}
}
@@ -0,0 +1,97 @@
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.LuaCs;
internal class ConsoleCommandsService : IConsoleCommandsService
{
private readonly List<DebugConsole.Command> _registeredCommands = new();
public void Dispose()
{
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
foreach (var cmd in _registeredCommands.ToImmutableArray())
{
DebugConsole.Commands.Remove(cmd);
}
_registeredCommands.Clear();
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public void RegisterCommand(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false)
{
IService.CheckDisposed(this);
if (DebugConsole.Commands.Any(cmd => cmd.Names.Contains(name)))
{
LuaCsSetup.Instance.Logger.LogWarning($"Registering console command {name} more than once!");
}
var cmd = new DebugConsole.Command(name, help, onExecute, getValidArgs, isCheat);
_registeredCommands.Add(cmd);
DebugConsole.Commands.Add(cmd);
}
public void AssignOnExecute(string names, Action<string[]> onExecute)
{
var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
if (matchingCommand == null)
{
throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found.");
}
else
{
matchingCommand.OnExecute = onExecute;
}
}
#if SERVER
public void AssignOnClientRequestExecute(string names, Action<Client, Vector2, string[]> onClientRequestExecute)
{
var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
if (matchingCommand == null)
{
throw new Exception("AssignOnClientRequestExecute failed. Command matching the name(s) \"" + names + "\" not found.");
}
else
{
matchingCommand.OnClientRequestExecute = onClientRequestExecute;
}
}
#endif
public void RemoveCommand(string name)
{
IService.CheckDisposed(this);
_registeredCommands.RemoveAll(cmd => cmd.Names.Contains(name));
DebugConsole.Commands.RemoveAll(cmd => cmd.Names.Contains(name));
}
public void RemoveRegisteredCommands()
{
IService.CheckDisposed(this);
foreach (var cmd in _registeredCommands.ToImmutableArray())
{
DebugConsole.Commands.Remove(cmd);
}
_registeredCommands.Clear();
}
}
@@ -0,0 +1,426 @@
using Barotrauma.LuaCs.Events;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.Interpreter;
using OneOf;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Barotrauma.LuaCs;
public partial class EventService : IEventService
{
private readonly record struct TypeStringKey : IEqualityComparer<TypeStringKey>, IEquatable<TypeStringKey>
{
public Type Type { get; init; }
public string TypeName { get; init; }
public readonly int HashCode;
public TypeStringKey(Type type)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
TypeName = type.Name.ToLowerInvariant();
HashCode = TypeName.GetHashCode();
}
public TypeStringKey(string typeName)
{
Type = null;
TypeName = typeName?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(typeName));
HashCode = TypeName.GetHashCode();
}
public bool Equals(TypeStringKey x, TypeStringKey y)
{
if (x.Type is not null && y.Type is not null)
return x.Type == y.Type;
return x.TypeName == y.TypeName;
}
public int GetHashCode(TypeStringKey obj)
{
return obj.HashCode;
}
public static implicit operator TypeStringKey(Type type) => new(type);
public static implicit operator TypeStringKey(string typeName) => new(typeName);
}
private readonly ILoggerService _loggerService;
private readonly ILuaPatcher _luaPatcher;
private readonly AsyncReaderWriterLock _operationsLock = new();
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<OneOf<IEvent, string>, IEvent>> _subscribers = new();
private readonly ConcurrentDictionary<TypeStringKey, (TypeStringKey Event, Func<LuaCsFunc, IEvent> RunnerFactory)> _luaAliasEventFactory = new();
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<TypeStringKey, LuaCsFunc>> _luaLegacyEventsSubscribers = new();
private readonly ConcurrentDictionary<IEventService, IEventService> _subscribedEventDispatchers = new();
#region LifeCycle
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_luaLegacyEventsSubscribers.Clear();
_luaAliasEventFactory.Clear();
_subscribers.Clear();
_luaPatcher.Dispose();
}
private int _isDisposed;
public EventService(ILoggerService loggerService, ILuaPatcher luaPatcher)
{
_loggerService = loggerService;
_luaPatcher = luaPatcher;
}
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public FluentResults.Result Reset()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_luaLegacyEventsSubscribers.Clear();
_luaAliasEventFactory.Clear();
_subscribers.Clear();
_luaPatcher.Reset();
return FluentResults.Result.Ok();
}
#endregion
#region LuaEventSystem
public void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
Guard.IsNotNull(callback, nameof(callback));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
{
var eventSubs = _subscribers.GetOrAdd(eventFunc.Event, key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
eventSubs[identifier] = eventFunc.RunnerFactory(callback);
}
else
{
var eventSubs = _luaLegacyEventsSubscribers.GetOrAdd(eventName, key => new ConcurrentDictionary<TypeStringKey, LuaCsFunc>());
eventSubs[identifier] = callback;
}
}
public void Add(string eventName, LuaCsFunc callback, object owner = null)
{
// random ident, we hope for no conflicts :barodev:.
Add(eventName, Random.Shared.NextInt64().ToString() ,callback);
}
public object Call(string eventName, params object[] args)
{
return Call<object>(eventName, args);
}
[MoonSharpHidden] // Needs to be hidden so Lua doesn't accidentally use this instead of the above
public T Call<T>(string eventName, params object[] args)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubscribers)
|| eventSubscribers.IsEmpty)
{
return default;
}
T returnValue = default;
foreach (var subscriber in eventSubscribers)
{
try
{
object result = subscriber.Value.Invoke(args);
if (result is DynValue luaResult)
{
if (luaResult.Type == DataType.Tuple)
{
bool replaceNil = luaResult.Tuple.Length > 1 && luaResult.Tuple[1].CastToBool();
if (!luaResult.Tuple[0].IsNil() || replaceNil)
{
returnValue = luaResult.ToObject<T>();
}
}
else if (!luaResult.IsNil())
{
returnValue = luaResult.ToObject<T>();
}
}
else
{
returnValue = (T)result;
}
}
catch (Exception e)
{
_loggerService.LogError(e.Message);
#if DEBUG
throw;
#endif
}
}
return returnValue;
}
public void Subscribe<T>(string identifier, IDictionary<string, LuaCsFunc> callbacks) where T : class, IEvent<T>
{
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
Guard.IsNotNull(callbacks, nameof(callbacks));
Guard.IsNotEmpty(callbacks, nameof(callbacks));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var eventSubs = _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
eventSubs[identifier] = T.GetLuaRunner(callbacks);
}
public void Remove(string eventName, string identifier)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
{
if (_subscribers.TryGetValue(eventFunc.Event, out var eventSubs))
{
eventSubs.TryRemove(identifier, out _);
}
}
else
{
if (_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubs))
{
eventSubs.TryRemove(identifier, out _);
}
}
}
public void Unsubscribe(string eventName, string identifier)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!_subscribers.TryGetValue(eventName, out var evtSubscribers))
{
return;
}
evtSubscribers.TryRemove(identifier, out _);
}
public void PublishLuaEvent<T>(LuaCsFunc subscriberRunner) where T : class, IEvent<T>
{
this.PublishEvent<T>(sub => subscriberRunner(sub));
}
public FluentResults.Result RegisterLuaEventAlias<T>(string luaEventName, string targetMethod) where T : class, IEvent<T>
{
Guard.IsNotNullOrWhiteSpace(luaEventName, nameof(luaEventName));
Guard.IsNotNullOrWhiteSpace(targetMethod, nameof(targetMethod));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.ContainsKey(luaEventName))
{
#if DEBUG
ThrowHelper.ThrowInvalidOperationException($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
#endif
return FluentResults.Result.Fail($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
}
var eventRunnerFactory = (LuaCsFunc function) => (IEvent)T.GetLuaRunner(new Dictionary<string, LuaCsFunc>
{
{ targetMethod, function }
});
_luaAliasEventFactory[luaEventName] = (Event: typeof(T), RunnerFactory: eventRunnerFactory);
// create the group
_subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
return FluentResults.Result.Ok();
}
#endregion
public FluentResults.Result Subscribe<T>(T subscriber) where T : class, IEvent<T>
{
Guard.IsNotNull(subscriber, nameof(subscriber));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var eventSubs =
_subscribers.GetOrAdd(typeof(T), (type) => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
if (eventSubs.ContainsKey(subscriber))
{
ThrowHelper.ThrowInvalidOperationException($"{nameof(Subscribe)}: The instance is already registered!");
}
return eventSubs.TryAdd(subscriber, subscriber)
? FluentResults.Result.Ok()
: FluentResults.Result.Fail($"{nameof(Subscribe)}: Failed to add subscriber.");
}
public void Unsubscribe<T>(T subscriber) where T : class, IEvent
{
Guard.IsNotNull(subscriber, nameof(subscriber));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_subscribers.TryGetValue(typeof(T), out var evtSubscribers))
{
return;
}
evtSubscribers.TryRemove(subscriber, out _);
}
public void ClearAllEventSubscribers<T>() where T : class, IEvent
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribers.TryRemove(typeof(T), out _);
}
public void ClearAllSubscribers()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribers.Clear();
}
public FluentResults.Result PublishEvent<T>(Action<T> action) where T : class, IEvent<T>
{
Guard.IsNotNull(action, nameof(action));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_subscribers.TryGetValue(typeof(T), out var subs) || subs.IsEmpty)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
foreach (var sub in subs)
{
try
{
action.Invoke(Unsafe.As<T>(sub.Value));
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
_loggerService.LogError(e.Message);
continue;
}
}
foreach (var dispatchers in _subscribedEventDispatchers.ToImmutableArray())
{
dispatchers.Value.PublishEvent(action);
}
return results;
}
public void AddDispatcherEventService(IEventService eventService)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribedEventDispatchers.TryAdd(eventService, eventService);
}
public void RemoveDispatcherEventService(IEventService eventService)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribedEventDispatchers.TryRemove(eventService, out _);
}
#region LuaPatcherAdapter
public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(identifier, className, methodName, parameterTypes, patch, hookType);
}
public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(identifier, className, methodName, patch, hookType);
}
public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(className, methodName, parameterTypes, patch, hookType);
}
public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(className, methodName, patch, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType)
{
return _luaPatcher.RemovePatch(className, className, methodName, parameterTypes, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType)
{
return _luaPatcher.RemovePatch(className, className, methodName, hookType);
}
public void HookMethod(string identifier, MethodBase method, LuaCsPatch patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null)
{
_luaPatcher.HookMethod(identifier, method, patch, hookType, owner);
}
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(identifier, className, methodName, parameterNames, patch, hookMethodType);
}
public void HookMethod(string identifier, string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(identifier, className, methodName, patch, hookMethodType);
}
public void HookMethod(string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(className, methodName, patch, hookMethodType);
}
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(className, methodName, parameterNames, patch, hookMethodType);
}
#endregion
}
@@ -0,0 +1,416 @@
using Barotrauma.Items.Components;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using Barotrauma.Steam;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using static Barotrauma.ContentPackageManager;
namespace Barotrauma.LuaCs;
[HarmonyPatch]
internal class HarmonyEventPatchesService : ISystem
{
public bool IsDisposed { get; private set; }
public FluentResults.Result Reset()
{
Unpatch();
Patch();
return FluentResults.Result.Ok();
}
private static IEventService _eventService;
private static ILoggerService _loggerService;
private readonly Harmony Harmony;
public HarmonyEventPatchesService(IEventService eventService, ILoggerService loggerService)
{
_eventService = eventService;
_loggerService = loggerService;
Harmony = new Harmony("LuaCsForBarotrauma.Events");
Patch();
}
private void Patch()
{
this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService));
#if SERVER
this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService.Patch_StartGame_End));
#endif
}
private void Unpatch()
{
this.Harmony?.UnpatchSelf();
}
[HarmonyPatch(typeof(CoroutineManager), nameof(CoroutineManager.Update)), HarmonyPostfix]
public static void CoroutineManager_Update_Post()
{
_eventService.PublishEvent<IEventUpdate>(x => x.OnUpdate(CoroutineManager.DeltaTime));
_loggerService.ProcessLogs();
}
#if CLIENT
[HarmonyPatch(typeof(GameSession), nameof(GameSession.StartRound), new Type[]
{
typeof(LevelData), typeof(bool), typeof(SubmarineInfo), typeof(SubmarineInfo)
}), HarmonyPostfix]
public static void GameSession_StartRound_Post()
{
_eventService.PublishEvent<IEventRoundStarted>(x => x.OnRoundStart());
}
#endif
[HarmonyPatch(typeof(GameSession), nameof(GameSession.EndRound)), HarmonyPrefix]
public static void GameSession_EndRound_Pre()
{
_eventService.PublishEvent<IEventRoundEnded>(x => x.OnRoundEnd());
}
[HarmonyPatch(typeof(GameSession), nameof(GameSession.LoadPreviousSave)), HarmonyPrefix]
public static void GameSession_LoadPreviousSave_Pre()
{
_eventService.PublishEvent<IEventRoundEnded>(x => x.OnRoundEnd());
}
[HarmonyPatch(typeof(GameSession), nameof(GameSession.EndMissions)), HarmonyPostfix]
public static void GameSession_EndMission_Post(GameSession __instance)
{
_eventService.PublishEvent<IEventMissionsEnded>(x => x.OnMissionsEnded(__instance.Missions.ToList()));
}
[HarmonyPatch(typeof(Screen), nameof(Screen.Select)), HarmonyPostfix]
public static void Screen_Selected_Post(Screen __instance)
{
_eventService.PublishEvent<IEventScreenSelected>(x => x.OnScreenSelected(__instance));
}
#if CLIENT
[HarmonyPatch(typeof(MainMenuScreen), "StartGame"), HarmonyPostfix]
public static void MainMenuScreen_StartGame_Pre(Screen __instance)
{
LuaCsSetup.Instance.SetRunState(RunState.Running);
}
[HarmonyPatch(typeof(MainMenuScreen), "LoadGame"), HarmonyPostfix]
public static void MainMenuScreen_LoadGame_Pre(Screen __instance)
{
LuaCsSetup.Instance.SetRunState(RunState.Running);
}
[HarmonyPatch(typeof(MutableWorkshopMenu), nameof(MutableWorkshopMenu.Apply)), HarmonyPostfix]
public static void MutableWorkshopMenu_Apply_Post(Screen __instance)
{
LuaCsSetup.Instance.PromptCSharpMods(selection => { }, joiningServer: false);
}
#endif
[HarmonyPatch(typeof(ContentPackageManager.PackageSource), nameof(ContentPackageManager.PackageSource.Refresh)), HarmonyPostfix]
public static void PackageSource_Refresh_Post()
{
_eventService.PublishEvent<IEventAllPackageListChanged>(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages));
}
[HarmonyPatch(typeof(ContentPackageManager), nameof(ContentPackageManager.Init)), HarmonyPostfix]
public static void ContentPackageManager_Init_Post()
{
_eventService.PublishEvent<IEventAllPackageListChanged>(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages));
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
}
[HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetCore)), HarmonyPostfix]
public static void EnabledPackages_SetCore_Post()
{
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
}
[HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetRegular)), HarmonyPostfix]
public static void EnabledPackages_SetRegular_Post()
{
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
}
#if CLIENT
[HarmonyPatch(typeof(GameClient), "ReadDataMessage"), HarmonyPrefix]
public static bool GameClient_ReadDataMessage_Pre(IReadMessage inc)
{
int prevBitPosition = inc.BitPosition;
ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte();
bool? skip = null;
_eventService.PublishEvent<IEventServerRawNetMessageReceived>(x => skip = x.OnReceivedServerNetMessage(inc, header) ?? skip);
if (skip == true)
{
return false;
}
inc.BitPosition = prevBitPosition; // rewind so the game can read the message
return true;
}
[HarmonyPatch(typeof(SubEditorScreen), nameof(SubEditorScreen.Select), new Type[] { }), HarmonyPostfix]
public static void SubEditorScreen_Selected_Post(Screen __instance)
{
_eventService.PublishEvent<IEventScreenSelected>(x => x.OnScreenSelected(__instance));
}
[HarmonyPatch(typeof(PlayerInput), nameof(PlayerInput.Update)), HarmonyPrefix]
public static void PlayerInput_Update_Pre(double deltaTime)
{
_eventService.PublishEvent<IEventKeyUpdate>(x => x.OnKeyUpdate(deltaTime));
}
[HarmonyPatch(typeof(DebugConsole), "IsCommandPermitted"), HarmonyPrefix]
public static bool DebugConsole_IsCommandPermitted(Identifier command, ref bool __result)
{
DebugConsole.Command c = DebugConsole.FindCommand(command.Value);
if (DebugConsole.Commands.IndexOf(c) >= LuaCsSetup.DebugConsoleCommandVanillaIndex)
{
__result = true;
return false;
}
return true;
}
#elif SERVER
[HarmonyPatch(typeof(GameServer), "ReadDataMessage"), HarmonyPrefix]
public static bool GameServer_ReadDataMessage_Pre(NetworkConnection sender, IReadMessage inc)
{
int prevBitPosition = inc.BitPosition;
ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte();
bool? skip = null;
_eventService.PublishEvent<IEventClientRawNetMessageReceived>(x => skip = x.OnReceivedClientNetMessage(inc, header, sender) ?? skip);
if (skip == true)
{
return false;
}
inc.BitPosition = prevBitPosition; // rewind so the game can read the message
return true;
}
[HarmonyPatch(typeof(GameServer), "OnInitializationComplete"), HarmonyPostfix]
public static void GameServer_OnInitializationComplete_Post(GameServer __instance)
{
Client client = __instance.ConnectedClients.LastOrDefault();
if (client == null) { return; }
_eventService.PublishEvent<IEventClientConnected>(x => x.OnClientConnected(client));
}
[HarmonyPatch(typeof(GameServer), nameof(GameServer.DisconnectClient), new Type[] { typeof(Client), typeof(PeerDisconnectPacket) }), HarmonyPrefix]
public static void GameServer_DisconnectClient_Pre(Client client, PeerDisconnectPacket peerDisconnectPacket)
{
if (client == null) { return; }
_eventService.PublishEvent<IEventClientDisconnected>(x => x.OnClientDisconnected(client));
}
[HarmonyPatch(typeof(GameServer), nameof(GameServer.AssignJobs)), HarmonyPostfix]
public static void GameServer_AssignJobs_Post(List<Client> unassigned)
{
_eventService.PublishEvent<IEventJobsAssigned>(x => x.OnJobsAssigned(unassigned));
}
#endif
[HarmonyPatch(typeof(Character), nameof(Character.Create), new[] {
typeof(CharacterPrefab),
typeof(Vector2),
typeof(string),
typeof(CharacterInfo),
typeof(ushort),
typeof(bool),
typeof(bool),
typeof(bool),
typeof(RagdollParams),
typeof(bool)
}), HarmonyPostfix]
public static void Character_Create_Post(Character __result)
{
_eventService.PublishEvent<IEventCharacterCreated>(x => x.OnCharacterCreated(__result));
}
[HarmonyPatch(typeof(Character), "KillProjSpecific"), HarmonyPostfix]
public static void Character_Kill_Post(Character __instance, Affliction causeOfDeathAffliction, CauseOfDeathType causeOfDeath)
{
_eventService.PublishEvent<IEventCharacterDeath>(x => x.OnCharacterDeath(__instance, causeOfDeathAffliction, causeOfDeath));
}
[HarmonyPatch(typeof(Character), nameof(Character.GiveJobItems)), HarmonyPostfix]
public static void Character_GiveJobItems_Post(Character __instance, WayPoint spawnPoint, bool isPvPMode)
{
_eventService.PublishEvent<IEventGiveCharacterJobItems>(x => x.OnGiveCharacterJobItems(__instance, spawnPoint, isPvPMode));
}
[HarmonyPatch(typeof(Character), nameof(Character.DamageLimb)), HarmonyPrefix]
public static bool Character_DamageLimb_Pre(AttackResult __result, Character __instance, Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker, float damageMultiplier, bool allowStacking, float penetration, bool shouldImplode, bool ignoreDamageOverlay, bool recalculateVitality)
{
AttackResult? result = null;
_eventService.PublishEvent<IEventCharacterDamageLimb>(x => result = x.OnCharacterDamageLimb(__instance, worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier, allowStacking, penetration, shouldImplode));
if (result != null)
{
__result = (AttackResult)result;
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Affliction), nameof(Affliction.Update)), HarmonyPostfix]
public static void Affliction_Update_Post(Affliction __instance, CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
_eventService.PublishEvent<IEventAfflictionUpdate>(x => x.OnAfflictionUpdate(__instance, characterHealth, targetLimb, deltaTime));
}
[HarmonyPatch(typeof(Connection), nameof(Connection.SendSignal)), HarmonyPostfix]
public static void Connection_SendSignal_Post(Connection __instance, Signal signal)
{
foreach (var wire in __instance.Wires)
{
Connection recipient = wire.OtherConnection(__instance);
if (recipient == null) { continue; }
_eventService.PublishEvent<IEventSignalReceived>(x => x.OnSignalReceived(signal, recipient));
_eventService.Call("signalReceived." + recipient.Item.Prefab.Identifier, signal, recipient);
}
foreach (CircuitBoxConnection connection in __instance.CircuitBoxConnections)
{
_eventService.PublishEvent<IEventSignalReceived>(x => x.OnSignalReceived(signal, connection.Connection));
_eventService.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection.Connection);
}
}
[HarmonyPatch(typeof(Item), MethodType.Constructor, new Type[] { typeof(Rectangle), typeof(ItemPrefab), typeof(Submarine), typeof(bool), typeof(ushort) }), HarmonyPostfix]
public static void Item_Ctor_Post(Item __instance)
{
_eventService.PublishEvent<IEventItemCreated>(x => x.OnItemCreated(__instance));
}
[HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix]
public static void Item_Remove_Post(Item __instance)
{
_eventService.PublishEvent<IEventItemRemoved>(x => x.OnItemRemoved(__instance));
}
[HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix]
public static void Item_ShallowRemove_Post(Item __instance)
{
_eventService.PublishEvent<IEventItemRemoved>(x => x.OnItemRemoved(__instance));
}
[HarmonyPatch(typeof(Item), nameof(Item.Use)), HarmonyPrefix]
public static bool Item_Use_Pre(Item __instance, Character user, Limb targetLimb, Entity useTarget)
{
if (__instance.RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
{
return true;
}
if (__instance.Condition <= 0.0f) { return true; }
bool? result = null;
_eventService.PublishEvent<IEventItemUse>(x => result = x.OnItemUsed(__instance, user, targetLimb, useTarget));
if (result == true)
{
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Item), nameof(Item.SecondaryUse)), HarmonyPrefix]
public static bool Item_SecondaryUse_Pre(Item __instance, Character character)
{
if (__instance.Condition <= 0.0f) { return true; }
bool? result = null;
_eventService.PublishEvent<IEventItemSecondaryUse>(x => result = x.OnItemSecondaryUsed(__instance, character));
if (result == true)
{
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Inventory), "PutItem"), HarmonyPrefix]
public static bool Inventory_PutItem_Prefix(Inventory __instance, Item item, int i, Character user, bool removeItem)
{
bool? result = null;
_eventService.PublishEvent<IEventInventoryPutItem>(x => result = x.OnInventoryPutItem(__instance, item, user, i, removeItem));
if (result == true)
{
return false; // skip
}
return true;
}
[HarmonyPatch(typeof(Inventory), "TrySwapping"), HarmonyPrefix]
public static bool Inventory_TrySwapping_Prefix(Inventory __instance, Item item, int index, Character user, bool swapWholeStack, ref bool __result)
{
// uncomment when we are plugin
// if (item?.ParentInventory == null || !__instance.slots[index].Any()) { return false; }
// if (__instance.slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; }
if (!__instance.AllowSwappingContainedItems) { return false; }
bool? result = null;
_eventService.PublishEvent<IEventInventoryItemSwap>(x => result = x.OnInventoryItemSwap(__instance, item, user, index, swapWholeStack));
if (result != null)
{
__result = (bool)result;
return false; // skip
}
return true;
}
public void Dispose()
{
IsDisposed = true;
this.Harmony?.UnpatchSelf();
}
#if SERVER
[HarmonyPatch]
class Patch_StartGame_End
{
static MethodBase TargetMethod()
{
var original = AccessTools.Method(
typeof(GameServer),
"StartGame"
);
return AccessTools.EnumeratorMoveNext(original);
}
[HarmonyPostfix]
static void Postfix(object __instance, bool __result)
{
if (!__result) { return; }
var enumerator = __instance as IEnumerator<CoroutineStatus>;
if (enumerator == null) { return; }
if (enumerator.Current == CoroutineStatus.Success)
{
_eventService.PublishEvent<IEventRoundStarted>(x => x.OnRoundStart());
}
}
}
#endif
}
@@ -0,0 +1,218 @@
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using FluentResults;
using HarmonyLib;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Barotrauma.LuaCs;
public partial class LoggerService : ILoggerService
{
private List<ILoggerSubscriber> logSubscribers = [];
private ConcurrentQueue<PendingLog> logQueue = [];
#if SERVER
private const string TargetPrefix = "[SV]";
private const int NetMaxLength = 1024; // character limit of vanilla Barotrauma's chat system.
private const int NetMaxMessages = 60;
// This is used so it's possible to call logging functions inside the serverLog
// hook without creating an infinite loop
private bool _isInsideLogCall = false;
#else
private const string TargetPrefix = "[CL]";
#endif
public LoggerService() { }
public void Subscribe(ILoggerSubscriber subscriber)
{
logSubscribers.Add(subscriber);
}
public void Unsubscribe(ILoggerSubscriber subscriber)
{
logSubscribers.Remove(subscriber);
}
public void ProcessLogs()
{
while (logQueue.TryDequeue(out PendingLog log))
{
logSubscribers.ForEach(s => s.OnLog(log));
DebugConsole.NewMessage(log.Message, log.Color);
#if SERVER
if (GameMain.Server != null)
{
if (GameMain.Server.ServerSettings.SaveServerLogs)
{
string logMessage = "[LuaCs] " + log.Message;
GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, log.MessageType, false);
if (!_isInsideLogCall)
{
_isInsideLogCall = true;
LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(logMessage, log.MessageType));
_isInsideLogCall = false;
}
}
for (int i = 0; i < log.Message.Length; i += NetMaxLength)
{
string subStr = log.Message.Substring(i, Math.Min(1024, log.Message.Length - i));
BroadcastMessage(subStr);
}
}
void BroadcastMessage(string m)
{
foreach (var client in GameMain.Server.ConnectedClients)
{
ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: log.Color);
GameMain.Server.SendDirectChatMessage(consoleMessage, client);
if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog))
{
continue;
}
ChatMessage logMessage = ChatMessage.Create(log.MessageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null);
GameMain.Server.SendDirectChatMessage(logMessage, client);
}
}
#endif
}
}
public void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
{
if (LuaCsSetup.Instance.HideUserNamesInLogs && !Environment.UserName.IsNullOrEmpty())
{
message = message.Replace(Environment.UserName, "USERNAME");
}
message = $"{TargetPrefix} {message}";
logQueue.Enqueue(new PendingLog(message, color, messageType));
}
public void LogError(string message)
{
Log($"{message}", Color.Red, ServerLog.MessageType.Error);
}
public void LogWarning(string message)
{
Log($"{message}", Color.Yellow, ServerLog.MessageType.ServerMessage);
}
public void LogMessage(string message, Color? serverColor = null, Color? clientColor = null)
{
serverColor ??= Color.MediumPurple;
clientColor ??= Color.Purple;
#if SERVER
Log(message, serverColor);
#else
Log(message, clientColor);
#endif
}
public void HandleException(Exception exception, string prefix = null)
{
string errorString = "";
switch (exception)
{
case NetRuntimeException netRuntimeException:
if (netRuntimeException.DecoratedMessage == null)
{
errorString = $"{prefix ?? ""}{netRuntimeException.ToString()}";
}
else
{
// FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace...
errorString = $"{prefix ?? ""}{netRuntimeException.DecoratedMessage}: {netRuntimeException}";
}
break;
case InterpreterException interpreterException:
if (interpreterException.DecoratedMessage == null)
{
errorString = $"{prefix ?? ""}{interpreterException.ToString()}";
}
else
{
errorString = $"{prefix ?? ""}{interpreterException.DecoratedMessage}";
}
break;
default:
string s = exception.StackTrace != null ? exception.ToString() : $"{exception}\n{Environment.StackTrace}";
errorString = $"{prefix ?? ""}{s}";
break;
}
LogError(prefix + Environment.UserName + " " + errorString);
}
public void LogResults(FluentResults.Result result)
{
if (result == null)
{
LogError("Result is null");
return;
}
if (result.IsSuccess)
{
return;
}
if (result.IsFailed)
{
foreach (var error in result.Errors)
{
if (error is ExceptionalError exceptionalError)
{
HandleException(exceptionalError.Exception);
}
else
{
LogError($"FluentResults::IError: {error.Message}");
/*if (error.Reasons != null)
{
foreach (var reason in error.Reasons)
{
LogError($" - {reason.Message}");
}
}*/
}
}
}
}
public void LogDebug(string message, Color? color = null)
{
Log(message, color ?? Color.Purple);
}
public void LogDebugWarning(string message)
{
Log(message, Color.Yellow);
}
public void LogDebugError(string message)
{
Log(message, Color.Red);
}
public void Dispose() { }
public FluentResults.Result Reset() => FluentResults.Result.Ok();
public bool IsDisposed { get; }
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.LuaCs;
public sealed class LuaCsInfoProvider : ILuaCsInfoProvider
{
public void Dispose()
{
// stateless service
}
public bool IsDisposed => false;
public bool IsCsEnabled => LuaCsSetup.Instance.IsCsEnabled;
public bool HideUserNamesInLogs => LuaCsSetup.Instance.HideUserNamesInLogs;
public bool UseCaching => LuaCsSetup.Instance.UseCaching;
public RunState CurrentRunState => LuaCsSetup.Instance.CurrentRunState;
public ContentPackage LuaCsForBarotraumaPackage
{
get
{
return ContentPackageManager.EnabledPackages.Regular.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName), null)
?? ContentPackageManager.LocalPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName))
?? ContentPackageManager.WorkshopPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName));
}
}
}
@@ -0,0 +1,670 @@
#nullable enable
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using FluentResults;
using Microsoft.CodeAnalysis;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Barotrauma.LuaCs;
class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService, IEventAssemblyUnloading
{
public Script? InternalScript => _script;
private Script? _script;
private bool _isRunning;
[MemberNotNullWhen(true, nameof(_script))]
public bool IsRunning => _isRunning;
private List<ILuaScriptResourceInfo> _resourcesInfo = new List<ILuaScriptResourceInfo>();
private readonly AsyncReaderWriterLock _operationsLock = new ();
private readonly ILuaUserDataService _userDataService;
private readonly ISafeLuaUserDataService _safeUserDataService;
private readonly ILuaScriptLoader _luaScriptLoader;
private readonly ILuaScriptServicesConfig _luaScriptServicesConfig;
private readonly ILoggerService _loggerService;
private readonly LuaGame _luaGame;
private readonly IEventService _eventService;
private readonly ILuaCsTimer _luaCsTimer;
private readonly IDefaultLuaRegistrar _defaultLuaRegistrar;
private readonly IPluginManagementService _pluginManagementService;
private readonly INetworkingService _networkingService;
private readonly IConsoleCommandsService _commandsService;
private readonly ILuaConfigService _configService;
private readonly ILuaCsInfoProvider _luaCsInfoProvider;
private readonly Lazy<IPackageManagementService> _packageManagementService;
//private readonly ILuaCsUtility _luaCsUtility;
public LuaScriptManagementService(
ILoggerService loggerService,
ILuaScriptLoader loader,
ILuaUserDataService userDataService,
ISafeLuaUserDataService safeUserDataService,
IDefaultLuaRegistrar defaultLuaRegistrar,
ILuaScriptServicesConfig luaScriptServicesConfig,
IPluginManagementService pluginManagementService,
INetworkingService networkingService,
LuaGame luaGame,
IEventService eventService,
//ILuaCsUtility luaCsUtility,
ILuaCsTimer luaCsTimer,
IConsoleCommandsService commandsService,
ILuaCsInfoProvider luaCsInfoProvider,
ILuaConfigService configService,
Lazy<IPackageManagementService> packageManagementService)
{
_luaScriptLoader = loader;
_userDataService = userDataService;
_safeUserDataService = safeUserDataService;
_defaultLuaRegistrar = defaultLuaRegistrar;
_luaScriptServicesConfig = luaScriptServicesConfig;
_loggerService = loggerService;
_pluginManagementService = pluginManagementService;
_networkingService = networkingService;
_luaGame = luaGame;
_eventService = eventService;
_commandsService = commandsService;
_luaCsInfoProvider = luaCsInfoProvider;
_configService = configService;
_packageManagementService = packageManagementService;
_luaCsTimer = luaCsTimer;
RegisterLuaEvents();
RegisterConsoleCommands(_commandsService);
}
private void RegisterConsoleCommands(IConsoleCommandsService commands)
{
#if CLIENT
commands.RegisterCommand("cl_reloadlua|cl_reloadcs|cl_reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) =>
{
LuaCsSetup.Instance.EventService.PublishEvent<IEventReloadAllPackages>(sub => sub.OnReloadAllPackages());
});
commands.RegisterCommand("cl_lua", $"cl_lua: Runs a string on the client.", (string[] args) =>
{
if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands))
{
DebugConsole.ThrowError("Command not permitted.");
return;
}
if (LuaCsSetup.Instance.CurrentRunState != RunState.Running)
{
DebugConsole.ThrowError("LuaCs not initialized, use the console command cl_reloadluacs to force initialization.");
return;
}
var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args));
LuaCsSetup.Instance.Logger.LogResults(result.ToResult());
});
commands.RegisterCommand("cl_toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) =>
{
DebugConsole.Log($"This command is currently not implemented. Please open a github issue if you need this feature.");
/*int port = 41912;
if (args.Length > 0)
{
int.TryParse(args[0], out port);
}
throw new NotImplementedException();
//GameMain.LuaCs.ToggleDebugger(port);*/
});
#elif SERVER
commands.RegisterCommand("lua", "lua: Runs a string.", (string[] args) =>
{
var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args));
LuaCsSetup.Instance.Logger.LogResults(result.ToResult());
});
commands.RegisterCommand("reloadlua|reloadcs|reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) =>
{
LuaCsSetup.Instance.EventService.PublishEvent<IEventReloadAllPackages>(sub => sub.OnReloadAllPackages());
});
commands.RegisterCommand("toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) =>
{
int port = 41912;
if (args.Length > 0)
{
int.TryParse(args[0], out port);
}
throw new NotImplementedException();
//GameMain.LuaCs.ToggleDebugger(port);
});
#endif
#if SERVER
commands.RegisterCommand("install_cl_lua|install_cl|install_cl_cs|install_cl_luacs", "Installs Client-Side LuaCs into your client.", (string[] args) =>
{
LuaCsInstaller.Install();
});
#endif
}
public bool IsDisposed { get; private set; }
public void SetCachingPolicy(bool useCaching)
{
_luaScriptLoader?.SetCachingPolicy(useCaching);
}
public async Task<FluentResults.Result> LoadScriptResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourcesInfo)
{
if (!_luaCsInfoProvider.UseCaching)
{
return FluentResults.Result.Ok();
}
// Do any exception checks you can before acquiring a lock to avoid needlessly holding up resources.
if (resourcesInfo.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadScriptResourcesAsync)}: The parameter is empty!");
}
// Acquire a lock:
// Reader = Allow parallel operations (try to avoid nesting acquiring the lock when possible)
// Writer = Exclusive use (ie. executing scripts or Dispose())
using var lck = await _operationsLock.AcquireWriterLock(); // IDisposable using with generate a try-finally and release for you.
IService.CheckDisposed(this); // Check disposed after you have the lock
// If you use a ConcurrentDictionary instead of a List, it will handle threading issues for you.
_resourcesInfo.AddRange(resourcesInfo.OrderBy(static r => r.LoadPriority));
// Use the StorageService's caching function by just loading the file with caching turned on.
// Right now the LuaScriptLoader has this on by default.
var cacheRes = await _luaScriptLoader.CacheResourcesAsync(resourcesInfo);
// Aggregate and return results to the caller to deal with. Optionally, log here if you want.
// Automatically converted to a Task<T> when 'async' is in the method declaration.
if (cacheRes.IsFailed)
{
return cacheRes.ToResult();
}
return new FluentResults.Result().WithReasons(cacheRes.Value.SelectMany(cr => cr.Item2.Reasons));
}
public FluentResults.Result<DynValue> DoString(string code)
{
IService.CheckDisposed(this);
if (_script == null || !IsRunning) { throw new Exception("Disposed"); }
try
{
var result = _script.DoString(code);
return FluentResults.Result.Ok(result);
}
catch (Exception ex)
{
return FluentResults.Result.Fail(new ExceptionalError(ex));
}
}
private DynValue DoFile(string file, Table? globalContext = null, string? codeStringFriendly = null)
{
if (_script == null)
{
throw new Exception("Not running");
}
if (!LuaCsFile.CanReadFromPath(file))
{
// TODO: Replace with LuaScriptLoader IsFileAccessible.
throw new ScriptRuntimeException($"dofile: File access to {file} not allowed.");
}
if (!LuaCsFile.Exists(file))
{
// TODO: Replace with LuaScriptLoader IsFileAccessible.
throw new ScriptRuntimeException($"dofile: File {file} not found.");
}
return _script.DoFile(file, globalContext, codeStringFriendly);
}
private DynValue LoadFile(string file, Table? globalContext = null, string? codeStringFriendly = null)
{
if (_script == null)
{
throw new Exception("Not running");
}
if (!LuaCsFile.CanReadFromPath(file))
{
throw new ScriptRuntimeException($"loadfile: File access to {file} not allowed.");
}
if (!LuaCsFile.Exists(file))
{
throw new ScriptRuntimeException($"loadfile: File {file} not found.");
}
return _script.LoadFile(file, globalContext, codeStringFriendly);
}
private void RegisterLuaEvents()
{
_eventService.Subscribe<IEventAssemblyUnloading>(this);
_eventService.RegisterLuaEventAlias<IEventUpdate>("think", nameof(IEventUpdate.OnUpdate));
_eventService.RegisterLuaEventAlias<IEventKeyUpdate>("keyUpdate", nameof(IEventKeyUpdate.OnKeyUpdate));
_eventService.RegisterLuaEventAlias<IEventAfflictionUpdate>("afflictionUpdate", nameof(IEventAfflictionUpdate.OnAfflictionUpdate));
_eventService.RegisterLuaEventAlias<IEventCharacterCreated>("character.created", nameof(IEventCharacterCreated.OnCharacterCreated));
_eventService.RegisterLuaEventAlias<IEventCharacterDeath>("character.death", nameof(IEventCharacterDeath.OnCharacterDeath));
_eventService.RegisterLuaEventAlias<IEventCharacterDamageLimb>("character.damageLimb", nameof(IEventCharacterDamageLimb.OnCharacterDamageLimb));
_eventService.RegisterLuaEventAlias<IEventGiveCharacterJobItems>("character.giveJobItems", nameof(IEventGiveCharacterJobItems.OnGiveCharacterJobItems));
_eventService.RegisterLuaEventAlias<IEventHumanCPRSuccess>("character.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess));
_eventService.RegisterLuaEventAlias<IEventHumanCPRFailed>("character.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed));
_eventService.RegisterLuaEventAlias<IEventHumanCPRSuccess>("human.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess));
_eventService.RegisterLuaEventAlias<IEventHumanCPRFailed>("human.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed));
_eventService.RegisterLuaEventAlias<IEventCharacterApplyDamage>("character.applyDamage", nameof(IEventCharacterApplyDamage.OnCharacterApplyDamage));
_eventService.RegisterLuaEventAlias<IEventCharacterApplyAffliction>("character.applyAffliction", nameof(IEventCharacterApplyAffliction.OnCharacterApplyAffliction));
_eventService.RegisterLuaEventAlias<IEventGapOxygenUpdate>("gapOxygenUpdate", nameof(IEventGapOxygenUpdate.OnGapOxygenUpdate));
_eventService.RegisterLuaEventAlias<IEventClientControlHusk>("husk.clientControlHusk", nameof(IEventClientControlHusk.OnClientControlHusk));
_eventService.RegisterLuaEventAlias<IEventMeleeWeaponHandleImpact>("meleeWeapon.handleImpact", nameof(IEventMeleeWeaponHandleImpact.OnMeleeWeaponHandleImpact));
_eventService.RegisterLuaEventAlias<IEventServerLog>("serverLog", nameof(IEventServerLog.OnServerLog));
_eventService.RegisterLuaEventAlias<IEventTryClientChangeName>("tryChangeClientName", nameof(IEventTryClientChangeName.OnTryClienChangeName));
_eventService.RegisterLuaEventAlias<IEventChangeFallDamage>("changeFallDamage", nameof(IEventChangeFallDamage.OnChangeFallDamage));
_eventService.RegisterLuaEventAlias<IEventChatMessage>("chatMessage", nameof(IEventChatMessage.OnChatMessage));
_eventService.RegisterLuaEventAlias<IEventCanUseVoiceRadio>("canUseVoiceRadio", nameof(IEventCanUseVoiceRadio.OnCanUseVoiceRadio));
_eventService.RegisterLuaEventAlias<IEventChangeLocalVoiceRange>("changeLocalVoiceRange", nameof(IEventChangeLocalVoiceRange.OnChangeLocalVoiceRange));
_eventService.RegisterLuaEventAlias<IEventRoundStarted>("roundStart", nameof(IEventRoundStarted.OnRoundStart));
_eventService.RegisterLuaEventAlias<IEventRoundEnded>("roundEnd", nameof(IEventRoundEnded.OnRoundEnd));
_eventService.RegisterLuaEventAlias<IEventMissionsEnded>("missionsEnded", nameof(IEventMissionsEnded.OnMissionsEnded));
_eventService.RegisterLuaEventAlias<IEventSignalReceived>("signalReceived", nameof(IEventSignalReceived.OnSignalReceived));
_eventService.RegisterLuaEventAlias<IEventItemCreated>("item.created", nameof(IEventItemCreated.OnItemCreated));
_eventService.RegisterLuaEventAlias<IEventItemRemoved>("item.removed", nameof(IEventItemRemoved.OnItemRemoved));
_eventService.RegisterLuaEventAlias<IEventItemUse>("item.use", nameof(IEventItemUse.OnItemUsed));
_eventService.RegisterLuaEventAlias<IEventItemSecondaryUse>("item.secondaryUse", nameof(IEventItemSecondaryUse.OnItemSecondaryUsed));
_eventService.RegisterLuaEventAlias<IEventItemReadPropertyChange>("item.readPropertyChange", nameof(IEventItemReadPropertyChange.OnItemReadPropertyChange));
_eventService.RegisterLuaEventAlias<IEventItemDeconstructed>("item.deconstructed", nameof(IEventItemDeconstructed.OnItemDeconstructed));
_eventService.RegisterLuaEventAlias<IEventInventoryPutItem>("inventoryPutItem", nameof(IEventInventoryPutItem.OnInventoryPutItem));
_eventService.RegisterLuaEventAlias<IEventInventoryItemSwap>("inventoryItemSwap", nameof(IEventInventoryItemSwap.OnInventoryItemSwap));
// Compatibility
_eventService.RegisterLuaEventAlias<IEventCharacterCreated>("characterCreated", nameof(IEventCharacterCreated.OnCharacterCreated));
_eventService.RegisterLuaEventAlias<IEventCharacterDeath>("characterDeath", nameof(IEventCharacterDeath.OnCharacterDeath));
#if SERVER
_eventService.RegisterLuaEventAlias<IEventClientConnected>("client.connected", nameof(IEventClientConnected.OnClientConnected));
_eventService.RegisterLuaEventAlias<IEventClientDisconnected>("client.disconnected", nameof(IEventClientDisconnected.OnClientDisconnected));
_eventService.RegisterLuaEventAlias<IEventJobsAssigned>("jobsAssigned", nameof(IEventJobsAssigned.OnJobsAssigned));
_eventService.RegisterLuaEventAlias<IEventClientRawNetMessageReceived>("netMessageReceived", nameof(IEventClientRawNetMessageReceived.OnReceivedClientNetMessage));
// Compatibility
_eventService.RegisterLuaEventAlias<IEventClientConnected>("clientConnected", nameof(IEventClientConnected.OnClientConnected));
_eventService.RegisterLuaEventAlias<IEventClientDisconnected>("clientDisconnected", nameof(IEventClientDisconnected.OnClientDisconnected));
_eventService.RegisterLuaEventAlias<IEventModifyChatMessage>("modifyChatMessage", nameof(IEventModifyChatMessage.OnModifyMessagePredicate));
#elif CLIENT
_eventService.RegisterLuaEventAlias<IEventServerRawNetMessageReceived>("netMessageReceived", nameof(IEventServerRawNetMessageReceived.OnReceivedServerNetMessage));
#endif
}
private void SetupEnvironment(bool enableSandbox)
{
_script = new Script(CoreModules.Preset_SoftSandbox | CoreModules.Debug | CoreModules.IO | CoreModules.OS_System);
_script.Options.DebugPrint = (string msg) =>
{
_loggerService.LogMessage($"[Lua] {msg}");
};
SetCachingPolicy(_luaCsInfoProvider.UseCaching);
_script.Options.ScriptLoader = _luaScriptLoader;
_script.Options.CheckThreadAccess = false;
Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; };
UserData.RegisterType<ILuaCsHook.HookMethodType>();
UserData.RegisterType(typeof(LuaGame));
StandardUserDataDescriptor descriptor = (StandardUserDataDescriptor)UserData.RegisterType(typeof(EventService));
descriptor.AddDynValue("HookMethodType", UserData.CreateStatic<ILuaCsHook.HookMethodType>());
UserData.RegisterType(typeof(ILuaCsNetworking));
UserData.RegisterType(typeof(ILuaCsUtility));
UserData.RegisterType(typeof(ILuaCsTimer));
UserData.RegisterType(typeof(LuaCsFile));
UserData.RegisterType(typeof(ILuaScriptResourceInfo));
UserData.RegisterType(typeof(IResourceInfo));
UserData.RegisterType(typeof(IUserDataDescriptor));
UserData.RegisterType(typeof(INetworkingService));
UserData.RegisterType(typeof(ILuaConfigService));
UserData.RegisterType(typeof(ILoggerService));
UserData.RegisterType(typeof(ISettingBase));
UserData.RegisterType(typeof(IDataInfo));
Type[] settingBaseTypes = [
typeof(ISettingBase<bool>),
typeof(ISettingBase<string>),
typeof(ISettingBase<byte>),
typeof(ISettingBase<sbyte>),
typeof(ISettingBase<ushort>),
typeof(ISettingBase<short>),
typeof(ISettingBase<char>),
typeof(ISettingBase<uint>),
typeof(ISettingBase<int>),
typeof(ISettingBase<ulong>),
typeof(ISettingBase<long>),
typeof(ISettingBase<float>),
typeof(ISettingBase<double>),
typeof(ISettingRangeBase<float>),
typeof(ISettingRangeBase<int>),
typeof(ISettingList<string>),
typeof(ISettingList<byte>),
typeof(ISettingList<sbyte>),
typeof(ISettingList<ushort>),
typeof(ISettingList<short>),
typeof(ISettingList<char>),
typeof(ISettingList<uint>),
typeof(ISettingList<int>),
typeof(ISettingList<ulong>),
typeof(ISettingList<long>),
typeof(ISettingList<float>),
typeof(ISettingList<double>),
];
Dictionary<string, Dictionary<string, object>> settingsTable = [];
foreach (Type type in settingBaseTypes)
{
UserData.RegisterType(type);
string baseName = type.Name.RemoveFromEnd("`1").Substring(1);
if (!settingsTable.ContainsKey(baseName))
{
settingsTable[baseName] = new Dictionary<string, object>();
}
settingsTable[baseName][type.GetGenericArguments()[0].Name] = UserData.CreateStatic(type);
}
foreach (var keyPair in settingsTable)
{
_script.Globals[keyPair.Key] = keyPair.Value;
}
UserData.RegisterType(typeof(ISettingRangeBase<int>));
#if CLIENT
UserData.RegisterType(typeof(ISettingControl));
#endif
new LuaConverters(this).RegisterLuaConverters();
var luaRequire = new LuaRequire(_script);
_script.Globals["setmodulepaths"] = (string[] str) => ((LuaScriptLoader)_luaScriptLoader).ModulePaths = str;
_script.Globals["dofile"] = (Func<string, Table, string, DynValue>)DoFile;
_script.Globals["loadfile"] = (Func<string, Table, string, DynValue>)LoadFile;
_script.Globals["require"] = (Func<string, Table, DynValue>)luaRequire.Require;
_script.Globals["printerror"] = (DynValue o) => { _loggerService.LogError($"[Lua] {o.ToString()}"); };
_script.Globals["dostring"] = (Func<string, Table, string, DynValue>)_script.DoString;
_script.Globals["load"] = (Func<string, Table, string, DynValue>)_script.LoadString;
_script.Globals["Game"] = _luaGame;
_script.Globals["Hook"] = _eventService;
_script.Globals["Timer"] = _luaCsTimer;
_script.Globals["File"] = UserData.CreateStatic<LuaCsFile>();
_script.Globals["ConfigService"] = _configService;
_script.Globals["Networking"] = _networkingService;
_script.Globals["trygetpackage"] = (string name, out ContentPackage package) =>
_packageManagementService.Value.TryGetLoadedPackageByName(name, out package);
_script.Globals["Logger"] = _loggerService;
//_script.Globals["Steam"] = Steam;
if (enableSandbox)
{
UserData.RegisterType(typeof(SafeLuaUserDataService));
_script.Globals["LuaUserData"] = _safeUserDataService;
}
else
{
UserData.RegisterType(typeof(LuaUserDataService));
_script.Globals["LuaUserData"] = _userDataService;
}
Table eventsTable = new Table(_script);
var typesValue = _pluginManagementService.GetImplementingTypes<IEvent>(includeInterfaces: true, includeAbstractTypes: true);
if (typesValue.IsSuccess)
{
foreach (var eventType in typesValue.Value)
{
if (eventType.IsGenericType) { continue; }
if (!eventType.IsInterface) { continue; }
UserData.RegisterType(eventType);
eventsTable[eventType.Name] = UserData.CreateStatic(eventType);
}
}
_script.Globals["Events"] = eventsTable;
_script.Globals["ExecutionNumber"] = 0;
_script.Globals["CSActive"] = !enableSandbox;
((Table)_script.Globals["debug"])["breakpoint"] = () => { Debugger.Break(); };
_script.Globals["SERVER"] = LuaCsSetup.IsServer;
_script.Globals["CLIENT"] = LuaCsSetup.IsClient;
_defaultLuaRegistrar.RegisterAll();
}
public FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaScriptResourceInfo> executionOrder, bool enableSandbox)
{
if (_isRunning)
{
return FluentResults.Result.Fail("Tried to execute Lua scripts without unloading first.");
}
_loggerService.LogMessage("[Lua] Executing scripts");
SetupEnvironment(enableSandbox);
if (_script == null) { return FluentResults.Result.Ok(); } // never happens
var result = FluentResults.Result.Ok();
_isRunning = true;
var packages = executionOrder.Select(r => r.OwnerPackage)
.Distinct()
.Select(p => $"{p.Dir}/Lua/?.lua")
.ToArray();
((LuaScriptLoader)_luaScriptLoader).ModulePaths = packages;
Table package = (Table)_script.Globals["package"];
package.Set("path", DynValue.FromObject(_script, packages));
#if CLIENT
if (GameMain.NetworkMember is { IsClient: true })
{
var startMessage = _networkingService.Start("_luastart");
var packagesToReport = ContentPackageManager.EnabledPackages.All
.Where(p => _packageManagementService.Value.PackageContainsAnyRunnableResource(p))
.Where(p => !p.NameMatches(LuaCsSetup.PackageName))
.ToList();
startMessage.WriteUInt16((UInt16)packagesToReport.Count());
foreach (var enabledPackage in packagesToReport)
{
var id = enabledPackage.UgcId;
string hash = enabledPackage.Hash.StringRepresentation ?? "";
startMessage.WriteString(enabledPackage.Name);
startMessage.WriteString(enabledPackage.ModVersion);
if (id.TryUnwrap(out ContentPackageId? packageId) && packageId is SteamWorkshopId steamId)
{
startMessage.WriteUInt64(steamId.Value);
}
else
{
startMessage.WriteUInt64(0);
}
startMessage.WriteString(hash);
}
_networkingService.Send(startMessage);
}
#elif SERVER
_networkingService.Receive("_luastart", (message, client) =>
{
var num = message.ReadUInt16();
List<Table> packages = new List<Table>();
for (int i = 0; i < num; i++)
{
Table table = new Table(_script);
table.Set("Name", DynValue.NewString(message.ReadString()));
table.Set("Version", DynValue.NewString(message.ReadString()));
table.Set("Id", DynValue.NewString(message.ReadUInt64().ToString()));
table.Set("Hash", DynValue.NewString(message.ReadString()));
packages.Add(table);
}
_eventService.Call("client.packages", client, packages);
});
#endif
foreach (ILuaScriptResourceInfo resource in executionOrder.Where(l => l.IsAutorun))
{
foreach (ContentPath filePath in resource.FilePaths)
{
try
{
_loggerService.LogMessage($"[Lua] - Run {filePath.Value}");
_script.Call(_script.LoadFile(filePath.FullPath), resource.OwnerPackage.Dir);
}
catch(Exception e)
{
result = result.WithError(new ExceptionalError(e));
}
}
}
_eventService.Call("loaded");
return result;
}
public DynValue? CallFunctionSafe(object luaFunction, params object[] args)
{
if (!IsRunning) { return null; }
lock (_script)
{
try
{
return _script.Call(luaFunction, args);
}
catch (Exception e)
{
_loggerService.HandleException(e);
}
return null;
}
}
public FluentResults.Result UnloadActiveScripts()
{
_isRunning = false;
_script = null;
return FluentResults.Result.Ok();
}
public FluentResults.Result DisposePackageResources(ContentPackage package)
{
return FluentResults.Result.Ok();
}
public FluentResults.Result DisposeAllPackageResources()
{
if (IsRunning)
{
UnloadActiveScripts();
}
_resourcesInfo.Clear();
_luaScriptLoader.ClearCaches();
return FluentResults.Result.Ok();
}
public FluentResults.Result Reset()
{
IService.CheckDisposed(this);
_luaScriptLoader.ClearCaches();
_userDataService.Reset();
_luaCsTimer.Reset();
RegisterLuaEvents();
return DisposeAllPackageResources();
}
public void Dispose()
{
IsDisposed = true;
_userDataService.Dispose();
_luaScriptLoader.Dispose();
_commandsService.Dispose();
}
public object? GetGlobalTableValue(string tableName)
{
if (!IsRunning) { return null; }
return _script.Globals[tableName];
}
public void OnAssemblyUnloading(Assembly assembly)
{
foreach (Type type in assembly.SafeGetTypes())
{
UserData.UnregisterType(type, deleteHistory: true);
}
}
}
@@ -0,0 +1,94 @@
using Barotrauma;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Events;
using FluentResults;
using HarmonyLib;
using Microsoft.Xna.Framework;
[HarmonyPatch]
internal class MainMenuPatch : ISystem, IEventScreenSelected
{
public bool IsDisposed { get; private set; }
private readonly IEventService _eventService;
private bool mainMenuUIAdded = false;
public MainMenuPatch(IEventService eventService)
{
_eventService = eventService;
RegisterEvents();
#if CLIENT
if (Screen.Selected is MainMenuScreen mainMenuScreen)
{
AddToMainMenu(mainMenuScreen);
}
#endif
}
public void OnScreenSelected(Screen screen)
{
#if CLIENT
if (screen is MainMenuScreen mainMenuScreen)
{
AddToMainMenu(mainMenuScreen);
}
#endif
}
#if CLIENT
private void AddToMainMenu(MainMenuScreen screen)
{
if (mainMenuUIAdded) { return; }
var textBlock = new GUITextBlock(new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, "", Color.Red)
{
IgnoreLayoutGroups = false
};
textBlock.OnAddedToGUIUpdateList = (GUIComponent component) =>
{
string mode = LuaCsSetup.Instance.CsRunPolicyValue;
if (mode is "Prompt")
{
string sessionState = LuaCsSetup.Instance.IsCsEnabledForSession ? "yes" : "no";
mode = $"enabled (prompt mode, allowed for this session: {sessionState})";
}
else if (mode is "Enabled")
{
mode = "always enabled";
}
else
{
mode = "disabled";
}
textBlock.Text = $"LuaCsForBarotrauma active (revision {AssemblyInfo.GitRevision}), C# is currently {mode}\nNew settings available in the game settings menu.";
};
mainMenuUIAdded = true;
}
#endif
private void RegisterEvents()
{
_eventService.Subscribe<IEventScreenSelected>(this);
}
public void Dispose()
{
_eventService.Unsubscribe<IEventScreenSelected>(this);
IsDisposed = true;
}
public FluentResults.Result Reset()
{
RegisterEvents();
return FluentResults.Result.Ok();
}
}
@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FarseerPhysics.Common;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public sealed partial class ModConfigFileParserService :
IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>,
IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>,
IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>
{
private IStorageService _storageService;
private readonly AsyncReaderWriterLock _operationsLock = new();
public ModConfigFileParserService(IStorageService storageService)
{
_storageService = storageService;
}
#region Dispose
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
try
{
_storageService.Dispose();
this._storageService = null;
}
catch
{
// ignored
}
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#endregion
// --- Assemblies
async Task<Result<IAssemblyResourceInfo>> IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
{
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (CheckThrowNullRefs(src, "Assembly") is { IsFailed: true } fail)
return fail;
var isScript = src.Element.GetAttributeBool("IsScript", false);
var runtimeEnv = GetRuntimeEnvironment(src.Element);
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, isScript ? ".cs" : ".dll");
if (fileResults.IsFailed)
return FluentResults.Result.Fail(fileResults.Errors);
return new AssemblyResourceInfo()
{
SupportedPlatforms = runtimeEnv.Platform,
SupportedTargets = runtimeEnv.Target,
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
FilePaths = fileResults.Value,
Optional = src.Element.GetAttributeBool("Optional", false),
InternalName = src.Element.GetAttributeString("Name", string.Empty),
OwnerPackage = src.Owner,
RequiredPackages = src.Required,
IncompatiblePackages = src.Incompatible,
// Type Specific
FriendlyName = src.Element.GetAttributeString("FriendlyName", GetFallbackCompliantAssemblyName(src.Owner)),
IsScript = isScript,
UseInternalAccessName = src.Element.GetAttributeBool("UseInternalAccessName", false),
IsReferenceModeOnly = src.Element.GetAttributeBool("IsReferenceModeOnly", false)
};
// helper methods
string GetFallbackCompliantAssemblyName(ContentPackage package)
{
if (package.Name.IsNullOrWhiteSpace())
{
return "FallbackAssemblyName";
}
// replace non az chars with '_'
var sanitizedPackageName = Regex.Replace(package.Name, @"[^a-zA-Z0-9_]", "_");
if (char.IsDigit(sanitizedPackageName[0]))
{
sanitizedPackageName = "ASM" + sanitizedPackageName;
}
// replace consecutive '_'
return Regex.Replace(sanitizedPackageName, @"[_.]{2,}", "_");
}
}
async Task<ImmutableArray<Result<IAssemblyResourceInfo>>> IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
{
return await this.TryParseGenericResourcesAsync<IAssemblyResourceInfo>(sources);
}
// --- Config
async Task<Result<IConfigResourceInfo>> IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
{
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (CheckThrowNullRefs(src, "Config") is { IsFailed: true } fail)
return fail;
var runtimeEnv = GetRuntimeEnvironment(src.Element);
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".xml");
if (fileResults.IsFailed)
return FluentResults.Result.Fail(fileResults.Errors);
return new ConfigResourceInfo()
{
SupportedPlatforms = runtimeEnv.Platform,
SupportedTargets = runtimeEnv.Target,
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
FilePaths = fileResults.Value,
Optional = src.Element.GetAttributeBool("Optional", false),
InternalName = src.Element.GetAttributeString("Name", string.Empty),
OwnerPackage = src.Owner,
RequiredPackages = src.Required,
IncompatiblePackages = src.Incompatible
};
}
async Task<ImmutableArray<Result<IConfigResourceInfo>>> IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
{
return await this.TryParseGenericResourcesAsync<IConfigResourceInfo>(sources);
}
// --- Lua Scripts
async Task<Result<ILuaScriptResourceInfo>> IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
{
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (CheckThrowNullRefs(src, "Lua") is { IsFailed: true } fail)
return fail;
var runtimeEnv = GetRuntimeEnvironment(src.Element);
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".lua");
if (fileResults.IsFailed)
return FluentResults.Result.Fail(fileResults.Errors);
return new LuaScriptsResourceInfo()
{
SupportedPlatforms = runtimeEnv.Platform,
SupportedTargets = runtimeEnv.Target,
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
FilePaths = fileResults.Value,
Optional = src.Element.GetAttributeBool("Optional", false),
InternalName = src.Element.GetAttributeString("Name", string.Empty),
OwnerPackage = src.Owner,
RequiredPackages = src.Required,
IncompatiblePackages = src.Incompatible,
// Type Specific
IsAutorun = src.Element.GetAttributeBool("IsAutorun", false),
RunUnrestricted = src.Element.GetAttributeBool("RunUnrestricted", false)
};
}
private FluentResults.Result CheckThrowNullRefs(ResourceParserInfo src, string elementName)
{
Guard.IsNotNull(src, nameof(src));
Guard.IsNotNull(src.Owner, nameof(src.Owner));
Guard.IsNotNull(src.Element, nameof(src.Element));
if (src.Element.Name != elementName)
{
return FluentResults.Result.Fail($"Element name '{elementName}' is incorrect");
}
return FluentResults.Result.Ok();
}
async Task<ImmutableArray<Result<ILuaScriptResourceInfo>>> IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
{
return await this.TryParseGenericResourcesAsync<ILuaScriptResourceInfo>(sources);
}
// --- Helpers
private async Task<Result<ImmutableArray<ContentPath>>> UnsafeGetCheckedFiles(XElement srcElement, ContentPackage srcOwner, string fileExtension)
{
var builder = ImmutableArray.CreateBuilder<ContentPath>();
var filePath = srcElement.GetAttributeContentPath("File", srcOwner);
var folderPath = srcElement.GetAttributeContentPath("Folder", srcOwner);
var res = new FluentResults.Result<ImmutableArray<ContentPath>>();
if ((!filePath?.Value.IsNullOrWhiteSpace()) ?? false)
{
if (_storageService.FileExists(filePath.FullPath) is { IsSuccess: true, Value: true })
{
builder.Add(filePath);
}
else
{
if (srcElement.GetAttributeBool("IsFileRequired", true))
{
res.WithError($"{srcOwner.Name}: The file '{filePath}' is missing!");
}
else
{
res.WithSuccess($"Skipped missing not-required file: '{filePath}'");
}
}
}
if ((!folderPath?.Value.IsNullOrWhiteSpace()) ?? false)
{
if (_storageService.DirectoryExists(folderPath.FullPath) is { IsSuccess: true, Value: true })
{
var searchLocation = System.IO.Path.GetRelativePath(srcOwner.Dir, folderPath.Value);
var files = _storageService.FindFilesInPackage(srcOwner, searchLocation, "*"+fileExtension, true);
if (files.IsFailed)
{
res.WithError($"{srcOwner.Name}: Failed to load files from {folderPath}!");
}
else
{
foreach (var file in files.Value)
{
builder.Add(ContentPath.FromRaw(srcOwner, $"%ModDir%/{System.IO.Path.GetRelativePath(System.IO.Path.GetFullPath(srcOwner.Dir), file)}"));
}
}
}
else
{
if (srcElement.GetAttributeBool("IsFileRequired", true))
{
res.WithError($"{srcOwner.Name}: The file '{folderPath}' is missing!");
}
else
{
res.WithSuccess($"Skipped missing not-required folder: '{folderPath}'");
}
}
}
return res.WithValue(builder.ToImmutable());
}
private (Platform Platform, Target Target) GetRuntimeEnvironment(XElement element)
{
return (
Platform: element.GetAttributeEnum("Platform", Platform.Any),
Target: element.GetAttributeEnum("Target", Target.Any));
}
private async Task<ImmutableArray<Result<T>>> TryParseGenericResourcesAsync<T>(IEnumerable<ResourceParserInfo> sources)
{
// ReSharper disable once PossibleMultipleEnumeration
Guard.IsNotNull(sources, nameof(IParserServiceAsync<ResourceParserInfo, T>.TryParseResourcesAsync));
var builder = ImmutableArray.CreateBuilder<Result<T>>();
foreach (var info in sources)
{
builder.Add(await Unsafe.As<IParserServiceAsync<ResourceParserInfo, T>>(this).TryParseResourceAsync(info));
}
return builder.ToImmutable();
}
}
@@ -0,0 +1,430 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.VsCodeDebugger.SDK;
namespace Barotrauma.LuaCs;
public sealed class ModConfigService : IModConfigService
{
private IStorageService _storageService;
private ILoggerService _logger;
private IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo> _assemblyParserService;
private IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo> _luaScriptParserService;
private IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo> _configParserService;
#if CLIENT
private IParserServiceAsync<ResourceParserInfo, IStylesResourceInfo> _stylesParserService;
#endif
private readonly AsyncReaderWriterLock _operationsLock = new();
public ModConfigService(IStorageService storageService,
IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo> assemblyParserService,
IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo> luaScriptParserService,
IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo> configParserService,
#if CLIENT
IParserServiceAsync<ResourceParserInfo, IStylesResourceInfo> stylesParserService,
#endif
ILoggerService logger)
{
_storageService = storageService;
_assemblyParserService = assemblyParserService;
_luaScriptParserService = luaScriptParserService;
_configParserService = configParserService;
_logger = logger;
#if CLIENT
_stylesParserService = stylesParserService;
#endif
}
#region Dispose
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
try
{
_storageService.Dispose();
_logger.Dispose();
_assemblyParserService.Dispose();
_luaScriptParserService.Dispose();
_configParserService.Dispose();
_storageService = null;
_logger = null;
_assemblyParserService = null;
_luaScriptParserService = null;
_configParserService = null;
#if CLIENT
_stylesParserService.Dispose();
_stylesParserService = null;
#endif
}
catch
{
// ignored
}
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#endregion
public async Task<Result<IModConfigInfo>> CreateConfigAsync(ContentPackage src)
{
Guard.IsNotNull(src, nameof(src));
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config })
{
return await CreateFromConfigXmlAsync(src, config);
}
return await CreateFromLegacyAsync(src);
}
public async Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src)
{
if (src.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException($"{nameof(CreateConfigsAsync)}: The supplied array is default or empty!");
using var lck = await _operationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
var builder = ImmutableArray.CreateBuilder<Task<Task<Result<IModConfigInfo>>>>(src.Length);
foreach (var srcItem in src)
{
builder.Add(Task.Factory.StartNew(async Task<Result<IModConfigInfo>> () => await CreateConfigAsync(srcItem)));
}
var taskResults = await Task.WhenAll(builder.ToImmutable());
var returnResults = ImmutableArray.CreateBuilder<(ContentPackage Source, Result<IModConfigInfo> Config)>();
foreach (var taskResult in taskResults)
{
if (taskResult.IsFaulted)
{
ThrowHelper.ThrowInvalidOperationException($"{nameof(CreateConfigsAsync)}: Task failed: {taskResult.Exception?.Message}");
}
var r = await taskResult;
returnResults.Add((r.Value.Package, r));
}
return returnResults.ToImmutable();
}
//--- Helpers
private async Task<Result<XElement>> TryGetModConfigXmlAsync(ContentPackage src)
{
return await _storageService.LoadPackageXmlAsync(ContentPath.FromRaw(src, "%ModDir%/ModConfig.xml")) is { IsSuccess: true, Value: { Root: {} config} }
? FluentResults.Result.Ok(config)
: FluentResults.Result.Fail<XElement>("ModConfig.xml not found");
}
private async Task<Result<IModConfigInfo>> CreateFromConfigXmlAsync(ContentPackage owner, XElement src)
{
var asmTask = Task.Factory.StartNew(async () => await GetAssembliesFromXml(owner, src));
var cfgTask = Task.Factory.StartNew(async () => await GetConfigsFromXml(owner, src));
var luaTask = Task.Factory.StartNew(async () => await GetLuaScriptsFromXml(owner, src));
#if CLIENT
var styleTask = Task.Factory.StartNew(async () => await GetStylesFromXml(owner, src));
#endif
await Task.WhenAll(
asmTask,
cfgTask,
#if CLIENT
styleTask,
#endif
luaTask);
return FluentResults.Result.Ok<IModConfigInfo>(new ModConfigInfo()
{
Package = owner,
Assemblies = await await asmTask,
Configs = await await cfgTask,
#if CLIENT
Styles = await await styleTask,
#endif
LuaScripts = await await luaTask
});
async Task<ImmutableArray<ILuaScriptResourceInfo>> GetLuaScriptsFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<ILuaScriptResourceInfo>(contentPackage, cfgElement, "Lua", "FileGroup", _luaScriptParserService);
}
async Task<ImmutableArray<IConfigResourceInfo>> GetConfigsFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<IConfigResourceInfo>(contentPackage, cfgElement, "Config", "FileGroup", _configParserService);
}
async Task<ImmutableArray<IAssemblyResourceInfo>> GetAssembliesFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<IAssemblyResourceInfo>(contentPackage, cfgElement, "Assembly", "FileGroup", _assemblyParserService);
}
#if CLIENT
async Task<ImmutableArray<IStylesResourceInfo>> GetStylesFromXml(ContentPackage contentPackage,
XElement cfgElement)
{
return await GetResourceFromXml<IStylesResourceInfo>(contentPackage, cfgElement, "Style", "FileGroup", _stylesParserService);
}
#endif
async Task<ImmutableArray<T>> GetResourceFromXml<T>(ContentPackage contentPackage, XElement cfgElement, string elemName, string fileGroupName, IParserServiceAsync<ResourceParserInfo, T> resourceService)
{
var elems = GetResourceElementsWithName(owner, cfgElement, elemName, fileGroupName);
if (elems.IsDefaultOrEmpty)
return ImmutableArray<T>.Empty;
var results = await resourceService.TryParseResourcesAsync(elems);
Guard.IsNotEmpty((IReadOnlyCollection<Result<T>>)results, nameof(results));
var resources = ImmutableArray.CreateBuilder<T>();
foreach (var result in results)
{
if (result.Errors.Count > 0)
{
_logger.LogResults(result.ToResult());
continue;
}
resources.Add(result.Value);
}
return resources.ToImmutable();
}
ImmutableArray<ResourceParserInfo> GetResourceElementsWithName(ContentPackage package, XElement root, string elemName, string groupName)
{
var elems = ImmutableArray.CreateBuilder<ResourceParserInfo>();
elems.AddRange(root.GetChildElements(elemName)
.Select(e => new ResourceParserInfo(package, e, ImmutableArray<Identifier>.Empty, ImmutableArray<Identifier>.Empty))
.ToImmutableArray());
if (root.GetChildElements(groupName).ToImmutableArray() is { IsDefaultOrEmpty: false } fileGroups)
{
foreach (var fileGroup in fileGroups)
{
if (fileGroup.GetChildElements(elemName).ToImmutableArray() is { IsDefaultOrEmpty: false } subLuaElems)
{
var cond = GetDependencyIdentifiers(fileGroup, true);
var negCond = GetDependencyIdentifiers(fileGroup, false);
foreach (var element in subLuaElems)
{
elems.Add(new ResourceParserInfo(package, element, cond, negCond));
}
}
}
}
return elems.ToImmutable();
}
ImmutableArray<Identifier> GetDependencyIdentifiers(XElement fg, bool depsLoadedSetting)
{
return fg.GetChildElements("Conditional")
.Where(cElem => bool.TryParse(cElem.GetAttribute("IsLoaded").Value, out bool isLoaded) && isLoaded == depsLoadedSetting)
.SelectMany(cElem2 => cElem2.GetAttributeString("Dependencies", String.Empty)
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.Select(ident => new Identifier(ident)))
.ToImmutableArray();
}
}
private async Task<Result<IModConfigInfo>> CreateFromLegacyAsync(ContentPackage src)
{
return new ModConfigInfo()
{
Package = src,
Assemblies = GetAssembliesLegacy(src),
Configs = GetConfigsLegacy(src),
LuaScripts = GetLuaScriptsLegacy(src)
};
ImmutableArray<IAssemblyResourceInfo> GetAssembliesLegacy(ContentPackage srcPackage)
{
var binSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[]
{
("bin/Client/Windows", Target.Client, Platform.Windows),
("bin/Client/Linux", Target.Client, Platform.Linux),
("bin/Client/OSX", Target.Client, Platform.OSX),
("bin/Server/Windows", Target.Server, Platform.Windows),
("bin/Server/Linux", Target.Server, Platform.Linux),
("bin/Server/OSX", Target.Server, Platform.OSX)
};
var builder = ImmutableArray.CreateBuilder<IAssemblyResourceInfo>();
foreach (var searchPathways in binSearchInd)
{
if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.dll",
true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
{
builder.Add(new AssemblyResourceInfo()
{
OwnerPackage = srcPackage,
InternalName = searchPathways.SubFolder,
SupportedPlatforms = searchPathways.Platforms,
SupportedTargets = searchPathways.Targets,
LoadPriority = 0,
FilePaths = result.Value.Select(fp => ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray(),
FriendlyName = $"{srcPackage.Name}.{searchPathways.SubFolder.Replace('/','.')}",
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
IsScript = false,
IsReferenceModeOnly = false
});
}
}
var sharedResult = _storageService.FindFilesInPackage(srcPackage,
Path.Combine("CSharp/Shared"),
"*.cs", true);
var sharedFiles = sharedResult.IsSuccess && !sharedResult.Value.IsDefaultOrEmpty
? sharedResult.Value.Select(fp =>
ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray()
: ImmutableArray<ContentPath>.Empty;
var srcSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[]
{
("CSharp/Client", Target.Client, Platform.Any),
("CSharp/Server", Target.Server, Platform.Any)
};
foreach (var searchPathways in srcSearchInd)
{
// we have architecture dependent files as well
if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.cs",
true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
{
builder.Add(new AssemblyResourceInfo()
{
OwnerPackage = srcPackage,
InternalName = searchPathways.SubFolder,
SupportedPlatforms = searchPathways.Platforms,
SupportedTargets = searchPathways.Targets,
LoadPriority = 0,
FilePaths = result.Value
.Select(fp => ContentPath.FromRaw(srcPackage,
$"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
.Concat(sharedFiles).ToImmutableArray(),
FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName, // give the best chance of success (InternalsAware + Publicizer)
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
UseInternalAccessName = false, //compile as public and then fallback to internals
IsScript = true,
IsReferenceModeOnly = false
});
}
// add the shared files by themselves
else if (!sharedFiles.IsDefaultOrEmpty)
{
builder.Add(new AssemblyResourceInfo()
{
OwnerPackage = srcPackage,
InternalName = searchPathways.SubFolder,
SupportedPlatforms = searchPathways.Platforms,
SupportedTargets = searchPathways.Targets,
LoadPriority = 0,
FilePaths = sharedFiles,
FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName,
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
UseInternalAccessName = false,
IsScript = true,
IsReferenceModeOnly = false
});
}
}
return builder.ToImmutable();
}
ImmutableArray<IConfigResourceInfo> GetConfigsLegacy(ContentPackage src)
{
return ImmutableArray<IConfigResourceInfo>.Empty;
}
ImmutableArray<ILuaScriptResourceInfo> GetLuaScriptsLegacy(ContentPackage src)
{
var builder = ImmutableArray.CreateBuilder<ILuaScriptResourceInfo>();
if (_storageService.FindFilesInPackage(src, "Lua", "*.lua", true)
is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
{
ImmutableArray<string> cleanedResult = result.Value.Select(fp => fp.CleanUpPathCrossPlatform()).ToImmutableArray();
ImmutableArray<string> autorun = cleanedResult
.Where(fp => fp.Contains("Lua/ForcedAutorun/") || fp.Contains("Lua/Autorun/"))
.ToImmutableArray();
ImmutableArray<ContentPath> autorunFP = autorun.Select(fp => ContentPath.FromRaw(src,
$"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray();
ImmutableArray<ContentPath> reg = cleanedResult.Except(autorun)
.Select(fp => ContentPath.FromRaw(src,
$"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform()))
.ToImmutableArray();
builder.Add(new LuaScriptsResourceInfo()
{
OwnerPackage = src,
InternalName = "LegacyAutorun",
SupportedPlatforms = Platform.Any,
SupportedTargets = Target.Any,
LoadPriority = 1, // autorun should be last to ensure that dependent code in other files are loaded first
FilePaths = autorunFP,
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
IsAutorun = true,
RunUnrestricted = false
});
builder.Add(new LuaScriptsResourceInfo()
{
OwnerPackage = src,
InternalName = "Legacy",
SupportedPlatforms = Platform.Any,
SupportedTargets = Target.Any,
LoadPriority = 0, // should be included first to ensure that dependent code in these files are available
FilePaths = reg,
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
RequiredPackages = ImmutableArray<Identifier>.Empty,
IsAutorun = false,
RunUnrestricted = false
});
}
return builder.ToImmutable();
}
}
}
@@ -0,0 +1,421 @@
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using FluentResults;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs;
internal partial class NetworkingService : INetworkingService, IEventSettingInstanceLifetime
{
public readonly record struct NetId
{
private readonly string _value;
public NetId(string netId)
{
_value = netId;
}
public static void Write(IWriteMessage message, NetId netId)
{
message.WriteString(netId._value);
}
public static NetId Read(IReadMessage message)
{
return new NetId(message.ReadString());
}
}
private enum ClientToServer
{
NetMessageInternalId,
NetMessageNetId,
RequestSingleNetId,
RequestSync,
}
private enum ServerToClient
{
NetMessageInternalId,
NetMessageNetId,
ReceiveNetIds
}
private ClientPacketHeader? clientHeader = null;
public ClientPacketHeader ClientHeader
{
get
{
if (clientHeader == null)
{
byte lastHeader = (byte)Enum.GetValues(typeof(ClientPacketHeader)).Cast<ClientPacketHeader>().Last();
clientHeader = (ClientPacketHeader)(lastHeader + 1);
}
return (ClientPacketHeader)clientHeader;
}
}
private ServerPacketHeader? serverHeader = null;
public ServerPacketHeader ServerHeader
{
get
{
if (serverHeader == null)
{
byte lastHeader = (byte)Enum.GetValues(typeof(ServerPacketHeader)).Cast<ServerPacketHeader>().Last();
serverHeader = (ServerPacketHeader)(lastHeader + 1);
}
return (ServerPacketHeader)serverHeader;
}
}
private ConcurrentDictionary<INetworkSyncVar, NetId> netVars = [];
private ConcurrentDictionary<NetId, NetMessageReceived> netReceives = [];
private ConcurrentDictionary<ushort, NetId> packetToId = [];
private ConcurrentDictionary<NetId, ushort> idToPacket = [];
public bool IsActive
{
get
{
return GameMain.NetworkMember != null;
}
}
public bool IsSynchronized { get; private set; }
public bool IsDisposed { get; private set; }
private readonly IEventService _eventService;
private readonly ILoggerService _loggerService;
private readonly INetworkIdProvider _networkIdProvider;
public NetworkingService(IEventService eventService, INetworkIdProvider networkIdProvider, ILoggerService loggerService)
{
_eventService = eventService;
_networkIdProvider = networkIdProvider;
_loggerService = loggerService;
#if SERVER
IsSynchronized = true;
#endif
SubscribeToEvents();
}
public void Receive(string netIdString, LuaCsAction callback)
{
#if SERVER
Receive(new NetId(netIdString), (IReadMessage message, Client client) => callback(message, client));
#elif CLIENT
Receive(new NetId(netIdString), (IReadMessage message) => callback(message, null));
#endif
}
public void Receive(string netIdString, NetMessageReceived callback) => Receive(new NetId(netIdString), callback);
public void Receive(Guid netIdGuid, NetMessageReceived callback) => Receive(new NetId(netIdGuid.ToString()), callback);
public IWriteMessage Start(string netIdString)
{
if (netIdString == null)
{
// idk why but Lua calls this method with null instead of the Start method with no arguments
return new WriteOnlyMessage();
}
return Start(new NetId(netIdString));
}
public IWriteMessage Start(Guid netIdGuid) => Start(new NetId(netIdGuid.ToString()));
public IWriteMessage Start() => new WriteOnlyMessage();
internal void Receive(NetId netId, NetMessageReceived callback)
{
#if SERVER
RegisterId(netId);
#elif CLIENT
RequestId(netId);
#endif
netReceives[netId] = callback;
}
private void HandleNetMessage(IReadMessage netMessage, NetId netId, Client client = null)
{
if (netReceives.ContainsKey(netId))
{
try
{
#if CLIENT
netReceives[netId](netMessage);
#elif SERVER
netReceives[netId](netMessage, client);
#endif
}
catch (Exception e)
{
_loggerService.LogResults(new ExceptionalError("Exception thrown inside NetMessageReceive({netId})", e));
}
}
else
{
if (GameSettings.CurrentConfig.VerboseLogging)
{
#if SERVER
_loggerService.LogError($"Received NetMessage for unknown netid {netId} from {GameServer.ClientLogName(client)}.");
#else
_loggerService.LogError($"Received NetMessage for unknown netid {netId} from server.");
#endif
}
}
}
private void HandleNetMessageString(IReadMessage netMessage, Client client = null)
{
NetId netId = NetId.Read(netMessage);
HandleNetMessage(netMessage, netId, client);
}
private void SubscribeToEvents()
{
_eventService.Subscribe<IEventSettingInstanceLifetime>(this);
#if CLIENT
_eventService.Subscribe<IEventServerConnected>(this);
_eventService.Subscribe<IEventServerRawNetMessageReceived>(this);
#elif SERVER
_eventService.Subscribe<IEventClientRawNetMessageReceived>(this);
#endif
}
public Guid GetNetworkIdForInstance(INetworkSyncVar var)
{
return _networkIdProvider.GetNetworkIdForInstance(var);
}
public void RegisterNetVar(INetworkSyncVar netVar)
{
netVar.SetNetworkOwner(this);
NetId netId = new NetId(netVar.InstanceId.ToString());
netVars[netVar] = netId;
#if CLIENT
Receive(netId, (IReadMessage message) =>
{
if (netVar.SyncType == NetSync.None)
{
_loggerService.LogWarning($"Received net var from server but {nameof(NetSync)} is {netVar.SyncType.ToString()}");
return;
}
netVar.ReadNetMessage(message);
});
#elif SERVER
Receive(netId, (IReadMessage message, Client client) =>
{
if (netVar.SyncType == NetSync.None || netVar.SyncType == NetSync.ServerAuthority)
{
_loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but {nameof(NetSync)} is {netVar.SyncType.ToString()}");
return;
}
if (!client.HasPermission(netVar.WritePermissions))
{
_loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but the client lacks permissions to modify it");
return;
}
netVar.ReadNetMessage(message);
// Sync back to all clients
if (netVar.SyncType != NetSync.ClientOneWay)
{
SendNetVar(netVar);
}
});
#endif
}
public void DeregisterNetVar(INetworkSyncVar netVar)
{
if (netVar is null)
{
return;
}
netVar.SetNetworkOwner(null);
netVars.TryRemove(netVar, out _);
}
public void SendNetVar(INetworkSyncVar netVar) => SendNetVar(netVar, null);
public void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection = null)
{
if (!netVars.TryGetValue(netVar, out NetId netId))
{
throw new InvalidOperationException("Tried to send net var across network without registering first");
}
if (netVar.SyncType == NetSync.None) { return; }
#if CLIENT
if (netVar.SyncType == NetSync.ServerAuthority) { return; }
#elif SERVER
if (netVar.SyncType == NetSync.ClientOneWay) { return; }
#endif
IWriteMessage message = Start(netId);
netVar.WriteNetMessage(message);
#if CLIENT
SendToServer(message);
#elif SERVER
SendToClient(message, connection);
#endif
}
public FluentResults.Result Reset()
{
IsSynchronized = false;
netReceives = new ConcurrentDictionary<NetId, NetMessageReceived>();
packetToId = new ConcurrentDictionary<ushort, NetId>();
idToPacket = new ConcurrentDictionary<NetId, ushort>();
netVars = new ConcurrentDictionary<INetworkSyncVar, NetId>();
SubscribeToEvents();
return FluentResults.Result.Ok();
}
public void Dispose()
{
IsDisposed = true;
}
#region Compatiblity
private static readonly HttpClient client = new HttpClient();
public async void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
{
try
{
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), url);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
if (data != null)
{
request.Content = new StringContent(data, Encoding.UTF8, contentType);
}
HttpResponseMessage response = await client.SendAsync(request);
if (savePath != null)
{
if (LuaCsFile.IsPathAllowedException(savePath))
{
byte[] responseData = await response.Content.ReadAsByteArrayAsync();
using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write))
{
fileStream.Write(responseData, 0, responseData.Length);
}
}
}
string responseBody = await response.Content.ReadAsStringAsync();
CrossThread.RequestExecutionOnMainThread(() =>
{
callback(responseBody, (int)response.StatusCode, response.Headers);
});
}
catch (HttpRequestException e)
{
CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, e.StatusCode, null); });
}
catch (Exception e)
{
CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, null, null); });
}
}
public void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, data, "POST", contentType, headers, savePath);
}
public void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, data, "POST", contentType, headers, savePath);
}
public void HttpGet(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, null, "GET", null, headers, savePath);
}
public void RequestGetHTTP(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null)
{
HttpRequest(url, callback, null, "GET", null, headers, savePath);
}
public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData)
{
GameMain.NetworkMember.CreateEntityEvent(entity, extraData);
}
public ushort LastClientListUpdateID
{
get { return GameMain.NetworkMember.LastClientListUpdateID; }
set { GameMain.NetworkMember.LastClientListUpdateID = value; }
}
#if SERVER
public void ClientWriteLobby(Client client) => GameMain.Server.ClientWriteLobby(client);
public void UpdateClientPermissions(Client client)
{
GameMain.Server.UpdateClientPermissions(client);
}
public int FileSenderMaxPacketsPerUpdate
{
get { return FileSender.FileTransferOut.MaxPacketsPerUpdate; }
set { FileSender.FileTransferOut.MaxPacketsPerUpdate = value; }
}
#endif
#endregion
public void OnSettingInstanceCreated<T>(T configInstance) where T : ISettingBase
{
if (configInstance is INetworkSyncVar syncVar)
{
RegisterNetVar(syncVar);
}
}
public void OnSettingInstanceDisposed<T>(T configInstance) where T : ISettingBase
{
if (configInstance is INetworkSyncVar syncVar)
{
DeregisterNetVar(syncVar);
}
}
}
@@ -0,0 +1,558 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Xml;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public sealed class PackageManagementService : IPackageManagementService
{
// svc
private ILoggerService _logger;
private IModConfigService _modConfigService;
private IConfigService _configService;
private ILuaScriptManagementService _luaScriptManagementService;
private IPluginManagementService _pluginManagementService;
private IConsoleCommandsService _commandsService;
#if CLIENT
private IUIStylesService _uiStylesService;
#endif
private IPackageManagementServiceConfig _runConfig;
// state
private readonly ConcurrentDictionary<ContentPackage, IModConfigInfo> _loadedPackages = new();
private readonly ConcurrentDictionary<ContentPackage, IModConfigInfo> _runningPackages = new();
private readonly ConcurrentDictionary<string, ContentPackage> _packageNameCache = new();
// control
/// <summary>
/// Service Disposal Lock.
/// </summary>
private readonly AsyncReaderWriterLock _operationsLock = new();
/// <summary>
/// Execution of packages lock.
/// <br/> Read: Package loading/unloading (Multi-operation mode).
/// <br/> Write: Package execution (exclusive mode).
/// </summary>
private readonly AsyncReaderWriterLock _executionLock = new();
public PackageManagementService(ILoggerService logger,
IModConfigService modConfigService,
ILuaScriptManagementService luaScriptManagementService,
IPluginManagementService pluginManagementService,
IConfigService configService,
IConsoleCommandsService commandsService,
#if CLIENT
IUIStylesService uiStylesService,
#endif
IPackageManagementServiceConfig runConfig)
{
_logger = logger;
_modConfigService = modConfigService;
_luaScriptManagementService = luaScriptManagementService;
_pluginManagementService = pluginManagementService;
_configService = configService;
_runConfig = runConfig;
#if CLIENT
_uiStylesService = uiStylesService;
#endif
_commandsService = commandsService;
commandsService.RegisterCommand("pms_getxmlname",
"Gets the XML encoded name for the given package, as used in localization.",
onExecute: args =>
{
if (args.Length < 1)
{
_logger.LogError("Please specify the name of the package.");
return;
}
if (ContentPackageManager.AllPackages.FirstOrDefault(p => p.Name == args[0]) is { } pkg)
{
_logger.Log($"Package Xml Name: '{XmlConvert.EncodeLocalName(pkg.Name)}'");
return;
}
_logger.Log($"Could not find package with the name '{args[0]}'");
},
getValidArgs: () =>
{
return new[]
{
this._loadedPackages.Keys.Select(p => p.Name).ToArray()
};
});
}
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
_logger.LogMessage($"{nameof(PackageManagementService)} is disposing.");
_luaScriptManagementService.Dispose();
_pluginManagementService.Dispose();
_modConfigService.Dispose();
_logger.Dispose();
#if CLIENT
_uiStylesService.Dispose();
#endif
_logger = null;
_luaScriptManagementService = null;
_pluginManagementService = null;
_modConfigService = null;
#if CLIENT
_uiStylesService = null;
#endif
_loadedPackages.Clear();
_runningPackages.Clear();
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public FluentResults.Result Reset()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (IsDisposed)
return FluentResults.Result.Fail($"{nameof(PackageManagementService)}failed to reset. Has already been disposed.");
try
{
var operationResult = new FluentResults.Result();
operationResult.WithReasons(_luaScriptManagementService.Reset().Reasons);
operationResult.WithReasons(_pluginManagementService.Reset().Reasons);
operationResult.WithReasons(_configService.Reset().Reasons);
#if CLIENT
operationResult.WithReasons(_uiStylesService.Reset().Reasons);
#endif
_runningPackages.Clear();
_loadedPackages.Clear();
_packageNameCache.Clear();
return operationResult;
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
public bool TryGetLoadedPackageByName(string name, out ContentPackage package)
{
package = null;
if (name.IsNullOrWhiteSpace())
{
return false;
}
using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
return _packageNameCache.TryGetValue(name, out package);
}
public FluentResults.Result LoadPackageInfo(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.TryGetValue(package, out var result))
{
_logger.LogWarning($"{nameof(LoadPackageInfo)}: Tried to load already-loaded package {package.Name}.");
return FluentResults.Result.Ok();
}
var pkgCfgInfo = _modConfigService.CreateConfigAsync(package).ConfigureAwait(false).GetAwaiter().GetResult();
if (pkgCfgInfo.IsFailed)
{
_logger.LogResults(pkgCfgInfo.ToResult());
return pkgCfgInfo.ToResult();
}
return UnsafeAddPackageInternal(package, pkgCfgInfo.Value);
}
public FluentResults.Result LoadPackagesInfo(ImmutableArray<ContentPackage> packages)
{
if (packages.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentException($"{nameof(LoadPackagesInfo)}: packages list is empty.");
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = new FluentResults.Result();
var packages2 = packages.OrderBy(pkg => pkg.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
.ThenBy(packages.IndexOf)
.ToImmutableArray();
var pkgConfigs = _modConfigService.CreateConfigsAsync([..packages2]).ConfigureAwait(false).GetAwaiter().GetResult();
foreach (var pkgConfig in pkgConfigs)
{
result.WithReasons(pkgConfig.Config.Reasons);
if (pkgConfig.Config.IsSuccess)
{
result.WithReasons(UnsafeAddPackageInternal(pkgConfig.Source, pkgConfig.Config.Value).Reasons);
}
}
return result;
}
private FluentResults.Result UnsafeAddPackageInternal(ContentPackage package, IModConfigInfo config)
{
if (_loadedPackages.TryGetValue(package, out _))
{
_logger.LogWarning($"Tried to load already-loaded package {package.Name}.");
return FluentResults.Result.Ok();
}
// We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe.
foreach (var info in config.Assemblies)
{
TouchMeFullPaths(info);
}
foreach (var info in config.Configs)
{
TouchMeFullPaths(info);
}
foreach (var info in config.LuaScripts)
{
TouchMeFullPaths(info);
}
// We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe.
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.PreserveSig)]
void TouchMeFullPaths(IBaseResourceInfo info)
{
foreach (var contentPath in info.FilePaths)
{
var s = contentPath.FullPath;
}
}
_loadedPackages[package] = config;
_packageNameCache[package.Name] = package;
try
{
var res = new FluentResults.Result();
var tasks = ImmutableArray.CreateBuilder<Task<Task<FluentResults.Result>>>();
if (!config.Configs.IsDefaultOrEmpty)
{
tasks.Add(Task.Factory.StartNew(async Task<FluentResults.Result> () =>
new FluentResults.Result()
.WithReasons((await _configService.LoadConfigsAsync(config.Configs)).Reasons)
.WithReasons((await _configService.LoadConfigsProfilesAsync(config.Configs)).Reasons)));
}
if (!config.LuaScripts.IsDefaultOrEmpty)
{
tasks.Add(Task.Factory.StartNew(async () =>
await _luaScriptManagementService.LoadScriptResourcesAsync(config.LuaScripts)));
}
if (tasks.Count == 0)
{
return FluentResults.Result.Ok();
}
#if CLIENT
if (!config.Styles.IsDefaultOrEmpty)
{
res.WithReasons(_uiStylesService.LoadAssets(config.Styles).Reasons);
}
#endif
var r = Task.WhenAll(tasks.ToArray()).ConfigureAwait(false).GetAwaiter().GetResult();
foreach (var task in r)
{
res.WithReasons(task.ConfigureAwait(false).GetAwaiter().GetResult().Reasons);
}
return res;
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
public FluentResults.Result ExecuteLoadedPackages(ImmutableArray<ContentPackage> executionOrder, bool executeCsAssemblies)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (executionOrder.IsDefaultOrEmpty)
{
return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages in the execution order list.");
}
if (!_runningPackages.IsEmpty)
{
return FluentResults.Result.Fail(
$"{nameof(ExecuteLoadedPackages)}: There are already packages running! List: {
_runningPackages.Aggregate(string.Empty, (acc, kvp) => "-" + kvp + "\n" + kvp.Key.Name)}");
}
if (_loadedPackages.IsEmpty)
{
return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages loaded. Nothing to run!)");
}
var result = new FluentResults.Result();
// get loading order. Note: packages not in the execution order list will load first.
var loadingOrderedPackages = _loadedPackages
.OrderBy(pkg => pkg.Key.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
.ThenBy(pkg => executionOrder.IndexOf(pkg.Key))
.ToImmutableArray();
var loadOrderByPackage = loadingOrderedPackages.Select(p => p.Key).ToImmutableArray();
var toLoadPackagesIndents = loadingOrderedPackages
.SelectMany(p => p.Key.AltNames.Union(new []{ p.Key.Name }).ToIdentifiers())
.ToImmutableHashSet();
// NOTE: Config/Settings are instanced in LoadPackages()
if (executeCsAssemblies)
{
var plugins = SelectCompatible(loadingOrderedPackages
.SelectMany(pkg => pkg.Value.Assemblies)
.ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage);
if (!plugins.IsDefaultOrEmpty)
{
result.WithReasons(_pluginManagementService.LoadAssemblyResources(plugins).Reasons);
result.WithReasons(_pluginManagementService.ActivatePluginInstances(
plugins.Select(p => p.OwnerPackage).ToImmutableArray(), false).Reasons);
}
}
//lua scripts
var luaScripts = SelectCompatible(loadingOrderedPackages
.Where(pkg => executeCsAssemblies
|| !pkg.Value.LuaScripts.Any(scr => scr.RunUnrestricted))
.SelectMany(pkg => pkg.Value.LuaScripts)
.ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage);
if (!luaScripts.IsDefaultOrEmpty)
{
result.WithReasons(_luaScriptManagementService.ExecuteLoadedScripts(luaScripts, enableSandbox: !executeCsAssemblies).Reasons);
}
foreach (var package in loadingOrderedPackages)
{
_runningPackages[package.Key] = package.Value;
}
return result;
}
private static ImmutableArray<T> SelectCompatible<T>(ImmutableArray<T> resources,
ImmutableHashSet<Identifier> enabledPackagesIdents,
ImmutableArray<ContentPackage> loadingOrder)
where T : IBaseResourceInfo
{
return resources
.Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform))
.Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget))
.Where(r => !r.Optional || (
(r.RequiredPackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.RequiredPackages).Any())
&& (r.IncompatiblePackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.IncompatiblePackages).None())))
.OrderBy(r => r.Optional ? 1 : 0) // optional content last
.ThenBy(r => loadingOrder.IndexOf(r.OwnerPackage))
.ThenBy(r => r.LoadPriority)
.ToImmutableArray();
}
public FluentResults.Result SyncLoadedPackagesList(ImmutableArray<ContentPackage> packages)
{
if (packages.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException(nameof(packages));
if (!_runningPackages.IsEmpty)
ThrowHelper.ThrowInvalidOperationException($"{nameof(SyncLoadedPackagesList)}: There are packages running!");
var toRemove = _loadedPackages.Keys.Except(packages).ToImmutableArray();
var toAdd = packages.Except(_loadedPackages.Keys)
.OrderBy(pack => packages.IndexOf(pack)).ToImmutableArray();
var result = new FluentResults.Result();
if (!toRemove.IsDefaultOrEmpty)
{
result.WithReasons(UnloadPackages(toRemove).Reasons);
}
if (!toAdd.IsDefaultOrEmpty)
{
result.WithReasons(LoadPackagesInfo(toAdd).Reasons);
}
return result;
}
public FluentResults.Result StopRunningPackages()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.IsEmpty || _runningPackages.IsEmpty)
{
_logger.LogWarning($"{nameof(StopRunningPackages)}: No packages are currently executing.");
return FluentResults.Result.Ok();
}
var res = new FluentResults.Result();
res.WithReasons(_luaScriptManagementService.UnloadActiveScripts().Reasons);
res.WithReasons(_pluginManagementService.UnloadManagedAssemblies().Reasons);
_runningPackages.Clear();
return res;
}
public FluentResults.Result UnloadPackage(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_loadedPackages.ContainsKey(package))
{
return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: The package is not loaded.");
}
if (!_runningPackages.IsEmpty)
{
return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: Packages are currently executing.");
}
var result = new FluentResults.Result();
result.WithReasons(_luaScriptManagementService.DisposePackageResources(package).Reasons);
result.WithReasons(_configService.DisposePackageData(package).Reasons);
#if CLIENT
result.WithReasons(_uiStylesService.UnloadPackage(package).Reasons);
#endif
_loadedPackages.TryRemove(package, out _);
_packageNameCache.TryRemove(package.Name, out _);
return result;
}
public FluentResults.Result UnloadPackages(ImmutableArray<ContentPackage> packages)
{
if (packages.IsDefaultOrEmpty)
return FluentResults.Result.Fail($"{nameof(UnloadPackages)}: Package list is empty.");
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = new FluentResults.Result();
foreach (var package in packages)
{
result.WithReasons(UnloadPackage(package).Reasons);
}
return result;
}
public FluentResults.Result UnloadAllPackages()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.IsEmpty)
return FluentResults.Result.Ok();
if (!_runningPackages.IsEmpty)
return FluentResults.Result.Fail($"{nameof(UnloadAllPackages)}: Packages are currently executing.");
var result = new FluentResults.Result();
result.WithReasons(_luaScriptManagementService.DisposeAllPackageResources().Reasons);
result.WithReasons(_configService.DisposeAllPackageData().Reasons);
_loadedPackages.Clear();
return result;
}
public ImmutableArray<ContentPackage> GetAllLoadedPackages()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return [.._loadedPackages.Keys];
}
public bool IsPackageRunning(ContentPackage package)
{
Guard.IsNotNull(package, nameof(package));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return _runningPackages.ContainsKey(package);
}
public bool IsAnyPackageLoaded()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return !_loadedPackages.IsEmpty;
}
public bool IsAnyPackageRunning()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
return !_runningPackages.IsEmpty;
}
public ImmutableArray<ContentPackage> GetLoadedUnrestrictedPackages()
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_loadedPackages.IsEmpty)
return ImmutableArray<ContentPackage>.Empty;
return [.._loadedPackages.Values
.Where(cfg => !cfg.Assemblies.IsDefaultOrEmpty || cfg.LuaScripts.Any(scr => scr.RunUnrestricted))
.Select(cfg => cfg.Package)];
}
public bool PackageContainsAnyRunnableResource(ContentPackage package)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var result = GetModConfigForPackage(package);
if (result.IsSuccess)
{
return result.Value.Assemblies.Any() || result.Value.LuaScripts.Any();
}
else
{
return false;
}
}
public Result<IModConfigInfo> GetModConfigForPackage(ContentPackage package)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_loadedPackages.TryGetValue(package, out var modConfig))
{
return FluentResults.Result.Fail($"Failed to find mod config for package {package.Name}");
}
return new FluentResults.Result<IModConfigInfo>().WithValue(modConfig);
}
}
@@ -0,0 +1,962 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using System.Xml.Serialization;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events;
using FluentResults;
using FluentResults.LuaCs;
using LightInject;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Diagnostics;
using OneOf;
namespace Barotrauma.LuaCs;
public class PluginManagementService : IAssemblyManagementService
{
#region CSHARP_COMPILATION_OPTIONS
private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default
.WithPreprocessorSymbols(new[]
{
#if SERVER
"SERVER"
#elif CLIENT
"CLIENT"
#else
"UNDEFINED"
#endif
#if DEBUG
,"DEBUG"
#endif
});
#if WINDOWS
private const string PLATFORM_TARGET = "Windows";
#elif OSX
private const string PLATFORM_TARGET = "OSX";
#elif LINUX
private const string PLATFORM_TARGET = "Linux";
#endif
#if CLIENT
private const string ARCHITECTURE_TARGET = "Client";
#elif SERVER
private const string ARCHITECTURE_TARGET = "Server";
#endif
private static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithMetadataImportOptions(MetadataImportOptions.All)
#if DEBUG
.WithOptimizationLevel(OptimizationLevel.Debug)
#else
.WithOptimizationLevel(OptimizationLevel.Release)
#endif
.WithAllowUnsafe(true);
private static readonly SyntaxTree BaseAssemblyImports = CSharpSyntaxTree.ParseText(
new StringBuilder()
.AppendLine("global using LuaCsHook = Barotrauma.LuaCs.Compatibility.ILuaCsHook;")
.AppendLine("global using System.Reflection;")
.AppendLine("global using Barotrauma;")
.AppendLine("global using Barotrauma.LuaCs;")
.AppendLine("global using Barotrauma.LuaCs.Compatibility;")
.AppendLine("using System.Runtime.CompilerServices;")
.AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]")
#if CLIENT
.AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]")
#elif SERVER
.AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]")
#endif
.ToString(),
ScriptParseOptions);
private ImmutableArray<MetadataReference> _baseMetadataReferences = ImmutableArray<MetadataReference>.Empty;
private ImmutableArray<MetadataReference> _baseMetadataReferencesNonPublicized = ImmutableArray<MetadataReference>.Empty;
private IEnumerable<MetadataReference> BaseMetadataReferences
{
get
{
if (_baseMetadataReferences.IsDefaultOrEmpty)
{
_baseMetadataReferences = Basic.Reference.Assemblies.Net80.References.All
.Union(AssemblyLoadContext.Default.Assemblies
.Where(ass =>
!ass.IsDynamic &&
!ass.GetName().FullName.StartsWith("BarotraumaCore") &&
!ass.GetName().FullName.StartsWith("Barotrauma") &&
!ass.GetName().FullName.StartsWith("DedicatedServer"))
.Where(ass => !ass.Location.IsNullOrWhiteSpace())
.Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location)))
.Where(ar => ar is not null)
.ToImmutableArray();
}
return _baseMetadataReferences;
}
}
private IEnumerable<MetadataReference> BaseMetadataReferencesWithBarotrauma
{
get
{
if (_baseMetadataReferencesNonPublicized.IsDefaultOrEmpty)
{
_baseMetadataReferencesNonPublicized = Basic.Reference.Assemblies.Net80.References.All
.Union(AssemblyLoadContext.Default.Assemblies
.Where(ass => !ass.IsDynamic)
.Where(ass => !ass.Location.IsNullOrWhiteSpace())
.Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location)))
.Where(ar => ar is not null)
.ToImmutableArray();
}
return _baseMetadataReferencesNonPublicized;
}
}
#endregion
#region Disposal
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
UnsafeDisposeResourcesInternal();
_assemblyLoaderFactory = null;
_storageService = null;
_eventService = null;
_logger = null;
_configService = null;
_luaScriptManagementService = null;
_luaCsInfoProvider = null;
GC.SuppressFinalize(this);
}
private void UnsafeDisposeResourcesInternal()
{
foreach (var packPlugin in _pluginInstances.SelectMany(kvp => kvp.Value.Select(pluginInst => (kvp.Key, pluginInst))))
{
try
{
packPlugin.pluginInst.Dispose();
}
catch (Exception e)
{
_logger.LogError($"Error while disposing plugin for ContentPackage {packPlugin.Key.Name}: \n{e.Message}");
}
}
_pluginInstances.Clear();
_pluginPackageLookup.Clear();
_pluginInjectorContainer?.Dispose();
_pluginInjectorContainer = null;
foreach (var loader in _assemblyLoaders)
{
try
{
loader.Value.Dispose();
_unloadingAssemblyLoaders.Add(loader.Value, loader.Key);
}
catch (Exception e)
{
_logger?.LogError($"Failed to dispose of {nameof(IAssemblyLoaderService)} for ContentPackage {loader.Key.Name}: \n{e.Message}");
if (loader.Value.Assemblies.Any())
{
foreach (var ass in loader.Value.Assemblies)
{
_logger?.LogWarning($"{nameof(PluginManagementService)}: Fallback manual unsubscription of assemblies: {ass.GetName()}");
ReflectionUtils.RemoveAssemblyFromCache(ass);
}
}
}
}
_assemblyLoaders.Clear();
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public FluentResults.Result Reset()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
UnsafeDisposeResourcesInternal();
return FluentResults.Result.Ok();
}
#endregion
private IAssemblyLoaderService.IFactory _assemblyLoaderFactory;
private IStorageService _storageService;
private ILoggerService _logger;
private Lazy<IEventService> _eventService;
private Lazy<IConfigService> _configService;
private Lazy<ILuaScriptManagementService> _luaScriptManagementService;
private IEventService _pluginEventService;
private Lazy<ILuaPatcher> _pluginLuaPatcherService;
private Func<IConsoleCommandsService> _consoleCommandServiceFactory;
private ILuaCsInfoProvider _luaCsInfoProvider;
private readonly ConcurrentDictionary<ContentPackage, IAssemblyLoaderService> _assemblyLoaders = new();
private readonly ConcurrentDictionary<Type, ContentPackage> _pluginPackageLookup = new();
private readonly ConcurrentDictionary<ContentPackage, ImmutableArray<IAssemblyPlugin>> _pluginInstances = new();
private readonly ConditionalWeakTable<IAssemblyLoaderService, ContentPackage> _unloadingAssemblyLoaders = new();
private readonly ConcurrentBag<IntPtr> _loadedNativeLibraries = new();
private readonly AsyncReaderWriterLock _operationsLock = new();
private ServiceContainer _pluginInjectorContainer;
public PluginManagementService(
IAssemblyLoaderService.IFactory assemblyLoaderFactory,
IStorageService storageService,
ILoggerService logger,
Lazy<IEventService> eventService,
Lazy<ILuaScriptManagementService> luaScriptManagementService,
Lazy<IConfigService> configService,
Lazy<ILuaPatcher> pluginLuaPatcherService,
Func<IConsoleCommandsService> consoleCommandServiceFactory,
ILuaCsInfoProvider luaCsInfoProvider)
{
_assemblyLoaderFactory = assemblyLoaderFactory;
_storageService = storageService;
_logger = logger;
_eventService = eventService;
_luaScriptManagementService = luaScriptManagementService;
_configService = configService;
_pluginLuaPatcherService = pluginLuaPatcherService;
_consoleCommandServiceFactory = consoleCommandServiceFactory;
_luaCsInfoProvider = luaCsInfoProvider;
}
private ServiceContainer CreatePluginServiceContainer()
{
var container = new ServiceContainer(new ContainerOptions()
{
EnablePropertyInjection = true
});
_pluginEventService ??= new EventService(_logger, _pluginLuaPatcherService.Value);
_eventService.Value.AddDispatcherEventService(_pluginEventService);
container.Register<ILoggerService>(fac => _logger);
container.Register<IStorageService>(fac => _storageService);
container.Register<IEventService>(fac => _pluginEventService);
container.Register<IPluginManagementService>(fac => this);
container.Register<ILuaScriptManagementService>(fac => _luaScriptManagementService.Value);
container.Register<IConfigService>(fac => _configService.Value);
container.Register<IConsoleCommandsService>(fac => _consoleCommandServiceFactory?.Invoke());
return container;
}
public Result<ImmutableArray<Type>> GetImplementingTypes<T>(bool includeInterfaces = false, bool includeAbstractTypes = false,
bool includeDefaultContext = true)
{
if (includeInterfaces)
{
includeAbstractTypes = true;
}
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var builder = ImmutableArray.CreateBuilder<Type>();
if (includeDefaultContext)
{
foreach (var ass in AssemblyLoadContext.Default.Assemblies)
{
AddTypesFromAssembly(ass);
}
}
foreach (var ass in _assemblyLoaders.Values.Where(al => !al.IsReferenceOnlyMode).SelectMany(al => al.Assemblies))
{
AddTypesFromAssembly(ass);
}
return builder.ToImmutable();
void AddTypesFromAssembly(Assembly assembly)
{
foreach (var type in assembly.GetSafeTypes())
{
if ((includeInterfaces || !type.IsInterface)
&& (includeAbstractTypes || !type.IsAbstract)
&& type.IsAssignableTo(typeof(T)))
{
builder.Add(type);
}
}
}
}
public bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage)
{
return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage);
}
public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false,
bool includeDefaultContext = true)
{
if (typeName.StartsWith("out ") || typeName.StartsWith("ref "))
{
typeName = typeName.Remove(0, 4);
isByRefType = true;
}
if (includeDefaultContext)
{
var type = Type.GetType(typeName, false, false);
if (type is not null && (includeInterfaces || !type.IsInterface))
{
if (isByRefType)
{
return type.MakeByRefType();
}
return type;
}
foreach (var ass in AssemblyLoadContext.Default.Assemblies)
{
if (ass.GetType(typeName, false, false) is not {} type2 || (!includeInterfaces && type2.IsInterface))
{
continue;
}
return isByRefType ? type2.MakeByRefType() : type2;
}
}
foreach (var ass in AssemblyLoadContext.All
.Where(alc => alc != AssemblyLoadContext.Default)
.SelectMany(alc => alc.Assemblies))
{
if (ass.GetType(typeName, false, false) is not {} type || (!includeInterfaces && type.IsInterface))
{
continue;
}
return isByRefType ? type.MakeByRefType() : type;
}
return null;
}
public FluentResults.Result ActivatePluginInstances(ImmutableArray<ContentPackage> executionOrder, bool excludeAlreadyRunningPackages = true)
{
if (executionOrder.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(ActivatePluginInstances)}: The ececution list provided is empty.");
}
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_assemblyLoaders.IsEmpty)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
var toLoad = _assemblyLoaders
.Where(al => executionOrder.Contains(al.Key))
.Where(al => !excludeAlreadyRunningPackages || !_pluginInstances.ContainsKey(al.Key))
.SelectMany(al => al.Value.Assemblies.Select(ass => (al.Key, ass)))
.SelectMany<(ContentPackage Key, Assembly ass), (ContentPackage Key, Type type)>(kvp =>
{
try
{
return kvp.ass.GetTypes()
.Where(type =>
type is { IsInterface: false, IsAbstract: false, IsGenericType: false }
&& type.IsAssignableTo(typeof(IAssemblyPlugin)))
.Select(type => (kvp.Key, type));
}
catch (ReflectionTypeLoadException re)
{
results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'"));
results.WithError(new ExceptionalError(re));
}
catch (Exception e)
{
results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'"));
results.WithError(new ExceptionalError(e));
}
return new List<(ContentPackage Key, Type type)>();
})
.GroupBy(kvp => kvp.Key, kvp => kvp.type)
.OrderBy(exeGrp => executionOrder.IndexOf(exeGrp.Key))
.ToImmutableArray();
if (toLoad.Length == 0)
{
return results;
}
_logger.LogMessage($"Activating {nameof(IAssemblyPlugin)} instances");
var loadedPackagePlugins =
ImmutableArray.CreateBuilder<(ContentPackage Package, ImmutableArray<IAssemblyPlugin> Plugins)>();
_pluginInjectorContainer ??= CreatePluginServiceContainer();
foreach (var packageTypes in toLoad)
{
var loadedTypes = ImmutableArray.CreateBuilder<IAssemblyPlugin>();
foreach (var pluginType in packageTypes)
{
try
{
_logger.LogMessage($"- Instantiating {pluginType.Name}");
var plugin = (IAssemblyPlugin)Activator.CreateInstance(pluginType);
_pluginInjectorContainer.InjectProperties(plugin);
_pluginInjectorContainer.Register(pluginType, fac => plugin);
loadedTypes.Add(plugin);
_pluginPackageLookup.TryAdd(pluginType, packageTypes.Key);
}
catch (Exception e)
{
results.WithError(new ExceptionalError($"Failed to instantiate mod: {packageTypes.Key.Name}", e));
continue;
}
}
loadedPackagePlugins.Add((packageTypes.Key, loadedTypes.ToImmutable()));
}
var packPluginGroups = loadedPackagePlugins.ToImmutable();
foreach (var packagePluginGrp in packPluginGroups)
{
if (_pluginInstances.TryGetValue(packagePluginGrp.Package, out var plugins))
{
_pluginInstances[packagePluginGrp.Package] = plugins.Concat(packagePluginGrp.Plugins).ToImmutableArray();
continue;
}
_pluginInstances[packagePluginGrp.Package] = packagePluginGrp.Plugins;
}
var pluginsToInit = packPluginGroups.SelectMany(ppg => ppg.Plugins).ToImmutableArray();
foreach (var plugin in pluginsToInit)
{
results.WithReasons(PluginInitRunner(plugin, p => p.PreInitPatching()).Reasons);
}
_eventService.Value.PublishEvent<IEventPluginPreInitialize>(sub => sub.PreInitPatching());
foreach (var plugin in pluginsToInit)
{
results.WithReasons(PluginInitRunner(plugin, p => p.Initialize()).Reasons);
}
_eventService.Value.PublishEvent<IEventPluginInitialize>(sub => sub.Initialize());
foreach (var plugin in pluginsToInit)
{
results.WithReasons(PluginInitRunner(plugin, p => p.OnLoadCompleted()).Reasons);
}
_eventService.Value.PublishEvent<IEventPluginLoadCompleted>(sub => sub.OnLoadCompleted());
return results;
// helper
FluentResults.Result PluginInitRunner(IAssemblyPlugin plugin, Action<IAssemblyPlugin> action)
{
try
{
action(plugin);
return FluentResults.Result.Ok();
}
catch (Exception e)
{
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
}
public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources)
{
if (resources.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAssemblyResources)} The resource list is empty.)");
}
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_storageService.UseCaching = _luaCsInfoProvider.UseCaching;
if (!_luaCsInfoProvider.UseCaching)
{
_storageService.PurgeCache();
}
var orderedContentPacks = resources.GroupBy(res => res.OwnerPackage)
.OrderBy(res => resources.FindIndex(r2 => r2.OwnerPackage == res.Key))
.ToImmutableArray();
var result = new FluentResults.Result();
foreach (var contentPack in orderedContentPacks)
{
LoadBinaries(contentPack);
LoadAndCompileScriptAssemblies(contentPack);
foreach (var ass in _assemblyLoaders[contentPack.Key].Assemblies)
{
ReflectionUtils.AddNonAbstractAssemblyTypes(ass);
}
}
return result;
// --- helper methods
void LoadBinaries(IGrouping<ContentPackage,IAssemblyResourceInfo> contentPackRes)
{
var binaries = contentPackRes.Where(cRes => !cRes.IsScript)
.OrderBy(bin => bin.LoadPriority)
.SelectMany(bin => bin.FilePaths)
.ToImmutableArray();
if (binaries.IsDefaultOrEmpty)
{
return;
}
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
new IAssemblyLoaderService.LoaderInitData(
InstanceId: Guid.NewGuid(),
contentPackRes.Key.Name,
IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly),
OwnerPackage: contentPackRes.Key,
OnUnload: OnAssemblyLoaderUnloading,
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
)));
var dependencyPaths = binaries
.Select(bin => System.IO.Path.GetDirectoryName(bin.FullPath))
.Distinct()
.ToImmutableArray();
foreach (var binResource in binaries)
{
var res = assemblyLoader.LoadAssemblyFromFile(binResource.FullPath, dependencyPaths);
result.WithReasons(res.Reasons);
_logger.LogResults(res.ToResult());
}
}
void LoadAndCompileScriptAssemblies(IGrouping<ContentPackage, IAssemblyResourceInfo> contentPackRes)
{
var scriptsGrp = contentPackRes.Where(cRes => cRes.IsScript)
.Select(scr => (scr.OwnerPackage, scr.FriendlyName, scr.FilePaths, scr.UseInternalAccessName, scr.LoadPriority))
.OrderBy(scr => scr.LoadPriority)
.GroupBy(scr => scr.FriendlyName)
.ToImmutableArray();
if (scriptsGrp.IsDefaultOrEmpty)
{
return;
}
var metadataReferences = GetMetadataReferences(false).ToImmutableArray();
var metadataReferencesNonPublicized = GetMetadataReferences(true).ToImmutableArray();
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
new IAssemblyLoaderService.LoaderInitData(
InstanceId: Guid.NewGuid(),
contentPackRes.Key.Name,
IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly),
OwnerPackage: contentPackRes.Key,
OnUnload: OnAssemblyLoaderUnloading,
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
)));
// create syntax trees
foreach (var scripts in scriptsGrp)
{
var syntaxTreesBuilder = ImmutableArray.CreateBuilder<SyntaxTree>();
bool hasInternalsAwareBeenAdded = false;
bool compileWithInternalName = true;
foreach (var resourceInfo in scripts)
{
// this should be the same for the entire collection of src files so we just grab it from the collection
compileWithInternalName = resourceInfo.UseInternalAccessName;
if (!hasInternalsAwareBeenAdded)
{
hasInternalsAwareBeenAdded = true;
syntaxTreesBuilder.Add(BaseAssemblyImports);
}
if (resourceInfo.FilePaths.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAndCompileScriptAssemblies)} The resource list is empty for package {resourceInfo.OwnerPackage}.");
}
foreach (var resourcePath in resourceInfo.FilePaths)
{
var loadRes = GetSourceFilesText(resourcePath);
if (loadRes.IsFailed)
{
_logger.LogResults(loadRes.ToResult());
continue;
}
CancellationToken token = CancellationToken.None;
string sourceCode = loadRes.Value;
sourceCode = DoSourceCodeTextCompatibilityPass(sourceCode);
syntaxTreesBuilder.Add(SyntaxFactory.ParseSyntaxTree(
text: sourceCode,
options: ScriptParseOptions,
path: resourcePath.FullPath,
encoding: Encoding.Default,
cancellationToken: token
));
}
}
if (syntaxTreesBuilder.Count < 1)
{
continue;
}
_logger.LogMessage($"Compiling assembly for {scripts.Key}, in ContentPackage {contentPackRes.Key.Name}");
var res = assemblyLoader.CompileScriptAssembly(
assemblyName: scripts.Key,
compileWithInternalAccess: compileWithInternalName,
syntaxTrees: syntaxTreesBuilder.ToImmutable(),
metadataReferences: compileWithInternalName ? metadataReferencesNonPublicized : metadataReferences,
compilationOptions: CompilationOptions);
// try with internal access instead for legacy mods
if (!compileWithInternalName && res.IsFailed)
{
_logger.LogMessage($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name}. Trying fallback method.");
var res2 = assemblyLoader.CompileScriptAssembly(
assemblyName: scripts.Key,
compileWithInternalAccess: true,
syntaxTrees: syntaxTreesBuilder.ToImmutable(),
metadataReferences: metadataReferencesNonPublicized,
compilationOptions: CompilationOptions);
// overwrite result with good compilation
if (res2.IsSuccess)
{
var reasonsStr = res.Reasons.Aggregate("", (accum, reason) => accum + "\n" + reason.Message);
_logger.LogWarning($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name} succeeded. Original errors were: \n {reasonsStr}");
res = res2;
}
}
result.WithReasons(res.Reasons);
}
}
Result<string> GetSourceFilesText(ContentPath resourceInfoFilePath)
{
if (_storageService.LoadPackageText(resourceInfoFilePath) is not { IsFailed: false } res)
{
_logger.LogError($"{nameof(GetSourceFilesText)}: Failed to load source file for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}.");
return FluentResults.Result.Fail($"{nameof(GetSourceFilesText)}: Failed to load source files for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}.");
}
return res;
}
IEnumerable<MetadataReference> GetMetadataReferences(bool useNonPublicizedAssemblies)
{
var builder = ImmutableArray.CreateBuilder<MetadataReference>();
if (useNonPublicizedAssemblies)
{
builder.AddRange(BaseMetadataReferencesWithBarotrauma);
foreach (var loaderService in _assemblyLoaders
.Where(asl => !asl.Key.Name.Equals("LuaCsForBarotrauma", StringComparison.InvariantCultureIgnoreCase))
.ToImmutableArray())
{
builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null));
}
}
else
{
builder.AddRange(BaseMetadataReferences);
foreach (var loaderService in _assemblyLoaders)
{
builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null));
}
}
return builder.ToImmutable();
}
}
private string DoSourceCodeTextCompatibilityPass(string sourceCode)
{
return sourceCode
.Replace("GameMain.LuaCs", "LuaCsSetup.Instance")
.Replace(" Client.ClientList", " ModUtils.Client.ClientList")
.Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList")
.Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab");
}
private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName)
{
Guard.IsNull(callerAssembly, nameof(callerAssembly));
Guard.IsNullOrWhiteSpace(targetAssemblyName, nameof(targetAssemblyName));
if (AssemblyLoadContext.GetLoadContext(callerAssembly) is not IAssemblyLoaderService loaderService)
{
return IntPtr.Zero;
}
var targetDirectory = Path.GetFullPath(loaderService.OwnerPackage.Dir);
if (!targetAssemblyName.TrimEnd().EndsWith(".dll"))
{
targetAssemblyName += ".dll";
}
var res = _storageService.FindFilesInPackage(loaderService.OwnerPackage, string.Empty, targetAssemblyName, true);
if (res.IsFailed || !res.Value.Any())
{
return IntPtr.Zero;
}
foreach (var path in res.Value)
{
if (System.Runtime.InteropServices.NativeLibrary.TryLoad(path, out IntPtr asmPtr))
{
_loadedNativeLibraries.Add(asmPtr);
return asmPtr;
}
}
return IntPtr.Zero;
}
private Assembly OnAssemblyLoaderResolvingManaged(IAssemblyLoaderService requestingLoader, AssemblyName searchName)
{
// This method is used during assembly instantiation, we cannot put a lock here.
//using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
foreach (var loader in _assemblyLoaders.Where(kvp => kvp.Value != requestingLoader)
.Select(kvp => kvp.Value).ToImmutableArray())
{
if (loader.IsReferenceOnlyMode || !loader.Assemblies.Any())
{
continue;
}
foreach (var assembly in loader.Assemblies)
{
if (assembly.GetName().FullName == searchName.FullName)
{
return assembly;
}
}
}
return null;
}
private void OnAssemblyLoaderUnloading(IAssemblyLoaderService loader)
{
if (!loader.Assemblies.Any())
{
return;
}
foreach (var assembly in loader.Assemblies)
{
_eventService?.Value?.PublishEvent<IEventAssemblyUnloading>(sub => sub.OnAssemblyUnloading(assembly));
}
}
public FluentResults.Result UnloadManagedAssemblies()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_assemblyLoaders.Count == 0)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons);
ReflectionUtils.ResetCache();
foreach (var loaderService in _assemblyLoaders)
{
try
{
loaderService.Value.Dispose();
_unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key);
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
}
}
_assemblyLoaders.Clear();
_storageService.PurgeCache();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
#if DEBUG
// Print still loaded assembly load ctx after giving some time
CoroutineManager.Invoke(() =>
{
if (!_unloadingAssemblyLoaders.Any())
{
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
{
sb.AppendLine($"- '{kvp.Value.Name}'");
}
// Use DebugConsole in case logger is null by the time this executes.
if (_logger is null)
{
DebugConsole.LogError(sb.ToString());
}
else
{
_logger.LogWarning(sb.ToString());
}
}, 3.0f);
#endif
// clear native libraries
if (_loadedNativeLibraries.Any())
{
foreach (var ptr in _loadedNativeLibraries)
{
try
{
System.Runtime.InteropServices.NativeLibrary.Free(ptr);
}
catch
{
// ignored
continue;
}
}
_loadedNativeLibraries.Clear();
}
return results;
}
private FluentResults.Result UnsafeDisposeManagedTypeInstances()
{
var results = new FluentResults.Result();
if (!_pluginInstances.IsEmpty)
{
foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value))
{
try
{
instance.Dispose();
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
continue;
}
}
}
if (_pluginEventService is not null)
{
_eventService.Value.RemoveDispatcherEventService(_pluginEventService);
_pluginEventService = null;
}
_pluginInjectorContainer = null;
_pluginInstances.Clear();
_pluginPackageLookup.Clear();
return results;
}
public Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts)
{
using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var guids = excludedContexts;
return assemblyName.Match<Assembly>((AssemblyName asm) =>
{
foreach (var ass in _assemblyLoaders.Values
.Where(al => guids.Length == 0 || !guids.Contains(al.Id))
.SelectMany(al => al.Assemblies)
.ToImmutableArray())
{
if (ass.GetName() == asm)
{
return ass;
}
}
return null;
},
(string asmName) =>
{
foreach (var ass in _assemblyLoaders.Values.SelectMany(al => al.Assemblies))
{
if (ass.GetName().Name?.Equals(asmName) ?? ass.GetName().FullName.Equals(asmName))
{
return ass;
}
}
return null;
});
}
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public class PluginService
{
}
@@ -0,0 +1,230 @@
using Barotrauma.IO;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using FarseerPhysics.Common;
using FluentResults;
using FluentResults.LuaCs;
using Microsoft.Toolkit.Diagnostics;
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Path = System.IO.Path;
namespace Barotrauma.LuaCs;
public class SafeStorageService : StorageService, ISafeStorageService
{
private ConcurrentDictionary<string, byte>
_fileListRead = new (),
_fileListWrite = new();
private readonly AsyncReaderWriterLock _higherOperationsLock = new();
public SafeStorageService(IStorageServiceConfig configData) : base(configData)
{
IsReadOperationAllowedEval = (fp) => IsFileAccessible(fp, true, true);
IsWriteOperationAllowedEval = (fp) => IsFileAccessible(fp, false, true);
}
private string GetFullPath(string path) => System.IO.Path.GetFullPath(path).CleanUpPathCrossPlatform();
public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
try
{
path = GetFullPath(path);
if (path.StartsWith(ConfigData.WorkshopModsDirectory)
|| path.StartsWith(ConfigData.LocalModsDirectory)
#if CLIENT
|| path.StartsWith(ConfigData.TempDownloadsDirectory)
#endif
)
{
return true;
}
if (!_fileListRead.ContainsKey(path))
{
return false;
}
if (!readOnly && !_fileListWrite.ContainsKey(path))
{
return false;
}
if (checkWhitelistOnly)
{
return true;
}
using var fs = System.IO.File.Open(
path, FileMode.Open, readOnly ? FileAccess.Read : FileAccess.ReadWrite, FileShare.ReadWrite);
return readOnly ? fs.CanRead : fs.CanWrite;
}
catch
{
return false;
}
}
public void AddFileToWhitelist(string path, bool readOnly = true)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
try
{
path = GetFullPath(path);
_fileListRead.AddOrUpdate(path, s => 0, (s, b) => 0);
if (!readOnly)
{
_fileListWrite.AddOrUpdate(path, s => 0, (s, b) => 0);
}
}
catch
{
return;
}
}
public void AddFilesToWhitelist(ImmutableArray<string> paths, bool readOnly = true)
{
if (paths.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException(nameof(paths));
foreach (var path in paths)
{
AddFileToWhitelist(path, readOnly);
}
}
public void RemoveFileFromAllWhitelists(string path)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
try
{
path = GetFullPath(path);
_fileListRead.TryRemove(path, out _);
_fileListWrite.TryRemove(path, out _);
}
catch
{
return;
}
}
public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths)
{
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (filePaths.IsDefaultOrEmpty)
{
return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty.");
}
_fileListRead.Clear();
var res = new FluentResults.Result();
foreach (var path in filePaths)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
try
{
var p = Path.GetFullPath(path.CleanUpPathCrossPlatform());
if (_fileListRead.ContainsKey(p))
{
res = res.WithReason(new Success($"Path already in whitelist: {p}"));
continue;
}
if (_fileListRead.TryAdd(p, 0))
{
res = res.WithSuccess($"Added path successfully: {p}");
continue;
}
res = res.WithError(new Error($"Failed to add path to list: {p}"));
}
catch (Exception e)
{
res = res.WithError(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.ExceptionDetails, e.Message)
.WithMetadata(MetadataType.RootObject, path)
);
continue;
}
}
return res;
}
public FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths)
{
if (filePaths.IsDefaultOrEmpty)
{
return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty.");
}
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_fileListRead.Clear();
_fileListWrite.Clear();
var res = new FluentResults.Result();
foreach (var path in filePaths)
{
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
try
{
var p = Path.GetFullPath(path.CleanUpPathCrossPlatform());
TryAddToList(_fileListRead, p);
TryAddToList(_fileListWrite, p);
res = res.WithError(new Error($"Failed to add path to list: {p}"));
}
catch (Exception e)
{
res = res.WithError(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.ExceptionDetails, e.Message)
.WithMetadata(MetadataType.RootObject, path)
);
continue;
}
}
void TryAddToList(ConcurrentDictionary<string, byte> dict, string p)
{
if (dict.ContainsKey(p))
{
res = res.WithReason(new Success($"Path already in whitelist: {p}"));
return;
}
if (dict.TryAdd(p, 0))
{
res = res.WithSuccess($"Added path successfully: {p}");
return;
}
}
return res;
}
public void ClearAllWhitelists()
{
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_fileListRead.Clear();
_fileListWrite.Clear();
}
}
@@ -0,0 +1,295 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using LightInject;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public class ServicesProvider : IServicesProvider
{
private ServiceContainer _serviceContainerInst;
private ServiceContainer ServiceContainer => _serviceContainerInst;
/// <summary>
/// Definition: [Key: ConcreteType, Value: TypeInstance]
/// </summary>
private ImmutableArray<ISystem> _systemInstances = ImmutableArray<ISystem>.Empty;
private readonly ReaderWriterLockSlim _serviceLock = new();
public ServicesProvider()
{
_serviceContainerInst = new ServiceContainer(new ContainerOptions()
{
EnablePropertyInjection = false
});
//_serviceContainerInst.Register<IServicesProvider>((f) => this);
}
public void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
{
// ISystem services must run as a lifetime singleton
if (typeof(TSvcInterface).IsAssignableTo(typeof(ISystem)))
{
lifetimeInstance = new PerContainerLifetime();
}
if (lifetimeInstance is null)
{
switch (lifetime)
{
case ServiceLifetime.Singleton:
lifetimeInstance = new PerContainerLifetime();
break;
case ServiceLifetime.PerThread:
lifetimeInstance = new PerThreadLifetime();
break;
// treat these as transient
case ServiceLifetime.Transient:
case ServiceLifetime.Invalid:
case ServiceLifetime.Custom:
default:
lifetimeInstance = null;
break;
}
}
try
{
_serviceLock.EnterReadLock();
if (lifetimeInstance is not null)
ServiceContainer.Register<TSvcInterface, TService>(lifetimeInstance);
else
ServiceContainer.Register<TSvcInterface, TService>();
}
finally
{
_serviceLock.ExitReadLock();
}
}
public void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime,
ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
{
if (name.IsNullOrWhiteSpace())
{
throw new ArgumentNullException($"Tried to register a service of type {typeof(TService).Name} but the name provided is null or empty." );
}
// ISystem services must run as a lifetime singleton
if (typeof(TService).IsAssignableTo(typeof(ISystem)))
{
lifetimeInstance = new PerContainerLifetime();
}
if (lifetimeInstance is null)
{
switch (lifetime)
{
case ServiceLifetime.Singleton:
lifetimeInstance = new PerContainerLifetime();
break;
case ServiceLifetime.PerThread:
lifetimeInstance = new PerThreadLifetime();
break;
// treat these as transient
case ServiceLifetime.Transient:
case ServiceLifetime.Invalid:
case ServiceLifetime.Custom: // lifetime should not be null here
default:
lifetimeInstance = new PerRequestLifeTime();
break;
}
}
try
{
_serviceLock.EnterReadLock();
ServiceContainer.Register<TSvcInterface, TService>(name, lifetimeInstance);
}
finally
{
_serviceLock.ExitReadLock();
}
}
public void RegisterServiceResolver<TSvcInterface>(Func<ServiceContainer, TSvcInterface> factory) where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
ServiceContainer.Register<TSvcInterface>(f => factory(ServiceContainer));
}
finally
{
_serviceLock.ExitReadLock();
}
}
public void CompileAndRun()
{
try
{
_serviceLock.EnterWriteLock();
ServiceContainer!.Compile();
if (!_systemInstances.IsDefaultOrEmpty)
{
ThrowHelper.ThrowInvalidOperationException($"Systems are already instanced!");
}
_systemInstances = ServiceContainer.GetAllInstances(typeof(ISystem))
.Select(obj => (ISystem)obj)
.ToImmutableArray();
}
finally
{
_serviceLock.ExitWriteLock();
}
}
public void InjectServices<T>(T inst) where T : class
{
try
{
_serviceLock.EnterReadLock();
ServiceContainer.InjectProperties(inst);
}
finally
{
_serviceLock.ExitReadLock();
}
}
public bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
service = ServiceContainer.TryGetInstance<TSvcInterface>();
return service is not null;
}
catch
{
service = null;
return false;
}
finally
{
_serviceLock.ExitReadLock();
}
}
public TSvcInterface GetService<TSvcInterface>() where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
return ServiceContainer.GetInstance<TSvcInterface>();
}
finally
{
_serviceLock.ExitReadLock();
}
}
public bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService
{
try
{
_serviceLock.EnterReadLock();
service = ServiceContainer.TryGetInstance<TSvcInterface>(name);
return service is not null;
}
catch
{
service = null;
return false;
}
finally
{
_serviceLock.ExitReadLock();
}
}
public event Action<Type, IService> OnServiceInstanced;
public ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService
{
try
{
_serviceLock.EnterReadLock();
return ServiceContainer.GetAllInstances<TSvc>().ToImmutableArray();
}
finally
{
_serviceLock.ExitReadLock();
}
}
[MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.Synchronized)]
public void DisposeAndReset()
{
// Plugins should never be allowed to execute this.
if (Assembly.GetCallingAssembly() != Assembly.GetExecutingAssembly())
{
throw new MethodAccessException(
$"Assembly {Assembly.GetCallingAssembly().FullName} attempted to call {nameof(DisposeAndReset)}().");
}
try
{
_serviceLock.EnterWriteLock();
foreach (var system in _systemInstances)
{
try
{
system.Dispose();
}
catch (Exception e)
{
// ignored, no logging services available.
}
}
_systemInstances = ImmutableArray<ISystem>.Empty;
_serviceContainerInst?.Dispose();
_serviceContainerInst = new ServiceContainer();
}
finally
{
_serviceLock.ExitWriteLock();
}
}
}
public class PerThreadLifetime : ILifetime
{
private readonly ThreadLocal<object> _instance = new();
public object GetInstance(Func<object> createInstance, Scope scope)
{
if (_instance.Value is null)
{
var inst = createInstance.Invoke();
// IDisposable dispatch
if (inst is IDisposable disposable)
{
if (scope is null)
{
throw new InvalidOperationException("Attempt disposable object without a valid scope.");
}
scope.TrackInstance(disposable);
}
_instance.Value = inst;
}
return _instance.Value;
}
}
@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using OneOf;
namespace Barotrauma.LuaCs;
public sealed class SettingsFileParserService :
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>,
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>
{
#region DisposalControl
private AsyncReaderWriterLock _operationLock = new();
public void Dispose()
{
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_storageService.Dispose();
_storageService = null;
}
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#endregion
private IStorageService _storageService;
public SettingsFileParserService(IStorageService storageService)
{
_storageService = storageService;
}
async Task<Result<ImmutableArray<IConfigInfo>>> IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>
.TryParseResourcesAsync(IConfigResourceInfo src)
{
Guard.IsNotNull(src, nameof(src));
Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage));
using var lck = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (src.FilePaths.IsDefaultOrEmpty)
{
return ReturnFail($"The config file list is empty.");
}
var parsedInfo = ImmutableArray.CreateBuilder<IConfigInfo>();
foreach ((ContentPath path, Result<XDocument> docLoadResult) res in await _storageService.LoadPackageXmlFilesAsync(src.FilePaths))
{
if (res.docLoadResult.IsFailed)
{
return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}").WithErrors(res.docLoadResult.Errors);
}
var settingElements = res.docLoadResult.Value.GetChildElement("Configuration")
.GetChildElements("Settings").SelectMany(e => e.GetChildElements("Setting")).ToImmutableArray();
if (settingElements.IsDefaultOrEmpty)
{
continue;
}
var packageIdent = XmlConvert.EncodeLocalName(res.path.ContentPackage!.Name);
foreach (var element in settingElements)
{
var name = element.GetAttributeString("Name", string.Empty);
if (name.IsNullOrWhiteSpace())
{
return ReturnFail(
$"The internal name for a setting in the config file '{res.path.FullPath}' is empty!");
}
var newSetting = new ConfigInfo()
{
InternalName = name,
OwnerPackage = res.path.ContentPackage,
DataType = element.GetAttributeString("Type", string.Empty),
Element = element,
EditableStates = element.GetAttributeBool("ReadOnly", false) ? RunState.Unloaded :
element.GetAttributeBool("AllowChangesWhileExecuting", true) ? RunState.Running :
RunState.LoadedNoExec,
NetSync = element.GetAttributeEnum("NetSync", NetSync.None),
#if CLIENT
DisplayName = $"{packageIdent}.{name}.DisplayName",
Description = $"{packageIdent}.{name}.Description",
DisplayCategory = $"{packageIdent}.{name}.DisplayCategory",
ShowInMenus = element.GetAttributeBool("ShowInMenus", true),
Tooltip = $"{packageIdent}.{name}.Tooltip",
ImageIconPath = element.GetAttributeString("ImageIcon", string.Empty) is {} val && !val.IsNullOrWhiteSpace() ?
ContentPath.FromRaw(res.path.ContentPackage, val) : ContentPath.Empty
#endif
};
if (!IsInfoValid(newSetting))
{
return ReturnFail($"A setting was invalid. ContentPackage: {res.path.ContentPackage.Name}. Name: {newSetting?.InternalName}");
}
parsedInfo.Add(newSetting);
}
}
return FluentResults.Result.Ok(parsedInfo.ToImmutable());
// Helpers
FluentResults.Result ReturnFail(string msg)
{
return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>.TryParseResourcesAsync)}: {msg}");
}
bool IsInfoValid(ConfigInfo info)
{
return info.OwnerPackage != null
&& !info.InternalName.IsNullOrWhiteSpace()
&& !info.DataType.IsNullOrWhiteSpace()
&& info.Element != null
#if CLIENT
&& !info.DisplayName.IsNullOrWhiteSpace()
&& !info.Description.IsNullOrWhiteSpace()
&& !info.DisplayCategory.IsNullOrWhiteSpace()
&& !info.Tooltip.IsNullOrWhiteSpace()
#endif
;
}
}
async Task<Result<ImmutableArray<IConfigProfileInfo>>>
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>
.TryParseResourcesAsync(IConfigResourceInfo src)
{
Guard.IsNotNull(src, nameof(src));
Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage));
using var lck = await _operationLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (src.FilePaths.IsDefaultOrEmpty)
{
return ReturnFail($"The config file list is empty.");
}
var parsedInfo = ImmutableArray.CreateBuilder<IConfigProfileInfo>();
foreach ((ContentPath path, Result<XDocument> docLoadResult) res in await _storageService
.LoadPackageXmlFilesAsync(src.FilePaths))
{
if (res.docLoadResult.IsFailed)
{
return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}")
.WithErrors(res.docLoadResult.Errors);
}
var profileCollection = res.docLoadResult.Value.GetChildElement("Configuration")
.GetChildElement("Profiles");
if (profileCollection == null)
{
continue;
}
foreach (var profile in profileCollection.GetChildElements("Profile"))
{
var profileName = profile.GetAttributeString("Name", string.Empty);
Guard.IsNotNullOrWhiteSpace(profileName, nameof(profileName));
var settingValues = profile.GetChildElements("SettingValue").ToImmutableArray();
if (settingValues.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException(nameof(settingValues));
}
var profileValuesBuilder = ImmutableArray.CreateBuilder<(string ConfigName, XElement Value)>();
foreach (var settingValue in settingValues)
{
var cfgName = settingValue.GetAttributeString("Name", string.Empty);
Guard.IsNotNullOrWhiteSpace(cfgName, nameof(cfgName));
profileValuesBuilder.Add((cfgName, settingValue));
}
parsedInfo.Add(new ConfigProfileInfo()
{
InternalName = profileName,
OwnerPackage = res.path.ContentPackage,
ProfileValues = profileValuesBuilder.ToImmutable()
});
}
}
return parsedInfo.ToImmutable();
FluentResults.Result ReturnFail(string msg)
{
return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>.TryParseResourcesAsync)}: {msg}");
}
}
}
@@ -0,0 +1,728 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using FluentResults;
using FluentResults.LuaCs;
using Microsoft.CodeAnalysis;
using Microsoft.Toolkit.Diagnostics;
using Error = FluentResults.Error;
using Path = System.IO.Path;
namespace Barotrauma.LuaCs;
public class StorageService : IStorageService
{
public StorageService(IStorageServiceConfig configData)
{
ConfigData = configData;
IsReadOperationAllowedEval = bool (str) => true;
IsWriteOperationAllowedEval = bool (str) => true;
}
private readonly ConcurrentDictionary<string, OneOf.OneOf<byte[], string, XDocument>> _fsCache = new();
protected readonly IStorageServiceConfig ConfigData;
protected readonly AsyncReaderWriterLock OperationsLock = new();
private Func<string, bool> _isReadOperationAllowedEval;
protected Func<string, bool> IsReadOperationAllowedEval
{
get => _isReadOperationAllowedEval;
set
{
if (value is not null)
_isReadOperationAllowedEval = value;
}
}
private Func<string, bool> _isWriteOperationAllowedEval;
protected Func<string, bool> IsWriteOperationAllowedEval
{
get => _isWriteOperationAllowedEval;
set
{
if (value is not null)
_isWriteOperationAllowedEval = value;
}
}
public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed);
private int _isDisposed = 0;
public virtual void Dispose()
{
using var lck = OperationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
return;
_fsCache.Clear();
}
public void PurgeCache()
{
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_fsCache.Clear();
}
public void PurgeFileFromCache(string absolutePath)
{
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (absolutePath.IsNullOrWhiteSpace())
return;
try
{
//sanitation pass
absolutePath = System.IO.Path.GetFullPath(absolutePath).CleanUpPath();
_fsCache.Remove(absolutePath, out _);
}
catch
{
// ignored
return;
}
}
public void PurgeFilesFromCache(params string[] absolutePaths)
{
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (absolutePaths.Length < 1)
return;
foreach (var path in absolutePaths)
{
try
{
if (path.IsNullOrWhiteSpace())
continue;
//sanitation pass
var path2 = System.IO.Path.GetFullPath(path).CleanUpPath();
_fsCache.Remove(path2, out _);
}
catch
{
// ignored
continue;
}
}
}
// --- Local Game Content
protected Result<string> GetAbsolutePathForLocal(ContentPackage package, string localFilePath)
{
if (Path.IsPathRooted(localFilePath))
ThrowHelper.ThrowArgumentException($"{nameof(GetAbsolutePathForLocal)}: The path {localFilePath} is an absolute path.");
try
{
var path = System.IO.Path.GetFullPath(Path.Combine(
ConfigData.LocalPackageDataPath.Replace(ConfigData.LocalDataPathRegex, XmlConvert.EncodeLocalName(package.Name)).CleanUpPathCrossPlatform(),
localFilePath.CleanUpPathCrossPlatform()));
if (!path.StartsWith(Path.GetFullPath(ConfigData.LocalDataSavePath)))
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(GetAbsolutePathForLocal)}: The local path of '{path}' is not a local path!");
return path;
}
catch (Exception e)
{
if (e is ArgumentNullException or ArgumentException or UnauthorizedAccessException)
throw; // these are dev errors and should be propagated.
return FluentResults.Result.Fail(new ExceptionalError(e));
}
}
private Result<T> LoadLocalData<T>(ContentPackage package, string localFilePath, Func<string, Result<T>> dataLoader)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : dataLoader(res.Value);
}
public Result<XDocument> LoadLocalXml(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadXml);
public Result<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadBinary);
public Result<string> LoadLocalText(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadText);
private FluentResults.Result SaveLocalData<T>(ContentPackage package, string localFilePath, in T data, Func<string, T, FluentResults.Result> dataSaver)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : dataSaver(res.Value, data);
}
public FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document)
=> SaveLocalData(package, localFilePath, document, (path, data) => TrySaveXml(path, in data));
public FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes)
=> SaveLocalData(package, localFilePath, bytes, (path, data) => TrySaveBinary(path, in data));
public FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text)
=> SaveLocalData(package, localFilePath, text, (path, data) => TrySaveText(path, in data));
private async Task<Result<T>> LoadLocalDataAsync<T>(ContentPackage package, string localFilePath,
Func<string, Task<Result<T>>> dataLoader)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : await dataLoader(res.Value);
}
public async Task<Result<XDocument>> LoadLocalXmlAsync(ContentPackage package, string localFilePath)
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadXmlAsync(path));
public async Task<Result<byte[]>> LoadLocalBinaryAsync(ContentPackage package, string localFilePath)
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadBinaryAsync(path));
public async Task<Result<string>> LoadLocalTextAsync(ContentPackage package, string localFilePath)
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadTextAsync(path));
private async Task<FluentResults.Result> SaveLocalDataAsync<T>(ContentPackage package, string localFilePath,
T data, Func<string, T, Task<FluentResults.Result>> dataSaver)
{
Guard.IsNotNull(package, nameof(package));
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
IService.CheckDisposed(this);
using var lck = await OperationsLock.AcquireReaderLock();
var res = GetAbsolutePathForLocal(package, localFilePath);
return res is { IsFailed: true } ? res.ToResult() : await dataSaver(res.Value, data);
}
public async Task<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document)
=> await SaveLocalDataAsync(package, localFilePath, document, async (path, doc) => await TrySaveXmlAsync(path, doc));
public async Task<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes)
=> await SaveLocalDataAsync(package, localFilePath, bytes, async (path, bin) => await TrySaveBinaryAsync(path, bin));
public async Task<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text)
=> await SaveLocalDataAsync(package, localFilePath, text, async (path, txt) => await TrySaveTextAsync(path, txt));
private bool IsPackagePathValid(ContentPath contentPath)
{
return contentPath.FullPath.StartsWith(ConfigData.WorkshopModsDirectory)
|| contentPath.FullPath.StartsWith(ConfigData.LocalModsDirectory)
#if CLIENT
|| contentPath.FullPath.StartsWith(ConfigData.TempDownloadsDirectory)
#endif
|| contentPath.FullPath.StartsWith(Path.GetFullPath(ContentPackageManager.VanillaCorePackage!.Dir).CleanUpPathCrossPlatform());
}
// --- Package Content
private Result<T> LoadPackageData<T>(ContentPath contentPath, Func<string, Result<T>> dataLoader)
{
Guard.IsNotNull(contentPath, nameof(contentPath));
Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!IsPackagePathValid(contentPath))
{
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageData)}: The filepath of `{contentPath.FullPath}' is not in a package directory!");
}
return dataLoader(contentPath.FullPath);
}
public Result<XDocument> LoadPackageXml(ContentPath filePath)
=> LoadPackageData(filePath, path => TryLoadXml(filePath.FullPath));
public Result<byte[]> LoadPackageBinary(ContentPath filePath)
=> LoadPackageData(filePath, path => TryLoadBinary(filePath.FullPath));
public Result<string> LoadPackageText(ContentPath filePath)
=> LoadPackageData(filePath, path => TryLoadText(filePath.FullPath));
private ImmutableArray<(ContentPath, Result<T>)> LoadPackageDataFiles<T>(ImmutableArray<ContentPath> filePaths, Func<string, Result<T>> dataLoader)
{
if (filePaths.IsDefaultOrEmpty)
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!");
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
var builder = ImmutableArray.CreateBuilder<(ContentPath, Result<T>)>();
foreach (var path in filePaths)
{
builder.Add((path, LoadPackageData(path, dataLoader)));
}
return builder.ToImmutable();
}
public ImmutableArray<(ContentPath, Result<XDocument>)> LoadPackageXmlFiles(ImmutableArray<ContentPath> filePaths)
=> LoadPackageDataFiles(filePaths, TryLoadXml);
public ImmutableArray<(ContentPath, Result<byte[]>)> LoadPackageBinaryFiles(ImmutableArray<ContentPath> filePaths)
=> LoadPackageDataFiles(filePaths, TryLoadBinary);
public ImmutableArray<(ContentPath, Result<string>)> LoadPackageTextFiles(ImmutableArray<ContentPath> filePaths)
=> LoadPackageDataFiles(filePaths, TryLoadText);
public Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively)
{
Guard.IsNotNull(package, nameof(package));
try
{
var cp = ContentPath.FromRaw(package, package.Dir);
var fullPath = localSubfolder.IsNullOrWhiteSpace()
? Path.GetFullPath(cp.FullPath)
: Path.GetFullPath(localSubfolder, cp.FullPath);
return System.IO.Directory.GetFiles(fullPath, regexFilter,
searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
.ToImmutableArray();
}
catch (Exception e)
{
if (e is ArgumentNullException or ArgumentException)
throw;
return FluentResults.Result.Fail(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
}
private async Task<Result<T>> LoadPackageDataAsync<T>(ContentPath contentPath, Func<string, Task<Result<T>>> dataLoader)
{
Guard.IsNotNull(contentPath, nameof(contentPath));
Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (!IsPackagePathValid(contentPath))
{
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageDataAsync)}: The filepath of `{contentPath.FullPath}' is not in a package directory!");
}
return await dataLoader(contentPath.FullPath);
}
public async Task<Result<XDocument>> LoadPackageXmlAsync(ContentPath filePath)
=> await LoadPackageDataAsync(filePath, async path => await TryLoadXmlAsync(path));
public async Task<Result<byte[]>> LoadPackageBinaryAsync(ContentPath filePath)
=> await LoadPackageDataAsync(filePath, async path => await TryLoadBinaryAsync(path));
public async Task<Result<string>> LoadPackageTextAsync(ContentPath filePath)
=> await LoadPackageDataAsync(filePath, async path => await TryLoadTextAsync(path));
private async Task<ImmutableArray<(ContentPath, Result<T>)>> LoadPackageDataFilesAsync<T>(
ImmutableArray<ContentPath> filePaths, Func<string, Task<Result<T>>> dataLoader)
{
if (filePaths.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!");
}
using var lck = await OperationsLock.AcquireReaderLock();
var builder = ImmutableArray.CreateBuilder<(ContentPath, Result<T>)>();
foreach (var path in filePaths)
{
builder.Add((path, await LoadPackageDataAsync(path, dataLoader)));
}
return builder.ToImmutable();
}
public async Task<ImmutableArray<(ContentPath, Result<XDocument>)>> LoadPackageXmlFilesAsync(ImmutableArray<ContentPath> filePaths)
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadXmlAsync(path));
public async Task<ImmutableArray<(ContentPath, Result<byte[]>)>> LoadPackageBinaryFilesAsync(ImmutableArray<ContentPath> filePaths)
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadBinaryAsync(path));
public async Task<ImmutableArray<(ContentPath, Result<string>)>> LoadPackageTextFilesAsync(ImmutableArray<ContentPath> filePaths)
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadTextAsync(path));
private int _useCaching;
public bool UseCaching
{
get => ModUtils.Threading.GetBool(ref _useCaching);
set => ModUtils.Threading.SetBool(ref _useCaching, value);
}
// Method group redirect
private FluentResults.Result<XDocument> TryLoadXml(string filePath) => TryLoadXml(filePath, null);
public virtual FluentResults.Result<XDocument> TryLoadXml(string filePath, Encoding encoding)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var r = TryLoadText(filePath, encoding);
if (r is { IsSuccess: true, Value: not null })
return XDocument.Parse(r.Value);
else
{
return r.ToResult<XDocument>(s => null)
.WithError(GetGeneralError(nameof(LoadLocalXml), filePath));
}
}
// Method group redirect
private FluentResults.Result<string> TryLoadText(string filePath) => TryLoadText(filePath, null);
public virtual FluentResults.Result<string> TryLoadText(string filePath, Encoding encoding)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadText)}: File '{filePath}' is not allowed.");
}
if (UseCaching && _fsCache.TryGetValue(filePath, out var result)
&& result.TryPickT1(out var cachedVal, out _))
{
return FluentResults.Result.Ok(cachedVal);
}
return IOExceptionsOperationRunner(nameof(TryLoadText), filePath, () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
var fileText = encoding is null ? System.IO.File.ReadAllText(fp) : System.IO.File.ReadAllText(fp, encoding);
if (UseCaching)
_fsCache[filePath] = fileText;
return new FluentResults.Result<string>().WithSuccess($"Loaded file successfully").WithValue(fileText);
});
}
public virtual FluentResults.Result<byte[]> TryLoadBinary(string filePath)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadBinary)}: File '{filePath}' is not allowed.");
}
if (UseCaching && _fsCache.TryGetValue(filePath, out var result)
&& result.TryPickT0(out var cachedVal, out _))
{
return FluentResults.Result.Ok(cachedVal);
}
return IOExceptionsOperationRunner(nameof(TryLoadBinary), filePath, () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
var fileData = System.IO.File.ReadAllBytes(fp);
if (UseCaching)
{
_fsCache[filePath] = fileData;
}
return new FluentResults.Result<byte[]>().WithSuccess($"Loaded file successfully").WithValue(fileData);
});
}
public virtual FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null) => TrySaveText(filePath, document.ToString(), encoding);
public virtual FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null)
{
Guard.IsNotNullOrWhiteSpace(text, nameof(text));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveText)}: File '{filePath}' is not allowed.");
}
string t = text; //copy
return IOExceptionsOperationRunner(nameof(TrySaveText), filePath, () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
Directory.CreateDirectory(Path.GetDirectoryName(fp)!);
System.IO.File.WriteAllText(fp, t, encoding ?? Encoding.UTF8);
if (UseCaching)
_fsCache[filePath] = t;
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
});
}
public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
Guard.IsNotNull(bytes, nameof(bytes));
Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes));
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveBinary)}: File '{filePath}' is not allowed.");
}
byte[] b = new byte[bytes.Length];
System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length);
return IOExceptionsOperationRunner(nameof(TrySaveBinary), filePath, () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
Directory.CreateDirectory(Path.GetDirectoryName(fp)!);
System.IO.File.WriteAllBytes(fp, b);
if (UseCaching)
_fsCache[filePath] = b;
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
});
}
public virtual FluentResults.Result<bool> FileExists(string filePath)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
IService.CheckDisposed(this);
// lock not needed
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(FileExists)}: File '{filePath}' is not allowed.");
}
return IOExceptionsOperationRunner<bool>(nameof(FileExists), filePath, () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
return System.IO.File.Exists(fp);
});
}
public virtual FluentResults.Result<bool> DirectoryExists(string directoryPath)
{
Guard.IsNotNullOrWhiteSpace(directoryPath, nameof(directoryPath));
IService.CheckDisposed(this);
// lock not needed
if (IsReadOperationAllowedEval?.Invoke(directoryPath) is not true)
{
return FluentResults.Result.Fail($"{nameof(DirectoryExists)}: File '{directoryPath}' is not allowed.");
}
try
{
var di = new DirectoryInfo(directoryPath);
return di.Exists;
}
catch (Exception ex)
{
return new FluentResults.Result<bool>().WithError(ex.Message);
}
}
public virtual async Task<FluentResults.Result<XDocument>> TryLoadXmlAsync(string filePath, Encoding encoding = null)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadXmlAsync)}: File '{filePath}' is not allowed.");
}
if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal)
&& cachedVal.TryPickT2(out var cachedDoc, out _))
{
return FluentResults.Result.Ok(cachedDoc);
}
try
{
await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var doc = await XDocument.LoadAsync(fs, LoadOptions.PreserveWhitespace, CancellationToken.None);
if (UseCaching)
_fsCache[filePath] = doc;
return FluentResults.Result.Ok(doc);
}
catch (Exception e)
{
return FluentResults.Result.Fail<XDocument>(GetGeneralError(nameof(TryLoadXmlAsync), filePath));
}
}
public virtual async Task<FluentResults.Result<string>> TryLoadTextAsync(string filePath, Encoding encoding = null)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadTextAsync)}: File '{filePath}' is not allowed.");
}
if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal)
&& cachedVal.TryPickT1(out var cachedTxt, out _))
{
return FluentResults.Result.Ok(cachedTxt);
}
return await IOExceptionsOperationRunnerAsync<string>(nameof(TryLoadTextAsync), filePath, async () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
var txt = await System.IO.File.ReadAllTextAsync(fp);
if (UseCaching)
_fsCache[filePath] = txt;
return FluentResults.Result.Ok(txt);
});
}
public virtual async Task<FluentResults.Result<byte[]>> TryLoadBinaryAsync(string filePath)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TryLoadBinaryAsync)}: File '{filePath}' is not allowed.");
}
if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal)
&& cachedVal.TryPickT0(out var cachedBin, out _))
{
return cachedBin;
}
return await IOExceptionsOperationRunnerAsync<byte[]>(nameof(TryLoadTextAsync), filePath, async () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
return await System.IO.File.ReadAllBytesAsync(fp);
});
}
// method group overload
public virtual async Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding);
public virtual async Task<FluentResults.Result> TrySaveTextAsync(string filePath, string text, Encoding encoding = null)
{
Guard.IsNotNullOrWhiteSpace(text, nameof(text));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveTextAsync)}: File '{filePath}' is not allowed.");
}
string t = text.ToString(); //copy
return await IOExceptionsOperationRunnerAsync(nameof(TrySaveText), filePath, async () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
await System.IO.File.WriteAllTextAsync(fp, t, encoding);
if (UseCaching)
_fsCache[filePath] = t;
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
});
}
public virtual async Task<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes)
{
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
Guard.IsNotNull(bytes, nameof(bytes));
Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes));
using var lck = await OperationsLock.AcquireReaderLock();
IService.CheckDisposed(this);
if (IsWriteOperationAllowedEval.Invoke(filePath) is not true)
{
return FluentResults.Result.Fail($"{nameof(TrySaveBinaryAsync)}: File '{filePath}' is not allowed.");
}
byte[] b = new byte[bytes.Length];
System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length);
return await IOExceptionsOperationRunnerAsync(nameof(TrySaveBinary), filePath, async () =>
{
var fp = filePath.CleanUpPath();
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
await System.IO.File.WriteAllBytesAsync(fp, b);
if (UseCaching)
_fsCache[filePath] = b;
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
});
}
private async Task<FluentResults.Result<T>> IOExceptionsOperationRunnerAsync<T>(string funcName, string filepath, Func<Task<FluentResults.Result<T>>> operation)
{
try
{
return await operation?.Invoke()!;
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private async Task<FluentResults.Result> IOExceptionsOperationRunnerAsync(string funcName, string filepath, Func<Task<FluentResults.Result>> operation)
{
try
{
return await operation?.Invoke()!;
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private FluentResults.Result<T> IOExceptionsOperationRunner<T>(string funcName, string filepath, Func<FluentResults.Result<T>> operation)
{
try
{
return operation?.Invoke();
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private FluentResults.Result IOExceptionsOperationRunner(string funcName, string filepath, Func<FluentResults.Result> operation)
{
try
{
return operation?.Invoke();
}
catch (Exception e)
{
if (e is ArgumentException or ArgumentNullException)
throw;
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
}
}
private Error GetGeneralError(string funcName, string localfp, ContentPackage package) =>
new Error($"{funcName}: Failed to load local file.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, localfp)
.WithMetadata(MetadataType.RootObject, package);
private Error GetGeneralError(string funcName, string localfp) =>
new Error($"{funcName}: Failed to load local file.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, localfp);
private FluentResults.Result<TReturn> ReturnException<TReturn, TException>(TException exception, ContentPackage package) where TException : Exception
{
return new FluentResults.Result<TReturn>().WithError(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
private FluentResults.Result ReturnException<TException>(TException exception, ContentPackage package) where TException : Exception
{
return new FluentResults.Result().WithError(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
private FluentResults.Result ReturnException<TException>(TException exception, string filePath) where TException : Exception
{
return new FluentResults.Result().WithError(new ExceptionalError(exception)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, filePath));
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using OneOf;
// ReSharper disable InconsistentNaming
namespace Barotrauma.LuaCs;
public interface IAssemblyManagementService : IPluginManagementService
{
/// <summary>
/// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied.
/// </summary>
/// <param name="assemblyName">The assembly info.</param>
/// <param name="excludedContexts">Guids of excluded contexts.</param>
/// <returns><b>On Success:</b> The assembly. <br/><b>On Failure:</b> nothing.</returns>
FluentResults.Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts);
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using FluentResults;
namespace Barotrauma.LuaCs;
public partial interface IConfigService : IReusableService, ILuaConfigService
{
void RegisterSettingTypeInitializer<T>(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory)
where T : class, ISettingBase;
Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources);
Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigResourceInfo> configProfileResources);
FluentResults.Result LoadSavedConfigsValues();
FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName);
FluentResults.Result DisposePackageData(ContentPackage package);
FluentResults.Result DisposeAllPackageData();
}
@@ -0,0 +1,16 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma.LuaCs;
public interface IConsoleCommandsService : IService
{
void RegisterCommand(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false);
void AssignOnExecute(string names, Action<string[]> onExecute);
#if SERVER
internal void AssignOnClientRequestExecute(string names, Action<Client, Vector2, string[]> onClientRequestExecute);
#endif
void RemoveCommand(string name);
void RemoveRegisteredCommands();
}
@@ -0,0 +1,52 @@
using System;
using System.Reflection;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.LuaCs;
namespace Barotrauma.LuaCs;
public interface IEventService : IReusableService, ILuaEventService
{
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result Subscribe<T>(T subscriber) where T : class, IEvent<T>;
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <typeparam name="T"></typeparam>
void Unsubscribe<T>(T subscriber) where T : class, IEvent;
/// <summary>
/// Clears all subscribers for a given event type and removes any registration to the type.
/// </summary>
/// <typeparam name="T">The event type.</typeparam>
void ClearAllEventSubscribers<T>() where T : class, IEvent;
/// <summary>
/// Clears all subscribers lists.
/// </summary>
void ClearAllSubscribers();
/// <summary>
/// Invokes all alive subscribers of the given event using the provided invocation factory.
/// </summary>
/// <param name="action"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result PublishEvent<T>(Action<T> action) where T : class, IEvent<T>;
/// <summary>
/// Adds an event service that will receive all published events.
/// </summary>
/// <param name="eventService"></param>
void AddDispatcherEventService(IEventService eventService);
/// <summary>
/// Removes an event service from the dispatcher list.
/// </summary>
/// <param name="eventService"></param>
void RemoveDispatcherEventService(IEventService eventService);
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FluentResults;
namespace Barotrauma.LuaCs;
public interface IParserService<in TSrc, TOut> : IService
{
Result<TOut> TryParseResource(TSrc src);
ImmutableArray<Result<TOut>> TryParseResources(IEnumerable<TSrc> sources);
}
public interface IParserServiceAsync<in TSrc, TOut> : IService
{
Task<Result<TOut>> TryParseResourceAsync(TSrc src);
Task<ImmutableArray<Result<TOut>>> TryParseResourcesAsync(IEnumerable<TSrc> sources);
}
public interface IParserServiceOneToManyAsync<in TSrc, TOut> : IService
{
Task<Result<ImmutableArray<TOut>>> TryParseResourcesAsync(TSrc src);
}
@@ -0,0 +1,59 @@
using System;
using Barotrauma.Networking;
using FluentResults;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs;
public readonly record struct PendingLog(string Message, Color? Color, ServerLog.MessageType MessageType);
public interface ILoggerSubscriber
{
void OnLog(PendingLog pendingLog);
}
/// <summary>
/// Provides console and debug logging services
/// </summary>
public interface ILoggerService : IReusableService
{
void Subscribe(ILoggerSubscriber subscriber);
void Unsubscribe(ILoggerSubscriber subscriber);
void ProcessLogs();
void HandleException(Exception exception, string prefix = null);
void LogError(string message);
void LogWarning(string message);
void LogMessage(string message, Color? serverColor = null, Color? clientColor = null);
void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage);
void LogResults(FluentResults.Result result);
#region DebugBuilds
void LogDebug(string message, Color? color = null);
void LogDebugWarning(string message);
void LogDebugError(string message);
#endregion
#region LegacyCompat_LuaCsLogger
public void HandleException(Exception ex, LuaCsMessageOrigin origin)
{
HandleException(ex, origin.ToString());
}
public void LogError(string message, LuaCsMessageOrigin origin)
{
LogError(message);
}
#endregion
}
public enum LuaCsMessageOrigin
{
LuaCs,
Unknown,
LuaMod,
CSharpMod,
}
@@ -0,0 +1,32 @@
namespace Barotrauma.LuaCs;
/// <summary>
/// Provides access to data from the current <see cref="LuaCsSetup"/>.
/// </summary>
public interface ILuaCsInfoProvider : IService
{
/// <summary>
/// Whether C# plugin code is enabled.
/// </summary>
public bool IsCsEnabled { get; }
/// <summary>
/// Whether usernames are anonymized or show in logs.
/// </summary>
public bool HideUserNamesInLogs { get; }
/// <summary>
/// Whether file system caching is enabled.
/// </summary>
public bool UseCaching { get; }
/// <summary>
/// The current state of the Execution State Machine.
/// </summary>
public RunState CurrentRunState { get; }
/// <summary>
/// Returns the best-matching LuaCsForBarotrauma package (enabled list > localMods > WorkshopMods).
/// </summary>
public ContentPackage LuaCsForBarotraumaPackage { get; }
}
@@ -0,0 +1,66 @@
#nullable enable
using System.Collections.Immutable;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs;
public interface ILuaScriptManagementService : IReusableService
{
/// <summary>
/// The running <see cref="Script"/> instance, if available.
/// </summary>
/// <remarks>
/// It is recommended to avoid using this directly if another API is available for the intended purposes.
/// </remarks>
Script? InternalScript { get; }
object? GetGlobalTableValue(string tableName);
FluentResults.Result<DynValue> DoString(string code);
DynValue? CallFunctionSafe(object luaFunction, params object[] args);
/// <summary>
/// Whether to enable/disable the file system caching for lua.
/// </summary>
/// <param name="useCaching"></param>
void SetCachingPolicy(bool useCaching);
/// <summary>
/// Parses and loads script sources (code) into a memory cache without executing it.
/// </summary>
/// <param name="resourcesInfo"></param>
/// <returns></returns>
// [Required]
Task<FluentResults.Result> LoadScriptResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourcesInfo);
/// <summary>
/// Executes already loaded into memory scripts data, in the supplied order.
/// </summary>
/// <param name="executionOrder"></param>
/// <returns></returns>
// [Required]
FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaScriptResourceInfo> executionOrder, bool enableSandbox);
/// <summary>
///
/// </summary>
/// <param name="package"></param>
/// <returns></returns>
// [Required]
FluentResults.Result DisposePackageResources(ContentPackage package);
/// <summary>
/// Calls dispose on, and clears active refs for, currently running scripts. Does not clear caches.
/// </summary>
/// <returns></returns>
FluentResults.Result UnloadActiveScripts();
/// <summary>
/// Unloads all scripts and clears all caches/references.
/// </summary>
/// <returns></returns>
/// <remarks>May be functionally equivalent to <see cref="IReusableService.Reset"/></remarks>
FluentResults.Result DisposeAllPackageResources();
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using FluentResults;
namespace Barotrauma.LuaCs;
public interface IModConfigService : IService
{
/// <summary>
/// Loads or dynamically generates a <see cref="IModConfigInfo"/> for the given <see cref="ContentPackage"/>.
/// <br/> Throws a <see cref="NullReferenceException"/> if the package is null.
/// </summary>
/// <param name="src"></param>
/// <returns></returns>
Task<Result<IModConfigInfo>> CreateConfigAsync([NotNull]ContentPackage src);
Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src);
}
@@ -0,0 +1,39 @@
using System;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Compatibility;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs;
#if CLIENT
public delegate void NetMessageReceived(IReadMessage netMessage);
#elif SERVER
internal delegate void NetMessageReceived(IReadMessage netMessage, Client connection);
#endif
internal interface INetworkingService : IReusableService, ILuaCsNetworking, IEntityNetworkingService
{
bool IsActive { get; }
bool IsSynchronized { get; }
IWriteMessage Start(string netId);
IWriteMessage Start(Guid netId);
void Receive(string netId, NetMessageReceived action);
void Receive(Guid netId, NetMessageReceived action);
#if SERVER
void SendToClient(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#elif CLIENT
void SendToServer(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#endif
}
public interface IEntityNetworkingService
{
Guid GetNetworkIdForInstance(INetworkSyncVar var);
void RegisterNetVar(INetworkSyncVar netVar);
void DeregisterNetVar(INetworkSyncVar netVar);
void SendNetVar(INetworkSyncVar netVar);
void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection);
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Threading.Tasks;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using FluentResults;
namespace Barotrauma.LuaCs;
public interface IPackageManagementService : IReusableService
{
public bool TryGetLoadedPackageByName(string name, out ContentPackage package);
public FluentResults.Result LoadPackageInfo(ContentPackage package);
public FluentResults.Result LoadPackagesInfo(ImmutableArray<ContentPackage> packages);
public FluentResults.Result ExecuteLoadedPackages(ImmutableArray<ContentPackage> executionOrder, bool executeCsAssemblies);
public FluentResults.Result SyncLoadedPackagesList(ImmutableArray<ContentPackage> packages);
public FluentResults.Result StopRunningPackages();
public FluentResults.Result UnloadPackage(ContentPackage package);
public FluentResults.Result UnloadPackages(ImmutableArray<ContentPackage> packages);
public FluentResults.Result UnloadAllPackages();
public ImmutableArray<ContentPackage> GetAllLoadedPackages();
public ImmutableArray<ContentPackage> GetLoadedUnrestrictedPackages();
public bool IsPackageRunning(ContentPackage package);
public bool IsAnyPackageLoaded();
public bool IsAnyPackageRunning();
public bool PackageContainsAnyRunnableResource(ContentPackage package);
public Result<IModConfigInfo> GetModConfigForPackage(ContentPackage package);
}
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Barotrauma.LuaCs.Data;
using Microsoft.CodeAnalysis;
namespace Barotrauma.LuaCs;
public interface IPluginManagementService : IReusableService
{
/// <summary>
/// Gets all types in searched <see cref="IAssemblyLoaderService"/> that implement the type supplied.
/// </summary>
/// <param name="includeInterfaces"></param>
/// <param name="includeAbstractTypes"></param>
/// <param name="includeDefaultContext"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result<ImmutableArray<Type>> GetImplementingTypes<T>(
bool includeInterfaces = false,
bool includeAbstractTypes = false,
bool includeDefaultContext = true);
/// <summary>
/// Gets the <see cref="ContentPackage"/> that contains the plugin type.
/// </summary>
/// <param name="ownerPackage"></param>
/// <typeparam name="TPlugin"></typeparam>
/// <returns></returns>
bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage);
/// <summary>
/// Tries to find the type given the fully qualified name and filters.
/// </summary>
/// <param name="typeName"></param>
/// <param name="isByRefType"></param>
/// <param name="includeInterfaces"></param>
/// <param name="includeDefaultContext"></param>
/// <returns></returns>
Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, bool includeDefaultContext = true);
/// <summary>
///
/// </summary>
/// <param name="executionOrder"></param>
/// <param name="excludeAlreadyRunningPackages"></param>
/// <returns></returns>
FluentResults.Result ActivatePluginInstances(ImmutableArray<ContentPackage> executionOrder, bool excludeAlreadyRunningPackages = true);
/// <summary>
/// Loads the provided assembly resources in the order of their dependencies and intra-mod priority load order.
/// </summary>
/// <param name="resources"></param>
/// <returns>Success/Failure and list of failed resources, if any.</returns>
FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources);
/// <summary>
/// Unloads all managed <see cref="IAssemblyPlugin"/>, <see cref="Assembly"/>, and <see cref="IAssemblyLoaderService"/>s.
/// </summary>
/// <returns>Success of the operation. <br/><b>Note: does not guarantee .NET runtime assembly unloading success.</b></returns>
FluentResults.Result UnloadManagedAssemblies();
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs;
public interface IPluginService : IReusableService
{
bool IsAssemblyLoaded(string friendlyName);
/// <summary>
/// Loads the assemblies for the given information
/// </summary>
/// <param name="assemblyResourcesInfo"></param>
/// <param name="injectServices"></param>
/// <param name="typeInstances"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result LoadAndInstanceTypes<T>(IEnumerable<IAssemblyResourceInfo> assemblyResourcesInfo, bool injectServices, out ImmutableArray<T> typeInstances) where T : class, IAssemblyPlugin;
FluentResults.Result<ImmutableArray<T>> GetLoadedPluginTypesInPackage<T>() where T : class, IAssemblyPlugin;
/// <summary>
/// Advances the loading/execution state of the plugin. IMPORTANT: You cannot set the execution state of plugins
/// to 'Disposed'. You must instead call the 'DisposePlugins' method.
/// </summary>
/// <param name="newState"></param>
/// <returns></returns>
FluentResults.Result AdvancePluginStates(PluginRunState newState);
/// <summary>
/// Disposes of all running plugins hosted by the service and releases their references to allow unloading.
/// </summary>
/// <returns>Success of the operation. Returns false if any plugin threw errors during disposal.</returns>
FluentResults.Result DisposePlugins();
/// <summary>
/// Gets the current plugin execution state.
/// </summary>
/// <returns></returns>
PluginRunState GetPluginRunState();
}
public enum PluginRunState
{
Instanced=0,
PreInitialization=1,
Initialized=2,
LoadingCompleted=3,
Disposed=4
}
@@ -0,0 +1,54 @@
using System.Collections.Immutable;
namespace Barotrauma.LuaCs;
public interface ISafeStorageService : IStorageService, ISafeStorageValidation { }
public interface ISafeStorageValidation
{
/// <summary>
/// Checks the given file path to see if it can be read. This includes any permissions, whitelists and OS checks.
/// </summary>
/// <param name="path">The absolute path to the file.</param>
/// <param name="readOnly">Whether to only check for read permissions only, or full RWM if false.</param>
/// <param name="checkWhitelistOnly">Whether to only check if the file is safe to access, without checking accessibility at the OS level.</param>
/// <returns>Whether the file is accessible.</returns>
bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true);
/// <summary>
/// Adds the given path to the specified whitelists.
/// </summary>
/// <param name="path">The path to the file, exactly as it will be passed to the Try(Load|Save) methods in <see cref="StorageService"/>.</param>
/// <param name="readOnly">Whether to add it to the read whitelist only, or Read+Write whitelists.</param>
void AddFileToWhitelist(string path, bool readOnly = true);
/// <summary>
/// Adds the given collection of file paths to whitelists (Read|+Write)
/// </summary>
/// <param name="paths">The paths to the files, formatted exactly as it will be passed to the Try(Load|Save) methods in <see cref="StorageService"/>.</param>
/// <param name="readOnly">Whether to add it to the read whitelist only, or Read+Write whitelists.</param>
void AddFilesToWhitelist(ImmutableArray<string> paths, bool readOnly = true);
/// <summary>
/// Removes the given path from all whitelists (Read|+Write).
/// </summary>
/// <param name="path"></param>
void RemoveFileFromAllWhitelists(string path);
/// <summary>
/// Sets the whitelist filtering for read-only file permissions for the instance. Overwrites previous list.
/// </summary>
/// <param name="filePaths">List of file paths allowed, as will be passed to the <see cref="StorageService"/> Try(Load|Save) methods.</param>
FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths);
/// <summary>
/// Sets the whitelist filtering for read & write file permissions for the instance. Overwrites previous lists.
/// </summary>
/// <param name="filePaths">List of file paths allowed, as will be passed to the <see cref="StorageService"/> Try(Load|Save) methods.</param>
FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths);
/// <summary>
/// Deletes all paths from all white lists.
/// </summary>
void ClearAllWhitelists();
}
@@ -0,0 +1,43 @@
using System;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
/// <summary>
/// Represents a <see cref="IReusableService"/> that is automatically instantiated at startup for the lifetime of the
/// <see cref="IServiceProvider"/> instance.
/// </summary>
public interface ISystem : IReusableService { }
/// <summary>
/// Defines a service that can be reset to it's post-constructor state and reused without needing to be disposed.
/// Intended for persistent services.
/// </summary>
public interface IReusableService : IService
{
/// <summary>
/// Returns the service to its original state (post-instantiation).
/// Allows a service instance to be reused without disposing of the instance.
/// </summary>
FluentResults.Result Reset();
}
/// <summary>
/// Base interface inherited by all services.
/// </summary>
/// <exception cref="ObjectDisposedException">Throws exception if `IsDisposed` return true.</exception>
public interface IService : IDisposable
{
bool IsDisposed { get; }
public void CheckDisposed()
{
if (IsDisposed)
ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!");
}
static void CheckDisposed(IService service)
{
if (service.IsDisposed)
ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{service.GetType().Name}'!");
}
}
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using LightInject;
namespace Barotrauma.LuaCs;
/// <summary>
/// Provides instancing and management of <see cref="IService"/>, <see cref="IReusableService"/>, and <see cref="ISystem"/>
/// instances.
/// </summary>
public interface IServicesProvider
{
#region Type_Registration
/// <summary>
/// Registers a type as a service for a given interface.
/// </summary>
/// <remarks>NOTE: <see cref="ISystem"/> services are forced to <see cref="ServiceLifetime.Singleton"/></remarks>
/// <param name="lifetime">The <see cref="ServiceLifetime"/> of the service when requested.</param>
/// <param name="lifetimeInstance">Custom lifetime instance.</param>
/// <typeparam name="TSvcInterface">Service interface.</typeparam>
/// <typeparam name="TService">Implementing service type.</typeparam>
void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
/// <summary>
/// Registers a type as a service for a given interface that can be requested by name.
/// </summary>
/// <remarks>NOTE: <see cref="ISystem"/> services are forced to <see cref="ServiceLifetime.Singleton"/></remarks>
/// <param name="name">Name of the service for lookup.</param>
/// <param name="lifetime">The <see cref="ServiceLifetime"/> of the service when requested.</param>
/// <param name="lifetimeInstance">Custom lifetime instance.</param>
/// <typeparam name="TSvcInterface">Service interface.</typeparam>
/// <typeparam name="TService">Implementing service type.</typeparam>
void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
/// <summary>
/// Registers a factory for resolving the service type.
/// </summary>
/// <param name="factory"></param>
/// <typeparam name="TSvcInterface"></typeparam>
void RegisterServiceResolver<TSvcInterface>(Func<ServiceContainer, TSvcInterface> factory) where TSvcInterface : class, IService;
/// <summary>
/// Compiles/Generates IL for registered services and instantiates all registered <see cref="ISystem"/> types.
/// </summary>
public void CompileAndRun();
#endregion
#region Services_Instancing_Injection
/// <summary>
/// Injects services into the properties of already instanced objects.
/// </summary>
/// <param name="inst"></param>
/// <typeparam name="T"></typeparam>
void InjectServices<T>(T inst) where T : class;
/// <summary>
/// Tries to get a service for the given interface, returns success/failure.
/// </summary>
/// <param name="service"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService;
/// <summary>
/// Tries to get a service for the given interface, throws an exception upon failure.
/// </summary>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
TSvcInterface GetService<TSvcInterface>() where TSvcInterface : class, IService;
/// <summary>
/// Tries to get a service for the given name and interface, returns success/failure.
/// </summary>
/// <param name="name"></param>
/// <param name="service"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService;
/// <summary>
/// Called whenever a new service is created/instanced.
/// Args[0]: The interface type of the service.
/// Args[1]: The instance of the service.
/// </summary>
event System.Action<Type, IService> OnServiceInstanced;
#endregion
#region ActiveServices
/// <summary>
/// Returns all services for the given interface.
/// </summary>
/// <typeparam name="TSvc"></typeparam>
/// <returns></returns>
ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService;
#endregion
// Notes: Left public due to the common use of Publicizers
#region Internal_Use
/// <summary>
/// Notes: Internal use only if hosted by LuaCsForBarotrauma. Disposes of all services and resets DI container. Warning: unable to dispose of services held by other objects.
/// </summary>
void DisposeAndReset();
#endregion
}
public enum ServiceLifetime
{
Transient, Singleton, PerThread, Invalid, Custom
}
@@ -0,0 +1,84 @@
using System;
using System.Collections.Immutable;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using FluentResults;
namespace Barotrauma.LuaCs;
public interface IStorageService : IService
{
bool UseCaching { get; set; }
/// <summary>
/// Deletes all cached file data.
/// </summary>
void PurgeCache();
/// <summary>
/// Deletes the data for the supplied file path from the data cache.
/// </summary>
/// <param name="absolutePath"></param>
void PurgeFileFromCache(string absolutePath);
/// <summary>
/// Deletes the data from the supplied file paths from the data cache.
/// </summary>
/// <param name="absolutePaths"></param>
void PurgeFilesFromCache(params string[] absolutePaths);
// -- local game folder storage
FluentResults.Result<XDocument> LoadLocalXml(ContentPackage package, string localFilePath);
FluentResults.Result<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath);
FluentResults.Result<string> LoadLocalText(ContentPackage package, string localFilePath);
FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document);
FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes);
FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text);
// async
Task<FluentResults.Result<XDocument>> LoadLocalXmlAsync(ContentPackage package, string localFilePath);
Task<FluentResults.Result<byte[]>> LoadLocalBinaryAsync(ContentPackage package, string localFilePath);
Task<FluentResults.Result<string>> LoadLocalTextAsync(ContentPackage package, string localFilePath);
Task<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document);
Task<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes);
Task<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text);
// -- package directory
// singles
Result<XDocument> LoadPackageXml(ContentPath filePath);
Result<byte[]> LoadPackageBinary(ContentPath filePath);
Result<string> LoadPackageText(ContentPath filePath);
// collections
ImmutableArray<(ContentPath, Result<XDocument>)> LoadPackageXmlFiles(ImmutableArray<ContentPath> filePaths);
ImmutableArray<(ContentPath, Result<byte[]>)> LoadPackageBinaryFiles(ImmutableArray<ContentPath> filePaths);
ImmutableArray<(ContentPath, Result<string>)> LoadPackageTextFiles(ImmutableArray<ContentPath> filePaths);
FluentResults.Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively);
// async
// singles
Task<Result<XDocument>> LoadPackageXmlAsync(ContentPath filePath);
Task<Result<byte[]>> LoadPackageBinaryAsync(ContentPath filePath);
Task<Result<string>> LoadPackageTextAsync(ContentPath filePath);
// collections
Task<ImmutableArray<(ContentPath, Result<XDocument>)>> LoadPackageXmlFilesAsync(ImmutableArray<ContentPath> filePaths);
Task<ImmutableArray<(ContentPath, Result<byte[]>)>> LoadPackageBinaryFilesAsync(ImmutableArray<ContentPath> filePaths);
Task<ImmutableArray<(ContentPath, Result<string>)>> LoadPackageTextFilesAsync(ImmutableArray<ContentPath> filePaths);
// -- absolute paths
FluentResults.Result<XDocument> TryLoadXml(string filePath, Encoding encoding = null);
FluentResults.Result<string> TryLoadText(string filePath, Encoding encoding = null);
FluentResults.Result<byte[]> TryLoadBinary(string filePath);
FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null);
FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null);
FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes);
FluentResults.Result<bool> FileExists(string filePath);
FluentResults.Result<bool> DirectoryExists(string directoryPath);
//async
Task<FluentResults.Result<XDocument>> TryLoadXmlAsync(string filePath, Encoding encoding = null);
Task<FluentResults.Result<string>> TryLoadTextAsync(string filePath, Encoding encoding = null);
Task<FluentResults.Result<byte[]>> TryLoadBinaryAsync(string filePath);
Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null);
Task<FluentResults.Result> TrySaveTextAsync(string filePath, string text, Encoding encoding = null);
Task<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes);
}
@@ -0,0 +1,240 @@
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using MoonSharp.Interpreter.Interop.BasicDescriptors;
using Sigil;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Barotrauma.LuaCs;
public interface IDefaultLuaRegistrar : IService
{
public void RegisterAll();
}
public class DefaultLuaRegistrar : IDefaultLuaRegistrar
{
public bool IsDisposed { get; private set; }
private readonly ILuaUserDataService _userDataService;
private readonly ISafeLuaUserDataService _safeUserDataService;
private readonly ILoggerService _loggerService;
private class SteamIDMemberDescriptor : IMemberDescriptor
{
public bool IsStatic => false;
public string Name => "SteamID";
public MemberDescriptorAccess MemberAccess => MemberDescriptorAccess.CanRead;
public DynValue GetValue(Script script, object obj)
{
if (obj is Client client)
{
return DynValue.FromObject(script, ModUtils.Client.GetSteamId(client));
}
throw new System.NotImplementedException();
}
public void SetValue(Script script, object obj, DynValue value)
{
throw new System.NotImplementedException();
}
}
public DefaultLuaRegistrar(ILoggerService loggerService, ILuaUserDataService userDataService, ISafeLuaUserDataService safeUserDataService)
{
_userDataService = userDataService;
_safeUserDataService = safeUserDataService;
_loggerService = loggerService;
}
private void RegisterShared()
{
_userDataService.RegisterType("System.TimeSpan");
_userDataService.RegisterType("System.Exception");
_userDataService.RegisterType("System.Console");
_userDataService.RegisterType("System.Exception");
_userDataService.RegisterType("Barotrauma.Success`2");
_userDataService.RegisterType("Barotrauma.Failure`2");
_userDataService.RegisterType("Barotrauma.Range`1");
_userDataService.RegisterType("Barotrauma.ItemPrefab");
_userDataService.RegisterType("Barotrauma.InputType");
List<Assembly> assembliesToScan = [typeof(DefaultLuaRegistrar).Assembly, typeof(Identifier).Assembly, typeof(Microsoft.Xna.Framework.Vector2).Assembly];
foreach (var type in assembliesToScan.SelectMany(a => a.GetTypes()))
{
if (type.IsEnum || type.Name.StartsWith("<") || type.IsDefined(typeof(CompilerGeneratedAttribute)) || !_safeUserDataService.IsAllowed(type.FullName))
{
continue;
}
_userDataService.RegisterType(type.FullName);
}
_userDataService.RegisterType("Barotrauma.LuaSByte");
_userDataService.RegisterType("Barotrauma.LuaByte");
_userDataService.RegisterType("Barotrauma.LuaInt16");
_userDataService.RegisterType("Barotrauma.LuaUInt16");
_userDataService.RegisterType("Barotrauma.LuaInt32");
_userDataService.RegisterType("Barotrauma.LuaUInt32");
_userDataService.RegisterType("Barotrauma.LuaInt64");
_userDataService.RegisterType("Barotrauma.LuaUInt64");
_userDataService.RegisterType("Barotrauma.LuaSingle");
_userDataService.RegisterType("Barotrauma.LuaDouble");
_userDataService.RegisterType("Barotrauma.Level+InterestingPosition");
_userDataService.RegisterType("Barotrauma.Networking.RespawnManager+TeamSpecificState");
_userDataService.RegisterType("Barotrauma.CharacterParams+AIParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+TargetParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+InventoryParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+HealthParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+ParticleParams");
_userDataService.RegisterType("Barotrauma.CharacterParams+SoundParams");
_userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByIdentifier");
_userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByTag");
_userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.StatusEffect"), "user");
_userDataService.RegisterType("Barotrauma.ContentPackageManager+PackageSource");
_userDataService.RegisterType("Barotrauma.ContentPackageManager+EnabledPackages");
_userDataService.RegisterType("System.Xml.Linq.XElement");
_userDataService.RegisterType("System.Xml.Linq.XName");
_userDataService.RegisterType("System.Xml.Linq.XAttribute");
_userDataService.RegisterType("System.Xml.Linq.XContainer");
_userDataService.RegisterType("System.Xml.Linq.XDocument");
_userDataService.RegisterType("System.Xml.Linq.XNode");
_userDataService.RegisterType("Barotrauma.Networking.ServerSettings+SavedClientPermission");
_userDataService.RegisterType("Barotrauma.Inventory+ItemSlot");
_userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface"), "customInterfaceElementList");
_userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface+CustomInterfaceElement");
_userDataService.RegisterType("Barotrauma.DebugConsole+Command");
{
var descriptor = _userDataService.RegisterType("Barotrauma.NetLobbyScreen");
#if SERVER
_userDataService.MakeFieldAccessible(descriptor, "subs");
#endif
}
_userDataService.RegisterType("FarseerPhysics.Dynamics.Body");
_userDataService.RegisterType("FarseerPhysics.Dynamics.World");
_userDataService.RegisterType("FarseerPhysics.Dynamics.Fixture");
_userDataService.RegisterType("FarseerPhysics.ConvertUnits");
_userDataService.RegisterType("FarseerPhysics.Collision.AABB");
_userDataService.RegisterType("FarseerPhysics.Collision.ContactFeature");
_userDataService.RegisterType("FarseerPhysics.Collision.ManifoldPoint");
_userDataService.RegisterType("FarseerPhysics.Collision.ContactID");
_userDataService.RegisterType("FarseerPhysics.Collision.Manifold");
_userDataService.RegisterType("FarseerPhysics.Collision.RayCastInput");
_userDataService.RegisterType("FarseerPhysics.Collision.ClipVertex");
_userDataService.RegisterType("FarseerPhysics.Collision.RayCastOutput");
_userDataService.RegisterType("FarseerPhysics.Collision.EPAxis");
_userDataService.RegisterType("FarseerPhysics.Collision.ReferenceFace");
_userDataService.RegisterType("FarseerPhysics.Collision.Collision");
_userDataService.RegisterType("Voronoi2.DoubleVector2");
_userDataService.RegisterType("Voronoi2.Site");
_userDataService.RegisterType("Voronoi2.Edge");
_userDataService.RegisterType("Voronoi2.Halfedge");
_userDataService.RegisterType("Voronoi2.VoronoiCell");
_userDataService.RegisterType("Voronoi2.GraphEdge");
_userDataService.RegisterType("Barotrauma.PrefabCollection`1");
_userDataService.RegisterType("Barotrauma.PrefabSelector`1");
_userDataService.RegisterType("Barotrauma.Pair`2");
_userDataService.RegisterExtensionType("Barotrauma.MathUtils");
_userDataService.RegisterExtensionType("Barotrauma.XMLExtensions");
var itemPrefabDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.ItemPrefab");
itemPrefabDescriptor.AddMember("GetItemPrefab", new MethodMemberDescriptor(typeof(ModUtils.ItemPrefab).GetMethod(nameof(ModUtils.ItemPrefab.GetItemPrefab), BindingFlags.NonPublic | BindingFlags.Static)));
var clientDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.Networking.Client");
clientDescriptor.AddMember("ClientList", new PropertyMemberDescriptor(typeof(ModUtils.Client).GetProperty(nameof(ModUtils.Client.ClientList), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
clientDescriptor.AddMember("SteamID", new SteamIDMemberDescriptor());
#if SERVER
clientDescriptor.AddMember("UnbanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.UnbanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
clientDescriptor.AddMember("BanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.BanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
#endif
_userDataService.RegisterExtensionType(typeof(ClientExtensions).FullName);
_userDataService.RegisterExtensionType(typeof(ItemExtensions).FullName);
_userDataService.RegisterExtensionType(typeof(MapEntityExtensions).FullName);
_userDataService.RegisterExtensionType(typeof(QualityExtensions).FullName);
var toolBox = UserData.RegisterType(typeof(ToolBox));
#if CLIENT
_userDataService.RemoveMember(toolBox, "OpenFileWithShell");
#endif
}
#if CLIENT
private void RegisterClient()
{
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Effect");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameterCollection");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameter");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.SpriteBatch");
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Texture2D");
_userDataService.RegisterType("EventInput.KeyboardDispatcher");
_userDataService.RegisterType("EventInput.KeyEventArgs");
_userDataService.RegisterType("Microsoft.Xna.Framework.Input.Keys");
_userDataService.RegisterType("Microsoft.Xna.Framework.Input.KeyboardState");
_userDataService.RegisterType("Barotrauma.Anchor");
_userDataService.RegisterType("Barotrauma.Alignment");
_userDataService.RegisterType("Barotrauma.Pivot");
_userDataService.RegisterType("Barotrauma.Key");
_userDataService.RegisterType("Barotrauma.PlayerInput");
_userDataService.RegisterType("Barotrauma.Inventory+SlotReference");
}
#elif SERVER
private void RegisterServer()
{
_userDataService.RegisterType("Barotrauma.Character+TeamChangeEventData");
}
#endif
public void RegisterAll()
{
RegisterShared();
#if CLIENT
RegisterClient();
#elif SERVER
RegisterServer();
#endif
}
public void Dispose()
{
IsDisposed = true;
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using Barotrauma.LuaCs.Data;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs;
public interface ILuaConfigService : ILuaService
{
FluentResults.Result LoadSavedValueForConfig(ISettingBase setting);
bool TryGetConfig<T>(ContentPackage package, string internalName, out T instance) where T : ISettingBase;
FluentResults.Result SaveConfigValue(ISettingBase setting);
}
@@ -0,0 +1,11 @@
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs;
/// <summary>
/// Service for providing stateful functions and in-memory storage for lua functions
/// </summary>
public interface ILuaDataService : ILuaService
{
}
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs.Compatibility;
namespace Barotrauma.LuaCs;
public interface ILuaSafeEventService : ILuaService, ILuaCsHook
{
/// <summary>
/// Subscribes lua scripts via <see cref="ImpromptuInterface"/> for the given <see cref="IEvent{T}"/> interface.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="identifier"></param>
/// <param name="callbacks">A 'method name'=='signature action' dictionary matching the interface method list.</param>
void Subscribe<T>(string identifier, IDictionary<string, LuaCsFunc> callbacks) where T : class, IEvent<T>;
/// <summary>
/// Removes a subscriber from an event that subscribed under the given identifier.
/// </summary>
/// <param name="eventName"></param>
/// <param name="identifier"></param>
void Unsubscribe(string eventName, string identifier);
/// <summary>
/// Send an event to all subscribers to an interface.
/// </summary>
/// <typeparam name="T">Interface type.</typeparam>
/// <param name="subscriberRunner">Execution runner, the subscriber is provided as the first argument in the lua runner.</param>
/// <returns></returns>
void PublishLuaEvent<T>(LuaCsFunc subscriberRunner) where T : class, IEvent<T>;
/// <summary>
/// Defines the target method name for legacy <see cref="ILuaCsHook.Add(string, LuaCsFunc)"/> to target on new <see cref="IEvent{T}"/>
/// interfaces.
/// </summary>
/// <param name="luaEventName">The <see cref="ILuaCsHook.Add(string, LuaCsFunc)"/> legacy event name.</param>
/// <param name="targetMethod">.</param>
/// <typeparam name="T">The event interface type.</typeparam>
/// <returns>Operation success.</returns>
/// <exception cref="ArgumentNullException">The <see cref="luaEventName"/> is <b>null or empty.</b></exception>
public FluentResults.Result RegisterLuaEventAlias<T>(string luaEventName, string targetMethod) where T : class, IEvent<T>;
}
public interface ILuaEventService : ILuaSafeEventService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaNetworkingService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaPackageManagementService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaPackageService : ILuaService
{
}
@@ -0,0 +1,23 @@
using Barotrauma.LuaCs.Compatibility;
using System;
using System.Reflection;
using static Barotrauma.LuaCs.Compatibility.ILuaCsHook;
using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch;
namespace Barotrauma.LuaCs;
public interface ILuaPatcher : IReusableService
{
string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
string Patch(string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, HookMethodType hookType);
bool RemovePatch(string identifier, string className, string methodName, HookMethodType hookType);
void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, HookMethodType hookType = HookMethodType.Before, IAssemblyPlugin owner = null);
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
public void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
public void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
}
@@ -0,0 +1,18 @@
using System.Collections.Immutable;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using FluentResults;
using MoonSharp.Interpreter.Loaders;
namespace Barotrauma.LuaCs;
public interface ILuaScriptLoader : IService, IScriptLoader, ISafeStorageValidation
{
void ClearCaches();
/// <summary>
/// Whether caching is enabled/disabled.
/// </summary>
/// <param name="useCaching"></param>
void SetCachingPolicy(bool useCaching);
Task<Result<ImmutableArray<(ContentPath Path, Result<string>)>>> CacheResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourceInfos);
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs;
public interface ILuaService : IService
{
}

Some files were not shown because too many files have changed in this diff Show More