Creating Commands
Basic Structure
local Command = Lyn.Command
Command("commandname")
:Permission("permission_name", "default_role")
:Param("type", { options })
:Execute(function(caller, arg1, arg2, ...)
-- Your code here
end)
:Add()
Always call :Add() at the end to register the command.
Setting Categories
Group commands by category for organization in the menu:
Command.SetCategory("Fun")
-- All commands below inherit this category
Command("slap"):Category("Fun") -- or override per-command
Permissions
-- Permission with default role
:Permission("kick", "admin")
-- Permission with multiple default roles
:Permission("vote", {"user", "vip"})
-- Permission with all roles (using Lyn.Role.Defaults())
:Permission("pm", Lyn.Role.Defaults())
-- No permission (anyone can use)
-- Simply omit :Permission()
Execute Function
The execute function receives the caller followed by parsed arguments in order:
:Param("player")
:Param("number", { hint = "amount" })
:Param("string", { hint = "reason", optional = true })
:Execute(function(caller, targets, amount, reason)
-- caller: Player who ran the command
-- targets: table of players (even single_target returns a table)
-- amount: number value
-- reason: string or nil
end)
The player parameter always returns a table of players. Use targets[1] for single target commands.
Notifications
Use LYN_NOTIFY to broadcast command results:
-- Notify everyone
LYN_NOTIFY("*", "#lyn.commands.heal.notify", {
P = caller, -- {P} in translation = caller name
T = targets, -- {T} in translation = target names
amount = 100 -- %amount% in translation
})
-- Notify specific players
LYN_NOTIFY(targets, "#lyn.commands.pm.received", { message = msg })
-- Notify caller only
LYN_NOTIFY(caller, "#lyn.commands.error.not_found")
Notification Data Keys
| Key | Description |
|---|---|
P | Caller player (formats as colored name) |
T | Target player(s) (formats as colored name list) |
D | Duration (formats as human readable time) |
| Any | Custom data for translation placeholders |
Common Patterns
Player Targeting Command
Command("heal")
:Permission("heal", "admin")
:Param("player", { default = "^" }) -- defaults to self
:Param("number", { hint = "amount", default = 100, min = 1, max = 100000 })
:Execute(function(caller, targets, amount)
for _, ply in ipairs(targets) do
ply:SetHealth(amount)
end
LYN_NOTIFY("*", "#lyn.commands.heal.notify", { P = caller, T = targets, amount = amount })
end)
:Add()
Timed Action with Reason
Command("mute")
:Permission("mute", "admin")
:Param("player")
:Param("duration", { default = "5m", min = 0 })
:Param("string", { hint = "reason", optional = true })
:GetRestArgs() -- Allows spaces in reason without quotes
:Execute(function(caller, targets, duration, reason)
if not reason then
reason = Lyn.I18n.t("#lyn.unspecified")
end
for _, ply in ipairs(targets) do
-- Apply mute logic
end
LYN_NOTIFY("*", "#lyn.commands.mute.notify", {
P = caller,
T = targets,
D = duration,
reason = reason
})
end)
:Add()
Single Target Command
Command("pm")
:Permission("pm", Lyn.Role.Defaults())
:Param("player", {
single_target = true, -- Only one player allowed
cant_target_self = true, -- Can't message yourself
allow_higher_target = true -- Can PM admins even if lower rank
})
:Param("string", {
hint = "message",
check = function(ctx)
return ctx.value and ctx.value:match("%S") ~= nil
end
})
:GetRestArgs()
:Execute(function(caller, targets, message)
local target = targets[1]
Lyn.Player.Chat.Send(caller, "To " .. target:Name() .. ": " .. message)
Lyn.Player.Chat.Send(target, "From " .. caller:Name() .. ": " .. message)
end)
:Add()
Hidden UI Command
For commands that open menus or custom UI (MOTD, rules, etc.):
When using Net.StartSV / Net.HookCL, you must register your net message keys as constants. See Constants for details.
local Net = Lyn.GoobieCore.Net
Command("menu")
:DenyConsole() -- Can't run from console
:NoConsoleLog() -- Don't log to console
:NoMenu() -- Hide from command menu
:Execute(function(ply)
Net.StartSV("Menu.Open", ply)
end)
:Add()
if CLIENT then
Net.HookCL("Menu.Open", function()
Lyn.Menu.Open()
end)
end
MOTD / Rules Page Example
When using Net.StartSV / Net.HookCL, you must register your net message keys as constants. See Constants for details.
local Net = Lyn.GoobieCore.Net
Command("motd")
:Aliases("rules", "info")
:DenyConsole()
:NoConsoleLog()
:Execute(function(ply)
Net.StartSV("MOTD.Show", ply)
end)
:Add()
if CLIENT then
Net.HookCL("MOTD.Show", function()
-- Open your custom MOTD panel
local frame = vgui.Create("DFrame")
frame:SetSize(600, 400)
frame:Center()
frame:MakePopup()
frame:SetTitle("Server Rules")
local html = frame:Add("DHTML")
html:Dock(FILL)
html:OpenURL("https://yourserver.com/rules")
end)
end
Ban Command with SteamID64
Command("banid")
:Permission("banid", "admin")
:Param("steamid64")
:Param("duration", { default = 0 }) -- 0 = permanent
:Param("string", { hint = "reason", optional = true })
:GetRestArgs()
:Execute(function(caller, steamid_promise, duration, reason)
if not reason then
reason = Lyn.I18n.t("#lyn.unspecified")
end
local steamid64 = steamid_promise.steamid64
steamid_promise:Handle(function()
Lyn.Player.BanSteamID64(steamid64, duration, reason, caller:SteamID64(), function(err)
if err then
Lyn.Player.Chat.Send(caller, "#lyn.commands_core.failed_to_run")
return
end
LYN_NOTIFY("*", "#lyn.commands.banid.notify", {
P = caller,
target_steamid64 = steamid64,
D = duration,
reason = reason
})
end)
end)
end)
:Add()
The steamid64 parameter returns a promise object. Use :Handle() to execute code after permission checks complete.
Role Management Command
Command("giverole")
:Permission("giverole")
:Param("player", { single_target = true })
:Param("role", {
check = function(ctx)
-- Only allow roles the caller can target
return ctx.value and ctx.caller:CanTargetRole(ctx.value)
end
})
:Param("duration", { default = 0 })
:Execute(function(caller, targets, role, duration)
local target = targets[1]
Lyn.Player.Role.Add(target, role, duration, function(err)
if err then
Lyn.Player.Chat.Send(caller, "#lyn.commands_core.failed_to_run")
return
end
LYN_NOTIFY("*", "#lyn.commands.giverole.notify", {
P = caller,
T = targets,
role = Lyn.Role.GetDisplayName(role),
D = duration
})
end)
end)
:Add()
Map Change Command
Command("map")
:Permission("map")
:Param("map", { exclude_current = true })
:Param("duration", { default = 10 })
:Execute(function(caller, mapname, delay)
LYN_NOTIFY("*", "#lyn.commands.map.notify", { P = caller, D = delay })
timer.Create("MapChange", delay, 1, function()
RunConsoleCommand("changelevel", mapname)
end)
end)
:Add()
Custom Parameter Validation
Use the check option for custom validation:
:Param("string", {
hint = "message",
check = function(ctx)
local value = ctx.value
-- Must not be empty/whitespace
if not value or not value:match("%S") then
return false
end
-- Max 200 characters
if #value > 200 then
return false
end
return true
end
})
Check Context
The ctx table contains:
| Field | Description |
|---|---|
value | Current parameter value |
caller | Player executing the command |
cmd | Command object |
metadata | Parameter options table |
param_index | Parameter position (1-based) |
source | SOURCE_CHAT, SOURCE_CONSOLE, or SOURCE_MENU |
results | Previously parsed arguments |
Command Modifiers Reference
| Method | Description |
|---|---|
:Aliases(...) | Add alternative names |
:CustomAlias(name, opts) | Alias with custom prefix options |
:Permission(name, default?) | Set required permission |
:Category(name) | Set category for menu |
:Help(text) | Set help text |
:Param(type, opts) | Add parameter |
:GetRestArgs() | Capture remaining input as last string param |
:DenyConsole() | Block console execution |
:NoConsoleLog() | Disable console logging |
:NoMenu() | Hide from command menu |
:ChatPrefix(prefix) | Override default chat prefix |
:ConsolePrefix(prefix) | Override default console prefix |
:Execute(func) | Set handler function (server-side) |
:Add() | Register the command |