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

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.

anyone got any idea why im having a issue? hwenever i utilize cmdr, the script says "CMDR disabled for security, BEforeRun not setup or something?

That’s because for safety, you can’t run any commands until you setup a BeforeRun hook. Read the documentation to learn more on how to set it up. The link will take you to the beta version of the Cmdr docs. For clarification, a BeforeRun hook is what you will use for command permissions. It will check if the player attempting to execute the command is allowed to but you can define custom behavior/logic because you’re going to be the one writing it.

It is setup… It says it is not however.

Then you probably didn’t do it correctly. The error clearly says you didn’t set up the BeforeRun hook. How did you set it up? Can you provide the code you used?

is it possible to make the CMDR usable only to specific people?

I’m having a very weird issue with Cmdr. I am making a viewstats command so that my Moderators can view other players stats by typing their username with the auto-complete menu if they’re in the server or use the @ prefix to type their full username if they’re not in the server. However, the issue is that when Cmdr gets to a point where it needs to return something inside the command, it just doesn’t. And that happens for every command for some really weird reason. After using commands a bunch more times, the very delayed command responses I should’ve gotten ages ago finally appear.

I tried using print statements and they got where they needed to. Once it gets to a return statement, it just self destructs.

Note: The game uses ProfileService and the game is called RoKarate. You can search it using the search bar and it’ll be the first game. I have confirmed that every variable gives what it’s supposed to such as the DataManager existing and the DataManager giving the current profile if they are in the server and all that.

Here is the following code I used:

return {
	Name = "viewstats",
	Aliases = {"stats"},
	Description = "View a players stats.",
	Group = "Moderator",
	Args = {
		{
			Type = "player @ string",
			Name = "player @ username",
			Description = "The full username of the player."
		}
	}
}
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DataManager = require(ServerStorage.DataManager) -- This does exist
local Format = require(ReplicatedStorage.Modules.FormatNumber.Simple) -- This does exist

local function toDHMS(seconds)
	local days = math.floor(seconds / 86400)
	local hours = math.floor((seconds % 86400) / 3600)
	local minutes = math.floor((seconds % 3600) / 60)
	local seconds = math.floor(seconds % 60)
	return ("%i:%02i:%02i:%02i"):format(days,hours,minutes,seconds)
end

return function(context, player)
	if typeof(player) == "Instance" then
		local profile = DataManager:GetProfile(player) -- This does work
		
		context:Reply(player.Name.."'s stats:")
		context:Reply("---------------------------------------------")
		context:Reply("Belt: "..profile.Data.Belt)
		context:Reply("Strength: "..Format.Format(profile.Data.Strength))
		context:Reply("Health: "..Format.Format(profile.Data.Health))
		context:Reply("MaxStamina: "..Format.Format(profile.Data.MaxStamina))
		context:Reply("Wins: "..Format.Format(profile.Data.Wins))
		context:Reply("Kills: "..Format.Format(profile.Data.TotalKills))
		context:Reply("Robux Donated: "..Format.Format(profile.Data.RobuxDonated))
		context:Reply("Playtime: "..toDHMS(profile.Data.Playtime))
		context:Reply("---------------------------------------------")

		return "Successfully retrieved the players stats."
	elseif typeof(player) == "string" then
		local gotUserId, userId = pcall(function()
			return Players:GetUserIdFromNameAsync(player)
		end)

		if not gotUserId then
			return `Failed to get UserId associated with {player}.\nError: {userId}`
		end

		local profileStore = DataManager:GetProfileStore()
		local profile = profileStore:ViewProfileAsync("Player_"..userId)

		if profile == nil then
			return player.." does not have any data!"
		end

		context:Reply(player.."'s stats:")
		context:Reply("---------------------------------------------")
		context:Reply("Belt: "..profile.Data.Belt)
		context:Reply("Strength: "..Format.Format(profile.Data.Strength))
		context:Reply("Health: "..Format.Format(profile.Data.Health))
		context:Reply("MaxStamina: "..Format.Format(profile.Data.MaxStamina))
		context:Reply("Wins: "..Format.Format(profile.Data.Wins))
		context:Reply("Kills: "..Format.Format(profile.Data.TotalKills))
		context:Reply("Robux Donated: "..Format.Format(profile.Data.RobuxDonated))
		context:Reply("Playtime: "..toDHMS(profile.Data.Playtime))
		context:Reply("---------------------------------------------")
		
		return "Successfully retrieved the players stats."
	end
end
1 Like

I’m having an issue where the mobile keyboard constantly flickers on/off. Looks like it changes every frame. This starts happening when you tap the command panel.

All I’m doing is ‘:Toggle()’ once to open the gui.

Does someone know how to fix this?