diff --git a/ASFFreeGames/ASFFreeGamesPlugin.cs b/ASFFreeGames/ASFFreeGamesPlugin.cs index bcf2ce9..24123ab 100644 --- a/ASFFreeGames/ASFFreeGamesPlugin.cs +++ b/ASFFreeGames/ASFFreeGamesPlugin.cs @@ -68,7 +68,13 @@ public async Task OnASFInit(IReadOnlyDictionary? additionalConfi await SaveOptions(CancellationToken).ConfigureAwait(false); } - public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) => await CommandDispatcher.Execute(bot, message, args, steamID).ConfigureAwait(false); + public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) { + if (!Context.Valid) { + CreateContext(); + } + + return await CommandDispatcher.Execute(bot, message, args, steamID).ConfigureAwait(false); + } public async Task OnBotDestroy(Bot bot) => await RemoveBot(bot).ConfigureAwait(false); @@ -93,14 +99,14 @@ public Task OnLoaded() { public async void CollectGamesOnClock(object? source) { CollectIntervalManager.RandomlyChangeCollectInterval(source); - if ((Bots.Count > 0) && (Context.Bots.Count != Bots.Count)) { - Context = new PluginContext(Bots, BotContextRegistry, Options, LoggerFilter, new Lazy(() => CancellationTokenSourceLazy.Value.Token)); + if (!Context.Valid || ((Bots.Count > 0) && (Context.Bots.Count != Bots.Count))) { + CreateContext(); } using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); cts.CancelAfter(TimeSpan.FromMilliseconds(CollectGamesTimeout)); - if (cts.IsCancellationRequested) { + if (cts.IsCancellationRequested || !Context.Valid) { return; } @@ -120,6 +126,11 @@ public async void CollectGamesOnClock(object? source) { } } + /// + /// Creates a new PluginContext instance and assigns it to the Context property. + /// + private void CreateContext() => Context = new PluginContext(Bots, BotContextRegistry, Options, LoggerFilter, new Lazy(() => CancellationTokenSourceLazy.Value.Token), true); + private async Task RegisterBot(Bot bot) { Bots.Add(bot); @@ -152,7 +163,7 @@ private async Task RemoveBot(Bot bot) { CollectIntervalManager.StopTimer(); } - Context.LoggerFilter.RemoveFilters(bot); + LoggerFilter.RemoveFilters(bot); } private async Task SaveOptions(CancellationToken cancellationToken) { diff --git a/ASFFreeGames/Commands/FreeGamesCommand.cs b/ASFFreeGames/Commands/FreeGamesCommand.cs index 025e876..d3372c3 100644 --- a/ASFFreeGames/Commands/FreeGamesCommand.cs +++ b/ASFFreeGames/Commands/FreeGamesCommand.cs @@ -41,13 +41,13 @@ internal sealed class FreeGamesCommand : IBotCommand, IDisposable { if (args.Length >= 2) { switch (args[1].ToUpperInvariant()) { case "SET": - return await HandleSetCommand(bot, args).ConfigureAwait(false); + return await HandleSetCommand(bot, args, cancellationToken).ConfigureAwait(false); case "RELOAD": return await HandleReloadCommand(bot).ConfigureAwait(false); case SaveOptionsInternalCommandString: - return await HandleInternalSaveOptionsCommand(bot).ConfigureAwait(false); + return await HandleInternalSaveOptionsCommand(bot, cancellationToken).ConfigureAwait(false); case CollectInternalCommandString: - return await HandleInternalCollectCommand(bot, args).ConfigureAwait(false); + return await HandleInternalCollectCommand(bot, args, cancellationToken).ConfigureAwait(false); } } @@ -56,43 +56,46 @@ internal sealed class FreeGamesCommand : IBotCommand, IDisposable { private static string FormatBotResponse(Bot? bot, string resp) => IBotCommand.FormatBotResponse(bot, resp); - private async Task HandleSetCommand(Bot? bot, string[] args) { + private async Task HandleSetCommand(Bot? bot, string[] args, CancellationToken cancellationToken) { + using CancellationTokenSource cts = CreateLinkedTokenSource(cancellationToken); + cancellationToken = cts.Token; + if (args.Length >= 3) { switch (args[2].ToUpperInvariant()) { case "VERBOSE": Options.VerboseLog = true; - await SaveOptions().ConfigureAwait(false); + await SaveOptions(cancellationToken).ConfigureAwait(false); return FormatBotResponse(bot, "Verbosity on"); case "NOVERBOSE": Options.VerboseLog = false; - await SaveOptions().ConfigureAwait(false); + await SaveOptions(cancellationToken).ConfigureAwait(false); return FormatBotResponse(bot, "Verbosity off"); case "F2P": case "FREETOPLAY": case "NOSKIPFREETOPLAY": Options.SkipFreeToPlay = false; - await SaveOptions().ConfigureAwait(false); + await SaveOptions(cancellationToken).ConfigureAwait(false); return FormatBotResponse(bot, $"{ASFFreeGamesPlugin.StaticName} is going to collect f2p games"); case "NOF2P": case "NOFREETOPLAY": case "SKIPFREETOPLAY": Options.SkipFreeToPlay = true; - await SaveOptions().ConfigureAwait(false); + await SaveOptions(cancellationToken).ConfigureAwait(false); return FormatBotResponse(bot, $"{ASFFreeGamesPlugin.StaticName} is now skipping f2p games"); case "DLC": case "NOSKIPDLC": Options.SkipDLC = false; - await SaveOptions().ConfigureAwait(false); + await SaveOptions(cancellationToken).ConfigureAwait(false); return FormatBotResponse(bot, $"{ASFFreeGamesPlugin.StaticName} is going to collect dlc"); case "NODLC": case "SKIPDLC": Options.SkipDLC = true; - await SaveOptions().ConfigureAwait(false); + await SaveOptions(cancellationToken).ConfigureAwait(false); return FormatBotResponse(bot, $"{ASFFreeGamesPlugin.StaticName} is now skipping dlc"); @@ -104,6 +107,13 @@ internal sealed class FreeGamesCommand : IBotCommand, IDisposable { return null; } + /// + /// Creates a linked cancellation token source from the given cancellation token and the Context cancellation token. + /// + /// The cancellation token to link. + /// A CancellationTokenSource that is linked to both tokens. + private static CancellationTokenSource CreateLinkedTokenSource(CancellationToken cancellationToken) => Context.Valid ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, Context.CancellationToken) : CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + private Task HandleReloadCommand(Bot? bot) { ASFFreeGamesOptionsLoader.Bind(ref Options); @@ -116,23 +126,24 @@ internal sealed class FreeGamesCommand : IBotCommand, IDisposable { return FormatBotResponse(bot, $"Collected a total of {collected} free game(s)"); } - private async ValueTask HandleInternalSaveOptionsCommand(Bot? bot) { - await SaveOptions().ConfigureAwait(false); + private async ValueTask HandleInternalSaveOptionsCommand(Bot? bot, CancellationToken cancellationToken) { + await SaveOptions(cancellationToken).ConfigureAwait(false); return null; } - private async ValueTask HandleInternalCollectCommand(Bot? bot, string[] args) { + private async ValueTask HandleInternalCollectCommand(Bot? bot, string[] args, CancellationToken cancellationToken) { Dictionary botMap = Context.Bots.ToDictionary(static b => b.BotName, static b => b, StringComparer.InvariantCultureIgnoreCase); - int collected = await CollectGames(args.Skip(2).Select(botName => botMap[botName]), ECollectGameRequestSource.Scheduled, Context.CancellationToken).ConfigureAwait(false); + int collected = await CollectGames(args.Skip(2).Select(botName => botMap[botName]), ECollectGameRequestSource.Scheduled, cancellationToken).ConfigureAwait(false); return FormatBotResponse(bot, $"Collected a total of {collected} free game(s)"); } - private async Task SaveOptions() { - using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(Context.CancellationToken); + private async Task SaveOptions(CancellationToken cancellationToken) { + using CancellationTokenSource cts = CreateLinkedTokenSource(cancellationToken); + cancellationToken = cts.Token; cts.CancelAfter(10_000); - await ASFFreeGamesOptionsLoader.Save(Options, cts.Token).ConfigureAwait(false); + await ASFFreeGamesOptionsLoader.Save(Options, cancellationToken).ConfigureAwait(false); } private SemaphoreSlim? SemaphoreSlim; @@ -156,6 +167,9 @@ private async Task SaveOptions() { #pragma warning restore CA1805 private async Task CollectGames(IEnumerable bots, ECollectGameRequestSource requestSource, CancellationToken cancellationToken = default) { + using CancellationTokenSource cts = CreateLinkedTokenSource(cancellationToken); + cancellationToken = cts.Token; + if (cancellationToken.IsCancellationRequested) { return 0; } diff --git a/ASFFreeGames/PluginContext.cs b/ASFFreeGames/PluginContext.cs index d8818a3..314012e 100644 --- a/ASFFreeGames/PluginContext.cs +++ b/ASFFreeGames/PluginContext.cs @@ -5,6 +5,6 @@ namespace Maxisoft.ASF; -internal readonly record struct PluginContext(IReadOnlyCollection Bots, IContextRegistry BotContexts, ASFFreeGamesOptions Options, LoggerFilter LoggerFilter, Lazy CancellationTokenLazy) { +internal readonly record struct PluginContext(IReadOnlyCollection Bots, IContextRegistry BotContexts, ASFFreeGamesOptions Options, LoggerFilter LoggerFilter, Lazy CancellationTokenLazy, bool Valid = false) { public CancellationToken CancellationToken => CancellationTokenLazy.Value; }