stride3d / stride

Stride (formerly Xenko), a free and open-source cross-platform C# game engine.
https://stride3d.net
MIT License
6.64k stars 957 forks source link

Plugin system RFC #1120

Open manio143 opened 3 years ago

manio143 commented 3 years ago

Plugin system RFC

A long await solution to #13. The goal of this document is to provide a description of a plugin system, both in terms of usability and user experience and in terms of steps necessary for its implementation. The discussion underneath should result in materializing a concrete task list which will be implemented over time.

TL;DR

Let's include a Plugins property to Package definition that defines additional assemblies loaded by GameStudio and AssetCompiler. And a bunch of other things to help write/use plugins. It sounds simple but there's a lot of work behind the scenes to make it happen.

Motivation

Stride offers a lot out of the box, however, there's many instances where a user may want to execute custom actions in the GameStudio to help their tasks, have custom assets and custom editors, without necessarily having to recompile Stride from sources. Moreover, allowing plugins would enable Stride users to share their custom code without having to extend the engine in the main repo, or doing painful merges of forks. Also, a plugin oriented architecture should allow for easier development down the line.

Extensibility points

It's important to highlight areas that extensibility will focus around.

Runtime

Runtime libraries, i.e. libraries with components, processors and systems are already in a decent state when it comes to extensibility. Previous work has been done to enable e.g. Stride.Physics, Stride.Video to depend on Stride.Engine and be optional for the user to include. Following their pattern users can create new runtime libraries, inclusion of which will allow their code to be executed. Therefore, this document will not look into further extensibility points to the runtime.

Asset pipeline

The assets related projects (assets compiler and its dependencies) have been designed in a modular way that allows for creating custom assets and custom assets compilers. I've previously shown to be able to create custom assets (here), however, it required my game to take a dependency on Stride.Core.Assets, which currently is compiled under StrideEditorTargetFramework (windows only). The later outlined idea of design projects allows to skip fiddling with the build system and greater cross-platform compatibility.

Editor

The Stride GameStudio is the central point for any asset related operations. It has the most potential for extensibility and is also the least familiar to the users in terms of its internals. The following are some things we would want to enable in plugins:

The good news is: there already is an extensibility point for most of those things - StrideAssetsPlugin allows to explicitly register anything that isn't being picked up by attribute or as an interface implementation. A custom plugin just needs a good entry point to be registered (see test for a less than ideal solution).

Usability scenarios

When talking about a plugin system we need to consider how users will interact with it. The following scenarios describe the proposed solution of referencing plugin assemblies.

Considerations

Task list

To be added after a discussion.

ykafia commented 3 years ago

I wonder if this work would still be useful if we decide to switch editor in the future?

manio143 commented 3 years ago

I wonder if this work would still be useful if we decide to switch editor in the future?

Yes, to an extent. There are changes around the package management system which are shared between the asset compiler and the editor - this is the entry point for loading plugins. If we do end up rewriting the editor, it will be up to the owner of a plugin to migrate their implementation onto the new version of presentation classes. But I suspect a good chunk of plugins will be only focused around {runtime component, asset, asset compiler} as the editor part is the one that takes a bit more time. Also if people do start to author plugins it will be easier to migrate onto new UI (and for the designers of the new editor to see which features are desired by plugin authors), then to wait with enabling plugins until after the potential switch.

Eideren commented 3 years ago

@manio143 I do think that a very large part of the use-case, if not most, will fall into level-design (augmenting the scene view, procedural scene composition, tools for blocking out levels, etc.) so working on passing editor input to the user, having a way to draw things on screen and in tool windows and interacting with the live scene view. Those things are intrinsically tied to the UI system. I might be wrong though so if some of you guys could chime in with what you would do with this plugin system that would help.

Or we could use a different UI system for plugins specifically which we would migrate the rest of the editor to later down the line ?

manio143 commented 3 years ago

For me the number one thing is the custom data assets - as you remember I worked on integrating something into Stride, but I had to do a lot not so nice changes to the internals of data serializers to handle generic classes and got hung up on the asset reloading on assembly reload - this is something that can be leveraged with the plugin system and roslyn code generators.

But yes, I have to agree that a large number of highly reusable plugins would attempt to make certain in-editor tasks easier, thus getting tied to the UI. I don't exactly see how we could use a different UI system for just plugins. It depends on what the plugin author wants to achieve - custom property editors have to be written in XAML as they would be rendered in the property grid, but advanced gizmo's could even use Stride's in-game UI system for modifying components.

Eideren commented 3 years ago

custom property editors have to be written in XAML as they would be rendered in the property grid

We could have a dock-able empty wpf element that another ui system renders to, I haven't investigated this yet but I'm sure it's feasible with more lightweight library like myra or imgui. Once/if we swap the editor ui we would have to swap the surface with whatever UI we end up using and it wouldn't break user UIs.

There is also the issue of giving users a way to change the scene, right now the only way to do so in engine is painful to use and extremely slow because of the multiple scenes synchronizing and serialization round-tripping the engine does on every changes, we might have to simplify that or gut it out before moving forward with scenes plugins.

Should there be an attribute for editor plugins discovery?

How the engine figures out which assembly is a plugin ? So we would ask of users to specify something like this in their plugin's csproj <AssemblyAttribute Include="Stride.Core.PluginAssembly"/> and then test that attribute in engine or something to that effect, I'm sure xen2 has some insight on this.

manio143 commented 3 years ago

It's not a bad idea, with the rendering part to be handled by another lib. I will have to think about how exactly the interface layer for the plugin creator would look like so that it's not too much pain to execute view model updates from the other UI lib.

Should there be an attribute for editor plugins discovery?

This was targetted at how a custom StrideAssetsPlugin instance would be registered (should've worded it better) - in general we should try to make all things registered with attributes as this makes it often easier to discover in one place, rather than have the user call an explicit Add or Register method in their code.

The point you mentioned - we can have a custom [assembly: Stride.Core.Plugin] attribute and read that off of a loaded assembly.

xen2 commented 3 years ago

Thanks for the well thought summary/proposal!

You identified one of the main design decisions to take: how to express what is a design-time plugin and make it fit with NuGet with proper dependencies?

As you mentioned, one possibility that sounds quite straightforward for the user to understand is to have 2 user projects: 1 runtime project and 1 design project.

If we take this approach, there is likely an explicit dependency (ProjectReference): the design project depends on the runtime project (i.e. the runtime project defines the runtime class of a custom asset). However, there is also an implicit dependency: the runtime project depends on the design project for asset compilation.

Runtime project is compiled in two phases: code compile, then asset compile. We need somehow to teach the build system to compile this design project after runtime code is compiled (runtime DLL might be needed for ProjectReference) and before assets can be compiled (design DLL needed by asset compiler). Basically, it would go in this order when compiling a runtime project:

  1. Compile Code (generate DLL, contains user custom runtime classes)
  2. New: Build design projects needed by asset compiler which depends on DLL from step 1 (user custom assets, etc.)
  3. Compile Assets (using both libraries)

It can be done by either MSBuild or by our asset compiler (it will need some careful considerations to figure out which is best).

Of course, we could think of another way to structure the projects and how they depend on each other (i.e. introduce an additional project for shared code). Let me know if you have any ideas on that front.

ykafia commented 3 years ago

One aspect of the engine that needs plug-in support would be the post process fx

Aggror commented 2 years ago

Hi @manio143, Would you be prepared to summarize this awesome suggestion for the Epic grant proposal? personally I think this topic is in the top 3 of most needed features of Stride. It would be great if we had budget for someone to start addressing this.

manio143 commented 2 years ago

I've written a short high level explanation https://hackmd.io/@Ed53RzBbTu2HT46rCM--rQ/Bk0ukH_Ct

Aggror commented 2 years ago

wow that is really well formulated.

ParadiseFallen commented 2 years ago

Hi. i have an idea about realization of this thing.

How it works?

Workflow must looks like

  1. Get list of required plugins.
  2. Read AssemblyInfo of plugins and analyze em.
  3. Use ICSharpDecompiler https://github.com/icsharpcode/ILSpy and get syntax tree of code.
  4. Made security checks on syntax tree and decide is plugin secured.
  5. Load plugin assembly to AssemblyLoadContext (ALC)
  6. Instantiate plugins with custom resolver.
  7. Register plugins in separate DI.
  8. Made composite DI and use it by engine.

Custom DI?

No actually. Just a wrapper that use 2 under the hood. It tryes to get from MAIN DI. if cant tryes get from second. First one is persistant while second one is reloadable (can be rebuilded in runtime). Also its for security reasons. When you loaded plugin to ALC you register factory method in second DI. You dont directly instantiate it. So basically it will looks like

var gridViewers= discoveredPlugins.Select(/* select all plugin types that was inhereted from IGridViewer and has attribute [GridViwer] */);

foraech(Type pluginPart in gridViewers)
{
  reloadableDI.Add/*lifetime*/<IGridViewer>(ConstructGridViewerFromType(pluginPart));
}

IGridViewer ConstructGridViewerFromType(IServiceProvider sp, Type type)
{
  return ActivatorUtilities.Instantiate(sp,type);
}

Then in engine editor when we drawing a grid viewrs we


// or composite di
reloadableDi.GetServices<IGridViewer>().Select(/* define which one should be used */);
// or we can have still DI but made custom discovery that will knows what to request

why need DI?

I made simple restriction analyzer https://github.com/ParadiseFallen/CsharpScripting/blob/master/Scripting/Analyzers/RestrictionAnalyzer.cs

it checks source code on Environment.Exit thing. Also it can be improved to use compilation and compare MethodInfo things.

With this we can restrict malware plugins.

ParadiseFallen commented 2 years ago

Also we can force users to made Factories that will know how to construct and how to register ther plugins. Then can get required services and instantiate em

ParadiseFallen commented 2 years ago

i can work on it after docs also with this we can easelly made package manager

  1. Open GameStudio
  2. Press Manage packages
  3. Manage em in window
  4. Press apply
  5. Packages will be downloaded and loaded at runtime usecase: made 2 plugins
Stride.Package.VR - core project that contains all `VR` related thing
+
Stride.Package.VR.Toolkit - project that contains `UI` for game stuio. custom editors and functionality for working with `VR` in game studio. Has dependency on `Stride.Package.VR`

NOTE: naming can be changed but wanna kinda `standart`.
like use suffix `Toolkit` for game engine related things (editor related and dev only things)

also then it will be easy to add kinda Stride.Package.2D.Tilemaps Stride.Package.2D.Tilemaps.Toolkit

manio143 commented 2 years ago

I think the key here is not to go too far too quickly - i.e. before we think about how to do plugin discovery on the GameStudio level and any UI around that we first need to handle the basics of defining an interface layer for the plugins, which is partly done by the existing StrideAssetsPlugin and AssetCompilerAttribute, and creating a mechanism to for them to be loaded.

In my RFC I mentioned describing plugins on a per project basis - adding a list of internal/external packages to the sdpkg file - this makes sense from the standpoint of custom assets. However, it may be cumbersome for users to add the same package to each of their projects manually. It would be interesting to think about a way of adding plugins on the editor level (saved across project sessions) that only get appended to the project if their contents is actually used. They wouldn't need to be referenced by the project at all if they only supply editor tooling. Why would the project need to know about plugins? For one it needs to reference the runtime package of a plugin to compile against and then the asset compiler needs to be aware of the design time package to process custom assets.

On the Toolkit suffix above - I personally prefer Design suffix.

When it comes to code analysis of a plugin - performing decompilation and checking the APIs called during runtime will be expensive. I'm not sure that's something we can go for. This is a complex problem in general and from what I understand there's currently no built-in security features in the .NET runtime (AppDomains and CAS have been removed) and even earlier it wouldn't have been easy - see this discussion. This means that by downloading the plugin the user must trust the library's authors. Since we want to use NuGet infrastructure, the user should be able to see if the given plugin has been downloaded many times from NuGet.org and see a link to the owners website.

I think the first task to look into is how to load a DLL from a NuGet package into the editor and the asset compiler - I attempted this a while back but was very much unsuccessful. Stride already has a mechanism for resolving NuGet packages - that's being used in the GameStudio entrypoint in order to ensure all its dependencies are downloaded and available from the NuGet cache - but the way it has been designed was as run-once type of thing, so not sure it can be reused here.

Once we have an experimental way of sideloading packages, we can start looking into defining the interaction layer - rather than having the plugin register its components, we would discover those using ScanTypes and use the existing mechanism of declaring assemblies that they are meant for use with "Assets" (see this line).

At the moment there's no standard DI framework used, and the AssemblyRegistry provides a small subset of what DI framework would with the ScanTypes and event callbacks. Introducing an additional DI layer would involve rewriting quite a few bits and would need to be considered thoroughly.

ParadiseFallen commented 2 years ago

so basically what i find out:

https://docs.microsoft.com/en-us/nuget/reference/nuget-client-sdk

  1. Need to add UI to game engine where you can manage pluguins.
  2. In ui MUST be button Add locally
  3. Game engine MUST store all plugins that was used (persist em in git if they r local plugins)

  1. Load NuGet package
  2. Define version to use end extract that dll
  3. Load with ALC.

also we need cache functionality for loading packages

ParadiseFallen commented 2 years ago

there is issue about How to manage dependencies for plugins

ParadiseFallen commented 2 years ago

UPD: should look at https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Build.Tasks/BuildTasksUtility.cs

ParadiseFallen commented 2 years ago

UPD: So basically. We DONT NEED to reinvent package restoring and adding em. Basically we want: When project generationg add one more XXX.Editor project. XXX.Editor has ref to XXX project (core logic) There can be placed editor plugins (that you can write for project. Example is CustomGridView that i.e. CustomInspector ).

How to add package:

  1. Add package to core (XXX). Lets imagine some plugin that affects not only edit mode but provides game logic. some ECS library for example.
  2. Add second package to Editor project (XXX.Editor). It contains edit only plugons such as custom grid view. profilers. etc.

Engine just need to scan XXX.Editor for added plugins. We can start without NuGet impl. Just extra plugins. So engine build XXX.Editor and then scan builded assemblies and load em (can be done directly with roslyn runtime.).

Right now we need extension points for editor. Abstractions and some ways to override predefined logic. For example override default play action. Right now it just starts project. If you developing some kinda networking game you wanna start 2 games to test networking. So you can override that behaviour with plugin.

Basically we want some DI analog to reregister some services.

Note: XXX - package name

ParadiseFallen commented 2 years ago

Upd: even better aproach will be creating of entry point in XXX.Editor and passing there some service to manipulate. And when you want to install plugin you add nuget package and then invoke extension method on that service

manio143 commented 2 years ago

Something that came to my head today - I think it would be easier if external plugins are installed globally into your editor and then if the plugin has data to persist in the user's project (i.e. custom asset type) then upon loading the project in another installation of the editor if we detect a type we cannot resolve we would notify the user, giving them a chance to install the plugin - the user's project could have a hint inside it as to where did the plugin come from (because the type that wasn't found could be an unresolved alias without full type name).

The treatment of internal plugins would need to be different because those need reloading and should be clearly referenced in the project as such.

Additionally we should think about providing a hook in the editor where a plugin can check if the user project contains a reference to it's runtime library and if not, add it. I remember there being an issue once that you could add navigation settings into a project that didn't have a reference to Stride.Navigation and it would crash when running - instead on adding the settings we would run the hook to ensure the reference is present.

AmbulantRex commented 1 year ago

At the moment there's no standard DI framework used, and the AssemblyRegistry provides a small subset of what DI framework would with the ScanTypes and event callbacks. Introducing an additional DI layer would involve rewriting quite a few bits and would need to be considered thoroughly.

I feel like diagramming the dependency relationships we want to see would be beneficial. The way projects interact in the engine currently creates some traps for circular dependency if we tried to flip things around and provide IoC in a more typical way (see MAUI or WebHost). I've experimented with swapping IServiceRegistry with IServiceProvider and introducing the same host builder concepts we see in other .NET applications. It was relatively painless, but I kept encountering areas where the GameSystem or EntityProcessor wanted to set up its own dependencies if they were null. Do we need to be more opinionated on that type of thing? Do we require that flexibility moving forward and will it impact your Plugin idea adversely?

manio143 commented 1 year ago

On one hand going with full DI would definitely make plenty of things simpler (like switching to the standard logging a abstraction). As you noted one thing is loosing the runtime ability to add services post initialization. A lot of parts of Stride have been implement in a way where things are dynamic and only added when needed, and usually auto detected. So for example when the user wants to add the physics engine (which outside of the editor/assets already is a plugin) all they need to do is add a reference to the library and create a physics component in the scene. Everything else will automatically get wired up. With the change, there would likely be an additional .AddPhysics() call before creating the Game object. This would likely flow through to how user code is written - e.g. would we keep the attribute based way of defining a processor for a component or switch to a generic factory pattern.

It would definitely be a breaking change and ideally should be a driver for Stride 5.0 - if approached right it could bring a lot of improvements to the design of Stride internals (allowing Game to run headless comes to my mind).

I would suggest creating a new Epic for it and starting to document in it what would be changing and any potential hard parts that needs design discussions.

manio143 commented 1 year ago

Two things that came to my head today:

I think the key here is dependency inversion and using a DI framework can help orchestrate it. However, MS's DI framework works well for statically known (or startup known) dependencies, while a plugin system should be robust enough to allow runtime loading and reloading of plugins.

BenjaBobs commented 1 year ago

Long time lurker here, not sure if it's been mentioned before, but it feels like the right time in the conversation to mention https://github.com/natemcmaster/DotNetCorePlugins to address concerns about robust loading/reloading of plugins.

ParadiseFallen commented 1 year ago

@manio143 well. for me it seems much better to configure di before and dont use any attributes at all. Why? Bc you can easilly create factory that will search for class with attribute. But you cant do it backwards.

So precofigurable pipeline looks pretty good.

Reloading in runtime? Do we really need it? What scenario when you want to reload DI at runtime?

I see there only one reason: Stride.Editor. But yeah. there is a way to configure reloadable DI with notifications. ServiceContainer from MS

ParadiseFallen commented 1 year ago

So basically why we thing ONLY about c# as plugin language?

We also can use Lua as plugin language for editor (moonsharp)

ykafia commented 1 year ago

With dotnet interactive we can have other languages including C# and F#, which in my opinion is better for us.

But i'm not sure how useful it would be for all the rendering related stuff

manio143 commented 1 year ago

Following discussion on Discord with @ParadiseFallen

The suggestion is to first focus on enabling user projects to be editor plugins. The Design/Editor project would be loaded by the editor/asset pipeline. This project would not be referenced by the runtime game library. 3rd party code could be copied into the users project to allow for using plugins developed by others. In a later stage 3rd party plugins would be referenced (project/dll/nuget) by the editor project. Only later we would need to understand dependency versioning concerns.

ParadiseFallen commented 1 year ago

ill take a try to make a basics for this impl. Basically our goals:

  1. Local editor project
  2. Rich registrar of plugins with autodescovery
  3. Loading basic Custom asset editor
  4. Displaying it on UI
  5. Dll feed
  6. Nuget feed
  7. Versioning

MVP is 1-4 points. We need to start there @manio143 ty

BenjaBobs commented 1 year ago

Just dropping this here in case it turns out to be relevant for the plugin loading functionality: https://github.com/SteveSandersonMS/DotNetIsolator

manio143 commented 1 year ago

I'm gonna update the main description at some point later, after closing down on some more thoughts.

Proof of concept - NuGet oriented plugin references

Because the user projects holding editor plugins have currently a few issues with reloading after change, I decided to focus first on NuGet provided plugins. Scenario:

  1. User A creates a runtime library X which defines a new content type and a component that works with this content.
  2. User A creates a plugin library X.Design which defines the asset type and an asset compiler that allows compiling resources into the content type defined in X.
  3. X and X.Design get published on NuGet.org
  4. User B adds X package to their game project.

Core ideas

  1. We should leverage MSBuild as much as we can to download packages for us.
    • By using dependencies with asset exclusion we can have Restore manage packages without them being copied to output
  2. The experience with plugins for the user should be as seamless as possible.
    • If X won't work without the plugin, it needs to be able to reference it, and we need to load the plugin without having the user take additional actions

Approach 1: explicit plugin reference

I started off with the idea of annotating PackageReference with a property that can be read by the editor and used to detect packages.

<PackageReference Include="Plugin" Version="1.0.0" IncludeAssets="build;buildTransitive" StridePlugin="true" />

This allows us to load any package as a plugin into editor's memory. However, it doesn't allow for core idea (2) because every plugin has to be explicitly listed in the csproj in the solution for Stride to read it.

Approach 2: custom package type

NuGet allows packages to define custom PackageType property (docs). This has an extra benefit of allowing us to search NuGet.org for all packages of that type - https://www.nuget.org/packages?packageType=StridePlugin

On the plugin manifest we would annotate

<PropertyGroup>
  <PackageType>StridePlugin</PackageType>
  <DevelopmentDependency>true</DevelopmentDependency>
</PropertyGroup>

And user could reference it explicitly pretty much as above

<PackageReference Include="Plugin" Version="1.0.0" IncludeAssets="build;buildTransitive" />

However, we can now potentially traverse the dependency tree and discover plugins referenced by user dependencies without explicitly listing them in their project.

However, currently it's not possible yet. I've opened an issue on NuGet side https://github.com/NuGet/Home/issues/12934 and if it gets implemented it would be very efficient. If not, I have in mind a workaround, such that plugins contain a file with sdplugin extension in the package and by iterating over all package files we can detect those. The file may be generated with our build targets when PackageType=StridePlugin is detected.

Another issue with custom package types is https://github.com/NuGet/Home/issues/10468 where VS complains when installing them through UI.

Potential issues

  1. Circular dependencies
    • X.Design likely has a runtime dependency on X. How would X be able to take a dependency on X.Design in order to communicate it has to be loaded by the editor? X.Design could reference X with PrivateAssets=all to skip the reference. However, this then yields to a problem, when X.Design is referenced explicitly by the user without X being referenced, as the editor will have no idea that X needs to be loaded as well.
  2. Plugin dependencies
    • If a plugin requires other packages to be loaded into the editor we need to ensure this done correctly, but it's a separate issue to how plugins are referenced. Consider assembly load context and a custom assembly resolution callback.
  3. Plugin reference duplication
    • If a plugin is referenced by multiple projects in a solution we should only load it once and for the highest version referenced.

Implementation spots

PackageSession.PreLoadPackageDependencies and PackageSession.UpdateDependencies for dealing with references. somewhere else in PackageSession we would load the plugins into memory.

manio143 commented 1 year ago

My idea for solving the circular dependencies didn't work out, but if you're interested https://devblog.dziubiak.pl/stride/01-nuget-plugins/#circular-dependencies Going back to the thinking board...

ParadiseFallen commented 1 year ago

@manio143 In fact, I don't think this should be any problem at all. In fact, NO project has circular dependencies. You simply won't be able to build it. Accordingly, if you cannot assemble it, then there will be no problem. I suggest moving on

manio143 commented 1 year ago

@manio143 In fact, I don't think this should be any problem at all. In fact, NO project has circular dependencies. You simply won't be able to build it. Accordingly, if you cannot assemble it, then there will be no problem. I suggest moving on

I know it's not an issue in this way. But the question remains - if you make a runtime package and a design package which provides the necessary plugin, can we somehow enforce that we add the plugin reference to the users project when they add the runtime reference?

Now, maybe this issue is not an issue at all and the users will be able to figure out that for every runtime package reference if a plugin exists it needs to be added separately. I was trying to see if we can make this experience more seemless. I suppose sometimes the runtime package reference doesn't need to imply using the plugin (code only approach without build time defined assets), so making plugins optional makes sense. It's just the matter of whether users will figure it out, or will they complain that something is missing from the editor even though they are referencing the package.

ykafia commented 1 year ago

I think it was a nice idea to look for seamless solutions to that problem, although as a lambda user I wouldn't mind having to add plug-ins to the editor and separately having to add the necessary nuget to the game.

Sometimes a user experience can be simplified with proper error messaging.

ParadiseFallen commented 1 year ago

@manio143 package manager will solve this issue. you need to have git repo with .stride-plugin-definition file where will be all instrunctions that will understande SPM (stride package manager)

manio143 commented 6 months ago

@Doprez As we discussed on Discord, I'm writing up the details to what I was working on for the MVP so that someone else can continue.

1. Developer defines a plugin as a NuGet package

To make this simple - we will just try to put in something that after being loaded will log a Warning log. For this assembly to be correctly picked up for asset management we need to put the following in Module.cs

[ModuleInitializer]
public static void Initialize()
{
    AssemblyRegistry.Register(typeof(Module).Assembly, AssemblyCommonCategories.Assets);
    // and for now for Editor plugins to be discovered also call
    AssetsPlugin.RegisterPlugin(typeof(MyPlugin));
}

class MyPlugin : AssetPlugin
{
    // override all abstract methods
    protected override void SessionLoaded(SessionViewModel session)
    {
        var logger = GlobalLogger.GetLogger("MyPlugin");
        session.AssetLog.AddLogger(logger);
        logger.Warning("Session loaded");
    }
}

For the asset compiler part of things we can try to get an asset compiler invoked - defined like in CustomAsset.

2. User imports plugin into their project

See comment above for some context. For this MVP we're going to go with the most basic explicit approach.

<PackageReference
    Include="Plugin"
    Version="1.0.0"
    <!-- Don't reference the assembly of the plugin (which may depend on editor libs)
         Instead only use build time dependencies -->
    IncludeAssets="build;buildTransitive"
    <!-- Mark as private so that projects which depend on this one do not pull the plugin
         as a dependency themselves -->
    PrivateAssets="all"
    <!-- Mark as a Stride plugin for the editor code below -->
    StridePlugin="true" />

3. Stride discovers plugin

Ok, so now that we've established the how the Developer and User orchestrate their code to define and import a plugin, let's look at how we're going to be discovering it in the editor/asset compiler. First of all let's understand that the plugin loading must be done at the Stride.Core.Assets level, which is the abstract base library for Stride's asset management, shared between editor and asset compiler. The way in which we load the plugins must be independent from the way plugin content is discovered and loaded.

We're going to modify this piece of code in the Dependencies loading logic to discover our plugins https://github.com/stride3d/stride/blob/cd8e17fdde15b0ef179e7b45eef52bac9a6ccf3d/sources/assets/Stride.Core.Assets/PackageSession.Dependencies.cs#L77-L81 I've spent a lot of time trying to figure out the best/most versatile approach, but for an MVP there's no point being too detailed. We will just check that a PackageReference has StridePlugin metadata set to true and if so, add it to a list of plugins to be loaded. In the future a proper way to handle package version conflicts will be necessary (e.g. someone changes package reference version after launching game studio OR one project in the solution imports plugin with one version and another project imports the same plugin with another version).

class PluginReference(string packageName) {
  public Dependency? PackageDependency { get; set; }
}

// add PluginReferences to the SolutionProject
if (packageReference.HasMetadata("StridePlugin") && bool.TryParse(packageReference.GetMetadataValue("StridePlugin"), out var isStridePlugin) && isStridePlugin)
    project.PluginReferences.Add(new PluginReference(packageReference.EvaluatedInclude));

Next we need to process the discovered plugins. In the same file there's an UpdateDependencies method which reads the project.assets.json lockfile created during package restore phase. We already iterate over the lockfile, so now for any dependency which is on the list of plugins we need to get its DLL location and add it to the project to be loaded later.

void UpdateDependencies(SolutionProject project, bool directDependencies, bool flattenedDependencies,
  bool pluginDependencies)
{
   //...
    if (pluginDependencies) {
      foreach (var targetLibrary in projectAssets.Targets.Last().Libraries) {
        var reference = project.PluginReferences.FirstOrDefault(pr => pr.packageName == targetLibrary.Name);
        if (reference is null) continue;
        reference.Dependency = new Dependency(targetLibrary.Name, targetLibrary.Version.ToPackageVersion(), DependencyType.Package);

        // Find library path by testing with each PackageFolders
        var libraryPath = projectAssets.PackageFolders
            .Select(packageFolder => Path.Combine(packageFolder.Path, library.Path.Replace('/', Path.DirectorySeparatorChar)))
            .FirstOrDefault(x => Directory.Exists(x));

        if (libraryPath != null)
        {
          // note: because we don't reference the assembly of the plugin - nuget will not populate the DLL path in the restore metadata
          // [MVP] find the DLL in any subdirectory
          // [Proper] read the nuspec to find the specific path to the DLL to be referenced
          // we may need to traverse the dependencies of this package and add their assemblies in here as well - I'd say to leave it for next step after MVP as it becomes a complicated topic
          var pathToDll = ...;
          reference.Dependency.Assemblies.Add(pathToDll);
        }
      }
    }
}

4. Load the plugins given the references

We now need to actually load those DLLs into the program. I think the correct place to do so is here:

public void LoadMissingReferences(ILogger log, PackageLoadParameters loadParameters = null)
{
  LoadMissingDependencies(log, loadParameters);
  // TODO: we have to load plugins before we load assets.
  LoadMissingAssets(log, Packages.ToList(), loadParameters);
}

See how Package.LoadAssemblies() works and how it calls AssemblyContainer.LoadAssemblyFromPath().

5. Try run it and observe logs