Cmdr: A fully extensible and type safe command console for Roblox Developers

It most definitely is! Even in 2024, it’s still one of the most customizable command systems out there. I’d daresay there’s nothing else like it.

If anyone else can find anything even remotely similar, I would love to be sent the links so I can check them out myself.

2 Likes

How in the world, do i give people perms to certain commands?

Yes and no, it’s good as a starting point for recoding an admin system that you like and for you, but the source code could be improved and be more “up to date.” Honestly, I would suggest and pull request some new features, but I’ve been blocked from doing that (not explaining why).

1 Like

You’d define the group that can access the command on the client like in this image:

And then when you’ve done that, all you do is check the group the command is a part of in your hook check, and if the player has permission to run the command, you don’t return anything.

Here’s what I did for my hook check as an example:

I’ve had this setup for a while, and it hasn’t failed even once.

1 Like

The group parameter under CommandContext has the “any” type, but says it’s typically defined as a string:

The issue is that the ‘help’ command uses a table.sort method, which requires group to be defined as a string or an integer or else it will error.

I think this should be more clear in the documentation.

1 Like

Is it possible to change CommandContext.Group? If so, how would I do it?
Edit: I used my brain a realized I was doing permissions wrong, it’s literally just a string lol.

How to set this up? i can’t do it

What part are you struggling with? Have you read the tutorial in Cmdr’s website? I’ll summarize the basic setup from their site.

Server:

-- An example from Cmdr's website
-- This is a script you would create in ServerScriptService, for example.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Cmdr = require(path.to.Cmdr)

Cmdr:RegisterDefaultCommands() -- This loads the default set of commands that Cmdr comes with. (Optional)
-- Cmdr:RegisterCommandsIn(script.Parent.CmdrCommands) -- Register commands from your own folder. (Optional)
Cmdr:RegisterHooksIn(path.to.Hooks) -- Register the BeforeRun hook

Client:

-- An example from Cmdr's website
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Cmdr = require(ReplicatedStorage:WaitForChild("CmdrClient"))

-- Configurable, and you can choose multiple keys
Cmdr:SetActivationKeys({ Enum.KeyCode.F2 })

In another folder (in this example CmdrCommands), define the custom command and its behavior.
A ModuleScript named Teleport (or whatever else you want it to be):

-- An example from Cmdr's website
-- Teleport.lua, inside your commands folder as defined above.
return {
	Name = "teleport";
	Aliases = {"tp"};
	Description = "Teleports a player or set of players to one target.";
	Group = "Admin";
	Args = {
		{
			Type = "players";
			Name = "from";
			Description = "The players to teleport";
		},
		{
			Type = "player";
			Name = "to";
			Description = "The player to teleport to"
		}
	};
}

Another ModuleScript, this time named TeleportServer:

-- An example from Cmdr's website
-- TeleportServer.lua

-- These arguments are guaranteed to exist and be correctly typed.
return function (context, fromPlayers, toPlayer)
  if toPlayer.Character and toPlayer:FindFirstChild("HumanoidRootPart") then
    local position = toPlayer.Character.HumanoidRootPart.CFrame

    for _, player in ipairs(fromPlayers) do
      if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
        player.Character.HumanoidRootPart.CFrame = position
      end
    end

    return "Teleported players."
  end

  return "Target player has no character."
end

You will also need to setup a BeforeRun hook, otherwise Cmdr will not run. I will put this inside a folder called Hooks in this example, but you can call it whatever you want

-- An example from Cmdr's website
-- A ModuleScript inside your hooks folder.
return function (registry)
	registry:RegisterHook("BeforeRun", function(context)
		if context.Group == "DefaultAdmin" and context.Executor.UserId ~= game.CreatorId then
			return "You don't have permission to run this command"
		end
	end)
end

And finally, in the same server script where we registered Cmdr’s default commands, register the custom commands and hooks. Here’s how you would do it:

Cmdr:RegisterCommandsIn(script.Parent.CmdrCommands) -- Register commands from your own folder. (Optional)
Cmdr:RegisterHooksIn(path.to.Hooks) -- Register the BeforeRun hook

I wrote this up in like 10 minutes so it might be a bit sloppy but hope this helps!

2 Likes

I would suggest changing your type to this:

local Players = game:GetService("Players")

if not game:IsLoaded() then
	game.Loaded:Wait()
end

local treasures = Players.LocalPlayer:WaitForChild("Treasures")

return function(registry)
	local Util = registry.Cmdr.Util
	
	local treasureNames = Util.GetNames(treasures:GetChildren())
	table.sort(treasureNames) -- Guarantees the same order in the list 
	
	registry:RegisterType("treasure",
		Util.MakeEnumType(
			"TreasureName",
			treasureNames
		)
	)
end
1 Like

Hello!!

I’m currently stuck on something with Cmdr. I’m trying to make a command that utilizes multiple custom types. However I want one of the arguments to pick a specific type depending on it’s previous argument.


Here’s a picture to show what I mean.
Any help is super appreciated!!
Thanks in advance!

We call this dynamic arguments. I’m not sure if this is covered in the current documentation site (which is quite out of date), but it is mentioned on the Beta documentation site: https://eryn.io/Cmdr/beta/docs/commands#dynamic-arguments-and-inline-types

1 Like

Thanks a bunch, yeah I’ve noticed that the newer site has a lot of missing things like Dynamic Arguments. This was exactly what I needed!

Now that the new Roblox ban API has come out, who agrees with me that there should be a ban command, unban command, and checkban command by default?

3 Likes

There’s two things that I would like for my project, are these able to be done on cmdr or is there an alternative with these that I could use?

  1. Is there a way to have optional arguments? like you could set it to this or that, or just leave it blank to do both.
  2. Is there a way to have a system where if you press enter and you don’t have a valid command typed in, instead of executing it will autocomplete the top result?
Autocomplete Example

Autocomplete
If I were to press enter here, it would put the whole “clear” command into the box
(NOT EXECUTE IT)

Yes.

Just use tab for autocompletion, but I am sure it might be possible to tweak it to use the enter key.

Sorry i must have missed that part about using tab for autocomplete :sweat_smile:

Nice! How can I validate the user is in one of the permitted groups?

I didn’t see any replies here mentioning support for Player’s DisplayNames, so I thought I’d include my remake of the Player type. The AutoComplete now includes DisplayNames in the format “DisplayName @Username”.

New Player type module
local Util = require(script.Parent.Parent.Shared.Util)
local Players = game:GetService("Players")

local function GetDisplayNamePair(list)
	local names = {}
	for _,player in list do
		table.insert(names, `{player.DisplayName} @{player.Name}`)
	end
	return names
end

local playerType = {
	Transform = function (text)
		local findPlayer = Util.MakeFuzzyFinder(Players:GetPlayers())
		local result = findPlayer(text)
		
		if not result or #result == 0 then
			local list = GetDisplayNamePair(Players:GetPlayers())
			findPlayer = Util.MakeFuzzyFinder(list)
			
			local fResult = findPlayer(text)
			result = {}
			
			for _,found in fResult do
				table.insert(result, Players:FindFirstChild(found:split("@")[2]))
			end
		end
		
		return result
	end;

	Validate = function (players)
		return #players > 0, "No player with that name could be found."
	end;

	Autocomplete = function (players)
		return GetDisplayNamePair(players)
	end;

	Parse = function (players)
		return players[1]
	end;

	Default = function(player)
		return player.Name
	end;

	ArgumentOperatorAliases = {
		me = ".";
		all = "*";
		others = "**";
		random = "?";
	};
}

return function (cmdr)
	cmdr:RegisterType("player", playerType)
	cmdr:RegisterType("players", Util.MakeListableType(playerType, {
		Prefixes = "% teamPlayers";
	}))
end

This is especially useful if your game displays Players with only their DisplayName, and you want to avoid the extra effort of opening the Roblox menu to check who’s who.

I’m having an issue with creating types. I’ve tried everything.

Each time Autocomplete (except for the initial function) is called, this error is called.

 00:41:36.568  ReplicatedStorage.CmdrClient.CmdrInterface.AutoComplete:122: attempt to perform arithmetic (sub) on nil and number  -  Client - AutoComplete:122
  00:41:36.568  Stack Begin  -  Studio
  00:41:36.568  Script 'ReplicatedStorage.CmdrClient.CmdrInterface.AutoComplete', Line 122 - function Show  -  Studio - AutoComplete:122
  00:41:36.568  Script 'ReplicatedStorage.CmdrClient.CmdrInterface', Line 84 - function OnTextChanged  -  Studio - CmdrInterface:84
  00:41:36.568  Script 'ReplicatedStorage.CmdrClient.CmdrInterface.Window', Line 328  -  Studio - Window:328
  00:41:36.568  Stack End  -  Studio

Here is the type code:


ANOMALYTYPAGE = {
	[1] = "ItsPlasmaRBLX2",
	[2] = "Packages",
	[3] = "Spigot",
	[4] = "Brick"
}

local Util = require(script.Parent.Parent.Shared.Util)
local anomalyTypageFinder = Util.MakeFuzzyFinder(ANOMALYTYPAGE)

local storedKeyType = {
	Validate = function(anomalyName : string)
		if typeof(anomalyName) == "string" then
			return ANOMALYTYPAGE[anomalyName] ~= nil, "No anomaly with that name could be found."
		end;
		return false, "Anomaly name must be a string"
	end;

	Transform = function(text)
		return anomalyTypageFinder(text)
	end;

	Autocomplete = function(anomalyName)
		return Util.GetNames(ANOMALYTYPAGE)
	end;

	Parse = function(anomalyName : string)
		return anomalyName
	end;
}

return function (cmdr)
	cmdr:RegisterType("anomaly", storedKeyType)
	cmdr:RegisterType("anomalies", Util.MakeListableType(storedKeyType))
end


Side note, even if the Autocomplete function is called it will throw an error, regardless of what is in the function. This means if the function is blank, it will still throw an error.

Apparently in the AutoComplete master function the variable start, stop returns nil for my type.

My work around was adding a continue condition. Surprisingly the type’s autocomplete works.