Intro
SDK in Playnite 11 has been reworked in significant ways. I describe major changes bellow, including some general recommendations when it comes to porting from P10.
This is not full documentation for the SDK. A lot of stuff has changed and probably will change based on your feedback, so I didn't write detailed documentation for everything SDK can do in P11.
Also nothing that's currently done in P11 and the SDK is set in store, I'm willing to change a lot based on feedback from you.
Small FAQ before we continue
Will there be another major/breaking change to the SDK?
UI library will likely change in P12. P11's UI is still implemented using WPF. I experimented with Avalonia, but for numerous reasons I decided it's not ready for our use case yet. They are for sure going in right direction for us, with opening previously paid components, improving docs and tooling and making good changes in Avalonia 12.
Playnite 12 is currently planned to be multi-platform release and it will be based on Avalonia. Switch to new UI layer is obviously a breaking change. That said, I purposely designed current SDK to make this switch as painless as possible. In theory, only the actual XAML views and things that directly interact with it outside of standard MVVM binding pattern, will need to be ported.
Avalonia still uses XAML based markup, as WPF, which is semi compatible with WPF. My personal estimation is that about 60% of WPF XAML on average could be directly copied over.
I do not plan and really do not want to do any other breaking changes in future releases. Add-ons are really important part of Playnite project and its community, so I really don't want to disturb this much going into the future.
Is there any automated way to port existing plugins to P11?
There isn't and nothing is planned when it comes to this. When you read list of changes, you'll understand why.
Where do I upload my plugins?
New portal for extensions and themes is not ready yet. You'll have to distribute .pext2 files on your own for now.
Where to send feedback, ask for help and discuss SDK changes?
Preferably on Discord please, in #extensions-devel-playnite11. If you hate Discord, you can open new "Feedback or question" issue in our new repository on Codeberg.
Breaking changes
.NET runtime
Playnite 11 runs on .NET 10, as opposed to .NET Framework 4.6.2 previously. What this means for you depends on what APIs from .NET you are using and also what 3rd party dependencies you are using.
I can't really give any advice here, but from my experience, there's not that many APIs that were removed or changed without providing any built-in alternative. There were also improvements that allows you to remove some 3rd party packages completely, like for example JSON serialization. .NET's built-in JSON library is very good now.
Nullable reference types
SDK and Playnite is now compiled with nullable reference types enabled. Plugin template also come with this enabled. You should 100% have this enabled in your plugins. This will force you get rid of the most prevalent runtime bug in probably all existing .NET code out there: NullReferenceException
You will probably get a lot of warnings on existing code and will be required add write extra null checks, but it's totally worth it, trust me. I also recommend configuring .editorconfig file to change nullable warnings into errors.
No event handlers
ItemCollectionChanged and ItemUpdated library events have been removed, as well as any other events it the SDK. The SDK now uses callback style pattern where specific plugin callback methods are called when library data change occurs.
For example, if you want to listen to changes does to game objects, you would define OnGameStateChangedAsync method override on your plugin class and Playnite will call this method when a change in game object collection occurs:
public override async Task OnGameStateChangedAsync(GameStateChangedArgs args)
{
// GameStateChangedArgs contains all changes, added, removed and updated entires
}
BufferedUpdate removed
The option to make bulk changes to library data using BufferedUpdate method was removed. This was flawed system that allowed plugins to completely break data change propagation to the UI when not used correctly. You will now need to call specific overrides for library methods that accept multiple items if you want to make changes to multiple items at once.
There's now also MakeBulkChangesAsync method that allows you to make add, remove and update methods in bulk with one call. As well all UpdateAsync overrides that take Func<T, bool> updateAction delegate, allowing you to do mass changes on select items in a collection with one without first enumerating all changes in advance, and in one call.
Async
A lot of methods and callbacks are now designed for use via asynchronous async/await pattern. This was necessary change because a lot of modern .NET APIs either come with async versions only, or async method version use is highly recommended.
This is probably the biggest breaking change that will proliferate through out your entire code base, because of how "viral" async is pattern is. Every method that calls async method should be async as well and should be awaiting all calls. You should always await asynchronous method calls! Exceptions can apply of course (not related to our SDK), but take this as general rule.
This might look scary at first if you never used it, and there are some footguns, but what it means in general for you is this:
When calling async method from the SDK, you will have to await it by using await keyword:
await Dialogs.ShowErrorMessageAsync("test message");
And since you can call await only from withing async method, you will need to define your method signatures as async as well:
public async Task MyMethod()
{
await Dialogs.ShowErrorMessageAsync("test message");
}
If it's a method that returns a value:
public override async Task<InstallController> GetInstallActionAsync(GetInstallActionArgs args)
{
return await GetInstallActionAsync(args);
}
One typical scenario is that you may have Playnite calling your async method, but you may not have any "await" work to be done there. In which case the compiler will probably scream at you that the method will run synchronously. There are multiple ways how to deal with it:
- ignore/suppress the warning (it's generally non-issue in our use case in these override methods)
- await completed task somewhere in the method: "await Task.CompletedTask;"
- return completed task from the method:
return Task.CompletedTask;or using Task.FromResult() if the method returns a value.
I personally recommend one of the first two options. Here's good blog post explaining why the third option might not be a good idea: https://blog.stephencleary.com/2016/12/eliding-async-await.html
Dependency loading
Given how modern .NET runtime works, Playnite no longer has the limitation of loading only one version of specific package/assembly for all plugins. If multiple plugins referenced different version of one package or different version compared to what Playnite uses, things could break in numerous ways.
This is no longer the case in P11. With exceptions being Playnite SDK package and its dependencies: ByteAether.Ulid, CommunityToolkit.Mvvm and Generator.Equals. Playnite will always force those to be loaded from Playnite's main assembly context.
This has also one negative side effect and that's in case where multiple plugins would want to exchange data between each other. This would mean using some shared assembly, and that would currently not work, even when using the same assembly version. I will be adding explicit API for this in beta 1, to make sure assembly sharing between plugins is working.
Removed non-Playnite features
Some features that were not directly related to Playnite were removed from the SDK. For example serialization APIs, SQLite API etc. This should not be an issue, since you can now directly use 3rd party packages for these things, without worrying about compatibility issues (see dependency loading section), and also .NET 10 itself now has good APIs you could use instead (in case of JSON serialization for example).
Web view data separation
Each plugin now has separate web view data store (profile), isolated from other plugins. This means that plugins can't break web view sessions of other plugins. But it also means that plugins that used web session data from other plugins, will have to reimplement that functionality specifically for their plugin.
For example, some Steam plugins use web login session made by Steam library integration plugin in P10. These will now need to implement Steam auth explicitly.
There are no plugin types
There are no more separate generic, library and metadata plugin types, one plugin can be all three. This is done by configuring LibrarySettings and MetadataSettings properties on your plugin class.
In practice this means that for example Steam integration plugin can now works also as generic metadata plugin providing metadata for non-Steam games. Instead of having to have separate metadata plugin for non-Steam games.
Localizations no longer use XAML resource files
Built-in localization support switched to Fluent format, which is specifically designed for localizations use and has features things like plural variants.
Plugin initialization procedure changed
There's now an explicit method called InitializeAsync that you should be using to initialize your plugin resources from. This is also where Playnite API instance is passed to your plugin. Only initialize resources in plugin constructor if they are resources independent on any Playnite functionality.
There's also OnApplicationStartupAsync callback method when the app is in "initialized" state, for when you need to do stuff after the UI is loaded.
Each plugin has its own log file
Stored in a folder designated for extensions to store user data in. The same folder that exists in P10. Can be got from the API via UserDataDir property.
Data models changes
There's been numerous changes to built-in data model. Some examples:
- IDs of objects in library collections are simple strings now. It allows more flexibility when storing data by arbitrary ID values.
- Things like scripts, descriptions and notes are now stored in their separate collection and must be loaded on demand.
- Date fields switched from
DateTimetoDateTimeOffset. - Added new fields, like individual games sessions, time to beat data, web link types.
Plugin ID handling changes
There's now only one plugin ID. There's no longer a system where there's manifest defined ID and Guid ID used by actual game models to reference your plugin. The manifest string ID is used for everything.
New features
Added new app event callbacks
For example there are now event for when game startup was cancelled, when specific library update was started, when metadata download was started and stopped, games merged and unmerged etc.
Plugins can now provide custom functionality related to library data
This means ability to implement custom:
- Game edit views. You can create new section on game edit dialog.
- Filtering, sorting, grouping, exploring.
- Metadata download "fields". This includes support for any metadata plugin to provide data to your plugin during metadata downloads.
- Game data "presenters". This is replacement for old system where plugins could provide custom controls for themes to integrate. This is technically already in the alpha, but theming system that would make use of it isn't yet.
Improvements to menu system
There's been a lot of changes to how various menus work. It should be now easier to make nested menus for example, including support for dynamically loading sub menus on demand. Also ability to add items to system tray menu and "add game" menu was added.
The whole system is also configurable by user, they can now choose what items should be displayed and in what order.
Sidebar items improvements
Sidebar items got similar improvements to the menu system. There's now built-in support for creating items with context menus and you can create completely arbitrary looking items. For example, built-in search box on the top bar is implemented using this API as a sidebar item.
And similarly to the menu system, this is controlled by a user where and what items will be shown.
Background tasks support
There's now new API for running background tasks. This is used now by Playnite itself to run things like library updates and metadata downloads.
For now, you can only queue new tasks, queue of how/when stuff is started is controlled by Playnite and user. I'm not sure if would be wise to give plugins control over the queue, so I'll leave that up for discussion. Let me know if you can think of some valid use cases for plugins to have this much control over the queue.
Blocking progress dialog is still available in dialogs API as before.
More control was given to library plugins over certain things
For example, you can implement your own functionality for calculating installed game size.
API for data storage
Library API now has support for arbitrary data collections backed by SQLite. This can be used to store any arbitrary data in as many collections (tables) as you want. The data is stored in one SQLite per plugin and serialized using JSON. The API is the same a for built-in game collections.
As an example, GOG plugin now uses it to store information about whether each game should be run via Galaxy or not, instead of having one global option.
Support for custom diag. data collection
You can now implement custom functionality for diagnostics info collection. There's now new button in addons view that users can click to call this and it's also used on crash dialogs initiated by plugins.
The default implementation collects extension log, if you don't provide your own implementation.
APIs for cross plugin communication
CallPluginAsync and GenerateCallbackAsync methods were added as a communication channel between plugins. This is fairly simple for now, but will be expanded in future. It also has some limitations when it comes to exchanging custom data types mentioned in dependency loading section, which will be addressed in beta builds.
How to get started
I recommend creating new plugin using Toolbox and starting moving things into it from your existing plugins, rather than the other way around.
# Toolbox now takes author name as well since default plugin ID is generated from author and plugin name, rather than nonsense UUID
Toolbox.exe new plugin "Crow" "Testing Plugin" c:\some\directory
The template comes with recommended project settings and necessary things like custom nuget.config because SDK is now being hosted on our own Nuget feed. The template also has examples for many new features and API changes. I recommend going through all included classes. ExamplePlugin generated from this template is also available on Codeberg, in case you want to check code examples that way.
Specific features implementation
I recommend looking at example plugins below for now. Especially ExamplePlugin, which is decently documented and contains various examples for new APIs and changes. You'll get the same from the Toolbox template.
I would prefer to continue updating these with examples of API use, rather than writing detailed docs, when the API is still subject to change. Also happy to answer any questions on Discord or Codeberg.
There are also automatically generated docs for SDK classes here, but they are not very well annotated with comments right now.
Example plugins
Sources for my plugins that currently ship with P11 alpha can be found on Codeberg.
Toolbox
Toolbox stays largely unchanged. It can be now downloaded and run independently of any Playnite install. So you can more easily integrate it in your workflow. It only requires .NET runtime to be installed, but you will need that anyways to be able to compile plugins. The latest version will be always available at http://playnite.link/download/Toolbox.exe.