From 08756eeb5c9d3ef3cf065099b2a577c994fec91a Mon Sep 17 00:00:00 2001 From: Toastie Date: Sun, 31 Mar 2024 23:51:18 +1300 Subject: [PATCH] Added marmalade subsystem to Ellie --- .../Marmalade/Adapters/BehaviorAdapter.cs | 76 ++ .../Adapters/ContextAdapterFactory.cs | 7 + .../Marmalade/Adapters/DmContextAdapter.cs | 49 + .../Marmalade/Adapters/FilterAdapter.cs | 31 + .../Marmalade/Adapters/GuildContextAdapter.cs | 53 + .../Marmalade/Adapters/ParamParserAdapter.cs | 32 + .../Common/Marmalade/CommandContextType.cs | 27 + .../Config/IMarmaladeConfigService.cs | 8 + .../Marmalade/Config/MarmaladeConfig.cs | 20 + .../Config/MarmaladeConfigService.cs | 45 + .../Marmalade/IMarmaladeLoaderService.cs | 23 + .../Marmalade/MarmaladeAssemblyLoadContext.cs | 36 + .../Marmalade/MarmaladeLoaderService.cs | 917 ++++++++++++++++++ .../Marmalade/MarmaladeServiceProvider.cs | 24 + .../Marmalade/Models/CanaryCommandData.cs | 46 + .../Common/Marmalade/Models/CanaryData.cs | 11 + .../Common/Marmalade/Models/ParamData.cs | 10 + .../Marmalade/Models/ResolvedMarmalade.cs | 14 + 18 files changed, 1429 insertions(+) create mode 100644 src/EllieBot/Common/Marmalade/Adapters/BehaviorAdapter.cs create mode 100644 src/EllieBot/Common/Marmalade/Adapters/ContextAdapterFactory.cs create mode 100644 src/EllieBot/Common/Marmalade/Adapters/DmContextAdapter.cs create mode 100644 src/EllieBot/Common/Marmalade/Adapters/FilterAdapter.cs create mode 100644 src/EllieBot/Common/Marmalade/Adapters/GuildContextAdapter.cs create mode 100644 src/EllieBot/Common/Marmalade/Adapters/ParamParserAdapter.cs create mode 100644 src/EllieBot/Common/Marmalade/CommandContextType.cs create mode 100644 src/EllieBot/Common/Marmalade/Config/IMarmaladeConfigService.cs create mode 100644 src/EllieBot/Common/Marmalade/Config/MarmaladeConfig.cs create mode 100644 src/EllieBot/Common/Marmalade/Config/MarmaladeConfigService.cs create mode 100644 src/EllieBot/Common/Marmalade/IMarmaladeLoaderService.cs create mode 100644 src/EllieBot/Common/Marmalade/MarmaladeAssemblyLoadContext.cs create mode 100644 src/EllieBot/Common/Marmalade/MarmaladeLoaderService.cs create mode 100644 src/EllieBot/Common/Marmalade/MarmaladeServiceProvider.cs create mode 100644 src/EllieBot/Common/Marmalade/Models/CanaryCommandData.cs create mode 100644 src/EllieBot/Common/Marmalade/Models/CanaryData.cs create mode 100644 src/EllieBot/Common/Marmalade/Models/ParamData.cs create mode 100644 src/EllieBot/Common/Marmalade/Models/ResolvedMarmalade.cs diff --git a/src/EllieBot/Common/Marmalade/Adapters/BehaviorAdapter.cs b/src/EllieBot/Common/Marmalade/Adapters/BehaviorAdapter.cs new file mode 100644 index 0000000..935d27a --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Adapters/BehaviorAdapter.cs @@ -0,0 +1,76 @@ +#nullable disable + +[DontAddToIocContainer] +public sealed class BehaviorAdapter : ICustomBehavior +{ + private readonly WeakReference _canaryWr; + private readonly IMarmaladeStrings _strings; + private readonly IServiceProvider _services; + private readonly string _name; + + // unused + public int Priority + => 0; + + public BehaviorAdapter(WeakReference canaryWr, IMarmaladeStrings strings, IServiceProvider services) + { + _canaryWr = canaryWr; + _strings = strings; + _services = services; + + _name = canaryWr.TryGetTarget(out var canary) + ? $"canary/{canary.GetType().Name}" + : "unknown"; + } + + public async Task ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command) + { + if (!_canaryWr.TryGetTarget(out var canary)) + return false; + + return await canary.ExecPreCommandAsync(ContextAdapterFactory.CreateNew(context, _strings, _services), + moduleName, + command.Name); + } + + public async Task ExecOnMessageAsync(IGuild? guild, IUserMessage msg) + { + if (!_canaryWr.TryGetTarget(out var canary)) + return false; + + return await canary.ExecOnMessageAsync(guild, msg); + } + + public async Task TransformInput( + IGuild guild, + IMessageChannel channel, + IUser user, + string input) + { + if (!_canaryWr.TryGetTarget(out var canary)) + return null; + + return await canary.ExecInputTransformAsync(guild, channel, user, input); + } + + public async Task ExecOnNoCommandAsync(IGuild? guild, IUserMessage msg) + { + if (!_canaryWr.TryGetTarget(out var canary)) + return; + + await canary.ExecOnNoCommandAsync(guild, msg); + } + + public async ValueTask ExecPostCommandAsync(ICommandContext context, string moduleName, string commandName) + { + if (!_canaryWr.TryGetTarget(out var canary)) + return; + + await canary.ExecPostCommandAsync(ContextAdapterFactory.CreateNew(context, _strings, _services), + moduleName, + commandName); + } + + public override string ToString() + => _name; +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Adapters/ContextAdapterFactory.cs b/src/EllieBot/Common/Marmalade/Adapters/ContextAdapterFactory.cs new file mode 100644 index 0000000..212e8ee --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Adapters/ContextAdapterFactory.cs @@ -0,0 +1,7 @@ +internal class ContextFactory +{ + public static AnyContext CreateNew(ICommandContext context, IMarmaladeStrings strings, IServiceProvider services) + => context.Guild is null + ? new DmContextAdapter(context, strings, services) + : new GuildContextAdapter(context, strings, services); +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Adapters/DmContextAdapter.cs b/src/EllieBot/Common/Marmalade/Adapters/DmContextAdapter.cs new file mode 100644 index 0000000..d0aaabc --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Adapters/DmContextAdapter.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.DependencyInjection; + +public sealed class DmContextAdapter : DmContext +{ + public override IMarmaladeStrings Strings { get; } + public override IDMChannel Channel { get; } + public override IUserMessage Message { get; } + public override ISelfUser Bot { get; } + public override IUser User + => Message.Author; + + private readonly IServiceProvider _services; + private readonly Lazy _ebs; + private readonly Lazy _botStrings; + private readonly Lazy _localization; + + public DmContextAdapter(ICommandContext ctx, IMarmaladeStrings strings, IServiceProvider services) + { + if (ctx is not { Channel: IDMChannel ch}) + { + throw new ArgumentException("Can't use non-dm context to create DmContextAdapter", nameof(ctx)); + } + + Strings = strings; + + _services = services; + + Channel = ch; + Message = ctx.Message; + Bot = ctx.Client.CurrentUser; + + _ebs = new(_services.GetRequiredService()); + _botStrings = new(_services.GetRequiredService); + _localization = new(_services.GetRequiredService()); + } + + public override IEmbedBuilder Embed() + => _ebs.Value.Create(); + + public override string GetText(string key, object[]? args = null) + { + var cultureInfo = _localization.Value.GetCultureInfo(default(ulong?)); + var output = Strings.GetText(key, cultureInfo, args ?? Array.Empty()); + if (!string.IsNullOrEmpty(output)) + return output; + + return _botStrings.Value.GetText(key, cultureInfo, args); + } +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Adapters/FilterAdapter.cs b/src/EllieBot/Common/Marmalade/Adapters/FilterAdapter.cs new file mode 100644 index 0000000..1934e38 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Adapters/FilterAdapter.cs @@ -0,0 +1,31 @@ +namespace Ellie.Marmalade.Adapters; + +public class FilterAdapter : PreconditionAttribute +{ + private readonly FilterAttribute _filterAttribute; + private readonly IMarmaladeStrings _strings; + + public FilterAdapter(FilterAttribute filterAttribute, + IMarmaladeStrings strings) + { + _filterAttribute = filterAttribute; + _strings = strings; + } + + public override async Task CheckPermissionsAsync( + ICommandContext context, + CommandInfo command, + IServiceProvider services) + { + var marmaladeContext = ContextAdapterFactory.CreateNew(context, + _strings, + services); + + var result = await _filterAttribute.CheckAsync(marmaladeContext); + + if (!result) + return PreconditionResult.FromError($"Precondition '{_filterAttribute.GetType().Name}' failed."); + + return PreconditionResult.FromSuccess(); + } +} diff --git a/src/EllieBot/Common/Marmalade/Adapters/GuildContextAdapter.cs b/src/EllieBot/Common/Marmalade/Adapters/GuildContextAdapter.cs new file mode 100644 index 0000000..f5f0a2b --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Adapters/GuildContextAdapter.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; + +public sealed class GuildContextAdapter : GuildContext +{ + private readonly IServiceProvider _services; + private readonly ICommandContext _ctx; + private readonly Lazy _ebs; + private readonly Lazy _botStrings; + private readonly Lazy _localization; + + public override IMarmaladeStrings Strings { get; } + public override IGuild Guild { get; } + public override ITextChannel Channel { get; } + public override ISelfUser Bot { get; } + public override IUserMessage Message + => _ctx.Message; + + public override IGuildUser User { get; } + + public override IEmbedBuilder Embed() + => _ebs.Value.Create(); + + public GuildContextAdapter(ICommandContext ctx, IMarmaladeStrings strings, IServiceProvider services) + { + if (ctx.Guild is not IGuild guild || ctx.Channel is not ITextChannel channel) + { + throw new ArgumentException("Can't use non-guild context to create GuildContextAdapter", nameof(ctx)); + } + + Strings = strings; + User = (IGuildUser)ctx.User; + Bot = ctx.Client.CurrentUser; + + _services = services; + _ebs = new(_services.GetRequiredService()); + _botStrings = new(_services.GetRequiredService); + _localization = new(_services.GetRequiredService()); + + (_ctx, Guild, Channel) = (ctx, guild, channel); + } + + public override string GetText(string key, object[]? args = null) + { + args ??= Array.Empty(); + + var cultureInfo = _localization.Value.GetCultureInfo(_ctx.Guild.Id); + var output = Strings.GetText(key, cultureInfo, args); + if (!string.IsNullOrWhiteSpace(output)) + return output; + + return _botStrings.Value.GetText(key, cultureInfo, args); + } +} diff --git a/src/EllieBot/Common/Marmalade/Adapters/ParamParserAdapter.cs b/src/EllieBot/Common/Marmalade/Adapters/ParamParserAdapter.cs new file mode 100644 index 0000000..1d3b948 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Adapters/ParamParserAdapter.cs @@ -0,0 +1,32 @@ +public sealed class ParamParserAdapter : TypeReader +{ + private readonly ParamParser _parser; + private readonly IMarmaladeStrings _strings; + private readonly IServiceProvider _services; + + public ParamParserAdapter(ParamParser parser, + IMarmaladeStrings strings, + IServiceProvider services) + { + _parser = parser; + _strings = strings; + _services = services; + } + + public override async Task ReadAsync( + ICommandContext context, + string input, + IServiceProvider services) + { + var marmaladeContext = ContextAdapterFactory.CreateNew(context, + _strings, + _services); + + var result = await _parser.TryParseAsync(marmaladeContext, input); + + if (result.IsSuccess) + return Discord.Commands.TypeReaderResult.FromSuccess(result.Data); + + return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, "Invalid input"); + } +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/CommandContextType.cs b/src/EllieBot/Common/Marmalade/CommandContextType.cs new file mode 100644 index 0000000..9f9459c --- /dev/null +++ b/src/EllieBot/Common/Marmalade/CommandContextType.cs @@ -0,0 +1,27 @@ +namespace Ellie.Marmalade; + +/// +/// Enum specifying in which context the command can be executed +/// +public enum CommandContextType +{ + /// + /// Command can only be executed in a guild + /// + Guild, + + /// + /// Command can only be executed in DMs + /// + Dm, + + /// + /// Command can be executed anywhere + /// + Any, + + /// + /// Command can be executed anywhere, and it doesn't require context to be passed to it + /// + Unspecified +} diff --git a/src/EllieBot/Common/Marmalade/Config/IMarmaladeConfigService.cs b/src/EllieBot/Common/Marmalade/Config/IMarmaladeConfigService.cs new file mode 100644 index 0000000..f3e6f6a --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Config/IMarmaladeConfigService.cs @@ -0,0 +1,8 @@ +namespace Ellie.Marmalade; + +public interface IMarmaladeConfigService +{ + IReadOnlyCollection GetLoadedMarmalades(); + void AddLoadedMarmalade(string name); + void RemoveLoadedMarmalade(string name); +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Config/MarmaladeConfig.cs b/src/EllieBot/Common/Marmalade/Config/MarmaladeConfig.cs new file mode 100644 index 0000000..82ec302 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Config/MarmaladeConfig.cs @@ -0,0 +1,20 @@ +#nullable disable +using Cloneable; +using EllieBot.Common.Yml; + +namespace Ellie.Marmalade; + +[Cloneable] +public sealed partial class MarmaladeConfig : ICloneable +{ + [Comment(@"DO NOT CHANGE")] + public int Version { get; set; } = 1; + + [Comment("List of marmalades automatically loaded at startup")] + public List? Loaded { get; set; } + + public MarmaladeConfig() + { + Loaded = new(); + } +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Config/MarmaladeConfigService.cs b/src/EllieBot/Common/Marmalade/Config/MarmaladeConfigService.cs new file mode 100644 index 0000000..fe475e8 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Config/MarmaladeConfigService.cs @@ -0,0 +1,45 @@ +using EllieBot.Common.Configs; + +namespace Ellie.Marmalade; + +public sealed class MarmaladeConfigService : ConfigServiceBase, IMarmaladeConfigService +{ + private const string FILE_PATH = "data/marmalades/marmalade.yml"; + private static readonly TypedKey _changeKey = new("config.marmalade.updated"); + + public override string Name + => "marmalade"; + + public MarmaladeConfigService( + IConfigSeria serializer, + IPubSub pubSub) + : base(FILE_PATH, serializer, pubSub, _changeKey) + { + } + + public IReadOnlyCollection GetLoadedMarmalades() + => Data.Loaded?.ToList() ?? new List(); + + public void AddLoadedMarmalade(string name) + { + ModifyConfig(conf => + { + if (conf.Loaded is null) + conf.Loaded = new(); + + if (!conf.Loaded.Contains(name)) + conf.Loaded.Add(name); + }); + } + + public void RemoveLoadedMarmalade(string name) + { + ModifyConfig(conf => + { + if (conf.Loaded is null) + conf.Loaded = new(); + + conf.Loaded.Remove(name); + }); + } +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/IMarmaladeLoaderService.cs b/src/EllieBot/Common/Marmalade/IMarmaladeLoaderService.cs new file mode 100644 index 0000000..7064f5c --- /dev/null +++ b/src/EllieBot/Common/Marmalade/IMarmaladeLoaderService.cs @@ -0,0 +1,23 @@ +using System.Globalization; + +namespace Ellie.Marmalade; + +public interface IMarmaladeLoaderService +{ + Task LoadMarmaladeAsync(string marmaladeName); + Task UnloadMarmaladeAsync(string marmaladeName); + string GetCommandDescription(string marmamaleName, string commandName, CultureInfo culture); + string[] GetCommandExampleArgs(string marmamaleName, string commandName, CultureInfo culture); + Task ReloadStrings(); + IReadOnlyCollection GetAllMarmalades(); + IReadOnlyCollection GetLoadedMarmalades(CultureInfo? cultureInfo = null); +} + +public sealed record MarmaladeStats(string Name, + string? Description, + IReadOnlyCollection Canaries); + +public sealed record CanaryStats(string Name, + IReadOnlyCollection Commands); + +public sealed record CanaryCommandStats(string Name); \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/MarmaladeAssemblyLoadContext.cs b/src/EllieBot/Common/Marmalade/MarmaladeAssemblyLoadContext.cs new file mode 100644 index 0000000..2e30d81 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/MarmaladeAssemblyLoadContext.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Ellie.Marmalade; + +public sealed class MarmaladeAssemblyLoadContext : AssemblyLoadContext +{ + private readonly AssemblyDependencyResolver _depResolver; + + public MarmaladeAssemblyLoadContext(string pluginPath) : base(isCollectible: true) + { + _depResolver = new(pluginPath); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + var assemblyPath = _depResolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath != null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var libraryPath = _depResolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (libraryPath != null) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + + return IntPtr.Zero; + } +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/MarmaladeLoaderService.cs b/src/EllieBot/Common/Marmalade/MarmaladeLoaderService.cs new file mode 100644 index 0000000..d31fc51 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/MarmaladeLoaderService.cs @@ -0,0 +1,917 @@ +using Discord.Commands.Builders; +using Microsoft.Extensions.DependencyInjection; +using Ellie.Marmalade.Adapters; +using EllieBot.Common.ModuleBehaviors; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Ellie.Marmalade; + +// ReSharper disable RedundantAssignment +public sealed class MarmaladeLoaderService : IMarmaladeLoaderService, IReadyExecutor, IEService +{ + private readonly CommandService _cmdService; + private readonly IServiceProvider _botServices; + private readonly IBehaviorHandler _behHandler; + private readonly IPubSub _pubSub; + private readonly IMarmaladeConfigService _marmaladeConfig; + + private readonly ConcurrentDictionary _resolved = new(); +#pragma warning disable IDE0090 // Use 'new(...)' + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); +#pragma warning restore IDE0090 // Use 'new(...)' + + private readonly TypedKey _loadKey = new("marmalade:load"); + private readonly TypedKey _unloadKey = new("marmalade:unload"); + + private readonly TypedKey _stringsReload = new("marmalade:reload_strings"); + + private const string BASE_DIR = "data/marmalades"; + + public MarmaladeLoaderService(CommandService cmdService, + IServiceProvider botServices, + IBehaviorHandler behHandler, + IPubSub pubSub, + IMarmaladeConfigService marmaladeConfig) + { + _cmdService = cmdService; + _botServices = botServices; + _behHandler = behHandler; + _pubSub = pubSub; + _marmaladeConfig = marmaladeConfig; + + // has to be done this way to support this feature on sharded bots + _pubSub.Sub(_loadKey, async name => await InternalLoadAsync(name)); + _pubSub.Sub(_unloadKey, async name => await InternalUnloadAsync(name)); + + _pubSub.Sub(_stringsReload, async _ => await ReloadStringsInternal()); + } + + public IReadOnlyCollection GetAllMarmalades() + { + if (!Directory.Exists(BASE_DIR)) + return Array.Empty(); + + return Directory.GetDirectories(BASE_DIR) + .Select(x => Path.GetRelativePath(BASE_DIR, x)) + .ToArray(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public IReadOnlyCollection GetLoadedMarmalades(CultureInfo? culture) + { + var toReturn = new List(_resolved.Count); + foreach (var (name, resolvedData) in _resolved) + { + var canaries = new List(resolvedData.CanaryInfos.Count); + + foreach (var canaryInfos in resolvedData.CanaryInfos.Concat(resolvedData.CanaryInfos.SelectMany(x => x.Subcanaries))) + { + var commands = new List(); + + foreach (var command in canaryInfos.Commands) + { + commands.Add(new CanaryCommandStats(command.Aliases.First())); + } + + canaries.Add(new CanaryStats(canaryInfos.Name, commands)); + } + + toReturn.Add(new MarmaladeStats(name, resolvedData.Strings.GetDescription(culture), canaries)); + } + return toReturn; + } + + public async Task OnReadyAsync() + { + foreach (var name in _marmaladeConfig.GetLoadedMarmalades()) + { + var result = await InternalLoadAsync(name); + if (result != MarmaladeLoadResult.Success) + Log.Warning("Unable to load '{MarmaladeName}' marmalade", name); + else + Log.Warning("Loaded marmalade '{MarmaladeName}'", name); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task LoadMarmaladeAsync(string marmaladeName) + { + // try loading on this shard first to see if it works + var res = await InternalLoadAsync(marmaladeName); + if (res == MarmaladeLoadResult.Success) + { + // if it does publish it so that other shards can load the medusa too + // this method will be ran twice on this shard but it doesn't matter as + // the second attempt will be ignored + await _pubSub.Pub(_loadKey, marmaladeName); + } + + return res; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task UnloadMarmaladeAsync(string marmaladeName) + { + var res = await InternalUnloadAsync(marmaladeName); + if (res == MarmaladeUnloadResult.Success) + { + await _pubSub.Pub(_unloadKey, marmaladeName); + } + + return res; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public string[] GetCommandExampleArgs(string marmaladeName, string commandName, CultureInfo culture) + { + if (!_resolved.TryGetValue(marmaladeName, out var data)) + return Array.Empty(); + + return data.Strings.GetCommandStrings(commandName, culture).Args + ?? data.CanaryInfos + .SelectMany(x => x.Commands) + .FirstOrDefault(x => x.Aliases.Any(alias + => alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase))) + ?.OptionalStrings + .Args + ?? new[] { string.Empty }; + } + + public Task ReloadStrings() + => _pubSub.Pub(_stringsReload, true); + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ReloadStringsSync() + { + foreach (var resolved in _resolved.Values) + { + resolved.Strings.Reload(); + } + } + + private async Task ReloadStringsInternal() + { + await _lock.WaitAsync(); + try + { + ReloadStringsSync(); + } + finally + { + _lock.Release(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public string GetCommandDescription(string marmaladeName, string commandName, CultureInfo culture) + { + if (!_resolved.TryGetValue(marmaladeName, out var data)) + return string.Empty; + + return data.Strings.GetCommandStrings(commandName, culture).Desc + ?? data.CanaryInfos + .SelectMany(x => x.Commands) + .FirstOrDefault(x => x.Aliases.Any(alias + => alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase))) + ?.OptionalStrings + .Desc + ?? string.Empty; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private async ValueTask InternalLoadAsync(string name) + { + if (_resolved.ContainsKey(name)) + return MarmaladeLoadResult.AlreadyLoaded; + + var safeName = Uri.EscapeDataString(name); + + await _lock.WaitAsync(); + try + { + if (LoadAssemblyInternal(safeName, + out var ctx, + out var canaryData, + out var services, + out var strings, + out var typeReaders)) + { + var moduleInfos = new List(); + + LoadTypeReadersInternal(typeReaders); + + foreach (var point in canaryData) + { + try + { + // initialize canary and subcanaries + await point.Instance.InitializeAsync(); + foreach (var sub in point.Subcanaries) + { + await sub.Instance.InitializeAsync(); + } + + var module = await LoadModuleInternalAsync(name, point, strings, services); + moduleInfos.Add(module); + } + catch (Exception ex) + { + Log.Warning(ex, + "Error loading canary {CanaryName}", + point.Name); + } + } + + var execs = GetExecsInternal(canaryData, strings, services); + await _behHandler.AddRangeAsync(execs); + + _resolved[name] = new(LoadContext: ctx, + ModuleInfos: moduleInfos.ToImmutableArray(), + CanaryInfos: canaryData.ToImmutableArray(), + strings, + typeReaders, + execs) + { + Services = services + }; + + + services = null; + _marmaladeConfig.AddLoadedMarmalade(safeName); + return MarmaladeLoadResult.Success; + } + + return MarmaladeLoadResult.Empty; + } + catch (Exception ex) when (ex is FileNotFoundException or BadImageFormatException) + { + return MarmaladeLoadResult.NotFound; + } + catch (Exception ex) + { + Log.Error(ex, "An error occurred loading a marmalade"); + return MarmaladeLoadResult.UnknownError; + } + finally + { + _lock.Release(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private IReadOnlyCollection GetExecsInternal(IReadOnlyCollection canaryData, IMarmaladeStrings strings, IServiceProvider services) + { + var behs = new List(); + foreach (var canary in canaryData) + { + behs.Add(new BehaviorAdapter(new(canary.Instance), strings, services)); + + foreach (var sub in canary.Subcanaries) + { + behs.Add(new BehaviorAdapter(new(sub.Instance), strings, services)); + } + } + + return behs; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void LoadTypeReadersInternal(Dictionary typeReaders) + { + var notAddedTypeReaders = new List(); + foreach (var (type, typeReader) in typeReaders) + { + // if type reader for this type already exists, it will not be replaced + if (_cmdService.TypeReaders.Contains(type)) + { + notAddedTypeReaders.Add(type); + continue; + } + + _cmdService.AddTypeReader(type, typeReader); + } + + // remove the ones that were not added + // to prevent them from being unloaded later + // as they didn't come from this marmalade + foreach (var toRemove in notAddedTypeReaders) + { + typeReaders.Remove(toRemove); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private bool LoadAssemblyInternal( + string safeName, + [NotNullWhen(true)] out WeakReference? ctxWr, + [NotNullWhen(true)] out IReadOnlyCollection? canaryData, + out IServiceProvider services, + out IMarmaladeStrings strings, + out Dictionary typeReaders) + { + ctxWr = null; + canaryData = null; + + var path = $"{BASE_DIR}/{safeName}/{safeName}.dll"; + strings = MarmaladeStrings.CreateDefault($"{BASE_DIR}/{safeName}"); + var ctx = new MarmaladeAssemblyLoadContext(Path.GetDirectoryName(path)); + var a = ctx.LoadFromAssemblyPath(Path.GetFullPath(path)); + var sis = LoadCanariesFromAssembly(a, out services); + typeReaders = LoadTypeReadersFromAssembly(a, strings, services); + + if (sis.Count == 0) + { + return false; + } + + ctxWr = new(ctx); + canaryData = sis; + + return true; + } + + + private static readonly Type _paramParserType = typeof(ParamParser<>); + + [MethodImpl(MethodImplOptions.NoInlining)] + private Dictionary LoadTypeReadersFromAssembly( + Assembly assembly, + IMarmaladeStrings strings, + IServiceProvider services) + { + var paramParsers = assembly.GetExportedTypes() + .Where(x => x.IsClass + && !x.IsAbstract + && x.BaseType is not null + && x.BaseType.IsGenericType + && x.BaseType.GetGenericTypeDefinition() == _paramParserType); + + var typeReaders = new Dictionary(); + foreach (var parserType in paramParsers) + { + var parserObj = ActivatorUtilities.CreateInstance(services, parserType); + + var targetType = parserType.BaseType!.GetGenericArguments()[0]; + var typeReaderInstance = (TypeReader)Activator.CreateInstance( + typeof(ParamParserAdapter<>).MakeGenericType(targetType), + args: new[] { parserObj, strings, services })!; + + typeReaders.Add(targetType, typeReaderInstance); + } + + return typeReaders; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private async Task LoadModuleInternalAsync(string marmaladeName, CanaryInfo canaryInfo, IMarmaladeStrings strings, IServiceProvider services) + { + var module = await _cmdService.CreateModuleAsync(canaryInfo.Instance.Prefix, + CreateModuleFactory(marmaladeName, canaryInfo, strings, services)); + + return module; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private Action CreateModuleFactory( + string marmaladeName, + CanaryInfo canaryInfo, + IMarmaladeStrings strings, + IServiceProvider marmaladeServices) + => mb => + { + var m = mb.WithName(canaryInfo.Name); + + foreach (var f in canaryInfo.Filters) + { + m.AddPrecondition(new FilterAdapter(f, strings)); + } + + foreach (var cmd in canaryInfo.Commands) + { + m.AddCommand(cmd.Aliases.First(), + CreateCallback(cmd.ContextType, + new(canaryInfo), + new(cmd), + new(marmaladeServices), + strings), + CreateCommandFactory(marmaladeName, cmd, strings)); + } + + foreach (var subInfo in canaryInfo.Subcanaries) + m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(marmaladeName, subInfo, strings, marmaladeServices)); + }; + +#pragma warning disable IDE0090 // Use 'new(...)' + private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute(ContextType.Guild); + private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute(ContextType.DM); +#pragma warning restore IDE0090 // Use 'new(...)' + private Action CreateCommandFactory(string marmaladeName, CanaryCommandData cmd, IMarmaladeStrings strings) + => (cb) => + { + cb.AddAliases(cmd.Aliases.Skip(1).ToArray()); + + if (cmd.ContextType == CommandContextType.Guild) + cb.AddPrecondition(_reqGuild); + else if (cmd.ContextType == CommandContextType.Dm) + cb.AddPrecondition(_reqDm); + + foreach (var f in cmd.Filters) + cb.AddPrecondition(new FilterAdapter(f, strings)); + + foreach (var ubp in cmd.UserAndBotPerms) + { + if (ubp is user_permAttribute up) + { + if (up.GuildPerm is { } gp) + cb.AddPrecondition(new UserPermAttribute(gp)); + else if (up.ChannelPerm is { } cp) + cb.AddPrecondition(new UserPermAttribute(cp)); + } + else if (ubp is bot_permAttribute bp) + { + if (bp.GuildPerm is { } gp) + cb.AddPrecondition(new BotPermAttribute(gp)); + else if (bp.ChannelPerm is { } cp) + cb.AddPrecondition(new BotPermAttribute(cp)); + } + else if (ubp is bot_owner_onlyAttribute) + { + cb.AddPrecondition(new OwnerOnlyAttribute()); + } + } + + cb.WithPriority(cmd.Priority); + + // using summary to save method name + // method name is used to retrieve desc/usages + cb.WithRemarks($"marmalade///{marmaladeName}"); + cb.WithSummary(cmd.MethodInfo.Name.ToLowerInvariant()); + + foreach (var param in cmd.Parameters) + { + cb.AddParameter(param.Name, param.Type, CreateParamFactory(param)); + } + }; + + private Action CreateParamFactory(ParamData paramData) + => (pb) => + { + pb.WithIsMultiple(paramData.IsParams) + .WithIsOptional(paramData.IsOptional) + .WithIsRemainder(paramData.IsLeftover); + + if (paramData.IsOptional) + pb.WithDefault(paramData.DefaultValue); + }; + + [MethodImpl(MethodImplOptions.NoInlining)] + private Func CreateCallback( + CommandContextType contextType, + WeakReference canaryDataWr, + WeakReference canaryCommandDataWr, + WeakReference marmaladeServicesWr, + IMarmaladeStrings strings) + => async (context, parameters, svcs, _) => + { + if (!canaryCommandDataWr.TryGetTarget(out var cmdData) + || !canaryDataWr.TryGetTarget(out var canaryData) + || !marmaladeServicesWr.TryGetTarget(out var marmaladeServices)) + { + Log.Warning("Attempted to run an unloaded canary's command"); + return; + } + + var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, marmaladeServices, strings); + + try + { + var methodInfo = cmdData.MethodInfo; + if (methodInfo.ReturnType == typeof(Task) + || (methodInfo.ReturnType.IsGenericType + && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))) + { + await (Task)methodInfo.Invoke(canaryData.Instance, paramObjs)!; + } + else if (methodInfo.ReturnType == typeof(ValueTask)) + { + await ((ValueTask)methodInfo.Invoke(canaryData.Instance, paramObjs)!).AsTask(); + } + else // if (methodInfo.ReturnType == typeof(void)) + { + methodInfo.Invoke(canaryData.Instance, paramObjs); + } + } + finally + { + paramObjs = null; + cmdData = null; + + canaryData = null; + marmaladeServices = null; + } + }; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static object[] ParamObjs( + CommandContextType contextType, + CanaryCommandData cmdData, + object[] parameters, + ICommandContext context, + IServiceProvider svcs, + IServiceProvider svcProvider, + IMarmaladeStrings strings) + { + var extraParams = contextType == CommandContextType.Unspecified ? 0 : 1; + extraParams += cmdData.InjectedParams.Count; + + var paramObjs = new object[parameters.Length + extraParams]; + + var startAt = 0; + if (contextType != CommandContextType.Unspecified) + { + paramObjs[0] = ContextAdapterFactory.CreateNew(context, strings, svcs); + + startAt = 1; + } + + for (var i = 0; i < cmdData.InjectedParams.Count; i++) + { + var svc = svcProvider.GetService(cmdData.InjectedParams[i]); + if (svc is null) + { + throw new ArgumentException($"Cannot inject a service of type {cmdData.InjectedParams[i]}"); + } + + paramObjs[i + startAt] = svc; + + svc = null; + } + + startAt += cmdData.InjectedParams.Count; + + for (var i = 0; i < parameters.Length; i++) + paramObjs[startAt + i] = parameters[i]; + + return paramObjs; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private async Task InternalUnloadAsync(string name) + { + if (!_resolved.Remove(name, out var lsi)) + return MarmaladeUnloadResult.NotLoaded; + + await _lock.WaitAsync(); + try + { + UnloadTypeReaders(lsi.TypeReaders); + + foreach (var mi in lsi.ModuleInfos) + { + await _cmdService.RemoveModuleAsync(mi); + } + + await _behHandler.RemoveRangeAsync(lsi.Execs); + + await DisposeCanaryInstances(lsi); + + var lc = lsi.LoadContext; + + // removing this line will prevent assembly from being unloaded quickly + // as this local variable will be held for a long time potentially + // due to how async works + lsi.Services = null!; + lsi = null; + + _marmaladeConfig.RemoveLoadedMarmalade(name); + return UnloadInternal(lc) + ? MarmaladeUnloadResult.Success + : MarmaladeUnloadResult.PossiblyUnable; + } + finally + { + _lock.Release(); + } + } + + private void UnloadTypeReaders(Dictionary valueTypeReaders) + { + foreach (var tr in valueTypeReaders) + { + _cmdService.TryRemoveTypeReader(tr.Key, false, out _); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private async Task DisposeCanaryInstances(ResolvedMarmalade marmalade) + { + foreach (var si in marmalade.CanaryInfos) + { + try + { + await si.Instance.DisposeAsync(); + foreach (var sub in si.Subcanaries) + { + await sub.Instance.DisposeAsync(); + } + } + catch (Exception ex) + { + Log.Warning(ex, + "Failed cleanup of Canary {CanaryName}. This marmalade might not unload correctly", + si.Instance.Name); + } + } + + // marmalades = null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private bool UnloadInternal(WeakReference lsi) + { + UnloadContext(lsi); + GcCleanup(); + + return !lsi.TryGetTarget(out _); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void UnloadContext(WeakReference lsiLoadContext) + { + if (lsiLoadContext.TryGetTarget(out var ctx)) + ctx.Unload(); + } + + private void GcCleanup() + { + // cleanup + for (var i = 0; i < 10; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.WaitForFullGCComplete(); + GC.Collect(); + } + } + + private static readonly Type _canaryType = typeof(Canary); + + [MethodImpl(MethodImplOptions.NoInlining)] + private IServiceProvider LoadMarmaladeServicesInternal(Assembly a) + => new ServiceCollection() + .Scan(x => x.FromAssemblies(a) + .AddClasses(static x => x.WithAttribute(x => x.Lifetime == Lifetime.Transient)) + .AsSelfWithInterfaces() + .WithTransientLifetime() + .AddClasses(static x => x.WithAttribute(x => x.Lifetime == Lifetime.Singleton)) + .AsSelfWithInterfaces() + .WithSingletonLifetime()) + .BuildServiceProvider(); + + + [MethodImpl(MethodImplOptions.NoInlining)] + public IReadOnlyCollection LoadCanariesFromAssembly(Assembly a, out IServiceProvider services) + { + var marmaladeServices = LoadMarmaladeServicesInternal(a); + services = new MarmaladeServiceProvider(_botServices, marmaladeServices); + + // find all types in teh assembly + var types = a.GetExportedTypes(); + // snek is always a public non abstract class + var classes = types.Where(static x => x.IsClass + && (x.IsNestedPublic || x.IsPublic) + && !x.IsAbstract + && x.BaseType == _canaryType + && (x.DeclaringType is null || x.DeclaringType.IsAssignableTo(_canaryType))) + .ToList(); + + var topModules = new Dictionary(); + + foreach (var cl in classes) + { + if (cl.DeclaringType is not null) + continue; + + // get module data, and add it to the topModules dictionary + var module = GetModuleData(cl, services); + topModules.Add(cl, module); + } + + foreach (var c in classes) + { + if (c.DeclaringType is not Type dt) + continue; + + // if there is no top level module which this module is a child of + // just print a warning and skip it + if (!topModules.TryGetValue(dt, out var parentData)) + { + Log.Warning("Can't load submodule {SubName} because parent module {Name} does not exist", + c.Name, + dt.Name); + continue; + } + + GetModuleData(c, services, parentData); + } + + return topModules.Values.ToArray(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private CanaryInfo GetModuleData(Type type, IServiceProvider services, CanaryInfo? parentData = null) + { + var filters = type.GetCustomAttributes(true) + .ToArray(); + + var instance = (Canary)ActivatorUtilities.CreateInstance(services, type); + + var module = new CanaryInfo(instance.Name, + parentData, + instance, + GetCommands(instance, type), + filters); + + if (parentData is not null) + parentData.Subcanaries.Add(module); + + return module; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private IReadOnlyCollection GetCommands(Canary instance, Type type) + { + var methodInfos = type + .GetMethods(BindingFlags.Instance + | BindingFlags.DeclaredOnly + | BindingFlags.Public) + .Where(static x => + { + if (x.GetCustomAttribute(true) is null) + return false; + + if (x.ReturnType.IsGenericType) + { + var genericType = x.ReturnType.GetGenericTypeDefinition(); + if (genericType == typeof(Task<>)) + return true; + + // if (genericType == typeof(ValueTask<>)) + // return true; + + Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}", + x.Name, + x.ReturnType); + + return false; + } + + var succ = x.ReturnType == typeof(Task) + || x.ReturnType == typeof(ValueTask) + || x.ReturnType == typeof(void); + + if (!succ) + { + Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}", + x.Name, + x.ReturnType); + } + + return succ; + }); + + + var cmds = new List(); + foreach (var method in methodInfos) + { + var filters = method.GetCustomAttributes(true).ToArray(); + var userAndBotPerms = method.GetCustomAttributes(true) + .ToArray(); + var prio = method.GetCustomAttribute(true)?.Priority ?? 0; + + var paramInfos = method.GetParameters(); + var cmdParams = new List(); + var diParams = new List(); + var cmdContext = CommandContextType.Unspecified; + var canInject = false; + for (var paramCounter = 0; paramCounter < paramInfos.Length; paramCounter++) + { + var pi = paramInfos[paramCounter]; + + var paramName = pi.Name ?? "unnamed"; + var isContext = paramCounter == 0 && pi.ParameterType.IsAssignableTo(typeof(AnyContext)); + + var leftoverAttribute = pi.GetCustomAttribute(true); + var hasDefaultValue = pi.HasDefaultValue; + var defaultValue = pi.DefaultValue; + var isLeftover = leftoverAttribute != null; + var isParams = pi.GetCustomAttribute() is not null; + var paramType = pi.ParameterType; + var isInjected = pi.GetCustomAttribute(true) is not null; + + if (isContext) + { + if (hasDefaultValue || leftoverAttribute != null || isParams) + throw new ArgumentException("IContext parameter cannot be optional, leftover, constant or params. " + GetErrorPath(method, pi)); + + if (paramCounter != 0) + throw new ArgumentException($"IContext parameter has to be first. {GetErrorPath(method, pi)}"); + + canInject = true; + + if (paramType.IsAssignableTo(typeof(GuildContext))) + cmdContext = CommandContextType.Guild; + else if (paramType.IsAssignableTo(typeof(DmContext))) + cmdContext = CommandContextType.Dm; + else + cmdContext = CommandContextType.Any; + + continue; + } + + if (isInjected) + { + if (!canInject && paramCounter != 0) + throw new ArgumentException($"Parameters marked as [Injected] have to come after IContext"); + + canInject = true; + + diParams.Add(paramType); + continue; + } + + canInject = false; + + if (isParams) + { + if (hasDefaultValue) + throw new NotSupportedException("Params can't have const values at the moment. " + + GetErrorPath(method, pi)); + // if it's params, it means it's an array, and i only need a parser for the actual type, + // as the parser will run on each array element, it can't be null + paramType = paramType.GetElementType()!; + } + + // leftover can only be the last parameter. + if (isLeftover && paramCounter != paramInfos.Length - 1) + { + var path = GetErrorPath(method, pi); + Log.Error("Only one parameter can be marked [Leftover] and it has to be the last one. {Path} ", + path); + throw new ArgumentException("Leftover attribute error."); + } + + cmdParams.Add(new ParamData(paramType, paramName, hasDefaultValue, defaultValue, isLeftover, isParams)); + } + + + var cmdAttribute = method.GetCustomAttribute(true)!; + var aliases = cmdAttribute.Aliases; + if (aliases.Length == 0) + aliases = new[] { method.Name.ToLowerInvariant() }; + + cmds.Add(new( + aliases, + method, + instance, + filters, + userAndBotPerms, + cmdContext, + diParams, + cmdParams, + new(cmdAttribute.desc, cmdAttribute.args), + prio + )); + } + + return cmds; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private string GetErrorPath(MethodInfo m, System.Reflection.ParameterInfo pi) + => $@"Module: {m.DeclaringType?.Name} +Command: {m.Name} +ParamName: {pi.Name} +ParamType: {pi.ParameterType.Name}"; +} + +public enum MarmaladeLoadResult +{ + Success, + NotFound, + AlreadyLoaded, + Empty, + UnknownError, +} + +public enum MarmaladeUnloadResult +{ + Success, + NotLoaded, + PossiblyUnable, + NotFound, +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/MarmaladeServiceProvider.cs b/src/EllieBot/Common/Marmalade/MarmaladeServiceProvider.cs new file mode 100644 index 0000000..c2f3c76 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/MarmaladeServiceProvider.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; + +namespace Ellie.Marmalade; + +public class MarmaladeServiceProvider : IServiceProvider +{ + private readonly IServiceProvider _ellieServices; + private readonly IServiceProvider _marmaladeServices; + + public MarmaladeServiceProvider(IServiceProvider ellieServices, IServiceProvider marmaladeServices) + { + _ellieServices = ellieServices; + _marmaladeServices = marmaladeServices; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object? GetService(Type serviceType) + { + if (!serviceType.Assembly.IsCollectible) + return _ellieServices.GetService(serviceType); + + return _marmaladeServices.GetService(serviceType); + } +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Models/CanaryCommandData.cs b/src/EllieBot/Common/Marmalade/Models/CanaryCommandData.cs new file mode 100644 index 0000000..412243d --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Models/CanaryCommandData.cs @@ -0,0 +1,46 @@ +using Microsoft.VisualBasic; +using System.Reflection; +using CommandStrings = Ellie.Marmalade.CommandStrings; + +namespace Ellie.Marmalade; + +public sealed class CanaryCommandData +{ + public CanaryCommandData( + IReadOnlyCollection aliases, + MethodInfo methodInfo, + Canary module, + FilterAttribute[] filters, + MarmaladePermAttribute[] userAndBotPerms, + CommandContextType contextType, + IReadOnlyList injectedParams, + IReadOnlyList parameters, + CommandStrings strings, + int priority) + { + Aliases = aliases; + MethodInfo = methodInfo; + Module = module; + Filters = filters; + UserAndBotPerms = userAndBotPerms; + ContextType = contextType; + InjectedParams = injectedParams; + Parameters = parameters; + Priority = priority; + OptionalStrings = strings; + } + + public MarmaladePermAttribute[] UserAndBotPerms { get; set; } + + public CommandStrings OptionalStrings { get; set; } + + public IReadOnlyCollection Aliases { get; } + public MethodInfo MethodInfo { get; set; } + public Canary Module { get; set; } + public FilterAttribute[] Filters { get; set; } + public CommandContextType ContextType { get; } + public IReadOnlyList InjectedParams { get; } + public IReadOnlyList Parameters { get; } + public int Priority { get; } + +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Models/CanaryData.cs b/src/EllieBot/Common/Marmalade/Models/CanaryData.cs new file mode 100644 index 0000000..b3938e7 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Models/CanaryData.cs @@ -0,0 +1,11 @@ +namespace Ellie.Marmalade; + +public sealed record CanaryInfo( + string Name, + CanaryInfo? Parent, + Canary Instance, + IReadOnlyCollection Commands, + IReadOnlyCollection Filters) +{ + public List Subcanaries { get; set; } = new(); +} \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Models/ParamData.cs b/src/EllieBot/Common/Marmalade/Models/ParamData.cs new file mode 100644 index 0000000..e329958 --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Models/ParamData.cs @@ -0,0 +1,10 @@ +namespace Ellie.Marmalade; + +public sealed record ParamData( + Type Type, + string Name, + bool IsOptional, + object? DefaultValue, + bool IsLeftover, + bool IsParams +); \ No newline at end of file diff --git a/src/EllieBot/Common/Marmalade/Models/ResolvedMarmalade.cs b/src/EllieBot/Common/Marmalade/Models/ResolvedMarmalade.cs new file mode 100644 index 0000000..bf40d7a --- /dev/null +++ b/src/EllieBot/Common/Marmalade/Models/ResolvedMarmalade.cs @@ -0,0 +1,14 @@ +using System.Collections.Immutable; + +namespace Ellie.Marmalade; + +public sealed record ResolvedMarmalade( + WeakReference LoadContext, + IImmutableList ModuleInfos, + IImmutableList CanaryInfos, + IMarmaladeStrings Strings, + Dictionary TypeReaders, + IReadOnlyCollection Execs) +{ + public IServiceProvider Services { get; set; } = null!; +} \ No newline at end of file