Module:AchievementSystem: Difference between revisions
Appearance
No edit summary |
// via Wikitext Extension for VSCode |
||
| Line 134: | Line 134: | ||
Checks if a user has any achievements | Checks if a user has any achievements | ||
@param | @param identifier string|number The page ID or username to check | ||
@return boolean True if the user has any achievements, false otherwise | @return boolean True if the user has any achievements, false otherwise | ||
]] | ]] | ||
function Achievements.hasAchievements( | function Achievements.hasAchievements(identifier) | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
if not | if not identifier or identifier == '' then return false end | ||
local data = Achievements.loadData() | local data = Achievements.loadData() | ||
| Line 151: | Line 146: | ||
end | end | ||
local userAchievements = data.user_achievements[ | -- Handle both page IDs and usernames for backward compatibility | ||
local key = identifier | |||
if type(identifier) == 'string' and not tonumber(identifier) then | |||
-- This is a username, normalize it | |||
if not identifier:match('^User:') then | |||
key = 'User:' .. identifier | |||
end | |||
else | |||
-- This is a page ID, ensure it's treated as a string for table lookup | |||
key = tostring(identifier) | |||
end | |||
local userAchievements = data.user_achievements[key] | |||
return userAchievements and #userAchievements > 0 | return userAchievements and #userAchievements > 0 | ||
| Line 167: | Line 174: | ||
Gets the highest tier achievement for a user | Gets the highest tier achievement for a user | ||
@param | @param identifier string|number The page ID or username to check | ||
@return table|nil The achievement type data or nil if user has no achievements | @return table|nil The achievement type data or nil if user has no achievements | ||
]] | ]] | ||
function Achievements.getHighestAchievement( | function Achievements.getHighestAchievement(identifier) | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
if not | if not identifier or identifier == '' then return nil end | ||
local data = Achievements.loadData() | local data = Achievements.loadData() | ||
| Line 184: | Line 186: | ||
end | end | ||
local userAchievements = data.user_achievements[ | -- Handle both page IDs and usernames for backward compatibility | ||
local key = identifier | |||
if type(identifier) == 'string' and not tonumber(identifier) then | |||
-- This is a username, normalize it | |||
if not identifier:match('^User:') then | |||
key = 'User:' .. identifier | |||
end | |||
else | |||
-- This is a page ID, ensure it's treated as a string for table lookup | |||
key = tostring(identifier) | |||
end | |||
local userAchievements = data.user_achievements[key] | |||
if not userAchievements or #userAchievements == 0 then return nil end | if not userAchievements or #userAchievements == 0 then return nil end | ||
| Line 216: | Line 230: | ||
Gets the CSS class for the highest achievement to be applied to the template title | Gets the CSS class for the highest achievement to be applied to the template title | ||
@param | @param identifier string|number The page ID or username to check | ||
@return string The CSS class name or empty string if no achievement | @return string The CSS class name or empty string if no achievement | ||
]] | ]] | ||
function Achievements.getTitleClass( | function Achievements.getTitleClass(identifier) | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
local achievement = Achievements.getHighestAchievement( | local achievement = Achievements.getHighestAchievement(identifier) | ||
if not achievement or not achievement.id then return '' end | if not achievement or not achievement.id then return '' end | ||
| Line 238: | Line 252: | ||
Gets all achievements for a user, formatted for display | Gets all achievements for a user, formatted for display | ||
@param | @param identifier string|number The page ID or username to check | ||
@return table Array of achievement data objects for display | @return table Array of achievement data objects for display | ||
]] | ]] | ||
function Achievements.getUserAchievements( | function Achievements.getUserAchievements(identifier) | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
if not | if not identifier or identifier == '' then return {} end | ||
local data = Achievements.loadData() | local data = Achievements.loadData() | ||
| Line 255: | Line 264: | ||
end | end | ||
local userAchievements = data.user_achievements[ | -- Handle both page IDs and usernames for backward compatibility | ||
local key = identifier | |||
if type(identifier) == 'string' and not tonumber(identifier) then | |||
-- This is a username, normalize it | |||
if not identifier:match('^User:') then | |||
key = 'User:' .. identifier | |||
end | |||
else | |||
-- This is a page ID, ensure it's treated as a string for table lookup | |||
key = tostring(identifier) | |||
end | |||
local userAchievements = data.user_achievements[key] or {} | |||
local results = {} | local results = {} | ||
| Line 304: | Line 325: | ||
Renders HTML for an achievement box to display in templates | Renders HTML for an achievement box to display in templates | ||
@param | @param identifier string|number The page ID or username to render achievements for | ||
@return string HTML for the achievement box or empty string if no achievements | @return string HTML for the achievement box or empty string if no achievements | ||
]] | ]] | ||
function Achievements.renderAchievementBox( | function Achievements.renderAchievementBox(identifier) | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
local achievements = Achievements.getUserAchievements( | local achievements = Achievements.getUserAchievements(identifier) | ||
if not achievements or #achievements == 0 then return '' end | if not achievements or #achievements == 0 then return '' end | ||
| Line 344: | Line 365: | ||
for now we rely on the cache version mechanism for invalidation | for now we rely on the cache version mechanism for invalidation | ||
@param pageName string The page name | @param pageId number|string The page ID to track | ||
@param pageName string The page name (for reference) | |||
@return boolean Always returns true (for future expansion) | @return boolean Always returns true (for future expansion) | ||
]] | ]] | ||
function Achievements.trackPage(pageName) | function Achievements.trackPage(pageId, pageName) | ||
-- This function is designed to be safe by default | -- This function is designed to be safe by default | ||
return true | return true | ||
Revision as of 22:46, 28 March 2025
Documentation for this module may be created at Module:AchievementSystem/doc
-- Module:AchievementSystem
-- Implements the ICANNWiki Achievement System. It provides functions to: 1) Load and cache achievement data from MediaWiki:AchievementData.json; 2) Retrieve achievement information for users; 3) Render achievement displays for Person templates; 4) Track pages using achievements for cache invalidation.
local Achievements = {}
-- Try to load JSON module with error handling
local json
local jsonLoaded = pcall(function()
json = require('Module:JSON')
end)
-- If JSON module failed to load, create a minimal fallback
if not jsonLoaded or not json then
json = {
decode = function() return nil end
}
mw.log('Warning: Module:JSON not available, achievement features will be limited')
end
-- Create a fallback htmlEncode if not available
local htmlEncode = function(str)
if mw.text and mw.text.htmlEncode then
return mw.text.htmlEncode(str or '')
else
return (str or ''):gsub('&', '&'):gsub('<', '<'):gsub('>', '>'):gsub('"', '"')
end
end
-- Constants
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local CACHE_VERSION_KEY = 'achievement_cache_version'
local DEFAULT_CACHE_VERSION = 0
-- Cache for achievement data (within request)
local dataCache = nil
local cacheVersion = nil
-- Default data structure to use if loading fails
local DEFAULT_DATA = {
schema_version = 1,
last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
achievement_types = {},
user_achievements = {},
cache_control = { version = DEFAULT_CACHE_VERSION }
}
-- Safely attempts to get page content, returns nil on error or if page doesn't exist
local function safeGetPageContent(pageName)
local success, result = pcall(function()
local page = mw.title.new(pageName)
if not page or not page.exists then
return nil
end
local content = page:getContent()
if not content or content == '' then
return nil
end
return content
end)
if not success then
mw.log('Error getting page content: ' .. (result or 'unknown error'))
return nil
end
return result
end
-- Safely attempts to parse JSON, returns nil on error
local function safeParseJSON(jsonString)
if not jsonString then return nil end
if not json or not json.decode then return nil end
local success, data = pcall(function() return json.decode(jsonString) end)
if not success or not data then
mw.log('Error parsing JSON: ' .. (data or 'unknown error'))
return nil
end
return data
end
--[[
Loads achievement data from MediaWiki:AchievementData.json
Uses caching to avoid repeated loading within the same page render
@return table The achievement data structure or default empty structure on failure
]]
function Achievements.loadData()
local success, result = pcall(function()
-- First, check the in-module request cache
if dataCache then
return dataCache
end
-- Try to load from mw.loadData cache (parser cache)
local cacheSuccess, cachedData = pcall(function()
return mw.loadData('Module:AchievementData')
end)
-- If cached data was loaded successfully, use it
if cacheSuccess and cachedData then
dataCache = cachedData
return dataCache
end
-- Load data directly from the wiki if cache is invalid
local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE)
local data = safeParseJSON(content)
-- If something went wrong, use default empty data
if not data then
mw.log('Error loading achievement data - using default empty structure')
data = DEFAULT_DATA
end
-- Update cache variables
dataCache = data
return data
end)
if not success then
mw.log('Error in loadData: ' .. (result or 'unknown error'))
return DEFAULT_DATA
end
return result
end
--[[
Checks if a user has any achievements
@param identifier string|number The page ID or username to check
@return boolean True if the user has any achievements, false otherwise
]]
function Achievements.hasAchievements(identifier)
local success, result = pcall(function()
if not identifier or identifier == '' then return false end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return false
end
-- Handle both page IDs and usernames for backward compatibility
local key = identifier
if type(identifier) == 'string' and not tonumber(identifier) then
-- This is a username, normalize it
if not identifier:match('^User:') then
key = 'User:' .. identifier
end
else
-- This is a page ID, ensure it's treated as a string for table lookup
key = tostring(identifier)
end
local userAchievements = data.user_achievements[key]
return userAchievements and #userAchievements > 0
end)
if not success then
mw.log('Error in hasAchievements: ' .. (result or 'unknown error'))
return false
end
return result
end
--[[
Gets the highest tier achievement for a user
@param identifier string|number The page ID or username to check
@return table|nil The achievement type data or nil if user has no achievements
]]
function Achievements.getHighestAchievement(identifier)
local success, result = pcall(function()
if not identifier or identifier == '' then return nil end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return nil
end
-- Handle both page IDs and usernames for backward compatibility
local key = identifier
if type(identifier) == 'string' and not tonumber(identifier) then
-- This is a username, normalize it
if not identifier:match('^User:') then
key = 'User:' .. identifier
end
else
-- This is a page ID, ensure it's treated as a string for table lookup
key = tostring(identifier)
end
local userAchievements = data.user_achievements[key]
if not userAchievements or #userAchievements == 0 then return nil end
-- Find achievement with lowest tier number (highest importance)
local highestAchievement = nil
local highestTier = 999
for _, achievement in ipairs(userAchievements) do
local achievementType = achievement.type
for _, typeData in ipairs(data.achievement_types or {}) do
if typeData.id == achievementType and (typeData.tier or 999) < highestTier then
highestAchievement = typeData
highestTier = typeData.tier or 999
end
end
end
return highestAchievement
end)
if not success then
mw.log('Error in getHighestAchievement: ' .. (result or 'unknown error'))
return nil
end
return result
end
--[[
Gets the CSS class for the highest achievement to be applied to the template title
@param identifier string|number The page ID or username to check
@return string The CSS class name or empty string if no achievement
]]
function Achievements.getTitleClass(identifier)
local success, result = pcall(function()
local achievement = Achievements.getHighestAchievement(identifier)
if not achievement or not achievement.id then return '' end
return 'achievement-' .. achievement.id
end)
if not success then
mw.log('Error in getTitleClass: ' .. (result or 'unknown error'))
return ''
end
return result
end
--[[
Gets all achievements for a user, formatted for display
@param identifier string|number The page ID or username to check
@return table Array of achievement data objects for display
]]
function Achievements.getUserAchievements(identifier)
local success, result = pcall(function()
if not identifier or identifier == '' then return {} end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return {}
end
-- Handle both page IDs and usernames for backward compatibility
local key = identifier
if type(identifier) == 'string' and not tonumber(identifier) then
-- This is a username, normalize it
if not identifier:match('^User:') then
key = 'User:' .. identifier
end
else
-- This is a page ID, ensure it's treated as a string for table lookup
key = tostring(identifier)
end
local userAchievements = data.user_achievements[key] or {}
local results = {}
for _, achievement in ipairs(userAchievements) do
if achievement and achievement.type then
local achievementType = achievement.type
for _, typeData in ipairs(data.achievement_types or {}) do
if typeData.id == achievementType then
table.insert(results, {
id = typeData.id,
name = typeData.name,
description = typeData.description,
icon = typeData.display and typeData.display.icon or '',
color = typeData.display and typeData.display.color or '',
background = typeData.display and typeData.display.background or '',
granted_date = achievement.granted_date
})
end
end
end
end
-- Sort by tier
table.sort(results, function(a, b)
local tierA = 999
local tierB = 999
for _, typeData in ipairs(data.achievement_types or {}) do
if typeData.id == a.id then tierA = typeData.tier or 999 end
if typeData.id == b.id then tierB = typeData.tier or 999 end
end
return tierA < tierB
end)
return results
end)
if not success then
mw.log('Error in getUserAchievements: ' .. (result or 'unknown error'))
return {}
end
return result
end
--[[
Renders HTML for an achievement box to display in templates
@param identifier string|number The page ID or username to render achievements for
@return string HTML for the achievement box or empty string if no achievements
]]
function Achievements.renderAchievementBox(identifier)
local success, result = pcall(function()
local achievements = Achievements.getUserAchievements(identifier)
if not achievements or #achievements == 0 then return '' end
local html = '<div class="achievement-box">'
html = html .. '<div class="achievement-box-title">Achievements</div>'
html = html .. '<div class="achievement-badges">'
for _, achievement in ipairs(achievements) do
html = html .. string.format(
'<div class="achievement-badge" style="color: %s; background-color: %s;" title="%s">%s %s</div>',
achievement.color or '',
achievement.background or '',
htmlEncode(achievement.description or ''),
achievement.icon or '',
htmlEncode(achievement.name or '')
)
end
html = html .. '</div></div>'
return html
end)
if not success then
mw.log('Error in renderAchievementBox: ' .. (result or 'unknown error'))
return ''
end
return result
end
--[[
Tracks a page that displays achievements for cache purging
Note: This would ideally update the JSON with page references, but
for now we rely on the cache version mechanism for invalidation
@param pageId number|string The page ID to track
@param pageName string The page name (for reference)
@return boolean Always returns true (for future expansion)
]]
function Achievements.trackPage(pageId, pageName)
-- This function is designed to be safe by default
return true
end
-- Return the module
return Achievements