Implement new crafting algorithm

This commit is contained in:
Andrey Kozlovskiy 2019-08-22 22:56:46 +03:00
parent 04b1cb9e7d
commit e0b28931c5
3 changed files with 316 additions and 94 deletions

View File

@ -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

312
match_craft.lua Normal file
View File

@ -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

View File

@ -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