Script Modules
Creating custom script modules in Moongate v2.
Overview
Script modules allow you to expose .NET functionality to Lua scripts. This enables powerful customization without recompiling the server.
Creating a Module
Basic Module
using Moongate.Scripting.Attributes;
[ScriptModule("custom")]
public sealed class CustomModule
{
[ScriptFunction("greet")]
public string Greet(string name)
{
return $"Hello, {name}! Welcome to Moongate v2.";
}
[ScriptFunction("add")]
public int Add(int a, int b)
{
return a + b;
}
}
Usage in Lua
-- Use custom module
local message = custom.greet("Player")
log.info(message) -- "Hello, Player! Welcome to Moongate v2."
local sum = custom.add(5, 3)
log.info("Sum: " .. sum) -- "Sum: 8"
Module Registration
Automatic Registration
Modules are automatically registered through source generation.
- Any class decorated with
[ScriptModule(...)]inMoongate.ScriptingorMoongate.Serveris picked up at compile time. Moongate.Generatorsemits:Moongate.Scripting.Generated.ScriptModuleRegistryMoongate.Server.Generated.ScriptModuleRegistry
- Bootstrap calls both registries during startup.
- Each discovered module is registered via
container.RegisterScriptModule<TModule>().
This removes runtime reflection scanning for module discovery.
Manual Registration
// In Program.cs or startup code
services.AddSingleton<CustomModule>();
// Then in bootstrap
var customModule = serviceProvider.GetRequiredService<CustomModule>();
luaEngine.RegisterModule(customModule);
ScriptFunction Attribute
Basic Function
[ScriptFunction("function_name")]
public void MyFunction()
{
// Function body
}
Function with Parameters
[ScriptFunction("spawn_mobile")]
public Serial SpawnMobile(int bodyId, int hue, int x, int y, int z)
{
var mobile = _mobileService.Create(bodyId, hue, new Point3D(x, y, z));
return mobile.Serial;
}
Function with Return Value
[ScriptFunction("get_player_count")]
public int GetPlayerCount()
{
return _sessionManager.ActiveCount;
}
Async Function
[ScriptFunction("save_world")]
public async Task SaveWorldAsync()
{
await _persistenceService.SaveSnapshotAsync(CancellationToken.None);
}
Function with Optional Parameters
[ScriptFunction("broadcast")]
public void Broadcast(string message, bool adminOnly = false)
{
foreach (var session in _sessionManager.GetAllSessions())
{
if (!adminOnly || session.IsAdmin)
{
session.SendPacket(new BroadcastPacket(message));
}
}
}
ScriptConstant Attribute
Exposing Constants
[ScriptModule("server")]
public sealed class ServerModule
{
[ScriptConstant("VERSION")]
public string Version => "0.7.0";
[ScriptConstant("MAX_PLAYERS")]
public int MaxPlayers => 1000;
[ScriptConstant("UO_PROTOCOL_VERSION")]
public int ProtocolVersion => 7;
}
Usage in Lua
-- Access constants
log.info("Server version: " .. server.VERSION)
log.info("Max players: " .. server.MAX_PLAYERS)
if player_count >= server.MAX_PLAYERS then
log.warning("Server is full!")
end
Dependency Injection
Constructor Injection
[ScriptModule("game")]
public sealed class GameModule
{
private readonly ILogger<GameModule> _logger;
private readonly IMobileService _mobileService;
private readonly IGameNetworkSessionService _sessionService;
public GameModule(
ILogger<GameModule> logger,
IMobileService mobileService,
IGameNetworkSessionService sessionService)
{
_logger = logger;
_mobileService = mobileService;
_sessionService = sessionService;
}
[ScriptFunction("spawn_npc")]
public Serial SpawnNpc(string npcId, int x, int y, int z)
{
_logger.LogInformation("Spawning NPC {NpcId} at {Location}",
npcId, new Point3D(x, y, z));
var template = _npcTemplateService.GetTemplate(npcId);
var mobile = _mobileService.Create(template.Body, template.Hue, new Point3D(x, y, z));
return mobile.Serial;
}
}
Error Handling
Try-Catch in Modules
[ScriptFunction("safe_operation")]
public ScriptResult SafeOperation(int value)
{
try
{
var result = PerformRiskyOperation(value);
return ScriptResult.Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Operation failed");
return ScriptResult.Error("Operation failed: " + ex.Message);
}
}
Validation
[ScriptFunction("teleport_player")]
public ScriptResult TeleportPlayer(Serial playerSerial, int x, int y, int z)
{
// Validate coordinates
if (x < 0 || x > 5119 || y < 0 || y > 4095)
{
return ScriptResult.Error("Invalid coordinates");
}
// Validate player exists
var player = _mobileService.GetBySerial(playerSerial);
if (player == null)
{
return ScriptResult.Error("Player not found");
}
// Perform teleport
_mobileService.Teleport(player, new Point3D(x, y, z));
return ScriptResult.Success();
}
Advanced Patterns
Event Subscription
[ScriptModule("events")]
public sealed class EventModule : IDisposable
{
private readonly IGameEventBusService _eventBus;
private readonly LuaScriptEngineService _luaEngine;
public EventModule(IGameEventBusService eventBus, LuaScriptEngineService luaEngine)
{
_eventBus = eventBus;
_luaEngine = luaEngine;
// Subscribe to events
_eventBus.Subscribe<PlayerConnectedEvent>(OnPlayerConnected);
}
private void OnPlayerConnected(PlayerConnectedEvent evt)
{
// Call Lua callback
_luaEngine.CallFunction("on_player_connected", evt.PlayerSerial);
}
public void Dispose()
{
_eventBus.Unsubscribe<PlayerConnectedEvent>(OnPlayerConnected);
}
}
State Management
[ScriptModule("storage")]
public sealed class StorageModule
{
private readonly ConcurrentDictionary<string, object> _storage = new();
[ScriptFunction("set")]
public void Set(string key, object value)
{
_storage[key] = value;
}
[ScriptFunction("get")]
public object? Get(string key)
{
return _storage.GetValueOrDefault(key);
}
[ScriptFunction("delete")]
public bool Delete(string key)
{
return _storage.TryRemove(key, out _);
}
[ScriptFunction("clear")]
public void Clear()
{
_storage.Clear();
}
}
Best Practices
DO:
- Use dependency injection for services
- Validate all input parameters
- Handle exceptions gracefully
- Use async/await for I/O operations
- Document your modules with XML comments
- Keep modules focused and single-purpose
DON'T:
- Expose sensitive operations without authorization
- Block in script functions (use async)
- Store mutable state without thread safety
- Create circular dependencies between modules
- Expose internal implementation details
Complete Example
Chat Command Module
using System.Collections.Concurrent;
using Moongate.Scripting.Attributes;
/// <summary>
/// Provides chat command functionality to Lua scripts.
/// </summary>
[ScriptModule("commands")]
public sealed class ChatCommandModule
{
private readonly ILogger<ChatCommandModule> _logger;
private readonly IGameNetworkSessionService _sessionService;
private readonly ConcurrentDictionary<string, Func<Serial, string, Task>> _commands = new();
public ChatCommandModule(
ILogger<ChatCommandModule> logger,
IGameNetworkSessionService sessionService)
{
_logger = logger;
_sessionService = sessionService;
// Register built-in commands
RegisterCommand("help", OnHelpCommand);
RegisterCommand("online", OnOnlineCommand);
}
/// <summary>
/// Registers a new chat command.
/// </summary>
[ScriptFunction("register")]
public void RegisterCommand(string name, Func<Serial, string, Task> handler)
{
_commands[name.ToLower()] = handler;
_logger.LogInformation("Registered chat command: {Command}", name);
}
/// <summary>
/// Processes a chat command from a player.
/// </summary>
[ScriptFunction("process")]
public async Task<ScriptResult> ProcessCommandAsync(Serial playerSerial, string text)
{
if (!text.StartsWith("/"))
{
return ScriptResult.Success(false); // Not a command
}
var parts = text[1..].Split(' ', 2);
var commandName = parts[0].ToLower();
var arguments = parts.Length > 1 ? parts[1] : string.Empty;
if (!_commands.TryGetValue(commandName, out var handler))
{
return ScriptResult.Error($"Unknown command: {commandName}");
}
try
{
await handler(playerSerial, arguments);
return ScriptResult.Success(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Command {Command} failed", commandName);
return ScriptResult.Error("Command failed: " + ex.Message);
}
}
private async Task OnHelpCommand(Serial playerSerial, string arguments)
{
var session = _sessionManager.GetGameSession(playerSerial);
session?.SendPacket(new MessagePacket("Available commands: /help, /online"));
}
private async Task OnOnlineCommand(Serial playerSerial, string arguments)
{
var session = _sessionManager.GetGameSession(playerSerial);
var count = _sessionManager.ActiveCount;
session?.SendPacket(new MessagePacket($"Players online: {count}"));
}
}
Usage in Lua
-- Register custom command
commands.register("announce", function(playerSerial, args)
local player = game.get_player(playerSerial)
if not player.IsAdmin then
return false, "You must be an admin!"
end
game.broadcast("[ANNOUNCEMENT] " .. player.Name .. ": " .. args)
return true
end)
-- Process command (called from on_player_speech)
function on_player_speech(player, text)
local success, result = commands.process(player.Serial, text)
if success and result then
return true -- Command was processed
end
return false -- Normal speech
end
Next Steps
- API Reference - Complete scripting API
- Overview - Scripting introduction
- Event System - Event-driven architecture
Previous: Overview | Next: API Reference