Module:Item: Difference between revisions

From Space Station 14 Wiki
(support for generating items inside the module fix #3)
(added a function for ID lookups by name overrides, which will provide proper errors on fails (instead of direct lookups which fail silently, causing a bug somewhere further down the line, requiring an hour and a half of debugging and pulling hair out RAAAAAAA); unified most lookup functions docs)
 
(37 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 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 p = {} --p stands for package
Line 5: Line 8:
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.
-- 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 of items IDs mapped to their image file names.
-- A table mapping item IDs to their image files.
local item_image_by_item_id = mw.loadJsonData("Module:Item/item image files by item id.json")
-- 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 config for resolving which item is returned from a name lookup when the query matches multiplte items.
-- A table mapping item IDs to specific pages.
local item_name_lookup_conflicts_resolvers = mw.loadJsonData("Module:Item/item lookup conflicts resolvers.json")
-- 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")


function numeric_table_length(t)
-- Get a reference to the current frame.
  local count = 0
local current_frame = mw:getCurrentFrame()
  for _ in ipairs(t) do count = count + 1 end
 
  return count
-- 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
end


function table_length(t)
local 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)
local 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 36: Line 76:
end
end


function assert_value_not_nil(value, error_message)
local 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
 
local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
    if value then
        return valueToReturnIfTrue
    else
        error(errorMessageOnFalse)
    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
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 .."'")
-- 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")


-- check if amount is set. if it's missing, then there's no need to resolve anything
    local item_name_override_lower = string.lower(item_name_override)
-- 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
    local item_id = item_ids_by_item_names_override[item_name_override_lower]
if resolver_config["resolvers"] ~= nil then
    if item_id == nil then
for i, resolver in ipairs(resolver_config["resolvers"]) do
        if no_error then
assert_value_not_nil(resolver["itemId"], "failed to resolve item name conflict: resolver #" .. i .. " doesn't have an 'itemId' defined")
            return nil
        else
-- check if resolver has coniditions
            error("item ID lookup by item name override failed: no item name override '" ..
-- if not, it automatically wins.
                item_name_override_lower ..
local conditions = resolver["conditions"]
                "' was found. Make sure that an override is defined in the item name overrides table of Module:Item")
if conditions == nil then
        end
return resolver["itemId"]
    end
end
 
    if not p.item_exists_with_id(item_id) then
-- if resolver has conditions - check for a single condition currently in use
        error("item ID lookup by item name override failed: item with looked up item ID '" ..
local condition_min_amount = resolver["conditions"]["min"]
            item_id .. "' does not exist (item name override: '" ..
            item_name_override_lower ..
-- if the single conditions is defined, check against it.
            "'). Make sure that the name override for this item is defined correctly in Module:Item and the item exist")
if condition_min_amount ~= nil then
    end
-- 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 item_id
return resolver_config["fallbackItemId"]
end
end


-- Checks whether item with given ID exists.
-- Lookups item ID by item name `item_name` (any casing).
-- The casing must match.
--
-- This is a cheap function.
-- Raises an error if no item was found.
function item_with_id_exists(item_id)
-- Set `no_error` to `true` to return `nil` instead.
assert_value_not_nil(item_id, "item ID was not provided")
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")
return item_names_by_item_id[item_id] ~= nil
 
    -- 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


-- Makes the first letter uppercase.
-- Lookups item ID by query.
-- Source: https://stackoverflow.com/a/2421746
-- Query can either be an item ID (strict casing) or item name (any casing).
function capitalize(str)
--
     return (str:gsub("^%l", string.upper))
-- 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
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")


-- Lookups the item's ID by its name (main name, ID or an alias) and an optional amount.  
    return item_names_by_item_ids[item_id]
-- Any casing for the name is allowed, except when using an ID.
        or
function p.lookup_item_id_by_name_and_amount(frame)
        passthrough_assert_true(
local args = getArgs(frame)
            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


local name_query = args[1]
-- Lookups item name by query.
assert_value_not_nil(name_query, "failed to lookip item id by name and amount: item name was not provided")
-- 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")


-- optional items amount
    local query_lower = string.lower(query)
-- can be passed in as a string or a number, or not passed (nil)
-- converted to number, if it can be converted, or left as nil.
local items_amount = tonumber(args[2])


-- check if name is an item ID in disguise
    if p.lookup_item_id_by_name_override(query, true) ~= nil or item_ids_by_item_names[query_lower] ~= nil then
local match_by_id = item_names_by_item_id[name_query]
        return query
if match_by_id ~= nil then
    else
-- if so - return the name = item ID
        return p.lookup_item_name_by_item_id(query, true)
return name_query
            or passthrough_assert_true(
end
                no_error,
                nil,
-- otherwise look through the names for each item there is
                "item name lookup failed: no item was found by ID/name '" ..
local name_query_lower = string.lower(name_query)
                query .. "'. Make sure that an item exist with this ID or a name override is defined"
for item_id, names in pairs(item_names_by_item_id) do
            )
for _, name in ipairs(names) do
    end
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.
-- Checks whether an item exists with name `item_name` (any casing).
-- The casing must match exactly.
function p.item_exists_with_name(item_name)
-- Returns "nil" if no image was found.
    -- query non-nil assertion is done in the subsequent function call
function p.lookup_item_image_by_id(frame)
local args = getArgs(frame)


-- [REQUIRED]
    return p.lookup_item_id_by_item_name(item_name, true) ~= nil
end


-- item ID to lookup
-- Checks whether an item exists with ID `item_id` (strict casing).
local id_query = args[1]
function p.item_exists_with_id(item_id)
assert_value_not_nil(id_query, "failed to lookup item image by its id: item ID was not provided")
    -- query non-nil assertion is done in the subsequent function call


-- [OPTIONAL]
    return p.lookup_item_name_by_item_id(item_id, true) ~= nil
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


-- ================
-- 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


if skip_assert_id_exists then
    return p.item_exists_with_id(query)
assert(item_with_id_exists(id_query) == true, "failed to lookup item image by its id: item with ID '" .. id_query .."' doesn't exist")
        or p.item_exists_with_name(query)
end
local match_or_nil = item_image_by_item_id[id_query]
return match_or_nil
end
end


-- Lookups the item's image by its name (main name, ID or an alias) and an optional amount.  
-- Lookups item image by query.
-- Any casing for the name is allowed, except when using an ID.
-- Query can either be an item ID (strict casing) or item name (any casing).
function p.lookup_item_image_by_name_and_amount(frame)
--
local item_id = p.lookup_item_id_by_name_and_amount(frame)
-- An `amount` can be specified to correctly pick an image for items
return p.lookup_item_image_by_id({ [1] = item_id, skip_assert_id_exists = true })
-- 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
end


-- Lookups the item's name by its ID.
-- Lookups item page name by query.
-- The case must match.
-- Query can either be an item ID (strict casing) or item name (any casing).
function p.lookup_item_name_by_id(frame)
--
local args = getArgs(frame)
-- Returns `nil` if no page is defined for an item.
local id_query = args[1]
function p.try_lookup_item_page(query)
assert_value_not_nil(id_query, "item ID was not provided")
    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 .. "'")
local names = item_names_by_item_id[id_query]
 
assert_value_not_nil(names, "Item with ID " .. id_query .. " doesn't exist")
    return item_page_links_by_item_ids[item_id]
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
end


-- Generates an item.
-- ==============================
-- Invoked from {{Item}}.
 
-- Generates an item element.
-- 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)
    local argsWithWhitespace = getArgs(frame, { trim = false, removeBlanks = false })


-- [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 (which is parsed to num), defaulting to 1
    assert_value_not_nil(input_item, "failed to generate an item: item was not provided")
local input_amount = tonumber(args[2]) or 1


-- 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 = argsWithWhitespace.label or argsWithWhitespace.l
 
    -- whether to capitalize the label. false by default.
    local input_capitalize_label = yesNo(args.capitalize or args.cap 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 input_link = args.link
 
    -- ============
 
    local item_id = p.lookup_item_id(input_item, true)
    assert_value_not_nil(item_id, "item generation failed: no item was found by ID/name '" .. input_item .. "'")
 
    local item_image_filename = p.try_lookup_item_image(item_id, input_amount)
 
    local item_page_link = input_link
        or p.try_lookup_item_page(item_id)
 
 
    local label
    if input_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 = input_label
    end
 
    if input_capitalize_label then
        label = capitalize(label)
    end
 
    if input_amount ~= nil then
        label = input_amount .. " " .. label
    end


-- ============
    if item_page_link ~= nil then
        label = "[[" .. item_page_link .. "|" .. label .. "]]"
    end


local item_id = p.lookup_item_id_by_name_and_amount{ [1] = input_item, [2] = input_amount }
    -- ============


local icon_filename = p.lookup_item_image_by_id({ [1] = item_id, skip_assert_id_exists = true })
    local item_el = mw.html.create("span")
        :addClass("item")


local label
    -- add icon element inside the label if icon is provided
if input_label == nil then
    if item_image_filename ~= nil then
-- if a custom label is not provided, lookup the item's label
        local link_param = ''
label = p.lookup_item_name_by_id{ [1] = item_id }
        if item_page_link ~= nil then
else
            link_param = '|link=' .. item_page_link
label = input_label
        end
-- if a label is provided - use it
end


if input_capitalize_label then
        item_el:node("[[File:" ..
label = capitalize(label)
            item_image_filename .. "|" .. input_icon_size .. "|class=item-icon" .. link_param .. "]]")
end
    end


-- ============
    item_el:node(label)


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


-- add icon element only if icon is provided
    if not was_template_styles_tag_el_added then
if icon_filename ~= nil then
        item_el:node(current_frame:extensionTag("templatestyles", "", { src = 'Template:Item/styles.css' }))
local item_icon_el_container = mw.html.create("span")
:addClass("item-icon")
:node(
mw.html.create() -- create an empty node
:wikitext("[[File:" .. icon_filename .. "|" .. input_icon_size .."]]") -- add image
)


container_el:node(item_icon_el_container)
        was_template_styles_tag_el_added = true
end
    end
local label_el = mw.html.create("span")
:addClass("item-label")
:node(label)


container_el:node(label_el)


return container_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)
 
    -- 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 function assert_looked_up_item_name_is_not_nil(item_id, item_name)
        assert_value_not_nil(item_name,
            "failed to generate a list of items with icons: no item was found by ID '" ..
            item_id ..
            "'. This likely indicates that the item with this ID was removed or the ID was misspelled in the item image files table")
    end
 
    -- 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)
 
        assert_looked_up_item_name_is_not_nil(first, first_item_name)
        assert_looked_up_item_name_is_not_nil(second, second_item_name)
 
        return first_item_name < second_item_name
    end)
 
    -- generate child elements from the template
    for _, item_id in ipairs(item_ids_with_images) do
        local item_el = p.generate_item { item_id }
            :css("display", "block")
 
        container:node(item_el)
    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


local container = mw.html.create("div")
    return container
:css("column-count", columns_count)
        :allDone()
-- 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
container:node(frame:preprocess("<div>{{item|" .. item_id .. "}}</div>"))
end
return container
:allDone()
end
end


return p
return p

Latest revision as of 11:22, 15 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 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")

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

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

local function passthrough_assert_true(value, valueToReturnIfTrue, errorMessageOnFalse)
    if value then
        return valueToReturnIfTrue
    else
        error(errorMessageOnFalse)
    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

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

-- 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 argsWithWhitespace = 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 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 = argsWithWhitespace.label or argsWithWhitespace.l

    -- whether to capitalize the label. false by default.
    local input_capitalize_label = yesNo(args.capitalize or args.cap 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 input_link = args.link

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

    local item_id = p.lookup_item_id(input_item, true)
    assert_value_not_nil(item_id, "item generation failed: no item was found by ID/name '" .. input_item .. "'")

    local item_image_filename = p.try_lookup_item_image(item_id, input_amount)

    local item_page_link = input_link
        or p.try_lookup_item_page(item_id)


    local label
    if input_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 = input_label
    end

    if input_capitalize_label then
        label = capitalize(label)
    end

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

    if item_page_link ~= nil then
        label = "[[" .. item_page_link .. "|" .. label .. "]]"
    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 item_page_link ~= nil then
            link_param = '|link=' .. item_page_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
        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

    local function assert_looked_up_item_name_is_not_nil(item_id, item_name)
        assert_value_not_nil(item_name,
            "failed to generate a list of items with icons: no item was found by ID '" ..
            item_id ..
            "'. This likely indicates that the item with this ID was removed or the ID was misspelled in the item image files table")
    end

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

        assert_looked_up_item_name_is_not_nil(first, first_item_name)
        assert_looked_up_item_name_is_not_nil(second, second_item_name)

        return first_item_name < second_item_name
    end)

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

        container:node(item_el)
    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