Commands
Chat commands with arguments, groups, aliases, permissions and preconditions.
Palforge hooks the server's chat entry point and turns prefixed chat messages into method calls. You
write a command as a method on a ChatCommandModule; the host discovers, binds and dispatches it — no
manual registration.
A first command
using Palforge.Commands.Attributes;
using Palforge.Commands.Modules.Chat;
public sealed class FunCommands : ChatCommandModule
{
[Command("ping")]
[CommandDescription("Replies with pong.")]
public void Ping() => Reply("pong");
}With the default ! prefix, a player types !ping and Palforge
whispers pong back. When a command matches, the raw text is not broadcast to chat.
The command prefix and the sender name shown on replies are configured in
Configuration.cfg under [Commands] (Prefix, SenderName). The
default prefix is !.
The command module
Command methods live on a class deriving from ChatCommandModule, which gives you the context and three
reply helpers. A new module instance is created per invocation via DI, so constructor injection
works.
| Helper | Sends |
|---|---|
Reply(message, args) | A whisper to the caller only. |
Broadcast(message, args) | A global chat message to everyone. |
Notify(message, args) | A server-notice banner. |
All three take a composite format string ("{0}", "{1}", …). The context exposes the caller:
Context.Player // PalPlayerController — the caller
Context.PlayerState // PalPlayerState? — Player.GetPalPlayerState()
Context.IsAdministrator // bool — the player's admin flag
Context.Input // the original raw chat messageA command method must return void and be an instance method. Reply through Reply /
Broadcast / Notify, not a return value. (private command methods are fine — the binder finds them.)
Arguments
Method parameters become command arguments, converted from the chat text. Tokens split on whitespace,
and double quotes group a token: !tp "Some Player" passes Some Player as one argument.
[Command("give")]
public void Give(string item, int count = 1) // !give Sphere 5 (count optional → defaults to 1)
{
Reply("Giving {0}x {1}.", count, item);
}Rules the binder enforces:
- Each parameter type needs a registered argument reader.
- Optional parameters (C# default values) are supported and must come last.
paramsarrays collect the remaining tokens.- A missing required argument, a conversion failure, or too many tokens produces a clear reply to the
caller (
Missing argument '…',Argument '…': …,Too many arguments.).
Capturing the rest of the line
Mark the final string parameter [CommandRemainder] to capture everything after the command as one
value:
[Command("say")]
public void Say([CommandRemainder] string message) => Notify("{0}", message); // !say hello everyoneArgument readers
These types are supported out of the box:
string, and everyISpanParsable<T>primitive —bool,int,long,float,double,decimal,Guid,DateTime,TimeSpan, and more (parsed with the invariant culture).- Any enum (case-insensitive by name).
Nullable<T>of any supportedT(the literalnullyields no value).PalPlayerController— resolves an online player by exact, case-insensitive name.
[Command("kick")]
public void Kick(PalPlayerController target, [CommandRemainder] string reason = "No reason")
{
Broadcast("{0} was kicked: {1}", target.GetPalPlayerState()?.PlayerNamePrivate, reason);
}To support a custom type, ship an ArgumentReader<T> in your plugin assembly — it's auto-discovered
(with DI) and overrides the built-ins for that type:
using Palforge.Commands.Arguments;
using Palforge.Commands.Modules;
public sealed class ItemReader : ArgumentReader<ItemId>
{
public override bool TryRead(CommandContext context, ReadOnlySpan<char> input,
out ItemId value, out string? errorMessage)
{
// parse `input` → value, or set errorMessage and return false
}
}Groups and aliases
Group related commands under a prefix with [CommandGroup] on the class; add extra names with
[CommandAliases] (on a method for command aliases, on a class for group aliases):
[CommandGroup("admin")]
[CommandAliases("a")] // group is "admin" or "a"
public sealed class AdminCommands : ChatCommandModule
{
[Command("teleport")]
[CommandAliases("tp")] // command is "teleport" or "tp"
[CommandDescription("Teleport to a named online player.")]
public void Teleport(PalPlayerController target)
{
// invoked as: !admin teleport "Some Player" (or !a tp "Some Player")
Reply("Teleporting to {0}.", target.GetPalPlayerState()?.PlayerNamePrivate);
}
}Grouped commands are invoked as <prefix><group> <command> <args…>; ungrouped as
<prefix><command> <args…>.
Permissions and preconditions
Permissions
[CommandPermissions(...)] gates a command (or every command in a class) behind named permissions:
[CommandPermissions("admin.teleport")]
[Command("teleport")]
public void Teleport(PalPlayerController target) { /* … */ }By default, permission checks resolve to the player's admin flag — any named permission passes only
for administrators. To implement real, granular permissions, register your own PermissionProvider in
ConfigureServices:
public sealed class MyPermissions : PermissionProvider
{
public override bool HasPermission(CommandContext context, string permission)
{
// look up the player (context.Player) against your own roles/permissions
}
}
// in PluginConfiguration.ConfigureServices:
services.AddSingleton<PermissionProvider, MyPermissions>();Custom preconditions
For arbitrary gates, subclass CommandPrecondition (allowed on a method or class, multiple times) and
return PreconditionResult.Success or PreconditionResult.Fail("why"). On failure, the message is sent
to the caller and the command doesn't run:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class RequireOverworld : CommandPreconditionAttribute
{
public override PreconditionResult Check(CommandContext context, IServiceProvider services)
=> context.IsAdministrator
? PreconditionResult.Success
: PreconditionResult.Fail("Admins only.");
}Built-in admin commands
Palforge itself registers a plugin command group (admin-only) for managing plugins live —
!plugin list, !plugin load <name>, !plugin reload <name>, !plugin unload <name>. See
Unload and reload.