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.
1. Need the scripting plugin
Section titled “1. Need the scripting plugin”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.
2. Register everything in RegisterAsync
Section titled “2. Register everything in RegisterAsync”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.
3. Add a command
Section titled “3. Add a command”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
nullfor none. Simpler overloads take anAction, aFunc<string?>or aFunc<IReadOnlyList<string>, string?>. - A second command with the same name replaces the first, but a script command cannot replace a native one.
AddCommandreturns 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").
4. Add a Lua module
Section titled “4. Add a Lua module”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.
5. Add an event
Section titled “5. Add an event”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.
Try it from a script
Section titled “Try it from a script”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.