Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Inventory slot

From Vault Hunters Official Wiki

This module implements {{Inventory slot}}.

Dependencies


local p = {}

-- Cache for this page load
local frameCache = {}
local aliasCache = {}
local fileExistsCache = {}

-- Internationalization data
local i18n = {
	filename = 'Invicon $1',
	legacyFilename = 'Grid $1',
	modLink = 'Mods/$1/$2',
	
	-- Dependencies
	moduleAliases = [[Module:Inventory slot/Aliases]],
	moduleModAliases = [[Module:Inventory slot/VHAliases]], -- New mod aliases module
	moduleRandom = [[Module:Random]],
	
	prefixes = {
		any = 'Any',
		matching = 'Matching',
		damaged = 'Damaged',
		unwaxed = 'Unwaxed',
	},
	
	suffixes = {
		rev = 'Revision %d+',
		be = 'BE',
		lce = 'LCE',
		sm = 'SM',
	},
}
p.i18n = i18n

-- Lazy-loaded dependencies
local random, aliases, VHAliases
local vanilla = { v = 1, vanilla = 1, mc = 1, minecraft = 1 }

-- Initialize dependencies only when needed
local function initDependencies()
	if not random then
		random = require(i18n.moduleRandom).random
	end
	if not aliases then
		aliases = mw.loadData(i18n.moduleAliases)
	end
end

-- Load mod aliases only when needed
local function getVHAliases()
	if not modAliases then
		local success, result = pcall(mw.loadData, i18n.moduleVHAliases)
		if success then
			VHAliases = result
		else
			VHAliases = {} -- Empty table if module doesn't exist
		end
	end
	return VHAliases
end

-- Optimized file existence check with caching
local function fileExists(filename)
	if fileExistsCache[filename] ~= nil then
		return fileExistsCache[filename]
	end
	
	local title = mw.title.new(filename, 'File')
	local exists = title and title.fileExists
	fileExistsCache[filename] = exists
	return exists
end

-- Fast semicolon splitting (optimized version)
local function splitOnUnenclosedSemicolons(text)
	local semicolon, lbrace, rbrace = 59, 91, 93 -- ASCII values for ;[]
	local bracketDepth = 0
	local splitStart = 1
	local frames = {}
	local frameIndex = 1
	
	for index = 1, #text do
		local byte = text:byte(index)
		if byte == semicolon and bracketDepth == 0 then
			frames[frameIndex] = text:sub(splitStart, index - 1):match("^%s*(.-)%s*$")
			frameIndex = frameIndex + 1
			splitStart = index + 1
		elseif byte == lbrace then
			bracketDepth = bracketDepth + 1
		elseif byte == rbrace then
			bracketDepth = bracketDepth - 1
		end
	end
	frames[frameIndex] = text:sub(splitStart):match("^%s*(.-)%s*$")
	
	return frames
end

-- Optimized table merging
local function mergeList(parentTable, content)
	local parentLen = #parentTable
	if content[1] then
		-- Merge list into table
		for i, v in ipairs(content) do
			parentTable[parentLen + i] = v
		end
	else
		-- Add single item
		parentTable[parentLen + 1] = content
	end
end

-- Optimized item creation with reduced expensive calls
local function makeItem(frame, args)
	local item = mw.html.create('span')
		:addClass('invslot-item')
		:addClass(args.imgclass)
		:cssText(args.imgstyle)
	
	if not frame.name or frame.name == '' then
		return item
	end
	
	-- Frame parameters
	local title = frame.title or args.title or ''
	title = title:match("^%s*(.-)%s*$") -- trim
	local mod = frame.mod
	local name = frame.name
	local num = frame.num
	local description = frame.text
	
	-- Optimized file extension detection
	local img, extension
	if mod then
		img = i18n.legacyFilename:gsub('%$1', name .. ' (' .. mod .. ')')
		extension = '.png' -- Default for legacy
	else
		local baseName = name
		local baseImg = i18n.filename:gsub('%$1', baseName)
		
		-- Check extensions in order of preference, with caching
		local extensions = {'.png', '.gif', '.webp'}
		extension = '.png' -- default
		for _, ext in ipairs(extensions) do
			if fileExists(baseImg .. ext) then
				extension = ext
				break
			end
		end
		img = baseImg
	end
	img = img .. extension
	
	-- Strip suffixes (optimized)
	local cleanName = name
	for _, suffix in pairs(i18n.suffixes) do
		cleanName = cleanName:gsub(' ' .. suffix .. '$', '')
	end
	
	-- Determine link target
	local link = args.link or ''
	if link == '' then
		if mod then
			link = i18n.modLink:gsub('%$1', mod):gsub('%$2', cleanName)
		else
			link = cleanName:gsub('^' .. i18n.prefixes.damaged .. ' ', '')
		end
	elseif link:lower() == 'none' then
		link = nil
	end
	
	-- Avoid self-links
	if link then
		local pageName = mw.title.getCurrentTitle().text
		if link:gsub('^%l', string.upper) == pageName then
			link = nil
		end
	end
	
	-- Process title and tooltips
	local formattedTitle, plainTitle
	if title == '' then
		plainTitle = cleanName
	elseif title:lower() ~= 'none' then
		plainTitle = title:gsub('\\\\', '\'):gsub('\\&', '&')
		
		-- Check for formatting codes
		if plainTitle:find('&[0-9a-jl-qs-vyzr]') or plainTitle:find('&#%x%x%x%x%x%x') or plainTitle:find('&$%x%x%x') then
			formattedTitle = title
			plainTitle = plainTitle:gsub('&[0-9a-jl-qs-vyzr]', ''):gsub('&#%x%x%x%x%x%x', ''):gsub('&$%x%x%x', '')
		end
		
		if plainTitle == '' then
			plainTitle = cleanName
		else
			plainTitle = plainTitle:gsub('\', '\\'):gsub('&', '&')
		end
	elseif link then
		formattedTitle = ''
	end
	
	-- Set minetip attributes
	if formattedTitle or description then
		item:attr('data-minetip-title', formattedTitle)
		item:attr('data-minetip-text', description)
	end
	
	-- Escape title for HTML
	local escapedTitle = (plainTitle or ''):gsub('&', '&')
	
	-- Alt text
	local altText = 'Inventory sprite for ' .. cleanName
	if link then
		altText = altText .. ' linking to ' .. link
	end
	
	-- Add the image
	item:addClass('invslot-item-image')
		:wikitext('[[File:', img, '|link=', link or '', '|alt=', altText, '|', escapedTitle, ']]')
	
	-- Add stack number
	if num and num > 1 and num < 1000 then
		local numberSpan = item:tag('span')
			:addClass('invslot-stacksize')
			:attr('title', plainTitle)
			:wikitext(num)
		
		if args.numstyle then
			numberSpan:cssText(args.numstyle)
		end
		
		if link then
			-- Wrap the number in a link
			item:wikitext('[[', link, '|'):node(numberSpan):wikitext(']]')
		end
	end
	
	return item
end

-- Optimized alias lookup with caching
local function getAliasFromCache(id, isModItem)
	local cacheKey = (isModItem and 'mod:' or 'vanilla:') .. id
	if aliasCache[cacheKey] then
		return aliasCache[cacheKey]
	end
	
	initDependencies()
	local alias
	
	if isModItem then
		alias = getVHAliases()[id]
	else
		alias = aliases[id]
	end
	
	aliasCache[cacheKey] = alias or false -- Cache negative results too
	return alias
end

-- Main slot function with caching
function p.slot(f)
	local args = f.args or f
	if f == mw.getCurrentFrame() and args[1] == nil then
		args = f:getParent().args
	end
	
	-- Prepare args and create cache key
	local frameText = ''
	if not args.parsed then
		frameText = (args[1] or ''):match("^%s*(.-)%s*$") -- trim
		args[1] = frameText
	else
		-- For parsed args, create a simple cache key from table contents
		if args[1] and type(args[1]) == 'table' then
			frameText = 'parsed_' .. tostring(#args[1])
		end
	end
	
	local cacheKey = frameText .. '|' .. (args.class or '') .. '|' .. (args.style or '') .. '|' .. (args.mod or '')
	if frameCache[cacheKey] then
		return frameCache[cacheKey]
	end
	
	-- Legacy mod support
	local modData = {
		default = args.mod ~= '' and args.mod or nil
	}
	
	-- Parse frames
	local frames
	if args.parsed then
		frames = args[1]
	elseif args[1] and args[1] ~= '' then
		local randomise = args.class == 'invslot-large' and 'never' or nil
		frames = p.parseFrameText(args[1], randomise, false, modData)
	end
	
	-- Create slot
	local body = mw.html.create('span')
		:addClass('invslot')
		:css('vertical-align', args.align)
	
	-- Handle animation
	if frames and #frames > 1 then
		body:addClass('animated')
	end
	
	-- Default background
	if args.default and args.default ~= '' then
		body:addClass('invslot-default-' .. args.default:lower():gsub(' ', '-'))
	end
	
	-- Custom styles
	if args.class then body:addClass(args.class) end
	if args.style then body:cssText(args.style) end
	
	if not frames or #frames == 0 then
		local result = tostring(body)
		frameCache[cacheKey] = result
		return result
	end
	
	-- Add frames
	local activeFrame = 1
	if frames.randomise == true then
		if not random then
			initDependencies()
		end
		activeFrame = random(#frames)
	end
	
	for i, frame in ipairs(frames) do
		local item
		if frame[1] then
			-- Subframe container
			item = body:tag('span'):addClass('animated-subframe')
			local subActiveFrame = frame.randomise == true and random(#frame) or 1
			
			for sI, sFrame in ipairs(frame) do
				local sItem = makeItem(sFrame, args)
				if sI == subActiveFrame then
					sItem:addClass('animated-active')
				end
				item:node(sItem)
			end
		else
			-- Simple frame
			item = makeItem(frame, args)
		end
		
		if i == activeFrame and #frames > 1 then
			item:addClass('animated-active')
		end
		
		body:node(item)
	end
	
	local result = tostring(body)
	frameCache[cacheKey] = result
	return result
end

-- Optimized frame parsing
function p.parseFrameText(framesText, randomise, aliasReference, modData)
	local frames = { randomise = randomise }
	local subframes = {}
	local subframe = false
	local expandedAliases = aliasReference and {} or nil
	
	local splitFrames = splitOnUnenclosedSemicolons(framesText)
	
	for i, frameText in ipairs(splitFrames) do
		-- Handle subframes
		if frameText:find('^%s*{') then
			frameText = frameText:gsub('^%s*{%s*', '')
			subframe = true
		end
		
		if subframe and frameText:find('}%s*$') then
			frameText = frameText:gsub('%s*}%s*$', '')
			subframe = 'last'
		end
		
		-- Create frame
		local frame = p.makeFrame(frameText, modData and modData.default)
		
		-- Alias processing (optimized)
		local newFrame = { frame }
		if frame.name and frame.name ~= '' then
			local isModItem = frame.mod and frame.mod ~= '' and not vanilla[frame.mod:lower()]
			local alias = getAliasFromCache(frame.name, isModItem)
			
			if alias then
				newFrame = p.getAlias(alias, frame)
				
				if aliasReference then
					local curFrame = #frames + 1
					local aliasData = { frame = frame, length = #newFrame }
					if subframe then
						if not subframes.aliasReference then
							subframes.aliasReference = {}
						end
						subframes.aliasReference[#subframes + 1] = aliasData
					else
						expandedAliases[curFrame] = aliasData
					end
				end
			end
		end
		
		-- Add frames
		if subframe then
			mergeList(subframes, newFrame)
			
			-- Handle randomization
			if frames.randomise ~= 'never' and subframes.randomise == nil and
				frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then
				subframes.randomise = true
			else
				subframes.randomise = false
			end
			
			if frames.randomise ~= 'never' then
				frames.randomise = false
			end
			
			if subframe == 'last' then
				if #subframes == 1 or (#splitFrames == i and #frames == 0) then
					local lastFrame = #frames
					mergeList(frames, subframes)
					
					if #splitFrames == 1 then
						frames.randomise = subframes.randomise
					end
					
					if aliasReference and subframes.aliasReference then
						for j, aliasRefData in pairs(subframes.aliasReference) do
							expandedAliases[lastFrame + j] = aliasRefData
						end
					end
				else
					frames[#frames + 1] = subframes
				end
				
				subframes = {}
				subframe = false
			end
		else
			if frames.randomise ~= 'never' and frame.name and frame.name:find('^' .. i18n.prefixes.any .. ' ') then
				frames.randomise = true
			else
				frames.randomise = false
			end
			
			mergeList(frames, newFrame)
		end
	end
	
	frames.aliasReference = expandedAliases
	return frames
end

-- Optimized alias expansion
function p.getAlias(aliasFrames, parentFrame)
	if type(aliasFrames) == 'string' then
		local expandedFrame = {
			name = aliasFrames,
			title = parentFrame.title,
			num = parentFrame.num,
			text = parentFrame.text,
			mod = parentFrame.mod
		}
		return { expandedFrame }
	end
	
	if aliasFrames.name then
		aliasFrames = { aliasFrames }
	end
	
	local expandedFrames = {}
	for i, aliasFrame in ipairs(aliasFrames) do
		local expandedFrame
		if type(aliasFrame) == 'string' then
			expandedFrame = { name = aliasFrame }
		else
			-- Shallow clone for performance
			expandedFrame = {}
			for k, v in pairs(aliasFrame) do
				expandedFrame[k] = v
			end
		end
		
		-- Apply parent settings (only if not already set)
		expandedFrame.title = parentFrame.title or expandedFrame.title
		expandedFrame.num = parentFrame.num or expandedFrame.num  
		expandedFrame.text = parentFrame.text or expandedFrame.text
		expandedFrame.mod = parentFrame.mod or expandedFrame.mod
		
		expandedFrames[i] = expandedFrame
	end
	
	return expandedFrames
end

-- Optimized frame creation
function p.makeFrame(frameText, defaultMod)
	-- Fast path for simple frames
	if not frameText:find('[%[:,]') then
		return {
			mod = defaultMod,
			name = frameText:match("^%s*(.-)%s*$"),
		}
	end
	
	-- Complex frame parsing
	local frame = { mod = defaultMod }
	
	-- Title
	local title, rest = frameText:match('^%s*%[([^%]]*)%]%s*(.*)')
	if title then
		frame.title = title
		frameText = rest
	end
	
	-- Text
	rest, frame.text = frameText:match('([^%]]*)%s*%[([^%]]*)%]%s*$')
	if frame.text then
		frameText = rest
	end
	
	-- Mod
	local mod, rest = frameText:match('^([^:]+):%s*(.*)')
	if mod and not vanilla[mod:lower()] then
		frame.mod = mod
		frameText = rest
	elseif mod then
		frameText = rest
	else
		frameText = frameText:gsub('^:', '')
	end
	
	-- Name and number
	local name, num = frameText:match('(.*),%s*(%d+)')
	if num then
		frame.name = name:match("^%s*(.-)%s*$")
		frame.num = math.floor(tonumber(num))
		if frame.num < 2 then
			frame.num = nil
		end
	else
		frame.name = frameText:match("^%s*(.-)%s*$")
	end
	
	return frame
end

-- Utility functions (unchanged)
function p.stringifyFrame(frame)
	if not frame.name then
		return ''
	end
	return string.format(
		'[%s]%s:%s,%s[%s]',
		frame.title or '',
		frame.mod or 'Minecraft',
		frame.name,
		frame.num or '',
		frame.text or ''
	)
end

function p.stringifyFrames(frames)
	local result = {}
	for i, frame in ipairs(frames) do
		if frame[1] then
			result[i] = '{' .. p.stringifyFrames(frame) .. '}'
		else
			result[i] = p.stringifyFrame(frame)
		end
	end
	return table.concat(result, ';')
end

return p