BlockySurvival / issue-tracker

A non-code repo for tracking issues w/ the Blocky Survival minetest server
12 stars 0 forks source link

Fix crafting once and for all #341

Open oversword opened 3 years ago

oversword commented 3 years ago
oversword commented 3 years ago

This code is capable of finding all crafting conflicts It may produce false positives where groups and shapeless crafts are concerned Each conflict will need to be manually confirmed

local function is_group(item)
    return string.sub(item,1,6) == "group:"
end

local function item_has_group(item, group)
    local def = minetest.registered_items[item]
    return def and def.groups and def.groups[group]
end

local function same_item(item, other_item)
    return item == other_item
            or minetest.registered_aliases[item] == other_item
            or minetest.registered_aliases[other_item] == item
end

local function item_match(item, other_item)
    if item == other_item then return true end

    if item == nil or other_item == nil then return end

    if is_group(item) then
        if is_group(other_item) then return end
        return item_has_group(other_item, string.sub(item, 7))
    end
    if is_group(other_item) then
        return item_has_group(item, string.sub(other_item, 7))
    end
end

local function normalize(l)
    local r = {}
    for _,i in pairs(l) do
        table.insert(r, i)
    end
    table.sort(r)
    return r
end

local function recipe_match(recipe, other_recipe)
    if recipe.method ~= other_recipe.method then return end
    if    recipe.width ~= 0
      and other_recipe.width ~= 0
      and recipe.width ~= other_recipe.width then return end

    local items = table.copy(recipe.items)
    local other_items = table.copy(other_recipe.items)
    if recipe.width == 0 or other_recipe.width == 0 then
        items = normalize(items)
        other_items = normalize(other_recipe.items)
    end
    if #items ~= #other_items then return end

    for i,r in pairs(items) do
        if not item_match(other_items[i], r) then return end
    end
    for i,r in pairs(other_items) do
        if not item_match(items[i], r) then return end
    end
    return true
end

local function is_slopey(item)
    return
       string.find(item, "moreblocks:")
    or string.find(item, "micro_")
    or string.find(item, "slab_")
    or string.find(item, "slope_")
    or string.find(item, "panel_")
    or string.find(item, "stair_")

end
local function all_slopey(items)
    for _,item in pairs(items) do
        if not is_slopey(item) then return end
    end
    return true
end

local function is_dye(item)
    return string.find(item, "dye:")
end

local enabled_reports = {
    conflict = true,
    duplicate = true,
    other = true,
    dye = true,
    slope = true,
    empty = true,
}
local report_files = {}

for report, enabled in pairs(enabled_reports) do
    if enabled then
        report_files[report] = io.open(minetest.get_worldpath().."/".."report_"..report, "a")
    end
end

local function report_conflict(recipe, other_recipe)

    local item_stack = ItemStack(recipe.output)
    local item_name = item_stack:get_name()

    local other_item_stack = ItemStack(other_recipe.output)
    local other_item_name = other_item_stack:get_name()

    local duplicate = same_item(item_name, other_item_name)

    local conflict_report = tostring(recipe.output).." "..(duplicate and "duplicates" or "conflicts with").." "..tostring(other_recipe.output).." (" .. tostring(recipe.type) .. "):"
        .."\n"..dump(recipe.items)
        .."\n"..dump(other_recipe.items)
        .."\n========================\n"
    -- minetest.log("error", conflict_report)

    if recipe.type ~= "normal" and recipe.type ~= "shapeless" then
        if enabled_reports.other then
            report_files.other:write(conflict_report)
        end
        return
    end
    if (is_slopey(item_name) and is_slopey(other_item_name))
    or (all_slopey(recipe.items) and all_slopey(other_recipe.items)) then
        if enabled_reports.slope then
            report_files.slope:write(conflict_report)
        end
        return
    end
    if is_dye(item_name) and is_dye(other_item_name) then
        if enabled_reports.dye then
            report_files.dye:write(conflict_report)
        end
        return
    end
    if duplicate then
        if enabled_reports.duplicate then
            report_files.duplicate:write(conflict_report)
        end
        return
    end
    if enabled_reports.conflict then
        report_files.conflict:write(conflict_report)
    end
end

local function get_all_conflicts()
    minetest.log("error", "Starting")
    local all_recipes = {}
    local all_items = minetest.registered_items
    for item_name,item_def in pairs(all_items) do

        local recipes = minetest.get_all_craft_recipes(item_name)
        if enabled_reports.empty and (not recipes or #recipes == 0) and not (item_def.groups and item_def.groups.not_in_creative_inventory) then
            report_files.empty:write("No craft for "..item_name.."\n")
        end
        if recipes then
            for i,recipe in ipairs(recipes) do
                -- if is_slopey(item_name) or all_slopey(recipe.items) then

                table.insert(all_recipes, recipe)

                -- end
            end
        end

    end
    minetest.log("error", ("Assessing %i items in %f combinations"):format(#all_recipes, ((#all_recipes-1)*#all_recipes)/2))
    local dones = 0
    for i,recipe in ipairs(all_recipes) do
        for j=i+1,#all_recipes do
            local other_recipe = all_recipes[j]
            dones = dones+1
            if recipe_match(recipe, other_recipe) then
                report_conflict(
                    recipe, other_recipe
                )
            end
            if dones % 1000000 == 0 then
                minetest.log("error", "Done: "..tostring(dones))
            end
        end
    end
    for report, file in pairs(report_files) do
        file:close()
    end
    minetest.log("error", "Done")
end

local function get_all_conflicts_ui()
    minetest.log("error", "Starting")

    local all_recipes_by_type = {}
    local all_items = unified_inventory.crafts_for.recipe

    for item_name,recipes in pairs(all_items) do
        if recipes then
            for i,recipe in ipairs(recipes) do
                local recipe_type = recipe.type
                if recipe_type == "shapeless" then recipe_type = "normal" end
                if not all_recipes_by_type[recipe_type] then
                    all_recipes_by_type[recipe_type] = {}
                end

                table.insert(all_recipes_by_type[recipe_type], recipe)
            end
        end
    end
    all_recipes_by_type.normal = nil
    all_recipes_by_type.sieving = nil
    all_recipes_by_type.digging_chance = nil

    for recipe_type, all_recipes in pairs(all_recipes_by_type) do
        minetest.log("error", ("Assessing %i items in %f combinations for %s"):format(#all_recipes, ((#all_recipes-1)*#all_recipes)/2, recipe_type))
        local dones = 0
        for i,recipe in ipairs(all_recipes) do
            for j=i+1,#all_recipes do
                local other_recipe = all_recipes[j]
                dones = dones+1
                if recipe_match(recipe, other_recipe) then
                    report_conflict(
                        recipe, other_recipe
                    )
                end
                if dones % 1000000 == 0 then
                    minetest.log("error", "Done: "..tostring(dones))
                end
            end
        end
    end
    for report, file in pairs(report_files) do
        file:close()
    end
    minetest.log("error", "Done")
end

minetest.register_on_mods_loaded(function()
    minetest.after(10, get_all_conflicts_ui)
end)
oversword commented 3 years ago

Stop closing it stupid robot

appgurueu commented 2 years ago

This should use some decent hash indexes instead of parallelization.

oversword commented 2 years ago

This should use some decent hash indexes instead of parallelization.

@appgurueu Could you be clearer? What could use hash indexes?

appgurueu commented 2 years ago

This should use some decent hash indexes instead of parallelization.

@appgurueu Could you be clearer? What could use hash indexes?

The way Minetest already uses a couple (unfortunately rather poor) indices to speed things up. Shapeful recipes not using groups can be fully hashed; other recipes are hashed by item count from what I've seen. Simply looping over all recipes and then feeding a valid input for the recipe into minetest.get_craft_result(recipe) and checking that against the output should work and leverage MT's hashes (assuming that get_craft_result is deterministic). It also cuts down on code significantly.