Plugins breaking: Convert all synchronous interface methods to Task

Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead.

Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal.

This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit().

Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things:

- If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those.
- If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic.
- Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down.

All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead.

This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net.

You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
This commit is contained in:
Archi 2021-12-08 16:52:27 +01:00
parent 3eae143c55
commit 0eab358af9
No known key found for this signature in database
GPG key ID: 6B138B4C64555AEA
15 changed files with 86 additions and 56 deletions

View file

@ -58,9 +58,9 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, I
// Thanks to that, you can extend default ASF config with your own stuff, then parse it here in order to customize your plugin during runtime
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// In addition to that, this method also guarantees that all plugins were already OnLoaded(), which allows cross-plugins-communication to be possible
public void OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
public Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
if (additionalConfigProperties == null) {
return;
return Task.CompletedTask;
}
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
@ -73,6 +73,10 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, I
break;
}
}
// ASF interface methods usually expect a Task as a return value, this allows you to optionally implement async operations in your functions (with async Task function signature)
// If your method does not implement any async operations (is fully synchronous), you could in theory still mark it as async, but a better idea is to just return Task.CompletedTask from it, like here
return Task.CompletedTask;
}
// This method is called when unknown command is received (starting with CommandPrefix)
@ -101,12 +105,12 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, I
// You should ensure that all of your references to this bot instance are cleared - most of the time this is anything you created in OnBotInit(), including deep roots in your custom modules
// This doesn't have to be done immediately (e.g. no need to cancel existing work), but it should be done in timely manner when everything is finished
// Doing so will allow the garbage collector to dispose the bot afterwards, refraining from doing so will create a "memory leak" by keeping the reference alive
public void OnBotDestroy(Bot bot) { }
public Task OnBotDestroy(Bot bot) => Task.CompletedTask;
// This method is called when bot is disconnected from Steam network, you may want to use this info in some kind of way, or not
// ASF tries its best to provide logical reason why the disconnection has happened, and will use EResult.OK if the disconnection was initiated by us (e.g. as part of a command)
// Still, you should take anything other than EResult.OK with a grain of salt, unless you want to assume that Steam knows why it disconnected us (hehe, you bet)
public void OnBotDisconnected(Bot bot, EResult reason) { }
public Task OnBotDisconnected(Bot bot, EResult reason) => Task.CompletedTask;
// This method is called when bot receives a friend request or group invite that ASF isn't willing to accept
// It allows you to generate a response whether ASF should accept it (true) or proceed like usual (false)
@ -117,10 +121,12 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, I
// This method is called at the end of Bot's constructor
// You can initialize all your per-bot structures here
// In general you should do that only when you have a particular need of custom modules or alike, since ASF's plugin system will always provide bot to you as a function argument
public void OnBotInit(Bot bot) {
public Task OnBotInit(Bot bot) {
// Apart of those two that are already provided by ASF, you can also initialize your own logger with your plugin's name, if needed
bot.ArchiLogger.LogGenericInfo($"Our bot named {bot.BotName} has been initialized, and we're letting you know about it from our {nameof(ExamplePlugin)}!");
ASF.ArchiLogger.LogGenericWarning("In case we won't have a bot reference or have something process-wide to log, we can also use ASF's logger!");
return Task.CompletedTask;
}
// This method, apart from being called during bot modules initialization, allows you to read custom bot config properties that are not recognized by ASF
@ -128,8 +134,7 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, I
// Keep in mind that, as noted in the interface, additionalConfigProperties can be null if no custom, unrecognized properties are found by ASF, you should handle that case appropriately
// Also keep in mind that this function can be called multiple times, e.g. when user edits his bot configs during runtime
// Take a look at OnASFInit() for example parsing code
public async void OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
// ASF marked this message as synchronous, in case we have async code to execute, we can just use async void return
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
// For example, we'll ensure that every bot starts paused regardless of Paused property, in order to do this, we'll just call Pause here in InitModules()
// Thanks to the fact that this method is called with each bot config reload, we'll ensure that our bot stays paused even if it'd get unpaused otherwise
bot.ArchiLogger.LogGenericInfo("Pausing this bot as asked from the plugin");
@ -137,7 +142,7 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, I
}
// This method is called when the bot is successfully connected to Steam network and it's a good place to schedule any on-connected tasks, as AWH is also expected to be available shortly
public void OnBotLoggedOn(Bot bot) { }
public Task OnBotLoggedOn(Bot bot) => Task.CompletedTask;
// This method is called when bot receives a message that is NOT a command (in other words, a message that doesn't start with CommandPrefix)
// Normally ASF entirely ignores such messages as the program should not respond to something that isn't recognized
@ -174,8 +179,10 @@ internal sealed class ExamplePlugin : IASF, IBot, IBotCommand, IBotConnection, I
// If you do not have any global structures to initialize, you can leave this function empty
// At this point you can access core ASF's functionality, such as logging, but more advanced structures (like ASF's WebBrowser) will be available in OnASFInit(), which itself takes place after every plugin gets OnLoaded()
// Typically you should use this function only for preparing core structures of your plugin, and optionally also sending a message to the user (e.g. support link, welcome message or similar), ASF-specific things should usually happen in OnASFInit()
public void OnLoaded() {
public Task OnLoaded() {
ASF.ArchiLogger.LogGenericInfo($"Hey! Thanks for checking if our example plugin works fine, this is a confirmation that indeed {nameof(OnLoaded)}() method was called!");
ASF.ArchiLogger.LogGenericInfo("Good luck in whatever you're doing!");
return Task.CompletedTask;
}
}

View file

@ -24,6 +24,7 @@ using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Runtime;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Plugins.Interfaces;
@ -41,7 +42,7 @@ internal sealed class PeriodicGCPlugin : IPlugin {
public Version Version => typeof(PeriodicGCPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
public void OnLoaded() {
public Task OnLoaded() {
TimeSpan timeSpan = TimeSpan.FromSeconds(GCPeriod);
ASF.ArchiLogger.LogGenericWarning($"Periodic GC will occur every {timeSpan.ToHumanReadable()}. Please keep in mind that this plugin should be used for debugging tests only.");
@ -49,6 +50,8 @@ internal sealed class PeriodicGCPlugin : IPlugin {
lock (LockObject) {
PeriodicGCTimer.Change(timeSpan, timeSpan);
}
return Task.CompletedTask;
}
private static void PerformGC(object? state = null) {

View file

@ -62,7 +62,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(Config?.Enabled == true ? GlobalCache?.LastChangeNumber ?? 0 : 0);
public async void OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
public async Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
if (!SharedInfo.HasValidToken) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledMissingBuildToken, nameof(SteamTokenDumperPlugin)));
@ -100,8 +100,6 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
config.Enabled = true;
}
Config = config;
if (!config.Enabled) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
@ -132,6 +130,8 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
}
}
Config = config;
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
@ -144,7 +144,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginInitializedAndEnabled, nameof(SteamTokenDumperPlugin), startIn.ToHumanReadable()));
}
public async void OnBotDestroy(Bot bot) {
public async Task OnBotDestroy(Bot bot) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
@ -160,7 +160,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
}
}
public async void OnBotInit(Bot bot) {
public async Task OnBotInit(Bot bot) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
@ -179,7 +179,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
}
}
public void OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
if (bot == null) {
throw new ArgumentNullException(nameof(bot));
}
@ -193,7 +193,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
}
if (Config is not { Enabled: true }) {
return;
return Task.CompletedTask;
}
subscription = callbackManager.Subscribe<SteamApps.LicenseListCallback>(callback => OnLicenseList(bot, callback));
@ -201,13 +201,19 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
if (!BotSubscriptions.TryAdd(bot, subscription)) {
subscription.Dispose();
}
return Task.CompletedTask;
}
public IReadOnlyCollection<ClientMsgHandler>? OnBotSteamHandlersInit(Bot bot) => null;
public Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlersInit(Bot bot) => Task.FromResult((IReadOnlyCollection<ClientMsgHandler>?) null);
public override void OnLoaded() => Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
public override Task OnLoaded() {
Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
public void OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
return Task.CompletedTask;
}
public Task OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
}
@ -221,7 +227,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
}
if (Config is not { Enabled: true }) {
return;
return Task.CompletedTask;
}
if (GlobalCache == null) {
@ -229,15 +235,17 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
}
GlobalCache.OnPICSChanges(currentChangeNumber, appChanges);
return Task.CompletedTask;
}
public void OnPICSChangesRestart(uint currentChangeNumber) {
public Task OnPICSChangesRestart(uint currentChangeNumber) {
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
}
if (Config is not { Enabled: true }) {
return;
return Task.CompletedTask;
}
if (GlobalCache == null) {
@ -245,6 +253,8 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotS
}
GlobalCache.OnPICSChangesRestart(currentChangeNumber);
return Task.CompletedTask;
}
private static async void OnBotRefreshTimer(object? state) {

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -32,5 +33,5 @@ public interface IASF : IPlugin {
/// ASF will call this method right after global config initialization.
/// </summary>
/// <param name="additionalConfigProperties">Extra config properties made out of <see cref="JsonExtensionDataAttribute" />. Can be null if no extra properties are found.</param>
void OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null);
Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null);
}

View file

@ -19,6 +19,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
using JetBrains.Annotations;
@ -32,12 +33,12 @@ public interface IBot : IPlugin {
/// Doing so will allow the garbage collector to dispose the bot afterwards, refraining from doing so will create a "memory leak" by keeping the reference alive.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotDestroy(Bot bot);
Task OnBotDestroy(Bot bot);
/// <summary>
/// ASF will call this method after creating the bot object, e.g. after config creation.
/// Bot config is not yet available at this stage. This function will execute only once for every bot object.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotInit(Bot bot);
Task OnBotInit(Bot bot);
}

View file

@ -19,6 +19,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
using JetBrains.Annotations;
@ -31,17 +32,17 @@ public interface IBotCardsFarmerInfo : IPlugin {
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="farmedSomething">Bool value indicating whether the module has finished successfully, so when there was at least one card to drop, and nothing has interrupted us in the meantime.</param>
void OnBotFarmingFinished(Bot bot, bool farmedSomething);
Task OnBotFarmingFinished(Bot bot, bool farmedSomething);
/// <summary>
/// ASF will call this method when cards farming module is started on given bot instance. The module is started only when there are valid cards to drop, so this method won't be called when there is nothing to idle.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotFarmingStarted(Bot bot);
Task OnBotFarmingStarted(Bot bot);
/// <summary>
/// ASF will call this method when cards farming module is stopped on given bot instance. The stop could be a result of a natural finish, or other situations (e.g. Steam networking issues, user commands).
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotFarmingStopped(Bot bot);
Task OnBotFarmingStopped(Bot bot);
}

View file

@ -19,6 +19,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
using JetBrains.Annotations;
using SteamKit2;
@ -32,11 +33,11 @@ public interface IBotConnection : IPlugin {
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="reason">Reason for disconnection, or <see cref="EResult.OK" /> if the disconnection was initiated by ASF (e.g. as a result of a command).</param>
void OnBotDisconnected(Bot bot, EResult reason);
Task OnBotDisconnected(Bot bot, EResult reason);
/// <summary>
/// ASF will call this method when bot successfully connects to Steam network.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
void OnBotLoggedOn(Bot bot);
Task OnBotLoggedOn(Bot bot);
}

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System.Collections.Generic;
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
using JetBrains.Annotations;
using Newtonsoft.Json;
@ -34,5 +35,5 @@ public interface IBotModules : IPlugin {
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="additionalConfigProperties">Extra config properties made out of <see cref="JsonExtensionDataAttribute" />. Can be null if no extra properties are found.</param>
void OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null);
Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null);
}

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System.Collections.Generic;
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
using JetBrains.Annotations;
using SteamKit2;
@ -33,12 +34,12 @@ public interface IBotSteamClient : IPlugin {
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="callbackManager">Callback manager object which can be used for establishing subscriptions to standard and custom callbacks.</param>
void OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager);
Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager);
/// <summary>
/// ASF will call this method right after bot initialization in order to allow you hooking custom SK2 client handlers into the SteamClient.
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <returns>Collection of custom client handlers that are supposed to be hooked into the SteamClient by ASF. If you do not require any, just return null or empty collection.</returns>
IReadOnlyCollection<ClientMsgHandler>? OnBotSteamHandlersInit(Bot bot);
Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlersInit(Bot bot);
}

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System.Collections.Generic;
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Exchange;
using JetBrains.Annotations;
@ -33,5 +34,5 @@ public interface IBotTradeOfferResults : IPlugin {
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="tradeResults">Trade results related to this callback.</param>
void OnBotTradeOfferResults(Bot bot, IReadOnlyCollection<ParseTradeResult> tradeResults);
Task OnBotTradeOfferResults(Bot bot, IReadOnlyCollection<ParseTradeResult> tradeResults);
}

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System.Collections.Generic;
using System.Threading.Tasks;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Integration.Callbacks;
using JetBrains.Annotations;
@ -33,5 +34,5 @@ public interface IBotUserNotifications : IPlugin {
/// </summary>
/// <param name="bot">Bot object related to this callback.</param>
/// <param name="newNotifications">Collection containing those notification types that are new (that is, when new count > previous count of that notification type).</param>
void OnBotUserNotifications(Bot bot, IReadOnlyCollection<UserNotificationsCallback.EUserNotification> newNotifications);
Task OnBotUserNotifications(Bot bot, IReadOnlyCollection<UserNotificationsCallback.EUserNotification> newNotifications);
}

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json;
@ -45,5 +46,5 @@ public interface IPlugin {
/// <summary>
/// ASF will call this method right after plugin initialization.
/// </summary>
void OnLoaded();
Task OnLoaded();
}

View file

@ -31,7 +31,7 @@ public interface ISteamPICSChanges : IPlugin {
/// <summary>
/// ASF uses this method for determining the point in time from which it should keep history going upon a restart. The actual point in time that will be used is calculated as the lowest change number from all loaded plugins, to guarantee that no plugin will miss any changes, while allowing possible duplicates for those plugins that were already synchronized with newer changes. If you don't care about persistent state and just want to receive the ongoing history, you should return 0 (which is equal to "I'm fine with any"). If there won't be any plugin asking for a specific point in time, ASF will start returning entries since the start of the program.
/// </summary>
/// <returns>The most recent change number from which you're fine to receive <see cref="OnPICSChanges" /></returns>
/// <returns>The most recent change number from which you're fine to receive <see cref="OnPICSChanges" />.</returns>
Task<uint> GetPreferredChangeNumberToStartFrom();
/// <summary>
@ -40,11 +40,11 @@ public interface ISteamPICSChanges : IPlugin {
/// <param name="currentChangeNumber">The change number of current callback.</param>
/// <param name="appChanges">App changes that happened since the previous call of this method. Can be empty.</param>
/// <param name="packageChanges">Package changes that happened since the previous call of this method. Can be empty.</param>
void OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges);
Task OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges);
/// <summary>
/// ASF will call this method when it'll be necessary to restart the history of PICS changes. This can happen due to Steam limitation in which we're unable to keep history going if we're too far behind (approx 5k changeNumbers). If you're relying on continuous history of app/package PICS changes sent by <see cref="OnPICSChanges" />, ASF can no longer guarantee that upon calling this method, therefore you should start clean.
/// </summary>
/// <param name="currentChangeNumber">The change number from which we're restarting the PICS history.</param>
void OnPICSChangesRestart(uint currentChangeNumber);
Task OnPICSChangesRestart(uint currentChangeNumber);
}

View file

@ -20,6 +20,7 @@
// limitations under the License.
using System;
using System.Threading.Tasks;
using ArchiSteamFarm.Plugins.Interfaces;
namespace ArchiSteamFarm.Plugins;
@ -27,7 +28,7 @@ namespace ArchiSteamFarm.Plugins;
internal abstract class OfficialPlugin : IPlugin {
public abstract string Name { get; }
public abstract Version Version { get; }
public abstract void OnLoaded();
public abstract Task OnLoaded();
internal bool HasSameVersion() => Version == SharedInfo.Version;
}

View file

@ -260,7 +260,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IASF>().Select(plugin => Task.Run(() => plugin.OnASFInit(additionalConfigProperties)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IASF>().Select(plugin => plugin.OnASFInit(additionalConfigProperties))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -310,7 +310,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBot>().Select(plugin => Task.Run(() => plugin.OnBotDestroy(bot)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBot>().Select(plugin => plugin.OnBotDestroy(bot))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -326,7 +326,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotConnection>().Select(plugin => Task.Run(() => plugin.OnBotDisconnected(bot, reason)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotConnection>().Select(plugin => plugin.OnBotDisconnected(bot, reason))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -342,7 +342,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotCardsFarmerInfo>().Select(plugin => Task.Run(() => plugin.OnBotFarmingFinished(bot, farmedSomething)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotCardsFarmerInfo>().Select(plugin => plugin.OnBotFarmingFinished(bot, farmedSomething))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -358,7 +358,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotCardsFarmerInfo>().Select(plugin => Task.Run(() => plugin.OnBotFarmingStarted(bot)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotCardsFarmerInfo>().Select(plugin => plugin.OnBotFarmingStarted(bot))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -374,7 +374,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotCardsFarmerInfo>().Select(plugin => Task.Run(() => plugin.OnBotFarmingStopped(bot)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotCardsFarmerInfo>().Select(plugin => plugin.OnBotFarmingStopped(bot))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -416,7 +416,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBot>().Select(plugin => Task.Run(() => plugin.OnBotInit(bot)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBot>().Select(plugin => plugin.OnBotInit(bot))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -432,7 +432,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotModules>().Select(plugin => Task.Run(() => plugin.OnBotInitModules(bot, additionalConfigProperties)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotModules>().Select(plugin => plugin.OnBotInitModules(bot, additionalConfigProperties))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -448,7 +448,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotConnection>().Select(plugin => Task.Run(() => plugin.OnBotLoggedOn(bot)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotConnection>().Select(plugin => plugin.OnBotLoggedOn(bot))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -498,7 +498,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotSteamClient>().Select(plugin => Task.Run(() => plugin.OnBotSteamCallbacksInit(bot, callbackManager)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotSteamClient>().Select(plugin => plugin.OnBotSteamCallbacksInit(bot, callbackManager))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -516,7 +516,7 @@ internal static class PluginsCore {
IList<IReadOnlyCollection<ClientMsgHandler>?> responses;
try {
responses = await Utilities.InParallel(ActivePlugins.OfType<IBotSteamClient>().Select(plugin => Task.Run(() => plugin.OnBotSteamHandlersInit(bot)))).ConfigureAwait(false);
responses = await Utilities.InParallel(ActivePlugins.OfType<IBotSteamClient>().Select(plugin => plugin.OnBotSteamHandlersInit(bot))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
@ -566,7 +566,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotTradeOfferResults>().Select(plugin => Task.Run(() => plugin.OnBotTradeOfferResults(bot, tradeResults)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotTradeOfferResults>().Select(plugin => plugin.OnBotTradeOfferResults(bot, tradeResults))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -586,7 +586,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<IBotUserNotifications>().Select(plugin => Task.Run(() => plugin.OnBotUserNotifications(bot, newNotifications)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<IBotUserNotifications>().Select(plugin => plugin.OnBotUserNotifications(bot, newNotifications))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -610,7 +610,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<ISteamPICSChanges>().Select(plugin => Task.Run(() => plugin.OnPICSChanges(currentChangeNumber, appChanges, packageChanges)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<ISteamPICSChanges>().Select(plugin => plugin.OnPICSChanges(currentChangeNumber, appChanges, packageChanges))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
@ -626,7 +626,7 @@ internal static class PluginsCore {
}
try {
await Utilities.InParallel(ActivePlugins.OfType<ISteamPICSChanges>().Select(plugin => Task.Run(() => plugin.OnPICSChangesRestart(currentChangeNumber)))).ConfigureAwait(false);
await Utilities.InParallel(ActivePlugins.OfType<ISteamPICSChanges>().Select(plugin => plugin.OnPICSChangesRestart(currentChangeNumber))).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}