Эксклюзив Софт MoonBot - внутриигровые боты для Moonloader

moonbot logo.png

Actual verison: 1.12
Немного лирики
Это мой первый серьезный проект на C++. Вероятно, многие опытные разработчики заметят дикие костыли (увы я не склонен бегать к каждому в ЛС задавать постоянно вопросы, а предпочитаю найти всё сам). Буду рад конструктивной доброжелательной критике, а также предложениям, как можно что-либо улучшить. До начала разработки я делал исключительно пару SF плагинов, многое наверстывал в процессе). По правде говоря я искренне хотел доделать этот проект и как видите, что-то у меня вроде вышло :)

MoonBot - модуль для Moonloader, позволяющая создавать внутриигровых ботов, вроде тех, что вы могли лицезреть в RakSAMP, RakBot, Overlight. Вы ограничиваетесь исключительно сервером.
У каждого бота имеется свой индекс, по которому к нему можно обращаться. По разумным причинам, индексы не объединены у всех скриптов. То есть у каждого скрипта может быть бот с 1-ым индексом, но доступа к чужому боту с таким же индексом у них нет.
Давайте сразу договоримся, что переменная mb будет значить это:
Lua:
local mb = require('MoonBot') -- Имя аргумента может быть любое, в дальнейшем для удобства будет использоваться это.
Примечание
То что в фигурных скобках {} - опциональный аргумент, то что в квадратных скобках [] - обязательный аргумент.
Название методаОписание
local bot = mb.add({Имя})Регистрирует бота. В ответ вы получаете его хендл
mb.remove([Индекс бота])Удаляет бота по его индексу
mb.registerIncomingRPC([ID RPC])Регистрирует отлавливание указанного RPC. Для приема используйте функцию onBotRPC(botHandle, rpcId, bitStream)
mb.disconnectAfterUnload([bool status])Устанавливает то, будут ли боты авто-удалятся при oтpaбaтывaнии функции ниже
mb.unload()Выгружает все хуки/ботов (касаемо последних, если не указан аргумент выше на true)
mb.updateCallbacks()Обновляет хуки. Я пытался сделать без этого костыля, но особо не вышло :/
local bots = mb.getBots()
Возвращает таблицу с хендлами ботов​
local bot = mb.getBotHandleByIndex([Индекс бота])Возвращает хендл бота по индексу
local bitStream = mb.getBitStream()Возвращает объект BitStream. Необходимо удалять!
local data = mb.getPlayerData()Возвращает объект PlayerData. Удаляется сам.
local data = mb.getIncarData()Возвращает объект IncarData. Удаляется сам.
local data = mb.getPassengerData()Возвращает объект PassengerData. Удаляется сам.
local data = mb.getUnoccupiedData()
Возвращает объект UnoccupiedData. Удаляется сам.​
local data = mb.getTrailerData()Возвращает объект TrailerData. Удаляется сам.
local data = mb.getBulletData()Возвращает объект BulletData. Удаляется сам.
local data = mb.getSpectatorData()Возвращает объект SpectatorData. Удаляется сам.
local data = mb.getAimData()
Возвращает объект AimData. Удаляется сам.​
Примечание
То что в фигурных скобках {} - опциональный аргумент, то что в квадратных скобках [] - обязательный аргумент.
Название метода / аргументаОписание
local index = bot.indexПолучает индекс бота. Доступно только для чтения
local name = bot.nameПолучает имя бота. Доступно только для чтения
local connected = bot.connectedПолучает статус подключенности бота (bool). Доступно только для чтения
local playerId = bot.playerIDПолучает серверный ID бота. Доступно только для чтения
local hp = bot.hp
bot.hp = 100
Получает/устанавливает здоровье бота
local armor = bot.armor
bot.armor = 100
Получает/устанавливает броню бота
local x, y, z = bot.position.x, bot.position.y, bot.position.z
bot.position.x, bot.position.y, bot.position.z = 0, 0, 0
Получает/устанавливает позицию бота
local password = bot.password
bot.password = '123123'
Получает/устанавливает пароль для сервера с паролем. Пустая строка - пароля нет.
bot:connect({IP}, {Port})Подключает бота к серверу
bot:changeName([Name])Меняет игровой ник боту. Учтите, что происходит переподключение к серверу
bot:connectAsNPC([bool state])Устанавливает то, будет ли бот подключаться как NPC. Полезно для обхода античита
bot:disconnect()Отключает бота от сервера
bot:sendRPC([rpcId], [BitStream])Отправляет указанный RPC от лица бота
bot:sendBitStream([BitStream])Отправляет указанный bitStream от лица бота
bot:sendPlayerData([data])Отправляет пакет ID_ONFOOT_SYNC от лица бота
bot:sendVehicleData([data])Отправляет пакет ID_DRIVER_SYNC от лица бота
bot:sendPassengerData([data)]Отправляет пакет ID_PASSENGER_SYNC от лица бота
bot:sendUnoccupiedData([data])Отправляет пакет ID_UNOCCUPIED_SYNC от лица бота
bot:sendTrailerData([data])Отправляет пакет ID_TRAILER_SYNC от лица бота
bot:sendSpectatorData([data])Отправляет пакет ID_SPECTATOR_SYNC от лица бота
bot:sendBulletData([data])Отправляет пакет ID_BULLET_SYNC от лица бота
bot:sendAimData([data])Отправляет пакет ID_AIM_SYNC от лица бота
bot:sendRequestClass([classId])Отправляет RPC RequestClass от лица бота
bot:sendEnterVehicle([vehicleId], [isPassenger])Отправляет RPC EnterVehicle от лица бота
bot:sendClickPlayer([playerID], [source])Отправляет RPC ClickPlayer от лица бота
bot:sendCommand([команда с /])Отправляет команду от лица бота
bot:sendChat([message])Отправляет сообщение от лица бота
bot:sendSpawn()Отправляет RPC Spawn от лица бота
bot:sendDeathNotification([reason], [killerId])Отправляет RPC смерти от лица бота
bot:sendDialogResponse([dialogId], [buttonId], [listboxId], [input])Отправляет ответ на диалог от лица бота
bot:sendClickTextdraw([textdrawId])Отправляет клик на textDraw от лица бота
bot:sendInteriorChange([interiorId])Отправляет смену интерьера от лица бота
bot:sendRequestSpawn()Отправляет RPC RequestSpawn от лица бота
bot:sendPickedUpPickup([pickupId])Поднимает пикап от лица бота
bot:sendExitVehicle([vehicleId])Отправляет RPC ExitVehicle от лица бота
bot:setReconnectTime([time in ms])Устанавливает время автореконнекта в мс
bot:setDelay([time in ms])Устанавливает задержку между синхрой в мс. От этого зависит пинг. Будьте аккуратны с этой опцией, ибо прокси пока нет! Изначальная формула расчета: 5 * (Индекс бота)
bot:giveDamage([damage])Наносит урон боту. Эта функция используется при выстреле в бота кем-то.
Примечание
То что в фигурных скобках {} - опциональный аргумент, то что в квадратных скобках [] - обязательный аргумент.
Большинство определений позаимствованы из вики мунлоадера, чтобы дать более грамотное описание
Название методаОписание
bs:remove()Удаляет битстрим. Необходимо вызывать после работы с ним, если вы получили его с помощью mb.getBitStream()
bs:setReadOffset([offset])Устанавливает смещение для последующего чтения битстрима (в битах)
bs:setWriteOffset([offset])Устанавливает смещение для последующей записи в битстрим (в битах)
bs:resetReadPointer()Сбрасывает указатель чтения битстрима
bs:resetWritePointer()Сбрасывает указатель записи битстрима
bs:ignoreBits([bits])Осуществляет пропуск битов в указателе чтения/записи битстрима
local bits = bs:getNumberOfBitsUsed()Возвращает количество записанных битов в битстриме
local bits = bs:getNumberOfUnreadBits()Возвращает количество непрочитанных битов в битстриме
local bool = bs:readBool()Читает значение типа boolean из BitStream
local int = bs:readInt8()Читает значение типа byte (1 байт) из BitStream
local int = bs:readInt16()Читает значение типа short (2 байта) из BitStream
local int = bs:readInt32()Читает значение типа integer (4 байта) из BitStream
local float = bs:readFloat()Читает значение типа float из BitStream
local string = bs:readString8()Читает строку, размером в 1 байт из BitStream
local string = bs:readString16()Читает строку, размером в 2 байта из BitStream
local string = bs:readString32()Читает строку, размером в 4 байта из BitStream
bs:writeBool([bool])Записывает значение типа boolean в BitStream
bs:writeInt8([int])Записывает значение типа byte (1 байт) в BitStream
bs:writeInt8([int])Записывает значение типа short (2 байта) в BitStream
bs:writeInt8([int])Записывает значение типа integer (4 байта) в BitStream
bs:writeFloat([float])Записывает значение типа float в BitStream
bs:writeString8([string])Записывает строку, размером в 1 байт из BitStream
bs:writeString16([string])Записывает строку, размером в 2 байта из BitStream
bs:writeString32([string])Записывает строку, размером в 4 байта из BitStream
local string = bs:decodeString([maxCharsToWrite])Декриптует строку из BitStream`a и записывает её в буфер
Примечания
1. Векторы (x, y, z, w) пока нельзя передавать в качестве массива. Возможно позже исправлю, пока имеет то, что имеем.
2. Структуры готовых пакетов идентичны тем, что описаны в библиотеке Samp.Lua
  • data.leftRightKeys
  • data.upDownKeys
  • data.keysData
  • data.position.x, data.position.y, data.position.z
  • data.quaternion.x, data.quaternion.y, data.quaternion.z, data.quaternion.w
  • data.health
  • data.armor
  • data.weapon
  • data.specialAction
  • data.moveSpeed.x, data.moveSpeed.y, data.moveSpeed.z
  • data.surfingOffsets.x, data.surfingOffsets.y, data.surfingOffsets.z
  • data.surfingVehicleId
  • data.animationId
  • data.animationFlags
  • data.vehicleId
  • data.leftRightKeys
  • data.upDownKeys
  • data.keysData
  • data.position.x, data.position.y, data.position.z
  • data.quaternion.x, data.quaternion.y, data.quaternion.z, data.quaternion.w
  • data.moveSpeed.x, data.moveSpeed.y, data.moveSpeed.z
  • data.vehicleHealth
  • data.playerHealth
  • data.armor
  • data.currentWeapon
  • data.siren
  • data.landingGearState
  • data.trailerId
  • data.trainSpeed
  • data.vehicleId
  • data.seatId
  • data.currentWeapon
  • data.health
  • data.armor
  • data.leftRightKeys
  • data.upDownKeys
  • data.keysData
  • data.position.x, data.position.y, data.position.z
  • data.vehicleId
  • data.seatId
  • data.position.x, data.position.y, data.position.z
  • data.roll.x, data.roll.y, data.roll.z
  • data.direction.x, data.direction.y, data.direction.z
  • data.moveSpeed.x, data.moveSpeed.y, data.moveSpeed.z
  • data.turnSpeed.x, data.turnSpeed.y, data.turnSpeed.z
  • data.vehicleHealth
  • data.trailerId
  • data.position.x, data.position.y, data.position.z
  • data.roll.x, data.roll.y, data.roll.z
  • data.direction.x, data.direction.y, data.direction.z
  • data.speed.x, data.speed.y, data.speed.z
  • data.unk
  • data.leftRightKeys
  • data.upDownKeys
  • data.keysData
  • data.position.x, data.position.y, data.position.z
  • data.targetType
  • data.targetId
  • data.origin.x, data.origin.y, data.origin.z
  • data.target.x, data.target.y, data.target.z
  • data.center.x, data.center.y, data.center.z
  • data.weaponId
  • data.camMode
  • data.camFront.x, data.camFront.y, data.camFront.z
  • data.camPos.x, data.camPos.y, data.camPos.z
  • data.aimZ
  • data.camExtZoom
  • data.weaponState
  • data.unknown
Первое, что мы делаем, подключаем библиотеку:
Lua:
local mb = require('MoonBot') -- Имя аргумента может быть любое, в дальнейшем для удобства будет использоваться это.
В конец кода или куда угодно добавляем следующее (или если у вас уже есть onScriptTerminate, то дописываете):
Lua:
function onScriptTerminate(script, quitGame)
    if script == thisScript() then
        mb.unload()
    end
end
Если мы хотим отлавливать пакеты, RPC, то:
  1. Добавляем в бесконечный цикл метод mb.updateCallbacks()
    Lua:
    function main()-- code
    while true do -- Важно! Если у вас уже есть беск. цикл, то не нужно делать второй! Просто добавьте туда вызов этого метода!
        wait(0) -- Задержка может быть любой, но лучше до 100 мс
        mb.updateCallbacks()
    end
  2. Объявляем функции: если хотите перелавливать входящие RPC, то:
    Lua:
    function onBotIncomingRPC(bot, rpcId, bs) -- bot - хендл бота, rpcId - id полученного rpc, bs - битстрим (с ним нельзя работать как в мунлоадеровским! Как им пользоваться описывалось ранее)
    -- code
    end
    Важно! В случае с RPC в function main еще нужно указать id RPC, которые вы намерены ловить! Делается это так:
    Lua:
    function main()    -- code
        mb.registerIncomingRPC(12) -- Регистрируем RPC SetPlayerPos для примера.
        while true do
            wait(0)
            mb.updateCallbacks()
        end
    end
    Если хотите перелавливать входящие пакеты, то:
    Lua:
    function onBotIncomingPacket(bot, packetId, bs) -- bot - хендл бота, packetId -- ид пакета, bs - битстрим (с ним нельзя работать как в мунлоадеровским! Как им пользоваться описывалось ранее)
    -- code
    end
    Важно! При работе с исходящей синхрой, не забывайте пользоваться сбрасыванием указателя.
    Если хотите перелавливать исходящие RPC, то:
    Lua:
    function onBotOutcomingRPC(bot, rpcId, bs) -- bot - хендл бота, rpcId - id полученного rpc, bs - битстрим (с ним нельзя работать как в мунлоадеровским! Как им пользоваться описывалось ранее)
    -- code
    end
    Если хотите перелавливать исходящие пакеты, то:
    Lua:
    function onBotOutcomingPacket(bot, packetId, bs) -- bot - хендл бота, packetId -- ид пакета, bs - битстрим (с ним нельзя работать как в мунлоадеровским! Как им пользоваться описывалось ранее)
    -- code
    end
Подготовительные работы успешно выполнены! Давайте создадим команду, которая будет подключать нам бота. Также сразу добавим команду на его удаление
Lua:
function main()
    -- code
    sampRegisterChatCommand('bot.add', addCommand) -- Можно не создавать отдельно функцию, сделал так для наглядности
    sampRegisterChatCommand('bot.remove', removeCommand)
    -- code
end

function addCommand(arg) -- Функция, вызываемая при команде /bot.add
    local bot = mb.add(arg) -- Создаём бота. Если бы ник был пустой, то модуль сам бы назвал бота в формате UnnamedИНДЕКС
    bot:connect() -- Подключаем бота. Как видите, айпи и порт не указан: бот возьмёт их сам. Мы бы могли их указать сами, но щас это не нужно
    sampAddChatMessage(string.format('Connecting %s', bot.name), -1) -- Выводим сообщение с ником бота
end

function removeCommand(arg) -- Функция, вызываемая при команде /bot.remove
    if arg:match('%d+') then -- Проверяем, что переданный аргумент - число
        local bot = mb.getBotHandleByIndex(tonumber(arg)) -- Получаем хендл бота по индексу (не рекомендуется хранить хендлы в таблицах)
        if bot ~= nil then -- Проверяем, что бот с таким индексом действительно есть
            sampAddChatMessage(string.format('Deleting %s', bot.name), -1) -- Выводим сообщение об удалении заранее (ибо после бот будет удалён и если мы попытаемся что-то прочитать, то нас крашнет)
            mb.remove(tonumber(arg)) -- Удаляем бота
        end
    end
end
По итогу должно получиться так:
Lua:
local mb = require('MoonBot')

function onBotRPC(bot, rpcId, bs)
    sampAddChatMessage(string.format('Bot %s got RPC: %d', bot.name, rpcId), -1)
end

function main()
    mb.registerIncomingRPC(12)
    sampRegisterChatCommand('bot.add', addCommand)
    sampRegisterChatCommand('bot.remove', removeCommand)
    while true do
        wait(0)
        mb.updateCallbacks()
    end
end

function addCommand(arg)
    local bot = mb.add(arg)
    bot:connect()
    sampAddChatMessage(string.format('Connecting %s', bot.name), -1)
end

function removeCommand(arg)
    if arg:match('%d+') then
        local bot = mb.getBotHandleByIndex(tonumber(arg))
        if bot ~= nil then
            sampAddChatMessage(string.format('Deleting %s', bot.name), -1)
            mb.remove(tonumber(arg))
        end
    end
end

function onScriptTerminate(script, quitGame)
    if script == thisScript() then
        mb.unload()
    end
end
Заходим в игру проверять команды! При добавлении и удалении (кроме строчки от сервера о подключении) должно получиться так:
1624440829296.png

Всё остальное вы можете пощупать уже самостоятельно, либо посмотреть в примерах.
Lua:
local mb = require('MoonBot')

function main()
    repeat wait(0) until isSampAvailable()
    sampRegisterChatCommand('cv.add', function(param)
        local bot = mb.add(param)
        bot:connect()
    end)
    sampRegisterChatCommand('cv.remove', function(param)
        if param:match('%d+') then
            mb.remove(tonumber(param))
        end
    end)
    while true do
        wait(0)
        mb.updateCallbacks()
    end
end

function onBotOutcomingRPC(bot, rpcId, bs)
    if rpcId == 25 then
        bs:resetReadPointer()
        local ver = bs:readInt32()
        local mod = bs:readInt8()
        local nick = bs:readString8()
        local challengeResponse = bs:readInt32()
        local authkey = bs:readString8()
        local clientVer = bs:readString8()
        local unk = bs:readInt32()
        bs:resetWritePointer()
        bs:writeInt32(ver)
        bs:writeInt8(mod)
        bs:writeString8(nick)
        bs:writeInt32(challengeResponse)
        bs:writeString8(authkey)
        bs:writeString8('MOONBOT')
        bs:writeInt32(unk)
        printStringNow('client version changed!', 1000)
    end
end

function onScriptTerminate(script, quitGame)
    if script == thisScript() then
        mb.unload()
    end
end
Примечание
Примеры ниже рассчитаны на MoonBot v1.0! В них используются устаревшие механики вроде: onBotPacket (а не onBotIncomingPacket), собственноручное отслеживание позиции и т.п. Рассматривайте их как пример, а не готовый код!
Примечание
С коробки у вас может не заработать. На вашей нубо-аризоне могут либо быть выключены боты, либо стоять другие текстдравы.
Также не считайте этот код образцовым, это просто пример, что можно сделать.
Lua:
local mb = require('MoonBot')
local ev = require('lib.samp.events')

local password = '123123'
local raznos = false

local botData = {}

function main()
    repeat wait(0) until isSampAvailable()
    --mb.disconnectAfterUnload(false)
    mb.registerIncomingRPC(61) -- dialog
    mb.registerIncomingRPC(68) -- spawnInfo
    mb.registerIncomingRPC(134) -- textdraw
    sampRegisterChatCommand('abot.add', function(param)
        if param:len() >= 3 then
            local bot = mb.add(param)
            table.insert(botData, {
                index = bot.index,
                logined = false,
                rvanka = false
            })
            bot:connectAsNPC(true)
            bot:connect()
            sampAddChatMessage(string.format('Connecting %s (Index: %d)', bot.name, bot.index), -1)
        end
    end)
    sampRegisterChatCommand('abot.remove', function(param)
        if param:match('%d+') then
            local index = tonumber(param)
            for k, bot in pairs(mb.getBots()) do
                if bot.index == index then
                    sampAddChatMessage(string.format('Removed %s (Index: %d)', bot.name, bot.index), -1)
                    for j, lBot in pairs(botData) do
                        if lBot.index == bot.index then
                            table.remove(botData, j)
                            break
                        end
                    end
                    mb.remove(index)
                    break
                end
            end
        end
    end)
    sampRegisterChatCommand('abot.raznos', function()
        raznos = not raznos
        for k, lBot in pairs(botData) do
            local bot = mb.getBotHandleByIndex(lBot.index)
            if bot ~= nil then
                if not bot.connected and lBot.logined then
                    lBot.rvanka = false
                end
            end
        end
        sampAddChatMessage('raznos mode ' .. (raznos and 'ON' or 'OFF'), -1)
    end)
    while true do
        wait(50)
        mb.updateCallbacks()
        local ped, playerId = nil, -1
        if raznos then
            local x, y, z = getCharCoordinates(PLAYER_PED)
            _, ped = findAllRandomCharsInSphere(x, y, z, 100, true, true)
            _, playerId = sampGetPlayerIdByCharHandle(ped)
            if _ and not sampIsPlayerPaused(playerId) and not sampIsPlayerNpc(playerId) then
                -- fine :3
            else
                ped = nil
            end
        end
        for k, lBot in pairs(botData) do
            local bot = mb.getBotHandleByIndex(lBot.index)
            if bot ~= nil then
                if not bot.connected and lBot.logined then
                    lBot.logined = false
                end
                if raznos then
                    if lBot.logined and ped ~= nil then
                        local angle = getCharHeading(ped)
                        local data = mb.getPlayerData()
                        local multiplier = 10
                        local x, y, z = getCharCoordinates(ped)
                        data.health = getCharHealth(PLAYER_PED)
                        data.armor = 0
                        data.weapon = 0
                        data.moveSpeed.x, data.moveSpeed.y, data.moveSpeed.z = math.sin(-math.rad(angle)) * multiplier, math.cos(-math.rad(angle)) * multiplier, multiplier
                        data.position.x, data.position.y, data.position.z = x - math.sin(-math.rad(angle)), y - math.cos(-math.rad(angle)), z + (isCharOnFoot(ped) and 0.5 or -1)
                        bot:sendPlayerData(data)
                        lBot.rvanka = true
                    else
                        lBot.rvanka = false
                    end
                else
                    lBot.rvanka = false
                end
            end
        end
        if ped ~= nil and raznos then
            printStringNow(string.format('Fisting for ~r~%s[%d]', sampGetPlayerNickname(playerId), playerId), 500)
        end
    end
end

function onBotIncomingPacket(bot, packetId, bs)
    --sampfuncsLog(string.format('%s [%d] got packet: %d', bot.name, bot.index, packetId))
    if packetId == 41 then
        sampAddChatMessage(string.format('%s [%d] successfully connected (PlayerID: %d)', bot.name, bot.index, bot.playerID), -1)
    end
end

function onBotIncomingRPC(bot, rpcId, bs)
    --sampAddChatMessage('got rpc: ' .. rpcId, -1)
    if rpcId == 61 then
        local dialogId = bs:readInt16()
        local style = bs:readInt8()
        local title = bs:readString8()
        local btn1 = bs:readString8()
        local btn2 = bs:readString8()
        if title:find('Авторизация') then
            sampAddChatMessage(string.format('%s [%d] logining...', bot.name, bot.index), -1)
            bot:sendDialogResponse(dialogId, 1, 0, password)
        end
        if title:find('%(1/4%)') then
            sampAddChatMessage(string.format('%s [%d] registering...', bot.name, bot.index), -1)
            bot:sendDialogResponse(dialogId, 1, 0, password)
        end
        if title:find('%[2/5%]') or title:find('%[3/5%]') or title:find('%[3/4%]') then
            bot:sendDialogResponse(dialogId, 1, 0, '')
            sampAddChatMessage(string.format('%s [%d] skipped dialog (title: %s)', bot.name, bot.index, title), -1)
        end
    end
    if rpcId == 68 then
        for k, lBot in pairs(botData) do
            if lBot.index == bot.index and not lBot.logined then
                lBot.logined = true
                bot:sendRequestSpawn()
                bot:sendSpawn()
                sampAddChatMessage(string.format('%s [%d] sent request spawn', bot.name, bot.index), -1)
            end
        end
    end
    if rpcId == 134 then
        local tdId = bs:readInt16()
        if tdId == 254 then
            bot:sendClickTextdraw(242)
            sampAddChatMessage(string.format('%s [%d] selected skin', bot.name, bot.index), -1)
            for k, lBot in pairs(botData) do
                if lBot.index == bot.index then
                    lBot.logined = true
                end
            end
        end
    end
end

function onScriptTerminate(script, quitGame)
    if script == thisScript() then
        mb.unload()
    end
end

function ev.onSendPlayerSync(data)
    local offset = 0
    for k, lBot in pairs(botData) do
        if lBot.logined and lBot ~= nil and not lBot.rvanka then
            offset = offset + 1
            local angle = getCharHeading(PLAYER_PED) - 90
            local sync = mb.getPlayerData()
            sync.position.x, sync.position.y, sync.position.z = data.position.x + math.sin(-math.rad(angle)) * offset, data.position.y + math.cos(-math.rad(angle)) * offset, data.position.z
            sync.health = data.health
            sync.armor = 0
            sync.quaternion.w, sync.quaternion.x, sync.quaternion.y, sync.quaternion.z = data.quaternion[0], data.quaternion[1], data.quaternion[2], data.quaternion[3]
            sync.moveSpeed.x, sync.moveSpeed.y, sync.moveSpeed.z = data.moveSpeed.x, data.moveSpeed.y, data.moveSpeed.z
            sync.weapon = 0
            local bot = mb.getBotHandleByIndex(lBot.index)
            bot:sendPlayerData(sync)
        end
    end
end
Примечание
Не считайте этот код образцовым, это просто пример, что можно сделать.
Также он может быть чучуть устаревшим, 27 числа я наверное выложу более крутую версию.
Lua:
local mb = require('MoonBot')
local ev = require('lib.samp.events')

local bots = {}
local time = 5
local password = '123123'

local targetId = -1
local dist = 10
local hi = false

function onScriptTerminate(script, quitGame)
    if script == thisScript() then
        mb.unload()
    end
end

function onBotIncomingPacket(bot, packetId, bs)
    printStringNow('got packet: ' .. packetId, 500)
    if packetId == 41 then
        sampAddChatMessage('connected: ' .. bot.playerID, -1)
    end
end

function onBotRPC(bot, rpcId, bs)
    if rpcId == 134 then
        local tdId = bs:readInt16()
        sampfuncsLog('td: ' .. tdId)
        if tdId == 637 then
            bot:sendClickTextdraw(637)
            sampAddChatMessage('skin selected', -1)
            bot:sendRequestSpawn()
            bot:sendSpawn()
        end
    end
    if rpcId == 93 then
        local color = bs:readInt32()
        local msg = bs:readString32()
        sampfuncsLog('message: ' .. msg)
        if msg == 'Поздравляем вас с успешной регистрацией!' or msg == '{FFCC00}Авторизация прошла успешно.' or msg == '{DFCFCF}[Подсказка] {DC4747}Вы можете задать вопрос в нашу техническую поддержку /report.' or msg == '[Подсказка] {FFFFFF}Благодарим вас за регистрацию на нашем сервере' then
            bot:sendRequestSpawn()
            bot:sendSpawn()
            sampAddChatMessage(string.format('[Bot #%d]: Spawned', bot.index), -1)
            if msg == '{DFCFCF}[Подсказка] {DC4747}Вы можете задать вопрос в нашу техническую поддержку /report.' then
                for k, botData in pairs(bots) do
                    if botData.index == bot.index then
                        botData.logined = true
                        sampAddChatMessage(string.format('[Bot #%d]: Logined', bot.index), -1)
                        break
                    end
                end
            end
        end
    end
    if rpcId == 61 then
        local dialogId = bs:readInt16()
        local style = bs:readInt8()
        local title = bs:readString8()
        local btn1 = bs:readString8()
        local btn2 = bs:readString8()
        sampAddChatMessage('dialogID: ' .. dialogId, -1)
        if (dialogId == 1 and title:find('Пароль')) or dialogId == 2 then
            bot:sendDialogResponse(dialogId, 1, 0, password)
            sampAddChatMessage(string.format('[Bot #%d]: Logining / Registering...', bot.index), -1)
        end
        if dialogId == 1 and not title:find('Пароль') then
            bot:sendDialogResponse(dialogId, 1, 0, '')
            sampAddChatMessage(string.format('[Bot #%d]: Skipping dialogs...', bot.index), -1)
        end
    end
    if rpcId == 12 then
        local x, y, z = bs:readFloat(), bs:readFloat(), bs:readFloat()
        for k, botData in pairs(bots) do
            if botData.index == bot.index then
                botData.position = {x = x, y = y, z = z}
                pcall(sampAddChatMessage, string.format('Position: %f, %f, %f', botData.position.x, botData.position.y, botData.position.z), -1)
            end
        end
    end
    if rpcId == 68 then
        local team = bs:readInt8()
        local skin = bs:readInt32()
        local unk = bs:readInt8()
        local x, y, z = bs:readFloat(), bs:readFloat(), bs:readFloat()
        for k, botData in pairs(bots) do
            if botData.index == bot.index then
                botData.position = {x = x, y = y, z = z}
            end
        end
    end
end

function getDistanceToPlayer(playerId)
    local _, ped = sampGetCharHandleBySampPlayerId(playerId)
    if _ then
        local mx, my, mz = getCharCoordinates(PLAYER_PED)
        local x, y, z = getCharCoordinates(ped)
        return getDistanceBetweenCoords3d(x, y, z, mx, my, mz)
    else
        return 9999
    end
end

function main()
    repeat wait(0) until isSampAvailable()
    mb.registerIncomingRPC(134)
    mb.registerIncomingRPC(93)
    mb.registerIncomingRPC(61)
    mb.registerIncomingRPC(12)
    mb.registerIncomingRPC(68)
    sampRegisterChatCommand('arizona.add', function(param)
        if param ~= nil then
            local ip, port = sampGetCurrentServerAddress()
            local bot = mb.add(param, ip, port)
            bot:setReconnectTime(10000)
            table.insert(bots, {
                name = bot.name,
                index = bot.index,
                logined = false,
                position = {
                    x = 0,
                    y = 0,
                    z = 0
                },
                timer = 0,
                incar = false,
                seatId = -1,
                hiTimer = 0,
            })
            sampAddChatMessage(string.format('Connecting %s... (index: %d)', param, bot.index), -1)
        end
    end)
    sampRegisterChatCommand('arizona.remove', function(param)
        if param:match('%d+') then
            local i = 0
            for k, botData in pairs(bots) do
                i = i + 1
                if botData.index == tonumber(param) then
                    table.remove(bots, i)
                    mb.remove(tonumber(param))
                    sampAddChatMessage('deleted ' .. param, -1)
                    break
                end
            end
        end
    end)
    sampRegisterChatCommand('arizona.hi', function(param)
        if hi then hi = false return sampAddChatMessage('off', -1) end
        if param:match('%d+') then
            targetId = tonumber(param)
            local _, ped = sampGetCharHandleBySampPlayerId(targetId)
            if _ then
                if isCharOnFoot(ped) and not sampIsPlayerNpc(targetId) then
                    if getDistanceToPlayer(targetId) <= dist then
                        hi = true
                        sampAddChatMessage('on', -1)
                    else
                        sampAddChatMessage('too far', -1)
                    end
                else
                    sampAddChatMessage('player onfoot/npc', -1)
                end
            else
                sampAddChatMessage('incorrect player', -1)
            end
        end
    end)
    while true do
        wait(0)
        mb.updateCallbacks()
        local offset = 0
        for k, botData in pairs(bots) do
            wait(10)
            if botData.logined then
                offset = offset + 1
                if botData.timer > 0 then
                    botData.timer = botData.timer - 1
                end
                if hi and botData.timer <= 0 then
                    local _, ped = sampGetCharHandleBySampPlayerId(targetId)
                    if _ then
                        if isCharOnFoot(ped) and not sampIsPlayerNpc(targetId) then
                            if getDistanceToPlayer(targetId) <= dist then
                                botData.hiTimer = botData.hiTimer + 1
                                local x, y, z = getCharCoordinates(ped)
                                local angle = getCharHeading(ped)

                                x = x + (math.sin(-math.rad(angle)) / 1)
                                y = y + (math.cos(-math.rad(angle)) / 1)

                                angle = angle + 180
                                local b = 0 * math.pi / 360.0
                                local h = 0 * math.pi / 360.0
                                local a = angle * math.pi / 360.0
  
                                local c1, c2, c3 = math.cos(h), math.cos(a), math.cos(b)
                                local s1, s2, s3 = math.sin(h), math.sin(a), math.sin(b)
                  
                                local sync = mb.getPlayerData()

                                sync.health = getCharHealth(PLAYER_PED)
                                sync.armor = 0
                                sync.quaternion.w = c1 * c2 * c3 - s1 * s2 * s3
                                sync.quaternion.z = -( c1 * s2 * c3 - s1 * c2 * s3 )
                                sync.position.x, sync.position.y, sync.position.z = x, y, z
                                sync.moveSpeed.x, sync.moveSpeed.y = 0.01, 0.01

                                local bot = mb.getBotHandleByIndex(botData.index)
                                bot:sendPlayerData(sync)
                                botData.position = {x = x, y = y, z = z}
                                --sync:remove()
                                --printStringNow('sent', 500)

                                printStringNow(botData.hiTimer, 500)
                                if botData.hiTimer >= 50 then
                      
                                    botData.hiTimer = 0
                                    bot:sendCommand('/hi ' .. targetId)
                                end
                            else
                                hi = false
                                sampAddChatMessage('too far', -1)
                            end
                        else
                            hi = false
                            sampAddChatMessage('player incar/npc', -1)
                        end
                    else
                        hi = false
                        sampAddChatMessage('player disappeared', -1)
                    end
      
                end
            end
        end
    end
end

function ev.onSendPlayerSync(data)
    local offset = 0
    for k, botData in pairs(bots) do
        if botData.logined and not hi and getDistanceBetweenCoords3d(botData.position.x, botData.position.y, botData.position.z, data.position.x, data.position.y, data.position.z) <= dist then
            local angle = getCharHeading(PLAYER_PED) + 90
            if botData.timer <= 0 and not stop then
                local bot = mb.getBotHandleByIndex(botData.index)
                offset = offset + 1
                botData.timer = time + offset
                local sync = mb.getPlayerData()
                sync.leftRightKeys = data.leftRightKeys
                sync.upDownKeys = data.upDownKeys
                sync.keysData = data.keysData
                sync.position.x, sync.position.y, sync.position.z = data.position.x + (math.sin(-math.rad(angle)) * offset), data.position.y + (math.cos(-math.rad(angle)) * offset), data.position.z
                sync.moveSpeed.x, sync.moveSpeed.y, sync.moveSpeed.z = data.moveSpeed.x, data.moveSpeed.y, data.moveSpeed.z
                sync.quaternion.w, sync.quaternion.x, sync.quaternion.y, sync.quaternion.z = data.quaternion[0], data.quaternion[1], data.quaternion[2], data.quaternion[3]
                sync.health = data.health
                sync.armor = data.armor
                sync.weapon = data.weapon
                sync.specialAction = data.specialAction
                sync.surfingOffsets.x, sync.surfingOffsets.y, sync.surfingOffsets.z = data.surfingOffsets.x, data.surfingOffsets.y, data.surfingOffsets.z
                sync.surfingVehicleId = data.surfingVehicleId
                sync.animationId = data.animationId
                sync.animationFlags = data.animationFlags
                bot:sendPlayerData(sync)
                botData.position = {x = data.position.x, y = data.position.y, z = data.position.z}
                botData.incar = false
                botData.seatId = -1
                --sync:remove()
            end
        end
    end
end
  • Присутствует ли поддержка прокси?
    • На текущий момент нет. Основная причина: отсутствие у меня подходящих прокси с нормальным пингом.
  • На чем создана библиотека?
    • C++ с применением sol2, а также RakNet (позаимствовано из RakSAMP).
  • Будет ли работать на Arizona, Evolve, Diamond?
    • Боты подключаться будут, а всё остальное зависит от вас.
  • Сколько велась разработка первой версии?
    • Чуть больше месяца. Большинство сил было потрачено на то, чтобы не использовать updateCallbacks(). Но, как видите, он все же используется.
  • На каких версиях проверялось?
    • SA:MP 0.3.7 R1, касаемо мобильных серверов пока ничего не смогу сказать
  • Будет ли поддержка модуля?
    • Смотря, будет ли на нее спрос. Лично я горю этим, так что буду стараться поддерживать.
  • Что на нем вообще можно сделать?
    • Начиная от Join флудера, заканчивая вашей фантазией. Ну условно еще можно сделать ботов-разносчиков, ботов-фармилок
  • При перезагрузке скрипта боты отключаются и их нужно заново подключать. Что с этим можно сделать?
    • Используйте mb.disconnectAfterUnload(false). Однако с ним временно есть проблемы при выходе из игры
1.0:
  • Релиз
1.01:
  • Теперь индексы ботов начинаются с 1, а не с 0
  • Фикс Vehicle (Incar) синхры, теперь она работает корректно
1.1:
  • Добавлена возможность ставить пароль
  • Пофикшены просады ФПС
  • Добавлена возможность собственноручно указывать задержку ботам
  • Теперь позиция, здоровье, броня хранится ботом. С этими значениями можете работать вы, а также сервер
  • Добавлена возможность редактировать исходящую синхронизацию. Будьте осторожны с ней! Пример добавлен в тему.
  • В связи с добавлением исходящей синхры, onBotPacket теперь следует называть onBotIncomingPacket, а onBotRPC следует называть onBotIncomingRPC. На текущий момент скрипты не сломаются, будет уведомление от библиотеки. Однако в дальнейших версиях поддержка старого написания хуков может быть удалено.
  • Бот может получать урон от пуль. Вы также можете сами наносить урон боту.
1.11:
  • Пофикшены просады ФПС, если бот не мог подключиться к серверу
  • Возращена предыдущая система задержки.
1.12
  • Убрано мое "гениальное" решение с синхрой. Теперь не должно отключать от сервера спустя время
  • Теперь bot:disconnect() не приводит к вылету игры
  • Пофиксить зависание игры с включенным mb.disconnectAfterUnload(false)
  • Добавить прокси (???)
  • Перевести векторы (x, y, z (w)) на массивы
Благодарности:
  • @damag за небольшой патч для серверов с React
  • @Lolendor за тесты
Установка: dll из архива закинуть в moonloader/lib.
О багах можете сообщать в эту тему, только пишите подробно, пожалуйста, что и как вы делали, при каких обстоятельствах.
Надеюсь на вашу поддержку! Для меня это серьезный шаг ☺️
 

Вложения

  • MoonBot v1.1.rar
    307.2 KB · Просмотры: 2,249
  • MoonBot 1.12.rar
    256.4 KB · Просмотры: 5,902
Последнее редактирование:

why ega

Известный
1,629
978
возможно-ли сменить имя, введенное в это поле?
Lua:
mb.add({Имя})
 

MrCreepTon

Неизвестный
Автор темы
Всефорумный модератор
1,939
3,997
  • Вау
Реакции: why ega

why ega

Известный
1,629
978
при удалении одного из бота, индекс остальных меняется?
 

why ega

Известный
1,629
978
bot:changeName([Name])Меняет игровой ник боту. Учтите, что происходит переподключение к серверу
так вроде меняется только ник на сервере, т.е. если он до этого не был законнекчен, то ник не сменится
 
  • Эм
Реакции: MrCreepTon

why ega

Известный
1,629
978
Lua:
local px, py, pz = getCharCoordinates(ped)
local b = 0 * math.pi / 360.0
local h = 0 * math.pi / 360.0
local angle = getCharHeading(ped)
local a = angle * math.pi / 360.0
local c1, c2, c3 = math.cos(h), math.cos(a), math.cos(b)
local s1, s2, s3 = math.sin(h), math.sin(a), math.sin(b)
local data = mb.getPlayerData()
local aim = mb.getAimData()
if math.random(0, 1) == 1 then data.keysData = 128 end
if math.random(0, 1) == 1 then data.keysData = 4 end                                             
data.position.x, data.position.y, data.position.z = get_coil_pos(ped, -0.7)
data.quaternion.w = c1 * c2 * c3 - s1 * s2 * s3
data.quaternion.z = -( c1 * s2 * c3 - s1 * c2 * s3 )
aim.camMode = 4
aim.camFront.x, aim.camFront.y, aim.camFront.z = -0.8, -0.5, -0.2
aim.camPos.x, aim.camPos.y, aim.camPos.z = getBodyPartCoordinates(7, ped)
data.weapon = 4
bot.handle:sendAimData(aim)
bot.handle:sendPlayerData(data)

function getBodyPartCoordinates(id, handle)
    local pedptr = getCharPointer(handle)
    local vec = ffi.new("float[3]")
    getBonePosition(ffi.cast("void*", pedptr), vec, id, true)
    return vec[0], vec[1], vec[2]
end
 

MrCreepTon

Неизвестный
Автор темы
Всефорумный модератор
1,939
3,997
bot:changeName([Name])Меняет игровой ник боту. Учтите, что происходит переподключение к серверу
так вроде меняется только ник на сервере, т.е. если он до этого не был законнекчен, то ник не сменится
Переменная с именем в любом случае вроде должна измениться
при удалении одного из бота, индекс остальных меняется?
Не должен
 
  • Нравится
Реакции: Mr.Decript

why ega

Известный
1,629
978
было прикольно бы дообавить функции по типу getCharCoordinates(ped) от лица бота, т.к. я так понимаю щас он может это получать только от меня
 

Mr.Decript

Потрачен
183
61
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Криптон, есть фигня, что бы бот самого себя убивал с дигла. Типо пакет о убивании с оружия отправлял. И что бы я сам себя убивал, ну бот. Просто на нуборп можно заспамить чат тип в розыск чел выдался.
 
  • Вау
Реакции: why ega

MrCreepTon

Неизвестный
Автор темы
Всефорумный модератор
1,939
3,997
было прикольно бы дообавить функции по типу getCharCoordinates(ped) от лица бота, т.к. я так понимаю щас он может это получать только от меня
bot.position есть, меняется тобой/при изменении позиции сервером.
 

MrCreepTon

Неизвестный
Автор темы
Всефорумный модератор
1,939
3,997
[/QUOTE]
Криптон, есть фигня, что бы бот самого себя убивал с дигла. Типо пакет о убивании с оружия отправлял. И что бы я сам себя убивал, ну бот. Просто на нуборп можно заспамить чат тип в розыск чел выдался.
bot:sendDeathNotification([reason], [killerId])Отправляет RPC смерти от лица бота

bot:sendRPC([rpcId], [BitStream])Отправляет указанный RPC от лица бота
 
  • Нравится
Реакции: Mr.Decript

MrCreepTon

Неизвестный
Автор темы
Всефорумный модератор
1,939
3,997
не, я про других педов (позицию можно через синхру, но допустим угол заебешься получать из кватерниона)
В дамагере моем можешь подсмотреть реализацию конвертации угла в кватернион. Только в твоем случае нужно будет операцию в обратном порядке сделать