Биндер для SA:MP на Lua: команды и отыгровки

Разбор небольшого биндера на Lua: от одной чат-команды до тегов и задержек по образцу Police Tools Lite.

Автор: Опубликовано: Обновлено: 3 мин чтения

Биндер отправляет в чат заранее записанные строки по команде или клавише. На RP-серверах так оформляют однотипные отыгровки: надеть наручники, проверить документы, доложить в рацию. Биндер был основой моей Police Tools, а в Police Tools Lite от большого скрипта остался только он. Ниже соберём упрощённую версию того же механизма: команда, несколько строк с паузами, теги $ID$, $NAME$, $ARGS$, $SLEEP:мс$.

Для работы нужны MoonLoader и SAMPFUNCS (как установить). Код написан по моим правилам оформления Lua.

Чат-команда

sampRegisterChatCommand связывает команду с функцией. Функция получает одну строку – всё, что игрок написал после команды:

script_name("mini-binder")
script_author("Pakulichev")
function main()
  while not isSampAvailable() do wait(100) end
  sampRegisterChatCommand("hi", function(args)
    sampSendChat("Здравствуйте, " .. args)
  end)
  wait(-1)
end

/hi Иван отправит в чат «Здравствуйте, Иван». Если вызвать /hi без аргументов, args будет пустой строкой, не nil. Регистрировать команды нужно после isSampAvailable(): до загрузки SA:MP функции чата недоступны.

Русский текст в коде сработает, если файл сохранён в CP1251. Почему так и что делать с UTF-8, разобрано в статье про inicfg и кодировку.

Несколько строк с задержкой

Отыгровка из трёх строк, отправленных одновременно, выглядит как флуд и может попасть под антифлуд сервера. Между строками нужна пауза, а wait внутри колбэка команды не работает. Паузы делают в отдельном потоке через lua_thread.create:

local function play_lines(lines)
  lua_thread.create(function()
    for i, line in ipairs(lines) do
      if i > 1 then wait(1000) end
      sampSendChat(line)
    end
  end)
end

Секунда между строками – значение по умолчанию и в Police Tools Lite. Для серверов со строгим антифлудом её стоит увеличить.

Теги: ID, имя и аргументы

Тег – метка в тексте бинда, которую скрипт заменяет перед отправкой. ID своего персонажа даёт sampGetPlayerIdByCharHandle(PLAYER_PED), ник по ID – sampGetPlayerNickname. На RP-серверах ник имеет вид Имя_Фамилия, поэтому имя и фамилию можно вырезать шаблоном:

local function apply_tags(line, args)
  local _, id = sampGetPlayerIdByCharHandle(PLAYER_PED)
  local nick = sampGetPlayerNickname(id)
  local name = nick:match("^([^_]+)") or nick
  local surname = nick:match("_(.+)$") or ""
  line = line:gsub("%$ID%$", function() return tostring(id) end)
  line = line:gsub("%$NAME%$", function() return name end)
  line = line:gsub("%$SURNAME%$", function() return surname end)
  line = line:gsub("%$ARGS%$", function() return args end)
  return line
end

$ в шаблонах Lua означает конец строки, поэтому в тегах он экранируется как %$. Замену передаю функцией: если игрок введёт в аргументах %, строковая замена его исказит.

Бинд целиком

Теперь соберём всё вместе и добавим $SLEEP:мс$: тег в начале строки задаёт паузу перед ней вместо стандартной секунды. Флаг busy не даёт запустить второй бинд, пока не закончился первый, иначе строки двух отыгровок перемешаются.

script_name("mini-binder")
script_author("Pakulichev")
local vkeys = require("vkeys")
local binds = {
  cuff = {
    "/me снял наручники с пояса",
    "$SLEEP:1500$/me надел наручники на $ARGS$",
    "/do Наручники застёгнуты.",
  },
  doc = {
    "Здравствуйте, я $NAME$ $SURNAME$, предъявите документы.",
  },
}
local busy = false
local function apply_tags(line, args)
  local _, id = sampGetPlayerIdByCharHandle(PLAYER_PED)
  local nick = sampGetPlayerNickname(id)
  local name = nick:match("^([^_]+)") or nick
  local surname = nick:match("_(.+)$") or ""
  line = line:gsub("%$ID%$", function() return tostring(id) end)
  line = line:gsub("%$NAME%$", function() return name end)
  line = line:gsub("%$SURNAME%$", function() return surname end)
  line = line:gsub("%$ARGS%$", function() return args end)
  return line
end
local function play_bind(lines, args)
  if busy then return end
  busy = true
  lua_thread.create(function()
    for i, raw in ipairs(lines) do
      local delay = tonumber(raw:match("^%$SLEEP:(%d+)%$"))
      local line = raw:gsub("^%$SLEEP:%d+%$", "")
      wait(delay or (i > 1 and 1000 or 0))
      sampSendChat(apply_tags(line, args))
    end
    busy = false
  end)
end
function main()
  while not isSampAvailable() do wait(100) end
  for cmd, lines in pairs(binds) do
    sampRegisterChatCommand(cmd, function(args)
      play_bind(lines, args)
    end)
  end
  while true do
    wait(0)
    local typing = sampIsChatInputActive() or sampIsDialogActive()
    if isKeyJustPressed(vkeys.VK_F2) and not typing then
      play_bind(binds.doc, "")
    end
  end
end

Бинд на клавишу проверяется в бесконечном цикле main. isKeyJustPressed срабатывает один раз на нажатие, коды клавиш берутся из библиотеки vkeys. Проверки sampIsChatInputActive() и sampIsDialogActive() нужны, чтобы F2 не запускал отыгровку, пока игрок печатает в чат или заполняет диалог.

Куда развивать

Если команда /cuff вызвана без аргументов, $ARGS$ превратится в пустое место. Простая защита – проверить args == "" в play_bind и вывести подсказку через sampAddChatMessage, не отправляя ничего в чат. В Police Tools Lite для строк «только для себя» есть отдельный тег $NOSEND$; его можно сделать так же: найти тег, убрать его из строки и вызвать sampAddChatMessage вместо sampSendChat.

Следующий шаг – вынести таблицу binds из кода в файл в moonloader/config/, чтобы бинды можно было менять без правки скрипта, и сделать окно для редактирования на ImGui. Так устроена Police Tools Lite: бинды в config/PToolsLite.list, редактор в окне /plite.

Биндер экономит время на рутинных отыгровках, но решения за игрока он принимать не должен. Перед использованием проверьте правила сервера: ограничения на частоту сообщений и автоматизацию там почти всегда есть.

Частые вопросы

Почему отыгровку нельзя отправить в цикле без lua_thread?

Колбэк команды выполняется вне корутины скрипта, и wait в нём вызвать нельзя. lua_thread.create запускает отдельный поток MoonLoader, где паузы между строками работают.

Зачем в gsub передавать функцию, а не строку?

Во второй строке-замене gsub символ % служебный. Если аргументы команды содержат %, строковая замена исказит текст или выдаст ошибку. Функция возвращает текст как есть.

Разрешены ли биндеры на серверах SA:MP?

На многих RP-серверах биндеры отыгровок разрешены, но правила у каждого проекта свои. Перед использованием проверьте их, особенно ограничения на флуд и частоту сообщений.