Skip to main content

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()
warning

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)
tip

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

KeyDescription
PCaller player (formats as colored name)
TTarget player(s) (formats as colored name list)
DDuration (formats as human readable time)
AnyCustom 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.):

warning

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

warning

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()
info

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:

FieldDescription
valueCurrent parameter value
callerPlayer executing the command
cmdCommand object
metadataParameter options table
param_indexParameter position (1-based)
sourceSOURCE_CHAT, SOURCE_CONSOLE, or SOURCE_MENU
resultsPreviously parsed arguments

Command Modifiers Reference

MethodDescription
: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