Jump to content

Module:TroubleshooterAchievements

Documentation for this module may be created at Module:TroubleshooterAchievements/doc

-- Module:TroubleshooterAchievements
-- A comprehensive diagnostic tool for ICANNWiki's achievement system that helps identify and resolve issues.
-- Features:
-- * Analyzes JSON achievement data loading and structure from both AchievementData.json and AchievementList.json
-- * Detects key type mismatches between string and numeric page IDs
-- * Displays detailed achievement information for the current page
-- * Provides testing capabilities for specific page IDs
-- * Handles various edge cases in achievement data parsing
-- * Renders diagnostic output for the split achievement system

local p = {}

-- Helper function to ensure an array is retrieved
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

-- Helper function to load JSON data with multiple fallback methods
local function loadJsonData(frame, pageName)
    local jsonText = frame:preprocess('{{MediaWiki:' .. pageName .. '}}')
    
    -- Try different JSON decode approaches
    local success, data
    
    -- First try WITHOUT PRESERVE_KEYS flag to ensure proper array indexing
    success, data = pcall(function()
        return mw.text.jsonDecode(jsonText)
    end)
    
    -- If that failed, try with JSON_TRY_FIXING flag
    if not success then
        success, data = pcall(function()
            return mw.text.jsonDecode(jsonText, mw.text.JSON_TRY_FIXING)
        end)
    end
    
    -- Only as a last resort, try with PRESERVE_KEYS, which is REALLY NOT RECOMMENDED
    if not success then
        success, data = pcall(function()
            return mw.text.jsonDecode(jsonText, mw.text.JSON_PRESERVE_KEYS)
        end)
    end
    
    return success, data, jsonText
end

-- Function to render diagnostic information
function p.run(frame)
    -- Get current page info in both string and number formats
    local currentPageIdStr = tostring(mw.title.getCurrentTitle().id)
    local currentPageIdNum = tonumber(currentPageIdStr)
    local currentPageName = mw.title.getCurrentTitle().fullText
    
    -- Load achievement data JSON
    local dataSuccess, data, dataJsonText = loadJsonData(frame, 'AchievementData.json')
    
    -- Load achievement types JSON
    local typesSuccess, types, typesJsonText = loadJsonData(frame, 'AchievementList.json')
    
    -- Start building HTML output
    local html = {}
    table.insert(html, '<div style="border: 1px solid rgba(128, 128, 128, 0.3); padding: 15px; margin: 15px 0; border-radius: 5px; font-family: monospace;">')
    table.insert(html, '<h3>Achievement System Diagnostic</h3>')
    
    -- Display current page info
    table.insert(html, string.format('<p>Current page: <b>%s</b> (ID: <b>%s</b>)</p>', currentPageName, currentPageIdStr))
    
    -- Display JSON loading results
    table.insert(html, '<h4>JSON Loading</h4>')
    
    -- AchievementData.json loading result
    if dataSuccess then
        table.insert(html, '<p style="color: #4CAF50;"><b>✓ AchievementData.json loaded successfully</b></p>')
        table.insert(html, '<pre style="background-color: rgba(128, 128, 128, 0.1); padding: 10px; border-radius: 3px; max-height: 150px; overflow: auto; color: inherit;">')
        table.insert(html, dataJsonText:sub(1, 300) .. (dataJsonText:len() > 300 and "..." or ""))
        table.insert(html, '</pre>')
    else
        table.insert(html, '<p style="color: #F44336;"><b>✗ Failed to load AchievementData.json</b></p>')
        table.insert(html, '<p>Error: ' .. tostring(data) .. '</p>')
    end
    
    -- AchievementList.json loading result
    if typesSuccess then
        table.insert(html, '<p style="color: #4CAF50;"><b>✓ AchievementList.json loaded successfully</b></p>')
        table.insert(html, '<pre style="background-color: rgba(128, 128, 128, 0.1); padding: 10px; border-radius: 3px; max-height: 150px; overflow: auto; color: inherit;">')
        table.insert(html, typesJsonText:sub(1, 300) .. (typesJsonText:len() > 300 and "..." or ""))
        table.insert(html, '</pre>')
    else
        table.insert(html, '<p style="color: #F44336;"><b>✗ Failed to load AchievementList.json</b></p>')
        table.insert(html, '<p>Error: ' .. tostring(types) .. '</p>')
    end
    
    -- If both JSON files failed to load, exit early
    if not dataSuccess and not typesSuccess then
        table.insert(html, '<p style="color: #F44336;"><b>Cannot proceed with diagnostic - both JSON files failed to load</b></p>')
        table.insert(html, '</div>')
        return table.concat(html)
    end
    
    -- Display split system information
    table.insert(html, '<h4>Split Achievement System</h4>')
    table.insert(html, '<p>The achievement system uses two JSON files:</p>')
    table.insert(html, '<ul>')
    table.insert(html, '<li><b>AchievementList.json</b>: Contains achievement type definitions</li>')
    table.insert(html, '<li><b>AchievementData.json</b>: Contains user achievement assignments</li>')
    table.insert(html, '</ul>')
    
    -- Count achievement types in each file
    local typesInListFile = 0
    local typesInDataFile = 0
    
    if typesSuccess and types and types.achievement_types then
        typesInListFile = #types.achievement_types
    end
    
    if dataSuccess and data and data.achievement_types then
        typesInDataFile = #data.achievement_types
    end
    
    table.insert(html, string.format('<p>Achievement types in AchievementList.json: <b>%d</b></p>', typesInListFile))
    table.insert(html, string.format('<p>Achievement types in AchievementData.json: <b>%d</b></p>', typesInDataFile))
    
    -- Determine which file to use for achievement types
    local useTypesFrom = "none"
    local achievementTypes = {}
    
    if typesInListFile > 0 then
        useTypesFrom = "AchievementList.json"
        achievementTypes = types.achievement_types
    elseif typesInDataFile > 0 then
        useTypesFrom = "AchievementData.json"
        achievementTypes = data.achievement_types
    end
    
    table.insert(html, string.format('<p>Using achievement types from: <b>%s</b></p>', useTypesFrom))
    
    -- Display key type information
    table.insert(html, '<h4>Key Type Analysis</h4>')
    table.insert(html, '<ul>')
    table.insert(html, string.format('<li>Current page ID as string: <b>%s</b> (type: string)</li>', currentPageIdStr))
    table.insert(html, string.format('<li>Current page ID as number: <b>%s</b> (type: number)</li>', tostring(currentPageIdNum)))
    
    -- Show all keys in the user_achievements object
    if dataSuccess and data and data.user_achievements then
        table.insert(html, '<li>Keys in user_achievements object:</li>')
        table.insert(html, '<ul>')
        for pageId, _ in pairs(data.user_achievements) do
            table.insert(html, string.format(
                '<li><b>%s</b> (type: %s)</li>',
                tostring(pageId),
                type(pageId)
            ))
        end
        table.insert(html, '</ul>')
    end
    table.insert(html, '</ul>')
    
    -- Display page achievements from JSON
    table.insert(html, '<h4>Page Achievements in JSON</h4>')
    table.insert(html, '<ul>')
    
    if dataSuccess and data and data.user_achievements then
        local foundAny = false
        for pageId, userEntry in pairs(data.user_achievements) do
            foundAny = true
            
            -- Ensure achievements is an array
            local achievementsArray = ensureArray(userEntry.achievements or {})
            local items = {}
            
            -- Extract achievement types from the objects
            for i, achievement in ipairs(achievementsArray) do
                if type(achievement) == "table" and achievement.type then
                    table.insert(items, achievement.type)
                else
                    table.insert(items, tostring(achievement))
                end
            end
            
            local achievementsList = table.concat(items, ", ")
            
            table.insert(html, string.format(
                '<li>Page ID <b>%s</b> (type: %s): %s</li>',
                tostring(pageId),
                type(pageId),
                achievementsList
            ))
        end
        
        if not foundAny then
            table.insert(html, '<li><i>No user achievements defined in JSON</i></li>')
        end
    else
        table.insert(html, '<li><i>No user_achievements section found in JSON</i></li>')
    end
    table.insert(html, '</ul>')
    
    -- Display current page status
    table.insert(html, '<h4>Current Page Status</h4>')
    
    -- Try to find achievements for the current page using different key types
    local currentPageAchievements = {}
    local keyTypeUsed = "none"
    
    if dataSuccess and data and data.user_achievements then
        -- Try string key
        if data.user_achievements[currentPageIdStr] then
            local userEntry = data.user_achievements[currentPageIdStr]
            local rawAchievements = ensureArray(userEntry.achievements or {})
            -- Extract achievement types
            for _, achievement in ipairs(rawAchievements) do
                if type(achievement) == "table" and achievement.type then
                    table.insert(currentPageAchievements, achievement.type)
                else
                    table.insert(currentPageAchievements, achievement)
                end
            end
            keyTypeUsed = "string"
        -- Try number key
        elseif data.user_achievements[currentPageIdNum] then
            local userEntry = data.user_achievements[currentPageIdNum]
            local rawAchievements = ensureArray(userEntry.achievements or {})
            -- Extract achievement types
            for _, achievement in ipairs(rawAchievements) do
                if type(achievement) == "table" and achievement.type then
                    table.insert(currentPageAchievements, achievement.type)
                else
                    table.insert(currentPageAchievements, achievement)
                end
            end
            keyTypeUsed = "number"
        -- Try string comparison as last resort
        else
            for pageId, userEntry in pairs(data.user_achievements) do
                if tostring(pageId) == currentPageIdStr then
                    local rawAchievements = ensureArray(userEntry.achievements or {})
                    -- Extract achievement types
                    for _, achievement in ipairs(rawAchievements) do
                        if type(achievement) == "table" and achievement.type then
                            table.insert(currentPageAchievements, achievement.type)
                        else
                            table.insert(currentPageAchievements, achievement)
                        end
                    end
                    keyTypeUsed = "string comparison with " .. type(pageId) .. " key"
                    break
                end
            end
        end
    end
    
    -- Show array details
    table.insert(html, '<p>Achievement array type: ' .. type(currentPageAchievements) .. '</p>')
    table.insert(html, '<p>Achievement array length: ' .. #currentPageAchievements .. '</p>')
    
    if #currentPageAchievements > 0 then
        table.insert(html, '<p>Achievement IDs:</p>')
        table.insert(html, '<ul>')
        for i, id in ipairs(currentPageAchievements) do
            table.insert(html, string.format('<li>%d: %s</li>', i, tostring(id)))
        end
        table.insert(html, '</ul>')
        
        table.insert(html, string.format(
            '<p style="color: #4CAF50;"><b>✓ This page (ID %s) has %d achievement(s) defined in JSON</b></p>',
            currentPageIdStr,
            #currentPageAchievements
        ))
        table.insert(html, string.format(
            '<p>Found using key type: <b>%s</b></p>',
            keyTypeUsed
        ))
    else
        table.insert(html, string.format(
            '<p style="color: #FF9800;"><b>⚠ This page (ID %s) has no achievements defined in JSON</b></p>',
            currentPageIdStr
        ))
    end
    
    -- Display achievement definitions
    table.insert(html, '<h4>Achievement Definitions</h4>')
    table.insert(html, '<ul>')
    
    if #achievementTypes > 0 then
        -- Count badge and title types
        local badgeCount = 0
        local titleCount = 0
        
        for _, achievement in ipairs(achievementTypes) do
            if achievement.type == "badge" then
                badgeCount = badgeCount + 1
            elseif achievement.type == "title" then
                titleCount = titleCount + 1
            end
            
            table.insert(html, string.format(
                '<li>ID: <b>%s</b>, Name: <b>%s</b>, Type: <b>%s</b></li>',
                achievement.id or "nil",
                achievement.name or "nil",
                achievement.type or "nil"
            ))
        end
        
        -- Display badge and title counts
        table.insert(html, string.format(
            '<p>Total achievement types: <b>%d</b> (Badges: <b>%d</b>, Titles: <b>%d</b>)</p>',
            #achievementTypes,
            badgeCount,
            titleCount
        ))
    else
        table.insert(html, '<li><i>No achievement definitions found in either JSON file</i></li>')
    end
    
    table.insert(html, '</ul>')
    
    -- Add help text for functions
    table.insert(html, '<p>To check achievements for a specific page ID: <code>{{#invoke:TroubleshooterAchievements|pageAchievements|18451}}</code></p>')
    
    table.insert(html, '</div>')
    
    return table.concat(html)
end

-- Function to test specific page ID (alias for backward compatibility)
function p.pageAchievements(frame)
    return p.testPage(frame)
end

-- Function to test specific page ID
function p.testPage(frame)
    local pageIdStr = frame.args[1] or "18451"
    local pageIdNum = tonumber(pageIdStr)
    
    -- Load achievement data JSON
    local dataSuccess, data = loadJsonData(frame, 'AchievementData.json')
    
    -- Load achievement types JSON
    local typesSuccess, types = loadJsonData(frame, 'AchievementList.json')
    
    -- Start building HTML output
    local html = {}
    table.insert(html, string.format('<div style="border: 1px solid rgba(128, 128, 128, 0.3); padding: 15px; margin: 15px 0; border-radius: 5px;"><h3>Test for Page ID: %s</h3>', pageIdStr))
    
    -- Check if both JSON files failed to load
    if not dataSuccess and not typesSuccess then
        table.insert(html, '<p style="color: #F44336;"><b>✗ Failed to load both JSON files</b></p>')
        table.insert(html, '</div>')
        return table.concat(html)
    end
    
    -- Determine which file to use for achievement types
    local useTypesFrom = "none"
    local achievementTypes = {}
    
    if typesSuccess and types and types.achievement_types then
        useTypesFrom = "AchievementList.json"
        achievementTypes = types.achievement_types
    elseif dataSuccess and data and data.achievement_types then
        useTypesFrom = "AchievementData.json"
        achievementTypes = data.achievement_types
    end
    
    table.insert(html, string.format('<p>Using achievement types from: <b>%s</b></p>', useTypesFrom))
    
    -- Try to find achievements for this page using different key types
    local pageAchievements = {}
    local rawAchievements = {}
    local keyTypeUsed = "none"
    
    if dataSuccess and data and data.user_achievements then
        -- Try string key
        if data.user_achievements[pageIdStr] then
            local userEntry = data.user_achievements[pageIdStr]
            rawAchievements = ensureArray(userEntry.achievements or {})
            keyTypeUsed = "string"
        -- Try number key
        elseif pageIdNum and data.user_achievements[pageIdNum] then
            local userEntry = data.user_achievements[pageIdNum]
            rawAchievements = ensureArray(userEntry.achievements or {})
            keyTypeUsed = "number"
        -- Try string comparison as last resort
        else
            for pageId, userEntry in pairs(data.user_achievements) do
                if tostring(pageId) == pageIdStr then
                    rawAchievements = ensureArray(userEntry.achievements or {})
                    keyTypeUsed = "string comparison with " .. type(pageId) .. " key"
                    break
                end
            end
        end
    end
    
    -- Extract achievement types from objects
    for _, achievement in ipairs(rawAchievements) do
        if type(achievement) == "table" and achievement.type then
            table.insert(pageAchievements, achievement.type)
        else
            table.insert(pageAchievements, achievement)
        end
    end
    
    -- Display key type information
    table.insert(html, '<h4>Key Type Analysis</h4>')
    table.insert(html, '<ul>')
    table.insert(html, string.format('<li>Test page ID as string: <b>%s</b> (type: string)</li>', pageIdStr))
    if pageIdNum then
        table.insert(html, string.format('<li>Test page ID as number: <b>%s</b> (type: number)</li>', tostring(pageIdNum)))
    else
        table.insert(html, '<li>Test page ID cannot be converted to number</li>')
    end
    
    -- Show all keys in the user_achievements object
    if dataSuccess and data and data.user_achievements then
        table.insert(html, '<li>Keys in user_achievements object:</li>')
        table.insert(html, '<ul>')
        for pageId, userEntry in pairs(data.user_achievements) do
            -- Extract achievement types and convert to string list
            local achievementsArray = ensureArray(userEntry.achievements or {})
            local items = {}
            for i, achievement in ipairs(achievementsArray) do
                if type(achievement) == "table" and achievement.type then
                    table.insert(items, achievement.type)
                else
                    table.insert(items, tostring(achievement))
                end
            end
            local achievementsList = table.concat(items, ", ")
            
            table.insert(html, string.format(
                '<li><b>%s</b> (type: %s): %s</li>',
                tostring(pageId),
                type(pageId),
                achievementsList
            ))
        end
        table.insert(html, '</ul>')
    end
    table.insert(html, '</ul>')
    
    -- Show array details
    table.insert(html, '<p>Achievement array type: ' .. type(pageAchievements) .. '</p>')
    table.insert(html, '<p>Achievement array length: ' .. #pageAchievements .. '</p>')
    
    if #pageAchievements > 0 then
        table.insert(html, '<p>Achievement IDs:</p>')
        table.insert(html, '<ul>')
        for i, id in ipairs(pageAchievements) do
            table.insert(html, string.format('<li>%d: %s</li>', i, tostring(id)))
        end
        table.insert(html, '</ul>')
        
        table.insert(html, string.format(
            '<p><b>Found %d achievement(s) in JSON:</b> %s</p>',
            #pageAchievements,
            table.concat(pageAchievements, ", ")
        ))
        table.insert(html, string.format(
            '<p>Found using key type: <b>%s</b></p>',
            keyTypeUsed
        ))
        
        -- Display achievement details
        if #achievementTypes > 0 then
            table.insert(html, '<h4>Achievement Details</h4>')
            table.insert(html, '<ul>')
            
            -- Count badge and title achievements
            local badgeCount = 0
            local titleCount = 0
            
            for _, achievementId in ipairs(pageAchievements) do
                -- Find the achievement details
                local found = false
                for _, achievement in ipairs(achievementTypes) do
                    if achievement.id == achievementId then
                        -- Count by type
                        if achievement.type == "badge" then
                            badgeCount = badgeCount + 1
                        elseif achievement.type == "title" then
                            titleCount = titleCount + 1
                        end
                        
                        table.insert(html, string.format(
                            '<li>ID: <b>%s</b>, Name: <b>%s</b>, Type: <b>%s</b></li>',
                            achievementId,
                            achievement.name or "nil",
                            achievement.type or "nil"
                        ))
                        found = true
                        break
                    end
                end
                
                if not found then
                    table.insert(html, string.format(
                        '<li>ID: <b>%s</b> (details not found in achievement types)</li>',
                        achievementId
                    ))
                end
            end
            
            -- Display badge and title counts
            table.insert(html, string.format(
                '<p>Achievement breakdown: <b>%d</b> total (<b>%d</b> badges, <b>%d</b> titles)</p>',
                #pageAchievements,
                badgeCount,
                titleCount
            ))
            
            table.insert(html, '</ul>')
        else
            table.insert(html, '<p style="color: #F44336;"><b>No achievement definitions found in either JSON file</b></p>')
        end
    else
        table.insert(html, '<p><i>No achievements defined for this page ID in JSON</i></p>')
    end
    
    table.insert(html, '</div>')
    
    return table.concat(html)
end

return p