From e0b28931c5efe70bbd5e2be969b504f7f57a9c25 Mon Sep 17 00:00:00 2001 From: Andrey Kozlovskiy Date: Thu, 22 Aug 2019 22:56:46 +0300 Subject: [PATCH] Implement new crafting algorithm --- init.lua | 1 + match_craft.lua | 312 ++++++++++++++++++++++++++++++++++++++++++++++++ register.lua | 97 +-------------- 3 files changed, 316 insertions(+), 94 deletions(-) create mode 100644 match_craft.lua diff --git a/init.lua b/init.lua index 1c73fad..6589fb3 100644 --- a/init.lua +++ b/init.lua @@ -71,6 +71,7 @@ dofile(modpath.."/group.lua") dofile(modpath.."/api.lua") dofile(modpath.."/internal.lua") dofile(modpath.."/callbacks.lua") +dofile(modpath.."/match_craft.lua") dofile(modpath.."/register.lua") if minetest.settings:get_bool("unified_inventory_bags") ~= false then diff --git a/match_craft.lua b/match_craft.lua new file mode 100644 index 0000000..277d4eb --- /dev/null +++ b/match_craft.lua @@ -0,0 +1,312 @@ +local MAX_COUNT = 99 + +local function extract_group_name(name) + return name:match("^group:(.+)") +end + +local function add_craft_item(t, item_name, craft_pos) + local item = t[item_name] + + if item == nil then + t[item_name] = { + craft_positions = {craft_pos}, + found = false + } + else + table.insert(item.craft_positions, craft_pos) + end +end + +local function add_craft_group(t, group_name, craft_pos) + local group = t[group_name] + + if group == nil then + t[group_name] = { + craft_positions = {craft_pos}, + found = false, + found_items = {} + } + else + table.insert(group.craft_positions, craft_pos) + end +end + +local function create_craft_index(craft_items) + local craft_index = { + items = {}, + groups = {} + } + + for craft_pos, name in pairs(craft_items) do + local group_name = extract_group_name(name) + + if group_name == nil then + add_craft_item(craft_index.items, name, craft_pos) + else + add_craft_group(craft_index.groups, group_name, craft_pos) + end + end + + return craft_index +end + +local function find_craft_item(item_name, craft_index) + local found = false + local item = craft_index.items[item_name] + local get_item_group = minetest.get_item_group + + if item ~= nil then + item.found = true + found = true + end + + for group_name, group in pairs(craft_index.groups) do + if get_item_group(item_name, group_name) > 0 then + group.found = true + found = true + + table.insert(group.found_items, item_name) + end + end + + return found +end + +local function all_items_found(craft_index) + for _, item in pairs(craft_index.items) do + if not item.found then + return false + end + end + + for _, group in pairs(craft_index.groups) do + if not group.found then + return false + end + end + + return true +end + +local function create_item_index(inv_list, craft_index) + local item_index = {} + local not_found = {} + + for inv_list_pos, stack in ipairs(inv_list) do + local item_name = stack:get_name() + + if item_name ~= "" then + local item_count = stack:get_count() + local item = item_index[item_name] + + if item == nil then + if not_found[item_name] == nil then + local item_found = find_craft_item(item_name, craft_index) + + if item_found then + item_index[item_name] = { + total_count = item_count, + times_matched = 0 + } + else + not_found[item_name] = true + end + end + else + item.total_count = item.total_count + item_count + end + end + end + + return item_index +end + +local function count_compare(item1, item2) + return item1.index.total_count > item2.index.total_count +end + +local function lex_compare(group1, group2) + local items1 = group1.items + local items2 = group2.items + + local len1 = #items1 + local len2 = #items2 + local min_len = math.min(len1, len2) + + for i = 1, min_len do + local count1 = items1[i].index.total_count + local count2 = items2[i].index.total_count + + if count1 ~= count2 then + return count1 < count2 + end + end + + return len1 < len2 +end + +local function get_group_items(group_name, craft_index, item_index) + local items = {} + local group = craft_index.groups[group_name] + + for _, item_name in ipairs(group.found_items) do + local item = item_index[item_name] + + table.insert(items, { + name = item_name, + index = item + }) + end + + return items +end + +local function ordered_groups(craft_index, item_index) + local groups = {} + + for group_name in pairs(craft_index.groups) do + local group_items = get_group_items(group_name, craft_index, item_index) + table.sort(group_items, count_compare) + + table.insert(groups, { + name = group_name, + items = group_items + }) + end + + table.sort(groups, lex_compare) + + local i = 0 + local n = #groups + + return function() + i = i + 1 + + if i <= n then + local group = groups[i] + return craft_index.groups[group.name], group.items + end + end +end + +local function match_items(m, craft_index, item_index) + for item_name, item in pairs(craft_index.items) do + local index = item_index[item_name] + local times_used = #item.craft_positions + local cell_count = math.floor(index.total_count / times_used) + + index.times_matched = times_used + m.count = math.min(m.count, cell_count) + + for _, craft_pos in ipairs(item.craft_positions) do + m.items[craft_pos] = item_name + end + end +end + +local function match_groups(m, craft_index, item_index) + for group, group_items in ordered_groups(craft_index, item_index) do + for _, craft_pos in ipairs(group.craft_positions) do + local cell_count = 0 + local matched_item = nil + + for _, item in ipairs(group_items) do + local index = item.index + + local item_count = index.total_count + local times_matched = index.times_matched + local match_count = math.floor(item_count / (times_matched + 1)) + + if match_count > cell_count then + cell_count = match_count + matched_item = item + end + end + + m.count = math.min(m.count, cell_count) + m.items[craft_pos] = matched_item.name + + local matched_index = matched_item.index + matched_index.times_matched = matched_index.times_matched + 1 + end + end +end + +local function get_match_table(craft_index, item_index) + local match_table = { + count = MAX_COUNT, + items = {} + } + + match_items(match_table, craft_index, item_index) + match_groups(match_table, craft_index, item_index) + + return match_table +end + +local function find_best_match(inv_list, craft_items) + local craft_index = create_craft_index(craft_items) + local item_index = create_item_index(inv_list, craft_index) + + if not all_items_found(craft_index) then + return + end + + return get_match_table(craft_index, item_index) +end + +local function can_move(match_items, inv_list) + for match_pos, match_name in pairs(match_items) do + local inv_item = inv_list[match_pos] + local inv_item_name = inv_item:get_name() + + if inv_item_name ~= "" and match_name ~= inv_item_name then + return false + end + end + + return true +end + +function craftguide_match_craft(inv, src_list_name, dst_list_name, craft, amount) + local src_list = inv:get_list(src_list_name) + local dst_list = inv:get_list(dst_list_name) + + local craft_items = craft.items + local craft_match = find_best_match(src_list, craft_items) + + if craft_match == nil then + return + end + + local matched_items = craft_match.items + local matched_count = craft_match.count + + if not can_move(matched_items, dst_list) then + return + end + + if amount == -1 then + amount = matched_count + elseif amount > matched_count then + return + end + + for match_pos, item_name in pairs(matched_items) do + local dst_stack = dst_list[match_pos] + local dst_count = dst_stack:get_count() + + local matched_stack = ItemStack(item_name) + local take_count = math.min(MAX_COUNT - dst_count, amount) + matched_stack:set_count(take_count) + + local removed_stack = inv:remove_item(src_list_name, matched_stack) + local removed_count = removed_stack:get_count() + local sum_count = dst_count + removed_count + matched_stack:set_count(sum_count) + + dst_list[match_pos] = matched_stack + end + + inv:set_list(dst_list_name, dst_list) +end diff --git a/register.lua b/register.lua index a61a4e9..500caa1 100644 --- a/register.lua +++ b/register.lua @@ -440,65 +440,6 @@ local function craftguide_giveme(player, formname, fields) player_inv:add_item("main", {name = output, count = amount}) end --- Takes any stack from "main" where the `amount` of `needed_item` may fit --- into the given crafting stack (`craft_item`) -local function craftguide_move_stacks(inv, craft_item, needed_item, amount) - if craft_item:get_count() >= amount then - return - end - - local get_item_group = minetest.get_item_group - local group = needed_item:match("^group:(.+)") - if group then - if not craft_item:is_empty() then - -- Source item must be the same to fill - if get_item_group(craft_item:get_name(), group) ~= 0 then - needed_item = craft_item:get_name() - else - -- TODO: Maybe swap unmatching "craft" items - -- !! Would conflict with recursive function call - return - end - else - -- Take matching group from the inventory (biggest stack) - local main = inv:get_list("main") - local max_found = 0 - for i, stack in ipairs(main) do - if stack:get_count() > max_found and - get_item_group(stack:get_name(), group) ~= 0 then - needed_item = stack:get_name() - max_found = stack:get_count() - if max_found >= amount then - break - end - end - end - end - else - if not craft_item:is_empty() and - craft_item:get_name() ~= needed_item then - return -- Item must be identical - end - end - - needed_item = ItemStack(needed_item) - local to_take = math.min(amount, needed_item:get_stack_max()) - to_take = to_take - craft_item:get_count() - if to_take <= 0 then - return -- Nothing to do - end - needed_item:set_count(to_take) - - local taken = inv:remove_item("main", needed_item) - local leftover = taken:add_item(craft_item) - if not leftover:is_empty() then - -- Somehow failed to add the existing "craft" item. Undo the action. - inv:add_item("main", leftover) - return taken - end - return taken -end - local function craftguide_craft(player, formname, fields) local amount for k, v in pairs(fields) do @@ -507,8 +448,8 @@ local function craftguide_craft(player, formname, fields) end if not amount then return end - amount = tonumber(amount) or 99 -- fallback for "all" - if amount <= 0 or amount > 99 then return end + amount = tonumber(amount) or -1 -- fallback for "all" + if amount == 0 or amount < -1 or amount > 99 then return end local player_name = player:get_player_name() @@ -516,7 +457,6 @@ local function craftguide_craft(player, formname, fields) if output == "" then return end local player_inv = player:get_inventory() - local craft_list = player_inv:get_list("craft") local crafts = unified_inventory.crafts_for[ unified_inventory.current_craft_direction[player_name]][output] or {} @@ -527,38 +467,7 @@ local function craftguide_craft(player, formname, fields) local craft = crafts[alternate] if craft.width > 3 then return end - local needed = craft.items - local width = craft.width - if width == 0 then - -- Shapeless recipe - width = 3 - end - - -- To spread the items evenly - local STEPSIZE = math.ceil(math.sqrt(amount) / 5) * 5 - local current_count = 0 - repeat - current_count = math.min(current_count + STEPSIZE, amount) - local index = 1 - for y = 1, 3 do - for x = 1, width do - local needed_item = needed[index] - if needed_item then - local craft_index = ((y - 1) * 3) + x - local craft_item = craft_list[craft_index] - local newitem = craftguide_move_stacks(player_inv, - craft_item, needed_item, current_count) - - if newitem then - craft_list[craft_index] = newitem - end - end - index = index + 1 - end - end - until current_count == amount - - player_inv:set_list("craft", craft_list) + craftguide_match_craft(player_inv, "main", "craft", craft, amount) unified_inventory.set_inventory_formspec(player, "craft") end