Module:Item recipe: Difference between revisions

From Space Station 14 Wiki
(updated due to deps on Module:Item and its rework)
(removed custom reagents handlers in favor of default logic; fixed a bug where lookup_recipe_ids_by_product_id() wasn't always returning an array; itty-bitty refactor)
 
(5 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Contains utilities for working with in-game item recipes.
-- Contains utilities for working with in-game item recipes.
-- todo: material sorting. based on alphabetical sorting? maybe at .json generation step, convert materials to an array?


local p = {} --p stands for package
local p = {} --p stands for package
Line 8: Line 6:
local yesNo = require('Module:Yesno')
local yesNo = require('Module:Yesno')


-- A table mapping production methods to recipes that a given method can produce.
-- A table mapping recipe IDs to recipes.
local recipes_by_method = mw.loadJsonData("Module:Item recipe/recipes/lathes.json")
local recipes_by_recipe_ids = mw.loadJsonData("Module:Item recipe/recipes by recipe IDs.json")


-- A table mapping product IDs to recipe IDs.
-- A product ID can have either a single recipe ID mapped to it, or multiple if there's are multiple recipes.
local recipe_ids_by_product_ids = mw.loadJsonData("Module:Item recipe/recipe IDs by product IDs.json")
-- A table mapping production methods to recipe IDs, with a intermediate mapping by availability.
local recipe_ids_by_method_and_availability = mw.loadJsonData(
    "Module:Item recipe/recipe IDs by method and availability.json")
-- A table mapping material IDs (item IDs) to their display order.
-- Order is just a number. Materials with lesser order number will appear first.
local materials_order_by_material_ids = mw.loadJsonData("Module:Item recipe/order of materials.json")
-- A table remapping product IDs.
--
-- Not all recipes produce products that you might think they do -
-- Not all recipes produce products that you might think they do -
-- some produce their own "printed" or "empty" or other variants.
-- some produce their own "printed" or "empty" or other variants.
--
-- Note that these mapping must be defined manually here.
--
--
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item,
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item,
Line 23: Line 33:
-- to the actual item `PowerCellSmall`.
-- to the actual item `PowerCellSmall`.
--
--
-- After that is done, lookups by small power cell or its id will return corresponding recipe.
-- After that is done, a lookup for small power cell (or its id) will return the corresponding recipe.
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")
local order_of_materials = mw.loadJsonData("Module:Item recipe/order of materials.json")


local current_frame = mw:getCurrentFrame()
local current_frame = mw:getCurrentFrame()
Line 49: Line 57:
end
end


local function table_has_value(tab, val)
local function numeric_table_has_value(tab, val)
     for _, value in ipairs(tab) do
     for _, value in ipairs(tab) do
         if value == val then
         if value == val then
Line 57: Line 65:


     return false
     return false
end
local function filter_table(tab, predicate)
    local tab_filtered = {}
    for key, value in pairs(tab) do
        if predicate(key, value) then
            tab_filtered[key] = value
        end
    end
    return tab_filtered
end
end


Line 101: Line 120:
             return key
             return key
         end
         end
    end
end
local function map_numeric_table(tbl, f)
    local t = {}
    for i, v in ipairs(tbl) do
        t[i] = f(v, i)
    end
    return t
end
local function map_table(tbl, f)
    local t = {}
    for k, v in pairs(tbl) do
        t[k] = f(k, v)
    end
    return t
end
local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
    if value then
        return valueToReturnIfTrue
    else
        error(errorMessageOnFalse)
    end
end
local function ternary_strict(valueToCheck, valueIfTrue, valueIfFalse)
    if valueToCheck == true then
        return valueIfTrue
    else
        return valueIfFalse
    end
end
-- formats seconds to a string `X min. Y sec.`.
--
-- - `X min.` part is omitted if there's less than a minute.
-- - `Y sec.` part is omitter if there's no seconds left.
local function format_seconds_to_short_string(input_seconds)
    local minutes = math.floor(input_seconds / 60)
    local seconds = input_seconds - minutes * 60
    local minutes_part = ternary_strict(minutes > 0, minutes .. " min.", nil)
    local seconds_part = ternary_strict(seconds > 0, seconds .. " sec.", nil)
    if minutes_part ~= nil and seconds_part ~= nil then
        return minutes_part .. " " .. seconds_part
    elseif seconds_part ~= nil then
        return seconds_part
    elseif minutes_part ~= nil then
        return minutes_part
    else
        return ''
     end
     end
end
end
Line 106: Line 178:
-- ====================
-- ====================


-- Searchs for recipe product in the product overrides table that maps to a an item with ID `item_id`.
-- ######################
-- ### METHOD LOOKUPS ###
-- ######################
 
-- Lookups recipe IDs by production method `production_method`.
--
-- Returns recipe IDs grouped by availability.
--
-- Raises an error if no recipe IDs were found by the production method.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_recipe_ids_and_availability_by_method(production_method, no_error)
    assert_value_not_nil(production_method)
 
    local method_item_id = p.lookup_method_item_id(production_method)
 
    return recipe_ids_by_method_and_availability[method_item_id]
        or passthrough_assert_true(
            no_error,
            nil,
            "recipe IDs by production method lookup failed: no recipes by production method '" ..
            method_item_id .. "' (input method: '" .. production_method .. "')"
        )
end
 
-- Lookups production methods by recipe ID `recipe_id`.
--
-- Returns an array of tables, each containg:
-- - `method` - production method.
-- - `availability` - availability for the recipe for this production method.
--
-- **NOTE:** This is an expensive function.
function p.lookup_methods_with_availability_by_recipe_id(inpit_recipe_id)
    local result = {}
 
    for method, recipe_ids_grouped in pairs(recipe_ids_by_method_and_availability) do
        for availability, recipe_ids in pairs(recipe_ids_grouped) do
            if numeric_table_has_value(recipe_ids, inpit_recipe_id) then
                table.insert(result, {
                    method = method,
                    availability = availability
                })
            end
        end
    end
 
    return result
end
 
-- Checks whether a production method `production_method` exists.
-- Any casing is allowed.
function p.method_exists(production_method)
    assert_value_not_nil(production_method)
 
    return p.lookup_recipe_ids_and_availability_by_method(production_method, true) ~= nil
end
 
-- Asserts that a production method `production_method` exists.
-- Any casing is allowed.
function p.assert_method_exists(production_method, custom_message)
    assert_value_not_nil(production_method)
 
    if not p.method_exists(production_method) then
        if custom_message then
            error("production method exists assertion failed for method '" ..
                production_method .. "': " .. custom_message)
        else
            error("production method exists assertion failed for method '" ..
                production_method .. "': production method is not defined")
        end
    end
end
 
-- Lookups method item ID.
--
-- Raises an error if no item ID was found.
-- Set `no_error` to `true` to return `nil` instead.
--
-- todo this will break in the future
function p.lookup_method_item_id(method, no_error)
    assert_value_not_nil(method)
 
    return itemModule.lookup_item_id(method, true)
        or passthrough_assert_true(
            no_error,
            nil,
            "production method item ID lookup failed: no item ID was found for method '" ..
            method .. "'"
        )
end
 
-- ################################
-- ### PRODUCT OVERRIDE LOOKUPS ###
-- ################################
 
-- Lookups an override for product ID `product_id`.
--
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`,
-- this functions would return `PowerCellSmall` if `product_id` was `PowerCellSmallPrinted`.
--
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead.
function p.lookup_product_id_override(product_id, no_error)
    assert_value_not_nil(product_id)
 
    return product_overrides[product_id]
        or passthrough_assert_true(
            no_error,
            nil,
            "product override lookup failed: no override was found with product ID '" ..
            product_id .. "'"
        )
end
 
-- Lookups the prodct ID that was overriden with item ID `item_id`.
--
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`,
-- this functions would return `PowerCellSmallPrinted` if `item_id` was `PowerCellSmall`.
--
--
-- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`,
-- **NOTE:** This is an expensive function.
-- this functions would return recipe product `PowerCellSmallPrinted` if given the item ID `PowerCellSmall`.
--
--
-- Returns `nil` if no match was found or `fallback_recipe_product`, if it was given.
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead.
local function try_lookup_override_recipe_product_by_item_id(item_id, fallback_recipe_product)
function p.reverse_lookup_product_id_override(item_id, no_error)
     local match = find_first_table_item_key_matching_condition(
     assert_value_not_nil(item_id)
        product_overrides,
        function(_, value) return value == item_id end
    )


     if match then
     return find_first_table_item_key_matching_condition(
         return match
            product_overrides,
     elseif fallback_recipe_product then
            function(_, value) return value == item_id end
         return fallback_recipe_product
        )
    else
        or passthrough_assert_true(
         return nil
            no_error,
            nil,
            "reverse product override lookup failed: no override was found that maps to item ID '" ..
            item_id .. "'"
        )
end
 
-- #######################
-- ### PRODUCT LOOKUPS ###
-- #######################
 
-- Checks whether a an item `product` exists.
-- Takes into account that `product` can have a product override.
function p.product_exsits(product)
    assert_value_not_nil(product)
 
    local product_with_override = p.lookup_product_id_override(product, true)
         or product
 
    return itemModule.item_exists(product_with_override)
end
 
-- Asserts that a product `product` exists.
-- `product` can be a product ID (including overriden ones), item ID or name.
function p.assert_product_exists(product, custom_message)
     assert_value_not_nil(product)
 
    if not p.product_exsits(product) then
         if custom_message then
            error("product exist assertion failed for product '" .. product .. "': " .. custom_message)
        else
            error("product exist assertion failed for product '" ..
                product ..
                "': no product was found. Make sure that a recipe exists with given product or that a product override is defined in Module:Item recipe")
         end
     end
     end
end
end


-- Searchs for an item ID in in the product overrides table that is mapped to from a product ID `product_item_id`.
-- Lookups recipe IDs by product ID `product_id`.
-- Accepts overriden `product_id`s.
--
--
-- For instance, for an override that maps recipe product `PowerCellSmallPrinted` to item ID `PowerCellSmall`,
-- Returns an array of matches.
-- this functions would return item ID `PowerCellSmall` based on recipe product `PowerCellSmallPrinted`.
--
--
-- Returns `nil` if no match was found or `fallback_item_id`, if it was given.
-- **NOTE:** This is an expensive function.
local function try_lookup_item_id_by_override_recipe_product(product_item_id, fallback_item_id)
function p.lookup_recipe_ids_by_product_id(product_id)
     local match = product_overrides[product_item_id]
    assert_value_not_nil(product_id)
 
     local result = recipe_ids_by_product_ids[
    p.reverse_lookup_product_id_override(product_id, true)
    or product_id
    ]


     if match then
    -- always returns an array
         return match
     if type(result) == 'string' then
     elseif fallback_item_id then
         return { result }
         return fallback_item_id
     elseif result ~= nil then
         return result
     else
     else
         return nil
         return {}
     end
     end
end
end


-- Asserts that a given product item exists.
-- ######################
-- Includes items defined in the products override table.
-- ### RECIPE LOOKUPS ###
local function assert_product_exists(product, skip_product_override_lookup)
-- ######################
     if not skip_product_override_lookup then
 
         product = try_lookup_item_id_by_override_recipe_product(product, product)
-- Lookups recipe by recipe ID `recipe_id`.
--
-- Returns `nil` if no recipe was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_recipe_by_recipe_id(recipe_id, no_error)
    assert_value_not_nil(recipe_id)
 
    return recipes_by_recipe_ids[recipe_id]
        or passthrough_assert_true(
            no_error,
            nil,
            "failed to lookup recipe by recipe id '" .. recipe_id .. "': no recipe was found"
        )
end
 
-- Asserts that a recipe ID `recipe_id` exists.
-- function assert_recipe_id_exists(recipe_id)
--    error("not impl")
-- end
 
-- Searches recipes by `query`. `query` can be an product ID (item ID - including overrden ones),
-- name or a recipe ID.
function p.search_recipes(query)
    assert_value_not_nil(query, "failed to lookup recipes by method and item ID: item ID was not provided")
 
    -- check if query is a recipe ID
    local recipe_by_recipe_id = recipes_by_recipe_ids[query]
     if recipe_by_recipe_id then
         -- if so - we got a direct match!
        return { recipe_by_recipe_id }
     end
     end


     if not itemModule.item_exists(product) then
     -- check if query is an item name/ID
        error("no recipe was found for product item ID '" ..
    -- this is a last possibility.
        product ..
    -- TODO add a custom errror here?
         "' (product lookup: '" ..
    local recipe_product_by_item_id_or_name = itemModule.lookup_item_id(query)
        item_id ..
 
         "'). Make sure that the product item exists with that name/ID or an override is defined in the item recipe module product overrides in Module:Item recipe")
    local recipe_ids_by_item_id_or_name = p.lookup_recipe_ids_by_product_id(recipe_product_by_item_id_or_name)
 
    local recipes = {}
    if recipe_ids_by_item_id_or_name ~= nil then
         for _, recipe_id in ipairs(recipe_ids_by_item_id_or_name) do
            table.insert(recipes, p.lookup_recipe_by_recipe_id(recipe_id))
         end
     end
     end
    return recipes
end
end


-- Searches a recipe of a given item.
-- Lookups recipes by production method.
-- If `production_method` is specified, only looks through the recipes with that method,
-- otherwise searching across all methods until a match is found.
--
--
-- Returns table with `production_method` and `recipe`, or `nil` if no recipe was found.
-- Raises an error if no recipes were found by the production method.
local function lookup_recipe_by_item_id(item_id, production_method)
function p.lookup_recipes_by_production_method(method)
     assert_value_not_nil(item_id, "failed to lookup recipes by method and item ID: item ID was not provided")
     assert_value_not_nil(method, "failed to lookup recipes by production method: no method was given")


     -- generate a list of production methods to search across
     p.assert_method_exists(method,
    local methods_to_lookup = {}
         "failed to lookup recipes by production method: method '" .. method .. "' doesn't exist")
    if production_method == nil then
        -- no method specified = search across all methods
        for recipe_group_method, _ in pairs(recipes_by_method) do
            table.insert(methods_to_lookup, recipe_group_method)
         end
    else
        -- method specified = only look through recipes with that production method
        -- lookup the proper name for the method, which is gonne be just another item ID
        table.insert(methods_to_lookup, itemModule.lookup_item_id(production_method))
    end


     -- lookup a product id based on existing overrides
     local recipe_ids_grouped = p.lookup_recipe_ids_and_availability_by_method(method)
    -- if there's none, use the same item ID
    item_id = try_lookup_override_recipe_product_by_item_id(item_id, item_id)


     -- do the search
     local recipes = {}
    for _, production_method in ipairs(methods_to_lookup) do
    for availability, recipe_ids in pairs(recipe_ids_grouped) do
        local match = recipes_by_method[production_method][item_id]
        for _, recipe_id in ipairs(recipe_ids) do
        if match then
            local recipe = p.lookup_recipe_by_recipe_id(recipe_id)
             return {
             table.insert(recipes, recipe)
                production_method = production_method,
                recipe = match
            }
         end
         end
     end
     end
    return recipes
end
end


-- Searches a material item in the material order config, returning the order number or `nil`,
-- Filters given recipes by production method.
-- if the config doesn't have the queried item.
-- Any casing is allowed for production method.
local function lookup_order_of_material(item_id)
-- function p.filter_recipes_by_production_method(recipes, production_method)
     assert_value_not_nil(item_id, "failed to lookup order of material: item ID was not provided")
--    p.assert_method_exists(production_method)
 
--    return filter_table(
--        p.lookup_recipe_ids_by_method(production_method),
--        function(_, recipe_ids_grouped)
--            return find_first_numeric_table_item_matching_condition(
--                recipes,
--                function(recipe) return recipe.id == recipe_ids_grouped end
--            ) ~= nil
--        end
--    )
-- end
 
-- ##############################
-- ### MATERIAL ORDER LOOKUPS ###
-- ##############################
 
-- Searches a material item using the material order config, returning the order number or `nil`,
-- if the material order config doesn't have the queried item.
--
-- Takes in an item ID or name.
local function try_lookup_order_of_material(material)
     assert_value_not_nil(material, "failed to lookup order of material: material was not provided")
 
    local item_id = itemModule.lookup_item_id(material, true)
 
    if item_id == nil then
        error("failed to lookup order of material: material '" .. material .. "' does not exist")
    end


     return order_of_materials[item_id]
     return materials_order_by_material_ids[item_id]
end
end
-- =======================


-- Produces a "note" element used in generation of item recipes.
-- Produces a "note" element used in generation of item recipes.
Line 255: Line 530:
     -- [REQUIRED]
     -- [REQUIRED]


     -- Item name, alias or ID. Required.
     -- Item name, alias, item ID or a recipe ID. Required.
     local item = args[1]
     local input_query = args[1]
     assert_value_not_nil(item, "failed to generate a recipe for item: item was not provided")
     assert_value_not_nil(input_query, "failed to generate a recipe for query: query was not provided")


     -- [OPTIONAL]
     -- [OPTIONAL]
Line 264: Line 539:
     -- Must be a string since Module:Item uses string amount.
     -- Must be a string since Module:Item uses string amount.
     -- All values from templates come as strings.
     -- All values from templates come as strings.
     local amount = nil_or(args[2], "1")
     local input_amount = nil_or(args[2], "1")


     -- Item production method. Can be "nil", in which case it's looked up.
     -- Item production method. Can be "nil", in which case it's looked up.
     local method = args[3]
     local input_method = args[3]




     -- Whether to only generate a materials block.
     -- Whether to only generate a materials block.
     local materials_only = yesNo(args["materials only"] or args["mat only"] or false)
     local input_materials_only = yesNo(args["materials only"] or args["mat only"] or false)


     -- Layout of materials in materials only mode.
     -- Layout of materials in materials only mode.
     local materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"
     local input_materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"


     -- ============
     -- ============


     -- first, try to lookup the item ID in case the given item as a overriden product (for some reason).
     -- search recipes
    -- fallback to the same item in case there's no overrides.
    item = try_lookup_item_id_by_override_recipe_product(item, item)


     -- then, check that the item exists.
     local recipes = p.search_recipes(input_query)
     -- at this point, it can either a name or an ID.
    local recipes_count = numeric_table_length(recipes)
     -- skip product override lookup with an extra `true` param.
    if recipes_count == 0 then
     -- in case items doesn't exist, this will give a pretty error message.
        error("failed to generate a recipe for item: no recipe was found for item '" ..
    assert_product_exists(item, true)
            input_query ..
            "' (input method: '" ..
            (input_method or "nil") ..
            "'). Make sure a recipe exists for this item or define a product override for an existing recipe in Module:Item recipe")
    elseif recipes_count > 1 then
        error("failed to generate a recipe for item: found multiple recipes for item '" ..
            input_query ..
            "' (input method: '" ..
            (input_method or "nil") ..
            "'). Rendering multiple recipes is currently unsupported")
    end
 
    local recipe = recipes[1]
 
     -- search recipe methods
 
    local recipe_methods_lookup = p.lookup_methods_with_availability_by_recipe_id(recipe.id)
    local recipe_methods_count = table_length(recipe_methods_lookup)
    if recipe_methods_count == 0 then
        error("failed to generate a recipe for item: no methods were found for recipe ID '" ..
            recipe.id ..
            "' (input query: '" .. input_query .. "'; input method: '" ..
            (input_method or "nil") ..
            "'). This shouldn't usually happen because the present recipes are bound to some production methods. Probable cause: bug in the recipe generation code")
    elseif recipe_methods_count > 1 and input_method == nil then
        local methods = map_numeric_table(
            recipe_methods_lookup,
            function(match)
                return match.method
            end
        )
 
        error("failed to generate a recipe for item: found multiple production methods for recipe ID '" ..
            recipe.id ..
            "' (input query: '" ..
            input_query ..
            "') and input production method was NOT specified. Rendering multiple recipes is unsupported, so please specify a production method from available methods for this recipe: '" ..
            table.concat(methods, "', '") .. "'")
    end
 
    local recipe_method
    local recipe_availability
     if input_method == nil then
        -- if no input methods is specified, use the single available recipe method
        recipe_method = recipe_methods_lookup[1].method
        recipe_availability = recipe_methods_lookup[1].availability
     else
        -- otherwise, the number of available methods can vary,
        -- so filter it down to a single one based on the input method.
 
        local method_item_id = p.lookup_method_item_id(input_method)
 
        local recipe_methods_lookup_match = find_first_numeric_table_item_matching_condition(
            recipe_methods_lookup,
            function(lookup_result)
                return lookup_result.method == method_item_id
            end
        )
 
        if recipe_methods_lookup_match == nil then
            error("failed to generate a recipe for item: no production methods were found for recipe ID '" ..
                recipe.id ..
                "' (input query: '" ..
                input_query ..
                "') matching input method '" ..
                (input_method or "nil") ..
                "'. Make sure a recipe exists for this item with the specified production method or define a product override for an existing recipe with the specified production method in Module:Item recipe")
        end


    -- finally, get the item ID.
        recipe_method = recipe_methods_lookup_match.method
    -- this is done after the assertion for purposes of a pretty error message in case a lookup fails.
        recipe_availability = recipe_methods_lookup_match.availability
     -- here we are guaranteed to have an item.
     end
    local item_id = itemModule.lookup_item_id(item)


     -- for a recipe using the resulting item ID.
     -- extract products
    local recipe_lookup_result = lookup_recipe_by_item_id(item_id, method)
    assert_value_not_nil(recipe_lookup_result,
        "failed to generate a recipe for item: no recipe found for item ID '" ..
        item_id ..
        "' (given item name: '" ..
        item ..
        "'; method: '" ..
        (method or "nil") ..
        "'). Make sure a recipe exists for this item or define a product override for an existing recipe in Module:Item recipe")


     local recipe = recipe_lookup_result.recipe
    -- recipe product IDs mapped to amounts
     local method = recipe_lookup_result.production_method
     local recipe_products = {}
    if recipe.result then
        recipe_products[recipe.result] = 1
    elseif recipe.resultReagents then
        for product_id, amount in pairs(recipe.resultReagents) do
            recipe_products[product_id] = amount
        end
     else
        error("failed to generate a recipe for item: no products were found for recipe ID '" ..
            recipe.id ..
            "' (input query: '" ..
            input_query ..
            "'). This might be due to recipe having another way to describe products")
    end


    -- generate recipe element


     local recipe_el = mw.html.create("div")
     local recipe_el = mw.html.create("div")
         :addClass("item-recipe")
         :addClass("item-recipe")


     if materials_only_layout == "vertical" or materials_only_layout == "ver" then
     if input_materials_only_layout == "vertical" or input_materials_only_layout == "ver" then
         recipe_el:addClass("item-recipe-materials-layout-vertical")
         recipe_el:addClass("item-recipe-materials-layout-vertical")
     else
     else
Line 327: Line 671:


     assert_value_not_nil(recipe.materials,
     assert_value_not_nil(recipe.materials,
         "failed to generate a recipe for item: no 'materials' are specified for item " ..
         "failed to generate a recipe for item: no 'materials' are specified for recipe ID '" ..
         item_id .. " recipe (method: " .. method .. ")")
         recipe.id .. "' (input query: '" ..
        input_query ..
        "')")


     -- get a list of materials (item ids)
     -- copy list of materials, but without costs
     local materials_item_ids = {}
     local materials = {}
     for item_id in pairs(recipe.materials) do
     for material, cost in pairs(recipe.materials) do
         table.insert(materials_item_ids, item_id)
         table.insert(materials, material)
     end
     end


     -- sort materials list based on material order config
     -- sort materials list based on material order config.
     -- if item doesn't have an order, use a big number as its order, placing them after
     -- materials not in config will come after the ordered materials.
    -- materials that have an order defined
     table.sort(materials, function(first, second)
     table.sort(materials_item_ids, function(first, second)
         return (try_lookup_order_of_material(first) or 999999999999)
         return (lookup_order_of_material(first) or 999999999999)
             < (try_lookup_order_of_material(second) or 999999999999)
             < (lookup_order_of_material(second) or 999999999999)
     end)
     end)


     for _, material in ipairs(materials_item_ids) do
    -- generate materials elements in sorted order
     for _, material in ipairs(materials) do
         local cost = recipe.materials[material]
         local cost = recipe.materials[material]


         if not itemModule.item_exists(material) then
         local material_item_id = itemModule.lookup_item_id(material, true)
             error("failed to generate a recipe for item ID '" ..
        if material_item_id == nil then
            item_id ..
             error("failed to generate a recipe for item: material '" ..
            "' produced on '" ..
                material ..
            method ..
                "' was not found in item registry (for recipe ID '" ..
            "': material '" ..
                recipe.id .. "'; input query: '" ..
            material ..
                input_query ..
            "' was not found in item registry. Make sure that the material is added to the item name overrides in Module:Item")
                "'). Make sure that the material is added to the item name overrides in Module:Item")
         end
         end


 
         materials_el:node(
         materials_el:node(itemModule.generate_item { material, cost })
            itemModule.generate_item {
                material_item_id,
                cost * input_amount
            }
        )
     end
     end


Line 364: Line 714:




     if materials_only then
     if input_materials_only then
         recipe_el:addClass("materials-only")
         recipe_el:addClass("materials-only")


Line 382: Line 732:




         local product_el = itemModule.generate_item { [1] = item_id, [2] = amount, cap = true }
         local products_el = mw.html.create("div")
             :addClass("item-recipe-product")
             :addClass("item-recipe-products")
 
        product_and_method_container_el:node(products_el)
 


         product_and_method_container_el:node(product_el)
         for product_id, amount in pairs(recipe_products) do
            local product_el = itemModule.generate_item {
                    [1] = product_id,
                    [2] = amount * input_amount,
                    cap = true
                }
                :addClass("item-recipe-product")
 
            products_el:node(product_el)
        end




         -- TODO: not all methods will be items, so this will eventually break.
         -- TODO: not all methods will be items, so this will eventually break.
         local method_el = methods_items_els_cache[method]
         local method_el = methods_items_els_cache[recipe_method]
         if method_el == nil then
         if method_el == nil then
             method_el = mw.html.create("span")
             method_el = mw.html.create("span")
                 :addClass('item-recipe-method')
                 :addClass('item-recipe-method')
                 :node(itemModule.generate_item { [1] = method, capitalize = true })
                 :node(itemModule.generate_item { [1] = recipe_method, capitalize = true })


             methods_items_els_cache[method] = method_el
             methods_items_els_cache[recipe_method] = method_el
         end
         end


Line 416: Line 778:
             complete_time_el:node("Instant")
             complete_time_el:node("Instant")
         else
         else
             complete_time_el:node((recipe.completetime * amount) .. " " .. "sec.")
             complete_time_el:node(format_seconds_to_short_string(recipe.completetime * input_amount))
         end
         end


Line 426: Line 788:
             :addClass("item-recipe-notes")
             :addClass("item-recipe-notes")
             :css('background', 'linear-gradient(40deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')
             :css('background', 'linear-gradient(40deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')
        assert_value_not_nil(recipe.availability,
            "failed to generate a recipe for item '" .. item_id .. "': recipe doesn't have its 'availability defined")


         -- if recipe is not available by default,
         -- if recipe is not available by default,
         -- generate "info icons" and notes (if needed) telling about it.
         -- generate "info icons" and notes (if needed) telling about it.
         if recipe.availability ~= 'static' then
         if recipe_availability ~= 'static' then
             local is_recipe_unlocked_by_research = string.find(recipe.availability, "dynamic", 1, true) ~= nil
             local is_recipe_unlocked_by_research = string.find(recipe_availability, "dynamic", 1, true) ~= nil
             local is_recipe_unlocked_by_emag = string.find(recipe.availability, "emag", 1, true) ~= nil
             local is_recipe_unlocked_by_emag = string.find(recipe_availability, "emag", 1, true) ~= nil
             local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and
             local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and
                 is_recipe_unlocked_by_emag
                 is_recipe_unlocked_by_emag
Line 510: Line 868:
     local recipes_limit = tonumber(args.limit) or 99999999
     local recipes_limit = tonumber(args.limit) or 99999999


     local recipes = recipes_by_method[itemModule.lookup_item_id(method)]
     local recipes = p.lookup_recipes_by_production_method(method)
     assert_value_not_nil(recipes, "failed to generate a list of recipes for a method: unknown method: " .. method)
     assert_value_not_nil(recipes, "failed to generate a list of recipes for a method: unknown method: " .. method)


Line 518: Line 876:
         :addClass("item-recipes-list")
         :addClass("item-recipes-list")


     -- generate a list of products
     -- -- generate a list of products
     local products = {}
     -- local products = {}
     local i = 1
     -- local i = 1
     for product_item_id, _ in pairs(recipes) do
     -- for product_item_id, _ in ipairs(recipes) do
        table.insert(products, product_item_id)
    --    table.insert(products, product_item_id)
 
    --    if i == recipes_limit then
    --        break
    --    end


        if i == recipes_limit then
    --    i = i + 1
            break
    -- end
        end


         i = i + 1
    -- -- sort the list of products alphabetically
     end
    -- -- by looking up the item names and comparing them
    -- table.sort(products, function(first, second)
    --    local first_item_id = p.lookup_product_id_override(first, true)
    --         or first
    --    local second_item_id = p.lookup_product_id_override(second, true)
     --        or second


     -- sort the list of products alphabetically
     --     p.assert_product_exists(first_item_id)
     -- by looking up the item names and comparing them
     --    p.assert_product_exists(second_item_id)
     table.sort(products, function(first, second)
        local first_item_id = try_lookup_item_id_by_override_recipe_product(first, first)
        local second_item_id = try_lookup_item_id_by_override_recipe_product(second, second)


        assert_product_exists(first_item_id, true)
    --    return itemModule.lookup_item_name_by_item_id(first_item_id)
         assert_product_exists(second_item_id, true)
    --         < itemModule.lookup_item_name_by_item_id(second_item_id)
    -- end)


         return itemModule.lookup_item_name(first_item_id)
    -- -- generate recipe elements
            < itemModule.lookup_item_name(second_item_id)
    -- for _, product in ipairs(products) do
     end)
    --    container_el:node(p.generate_item_recipe {
    --         [1] = p.lookup_product_id_override(product),
    --        [2] = 1,
    --        [3] = method
    --    })
     -- end


     -- generate recipe elements
     -- generate recipe elements
     for _, product in ipairs(products) do
     for _, recipe in ipairs(recipes) do
         container_el:node(p.generate_item_recipe {
         container_el:node(
            [1] = try_lookup_item_id_by_override_recipe_product(product, product),
            p.generate_item_recipe {
            [2] = 1,
                [1] = recipe.id,
            [3] = method
                [2] = 1,
         })
                [3] = method
            }
         )
     end
     end



Latest revision as of 18:51, 16 September 2024

Module documentation
View or edit this documentation (about module documentation)

Implements {{item recipe}}.

JSON files

JSON files that are updated automatically, syncing with the upstream:

Warning
Do not make changes to the above JSON files - any changes made will be erased on next update.

JSON files that are filled manually:

  • Module:Item recipe/order of materials.json - a 1 to 1 mapping of recipe materials to order at which they appear in recipes. Less number = higher order. Materials that do not have an order defined here, will appear after those that do.
  • Module:Item recipe/product overrides.json - a 1 to 1 mapping of recipe products to item IDs. Not all products are the same as item IDs they "represent", so sometimes a connection needs to be established explicitly.

-- Contains utilities for working with in-game item recipes.

local p = {} --p stands for package
local getArgs = require('Module:Arguments').getArgs
local itemModule = require('Module:Item')
local yesNo = require('Module:Yesno')

-- A table mapping recipe IDs to recipes.
local recipes_by_recipe_ids = mw.loadJsonData("Module:Item recipe/recipes by recipe IDs.json")

-- A table mapping product IDs to recipe IDs.
-- A product ID can have either a single recipe ID mapped to it, or multiple if there's are multiple recipes.
local recipe_ids_by_product_ids = mw.loadJsonData("Module:Item recipe/recipe IDs by product IDs.json")

-- A table mapping production methods to recipe IDs, with a intermediate mapping by availability.
local recipe_ids_by_method_and_availability = mw.loadJsonData(
    "Module:Item recipe/recipe IDs by method and availability.json")

-- A table mapping material IDs (item IDs) to their display order.
-- Order is just a number. Materials with lesser order number will appear first.
local materials_order_by_material_ids = mw.loadJsonData("Module:Item recipe/order of materials.json")

-- A table remapping product IDs.
--
-- Not all recipes produce products that you might think they do -
-- some produce their own "printed" or "empty" or other variants.
--
-- For instance, a recipe for the small power cell produces `PowerCellSmallPrinted` item,
-- whereas the actual power cell item has ID `PowerCellSmall`.
--
-- So, for the module functions to be able to find the recipe for the small power cell,
-- we first would need to define a mapping from the recipe product `PowerCellSmallPrinted`
-- to the actual item `PowerCellSmall`.
--
-- After that is done, a lookup for small power cell (or its id) will return the corresponding recipe.
local product_overrides = mw.loadJsonData("Module:Item recipe/product overrides.json")

local current_frame = mw:getCurrentFrame()

local methods_items_els_cache = {}

local was_template_styles_tag_el_added = false

-- ====================


local function numeric_table_length(t)
    local count = 0
    for _ in ipairs(t) do count = count + 1 end
    return count
end

local function table_length(t)
    local count = 0
    for _ in pairs(t) do count = count + 1 end
    return count
end

local function numeric_table_has_value(tab, val)
    for _, value in ipairs(tab) do
        if value == val then
            return true
        end
    end

    return false
end

local function filter_table(tab, predicate)
    local tab_filtered = {}
    for key, value in pairs(tab) do
        if predicate(key, value) then
            tab_filtered[key] = value
        end
    end

    return tab_filtered
end

local function assert_value_not_nil(value, error_message)
    if value == nil then
        if error_message == nil then
            error("value is nil")
        else
            error(error_message)
        end
    end
end

-- Given a value, checks if it's "nil".
-- * If it's not - returns the `value`.
-- * IF it is - returns the `value_if_nil`.
local function nil_or(value, value_if_nil)
    if value == nil then
        return value_if_nil
    else
        return value
    end
end

local function find_first_numeric_table_item_matching_condition(table, condition)
    for i, item in ipairs(table) do
        if condition(item, i, table) then
            return item
        end
    end
end

local function find_first_table_item_matching_condition(table, condition)
    for key, value in pairs(table) do
        if condition(key, value, table) then
            return value
        end
    end
end

local function find_first_table_item_key_matching_condition(table, condition)
    for key, value in pairs(table) do
        if condition(key, value, table) then
            return key
        end
    end
end

local function map_numeric_table(tbl, f)
    local t = {}
    for i, v in ipairs(tbl) do
        t[i] = f(v, i)
    end
    return t
end
local function map_table(tbl, f)
    local t = {}
    for k, v in pairs(tbl) do
        t[k] = f(k, v)
    end
    return t
end

local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
    if value then
        return valueToReturnIfTrue
    else
        error(errorMessageOnFalse)
    end
end

local function ternary_strict(valueToCheck, valueIfTrue, valueIfFalse)
    if valueToCheck == true then
        return valueIfTrue
    else
        return valueIfFalse
    end
end

-- formats seconds to a string `X min. Y sec.`.
--
-- - `X min.` part is omitted if there's less than a minute.
-- - `Y sec.` part is omitter if there's no seconds left.
local function format_seconds_to_short_string(input_seconds)
    local minutes = math.floor(input_seconds / 60)
    local seconds = input_seconds - minutes * 60

    local minutes_part = ternary_strict(minutes > 0, minutes .. " min.", nil)
    local seconds_part = ternary_strict(seconds > 0, seconds .. " sec.", nil)

    if minutes_part ~= nil and seconds_part ~= nil then
        return minutes_part .. " " .. seconds_part
    elseif seconds_part ~= nil then
        return seconds_part
    elseif minutes_part ~= nil then
        return minutes_part
    else
        return ''
    end
end

-- ====================

-- ######################
-- ### METHOD LOOKUPS ###
-- ######################

-- Lookups recipe IDs by production method `production_method`.
--
-- Returns recipe IDs grouped by availability.
--
-- Raises an error if no recipe IDs were found by the production method.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_recipe_ids_and_availability_by_method(production_method, no_error)
    assert_value_not_nil(production_method)

    local method_item_id = p.lookup_method_item_id(production_method)

    return recipe_ids_by_method_and_availability[method_item_id]
        or passthrough_assert_true(
            no_error,
            nil,
            "recipe IDs by production method lookup failed: no recipes by production method '" ..
            method_item_id .. "' (input method: '" .. production_method .. "')"
        )
end

-- Lookups production methods by recipe ID `recipe_id`.
--
-- Returns an array of tables, each containg:
-- - `method` - production method.
-- - `availability` - availability for the recipe for this production method.
--
-- **NOTE:** This is an expensive function.
function p.lookup_methods_with_availability_by_recipe_id(inpit_recipe_id)
    local result = {}

    for method, recipe_ids_grouped in pairs(recipe_ids_by_method_and_availability) do
        for availability, recipe_ids in pairs(recipe_ids_grouped) do
            if numeric_table_has_value(recipe_ids, inpit_recipe_id) then
                table.insert(result, {
                    method = method,
                    availability = availability
                })
            end
        end
    end

    return result
end

-- Checks whether a production method `production_method` exists.
-- Any casing is allowed.
function p.method_exists(production_method)
    assert_value_not_nil(production_method)

    return p.lookup_recipe_ids_and_availability_by_method(production_method, true) ~= nil
end

-- Asserts that a production method `production_method` exists.
-- Any casing is allowed.
function p.assert_method_exists(production_method, custom_message)
    assert_value_not_nil(production_method)

    if not p.method_exists(production_method) then
        if custom_message then
            error("production method exists assertion failed for method '" ..
                production_method .. "': " .. custom_message)
        else
            error("production method exists assertion failed for method '" ..
                production_method .. "': production method is not defined")
        end
    end
end

-- Lookups method item ID.
--
-- Raises an error if no item ID was found.
-- Set `no_error` to `true` to return `nil` instead.
--
-- todo this will break in the future
function p.lookup_method_item_id(method, no_error)
    assert_value_not_nil(method)

    return itemModule.lookup_item_id(method, true)
        or passthrough_assert_true(
            no_error,
            nil,
            "production method item ID lookup failed: no item ID was found for method '" ..
            method .. "'"
        )
end

-- ################################
-- ### PRODUCT OVERRIDE LOOKUPS ###
-- ################################

-- Lookups an override for product ID `product_id`.
--
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`,
-- this functions would return `PowerCellSmall` if `product_id` was `PowerCellSmallPrinted`.
--
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead.
function p.lookup_product_id_override(product_id, no_error)
    assert_value_not_nil(product_id)

    return product_overrides[product_id]
        or passthrough_assert_true(
            no_error,
            nil,
            "product override lookup failed: no override was found with product ID '" ..
            product_id .. "'"
        )
end

-- Lookups the prodct ID that was overriden with item ID `item_id`.
--
-- For instance, if there was an override `PowerCellSmallPrinted` that maps to to item ID `PowerCellSmall`,
-- this functions would return `PowerCellSmallPrinted` if `item_id` was `PowerCellSmall`.
--
-- **NOTE:** This is an expensive function.
--
-- Raises an error if no match was found. Set `no_error` to `true` to return `nil` instead.
function p.reverse_lookup_product_id_override(item_id, no_error)
    assert_value_not_nil(item_id)

    return find_first_table_item_key_matching_condition(
            product_overrides,
            function(_, value) return value == item_id end
        )
        or passthrough_assert_true(
            no_error,
            nil,
            "reverse product override lookup failed: no override was found that maps to item ID '" ..
            item_id .. "'"
        )
end

-- #######################
-- ### PRODUCT LOOKUPS ###
-- #######################

-- Checks whether a an item `product` exists.
-- Takes into account that `product` can have a product override.
function p.product_exsits(product)
    assert_value_not_nil(product)

    local product_with_override = p.lookup_product_id_override(product, true)
        or product

    return itemModule.item_exists(product_with_override)
end

-- Asserts that a product `product` exists.
-- `product` can be a product ID (including overriden ones), item ID or name.
function p.assert_product_exists(product, custom_message)
    assert_value_not_nil(product)

    if not p.product_exsits(product) then
        if custom_message then
            error("product exist assertion failed for product '" .. product .. "': " .. custom_message)
        else
            error("product exist assertion failed for product '" ..
                product ..
                "': no product was found. Make sure that a recipe exists with given product or that a product override is defined in Module:Item recipe")
        end
    end
end

-- Lookups recipe IDs by product ID `product_id`.
-- Accepts overriden `product_id`s.
--
-- Returns an array of matches.
--
-- **NOTE:** This is an expensive function.
function p.lookup_recipe_ids_by_product_id(product_id)
    assert_value_not_nil(product_id)

    local result = recipe_ids_by_product_ids[
    p.reverse_lookup_product_id_override(product_id, true)
    or product_id
    ]

    -- always returns an array
    if type(result) == 'string' then
        return { result }
    elseif result ~= nil then
        return result
    else
        return {}
    end
end

-- ######################
-- ### RECIPE LOOKUPS ###
-- ######################

-- Lookups recipe by recipe ID `recipe_id`.
--
-- Returns `nil` if no recipe was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_recipe_by_recipe_id(recipe_id, no_error)
    assert_value_not_nil(recipe_id)

    return recipes_by_recipe_ids[recipe_id]
        or passthrough_assert_true(
            no_error,
            nil,
            "failed to lookup recipe by recipe id '" .. recipe_id .. "': no recipe was found"
        )
end

-- Asserts that a recipe ID `recipe_id` exists.
-- function assert_recipe_id_exists(recipe_id)
--     error("not impl")
-- end

-- Searches recipes by `query`. `query` can be an product ID (item ID - including overrden ones),
-- name or a recipe ID.
function p.search_recipes(query)
    assert_value_not_nil(query, "failed to lookup recipes by method and item ID: item ID was not provided")

    -- check if query is a recipe ID
    local recipe_by_recipe_id = recipes_by_recipe_ids[query]
    if recipe_by_recipe_id then
        -- if so - we got a direct match!
        return { recipe_by_recipe_id }
    end

    -- check if query is an item name/ID
    -- this is a last possibility.
    -- TODO add a custom errror here?
    local recipe_product_by_item_id_or_name = itemModule.lookup_item_id(query)

    local recipe_ids_by_item_id_or_name = p.lookup_recipe_ids_by_product_id(recipe_product_by_item_id_or_name)

    local recipes = {}
    if recipe_ids_by_item_id_or_name ~= nil then
        for _, recipe_id in ipairs(recipe_ids_by_item_id_or_name) do
            table.insert(recipes, p.lookup_recipe_by_recipe_id(recipe_id))
        end
    end

    return recipes
end

-- Lookups recipes by production method.
--
-- Raises an error if no recipes were found by the production method.
function p.lookup_recipes_by_production_method(method)
    assert_value_not_nil(method, "failed to lookup recipes by production method: no method was given")

    p.assert_method_exists(method,
        "failed to lookup recipes by production method: method '" .. method .. "' doesn't exist")

    local recipe_ids_grouped = p.lookup_recipe_ids_and_availability_by_method(method)

    local recipes = {}
    for availability, recipe_ids in pairs(recipe_ids_grouped) do
        for _, recipe_id in ipairs(recipe_ids) do
            local recipe = p.lookup_recipe_by_recipe_id(recipe_id)
            table.insert(recipes, recipe)
        end
    end

    return recipes
end

-- Filters given recipes by production method.
-- Any casing is allowed for production method.
-- function p.filter_recipes_by_production_method(recipes, production_method)
--     p.assert_method_exists(production_method)

--     return filter_table(
--         p.lookup_recipe_ids_by_method(production_method),
--         function(_, recipe_ids_grouped)
--             return find_first_numeric_table_item_matching_condition(
--                 recipes,
--                 function(recipe) return recipe.id == recipe_ids_grouped end
--             ) ~= nil
--         end
--     )
-- end

-- ##############################
-- ### MATERIAL ORDER LOOKUPS ###
-- ##############################

-- Searches a material item using the material order config, returning the order number or `nil`,
-- if the material order config doesn't have the queried item.
--
-- Takes in an item ID or name.
local function try_lookup_order_of_material(material)
    assert_value_not_nil(material, "failed to lookup order of material: material was not provided")

    local item_id = itemModule.lookup_item_id(material, true)

    if item_id == nil then
        error("failed to lookup order of material: material '" .. material .. "' does not exist")
    end

    return materials_order_by_material_ids[item_id]
end

-- =======================

-- Produces a "note" element used in generation of item recipes.
-- Takes in a CSS-compatible color and text content.
local function generate_note_element(color, text)
    local el = mw.html.create('span')
        :addClass("item-recipe-note")
        :node(text)

    if color then
        el:css('color', color)
    end

    return el
end

local function generate_info_icon(frame, kind)
    if kind == 'research' then
        return frame:expandTemplate {
            title = 'Tooltip',
            args = {
                "[[File:JobIconResearchDirector.png|24px|link=Research_and_Development#R&D_Tree]]",
                "This recipe is unlocked by '''research'''"
            }
        }
    elseif kind == 'emag' then
        return frame:expandTemplate {
            title = 'Tooltip',
            args = {
                "[[File:Emag.png|42px|link=Cryptographic Sequencer]]",
                "This recipe is unlocked by '''EMAG'''"
            }
        }
    elseif kind == 'progression-symbol' then
        return mw.html.create("span")
            :addClass('info-icon-progression-symbol')
            :node("↓")
    else
        error("failed to generate an info icon: unknown kind " .. kind)
    end
end

-- ====================

-- Generates a recipe element for a given item.
-- This is the main external function of this module.
function p.generate_item_recipe(frame)
    local args = getArgs(frame)

    -- [REQUIRED]

    -- Item name, alias, item ID or a recipe ID. Required.
    local input_query = args[1]
    assert_value_not_nil(input_query, "failed to generate a recipe for query: query was not provided")

    -- [OPTIONAL]

    -- Amount of item. Default is 1.
    -- Must be a string since Module:Item uses string amount.
    -- All values from templates come as strings.
    local input_amount = nil_or(args[2], "1")

    -- Item production method. Can be "nil", in which case it's looked up.
    local input_method = args[3]


    -- Whether to only generate a materials block.
    local input_materials_only = yesNo(args["materials only"] or args["mat only"] or false)

    -- Layout of materials in materials only mode.
    local input_materials_only_layout = args["materials only layout"] or args["mat only layout"] or "vertical"

    -- ============

    -- search recipes

    local recipes = p.search_recipes(input_query)
    local recipes_count = numeric_table_length(recipes)
    if recipes_count == 0 then
        error("failed to generate a recipe for item: no recipe was found for item '" ..
            input_query ..
            "' (input method: '" ..
            (input_method or "nil") ..
            "'). Make sure a recipe exists for this item or define a product override for an existing recipe in Module:Item recipe")
    elseif recipes_count > 1 then
        error("failed to generate a recipe for item: found multiple recipes for item '" ..
            input_query ..
            "' (input method: '" ..
            (input_method or "nil") ..
            "'). Rendering multiple recipes is currently unsupported")
    end

    local recipe = recipes[1]

    -- search recipe methods

    local recipe_methods_lookup = p.lookup_methods_with_availability_by_recipe_id(recipe.id)
    local recipe_methods_count = table_length(recipe_methods_lookup)
    if recipe_methods_count == 0 then
        error("failed to generate a recipe for item: no methods were found for recipe ID '" ..
            recipe.id ..
            "' (input query: '" .. input_query .. "'; input method: '" ..
            (input_method or "nil") ..
            "'). This shouldn't usually happen because the present recipes are bound to some production methods. Probable cause: bug in the recipe generation code")
    elseif recipe_methods_count > 1 and input_method == nil then
        local methods = map_numeric_table(
            recipe_methods_lookup,
            function(match)
                return match.method
            end
        )

        error("failed to generate a recipe for item: found multiple production methods for recipe ID '" ..
            recipe.id ..
            "' (input query: '" ..
            input_query ..
            "') and input production method was NOT specified. Rendering multiple recipes is unsupported, so please specify a production method from available methods for this recipe: '" ..
            table.concat(methods, "', '") .. "'")
    end

    local recipe_method
    local recipe_availability
    if input_method == nil then
        -- if no input methods is specified, use the single available recipe method
        recipe_method = recipe_methods_lookup[1].method
        recipe_availability = recipe_methods_lookup[1].availability
    else
        -- otherwise, the number of available methods can vary,
        -- so filter it down to a single one based on the input method.

        local method_item_id = p.lookup_method_item_id(input_method)

        local recipe_methods_lookup_match = find_first_numeric_table_item_matching_condition(
            recipe_methods_lookup,
            function(lookup_result)
                return lookup_result.method == method_item_id
            end
        )

        if recipe_methods_lookup_match == nil then
            error("failed to generate a recipe for item: no production methods were found for recipe ID '" ..
                recipe.id ..
                "' (input query: '" ..
                input_query ..
                "') matching input method '" ..
                (input_method or "nil") ..
                "'. Make sure a recipe exists for this item with the specified production method or define a product override for an existing recipe with the specified production method in Module:Item recipe")
        end

        recipe_method = recipe_methods_lookup_match.method
        recipe_availability = recipe_methods_lookup_match.availability
    end

    -- extract products

    -- recipe product IDs mapped to amounts
    local recipe_products = {}
    if recipe.result then
        recipe_products[recipe.result] = 1
    elseif recipe.resultReagents then
        for product_id, amount in pairs(recipe.resultReagents) do
            recipe_products[product_id] = amount
        end
    else
        error("failed to generate a recipe for item: no products were found for recipe ID '" ..
            recipe.id ..
            "' (input query: '" ..
            input_query ..
            "'). This might be due to recipe having another way to describe products")
    end

    -- generate recipe element

    local recipe_el = mw.html.create("div")
        :addClass("item-recipe")

    if input_materials_only_layout == "vertical" or input_materials_only_layout == "ver" then
        recipe_el:addClass("item-recipe-materials-layout-vertical")
    else
        recipe_el:addClass("item-recipe-materials-layout-horizontal")
    end


    local body_el = mw.html.create("div")
        :addClass('item-recipe-body')
        :css('background', 'linear-gradient(135deg, var(--bg-color-light-x2), var(--bg-color-light))')


    local materials_el = mw.html.create("div")
        :addClass("item-recipe-materials")

    assert_value_not_nil(recipe.materials,
        "failed to generate a recipe for item: no 'materials' are specified for recipe ID '" ..
        recipe.id .. "' (input query: '" ..
        input_query ..
        "')")

    -- copy list of materials, but without costs
    local materials = {}
    for material, cost in pairs(recipe.materials) do
        table.insert(materials, material)
    end

    -- sort materials list based on material order config.
    -- materials not in config will come after the ordered materials.
    table.sort(materials, function(first, second)
        return (try_lookup_order_of_material(first) or 999999999999)
            < (try_lookup_order_of_material(second) or 999999999999)
    end)

    -- generate materials elements in sorted order
    for _, material in ipairs(materials) do
        local cost = recipe.materials[material]

        local material_item_id = itemModule.lookup_item_id(material, true)
        if material_item_id == nil then
            error("failed to generate a recipe for item: material '" ..
                material ..
                "' was not found in item registry (for recipe ID '" ..
                recipe.id .. "'; input query: '" ..
                input_query ..
                "'). Make sure that the material is added to the item name overrides in Module:Item")
        end

        materials_el:node(
            itemModule.generate_item {
                material_item_id,
                cost * input_amount
            }
        )
    end

    body_el:node(materials_el)


    if input_materials_only then
        recipe_el:addClass("materials-only")

        recipe_el:node(body_el)
    else
        local header_el = mw.html.create("div")
            :addClass("item-recipe-header")
            :css('background', 'linear-gradient(120deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')

        recipe_el:node(header_el)


        local product_and_method_container_el = mw.html.create("div")
            :addClass("item-recipe-product-and-method-container")

        header_el:node(product_and_method_container_el)


        local products_el = mw.html.create("div")
            :addClass("item-recipe-products")

        product_and_method_container_el:node(products_el)


        for product_id, amount in pairs(recipe_products) do
            local product_el = itemModule.generate_item {
                    [1] = product_id,
                    [2] = amount * input_amount,
                    cap = true
                }
                :addClass("item-recipe-product")

            products_el:node(product_el)
        end


        -- TODO: not all methods will be items, so this will eventually break.
        local method_el = methods_items_els_cache[recipe_method]
        if method_el == nil then
            method_el = mw.html.create("span")
                :addClass('item-recipe-method')
                :node(itemModule.generate_item { [1] = recipe_method, capitalize = true })

            methods_items_els_cache[recipe_method] = method_el
        end

        product_and_method_container_el:node(method_el)


        local info_icons_el = mw.html.create("div")
            :addClass("item-recipe-info-icons")

        header_el:node(info_icons_el)


        recipe_el:node(body_el)


        local complete_time_el = mw.html.create("span")
            :addClass('item-recipe-complete-time')

        if recipe.completetime == 0 then
            complete_time_el:node("Instant")
        else
            complete_time_el:node(format_seconds_to_short_string(recipe.completetime * input_amount))
        end


        body_el:node(complete_time_el)


        local notes_el = mw.html.create("div")
            :addClass("item-recipe-notes")
            :css('background', 'linear-gradient(40deg, var(--bg-color-light-x3), var(--bg-color-light-x2))')

        -- if recipe is not available by default,
        -- generate "info icons" and notes (if needed) telling about it.
        if recipe_availability ~= 'static' then
            local is_recipe_unlocked_by_research = string.find(recipe_availability, "dynamic", 1, true) ~= nil
            local is_recipe_unlocked_by_emag = string.find(recipe_availability, "emag", 1, true) ~= nil
            local is_recipe_unlocked_by_research_and_then_emag = is_recipe_unlocked_by_research and
                is_recipe_unlocked_by_emag

            if is_recipe_unlocked_by_research_and_then_emag then
                recipe_el:addClass('item-recipe-by-research')
                recipe_el:addClass('item-recipe-by-emag')

                info_icons_el:node(generate_info_icon(current_frame, 'research'))
                info_icons_el:node(generate_info_icon(current_frame, 'progression-symbol'))
                info_icons_el:node(generate_info_icon(current_frame, 'emag'))

                notes_el:node(
                    generate_note_element(
                        nil,
                        current_frame:preprocess(
                            "'''This recipe is unlocked by [[Cryptographic Sequencer|EMAG]] after it has been [[Research_and_Development#R&D_Tree|researched]]'''")
                    )
                )
            elseif is_recipe_unlocked_by_research then
                recipe_el:addClass('item-recipe-by-research')

                info_icons_el:node(generate_info_icon(current_frame, 'research'))

                -- if not is_recipe_unlocked_by_research_and_then_emag then
                -- 	notes_el:node(
                -- 		generate_note_element(
                -- 			"gold",
                -- 			"'''This recipe is unlocked by research'''"
                -- 		)
                -- 	)
                -- end
            else
                recipe_el:addClass('item-recipe-by-emag')

                info_icons_el:node(generate_info_icon(current_frame, 'emag'))

                -- if not is_recipe_unlocked_by_research_and_then_emag then
                -- 	notes_el:node(
                -- 		generate_note_element(
                -- 			"var(--danger-color)",
                -- 			current_frame:preprocess(
                -- 			"'''This recipe is unlocked by [[Cryptographic Sequencer|{{item|EmagUnlimited|l=EMAG}}]]'''")
                -- 		)
                -- 	)
                -- end
            end
        end

        recipe_el:node(notes_el)
    end


    if not was_template_styles_tag_el_added then
        recipe_el:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item recipe/styles.css' }))

        was_template_styles_tag_el_added = true
    end

    return recipe_el
        :allDone()
end

-- Generates an alphabetical list of recipes (elements) for a given production method.
-- Used to list all recipes for a particular method.
-- Recipes are sorted based on their products (the items display names).
function p.generate_list_of_recipes_for_method(frame)
    local args = getArgs(frame)

    local method = args[1]
    assert_value_not_nil(method, "failed to generate a list of recipes for a method: method was not provided")

    -- Limit on how many recipes to generate
    local recipes_limit = tonumber(args.limit) or 99999999

    local recipes = p.lookup_recipes_by_production_method(method)
    assert_value_not_nil(recipes, "failed to generate a list of recipes for a method: unknown method: " .. method)

    -- =======================

    local container_el = mw.html.create("div")
        :addClass("item-recipes-list")

    -- -- generate a list of products
    -- local products = {}
    -- local i = 1
    -- for product_item_id, _ in ipairs(recipes) do
    --     table.insert(products, product_item_id)

    --     if i == recipes_limit then
    --         break
    --     end

    --     i = i + 1
    -- end

    -- -- sort the list of products alphabetically
    -- -- by looking up the item names and comparing them
    -- table.sort(products, function(first, second)
    --     local first_item_id = p.lookup_product_id_override(first, true)
    --         or first
    --     local second_item_id = p.lookup_product_id_override(second, true)
    --         or second

    --     p.assert_product_exists(first_item_id)
    --     p.assert_product_exists(second_item_id)

    --     return itemModule.lookup_item_name_by_item_id(first_item_id)
    --         < itemModule.lookup_item_name_by_item_id(second_item_id)
    -- end)

    -- -- generate recipe elements
    -- for _, product in ipairs(products) do
    --     container_el:node(p.generate_item_recipe {
    --         [1] = p.lookup_product_id_override(product),
    --         [2] = 1,
    --         [3] = method
    --     })
    -- end

    -- generate recipe elements
    for _, recipe in ipairs(recipes) do
        container_el:node(
            p.generate_item_recipe {
                [1] = recipe.id,
                [2] = 1,
                [3] = method
            }
        )
    end

    return container_el
        :allDone()
end

return p