Module:Item: Difference between revisions

From Space Station 14 Wiki
(added styles reference)
(rework)
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


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


-- A table of items IDs mapped to their names.
-- A table mapping item IDs to their names.
local item_names_by_item_id = mw.loadJsonData("Module:Item/item names by item id.json")
-- Keys are item IDs; each value is a string.
-- These names are used for labels.
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./
local item_ids_by_item_names = mw.loadJsonData("Module:Item/item ids by item names.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).
-- 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 of items IDs mapped to their image file names.
local current_frame = mw:getCurrentFrame()
local item_image_by_item_id = mw.loadJsonData("Module:Item/item image files by item id.json")


-- A config for resolving which item is returned from a name lookup when the query matches multiplte items.
-- =======================
local item_name_lookup_conflicts_resolvers = mw.loadJsonData("Module:Item/item lookup conflicts resolvers.json")


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


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


function table_has_value (tab, val)
function 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 37: Line 50:


function assert_value_not_nil(value, error_message)
function assert_value_not_nil(value, error_message)
if value == nil then
    if value == nil then
if error_message == nil then
        if error_message == nil then
error("value is nil")
            error("value is nil")
else
        else
error(error_message)
            error(error_message)
end
        end
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
end


-- Check is item with given ID has a name conflict resolver - if so, resolve the conflict using the item's ID and amount.
-- Note: The item ID provided must exist in item names table.
function resolve_item_name_conflict_if_needed(item_id, items_amount)
-- find a matching resolver config
local resolver_config = nil
for _, resolver_config_candidate in ipairs(item_name_lookup_conflicts_resolvers) do
assert_value_not_nil(resolver_config_candidate["match"])
if table_has_value(resolver_config_candidate["match"], item_id) then
resolver_config = resolver_config_candidate
break
end
end
-- if no matching config was found, then there's nothing to resolve.
-- return the item ID as-is.
if resolver_config == nil then
return item_id
end


assert_value_not_nil(resolver_config["fallbackItemId"], "failed to resolve item name conflict: 'fallbackItemId' field is empty for item '" .. item_id .."'")
-- =======================


-- check if amount is set. if it's missing, then there's no need to resolve anything
-- -- Check is item with given ID has a name conflict resolver - if so, resolve the conflict using the item's ID and amount.
-- since the only condition currnetly defined uses the amount to resolve.
-- -- Note: The item ID provided must exist in item names table.
-- so, we just use the fallback item ID.
-- local function resolve_item_name_conflict_if_needed(item_id, items_amount)
if items_amount == nil then
-- -- find a matching resolver config
return resolver_config["fallbackItemId"]
-- local resolver_config = nil
end
-- for _, resolver_config_candidate in ipairs(item_name_lookup_conflicts_resolvers) do
-- assert_value_not_nil(resolver_config_candidate["match"])
-- if table_has_value(resolver_config_candidate["match"], item_id) then
-- resolver_config = resolver_config_candidate
-- break
-- end
-- end


-- do the resolve stuff, if resolvers are defined
-- -- if no matching config was found, then there's nothing to resolve.
if resolver_config["resolvers"] ~= nil then
-- -- return the item ID as-is.
for i, resolver in ipairs(resolver_config["resolvers"]) do
-- if resolver_config == nil then
assert_value_not_nil(resolver["itemId"], "failed to resolve item name conflict: resolver #" .. i .. " doesn't have an 'itemId' defined")
-- return item_id
-- end
-- check if resolver has coniditions
-- if not, it automatically wins.
local conditions = resolver["conditions"]
if conditions == nil then
return resolver["itemId"]
end
-- if resolver has conditions - check for a single condition currently in use
local condition_min_amount = resolver["conditions"]["min"]
-- if the single conditions is defined, check against it.
if condition_min_amount ~= nil then
-- perform the check
if items_amount >= condition_min_amount then
-- if successful - the resolver wins. use its item ID
return resolver["itemId"]
end
end
end
end


-- if all resolvers are exhausted or not defined, use the fallback item ID.
-- assert_value_not_nil(resolver_config["fallbackItemId"], "failed to resolve item name conflict: 'fallbackItemId' field is empty for item '" .. item_id .."'")
return resolver_config["fallbackItemId"]
 
-- -- check if amount is set. if it's missing, then there's no need to resolve anything
-- -- since the only condition currnetly defined uses the amount to resolve.
-- -- so, we just use the fallback item ID.
-- if items_amount == nil then
-- return resolver_config["fallbackItemId"]
-- end
 
-- -- do the resolve stuff, if resolvers are defined
-- if resolver_config["resolvers"] ~= nil then
-- for i, resolver in ipairs(resolver_config["resolvers"]) do
-- assert_value_not_nil(resolver["itemId"], "failed to resolve item name conflict: resolver #" .. i .. " doesn't have an 'itemId' defined")
 
-- -- check if resolver has coniditions
-- -- if not, it automatically wins.
-- local conditions = resolver["conditions"]
-- if conditions == nil then
-- return resolver["itemId"]
-- end
 
-- -- if resolver has conditions - check for a single condition currently in use
-- local condition_min_amount = resolver["conditions"]["min"]
 
-- -- if the single conditions is defined, check against it.
-- if condition_min_amount ~= nil then
-- -- perform the check
-- if items_amount >= condition_min_amount then
-- -- if successful - the resolver wins. use its item ID
-- return resolver["itemId"]
-- end
-- end
-- end
-- end
 
-- -- if all resolvers are exhausted or not defined, use the fallback item ID.
-- return resolver_config["fallbackItemId"]
-- end
 
-- Checks whether there an item with given ID.
-- Casing must match exactly.
--
-- Raises an error if ID is `nil`.
local function item_exists_by_id(item_id)
    assert_value_not_nil(item_id, "failed to check whether an item exists by ID: ID was not provided")
 
    return item_names_by_item_ids[item_id] ~= nil
end
end


-- Checks whether item with given ID exists.
-- Checks whether there an item with given name.
-- The casing must match.
-- Name is turned lowercase before checking, so any casing is allowed.
-- This is a cheap function.
--
function item_with_id_exists(item_id)
-- Raises an error if name is `nil`.
assert_value_not_nil(item_id, "item ID was not provided")
local function item_exists_by_name(item_name)
    assert_value_not_nil(item_name, "failed to check whether an item exists by name: name was not provided")
return item_names_by_item_id[item_id] ~= nil
 
    return item_ids_by_item_names[string.lower(item_name)] ~= nil
end
end


-- Makes the first letter uppercase.
-- Checks whether there an item with given ID or name.
-- Source: https://stackoverflow.com/a/2421746
-- Casing must match exactly for an ID, but can vary for a name.
function capitalize(str)
--
     return (str:gsub("^%l", string.upper))
-- Raises an error if given ID or name is `nil`.
local function item_exists_by_id_or_name(query)
     return item_exists_by_id(query) or item_exists_by_name(query)
end
end


-- ==============================
-- Lookups item ID by name.
 
-- Name is turned lowercase before checking, so any casing is allowed.
-- Lookups the item's ID by its name (main name, ID or an alias) and an optional amount.  
--
-- Any casing for the name is allowed, except when using an ID.
-- Raises an error if name is `nil`.
function p.lookup_item_id_by_name_and_amount(frame)
--
local args = getArgs(frame)
-- Returns `nil` if there's no item matching given name.
local function lookup_item_id_by_name(item_name)
    assert_value_not_nil(item_name, "failed to lookup an item by name: name was not provided")


local name_query = args[1]
    return item_ids_by_item_names[string.lower(item_name)]
assert_value_not_nil(name_query, "failed to lookip item id by name and amount: item name was not provided")
end


-- optional items amount
-- Lookups item name by ID.
-- can be passed in as a string or a number, or not passed (nil)
-- Casing must match exactly for an ID, but can vary for a name.
-- converted to number, if it can be converted, or left as nil.
--
local items_amount = tonumber(args[2])
-- Raises an error if ID is `nil`.
--
-- Returns `nil` if there's no item matching given ID.
local function lookup_item_name_by_id(item_id)
    assert_value_not_nil(item_id, "failed to lookup an item's name by id: id was not provided")


-- check if name is an item ID in disguise
    return item_names_by_item_ids[item_id]
local match_by_id = item_names_by_item_id[name_query]
if match_by_id ~= nil then
-- if so - return the name = item ID
return name_query
end
-- otherwise look through the names for each item there is
local name_query_lower = string.lower(name_query)
for item_id, names in pairs(item_names_by_item_id) do
for _, name in ipairs(names) do
if string.lower(name) == name_query_lower then
-- check for name conflicts - resolve if needed.
return resolve_item_name_conflict_if_needed(item_id, items_amount)
end
end
end
error("No item ID found for item with name: " .. name_query)
end
end


-- Lookups the item's image by the item's ID.  
-- Lookups item ID by a query. Query can either be a name or the item ID itself.
-- The casing must match exactly.
-- Name is turned lowercase before checking, so any casing is allowed.
-- Returns "nil" if no image was found.
--
function p.lookup_item_image_by_id(frame)
-- Raises an error if query is `nil`.
local args = getArgs(frame)
--
-- Raises an error is there's no item matching query.
local function try_lookup_item_id(query)
    assert_value_not_nil(query, "failed to lookup an item by query: query was not provided")


-- [REQUIRED]
    if item_exists_by_id(query) then
        return query
    end


-- item ID to lookup
    -- check by name
local id_query = args[1]
    local match_by_name = lookup_item_id_by_name(query)
assert_value_not_nil(id_query, "failed to lookup item image by its id: item ID was not provided")
    if match_by_name then
        return match_by_name
    end


-- [OPTIONAL]
    error("item id lookup by ID or name failed: no item was found with query: " .. query)
end
-- if true, skips the assertion for item ID's existence.
-- disabled by default, enabling the assertion.
local skip_assert_id_exists = args.skip_assert_id_exists or false


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


if skip_assert_id_exists then
-- Searches for item image file using its ID.
assert(item_with_id_exists(id_query) == true, "failed to lookup item image by its id: item with ID '" .. id_query .."' doesn't exist")
-- The casing must match exactly.
end
--
-- Raises an error if item with given ID doesn't exists.
local match_or_nil = item_image_by_item_id[id_query]
--
return match_or_nil
-- Returns "nil" if item doesn't have an image defined.
end
-- TODO amount
local function lookup_item_image_by_id(item_id, amount)
    assert_value_not_nil(item_id, "failed to lookup item image by its id: item ID was not provided")
    assert(item_exists_by_id(item_id) == true,
        "failed to lookup item image by its id: item with ID '" .. item_id .. "' doesn't exist")


-- Lookups the item's image by its name (main name, ID or an alias) and an optional amount.
    return item_images_by_item_ids[item_id]
-- Any casing for the name is allowed, except when using an ID.
function p.lookup_item_image_by_name_and_amount(frame)
local item_id = p.lookup_item_id_by_name_and_amount(frame)
return p.lookup_item_image_by_id({ [1] = item_id, skip_assert_id_exists = true })
end
end


-- Lookups the item's name by its ID.
-- ==============================
-- The case must match.
function p.lookup_item_name_by_id(frame)
local args = getArgs(frame)
local id_query = args[1]
assert_value_not_nil(id_query, "item ID was not provided")
local names = item_names_by_item_id[id_query]
assert_value_not_nil(names, "Item with ID " .. id_query .. " doesn't exist")
local names_length = numeric_table_length(names)
if names_length == 0 then
error("Expected item with ID'" .. id_query .. "' to have atleast one name")
end
return names[1]
end


-- Generates an item.
-- Generates an item element.
-- Invoked from {{Item}}.
-- This is the main function of this module.
function p.generate_item(frame)
function p.generate_item(frame)
local args = getArgs(frame)
    local args = getArgs(frame)


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


-- [OPTIONAL]
    -- input item name or ID.
    -- any casing is allowed for name, but ID must follow strict casing.
-- amount of item.
    local input_item = args[1]
-- input is a string number or nil
    assert_value_not_nil(input_item, "failed to generate an item: item was not provided")
local input_amount = tonumber(args[2])


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


-- text label. can be set, otherwise inferred from the item later (so "nil" for now).
    -- amount of item.
local input_label = args.label or args.l
    -- input is a string number or nil
    local input_amount = tonumber(args[2])


-- whether to capitalize the label. false by default.
    -- item icon size. uses CSS units.
local input_capitalize_label = yesNo(args.capitalize or args.cap or false)
    local input_icon_size = args.size or "32px"


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


local item_id = p.lookup_item_id_by_name_and_amount{ [1] = input_item, [2] = input_amount }
    -- whether to capitalize the label. false by default.
    local input_capitalize_label = yesNo(args.capitalize or args.cap or false)


local icon_filename = p.lookup_item_image_by_id({ [1] = item_id, skip_assert_id_exists = true })
    -- ============


local label
    local item_id = try_lookup_item_id(input_item)
if input_label == nil then
-- if a custom label is not provided, lookup the item's label
label = p.lookup_item_name_by_id{ [1] = item_id }
else
label = input_label
-- if a label is provided - use it
end


if input_capitalize_label then
    local item_image_filename = lookup_item_image_by_id(item_id)
label = capitalize(label)
end


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


-- ============
    if input_capitalize_label then
        label = capitalize(label)
    end


local current_frame = mw:getCurrentFrame()
    if input_amount ~= nil then
        label = input_amount .. " " .. label
    end


local item_el = mw.html.create("span")
    -- ============
:addClass("item")
:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item/styles.css' }))


-- add icon element inside the label if icon is provided
    local item_el = mw.html.create("span")
if icon_filename ~= nil then
        :addClass("item")
item_el:wikitext("[[File:" .. icon_filename .. "|" .. input_icon_size .."|class=item-icon]]") -- add image
        :node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item/styles.css' }))
end
 
    -- add icon element inside the label if icon is provided
    if item_image_filename ~= nil then
        item_el:node("[[File:" .. item_image_filename .. "|" .. input_icon_size .. "|class=item-icon]]")
    end


item_el:node(label)
    item_el:node(label)


return item_el:allDone()
    return item_el:allDone()
end
end


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)


local container = mw.html.create("div")
-- container:node(current_frame:preprocess("<templatestyles src='Item/styles.css' />"))
:css("column-count", columns_count)


container:node(mw.getCurrentFrame():preprocess("<templatestyles src='Item/styles.css' />"))
-- -- an array of item ids that have images
-- local item_ids_with_images = {}
-- for item_id, _ in pairs(item_image_by_item_id) do
-- table.insert(item_ids_with_images, item_id)
-- end


-- 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_image_by_item_id) do
--     return p.lookup_item_name_by_id({ [1] = first }) < p.lookup_item_name_by_id({ [1] = second })
table.insert(item_ids_with_images, item_id)
-- end)
end
-- sort alphabetically
table.sort(item_ids_with_images, function (first, second)
    return p.lookup_item_name_by_id({ [1] = first }) < p.lookup_item_name_by_id({ [1] = second })
end)
-- generate child elements from the template
for _, item_id in ipairs(item_ids_with_images) do
local item_el = p.generate_item{ [1] = item_id }
:css("display", "block")


container:node(item_el)
-- -- generate child elements from the template
end
-- for _, item_id in ipairs(item_ids_with_images) do
-- local item_el = p.generate_item{ [1] = item_id }
return container
-- :css("display", "block")
:allDone()
 
end
-- container:node(item_el)
-- end
 
-- return container
-- :allDone()
-- end
 
-- -- Generates a list of ALL items.
-- -- Will likely break.
-- 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)
 
-- container:node(mw.getCurrentFrame():preprocess("<templatestyles src='Item/styles.css' />"))


-- Generates a list of ALL items.
-- local item_ids = {}
-- Will likely break.
-- for item_id, _ in pairs(item_names_by_item_ids) do
function p.generate_list_of_all_items(frame)
-- table.insert(item_ids, item_id)
local args = getArgs(frame)
-- end
local columns_count = args[1]
assert_value_not_nil(columns_count, "columns count was not provided")


local container = mw.html.create("div")
-- -- sort alphabetically
:css("column-count", columns_count)
-- table.sort(item_ids, function (first, second)
-- return p.lookup_item_name_by_id({ [1] = first }) < p.lookup_item_name_by_id({ [1] = second })
-- end)


container:node(mw.getCurrentFrame():preprocess("<templatestyles src='Item/styles.css' />"))
-- -- generate child elements from the template
-- for _, item_id in ipairs(item_ids) do
-- local item_el = p.generate_item{ [1] = item_id }
-- :css("display", "block")


local item_ids = {}
-- container:node(item_el)
for item_id, _ in pairs(item_names_by_item_id) do
-- end
table.insert(item_ids, item_id)
end
-- sort alphabetically
table.sort(item_ids, function (first, second)
return p.lookup_item_name_by_id({ [1] = first }) < p.lookup_item_name_by_id({ [1] = second })
end)
-- generate child elements from the template
for _, item_id in ipairs(item_ids) do
local item_el = p.generate_item{ [1] = item_id }
:css("display", "block")


container:node(item_el)
-- return container
end
-- :allDone()
-- end
return container
:allDone()
end


return p
return p

Revision as of 09:56, 3 September 2024

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_ids_by_item_lowercase_names_overrides.json.

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

Example
"Protolathe": "Research_and_Development#Protolathe"

3. Save the file.

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 try to include template styles only once if that makes sense

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.
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./
local item_ids_by_item_names = mw.loadJsonData("Module:Item/item ids by item names.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).
-- 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 current_frame = mw:getCurrentFrame()

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

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

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

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

    return false
end

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


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

-- -- Check is item with given ID has a name conflict resolver - if so, resolve the conflict using the item's ID and amount.
-- -- Note: The item ID provided must exist in item names table.
-- local function resolve_item_name_conflict_if_needed(item_id, items_amount)
-- 	-- find a matching resolver config
-- 	local resolver_config = nil
-- 	for _, resolver_config_candidate in ipairs(item_name_lookup_conflicts_resolvers) do
-- 		assert_value_not_nil(resolver_config_candidate["match"])
-- 		if table_has_value(resolver_config_candidate["match"], item_id) then
-- 			resolver_config = resolver_config_candidate
-- 			break
-- 		end
-- 	end

-- 	-- if no matching config was found, then there's nothing to resolve.
-- 	-- return the item ID as-is.
-- 	if resolver_config == nil then
-- 		return item_id
-- 	end

-- 	assert_value_not_nil(resolver_config["fallbackItemId"], "failed to resolve item name conflict: 'fallbackItemId' field is empty for item '" .. item_id .."'")

-- 	-- check if amount is set. if it's missing, then there's no need to resolve anything
-- 	-- since the only condition currnetly defined uses the amount to resolve.
-- 	-- so, we just use the fallback item ID.
-- 	if items_amount == nil then
-- 		return resolver_config["fallbackItemId"]
-- 	end

-- 	-- do the resolve stuff, if resolvers are defined
-- 	if resolver_config["resolvers"] ~= nil then		
-- 		for i, resolver in ipairs(resolver_config["resolvers"]) do
-- 			assert_value_not_nil(resolver["itemId"], "failed to resolve item name conflict: resolver #" .. i .. " doesn't have an 'itemId' defined")

-- 			-- check if resolver has coniditions
-- 			-- if not, it automatically wins.
-- 			local conditions = resolver["conditions"]
-- 			if conditions == nil then
-- 				return resolver["itemId"]
-- 			end

-- 			-- if resolver has conditions - check for a single condition currently in use
-- 			local condition_min_amount = resolver["conditions"]["min"]

-- 			-- if the single conditions is defined, check against it.
-- 			if condition_min_amount ~= nil then
-- 				-- perform the check
-- 				if items_amount >= condition_min_amount then
-- 					-- if successful - the resolver wins. use its item ID
-- 					return resolver["itemId"]
-- 				end
-- 			end
-- 		end
-- 	end

-- 	-- if all resolvers are exhausted or not defined, use the fallback item ID.
-- 	return resolver_config["fallbackItemId"]
-- end

-- Checks whether there an item with given ID.
-- Casing must match exactly.
--
-- Raises an error if ID is `nil`.
local function item_exists_by_id(item_id)
    assert_value_not_nil(item_id, "failed to check whether an item exists by ID: ID was not provided")

    return item_names_by_item_ids[item_id] ~= nil
end

-- Checks whether there an item with given name.
-- Name is turned lowercase before checking, so any casing is allowed.
--
-- Raises an error if name is `nil`.
local function item_exists_by_name(item_name)
    assert_value_not_nil(item_name, "failed to check whether an item exists by name: name was not provided")

    return item_ids_by_item_names[string.lower(item_name)] ~= nil
end

-- Checks whether there an item with given ID or name.
-- Casing must match exactly for an ID, but can vary for a name.
--
-- Raises an error if given ID or name is `nil`.
local function item_exists_by_id_or_name(query)
    return item_exists_by_id(query) or item_exists_by_name(query)
end

-- Lookups item ID by name.
-- Name is turned lowercase before checking, so any casing is allowed.
--
-- Raises an error if name is `nil`.
--
-- Returns `nil` if there's no item matching given name.
local function lookup_item_id_by_name(item_name)
    assert_value_not_nil(item_name, "failed to lookup an item by name: name was not provided")

    return item_ids_by_item_names[string.lower(item_name)]
end

-- Lookups item name by ID.
-- Casing must match exactly for an ID, but can vary for a name.
--
-- Raises an error if ID is `nil`.
--
-- Returns `nil` if there's no item matching given ID.
local function lookup_item_name_by_id(item_id)
    assert_value_not_nil(item_id, "failed to lookup an item's name by id: id was not provided")

    return item_names_by_item_ids[item_id]
end

-- Lookups item ID by a query. Query can either be a name or the item ID itself.
-- Name is turned lowercase before checking, so any casing is allowed.
--
-- Raises an error if query is `nil`.
--
-- Raises an error is there's no item matching query.
local function try_lookup_item_id(query)
    assert_value_not_nil(query, "failed to lookup an item by query: query was not provided")

    if item_exists_by_id(query) then
        return query
    end

    -- check by name
    local match_by_name = lookup_item_id_by_name(query)
    if match_by_name then
        return match_by_name
    end

    error("item id lookup by ID or name failed: no item was found with query: " .. query)
end


-- Searches for item image file using its ID.
-- The casing must match exactly.
--
-- Raises an error if item with given ID doesn't exists.
--
-- Returns "nil" if item doesn't have an image defined.
-- TODO amount
local function lookup_item_image_by_id(item_id, amount)
    assert_value_not_nil(item_id, "failed to lookup item image by its id: item ID was not provided")
    assert(item_exists_by_id(item_id) == true,
        "failed to lookup item image by its id: item with ID '" .. item_id .. "' doesn't exist")

    return item_images_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)

    -- [REQUIRED]

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

    -- [OPTIONAL]

    -- amount of item.
    -- input is a string number or nil
    local input_amount = tonumber(args[2])

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

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

    -- whether to capitalize the label. false by default.
    local input_capitalize_label = yesNo(args.capitalize or args.cap or false)

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

    local item_id = try_lookup_item_id(input_item)

    local item_image_filename = lookup_item_image_by_id(item_id)

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

    if input_capitalize_label then
        label = capitalize(label)
    end

    if input_amount ~= nil then
        label = input_amount .. " " .. label
    end

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

    local item_el = mw.html.create("span")
        :addClass("item")
        :node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item/styles.css' }))

    -- add icon element inside the label if icon is provided
    if item_image_filename ~= nil then
        item_el:node("[[File:" .. item_image_filename .. "|" .. input_icon_size .. "|class=item-icon]]")
    end

    item_el:node(label)

    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)

-- 	container:node(current_frame:preprocess("<templatestyles src='Item/styles.css' />"))

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

-- 	-- sort alphabetically
-- 	table.sort(item_ids_with_images, function (first, second)
-- 	    return p.lookup_item_name_by_id({ [1] = first }) < p.lookup_item_name_by_id({ [1] = second })
-- 	end)

-- 	-- generate child elements from the template
-- 	for _, item_id in ipairs(item_ids_with_images) do
-- 		local item_el = p.generate_item{ [1] = item_id }
-- 			:css("display", "block")

-- 		container:node(item_el)
-- 	end

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

-- -- Generates a list of ALL items.
-- -- Will likely break.
-- 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)

-- 	container:node(mw.getCurrentFrame():preprocess("<templatestyles src='Item/styles.css' />"))

-- 	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_id({ [1] = first }) < p.lookup_item_name_by_id({ [1] = second })
-- 	end)

-- 	-- generate child elements from the template
-- 	for _, item_id in ipairs(item_ids) do
-- 		local item_el = p.generate_item{ [1] = item_id }
-- 			:css("display", "block")

-- 		container:node(item_el)
-- 	end

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

return p