OpenComputers+TGBotApi = ❤
Начнём с базы.
База
Наш сетап:
- Minecraft 1.12.2
- Мод OpenComputers
- Мод Storage Drawers
- Сервер, на котором это всё дело будет вариться
- Бот в телеграме
Если кто не знает: OpenComputers (а так же его соперник Computer craft) - это мод, которой добавляет компьютеры, сервера и, самое главное, роботов. Все эти штуки можно программировать под свои нужды: робот-копатель, компьютер-контроллер-драконьего-реактора, {любой-другой-вариант-применения-нейм}. Программируются они на языке Lua.
Lua - замечательный язык - работает поверх чистого си (есть другие реализации интерпретаторов, но мы сейчас не об этом), обладает динамической типизацией, прост в освоении, даже при полном незнании никаких других языков. А ещё,в сталкере на нём скрипты пишутся, воть.
Проблема
В чём, собственно, заключается проблема? А вот: наше развитие на севере в текущей сборке подходит к концу, совсем немного остаётся изучить/попробовать. На мои плечи упал мод EnvironmentalTech. Если кто играл - знает, что там есть замечательные void-майнеры. И хотя никаких проблем с ресурсами не наблюдалось, хочется же замутить классный сетап, а если ещё там что-нибудь автоматизировать… Ух!
Вот отсюда и растут ноги проблемы: майнеры на начальных уровнях очень медленные. И вроде бы, чёрт с этим, бери да апгрейдь, так нет - каждый следующий тир требует Кристал, который может добыть предыдущий тир.таким образом, ты не можешь с самого начала сделать себе самую крутую сборку и тащить ресурсы из воздуха. К очень маленькой скорости начальных майнеров добавляется расширение списка добываемых руд у каждого последующего тира. Таким образом, чем выше тир, тем больше разных руд; чем больше разных руд, тем меньше шанс, что Майнер добудет Кристал нужного тира.
Вот они слева направо
Мучить себя постоянными заходами на сервер не очень хочется, в конце-концов - есть другие дела, поэтому было решено дополнить сборку модом OpenComputers и сделать бота, который бы раз в час оповещал о наличии различных руд в нашей системе (я рядом с майнерами поставил контроллер из Storage Drawers и к нему присобачил кучу ящиков, чтобы автоматом сортировалось; данные ресурсы не идут в систему Refined Storage, просто стоят рядом, гыгы).
А это - сам массив ящиков
Решение проблемы
Разделим решение проблемы на три этапа:
- Создание телеграм-бота
- Создание робота на сервере
- Написание программы
1. Создание телеграм-бота
Это довольно просто:
- Открываем @BotFather
- Прописываем
/newbot
- Следуем инструкции создания бота
По окончании создания (можно ещё потыкать по кнопкам, настроить всякие штуки, но это сейчас не важно) @BotFather отправит сообщение такого вида:
Такого бота уже нет, токен не работает
Выдёргиваем токен и записываем куди-нибудь. Главное - никому его не показывать!
Note: начните диалог с ботом первым - боты не могут инициировать диалог с незнакомым пользователем
2. Создание робота на сервере
В OpenComputers роботы делаются в ассемблере.
Вот примерно такая базовая сборка у меня. Буквально - необходимый для работы минимум:
- CPU (у меня - Tier 3)
- Memory x2 (у меня - Tier 3.5)
- EEPROM (Lua BIOS)
- HDD (у меня - Tier 3)
- GPU (у меня - Tier 3)
- Inventory Upgrade
- Inventory Controller Upgrade
- Screen (только Tier 1)
- Computer Case (у меня - Tier 3)
Note: я использую Tier 3 элементы для демонстрации (и потому что ресурсов много), можно обойтись меньшим ресурсопотреблением, потому что в действительности роботу не нужно будет столько дисковой и оперативной памятию
3. Написание программы
Программу можно писать в интерфейсе робота, а можно на локальном компьютере (потом залить через copy+paster либо pastebin). Всё зависит от ваших предпочтений.
Начнём с улучшения методов ходьбы. Бот может ходить прямо/назад/вверх/вниз. Нам нужно сделать сдвиг влево/вправо (чтобы постоянно не повторяться).
-- Этот импорт нам нужен для управления роботом
local robot = require("robot")
...
-- Данная функция передвинет бота на блок враво
local function right()
robot.turnRight();
robot.forward();
robot.turnLeft();
end
-- Данная функция передвинет бота на блок влево
local function left()
robot.turnLeft();
robot.forward();
robot.turnRight();
end
Вернёмся к обзорному изобраажению массива ящиков и запишем, сколько у нас ящиков в ряду. В моём случае их 13.
Напишем функцию, которая будет проходить роботом по ряду и как-то с этим рядом взаимодействовать.
-- Мы можем писать <direction> в виде целых чисел, но куда
-- удобнее пользоваться той библиотекой
local sides = require("sides")
...
-- Данная функция пройдёт <times> раз в направлении <direction>
-- и применит на каждый блок функцию <fn>
local function scan_row(direction, times, fn)
local mf = nil
-- <left> и <right> - это функции, которые мы описали выше
if direction == sides.left then
mf = left
elseif direction == sides.right then
mf = right
end
-- В lua цикл for идёт [start, end], не как в C++: [start, end)
-- Поэтому и <times-1> (хотя можно начать с 1 и проходить до times, да)
for i=0,times-1 do
fn()
mf()
end
end
Почему я решил передавать функцию fn
? Потому что это делает систему более гибкой. Данную функцию можно будет просто запихать в любой другой скрипт и просто передавать нужную функцию взаимодействия.
Теперь нам нужна эта самая функция-взаимодействия.
-- Библиотека взаимодействия с компонентам
local component = require("component")
...
-- Номер слота ящика. Всегда 2 🤷
local slot = 2
-- Запишем нужный компонент в переменную, для простоты доступа
local ic = component.inventory_controller
-- Здесь будем хранить данные о состоянии предметов в массиве
local data_table = {}
...
-- А вот и сама функция
local function get_drawer_data(side)
-- Пытаемся получить доступ к слоту <slot> через сторону <side>
local slot_data = ic.getStackInSlot(side, slot)
-- Если получилось, то записываем имя [modid:itemname], надпись
-- и кол-во предметов в слоте, затем записываем в таблицу
if slot_data then
local item_data = {}
item_data.name = slot_data.name
item_data.label = slot_data.label
item_data.size = slot_data.size
data_table[slot_data.name] = item_data
end
end
Теперь, когда у нас есть (пока не совсем) рапорт о состоянии нашего массива ящиков, нам надо бы привести это к человеко-читаемому виду и отправить в телеграмм.
-- Нам понадобится конвертировать данные в формат JSON,
-- а стандартных средст для этого нет, поэтому
-- воспользуемся этой замечательной библиотекой
-- (файл дать не могу из-за авторских прав)
-- from: http://regex.info/code/JSON.lua
JSON = (loadfile "JSON.lua")()
-- Ваш идентификатор TG (можно узнать через @JsonDumpBot)
local chat_id = "XXXXXXXXX"
-- Токен бота
local token = "XXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
local method = "sendMessage"
local url = "https://api.telegram.org/bot" .. token .. "/" .. method
...
-- Проходим по таблице с данными, собираем отчёт
local function form_report()
local outcome = "Report on " .. os.date() .. "@MLC (Minecraft Local Time)"
for k,v in pairs(data_table) do
outcome = outcome .. "\n" .. v.label .. " - " .. v.size
end
outcome = outcome .. "\n\nNext update in 1 hour"
return outcome
end
-- Отправляем данные через бота себе в TG
local function send_data(data)
-- Собираем отправляемые данные в новую таблицу
local j_data = {}
j_data.chat_id = chat_id
j_data.text = data
-- Нужные для отправки заголовки
local r_headers = {}
r_headers["Content-Type"] = "application/json"
-- Конвертируем нагрузку в JSON
local raw_json_text = JSON:encode(j_data)
-- Отправляем запрос
local handle = internet.request(url, raw_json_text, r_headers, "POST")
end
Note:
os.date()
возвращает текущее время с момента старта мира, не настоящее время
И в завершим скрипт функцией, которая будет объединять всё то, что написали выше.
-- Почему бы не использовать глобальную переменную для цикла работы?
-- Можно будет придумать какое-то внешнее действие, переключающее
-- переменную в False, но об этом я сейчас говорить не буду
local running = true
...
-- Основная функция
local function main()
-- Будем считать итерации - просто для удобства
local iterations = 0
while running do
print("Iteraion", iterations)
iterations = iterations + 1
-- Можно было бы как-то по-другому сделать, но так тоже хорошо
-- Тут в качестве <fn> передаётся анонимная функция, которая будет вызывать
-- функцию получения данных из слота <sides.front>
scan_row(sides.right, row_width, function() get_drawer_data(sides.front) end)
robot.up()
scan_row(sides.left, row_width, function() get_drawer_data(sides.front) end)
robot.up()
scan_row(sides.right, row_width, function() get_drawer_data(sides.front) end)
robot.up()
scan_row(sides.left, row_width, function() get_drawer_data(sides.front) end)
robot.down()
robot.down()
robot.down()
local report = form_report()
send_data(report)
print("Sleeping for 1 hour...")
os.sleep(60 * 60)
data_table = {}
end
end
-- И естественно - надо вызвать нашу функцию
main()
Note:
os.sleep(seconds)
зависит от TPS сервера.
В общем-то на этом и всё. Осталось зарядить робота, залить файл (если писали не на роботе) и запустить скрипт - робот пойдёт. После каждого прохода бот будет отправлять вам отчёт и засыпать на час. Сообщения будут такого вида:
Блок-дополнение
Ссылка на файл: roroutine_cp.lua
Листинг кода:
local component = require("component")
local internet = require("internet")
local sides = require("sides")
local robot = require("robot")
-- from: http://regex.info/code/JSON.lua
JSON = (loadfile "JSON.lua")()
local chat_id = "XXXXXXXXX"
local token = "XXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
local method = "sendMessage"
local url = "https://api.telegram.org/bot" .. token .. "/" .. method
local row_width = 12
local slot = 2
local ic = component.inventory_controller
local data_table = {}
local running = true
local function get_drawer_data(side)
local slot_data = ic.getStackInSlot(side, slot)
if slot_data then
local item_data = {}
item_data.name = slot_data.name
item_data.label = slot_data.label
item_data.size = slot_data.size
data_table[slot_data.name] = item_data
end
end
local function right()
robot.turnRight();
robot.forward();
robot.turnLeft();
end
local function left()
robot.turnLeft();
robot.forward();
robot.turnRight();
end
local function scan_row(direction, times, fn)
local mf = nil
if direction == sides.left then
mf = left
elseif direction == sides.right then
mf = right
end
for i=0,times-1 do
fn()
mf()
end
end
local function form_report()
local outcome = "Report on " .. os.date() .. "@MLC (Minecraft Local Time)"
for k,v in pairs(data_table) do
outcome = outcome .. "\n" .. v.label .. " - " .. v.size
end
outcome = outcome .. "\n\nNext update in 1 hour"
return outcome
end
local function send_data(data)
local j_data = {}
j_data.chat_id = chat_id
j_data.text = data
local r_headers = {}
r_headers["Content-Type"] = "application/json"
local raw_json_text = JSON:encode(j_data)
local handle = internet.request(url, raw_json_text, r_headers, "POST")
end
local function main()
local iterations = 0
while running do
print("Iteraion", iterations)
iterations = iterations + 1
scan_row(sides.right, row_width, function() get_drawer_data(sides.front) end)
robot.up()
scan_row(sides.left, row_width, function() get_drawer_data(sides.front) end)
robot.up()
scan_row(sides.right, row_width, function() get_drawer_data(sides.front) end)
robot.up()
scan_row(sides.left, row_width, function() get_drawer_data(sides.front) end)
robot.down()
robot.down()
robot.down()
local report = form_report()
send_data(report)
print("Sleeping for 1 hour...")
os.sleep(60 * 60)
data_table = {}
end
end
main()