Module:AchievementSystem
Documentation for this module may be created at Module:AchievementSystem/doc
-- Module:AchievementSystem
-- Achievement system that loads data from MediaWiki:AchievementData.json.
-- STYLING NOTE: All achievement styling is defined in CSS/Templates.css, not in the JSON.
-- This module only assigns CSS classes based on achievement IDs in the format:
-- .person-template .template-title.achievement-{id}::after {}
--
-- The module does not use any styling information from the JSON data structure.
local Achievements = {}
-- Debug configuration
local DEBUG_MODE = true
local function debugLog(message)
if not DEBUG_MODE then return end
pcall(function()
mw.logObject({
system = "achievement_simple",
message = message,
timestamp = os.date('%H:%M:%S')
})
end)
mw.log("ACHIEVEMENT-DEBUG: " .. message)
end
-- Attempt to load JSON handling
local json
local jsonLoaded = pcall(function()
json = require('Module:JSON')
end)
if not jsonLoaded or not json then
json = { decode = function() return nil end }
debugLog('WARNING: Module:JSON not available, achievement features will be limited')
end
-- Simple HTML encode fallback
local function htmlEncode(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
-- The page storing JSON data
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
-- Cache for achievement data (within one request)
local dataCache = nil
-- Default data if JSON load fails
local DEFAULT_DATA = {
schema_version = 1,
last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
achievement_types = {},
user_achievements = {},
cache_control = { version = 0 }
}
--------------------------------------------------------------------------------
-- Internal: load achievement data from the page, or from cache
--------------------------------------------------------------------------------
function Achievements.loadData()
mw.log("JSON-DEBUG: Starting to load achievement data")
if dataCache then
mw.log("JSON-DEBUG: Using request-level cached data")
return dataCache
end
local success, data = pcall(function()
-- First try to load from parser cache
local loadDataSuccess, cachedData = pcall(function()
return mw.loadData('Module:AchievementSystem')
end)
if loadDataSuccess and cachedData then
mw.log("JSON-DEBUG: Using mw.loadData cached data")
return cachedData
else
mw.log("JSON-DEBUG: mw.loadData failed or returned empty, using direct page load")
end
local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
if not pageTitle or not pageTitle.exists then
mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
return DEFAULT_DATA
end
local content = pageTitle:getContent()
if not content or content == '' then
mw.log("JSON-DEBUG: Page content is empty")
return DEFAULT_DATA
end
mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
local parseSuccess, parsedData = pcall(function()
return json.decode(content)
end)
if not parseSuccess or not parsedData then
mw.log("JSON-DEBUG: JSON parse failed: " .. tostring(parsedData or 'unknown error'))
return DEFAULT_DATA
end
mw.log("JSON-DEBUG: Successfully loaded achievement data")
return parsedData
end)
if not success or not data then
mw.log("JSON-DEBUG: Critical error in load process: " .. tostring(data or 'unknown error'))
data = DEFAULT_DATA
end
dataCache = data
return data
end
--------------------------------------------------------------------------------
-- Checks if a user has any achievements
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
if not pageId or pageId == '' then
return false
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return false
end
local key = tostring(pageId)
if data.user_achievements[key] and #data.user_achievements[key] > 0 then
return true
end
-- Check for achievements under n-prefixed key (legacy)
if key:match("^%d+$") then
local alt = "n" .. key
if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
return true
end
end
return false
end
--------------------------------------------------------------------------------
-- Retrieves the display name for a given achievement type
--------------------------------------------------------------------------------
function Achievements.getAchievementName(achievementType)
if not achievementType or achievementType == '' then
debugLog("Empty achievement type provided to getAchievementName")
mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
return 'Unknown'
end
debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")
local data = Achievements.loadData()
if not data or not data.achievement_types then
mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data or achievement_types missing")
return achievementType
end
for i, typeData in ipairs(data.achievement_types) do
if typeData.id == achievementType then
if typeData.name and typeData.name ~= "" then
debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
return typeData.name
else
mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name")
return achievementType
end
end
end
mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type: '" .. achievementType .. "'")
return achievementType
end
--------------------------------------------------------------------------------
-- Finds the top-tier achievement for a user
-- Returns the CSS class and the achievement name
--------------------------------------------------------------------------------
function Achievements.getTitleClass(pageId)
if not pageId or pageId == '' then
debugLog("Empty page ID provided to getTitleClass")
return '', ''
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
debugLog("No achievement data available in getTitleClass")
return '', ''
end
local key = tostring(pageId)
debugLog("Looking up achievements for ID: " .. key)
local userAchievements = data.user_achievements[key] or {}
if #userAchievements == 0 and key:match("^%d+$") then
local altKey = "n" .. key
userAchievements = data.user_achievements[altKey] or {}
end
if #userAchievements == 0 then
debugLog("No achievements found for user " .. key)
return '', ''
end
local highestTier = 999
local highestAchievement = nil
for _, achievement in ipairs(userAchievements) do
local achType = achievement.type
for _, typeData in ipairs(data.achievement_types) do
if typeData.id == achType then
local tier = typeData.tier or 999
if tier < highestTier then
highestTier = tier
highestAchievement = typeData
end
end
end
end
if not highestAchievement or not highestAchievement.id then
debugLog("No valid top-tier achievement found for user " .. key)
return '', ''
end
local cssClass = "achievement-" .. highestAchievement.id
local displayName = highestAchievement.name or highestAchievement.id or "Award"
debugLog("Using top-tier achievement: " .. cssClass .. " with name: " .. displayName)
return cssClass, displayName
end
--------------------------------------------------------------------------------
-- Renders an achievement box showing the top-tier achievement
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
if not pageId or pageId == '' then
return ''
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return ''
end
local key = tostring(pageId)
local userAchievements = data.user_achievements[key]
if (not userAchievements or #userAchievements == 0) and key:match("^%d+$") then
userAchievements = data.user_achievements["n" .. key]
end
if not userAchievements or #userAchievements == 0 then
return ''
end
local highestTier = 999
local topAch = nil
for _, achievement in ipairs(userAchievements) do
local achType = achievement.type
for _, typeData in ipairs(data.achievement_types) do
if typeData.id == achType then
local tier = typeData.tier or 999
if tier < highestTier then
highestTier = tier
topAch = typeData
end
end
end
end
if topAch then
return string.format(
'<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
topAch.id,
htmlEncode(topAch.name or topAch.id or "")
)
end
return ''
end
--------------------------------------------------------------------------------
-- Marks a page for cache purging
--------------------------------------------------------------------------------
function Achievements.trackPage(pageId, pageName)
return true
end
--------------------------------------------------------------------------------
-- Retrieves a specific achievement from a user if present
--------------------------------------------------------------------------------
function Achievements.getSpecificAchievement(pageId, achievementType)
debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. tostring(achievementType) ..
"' in page ID: " .. tostring(pageId))
if not pageId or not achievementType or pageId == '' then
debugLog("ACHIEVEMENT-DEBUG: Invalid arguments for getSpecificAchievement")
return nil
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
debugLog("ACHIEVEMENT-DEBUG: No achievement data loaded")
return nil
end
local key = tostring(pageId)
local userAchievements = data.user_achievements[key] or {}
if #userAchievements == 0 and key:match("^%d+$") then
userAchievements = data.user_achievements["n" .. key] or {}
end
for _, achievement in ipairs(userAchievements) do
if achievement.type == achievementType then
return achievement
end
end
return nil
end
return Achievements