Jump to content

Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 13: Line 13:
local function debugLog(message)
local function debugLog(message)
     if not DEBUG_MODE then return end
     if not DEBUG_MODE then return end
     pcall(function()
     -- Only use mw.log for console visibility
        mw.logObject({
     mw.log("ACHIEVEMENT: " .. message)
            system = "achievement_simple",
            message = message,
            timestamp = os.date('%H:%M:%S')
        })
    end)
     mw.log("ACHIEVEMENT-DEBUG: " .. message)
end
end


Line 26: Line 20:
-- JSON Handling
-- JSON Handling
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Helper function to ensure we get an array
local function ensureArray(value)
    if type(value) ~= "table" then
        return {}
    end
   
    -- Check if it's an array-like table
    local isArray = true
    local count = 0
    for _ in pairs(value) do
        count = count + 1
    end
   
    -- If it has no numeric indices or is empty, return empty array
    if count == 0 then
        return {}
    end
   
    -- If it's a single string, wrap it in an array
    if count == 1 and type(value[1]) == "string" then
        return {value[1]}
    end
   
    -- If it has a single non-array value, try to convert it to an array
    if count == 1 and next(value) and type(next(value)) ~= "number" then
        local k, v = next(value)
        if type(v) == "string" then
            return {v}
        end
    end
   
    -- Return the original table if it seems to be an array
    return value
end
-- We'll use MediaWiki's built-in JSON functions directly, no external module needed
-- We'll use MediaWiki's built-in JSON functions directly, no external module needed
local function jsonDecode(jsonString)
local function jsonDecode(jsonString)
     if not jsonString then  
     if not jsonString then return nil end
        debugLog('ERROR: Empty JSON string provided')
        return nil  
    end
      
      
     if mw.text and mw.text.jsonDecode then
     if mw.text and mw.text.jsonDecode then
         local success, result = pcall(function()
         local success, result = pcall(function()
            -- Use WITHOUT PRESERVE_KEYS flag to ensure proper array handling
             return mw.text.jsonDecode(jsonString)
             return mw.text.jsonDecode(jsonString)
         end)
         end)
          
          
         if success and result then
         if success and result then
             -- Validate the structure has achievement_types array
             return result
            if type(result) == 'table' and result.achievement_types then
                debugLog('SUCCESS: JSON decode successful with ' .. #result.achievement_types .. ' achievement types')
                return result
            else
                debugLog('ERROR: JSON decode succeeded but missing achievement_types array or not a table')
                if type(result) == 'table' then
                    for k, _ in pairs(result) do
                        debugLog('Found key in result: ' .. k)
                    end
                end
            end
         else
         else
             debugLog('ERROR: JSON decode failed: ' .. tostring(result or 'unknown error'))
             debugLog('ERROR: JSON decode failed: ' .. tostring(result or 'unknown error'))
Line 101: Line 117:
-- Normalizes achievement type to handle variants or legacy types
-- Normalizes achievement type to handle variants or legacy types
local function normalizeAchievementType(achievementType)
local function normalizeAchievementType(achievementType)
     if not achievementType then  
     if not achievementType then return nil end
        debugLog("Normalize called with nil achievement type")
        return nil  
    end
   
    -- Always log the normalization attempt for debugging
    debugLog("Normalizing achievement type: '" .. tostring(achievementType) .. "'")
      
      
     -- If it's already a standard type, return it directly
     -- If it's already a standard type, return it directly
Line 113: Line 123:
       achievementType == "ach1" or  
       achievementType == "ach1" or  
       achievementType == "ach2" or  
       achievementType == "ach2" or  
       achievementType == "ach3" or
       achievementType == "ach3" then
      achievementType == "sponsor-role" then
        debugLog("Achievement type already standardized: " .. achievementType)
         return achievementType
         return achievementType
     end
     end
      
      
     -- Otherwise check the mapping table
     -- Otherwise check the mapping table
     local mappedType = ACHIEVEMENT_TYPE_MAPPING[achievementType] or achievementType
     return ACHIEVEMENT_TYPE_MAPPING[achievementType] or achievementType
   
    if mappedType ~= achievementType then
        debugLog("Mapped legacy achievement type '" .. achievementType .. "' to '" .. mappedType .. "'")
    end
   
    return mappedType
end
end


Line 133: Line 135:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.loadData()
function Achievements.loadData()
     mw.log("JSON-DEBUG: Starting to load achievement data")
     debugLog("Starting to load achievement data")


     -- Use the request-level cache if we already loaded data once
     -- Use the request-level cache if we already loaded data once
     if dataCache then
     if dataCache then
         mw.log("JSON-DEBUG: Using request-level cached data")
         debugLog("Using request-level cached data")
         return dataCache
         return dataCache
     end
     end


     local data = DEFAULT_DATA
     local success, data = pcall(function()
    local jsonLoadingMethod = "none"
        -- Try using mw.loadJsonData first (preferred method)
   
        if mw.loadJsonData then
    -- SIMPLIFIED LOADING APPROACH - prioritizing mw.text.jsonDecode as it correctly processes achievement types
            debugLog("Attempting to use mw.loadJsonData for " .. ACHIEVEMENT_DATA_PAGE)
    mw.log("JSON-DEBUG: === SIMPLIFIED JSON LOADING START ===")
           
   
            local loadJsonSuccess, jsonData = pcall(function()
    -- First get the title and check if page exists
                return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
    local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
            end)
    if not pageTitle or not pageTitle.exists then
           
        mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
            if loadJsonSuccess and jsonData and type(jsonData) == 'table' then
        return DEFAULT_DATA
                debugLog("Successfully loaded data with mw.loadJsonData")
    end
                return jsonData
   
            else
    mw.log("JSON-DEBUG: Page exists with content model: " .. tostring(pageTitle.contentModel))
                debugLog("mw.loadJsonData failed: " .. tostring(jsonData or 'unknown error'))
   
            end
    -- First attempt: Use direct raw content with mw.text.jsonDecode
        else
    -- Based on diagnostic output, this is the most reliable method
            debugLog("mw.loadJsonData not available, falling back to direct content loading")
    if mw.text and mw.text.jsonDecode then
        end
        local content = nil
       
         local contentSuccess, contentResult = pcall(function()
        -- Direct content loading approach as fallback
        local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
        if not pageTitle or not pageTitle.exists then
            debugLog(ACHIEVEMENT_DATA_PAGE .. " does not exist")
            return DEFAULT_DATA
        end
       
        -- Get raw content from the wiki page
         local contentSuccess, content = pcall(function()
             return pageTitle:getContent()
             return pageTitle:getContent()
         end)
         end)
          
          
         if contentSuccess and contentResult and contentResult ~= "" then
         if contentSuccess and content and content ~= "" then
             content = contentResult
             debugLog("Successfully retrieved raw content, length: " .. #content)
            mw.log("JSON-DEBUG: Successfully retrieved raw content, length: " .. #content)
              
              
             -- Remove any BOM or leading whitespace that might cause issues
             -- Remove any BOM or leading whitespace that might cause issues
             content = content:gsub("^%s+", "")
             content = content:gsub("^%s+", "")
             if content:byte(1) == 239 and content:byte(2) == 187 and content:byte(3) == 191 then
             if content:byte(1) == 239 and content:byte(2) == 187 and content:byte(3) == 191 then
                 mw.log("JSON-DEBUG: Removing UTF-8 BOM from content")
                 debugLog("Removing UTF-8 BOM from content")
                 content = content:sub(4)
                 content = content:sub(4)
             end
             end
              
              
             local jsonDecodeSuccess, jsonData = pcall(function()
             -- Use mw.text.jsonDecode for parsing WITHOUT PRESERVE_KEYS flag
                return mw.text.jsonDecode(content)
            if mw.text and mw.text.jsonDecode then
            end)
                local jsonDecodeSuccess, jsonData = pcall(function()
           
                    return mw.text.jsonDecode(content)
            if jsonDecodeSuccess and jsonData and type(jsonData) == 'table' then
                end)
                 -- Validate the structure
                  
                 if jsonData.achievement_types and #jsonData.achievement_types > 0 and jsonData.user_achievements then
                 if jsonDecodeSuccess and jsonData then
                     mw.log("JSON-DEBUG: ✓ Successfully decoded with mw.text.jsonDecode, found " ..
                     debugLog("Successfully decoded content with mw.text.jsonDecode")
                          #jsonData.achievement_types .. " achievement types")
                     return jsonData
                      
                    -- Log achievement types
                    for i, typeData in ipairs(jsonData.achievement_types) do
                        mw.log("JSON-DEBUG: Type[" .. i .. "]: id=" .. (typeData.id or "nil") ..
                              ", name=" .. (typeData.name or "nil") ..
                              ", type=" .. (typeData.type or "nil"))
                    end
                   
                    data = jsonData
                    jsonLoadingMethod = "mw.text.jsonDecode"
                 else
                 else
                     mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode result missing required fields or empty achievement_types")
                     debugLog("mw.text.jsonDecode failed: " .. tostring(jsonData or 'unknown error'))
                    if jsonData.achievement_types then
                        mw.log("JSON-DEBUG: Found achievement_types but it contains " ..
                              #jsonData.achievement_types .. " items")
                    end
                 end
                 end
             else
             else
                 mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode failed: " ..
                 debugLog("mw.text.jsonDecode not available")
                      tostring(jsonData or 'unknown error'))
             end
             end
         else
         else
             mw.log("JSON-DEBUG: ✗ Failed to get raw content: " ..  
             debugLog("Failed to get content: " .. tostring(content or 'unknown error'))
                  tostring(contentResult or 'unknown error'))
         end
         end
    else
        mw.log("JSON-DEBUG: ✗ mw.text.jsonDecode not available")
    end
   
    -- Second attempt: Only if first attempt failed, try mw.loadJsonData
    if jsonLoadingMethod == "none" and mw.loadJsonData then
        mw.log("JSON-DEBUG: Attempting mw.loadJsonData as fallback")
          
          
         local loadJsonSuccess, jsonData = pcall(function()
         -- As absolute last resort, use local default data
            return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
         debugLog("All JSON loading approaches failed, using default data")
         end)
        return DEFAULT_DATA
       
    end)
        if loadJsonSuccess and jsonData and type(jsonData) == 'table' then
 
            -- Validate the structure
    if not success or not data then
            if jsonData.achievement_types and #jsonData.achievement_types > 0 and jsonData.user_achievements then
         debugLog("Critical error in load process: " .. tostring(data or 'unknown error'))
                mw.log("JSON-DEBUG: ✓ Successfully loaded with mw.loadJsonData, found " ..
         data = DEFAULT_DATA
                      #jsonData.achievement_types .. " achievement types")
               
                data = jsonData
                jsonLoadingMethod = "mw.loadJsonData"
            else
                mw.log("JSON-DEBUG: ✗ mw.loadJsonData result missing required fields or empty achievement_types")
                if jsonData.achievement_types then
                    mw.log("JSON-DEBUG: Found achievement_types but it contains " ..
                          #jsonData.achievement_types .. " items")
                   
                    -- This is likely the issue identified in diagnostics - achievement_types exists but is empty
                    -- We'll use the data anyway and log the issue
                    if jsonData.user_achievements then
                        mw.log("JSON-DEBUG: Using partial data from mw.loadJsonData despite empty achievement_types")
                        data = jsonData
                        jsonLoadingMethod = "mw.loadJsonData (partial)"
                    end
                end
            end
         else
            mw.log("JSON-DEBUG: ✗ mw.loadJsonData failed: " ..  
                  tostring(jsonData or 'unknown error'))
         end
     end
     end
   
 
     -- Log which method we used and validation
     -- Show success source in log
    mw.log("JSON-DEBUG: JSON loading complete using method: " .. jsonLoadingMethod)
     if data ~= DEFAULT_DATA then
     if data ~= DEFAULT_DATA then
         local achievementTypeCount = data.achievement_types and #data.achievement_types or 0
         debugLog("Successfully loaded JSON data with " .. tostring(#(data.achievement_types or {})) .. " achievement types")
        local userCount = 0
        if data.user_achievements then
            for _, _ in pairs(data.user_achievements) do
                userCount = userCount + 1
            end
        end
       
        mw.log("JSON-DEBUG: Loaded data with " .. achievementTypeCount ..
              " achievement types and " .. userCount .. " users")
       
        -- Validate achievement types have required fields
        if data.achievement_types then
            for i, typeData in ipairs(data.achievement_types) do
                if not typeData.id or not typeData.name or not typeData.type then
                    mw.log("JSON-DEBUG: WARNING: Achievement type " .. i ..
                          " missing required fields (id, name, or type)")
                end
            end
        end
    else
        mw.log("JSON-DEBUG: WARNING: Using default data due to loading failures")
     end
     end
   
 
    mw.log("JSON-DEBUG: === SIMPLIFIED JSON LOADING END ===")
   
     dataCache = data
     dataCache = data
     return data
     return data
Line 284: Line 223:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Check if a page/user has any achievements
-- Get user achievements with multiple lookup methods
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
function Achievements.getUserAchievements(pageId)
     if not pageId or pageId == '' then
     if not pageId or pageId == '' then
         return false
        debugLog("Empty page ID provided to getUserAchievements")
         return {}
     end
     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
         return false
        debugLog("No achievement data available in getUserAchievements")
         return {}
     end
     end


     local key = tostring(pageId)
     local key = tostring(pageId)
     if data.user_achievements[key] and #data.user_achievements[key] > 0 then
     debugLog("Looking up achievements for ID: " .. key)
         return true
   
    -- Try string key first
    local userAchievements = data.user_achievements[key] or {}
    if #userAchievements > 0 then
        debugLog("Found achievements using string key: " .. key)
        return ensureArray(userAchievements)
    end
   
    -- Try numeric key if string key didn't work
    local numKey = tonumber(key)
    if numKey and data.user_achievements[numKey] then
        debugLog("Found achievements using numeric key: " .. numKey)
         return ensureArray(data.user_achievements[numKey])
     end
     end
 
   
     -- Check for legacy "n123" style
     -- Try legacy "n123" style
     if key:match("^%d+$") then
     if key:match("^%d+$") then
         local alt = "n" .. key
         local alt = "n" .. key
         if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
         if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
             return true
            debugLog("Found achievements using legacy key: " .. alt)
             return ensureArray(data.user_achievements[alt])
        end
    end
   
    -- Try string comparison as last resort
    for userId, achievements in pairs(data.user_achievements) do
        if tostring(userId) == key then
            debugLog("Found achievements using string comparison with key type: " .. type(userId))
            return ensureArray(achievements)
         end
         end
     end
     end
   
    debugLog("No achievements found for user " .. key)
    return {}
end


    -- We removed the forced "true" for test pages to avoid dev-role injection
--------------------------------------------------------------------------------
     return false
-- Check if a page/user has any achievements
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
     if not pageId or pageId == '' then
        return false
    end
 
    local userAchievements = Achievements.getUserAchievements(pageId)
    return #userAchievements > 0
end
end


Line 319: Line 293:
     if not achievementType or achievementType == '' then
     if not achievementType or achievementType == '' then
         debugLog("Empty achievement type provided to getAchievementName")
         debugLog("Empty achievement type provided to getAchievementName")
        mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
         return 'Unknown'
         return 'Unknown'
     end
     end
Line 327: Line 300:
     local data = Achievements.loadData()
     local data = Achievements.loadData()
     if not data or not data.achievement_types then
     if not data or not data.achievement_types then
         mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data or achievement_types missing")
         debugLog("No achievement data or achievement_types missing")
         return achievementType
         return achievementType
     end
     end
   
 
    -- Log achievement type count to help diagnose issues
    debugLog("Found " .. #data.achievement_types .. " achievement types in data")
   
     -- Try to match achievement ID
     -- Try to match achievement ID
     for i, typeData in ipairs(data.achievement_types) do
     for _, typeData in ipairs(data.achievement_types) do
         if typeData.id == achievementType then
         if typeData.id == achievementType then
            debugLog("Found match for " .. achievementType .. " at index " .. i)
           
             if typeData.name and typeData.name ~= "" then
             if typeData.name and typeData.name ~= "" then
                 debugLog("Using name '" .. typeData.name .. "' for type '" .. achievementType .. "'")
                 debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
                 return typeData.name
                 return typeData.name
             else
             else
                 debugLog("Type " .. achievementType .. " has no name; using ID as fallback")
                 debugLog("'" .. typeData.id .. "' has no name; using ID")
                 return achievementType
                 return achievementType
             end
             end
Line 349: Line 317:
     end
     end


     -- If we reach here, no match was found - log all achievement types to help diagnose
     -- Special case for dev-role lookup
     debugLog("No match found for '" .. achievementType .. "' - logging all available types")
     if achievementType == "dev-role" then
    for i, typeData in ipairs(data.achievement_types) do
         debugLog("Could not find dev-role in achievement_types!")
         debugLog("Available type[" .. i .. "]: id=" .. (typeData.id or "nil") ..
              ", name=" .. (typeData.name or "nil") ..
              ", type=" .. (typeData.type or "nil"))
     end
     end


Line 371: Line 336:
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
     if not data or not data.user_achievements then
     if #userAchievements == 0 then
         debugLog("No achievement data available in getTitleClass")
         debugLog("No achievements found for user " .. tostring(pageId))
         return '', ''
         return '', ''
     end
     end


     local key = tostring(pageId)
     local data = Achievements.loadData()
    debugLog("Looking up achievements for page ID: " .. key)
   
    -- Try to fetch achievements for this pageId
    local userAchievements = {}
    local userAchievementKey = key
   
    -- Try the direct key first
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        debugLog("Found achievements directly under key: " .. key)
        userAchievements = data.user_achievements[key]
        userAchievementKey = key
    -- If no achievements found under normal ID, try alternative formats
    elseif key:match("^%d+$") then
        local alternateKeys = {
            "n" .. key,      -- n12345 format
            "page" .. key,    -- page12345 format
            "user" .. key    -- user12345 format
        }
       
        for _, altKey in ipairs(alternateKeys) do
            if data.user_achievements[altKey] and #data.user_achievements[altKey] > 0 then
                debugLog("Found achievements under alternate key: " .. altKey)
                userAchievements = data.user_achievements[altKey]
                userAchievementKey = altKey
                break
            end
        end
    end
 
    -- Log achievement count and details
    if #userAchievements > 0 then
        debugLog("Found " .. #userAchievements .. " achievements for page ID " .. key .. " under key " .. userAchievementKey)
        for i, ach in ipairs(userAchievements) do
            debugLog("  Achievement " .. i .. ": type=" .. (ach.type or "nil"))
        end
    else
        debugLog("No achievements found for page ID " .. key .. " under any key")
        return '', ''
    end
 
    -- Find the highest-tier achievement (lowest tier number)
     local highestTier = 999
     local highestTier = 999
     local highestAchievement = nil
     local highestAchievement = nil
Line 424: Line 348:
     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         local achType = achievement.type
         local achType = achievement.type
         if not achType then
         debugLog("Processing achievement type: " .. (achType or "nil"))
            debugLog("Achievement missing type property - skipping")
       
        else
        for _, typeData in ipairs(data.achievement_types) do
            debugLog("Processing achievement type: " .. achType)
            if typeData.id == achType then
           
                 local tier = typeData.tier or 999
            -- Find the achievement definition for this type
                 debugLog("  Found type '" .. typeData.id .. "' with tier " .. tier .. " and name '" .. (typeData.name or "nil") .. "'")
            local achDef = nil
            for _, typeData in ipairs(data.achievement_types) do
                if typeData.id == achType then
                    achDef = typeData
                    break
                end
            end
           
            if not achDef then
                debugLog("No definition found for achievement type: " .. achType)
            else
                 local tier = achDef.tier or 999
                 debugLog("  Found type '" .. achDef.id .. "' with tier " .. tier ..  
                      ", name '" .. (achDef.name or "nil") ..  
                      "', type '" .. (achDef.type or "nil") .. "'")
               
                 if tier < highestTier then
                 if tier < highestTier then
                     highestTier = tier
                     highestTier = tier
                     highestAchievement = achDef
                     highestAchievement = typeData
                     debugLog("  New highest tier achievement: " .. achDef.id)
                     debugLog("  New highest tier achievement: " .. typeData.id)
                 end
                 end
             end
             end
Line 456: Line 364:


     if not highestAchievement or not highestAchievement.id then
     if not highestAchievement or not highestAchievement.id then
         debugLog("No valid top-tier achievement found for page ID " .. key)
         debugLog("No valid top-tier achievement found for user " .. tostring(pageId))
         return '', ''
         return '', ''
     end
     end
Line 476: Line 384:
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
    if not data or not data.user_achievements then
     if #userAchievements == 0 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 ''
         return ''
     end
     end
   
    local data = Achievements.loadData()
      
      
     -- Build a lookup table for achievement type definitions
     -- Build a lookup table for achievement type definitions
Line 541: Line 441:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.getSpecificAchievement(pageId, achievementType)
function Achievements.getSpecificAchievement(pageId, achievementType)
     debugLog("ACHIEVEMENT-DEBUG: Looking for '" .. tostring(achievementType) ..
     debugLog("Looking for achievement '" .. tostring(achievementType) ..
             "' in page ID: " .. tostring(pageId))
             "' in page ID: " .. tostring(pageId))


     if not pageId or not achievementType or pageId == '' then
     if not pageId or not achievementType or pageId == '' then
         debugLog("ACHIEVEMENT-DEBUG: Invalid arguments for getSpecificAchievement")
         debugLog("Invalid arguments for getSpecificAchievement")
         return nil
         return nil
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
    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 no achievements found under normal ID, try alternative format
    if #userAchievements == 0 and key:match("^%d+$") then
        local altKey = "n" .. key
        debugLog("No achievements under ID '" .. key .. "', trying alternative ID: '" .. altKey .. "'")
        userAchievements = data.user_achievements[altKey] or {}
    end
      
      
     -- Direct lookup for the requested achievement type
     -- Direct lookup for the requested achievement type
     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         if achievement.type == achievementType then
         if achievement.type == achievementType then
             debugLog("FOUND ACHIEVEMENT: " .. achievementType .. " for user " .. key)
             debugLog("Found achievement: " .. achievementType .. " for user " .. tostring(pageId))
             return achievement
             return achievement
         end
         end
     end
     end


     debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
     debugLog("No match found for achievement type: " .. achievementType)
     return nil
     return nil
end
end
Line 605: Line 491:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Diagnostic Function: List all badges for a page with details
-- Diagnostic Function: Log badges for a page to console
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.debugBadgesForPage(pageId)
function Achievements.debugBadgesForPage(pageId)
     if not pageId or pageId == '' then
     if not pageId or pageId == '' then
         mw.log("BADGE-DEBUG: Empty page ID")
         mw.log("ACHIEVEMENT-BADGES: No page ID provided")
         return "ERROR: No page ID provided"
         return "ERROR: No page ID provided"
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
    if not data then
        mw.log("BADGE-DEBUG: Failed to load achievement data")
        return "ERROR: Failed to load achievement data"
    end
 
    local key = tostring(pageId)
    local userAchievements = data.user_achievements[key] or {}
   
    -- Check alternate keys if needed
    if #userAchievements == 0 and key:match("^%d+$") then
        local altKey = "n" .. key
        userAchievements = data.user_achievements[altKey] or {}
        if #userAchievements > 0 then
            mw.log("BADGE-DEBUG: Found achievements under alternate key: " .. altKey)
        end
    end
 
     if #userAchievements == 0 then
     if #userAchievements == 0 then
         mw.log("BADGE-DEBUG: No achievements found for page ID " .. pageId)
         mw.log("ACHIEVEMENT-BADGES: No achievements found for page ID " .. pageId)
         return "No achievements found for page ID " .. pageId
         return "No achievements found for page ID " .. pageId
     end
     end


     -- Build debug report
     mw.log("ACHIEVEMENT-BADGES: Found " .. #userAchievements .. " achievements for page ID " .. pageId)
    local output = {}
    table.insert(output, "=== BADGE DEBUG FOR PAGE " .. pageId .. " ===")
    table.insert(output, "Found " .. #userAchievements .. " achievements")
      
      
     -- Check each achievement in detail
     -- Log each achievement to console
     for i, achievement in ipairs(userAchievements) do
     for i, achievement in ipairs(userAchievements) do
         local achType = achievement.type or "nil"
         local achType = achievement.type or "nil"
         local typeDef = Achievements.getAchievementDefinition(achType)
         local typeDef = Achievements.getAchievementDefinition(achType)
       
        table.insert(output, "\nACHIEVEMENT " .. i .. ":")
        table.insert(output, "  Type: " .. achType)
          
          
         if typeDef then
         if typeDef then
             table.insert(output, " Name: " .. (typeDef.name or "unnamed"))
             mw.log("ACHIEVEMENT-BADGES: [" .. i .. "] " .. achType ..
            table.insert(output, "  Definition Type: " .. (typeDef.type or "unspecified"))
                  " (Name: " .. (typeDef.name or "unnamed") ..
            table.insert(output, "  Description: " .. (typeDef.description or "none"))
                  ", Type: " .. (typeDef.type or "unspecified") ..
            if typeDef.tier then
                  ", Tier: " .. (typeDef.tier or "none") .. ")")
                table.insert(output, " Tier: " .. typeDef.tier)
            end
         else
         else
             table.insert(output, " WARNING: No type definition found!")
             mw.log("ACHIEVEMENT-BADGES: [" .. i .. "] " .. achType .. " (WARNING: No definition found)")
         end
         end
     end
     end
      
      
     return table.concat(output, "\n")
     return "Found " .. #userAchievements .. " achievements for page ID " .. pageId
end
end


Line 675: Line 536:
     end
     end


     local data = Achievements.loadData()
     local userAchievements = Achievements.getUserAchievements(pageId)
     if not data or not data.user_achievements then
     if #userAchievements == 0 then
         debugLog("No achievement data available in getTitleAchievement")
         debugLog("No achievements found for user " .. tostring(pageId))
         return '', '', ''
         return '', '', ''
     end
     end


     local key = tostring(pageId)
     local data = Achievements.loadData()
     debugLog("Looking up title achievements for page ID: " .. key)
     debugLog("Looking up title achievements for ID: " .. tostring(pageId))
   
    -- Try to fetch achievements for this pageId, checking multiple possible key formats
    local userAchievements = {}
    local userAchievementKey = key
      
      
    -- Try the direct key first
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        debugLog("Found achievements directly under key: " .. key)
        userAchievements = data.user_achievements[key]
        userAchievementKey = key
    -- If no achievements found under normal ID, try alternative formats
    elseif key:match("^%d+$") then
        local alternateKeys = {
            "n" .. key,      -- n12345 format
            "page" .. key,    -- page12345 format
            "user" .. key    -- user12345 format
        }
       
        for _, altKey in ipairs(alternateKeys) do
            if data.user_achievements[altKey] and #data.user_achievements[altKey] > 0 then
                debugLog("Found achievements under alternate key: " .. altKey)
                userAchievements = data.user_achievements[altKey]
                userAchievementKey = altKey
                break
            end
        end
    end
    -- Log achievement count and details for all pages consistently
    if #userAchievements > 0 then
        debugLog("Found " .. #userAchievements .. " achievements for page ID " .. key .. " under key " .. userAchievementKey)
        for i, ach in ipairs(userAchievements) do
            debugLog("  Achievement " .. i .. ": type=" .. (ach.type or "nil"))
        end
    else
        debugLog("No achievements found for page ID " .. key .. " under any key")
        return '', '', ''
    end
     -- Build a table of achievement definitions for quick lookup
     -- Build a table of achievement definitions for quick lookup
     local typeDefinitions = {}
     local typeDefinitions = {}
     for _, typeData in ipairs(data.achievement_types) do
     for _, typeData in ipairs(data.achievement_types) do
         typeDefinitions[typeData.id] = typeData
         typeDefinitions[typeData.id] = typeData
        debugLog("Loaded definition for type '" .. typeData.id ..
                "': type=" .. (typeData.type or "nil") ..
                ", name=" .. (typeData.name or "nil"))
     end
     end


     -- Find title achievements only by strictly checking the "type" field
     -- Find title achievements only
     local highestTier = 999
     local highestTier = 999
     local titleAchievement = nil
     local titleAchievement = nil
Line 737: Line 557:
     for i, achievement in ipairs(userAchievements) do
     for i, achievement in ipairs(userAchievements) do
         local achType = achievement.type
         local achType = achievement.type
         if not achType then
         if achType then
            debugLog("Achievement " .. i .. " missing type property - skipping")
        else
             local typeData = typeDefinitions[achType]
             local typeData = typeDefinitions[achType]
             if not typeData then
             if typeData then
                debugLog("No definition found for achievement type: " .. achType)
                 -- Check if it's a title type by examining "type" property
            else
                 -- Check if it's a title type ONLY by examining the "type" property
                 local achDefType = typeData.type
                 local achDefType = typeData.type
                debugLog("Checking achievement " .. i .. ": id=" .. achType ..
                      ", definition type=" .. (achDefType or "nil"))
                  
                  
                 -- Only consider achievements with type="title"
                 -- Only consider achievements with type="title"
                 if achDefType == "title" then
                 if achDefType == "title" then
                     debugLog("Found title achievement: " .. achType)
                     debugLog("Found potential title achievement: " .. achType)
                     local tier = typeData.tier or 999
                     local tier = typeData.tier or 999
                     if tier < highestTier then
                     if tier < highestTier then
                         highestTier = tier
                         highestTier = tier
                         titleAchievement = typeData
                         titleAchievement = typeData
                         debugLog("Using as highest tier title: " .. typeData.id .. " (tier " .. tier .. ")")
                         debugLog("Using as highest tier: " .. typeData.id)
                     end
                     end
                else
                    debugLog("Skipping non-title achievement: " .. achType .. " (type=" .. (achDefType or "nil") .. ")")
                 end
                 end
             end
             end
Line 766: Line 578:


     if not titleAchievement or not titleAchievement.id then
     if not titleAchievement or not titleAchievement.id then
         debugLog("No valid title achievement found for page ID " .. key)
         debugLog("No valid title achievement found for user " .. tostring(pageId))
         return '', '', ''
         return '', '', ''
     end
     end
Line 780: Line 592:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Diagnostic function that can be directly called to troubleshoot JSON loading
-- Simplified diagnostic function for JSON loading issues
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.diagnoseJsonLoading()
function Achievements.diagnoseJsonLoading()
     local output = {}
     mw.log("ACHIEVEMENT-DIAG: Starting JSON diagnostics")
    table.insert(output, "===== ACHIEVEMENT SYSTEM JSON DIAGNOSTICS =====")
      
      
     -- Check MediaWiki version and capabilities
     -- Check MediaWiki capabilities
    table.insert(output, "\n== MEDIAWIKI CAPABILITIES ==")
     if not mw.loadJsonData then
     if mw.loadJsonData then
         mw.log("ACHIEVEMENT-DIAG: ERROR - mw.loadJsonData not available!")
         table.insert(output, "✓ mw.loadJsonData: Available")
    else
        table.insert(output, "❌ mw.loadJsonData: Not available! MediaWiki may be too old or misconfigured")
     end
     end
      
      
     if mw.text and mw.text.jsonDecode then
     if not (mw.text and mw.text.jsonDecode) then
         table.insert(output, "mw.text.jsonDecode: Available")
         mw.log("ACHIEVEMENT-DIAG: CRITICAL ERROR - mw.text.jsonDecode not available!")
    else
         return "JSON decoding not available"
         table.insert(output, "❌ mw.text.jsonDecode: Not available! This is a critical function")
     end
     end
      
      
    -- We don't use Module:JSON anymore, just note that we're using built-in functions
     -- Check page existence
    table.insert(output, "ℹ️ Using MediaWiki's built-in JSON functions")
   
     -- Check the page configuration
    table.insert(output, "\n== JSON PAGE STATUS ==")
     local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
     local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
     if not pageTitle then
     if not pageTitle or not pageTitle.exists then
         table.insert(output, "❌ Could not create title object for: " .. ACHIEVEMENT_DATA_PAGE)
         mw.log("ACHIEVEMENT-DIAG: ERROR - " .. ACHIEVEMENT_DATA_PAGE .. " does not exist")
        return table.concat(output, "\n")
         return "JSON data page does not exist"
    end
   
    if not pageTitle.exists then
        table.insert(output, "❌ Page does not exist: " .. ACHIEVEMENT_DATA_PAGE)
         return table.concat(output, "\n")
    end
   
    table.insert(output, "✓ Page exists: " .. ACHIEVEMENT_DATA_PAGE)
   
    -- Check the content model
    if pageTitle.contentModel then
        table.insert(output, "Content model: " .. pageTitle.contentModel)
        if pageTitle.contentModel == "json" then
            table.insert(output, "✓ Page has correct content model: 'json'")
        else
            table.insert(output, "❌ Page has INCORRECT content model: '" .. pageTitle.contentModel .. "' (should be 'json')")
        end
    else
        table.insert(output, "⚠ Could not determine content model")
     end
     end
      
      
     -- Try to fetch page content
     -- Check content model
     local content = nil
     if pageTitle.contentModel and pageTitle.contentModel ~= "json" then
    local contentSuccess, contentResult = pcall(function()
         mw.log("ACHIEVEMENT-DIAG: ERROR - Incorrect content model: " .. pageTitle.contentModel)
        return pageTitle:getContent()
        mw.log("ACHIEVEMENT-DIAG: Page must be set to 'json' content model")
    end)
         return "Incorrect content model: " .. pageTitle.contentModel
   
    if not contentSuccess or not contentResult or contentResult == "" then
         table.insert(output, "❌ Failed to get page content: " .. tostring(contentResult or "Unknown error"))
         return table.concat(output, "\n")
     end
     end
      
      
    table.insert(output, "✓ Got page content, length: " .. #contentResult)
     -- Try to load JSON data
   
     -- Check page content
    content = contentResult
    local contentPreview = content:sub(1, 50):gsub("\n", "\\n"):gsub("\t", "\\t")
    table.insert(output, "Content preview: \"" .. contentPreview .. "...\"")
   
    if content:match("^%s*{") then
        table.insert(output, "✓ Content starts with { (correct)")
    else
        table.insert(output, "❌ Content does NOT start with { (INCORRECT)")
    end
   
    -- Check for common issues
    if content:match("^<!DOCTYPE") or content:match("^<[Hh][Tt][Mm][Ll]") then
        table.insert(output, "❌ CRITICAL ERROR: Content appears to be HTML, not JSON!")
    elseif content:match("^%s*<") then
        table.insert(output, "❌ CRITICAL ERROR: Content appears to have XML/HTML markup!")
    end
   
    -- Try mw.loadJsonData
    table.insert(output, "\n== TESTING mw.loadJsonData ==")
     local loadJsonSuccess, loadJsonResult = pcall(function()
     local loadJsonSuccess, loadJsonResult = pcall(function()
         return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
         return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
Line 868: Line 627:
      
      
     if not loadJsonSuccess then
     if not loadJsonSuccess then
         table.insert(output, "❌ mw.loadJsonData failed: " .. tostring(loadJsonResult or "Unknown error"))
         mw.log("ACHIEVEMENT-DIAG: ERROR - Failed to load JSON: " .. tostring(loadJsonResult or "unknown error"))
          
          
         -- Analyze error
         -- Try to get raw content for further diagnosis
         local errorMsg = tostring(loadJsonResult or "")
         local contentSuccess, content = pcall(function()
        if errorMsg:match("content model") then
             return pageTitle:getContent()
             table.insert(output, "ERROR CAUSE: Content model issue - page must be set to 'json' model")
         end)
        elseif errorMsg:match("JSON decode") or errorMsg:match("syntax") then
            table.insert(output, "ERROR CAUSE: JSON syntax error - check for invalid JSON formatting")
        elseif errorMsg:match("permission") or errorMsg:match("access") then
            table.insert(output, "ERROR CAUSE: Permission/access error - check page permissions")
         end
    else
        table.insert(output, "✓ mw.loadJsonData SUCCESSFUL!")
        table.insert(output, "Data type: " .. type(loadJsonResult))
          
          
         if type(loadJsonResult) == "table" then
         if contentSuccess and content and content ~= "" then
             local keysFound = {}
             -- Check for common issues
             for k, _ in pairs(loadJsonResult) do
             if content:match("^<!DOCTYPE") or content:match("^<[Hh][Tt][Mm][Ll]") then
                 table.insert(keysFound, k)
                mw.log("ACHIEVEMENT-DIAG: CRITICAL ERROR - Content appears to be HTML, not JSON!")
            elseif not content:match("^%s*{") then
                 mw.log("ACHIEVEMENT-DIAG: ERROR - Content does not start with {")
             end
             end
            table.insert(output, "Top-level keys: " .. table.concat(keysFound, ", "))
              
              
             if loadJsonResult.achievement_types then
             -- Try direct JSON decoding as fallback
                table.insert(output, "✓ Found " .. #loadJsonResult.achievement_types .. " achievement types")
            local jsonDecodeSuccess, _ = pcall(function()
            else
                 return mw.text.jsonDecode(content)
                 table.insert(output, "❌ Missing 'achievement_types' array!")
             end)
             end
              
              
             if loadJsonResult.user_achievements then
             if not jsonDecodeSuccess then
                 local userCount = 0
                 mw.log("ACHIEVEMENT-DIAG: ERROR - JSON syntax error in content")
                for _, _ in pairs(loadJsonResult.user_achievements) do
                    userCount = userCount + 1
                end
                table.insert(output, "✓ Found user achievements for " .. userCount .. " users")
            else
                table.insert(output, "❌ Missing 'user_achievements' object!")
             end
             end
        else
            mw.log("ACHIEVEMENT-DIAG: ERROR - Could not read page content")
         end
         end
       
        return "JSON loading failed"
     end
     end
      
      
     -- Try direct JSON decoding
     -- Verify data structure
     table.insert(output, "\n== TESTING DIRECT JSON DECODING ==")
     if type(loadJsonResult) ~= "table" then
        mw.log("ACHIEVEMENT-DIAG: ERROR - Loaded data is not a table")
        return "Invalid JSON structure"
    end
      
      
    -- Use mw.text.jsonDecode
     if not loadJsonResult.achievement_types then
     if mw.text and mw.text.jsonDecode then
         mw.log("ACHIEVEMENT-DIAG: ERROR - Missing achievement_types array")
         local jsonDecodeSuccess, jsonData = pcall(function()
        return "Missing achievement_types in JSON"
            return mw.text.jsonDecode(content)
        end)
       
        if jsonDecodeSuccess and jsonData then
            table.insert(output, "✓ mw.text.jsonDecode SUCCESSFUL!")
           
            if jsonData.achievement_types then
                table.insert(output, "✓ Found " .. #jsonData.achievement_types .. " achievement types")
            else
                table.insert(output, "Missing 'achievement_types' array in decoded data!")
            end
        else
            table.insert(output, "❌ mw.text.jsonDecode failed: " .. tostring(jsonData or "Unknown error"))
        end
    else
        table.insert(output, "⚠ Cannot test mw.text.jsonDecode (not available)")
     end
     end
      
      
     -- Use Module:JSON if available
     if not loadJsonResult.user_achievements then
    if json and json.decode then
         mw.log("ACHIEVEMENT-DIAG: ERROR - Missing user_achievements object")
         local parseSuccess, parsedData = pcall(function()
        return "Missing user_achievements in JSON"
            return json.decode(content)
        end)
       
        if parseSuccess and parsedData then
            table.insert(output, "✓ Module:JSON's json.decode SUCCESSFUL!")
           
            if parsedData.achievement_types then
                table.insert(output, "✓ Found " .. #parsedData.achievement_types .. " achievement types")
            else
                table.insert(output, "❌ Missing 'achievement_types' array in decoded data!")
            end
        else
            table.insert(output, "❌ Module:JSON's json.decode failed: " .. tostring(parsedData or "Unknown error"))
        end
    else
        table.insert(output, "⚠ Cannot test Module:JSON (not available)")
     end
     end
      
      
     -- Add recommendations
     -- Success
     table.insert(output, "\n== RECOMMENDATIONS ==")
     mw.log("ACHIEVEMENT-DIAG: JSON loading successful")
     if not loadJsonSuccess then
     mw.log("ACHIEVEMENT-DIAG: Found " .. #loadJsonResult.achievement_types .. " achievement types")
        table.insert(output, "1. Ensure '" .. ACHIEVEMENT_DATA_PAGE .. "' has content model set to 'json'")
   
        table.insert(output, "2. Verify the JSON is valid with no syntax errors")
    local userCount = 0
        table.insert(output, "3. Check the page starts with { with no leading whitespace or comments")
    for _, _ in pairs(loadJsonResult.user_achievements) do
    else
         userCount = userCount + 1
         table.insert(output, "✓ JSON loading appears to be working correctly!")
     end
     end
    mw.log("ACHIEVEMENT-DIAG: Found achievements for " .. userCount .. " users")
      
      
     return table.concat(output, "\n")
     return "JSON diagnostics complete - all checks passed"
end
end


return Achievements
return Achievements

Revision as of 03:23, 2 April 2025

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
    -- Only use mw.log for console visibility
    mw.log("ACHIEVEMENT: " .. message)
end

--------------------------------------------------------------------------------
-- JSON Handling
--------------------------------------------------------------------------------
-- Helper function to ensure we get an array
local function ensureArray(value)
    if type(value) ~= "table" then
        return {}
    end
    
    -- Check if it's an array-like table
    local isArray = true
    local count = 0
    for _ in pairs(value) do
        count = count + 1
    end
    
    -- If it has no numeric indices or is empty, return empty array
    if count == 0 then
        return {}
    end
    
    -- If it's a single string, wrap it in an array
    if count == 1 and type(value[1]) == "string" then
        return {value[1]}
    end
    
    -- If it has a single non-array value, try to convert it to an array
    if count == 1 and next(value) and type(next(value)) ~= "number" then
        local k, v = next(value)
        if type(v) == "string" then
            return {v}
        end
    end
    
    -- Return the original table if it seems to be an array
    return value
end

-- We'll use MediaWiki's built-in JSON functions directly, no external module needed
local function jsonDecode(jsonString)
    if not jsonString then return nil end
    
    if mw.text and mw.text.jsonDecode then
        local success, result = pcall(function()
            -- Use WITHOUT PRESERVE_KEYS flag to ensure proper array handling
            return mw.text.jsonDecode(jsonString)
        end)
        
        if success and result then
            return result
        else
            debugLog('ERROR: JSON decode failed: ' .. tostring(result or 'unknown error'))
        end
    end
    
    debugLog('CRITICAL ERROR: mw.text.jsonDecode not available!')
    return nil
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('&', '&amp;')
            :gsub('<', '&lt;')
            :gsub('>', '&gt;')
            :gsub('"', '&quot;')
    end
end

--------------------------------------------------------------------------------
-- Configuration, Default Data, and Cache
--------------------------------------------------------------------------------

local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local dataCache = nil

local DEFAULT_DATA = {
    schema_version = 1,
    last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
    achievement_types = {},
    user_achievements = {},
    cache_control = { version = 0 }
}

--------------------------------------------------------------------------------
-- Configuration
--------------------------------------------------------------------------------
-- This array maps legacy achievement IDs to standardized ones
local ACHIEVEMENT_TYPE_MAPPING = {
    ["title-test"] = "dev-role",
    ["jedi"]      = "ach1",
    ["champion"]  = "ach2",
    ["sponsor"]   = "ach3"
}

-- Normalizes achievement type to handle variants or legacy types
local function normalizeAchievementType(achievementType)
    if not achievementType then return nil end
    
    -- If it's already a standard type, return it directly
    if achievementType == "dev-role" or 
       achievementType == "ach1" or 
       achievementType == "ach2" or 
       achievementType == "ach3" then
        return achievementType
    end
    
    -- Otherwise check the mapping table
    return ACHIEVEMENT_TYPE_MAPPING[achievementType] or achievementType
end

--------------------------------------------------------------------------------
-- Load achievement data from the JSON page
--------------------------------------------------------------------------------
function Achievements.loadData()
    debugLog("Starting to load achievement data")

    -- Use the request-level cache if we already loaded data once
    if dataCache then
        debugLog("Using request-level cached data")
        return dataCache
    end

    local success, data = pcall(function()
        -- Try using mw.loadJsonData first (preferred method)
        if mw.loadJsonData then
            debugLog("Attempting to use mw.loadJsonData for " .. ACHIEVEMENT_DATA_PAGE)
            
            local loadJsonSuccess, jsonData = pcall(function()
                return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
            end)
            
            if loadJsonSuccess and jsonData and type(jsonData) == 'table' then
                debugLog("Successfully loaded data with mw.loadJsonData")
                return jsonData
            else
                debugLog("mw.loadJsonData failed: " .. tostring(jsonData or 'unknown error'))
            end
        else
            debugLog("mw.loadJsonData not available, falling back to direct content loading")
        end
        
        -- Direct content loading approach as fallback
        local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
        if not pageTitle or not pageTitle.exists then
            debugLog(ACHIEVEMENT_DATA_PAGE .. " does not exist")
            return DEFAULT_DATA
        end
        
        -- Get raw content from the wiki page
        local contentSuccess, content = pcall(function()
            return pageTitle:getContent()
        end)
        
        if contentSuccess and content and content ~= "" then
            debugLog("Successfully retrieved raw content, length: " .. #content)
            
            -- Remove any BOM or leading whitespace that might cause issues
            content = content:gsub("^%s+", "")
            if content:byte(1) == 239 and content:byte(2) == 187 and content:byte(3) == 191 then
                debugLog("Removing UTF-8 BOM from content")
                content = content:sub(4)
            end
            
            -- Use mw.text.jsonDecode for parsing WITHOUT PRESERVE_KEYS flag
            if mw.text and mw.text.jsonDecode then
                local jsonDecodeSuccess, jsonData = pcall(function()
                    return mw.text.jsonDecode(content)
                end)
                
                if jsonDecodeSuccess and jsonData then
                    debugLog("Successfully decoded content with mw.text.jsonDecode")
                    return jsonData
                else
                    debugLog("mw.text.jsonDecode failed: " .. tostring(jsonData or 'unknown error'))
                end
            else
                debugLog("mw.text.jsonDecode not available")
            end
        else
            debugLog("Failed to get content: " .. tostring(content or 'unknown error'))
        end
        
        -- As absolute last resort, use local default data
        debugLog("All JSON loading approaches failed, using default data")
        return DEFAULT_DATA
    end)

    if not success or not data then
        debugLog("Critical error in load process: " .. tostring(data or 'unknown error'))
        data = DEFAULT_DATA
    end

    -- Show success source in log
    if data ~= DEFAULT_DATA then
        debugLog("Successfully loaded JSON data with " .. tostring(#(data.achievement_types or {})) .. " achievement types")
    end

    dataCache = data
    return data
end

--------------------------------------------------------------------------------
-- Get user achievements with multiple lookup methods
--------------------------------------------------------------------------------
function Achievements.getUserAchievements(pageId)
    if not pageId or pageId == '' then
        debugLog("Empty page ID provided to getUserAchievements")
        return {}
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("No achievement data available in getUserAchievements")
        return {}
    end

    local key = tostring(pageId)
    debugLog("Looking up achievements for ID: " .. key)
    
    -- Try string key first
    local userAchievements = data.user_achievements[key] or {}
    if #userAchievements > 0 then
        debugLog("Found achievements using string key: " .. key)
        return ensureArray(userAchievements)
    end
    
    -- Try numeric key if string key didn't work
    local numKey = tonumber(key)
    if numKey and data.user_achievements[numKey] then
        debugLog("Found achievements using numeric key: " .. numKey)
        return ensureArray(data.user_achievements[numKey])
    end
    
    -- Try legacy "n123" style
    if key:match("^%d+$") then
        local alt = "n" .. key
        if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
            debugLog("Found achievements using legacy key: " .. alt)
            return ensureArray(data.user_achievements[alt])
        end
    end
    
    -- Try string comparison as last resort
    for userId, achievements in pairs(data.user_achievements) do
        if tostring(userId) == key then
            debugLog("Found achievements using string comparison with key type: " .. type(userId))
            return ensureArray(achievements)
        end
    end
    
    debugLog("No achievements found for user " .. key)
    return {}
end

--------------------------------------------------------------------------------
-- Check if a page/user has any achievements
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
    if not pageId or pageId == '' then
        return false
    end

    local userAchievements = Achievements.getUserAchievements(pageId)
    return #userAchievements > 0
end

--------------------------------------------------------------------------------
-- Get a user-friendly name for a given achievement type
--------------------------------------------------------------------------------
function Achievements.getAchievementName(achievementType)
    if not achievementType or achievementType == '' then
        debugLog("Empty achievement type provided to getAchievementName")
        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
        debugLog("No achievement data or achievement_types missing")
        return achievementType
    end

    -- Try to match achievement ID
    for _, 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
                debugLog("'" .. typeData.id .. "' has no name; using ID")
                return achievementType
            end
        end
    end

    -- Special case for dev-role lookup
    if achievementType == "dev-role" then
        debugLog("Could not find dev-role in achievement_types!")
    end

    debugLog("No achievement found with type '" .. achievementType .. "'; using ID fallback")
    return achievementType
end

--------------------------------------------------------------------------------
-- Find the top-tier achievement for the user (lowest tier number)
-- Return the CSS class and the readable achievement name
--------------------------------------------------------------------------------
function Achievements.getTitleClass(pageId)
    if not pageId or pageId == '' then
        debugLog("Empty page ID provided to getTitleClass")
        return '', ''
    end

    local userAchievements = Achievements.getUserAchievements(pageId)
    if #userAchievements == 0 then
        debugLog("No achievements found for user " .. tostring(pageId))
        return '', ''
    end

    local data = Achievements.loadData()
    local highestTier = 999
    local highestAchievement = nil

    for _, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        debugLog("Processing achievement type: " .. (achType or "nil"))
        
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id == achType then
                local tier = typeData.tier or 999
                debugLog("  Found type '" .. typeData.id .. "' with tier " .. tier .. " and name '" .. (typeData.name or "nil") .. "'")
                if tier < highestTier then
                    highestTier = tier
                    highestAchievement = typeData
                    debugLog("  New highest tier achievement: " .. typeData.id)
                end
            end
        end
    end

    if not highestAchievement or not highestAchievement.id then
        debugLog("No valid top-tier achievement found for user " .. tostring(pageId))
        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 a simple "box" with the top-tier achievement for the user
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
    if not pageId or pageId == '' then
        return ''
    end

    local userAchievements = Achievements.getUserAchievements(pageId)
    if #userAchievements == 0 then
        return ''
    end
    
    local data = Achievements.loadData()
    
    -- Build a lookup table for achievement type definitions
    local typeDefinitions = {}
    if data and data.achievement_types then
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id and typeData.name then
                typeDefinitions[typeData.id] = {
                    name = typeData.name,
                    tier = typeData.tier or 999
                }
            end
        end
    end

    -- Look for the highest-tier achievement (lowest tier number)
    local highestTier = 999
    local topAchType = nil

    for _, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        if typeDefinitions[achType] and typeDefinitions[achType].tier < highestTier then
            highestTier = typeDefinitions[achType].tier
            topAchType = achType
        end
    end

    -- If we found an achievement, render it
    if topAchType and typeDefinitions[topAchType] then
        local achName = typeDefinitions[topAchType].name or topAchType
        
        return string.format(
            '<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
            topAchType,
            htmlEncode(achName)
        )
    end

    return ''
end

--------------------------------------------------------------------------------
-- Simple pass-through to track pages (for future expansions)
--------------------------------------------------------------------------------
function Achievements.trackPage(pageId, pageName)
    return true
end

--------------------------------------------------------------------------------
-- Retrieve a specific achievement if present, by type
--------------------------------------------------------------------------------
function Achievements.getSpecificAchievement(pageId, achievementType)
    debugLog("Looking for achievement '" .. tostring(achievementType) ..
             "' in page ID: " .. tostring(pageId))

    if not pageId or not achievementType or pageId == '' then
        debugLog("Invalid arguments for getSpecificAchievement")
        return nil
    end

    local userAchievements = Achievements.getUserAchievements(pageId)
    
    -- Direct lookup for the requested achievement type
    for _, achievement in ipairs(userAchievements) do
        if achievement.type == achievementType then
            debugLog("Found achievement: " .. achievementType .. " for user " .. tostring(pageId))
            return achievement
        end
    end

    debugLog("No match found for achievement type: " .. achievementType)
    return nil
end

--------------------------------------------------------------------------------
-- Get achievement definition directly from JSON data
--------------------------------------------------------------------------------
function Achievements.getAchievementDefinition(achievementType)
    if not achievementType or achievementType == '' then
        debugLog("ACHIEVEMENT-DEF: Empty achievement type")
        return nil
    end
    
    local data = Achievements.loadData()
    if not data or not data.achievement_types then
        debugLog("ACHIEVEMENT-DEF: No achievement data loaded")
        return nil
    end
    
    -- Direct lookup in achievement_types array
    for _, typeData in ipairs(data.achievement_types) do
        if typeData.id == achievementType then
            debugLog("ACHIEVEMENT-DEF: Found definition for " .. achievementType)
            return typeData
        end
    end
    
    debugLog("ACHIEVEMENT-DEF: No definition found for " .. achievementType)
    return nil
end

--------------------------------------------------------------------------------
-- Diagnostic Function: Log badges for a page to console
--------------------------------------------------------------------------------
function Achievements.debugBadgesForPage(pageId)
    if not pageId or pageId == '' then
        mw.log("ACHIEVEMENT-BADGES: No page ID provided")
        return "ERROR: No page ID provided"
    end

    local userAchievements = Achievements.getUserAchievements(pageId)
    if #userAchievements == 0 then
        mw.log("ACHIEVEMENT-BADGES: No achievements found for page ID " .. pageId)
        return "No achievements found for page ID " .. pageId
    end

    mw.log("ACHIEVEMENT-BADGES: Found " .. #userAchievements .. " achievements for page ID " .. pageId)
    
    -- Log each achievement to console
    for i, achievement in ipairs(userAchievements) do
        local achType = achievement.type or "nil"
        local typeDef = Achievements.getAchievementDefinition(achType)
        
        if typeDef then
            mw.log("ACHIEVEMENT-BADGES: [" .. i .. "] " .. achType .. 
                  " (Name: " .. (typeDef.name or "unnamed") .. 
                  ", Type: " .. (typeDef.type or "unspecified") .. 
                  ", Tier: " .. (typeDef.tier or "none") .. ")")
        else
            mw.log("ACHIEVEMENT-BADGES: [" .. i .. "] " .. achType .. " (WARNING: No definition found)")
        end
    end
    
    return "Found " .. #userAchievements .. " achievements for page ID " .. pageId
end

--------------------------------------------------------------------------------
-- Find and return title achievement for the user if one exists
-- This specifically looks for achievements with type="title"
-- Return the CSS class, readable achievement name, and achievement ID (or empty strings if none found)
--------------------------------------------------------------------------------
function Achievements.getTitleAchievement(pageId)
    if not pageId or pageId == '' then
        debugLog("Empty page ID provided to getTitleAchievement")
        return '', '', ''
    end

    local userAchievements = Achievements.getUserAchievements(pageId)
    if #userAchievements == 0 then
        debugLog("No achievements found for user " .. tostring(pageId))
        return '', '', ''
    end

    local data = Achievements.loadData()
    debugLog("Looking up title achievements for ID: " .. tostring(pageId))
    
    -- Build a table of achievement definitions for quick lookup
    local typeDefinitions = {}
    for _, typeData in ipairs(data.achievement_types) do
        typeDefinitions[typeData.id] = typeData
    end

    -- Find title achievements only
    local highestTier = 999
    local titleAchievement = nil

    for i, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        if achType then
            local typeData = typeDefinitions[achType]
            if typeData then
                -- Check if it's a title type by examining "type" property
                local achDefType = typeData.type
                
                -- Only consider achievements with type="title"
                if achDefType == "title" then
                    debugLog("Found potential title achievement: " .. achType)
                    local tier = typeData.tier or 999
                    if tier < highestTier then
                        highestTier = tier
                        titleAchievement = typeData
                        debugLog("Using as highest tier: " .. typeData.id)
                    end
                end
            end
        end
    end

    if not titleAchievement or not titleAchievement.id then
        debugLog("No valid title achievement found for user " .. tostring(pageId))
        return '', '', ''
    end

    local cssClass = "achievement-" .. titleAchievement.id
    local displayName = titleAchievement.name or titleAchievement.id or "Award"
    local achievementId = titleAchievement.id
    
    debugLog("Using title achievement: " .. cssClass .. " with name: " .. displayName)
    
    return cssClass, displayName, achievementId
end

--------------------------------------------------------------------------------
-- Simplified diagnostic function for JSON loading issues
--------------------------------------------------------------------------------
function Achievements.diagnoseJsonLoading()
    mw.log("ACHIEVEMENT-DIAG: Starting JSON diagnostics")
    
    -- Check MediaWiki capabilities
    if not mw.loadJsonData then
        mw.log("ACHIEVEMENT-DIAG: ERROR - mw.loadJsonData not available!")
    end
    
    if not (mw.text and mw.text.jsonDecode) then
        mw.log("ACHIEVEMENT-DIAG: CRITICAL ERROR - mw.text.jsonDecode not available!")
        return "JSON decoding not available"
    end
    
    -- Check page existence
    local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
    if not pageTitle or not pageTitle.exists then
        mw.log("ACHIEVEMENT-DIAG: ERROR - " .. ACHIEVEMENT_DATA_PAGE .. " does not exist")
        return "JSON data page does not exist"
    end
    
    -- Check content model
    if pageTitle.contentModel and pageTitle.contentModel ~= "json" then
        mw.log("ACHIEVEMENT-DIAG: ERROR - Incorrect content model: " .. pageTitle.contentModel)
        mw.log("ACHIEVEMENT-DIAG: Page must be set to 'json' content model")
        return "Incorrect content model: " .. pageTitle.contentModel
    end
    
    -- Try to load JSON data
    local loadJsonSuccess, loadJsonResult = pcall(function()
        return mw.loadJsonData(ACHIEVEMENT_DATA_PAGE)
    end)
    
    if not loadJsonSuccess then
        mw.log("ACHIEVEMENT-DIAG: ERROR - Failed to load JSON: " .. tostring(loadJsonResult or "unknown error"))
        
        -- Try to get raw content for further diagnosis
        local contentSuccess, content = pcall(function()
            return pageTitle:getContent()
        end)
        
        if contentSuccess and content and content ~= "" then
            -- Check for common issues
            if content:match("^<!DOCTYPE") or content:match("^<[Hh][Tt][Mm][Ll]") then
                mw.log("ACHIEVEMENT-DIAG: CRITICAL ERROR - Content appears to be HTML, not JSON!")
            elseif not content:match("^%s*{") then
                mw.log("ACHIEVEMENT-DIAG: ERROR - Content does not start with {")
            end
            
            -- Try direct JSON decoding as fallback
            local jsonDecodeSuccess, _ = pcall(function()
                return mw.text.jsonDecode(content)
            end)
            
            if not jsonDecodeSuccess then
                mw.log("ACHIEVEMENT-DIAG: ERROR - JSON syntax error in content")
            end
        else
            mw.log("ACHIEVEMENT-DIAG: ERROR - Could not read page content")
        end
        
        return "JSON loading failed"
    end
    
    -- Verify data structure
    if type(loadJsonResult) ~= "table" then
        mw.log("ACHIEVEMENT-DIAG: ERROR - Loaded data is not a table")
        return "Invalid JSON structure"
    end
    
    if not loadJsonResult.achievement_types then
        mw.log("ACHIEVEMENT-DIAG: ERROR - Missing achievement_types array")
        return "Missing achievement_types in JSON"
    end
    
    if not loadJsonResult.user_achievements then
        mw.log("ACHIEVEMENT-DIAG: ERROR - Missing user_achievements object")
        return "Missing user_achievements in JSON"
    end
    
    -- Success
    mw.log("ACHIEVEMENT-DIAG: JSON loading successful")
    mw.log("ACHIEVEMENT-DIAG: Found " .. #loadJsonResult.achievement_types .. " achievement types")
    
    local userCount = 0
    for _, _ in pairs(loadJsonResult.user_achievements) do
        userCount = userCount + 1
    end
    mw.log("ACHIEVEMENT-DIAG: Found achievements for " .. userCount .. " users")
    
    return "JSON diagnostics complete - all checks passed"
end

return Achievements