[Milestone] AssemblyLoader completed.

Details:
- Assembly Mgmt Service for loading now a separate interface, not intended for normal use.
- Assembly Loader work; implemented custom dictionary key and table.
- Assembly loading work.
- EventService completed.
- Moved assembly extensions to ModUtils.cs
- Work to event service.
NetworkService work
- Added ImpromptuInterfaces package.
- Networking Service work to support NetVars
- Event Service
- Added assemblies references package for script compilation. Updated Roslyn version for compatibility.
- Package Loading work.
Swap Harmony to HarmonyX
- More refactor conversion to FluentResults.
- Updated StylesService to return Results.
- Refactor of PackageService partially complete.
- Made IService.Reset() required to return a Result.
- Moved plugin/assembly related code to their own folder (same namespace).
- Updated interfaces to reflect the use of Result<T>.
- Partial refactor, incomplete.
- Added 'FluentResults' so we can stop using cursed Exception-based flow control in loading code.
- Added 'OneOf' nuget package: https://github.com/mcintyre321/OneOf
for the implementation of the Optional<T> pattern and complex discrete return types instead of cursed enums (see current AssemblyManager.cs).
- Reapplied old branch changes.
This commit is contained in:
MapleWheels
2024-11-04 02:33:31 -05:00
committed by Maplewheels
parent 01cc1d331b
commit 6880e5e9ee
97 changed files with 4100 additions and 1512 deletions
@@ -8,6 +8,8 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Threading;
using FluentResults;
using FluentResults.LuaCs;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -25,7 +27,8 @@ namespace Barotrauma.LuaCs.Services;
/// 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
[Obsolete]
public class AssemblyManager : IAssemblyManagementService, IPluginManagementService
{
#region ExternalAPI
@@ -275,6 +278,11 @@ public class AssemblyManager : IAssemblyManagementService
}
}
public bool IsAssemblyLoadedGlobal(string friendlyName)
{
throw new NotImplementedException();
}
#endregion
#region InternalAPI
@@ -740,41 +748,12 @@ public class AssemblyManager : IAssemblyManagementService
TryBeginDispose();
}
public void Reset()
public FluentResults.Result Reset()
{
TryBeginDispose();
return TryBeginDispose() ? FluentResults.Result.Ok()
: FluentResults.Result.Fail(new Error($"{nameof(AssemblyManager)}: failed to Reset service.")
.WithMetadata(MetadataType.ExceptionObject, this));
}
}
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,15 @@
using System;
namespace Barotrauma.LuaCs.Services.Compatibility;
public interface ILuaCsHook : ILuaCsShim
{
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
void Add(string eventName, string identifier, LuaCsFunc callback, ACsMod mod = null);
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
void Add(string eventName, LuaCsFunc callback, ACsMod mod = null);
bool Exists(string eventName, string identifier);
[Obsolete("Only Lua subscribers will receive events from call. Use ILuaEventService.Add() instead.")]
T Call<T>(string eventName, params object[] args);
object Call(string eventName, params object[] args);
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Compatibility;
public interface ILuaCsLogger : ILuaCsShim
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Compatibility;
internal partial interface ILuaCsNetworking : ILuaCsShim
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Compatibility;
public interface ILuaCsShim
{
}
@@ -0,0 +1,6 @@
namespace Barotrauma.LuaCs.Services.Compatibility;
public interface ILuaCsUtility : ILuaCsShim
{
}
@@ -0,0 +1,376 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs.Services.Compatibility;
using Barotrauma.LuaCs.Services.Safe;
using Dynamitey;
using FluentResults;
using FluentResults.LuaCs;
using HarmonyLib;
using ImpromptuInterface;
using OneOf;
namespace Barotrauma.LuaCs.Services;
public class EventService : IEventService, IEventAssemblyContextUnloading
{
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;
HashCode = TypeName.GetHashCode();
}
public TypeStringKey(string typeName)
{
Type = null;
TypeName = typeName ?? 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);
}
/// <summary>
/// <para>Contains subscriber delegates by event and identifier.</para>
/// Structure:<br/>
/// - Key: Type or String, TypeName == String Equality.<br/>
/// - Value: Dictionary<br/>
/// ---- Key: Either string identifier or subscriber instance pointer<br/>
/// ---- Value: Subscriber delegate<br/>
/// </summary>
private readonly Dictionary<TypeStringKey, Dictionary<OneOf<string, IEvent>, IEvent>> _subscriptions = new();
private readonly Dictionary<string, string> _eventTypeNameAliases = new();
private readonly Lazy<IPluginManagementService> _pluginManagementService;
private readonly Dictionary<TypeStringKey, Action<string, IDictionary<string, LuaCsFunc>>> _luaSubscriptionFactories = new();
/// <summary>
/// A collection of factories to produce subscribers from a single lua function handle. For legacy Add() API.
/// </summary>
private readonly Dictionary<TypeStringKey, Action<string, LuaCsFunc>> _luaLegacySubscriptionFactories = new();
/// <summary>
/// A collection of lua event subscribers from Add() that had neither a valid event name nor an event alias pointing to one.
/// Only actionable via Call().
/// </summary>
private readonly Dictionary<string, Dictionary<string, LuaCsFunc>> _luaOrphanSubscribers = new();
public EventService(Lazy<IPluginManagementService> pluginManagementService)
{
_pluginManagementService = pluginManagementService ?? throw new ArgumentNullException(nameof(pluginManagementService));
}
public bool IsDisposed { get; private set; } = false;
#region Compatibility
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
void ILuaCsHook.Add(string eventName, string identifier, LuaCsFunc callback, ACsMod mod = null)
{
Add(eventName, identifier, callback);
}
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
void ILuaCsHook.Add(string eventName, LuaCsFunc callback, ACsMod mod = null)
{
Add(eventName, callback);
}
public bool Exists(string eventName, string identifier)
{
((IService)this).CheckDisposed();
if (_subscriptions.ContainsKey(eventName) && _subscriptions[eventName].ContainsKey(identifier))
return true;
if (_luaOrphanSubscribers.ContainsKey(eventName))
return true;
return false;
}
[Obsolete("Part of the legacy events API, only works for Lua-only custom events.")]
public T Call<T>(string eventName, params object[] args)
{
((IService)this).CheckDisposed();
if (!_luaOrphanSubscribers.TryGetValue(eventName, out var dict))
return default;
T returnValue = default;
foreach (var sub in dict.Values)
{
try
{
var r = sub(args);
if (r != default)
returnValue = (T)r;
}
catch
{
continue;
}
}
return returnValue;
}
[Obsolete("Part of the legacy events API, only works for Lua-only custom events.")]
public object Call(string eventName, params object[] args) => Call<object>(eventName, args);
#endregion
public void Add(string eventName, string identifier, LuaCsFunc callback)
{
var eventKey = eventName;
if (_eventTypeNameAliases.TryGetValue(eventName, out var aliasType))
eventKey = aliasType;
if (_luaLegacySubscriptionFactories.TryGetValue(eventKey, out var factory))
{
factory(identifier, callback);
return;
}
_luaOrphanSubscribers.TryGetOrSet(eventName, () => new Dictionary<string, LuaCsFunc>())
.Add(identifier.IsNullOrWhiteSpace() ? string.Empty : identifier, callback);
}
public void Add(string eventName, LuaCsFunc callback)
{
Add(eventName, string.Empty, callback);
}
public void Remove(string eventName, string identifier)
{
if (_luaOrphanSubscribers.TryGetValue(eventName, out var dict))
dict.Remove(identifier);
if (_subscriptions.TryGetValue(eventName, out var dict2))
dict2.Remove(identifier);
}
public void PublishLuaEvent(string interfaceName, LuaCsFunc runner)
{
((IService)this).CheckDisposed();
if (interfaceName.IsNullOrWhiteSpace())
return;
if (!_subscriptions.TryGetValue(interfaceName, out var dict))
return;
var type = _subscriptions
.Select(x => x.Key)
.FirstOrNull(x => x.Type?.Name == interfaceName)?.Type;
var errors = new Queue<IError>();
foreach (var eventSub in dict.Values)
{
try
{
runner(type is null ? eventSub : Convert.ChangeType(eventSub, type)); // cast if possible
}
catch
{
continue;
}
}
}
public FluentResults.Result RegisterSafeEvent<T>() where T : IEvent<T>
{
((IService)this).CheckDisposed();
var type = typeof(T);
if (_luaSubscriptionFactories.ContainsKey(type))
return FluentResults.Result.Ok().WithReason(new Success($"The event {type.Name} is already registered."));
try
{
_luaSubscriptionFactories.Add(type, (ident, funcDict) =>
{
var runner = T.GetLuaRunner(funcDict);
var dict = _subscriptions.TryGetOrSet(type, () => new Dictionary<OneOf<string, IEvent>, IEvent>());
if (!ident.IsNullOrWhiteSpace())
dict[ident] = runner;
else
dict[runner] = runner;
});
return FluentResults.Result.Ok();
}
catch (NullReferenceException e)
{
return FluentResults.Result.Fail(new Error($"The lua runner for {type.Name} is not registered.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, type));
}
}
public FluentResults.Result UnregisterSafeEvent<T>() where T : IEvent<T>
{
((IService)this).CheckDisposed();
_luaSubscriptionFactories.Remove(typeof(T));
if (!_subscriptions.TryGetValue(typeof(T), out var dict))
return FluentResults.Result.Ok();
dict.Values.Where(value => value.IsLuaRunner()).ToImmutableArray().ForEach(Unsubscribe);
return FluentResults.Result.Ok();
}
// lua subscribe
public void Subscribe(string interfaceName, string identifier, IDictionary<string, LuaCsFunc> callbacks)
{
((IService)this).CheckDisposed();
if (_luaSubscriptionFactories.TryGetValue(interfaceName, out var subFactory))
subFactory(identifier, callbacks);
}
public FluentResults.Result SetLegacyLuaRunnerFactory<T>(Func<LuaCsFunc, T> runnerFactory) where T : IEvent<T>
{
var type = typeof(T);
if (!_luaSubscriptionFactories.TryGetValue(type, out var dict))
return FluentResults.Result.Fail(new Error($"Tried to add legacy lua factory for an event not registered for lua subscriptions."));
_luaLegacySubscriptionFactories[type] = (ident, func) =>
{
var runner = runnerFactory(func);
_subscriptions.TryGetOrSet(type, () => new Dictionary<OneOf<string, IEvent>, IEvent>())[ident] = runner;
};
return FluentResults.Result.Ok();
}
public void RemoveLegacyLuaRunnerFactory<T>() where T : IEvent<T>
{
_luaLegacySubscriptionFactories.Remove(typeof(T));
}
public void SetAliasToEvent<T>(string alias) where T : IEvent<T>
{
if (alias.IsNullOrWhiteSpace())
return;
_eventTypeNameAliases[alias] = typeof(T).Name;
}
public void RemoveEventAlias(string alias)
{
_eventTypeNameAliases.Remove(alias);
}
public void RemoveAllEventAliases<T>() where T : IEvent<T>
{
foreach (var keys in _eventTypeNameAliases
.Where(kvp => kvp.Value.IsNullOrWhiteSpace() || kvp.Value == typeof(T).Name)
.Select(kvp => kvp.Key).ToImmutableArray())
{
_eventTypeNameAliases.Remove(keys);
}
}
public FluentResults.Result Subscribe<T>(T subscriber) where T : IEvent<T>
{
((IService)this).CheckDisposed();
var eventType = typeof(T);
var dict = _subscriptions.TryGetOrSet(eventType, () => new Dictionary<OneOf<string, IEvent>, IEvent>());
if (dict.ContainsKey(OneOf<string, IEvent>.FromT1(subscriber)))
{
return FluentResults.Result.Fail(
new Error($"The subscriber for {eventType.Name} is already registered to the event.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, subscriber));
}
dict[subscriber] = subscriber;
return FluentResults.Result.Ok();
}
public void Unsubscribe<T>(T subscriber) where T : IEvent
{
((IService)this).CheckDisposed();
if (!_subscriptions.TryGetValue(typeof(T), out var dict))
return;
dict.Remove(OneOf<string, IEvent>.FromT1(subscriber));
}
public void ClearAllEventSubscribers<T>() where T : IEvent => _subscriptions.Remove(typeof(T));
public void ClearAllSubscribers() => _subscriptions.Clear();
public FluentResults.Result PublishEvent<T>(Action<T> action) where T : IEvent<T>
{
((IService)this).CheckDisposed();
var eventType = typeof(T);
if (!_subscriptions.TryGetValue(eventType, out var dict))
{
return FluentResults.Result.Fail(new Error($"The event {eventType.Name} is not registered.")
.WithMetadata(MetadataType.ExceptionObject, this));
}
var errors = new Queue<IError>();
foreach (var eventSub in dict.Values)
{
try
{
action((T)eventSub);
}
catch (Exception e)
{
errors.Enqueue(new Error($"Error while executing runner for {eventType.Name} on type {eventSub.GetType().Name}.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, eventSub)
.WithMetadata(MetadataType.ExceptionDetails, e.Message)
.WithMetadata(MetadataType.StackTrace, e.StackTrace));
}
}
var result = errors.Count > 0 ? FluentResults.Result.Fail($"Errors while executing event type {eventType.Name}") : FluentResults.Result.Ok();
while (errors.Count > 0)
result = result.WithError(errors.Dequeue());
return result;
}
public void Dispose()
{
IsDisposed = true;
_subscriptions.Clear();
_luaSubscriptionFactories.Clear();
_eventTypeNameAliases.Clear();
GC.SuppressFinalize(this);
}
public FluentResults.Result Reset()
{
((IService)this).CheckDisposed();
_subscriptions.Clear();
_luaSubscriptionFactories.Clear();
_eventTypeNameAliases.Clear();
return FluentResults.Result.Ok();
}
public void OnAssemblyUnloading(WeakReference<IAssemblyLoaderService> loaderService)
{
if (!loaderService.TryGetTarget(out var loader))
return;
foreach (var assembly in loader.Assemblies)
{
var types = assembly.GetSafeTypes()
.Where(t => typeof(IEvent).IsAssignableFrom(t))
.ToImmutableArray();
if (!types.Any())
continue;
foreach (var type in types)
{
_subscriptions.Remove(type);
_luaSubscriptionFactories.Remove(type);
}
}
}
}
@@ -1,189 +0,0 @@
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
}
@@ -1,50 +0,0 @@
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();
}
@@ -1,6 +0,0 @@
namespace Barotrauma.LuaCs.Services;
public interface IHookManagementService : IService
{
}
@@ -1,8 +0,0 @@
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface ILegacyConfigService : IService
{
bool TryBuildModConfigFromLegacy(ContentPackage package, out IModConfigInfo configInfo);
}
@@ -1,21 +0,0 @@
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);
}
@@ -1,23 +0,0 @@
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
}
@@ -1,21 +0,0 @@
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);
}
@@ -1,7 +0,0 @@
namespace Barotrauma.LuaCs.Services;
public interface IPluginManagementService : IService
{
bool IsAssemblyLoadedGlobal(string friendlyName);
}
@@ -1,15 +0,0 @@
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();
}
@@ -1,42 +0,0 @@
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
}
@@ -129,6 +129,11 @@ public partial class LoggerService : ILoggerService
#endif
}
public void LogResults(FluentResults.Result result)
{
throw new NotImplementedException();
}
public void LogDebug(string message, Color? color = null)
{
throw new NotImplementedException();
@@ -145,5 +150,5 @@ public partial class LoggerService : ILoggerService
}
public void Dispose() { }
public void Reset() { }
public FluentResults.Result Reset() => FluentResults.Result.Ok();
}
@@ -1,6 +1,176 @@
namespace Barotrauma.LuaCs.Services;
using Barotrauma.LuaCs.Data;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Immutable;
using System.Reflection;
public class LuaScriptService
namespace Barotrauma.LuaCs.Services;
public class LuaScriptService : ILuaScriptService, ILuaScriptManagementService
{
public void AddField(IUserDataDescriptor descriptor, string fieldName, DynValue value)
{
throw new NotImplementedException();
}
public void AddMethod(IUserDataDescriptor descriptor, string methodName, object function)
{
throw new NotImplementedException();
}
public FluentResults.Result AddScriptFiles(ImmutableArray<ILuaResourceInfo> luaResource)
{
throw new System.NotImplementedException();
}
public object CreateEnumTable(string typeName)
{
throw new NotImplementedException();
}
public object CreateStatic(string typeName)
{
throw new NotImplementedException();
}
public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor descriptor)
{
throw new NotImplementedException();
}
public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
throw new NotImplementedException();
}
public void Dispose()
{
throw new System.NotImplementedException();
}
public FluentResults.Result ExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false)
{
throw new NotImplementedException();
}
public FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaResourceInfo> scripts, bool pauseExecutionOnError = false, bool verboseLogging = false)
{
throw new NotImplementedException();
}
public FluentResults.Result ExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false)
{
throw new NotImplementedException();
}
public FluentResults.Result ExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false)
{
throw new System.NotImplementedException();
}
public FieldInfo FindFieldRecursively(Type type, string fieldName)
{
throw new NotImplementedException();
}
public MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null)
{
throw new NotImplementedException();
}
public PropertyInfo FindPropertyRecursively(Type type, string propertyName)
{
throw new NotImplementedException();
}
public ImmutableArray<ILuaResourceInfo> GetScriptResources()
{
throw new System.NotImplementedException();
}
public bool HasMember(object obj, string memberName)
{
throw new NotImplementedException();
}
public bool IsRegistered(Type type)
{
throw new NotImplementedException();
}
public bool IsTargetType(object obj, string typeName)
{
throw new NotImplementedException();
}
public void MakeFieldAccessible(IUserDataDescriptor descriptor, string fieldName)
{
throw new NotImplementedException();
}
public void MakeMethodAccessible(IUserDataDescriptor descriptor, string methodName, string[] parameters = null)
{
throw new NotImplementedException();
}
public void MakePropertyAccessible(IUserDataDescriptor descriptor, string propertyName)
{
throw new NotImplementedException();
}
public IUserDataDescriptor RegisterGenericType(Type type)
{
throw new NotImplementedException();
}
public IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArgs)
{
throw new NotImplementedException();
}
public IUserDataDescriptor RegisterType(Type type)
{
throw new NotImplementedException();
}
public IUserDataDescriptor RegisterType(string typeName)
{
throw new NotImplementedException();
}
public void RemoveMember(IUserDataDescriptor descriptor, string memberName)
{
throw new NotImplementedException();
}
public void RemoveScriptFiles(ImmutableArray<ILuaResourceInfo> luaResource)
{
throw new System.NotImplementedException();
}
public FluentResults.Result Reset()
{
throw new System.NotImplementedException();
}
public string TypeOf(object obj)
{
throw new NotImplementedException();
}
public void UnregisterAllTypes()
{
throw new NotImplementedException();
}
public void UnregisterType(Type type)
{
throw new NotImplementedException();
}
public void UnregisterType(string typeName)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,128 @@
using Barotrauma.LuaCs.Services;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
namespace Barotrauma.LuaCs.Networking;
internal partial class NetworkingService : INetworkingService
{
private enum LuaCsClientToServer
{
NetMessageId,
NetMessageString,
RequestSingleId,
RequestAllIds,
}
private enum LuaCsServerToClient
{
NetMessageId,
NetMessageString,
ReceiveIds
}
private Dictionary<Guid, INetVar> netVars = new Dictionary<Guid, INetVar>();
private Dictionary<Guid, NetMessageReceived> netReceives = new Dictionary<Guid, NetMessageReceived>();
private Dictionary<ushort, Guid> packetToId = new Dictionary<ushort, Guid>();
private Dictionary<Guid, ushort> idToPacket = new Dictionary<Guid, ushort>();
public bool IsActive
{
get
{
return GameMain.NetworkMember != null; // ehh?
}
}
public bool IsSynchronized { get; private set; }
public bool IsDisposed { get; private set; }
public void Initialize()
{
#if SERVER
IsSynchronized = true;
#elif CLIENT
SendSyncMessage();
#endif
}
public void RegisterNetVar(INetVar netVar)
{
netVars[netVar.InstanceId] = netVar;
netReceives[netVar.InstanceId] = (IReadMessage netMessage) =>
{
INetReadMessage internalMind = new NetReadMessage();
internalMind.SetMessage(netMessage);
netVar.ReadNetMessage(internalMind);
};
}
public void SendNetVar(INetVar netVar)
{
if (netVars.ContainsKey(netVar.InstanceId))
{
INetWriteMessage message = Start(netVar.InstanceId);
netVar.WriteNetMessage(message);
Send(message.Message);
}
}
public void Receive(Guid netId, NetMessageReceived callback)
{
#if SERVER
RegisterId(netId);
#elif CLIENT
RequestId(netId);
#endif
netReceives[netId] = callback;
}
private void HandleNetMessage(IReadMessage netMessage, Guid netId, Client client = null)
{
if (netReceives.ContainsKey(netId))
{
try
{
netReceives[netId](netMessage);
}
catch (Exception e)
{
LuaCsLogger.LogError($"Exception thrown inside NetMessageReceive({netId})", LuaCsMessageOrigin.CSharpMod);
LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod);
}
}
else
{
if (GameSettings.CurrentConfig.VerboseLogging)
{
#if SERVER
LuaCsLogger.LogError($"Received NetMessage for unknown netid {netId} from {GameServer.ClientLogName(client)}.");
#else
LuaCsLogger.LogError($"Received NetMessage for unknown netid {netId} from server.");
#endif
}
}
}
private void HandleNetMessageString(IReadMessage netMessage, Client client = null)
{
Guid guid = new Guid(netMessage.ReadBytes(16));
HandleNetMessage(netMessage, guid, client);
}
public FluentResults.Result Reset()
{
IsSynchronized = false;
netReceives = new Dictionary<Guid, NetMessageReceived>();
packetToId = new Dictionary<ushort, Guid>();
idToPacket = new Dictionary<Guid, ushort>();
return FluentResults.Result.Ok();
}
public void Dispose()
{
IsDisposed = true;
}
}
@@ -1,14 +1,41 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using Barotrauma.Steam;
using FluentResults;
using FluentResults.LuaCs;
using QuikGraph;
namespace Barotrauma.LuaCs.Services;
public class PackageManagementService : IPackageManagementService, IPluginManagementService
public class PackageManagementService : IPackageManagementService
{
private readonly Func<IPackageService> _contentPackageServiceFactory;
private readonly Lazy<IAssemblyManagementService> _assemblyManagementService;
private readonly ConcurrentDictionary<ContentPackage, IPackageService> _contentPackages = new();
private readonly ConcurrentQueue<LoadablePackage> _queuedPackages = new();
private readonly ConcurrentDictionary<DependencyEntryKey, IPackageDependencyInfo> _packageDependencyInfos = new();
/// <summary>
/// ConcurrentDictionary handles access/read synchronization. This is to ensure that we are not trying to
/// access the collection during a load/unload/modify operation.
/// </summary>
private readonly ReaderWriterLockSlim _contentPackagesModificationsLock = new();
/// <summary>
/// This lock ensures that we are not adding new entries to the queue between when we read the contents and
/// empty the buffer.
/// </summary>
private readonly ReaderWriterLockSlim _packageQueueProcessingLock = new();
public PackageManagementService(
Func<IPackageService> getPackageService,
Lazy<IAssemblyManagementService> assemblyManagementService)
@@ -17,55 +44,418 @@ public class PackageManagementService : IPackageManagementService, IPluginManage
this._assemblyManagementService = assemblyManagementService;
}
#region STATE_RESET
public void Dispose()
{
// TODO release managed resources here
}
public void Reset()
public FluentResults.Result Reset()
{
throw new NotImplementedException();
}
public bool IsAssemblyLoadedGlobal(string friendlyName)
#endregion
public void QueuePackages(ImmutableArray<LoadablePackage> packages)
{
_packageQueueProcessingLock.EnterReadLock();
try
{
foreach (LoadablePackage package in packages)
_queuedPackages.Enqueue(package);
}
finally
{
_packageQueueProcessingLock.ExitReadLock();
}
}
public FluentResults.Result ParseQueuedPackages(bool loadParallel = true, bool reportFailOnDuplicates = false)
{
if (!ModUtils.Environment.IsMainThread)
throw new InvalidOperationException($"{nameof(ParseQueuedPackages)}: This method can only be called on the main thread.");
ImmutableArray<LoadablePackage> packagesToProcess = ImmutableArray<LoadablePackage>.Empty;
_packageQueueProcessingLock.EnterWriteLock();
try
{
Interlocked.MemoryBarrier();
if (_queuedPackages.IsEmpty)
return FluentResults.Result.Ok().WithSuccess($"{nameof(ParseQueuedPackages)}: The Queue is empty.");
packagesToProcess = _queuedPackages.Where(p => p.Package is not null)
.Distinct().ToImmutableArray();
_queuedPackages.Clear();
}
finally
{
_packageQueueProcessingLock.ExitWriteLock();
}
FluentResults.Result[] loadResults = new FluentResults.Result[packagesToProcess.Length];
FluentResults.Result res = new FluentResults.Result();
// Load ModConfigInfo
_contentPackagesModificationsLock.EnterWriteLock();
try
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
Interlocked.MemoryBarrier();
if (loadParallel)
{
Parallel.For(0, loadResults.Length, new ParallelOptions()
{
/*
* This is an IO-bound operation. The purpose of parallelism here is to allow loaded package
* data to be processed while another package is waiting on the storage device for its info.
*/
MaxDegreeOfParallelism = 2
},i =>
{
loadResults[i] = LoadPackageInfo(packagesToProcess[i]);
});
}
else
{
for (int i = 0; i < loadResults.Length; i++)
{
loadResults[i] = LoadPackageInfo(packagesToProcess[i]);
}
}
stopwatch.Stop();
res.WithSuccess(new Success(
$"Completed parsing of {loadResults.Length} packages in {stopwatch.ElapsedMilliseconds} milliseconds."));
for (int i = 0; i < loadResults.Length; i++)
{
res = loadResults[i].IsSuccess
? res.WithSuccesses(loadResults[i].Successes)
: res.WithErrors(loadResults[i].Errors);
}
return res;
}
catch (AggregateException ae)
{
return FluentResults.Result.Fail(new Error($"{nameof(ParseQueuedPackages)}: Failed to load packages! AE.")
.WithMetadata(MetadataType.ExceptionDetails, ae.InnerException?.Message ?? ae.Message)
.WithMetadata(MetadataType.StackTrace, ae.StackTrace)
.WithMetadata(MetadataType.ExceptionObject, this));
}
catch (ArgumentNullException ane)
{
return FluentResults.Result.Fail(
new Error($"{nameof(ParseQueuedPackages)}: Failed to load packages! ANE.")
.WithMetadata(MetadataType.ExceptionDetails, ane.InnerException?.Message ?? ane.Message)
.WithMetadata(MetadataType.StackTrace, ane.StackTrace)
.WithMetadata(MetadataType.ExceptionObject, this));
}
finally
{
_contentPackagesModificationsLock.ExitWriteLock();
}
/*
* Helper functions
*/
// register in the list so we can check against it.
FluentResults.Result LoadPackageInfo(LoadablePackage package)
{
try
{
if (package.Package == null)
{
return FluentResults.Result.Fail(
new Error($"{nameof(LoadPackageInfo)}: Package is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
if (_contentPackages.TryGetValue(package.Package, out var packageService))
{
if (reportFailOnDuplicates)
{
return FluentResults.Result.Fail(new Error($"The package {package.Package?.Name} is already loaded.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package.Package));
}
return FluentResults.Result.Ok();
}
packageService = _contentPackageServiceFactory.Invoke();
_contentPackages[package.Package] = packageService;
return packageService.LoadResourcesInfo(package);
}
catch (NullReferenceException nre)
{
return FluentResults.Result.Fail(new Error($"{nameof(LoadPackageInfo)}: NRE while loading package {package.Package?.Name}!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.StackTrace, nre.StackTrace ?? "StackTrace not available")
.WithMetadata(MetadataType.ExceptionDetails, nre.InnerException?.Message ?? nre.Message)
.WithMetadata(MetadataType.RootObject, package));
}
}
}
public FluentResults.Result LoadPackageConfigsResourcesGroup(bool loadParallel = true)
{
throw new NotImplementedException();
}
public void AddPackages(ref ReadOnlySpan<(ContentPackage, bool)> packages, bool executeImmediately = false, bool errorOnFailures = false,
bool errorOnExistingPackageFound = false)
public FluentResults.Result LoadAllPackageResources(bool loadParallel = true, bool safeResourcesOnly = true)
{
throw new NotImplementedException();
}
public void LoadPackages(bool onlyUnloadedPackages = true, bool rescanPackages = false)
public FluentResults.Result UnloadPackages()
{
if (!ModUtils.Environment.IsMainThread)
{
return FluentResults.Result.Fail(
new ExceptionalError(new InvalidOperationException($"{nameof(UnloadPackages)}: This method can only be called on the main thread."))
.WithMetadata(MetadataType.ExceptionObject, this));
}
var res = new FluentResults.Result();
_contentPackagesModificationsLock.EnterWriteLock();
try
{
// TODO: Finish him
}
finally
{
_contentPackagesModificationsLock.ExitWriteLock();
}
throw new NotImplementedException();
}
public void UnloadPackages(bool errorOnFailures = true)
{
throw new NotImplementedException();
}
public bool IsPackageLoaded(ContentPackage package) => package is not null && _contentPackages.ContainsKey(package);
public bool IsPackageLoaded(ContentPackage package)
{
throw new NotImplementedException();
}
public bool CheckDependencyLoaded(IPackageDependencyInfo info) =>
info is not null && IsPackageLoaded(info.DependencyPackage);
public bool CheckDependencyLoaded(IPackageDependencyInfo info)
public bool CheckDependenciesLoaded([NotNull]IEnumerable<IPackageDependencyInfo> infos, out ImmutableArray<IPackageDependencyInfo> missingPackages)
{
throw new NotImplementedException();
var missing = ImmutableArray.CreateBuilder<IPackageDependencyInfo>();
missing.AddRange(infos
.Where(i => i.DependencyPackage is not null)
.DistinctBy(i => i.DependencyPackage)
.Where(i => !CheckDependencyLoaded(i)));
missingPackages = missing.MoveToImmutable();
return missingPackages.Length == 0;
}
public bool CheckDependenciesLoaded(IEnumerable<IPackageDependencyInfo> infos, out IReadOnlyList<IPackageDependencyInfo> missingPackages)
{
throw new NotImplementedException();
}
public bool CheckEnvironmentSupported(IPlatformInfo platform)
{
return (platform.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0
&& (platform.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0;
}
public Result<IPackageDependencyInfo> GetPackageDependencyInfoRecord(ContentPackage package, bool addIfMissing = false)
{
if (package is null)
{
return new FluentResults.Result<IPackageDependencyInfo>()
.WithError(new Error($"{nameof(GetPackageDependencyInfoRecord)}: Package is null!")
.WithMetadata(MetadataType.ExceptionObject, this));
}
if (_packageDependencyInfos.TryGetValue(package, out var result))
{
return new FluentResults.Result<IPackageDependencyInfo>()
.WithValue(result);
}
if (addIfMissing)
{
return AddDependencyRecord(package, package.Name, package.Path,
package.TryExtractSteamWorkshopId(out var id) ? id.Value : 0,
false);
}
return FluentResults.Result.Fail<IPackageDependencyInfo>(new Error($"Could not find package {package.Name}!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
public Result<IPackageDependencyInfo> GetPackageDependencyInfoRecord(ulong steamWorkshopId, string packageName, string folderPath = null,
bool addIfMissing = false)
{
if (packageName.IsNullOrWhiteSpace() || folderPath.IsNullOrWhiteSpace())
{
return new FluentResults.Result<IPackageDependencyInfo>()
.WithError(new Error($"{nameof(GetPackageDependencyInfoRecord)}: folder path and/or package name are null!")
.WithMetadata(MetadataType.ExceptionObject, this));
}
if (_packageDependencyInfos.TryGetValue((packageName,steamWorkshopId,folderPath), out var result))
{
return new FluentResults.Result<IPackageDependencyInfo>()
.WithValue(result);
}
// TODO: Finish this
throw new NotImplementedException();
}
public Result<IPackageDependencyInfo> GetPackageDependencyInfoRecord(string folderPath)
{
throw new NotImplementedException();
}
public IPackageDependencyInfo CreateOrphanPackageDependencyInfoRecord(
string packageName,
string packagePath,
ulong steamWorkshopId)
{
return new DependencyInfo()
{
DependencyPackage = null,
FallbackPackageName = packageName,
FolderPath = packagePath.IsNullOrWhiteSpace() ? null : System.IO.Path.GetFullPath(packagePath),
SteamWorkshopId = steamWorkshopId,
IsMissing = true,
IsWorkshopInstallation = false
};
}
private Result<IPackageDependencyInfo> AddDependencyRecord(
ContentPackage package,
string packageName,
string folderPath,
ulong steamWorkshopId,
bool isMissing)
{
// TODO: Redo
try
{
var dependencyInfo = new DependencyInfo()
{
DependencyPackage = package,
FallbackPackageName = packageName,
FolderPath = System.IO.Path.GetFullPath(folderPath),
SteamWorkshopId = steamWorkshopId,
IsMissing = isMissing,
IsWorkshopInstallation = steamWorkshopId != 0
};
if (package is not null)
{
_packageDependencyInfos.AddOrUpdate(package, pack => dependencyInfo,
(pack, dep) => dependencyInfo);
}
return new FluentResults.Result<IPackageDependencyInfo>()
.WithValue(dependencyInfo)
.WithSuccess($"New value created.");
}
catch (Exception ex)
{
return new FluentResults.Result<IPackageDependencyInfo>()
.WithError(new ExceptionalError(ex)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.ExceptionDetails, ex.Message)
.WithMetadata(MetadataType.RootObject, package)
.WithMetadata(MetadataType.StackTrace, ex.StackTrace ?? "StackTrace not available"));
}
}
private readonly record struct DependencyEntryKey : IEqualityComparer<DependencyEntryKey>, IEquatable<DependencyEntryKey>
{
public ContentPackage Package { get; init; }
public string FolderPath { get; init; }
public string PackageName { get; init; }
public ulong SteamWorkshopId { get; init; }
public DependencyEntryKey(ContentPackage package)
{
Package = package ?? throw new ArgumentNullException(nameof(package), $"{nameof(DependencyEntryKey)}.ctor: Package cannot be null!");
PackageName = package.Name;
SteamWorkshopId = package.TryExtractSteamWorkshopId(out var id) ? id.Value : (ulong)0;
FolderPath = package.Path;
}
public DependencyEntryKey(string packageName, string folderPath, ulong steamWorkshopId)
{
PackageName = packageName;
SteamWorkshopId = steamWorkshopId;
FolderPath = folderPath;
Package = null;
}
public DependencyEntryKey(string packageName, ulong steamWorkshopId)
{
PackageName = packageName;
SteamWorkshopId = steamWorkshopId;
FolderPath = null;
Package = null;
}
public bool Equals(DependencyEntryKey other)
{
return Equals(this, other);
}
public override int GetHashCode()
{
return GetHashCode(this);
}
public bool Equals(DependencyEntryKey x, DependencyEntryKey y)
{
if (x == y)
return true;
if (x.Package is not null && y.Package is not null && x.Package == Package)
return true;
// folder should be a unique key if not unset.
if (!x.FolderPath.IsNullOrWhiteSpace() && !y.FolderPath.IsNullOrWhiteSpace() &&
x.FolderPath == FolderPath)
return true;
if (!x.PackageName.IsNullOrWhiteSpace() && !y.PackageName.IsNullOrWhiteSpace()
&& x.SteamWorkshopId != 0 && y.SteamWorkshopId != 0)
return x.PackageName == y.PackageName && x.SteamWorkshopId == y.SteamWorkshopId;
if (!x.PackageName.IsNullOrWhiteSpace() && !y.PackageName.IsNullOrWhiteSpace() && x.PackageName == PackageName)
return true;
if (x.SteamWorkshopId != 0 && y.SteamWorkshopId != 0 &&
x.SteamWorkshopId == y.SteamWorkshopId)
return true;
return false;
}
public int GetHashCode(DependencyEntryKey obj)
{
if (!obj.PackageName.IsNullOrWhiteSpace())
return obj.PackageName.GetHashCode();
if (obj.SteamWorkshopId != 0)
return obj.SteamWorkshopId.GetHashCode();
if (obj.Package is not null)
return obj.Package.GetHashCode();
// We don't want to check the FolderPath because we want to resolve dependencies using packages
// that might be local instead in the workshop folder.
return 2342568; // random const value: collisions are fine as we want to call Equals()
}
public static implicit operator DependencyEntryKey(ContentPackage package) => new(package);
public static implicit operator DependencyEntryKey((string packageName, ulong steamWorkshopId) tuple1) =>
new (tuple1.packageName, tuple1.steamWorkshopId);
public static implicit operator DependencyEntryKey((string packageName, ulong steamWorkshopId, string folderPath) tuple1) =>
new (tuple1.packageName, tuple1.folderPath, tuple1.steamWorkshopId);
}
}
@@ -10,6 +10,9 @@ using System.Threading;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Services.Processing;
using FluentResults;
using FluentResults.LuaCs;
using OneOf;
namespace Barotrauma.LuaCs.Services;
@@ -20,8 +23,7 @@ public partial class PackageService : IPackageService
// mod config / package scanners/parsers
private readonly Lazy<IXmlModConfigConverterService> _modConfigConverterService;
private readonly Lazy<ILegacyConfigService> _legacyConfigService;
private readonly Lazy<IModConfigParserService> _configParserService;
private readonly Lazy<ILuaScriptService> _luaScriptService;
private readonly Lazy<ILocalizationService> _localizationService;
private readonly Lazy<IPluginService> _pluginService;
@@ -35,31 +37,32 @@ public partial class PackageService : IPackageService
// state monitors
private int _configsLoaded, _localizationsLoaded, _luaScriptsLoaded, _pluginsLoaded, _isDisposed;
private int _loadingOperationsRunning;
private int _isEnabledInModList;
public bool ConfigsLoaded
{
get => GetThreadSafeBool(ref _configsLoaded);
private set => SetThreadSafeBool(ref _configsLoaded, value);
get => ModUtils.Threading.GetBool(ref _configsLoaded);
private set => ModUtils.Threading.SetBool(ref _configsLoaded, value);
}
public bool LocalizationsLoaded
{
get => GetThreadSafeBool(ref _localizationsLoaded);
private set => SetThreadSafeBool(ref _localizationsLoaded, value);
get => ModUtils.Threading.GetBool(ref _localizationsLoaded);
private set => ModUtils.Threading.SetBool(ref _localizationsLoaded, value);
}
public bool LuaScriptsLoaded
{
get => GetThreadSafeBool(ref _luaScriptsLoaded);
private set => SetThreadSafeBool(ref _luaScriptsLoaded, value);
get => ModUtils.Threading.GetBool(ref _luaScriptsLoaded);
private set => ModUtils.Threading.SetBool(ref _luaScriptsLoaded, value);
}
public bool PluginsLoaded
{
get => GetThreadSafeBool(ref _pluginsLoaded);
private set => SetThreadSafeBool(ref _pluginsLoaded, value);
get => ModUtils.Threading.GetBool(ref _pluginsLoaded);
private set => ModUtils.Threading.SetBool(ref _pluginsLoaded, value);
}
public bool IsDisposed
{
get => GetThreadSafeBool(ref _isDisposed);
private set => SetThreadSafeBool(ref _isDisposed, value);
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
private bool LoadingOperationsRunning
@@ -146,6 +149,12 @@ public partial class PackageService : IPackageService
}
}
public bool IsEnabledInModList
{
get => ModUtils.Threading.GetBool(ref _isEnabledInModList);
private set => ModUtils.Threading.SetBool(ref _isEnabledInModList, value);
}
#endregion
public ImmutableArray<CultureInfo> SupportedCultures => ModConfigInfo?.SupportedCultures ?? ImmutableArray<CultureInfo>.Empty;
@@ -159,43 +168,48 @@ public partial class PackageService : IPackageService
#region PublicAPI
public bool TryLoadResourcesInfo(ContentPackage package)
public FluentResults.Result LoadResourcesInfo(LoadablePackage cpackage)
{
if (cpackage.Package == null)
{
return FluentResults.Result.Fail(new Error($"{nameof(LoadResourcesInfo)}: Package is null!")
.WithMetadata(MetadataType.ExceptionObject,this)
.WithMetadata(MetadataType.RootObject, cpackage));
}
ContentPackage package = cpackage.Package;
_operationsUsageLock.EnterWriteLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
return FluentResults.Result.Fail(
new Error("Service is disposed.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
// 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)
var res = _configParserService.Value.BuildConfigForPackage(package);
if (res.IsFailed)
{
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 FluentResults.Result.Fail(res.Errors)
.WithError(new Error("PackageService failed to load ModConfigInfo")
.WithMetadata(MetadataType.ExceptionObject, _configParserService)
.WithMetadata(MetadataType.RootObject, package));
}
return true;
this.ModConfigInfo = res.Value;
this.IsEnabledInModList = cpackage.IsEnabled;
return FluentResults.Result.Ok();
}
catch (Exception e)
{
return FluentResults.Result.Fail(new Error(e.Message)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package)
.WithMetadata(MetadataType.StackTrace, e.StackTrace));
}
finally
{
@@ -204,31 +218,19 @@ public partial class PackageService : IPackageService
}
}
public void LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false)
public FluentResults.Result LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT0(assembliesInfo)) is { IsFailed: true } failed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
return failed;
}
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)
@@ -243,12 +245,15 @@ public partial class PackageService : IPackageService
}
// Try loading them, throw on failure.
if (!_pluginService.Value.TryLoadAndInstanceTypes<IAssemblyPlugin>(resources, true, out var instancedTypes))
if (_pluginService.Value.LoadAndInstanceTypes<IAssemblyPlugin>(resources, true, out var instancedTypes) is { IsFailed: true} failed2)
{
throw new TypeLoadException($"PackageService: unable to load assemblies for package {this.Package.Name}! Aborting loading!");
return failed2.WithError(new Error($"{nameof(LoadPlugins)}: Failed to load plugins for {this.Package.Name}")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, assembliesInfo));
}
PluginsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
@@ -257,37 +262,28 @@ public partial class PackageService : IPackageService
}
}
public void LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo)
public FluentResults.Result LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT1(localizationsInfo)) is { IsFailed: true } failed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
return failed;
}
SanitationChecksCore(localizationsInfo, "localizations", nameof(LoadLocalizations));
SanitationChecksEnumerable(localizationsInfo.Localizations, "localizations", nameof(LoadLocalizations));
#if DEBUG
localizationsInfo.Localizations.ForEach(ri =>
if (_localizationService.Value.LoadLocalizations(localizationsInfo.Localizations) is { IsFailed: true} failed2)
{
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!");
return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load localizations")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, localizationsInfo));
}
LocalizationsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
@@ -296,38 +292,28 @@ public partial class PackageService : IPackageService
}
}
public void AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo)
public FluentResults.Result AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT4(luaScriptsInfo)) is { IsFailed: true } failed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
return failed;
}
SanitationChecksCore(luaScriptsInfo, "luaScripts", nameof(AddLuaScripts));
SanitationChecksEnumerable(luaScriptsInfo.LuaScripts, "luaScripts", nameof(AddLuaScripts));
#if DEBUG
luaScriptsInfo.LuaScripts.ForEach(ri =>
if (_luaScriptService.Value.AddScriptFiles(luaScriptsInfo.LuaScripts) is { IsFailed: true} failed2)
{
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!");
return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load lua scripts.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, luaScriptsInfo));
}
LuaScriptsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
@@ -336,7 +322,7 @@ public partial class PackageService : IPackageService
}
}
public void LoadConfig(
public FluentResults.Result LoadConfig(
[NotNull]IConfigsResourcesInfo configsResourcesInfo,
[NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo)
{
@@ -344,48 +330,38 @@ public partial class PackageService : IPackageService
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
// register configs
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT2(configsResourcesInfo)) is { IsFailed: true } failed)
{
throw new ObjectDisposedException($"This package service instance is disposed!");
return failed;
}
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 (_configService.Value.AddConfigs(configsResourcesInfo.Configs) is { IsFailed: true} failed2)
{
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!");
return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load configs.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, configsResourcesInfo));
}
if (!_configService.Value.TryAddConfigsProfiles(configProfilesResourcesInfo.ConfigProfiles))
// register config profiles
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT3(configProfilesResourcesInfo)) is { IsFailed: true } failed3)
{
throw new ArgumentException(
$"Package Service: unable to add configs profiles for package {this.Package.Name}! Aborting!");
return failed3;
}
if (_configService.Value.AddConfigsProfiles(configProfilesResourcesInfo.ConfigProfiles) is { IsFailed: true} failed4)
{
return failed4.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load config profiles.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, configProfilesResourcesInfo));
}
ConfigsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
@@ -463,22 +439,24 @@ public partial class PackageService : IPackageService
}
}
public void Reset()
public FluentResults.Result 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;
return FluentResults.Result.Fail(new Error($"Package Service: cannot Dispose of service as ContentPackage and info is not set!")
.WithMetadata(MetadataType.ExceptionDetails, nameof(Reset))
.WithMetadata(MetadataType.ExceptionObject, this));
}
if (this.ModConfigInfo is null)
{
_loggerService.LogError($"Package Service: cannot Dispose of service as ModConfigInfo is not loaded!");
return;
return FluentResults.Result.Fail(new Error($"Package Service: cannot Dispose of service as ModConfigInfo is not set!")
.WithMetadata(MetadataType.ExceptionDetails, nameof(Reset))
.WithMetadata(MetadataType.ExceptionObject, this));
}
Interlocked.MemoryBarrier(); //ensure cache states
@@ -491,7 +469,7 @@ public partial class PackageService : IPackageService
_operationsUsageLock.EnterWriteLock();
if (timeoutLimit < DateTime.Now)
{
_loggerService.LogError($"Package Service: Dispose() time out reached while waiting for other operations. Continuing.");
_loggerService.LogError($"Package Service: Dispose() grace time-out reached while waiting for other operations. Continuing.");
break;
}
}
@@ -520,6 +498,7 @@ public partial class PackageService : IPackageService
_localizationService.Value.Remove(this.Localizations);
LocalizationsLoaded = false;
}
return FluentResults.Result.Ok();
}
finally
{
@@ -531,96 +510,176 @@ public partial class PackageService : IPackageService
#region INTERNAL
private void SanitationChecksCore(object o, string resTypeInfoName, string callerName)
/// <summary>
/// [Thread Unsafe] Performs sanitation and null checks on resources and returns the results.
/// NOTE: Requires that resource locks be set by the caller.
/// </summary>
/// <param name="resourcesInfos"></param>
/// <returns></returns>
private FluentResults.Result CheckResourceSanitation(
OneOf.OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo> resourcesInfos)
{
if (o is null)
// execute checks based on known types
return resourcesInfos.Match<FluentResults.Result>(
ass => ChecksDispatcher(ass, nameof(ass.Assemblies), nameof(LoadPlugins),
ass.Assemblies, this.Assemblies),
loc => ChecksDispatcher(loc, nameof(loc.Localizations), nameof(LoadLocalizations),
loc.Localizations, this.Localizations),
cfg => ChecksDispatcher(cfg, nameof(cfg.Configs), nameof(LoadConfig),
cfg.Configs, this.Configs),
cfp => ChecksDispatcher(cfp, nameof(cfp.ConfigProfiles), nameof(LoadConfig),
cfp.ConfigProfiles, this.ConfigProfiles),
lua => ChecksDispatcher(lua, nameof(lua.LuaScripts), nameof(AddLuaScripts),
lua.LuaScripts, this.LuaScripts));
/*
* Helper functions
*/
FluentResults.Result ChecksDispatcher<T>(object obj, string resName, string callerName,
ImmutableArray<T> resList, ImmutableArray<T> compareList)
where T : class, IPackageInfo, IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo
{
_loggerService.LogError($"Package Service: {resTypeInfoName} resources list is null!");
throw new NullReferenceException($"Package Service: {resTypeInfoName} resources list is null!");
string errMsg = $"{callerName}: Failed to load {resName}.";
if (DisposeCheck(obj) is { IsFailed: true } failed)
return failed;
if (SanitationChecksCore(obj, resName, callerName) is { IsFailed: true } failed1)
return failed1.WithError(new Error(errMsg));
if (SanitationChecksEnumerable(resList, resName, callerName) is { IsFailed: true } failed2)
return failed2.WithError(new Error(errMsg));
if (DebugCheck(resList, compareList, resName) is {IsFailed: true} failed3)
return failed3.WithError(new Error(errMsg));
return FluentResults.Result.Ok();
}
FluentResults.Result DisposeCheck(object obj)
{
if (IsDisposed)
{
return FluentResults.Result.Fail(new Error($"{nameof(PackageService)}: Tried to load resources when disposed.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, obj));
}
return FluentResults.Result.Ok();
}
FluentResults.Result DebugCheck<T>(ImmutableArray<T> resList, ImmutableArray<T> compareList, string resName)
where T : class, IPackageInfo
{
#if DEBUG
Stack<Error> errors = new();
resList.ForEach(res =>
{
if (!compareList.Contains(res))
{
errors.Push(new Error($"Failed to load {resName} for: {this.Package.Name}")
.WithMetadata(MetadataType.ExceptionDetails, $"Tries to load {resName} resource {res.InternalName} but it is not from this package!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, res));
}
});
if (errors.Count > 0)
{
return FluentResults.Result.Fail(errors).WithError(
new Error($"{nameof(LoadPlugins)}: errors in {resName} resources.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, this.Package));
}
#endif
return FluentResults.Result.Ok();
}
}
private FluentResults.Result SanitationChecksCore(object obj, string resTypeInfoName, string callerName)
{
Error e = null;
if (obj is null)
{
e = new Error($"{nameof(SanitationChecksCore)}: null checks failed!")
.WithMetadata(MetadataType.ExceptionDetails, "Object is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, new List<string>() { resTypeInfoName, callerName });
}
if (this.Package is null)
{
_loggerService.LogError($"Package Service: package not set at {callerName}()!");
throw new NullReferenceException($"Package Service: package not set at {callerName}()!");
e = (e ?? new Error($"{nameof(SanitationChecksCore)}: null checks failed!"))
.WithMetadata(MetadataType.ExceptionDetails, "The Package is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, new List<string>() { resTypeInfoName, callerName });
}
return e is null ? FluentResults.Result.Ok() : FluentResults.Result.Fail(e);
}
private void SanitationChecksEnumerable<T>(ImmutableArray<T> resourceInfos, string resTypeInfoName, string callerName) where T : IResourceInfo, IResourceCultureInfo, IPackageInfo, IPackageDependenciesInfo
private FluentResults.Result 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;
return FluentResults.Result.Ok();
Stack<Error> errors = new();
// 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}.");
{
errors.Push(new Error($"Error for resource: {resTypeInfoName}. OwnerPackage is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
continue;
}
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}.");
errors.Push(new Error($"Error for resource: {resTypeInfoName}. $\"OwnerPackage {{resourceInfo.OwnerPackage?.Name}} is not the same as this package: {{this.Package}}")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
continue;
}
// 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 =>
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var pdi in resourceInfo.Dependencies)
{
// for clarification: assemblies passed to the function should always be loaded.
// optional assemblies should be filtered out before the list is sent.
// for clarification: all resources passed to the function should always be loaded.
// unneeded optional resources 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)}");
errors.Push(new Error($"Dependency missing for resource: {resourceInfo.OwnerPackage.Name}")
.WithMetadata(MetadataType.ExceptionDetails, $"Missing dependency: {pdi.DependencyPackage?.Name ?? (pdi.FallbackPackageName.IsNullOrWhiteSpace() ? pdi.SteamWorkshopId.ToString() : pdi.FallbackPackageName)}")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
}
});
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.");
errors.Push(new Error($"The resource {resourceInfo.OwnerPackage?.Name} does not support the current platform!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
}
// 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.");
errors.Push(new Error($"The resource {resourceInfo.OwnerPackage?.Name} does not support the current culture/region!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
}
}
}
[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);
}
return errors.Count > 0 ? FluentResults.Result.Fail(errors) : FluentResults.Result.Ok();
}
#endregion
@@ -0,0 +1,52 @@
using System.Collections.Immutable;
using Barotrauma.LuaCs.Data;
using FluentResults;
using Microsoft.CodeAnalysis;
namespace Barotrauma.LuaCs.Services;
public class PluginManagementService : IPluginManagementService
{
public void Dispose()
{
throw new System.NotImplementedException();
}
public FluentResults.Result Reset()
{
throw new System.NotImplementedException();
}
public bool IsAssemblyLoadedGlobal(string friendlyName)
{
throw new System.NotImplementedException();
}
public Result<ImmutableArray<T>> GetTypes<T>(ContentPackage package = null, string namespacePrefix = null, bool includeInterfaces = false,
bool includeAbstractTypes = false, bool includeDefaultContext = true, bool includeExplicitAssembliesOnly = false)
{
throw new System.NotImplementedException();
}
public ImmutableArray<MetadataReference> GetStandardMetadataReferences()
{
throw new System.NotImplementedException();
}
public ImmutableArray<MetadataReference> GetPluginMetadataReferences()
{
throw new System.NotImplementedException();
}
public Result<ImmutableArray<IAssemblyResourceInfo>> GetCachedAssembliesForPackage(ContentPackage package)
{
throw new System.NotImplementedException();
}
public Result<ImmutableArray<IAssemblyResourceInfo>> LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resource)
{
throw new System.NotImplementedException();
}
}
@@ -1,6 +1,6 @@
namespace Barotrauma.LuaCs.Services;
public interface IEventService
public class PluginService
{
}
@@ -1,16 +1,17 @@
using System.Collections.Generic;
using System.Xml.Linq;
using Barotrauma.LuaCs.Data;
using FluentResults;
namespace Barotrauma.LuaCs.Services.Processing;
#region TypeDef
// ReSharper disable once TypeParameterCanBeVariant
public interface IConverterService<TSrc, TOut> : IService
public interface IConverterService<TSrc, TOut> : IReusableService
{
bool TryParseResource(TSrc src, out TOut resources);
bool TryParseResources(IEnumerable<TSrc> sources, out List<TOut> resources);
Result<TOut> TryParseResource(TSrc src);
Result<TOut> TryParseResources(IEnumerable<TSrc> sources);
}
public interface IXmlResourceConverterService<TOut> : IConverterService<XElement, TOut> { }
@@ -0,0 +1,9 @@
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services.Processing;
public interface IModConfigParserService : IReusableService
{
FluentResults.Result<IModConfigInfo> BuildConfigForPackage(ContentPackage package);
FluentResults.Result<IModConfigInfo> BuildConfigFromManifest(string manifestPath);
}
@@ -1,4 +1,6 @@
namespace Barotrauma.LuaCs.Services.Safe;
using Barotrauma.LuaCs.Configuration;
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaConfigService : ILuaService
{
@@ -1,6 +1,30 @@
namespace Barotrauma.LuaCs.Services.Safe;
using System;
using System.Collections.Generic;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs.Services.Compatibility;
public interface ILuaEventService : ILuaService
namespace Barotrauma.LuaCs.Services.Safe;
public interface ILuaSafeEventService : ILuaService, ILuaCsHook
{
void Subscribe(string interfaceName, string identifier, IDictionary<string, LuaCsFunc> callbacks);
/// <summary>
/// Removes a subscriber from an event that subscribed under the given identifier.
/// </summary>
/// <param name="eventName"></param>
/// <param name="identifier"></param>
void Remove(string eventName, string identifier);
/// <summary>
/// Send an event to all subscribers to an interface.
/// </summary>
/// <param name="interfaceName">Name of the interface (must be registered with Lua).</param>
/// <param name="runner">Execution runner, the subscriber is provided as the first argument in the lua runner.</param>
/// <returns></returns>
void PublishLuaEvent(string interfaceName, LuaCsFunc runner);
}
public interface ILuaEventService : ILuaSafeEventService
{
public FluentResults.Result RegisterSafeEvent<T>() where T : IEvent<T>;
public FluentResults.Result UnregisterSafeEvent<T>() where T : IEvent<T>;
}
@@ -24,7 +24,7 @@ public class ServicesProvider : IServicesProvider
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()
public void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
{
if (lifetimeInstance is null)
{
@@ -50,7 +50,6 @@ public class ServicesProvider : IServicesProvider
{
_serviceLock.EnterReadLock();
ServiceContainer.Register<TSvcInterface, TService>(lifetimeInstance);
ServiceContainer.Compile<TService>();
OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(TService));
}
finally
@@ -60,7 +59,7 @@ public class ServicesProvider : IServicesProvider
}
public void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime,
ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new()
ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
{
if (name.IsNullOrWhiteSpace())
{
@@ -91,7 +90,6 @@ public class ServicesProvider : IServicesProvider
{
_serviceLock.EnterReadLock();
ServiceContainer.Register<TSvcInterface, TService>(name, lifetimeInstance);
ServiceContainer.Compile<TService>();
OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(TService));
}
finally
@@ -128,7 +126,7 @@ public class ServicesProvider : IServicesProvider
}
}
public bool TryGetService<TSvcInterface>(out IService service) where TSvcInterface : class, IService
public bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService
{
try
{
@@ -147,7 +145,7 @@ public class ServicesProvider : IServicesProvider
}
}
public bool TryGetService<TSvcInterface>(string name, out IService service) where TSvcInterface : class, IService
public bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService
{
try
{
@@ -0,0 +1,46 @@
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 : IReusableService
{
/// <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 fully-qualified assembly name.</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(string assemblyName, in Guid[] excludedContexts);
/// <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(AssemblyName assemblyName, in Guid[] excludedContexts);
/// <summary>
/// Gets the assembly <see cref="MetadataReference"/> collection for the BCL and base game assemblies.
/// </summary>
/// <returns><see cref="MetadataReference"/> collection, if any are found. Returns an empty collection otherwise.</returns>
ImmutableArray<MetadataReference> GetDefaultMetadataReferences();
/// <summary>
/// Gets the assembly <see cref="MetadataReference"/> collection for all add-in assemblies loaded.
/// </summary>
/// <returns><see cref="MetadataReference"/> collection, if any are found. Returns an empty collection otherwise.</returns>
ImmutableArray<MetadataReference> GetAddInContextsMetadataReferences();
/// <summary>
///
/// </summary>
ImmutableArray<IAssemblyLoaderService> AssemblyLoaderServices { get; }
}
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.LuaCs.Configuration;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Networking;
using Barotrauma.LuaCs.Services.Safe;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs.Services;
public partial interface IConfigService : IReusableService, ILuaConfigService
{
/*
* Resource Files.
*/
FluentResults.Result AddConfigs(ImmutableArray<IConfigResourceInfo> configResources);
FluentResults.Result AddConfigsProfiles(ImmutableArray<IConfigProfileResourceInfo> configProfileResources);
FluentResults.Result RemoveConfigs(ImmutableArray<IConfigResourceInfo> configResources);
FluentResults.Result RemoveConfigsProfiles(ImmutableArray<IConfigProfileResourceInfo> configProfilesResources);
/*
* From resources
*/
FluentResults.Result AddConfigs(ImmutableArray<IConfigInfo> configs);
FluentResults.Result AddConfigsProfiles(ImmutableArray<IConfigProfileInfo> configProfiles);
FluentResults.Result RemoveConfigs(ImmutableArray<IConfigInfo> configs);
FluentResults.Result RemoveConfigsProfiles(ImmutableArray<IConfigProfileInfo> configProfiles);
/*
* Immediate mode
*/
FluentResults.Result<IConfigEntry<T>> AddConfigEntry<T>(ContentPackage package, string name,
T defaultValue,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigList> AddConfigList(ContentPackage package, string name,
int defaultIndex, IReadOnlyList<string> values,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<IConfigList, int, bool> valueChangePredicate = null,
Action<IConfigList, int> onValueChanged = null);
FluentResults.Result<IConfigRangeEntry<T>> AddConfigRangeEntry<T>(ContentPackage package, string name,
T defaultValue, T minValue, T maxValue,
Func<IConfigRangeEntry<T>, int> getStepCount,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigEntry<T>> AddConfigEntry<T>(string packageName, string name,
T defaultValue,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IConfigList> AddConfigList(string packageName, string name,
int defaultIndex, IReadOnlyList<string> values,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<IConfigList, int, bool> valueChangePredicate = null,
Action<IConfigList, int> onValueChanged = null);
FluentResults.Result<IConfigRangeEntry<T>> AddConfigRangeEntry<T>(string packageName, string name,
T defaultValue, T minValue, T maxValue,
Func<IConfigRangeEntry<T>, int> getStepCount,
NetSync syncMode = NetSync.None,
ClientPermissions permissions = ClientPermissions.None,
Func<T, bool> valueChangePredicate = null,
Action<IConfigEntry<T>> onValueChanged = null) where T : IConvertible, IEquatable<T>;
FluentResults.Result<IReadOnlyDictionary<string, IConfigBase>> GetConfigsForPackage(ContentPackage package);
FluentResults.Result<IReadOnlyDictionary<string, IConfigBase>> GetConfigsForPackage(string packageName);
IReadOnlyDictionary<(ContentPackage, string), IConfigBase> GetAllConfigs();
FluentResults.Result<IConfigBase> GetConfig(ContentPackage package, string name);
FluentResults.Result<IConfigBase> GetConfig(string packageName, string name);
FluentResults.Result<T> GetConfig<T>(ContentPackage package, string name) where T : IConfigBase;
FluentResults.Result<T> GetConfig<T>(string packageName, string name) where T : IConfigBase;
}
@@ -0,0 +1,45 @@
using System;
using System.Reflection;
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs.Services.Compatibility;
using Barotrauma.LuaCs.Services.Safe;
namespace Barotrauma.LuaCs.Services;
public interface IEventService : IReusableService, ILuaEventService
{
FluentResults.Result SetLegacyLuaRunnerFactory<T>(Func<LuaCsFunc, T> runnerFactory) where T : IEvent<T>;
void RemoveLegacyLuaRunnerFactory<T>() where T : IEvent<T>;
void SetAliasToEvent<T>(string alias) where T : IEvent<T>;
void RemoveEventAlias(string alias);
void RemoveAllEventAliases<T>() where T : IEvent<T>;
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
FluentResults.Result Subscribe<T>(T subscriber) where T : IEvent<T>;
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <typeparam name="T"></typeparam>
void Unsubscribe<T>(T subscriber) where T : 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 : 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 : IEvent<T>;
}
@@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface ILocalizationService : IReusableService
{
IReadOnlyCollection<CultureInfo> GetLoadedLocales();
void Remove(ImmutableArray<ILocalizationResourceInfo> localizations);
FluentResults.Result SetCurrentCulture(CultureInfo culture);
FluentResults.Result SetCurrentCulture(string cultureName);
FluentResults.Result LoadLocalizations(ImmutableArray<ILocalizationResourceInfo> localizationResources);
/// <summary>
/// Tries to get a localized string without a fallback. Returns success/failure and associated data.
/// </summary>
/// <param name="key">Neutral localization key.</param>
/// <returns></returns>
FluentResults.Result<string> GetLocalizedString(string key);
FluentResults.Result<string> GetLocalizedString(string key, CultureInfo targetCulture);
string GetLocalizedString(string key, string fallback);
string GetLocalizedString(string key, string fallback, CultureInfo targetCulture);
FluentResults.Result<string> GetLocalizedStringForPackage(ContentPackage package, string key);
FluentResults.Result<string> GetLocalizedStringForPackage(ContentPackage package, string key, CultureInfo targetCulture);
string GetLocalizedStringForPackage(ContentPackage package, string key, string fallback);
string GetLocalizedStringForPackage(ContentPackage package, string key, string fallback, CultureInfo targetCulture);
FluentResults.Result RegisterLocalizationResolver(CultureInfo targetCulture, Func<string, CultureInfo, string> factoryResolver);
bool IsCurrentCultureSupported(IResourceCultureInfo culturesInfo);
}
@@ -1,5 +1,6 @@
using System;
using Barotrauma.Networking;
using FluentResults;
using Microsoft.Xna.Framework;
namespace Barotrauma.LuaCs.Services;
@@ -7,14 +8,15 @@ namespace Barotrauma.LuaCs.Services;
/// <summary>
/// Provides console and debug logging services
/// </summary>
public interface ILoggerService : IService
public interface ILoggerService : IReusableService
{
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);
@@ -8,7 +8,7 @@ using MoonSharp.Interpreter.Interop;
namespace Barotrauma.LuaCs.Services;
public interface ILuaScriptService : IService
public interface ILuaScriptService : IReusableService
{
#region Script_File_Collector
@@ -17,7 +17,8 @@ public interface ILuaScriptService : IService
/// </summary>
/// <param name="luaResource"></param>
/// <returns></returns>
bool TryAddScriptFiles(ImmutableArray<ILuaResourceInfo> luaResource);
FluentResults.Result AddScriptFiles(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!
@@ -31,19 +32,20 @@ public interface ILuaScriptService : IService
/// <param name="pauseExecutionOnScriptError"></param>
/// <param name="verboseLogging"></param>
/// <returns></returns>
bool TryExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false);
FluentResults.Result ExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false);
ImmutableArray<ILuaResourceInfo> GetScriptResources();
#endregion
}
public interface ILuaScriptManagementService : IService
public interface ILuaScriptManagementService : IReusableService
{
#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);
FluentResults.Result ExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false);
FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaResourceInfo> scripts, bool pauseExecutionOnError = false, bool verboseLogging = false);
FluentResults.Result ExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false);
#endregion
@@ -0,0 +1,25 @@
using System;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Networking;
using Barotrauma.LuaCs.Services.Compatibility;
using Barotrauma.Networking;
namespace Barotrauma.LuaCs.Services;
internal delegate void NetMessageReceived(IReadMessage netMessage);
internal interface INetworkingService : IReusableService, ILuaCsNetworking
{
bool IsActive { get; }
bool IsSynchronized { get; }
public INetWriteMessage Start(Guid netId);
public void Receive(Guid netId, NetMessageReceived action);
#if SERVER
public void Send(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#elif CLIENT
public void Send(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
#endif
public void RegisterNetVar(INetVar netVar);
public void SendNetVar(INetVar netVar);
}
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface IPackageManagementService : IReusableService
{
/// <summary>
/// Adds packages to the queue of loadable packages without initializing them.
/// </summary>
/// <param name="packages"></param>
void QueuePackages(ImmutableArray<LoadablePackage> packages);
/// <summary>
/// Generates the ModConfigInfo for all queued packages and adds them to the store.
/// </summary>
/// <param name="loadParallel">Use multithreaded loading.</param>
/// <param name="reportFailOnDuplicates">Whether duplicate packages should be reported as errors.</param>
/// <returns>Failure/Success records for each package.</returns>
FluentResults.Result ParseQueuedPackages(bool loadParallel = true, bool reportFailOnDuplicates = false);
/// <summary>
/// Loads only the localizations, configs, and config profiles for stored packages.
/// </summary>
/// <param name="loadParallel"></param>
/// <returns></returns>
FluentResults.Result LoadPackageConfigsResourcesGroup(bool loadParallel = true);
/// <summary>
/// Loads all resources for stored packages.
/// </summary>
/// <param name="loadParallel">Use multithreaded loading.</param>
/// <param name="safeResourcesOnly">Only load safe scripting resources, such as Lua. C# plugins disabled.</param>
/// <returns></returns>
FluentResults.Result LoadAllPackageResources(bool loadParallel = true, bool safeResourcesOnly = true);
FluentResults.Result UnloadPackages();
bool IsPackageLoaded(ContentPackage package);
bool CheckDependencyLoaded(IPackageDependencyInfo info);
bool CheckDependenciesLoaded([NotNull]IEnumerable<IPackageDependencyInfo> infos, out ImmutableArray<IPackageDependencyInfo> missingPackages);
bool CheckEnvironmentSupported(IPlatformInfo platform);
/// <summary>
/// Tries to get the package dependency record to refer to that specific package if it exists, optionally create it.
/// </summary>
/// <param name="package">ContentPackage reference</param>
/// <param name="addIfMissing">Register a new IPackageDependencyInfo reference.</param>
/// <returns></returns>
FluentResults.Result<IPackageDependencyInfo> GetPackageDependencyInfoRecord(ContentPackage package,
bool addIfMissing = false);
/// <summary>
/// Tries to get the package dependency record to refer to that specific package if it exists, optionally create it.
/// </summary>
/// <param name="steamWorkshopId">The Steam Workshop ID, if available, if not enter zero ('0').</param>
/// <param name="packageName">The name of the package.</param>
/// <param name="folderPath">The folder path, as formatted in [ContentPackage.Path].</param>
/// <param name="addIfMissing">Register a new IPackageDependencyInfo reference.</param>
/// <returns></returns>
FluentResults.Result<IPackageDependencyInfo> GetPackageDependencyInfoRecord(ulong steamWorkshopId,
string packageName, string folderPath = null, bool addIfMissing = false);
/// <summary>
/// Tries to get the package dependency record to refer to that specific package if it exists.
/// Note: This overload does not allow the registration of a new dependency.
/// </summary>
/// <param name="folderPath">The folder path, as formatted in [ContentPackage.Path].</param>
/// <returns></returns>
FluentResults.Result<IPackageDependencyInfo> GetPackageDependencyInfoRecord(string folderPath);
IPackageDependencyInfo CreateOrphanPackageDependencyInfoRecord(string packageName,
string packagePath, ulong steamWorkshopId);
}
public readonly record struct LoadablePackage
{
public ContentPackage Package { get; }
public bool IsEnabled { get; }
public LoadablePackage(ContentPackage package, bool isEnabled)
{
Package = package;
IsEnabled = isEnabled;
}
public static ImmutableArray<LoadablePackage> FromEnumerable(IEnumerable<ContentPackage> packages, bool isEnabled)
{
var builder = ImmutableArray.CreateBuilder<LoadablePackage>();
packages.ForEach(p => builder.Add(new LoadablePackage(p, isEnabled)));
return builder.ToImmutable();
}
}
@@ -3,21 +3,23 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.LuaCs.Data;
using FluentResults;
namespace Barotrauma.LuaCs.Services;
public interface IPackageService : IService,
public interface IPackageService : IReusableService,
// 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; }
bool IsEnabledInModList { 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);
/// <returns>Whether the package was parsed without errors.</returns>
FluentResults.Result LoadResourcesInfo([NotNull]LoadablePackage 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.
@@ -25,12 +27,12 @@ public interface IPackageService : IService,
/// <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);
FluentResults.Result LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false);
FluentResults.Result LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo);
FluentResults.Result AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo);
#if CLIENT
void LoadStyles([NotNull]IStylesResourcesInfo stylesInfo);
FluentResults.Result LoadStyles([NotNull]IStylesResourcesInfo stylesInfo);
#endif
void LoadConfig([NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo);
FluentResults.Result LoadConfig([NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo);
}
@@ -0,0 +1,53 @@
using System;
using System.Collections.Immutable;
using System.Reflection;
using Barotrauma.LuaCs.Data;
using Microsoft.CodeAnalysis;
namespace Barotrauma.LuaCs.Services;
public interface IPluginManagementService : IReusableService
{
/// <summary>
/// Checks if an assembly with either the fully-qualified name globally or a 'friendly name' within loaded plugins
/// with the given name is loaded.
/// </summary>
/// <param name="friendlyName"></param>
/// <returns></returns>
bool IsAssemblyLoadedGlobal(string friendlyName);
// TODO: Documentation.
FluentResults.Result<ImmutableArray<T>> GetTypes<T>(
ContentPackage package = null,
string namespacePrefix = null,
bool includeInterfaces = false,
bool includeAbstractTypes = false,
bool includeDefaultContext = true,
bool includeExplicitAssembliesOnly = false);
/// <summary>
/// Gets the assembly <c>MetadataReference</c> collection for the BCL and base game assemblies.
/// </summary>
/// <returns></returns>
ImmutableArray<MetadataReference> GetStandardMetadataReferences();
/// <summary>
///
/// </summary>
/// <returns></returns>
ImmutableArray<MetadataReference> GetPluginMetadataReferences();
/// <summary>
///
/// </summary>
/// <param name="package"></param>
/// <returns></returns>
FluentResults.Result<ImmutableArray<IAssemblyResourceInfo>> GetCachedAssembliesForPackage(ContentPackage package);
/// <summary>
///
/// </summary>
/// <param name="resource"></param>
/// <returns>Success/Failure and list of failed resources, if any.</returns>
FluentResults.Result<ImmutableArray<IAssemblyResourceInfo>> LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resource);
}
@@ -6,7 +6,7 @@ using Barotrauma.LuaCs.Data;
namespace Barotrauma.LuaCs.Services;
public interface IPluginService : IService
public interface IPluginService : IReusableService
{
bool IsAssemblyLoaded(string friendlyName);
/// <summary>
@@ -17,21 +17,21 @@ public interface IPluginService : IService
/// <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;
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>
bool AdvancePluginStates(PluginRunState newState);
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>
bool DisposePlugins();
FluentResults.Result DisposePlugins();
/// <summary>
/// Gets the current plugin execution state.
@@ -0,0 +1,29 @@
using System;
namespace Barotrauma.LuaCs.Services;
/// <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>
public interface IService : IDisposable
{
bool IsDisposed { get; }
public void CheckDisposed()
{
if (IsDisposed)
throw new ObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!");
}
}
@@ -19,7 +19,7 @@ public interface IServicesProvider
/// <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();
void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface;
/// <summary>
/// Registers a type as a service for a given interface that can be requested by name.
@@ -29,7 +29,7 @@ public interface IServicesProvider
/// <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();
void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface;
/// <summary>
/// Called whenever a new service type for a given interface is implemented.
@@ -61,7 +61,7 @@ public interface IServicesProvider
/// <param name="lifetime"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(out IService service) where TSvcInterface : class, IService;
bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IReusableService;
/// <summary>
/// Tries to get a service for the given name and interface, returns success/failure.
@@ -71,14 +71,14 @@ public interface IServicesProvider
/// <param name="lifetime"></param>
/// <typeparam name="TSvcInterface"></typeparam>
/// <returns></returns>
bool TryGetService<TSvcInterface>(string name, out IService service) where TSvcInterface : class, IService;
bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IReusableService;
/// <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;
event System.Action<Type, IReusableService> OnServiceInstanced;
#endregion
@@ -89,7 +89,7 @@ public interface IServicesProvider
/// </summary>
/// <typeparam name="TSvc"></typeparam>
/// <returns></returns>
ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService;
ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IReusableService;
#endregion
@@ -0,0 +1,42 @@
using System.Collections.Immutable;
using System.Xml.Linq;
namespace Barotrauma.LuaCs.Services;
public interface IStorageService : IReusableService
{
#region LocalGameData
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<bool> FileExistsInLocalData(ContentPackage package, string localFilePath);
#endregion
#region ContentPackageData
FluentResults.Result<XDocument> LoadPackageXml(ContentPackage package, string localFilePath, out XDocument document);
FluentResults.Result<byte[]> LoadPackageBinary(ContentPackage package, string localFilePath, out byte[] bytes);
FluentResults.Result<string> LoadPackageText(ContentPackage package, string localFilePath, out string text);
FluentResults.Result<ImmutableArray<XDocument>> LoadPackageXmlFiles(ContentPackage package, ImmutableArray<string> localFilePath);
FluentResults.Result<ImmutableArray<byte[]>> TryLoadPackageBinaryFiles(ContentPackage package, ImmutableArray<string> localFilePath);
FluentResults.Result<ImmutableArray<string>> TryLoadPackageTextFiles(ContentPackage package, ImmutableArray<string> localFilePath);
FluentResults.Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively);
FluentResults.Result<bool> FileExistsInPackage(ContentPackage package, string localFilePath);
#endregion
#region AbsolutePaths
FluentResults.Result<XDocument> TryLoadXml(string filePatht);
FluentResults.Result TrySaveXml(string filePath, in XDocument document);
FluentResults.Result<byte[]> TryLoadBinary(string filePath);
FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes);
FluentResults.Result<string> TryLoadText(string filePath);
FluentResults.Result TrySaveText(string filePath, string text);
FluentResults.Result<bool> FileExists(string filePath);
#endregion
}