Module:AchievementSystem: Difference between revisions
Appearance
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
| Line 3: | Line 3: | ||
local Achievements = {} | local Achievements = {} | ||
-- Debug configuration | |||
local DEBUG_MODE = true | |||
local function debugLog(message) | |||
if not DEBUG_MODE then return end | |||
-- Format the message for visibility | |||
local formattedMsg = "ACHIEVEMENT-DEBUG: " .. message | |||
-- Log to MediaWiki's debug log | |||
mw.log(formattedMsg) | |||
-- Also try to log to JavaScript console if possible | |||
pcall(function() | |||
mw.logObject({debug="achievement", message=formattedMsg}) | |||
end) | |||
end | |||
-- Try to load JSON module with error handling | -- Try to load JSON module with error handling | ||
| Line 93: | Line 110: | ||
-- First, check the in-module request cache | -- First, check the in-module request cache | ||
if dataCache then | if dataCache then | ||
debugLog("Using cached achievement data") | |||
return dataCache | return dataCache | ||
end | end | ||
| Line 103: | Line 121: | ||
-- If cached data was loaded successfully, use it | -- If cached data was loaded successfully, use it | ||
if cacheSuccess and cachedData then | if cacheSuccess and cachedData then | ||
debugLog("Loaded achievement data from mw.loadData cache") | |||
dataCache = cachedData | dataCache = cachedData | ||
return dataCache | return dataCache | ||
| Line 108: | Line 127: | ||
-- Load data directly from the wiki if cache is invalid | -- Load data directly from the wiki if cache is invalid | ||
debugLog("Loading achievement data directly from " .. ACHIEVEMENT_DATA_PAGE) | |||
local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE) | local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE) | ||
local data = safeParseJSON(content) | local data = safeParseJSON(content) | ||
| Line 113: | Line 133: | ||
-- If something went wrong, use default empty data | -- If something went wrong, use default empty data | ||
if not data then | if not data then | ||
debugLog("ERROR: Failed to load achievement data - using default empty structure") | |||
data = DEFAULT_DATA | data = DEFAULT_DATA | ||
else | |||
-- Count the achievement entries for debug purposes | |||
local count = 0 | |||
for k, _ in pairs(data.user_achievements or {}) do | |||
count = count + 1 | |||
end | |||
debugLog("Loaded achievement data with " .. count .. " achievement entries") | |||
end | end | ||
| Line 124: | Line 151: | ||
if not success then | if not success then | ||
debugLog('Error in loadData: ' .. (result or 'unknown error')) | |||
return DEFAULT_DATA | return DEFAULT_DATA | ||
end | end | ||
| Line 179: | Line 206: | ||
function Achievements.getHighestAchievement(identifier) | function Achievements.getHighestAchievement(identifier) | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
if not identifier or identifier == '' then return nil end | if not identifier or identifier == '' then | ||
debugLog("getHighestAchievement: Empty identifier provided") | |||
return nil | |||
end | |||
local data = Achievements.loadData() | local data = Achievements.loadData() | ||
if not data or not data.user_achievements then | if not data or not data.user_achievements then | ||
debugLog("getHighestAchievement: No achievement data available") | |||
return nil | return nil | ||
end | end | ||
| Line 193: | Line 224: | ||
key = 'User:' .. identifier | key = 'User:' .. identifier | ||
end | end | ||
debugLog("Searching for username: " .. key) | |||
else | else | ||
-- This is a page ID, ensure it's treated as a string for table lookup | -- This is a page ID, ensure it's treated as a string for table lookup | ||
key = tostring(identifier) | key = tostring(identifier) | ||
debugLog("Searching for Page ID: " .. key) | |||
end | end | ||
local userAchievements = data.user_achievements[key] | local userAchievements = data.user_achievements[key] | ||
if not userAchievements or #userAchievements == 0 then return nil end | if not userAchievements or #userAchievements == 0 then | ||
debugLog("No achievements found for identifier: " .. key) | |||
return nil | |||
end | |||
debugLog("Found " .. #userAchievements .. " achievement(s) for identifier: " .. key) | |||
-- Find achievement with lowest tier number (highest importance) | -- Find achievement with lowest tier number (highest importance) | ||
| Line 208: | Line 246: | ||
for _, achievement in ipairs(userAchievements) do | for _, achievement in ipairs(userAchievements) do | ||
local achievementType = achievement.type | local achievementType = achievement.type | ||
debugLog("Checking achievement type: " .. achievementType) | |||
for _, typeData in ipairs(data.achievement_types or {}) do | for _, typeData in ipairs(data.achievement_types or {}) do | ||
if typeData.id == achievementType and (typeData.tier or 999) < highestTier then | if typeData.id == achievementType and (typeData.tier or 999) < highestTier then | ||
highestAchievement = typeData | highestAchievement = typeData | ||
highestTier = typeData.tier or 999 | highestTier = typeData.tier or 999 | ||
debugLog("Found higher tier achievement: " .. typeData.id .. " (tier " .. (typeData.tier or "unknown") .. ")") | |||
end | end | ||
end | end | ||
end | |||
if highestAchievement then | |||
debugLog("Highest achievement for " .. key .. ": " .. highestAchievement.id) | |||
else | |||
debugLog("No matching achievement type found in achievement_types") | |||
end | end | ||
| Line 220: | Line 266: | ||
if not success then | if not success then | ||
debugLog('Error in getHighestAchievement: ' .. (result or 'unknown error')) | |||
return nil | return nil | ||
end | end | ||
| Line 235: | Line 281: | ||
function Achievements.getTitleClass(identifier) | function Achievements.getTitleClass(identifier) | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
debugLog("getTitleClass called with identifier: " .. tostring(identifier)) | |||
local achievement = Achievements.getHighestAchievement(identifier) | local achievement = Achievements.getHighestAchievement(identifier) | ||
if not achievement or not achievement.id then return '' end | if not achievement or not achievement.id then | ||
debugLog("No achievement found, returning empty class") | |||
return '' | |||
end | |||
local className = 'achievement-' .. achievement.id | |||
debugLog("Returning CSS class: " .. className) | |||
return className | |||
end) | end) | ||
if not success then | if not success then | ||
debugLog('Error in getTitleClass: ' .. (result or 'unknown error')) | |||
return '' | return '' | ||
end | end | ||
Revision as of 23:00, 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 = {}
-- Debug configuration
local DEBUG_MODE = true
local function debugLog(message)
if not DEBUG_MODE then return end
-- Format the message for visibility
local formattedMsg = "ACHIEVEMENT-DEBUG: " .. message
-- Log to MediaWiki's debug log
mw.log(formattedMsg)
-- Also try to log to JavaScript console if possible
pcall(function()
mw.logObject({debug="achievement", message=formattedMsg})
end)
end
-- 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
debugLog("Using cached achievement data")
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
debugLog("Loaded achievement data from mw.loadData cache")
dataCache = cachedData
return dataCache
end
-- Load data directly from the wiki if cache is invalid
debugLog("Loading achievement data directly from " .. ACHIEVEMENT_DATA_PAGE)
local content = safeGetPageContent(ACHIEVEMENT_DATA_PAGE)
local data = safeParseJSON(content)
-- If something went wrong, use default empty data
if not data then
debugLog("ERROR: Failed to load achievement data - using default empty structure")
data = DEFAULT_DATA
else
-- Count the achievement entries for debug purposes
local count = 0
for k, _ in pairs(data.user_achievements or {}) do
count = count + 1
end
debugLog("Loaded achievement data with " .. count .. " achievement entries")
end
-- Update cache variables
dataCache = data
return data
end)
if not success then
debugLog('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
debugLog("getHighestAchievement: Empty identifier provided")
return nil
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
debugLog("getHighestAchievement: No achievement data available")
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
debugLog("Searching for username: " .. key)
else
-- This is a page ID, ensure it's treated as a string for table lookup
key = tostring(identifier)
debugLog("Searching for Page ID: " .. key)
end
local userAchievements = data.user_achievements[key]
if not userAchievements or #userAchievements == 0 then
debugLog("No achievements found for identifier: " .. key)
return nil
end
debugLog("Found " .. #userAchievements .. " achievement(s) for identifier: " .. key)
-- Find achievement with lowest tier number (highest importance)
local highestAchievement = nil
local highestTier = 999
for _, achievement in ipairs(userAchievements) do
local achievementType = achievement.type
debugLog("Checking achievement type: " .. achievementType)
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
debugLog("Found higher tier achievement: " .. typeData.id .. " (tier " .. (typeData.tier or "unknown") .. ")")
end
end
end
if highestAchievement then
debugLog("Highest achievement for " .. key .. ": " .. highestAchievement.id)
else
debugLog("No matching achievement type found in achievement_types")
end
return highestAchievement
end)
if not success then
debugLog('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()
debugLog("getTitleClass called with identifier: " .. tostring(identifier))
local achievement = Achievements.getHighestAchievement(identifier)
if not achievement or not achievement.id then
debugLog("No achievement found, returning empty class")
return ''
end
local className = 'achievement-' .. achievement.id
debugLog("Returning CSS class: " .. className)
return className
end)
if not success then
debugLog('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