Completely rewritten craft matching and moving code.

This commit is contained in:
Andrey Kozlovskiy 2019-10-16 00:55:00 +03:00
parent 542065f671
commit 21a70df468
2 changed files with 314 additions and 439 deletions

View File

@ -1,493 +1,368 @@
local function extract_group_name(name) -- match_craft.lua
return name:match("^group:(.+)") -- Find and automatically move inventory items to the crafting grid
end -- according to the recipe.
local function count_compare(item1, item2) --[[
return item1.index.total_count > item2.index.total_count Retrieve items from inventory lists and calculate their total count.
end Return a table of "item name" - "total count" pairs.
local function lex_compare(group1, group2) Arguments:
local items1 = group1.items inv: minetest inventory reference
local items2 = group2.items lists: names of inventory lists to use
local len1 = group1.items_count Example usage:
local len2 = group2.items_count -- Count items in "main" and "craft" lists of player inventory
local min_len = math.min(len1, len2) unified_inventory.count_items(player_inv_ref, {"main", "craft"})
for i = 1, min_len do Example output:
local count1 = items1[i].index.total_count {
local count2 = items2[i].index.total_count ["default:pine_wood"] = 2,
["default:acacia_wood"] = 4,
if count1 ~= count2 then ["default:chest"] = 3,
return count1 < count2 ["default:axe_diamond"] = 2, -- unstackable item are counted too
end ["wool:white"] = 6
end
return len1 < len2
end
function unified_inventory.add_craft_item(t, item_name, craft_pos)
local item = t[item_name]
if item == nil then
t[item_name] = {
times_used = 1,
craft_positions = {craft_pos},
found = false
} }
else ]]--
local times_used = item.times_used + 1 function unified_inventory.count_items(inv, lists)
local counts = {}
item.craft_positions[times_used] = craft_pos for i = 1, #lists do
item.times_used = times_used local name = lists[i]
end local size = inv:get_size(name)
end local list = inv:get_list(name)
function unified_inventory.add_craft_group(t, group_name, craft_pos) for j = 1, size do
local group = t[group_name] local stack = list[j]
if group == nil then
t[group_name] = {
times_used = 1,
craft_positions = {craft_pos},
found = false,
items_count = 0,
found_items = {}
}
else
local times_used = group.times_used + 1
group.craft_positions[times_used] = craft_pos
group.times_used = times_used
end
end
function unified_inventory.create_craft_index(craft)
local craft_index = {
items = {},
groups = {}
}
local MAX_HEIGHT = 3
local MAX_WIDTH = 3
local craft_items = craft.items
local craft_width = craft.width
if craft_width == 0 then
craft_width = MAX_WIDTH
end
local pos = 1
for y = 1, MAX_HEIGHT do
for x = 1, craft_width do
local craft_pos = (y - 1) * MAX_WIDTH + x
local item = craft_items[pos]
if item ~= nil then
local group = extract_group_name(item)
if group == nil then
unified_inventory.add_craft_item(craft_index.items, item, craft_pos)
else
unified_inventory.add_craft_group(craft_index.groups, group, craft_pos)
end
end
pos = pos + 1
end
end
return craft_index
end
function unified_inventory.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
local items_count = group.items_count + 1
group.found_items[items_count] = item_name
group.items_count = items_count
end
end
return found
end
function unified_inventory.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
function unified_inventory.create_item_index(craft_index)
local item_index = {
craft_index = craft_index,
stack_max = nil,
used_items = {},
unused_items = {}
}
return item_index
end
function unified_inventory.add_list_items(item_index, inv_list)
local craft_index = item_index.craft_index
local index_used = item_index.used_items
local index_unused = item_index.unused_items
local list_count = #inv_list
for i = 1, list_count do
local stack = inv_list[i]
if not stack:is_empty() then if not stack:is_empty() then
local item_name = stack:get_name() local item = stack:get_name()
local item_count = stack:get_count() local count = stack:get_count()
local item = index_used[item_name]
if item == nil then counts[item] = (counts[item] or 0) + count
if index_unused[item_name] == nil then end
local item_found = unified_inventory.find_craft_item(item_name, craft_index) end
end
if item_found then return counts
index_used[item_name] = {
total_count = item_count,
times_matched = 0
}
local stack_max = stack:get_stack_max()
local index_stack_max = item_index.stack_max
if index_stack_max == nil or stack_max < index_stack_max then
item_index.stack_max = stack_max
end
else
index_unused[item_name] = true
end
end
else
item.total_count = item.total_count + item_count
end
end
end
end end
function unified_inventory.get_group_items(group, item_index) --[[
local items = {} Retrieve craft recipe items and their positions in the crafting grid.
local items_names = group.found_items Return a table of "craft item name" - "set of positions" pairs.
local items_count = group.items_count
local index_used = item_index.used_items Note that if craft width is not 3 then positions are recalculated as
if items were placed on a 3x3 grid. Also note that craft can contain
groups of items with "group:" prefix.
for i = 1, items_count do Arguments:
local item_name = items_names[i] craft: minetest craft recipe
local item = index_used[item_name]
items[i] = { Example output:
name = item_name, -- Bed recipe
index = item {
["wool:white"] = {[1] = true, [2] = true, [3] = true}
["group:wood"] = {[4] = true, [5] = true, [6] = true}
} }
end --]]
function unified_inventory.count_craft_positions(craft)
return items local positions = {}
end local items = craft.items
local width = craft.width
function unified_inventory.ordered_groups(craft_index, item_index)
local groups = {}
local groups_count = 0
for group_name, group in pairs(craft_index.groups) do
local group_items = unified_inventory.get_group_items(group, item_index)
table.sort(group_items, count_compare)
groups_count = groups_count + 1
groups[groups_count] = {
name = group_name,
items_count = group.items_count,
items = group_items
}
end
table.sort(groups, lex_compare)
local i = 0 local i = 0
return function() if width == 0 then
width = 3
end
for y = 1, 3 do
for x = 1, width do
i = i + 1 i = i + 1
local item = items[i]
if i <= groups_count then if item ~= nil then
local group = groups[i] local pos = 3 * (y - 1) + x
return craft_index.groups[group.name], group.items local set = positions[item]
end
end
end
function unified_inventory.match_items(m, craft_index, item_index) if set ~= nil then
local index_used = item_index.used_items set[pos] = true
for item_name, item in pairs(craft_index.items) do
local index = index_used[item_name]
local times_used = item.times_used
local cell_count = math.floor(index.total_count / times_used)
if cell_count == 0 then
m.count = 0
return
end
index.times_matched = times_used
m.count = math.min(m.count, cell_count)
local positions = item.craft_positions
for i = 1, times_used do
local craft_pos = positions[i]
m.items[craft_pos] = item_name
end
end
end
function unified_inventory.match_groups(m, craft_index, item_index)
for group, group_items in unified_inventory.ordered_groups(craft_index, item_index) do
local times_used = group.times_used
local positions = group.craft_positions
local items_count = group.items_count
for i = 1, times_used do
local craft_pos = positions[i]
local cell_count = 0
local matched_item = nil
for j = 1, items_count do
local item = group_items[j]
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
if cell_count == 0 then
m.count = 0
return
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
function unified_inventory.get_match_table(craft_index, item_index)
local match_table = {
count = item_index.stack_max,
items = {}
}
unified_inventory.match_items(match_table, craft_index, item_index)
unified_inventory.match_groups(match_table, craft_index, item_index)
if match_table.count == 0 then
return
end
return match_table
end
function unified_inventory.find_best_match(src_list, dst_list, craft)
local craft_index = unified_inventory.create_craft_index(craft)
local item_index = unified_inventory.create_item_index(craft_index)
unified_inventory.add_list_items(item_index, src_list)
unified_inventory.add_list_items(item_index, dst_list)
if not unified_inventory.all_items_found(craft_index) then
return
end
return unified_inventory.get_match_table(craft_index, item_index)
end
function unified_inventory.take_item_skip(inv, list_name, item_stack, skipped)
local inv_list = inv:get_list(list_name)
local list_count = #inv_list
local item_name = item_stack:get_name()
local item_count = item_stack:get_count()
local removed = ItemStack(item_name)
local removed_count = 0
for i = list_count, 1, -1 do
if skipped[i] == nil then
local stack = inv:get_stack(list_name, i)
local name = stack:get_name()
if name == item_name then
local count = stack:get_count()
local left = count - item_count
if left > 0 then
removed_count = removed_count + item_count
stack:set_count(left)
inv:set_stack(list_name, i, stack)
break
else else
removed_count = removed_count + count positions[item] = {[pos] = true}
item_count = item_count - count
inv:set_stack(list_name, i, nil)
end end
end end
end end
end end
removed:set_count(removed_count) return positions
end
--[[
For every craft item find all matching inventory items.
- If craft item is a group then find all inventory items that matches
this group.
- If craft item is not a group (regular item) then find only this item.
If inventory doesn't contain needed item then found set is empty for
this item.
Return a table of "craft item name" - "set of matching inventory items"
pairs.
Arguments:
inv_items: table with items names as keys
craft_items: table with items names or groups as keys
Example output:
{
["group:wood"] = {
["default:pine_wood"] = true,
["default:acacia_wood"] = true
},
["wool:white"] = {
["wool:white"] = true
}
}
--]]
function unified_inventory.find_usable_items(inv_items, craft_items)
local get_group = minetest.get_item_group
local result = {}
for craft_item in pairs(craft_items) do
local group = craft_item:match("^group:(.+)")
local found = {}
if group ~= nil then
for inv_item in pairs(inv_items) do
if get_group(inv_item, group) > 0 then
found[inv_item] = true
end
end
else
if inv_items[craft_item] ~= nil then
found[craft_item] = true
end
end
result[craft_item] = found
end
return result
end
--[[
Match inventory items with craft grid positions.
For every position select the matching inventory item with maximum
(total_count / (times_matched + 1)) value.
If for some position matching item cannot be found or match count is 0
then return nil.
Return a table of "matched item name" - "set of craft positions" pairs
and overall match count.
Arguments:
inv_counts: table of inventory items counts from "count_items"
craft_positions: table of craft positions from "count_craft_positions"
Example output:
match_table = {
["wool:white"] = {[1] = true, [2] = true, [3] = true}
["default:acacia_wood"] = {[4] = true, [6] = true}
["default:pine_wood"] = {[5] = true}
}
match_count = 2
--]]
function unified_inventory.match_items(inv_counts, craft_positions)
local usable = unified_inventory.find_usable_items(inv_counts, craft_positions)
local match_table = {}
local match_count
local matches = {}
for craft_item, pos_set in pairs(craft_positions) do
local use_set = usable[craft_item]
for pos in pairs(pos_set) do
local pos_item
local pos_count
for use_item in pairs(use_set) do
local count = inv_counts[use_item]
local times_matched = matches[use_item] or 0
local new_pos_count = math.floor(count / (times_matched + 1))
if pos_count == nil or pos_count < new_pos_count then
pos_item = use_item
pos_count = new_pos_count
end
end
if pos_item == nil or pos_count == 0 then
return nil
end
local set = match_table[pos_item]
if set ~= nil then
set[pos] = true
else
match_table[pos_item] = {[pos] = true}
end
matches[pos_item] = (matches[pos_item] or 0) + 1
end
end
for match_item, times_matched in pairs(matches) do
local count = inv_counts[match_item]
local item_count = math.floor(count / times_matched)
if match_count == nil or item_count < match_count then
match_count = item_count
end
end
return match_table, match_count
end
--[[
Remove item from inventory lists.
Return stack of actually removed items.
This function replicates the inv:remove_item function but can accept
multiple lists.
Arguments:
inv: minetest inventory reference
lists: names of inventory lists
stack: minetest item stack
--]]
function unified_inventory.remove_item(inv, lists, stack)
local removed = ItemStack(nil)
local leftover = ItemStack(stack)
for i = 1, #lists do
if leftover:is_empty() then
break
end
local cur_removed = inv:remove_item(lists[i], leftover)
removed:add_item(cur_removed)
leftover:take_item(cur_removed:get_count())
end
return removed return removed
end end
function unified_inventory.add_item_skip(inv, list_name, item_stack, skipped) --[[
local inv_list = inv:get_list(list_name) Add item to inventory lists.
local list_count = #inv_list Return leftover stack.
local item_name = item_stack:get_name() This function replicates the inv:add_item function but can accept
local item_count = item_stack:get_count() multiple lists.
local leftover = ItemStack(item_stack) Arguments:
inv: minetest inventory reference
lists: names of inventory lists
stack: minetest item stack
--]]
function unified_inventory.add_item(inv, lists, stack)
local leftover = ItemStack(stack)
for i = 1, list_count do for i = 1, #lists do
if skipped[i] == nil then if leftover:is_empty() then
local stack = inv:get_stack(list_name, i)
if stack:is_empty() then
leftover = stack:add_item(leftover)
inv:set_stack(list_name, i, stack)
break break
end end
local name = stack:get_name() leftover = inv:add_item(lists[i], leftover)
if name == item_name then
leftover = stack:add_item(leftover)
inv:set_stack(list_name, i, stack)
end
end
end end
return leftover return leftover
end end
function unified_inventory.craftguide_match_craft(inv, src_list_name, dst_list_name, craft, amount) --[[
local src_list = inv:get_list(src_list_name) Move matched items to the destination list.
local dst_list = inv:get_list(dst_list_name)
local craft_match = unified_inventory.find_best_match(src_list, dst_list, craft) Note that function accepts multiple source lists and destination list
can be one of the source lists.
if craft_match == nil then If destination list position is already occupied with some other item
return then function tries to move it to the source lists if possible.
Arguments:
inv: minetest inventory reference
src_lists: names of source lists
dst_list: name of destination list
match_table: table of matched items
amount: amount of items per every position
--]]
function unified_inventory.move_match(inv, src_lists, dst_list, match_table, amount)
for item, pos_set in pairs(match_table) do
local stack_max = ItemStack(item):get_stack_max()
local bounded_amount = math.min(stack_max, amount)
local pos_count = 0;
for _ in pairs(pos_set) do
pos_count = pos_count + 1
end end
local matched_items = craft_match.items local total = ItemStack{
local matched_count = craft_match.count name = item,
count = bounded_amount * pos_count
}
if amount == -1 then local removed = unified_inventory.remove_item(inv, src_lists, total)
amount = matched_count local current = ItemStack(removed)
elseif amount > matched_count then current:set_count(bounded_amount)
return
end
-- Clear crafting grid (if possible) for pos in pairs(pos_set) do
local dst_count = #dst_list local occupied = inv:get_stack(dst_list, pos)
inv:set_stack(dst_list, pos, current)
for i = 1, dst_count do if not occupied:is_empty() then
local dst_stack = inv:get_stack(dst_list_name, i) local leftover = unified_inventory.add_item(inv, src_lists, occupied)
local leftover = inv:add_item(src_list_name, dst_stack)
inv:set_stack(dst_list_name, i, leftover)
end
local skipped = {}
for match_pos = 1, dst_count do
local item_name = matched_items[match_pos]
if item_name ~= nil then
local matched_stack = ItemStack(item_name)
matched_stack:set_count(amount)
local src_take = inv:remove_item(src_list_name, matched_stack)
local src_take_count = src_take:get_count()
local diff = amount - src_take_count
if diff > 0 then
matched_stack:set_count(diff)
-- Because we take from dst_list we need to exclude already matched positions
local dst_take = unified_inventory.take_item_skip(inv, dst_list_name, matched_stack, skipped)
src_take:add_item(dst_take)
end
local dst_stack = inv:get_stack(dst_list_name, match_pos)
inv:set_stack(dst_list_name, match_pos, src_take)
skipped[match_pos] = true
local src_leftover = inv:add_item(src_list_name, dst_stack)
if not src_leftover:is_empty() then
-- Because we add to dst_list we need to exclude already matched positions
local dst_leftover = unified_inventory.add_item_skip(inv, dst_list_name, src_leftover, skipped)
if not dst_leftover:is_empty() then
-- Final try
local dst_leftover_full = inv:add_item(dst_list_name, dst_leftover)
if not dst_leftover_full:is_empty() then
inv:set_stack(dst_list_name, match_pos, dst_leftover_full)
local src_reverse = inv:add_item(src_list_name, src_take)
if not src_reverse:is_empty() then
local dst_reverse = inv:add_item(dst_list_name, src_reverse)
-- Can dst_reverse be not empty here?
end
if not leftover:is_empty() then
inv:set_stack(dst_list, pos, leftover)
break break
end end
end end
removed:take_item(bounded_amount)
end end
end
unified_inventory.add_item(inv, src_lists, removed)
end end
end end
--[[
Find craft match and move matched items to the destination list.
If match cannot be found or match count is smaller than the desired
amount then do nothing.
If amount passed is -1 then amount is defined by match count itself.
This is used to indicate "craft All" case.
Note that function accepts multiple source lists.
Arguments:
inv: minetest inventory reference
src_lists: names of source lists
dst_list: name of destination list
craft: minetest craft recipe
amount: desired amount of output items
--]]
function unified_inventory.craftguide_match_craft(inv, src_lists, dst_list, craft, amount)
local counts = unified_inventory.count_items(inv, src_lists)
local positions = unified_inventory.count_craft_positions(craft)
local match_table, match_count = unified_inventory.match_items(counts, positions)
if match_table == nil or match_count < amount then
return
end
if amount == -1 then
amount = match_count
end
unified_inventory.move_match(inv, src_lists, dst_list, match_table, amount)
end

View File

@ -467,7 +467,7 @@ local function craftguide_craft(player, formname, fields)
local craft = crafts[alternate] local craft = crafts[alternate]
if craft.width > 3 then return end if craft.width > 3 then return end
unified_inventory.craftguide_match_craft(player_inv, "main", "craft", craft, amount) unified_inventory.craftguide_match_craft(player_inv, {"main", "craft"}, "craft", craft, amount)
unified_inventory.set_inventory_formspec(player, "craft") unified_inventory.set_inventory_formspec(player, "craft")
end end