Commands¶
Chat and console commands have the same purpose in both frameworks — a registered name, a permission check, an arguments parser, and a callback. The registration call is similar. What changes is the shape of the arguments table, the signature of OnRun, and how casing is handled.
Table of Contents¶
- Registration API
- Command Name Casing
- Arguments: The Big Change
- Argument Types
- Permission Checks
- OnRun Signature
- A Full Worked Example
- Handling Optional Arguments
- Choice Restrictions
- Running Commands Programmatically
- Pitfalls
Registration API¶
| Helix | Parallax | |
|---|---|---|
| Register | ix.command.Add("Name", data) |
ax.command:Add("name", def) |
| Registry | ix.command.list[lowerName] |
ax.command.registry[name] |
| Lookup | ix.command.FindAll(id, ...) |
ax.command:Find(look) / :FindAll(partial) |
| Run | ix.command.Run(client, cmd, args) |
ax.command:Run(caller, name, args) |
| Parse text | ix.command.Parse(client, text, realCmd, args) |
ax.command:Parse(text) |
| Access check | ix.command.HasAccess(client, cmd) |
ax.command:HasAccess(caller, def) |
The function names are mostly parallel. The two key call-site changes are dot → colon, and casing.
Command Name Casing¶
Helix commands are registered with PascalCase names by convention: ix.command.Add("CharSlap", ...), ix.command.Add("PM", ...). The name is lowercased internally for lookup (ix.command.list.charslap) but the original casing is stored on the command for display.
Parallax normalizes everything to lowercase. You pass "charslap" or "CharSlap"; both produce a registered command keyed by "charslap". For the display name, set displayName explicitly:
ax.command:Add("charslap", {
displayName = "CharSlap", -- what users see in /help output
description = "Slap a character.",
-- ...
})
If you omit displayName, Parallax generates a reasonable default by calling ax.util:UniqueIDToName(name) — "charslap" becomes "Charslap" (first letter capitalized). Good enough for most commands.
Arguments: The Big Change¶
Helix describes command arguments with a flat list of type constants, using bit.bor to combine flags:
-- Helix
ix.command.Add("CharSetMoney", {
description = "Set a character's money.",
adminOnly = true,
arguments = {
ix.type.character,
ix.type.number,
bit.bor(ix.type.string, ix.type.optional),
},
argumentNames = { "target", "amount", "reason" },
OnRun = function(self, client, target, amount, reason)
-- target is the character object
-- amount is a number
-- reason is a string or nil
target:SetMoney(amount)
end,
})
Parallax uses a list of argument descriptor tables, one per argument:
-- Parallax
ax.command:Add("charsetmoney", {
displayName = "CharSetMoney",
description = "Set a character's money.",
adminOnly = true,
arguments = {
{ name = "target", type = ax.type.character },
{ name = "amount", type = ax.type.number, min = 0 },
{ name = "reason", type = ax.type.string, optional = true },
},
OnRun = function(self, client, args)
local target = args[1]
local amount = args[2]
local reason = args[3]
target:SetMoney(amount)
end,
})
What changed:
- Each argument is a table with
type(required),name, and per-type modifiers. optional = truereplaces thebit.bor(type, ix.type.optional)bitmask.ix.type.optionaldoes not exist in Parallax.argumentNamesgoes away — put the name inside each argument descriptor.OnRunreceives(self, caller, args)— a single args table — instead of spread positional arguments.
Per-argument modifiers¶
| Modifier | Applies to | Meaning |
|---|---|---|
optional |
any | Argument may be missing. Later args must also be optional. |
min, max |
ax.type.number |
Numeric bounds (inclusive). |
decimals |
ax.type.number |
Round to N decimal places. |
choices |
ax.type.string, ax.type.text |
Map of valid values; parser rejects input not in this map. |
Argument Types¶
All Parallax type constants cover the Helix equivalents with the same bitmask values:
| Helix | Parallax |
|---|---|
ix.type.string |
ax.type.string |
ix.type.text |
ax.type.text — consumes remainder of input |
ix.type.number |
ax.type.number |
ix.type.bool |
ax.type.bool |
ix.type.player |
ax.type.player |
ix.type.character |
ax.type.character |
ix.type.steamid |
ax.type.steamid |
ix.type.steamid64 |
ax.type.steamid64 |
text behaves the same in both: it consumes the rest of the input as a single string (so an unquoted phrase can be the last argument).
Permission Checks¶
Both frameworks use CAMI-compatible permission hooks and both accept adminOnly / superAdminOnly booleans as shortcuts:
For custom checks:
-- Helix
OnCheckAccess = function(client)
return client:GetCharacter():HasFlag("x")
end
-- Parallax
CanRun = function(caller)
return caller:GetCharacter():HasFlag("x")
end
CAMI privilege registration is automatic. The privilege name differs:
| Framework | Privilege name for /charslap |
|---|---|
| Helix | Helix - CharSlap |
| Parallax | Command - charslap |
If your server uses ULX / sam / serverguard with hand-tuned privileges that reference the Helix names, migrate those entries to the Parallax naming.
OnRun Signature¶
The first two arguments are the same in both frameworks: self (the command def) and client (the caller).
Helix spreads the parsed arguments as positional params:
Parallax packs them into a single args table:
OnRun = function(self, caller, args)
local target = args[1]
local amount = args[2]
local reason = args[3]
end
The reason for the change: Parallax commands can be invoked programmatically with partial args or from console without a player, and a uniform args shape is easier to validate defensively.
If you prefer positional destructuring, do it at the top of the function:
OnRun = function(self, caller, args)
local target, amount, reason = args[1], args[2], args[3]
-- ...
end
A Full Worked Example¶
A whisper command that supports an optional duration.
Helix (plugins/whisper.lua):
ix.command.Add("Whisper", {
description = "Send a private whispered message to someone nearby.",
arguments = {
ix.type.player,
ix.type.text,
},
OnRun = function(self, client, target, message)
if ( client:GetPos():Distance(target:GetPos()) > 100 ) then
return "@toofar"
end
client:ConCommand("") -- clear chat
target:ChatPrint(client:Name() .. " whispers: " .. message)
client:ChatPrint("You whisper to " .. target:Name() .. ": " .. message)
end,
})
Parallax (<your-schema>/gamemode/modules/whisper/boot.lua):
MODULE.name = "Whisper"
ax.command:Add("whisper", {
displayName = "Whisper",
description = "Send a private whispered message to someone nearby.",
arguments = {
{ name = "target", type = ax.type.player },
{ name = "message", type = ax.type.text },
},
OnRun = function(self, caller, args)
local target = args[1]
local message = args[2]
if ( !IsValid(target) ) then
caller:Notify("Target not found.")
return
end
if ( caller:GetPos():Distance(target:GetPos()) > 100 ) then
caller:Notify("You are too far away.")
return
end
target:ChatPrint(caller:Name() .. " whispers: " .. message)
caller:ChatPrint("You whisper to " .. target:Name() .. ": " .. message)
end,
})
return MODULE
Changes from the port:
ix.command.Add→ax.command:Add.- Name lowercased;
displayNameadded. - Arguments converted to descriptor tables with names.
OnRunsignature usesargstable.- Return-a-language-key error style (
"@toofar") replaced with an explicitcaller:Notify(...). Parallax does support phrase lookups viaax.localization:GetPhrase("toofar"), butNotifyhandles the display in the same line.
Handling Optional Arguments¶
-- Helix
arguments = {
ix.type.player,
bit.bor(ix.type.number, ix.type.optional),
},
OnRun = function(self, client, target, amount)
amount = amount or 1
-- ...
end
-- Parallax
arguments = {
{ name = "target", type = ax.type.player },
{ name = "amount", type = ax.type.number, optional = true },
},
OnRun = function(self, caller, args)
local target = args[1]
local amount = args[2] or 1
-- ...
end
Optional arguments must always come after required ones — this is true in both frameworks.
Choice Restrictions¶
Parallax supports a built-in "one of these values" check that Helix does not:
arguments = {
{ name = "difficulty",
type = ax.type.string,
choices = {
easy = true,
medium = true,
hard = true,
},
},
},
Input that doesn't match a key in choices is rejected before OnRun fires, and the user sees a message listing valid choices. When porting Helix commands that manually checked the first arg against a list of valid strings, move that into choices.
Running Commands Programmatically¶
Both frameworks let you run a command from code:
-- Helix
ix.command.Run(client, "CharSetMoney", { targetName, "500" })
-- Parallax
ax.command:Run(caller, "charsetmoney", { targetName, "500" })
The raw-args form is a list of strings; the parser type-converts them as if they had been typed in chat. If you already have typed values (a player entity, a number), you can pass them directly as elements of the args table too — Parallax's converter short-circuits when the value is already the right type.
Pitfalls¶
- Forgetting the outer args table. If
OnRunis still trying to unpack positional arguments, the port is incomplete. All command bodies go through the sameargs[N]pattern. ix.type.optionalas a constant. Does not exist; Parallax usesoptional = trueinside the descriptor.- Uppercase names in
ax.command:Run. Pass lowercase; uppercase is accepted but gets normalized. - Relying on Helix's
@language_keyreturn. The return value from Helix'sOnRunwas displayed to the caller as a notification. Parallax'sOnRunreturn value is ignored; callcaller:Notify(...)orax.util:Notify(caller, ...)explicitly. - CAMI privilege name changes. If a non-Parallax admin mod has pre-configured permissions under the
Helix -prefix, re-grant them underCommand -or your server's command permissions won't carry over. - Console-only commands. Helix had
command.bAllowConsole = false(default false? inconsistent). Parallax defaultsbAllowConsole = true; set it tofalseexplicitly if your command requires a player caller. argumentNamesignored. Parallax will not error if you includeargumentNames = {...}; it just ignores the field because names are on each arg descriptor instead. But auto-generated syntax help will look wrong if the two disagree.
Next: 07-hooks.md