Building Custom Admin GUIs

Tutorial

Step-by-step implementation guide for building custom in-game admin panels using Native ScreenGui, Roact, or Chat Commands.

# Overview

Because R Anti-Cheat provides a headless ClientAPI, you have complete freedom to build any in-game admin panel that suits your experience.

Below are three complete, production-ready examples:

  1. Example 1: Native Roblox ScreenGui Admin Panel (Zero external libraries).
  2. Example 2: Declarative Reactive Component (Roact / React-lua / Fusion).
  3. Example 3: Custom Chat Commands (/kick, /ban, /freeze).

# Example 1: Native Roblox ScreenGui Admin Panel

Create a LocalScript inside StarterPlayer.StarterPlayerScripts (e.g. CustomAdminPanel.client.luau):

--!strict
-- StarterPlayerScripts/CustomAdminPanel.client.luau
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local RACClient = require(ReplicatedStorage.RAC.ClientAPI)

local isOpen = false
local screenGui: ScreenGui? = nil

local function RefreshPlayerList(scrollingFrame: ScrollingFrame)
    -- Clear old entries
    for _, child in ipairs(scrollingFrame:GetChildren()) do
        if child:IsA("Frame") then child:Destroy() end
    end

    local success, players = RACClient.GetLivePlayers()
    if not success or not players then return end

    local yOffset = 0
    for _, p in ipairs(players) do
        local row = Instance.new("Frame")
        row.Size = UDim2.new(1, -10, 0, 44)
        row.Position = UDim2.new(0, 5, 0, yOffset)
        row.BackgroundColor3 = Color3.fromRGB(30, 34, 40)
        row.BorderSizePixel = 0
        row.Parent = scrollingFrame

        local corner = Instance.new("UICorner")
        corner.CornerRadius = UDim.new(0, 6)
        corner.Parent = row

        -- Player info label
        local infoLabel = Instance.new("TextLabel")
        infoLabel.Size = UDim2.new(0.6, 0, 1, 0)
        infoLabel.Position = UDim2.new(0, 10, 0, 0)
        infoLabel.BackgroundTransparency = 1
        infoLabel.Font = Enum.Font.GothamMedium
        infoLabel.TextSize = 13
        infoLabel.TextColor3 = Color3.fromRGB(240, 240, 240)
        infoLabel.TextXAlignment = Enum.TextXAlignment.Left
        infoLabel.Text = string.format("%s (Ping: %dms | Risk: %d)", p.username, p.ping, p.riskScore)
        infoLabel.Parent = row

        -- Risk Color Indicator
        if p.riskScore >= 70 then
            infoLabel.TextColor3 = Color3.fromRGB(239, 68, 68) -- Critical Red
        elseif p.riskScore >= 35 then
            infoLabel.TextColor3 = Color3.fromRGB(245, 158, 11) -- Warning Amber
        end

        -- Kick Button
        local kickBtn = Instance.new("TextButton")
        kickBtn.Size = UDim2.new(0, 60, 0, 28)
        kickBtn.Position = UDim2.new(1, -135, 0.5, -14)
        kickBtn.BackgroundColor3 = Color3.fromRGB(220, 38, 38)
        kickBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
        kickBtn.Font = Enum.Font.GothamBold
        kickBtn.TextSize = 11
        kickBtn.Text = "KICK"
        kickBtn.Parent = row
        Instance.new("UICorner", kickBtn).CornerRadius = UDim.new(0, 4)

        kickBtn.MouseButton1Click:Connect(function()
            local ok, err = RACClient.PunishPlayer("Kick", p.userId, p.username, "Kicked by Administrator")
            if ok then
                row:Destroy()
            else
                warn("Failed to kick:", err)
            end
        end)

        -- Ban Button
        local banBtn = Instance.new("TextButton")
        banBtn.Size = UDim2.new(0, 60, 0, 28)
        banBtn.Position = UDim2.new(1, -70, 0.5, -14)
        banBtn.BackgroundColor3 = Color3.fromRGB(153, 27, 27)
        banBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
        banBtn.Font = Enum.Font.GothamBold
        banBtn.TextSize = 11
        banBtn.Text = "BAN"
        banBtn.Parent = row
        Instance.new("UICorner", banBtn).CornerRadius = UDim.new(0, 4)

        banBtn.MouseButton1Click:Connect(function()
            RACClient.PunishPlayer("PermBan", p.userId, p.username, "Banned by Administrator")
            row:Destroy()
        end)

        yOffset += 48
    end

    scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, yOffset + 10)
end

local function OpenAdminPanel()
    local ok, stats, role = RACClient.GetOverviewData()
    if not ok then
        warn("[CustomAdmin] Access Denied: You are not authorized to open the Admin Panel.")
        return
    end

    isOpen = true
    screenGui = Instance.new("ScreenGui")
    screenGui.Name = "CustomRACAdminGui"
    screenGui.ResetOnSpawn = false
    screenGui.Parent = playerGui

    -- Main Container Window
    local mainFrame = Instance.new("Frame")
    mainFrame.Size = UDim2.new(0, 520, 0, 380)
    mainFrame.Position = UDim2.new(0.5, -260, 0.5, -190)
    mainFrame.BackgroundColor3 = Color3.fromRGB(20, 23, 28)
    mainFrame.BorderSizePixel = 0
    mainFrame.Parent = screenGui
    Instance.new("UICorner", mainFrame).CornerRadius = UDim.new(0, 10)

    -- Header Title
    local title = Instance.new("TextLabel")
    title.Size = UDim2.new(1, -40, 0, 36)
    title.Position = UDim2.new(0, 16, 0, 8)
    title.BackgroundTransparency = 1
    title.Font = Enum.Font.GothamBold
    title.TextSize = 16
    title.TextColor3 = Color3.fromRGB(255, 255, 255)
    title.TextXAlignment = Enum.TextXAlignment.Left
    title.Text = string.format("🛡️ Security Admin Panel — Role: %s", role or "Admin")
    title.Parent = mainFrame

    -- Scrolling Player List
    local scroll = Instance.new("ScrollingFrame")
    scroll.Size = UDim2.new(1, -32, 1, -60)
    scroll.Position = UDim2.new(0, 16, 0, 48)
    scroll.BackgroundTransparency = 1
    scroll.BorderSizePixel = 0
    scroll.ScrollBarThickness = 4
    scroll.Parent = mainFrame

    RefreshPlayerList(scroll)
end

local function CloseAdminPanel()
    isOpen = false
    if screenGui then
        screenGui:Destroy()
        screenGui = nil
    end
end

-- Keybind toggle: F4 key
UserInputService.InputBegan:Connect(function(input, gpe)
    if gpe then return end
    if input.KeyCode == Enum.KeyCode.F4 then
        if isOpen then CloseAdminPanel() else OpenAdminPanel() end
    end
end)

# Example 2: In-Game Chat Moderation Commands

You can connect RAC directly to Roblox's modern TextChatService:

-- ServerScriptService/ChatModerator.luau
local TextChatService = game:GetService("TextChatService")
local Players = game:GetService("Players")
local RAC = require(game.ServerScriptService.RAC.Core.Engine)
local PermissionManager = RAC:GetPermissionManager()

local kickCommand = Instance.new("TextChatCommand")
kickCommand.Name = "RACKickCommand"
kickCommand.PrimaryAlias = "/kick"
kickCommand.Parent = TextChatService:WaitForChild("TextChatCommands")

kickCommand.Triggered:Connect(function(originTextSource, message)
    local sender = Players:GetPlayerByUserId(originTextSource.UserId)
    if not sender or not PermissionManager.CanExecute(sender, "KICK_PLAYER") then
        return
    end

    local tokens = string.split(message, " ")
    local targetName = tokens[2]
    local reason = table.concat(tokens, " ", 3) or "Kicked by staff"

    local targetPlayer = Players:FindFirstChild(targetName) :: Player?
    if targetPlayer then
        RAC:KickPlayer(targetPlayer, reason, sender.Name)
    end
end)

# Next Steps