Creating plugins

TIP

This is written in a tutorial-like style. Skip to next sections for raw documentation.

Basic template

Create new .net standard project in Visual Studio or using command line:

dotnet new classlib -f netstandard2.1

Open csproj file using Visual Studio and double click on your project name to open its configuration. Inside of it add reference to newest StreamCompanionTypes nuget packageopen in new window:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
    <Nullable>enable</Nullable>
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="StreamCompanionTypes" Version="8.0.0" />
  </ItemGroup>
</Project>




 

 
 
 

Rename Class1 that got created by default to some meaningful name(MyPlugin) and implement base IPlugin interface:

using StreamCompanionTypes.Interfaces;

namespace newTestPlugin
{
    public class MyPlugin : IPlugin
    {
        public string Description => "my plugin description";
        public string Name => "my plugin name";
        public string Author => "my name";
        public string Url => "Plugin homepage url(github/site)";
    }
}

At this point this project could get compiled and ran by StreamCompanion, but what is the point if it does nothing? Lets make it log something at startup:

using StreamCompanionTypes.Interfaces;
using StreamCompanionTypes.Enums;
using StreamCompanionTypes.Interfaces.Services;

namespace newTestPlugin
{
    public class MyPlugin : IPlugin
    {
        public string Description => "my plugin description";
        public string Name => "my plugin name";
        public string Author => "my name";
        public string Url => "Plugin homepage url(github/site)";
        public MyPlugin(ILogger logger)
        {
            logger.Log($"Message from ${Name}!", LogLevel.Trace);
        }
    }
}

 
 









 
 
 
 


Our plugin now requests ILogger Service at startup from StreamCompanion and uses it to log our message.
Lets see it in action: Build whole solution, copy everything from bin\Debug\netstandard2.1 in solution folder to StreamCompanion plugins folder. It should be loaded along with log message logged.
That's cool and all but this copying and manual running will get old and annoying really quick - so we need to automate things a bit.

Testing enviroment

Create an empty folder with 2 directories inside:

  • newTestPlugin - folder with your plugin project, its name doesn't matter.
  • SCInstall - folder with installed/portable SC. Existing installation can be just copied over.

Add OutputPath to your project configuration:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
    <Nullable>enable</Nullable>
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
	<OutputPath>..\SCInstall\Plugins\</OutputPath>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference Include="StreamCompanionTypes" Version="8.0.0" />
	</ItemGroup>
</Project>





 





Inside your solution folder create Properties folder with launchSettings.json inside and populate it with:

{
  "profiles": {
    "newTestPlugin": {
      "commandName": "Executable",
      "executablePath": "..\\osu!StreamCompanion.exe"
    }
  }
}


 





Replace newTestPlugin with name of your project(not class name!)

With that done, your plugin can be now easily tested and debugged without ever leaving Visual Studio - Start debugging (Debug->Start debugging at the top menu) to test any changes.

TIP

Project with everything mentioned so far can be found here and can be used as a template. Remember to change namespace and plugin class name!

Interacting with events

CreateTokensAsync(from ITokensSource) & SetNewMapAsync(from IMapDataConsumer) are 2 hooks you'll most likely want to use. Code below demonstrates how to:

  • request multiple services from SC and store these for later use (lines 19-27)
  • create&update tokens (lines 31-41).
  • store persistent settings between runs (lines 22 and 38).
  • use final event data(tokens/map search result) (lines 43-56).
using System.Threading;
using System.Threading.Tasks;
using StreamCompanionTypes.Interfaces;
using StreamCompanionTypes.Enums;
using StreamCompanionTypes.Interfaces.Services;
using StreamCompanionTypes.Interfaces.Consumers;
using StreamCompanionTypes.Interfaces.Sources;
using StreamCompanionTypes.DataTypes;

namespace newTestPlugin
{
    public class MyPlugin : IPlugin, ITokensSource, IMapDataConsumer
    {
        public string Description => "my plugin description";
        public string Name => "my plugin name";
        public string Author => "my name";
        public string Url => "Plugin homepage url(github/site)";

        private ISettings Settings;
        private ILogger Logger;
        private Tokens.TokenSetter tokenSetter;
        public static ConfigEntry lastMapConfigEntry = new ConfigEntry("myConfigName", "defaultValue");
        public MyPlugin(ISettings settings, ILogger logger)
        {
            Settings = settings;
            Logger = logger;
            tokenSetter = Tokens.CreateTokenSetter("MyPlugin");
            Logger.Log(settings.Get<string>(lastMapConfigEntry), LogLevel.Trace);
        }

        public Task CreateTokensAsync(IMapSearchResult map, CancellationToken cancellationToken)
        {
            //do: update token values
            //do: execute actions based on map search results
            //don't: execute actions based on token values from other plugins

            tokenSetter("someTokenName", "token value", TokenType.Normal, "{0}", "default value", OsuStatus.Playing | OsuStatus.Watching);
            Settings.Add(lastMapConfigEntry.Name, map.MapSearchString);
            Logger.Log("CreateTokensAsync", LogLevel.Trace);
            return Task.CompletedTask;
        }

        public Task SetNewMapAsync(IMapSearchResult map, CancellationToken cancellationToken)
        {
            //do: execute actions based on token values
            //don't: update token values(unless these are live)

            if (map.PlayMode == CollectionManager.Enums.PlayMode.Osu && map.BeatmapsFound.Count > 0)
            {
                var beatmap = map.BeatmapsFound[0];
                var starRating = (double)Tokens.AllTokens["mStars"].Value;
            }

            Logger.Log("SetNewMapAsync", LogLevel.Trace);
            return Task.CompletedTask;
        }
    }
}

For more understanding when these methods are executed proceed to next section.

Creating settings GUI

In order to create user interface for settings, we will have to do few small modifications to the project. Namely:

  • Use net6.0-windows instead of net standard
  • Specify that we want to use winForms, which is current way of adding settings GUI
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
	<TargetFramework>net5.0-windows</TargetFramework>
	<Nullable>enable</Nullable>
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
	<UseWindowsForms>true</UseWindowsForms>
	<OutputPath>..\SCInstall\Plugins\</OutputPath>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference Include="StreamCompanionTypes" Version="8.0.0" />
	</ItemGroup>
</Project>


 


 






Create new UserControl in the project by right clicking on project name, Add & New item. From the list select UserControl and name it. We'll name it SettingsUserControl for this plugin.

Project add new item

Now that we have UserControl in our plugin lets provide it to StreamCompanion by implementing ISettingsSource:

using StreamCompanionTypes.Interfaces;
using StreamCompanionTypes.Enums;
using StreamCompanionTypes.Interfaces.Services;
using StreamCompanionTypes.Interfaces.Sources;

namespace newTestPlugin
{
    public class MyPlugin : IPlugin, ISettingsSource
    {
        public string Description => "my plugin description";
        public string Name => "my plugin name";
        public string Author => "my name";
        public string Url => "Plugin homepage url(github/site)";
        public string SettingGroup => "myGroupName";
        private SettingsUserControl SettingsUserControl;
        public MyPlugin(ILogger logger)
        {
            logger.Log($"Message from {Name}!", LogLevel.Trace);
        }

        public void Free()
        {
            SettingsUserControl?.Dispose();
        }

        public object GetUiSettings()
        {
            if(SettingsUserControl == null || SettingsUserControl.IsDisposed)
                SettingsUserControl = new SettingsUserControl();

            return SettingsUserControl;
        }
    }
}







 





 
 





 
 
 
 
 
 
 
 
 
 
 
 


First we have to provide SettingGroup with will be used as group name in settings tabs. As for 2 methods:

  • GetUiSettings() - This should be used for initalizing your userControl. Every time user navigates to your SettingGroup this will get called.
  • Free() - Destroy UserControl instance and do any other necessary cleanup work. Every time user navigates away from your SettingGroup this will get called.

With that done it's now up to you, to decide how you want to handle/design your UserControl. I'd suggest to either: