How to make basic admin commands

Note: This tutorial is for people who understand basic to intermediate scripting.
There is a small glossary at the bottom of the tutorial if you want some quick explanations of certain functions or concepts.

So, with the recent drama, and since custom admin commands are very useful, I bring you this admin commands tutorial.

Why not use free model admin commands?

You can use custom admin commands for anything from testing your game to managing your clan’s fort.
Since they are custom, you can edit them to work however you want, understanding exactly how they work. You can also make commands specific to your game.

As a bonus, you know exactly what is being done to your game and who has access to what - something that not a good amount of free models can boast - so your game is more secure.

This tutorial covers:

  • Adding & identifying admins
  • Parsing arguments using string patterns
  • Finding and calling command functions using a dictionary

Setup

Body

The type of admin we are making automatically parses commands. This is preferable for two reasons: it’s faster to add commands, and it is more consistent.

It is preferable to put your admin script in ServerScriptService instead of Workspace. ServerScriptService is designed for scripts, and Workspace is designed for physical objects.
It’s also recommended to give your script a comprehensive and unique name so you can find it easily and know what is for.

Once you have your script, define the Players service and the PlayerAdded callback:

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(Player)
    -- Empty
end)

Next, you want to setup the listener for the Chatted event:

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(Player)
      Player.Chatted:Connect(function(Message,Recipient)
        if not Recipient then
            -- Empty
        end
    end)
end)

The if statement stops the command being parsed if the player is whispering to someone. You may not add this if you want to be able to do a command anywhere - but it’s best to keep commands out in the open, at least until you have a command log.

Adding Administrators

Body

There are many variations of how this can be done, but my favourite is to be able to define using either username or user ID in an array.

Make your array:

local Admins = {
    "EmeraldSlash"; -- Username example
    17614882; -- User ID example
}
Extra: Group admins

Using this method, you can also add groups and minimum ranks.
The formatting in this tutorial for a group entry in to the array will be like so:
{GroupId = 0000; RankId = 255;}
RankId will be optional.

Example:

local Admins = {
"EmeraldSlash",
{GroupId = 12345; RankId = 255}
}

From now on, any details named the same as this one will add functionality to this aspect of the admin commands.

Next, you want to be able to check if a player is an admin. We will make a function so this can be done easily in the middle of code.

local function IsAdmin(Player)
     -- Empty
end

Loop through the admins, and add an if statement checking if the player has the same username as one in the admins. Return true when the player is identified and return false at the end of the function, which will be reached if nothing matches up.

Since we are using a number of data types, make sure to check what type of value you are checking before altering and comparing it.

local function IsAdmin(Player)
    for _,Admin in pairs (Admins) do
        if type(Admin) == "string" and string.lower(Admin) == string.lower(Player.Name) then
            return true
        end
    end
    return false
end

Then, you want to check if the player has the same user ID as one that has been defined in the Admins array:

local function IsAdmin(Player)
    for _,Admin in pairs (Admins) do
        if type(Admin) == "string" and string.lower(Admin) == string.lower(Player.Name) then
            return true
        elseif type(Admin) == "number" and Admin == Player.UserId then
            return true
        end
    end
    return false
end
Extra: Group Admins

Add an extra elseif to the if statement in the IsAdmin function.

if Admin.RankId has not been defined, replace it with 1, as a player is not in a group if their rank is 0.

local function IsAdmin(Player)
	for _,Admin in pairs (Admins) do
		if type(Admin) == "string" and string.lower(Admin) == string.lower(Player.Name) then
			return true
		elseif type(Admin) == "number" and Admin == Player.UserId then
			return true
		elseif type(Admin) == "table" then
			local Rank = Player:GetRankInGroup(Admin.GroupId)
			if Rank >= (Admin.RankId or 1) then
				return true
			end
		end
	end
	return false
end

Now, all you need to do is call the function to see if a player is an admin in the PlayerAdded callback:

Players.PlayerAdded:Connect(function(Player)
	Player.Chatted:Connect(function(Message,Recipient)
		if not Recipient and IsAdmin(Player) then
			-- Empty
		end
	end)
end)

Parsing commands

Body

Now that we can check if a player actually has permissions to use the commands, we need to parse (decode) their message.

For the purposes of this tutorial, I will create a separate function to parse the message to stop spamming lots of lines of code. As the function’s body, use string.lower to make the message lowercase.

local function ParseMessage(Player,Message)
	Message = string.lower(Message)
end

Don’t forget to call the function from the PlayerAdded callback!

Players.PlayerAdded:Connect(function(Player)
	Player.Chatted:Connect(function(Message,Recipient)
		if not Recipient and IsAdmin(Player) then
			ParseMessage(Player,Message)
		end
	end)
end)

Prefix

The first part of parsing is to check if the player actually was running a command at all - in most cases, they probably won’t be.

First; define the prefix under the admins.

local Prefix = "!"

Note that some characters are used in string patterns, such as . which is the string pattern for ‘anything’. If you put one of these characters in your prefix variable your parser will probably break. For characters such as these, you will need to ‘escape’ the pattern by putting a % in front of the pattern character – for example, %.
When making a prefix, have a look at the table of string patterns in the Roblox documentation to see whether your prefix needs to have any string patterns escaped.

Next, we move on to the ParseMessage function.

The first step is to check if the prefix is present in the message. For this, we use the string.match function. This is used to find one string in another string. Example:

local MyString = "Hello, world!"
print(string.match(MyString,"world"))
-- outputs 'world'

A limitation with this is that you can only find a single value with a conventional string - but, Lua has another useful feature, called string patterns.
We will focus more on it later, but right now, all you need to know is that the ^ character anchors the match to the beginning of the string - meaning that it will only match the string if it is at the start of the other string.

An example of this:

local PrefixMatch = string.match(Message,"^"..Prefix)
-- concatenates '^' with the prefix, creating a string similar to this: ^!
-- will only return a match if the prefix is found at the start of the message

You can read more on the string.match function here and string patterns here.

Moving on, we want to use the previous example and an if statement checking if the prefix was found:

local PrefixMatch = string.match(Message,"^"..Prefix)
	
if PrefixMatch then
    -- Empty
end

Finally, we want to remove the prefix from the message if it was found using string.gsub. This function is similar to string.match except it replaces a one string with another string.

string.gsub("mainString","toReplace","replacementString",numberOfReplacements)

Since this function has the g letter in the name, that also means it will replace every match in the main string with the replacement string, unless told otherwise. We can do this using the numberOfReplacements argument.

The wiki article on this is here.

You can replace with an empty string ("") to effectively remove parts of a string.
That’s what we will be doing now.
Redefine message as the version where it has the prefix removed:

Message = string.gsub(Message,PrefixMatch,"",1)

Final prefix code:

local function ParseMessage(Player,Message)
	Message = string.lower(Message)
	local PrefixMatch = string.match(Message,"^"..Prefix)
	
	if PrefixMatch then
		Message = string.gsub(Message,PrefixMatch,"",1)
	end
end

Arguments

Now it’s time to split the message up into arguments. For future reference, we will be using the first argument as the name of the command we will be running.

Firstly, define the array that will hold the arguments:

local Arguments = {}

Once you’ve done that, it’s time to actually split the message.

We will do use this using string.gmatch. This function acts like string.match, but every time it is called on the same string, it will return the next match - and instead of returning a string, it returns a function, that, when called, will return what it would have.
For example:

local MyString = "I'm 7 years and 2 months old lol"
local Match = string.gmatch(MyString,"%d") -- %d is the string pattern for a number
Match()
-- returns returns 7
Match()
-- returns returns 2

The wiki article for this function is here.

For this particular use of string.gmatch we will be using the pattern [^%s]+.
To break it down:

  • + returns the largest string possible whilst retaining the pattern
  • [] is a set. This allows you to have more than one pattern be searched for, have a range of characters to search for, or use complements
  • ^, when used in a set, is a complement. This means the pattern will search for everything but what is defined inside the set
  • %s is the pattern to match whitespace characters

You can find the string patterns wiki article here.

To summarise: we are searching for as many non-whitespace characters in a row as possible.

A great thing about string.gmatch (and string.gsub, but that’s irrelevant) is that you can use them as the iterating function for a for loop. This means we can easily loop though every argument the player uses when using the previously defined pattern.

for Argument in string.gmatch(Message,"[^%s]+") do
    -- Empty
end

Now all we need to do is insert the argument into the Arguments table:

local function ParseMessage(Player,Message)
	Message = string.lower(Message)
	local PrefixMatch = string.match(Message,"^"..Prefix)
	
	if PrefixMatch then
		Message = string.gsub(Message,PrefixMatch,"",1)
		local Arguments = {}
		
		for Argument in string.gmatch(Message,"[^%s]+") do
			table.insert(Arguments,Argument)
		end
	end
end

Now you have every argument the player sent in the message.

Creating Commands

Body

Before we process the commands, we first need to create some commands, and also setup a consistent format.
Command functions will be indexed by name, and passed a consistent sequence of arguments.

Firstly, create the table we will use to create commands:

local Commands = {}

Next, let’s add a basic command.
The parameters in a command function will be, in this tutorial, <player> Sender and <array> Arguments. In this tutorial, the command will be print. It will print a given message to the output.

Commands.print = function(Sender,Arguments)
	local Message = table.concat(Arguments," ")
	print("From " ..Sender.Name..":\n"..Message)
    -- /n creates a new line
end

When this is called, it should print something similar to the following example:
From EmeraldSlash:
wow this command is pretty cool

Processing Commands

Body

Now that we have a command to use, let’s set it up with the parser function.

Firstly, retrieve the first value from the array after the for loop that splits the message into the arguments array. This will be used to index the command.

local CommandName = Arguments[1]

It is also a good idea to remove it from the table, as you don’t want to pass the command name as an argument.

table.remove(Arguments,1)

Next, try and index the command function and save it to a variable, then check to see if it exists.

local CommandFunc = Commands[CommandName]

if CommandFunc ~= nil then
    -- Empty
end

If it does exist, call it.

local function ParseMessage(Player,Message)
	Message = string.lower(Message)
	local PrefixMatch = string.match(Message,"^"..Prefix)
	
	if PrefixMatch then
		Message = string.gsub(Message,PrefixMatch,"",1)
		local Arguments = {}
		
		for Argument in string.gmatch(Message,"[^%s]+") do
			table.insert(Arguments,Argument)
		end
		
		local CommandName = Arguments[1]
		table.remove(Arguments,1)
		local CommandFunc = Commands[CommandName]
		
		if CommandFunc ~= nil then
			CommandFunc(Player,Arguments)
		end
	end
end

The wiki page for table functions is here.

Finale

Body

Your code should look roughly like this:

local Admins = {
	"EmeraldSlash"; -- Username example
	17614882; -- User ID example
	-- {GroupId = 0000;RankId = 255;} -- Group example
}
local Prefix = "!"

local Players = game:GetService("Players")

local Commands = {}

Commands.print = function(Sender,Arguments)
	local Message = table.concat(Arguments," ")
	print("From " ..Sender.Name..":\n"..Message)
end

local function IsAdmin(Player)
	for _,Admin in pairs (Admins) do
		print(Admin,Player)
		if type(Admin) == "string" and string.lower(Admin) == string.lower(Player.Name) then
			return true
		elseif type(Admin) == "number" and Admin == Player.UserId then
			return true
		--[[elseif type(Admin) == "table" then
			local Rank = Player:GetRankInGroup(Admin.GroupId)
			if Rank >= (Admin.RankId or 1) then
				return true
			end]]
		end
	end
	return false
end

local function ParseMessage(Player,Message)
	Message = string.lower(Message)
	local PrefixMatch = string.match(Message,"^"..Prefix)
	
	if PrefixMatch then
		Message = string.gsub(Message,PrefixMatch,"",1)
		local Arguments = {}
		
		for Argument in string.gmatch(Message,"[^%s]+") do
			table.insert(Arguments,Argument)
		end
		
		local CommandName = Arguments[1]
		table.remove(Arguments,1)
		local CommandFunc = Commands[CommandName]
		
		if CommandFunc ~= nil then
			CommandFunc(Player,Arguments)
		end
	end
end

Players.PlayerAdded:Connect(function(Player)
	Player.Chatted:Connect(function(Message,Recipient)
		if not Recipient and IsAdmin(Player) then
			ParseMessage(Player,Message)
		end
	end)
end)

And there you go! You now have working admin commands :slight_smile:


Glossary

Body

Parse – resolve (a sentence) into its component parts and describe their syntactic roles. (Google)

type() – returns what data type a value is
string.lower() – returns a lowercase version of a given string
string.match() – finds and returns one string in another
string.gmatch()string.match() but it returns a function that will return the next match every time it is called
string.gsub() – replaces a given string in another string with a replacement string
table.conat() – concatenates values in a table into a string using the second parameter as the divider
\n – when used in a string, it creates a new line

String Patterns
A string pattern is essentially a way of using variables in strings. You can use a certain pattern to search for a variable set of characters.
For example, using the number pattern (%d) in a string.match will return a variable number.

string.match("Age:7","%d")
-- returns 7
string.match("Weight:3kgs","%d")
-- returns 3

Thanks for reading - hopefully this helped you out in some way :slight_smile:
Make sure to tell me if something is wrong.

269 Likes

What sorts of things do people still use chat commands for now that the command line is available in the developer console?

13 Likes

A few things.

This allows people who are not able to edit the game to use some “administrative” features.

If someone wants to quickly do something a number of times, it allows that to happen faster as they don’t need to code everything as they go.
Similar situation to why use functions instead of writing the same code over and over again.

This doesn’t really apply to people doing something one-off, but they are useful when doing repetitive things.

It’s also just something useful to learn - for example, being able to parse messages can apply to more than just admin commands.

12 Likes

I agree with you that one option is the console (for developers). From what I can tell OP is building commands for moderators and such. I actually support developers making their own commands instead of using others because of malicious code.

8 Likes

I was planning on making a tutorial similar to this sometime, but oh well ¯\_(ツ)_/¯

I always use a Commands table too, but my string matching is a bit different:

local message = "print/this syntax is fine"
local cmd,args = message:match("^(%w+)/(.*)$")
-- if I need it split
args = split(args)

I’ve looked the past 5m, but apparently I’ve lost the code long ago. I wrote some piece of code that allowed you to basically do something like this:

local parser = GenerateSyntax("cmd; arg1 arg2")
parser("print; Seven things") --> "print", {"Seven", "things"}

It worked with a ton of syntax (templates):

cmd/arg1/arg2
:cmd arg1 arg2
[ cmd ] arg1 # arg2
Server! cmd arg1 arg2
cmd arg1,arg2

actually I remember creating a place to test this online, lemme see if I can find it
EDIT: Nope definitely lost

8 Likes

Nice tutorial. Although I currently don’t have use cases for admin except my serious roleplay groups, this served as a “string manipulation for dummies” guide for me. Thank you very much. Tutorial was easy to follow and understand quickly.

6 Likes

Just read this now, learned some things about string patterns that oddly applied to something I was coding in my spare time. Didn’t know that ^ used in a set acts differently to its normal use. Good to know. Wish the new wiki displayed more information on “magic” characters like it used to do back in the day.

1 Like

Good tutorial, though I prefer tinkering with the Chat System. Just wanted you to know about the Prefix variable being used in string.match. Some people will not get this and the confusion is when using punctuation characters as prefixes when they actually have a role in string patterns. Someone asked me recently what the issue was when they showed me this code:

local Prefix = "."
local PrefixMatch = string.match(Message,"^"..Prefix)

You should explain in the post how to fix this issue by adding a percentage symbol in front of the character (%.). You could also make an automatic fix in the script by doing something like this:

local PrefixSearch = Prefix:gsub("%p", "%%%1")

and using PrefixSearch when searching strings for the prefix.

5 Likes

I believe this was made before the new chat system was implemented.

For prefixes, I usually just use something along these lines:

string.match("!cmd", "(.?)(%w+)"), as that in most cases does the trick for me.

Pretty neat tips, though there are probably many more ways to approach the same situation.

2 Likes

The new chat system was implemented before I made this post actually, but I wanted to do a fairly simple option.

@Sir_Melio Thanks for the feedback, will add that in.

2 Likes

Thank you about this topic. It’s very interesting!
It’s time to improve my admin commands :stuck_out_tongue:

2 Likes

how would i use this to join multiple args? like in a kick command someone says something like
/kick user_name this is the reason
would i join args or something?

You can use combine the arguments coming after the user_name. So for example:

Commands.kick = function(Sender, Arguments)
    local Username = Arguments[1]
    table.remove(Arguments, 1)
    local Message = table.concat(Arguments, " ")
end

table.concat is just used to make an array like ["One", "two", "three"] become a string with a given separator (in this case, space) like "One two three".

10 Likes

This is a very helpful topic! Though, what type of script would I put it in?

It should be inside of a Script, since this code should be run serverside.

2 Likes

How would you have 2 arguments? For example a name and a boolvalue.
Something like:
!addbounty Dorthsky 100

I know this topic is like 4 years old, but it still works :smiley:

Using this you would get:

Argument[1] = addbounty
Argument[2] = dorthsky
Argument[3] = 100

So depending on the first argument (the action), you can check for how ever many variables as you would expect there to be and deal with potential nil’s and such accordingly.

3 Likes

Hmm… And they remove my post for being to easy. This is cool.

1 Like

you taught me how string.match and string.gmatch and string.gsub works, but what do i have to type to execute the command?