Skip to content

Commit

Permalink
Improved: Documentation Related to Controllers & Plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
Sewer56 committed Dec 18, 2021
1 parent 6816712 commit 7601d7f
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 262 deletions.
2 changes: 1 addition & 1 deletion docs/APIOverview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# API Overview

In Reloaded II, in-process communication between mods and the loader as well as [Inter Mod Communication (IMC)](./InterModCommunication.md) is performed mainly through the use of extensible interfaces that can be found in the `Reloaded.Mod.Interfaces` namespace.
In Reloaded II, in-process communication between mods and the loader as well as [Inter Mod Communication (IMC)](./DependencyInjection.md) is performed mainly through the use of extensible interfaces that can be found in the `Reloaded.Mod.Interfaces` namespace.

`Reloaded.Mod.Interfaces` is contained and distributed as a tiny interface only library that contains various interfaces such as `IModLoader` or `IMod`. It is included by default in Reloaded mod projects and also available as a NuGet package.

Expand Down
284 changes: 284 additions & 0 deletions docs/DependencyInjection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
# Introduction

Sometimes you might want to check the state of another mod, run some code from it and/or instruct another mod to perform a certain action. Reloaded-II provides a mechanism to do this.

## How Does it Work?

Implemented using interfaces, the concept is that the mod loader acts as a middleman between mods (it's a DI container!). This middleman allows mods to communicate by passing implementations of interfaces between each other.

This may be illustrated by the following diagram:

![Example](./Diagrams/Images/InterModCommunication.png)

*An example of an external mod communicating with the [Universal File Redirector](https://github.com/Reloaded-Project/reloaded.universal.redirector).*

- During initialization, Mod A (Redirector) publishes an interface to the Mod Loader.
- During initialization, Mod B (Other Mod) asks the Mod Loader for the interface.

Communication with the Mod Loader is performed using the `IModLoader` interface, available at your mod's entry point.

## Usage: Consumer

### Prerequisites

Make sure you add the `ModId` of the mod you are consuming instances/interfaces from in the `ModDependencies` field of your `ModConfig.json`.

```csharp
"ModDependencies": [
"reloaded.sharedlib.hooks",
"Riders.Controller.Hook"
]
```

You can also do this from within the launcher (`Edit Mod`) and copy the new `ModConfig.json` back to your source code.

*Reloaded will not allow mods to consume types from other mods without explicit opt-in*, this is a safeguard to ensure no unintended types are shared between mods (ensuring isolation).

### Consuming Dependencies

Use the `IModLoader` API to get an instance of the interface you want to consume.
This interface should be available at the entry point (`Start`) of the mod you are consuming.

```csharp
WeakReference<Controller> _controller;
void GetController()
{
_controller = _loader.GetController<IController>();
}
```

✅ Always check the controller is valid and hasn't been disposed before usage.
```csharp
void DoSomethingWithController()
{
// If the mod we got IController from is unloaded, `TryGetTarget` will fail.
if (_controller != null && _controller.TryGetTarget(out var controller))
{
// Do something with `controller`
}
}
```

### Life Cycle & Disposal

`*Note: If the publisher is not unloadable, you can ignore this section.*`

Some Reloaded mods support live, real time loading and unloading.
As such, you must be careful with how you consume interfaces from other mods.

In order to ensure unloading of publishers correctly happens, Reloaded uses "Weak References" (`WeakReference<T>`).
This also means that when mods are unloaded, the Mod Loader can dispose of them cleanly without explicit code in other mods.

Here are some guidelines on consuming interfaces:

✅ Storing Weak References on the Heap is OK
```csharp
WeakReference<IController> _reference;
void AcquireController()
{
_reference = _loader.GetController<IController>();
}
```

✅ Storing referenced objects on the Stack is OK
```csharp
void AcquireController()
{
IController controller = _loader.GetController<IController>().Target;
// controller is no longer referenced outside of the scope of the method.
}
```

❌ Storing referenced objects on the Heap is NOT OK.
```csharp
IController _controller;
void AcquireController()
{
_controller = _loader.GetController<IController>().Target;
// This prevents the mod loader from being unable to dispose the controller.
}
```

## Usage: Publisher

### Prerequisites

If you are developing a mod which will share interfaces to be used by other mods, *you must first share the interface with the Mod Loader*.

This is done by inheriting the interface `IExports` (from `Reloaded.Mod.Interfaces`) in your main mod, with single method `GetTypes` which should return an array of interfaces to be consumed by other mods.

**Example:**

```csharp
public class Exports : IExports
{
public Type[] GetTypes() => new[] { typeof(IController) };
}
```

Sharing forces other mods to load the same instance of the DLL containing the shared interfaces, regardless of whether the version is newer or older.

**If you do not do this, `GetController` will not work for other mods.**

### Publishing Dependencies

For controllers an additional step is required. During initialization (`Start()`), you need to register it with the mod loader. Simply use the `AddOrReplaceController` method of `IModLoader`.

```csharp
Controller _controller;

_controller = new Controller(); // Implements IController
_loader.AddOrReplaceController<IController>(this, _controller);
```

### Disposing (Publisher)

The Mod Loader is capable of automatically disposing your dependencies when your mod is unloaded, however you can still manually (if desired) dispose and/or replace your dependency instances with the `RemoveController` method.

```csharp
void Unload()
{
_loader.RemoveController<IController>();
}
```

## Recommendations & Limitations

### Interface DLLs are Immutable
***Once a DLL containing interfaces has been loaded into the process, it cannot be unloaded.***

e.g. If you recompile `SomeMod.Interfaces`, it cannot be swapped out without restarting the application/process.

This is unfortunately a limitation of .NET (Core), as DLLs cannot be unloaded from `AssemblyLoadContext`(s), only whole load contexts.

**Note:** This does not mean mods using the interface DLLs cannot be unloaded. Said mods can be loaded and unloaded during runtime as usual without any problems.

### Keep all your Interfaces in a separate library

***Interface libraries should contain no external, 3rd party references*** (single DLL).

There are two issues:

1. If you store your interfaces in the same DLL as your main mod.
- Not Good: Consumers using them will your entire mod compiled and copied to their output! (Not Good!)

1. Sharing an interface shares its whole assembly, and all of the assemblies' dependencies.
- This can cause mods to unintentionally be forced to use another mod's dependencies.

Therefore it is recommended that you make a separate library for storing only your shared interfaces. Both the main mod (producer), and other mods (consumer) should reference that same library.

### Upgrading Interfaces
Changes to existing interfaces will break mods using the interfaces. This should be fairly obvious to anyone who has used a plugin or a plugin-like system before.

If you want to add more functionality to existing interfaces, either make a new interface or extend the current interface via inheritance. After being published (Mod Released), interfaces should never change (this includes method names).

Here is an example of a possible setup:

```csharp
// Recommended setup using inheritance.
// User can acquire `IController` and have the latest version known at compile time.
interface IController : IControllerV3 { /* Dummy */ }

interface IControllerV3 : IControllerV2 { void DoZ(); }
interface IControllerV2 : IControllerV1 { void DoY(); }
interface IControllerV1 { void DoX(); }
```

## Example Mods

The following examples contain mods that export interfaces to be used by other mods.

**Universal Mods**

- [Reloaded Universal File Redirector](https://github.com/Reloaded-Project/Reloaded.Mod.Universal.Redirector)
- Producer: `Reloaded.Universal.Redirector`
- Contract: `Reloaded.Universal.Redirector.Interfaces`
- Consumer(s): `Reloaded.Universal.Monitor`, `Reloaded.Universal.RedirectorMonitor`

**Application Specific**

- [Sonic Heroes Controller Hook](https://github.com/Sewer56/Heroes.Controller.Hook.ReloadedII) (Allows other mods to receive/send game inputs.)
- Producer: `Riders.Controller.Hook`
- Contract: `Riders.Controller.Hook.Interfaces`
- Consumer(s): `Riders.Controller.Hook.Custom`, `Riders.Controller.Hook.XInput`, `Riders.Controller.Hook.PostProcess`

- [Sonic Riders Controller Hook](https://github.com/Sewer56/Riders.Controller.Hook) (Allows other mods to receive/send game inputs.)
- Producer: `Heroes.Controller.Hook`
- Contract: `Heroes.Controller.Hook.Interfaces`
- Consumer(s): `Heroes.Controller.Hook.Custom`, `Heroes.Controller.Hook.XInput`, `Heroes.Controller.Hook.PostProcess`

**Libraries as Dependencies**

- [PRS Compressor/Decompressor](https://github.com/Sewer56/Reloaded.SharedLib.Csharp.Prs.ReloadedII)
- [Reloaded.Hooks (Function Hooking/Detour Library)](https://github.com/Sewer56/Reloaded.SharedLib.Hooks.ReloadedII)

## Extra Features

### Factories

Reloaded's API also provides a functionality to create instances implementing a type. These are sometimes also referred to as `"Plugins"` in older docs.
Acquiring new instances of **Plugins** is performed through the use of the `MakeInterfaces` method.

```csharp
// _loader is an instance of IModLoader
var interfaces = _loader.MakeInterfaces<ISharedInterface>();
```

When you run the above code, Reloaded will iterate through every loaded mod and create an instance of every class implementing `ISharedInterface`.
The result of this operation (all instances) will be returned as an array.

### Optional Dependencies

(`OptionalDependencies` in `ModConfig.json`)

Mods can also use the `OptionalDependencies` field instead of the `ModDependencies` field.
If the mod is an optional dependency, then the preferred option is to acquire Controllers/Plugins after the Mod Loader has finished initializing (all mods are loaded).

To do this, simply subscribe to `IModLoader`'s `OnModLoaderInitialized`, and try to obtain an interface instance at that point.

```csharp
IModLoader _loader;
WeakReference<IController> _controller;
void Start(IModLoaderV1 loader)
{
_loader = (IModLoader)loader;
_loader.OnModLoaderInitialized += Initialized;
}

// Called by the mod loader after all mods finished loading.
void Initialized()
{
_controller = _loader.GetController<IController>();
}
```

*Unlike required dependencies, the Mod Loader does not take load order into account with optional dependencies.*

## How does it *Really* Work?

Reloaded makes extensive use of [AssemblyLoadContext](https://docs.microsoft.com/en-us/dotnet/core/dependency-loading/understanding-assemblyloadcontext)(s) to provide isolation between mods, such that they may use 3rd library plugins and dependencies of their choice without interfering with other mods. This can make it difficult to share state between a mod and the loader, and even more so between individual mods.

Reloaded overcomes this challenge by sharing instances of `Assemblies` (DLLs), loosening up isolation around shared code. Mods can nominate which `Assemblies` they wish to share with other mods, and the loader will load them into a shared `AssemblyLoadContext`. When the mod is loaded, the Mod Loader will then force it use the shared `Assembly` instances from the shared `AssemblyLoadContext`.

For publishers its own shared types are automatically shared with itself, for consumers this is determined using the `ModDependencies` field in `ModConfig.json`.
![Example](./Diagrams/Images/InterModCommunication-Internal.png)

### How is this arrangement Setup?

The whole process for sharing an `Assembly` looks something like this:

1. Gathering Dependencies
1. Mod Loader loads `Redirector` (Publisher).
2. Mod Loader loads all shared Assemblies into Shared `AssemblyLoadContext`
3. Mod Loader unloads `Redirector` (Publisher).

2. Loading Mods
1. Mod Loader loads `Redirector`, sharing its own `Assemblies` with itself (from 1.b).
2. Mod Loader loads `RedirectorMonitor` (Consumer).
1. `RedirectorMonitor` specified `Redirector` in its `ModDependencies` field.
2. Therefore Mod Loader shares all of `Redirector`'s exports with `RedirectorMonitor`.

3. Executing Mods
1. Mod Loader rearranges mod load order to account for mod dependencies.
1. `Redirector` code executes. Redirector publishes `IRedirectorController` (from Reloaded.Mod.Interfaces.IRedirector.dll) to Mod Loader.
2. `Monitor` code executes. Monitor obtains `IRedirectorController` from Mod Loader.
1 change: 1 addition & 0 deletions docs/Diagrams/Files/InterModCommunication-Internal.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mxfile modified="2021-12-18T05:17:55.437Z" host="app.diagrams.net" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.113 Safari/537.36" etag="yhV1A5qjzK6I-Y0XT-0L" version="16.0.0" type="device"><diagram id="4yicWO6-0QshSFdzmrWn" name="Page-1">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</diagram></mxfile>
2 changes: 1 addition & 1 deletion docs/Diagrams/Files/InterModCommunication.drawio
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<mxfile modified="2020-05-11T18:28:15.589Z" host="app.diagrams.net" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.132 Safari/537.36" etag="OdJZM29RYcq97WwWZS41" version="13.1.0" type="google"><diagram id="4yicWO6-0QshSFdzmrWn" name="Page-1">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</diagram></mxfile>
<mxfile modified="2021-12-18T04:59:07.280Z" host="app.diagrams.net" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.113 Safari/537.36" etag="6qqPNH3Je2uBFi1e3nTI" version="16.0.0" type="device"><diagram id="4yicWO6-0QshSFdzmrWn" name="Page-1">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</diagram></mxfile>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7601d7f

Please sign in to comment.