Module:TroubleshooterAchievements
Appearance
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