Creating a marmalade.
Practice
This section will guide you through how to create a simple custom marmalade. You can find the entirety of this code hosted here
Prerequisite
There are currently two ways of creating a marmalade and you can view both of them using the tabs below.
Prerequisite
- git installed
Guide
Open your favorite terminal and navigate to a folder where you will keep your project .
Install the ellie-marmalade template
git clone https://toastielab.dev/Emotions-stuff/ellie-marmalade
cd ellie-marmalade
dotnet new install .\
Create a new folder and move into it
mkdir example_marmalade
cd example_marmalade
Make a new Ellie Marmalade project
dotnet new ellie-marmalade
- This can be any name you want you just have to specify
-n <any name you want here>
after the first part of the command - Here is an example
dotnet new ellie-marmalade -n my-cool-marmalade
- This will create a marmalade project with the name my-cool-marmalade
Now follow the instructions below and you should be good to go.
Guide
This requires writing a little bit of code but we will help you through it as much as we can.
Without any further issues we shall now begin
Open your favorite terminal and navigate to a folder where you will keep your project .
Create a new folder
mkdir example_marmalade
Create a new .net class library
dotnet new classlib
Open the current folder with your favorite editor/IDE. In this case we’ll use VsCode
code .
Remove the
Class1.cs
fileReplace the contents of the
.csproj
file with the following contents
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- Reduces some boilerplate in your .cs files -->
<ImplicitUsings>enable</ImplicitUsings>
<!-- Use latest .net features -->
<LangVersion>preview</LangVersion>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<GenerateRequiresPreviewFeaturesAttribute>true</GenerateRequiresPreviewFeaturesAttribute>
<!-- tell .net that this library will be used as a plugin -->
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<!-- Base marmalade package. You MUST reference this in order to have a working marmalade -->
<!-- Also, this package comes from Toastielab, which requires you to have a NuGet.Config file next to your .csproj -->
<PackageReference Include="Ellie.Marmalade" Version="5.*">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- Note: If you want to use EllieBot services etc... You will have to manually clone
the https://toastielab.dev/Emotions-stuff/elliebot repo locally and reference the EllieBot.csproj because there is no EllieBot package atm.
It is strongly recommended that you checkout a specific tag which matches your version of ellie,
as there could be breaking changes even between minor versions of EllieBot.
For example if you're running EllieBot 4.1.0 locally for which you want to create a marmalade for,
you should do "git checkout 4.1.0" in your EllieBot solution and then reference the EllieBot.csproj
-->
</ItemGroup>
<!-- Copy shortcut and full strings to output (if they exist) -->
<ItemGroup>
<None Update="res.yml;cmds.yml;strings/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
- Create a
MyCanary.cs
file and add the following contents
using EllieBot.Marmalade;
using Discord;
public sealed class MyCanary : Canary
{
[cmd]
public async Task Hello(AnyContext ctx)
{
await ctx.Channel.SendMessageAsync($"Hello everyone!");
}
[cmd]
public async Task Hello(AnyContext ctx, IUser target)
{
await ctx.ConfirmLocalizedAsync("hello", target);
}
}
- Create
res.yml
andcmds.yml
files with the following contents
res.yml
marmalade.description: "This is my marmalade's description"
hello: "Hello {0}, from res.yml!"
cmds.yml
hello:
desc: "This is a basic hello command"
args:
- ""
- "@Someone"
- Add
NuGet.Config
file which will let you use the base Ellie.Marmalade package. This file should always look like this and you shouldn’t change it
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="toastielab.dev" value="https://toastielab.dev/api/packages/ellie/nuget/index.json" protocolVersion="3" />
</packageSources>
</configuration>
Build it
Build your Marmalade into a dll that Ellie can load. In your terminal, type:
dotnet publish -o bin/marmalades/example_marmalade /p:DebugType=embedded
Done. You can now try it out in action.
Try it out
Copy the
bin/marmalades/example_marmalade
folder into your EllieBot’sdata/marmalades/
folder. (Ellie version 4.1.0+)Load it with
.maload example_marmalade
In the channel your bot can see, run the following commands to try it out
.hello
and.hello @<someone>
Check its information with
.mainfo example_marmalade
Unload it
.maunload example_marmalade
🎉 Congrats! You’ve just made your first marmalade! 🎉
Theory
Marmalade system allows you to write independent marmalades (known as “modules”, “cogs” or “plugins” in other software) which you can then load, unload and update at will without restarting the bot.
The marmalade base classes used for development are open source here in case you need reference, as there is no generated documentation at the moment.
Term list
Marmalade
- The project itself which compiles to a single
.dll
(and some optional auxiliary files), it can contain multiple Canaries, Services, and ParamParsers
Canary
- A class which will be added as a single Module to EllieBot on load. It also acts as a lifecycle handler and as a singleton service with the support for initialize and cleanup.
- It can contain a Canary (called SubCanary) but only 1 level of nesting is supported (you can only have a canary contain a subcanary, but a subcanary can’t contain any other canaries)
- Canaries can have their own prefix
- For example if you set this to ’test’ then a command called ‘cmd’ will have to be invoked by using
.test cmd
instead of.cmd
- For example if you set this to ’test’ then a command called ‘cmd’ will have to be invoked by using
Canary Command
- Acts as a normal command
- Has context injected as a first argument which controls where the command can be executed
AnyContext
the command can be executed in both DMs and ServersGuildContext
the command can only be executed in ServersDmContext
the command can only be executed in DMs
- Support the usual features such as default values, leftover, params, etc.
- It also supports dependency injection via
[inject]
attribute. These dependencies must come after the context and before any input parameters - Supports
ValueTask
,Task
,Task<T>
andvoid
return types
Param Parser
- Allows custom parsing of command arguments into your own types.
- Overriding existing parsers (for example for IGuildUser, etc…) can cause issues.
Service
- Usually not needed.
- They are marked with a
[svc]
attribute, and offer a way to inject dependencies to different parts of your marmalade. - Transient and Singleton lifetimes are supported.
Localization
Response and command strings can be kept in one of three different places based on whether you plan to allow support for localization
option 1) res.yml
and cmds.yml
If you don’t plan on having your app localized, but you just may in the future, you should keep your strings in the res.yml
and cmds.yml
file the root folder of your project, and they will be automatically copied to the output whenever you build your marmalade.
Example project folder structure:
- uwu/
- uwu.csproj
- uwu.cs
- res.yml
- cmds.yml
Example output folder structure:
- marmalades/uwu/
- uwu.dll
- res.yml
- cmds.yml
option 2) strings
folder
If you plan on having your app localized (or want to allow your consumers to easily add languages themselves), you should keep your response strings in the strings/res/en-us.yml
and your command strings in strings/cmds/en-us.yml
file. This will be your base file, and from there you can make support for additional languages, for example strings/res/ru-ru.yml
and strings/cmds/ru-ru.yml
Example project folder structure:
- uwu/
- uwu.csproj
- uwu.cs
- strings/
- res/
- en-us.yml
- ru-ru.yml
- cmds/
- en-us.yml
- ru-ru.yml
Example output folder structure:
- marmalades/uwu/
- uwu.dll
- strings/
- res/
- en-us.yml
- ru-ru.yml
- cmds/
- en-us.yml
- ru-ru.yml
option 3) In the code
If you don’t want any auxiliary files, and you don’t want to bother making new .yml files to keep your strings in, you can specify the command strings directly in the [cmd]
attribute itself, and use non-localized methods for message sending in your commands.
If you update your response strings .yml file(s) while the marmalade is loaded and running, running .stringsreload
will reload the responses without the need to reload the marmalade or restart the bot.
Bot marmalade config file
- Marmalade config is kept in
marmalades/marmalade.yml
file - At the moment this config only keeps track of which marmalades are currently loaded (they will also be always loaded at startup)
- If a marmalade is causing issues and you’re unable to unload it, you can remove it from the
loaded:
list in this config file and restart the bot. It won’t be loaded next time the bot is started up
Unloadability issues
To make sure your marmalade can be properly unloaded/reloaded you must:
Make sure that none of your types and objects are referenced by the Bot or Bot’s services after the DisposeAsync is called on your Canary instances.
Make sure that all of your commands execute quickly and don’t have any long running tasks, as they will hold a reference to a type from your assembly
If you are still having issues, you can always run
.maunload
followed by a bot restart, or if you want to find what is causing the marmalade unloadability issues, you can check the microsoft’s assembly unloadability debugging guide