NVault

OpenComputers+TGBotApi = ❤

Начнём с базы.

База

Наш сетап:

  • Minecraft 1.12.2
  • Мод OpenComputers
  • Мод Storage Drawers
  • Сервер, на котором это всё дело будет вариться
  • Бот в телеграме

Если кто не знает: OpenComputers (а так же его соперник Computer craft) - это мод, которой добавляет компьютеры, сервера и, самое главное, роботов. Все эти штуки можно программировать под свои нужды: робот-копатель, компьютер-контроллер-драконьего-реактора, {любой-другой-вариант-применения-нейм}. Программируются они на языке Lua.

Lua - замечательный язык - работает поверх чистого си (есть другие реализации интерпретаторов, но мы сейчас не об этом), обладает динамической типизацией, прост в освоении, даже при полном незнании никаких других языков. А ещё,в сталкере на нём скрипты пишутся, воть.

Проблема

В чём, собственно, заключается проблема? А вот: наше развитие на севере в текущей сборке подходит к концу, совсем немного остаётся изучить/попробовать. На мои плечи упал мод EnvironmentalTech. Если кто играл - знает, что там есть замечательные void-майнеры. И хотя никаких проблем с ресурсами не наблюдалось, хочется же замутить классный сетап, а если ещё там что-нибудь автоматизировать… Ух!

Вот отсюда и растут ноги проблемы: майнеры на начальных уровнях очень медленные. И вроде бы, чёрт с этим, бери да апгрейдь, так нет - каждый следующий тир требует Кристал, который может добыть предыдущий тир.таким образом, ты не можешь с самого начала сделать себе самую крутую сборку и тащить ресурсы из воздуха. К очень маленькой скорости начальных майнеров добавляется расширение списка добываемых руд у каждого последующего тира. Таким образом, чем выше тир, тем больше разных руд; чем больше разных руд, тем меньше шанс, что Майнер добудет Кристал нужного тира.

miners_overview

Вот они слева направо

Мучить себя постоянными заходами на сервер не очень хочется, в конце-концов - есть другие дела, поэтому было решено дополнить сборку модом OpenComputers и сделать бота, который бы раз в час оповещал о наличии различных руд в нашей системе (я рядом с майнерами поставил контроллер из Storage Drawers и к нему присобачил кучу ящиков, чтобы автоматом сортировалось; данные ресурсы не идут в систему Refined Storage, просто стоят рядом, гыгы).

drawers

А это - сам массив ящиков

Решение проблемы

Разделим решение проблемы на три этапа:

  1. Создание телеграм-бота
  2. Создание робота на сервере
  3. Написание программы

1. Создание телеграм-бота

Это довольно просто:

  1. Открываем @BotFather
  2. Прописываем /newbot
  3. Следуем инструкции создания бота

По окончании создания (можно ещё потыкать по кнопкам, настроить всякие штуки, но это сейчас не важно) @BotFather отправит сообщение такого вида:

msg

Такого бота уже нет, токен не работает

Выдёргиваем токен и записываем куди-нибудь. Главное - никому его не показывать!

Note: начните диалог с ботом первым - боты не могут инициировать диалог с незнакомым пользователем

2. Создание робота на сервере

В OpenComputers роботы делаются в ассемблере.

robot_assembler_over_wrin4

Вот примерно такая базовая сборка у меня. Буквально - необходимый для работы минимум:

  • 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 сервера.

В общем-то на этом и всё. Осталось зарядить робота, залить файл (если писали не на роботе) и запустить скрипт - робот пойдёт. После каждого прохода бот будет отправлять вам отчёт и засыпать на час. Сообщения будут такого вида:

bmsg


Блок-дополнение

Ссылка на файл: 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()