-- Squash:

- In progress implementation of services model.
This commit is contained in:
MapleWheels
2024-09-18 20:54:56 -04:00
committed by Maplewheels
parent 9e957a75b0
commit 01cc1d331b
68 changed files with 3083 additions and 152 deletions
@@ -0,0 +1,780 @@
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.LuaCs.Services;
/***
* 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 : IAssemblyManagementService
{
#region ExternalAPI
public event Action<Assembly> OnAssemblyLoaded;
public event Action<Assembly> OnAssemblyUnloading;
public event Action<string, Exception> OnException;
public event Action<Guid> OnACLUnload;
public ImmutableList<WeakReference<MemoryFileAssemblyContextLoader>> StillUnloadingACLs
{
get
{
OpsLockUnloaded.EnterReadLock();
try
{
return UnloadingACLs.ToImmutableList();
}
finally
{
OpsLockUnloaded.ExitReadLock();
}
}
}
public bool IsCurrentlyUnloading
{
get
{
OpsLockUnloaded.EnterReadLock();
try
{
return UnloadingACLs.Any();
}
catch (Exception)
{
return false;
}
finally
{
OpsLockUnloaded.ExitReadLock();
}
}
}
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();
}
}
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;
}
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;
}
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();
}
}
}
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();
}
}
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
[MethodImpl(MethodImplOptions.Synchronized | MethodImplOptions.NoInlining)]
ImmutableList<LoadedACL> IAssemblyManagementService.UnsafeGetAllLoadedACLs()
{
if (LoadedACLs.IsEmpty)
return ImmutableList<LoadedACL>.Empty;
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
}
public event System.Func<LoadedACL, bool> IsReadyToUnloadACL;
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;
}
public bool SetACLToTemplateMode(Guid guid)
{
if (!TryGetACL(guid, out var acl))
return false;
acl.Acl.IsTemplateMode = true;
return true;
}
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)]
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;
}
[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 void Dispose()
{
TryBeginDispose();
}
public void Reset()
{
TryBeginDispose();
}
}
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>();
}
}
}
@@ -0,0 +1,189 @@
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;
// ReSharper disable InconsistentNaming
namespace Barotrauma.LuaCs.Services;
public interface IAssemblyManagementService : IService
{
#region Public API
/// <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>
// ReSharper disable once InconsistentNaming
public event Action<Guid> OnACLUnload;
/// <summary>
/// [DEBUG ONLY]
/// Returns a list of the current unloading ACLs.
/// </summary>
// ReSharper disable once InconsistentNaming
public ImmutableList<WeakReference<MemoryFileAssemblyContextLoader>> StillUnloadingACLs { get; }
// ReSharper disable once MemberCanBePrivate.Global
/// <summary>
/// Checks if there are any AssemblyLoadContexts still in the process of unloading.
/// </summary>
public bool IsCurrentlyUnloading { get; }
/// <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);
/// <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>Operation success.</returns>
public bool TryGetSubTypesFromACL<T>(Guid id, out IEnumerable<Type> types);
/// <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);
/// <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);
/// <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();
/// <summary>
/// Returns a list of all loaded ACLs.
/// WARNING: References to these ACLs outside the AssemblyManager should be kept in a WeakReference in order
/// to avoid causing issues with unloading/disposal.
/// </summary>
/// <returns></returns>
public IEnumerable<AssemblyManager.LoadedACL> GetAllLoadedACLs();
#endregion
#region InternalAPI
/*** Notes: Internal API uses the 'public' modifier because of the common and recommended use of publicized APIs
* by third-party add-ins.
*/
/// <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>
public ImmutableList<AssemblyManager.LoadedACL> UnsafeGetAllLoadedACLs();
/// <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<AssemblyManager.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);
/// <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 an ACL was found with the given ID.</returns>
public bool SetACLToTemplateMode(Guid guid);
/// <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);
/// <summary>
/// Tries to begin the disposal process of ACLs.
/// </summary>
/// <returns>Returns whether the unloading process could be initiated.</returns>
public bool TryBeginDispose();
/// <summary>
/// Returns whether unloading is completed and updates the styate of the unloading cache.
/// </summary>
/// <returns></returns>
public bool FinalizeDispose();
/// <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 an ACL was found.</returns>
public bool TryGetACL(Guid id, out AssemblyManager.LoadedACL acl);
#endregion
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.LuaCs.Configuration;
using Barotrauma.LuaCs.Data;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs.Services;
public interface IConfigService : IService
{
/*
* Resource Files.
*/
bool TryAddConfigs(ImmutableArray<IConfigResourceInfo> configResources);
bool TryAddConfigsProfiles(ImmutableArray<IConfigProfileResourceInfo> configProfileResources);
void RemoveConfigs(ImmutableArray<IConfigResourceInfo> configResources);
void RemoveConfigsProfiles(ImmutableArray<IConfigProfileResourceInfo> configProfilesResources);
/*
* Already processed
*/
bool TryAddConfigs(ImmutableArray<IConfigInfo> configs);
bool TryAddConfigsProfiles(ImmutableArray<IConfigProfileInfo> configProfiles);
void RemoveConfigs(ImmutableArray<IConfigInfo> configs);
void RemoveConfigsProfiles(ImmutableArray<IConfigProfileInfo> configProfiles);
/*
* Immediate mode, does not have displayable functionality
*/
IConfigEntry<T> AddConfigEntry<T>(ContentPackage package, string name,
T defaultValue,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
IConfigList AddConfigList(ContentPackage package, string name,
int defaultIndex, IReadOnlyList<string> values,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<IConfigList, int, bool> valueChangePredicate = null,
Action<IConfigList, int> onValueChanged = null);
IReadOnlyDictionary<string, IConfigBase> GetConfigsForPackage(ContentPackage package);
IReadOnlyDictionary<string, IConfigBase> GetConfigsForPackage(string packageName);
IReadOnlyDictionary<(ContentPackage, string), IConfigBase> GetAllConfigs();
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services;
public interface IEventService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services;
public interface IHookManagementService : IService
{
}
@@ -0,0 +1,8 @@
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface ILegacyConfigService : IService
{
bool TryBuildModConfigFromLegacy(ContentPackage package, out IModConfigInfo configInfo);
}
@@ -0,0 +1,21 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface ILocalizationService : IService
{
IReadOnlyCollection<CultureInfo> GetLoadedLocales();
void Remove(ImmutableArray<ILocalizationResourceInfo> localizations);
bool TrySetCurrentCulture(CultureInfo culture);
bool TrySetCurrentCulture(string cultureName);
bool TryLoadLocalizations(ImmutableArray<ILocalizationResourceInfo> localizationResources);
string GetLocalizedString(string key, string fallback);
string GetLocalizedString(string key, CultureInfo targetCulture);
bool TryRegisterLocalizationResolver(CultureInfo targetCulture, Func<string, CultureInfo, string> factoryResolver);
bool ReplaceSymbols(string text, string symbolExpr);
bool IsCurrentCultureSupported(IResourceCultureInfo culturesInfo);
}
@@ -0,0 +1,25 @@
using System;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs.Services;
/// <summary>
/// Provides console and debug logging services
/// </summary>
public interface ILoggerService : IService
{
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);
#region DebugBuilds
void LogDebug(string message, Color? color = null);
void LogDebugWarning(string message);
void LogDebugError(string message);
#endregion
}
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Barotrauma.LuaCs.Data;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
namespace Barotrauma.LuaCs.Services;
public interface ILuaScriptService : IService
{
#region Script_File_Collector
/// <summary>
/// Adds the script files to the runner but does not execute them.
/// </summary>
/// <param name="luaResource"></param>
/// <returns></returns>
bool TryAddScriptFiles(ImmutableArray<ILuaResourceInfo> luaResource);
/// <summary>
/// Removes the specific resources from the script runner. Important: Does not stop the
/// execution of any code related to the files nor guarantee cleanup of resources!
/// </summary>
/// <param name="luaResource"></param>
void RemoveScriptFiles(ImmutableArray<ILuaResourceInfo> luaResource);
/// <summary>
/// Executes loaded script files on the management service.
/// </summary>
/// <param name="pauseExecutionOnScriptError"></param>
/// <param name="verboseLogging"></param>
/// <returns></returns>
bool TryExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false);
ImmutableArray<ILuaResourceInfo> GetScriptResources();
#endregion
}
public interface ILuaScriptManagementService : IService
{
#region Script_File_Execution
bool TryExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false);
bool TryExecuteLoadedScripts(ImmutableArray<ILuaResourceInfo> scripts, bool pauseExecutionOnError = false, bool verboseLogging = false);
bool TryExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false);
#endregion
#region Type_Registration
IUserDataDescriptor RegisterType(Type type);
IUserDataDescriptor RegisterType(string typeName);
IUserDataDescriptor RegisterGenericType(Type type);
IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArgs);
void UnregisterType(Type type);
void UnregisterType(string typeName);
void UnregisterAllTypes();
#endregion
#region Type_Checks_&Utilities
bool IsRegistered(Type type);
bool IsTargetType(object obj, string typeName);
string TypeOf(object obj);
object CreateStatic(string typeName);
object CreateEnumTable(string typeName);
FieldInfo FindFieldRecursively(Type type, string fieldName);
void MakeFieldAccessible(IUserDataDescriptor descriptor, string fieldName);
MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null);
void MakeMethodAccessible(IUserDataDescriptor descriptor, string methodName, string[] parameters = null);
PropertyInfo FindPropertyRecursively(Type type, string propertyName);
void MakePropertyAccessible(IUserDataDescriptor descriptor, string propertyName);
void AddMethod(IUserDataDescriptor descriptor, string methodName, object function);
void AddField(IUserDataDescriptor descriptor, string fieldName, DynValue value);
void RemoveMember(IUserDataDescriptor descriptor, string memberName);
bool HasMember(object obj, string memberName);
DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor descriptor);
DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType);
#endregion
}
@@ -0,0 +1,23 @@
using System;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Networking;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs.Services;
public interface INetworkingService : IService
{
bool IsActive { get; }
bool IsSynchronized { get; }
bool TryRegisterVar(INetVar var, NetSync mode, ClientPermissions permissions);
void UnregisterVar(Guid varId);
bool SendEvent(Guid varId);
void SendMessageGlobal(string id, string message);
void Synchronize();
#region LegacyAPI
bool RestrictMessageSize { get; set; }
#endregion
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface IPackageManagementService : IService
{
void AddPackages(ref ReadOnlySpan<(ContentPackage, bool)> packages,
bool executeImmediately = false,
bool errorOnFailures = false,
bool errorOnExistingPackageFound = false);
void LoadPackages(bool onlyUnloadedPackages = true, bool rescanPackages = false);
void UnloadPackages(bool errorOnFailures = true);
bool IsPackageLoaded(ContentPackage package);
bool CheckDependencyLoaded(IPackageDependencyInfo info);
bool CheckDependenciesLoaded(IEnumerable<IPackageDependencyInfo> infos, out IReadOnlyList<IPackageDependencyInfo> missingPackages);
bool CheckEnvironmentSupported(IPlatformInfo platform);
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface IPackageService : IService,
// These allow us the pass the IContentPackageService to anything that needs the data without having to directly reference the member
IResourceCultureInfo, IAssembliesResourcesInfo, ILocalizationsResourcesInfo, ILuaScriptsResourcesInfo
{
ContentPackage Package { get; }
IModConfigInfo ModConfigInfo { get; }
/// <summary>
/// Try to load the XML config and resources information from the given package.
/// </summary>
/// <param name="package"></param>
/// <returns>Whether the package was parsed without errors and any information was found. Will return false for purely vanilla packages.</returns>
bool TryLoadResourcesInfo([NotNull]ContentPackage package);
/// <summary>
/// Tries to load all assemblies and instance plugins for the given resources list, regardless whether they're marked as optional and/or lazy load.
/// Will sort by load priority unless overriden/bypassed.
/// </summary>
/// <param name="assembliesInfo"></param>
/// <param name="ignoreDependencySorting"></param>
/// <returns>Whether loading is successful. Returns true on an empty list.</returns>
void LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false);
void LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo);
void AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo);
#if CLIENT
void LoadStyles([NotNull]IStylesResourcesInfo stylesInfo);
#endif
void LoadConfig([NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo);
}
@@ -0,0 +1,7 @@
namespace Barotrauma.LuaCs.Services;
public interface IPluginManagementService : IService
{
bool IsAssemblyLoadedGlobal(string friendlyName);
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface IPluginService : IService
{
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>
bool TryLoadAndInstanceTypes<T>(IEnumerable<IAssemblyResourceInfo> assemblyResourcesInfo, bool injectServices, out ImmutableArray<T> typeInstances) where T : class, IAssemblyPlugin;
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>
bool 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>
bool 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,15 @@
using System;
namespace Barotrauma.LuaCs.Services;
/// <summary>
/// Base interface inherited by all services
/// </summary>
public interface IService : IDisposable
{
/// <summary>
/// Returns the service to its original state (post-instantiation).
/// Allows a service instance to be reused without disposing of the instance.
/// </summary>
void Reset();
}
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using LightInject;
namespace Barotrauma.LuaCs.Services;
/// <summary>
/// Provides instancing and management of IServices.
/// </summary>
public interface IServicesProvider
{
#region Type_Registration
/// <summary>
/// Registers a type as a service for a given interface.
/// </summary>
/// <param name="lifetime"></param>
/// <param name="lifetimeInstance"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <typeparam name="TService"></typeparam>
void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new();
/// <summary>
/// Registers a type as a service for a given interface that can be requested by name.
/// </summary>
/// <param name="name"></param>
/// <param name="lifetime"></param>
/// <param name="lifetimeInstance"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <typeparam name="TService"></typeparam>
void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new();
/// <summary>
/// Called whenever a new service type for a given interface is implemented.
/// Args[0]: Interface type
/// Args[1]: Implementing type
/// </summary>
event System.Action<Type, Type> OnServiceRegistered;
/// <summary>
/// Runs compilation of registered services.
/// </summary>
public void Compile();
#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>
/// <param name="lifetime"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(out IService service) 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>
/// <param name="lifetime"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(string name, out IService 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,42 @@
using System.Collections.Immutable;
using System.Xml.Linq;
namespace Barotrauma.LuaCs.Services;
public interface IStorageService : IService
{
#region LocalGameData
bool TryLoadLocalXml(ContentPackage package, string localFilePath, out XDocument document);
bool TryLoadLocalBinary(ContentPackage package, string localFilePath, out byte[] bytes);
bool TryLoadLocalText(ContentPackage package, string localFilePath, out string text);
bool FileExistsInLocalData(ContentPackage package, string localFilePath);
#endregion
#region ContentPackageData
bool TryLoadPackageXml(ContentPackage package, string localFilePath, out XDocument document);
bool TryLoadPackageBinary(ContentPackage package, string localFilePath, out byte[] bytes);
bool TryLoadPackageText(ContentPackage package, string localFilePath, out string text);
ImmutableArray<bool> TryLoadPackageXmlFiles(ContentPackage package, ImmutableArray<string> localFilePath, out ImmutableArray<XDocument> document);
ImmutableArray<bool> TryLoadPackageBinaryFiles(ContentPackage package, ImmutableArray<string> localFilePath, out ImmutableArray<byte[]> bytes);
ImmutableArray<bool> TryLoadPackageTextFiles(ContentPackage package, ImmutableArray<string> localFilePath, out ImmutableArray<string> text);
bool FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively, out ImmutableArray<string> localFilePaths);
bool FileExistsInPackage(ContentPackage package, string localFilePath);
#endregion
#region AbsolutePaths
bool TryLoadXml(string filePath, out XDocument document);
bool TrySaveXml(string filePath, in XDocument document);
bool TryLoadBinary(string filePath, out byte[] bytes);
bool TrySaveBinary(string filePath, in byte[] bytes);
bool TryLoadText(string filePath, out string text);
bool TrySaveText(string filePath, string text);
bool FileExists(string filePath);
#endregion
}
@@ -0,0 +1,149 @@
using System;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs.Services;
public partial class LoggerService : ILoggerService
{
public bool HideUserNames = true;
#if SERVER
private const string LogPrefix = "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 _lockLog = false;
#else
private const string LogPrefix = "CL";
#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 LogError(string message)
{
if (HideUserNames && !Environment.UserName.IsNullOrEmpty())
{
message = message.Replace(Environment.UserName, "USERNAME");
}
Log($"{message}", Color.Red, ServerLog.MessageType.Error);
}
public void LogWarning(string message)
{
throw new NotImplementedException();
}
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 Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
{
DebugConsole.NewMessage(message, color);
#if SERVER
void BroadcastMessage(string m)
{
foreach (var client in GameMain.Server.ConnectedClients)
{
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
}
public void LogDebug(string message, Color? color = null)
{
throw new NotImplementedException();
}
public void LogDebugWarning(string message)
{
throw new NotImplementedException();
}
public void LogDebugError(string message)
{
throw new NotImplementedException();
}
public void Dispose() { }
public void Reset() { }
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services;
public class LuaScriptService
{
}
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public class PackageManagementService : IPackageManagementService, IPluginManagementService
{
private readonly Func<IPackageService> _contentPackageServiceFactory;
private readonly Lazy<IAssemblyManagementService> _assemblyManagementService;
public PackageManagementService(
Func<IPackageService> getPackageService,
Lazy<IAssemblyManagementService> assemblyManagementService)
{
this._contentPackageServiceFactory = getPackageService;
this._assemblyManagementService = assemblyManagementService;
}
public void Dispose()
{
// TODO release managed resources here
}
public void Reset()
{
throw new NotImplementedException();
}
public bool IsAssemblyLoadedGlobal(string friendlyName)
{
throw new NotImplementedException();
}
public void AddPackages(ref ReadOnlySpan<(ContentPackage, bool)> packages, bool executeImmediately = false, bool errorOnFailures = false,
bool errorOnExistingPackageFound = false)
{
throw new NotImplementedException();
}
public void LoadPackages(bool onlyUnloadedPackages = true, bool rescanPackages = false)
{
throw new NotImplementedException();
}
public void UnloadPackages(bool errorOnFailures = true)
{
throw new NotImplementedException();
}
public bool IsPackageLoaded(ContentPackage package)
{
throw new NotImplementedException();
}
public bool CheckDependencyLoaded(IPackageDependencyInfo info)
{
throw new NotImplementedException();
}
public bool CheckDependenciesLoaded(IEnumerable<IPackageDependencyInfo> infos, out IReadOnlyList<IPackageDependencyInfo> missingPackages)
{
throw new NotImplementedException();
}
public bool CheckEnvironmentSupported(IPlatformInfo platform)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,627 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Services.Processing;
namespace Barotrauma.LuaCs.Services;
public partial class PackageService : IPackageService
{
private readonly ReaderWriterLockSlim _operationsUsageLock = new();
// only stops race conditions for pointer access
// mod config / package scanners/parsers
private readonly Lazy<IXmlModConfigConverterService> _modConfigConverterService;
private readonly Lazy<ILegacyConfigService> _legacyConfigService;
private readonly Lazy<ILuaScriptService> _luaScriptService;
private readonly Lazy<ILocalizationService> _localizationService;
private readonly Lazy<IPluginService> _pluginService;
private readonly Lazy<IConfigService> _configService;
private readonly IPackageManagementService _packageManagementService;
private readonly IStorageService _storageService;
private readonly ILoggerService _loggerService;
// .ctor in server source and client source
// state monitors
private int _configsLoaded, _localizationsLoaded, _luaScriptsLoaded, _pluginsLoaded, _isDisposed;
private int _loadingOperationsRunning;
public bool ConfigsLoaded
{
get => GetThreadSafeBool(ref _configsLoaded);
private set => SetThreadSafeBool(ref _configsLoaded, value);
}
public bool LocalizationsLoaded
{
get => GetThreadSafeBool(ref _localizationsLoaded);
private set => SetThreadSafeBool(ref _localizationsLoaded, value);
}
public bool LuaScriptsLoaded
{
get => GetThreadSafeBool(ref _luaScriptsLoaded);
private set => SetThreadSafeBool(ref _luaScriptsLoaded, value);
}
public bool PluginsLoaded
{
get => GetThreadSafeBool(ref _pluginsLoaded);
private set => SetThreadSafeBool(ref _pluginsLoaded, value);
}
public bool IsDisposed
{
get => GetThreadSafeBool(ref _isDisposed);
private set => SetThreadSafeBool(ref _isDisposed, value);
}
private bool LoadingOperationsRunning
{
get => Interlocked.CompareExchange(ref _loadingOperationsRunning, 0, 0) > 0;
set // we use the set as our inc/decr
{
if (value)
{
Interlocked.Add(ref _loadingOperationsRunning, 1);
}
else
{
Interlocked.Add(ref _loadingOperationsRunning, -1);
}
}
}
#region Member: ContentPackage
private readonly ReaderWriterLockSlim _packageAccessLock = new();
private ContentPackage _package;
public ContentPackage Package
{
get
{
_packageAccessLock.EnterReadLock();
try
{
return _package;
}
finally
{
_packageAccessLock.ExitReadLock();
}
}
private set
{
_packageAccessLock.EnterWriteLock();
try
{
_package = value;
}
finally
{
_packageAccessLock.ExitWriteLock();
}
}
}
#endregion
#region DataContracts
#region Member: ModConfigInfo
private readonly ReaderWriterLockSlim _modConfigUsageLock = new();
private IModConfigInfo _modConfigInfo;
public IModConfigInfo ModConfigInfo
{
get
{
_modConfigUsageLock.EnterReadLock();
try
{
return _modConfigInfo;
}
finally
{
_modConfigUsageLock.ExitReadLock();
}
}
private set
{
_modConfigUsageLock.EnterWriteLock();
try
{
_modConfigInfo = value;
}
finally
{
_modConfigUsageLock.ExitWriteLock();
}
}
}
#endregion
public ImmutableArray<CultureInfo> SupportedCultures => ModConfigInfo?.SupportedCultures ?? ImmutableArray<CultureInfo>.Empty;
public ImmutableArray<IAssemblyResourceInfo> Assemblies => ModConfigInfo?.Assemblies ?? ImmutableArray<IAssemblyResourceInfo>.Empty;
public ImmutableArray<ILocalizationResourceInfo> Localizations => ModConfigInfo?.Localizations ?? ImmutableArray<ILocalizationResourceInfo>.Empty;
public ImmutableArray<ILuaResourceInfo> LuaScripts => ModConfigInfo?.LuaScripts ?? ImmutableArray<ILuaResourceInfo>.Empty;
public ImmutableArray<IConfigResourceInfo> Configs => ModConfigInfo?.Configs ?? ImmutableArray<IConfigResourceInfo>.Empty;
public ImmutableArray<IConfigProfileResourceInfo> ConfigProfiles => ModConfigInfo?.ConfigProfiles ?? ImmutableArray<IConfigProfileResourceInfo>.Empty;
#endregion
#region PublicAPI
public bool TryLoadResourcesInfo(ContentPackage package)
{
_operationsUsageLock.EnterWriteLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
}
// try loading the ModConfig.xml. If it fails, use the Legacy loader to try and construct one from the package structure.
if (_storageService.TryLoadPackageXml(package, "ModConfig.xml", out var configXml)
&& configXml.Root is not null)
{
if (_modConfigConverterService.Value.TryParseResource(configXml.Root, out IModConfigInfo configInfo))
{
ModConfigInfo = configInfo;
}
else
{
_loggerService.LogError(
$"Failed to parse ModConfig.xml for package {package.Name}, package mod content not loaded.");
return false;
}
}
else if (_legacyConfigService.Value.TryBuildModConfigFromLegacy(package, out var legacyConfig))
{
ModConfigInfo = legacyConfig;
}
else
{
// vanilla mod or broken
return false;
}
return true;
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitWriteLock();
}
}
public void LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
}
SanitationChecksCore(assembliesInfo, "assemblies", nameof(LoadPlugins));
SanitationChecksEnumerable(assembliesInfo.Assemblies, "assemblies", nameof(LoadPlugins));
#if DEBUG
assembliesInfo.Assemblies.ForEach(ari =>
{
if (!this.Assemblies.Contains(ari))
{
throw new ArgumentException(
$"Package Service: tried to load the assembly resource {ari.InternalName} for package {this.Package.Name} but it is not in the list for this package.");
}
});
#endif
// Order these assemblies by internal dependencies
ImmutableArray<IAssemblyResourceInfo> resources;
if (ignoreDependencySorting)
{
resources = assembliesInfo.Assemblies;
}
else // sort by load order
{
resources = assembliesInfo.Assemblies
.OrderByDescending(a => a.LoadPriority)
.ToImmutableArray();
}
// Try loading them, throw on failure.
if (!_pluginService.Value.TryLoadAndInstanceTypes<IAssemblyPlugin>(resources, true, out var instancedTypes))
{
throw new TypeLoadException($"PackageService: unable to load assemblies for package {this.Package.Name}! Aborting loading!");
}
PluginsLoaded = true;
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public void LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
}
SanitationChecksCore(localizationsInfo, "localizations", nameof(LoadLocalizations));
SanitationChecksEnumerable(localizationsInfo.Localizations, "localizations", nameof(LoadLocalizations));
#if DEBUG
localizationsInfo.Localizations.ForEach(ri =>
{
if (!this.Localizations.Contains(ri))
{
throw new ArgumentException(
$"Package Service: tried to load the localization resource for package {this.Package.Name} but it is not in the list for this package.");
}
});
#endif
if (!_localizationService.Value.TryLoadLocalizations(localizationsInfo.Localizations))
{
throw new FileLoadException($"Package Service: unable to load localizations for package {this.Package.Name}! Aborting!");
}
LocalizationsLoaded = true;
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public void AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
}
SanitationChecksCore(luaScriptsInfo, "luaScripts", nameof(AddLuaScripts));
SanitationChecksEnumerable(luaScriptsInfo.LuaScripts, "luaScripts", nameof(AddLuaScripts));
#if DEBUG
luaScriptsInfo.LuaScripts.ForEach(ri =>
{
if (!this.LuaScripts.Contains(ri))
{
throw new ArgumentException(
$"Package Service: tried to load the lua script resource for package {this.Package.Name} but it is not in the list for this package.");
}
});
#endif
if (!_luaScriptService.Value.TryAddScriptFiles(luaScriptsInfo.LuaScripts))
{
throw new ArgumentException(
$"Package Service: unable to add lua files for package {this.Package.Name}! Aborting!");
}
LuaScriptsLoaded = true;
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public void LoadConfig(
[NotNull]IConfigsResourcesInfo configsResourcesInfo,
[NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
}
SanitationChecksCore(configsResourcesInfo, "config", nameof(LoadConfig));
SanitationChecksCore(configProfilesResourcesInfo, "config profiles", nameof(LoadConfig));
SanitationChecksEnumerable(configsResourcesInfo.Configs, "config", nameof(LoadConfig));
SanitationChecksEnumerable(configProfilesResourcesInfo.ConfigProfiles, "config profiles", nameof(LoadConfig));
#if DEBUG
configsResourcesInfo.Configs.ForEach(ri =>
{
if (!this.Configs.Contains(ri))
{
throw new ArgumentException(
$"Package Service: tried to load the configs resource for package {this.Package.Name} but it is not in the list for this package.");
}
});
configProfilesResourcesInfo.ConfigProfiles.ForEach(ri =>
{
if (!this.ConfigProfiles.Contains(ri))
{
throw new ArgumentException(
$"Package Service: tried to load the localization resource for package {this.Package.Name} but it is not in the list for this package.");
}
});
#endif
if (!_configService.Value.TryAddConfigs(configsResourcesInfo.Configs))
{
throw new ArgumentException(
$"Package Service: unable to add configs for package {this.Package.Name}! Aborting!");
}
if (!_configService.Value.TryAddConfigsProfiles(configProfilesResourcesInfo.ConfigProfiles))
{
throw new ArgumentException(
$"Package Service: unable to add configs profiles for package {this.Package.Name}! Aborting!");
}
ConfigsLoaded = true;
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public void Dispose()
{
/*
* Notes: we need to unload this package from services in the order that the services are dependent on each other.
* Unloading Order: Lua Scripts > Assemblies > Config Profiles > Configs > Styles > Localizations
*/
_operationsUsageLock.EnterWriteLock();
try
{
if (this.Package is null)
{
_loggerService.LogError(
$"Package Service: cannot Dispose of service as ContentPackage and info is not set!");
return;
}
if (this.ModConfigInfo is null)
{
_loggerService.LogError($"Package Service: cannot Dispose of service as ModConfigInfo is not loaded!");
return;
}
/*
* To be graceful, we want to ensure that any async calls and other threads are allowed to be processed before we begin
* disposal to reduce friction with other thread operations, so we release the lock and periodically check it
* to see of other threads have finished operations before cleaning everything up.
*/
IsDisposed = true; // set stop flag, callers should handle exception cases
Interlocked.MemoryBarrier(); //ensure cache states
DateTime timeoutLimit = DateTime.Now.AddSeconds(10);
while (LoadingOperationsRunning)
{
_operationsUsageLock.ExitWriteLock();
Thread.Sleep(1);
_operationsUsageLock.EnterWriteLock();
if (timeoutLimit < DateTime.Now)
{
_loggerService.LogError($"Package Service: Dispose() time out reached while waiting for other operations. Continuing.");
break;
}
}
GC.SuppressFinalize(this);
_luaScriptService.Value.RemoveScriptFiles(this.LuaScripts);
_pluginService.Value.DisposePlugins();
_configService.Value.RemoveConfigsProfiles(this.ConfigProfiles);
_configService.Value.RemoveConfigs(this.Configs);
#if CLIENT
_stylesService.Value.UnloadAllStyles();
#endif
_localizationService.Value.Remove(this.Localizations);
ModConfigInfo = null;
Package = null;
}
catch
{
_loggerService.LogError($"Package Service: exception while running Dispose().");
throw;
}
finally
{
_operationsUsageLock.ExitWriteLock();
}
}
public void Reset()
{
_operationsUsageLock.EnterWriteLock();
try
{
if (this.Package is null)
{
_loggerService.LogError(
$"Package Service: cannot Dispose of service as ContentPackage and info is not set!");
return;
}
if (this.ModConfigInfo is null)
{
_loggerService.LogError($"Package Service: cannot Dispose of service as ModConfigInfo is not loaded!");
return;
}
Interlocked.MemoryBarrier(); //ensure cache states
DateTime timeoutLimit = DateTime.Now.AddSeconds(10);
while (LoadingOperationsRunning)
{
_operationsUsageLock.ExitWriteLock();
Thread.Sleep(1);
_operationsUsageLock.EnterWriteLock();
if (timeoutLimit < DateTime.Now)
{
_loggerService.LogError($"Package Service: Dispose() time out reached while waiting for other operations. Continuing.");
break;
}
}
if (LuaScriptsLoaded)
{
_luaScriptService.Value.RemoveScriptFiles(this.LuaScripts);
LuaScriptsLoaded = false;
}
if (PluginsLoaded)
{
_pluginService.Value.DisposePlugins();
PluginsLoaded = false;
}
if (ConfigsLoaded)
{
_configService.Value.RemoveConfigsProfiles(this.ConfigProfiles);
_configService.Value.RemoveConfigs(this.Configs);
ConfigsLoaded = false;
}
if (LocalizationsLoaded)
{
_localizationService.Value.Remove(this.Localizations);
LocalizationsLoaded = false;
}
}
finally
{
_operationsUsageLock.ExitWriteLock();
}
}
#endregion
#region INTERNAL
private void SanitationChecksCore(object o, string resTypeInfoName, string callerName)
{
if (o is null)
{
_loggerService.LogError($"Package Service: {resTypeInfoName} resources list is null!");
throw new NullReferenceException($"Package Service: {resTypeInfoName} resources list is null!");
}
if (this.Package is null)
{
_loggerService.LogError($"Package Service: package not set at {callerName}()!");
throw new NullReferenceException($"Package Service: package not set at {callerName}()!");
}
}
private void SanitationChecksEnumerable<T>(ImmutableArray<T> resourceInfos, string resTypeInfoName, string callerName) where T : IResourceInfo, IResourceCultureInfo, IPackageInfo, IPackageDependenciesInfo
{
// Check if list is empty. Nothing more to do.
if (resourceInfos.IsDefaultOrEmpty)
return;
// Check if all resources in the list are registered to this package, throw if not.
foreach (var resourceInfo in resourceInfos)
{
// ownership checks
if (resourceInfo.OwnerPackage is null)
{
throw new ArgumentException($"Package Service: {resTypeInfoName} info for resource does not have a package name set! Run by {this.Package.Name}.");
}
if (resourceInfo.OwnerPackage != this.Package)
{
throw new ArgumentException(
$"Package Service: {resTypeInfoName} info does not belong to this package! Owned by {resourceInfo.OwnerPackage.Name} but is run by {this.Package.Name}.");
}
// Check if external dependencies are loaded and if current environment is supported, throw if not
if (resourceInfo.Dependencies.IsDefaultOrEmpty)
continue;
bool resourceMissing = false;
resourceInfo.Dependencies.ForEach(pdi =>
{
// for clarification: assemblies passed to the function should always be loaded.
// optional assemblies should be filtered out before the list is sent.
// left this as a reminder :)
/*if (pdi.Optional)
return;*/
if (!_packageManagementService.CheckDependencyLoaded(pdi))
{
resourceMissing = true;
_loggerService.LogError(
$"Package Service: the following dependency for package {resourceInfo.OwnerPackage.Name} is not loaded: {pdi.DependencyPackage?.Name ?? (pdi.PackageName.IsNullOrWhiteSpace() ? pdi.SteamWorkshopId.ToString() : pdi.PackageName)}");
}
});
if (!resourceMissing)
{
throw new FileLoadException($"Package Service: dependencies for package {resourceInfo.OwnerPackage.Name} are not loaded.");
}
// check runtime platform
if (!_packageManagementService.CheckEnvironmentSupported(resourceInfo))
{
throw new PlatformNotSupportedException($"Package service: the {resTypeInfoName} from {resourceInfo.OwnerPackage.Name} is not supported on this platform.");
}
// check local culture
if (!_localizationService.Value.IsCurrentCultureSupported(resourceInfo))
{
throw new PlatformNotSupportedException($"Package service: the {resTypeInfoName} from {resourceInfo.OwnerPackage.Name} is not supported in this culture.");
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool GetThreadSafeBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) == 1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetThreadSafeBool(ref int var, bool value)
{
if (value)
{
Interlocked.CompareExchange(ref var, 1, 0);
}
else
{
Interlocked.CompareExchange(ref var, 0, 1);
}
}
#endregion
}
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services.Processing;
#region TypeDef
// ReSharper disable once TypeParameterCanBeVariant
public interface IConverterService<TSrc, TOut> : IService
{
bool TryParseResource(TSrc src, out TOut resources);
bool TryParseResources(IEnumerable<TSrc> sources, out List<TOut> resources);
}
public interface IXmlResourceConverterService<TOut> : IConverterService<XElement, TOut> { }
public interface IResourceToXmlConverterService<TSrc> : IConverterService<TSrc, XElement> { }
#endregion
/// <summary>
/// Parses Xml to produce loading metadata info for linked loadable files.
/// </summary>
#region XmlToResourceInfoParsers
public interface IXmlAssemblyResConverter : IXmlResourceConverterService<IAssemblyResourceInfo> { }
public interface IXmlConfigResConverterService : IXmlResourceConverterService<IConfigResourceInfo> { }
public interface IXmlLocalizationResConverterService : IXmlResourceConverterService<ILocalizationResourceInfo> { }
#endregion
/// <summary>
/// Parses Xml to produce ready-to-use info/data without any additional file/data loading.
/// </summary>
#region XmlToInfoParsers
public interface IXmlDependencyConverterService : IXmlResourceConverterService<IPackageDependencyInfo> { }
public interface IXmlModConfigConverterService : IXmlResourceConverterService<IModConfigInfo> { }
/// <summary>
/// Parses legacy packages that make use of the RunConfig.xml structure to produce a ModConfig.
/// </summary>
public interface IXmlLegacyModConfigConverterService : IXmlResourceConverterService<IModConfigInfo> { }
#endregion
#region ResToInfoParsers
public interface ILocalizationResToInfoParser : IConverterService<ILocalizationResourceInfo, ILocalizationInfo> { }
public interface IConfigResConverterService : IConverterService<IConfigResourceInfo, IConfigInfo> { }
public interface IConfigProfileResConverterService : IConverterService<IConfigProfileResourceInfo, IConfigProfileInfo> { }
#endregion
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaConfigService : ILuaService
{
}
@@ -0,0 +1,39 @@
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs.Services.Safe;
/// <summary>
/// Service for providing stateful functions and in-memory storage for lua functions
/// </summary>
public interface ILuaDataService : ILuaService
{
/// <summary>
/// Returns stored table for the given object if it exists.
/// </summary>
/// <param name="obj"></param>
/// <param name="tableName"></param>
/// <returns>The table data or null if none exists.</returns>
Table GetObjectTable(object obj, string tableName);
/// <summary>
/// Returns stored table data under the given name if it exists.
/// </summary>
/// <param name="tableName"></param>
/// <returns>The table data or null if none exists.</returns>
Table GetTable(string tableName);
/// <summary>
/// Returns stored table data for the given object or creates a new table if one doesn't exist.
/// </summary>
/// <param name="obj"></param>
/// <param name="tableName"></param>
/// <returns></returns>
Table GetOrCreateObjectTable(object obj, string tableName);
/// <summary>
/// Returns stored table data or creates a new table if one doesn't exist.
/// </summary>
/// <param name="tableName"></param>
/// <returns></returns>
Table GetOrCreateTable(string tableName);
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaEventService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaNetworkingService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaPackageManagementService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaPackageService : ILuaService
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaService
{
}
@@ -0,0 +1,231 @@
using System;
using System.Collections.Immutable;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using LightInject;
namespace Barotrauma.LuaCs.Services;
public class ServicesProvider : IServicesProvider
{
private ServiceContainer _serviceContainerInst;
private ServiceContainer ServiceContainer
{
get
{
// ReSharper disable once ConvertIfStatementToNullCoalescingExpression
if (_serviceContainerInst is null)
_serviceContainerInst = new ServiceContainer();
return _serviceContainerInst;
}
}
private readonly ReaderWriterLockSlim _serviceLock = new();
public void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new()
{
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>(lifetimeInstance);
ServiceContainer.Compile<TService>();
OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(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, new()
{
if (name.IsNullOrWhiteSpace())
{
throw new ArgumentNullException($"Tried to register a service of type {typeof(TService).Name} but the name provided is null or empty." );
}
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);
ServiceContainer.Compile<TService>();
OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(TService));
}
finally
{
_serviceLock.ExitReadLock();
}
}
public void Compile()
{
try
{
_serviceLock.EnterReadLock();
ServiceContainer?.Compile();
}
finally
{
_serviceLock.ExitReadLock();
}
}
public event Action<Type, Type> OnServiceRegistered;
public void InjectServices<T>(T inst) where T : class
{
try
{
_serviceLock.EnterReadLock();
ServiceContainer.InjectProperties(inst);
}
finally
{
_serviceLock.ExitReadLock();
}
}
public bool TryGetService<TSvcInterface>(out IService 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 bool TryGetService<TSvcInterface>(string name, out IService 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.NoInlining)]
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 DisposeAllServices().");
}
try
{
_serviceLock.EnterWriteLock();
_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;
}
}