Module:AchievementSystem: Difference between revisions
Appearance
minor rev // via Wikitext Extension for VSCode |
No edit summary |
||
| Line 1: | Line 1: | ||
-- Module:AchievementSystem | -- 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 | -- 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 = {} | local Achievements = {} | ||
local json = require('Module:JSON') | |||
-- 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 | -- Constants | ||
| Line 13: | Line 35: | ||
local dataCache = nil | local dataCache = nil | ||
local cacheVersion = 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 | -- Safely attempts to get page content, returns nil on error or if page doesn't exist | ||
local function safeGetPageContent(pageName) | local function safeGetPageContent(pageName) | ||
local success, | 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 | if not success then | ||
mw.log('Error getting page content: ' .. (result or 'unknown error')) | |||
return nil | return nil | ||
end | end | ||
return result | |||
end | end | ||
| Line 33: | Line 72: | ||
local function safeParseJSON(jsonString) | local function safeParseJSON(jsonString) | ||
if not jsonString then return nil end | 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) | local success, data = pcall(function() return json.decode(jsonString) end) | ||
if not success or not data then | if not success or not data then | ||
mw.log('Error parsing JSON: ' .. (data or 'unknown error')) | |||
return nil | return nil | ||
end | end | ||
| Line 42: | Line 83: | ||
end | 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() | function Achievements.loadData() | ||
-- First, check the in-module request cache | 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) | |||
end | |||
-- If | -- If cached data was loaded successfully, use it | ||
if | if cacheSuccess and cachedData then | ||
dataCache = cachedData | dataCache = cachedData | ||
return dataCache | return dataCache | ||
end | 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 | |||
if not | mw.log('Error in loadData: ' .. (result or 'unknown error')) | ||
mw.log('Error | return DEFAULT_DATA | ||
end | end | ||
return result | |||
return | |||
end | end | ||
--[[ | --[[ | ||
Checks if a user has any achievements | Checks if a user has any achievements | ||
@param username string to check (with or without "User:" prefix) | |||
@param username string The username to check (with or without "User:" prefix) | |||
@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(username) | function Achievements.hasAchievements(username) | ||
if not username or username == '' then return false end | local success, result = pcall(function() | ||
if not username or username == '' then return false end | |||
-- Normalize username (ensure it has User: prefix) | |||
if not username:match('^User:') then | |||
username = 'User:' .. username | |||
end | |||
local data = Achievements.loadData() | |||
if not data or not data.user_achievements then | |||
return false | |||
end | |||
local userAchievements = data.user_achievements[username] | |||
return userAchievements and #userAchievements > 0 | |||
end) | |||
if not success then | |||
if not | mw.log('Error in hasAchievements: ' .. (result or 'unknown error')) | ||
return false | |||
end | end | ||
return result | |||
return | |||
end | end | ||
--[[ | --[[ | ||
Gets the highest tier achievement for a user | Gets the highest tier achievement for a user | ||
@param username string The username to check (with or without "User:" prefix) | @param username string The username to check (with or without "User:" prefix) | ||
@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(username) | function Achievements.getHighestAchievement(username) | ||
if not username or username == '' then return nil end | local success, result = pcall(function() | ||
if not username or username == '' then return nil end | |||
-- Normalize username (ensure it has User: prefix) | |||
if not username:match('^User:') then | |||
username = 'User:' .. username | |||
end | |||
local data = Achievements.loadData() | |||
if not data or not data.user_achievements then | |||
return nil | |||
end | |||
local userAchievements = data.user_achievements[username] | |||
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 | ||
end | end | ||
return highestAchievement | |||
end) | |||
if not success then | |||
mw.log('Error in getHighestAchievement: ' .. (result or 'unknown error')) | |||
return nil | |||
end | end | ||
return | return result | ||
end | end | ||
--[[ | --[[ | ||
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 username string The username to check | @param username string The 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(username) | function Achievements.getTitleClass(username) | ||
local achievement = Achievements.getHighestAchievement(username) | local success, result = pcall(function() | ||
if not | local achievement = Achievements.getHighestAchievement(username) | ||
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 | return result | ||
end | end | ||
--[[ | --[[ | ||
Gets all achievements for a user, formatted for display | Gets all achievements for a user, formatted for display | ||
@param username string The username to check | @param username string The username to check | ||
@return table Array of achievement data objects for display | @return table Array of achievement data objects for display | ||
]] | ]] | ||
function Achievements.getUserAchievements(username) | function Achievements.getUserAchievements(username) | ||
if not username or username == '' then return {} end | local success, result = pcall(function() | ||
if not username or username == '' then return {} end | |||
-- Normalize username (ensure it has User: prefix) | |||
if not username:match('^User:') then | |||
username = 'User:' .. username | |||
end | |||
local data = Achievements.loadData() | |||
if not data or not data.user_achievements then | |||
return {} | |||
end | |||
local userAchievements = data.user_achievements[username] 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 | ||
end | end | ||
for _, typeData in ipairs(data.achievement_types) do | -- Sort by tier | ||
table.sort(results, function(a, b) | |||
local tierA = 999 | |||
end | 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 | return results | ||
end) | end) | ||
if not success then | |||
mw.log('Error in getUserAchievements: ' .. (result or 'unknown error')) | |||
return {} | |||
end | |||
return result | return result | ||
| Line 223: | Line 302: | ||
--[[ | --[[ | ||
Renders HTML for an achievement box to display in templates | Renders HTML for an achievement box to display in templates | ||
@param username string The username to render achievements for | @param username string The 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(username) | function Achievements.renderAchievementBox(username) | ||
local achievements = Achievements.getUserAchievements(username) | local success, result = pcall(function() | ||
local achievements = Achievements.getUserAchievements(username) | |||
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 | end | ||
return result | |||
return | |||
end | end | ||
| Line 254: | Line 343: | ||
Note: This would ideally update the JSON with page references, but | Note: This would ideally update the JSON with page references, but | ||
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 to track | @param pageName string The page name to track | ||
@return boolean Always returns true (for future expansion) | @return boolean Always returns true (for future expansion) | ||
]] | ]] | ||
function Achievements.trackPage(pageName) | function Achievements.trackPage(pageName) | ||
-- This | -- This function is designed to be safe by default | ||
return true | return true | ||
end | end | ||
Revision as of 22:21, 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 username string The username to check (with or without "User:" prefix)
@return boolean True if the user has any achievements, false otherwise
]]
function Achievements.hasAchievements(username)
local success, result = pcall(function()
if not username or username == '' then return false end
-- Normalize username (ensure it has User: prefix)
if not username:match('^User:') then
username = 'User:' .. username
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return false
end
local userAchievements = data.user_achievements[username]
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 username string The username to check (with or without "User:" prefix)
@return table|nil The achievement type data or nil if user has no achievements
]]
function Achievements.getHighestAchievement(username)
local success, result = pcall(function()
if not username or username == '' then return nil end
-- Normalize username (ensure it has User: prefix)
if not username:match('^User:') then
username = 'User:' .. username
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return nil
end
local userAchievements = data.user_achievements[username]
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 username string The username to check
@return string The CSS class name or empty string if no achievement
]]
function Achievements.getTitleClass(username)
local success, result = pcall(function()
local achievement = Achievements.getHighestAchievement(username)
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 username string The username to check
@return table Array of achievement data objects for display
]]
function Achievements.getUserAchievements(username)
local success, result = pcall(function()
if not username or username == '' then return {} end
-- Normalize username (ensure it has User: prefix)
if not username:match('^User:') then
username = 'User:' .. username
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return {}
end
local userAchievements = data.user_achievements[username] 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 username string The username to render achievements for
@return string HTML for the achievement box or empty string if no achievements
]]
function Achievements.renderAchievementBox(username)
local success, result = pcall(function()
local achievements = Achievements.getUserAchievements(username)
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 pageName string The page name to track
@return boolean Always returns true (for future expansion)
]]
function Achievements.trackPage(pageName)
-- This function is designed to be safe by default
return true
end
-- Return the module
return Achievements