Module:Item

From Space Station 14 Wiki
Revision as of 05:04, 4 September 2024 by Aliser (talk | contribs) (added auto linking for items that have links, and support for "link" parameter to add/replace existing link; added extra validation for item existence into item name lookup by id; improv fns docs)
Module documentation
View or edit this documentation (about module documentation)
Lua error in mw.lua at line 800: bad argument #1 to 'mw.loadJsonData' ('Module:Item/item ids by item names override.json' is not a valid JSON page).

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:

Lua error in mw.lua at line 800: bad argument #1 to 'mw.loadJsonData' ('Module:Item/item ids by item names override.json' is not a valid JSON page).

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>" Lua error in mw.lua at line 800: bad argument #1 to 'mw.loadJsonData' ('Module:Item/item ids by item names override.json' is not a valid JSON page).

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: Lua error in mw.lua at line 800: bad argument #1 to 'mw.loadJsonData' ('Module:Item/item ids by item names override.json' is not a valid JSON page).

  • 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. Lua error in mw.lua at line 800: bad argument #1 to 'mw.loadJsonData' ('Module:Item/item ids by item names override.json' is not a valid JSON page).

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>". Lua error in mw.lua at line 800: bad argument #1 to 'mw.loadJsonData' ('Module:Item/item ids by item names override.json' is not a valid JSON page).

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
-- todo make image lookup json data validations run once per page instead of on each lookup
-- todo make `generate_list_of_all_items_with_icons` also display items with conditions

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 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 names override.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


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

-- Contains item IDs of image files json file entries that were validated.
local items_ids_of_validated_item_images_by_item_ids_entries = {}

-- Validates an entry from image files json file.
-- Validation is done once per unique entry.
local function validate_item_images_by_item_ids_entry(entry, item_id)
    -- skip validation for already validated entries
    if items_ids_of_validated_item_images_by_item_ids_entries[item_id] ~= nil then
        return
    end

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

    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 

    items_ids_of_validated_item_images_by_item_ids_entries[item_id] = true
end

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

-- Checks whether there an item with given ID.
-- Casing must match exactly.
--
-- Raises an error if ID is `nil`.
function p.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`.
function p.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")

    local item_name_lower = string.lower(item_name)

    return item_ids_by_item_names_override[item_name_lower] ~= nil and item_ids_by_item_names[item_name_lower] ~= 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`.
function p.item_exists_by_id_or_name(query)
    return p.item_exists_by_id(query) or p.item_exists_by_name(query)
end

-- Lookups item ID by name.
-- Name is turned lowercase before the lookup, so any casing is allowed.
--
-- Raises an error if name is `nil`.
--
-- Returns `nil` if there's no item matching given name.
function p.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 item_name_lower = string.lower(item_name)

    return item_ids_by_item_names_override[item_name_lower] or item_ids_by_item_names[item_name_lower]
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`.
-- 
-- Raises an error if item with given ID doesn't exist.
--
-- Returns `nil` if there's no item matching given ID.
function p.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")
    assert(p.item_exists_by_id(item_id) == true, "failed to lookup an item's name: item with ID '" .. item_id .. "' doesn't exist")

    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.
function p.try_lookup_item_id(query)
    assert_value_not_nil(query, "failed to lookup an item by query: query was not provided")

    if p.item_exists_by_id(query) then
        return query
    end

    -- check by name
    local match_by_name = p.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.
-- 
-- Has an optional amount, which can affect what image is choosen in some cases.
-- 
-- Raises an error if item ID is `nil`.
--
-- Raises an error if item with given ID doesn't exists.
--
-- Returns "nil" if item doesn't have an image defined.
-- todo fix duplicate default 
function p.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(p.item_exists_by_id(item_id) == true,
        "failed to lookup item image by its id: item with ID '" .. item_id .. "' doesn't exist")

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

    -- otherwise, a table with config for multiple textures is expected.
    -- validate that it's a table and its structure, and also the types.
    validate_item_images_by_item_ids_entry(match, item_id)

    -- if no amount is specified, use the default value.
    if amount == nil then
        return match.default
    end

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

        if entry_type == 'amount' then
            local conditions = byConditionEntry.conditions
    
            for _, condition in ipairs(conditions) do
                local conditionMin = condition.min
                if conditionMin == nil then
                    -- this check is for when there's no conditions whatsoever
                    -- if you add more condition types - this code will need to be updated.
                    -- current logic - is to just return the "file".

                    return condition.file
                elseif amount >= conditionMin then
                    return condition.file
                end
            end
        else
            error("failed to lookup item image by its id: lookup value is a table; expected 'type' to be one of known values on one of 'byCondition' entries, but found '" .. entry_type .. "' entries; item: " .. item_id)    
        end
    end

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

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

    return item_page_links_by_item_ids[item_id]
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.try_lookup_item_id(input_item)

    local item_image_filename = p.lookup_item_image_by_id(item_id, input_amount)


    if input_link == nil then
        input_link = p.lookup_item_page_by_item_id(item_id)
    end


    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_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 input_link ~= nil then
        label = "[[" .. input_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 input_link ~= nil then
            link_param = '|link=' .. input_link
        end

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

    item_el:node(label)


    if not was_template_styles_tag_el_added then
        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

	-- sort alphabetically
	table.sort(item_ids_with_images, function (first, second)
	    return p.lookup_item_name_by_id(first) < p.lookup_item_name_by_id(second)
	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_id(first) < p.lookup_item_name_by_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")

		container:node(item_el)
	end

	return container
		:allDone()
end

return p