Skip to content

Plugins development zh CN

ArchiBot edited this page Apr 2, 2024 · 11 revisions

Plugins development

ASF 支持在运行时加载自定义插件。 插件允许您自定义 ASF 行为,例如添加自定义命令、自定义交易逻辑或者与其他第三方服务与 API 进行整体集成。

This page describes ASF plugins from developers perspective - creating, maintaining, publishing and likewise. If you want to view user's perspective, move here instead.


内核

Plugins are standard .NET libraries that define a class inheriting from common IPlugin interface declared in ASF. You can develop plugins entirely independently of mainline ASF and reuse them in current and future ASF versions, as long as internal ASF API remains compatible. Plugin system used in ASF is based on System.Composition, formerly known as Managed Extensibility Framework which allows ASF to discover and load your libraries during runtime.


新手指南

We've prepared ASF-PluginTemplate for you, which you can use as a base for your plugin project. Using the template is not a requirement (as you can do everything from scratch), but we heavily recommend to pick it up as it can drastically kickstart your development and cut on time required to get all things right. Simply check out the README of the template and it'll guide you further. Regardless, we'll cover the basics below in case you wanted to start from scratch, or get to understand better the concepts used in the plugin template - you don't typically need to do any of that if you've decided to use our plugin template.

Your project should be a standard .NET library targetting appropriate framework of your target ASF version, as specified in the compilation section.

The project must reference main ArchiSteamFarm assembly, either its prebuilt ArchiSteamFarm.dll library that you've downloaded as part of the release, or the source project (e.g. if you decided to add ASF tree as a submodule). This will allow you to access and discover ASF structures, methods and properties, especially core IPlugin interface which you'll need to inherit from in the next step. The project must also reference System.Composition.AttributedModel at the minimum, which allows you to [Export] your IPlugin for ASF to use. 此外,您可能还需要引用其他公共库,以便解析某些接口提供给您的数据结构,但除非您明确需要它们,否则现在这样就足够了。

If you did everything properly, your csproj will be similar to below:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Composition.AttributedModel" IncludeAssets="compile" Version="8.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="C:\\Path\To\ArchiSteamFarm\ArchiSteamFarm.csproj" ExcludeAssets="all" Private="false" />

    <!-- If building with downloaded DLL binary, use this instead of <ProjectReference> above -->
    <!-- <Reference Include="ArchiSteamFarm" HintPath="C:\\Path\To\Downloaded\ArchiSteamFarm.dll" /> -->
  </ItemGroup>
</Project>

From the code side, your plugin class must inherit from IPlugin interface (either explicitly, or implicitly by inheriting from more specialized interface, such as IASF) and [Export(typeof(IPlugin))] in order to be recognized by ASF during runtime. 实现该目标的最简示例如下:

using System;
using System.Composition;
using System.Threading.Tasks;
using ArchiSteamFarm;
using ArchiSteamFarm.Plugins;

namespace YourNamespace.YourPluginName;

[Export(typeof(IPlugin))]
public sealed class YourPluginName : IPlugin {
	public string Name => nameof(YourPluginName);
	public Version Version => typeof(YourPluginName).Assembly.GetName().Version;

	public Task OnLoaded() {
		ASF.ArchiLogger.LogGenericInfo("Meow");

		return Task.CompletedTask;
	}
}

要使用您的插件,就必须先进行编译。 您可以通过 IDE 进行此操作,也可以在您项目的根目录执行命令:

# 如果您的项目是独立的(不需要定义其名称,因为它是唯一的项目)
dotnet publish -c "Release" -o "out"

# 如果您的项目属于 ASF 代码树的一部分(防止编译不需要的部分)
dotnet publish YourPluginName -c "Release" -o "out"

然后,您的插件已经准备好进行部署。 It's up to you how exactly you want to distribute and publish your plugin, but we recommend creating a zip archive where you'll put your compiled plugin together with its dependencies. This way user will simply need to unpack your zip archive into a standalone subdirectory inside their plugins directory and do nothing else.

这只是开发插件最基本的场景。 We have ExamplePlugin project that shows you example interfaces and actions that you can do within your own plugin, including helpful comments. Feel free to take a look if you'd like to learn from a working code, or discover ArchiSteamFarm.Plugins namespace yourself and refer to the included documentation for all available options. We'll also further elaborate on some core concepts below to explain them better.

If instead of example plugin you'd want to learn from real projects, there are several official plugins developed by us, e.g. ItemsMatcher, MobileAuthenticator or SteamTokenDumper. In addition to that, there are also plugins developed by other developers, in our third-party section.


API 可用性

除了您有权在接口本身中访问的内容外,ASF 还向您公开了许多内部 API,您可以使用这些 API 来扩展功能。 例如,如果您想向 Steam Web 发送某种新请求,则无需从零开始实现一切,特别是无需自行处理我们已经处理过的问题。 Simply use our Bot.ArchiWebHandler which already exposes a lot of UrlWithSession() methods for you to use, handling all the lower-level stuff such as authentication, session refresh or web limiting for you. Likewise, for sending web requests outside of Steam platform, you could use standard .NET HttpClient class, but it's much better idea to use Bot.ArchiWebHandler.WebBrowser that is available for you, which once again offers you a helpful hand, for example in regards to retrying failed requests.

We have a very open policy in terms of our API availability, so if you'd like to make use of something that ASF code already includes, simply open an issue and explain in it your planned use case of our ASF's internal API. 只要您的使用场景有意义,我们就很可能不会反对。 This also includes all suggestions in regards to new IPlugin interfaces that could make sense to be added in order to extend existing functionality.

Regardless of ASF API availability however, nothing is stopping you from e.g. including Discord.Net library in your application and creating a bridge between your Discord bot and ASF commands, since your plugin can also have dependencies on its own. The possibilities are endless, and we made our best to give you as much freedom and flexibility as possible within your plugin, so there are no artificial limits on anything - your plugin is loaded into the main ASF process and can do anything that is realistically possible to do from within C# code.


API 兼容性

需要强调的是,ASF 是一个面向用户的应用程序,而不是一个您可以无条件依赖的、具有稳定 API 接口的库。 This means that you can't assume that your plugin once compiled will keep working with all future ASF releases regardless, it's simply impossible if we want to keep developing the program further, and being unable to adapt to ever-ongoing Steam changes for the sake of backwards compatibility is just not appropriate for our case. 这对您来说应该是符合逻辑的,但强调这个事实很重要。

We'll do our best to keep public parts of ASF working and stable, but we'll not be afraid to break the compatibility if good enough reasons arise, following our deprecation policy in the process. This is especially important in regards to internal ASF structures that are exposed to you as part of ASF infrastructure (e.g. ArchiWebHandler) which could be improved (and therefore rewritten) as part of ASF enhancements in one of the future versions. 我们会尽全力在更新日志中为您提供适当的说明,并且在运行时显示适当的与过时功能有关的警告。 我们不会故意为了重写而重写,因此您可以确信,下一个次要 ASF 版本不会仅仅因为版本号更高就让插件完全失效,但您最好时刻关注更新日志,并且偶尔实际验证是否一切都正常工作。


插件依赖项

Your plugin will include at least two dependencies by default, ArchiSteamFarm reference for internal API (IPlugin at the minimum), and PackageReference of System.Composition.AttributedModel that is required for being recognized as ASF plugin to begin with ([Export] clause). In addition to that, it may include more dependencies in regards to what you've decided to do in your plugin (e.g. Discord.Net library if you've decided to integrate with Discord).

The output of your build will include your core YourPluginName.dll library, as well as all the dependencies that you've referenced. Since you're developing a plugin to already-working program, you don't have to, and even shouldn't include dependencies that ASF already includes, for example ArchiSteamFarm, SteamKit2 or AngleSharp. 在构建中削减与 ASF 共享的依赖项并不是使插件运行所强制要求的,但这样做将极大地减少内存占用和插件本身的大小,同时提高性能,因为 ASF 会与您的插件共享自己的依赖项,并且只会加载它未知的库。

通常,建议的做法是,只打包 ASF 不包含的或者与 ASF 包含版本不同/不兼容的库。 Examples of those would be obviously YourPluginName.dll, but for example also Discord.Net.dll if you decided to depend on it, as ASF doesn't include it itself. Bundling libraries that are shared with ASF can still make sense if you want to ensure API compatibility (e.g. being sure that AngleSharp which you depend on in your plugin will always be in version X and not the one that ASF ships with), but obviously doing that comes for a price of increased memory/size and worse performance, and therefore should be carefully evaluated.

If you know that the dependency which you need is included in ASF, you can mark it with IncludeAssets="compile" as we showed you in the example csproj above. 这会告诉编译器不发布被引用的库,因为 ASF 已经包含该库。 Likewise, notice that we reference the ASF project with ExcludeAssets="all" Private="false" which works in a very similar way - telling the compiler to not produce any ASF files (as the user already has them). This applies only when referencing ASF project, since if you reference a dll library, then you're not producing ASF files as part of your plugin.

If you're confused about above statement and you don't know better, check which dll libraries are included in ASF-generic.zip package and ensure that your plugin includes only those that are not part of it yet. This will be only YourPluginName.dll for the most simple plugins. 如果您在运行时遇到某些库出现问题,也请一并打包受到影响的库。 如果一切尝试都失败,您仍然可以自行决定打包哪些内容。


本机依赖项

本机依赖项是作为特定操作系统构建的一部分生成的,因为此时宿主机上没有可用的 .NET 运行时环境,而 ASF 需要通过特定操作系统构建内自带的 .NET 运行时环境运行。 为了最大限度地减小构建大小,ASF 会削减其本机依赖项,使其仅包括程序中可能用到的代码,从而有效地减少运行时不会使用的部分。 This can create a potential problem for you in regards to your plugin, if suddenly you find out yourself in a situation where your plugin depends on some .NET feature that isn't being used in ASF, and therefore OS-specific builds can't execute it properly, usually throwing System.MissingMethodException or System.Reflection.ReflectionTypeLoadException in the process. As your plugin grows in size and becomes more and more complex, sooner or later you'll hit the surface that is not covered by our OS-specific build.

这对于 Generic 构建来说从来都不是问题,因为它们本来就不会自己处理本机依赖项(它们需要宿主机上的完整运行时环境来执行 ASF)。 This is why it's a recommended practice to use your plugin in generic builds exclusively, but obviously that has its own downside of cutting your plugin from users that are running OS-specific builds of ASF. 如果您想知道您遇到的问题是否与本机依赖有关,也可以使用这种方法来验证,即在 ASF Generic 构建中加载您的插件,看看它是否正常工作。 如果能,则说明您的插件依赖项已完备,问题的根源在于本机依赖项。

不幸的是,我们必须做出一个难以抉择的决定,一是在特定操作系统构建中打包完整的运行时环境,二是删除其中未被使用的部分,使构建能比完整版本减少超过 80 MB。 我们选择了后者,因而,您无法在您的插件中使用运行时环境缺失的功能。 If your project requires access to runtime features that are left out, you have to include full .NET runtime that you depend on, and that means running your plugin in generic ASF flavour. 您无法在特定操作系统版本中使用此插件,因为那些版本缺少您所需的运行时环境功能,而 .NET 运行时环境目前无法支持“合并”您自己额外提供的本机依赖项。 也许在将来某天会支持,但现在不可能做到。

ASF 特定操作系统包已包含运行我们官方插件的最基本功能。 除此之外,我们也涵盖到大多数基础插件所能用到的一些额外依赖。 因此,不是所有插件都需要担心本机依赖项——只有超出 ASF 与其官方插件功能的插件才需要考虑。 这是额外提供的,因为如果我们需要为我们自己的需求添加额外的本机依赖项,就可以直接把它们与 ASF 一同打包,使您可以更容易地使用它们。 很遗憾,这并不总能满足要求,随着插件变得越来越大、越来越复杂,用到被修剪掉功能的可能性就会增加。 Therefore, we usually recommend you to run your custom plugins in generic ASF flavour exclusively. 您仍然可以手动验证 ASF 的特定操作系统构建是否包含您插件所需的所有功能——但因为您的插件和 ASF 本身都在不断变化,这样的维护过程可能会非常困难。

Sometimes it may be possible to "workaround" missing features by either using alternative options or reimplementing them in your plugin. This is however not always possible or viable, especially if the missing feature comes from third-party dependencies that you include as part of your plugin. You can always try to run your plugin in OS-specific build and attempt to make it work, but it might become too much hassle in the long-run, especially if you want from your code to just work, rather than fight with ASF surface.


Automatic updates

ASF offers you two interfaces for implementing automatic updates in your plugin:

  • IGitHubPluginUpdates providing you easy way to implement GitHub-based updates similar to general ASF update mechanism
  • IPluginUpdates providing you lower-level functionality that allows for custom update mechanism, if you require something more complex

The minimum checklist of things that are required for updates to work:

  • You need to declare RepositoryName, which defines where the updates are pulled from.
  • You need to make use of tags and releases provided by GitHub. Tag must be in format parsable to a plugin version, e.g. 1.0.0.0.
  • Version property of the plugin must match tag that it comes from. This means that binary available under tag 1.2.3.4 must present itself as 1.2.3.4.
  • Each tag should have appropriate release available on GitHub with zip file release asset that includes your plugin binary files. The binary files should be available in the root directory within the zip file.

This will allow the mechanism in ASF to:

  • Resolve current Version of your plugin, e.g. 1.0.1.
  • Use GitHub API to fetch latest tag available in RepositoryName repo, e.g. 1.0.2.
  • Determine that 1.0.2 > 1.0.1, check release of 1.0.2 tag to find .zip file with the plugin update.
  • Download that .zip file, extract it, and put its content in the directory that included YourPlugin.dll before.
  • Restart ASF to load your plugin in 1.0.2 version.

Additional notes:

  • Plugin updates require appropriate ASF config values, namely PluginsUpdateMode and/or PluginsUpdateList.
  • Our plugin template already includes everything you need, simply rename the plugin to proper values, and it'll work out of the box.
  • You can use combination of latest release as well as pre-releases, they'll be used as per UpdateChannel the user has defined.
  • There is CanUpdate boolean property you can override for disabling/enabling plugins update on your side, for example if you want to skip updates temporarily, or through any other reason.
  • It's possible to have more than one zip file in the release if you want to target different ASF versions. See GetTargetReleaseAsset() remarks, this allows you to have e.g. MyPlugin-V6.1.zip as well as MyPlugin-V6.2.zip and support both ASF V6.1.x.x as well as V6.2.x.x at once.
  • If the above is not sufficient to you and you require custom logic for picking the correct .zip file, you can override GetTargetReleaseAsset() function and provide the artifact for ASF to use.
  • If your plugin needs to do some extra work before/after update, you can override OnPluginUpdateProceeding() and/or OnPluginUpdateFinished() methods.

This interface allows you to implement custom logic for updates if by any reason IGithubPluginUpdates is not sufficient to you, for example because you have tags that do not parse to versions, or because you're not using GitHub platform at all.

There is a single GetTargetReleaseURL() function for you to override, which expects from you Uri? of target plugin update location. ASF provides you some core variables that relate to the context the function was called with, but other than that, you're responsible to implement everything you need in that function and provide ASF the URL that should be used, or null if the update is not available.

Other than that, it's similar to GitHub updates, where the URL should point to a .zip file with the binary files available in the root directory. You also have OnPluginUpdateProceeding() and OnPluginUpdateFinished() methods available.

Clone this wiki locally