Module:Item: Difference between revisions

From Space Station 14 Wiki
Aliser (talk | contribs)
added auto linking for items that have links, and support for "link" parameter to add/replace existing link; added extra validation for item existence into item name lookup by id; improv fns docs
Aliser (talk | contribs)
use no break space between label and amount
 
(28 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Contains utilities for working with in-game items.
-- Contains utilities for working with in-game items.


-- todo try to include template styles only once if that makes sense
-- todo create external tests for schema tables (under /doc)
-- todo make image lookup json data validations run once per page instead of on each lookup
-- todo make `generate_list_of_all_items_with_icons` also display items with nontrivial image files
-- todo make `generate_list_of_all_items_with_icons` also display items with conditions


local p = {} --p stands for package
local p = {} --p stands for package
Line 11: Line 10:
-- A table mapping item IDs to their names.
-- A table mapping item IDs to their names.
-- Keys are item IDs; each value is a string.
-- Keys are item IDs; each value is a string.
--  
--
-- These names are used for labels.
-- These names are used for labels.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
Line 19: Line 18:
-- Keys are item names; each value is an item ID.
-- Keys are item names; each value is an item ID.
-- An item can have multiple names.
-- An item can have multiple names.
--  
--
-- These names are used for ID lookups.
-- These names are used for ID lookups.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
local item_ids_by_item_names = mw.loadJsonData("Module:Item/item ids by item names.json")
local item_ids_by_item_names = mw.loadJsonData("Module:Item/item ids by item lowercase names.json")


-- Same as `item_ids_by_item_names`, but has a higher priority
-- Same as `item_ids_by_item_names`, but has a higher priority
-- and meant to be filled manually.
-- and meant to be filled manually.
--  
--
-- These names are used for ID lookups.
-- These names are used for ID lookups.
local item_ids_by_item_names_override = mw.loadJsonData("Module:Item/item ids by item names override.json")
local item_ids_by_item_names_override = mw.loadJsonData("Module:Item/item ids by item lowercase names overrides.json")


-- A table mapping item IDs to their image files.
-- A table mapping item IDs to their image files.
-- Keys are item IDs; each value is a file name or an object (for items with multiple textures).
-- Keys are item IDs; each value is a file name or an object (for items with multiple textures).
--  
--
-- Meant to be filled manually.
-- Meant to be filled manually.
--  
--
-- These are used to display item icons.
-- These are used to display item icons.
local item_images_by_item_ids = mw.loadJsonData("Module:Item/item image files by item id.json")
local item_images_by_item_ids = mw.loadJsonData("Module:Item/item image files by item id.json")
Line 40: Line 39:
-- A table mapping item IDs to specific pages.
-- A table mapping item IDs to specific pages.
-- Keys are item IDs; each value is a page name.
-- Keys are item IDs; each value is a page name.
--  
--
-- Meant to be filled manually.
-- Meant to be filled manually.
--  
--
-- These are used to turn items into links.
-- These are used to turn items into links.
local item_page_links_by_item_ids = mw.loadJsonData("Module:Item/item page links by item ids.json")
local item_page_links_by_item_ids = mw.loadJsonData("Module:Item/item page links by item ids.json")
-- A table mapping lowercased size IDs to size objects (or aliases to another size IDs).
--
-- Meant to be filled manually.
local known_sizes = mw.loadJsonData("Module:Item/sizes.json")


-- Get a reference to the current frame.
-- Get a reference to the current frame.
local current_frame = mw:getCurrentFrame()
local current_frame = mw:getCurrentFrame()
local current_language_obj = mw.getContentLanguage()


-- A boolean that becomes `true` once the template styles for {{Item}} has been added to the page.
-- A boolean that becomes `true` once the template styles for {{Item}} has been added to the page.
Line 91: Line 98:
local function capitalize(str)
local function capitalize(str)
     return (str:gsub("^%l", string.upper))
     return (str:gsub("^%l", string.upper))
end
-- Makes the first letter in every word uppercase.
local function capitalize_all(str)
    local res = { }
    for _, word in ipairs(mw.text.split(str, ' ', true)) do
        table.insert(res, capitalize(word))
    end
    return table.concat(res, " ")
end
local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
    if value then
        return valueToReturnIfTrue
    else
        error(errorMessageOnFalse)
    end
end
-- Lookup a field in a json table by key.
-- Json tables are assumed to be tables with lowercase keys. The `key` is automatically lowercased for the lookup.
-- If a value is a string, it's treated as an alias (another key), triggering a new lookup.
--
-- Returns the value by the specified key, or by aliased key if the value was an alias.
-- If value doesn't exist, returns `nil`.
local function lookup_json_table(t, key)
local result = t[string.lower(key)]
if type(result) == "string" then
-- alias. lookup actual definition.
return t[result]
else
return result
end
end
end


Line 96: Line 137:
-- =======================
-- =======================


-- Contains item IDs of image files json file entries that were validated.
-- A table of item IDs that were validated for `validate_item_images_by_item_ids_table_entry`..
local items_ids_of_validated_item_images_by_item_ids_entries = {}
local validate_item_images_by_item_ids_table_entry__validated_item_ids = {}


-- Validates an entry from image files json file.
-- Validator for item images table entries.
-- Validation is done once per unique entry.
--
local function validate_item_images_by_item_ids_entry(entry, item_id)
-- Used internally for lazilly validating schema of entries.
--
-- Once a validation is conducted for an entry, subsequent calls for the same item ID will
-- not trigger revalidation, thus the lazy part.
local function validate_item_images_by_item_ids_table_entry(entry, item_id)
     -- skip validation for already validated entries
     -- skip validation for already validated entries
     if items_ids_of_validated_item_images_by_item_ids_entries[item_id] ~= nil then
     if validate_item_images_by_item_ids_table_entry__validated_item_ids[item_id] ~= nil then
         return
         return
     end
     end


    local entry = item_images_by_item_ids[item_id]
     assert_value_not_nil(entry, "item images json file validation failed: no entry was found with item ID: " .. item_id)
     assert_value_not_nil(entry, "item images json file validation failed: no entry was found with item ID: " .. item_id)


     if type(entry) == 'table' then
     if type(entry) == 'table' then
         assert_value_not_nil(entry.default, "item images json file validation failed: expected 'default' to be defined for item: " .. item_id)
         assert_value_not_nil(entry.default,
         assert(type(entry.default) == "string", "item images json file validation failed: expected 'default' to be a string, found '" .. type(entry.default) .. "' for item: " .. item_id)
            "item images json file validation failed: expected 'default' to be defined for item: " .. item_id)
         assert(type(entry.default) == "string",
            "item images json file validation failed: expected 'default' to be a string, found '" ..
            type(entry.default) .. "' for item: " .. item_id)


         local byCondition = entry.byCondition
         local byCondition = entry.byCondition
         assert_value_not_nil(entry.byCondition, "item images json file validation failed: expected 'byCondition' to be defined since 'amount' was given; item: " .. item_id)
         assert_value_not_nil(entry.byCondition,
         assert(type(entry.byCondition) == "table", "item images json file validation failed: expected 'byCondition' to be a table, found '" .. type(entry.byCondition) .. "' for item: " .. item_id)
            "item images json file validation failed: expected 'byCondition' to be defined since 'amount' was given; item: " ..
   
            item_id)
         assert(type(entry.byCondition) == "table",
            "item images json file validation failed: expected 'byCondition' to be a table, found '" ..
            type(entry.byCondition) .. "' for item: " .. item_id)
 
         for _, byConditionEntry in ipairs(byCondition) do
         for _, byConditionEntry in ipairs(byCondition) do
             local entry_type = byConditionEntry.type
             local entry_type = byConditionEntry.type
             assert_value_not_nil(entry_type, "item images json file validation failed: expected 'type' to be defined on one of 'byCondition' entries; item: " .. item_id)
             assert_value_not_nil(entry_type,
             assert(type(entry_type) == "string", "item images json file validation failed: expected 'type' to be a string on one of 'byCondition' entries, found '" .. type(entry_type) .. "' for item: " .. item_id)
                "item images json file validation failed: expected 'type' to be defined on one of 'byCondition' entries; item: " ..
   
                item_id)
             assert(type(entry_type) == "string",
                "item images json file validation failed: expected 'type' to be a string on one of 'byCondition' entries, found '" ..
                type(entry_type) .. "' for item: " .. item_id)
 
             if entry_type == 'amount' then
             if entry_type == 'amount' then
                 local conditions = byConditionEntry.conditions
                 local conditions = byConditionEntry.conditions
                 assert_value_not_nil(conditions, "item images json file validation failed: expected 'conditions' to be defined on one of 'byCondition' entries; item: " .. item_id)
                 assert_value_not_nil(conditions,
                 assert(type(conditions) == "table", "item images json file validation failed: expected 'conditions' to be a table on one of 'byCondition' entries, found '" .. type(conditions) .. "' for item: " .. item_id)
                    "item images json file validation failed: expected 'conditions' to be defined on one of 'byCondition' entries; item: " ..
       
                    item_id)
                 assert(type(conditions) == "table",
                    "item images json file validation failed: expected 'conditions' to be a table on one of 'byCondition' entries, found '" ..
                    type(conditions) .. "' for item: " .. item_id)
 
                 for _, condition in ipairs(conditions) do
                 for _, condition in ipairs(conditions) do
                     local file = condition.file
                     local file = condition.file
                     assert_value_not_nil(file, "item images json file validation failed: expected 'file' in one of 'conditions' entries in on one of 'byCondition' entries to be defined for item: " .. item_id)
                     assert_value_not_nil(file,
                        "item images json file validation failed: expected 'file' in one of 'conditions' entries in on one of 'byCondition' entries to be defined for item: " ..
                        item_id)


                     local conditionMin = condition.min
                     local conditionMin = condition.min
                     if conditionMin ~= nil then
                     if conditionMin ~= nil then
                         assert(type(conditionMin) == "number", "item images json file validation failed: expected 'min' in one of 'conditions' entries in on one of 'byCondition' entries to be a number, found '" .. type(condition.min) .. "' for item: " .. item_id)
                         assert(type(conditionMin) == "number",
                            "item images json file validation failed: expected 'min' in one of 'conditions' entries in on one of 'byCondition' entries to be a number, found '" ..
                            type(condition.min) .. "' for item: " .. item_id)
                     end
                     end
                 end
                 end
             else
             else
                 error("item images json file validation failed: expected 'type' to be one of known values on one of 'byCondition' entries, but found '" .. entry_type .. "' entries; item: " .. item_id)  
                 error(
                    "item images json file validation failed: expected 'type' to be one of known values on one of 'byCondition' entries, but found '" ..
                    entry_type .. "' entries; item: " .. item_id)
             end
             end
         end
         end
     end  
     end
 
    validate_item_images_by_item_ids_table_entry__validated_item_ids[item_id] = true
end
 
-- Item compositor producing following format: `icon label [amount]`, for example:
-- - `<glass icon> glass [25]`
local function compositor_icon_label_amount_square_bracketed(item_el, icon_maybe, icon_size, label_maybe, amount_maybe, page_link_maybe)
    local icon_res
    if icon_maybe then
        icon_res = "[[File:" .. icon_maybe .. "|" .. icon_size
            .. (page_link_maybe and ("|link=" .. page_link_maybe) or "")
            .. "]]"
    end
 
    local label_res
    if label_maybe then
        label_res = (page_link_maybe and label_maybe ~= "")
            and ("[[" .. page_link_maybe .. "|" .. label_maybe .. "]]")
            or label_maybe
    end
 
    local amount_res
    if amount_maybe then
        amount_res = "[" .. amount_maybe .. "]"
    end
 
    item_el:wikitext(
        (icon_res and icon_res or "")
        .. (label_res and label_res or "")
        .. (amount_res and ("&nbsp;" .. amount_res) or "")
    )
end
 
-- Item compositor producing following format: `icon amount label`, for example:
-- - `<glass icon> 25 glass`
local function compositor_icon_amount_label(item_el, icon_maybe, icon_size, label_maybe, amount_maybe, page_link_maybe)
    local icon_res
    if icon_maybe then
        icon_res = "[[File:" .. icon_maybe .. "|" .. icon_size
            .. (page_link_maybe and ("|link=" .. page_link_maybe) or "")
            .. "]]"
    end
 
    local label_amount_res
    if label_maybe or amount_maybe then
        local label_amount_combined
        if label_maybe and amount_maybe then
            label_amount_combined = amount_maybe .. " " .. label_maybe
        elseif label_maybe then
            label_amount_combined = label_maybe
        else -- amount
            label_amount_combined = amount_maybe
        end


    items_ids_of_validated_item_images_by_item_ids_entries[item_id] = true
        label_amount_res = (page_link_maybe and label_amount_combined ~= "")
            and ("[[" .. page_link_maybe .. "|" .. label_amount_combined .. "]]")
            or label_amount_combined
    end
 
    item_el:wikitext(
            (icon_res and icon_res or "")
            .. (label_amount_res and label_amount_res or "")
        )
end
end


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


-- Checks whether there an item with given ID.
-- Lookups item ID by item name override (any casing).
-- Casing must match exactly.
--
--
-- Raises an error if ID is `nil`.
-- Raises an error if no item name override was found.
function p.item_exists_by_id(item_id)
-- Set `no_error` to `true` to return `nil` instead.
     assert_value_not_nil(item_id, "failed to check whether an item exists by ID: ID was not provided")
function p.lookup_item_id_by_name_override(item_name_override, no_error)
     assert_value_not_nil(item_name_override,
        "item ID lookup by item name override failed: item name override was not provided")


     return item_names_by_item_ids[item_id] ~= nil
     local item_name_override_lower = string.lower(item_name_override)
end


-- Checks whether there an item with given name.
    local item_id = item_ids_by_item_names_override[item_name_override_lower]
-- Name is turned lowercase before checking, so any casing is allowed.
    if item_id == nil then
--
        if no_error then
-- Raises an error if name is `nil`.
            return nil
function p.item_exists_by_name(item_name)
        else
    assert_value_not_nil(item_name, "failed to check whether an item exists by name: name was not provided")
            error("item ID lookup by item name override failed: no item name override '" ..
                item_name_override_lower ..
                "' was found. Make sure that an override is defined in the item name overrides table of Module:Item")
        end
    end


     local item_name_lower = string.lower(item_name)
     if not p.item_exists_with_id(item_id) then
        error("item ID lookup by item name override failed: item with looked up item ID '" ..
            item_id .. "' does not exist (item name override: '" ..
            item_name_override_lower ..
            "'). Make sure that the name override for this item is defined correctly in Module:Item and the item exist")
    end


     return item_ids_by_item_names_override[item_name_lower] ~= nil and item_ids_by_item_names[item_name_lower] ~= nil
     return item_id
end
end


-- Checks whether there an item with given ID or name.
-- Lookups item ID by item name `item_name` (any casing).
-- Casing must match exactly for an ID, but can vary for a name.
--
--
-- Raises an error if given ID or name is `nil`.
-- Raises an error if no item was found.
function p.item_exists_by_id_or_name(query)
-- Set `no_error` to `true` to return `nil` instead.
     return p.item_exists_by_id(query) or p.item_exists_by_name(query)
function p.lookup_item_id_by_item_name(item_name, no_error)
    assert_value_not_nil(item_name, "item ID lookup by item name failed: item name was not provided")
 
    -- first, try to lookup item name in name overrides
     return p.lookup_item_id_by_name_override(item_name, true)
        -- then, look in regular item names
        or item_ids_by_item_names[string.lower(item_name)]
        or passthrough_assert_true(
            no_error,
            nil,
            "item ID lookup by item name failed: no item name was found by item ID '" ..
            item_name .. "'. Make sure that an item exist with this ID or a name override is defined"
        )
end
end


-- Lookups item ID by name.
-- Lookups item ID by query.
-- Name is turned lowercase before the lookup, so any casing is allowed.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- Raises an error if name is `nil`.
--
--
-- Returns `nil` if there's no item matching given name.
-- Raises an error if no item was found.
function p.lookup_item_id_by_name(item_name)
-- Set `no_error` to `true` to return `nil` instead.
     assert_value_not_nil(item_name, "failed to lookup an item by name: name was not provided")
function p.lookup_item_id(query, no_error)
     assert_value_not_nil(query, "item ID lookup failed: item ID/name (query) was not provided")


     local item_name_lower = string.lower(item_name)
     if item_names_by_item_ids[query] ~= nil then
 
        return query
     return item_ids_by_item_names_override[item_name_lower] or item_ids_by_item_names[item_name_lower]
    else
        return p.lookup_item_id_by_item_name(query, true)
            or passthrough_assert_true(
                no_error,
                nil,
                "item ID lookup failed: no item was found by ID/name '" ..
                query .. "'. Make sure that an item exist with this ID or a name override is defined"
            )
     end
end
end


-- Lookups item name by ID.
-- Lookups item name by item ID `item_id` (strict casing).
-- Casing must match exactly for an ID, but can vary for a name.
--
-- Raises an error if ID is `nil`.
--
-- Raises an error if item with given ID doesn't exist.
--
--
-- Returns `nil` if there's no item matching given ID.
-- Raises an error if if no item was found.
function p.lookup_item_name_by_id(item_id)
-- Set `no_error` to `true` to return `nil` instead.
     assert_value_not_nil(item_id, "failed to lookup an item's name by id: id was not provided")
function p.lookup_item_name_by_item_id(item_id, no_error)
    assert(p.item_exists_by_id(item_id) == true, "failed to lookup an item's name: item with ID '" .. item_id .. "' doesn't exist")
     assert_value_not_nil(item_id, "item name lookup by item ID failed: item ID was not provided")


     return item_names_by_item_ids[item_id]
     return item_names_by_item_ids[item_id]
        or
        passthrough_assert_true(
            no_error,
            nil,
            "item name lookup by item ID failed: no item name was found by item ID '" ..
            item_id .. "'. Make sure that an item exist with this ID or a name override is defined"
        )
end
end


-- Lookups item ID by a query. Query can either be a name or the item ID itself.
-- Lookups item name by query.
-- Name is turned lowercase before checking, so any casing is allowed.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
--
-- Raises an error if query is `nil`.
-- Raises an error if if no item was found.
--
-- Set `no_error` to `true` to return `nil` instead.
-- Raises an error is there's no item matching query.
function p.lookup_item_name(query, no_error)
function p.try_lookup_item_id(query)
     assert_value_not_nil(query, "item name lookup failed: item ID/name (query) was not provided")
     assert_value_not_nil(query, "failed to lookup an item by query: query was not provided")
 
    local query_lower = string.lower(query)


     if p.item_exists_by_id(query) then
     if p.lookup_item_id_by_name_override(query, true) ~= nil or item_ids_by_item_names[query_lower] ~= nil then
         return query
         return query
    else
        return p.lookup_item_name_by_item_id(query, true)
            or passthrough_assert_true(
                no_error,
                nil,
                "item name lookup failed: no item was found by ID/name '" ..
                query .. "'. Make sure that an item exist with this ID or a name override is defined"
            )
     end
     end
end


    -- check by name
-- Checks whether an item exists with name `item_name` (any casing).
    local match_by_name = p.lookup_item_id_by_name(query)
function p.item_exists_with_name(item_name)
     if match_by_name then
     -- query non-nil assertion is done in the subsequent function call
        return match_by_name
 
     end
    return p.lookup_item_id_by_item_name(item_name, true) ~= nil
end
 
-- Checks whether an item exists with ID `item_id` (strict casing).
function p.item_exists_with_id(item_id)
     -- query non-nil assertion is done in the subsequent function call


     error("item id lookup by ID or name failed: no item was found with query: " .. query)
     return p.lookup_item_name_by_item_id(item_id, true) ~= nil
end
end


-- Checks whether an item exists by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
function p.item_exists(query)
    -- query non-nil assertion is done in the subsequent function calls


-- Searches for item image file using its ID.
    return p.item_exists_with_id(query)
-- The casing must match exactly.
        or p.item_exists_with_name(query)
--  
end
-- Has an optional amount, which can affect what image is choosen in some cases.
 
--  
-- Lookups item image by query.
-- Raises an error if item ID is `nil`.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- An `amount` can be specified to correctly pick an image for items
-- with multiple images (depending on the amount). By default, it has no value.
--
--
-- Raises an error if item with given ID doesn't exists.
-- Raises an error if no item was found by `query`.
--
--
-- Returns "nil" if item doesn't have an image defined.
-- Returns `nil` if no image is defined for an item.
-- todo fix duplicate default
function p.try_lookup_item_image(query, amount)
function p.lookup_item_image_by_id(item_id, amount)
     local item_id = p.lookup_item_id(query, true)
     assert_value_not_nil(item_id, "failed to lookup item image by its id: item ID was not provided")
     assert_value_not_nil(item_id, "item image lookup failed: no item was found by ID/name '" .. query .. "'")
     assert(p.item_exists_by_id(item_id) == true,
        "failed to lookup item image by its id: item with ID '" .. item_id .. "' doesn't exist")


     local match = item_images_by_item_ids[item_id]
     local item_image = item_images_by_item_ids[item_id]
     -- if no image was found for an item OR match was successful, and it's a string
     if item_image == nil then
    -- = return. in first case, "nil" will be returning, meaning no image file,
        return nil
     -- in second case - the image file will be returned.
     elseif type(item_image) == 'string' then
    if match == nil or type(match) == "string" then
         return item_image
         return match
     end
     end


     -- otherwise, a table with config for multiple textures is expected.
     -- if item image "entry" was found and it's not a string,
     -- validate that it's a table and its structure, and also the types.
    -- then it must be a config for multiple images.
     validate_item_images_by_item_ids_entry(match, item_id)
     --
    -- send the "entry" to validation.
     validate_item_images_by_item_ids_table_entry(item_image, item_id)
 
    -- if validation was successful with no errors,
    -- now we can utilize the config schema without doing any checks.


     -- if no amount is specified, use the default value.
     -- if no amount is specified,
    -- then there's no reason to resolve further.
    -- use the default image file for that config.
     if amount == nil then
     if amount == nil then
         return match.default
         return item_image.default
     end
     end


     -- if amount is specified, then there must be conditions defined.
     -- if amount is specified, then resolve further
     for _, byConditionEntry in ipairs(match.byCondition) do
     for _, byConditionEntry in ipairs(item_image.byCondition) do
         local entry_type = byConditionEntry.type
         local entry_type = byConditionEntry.type


         if entry_type == 'amount' then
         if entry_type == 'amount' then
             local conditions = byConditionEntry.conditions
             local conditions = byConditionEntry.conditions
   
 
             for _, condition in ipairs(conditions) do
             for _, condition in ipairs(conditions) do
                -- currently, there's a single condition - "min".
                -- it might be unset, meaning no other conditions,
                 local conditionMin = condition.min
                 local conditionMin = condition.min
                 if conditionMin == nil then
                 if conditionMin == nil then
                     -- this check is for when there's no conditions whatsoever
                     -- if this condition is not set, then there's no other conditions to check.
                    -- if you add more condition types - this code will need to be updated.
                     -- use the file from this condition
                     -- current logic - is to just return the "file".
 
                     return condition.file
                     return condition.file
                 elseif amount >= conditionMin then
                 elseif amount >= conditionMin then
                    -- if condition is set - validate it.
                    -- if it satisfies - return the file.
                     return condition.file
                     return condition.file
                 end
                 end
             end
             end
         else
         else
             error("failed to lookup item image by its id: lookup value is a table; expected 'type' to be one of known values on one of 'byCondition' entries, but found '" .. entry_type .. "' entries; item: " .. item_id)  
             error(
                "item image lookup failed: unknown entry type '" .. entry_type .. "' for item '" .. item_id .. "'")
         end
         end
     end
     end


     -- if no condition was satisfied, throw en error
     -- if not a single condition satisfied - raise an error
     error("failed to lookup item image by its id: lookup value is a table; no condition was satisifed for item: " .. item_id)
     error("item image lookup failed: no condition satisfied for item '" .. item_id .. "'")
end
end


-- Searches for item page name using its ID.
-- Lookups item page name by query.
-- The casing must match exactly.
-- Query can either be an item ID (strict casing) or item name (any casing).
--  
-- Raises an error if item ID is `nil`.
--
-- Raises an error if item with given ID doesn't exists.
--
--
-- Returns "nil" if item doesn't have an page name defined.
-- Returns `nil` if no page is defined for an item.
function p.lookup_item_page_by_item_id(item_id)
function p.try_lookup_item_page(query)
     assert_value_not_nil(item_id, "failed to lookup item page by its id: item ID was not provided")
     local item_id = p.lookup_item_id(query, true)
     assert(p.item_exists_by_id(item_id) == true,
     assert_value_not_nil(item_id, "item page lookup failed: no item was found by ID/name '" .. query .. "'")
        "failed to lookup item page by its id: item with ID '" .. item_id .. "' doesn't exist")


     return item_page_links_by_item_ids[item_id]
     return item_page_links_by_item_ids[item_id]
Line 313: Line 492:
function p.generate_item(frame)
function p.generate_item(frame)
     local args = getArgs(frame)
     local args = getArgs(frame)
     local argsWithWhitespace = getArgs(frame, { trim = false, removeBlanks = false })
     local args_with_whitespace = getArgs(frame, { trim = false, removeBlanks = false })


     -- [REQUIRED]
     -- [REQUIRED]
Line 319: Line 498:
     -- input item name or ID.
     -- input item name or ID.
     -- any casing is allowed for name, but ID must follow strict casing.
     -- any casing is allowed for name, but ID must follow strict casing.
     local input_item = args[1]
     local arg_item = args[1]
     assert_value_not_nil(input_item, "failed to generate an item: item was not provided")
     assert_value_not_nil(arg_item, "failed to generate an item: item was not provided")


     -- [OPTIONAL]
     -- [OPTIONAL]


     -- amount of item.
     -- amount of item.
     -- input is a string number or nil
     -- input is a string, number or nil.
     local input_amount = tonumber(args[2])
     -- try to convert to a number if not nil.
    -- if conversion fails, keep it as a string for lax interpretation.
    -- if it succeeds, we got a proper amount, yay.
    local arg_amount = args[2]
    local is_arg_amount_nan = false
    if arg_amount ~= nil then
        local num = tonumber(arg_amount)
        if num ~= nil then
            arg_amount = num
        else
            arg_amount = args[2]
            is_arg_amount_nan = true
        end
    end


     -- item icon size. uses CSS units.
     -- item icon size. uses CSS units.
     local input_icon_size = args.size or "32px"
     local arg_icon_size = args.size or "32px"


     -- text label. can be set, otherwise inferred from the item later (so "nil" for now).
     -- text label. can be set, otherwise inferred from the item later (so "nil" for now).
     local input_label = argsWithWhitespace.label or argsWithWhitespace.l
     local arg_label = args_with_whitespace.label or args_with_whitespace.l


     -- whether to capitalize the label. false by default.
     -- whether to capitalize the label. false by default.
     local input_capitalize_label = yesNo(args.capitalize or args.cap or false)
     local arg_capitalize_label = yesNo(args.capitalize or args.cap or false)
   
    -- whether to capitalize every word in the label. false by default.
    local arg_capitalize_all_label = yesNo(args["capitalize all"] or args["cap all"] or false)


     -- a link to a page.
     -- a link to a page.
     -- if set, turns item into a link.
     -- if set, turns item into a link.
     -- if unset, and item has a link defined for it in the config - uses it.
     -- if unset, and item has a link defined for it in the config - uses it.
     local input_link = args.link
     local arg_link = args.link
 
    -- whether to apply a number formatting to the amount.
    local arg_do_format_amount = args["format amount"] or false
 
    -- a class to add to the item element
    local arg_extra_class = args.class or nil
 
    -- composition of item elements
    local arg_composition = args.composition or "icon label [amount]"


     -- ============
     -- ============
    -- CALCULATE INTERMEDIATE PARAMS


     local item_id = p.try_lookup_item_id(input_item)
     local item_el = mw.html.create("span")
        :addClass("item")
 
    if arg_extra_class ~= nil then
        item_el:addClass(arg_extra_class)
    end


     local item_image_filename = p.lookup_item_image_by_id(item_id, input_amount)
     local item_id = p.lookup_item_id(arg_item, true)
    if item_id == nil then
        item_el:addClass("unknown");
        return item_el:wikitext("unknown item '" .. arg_item .. "'")
    end
    -- assert_value_not_nil(item_id, "item generation failed: no item was found by ID/name '" .. input_item .. "'")


    -- filename of an icon
    local icon_filename = p.try_lookup_item_image(
        item_id,
        is_arg_amount_nan and nil or arg_amount
    )


     if input_link == nil then
     -- page to link to
         input_link = p.lookup_item_page_by_item_id(item_id)
    local page_link = arg_link
    end
         or p.try_lookup_item_page(item_id)




     local label
     local label
     if input_label == nil then
     if arg_label == nil then
         -- if a custom label is not provided, lookup the item's label
         -- if a custom label is not provided, lookup the item's label
         label = p.lookup_item_name_by_id(item_id)
         label = p.lookup_item_name_by_item_id(item_id)
     else
     else
         -- if a label is provided - use it
         -- if a label is provided - use it
         label = input_label
         label = arg_label
     end
     end


     if input_capitalize_label then
     if arg_capitalize_label then
         label = capitalize(label)
         label = capitalize(label)
    elseif arg_capitalize_all_label then
        label = capitalize_all(label)
     end
     end


     if input_amount ~= nil then
    local amount
         label = input_amount .. " " .. label
     if arg_amount ~= nil then
         if is_arg_amount_nan then
            -- failed to parse as a number, it is likely an arbitrary string
            -- add it as is
            amount = arg_amount
        else
            -- valid number
            amount = (arg_do_format_amount and current_language_obj:formatNum(arg_amount) or arg_amount)
        end
     end
     end


     if input_link ~= nil then
    -- ===============
         label = "[[" .. input_link .. "|" .. label .. "]]"
    -- COMPOSITE PARAMS INTO THE ITEM ELEMENT
   
     if arg_composition == "icon label [amount]" then
         compositor_icon_label_amount_square_bracketed(item_el, icon_filename, arg_icon_size, label, amount, page_link)
    elseif arg_composition == "icon amount label" then
        compositor_icon_amount_label(item_el, icon_filename, arg_icon_size, label, amount, page_link)
    else
        error("unknown item composition: " .. arg_composition)
     end
     end
 
   
     -- ============
     -- ===============
 
    local item_el = mw.html.create("span")
        :addClass("item")
 
    -- add icon element inside the label if icon is provided
    if item_image_filename ~= nil then
        local link_param = ''
        if input_link ~= nil then
            link_param = '|link=' .. input_link
        end
 
        item_el:node("[[File:" .. item_image_filename .. "|" .. input_icon_size .. "|class=item-icon" .. link_param .. "]]")
    end
 
    item_el:node(label)
 


     if not was_template_styles_tag_el_added then
     if not was_template_styles_tag_el_added then
Line 398: Line 619:
         was_template_styles_tag_el_added = true
         was_template_styles_tag_el_added = true
     end
     end


     return item_el
     return item_el
Line 405: Line 625:


function p.generate_list_of_all_items_with_icons(frame)
function p.generate_list_of_all_items_with_icons(frame)
local args = getArgs(frame)
    local args = getArgs(frame)
local columns_count = args[1]
    local columns_count = args[1]
assert_value_not_nil(columns_count, "columns count was not provided")
    assert_value_not_nil(columns_count, "columns count was not provided")
 
    local container = mw.html.create("div")
        :css("column-count", columns_count)
 
    -- an array of item ids that have images
    local item_ids_with_images = {}
    for item_id, _ in pairs(item_images_by_item_ids) do
        table.insert(item_ids_with_images, item_id)
    end


local container = mw.html.create("div")
    -- item IDs that were not found in the item ids table
:css("column-count", columns_count)
    local non_existing_item_ids = {}


-- an array of item ids that have images
    -- sort alphabetically
local item_ids_with_images = {}
    table.sort(item_ids_with_images, function(first, second)
for item_id, _ in pairs(item_images_by_item_ids) do
        local first_item_name = p.lookup_item_name_by_item_id(first, true)
table.insert(item_ids_with_images, item_id)
        local second_item_name = p.lookup_item_name_by_item_id(second, true)
end
 
        if first_item_name == nil then
            non_existing_item_ids[first] = true
            return false
        end
 
        if second_item_name == nil then
            non_existing_item_ids[second] = true
            return false
        end


-- sort alphabetically
        return first_item_name < second_item_name
table.sort(item_ids_with_images, function (first, second)
    end)
    return p.lookup_item_name_by_id(first) < p.lookup_item_name_by_id(second)
end)


-- generate child elements from the template
    -- generate child elements from the template
for _, item_id in ipairs(item_ids_with_images) do
    for _, item_id in ipairs(item_ids_with_images) do
local item_el = p.generate_item{ item_id }
        if non_existing_item_ids[item_id] ~= true then
:css("display", "block")
            local item_el = p.generate_item { item_id }
                :css("display", "block")
                :node(" <span style='color: gray;'>ID " .. item_id .. "</span>")


container:node(item_el)
            container:node(item_el)
end
        end
    end


return container
    return container
:allDone()
        :allDone()
end
end


-- -- Generates a list of ALL items.
-- -- Generates a list of ALL items.
-- -- Will likely break :clueless:  
-- -- Will likely break :clueless:
function p.generate_list_of_all_items(frame)
function p.generate_list_of_all_items(frame)
local args = getArgs(frame)
    local args = getArgs(frame)
local columns_count = args[1]
    local columns_count = args[1]
assert_value_not_nil(columns_count, "columns count was not provided")
    assert_value_not_nil(columns_count, "columns count was not provided")


local container = mw.html.create("div")
    local container = mw.html.create("div")
:css("column-count", columns_count)
        :css("column-count", columns_count)


local item_ids = {}
    local item_ids = {}
for item_id, _ in pairs(item_names_by_item_ids) do
    for item_id, _ in pairs(item_names_by_item_ids) do
table.insert(item_ids, item_id)
        table.insert(item_ids, item_id)
end
    end


-- sort alphabetically
    -- sort alphabetically
table.sort(item_ids, function (first, second)
    table.sort(item_ids, function(first, second)
return p.lookup_item_name_by_id(first) < p.lookup_item_name_by_id(second)
        return p.lookup_item_name_by_item_id(first) < p.lookup_item_name_by_item_id(second)
end)
    end)


-- generate child elements from the template
    -- generate child elements from the template
for _, item_id in ipairs(item_ids) do
    for _, item_id in ipairs(item_ids) do
local item_el = p.generate_item{ item_id }
        local item_el = p.generate_item { item_id }
:css("display", "block")
            :css("display", "block")
            :node(" <span style='color: gray;'>ID " .. item_id .. "</span>")


container:node(item_el)
        container:node(item_el)
end
    end


return container
    return container
:allDone()
        :allDone()
end
end


return p
return p

Latest revision as of 14:03, 27 May 2025

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

Implements {{Item}}.

Known items are synced regularly from the upstream, but things like icons and links must be defined manually. See #JSON files to see what data files there are, and see #FAQ on specific instructions.

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:

FAQ

How to add new item?

New items are added automatically. This doesn't include icons - for that, see #How to add icon to item?.

Where to get item ID?

From Module:Item/item_names_by_item_ids.json.

How to add icon to item?

If you want to add multiple textures per single item, see #Adding multiple icons to item

1. Upload new icon to the wiki.

2. Go to Module:Item/item_image_files_by_item_id.json.

3. Add a new line. Follow the format: "<item ID>": "<file name>"

Example
"WeaponLaserCarbine": "laser rifle-East-35325.png"

4. Save the file. The icon should now appear when using {{item}}.

Adding multiple icons to item

Currently, the only supported use case if for items that have a different icon based on the amount of item.

1. Upload new icons to the wiki.

2. Go to Module:Item/item_image_files_by_item_id.json.

3. Add a new line. Follow the format:

Format
"<item ID>": {
	"default": "<default file name>",
	"byCondition": [
		{
			"type": "amount",
			"conditions": [
				{
					"file": "<file name 1>",
					"min": <minimum amount 1>
				},
				{
					"file": "<file name 2>",
					"min": <minimum amount 2>
				},
				{
					"file": "<file name 3>",
				}
			]
		}
	]
}
  • item ID - item ID to add icons for.
  • default file name - icon to use when amount is not specified.
  • file name 1/2/N - icons to use with specified amounts.
  • "min": <amount 1/2/N> - icon to use when there's at least this much of item.

Last condition entry (objects that have "file" and "min" fields) shouldn't have any condition in it (i.e. no "min" specified), because it will be used in cases where other conditions do not satisfy.

Conditions are evaluated top to bottom, meaning the file from the first one that satisfies will be used.

4. Save the file. The icons should now appear when using {{item}} and differ based on the amount.

How to add custom names to item?

When using {{item}}, you probably don't want to use item IDs because that's internal game info which is a pain in the ass to write. Gladly, there's an existing set of item names defined in Module:Item/item_ids_by_item_lowercase_names.json, which are human-readable. But not all existing items will have their names in there, because some names do repeat (for instance, various bottles named bottle).

To add new cool names and have them not be erased on new update (which happens to the JSON file linked in previous paragraph), add them to Module:Item/item_ids_by_item_lowercase_names_overrides.json. These will have higher priority and will be used instead. You can define as much "aliases" for an item ID as you wish.

Step-by-step: 1. Go to Module:Item/item_ids_by_item_lowercase_names_overrides.json.

2. Add a new line. Follow format: "<item lowercase name>": "<item ID>". Please note, that all items names defined here must be lowercase.

Example
"emag": "EmagUnlimited"

3. Save the file.

How to add a link to item?

To make {{item}} behave like a link all the time, a page link needs to be established in Module:Item/item page links by item ids.json. Please note, if you need a one-time link, use the link parameter in the {{item}} template.

1. Go to Module:Item/item page links by item ids.json.

2. Add a new line. Follow format: "<item ID>": "<page name>".

Example
"Protolathe": "Research_and_Development#Protolathe"

3. Save the file.

Adding new sizes

Info
This section covers adding new size variations, not sizes for specific items.

Sizes are defined the Module:Item/sizes.json file.

Keys are lowercased size IDs, values are size objects. A key can also be an alias to another size ID, in which case its value must be another lowercased size ID defined elsewhere in the file.

Size object
Key Type Description
weight Integer Weight that an item of that size would have.
size Tuple (integer, integer) The size (width, height).
display String The label that would be displayed when looking up this size.

TODO

  • Ores are currently hardcoded into names overrides. Figure out a way to pull them from game resources. This is for Module:Item recipe.



-- Contains utilities for working with in-game items.

-- todo create external tests for schema tables (under /doc)
-- todo make `generate_list_of_all_items_with_icons` also display items with nontrivial image files

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

-- A table mapping item IDs to their names.
-- Keys are item IDs; each value is a string.
--
-- These names are used for labels.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
local item_names_by_item_ids = mw.loadJsonData("Module:Item/item names by item ids.json")

-- A table mapping item names to their IDs.
-- Keys are item names; each value is an item ID.
-- An item can have multiple names.
--
-- These names are used for ID lookups.
-- This table will be updated automatically, DO NOT make manual changes to it - they will be lost.
local item_ids_by_item_names = mw.loadJsonData("Module:Item/item ids by item lowercase names.json")

-- Same as `item_ids_by_item_names`, but has a higher priority
-- and meant to be filled manually.
--
-- These names are used for ID lookups.
local item_ids_by_item_names_override = mw.loadJsonData("Module:Item/item ids by item lowercase names overrides.json")

-- A table mapping item IDs to their image files.
-- Keys are item IDs; each value is a file name or an object (for items with multiple textures).
--
-- Meant to be filled manually.
--
-- These are used to display item icons.
local item_images_by_item_ids = mw.loadJsonData("Module:Item/item image files by item id.json")

-- A table mapping item IDs to specific pages.
-- Keys are item IDs; each value is a page name.
--
-- Meant to be filled manually.
--
-- These are used to turn items into links.
local item_page_links_by_item_ids = mw.loadJsonData("Module:Item/item page links by item ids.json")


-- A table mapping lowercased size IDs to size objects (or aliases to another size IDs).
-- 
-- Meant to be filled manually.
local known_sizes = mw.loadJsonData("Module:Item/sizes.json")

-- Get a reference to the current frame.
local current_frame = mw:getCurrentFrame()

local current_language_obj = mw.getContentLanguage()

-- A boolean that becomes `true` once the template styles for {{Item}} has been added to the page.
-- Used to not add them a million times for all items generations.
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 table_has_value(tab, val)
    for _, value in ipairs(tab) do
        if value == val then
            return true
        end
    end

    return false
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

-- Makes the first letter uppercase.
-- Source: https://stackoverflow.com/a/2421746
local function capitalize(str)
    return (str:gsub("^%l", string.upper))
end

-- Makes the first letter in every word uppercase.
local function capitalize_all(str)
    local res = { }
    for _, word in ipairs(mw.text.split(str, ' ', true)) do
        table.insert(res, capitalize(word))
    end
    return table.concat(res, " ")
end

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


-- Lookup a field in a json table by key.
-- Json tables are assumed to be tables with lowercase keys. The `key` is automatically lowercased for the lookup.
-- If a value is a string, it's treated as an alias (another key), triggering a new lookup.
-- 
-- Returns the value by the specified key, or by aliased key if the value was an alias.
-- If value doesn't exist, returns `nil`.
local function lookup_json_table(t, key)
	local result = t[string.lower(key)]
	if type(result) == "string" then
		-- alias. lookup actual definition.
		return t[result]
	else
		return result
	end
end


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

-- A table of item IDs that were validated for `validate_item_images_by_item_ids_table_entry`..
local validate_item_images_by_item_ids_table_entry__validated_item_ids = {}

-- Validator for item images table entries.
--
-- Used internally for lazilly validating schema of entries.
--
-- Once a validation is conducted for an entry, subsequent calls for the same item ID will
-- not trigger revalidation, thus the lazy part.
local function validate_item_images_by_item_ids_table_entry(entry, item_id)
    -- skip validation for already validated entries
    if validate_item_images_by_item_ids_table_entry__validated_item_ids[item_id] ~= nil then
        return
    end

    assert_value_not_nil(entry, "item images json file validation failed: no entry was found with item ID: " .. item_id)

    if type(entry) == 'table' then
        assert_value_not_nil(entry.default,
            "item images json file validation failed: expected 'default' to be defined for item: " .. item_id)
        assert(type(entry.default) == "string",
            "item images json file validation failed: expected 'default' to be a string, found '" ..
            type(entry.default) .. "' for item: " .. item_id)

        local byCondition = entry.byCondition
        assert_value_not_nil(entry.byCondition,
            "item images json file validation failed: expected 'byCondition' to be defined since 'amount' was given; item: " ..
            item_id)
        assert(type(entry.byCondition) == "table",
            "item images json file validation failed: expected 'byCondition' to be a table, found '" ..
            type(entry.byCondition) .. "' for item: " .. item_id)

        for _, byConditionEntry in ipairs(byCondition) do
            local entry_type = byConditionEntry.type
            assert_value_not_nil(entry_type,
                "item images json file validation failed: expected 'type' to be defined on one of 'byCondition' entries; item: " ..
                item_id)
            assert(type(entry_type) == "string",
                "item images json file validation failed: expected 'type' to be a string on one of 'byCondition' entries, found '" ..
                type(entry_type) .. "' for item: " .. item_id)

            if entry_type == 'amount' then
                local conditions = byConditionEntry.conditions
                assert_value_not_nil(conditions,
                    "item images json file validation failed: expected 'conditions' to be defined on one of 'byCondition' entries; item: " ..
                    item_id)
                assert(type(conditions) == "table",
                    "item images json file validation failed: expected 'conditions' to be a table on one of 'byCondition' entries, found '" ..
                    type(conditions) .. "' for item: " .. item_id)

                for _, condition in ipairs(conditions) do
                    local file = condition.file
                    assert_value_not_nil(file,
                        "item images json file validation failed: expected 'file' in one of 'conditions' entries in on one of 'byCondition' entries to be defined for item: " ..
                        item_id)

                    local conditionMin = condition.min
                    if conditionMin ~= nil then
                        assert(type(conditionMin) == "number",
                            "item images json file validation failed: expected 'min' in one of 'conditions' entries in on one of 'byCondition' entries to be a number, found '" ..
                            type(condition.min) .. "' for item: " .. item_id)
                    end
                end
            else
                error(
                    "item images json file validation failed: expected 'type' to be one of known values on one of 'byCondition' entries, but found '" ..
                    entry_type .. "' entries; item: " .. item_id)
            end
        end
    end

    validate_item_images_by_item_ids_table_entry__validated_item_ids[item_id] = true
end

-- Item compositor producing following format: `icon label [amount]`, for example:
-- - `<glass icon> glass [25]`
local function compositor_icon_label_amount_square_bracketed(item_el, icon_maybe, icon_size, label_maybe, amount_maybe, page_link_maybe)
    local icon_res
    if icon_maybe then
        icon_res = "[[File:" .. icon_maybe .. "|" .. icon_size
            .. (page_link_maybe and ("|link=" .. page_link_maybe) or "")
            .. "]]"
    end

    local label_res
    if label_maybe then
        label_res = (page_link_maybe and label_maybe ~= "") 
            and ("[[" .. page_link_maybe .. "|" .. label_maybe .. "]]") 
            or label_maybe
    end

    local amount_res
    if amount_maybe then
        amount_res = "[" .. amount_maybe .. "]" 
    end

    item_el:wikitext(
        (icon_res and icon_res or "")
        .. (label_res and label_res or "")
        .. (amount_res and ("&nbsp;" .. amount_res) or "")
    )
end

-- Item compositor producing following format: `icon amount label`, for example:
-- - `<glass icon> 25 glass`
local function compositor_icon_amount_label(item_el, icon_maybe, icon_size, label_maybe, amount_maybe, page_link_maybe)
    local icon_res
    if icon_maybe then
        icon_res = "[[File:" .. icon_maybe .. "|" .. icon_size
            .. (page_link_maybe and ("|link=" .. page_link_maybe) or "")
            .. "]]"
    end

    local label_amount_res
    if label_maybe or amount_maybe then
        local label_amount_combined
        if label_maybe and amount_maybe then
            label_amount_combined = amount_maybe .. " " .. label_maybe
        elseif label_maybe then
            label_amount_combined = label_maybe
        else -- amount
            label_amount_combined = amount_maybe
        end

        label_amount_res = (page_link_maybe and label_amount_combined ~= "")
            and ("[[" .. page_link_maybe .. "|" .. label_amount_combined .. "]]") 
            or label_amount_combined
    end

    item_el:wikitext(
            (icon_res and icon_res or "")
            .. (label_amount_res and label_amount_res or "")
        )
end

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

-- Lookups item ID by item name override (any casing).
--
-- Raises an error if no item name override was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_id_by_name_override(item_name_override, no_error)
    assert_value_not_nil(item_name_override,
        "item ID lookup by item name override failed: item name override was not provided")

    local item_name_override_lower = string.lower(item_name_override)

    local item_id = item_ids_by_item_names_override[item_name_override_lower]
    if item_id == nil then
        if no_error then
            return nil
        else
            error("item ID lookup by item name override failed: no item name override '" ..
                item_name_override_lower ..
                "' was found. Make sure that an override is defined in the item name overrides table of Module:Item")
        end
    end

    if not p.item_exists_with_id(item_id) then
        error("item ID lookup by item name override failed: item with looked up item ID '" ..
            item_id .. "' does not exist (item name override: '" ..
            item_name_override_lower ..
            "'). Make sure that the name override for this item is defined correctly in Module:Item and the item exist")
    end

    return item_id
end

-- Lookups item ID by item name `item_name` (any casing).
--
-- Raises an error if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_id_by_item_name(item_name, no_error)
    assert_value_not_nil(item_name, "item ID lookup by item name failed: item name was not provided")

    -- first, try to lookup item name in name overrides
    return p.lookup_item_id_by_name_override(item_name, true)
        -- then, look in regular item names
        or item_ids_by_item_names[string.lower(item_name)]
        or passthrough_assert_true(
            no_error,
            nil,
            "item ID lookup by item name failed: no item name was found by item ID '" ..
            item_name .. "'. Make sure that an item exist with this ID or a name override is defined"
        )
end

-- Lookups item ID by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- Raises an error if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_id(query, no_error)
    assert_value_not_nil(query, "item ID lookup failed: item ID/name (query) was not provided")

    if item_names_by_item_ids[query] ~= nil then
        return query
    else
        return p.lookup_item_id_by_item_name(query, true)
            or passthrough_assert_true(
                no_error,
                nil,
                "item ID lookup failed: no item was found by ID/name '" ..
                query .. "'. Make sure that an item exist with this ID or a name override is defined"
            )
    end
end

-- Lookups item name by item ID `item_id` (strict casing).
--
-- Raises an error if if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_name_by_item_id(item_id, no_error)
    assert_value_not_nil(item_id, "item name lookup by item ID failed: item ID was not provided")

    return item_names_by_item_ids[item_id]
        or
        passthrough_assert_true(
            no_error,
            nil,
            "item name lookup by item ID failed: no item name was found by item ID '" ..
            item_id .. "'. Make sure that an item exist with this ID or a name override is defined"
        )
end

-- Lookups item name by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- Raises an error if if no item was found.
-- Set `no_error` to `true` to return `nil` instead.
function p.lookup_item_name(query, no_error)
    assert_value_not_nil(query, "item name lookup failed: item ID/name (query) was not provided")

    local query_lower = string.lower(query)

    if p.lookup_item_id_by_name_override(query, true) ~= nil or item_ids_by_item_names[query_lower] ~= nil then
        return query
    else
        return p.lookup_item_name_by_item_id(query, true)
            or passthrough_assert_true(
                no_error,
                nil,
                "item name lookup failed: no item was found by ID/name '" ..
                query .. "'. Make sure that an item exist with this ID or a name override is defined"
            )
    end
end

-- Checks whether an item exists with name `item_name` (any casing).
function p.item_exists_with_name(item_name)
    -- query non-nil assertion is done in the subsequent function call

    return p.lookup_item_id_by_item_name(item_name, true) ~= nil
end

-- Checks whether an item exists with ID `item_id` (strict casing).
function p.item_exists_with_id(item_id)
    -- query non-nil assertion is done in the subsequent function call

    return p.lookup_item_name_by_item_id(item_id, true) ~= nil
end

-- Checks whether an item exists by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
function p.item_exists(query)
    -- query non-nil assertion is done in the subsequent function calls

    return p.item_exists_with_id(query)
        or p.item_exists_with_name(query)
end

-- Lookups item image by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- An `amount` can be specified to correctly pick an image for items
-- with multiple images (depending on the amount). By default, it has no value.
--
-- Raises an error if no item was found by `query`.
--
-- Returns `nil` if no image is defined for an item.
function p.try_lookup_item_image(query, amount)
    local item_id = p.lookup_item_id(query, true)
    assert_value_not_nil(item_id, "item image lookup failed: no item was found by ID/name '" .. query .. "'")

    local item_image = item_images_by_item_ids[item_id]
    if item_image == nil then
        return nil
    elseif type(item_image) == 'string' then
        return item_image
    end

    -- if item image "entry" was found and it's not a string,
    -- then it must be a config for multiple images.
    --
    -- send the "entry" to validation.
    validate_item_images_by_item_ids_table_entry(item_image, item_id)

    -- if validation was successful with no errors,
    -- now we can utilize the config schema without doing any checks.

    -- if no amount is specified,
    -- then there's no reason to resolve further.
    -- use the default image file for that config.
    if amount == nil then
        return item_image.default
    end

    -- if amount is specified, then resolve further
    for _, byConditionEntry in ipairs(item_image.byCondition) do
        local entry_type = byConditionEntry.type

        if entry_type == 'amount' then
            local conditions = byConditionEntry.conditions

            for _, condition in ipairs(conditions) do
                -- currently, there's a single condition - "min".
                -- it might be unset, meaning no other conditions,
                local conditionMin = condition.min
                if conditionMin == nil then
                    -- if this condition is not set, then there's no other conditions to check.
                    -- use the file from this condition
                    return condition.file
                elseif amount >= conditionMin then
                    -- if condition is set - validate it.
                    -- if it satisfies - return the file.
                    return condition.file
                end
            end
        else
            error(
                "item image lookup failed: unknown entry type '" .. entry_type .. "' for item '" .. item_id .. "'")
        end
    end

    -- if not a single condition satisfied - raise an error
    error("item image lookup failed: no condition satisfied for item '" .. item_id .. "'")
end

-- Lookups item page name by query.
-- Query can either be an item ID (strict casing) or item name (any casing).
--
-- Returns `nil` if no page is defined for an item.
function p.try_lookup_item_page(query)
    local item_id = p.lookup_item_id(query, true)
    assert_value_not_nil(item_id, "item page lookup failed: no item was found by ID/name '" .. query .. "'")

    return item_page_links_by_item_ids[item_id]
end

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

-- Generates an item element.
-- This is the main function of this module.
function p.generate_item(frame)
    local args = getArgs(frame)
    local args_with_whitespace = getArgs(frame, { trim = false, removeBlanks = false })

    -- [REQUIRED]

    -- input item name or ID.
    -- any casing is allowed for name, but ID must follow strict casing.
    local arg_item = args[1]
    assert_value_not_nil(arg_item, "failed to generate an item: item was not provided")

    -- [OPTIONAL]

    -- amount of item.
    -- input is a string, number or nil.
    -- try to convert to a number if not nil.
    -- if conversion fails, keep it as a string for lax interpretation.
    -- if it succeeds, we got a proper amount, yay.
    local arg_amount = args[2]
    local is_arg_amount_nan = false
    if arg_amount ~= nil then
        local num = tonumber(arg_amount)
        if num ~= nil then
            arg_amount = num
        else
            arg_amount = args[2]
            is_arg_amount_nan = true
        end
    end

    -- item icon size. uses CSS units.
    local arg_icon_size = args.size or "32px"

    -- text label. can be set, otherwise inferred from the item later (so "nil" for now).
    local arg_label = args_with_whitespace.label or args_with_whitespace.l

    -- whether to capitalize the label. false by default.
    local arg_capitalize_label = yesNo(args.capitalize or args.cap or false)
    
    -- whether to capitalize every word in the label. false by default.
    local arg_capitalize_all_label = yesNo(args["capitalize all"] or args["cap all"] or false)

    -- a link to a page.
    -- if set, turns item into a link.
    -- if unset, and item has a link defined for it in the config - uses it.
    local arg_link = args.link

    -- whether to apply a number formatting to the amount.
    local arg_do_format_amount = args["format amount"] or false

    -- a class to add to the item element
    local arg_extra_class = args.class or nil

    -- composition of item elements
    local arg_composition = args.composition or "icon label [amount]"

    -- ============
    -- CALCULATE INTERMEDIATE PARAMS

    local item_el = mw.html.create("span")
        :addClass("item")

    if arg_extra_class ~= nil then
        item_el:addClass(arg_extra_class)
    end

    local item_id = p.lookup_item_id(arg_item, true)
    if item_id == nil then
        item_el:addClass("unknown");
        return item_el:wikitext("unknown item '" .. arg_item .. "'")
    end
    -- assert_value_not_nil(item_id, "item generation failed: no item was found by ID/name '" .. input_item .. "'")

    -- filename of an icon
    local icon_filename = p.try_lookup_item_image(
        item_id, 
        is_arg_amount_nan and nil or arg_amount
    )

    -- page to link to
    local page_link = arg_link
        or p.try_lookup_item_page(item_id)


    local label
    if arg_label == nil then
        -- if a custom label is not provided, lookup the item's label
        label = p.lookup_item_name_by_item_id(item_id)
    else
        -- if a label is provided - use it
        label = arg_label
    end

    if arg_capitalize_label then
        label = capitalize(label)
    elseif arg_capitalize_all_label then
        label = capitalize_all(label)
    end

    local amount
    if arg_amount ~= nil then
        if is_arg_amount_nan then
            -- failed to parse as a number, it is likely an arbitrary string
            -- add it as is
            amount = arg_amount
        else
            -- valid number
            amount = (arg_do_format_amount and current_language_obj:formatNum(arg_amount) or arg_amount)
        end
    end

    -- ===============
    -- COMPOSITE PARAMS INTO THE ITEM ELEMENT
    
    if arg_composition == "icon label [amount]" then
        compositor_icon_label_amount_square_bracketed(item_el, icon_filename, arg_icon_size, label, amount, page_link)
    elseif arg_composition == "icon amount label" then
        compositor_icon_amount_label(item_el, icon_filename, arg_icon_size, label, amount, page_link)
    else
        error("unknown item composition: " .. arg_composition)
    end
    
    -- ===============

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

        was_template_styles_tag_el_added = true
    end

    return item_el
        :allDone()
end

function p.generate_list_of_all_items_with_icons(frame)
    local args = getArgs(frame)
    local columns_count = args[1]
    assert_value_not_nil(columns_count, "columns count was not provided")

    local container = mw.html.create("div")
        :css("column-count", columns_count)

    -- an array of item ids that have images
    local item_ids_with_images = {}
    for item_id, _ in pairs(item_images_by_item_ids) do
        table.insert(item_ids_with_images, item_id)
    end

    -- item IDs that were not found in the item ids table
    local non_existing_item_ids = {}

    -- sort alphabetically
    table.sort(item_ids_with_images, function(first, second)
        local first_item_name = p.lookup_item_name_by_item_id(first, true)
        local second_item_name = p.lookup_item_name_by_item_id(second, true)

        if first_item_name == nil then
            non_existing_item_ids[first] = true
            return false
        end

        if second_item_name == nil then
            non_existing_item_ids[second] = true
            return false
        end

        return first_item_name < second_item_name
    end)

    -- generate child elements from the template
    for _, item_id in ipairs(item_ids_with_images) do
        if non_existing_item_ids[item_id] ~= true then 
            local item_el = p.generate_item { item_id }
                :css("display", "block")
                :node(" <span style='color: gray;'>ID " .. item_id .. "</span>")

            container:node(item_el)
        end
    end

    return container
        :allDone()
end

-- -- Generates a list of ALL items.
-- -- Will likely break :clueless:
function p.generate_list_of_all_items(frame)
    local args = getArgs(frame)
    local columns_count = args[1]
    assert_value_not_nil(columns_count, "columns count was not provided")

    local container = mw.html.create("div")
        :css("column-count", columns_count)

    local item_ids = {}
    for item_id, _ in pairs(item_names_by_item_ids) do
        table.insert(item_ids, item_id)
    end

    -- sort alphabetically
    table.sort(item_ids, function(first, second)
        return p.lookup_item_name_by_item_id(first) < p.lookup_item_name_by_item_id(second)
    end)

    -- generate child elements from the template
    for _, item_id in ipairs(item_ids) do
        local item_el = p.generate_item { item_id }
            :css("display", "block")
            :node(" <span style='color: gray;'>ID " .. item_id .. "</span>")

        container:node(item_el)
    end

    return container
        :allDone()
end

return p