Module:Item: Difference between revisions

From Space Station 14 Wiki
(support for generating items inside the module fix #1)
(support for generating items inside the module fix #2)
Line 264: Line 264:
:addClass("item-icon")
:addClass("item-icon")


item_icon_el_container
local item_icon_el = mw.html.create() -- create an empty node
:node(frame:preprocess(("[[File:" .. icon_filename .. "|" .. input_icon_size .."]]")))
:wikitext("[[File:" .. icon_filename .. "|" .. input_icon_size .."]]") -- add image
 
item_icon_el_container:node(item_icon_el)


container_el:node(item_icon_el_container)
container_el:node(item_icon_el_container)

Revision as of 00:07, 28 August 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.

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

-- A table of items IDs mapped to their names.
local item_names_by_item_id = mw.loadJsonData("Module:Item/item names by item id.json")

-- A table of items IDs mapped to their image file names.
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)
  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

-- 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 
	-- 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 item with given ID exists.
-- The casing must match.
-- This is a cheap function.
function item_with_id_exists(item_id)
	assert_value_not_nil(item_id, "item ID was not provided")
	
	return item_names_by_item_id[item_id] ~= nil
end

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

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

-- 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.
function p.lookup_item_id_by_name_and_amount(frame)
	local args = getArgs(frame)

	local name_query = args[1]
	assert_value_not_nil(name_query, "failed to lookip item id by name and amount: item name was not provided")

	-- optional items amount
	-- 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 
	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

-- Lookups the item's image by the item's ID. 
-- The casing must match exactly.
-- Returns "nil" if no image was found.
function p.lookup_item_image_by_id(frame)
	local args = getArgs(frame)

	-- [REQUIRED]

	-- item ID to lookup
	local id_query = args[1]
	assert_value_not_nil(id_query, "failed to lookup item image by its id: item ID was not provided")

	-- [OPTIONAL]
	
	-- 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
		assert(item_with_id_exists(id_query) == true, "failed to lookup item image by its id: item with ID '" .. id_query .."' doesn't exist")
	end
	
	local match_or_nil = item_image_by_item_id[id_query]
	return match_or_nil
end

-- Lookups the item's image 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.
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

-- 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.
-- Invoked from {{Item}}.
function p.generate_item(frame)
	local args = getArgs(frame)

	-- [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]
	
	-- amount of item.
	-- input is a string number or nil (which is parsed to num), defaulting to 1
	local input_amount = tonumber(args[2]) or 1

	-- 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 = 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 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{ [1] = item_id }
	else
		label = input_label
		-- if a label is provided - use it
	end

	if input_capitalize_label then
		label = capitalize(label)
	end

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

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

	-- add icon element only if icon is provided
	if icon_filename ~= nil then
		local item_icon_el_container = mw.html.create("span")
			:addClass("item-icon")

		local item_icon_el = mw.html.create() -- create an empty node
			:wikitext("[[File:" .. icon_filename .. "|" .. input_icon_size .."]]") -- add image

		item_icon_el_container:node(item_icon_el)

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

	container_el:node(label_el)

	return container_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_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

return p