Studio

Команды администратора: как раздать права и не отдать игру

Как сделать админ-команды, которые нельзя вызвать чужим клиентом: где хранить список прав, почему проверка при входе не работает и что писать в журнал.

Сложно 20 минут проверен 34 дня назад

Что понадобится

Опубликованная игра и основы Luau. Понадобится понимание клиента и сервера — оно здесь главное.

Текст гайда «Команды администратора: как раздать права и не отдать игру»

Содержание

Админ-команды появляются в игре в тот момент, когда автор перестаёт справляться один: нужно кого-то выгнать, что-то перезапустить, кому-то выдать пропущенную награду. Команды пишутся за вечер и почти всегда — с одной и той же дырой.

Дыра выглядит так: клиент проверяет, админ ли игрок, и, если да, показывает панель и отправляет команду на сервер. Сервер команду выполняет — он же «получил её от панели, а панель показывается только админам». Никакой панели у нападающего нет. Есть сообщение, отправленное напрямую.

Правило, которое заменяет весь остальной гайд

Права проверяются в момент выполнения команды и на сервере. Не при входе в игру, не при показе кнопки, не «один раз в начале». Каждый вызов — своя проверка, каждый раз заново.

Шаг 1. Список прав живёт на сервере#

Первое решение — где хранить, кто админ. Вариантов три, и они по-разному удобны.

Где держать список администраторов
СпособКогда подходитЧто учесть
Модуль в ServerStorage со списком UserIdИгра одного автора и пары друзейИзменения только через обновление игры
Ранг в группе RobloxКоманда, которая меняется (как её собрать — в гайде про Team Create и группу)Проверка ходит в сеть — нужен pcall и кэш
Отдельное хранилище DataStoreПрава выдаются прямо в игреВыдавать права умеет только тот, у кого они уже есть

Начинать стоит с первого: он самый простой и самый надёжный. Главное — UserId, а не имя. Имя игрок меняет, и в этот день админом становится тот, кто успел занять освободившийся ник.

ServerStorage → ModuleScript «Admins» · Luau
-- Числовые идентификаторы, а не имена: имя можно сменить,
-- UserId — нет.
local LEVELS = {
    [1234567] = "owner",
    [7654321] = "moderator",
}

local RIGHTS = {
    owner     = { kick = true, respawn = true, announce = true, give = true },
    moderator = { kick = true, respawn = true, announce = true, give = false },
}

local Admins = {}

function Admins.levelOf(player)
    return LEVELS[player.UserId]
end

function Admins.can(player, action)
    local level = LEVELS[player.UserId]

    if level == nil then
        return false
    end

    return RIGHTS[level][action] == true
end

return Admins

Права по действию, а не по человеку

Проверка вида «этот игрок админ» рано или поздно упирается в вопрос «а вот этому можно выдавать предметы?». Проверка вида «этому игроку можно вот это действие» отвечает на него сразу и без переписывания.

Шаг 2. Один канал и одна точка проверки#

Заводим один RemoteEvent в ReplicatedStorage — например, AdminCommand. Не по событию на команду: чем больше входов, тем больше шансов, что в одном из них забудут проверку. Один вход — одно место, где проверка обязана быть.

ServerScriptService → Script «AdminCommands» · Luau
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")

local Admins = require(ServerStorage.Admins)
local channel = ReplicatedStorage:WaitForChild("AdminCommand")

-- Команда описывается правом, которое она требует,
-- и функцией. Забыть проверку тут негде: право
-- лежит рядом с самой командой.
local COMMANDS = {
    kick = {
        right = "kick",
        run = function(_, targetName, reason)
            local target = Players:FindFirstChild(targetName)

            if target == nil then
                return false, "Игрок не найден"
            end

            if Admins.levelOf(target) ~= nil then
                -- Админы не выгоняют друг друга: иначе один
                -- обиженный модератор чистит сервер от своих.
                return false, "Нельзя выгнать администратора"
            end

            target:Kick(reason or "Нарушение правил")

            return true
        end,
    },

    respawn = {
        right = "respawn",
        run = function(_, targetName)
            local target = Players:FindFirstChild(targetName)

            if target == nil then
                return false, "Игрок не найден"
            end

            target:LoadCharacter()

            return true
        end,
    },
}

-- Защита от лавины вызовов: даже у настоящего админа
-- команда не должна уходить сто раз в секунду.
local lastCall = {}

channel.OnServerEvent:Connect(function(player, name, ...)
    if type(name) ~= "string" then
        return
    end

    local command = COMMANDS[name]

    if command == nil then
        return
    end

    -- Вот она, единственная и обязательная проверка.
    if not Admins.can(player, command.right) then
        warn(("%s (%d) попробовал команду %s без прав")
            :format(player.Name, player.UserId, name))

        return
    end

    local now = os.clock()

    if lastCall[player] and now - lastCall[player] < 0.5 then
        return
    end

    lastCall[player] = now

    local ok, err = pcall(command.run, player, ...)

    if not ok then
        warn("Команда " .. name .. " упала: " .. tostring(err))
    end
end)

Players.PlayerRemoving:Connect(function(player)
    lastCall[player] = nil
end)

Обрати внимание на то, чего в обработчике нет. Он не спрашивает клиента, админ ли тот. Он не принимает уровень прав в сообщении. Он не доверяет имени команды дальше, чем «есть такая в списке». Всё, что приходит снаружи, — данные, а не решения.

Три способа сломать собственную админку

Первый: проверить права один раз при входе и запомнить в переменной на клиенте. Второй: передавать в сообщении уровень доступа — его подставят. Третий: сделать «отладочную» команду без проверки и забыть её убрать перед публикацией. Третий встречается чаще первых двух вместе.

Шаг 3. Панель — это удобство, а не защита#

Кнопки админ-панели можно смело показывать только тем, у кого есть права: так удобнее. Но делать это нужно с ясной головой — панель прячется ради порядка на экране, а не ради безопасности. Сервер не должен становиться слабее оттого, что панель у кого-то всё-таки открылась.

Показ панели: сервер сам сообщает клиенту, что тому доступно · Luau
-- Сервер, при входе игрока:
local rights = ReplicatedStorage:WaitForChild("AdminRights")

Players.PlayerAdded:Connect(function(player)
    local level = Admins.levelOf(player)

    if level ~= nil then
        -- Клиенту отправляется только то, что ему рисовать.
        -- Это подсказка интерфейсу, а не пропуск.
        rights:FireClient(player, level)
    end
end)

Клиент получил уровень — нарисовал кнопки. Не получил — не нарисовал. Игрок, который нарисует их себе сам, ничего этим не добьётся: команды всё равно уйдут в тот же канал и упрутся в ту же проверку.

Шаг 4. Журнал#

Команды администратора — это единственное место в игре, где один человек может испортить вечер многим. Значит, каждое применение должно оставлять след: кто, что, кому и когда.

  • UserId того, кто выполнил, — не имя
  • Название команды и её аргументы
  • Кого команда затронула
  • Время и идентификатор сервера — иначе не понять, о каком раунде речь
  • Отдельно — отказы: попытка без прав важнее удачного вызова

Простейший журнал — warn() в вывод сервера: он виден в Creator Hub. Серьёзнее — отдельный DataStore или запись во внешний сервис. Начинать можно с первого, но начинать нужно сразу: журнал, добавленный после первого инцидента, ничего про этот инцидент не расскажет.

Чужие админ-панели#

Готовых наборов админ-команд в свободном доступе много, и часть из них написана хорошо. Проблема не в качестве, а в том, что автор игры ставит к себе код, которого не читал, и раздаёт ему полные права над своей игрой.

Что посмотреть до того, как ставить чужую панель

Открой список админов внутри неё: не вписан ли туда кто-то, кроме тебя. Найди все RemoteEvent и проверь, что каждый обработчик начинается с проверки прав. Поищи обращения к сети — панели незачем ходить на сторонние адреса. Если читать чужой код тяжело, это само по себе ответ: свои двадцать строк надёжнее чужих двух тысяч.

Про что этот гайд — и про что он не про то#

Здесь речь о правах в своей игре: ты автор, ты решаешь, кто и что может. Это обычная работа разработчика.

Получение прав в чужой игре — не работа разработчика, а взлом, и заканчивается он баном аккаунта. Портал такие способы не публикует и не подсказывает — ни здесь, ни где-либо ещё. Как выглядят предложения «скрипта на админку» со стороны и почему за ними всегда стоит кража аккаунта, разобрано в разделе безопасности.

Проверь себя#

  • Каждый обработчик команды начинается с проверки прав — не «панель», а именно обработчик
  • В списке админов только идентификаторы, и в нём нет никого лишнего
  • Ни одна команда не принимает от клиента уровень доступа
  • Отладочные команды без проверки удалены до публикации
  • Каждое применение и каждый отказ попадают в журнал
  • Зайди в свою игру с обычного второго аккаунта и убедись, что панели нет, а прямой вызов канала ничего не даёт

Тот же принцип — «сервер решает, клиент просит» — лежит в основе магазина внутри игры и любой выдачи наград. Если он усвоен на админ-командах, остальное собирается по образцу.

Реклама · игра в Telegram Moki Blast — шарики, боссы и дуэли в Telegram Надуй щёки Моки и собери три шара одного цвета Шесть миров: от руин до Золотого города Король обезьян ждёт на 120-м уровне Дуэли с живыми игроками и турниры Играть в Telegram

Обсуждение

Пиши по делу и без личных данных. Ссылки на сторонние сайты, обмен и «бесплатные робуксы» мы скрываем — почему.

Комментарии пишут те, кто вошёл — так спокойнее всем.

Войти

Пока никто ничего не написал. Будь первым.