Creating Plugins
The Plugin API is included as part of the MSFS SDK library (@microsoft/msfs-sdk
). In order to use the API, the first step is to make sure your plugin imports @microsoft/msfs-sdk
. Once the library is imported, you can start using the Plugin API classes.
Setting Up Your Plugin Class
Plugins are implemented as classes that extend the abstract class AvionicsPlugin
. Individual instruments have the option to define their own plugin interfaces, so before doing anything else, you should take a minute to look up the specific interface required by the instrument for which you are developing your plugin. Once you have done that, simply create a class that both inherits from AvionicsPlugin
and implements the instrument-specific plugin interface (if one exists).
Below is an example of a very basic plugin. To keep things simple, the example assumes that there is no instrument-specific plugin interface, so we just have to extend AvionicsPlugin
.
import { AvionicsPlugin } from '@microsoft/msfs-sdk';
// Don't worry about the type parameter for now; we'll get to that later.
class MyPlugin extends AvionicsPlugin<void> {
public onInstalled(): void {
// ...
}
}
AvionicsPlugin
only requires one method to be implemented: onInstalled()
. This is a callback method that is called when the plugin is first loaded by the instrument. It is a good place to run initialization code.
Once you have defined your plugin class, the next step is to register it with the plugin system. You can do this using the global function registerPlugin()
:
import { AvionicsPlugin, registerPlugin } from '@microsoft/msfs-sdk';
// Don't worry about the type parameter for now; we'll get to that later.
class MyPlugin extends AvionicsPlugin<void> {
public onInstalled(): void {
// ...
}
}
registerPlugin(MyPlugin);
Loading Your Plugin
Next, you need to build your plugin code to a Javascript (.js) file and place the file in a directory that will be loaded by the sim's virtual file system.
If you are creating an airplane-specific plugin, then a good location would be under the airplane's panel/Instruments/
directory. Another possible location is under the global html_ui/
directory; however care must be used when adding files here to avoid conflicts with files from the base sim or other third-party packages.
After you have chosen a suitable location for your plugin file, you need to tell the sim where your plugin file is located so it can be loaded. The method to do this depends on if your plugin is a global plugin or an airplane plugin.
Loading Global Plugins
Global plugins are loaded using XML files placed in the following directory:
html_ui/Plugins
While you are free to choose any name for your global plugin XML files, please take care to choose names that are not likely to conflict with files used by other plugins. Files with the same names will overwrite one another and will cause issues if the overwriting is not intended.
Global plugin XML files should contain the <Plugins>
tag at the root. The <Plugins>
tag should in turn contain one or more <Plugin>
tags. Each <Plugin>
tag loads one plugin .js file:
<Plugins>
<Plugin target="MyInstrument">
coui://html_ui/Path/To/My/Plugin/MyPlugin.js
</Plugin>
<Plugin target="MyOtherInstrument">
coui://html_ui/Path/To/My/Plugin/MyOtherPlugin.js
</Plugin>
</Plugins>
The target
attribute of each <Plugin>
tag defines the target of all plugins loaded from the tag's .js file. The target is used by instruments to determine whether to use a particular global plugin. This allows instruments to avoid using plugins that were not meant to be applied to them. Each instrument that supports plugins is free to filter global plugins based on whatever arbitrary criteria it chooses, so please always confirm that your plugin's declared target matches what is expected by the instrument. If you want your plugin to apply to multiple instruments that expect different target strings, create separate <Plugin>
tags with the appropriate target strings for the different instruments.
Loading Airplane Plugins
Airplane plugins are loaded using the airplane's panel.xml
file. Find (or create) the <Instrument>
tag associated with your plugin's parent instrument, then add a <Plugin>
tag as a child with the content of the tag specifying the full absolute path to the plugin file.
For example, the following panel.xml
will cause the instrument with ID MyInstrument
to load MyPlugin.js
.
<PlaneHTMLConfig>
<Instrument>
<Name>MyInstrument</Name>
<Plugin>
coui://SimObjects/Airplanes/Company_MyAirplane/panel/Instruments/MyPlugin.js
</Plugin>
</Instrument>
</PlaneHTMLConfig>
If you wish to load multiple plugins for a single instrument, you may either build them all to a single .js file and load that file or build each plugin to its own .js file and specify multiple files to be loaded in panel.xml
. When loading more than one .js file, create a separate <Plugin>
tag for each file to be loaded.
If an airplane has multiple instances of an instrument that all need to load the same plugin(s), the plugin file(s) should be specified for all instances of the instrument, each of which will have its own <Instrument>
tag in panel.xml
.
Getting Data from the Instrument
Instruments may choose pass data to plugins via a binder, which is an object that is passed to plugins via their constructors. Instruments that use plugin binders should also declare an interface for such binders so that plugins are aware of what is contained in the binder. Once the binder interface is known, it can be used as the type parameter on AvionicsPlugin
to properly expose the type of the binder
class property:
// ------------------------------------
// Declared by the instrument somewhere
export interface PluginBinder {
readonly bus: EventBus;
}
// ------------------------------------
class MyPlugin extends AvionicsPlugin<PluginBinder> {
public onInstalled(): void {
// 'this.binder' now has the type 'PluginBinder'
this.binder.bus
.getSubscriber<ClockEvents>()
.sub('realTime')
.handle(time => { console.log(`The time is now ${new Date(time).toTimeString()}`); });
}
}
Overriding Rendered Components
Normally, instruments get to decide how plugin-specific functionality is integrated. For example, an instrument may allow plugins to render or replace certain display components through the use of specific methods declared by its plugin interface:
export interface InstrumentPlugin extends AvionicsPlugin<void> {
/**
* Renders a component.
* @returns The rendered component, or null if this plugin does not support rendering the component.
*/
renderComponent(): VNode | null;
}
This allows instruments a certain degree of control over what parts of themselves plugins can and cannot override. However, sometimes you may find yourself in the position of needing your plugin to override a display component in a manner that the instrument plugin interface does not explicitly support. The Plugin API allows you to accomplish this without resorting to forking the instrument code.
AvionicsPlugin
supports an optional callback method onComponentCreating()
. If this method is defined in a subclass, then it will be called whenever any instance of DisplayComponent
is about to be created on the plugin's parent instrument. It is passed the constructor of the component and the props that are to be used to create the component. If onComponentCreating()
returns undefined
, then the original component will be created as usual. However, if onComponentCreating()
returns its own instance of DisplayComponent
, the returned instance will be used in place of the original component. Effectively, you can use onComponentCreating()
to intercept the creation of certain components and silently replace them with your own versions.
For example, the following code will cause all instances of MyComponent
to be replaced with MyPluginComponent
. Everywhere MyComponent
would have been rendered, there will instead be a <div>
with the text "Hello, world!".
class MyPluginComponent extends DisplayComponent<ComponentProps> {
public render(): VNode {
return (
<div>Hello, world!</div>
);
}
}
class MyPlugin extends AvionicsPlugin<void> {
public onInstalled(): void {
// ...
}
public onComponentCreating = (ctor: DisplayComponentFactory<any>, props: any): DisplayComponent<any> | undefined => {
if (ctor.name === 'MyComponent') {
return new MyPluginComponent(props);
}
return undefined;
};
}
When replacing components with onComponentCreating()
, it is imperative that the replacement component implement the same interface as the original. Otherwise, you may cause runtime errors to be thrown when instrument code attempts to access properties on the replacement that do not exist.
Component Hooks
If your plugin needs to know when certain components are created or rendered in the instrument, you can subscribe to those events by defining the optional onComponentCreated()
and onComponentRendered()
methods in your plugin class. The former is called immediately after any component is created in the instrument, and the latter is called immediately after any component is rendered.