local echest_parser = require("storage_parser") local player = require("player") local json = require("json") local filepath = "config/neoscripts/scripts/data/hotbar_eqpupper.json" local echests_filepath = "config/neoscripts/scripts/data/storage.json" -- Настройка задержки между действиями (в игровых тиках: 1 тик = 50 мс) local CLICK_DELAY = 7 local file, err = io.open(filepath, "r") local data_table = {} if file then local content = file:read("*all") file:close() local success, decoded = pcall(json.decode, content) if success and type(decoded) == "table" then data_table = decoded end end -- Функция для чтения базы данных storage.json local function load_echests() local f = io.open(echests_filepath, "r") if not f then return {} end local content = f:read("*all") f:close() local success, decoded = pcall(json.decode, content) if success and type(decoded) == "table" then return decoded end return {} end -- Безопасная проверка: действительно ли пуст слот в контейнере в игре local function is_container_slot_empty(slot) local stack = player.inventory.getStackFromContainer(slot) if not stack then return true end -- Если API возвращает объект воздуха, слот также считается пустым if stack.name == "air" or stack.skyblock_id == "air" then return true end return false end -- Вспомогательная функция поиска предмета в базе данных storage.json local function find_item_in_storage(echests_data, target_id, target_reforge, used_items) for type_key, pages in pairs(echests_data) do -- "ender_chest" или "backpack" for page_key, slots in pairs(pages) do -- "page_1", "page_2" и т.д. local page_num = page_key:match("page_(%d+)") or page_key:match("(%d+)") if page_num then for slot_key, item_data in pairs(slots) do local slot_num = tonumber(slot_key:match("slot_(%d+)")) if slot_num then local match_id = item_data.skyblock_id == target_id local match_reforge = (item_data.reforge or "") == (target_reforge or "") local unique_key = type_key .. "_" .. page_num .. "_" .. slot_num if match_id and match_reforge and not used_items[unique_key] then used_items[unique_key] = true return type_key, tonumber(page_num), slot_num end end end end end end return nil end -- Динамически определяет реальные границы слотов для страницы на основе базы данных local function detect_container_bounds(type_key, slots) local highest_slot = 0 for slot_key, _ in pairs(slots) do local slot_num = tonumber(slot_key:match("slot_(%d+)")) if slot_num and slot_num > highest_slot then highest_slot = slot_num end end local start_slot = 9 local end_slot = 44 -- Значение по умолчанию if type_key == "backpack" then start_slot = 0 -- Округляем до стандартных размеров рюкзаков Hypixel (8, 17, 26, 35, 44) if highest_slot <= 8 then end_slot = 8 elseif highest_slot <= 17 then end_slot = 17 elseif highest_slot <= 26 then end_slot = 26 elseif highest_slot <= 35 then end_slot = 35 else end_slot = 44 end else -- ender_chest start_slot = 9 -- Округляем до стандартных уровней прокачки эндерчеста (17, 26, 35, 44, 53) if highest_slot <= 17 then end_slot = 17 elseif highest_slot <= 26 then end_slot = 26 elseif highest_slot <= 35 then end_slot = 35 elseif highest_slot <= 44 then end_slot = 44 else end_slot = 53 end end return start_slot, end_slot end -- Анализирует свободные места на страницах и сортирует их (сначала самые пустые страницы) local function find_empty_slots_sorted(echests_data) local candidates = {} for type_key, pages in pairs(echests_data) do for page_key, slots in pairs(pages) do local page_num = page_key:match("page_(%d+)") or page_key:match("(%d+)") if page_num then -- Динамически вычисляем рабочие границы слотов для этой страницы local start_slot, end_slot = detect_container_bounds(type_key, slots) local empty_count = 0 local free_slots = {} for s = start_slot, end_slot do local slot_key = "slot_" .. s if not slots[slot_key] then empty_count = empty_count + 1 table.insert(free_slots, s) end end if empty_count > 0 then table.insert(candidates, { type = type_key, page = tonumber(page_num), empty_count = empty_count, free_slots = free_slots }) end end end end -- Сортировка: сначала идут страницы, где свободных мест больше всего table.sort(candidates, function(a, b) return a.empty_count > b.empty_count end) return candidates end -- Получение следующего свободного слота из отсортированного списка страниц local function get_next_free_slot(sorted_pages, occupied_empty_slots) for _, candidate in ipairs(sorted_pages) do for _, s in ipairs(candidate.free_slots) do local unique_key = candidate.type .. "_" .. candidate.page .. "_" .. s if not occupied_empty_slots[unique_key] then occupied_empty_slots[unique_key] = true return candidate.type, candidate.page, s end end end return nil end -- Отправка команд local function run_command(cmd) player.sendCommand(cmd) end -- Переменные состояния для потиковой очереди перемещения local active_queue = nil local current_group_idx = 1 local current_move_idx = 1 local move_step = 1 local state = "IDLE" -- "IDLE", "START_GROUP", "WAITING_FOR_SCREEN", "MOVING_ITEMS", "CLOSING" local timer = 0 local click_timer = 0 -- Таймер задержки между кликами registerClientTick(function() if state == "IDLE" then return end -- Обработка закрытия в самом верху для стабильной работы if state == "CLOSING" then player.inventory.closeScreen() player.addMessage("Hotbar preset loaded successfully!") state = "IDLE" return end timer = timer + 1 local group = active_queue[current_group_idx] if not group then state = "CLOSING" return end if state == "START_GROUP" then -- Открываем контейнер local cmd if group.type == "ender_chest" then cmd = "/ec " .. group.page else cmd = "/backpack " .. group.page end run_command(cmd) state = "WAITING_FOR_SCREEN" timer = 0 elseif state == "WAITING_FOR_SCREEN" then -- Если экран не открылся за 3 секунды (60 тиков), отменяем операцию if timer > 60 then current_group_idx = current_group_idx + 1 if current_group_idx > #active_queue then state = "CLOSING" else state = "START_GROUP" end timer = 0 return end local title = player.inventory.getChestTitle() if title then local is_correct = false if group.type == "ender_chest" and title:find("Ender Chest") then local p = string.match(title, "%((%d+)/") if tonumber(p) == group.page then is_correct = true end elseif group.type == "backpack" and title:find("Backpack") then local p = string.match(title, "#(%d+)") if tonumber(p) == group.page then is_correct = true end end if is_correct then state = "MOVING_ITEMS" current_move_idx = 1 move_step = 1 click_timer = 0 timer = 0 end end elseif state == "MOVING_ITEMS" then local title = player.inventory.getChestTitle() if not title then state = "IDLE" return end local moves = group.moves local move = moves[current_move_idx] if not move then -- Даем серверу время обработать последнее действие перед сменой страницы или завершением работы click_timer = click_timer + 1 if click_timer < CLICK_DELAY then return end click_timer = 0 -- Переходим к следующему контейнеру/странице current_group_idx = current_group_idx + 1 if current_group_idx > #active_queue then state = "CLOSING" else state = "START_GROUP" end return end -- Счётчик задержки между действиями click_timer = click_timer + 1 if click_timer < CLICK_DELAY then return -- Ждем следующего тика, пока не пройдет установленная задержка end click_timer = 0 -- Сбрасываем таймер для следующего шага if move.type == "PUT_AWAY" then -- Выгрузка лишнего предмета через Shift-Click (1 шаг) if move_step == 1 then -- САМОКОРРЕКЦИЯ: Проверяем в игре, действительно ли свободен намеченный слот if not is_container_slot_empty(move.from_slot) then -- Слот занят в игре! Сканируем открытый сундук прямо сейчас и ищем реальный пустой слот local found_real_empty = false local total_slots = player.inventory.getContainerSlots() local container_size = total_slots - 36 local max_slot = container_size - 1 -- Сканируем только доступные слоты открытого окна (исключая Locked Slots) for s = 9, max_slot do if is_container_slot_empty(s) then move.from_slot = s found_real_empty = true break end end -- Если сундук полностью забит в игре if not found_real_empty then player.addMessage("Container " .. group.type .. " page " .. group.page .. " is completely full in-game! Skipping item.") move_step = 1 current_move_idx = current_move_idx + 1 return end end -- Если нашли действительно пустой слот, совершаем Shift-Click local total_slots = player.inventory.getContainerSlots() local target_screen_slot = (total_slots - 9) + move.to_hotbar_slot player.inventory.shiftLeftClick(target_screen_slot) -- Шаг завершен move_step = 1 current_move_idx = current_move_idx + 1 end else -- Логика обычного обмена предметов (3 шага) if move_step == 1 then -- САМОКОРРЕКЦИЯ: Проверяем, на месте ли забираемый предмет в игре local stack_in_container = player.inventory.getStackFromContainer(move.from_slot) local is_valid = false if stack_in_container and stack_in_container.skyblock_id == move.skyblock_id then is_valid = true end if not is_valid then player.addMessage("Item not found in container slot " .. move.from_slot .. "! Skipping.") move_step = 1 current_move_idx = current_move_idx + 1 return end -- Шаг 1: Берем нужный предмет из хранилища на курсор player.inventory.leftClick(move.from_slot) move_step = 2 elseif move_step == 2 then -- Шаг 2: Кладем предмет в хотбар (старый предмет идет на курсор) local total_slots = player.inventory.getContainerSlots() local target_screen_slot = (total_slots - 9) + move.to_hotbar_slot player.inventory.leftClick(target_screen_slot) move_step = 3 elseif move_step == 3 then -- Шаг 3: Возвращаем старый предмет на освободившееся место в хранилище player.inventory.leftClick(move.from_slot) -- Шаг завершен move_step = 1 current_move_idx = current_move_idx + 1 end end end end) registerCommand("hotbar", function(_, args) if args[1] == "load" then local name = args[2] if data_table[name] then local echests_data = load_echests() local used_items = {} local occupied_empty_slots = {} local target_moves = {} -- Анализируем свободное место и динамически рассчитываем границы слотов сундуков local sorted_pages = find_empty_slots_sorted(echests_data) -- Сопоставляем сохраненный хотбар с базой данных storage.json for i = 0, 7 do local slot_key = "slot_" .. i local saved_item = data_table[name][slot_key] local current_stack = player.inventory.getStack(i) local needs_action = true if saved_item and saved_item.skyblock_id and saved_item.skyblock_id ~= "" then -- Если в пресете сохранен предмет для этого слота if current_stack then local current_id = current_stack.skyblock_id local current_reforge = current_stack.reforge_modifier or "" -- Оптимизация: если нужный предмет уже лежит на своем месте в хотбаре, ничего не делаем if current_id == saved_item.skyblock_id and current_reforge == (saved_item.reforge or "") then needs_action = false end end if needs_action then local t, p, s = find_item_in_storage(echests_data, saved_item.skyblock_id, saved_item.reforge, used_items) if t and p and s then local group_key = t .. "_" .. p if not target_moves[group_key] then target_moves[group_key] = { type = t, page = p, moves = {} } end table.insert(target_moves[group_key].moves, { type = "SWAP", from_slot = s, to_hotbar_slot = i, skyblock_id = saved_item.skyblock_id -- Записываем ID для сверки в игре }) else player.addMessage("Item not found in storage: " .. (saved_item.name or saved_item.skyblock_id)) end end else -- Если пресет предполагает, что этот слот хотбара должен быть ПУСТЫМ, -- но у нас в хотбаре там сейчас лежит какой-то предмет — выгружаем его if current_stack then -- Ищем свободную ячейку, начиная с самых пустых страниц в базе данных local t, p, s = get_next_free_slot(sorted_pages, occupied_empty_slots) if t and p and s then local group_key = t .. "_" .. p if not target_moves[group_key] then target_moves[group_key] = { type = t, page = p, moves = {} } end table.insert(target_moves[group_key].moves, { type = "PUT_AWAY", from_slot = s, -- Предварительная ячейка для выгрузки to_hotbar_slot = i }) else player.addMessage("No empty slots found in storage.json to clear hotbar slot " .. i) end end end end -- Формируем линейную очередь посещения контейнеров active_queue = {} for _, group in pairs(target_moves) do table.insert(active_queue, group) end if #active_queue > 0 then current_group_idx = 1 current_move_idx = 1 move_step = 1 state = "START_GROUP" timer = 0 click_timer = 0 player.addMessage("Loading hotbar preset '" .. name .. "'...") else player.addMessage("Hotbar is already configured correctly.") end else player.addMessage("Preset '" .. name .. "' not found!") end elseif args[1] == "save" then local name = args[2] data_table[name] = {} for i = 0, 7, 1 do local stack = player.inventory.getStack(i) data_table[name]["slot_" .. i] = {} if stack then data_table[name]["slot_" .. i] = { name = stack.name, skyblock_id = stack.skyblock_id, count = stack.count, reforge = stack.reforge_modifier, } end end local file, err = io.open(filepath, "w") if file then file:write(json.encode(data_table, true)) file:close() player.addMessage("Preset '" .. name .. "' saved successfully.") end elseif args[1] == "list" then local list = {} for name, _ in pairs(data_table) do table.insert(list, name) end if #list == 0 then player.addMessage("No hotbar presets found.") else player.addMessage("Saved hotbar presets: " .. table.concat(list, ", ")) end elseif args[1] == "remove" then local name = args[2] if not name or name == "" then player.addMessage("Usage: /hotbar remove ") return end if data_table[name] then data_table[name] = nil local file, err = io.open(filepath, "w") if file then file:write(json.encode(data_table, true)) file:close() player.addMessage("Preset '" .. name .. "' removed successfully.") else player.addMessage("Failed to save database: " .. tostring(err)) end else player.addMessage("Preset '" .. name .. "' not found!") end end end)