Skip to content
Tildev0.6.1

Commands, Lua modules and events

This page shows how a plugin adds a command, a Lua module and an event that the scripts can follow. The code is a small plugin I built against the interfaces and ran with a test: a hello.say command, a hello module and a hello_said event.

A Lua module or event needs the scripting plugin, so the plugin lists it among its dependencies and its project references src/Tilde.Scripting/Tilde.Scripting.csproj:

public PluginMetadata Metadata { get; } = new("example.hello", "Hello", new Version(1, 0))
{
Author = "Ada (ada@example.org)",
Dependencies = [new PluginDependency("tilde.scripting")]
};

Without it, AddScriptModule throws The scripting plugin is not registered: add tilde.scripting to the dependencies of the plugin. A plugin that only adds commands does not need it.

public async ValueTask RegisterAsync(Container container, IConfigService config, CancellationToken cancellationToken)
{
var hello = await config.AddSectionAsync<HelloConfig>("hello", cancellationToken);
container.RegisterInstance(hello);
container.AddMoongateService<HelloService, HelloService>((int)ServicePriorityType.Plugins);
container.AddScriptModule<HelloModule>();
container.AddScriptEvent<HelloSaidEvent>(
"hello_said",
e => new Dictionary<string, object?> { ["name"] = e.Name }
);
}

AddScriptModule and AddScriptEvent are in Tilde.Scripting.Extensions (ScriptingContainerExtensions.cs). They work only here, before the Lua engine starts.

A command is added to ICommandService while the application starts, by a service of the plugin. RegisterAsync cannot do it, because it must not resolve services. The OpenPGP plugin does it this way in PgpKeyService.cs.

public sealed record HelloSaidEvent(string Name) : IEvent;
public sealed class HelloService : IMoongateStartupService
{
private readonly ICommandService _commands;
private readonly IEventHubService _hub;
private readonly HelloConfig _config;
private IDisposable? _command;
public HelloService(ICommandService commands, IEventHubService hub, HelloConfig config)
{
_commands = commands;
_hub = hub;
_config = config;
}
public Task StartAsync()
{
_command = _commands.AddCommand(
"hello.say",
async (context, token) =>
{
var name = context.Arguments.Count > 0 ? context.Arguments[0] : "world";
await _hub.PublishAsync(new HelloSaidEvent(name), token);
return $"{_config.Greeting}, {name}";
},
"Says hello to a name."
);
return Task.CompletedTask;
}
public Task StopAsync()
{
_command?.Dispose();
return Task.CompletedTask;
}
}
  • A name is lower case, may be grouped with dots (hello.say) and has at most 64 characters. The description is cut to 120 characters.
  • The handler returns the message to show, or null for none. Simpler overloads take an Action, a Func<string?> or a Func<IReadOnlyList<string>, string?>.
  • A second command with the same name replaces the first, but a script command cannot replace a native one.
  • AddCommand returns a handle: disposing it removes the command.

The same command then runs from the : line of tt, from a key bound to its name, and from Lua with commands.run("hello.say", "Ada").

A module is a class with [ScriptModule], and each [ScriptFunction] method is a function of it. The attributes are in Moongate.Scripting.Attributes.Scripts.

[ScriptModule("hello", "Says hello.")]
public sealed class HelloModule
{
private readonly HelloConfig _config;
public HelloModule(HelloConfig config)
{
_config = config;
}
[ScriptFunction(helpText: "hello.greeting() returns the greeting of the [hello] section.")]
public string Greeting()
{
return _config.Greeting;
}
}

The method Greeting is hello.greeting() in Lua. The container creates the module once, so its constructor can ask for services. A larger module is RulesModule.cs, which takes Lua tables.

An event is a record that implements IEvent, published on IEventHubService with PublishAsync, as hello.say does above. AddScriptEvent<TEvent>(name, map) carries it to the scripts:

  • The name is ASCII lower_snake_case and starts with a letter. Each name is registered once.
  • The map turns the event into the fields a script gets: strings, booleans, numbers, dates, enums, lists and dictionaries. A map that throws drops that event, with a log line that names the type of the exception.

C# code follows an event with SubscribeAsync<TEvent>(handler), which returns a handle that removes the handler when it is disposed. Subscribe in StartAsync, not in RegisterAsync.

This init.lua uses all three:

local last = "none"
events.on("hello_said", function(e) last = e.name end)
commands.add("check.greeting", function() return hello.greeting() end, "test")
commands.add("check.last", function() return last end, "test")

With greeting = "Hi" in [hello], the command line hello.say Ada says Hi, Ada, check.greeting says Hi, and then check.last says Ada. The Lua side is on Commands and Events.

Next: Test a plugin.