This repository has been archived on 2024-12-22. You can view files and clone it, but cannot push or open issues or pull requests.
elliebot/src/EllieBot/_common/Marmalade/Common/MarmaladeLoaderService.cs

916 lines
No EOL
32 KiB
C#

using Discord.Commands.Builders;
using DryIoc;
using Microsoft.Extensions.DependencyInjection;
using Ellie.Common.Marmalade;
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 EllieBot.Marmalade;
// ReSharper disable RedundantAssignment
public sealed class MarmaladeLoaderService : IMarmaladeLoaderService, IReadyExecutor, IEService
{
private readonly CommandService _cmdService;
private readonly IBehaviorHandler _behHandler;
private readonly IPubSub _pubSub;
private readonly IMarmaladeConfigService _marmaladeConfig;
private readonly IContainer _cont;
private readonly ConcurrentDictionary<string, ResolvedMarmalade> _resolved = new();
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly TypedKey<string> _loadKey = new("marmalade:load");
private readonly TypedKey<string> _unloadKey = new("marmalade:unload");
private readonly TypedKey<bool> _stringsReload = new("marmalade:reload_strings");
private const string BASE_DIR = "data/marmalades";
public MarmaladeLoaderService(
CommandService cmdService,
IContainer cont,
IBehaviorHandler behHandler,
IPubSub pubSub,
IMarmaladeConfigService marmaladeConfig)
{
_cmdService = cmdService;
_behHandler = behHandler;
_pubSub = pubSub;
_marmaladeConfig = marmaladeConfig;
_cont = cont;
// 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<string> GetAllMarmalades()
{
if (!Directory.Exists(BASE_DIR))
return Array.Empty<string>();
return Directory.GetDirectories(BASE_DIR)
.Select(x => Path.GetRelativePath(BASE_DIR, x))
.ToArray();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public IReadOnlyCollection<MarmaladeStats> GetLoadedMarmalades(CultureInfo? culture)
{
var toReturn = new List<MarmaladeStats>(_resolved.Count);
foreach (var (name, resolvedData) in _resolved)
{
var canaries = new List<CanaryStats>(resolvedData.CanaryInfos.Count);
foreach (var canaryInfos in resolvedData.CanaryInfos.Concat(resolvedData.CanaryInfos.SelectMany(x => x.Subcanaries)))
{
var commands = new List<CanaryCommandStats>();
foreach (var command in canaryInfos.Commands)
{
commands.Add(new CanaryCommandStats(command.Aliases.First()));
}
canaries.Add(new CanaryStats(canaryInfos.Name, canaryInfos.Instance.Prefix, 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<MarmaladeLoadResult> 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 marmalade 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<MarmaladeUnloadResult> 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<string>();
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
?? [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<MarmaladeLoadResult> 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 iocModule,
out var strings,
out var typeReaders))
{
var moduleInfos = new List<ModuleInfo>();
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, iocModule);
moduleInfos.Add(module);
}
catch (Exception ex)
{
Log.Warning(ex,
"Error loading canary {CanaryName}",
point.Name);
}
}
var execs = GetExecsInternal(canaryData, strings);
await _behHandler.AddRangeAsync(execs);
_resolved[name] = new(LoadContext: ctx,
ModuleInfos: moduleInfos.ToImmutableArray(),
CanaryInfos: canaryData.ToImmutableArray(),
strings,
typeReaders,
execs)
{
IocModule = iocModule
};
_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<ICustomBehavior> GetExecsInternal(
IReadOnlyCollection<CanaryInfo> canaryData,
IMarmaladeStrings strings)
{
var behs = new List<ICustomBehavior>();
foreach (var canary in canaryData)
{
behs.Add(new BehaviorAdapter(new(canary.Instance), strings, _cont));
foreach (var sub in canary.Subcanaries)
{
behs.Add(new BehaviorAdapter(new(sub.Instance), strings, _cont));
}
}
return behs;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void LoadTypeReadersInternal(Dictionary<Type, TypeReader> typeReaders)
{
var notAddedTypeReaders = new List<Type>();
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<MarmaladeAssemblyLoadContext>? ctxWr,
[NotNullWhen(true)] out IReadOnlyCollection<CanaryInfo>? canaryData,
[NotNullWhen(true)] out IIocModule? iocModule,
out IMarmaladeStrings strings,
out Dictionary<Type, TypeReader> typeReaders)
{
ctxWr = null;
canaryData = null;
var path = Path.GetFullPath($"{BASE_DIR}/{safeName}/{safeName}.dll");
var dir = Path.GetFullPath($"{BASE_DIR}/{safeName}");
if (!Directory.Exists(dir))
throw new DirectoryNotFoundException($"Marmalade folder not found: {dir}");
if (!File.Exists(path))
throw new FileNotFoundException($"Marmalade dll not found: {path}");
strings = MarmaladeStrings.CreateDefault(dir);
var ctx = new MarmaladeAssemblyLoadContext(path);
var a = ctx.LoadFromAssemblyPath(Path.GetFullPath(path));
// ctx.LoadDependencies(a);
// load services
iocModule = new MarmaladeNinjectIocModule(_cont, a, safeName);
iocModule.Load();
var sis = LoadCanariesFromAssembly(safeName, a);
typeReaders = LoadTypeReadersFromAssembly(a, strings);
if (sis.Count == 0)
{
iocModule.Unload();
return false;
}
ctxWr = new(ctx);
canaryData = sis;
return true;
}
private static readonly Type _paramParserType = typeof(ParamParser<>);
[MethodImpl(MethodImplOptions.NoInlining)]
private Dictionary<Type, TypeReader> LoadTypeReadersFromAssembly(
Assembly assembly,
IMarmaladeStrings strings)
{
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<Type, TypeReader>();
foreach (var parserType in paramParsers)
{
var parserObj = ActivatorUtilities.CreateInstance(_cont, parserType);
var targetType = parserType.BaseType!.GetGenericArguments()[0];
var typeReaderInstance = (TypeReader)Activator.CreateInstance(
typeof(ParamParserAdapter<>).MakeGenericType(targetType),
args: [parserObj, strings, _cont])!;
typeReaders.Add(targetType, typeReaderInstance);
}
return typeReaders;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task<ModuleInfo> LoadModuleInternalAsync(
string marmaladeName,
CanaryInfo canaryInfo,
IMarmaladeStrings strings,
IIocModule services)
{
var module = await _cmdService.CreateModuleAsync(canaryInfo.Instance.Prefix,
CreateModuleFactory(marmaladeName, canaryInfo, strings, services));
return module;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private Action<ModuleBuilder> CreateModuleFactory(
string marmaladeName,
CanaryInfo canaryInfo,
IMarmaladeStrings strings,
IIocModule iocModule)
=> 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),
strings),
CreateCommandFactory(marmaladeName, cmd, strings));
}
foreach (var subInfo in canaryInfo.Subcanaries)
m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(marmaladeName, subInfo, strings, iocModule));
};
private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute(ContextType.Guild);
private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute(ContextType.DM);
private Action<CommandBuilder> 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<ParameterBuilder> 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<ICommandContext, object[], IServiceProvider, CommandInfo, Task> CreateCallback(
CommandContextType contextType,
WeakReference<CanaryInfo> canaryDataWr,
WeakReference<CanaryCommandData> canaryCommandDataWr,
IMarmaladeStrings strings)
=> async (
context,
parameters,
svcs,
_) =>
{
if (!canaryCommandDataWr.TryGetTarget(out var cmdData)
|| !canaryDataWr.TryGetTarget(out var canaryData))
{
Log.Warning("Attempted to run an unloaded canary's command");
return;
}
var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, _cont, 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;
}
};
[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<MarmaladeUnloadResult> 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;
var km = lsi.IocModule;
lsi.IocModule.Unload();
lsi.IocModule = null!;
if (km is IDisposable d)
d.Dispose();
lsi = null;
_marmaladeConfig.RemoveLoadedMarmalade(name);
return UnloadInternal(lc)
? MarmaladeUnloadResult.Success
: MarmaladeUnloadResult.PossiblyUnable;
}
finally
{
_lock.Release();
}
}
private void UnloadTypeReaders(Dictionary<Type, TypeReader> 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<MarmaladeAssemblyLoadContext> lsi)
{
UnloadContext(lsi);
GcCleanup();
return !lsi.TryGetTarget(out _);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void UnloadContext(WeakReference<MarmaladeAssemblyLoadContext> 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 MarmaladeIoCKernelModule LoadMarmaladeServicesInternal(string name, Assembly a)
// => new MarmaladeIoCKernelModule(name, a);
[MethodImpl(MethodImplOptions.NoInlining)]
public IReadOnlyCollection<CanaryInfo> LoadCanariesFromAssembly(string name, Assembly a)
{
// find all types in teh assembly
var types = a.GetExportedTypes();
// canary 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<Type, CanaryInfo>();
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);
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, parentData);
}
return topModules.Values.ToArray();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private CanaryInfo GetModuleData(Type type, CanaryInfo? parentData = null)
{
var filters = type.GetCustomAttributes<FilterAttribute>(true)
.ToArray();
var instance = (Canary)ActivatorUtilities.CreateInstance(_cont, 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<CanaryCommandData> GetCommands(Canary instance, Type type)
{
var methodInfos = type
.GetMethods(BindingFlags.Instance
| BindingFlags.DeclaredOnly
| BindingFlags.Public)
.Where(static x =>
{
if (x.GetCustomAttribute<cmdAttribute>(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<CanaryCommandData>();
foreach (var method in methodInfos)
{
var filters = method.GetCustomAttributes<FilterAttribute>(true).ToArray();
var userAndBotPerms = method.GetCustomAttributes<MarmaladePermAttribute>(true)
.ToArray();
var prio = method.GetCustomAttribute<prioAttribute>(true)?.Priority ?? 0;
var paramInfos = method.GetParameters();
var cmdParams = new List<ParamData>();
var diParams = new List<Type>();
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<leftoverAttribute>(true);
var hasDefaultValue = pi.HasDefaultValue;
var defaultValue = pi.DefaultValue;
var isLeftover = leftoverAttribute != null;
var isParams = pi.GetCustomAttribute<ParamArrayAttribute>() is not null;
var paramType = pi.ParameterType;
var isInjected = pi.GetCustomAttribute<injectAttribute>(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<cmdAttribute>(true)!;
var aliases = cmdAttribute.Aliases;
if (aliases.Length == 0)
aliases = [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}";
}