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

you can prob make a rank module then put the admins’ user id in the ranks
then check on the client if they have a rank and if not, it disables CMDR since it’s on the client
something like this

-- the module script
local Ranks = {}

Ranks.Owner = {game.CreatorId}
Ranks.Developer = {1}

-- checks if a player has a rank. returns true if true, false if not
function Ranks:HasRank(player: Player)
      if table.find(Ranks.Developer, player.UserId) then
             return true
      end
      return false
end

return Ranks

-- the local script
local Ranks = require(path to module script)
local player = game.Players.LocalPlayer
if Ranks:HasRank(player) == false then
       cmdr:SetEnabled(false)
end

I feel there is a more simple way to this, this seem excessive

CC @ltsmrsky

The way @officialnabalt wrote it is one way to go about it, but instead of seperate tables for each rank I would just put it all together in one table (Don’t mind the blurred bits):
image

After this, I would convert each rank into a number (Ex: Tester rank is 1, Admin is 2, etc)

The way I check the rank is a bit complicated, cause I’m not requiring the module on the client directly, but instead I’m using a remote function to get the rank on the server (To prevent spoofing, considering this is an admin system)

i suck at programming my bad man :sob:

Can you give me some code? Can’t really do much without any sample code

Client Script Example:

local Remote = game.ReplicatedStorage.RemoteFunction
local Rank = Remote:InvokeServer() -- Get the player's rank

if Rank <= 0 then
    Cmdr:SetEnabled(false)
end

Server Script Example:

local Remote = game.ReplicatedStorage.RemoteFunction
local Ranks = require(script.Ranks)

Remote.OnServerInvoke = function(Player)
    return Ranks:CheckRank(Player) -- Make a check rank function and return the result
end

Here’s an example. I am not giving out the code I use to check a player’s rank, but this should suffice.

1 Like

A rank system really isn’t that hard, I hate to say it but if you aren’t really experienced with luau I wouldn’t recommend using this module. This system doesn’t give you a lot of commands out of the box as its designed for you to make your own commands tailored towards your game.

The docs for this system are more than helpful and I’d highly recommend taking a look into them. Particularly this one - Registry | Cmdr

1 Like

Sorry it was 10pm for me and I regret making that post, but thanks for your contribution

Actually, do you know a way to be able to punish offline players? I need this for our muting system. I’ve checked the docs but I haven’t found anything; and I can’t use playerid because my muting system uses player names. Unless I forked it a lot which would take a while

Add a separate parameter to run the command using user ID by using the Cmdr data type for numbers or you could modify your code to write to a datastore for the next time the player joins by getting the players user ID by their username.

Moving this to DMs because this contains slightly private information

I’ve got this system to unban players, why won’t it work? The for players loop just won’t run.

Client:

return {
	Name = "unban";
	Description = "Unbans a player or a group of players";
	Group = "Moderator";
	Args = {
		{
			Type = "playerIds";
			Name = "players";
			Description = "The players to unban - Use player ids";
		},
	};
}

Server:

local Players = game:GetService("Players")

return function (context, players)
	for _, player in pairs(players) do
		Players:UnbanAsync(player.UserId)
	end

	return("Unbanned %d players."):format(#players)
end

Can anyone help me?

Looking into the Types it seems like playerIds only returns for users inside the server.
As this is an unbanning script it’s highly unlikely that the user your unbanning is going to be inside the server so there is a few things you’d need to modify within the Cmdr Types.

Inside your Cmdr > Types folder you should see the module called “PlayerId”, update the source code of that module with what’s below, this will allow for a mixture of users in-game and users not in-game to be added to the list.

What was happening before is the ID you were trying to unban wasn’t in the server so it wasn’t adding them to the list.

local Util = require(script.Parent.Parent.Shared.Util)
local Players = game:GetService("Players")

local nameCache = {}
local function getUserId(name)
	if nameCache[name] then
		return nameCache[name]
	elseif Players:FindFirstChild(name) then
		nameCache[name] = Players[name].UserId
		return Players[name].UserId
	else
		local fetchedName, username = pcall(Players.GetNameFromUserIdAsync, Players, name)
		local fetchedUserId, userid = pcall(Players.GetUserIdFromNameAsync, Players, username)

		if userid ~= tonumber(name) then
			return nil
		end

		nameCache[username] = userid
		return userid
	end
end

local playerIdType = {
	DisplayName = "Full Player Name";
	Prefixes = "# integer";

	Transform = function (text)
		local findPlayer = Util.MakeFuzzyFinder(Players:GetPlayers())

		return text, findPlayer(text)
	end;

	ValidateOnce = function (text)
		return getUserId(text) ~= nil, "No player with that name could be found."
	end;

	Autocomplete = function (_, players)
		return Util.GetNames(players)
	end;

	Parse = function (text)
		return getUserId(text)
	end;

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

return function (cmdr)
	cmdr:RegisterType("playerId", playerIdType)
	cmdr:RegisterType("playerIds", Util.MakeListableType(playerIdType, {
		Prefixes = "# integers"
	}))
end

This would be the updated unban script itself, regardless of the issue above even if the script above did allow for users to be added to the list if they weren’t in-game unbanAsync requires the Parameters to be wrapped in a table, with unbanAsync you can also bulk unban so you don’t need to send 25 different unbanAsync request to unban 25 users instead you can use the original list and just unban all 25 of those users at the same time.

If it errors on a particular user according to the PlayerService docs for BanAPI it’ll join all the errors together separated by a comma.

For example, if this method is invoked for five UserIds: {1, 2, 3, 4, 5} and requests for users 2 and 4 fail then the following error message appears: HTTP failure for UserId 2: Timedout, HTTP 504 (Service unavailable) failure for UserId 4: Service exception.

I’ve utilized that with the CommandContext | Reply to differentiate the error messages and the final request.

local PlayerService = game:GetService("Players")

return function (context, Players)
	local config: UnbanConfigType = {
		UserIds = Players,
		ApplyToUniverse = false
	}
	
	local success, err = pcall(function()
		return PlayerService:UnbanAsync(config)
	end)
	
	if not success then
		local formatError = string.split(err, ",")
		local failedRequest = #formatError
		
		for _, readError in formatError do
			context:Reply(readError, Color3.fromRGB(177, 49, 49))
		end
		
		return `Unbanned {#Players - failedRequest} player(s), {failedRequest} request failed.`
	end

	return `Unbanned {#Players} player(s).`
end
1 Like

Oh my god you’re an actual legend :pray:

Thanks so much

Is there any way you could do the same with the “Player” system to allow it for offline players too or is that not possible?

EDIT: I just testing this out and it doesn’t seem to be working, no errors though it does say “Unbanned 1 player(s)” so it seems to be working?

It does work for offline users although it won’t have the autofill like it does with users that are in that server.

With it not working I enabled “ApplyToUniverse” and that seemed to work.

	local config: UnbanConfigType = {
		UserIds = Players,
		ApplyToUniverse = true
	}
1 Like

Hey, for anyone that needs help hiding the gui for people that aren’t supposed to use it, here is what you can add into the local script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Cmdr = require(ReplicatedStorage:WaitForChild("CmdrClient"))

local playerRank = game.Players.LocalPlayer:GetRankInGroup(123456789) -- add group id here

if playerRank >= 1 then -- obviously change this or everyone in the group will have access to it
	Cmdr:SetActivationKeys({ Enum.KeyCode.F2 }); -- this can also be changed
else
	Cmdr:SetEnabled(false);
end

You can implement your own checking system, but this is a good base to work off (and there is no other scripts or remote events involved!)

Hey, I sent you a DM, can you respond to it? thx

Alright so I have a dynamic argument system, I’ve checked the docs but it won’t work.

Output:

ReplicatedStorage.CmdrClient.Shared.Util:115: invalid argument #1 to 'gmatch' (string expected, got nil)  -  Client - Util:115
  18:13:01.040  Stack Begin  -  Studio
  18:13:01.040  Script 'ReplicatedStorage.CmdrClient.Shared.Util', Line 115 - function SplitStringSimple  -  Studio - Util:115
  18:13:01.040  Script 'ReplicatedStorage.CmdrClient.Shared.Util', Line 250 - function ParsePrefixedUnionType  -  Studio - Util:250
  18:13:01.040  Script 'ReplicatedStorage.CmdrClient.Shared.Argument', Line 34 - function new  -  Studio - Argument:34
  18:13:01.040  Script 'ReplicatedStorage.CmdrClient.Shared.Command', Line 62 - function Parse  -  Studio - Command:62
  18:13:01.041  Script 'ReplicatedStorage.CmdrClient.Shared.Dispatcher', Line 44 - function Evaluate  -  Studio - Dispatcher:44
  18:13:01.041  Script 'ReplicatedStorage.CmdrClient.CmdrInterface', Line 31 - function OnTextChanged  -  Studio - CmdrInterface:31
  18:13:01.041  Script 'ReplicatedStorage.CmdrClient.CmdrInterface.Window', Line 341  -  Studio - Window:341
  18:13:01.041  Stack End  -  Studio
  18:13:01.125  ReplicatedStorage.CmdrClient.Shared.Util:115: invalid argument #1 to 'gmatch' (string expected, got nil)  -  Client - Util:115
  18:13:01.125  Stack Begin  -  Studio
  18:13:01.125  Script 'ReplicatedStorage.CmdrClient.Shared.Util', Line 115 - function SplitStringSimple  -  Studio - Util:115
  18:13:01.125  Script 'ReplicatedStorage.CmdrClient.Shared.Util', Line 250 - function ParsePrefixedUnionType  -  Studio - Util:250
  18:13:01.125  Script 'ReplicatedStorage.CmdrClient.Shared.Argument', Line 34 - function new  -  Studio - Argument:34
  18:13:01.126  Script 'ReplicatedStorage.CmdrClient.Shared.Command', Line 62 - function Parse  -  Studio - Command:62
  18:13:01.126  Script 'ReplicatedStorage.CmdrClient.Shared.Dispatcher', Line 44 - function Evaluate  -  Studio - Dispatcher:44
  18:13:01.126  Script 'ReplicatedStorage.CmdrClient.CmdrInterface', Line 31 - function OnTextChanged  -  Studio - CmdrInterface:31
  18:13:01.126  Script 'ReplicatedStorage.CmdrClient.CmdrInterface.Window', Line 341  -  Studio - Window:341
  18:13:01.126  Stack End  -  Studio
  18:13:01.744  ReplicatedStorage.CmdrClient.Shared.Util:115: invalid argument #1 to 'gmatch' (string expected, got nil)  -  Client - Util:115
  18:13:01.744  Stack Begin  -  Studio
  18:13:01.744  Script 'ReplicatedStorage.CmdrClient.Shared.Util', Line 115 - function SplitStringSimple  -  Studio - Util:115
  18:13:01.744  Script 'ReplicatedStorage.CmdrClient.Shared.Util', Line 250 - function ParsePrefixedUnionType  -  Studio - Util:250
  18:13:01.744  Script 'ReplicatedStorage.CmdrClient.Shared.Argument', Line 34 - function new  -  Studio - Argument:34
  18:13:01.745  Script 'ReplicatedStorage.CmdrClient.Shared.Command', Line 62 - function Parse  -  Studio - Command:62
  18:13:01.745  Script 'ReplicatedStorage.CmdrClient.Shared.Dispatcher', Line 44 - function Evaluate  -  Studio - Dispatcher:44
  18:13:01.745  Script 'ReplicatedStorage.CmdrClient.CmdrInterface', Line 31 - function OnTextChanged  -  Studio - CmdrInterface:31
  18:13:01.745  Script 'ReplicatedStorage.CmdrClient.CmdrInterface.Window', Line 341  -  Studio - Window:341
  18:13:01.746  Stack End  -  Studio

Script (client):

return {
	Name = "clans";
	Description = "Everything related to the clans system.";
	Group = "Moderator";
	Args = {
		{
			Type = "string";
			Name = "clan";
			Description = "The clan to change.";
		},
		function(context)
			return {
				Type = context.Cmdr.Util.MakeEnumType("option", {"Disband", "Kick", "Change Owner", "Change Rank", "Edit Info"}),
				Name = "action",
				Description = "The action you would like to do.",
			}
		end,
		function(context)
			local action = context:GetArgument(2):GetValue()

			if action == "Change Rank" then
				return {
					{
						Type = "player";
						Name = "player";
						Description = "Player whose rank is being changed.";
					},
					{
						Type = context.Cmdr.Util.MakeEnumType("option", {"High Commander", "Captain", "Member"}),
						Name = "rank",
						Description = "Rank to change player to.",
					}
				}
			elseif action == "Kick" then
				return {
					{
						Type = "player";
						Name = "player";
						Description = "Player who is being kicked.";
					}
				}
			elseif action == "Change Owner" then
				return {
					{
						Type = "player";
						Name = "newOwner";
						Description = "Player who will be the new owner.";
					}
				}
			elseif action == "Edit Info" then
				return {
					{
						Type = "string";
						Name = "description";
						Description = "New description for the clan.";
					},
					{
						Type = "integer";
						Name = "imageid";
						Description = "(optional) New image ID for the clan.";
						Optional = true;
					}
				}
			end
		end
	};
}

Is there any reason why this won’t work? I am certain it is not the server that is erroring.

I would use it if you guys added more built in commands like warn and ban

This system is made to allow you to create your own commands, if they included a bunch of commands people won’t use this system as it was originally created to be.

This system was created to be both functional and allow for the developer add/remove what commands they want being tailored towards that particular game. Making those commands aren’t hard at all but its up to you as to how you’d want to implement them.

2 Likes