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

> Как написать биндер для SA:MP на MoonLoader: чат-команда, несколько строк отыгровки с задержкой в lua_thread, теги $ID$, $NAME$, $ARGS$ через string.gsub и бинд на клавишу.

- URL: https://pakulichev.com/wiki/lua-binder
- Author: Pakulichev (https://pakulichev.com/)
- Language: ru
- Updated: 2026-10-16
- Whole site for LLMs: https://pakulichev.com/llms.txt (index), https://pakulichev.com/llms-full.txt (full text)

# Биндер для SA:MP на Lua: команды и отыгровки

Биндер отправляет в чат заранее записанные строки по команде или клавише. На RP-серверах так оформляют однотипные отыгровки: надеть наручники, проверить документы, доложить в рацию. Биндер был основой моей [Police Tools](https://pakulichev.com/projects/police-tools), а в [Police Tools Lite](https://pakulichev.com/projects/police-tools-lite) от большого скрипта остался только он. Ниже соберём упрощённую версию того же механизма: команда, несколько строк с паузами, теги `$ID$`, `$NAME$`, `$ARGS$`, `$SLEEP:мс$`.

Для работы нужны MoonLoader и SAMPFUNCS ([как установить](https://pakulichev.com/wiki/moonloader-install)). Код написан по моим [правилам оформления Lua](https://pakulichev.com/wiki/lua-code-style).

## Чат-команда

`sampRegisterChatCommand` связывает команду с функцией. Функция получает одну строку – всё, что игрок написал после команды:

```lua
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 и кодировку](https://pakulichev.com/wiki/moonloader-config).

## Несколько строк с задержкой

Отыгровка из трёх строк, отправленных одновременно, выглядит как флуд и может попасть под антифлуд сервера. Между строками нужна пауза, а `wait` внутри колбэка команды не работает. Паузы делают в отдельном потоке через `lua_thread.create`:

```lua
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-серверах ник имеет вид `Имя_Фамилия`, поэтому имя и фамилию можно вырезать шаблоном:

```lua
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` не даёт запустить второй бинд, пока не закончился первый, иначе строки двух отыгровок перемешаются.

```lua
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](https://pakulichev.com/wiki/imgui-for-gta-sa). Так устроена Police Tools Lite: бинды в `config/PToolsLite.list`, редактор в окне `/plite`.

Биндер экономит время на рутинных отыгровках, но решения за игрока он принимать не должен. Перед использованием проверьте правила сервера: ограничения на частоту сообщений и автоматизацию там почти всегда есть.

## FAQ

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

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

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

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

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

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