Инвентарь и действия
Как найти предмет, взять его в руку, переложить, выбросить или забрать из сундука — и не попасть на рассинхрон с сервером.
Две системы номеров
В API встречаются два разных номера слота, и путать их — главная причина скриптов, которые «кликают не туда».
hotbar — 0…8позиция в хотбаре слева направо. Её принимают select_slot и второй аргумент swap_slot, её отдают hotbar_slot() и hotbar()slot — id слота окнаномер внутри открытого контейнера. Его принимают все click-функции, его отдают inventory(), find_item() и slot()Id слота зависит от того, что открыто. В собственном инвентаре раскладка всегда одна и та же:
0результат крафта1–4сетка крафта 2×25–8броня: шлем, нагрудник, поножи, ботинки9–35основной инвентарь, три ряда36–44хотбар: id = 36 + hotbar45левая рукаВ сундуке сначала идут его слоты, а инвентарь игрока — после них, поэтому жёстко прописанные числа ломаются. Надёжнее не считать id самому, а брать их из inventory() или find_item(). У каждого предмета оттуда есть поля player (слот принадлежит игроку, а не сундуку), index (номер внутри своего инвентаря) и hotbar (0–8 или nil).
Поиск
player.find_item(id)slot и hotbar. Хотбар в приоритете; если предмет только в инвентаре — hotbar = nilplayer.count_item(id)сколько штук во всём окнеplayer.inventory()все непустые слоты окнаplayer.slot(slotId)один слот, в том числе пустойplayer.held_item(hand)что в руке сейчас: main или offlocal slot, hotbar = player.find_item("minecraft:golden_apple")
if not slot then
client.notify("яблок нет")
elseif hotbar then
print("в хотбаре, позиция", hotbar)
else
print("в инвентаре, id слота", slot)
endId предмета — полный, с неймспейсом: minecraft:trident, а не trident. Узнать его проще всего так: print(player.held_item().id).
Взять предмет в руку
player.select_slot(hotbar)переключить слот хотбара, как колесом мышиplayer.swap_slot(slot, hotbar)поменять слот окна со слотом хотбара. hotbar = 40 — левая рукаЕсли предмет в хотбаре — достаточно select_slot. Если в инвентаре — swap_slot кладёт его в текущий слот хотбара, а то, что там лежало, уходит на его место. Инвентарь открывать не нужно: это та же операция, что и клавиша 1–9 над слотом.
local function take(id)
if player.held_item().id == id then return true end
local slot, hotbar = player.find_item(id)
if not slot then return false end
if hotbar then
player.select_slot(hotbar)
else
player.swap_slot(slot, player.hotbar_slot())
end
return false
endФункция возвращает true, только когда предмет уже в руке. Вызывайте её каждый тик, пока не получите true, — и только потом действуйте.
Почему нужна пауза в тиках
Смена слота и клик уходят на сервер пакетом, а команда в чат — другим пакетом. Если отправить команду в том же тике, сервер может увидеть в руке старый предмет: продать не то, выставить воздух или ответить «возьмите предмет в руку». Поэтому после каждого действия с инвентарём ждите 1–2 тика и проверяйте результат через held_item().
Удобнее всего строить такие скрипты как машину состояний: переменная stage, счётчик wait и один колбэк на tick. Пример — ниже.
Пример: продать предмет при включении
Скрипт срабатывает один раз: берёт трезубец из хотбара или инвентаря, выставляет его командой и сам выключает свой модуль. Верхний уровень файла выполняется при каждом включении модуля, поэтому переменные сбрасываются сами.
local ITEM = "minecraft:trident"
local PRICE = 500
local stage, wait, tries = "find", 0, 0
local function finish(text)
client.notify(text)
stage = "done"
modules.get(client.name).set(false)
end
events.on("tick", function()
if stage == "done" or not player.exists() then return end
if wait > 0 then wait = wait - 1 return end
if stage == "find" then
if player.held_item().id == ITEM then stage = "sell" return end
local slot, hotbar = player.find_item(ITEM)
if not slot then return finish("трезубца нет") end
if hotbar then
player.select_slot(hotbar)
else
tries = tries + 1
if tries > 3 then return finish("не удалось взять") end
player.swap_slot(slot, player.hotbar_slot())
end
wait = 2
elseif stage == "sell" then
player.send_command("market sell " .. PRICE)
finish("выставлен за " .. PRICE)
end
end)Счётчик tries — страховка: если сервер отклонил клик (античит, открытое меню), скрипт не будет свапать бесконечно.
Перекладывание и выброс
player.quick_move(slot)shift-клик: из сундука в инвентарь и обратноplayer.drop_slot(slot, all)выбросить один предмет или весь стакplayer.click_slot(slot, button, action)любой клик окна, если готовых функций не хватаетДействия для click_slot: pickup (обычный клик, button 0 — левая, 1 — правая), quick_move, swap (button — позиция хотбара), clone, throw, quick_craft и pickup_all (двойной клик). Слот -999 — клик за пределами окна: так выбрасывают предмет с курсора.
-- переложить стак в другой слот двумя кликами
player.click_slot(from, 0, "pickup")
player.click_slot(to, 0, "pickup")
-- выкинуть весь мусор
for _, item in ipairs(player.inventory()) do
if item.player and item.id == "minecraft:rotten_flesh" then
player.drop_slot(item.slot, true)
end
endСерверы с античитом не любят десятки кликов за тик. Делайте одно действие за тик или раз в несколько тиков — как в примере с сундуком.
Сундуки и меню сервера
player.container().openоткрыто ли чужое окноplayer.container().titleзаголовок: так отличают аукцион от обычного сундукаplayer.container().container_sizeсколько слотов у самого контейнера, без инвентаря игрокаplayer.container().cursorпредмет на курсореplayer.close_screen()закрыть окно с уведомлением сервераlocal WANT = { ["minecraft:diamond"] = true }
local cooldown = 0
events.on("tick", function()
if cooldown > 0 then cooldown = cooldown - 1 return end
local box = player.container()
if not box or not box.open then return end
for _, item in ipairs(player.inventory()) do
if not item.player and WANT[item.id] then
player.quick_move(item.slot)
cooldown = 2
return
end
end
end)Серверные меню (аукцион, магазины, варпы) — это тоже контейнеры. Открытие отслеживайте событием screen_open, а кликайте не раньше следующего тика: содержимое приходит отдельным пакетом, и в момент открытия окно ещё пустое.
Частые ошибки
hotbar() вместо inventory() для кликову hotbar() поле slot — это 0–8, а не id слота окнакоманда в том же тике, что и свапсервер видит старый предмет — ждите 1–2 тикажёсткие id слотовв сундуке они сдвигаются — берите slot из find_item / inventoryбесконечный цикл без лимита попытокесли клик отклонён, скрипт спамит — считайте попыткиsend_command("/market ...")слеш не нужен: send_command("market sell 500")