Jump to content

Module:AchievementSystem: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 23: Line 23:
end
end


-- Attempt to load JSON handling
--------------------------------------------------------------------------------
-- JSON Handling
--------------------------------------------------------------------------------
local json
local json
local jsonLoaded = pcall(function()
local jsonLoaded = pcall(function()
Line 47: Line 49:
end
end


-- The page storing JSON data
--------------------------------------------------------------------------------
-- Configuration, Default Data, and Cache
--------------------------------------------------------------------------------
 
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
local ACHIEVEMENT_DATA_PAGE = 'MediaWiki:AchievementData.json'
-- Cache for achievement data (within one request)
local dataCache = nil
local dataCache = nil


-- Default data if JSON load fails
local DEFAULT_DATA = {
local DEFAULT_DATA = {
     schema_version = 1,
     schema_version = 1,
Line 63: Line 65:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Internal: load achievement data from the page, or from cache
-- (Optional) Testing config
-- Removed forced dev-role injection so it won't override normal JSON lookups.
--------------------------------------------------------------------------------
local TEST_CONFIG = {
    enabled = true,
    test_page_id = "18451",
    test_user = "direct-test-user",
    debug_messages = true,
    force_achievements = false, -- Disabled forcing achievements
    type_mapping = {
        ["jedi"]      = "ach1",
        ["champion"]  = "ach2",
        ["sponsor"]  = "ach3",
        ["ach1"]      = "ach1",
        ["ach2"]      = "ach2",
        ["ach3"]      = "ach3",
        ["title-test"] = "dev-role",
        ["dev-role"]  = "dev-role"
    },
    test_achievements = {"title-test", "ach1", "ach2", "ach3"}
}
 
local function isTestPage(pageId)
    return TEST_CONFIG.enabled and tostring(pageId) == TEST_CONFIG.test_page_id
end
 
--------------------------------------------------------------------------------
-- Load achievement data from the JSON page
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.loadData()
function Achievements.loadData()
     mw.log("JSON-DEBUG: Starting to load achievement data")
     mw.log("JSON-DEBUG: Starting to load achievement data")


    -- 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")
         mw.log("JSON-DEBUG: Using request-level cached data")
Line 74: Line 104:


     local success, data = pcall(function()
     local success, data = pcall(function()
         -- First try to load from parser cache
         -- Try parser cache first
         local loadDataSuccess, cachedData = pcall(function()
         local loadDataSuccess, cachedData = pcall(function()
             return mw.loadData('Module:AchievementSystem')
             return mw.loadData('Module:AchievementSystem')
Line 83: Line 113:
             return cachedData
             return cachedData
         else
         else
             mw.log("JSON-DEBUG: mw.loadData failed or returned empty, using direct page load")
             mw.log("JSON-DEBUG: mw.loadData failed or returned empty, proceeding to direct page load")
         end
         end


Line 122: Line 152:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Checks if a user has any achievements
-- Check if a page/user has any achievements
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.hasAchievements(pageId)
function Achievements.hasAchievements(pageId)
Line 139: Line 169:
     end
     end


     -- Check for achievements under n-prefixed key (legacy)
     -- Check for legacy "n123" style
     if key:match("^%d+$") then
     if key:match("^%d+$") then
         local alt = "n" .. key
         local alt = "n" .. key
Line 147: Line 177:
     end
     end


    -- We removed the forced "true" for test pages to avoid dev-role injection
     return false
     return false
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Retrieves the display name for a given achievement type
-- Get a user-friendly name for a given achievement type
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.getAchievementName(achievementType)
function Achievements.getAchievementName(achievementType)
Line 174: Line 205:
                 return typeData.name
                 return typeData.name
             else
             else
                 mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name")
                 mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name; using ID")
                 return achievementType
                 return achievementType
             end
             end
Line 180: Line 211:
     end
     end


     mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type: '" .. achievementType .. "'")
     mw.log("ACHIEVEMENT-NAME-ERROR: No achievement found with type '" .. achievementType .. "'; using ID fallback")
     return achievementType
     return achievementType
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Finds the top-tier achievement for a user
-- Find the top-tier achievement for the user (lowest tier number)
-- Returns the CSS class and the achievement name
-- Return the CSS class and the readable achievement name
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.getTitleClass(pageId)
function Achievements.getTitleClass(pageId)
Line 242: Line 273:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Renders an achievement box showing the top-tier achievement
-- Renders a simple "box" with the top-tier achievement for the user
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
function Achievements.renderAchievementBox(pageId)
Line 266: Line 297:
     local highestTier = 999
     local highestTier = 999
     local topAch = nil
     local topAch = nil
     for _, achievement in ipairs(userAchievements) do
     for _, achievement in ipairs(userAchievements) do
         local achType = achievement.type
         local achType = achievement.type
Line 286: Line 318:
         )
         )
     end
     end
     return ''
     return ''
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Marks a page for cache purging
-- Simple pass-through to track pages (for future expansions)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.trackPage(pageId, pageName)
function Achievements.trackPage(pageId, pageName)
Line 297: Line 330:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Retrieves a specific achievement from a user if present
-- Retrieve a specific achievement if present, by type
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function Achievements.getSpecificAchievement(pageId, achievementType)
function Achievements.getSpecificAchievement(pageId, achievementType)
Line 326: Line 359:
     end
     end


    -- Removed the forced injection code for test pages to avoid dev-role overrides
    debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
     return nil
     return nil
end
end


return Achievements
return Achievements

Revision as of 02:00, 1 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
    pcall(function()
        mw.logObject({
            system = "achievement_simple",
            message = message,
            timestamp = os.date('%H:%M:%S')
        })
    end)
    mw.log("ACHIEVEMENT-DEBUG: " .. message)
end

--------------------------------------------------------------------------------
-- JSON Handling
--------------------------------------------------------------------------------
local json
local jsonLoaded = pcall(function()
    json = require('Module:JSON')
end)

if not jsonLoaded or not json then
    json = { decode = function() return nil end }
    debugLog('WARNING: Module:JSON not available, achievement features will be limited')
end

-- Simple HTML encode fallback
local function htmlEncode(str)
    if mw.text and mw.text.htmlEncode then
        return mw.text.htmlEncode(str or '')
    else
        return (str or '')
            :gsub('&', '&')
            :gsub('<', '&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 }
}

--------------------------------------------------------------------------------
-- (Optional) Testing config
-- Removed forced dev-role injection so it won't override normal JSON lookups.
--------------------------------------------------------------------------------
local TEST_CONFIG = {
    enabled = true,
    test_page_id = "18451",
    test_user = "direct-test-user",
    debug_messages = true,
    force_achievements = false, -- Disabled forcing achievements
    type_mapping = {
        ["jedi"]      = "ach1",
        ["champion"]  = "ach2",
        ["sponsor"]   = "ach3",
        ["ach1"]      = "ach1",
        ["ach2"]      = "ach2",
        ["ach3"]      = "ach3",
        ["title-test"] = "dev-role",
        ["dev-role"]   = "dev-role"
    },
    test_achievements = {"title-test", "ach1", "ach2", "ach3"}
}

local function isTestPage(pageId)
    return TEST_CONFIG.enabled and tostring(pageId) == TEST_CONFIG.test_page_id
end

--------------------------------------------------------------------------------
-- Load achievement data from the JSON page
--------------------------------------------------------------------------------
function Achievements.loadData()
    mw.log("JSON-DEBUG: Starting to load achievement data")

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

    local success, data = pcall(function()
        -- Try parser cache first
        local loadDataSuccess, cachedData = pcall(function()
            return mw.loadData('Module:AchievementSystem')
        end)

        if loadDataSuccess and cachedData then
            mw.log("JSON-DEBUG: Using mw.loadData cached data")
            return cachedData
        else
            mw.log("JSON-DEBUG: mw.loadData failed or returned empty, proceeding to direct page load")
        end

        local pageTitle = mw.title.new(ACHIEVEMENT_DATA_PAGE)
        if not pageTitle or not pageTitle.exists then
            mw.log("JSON-DEBUG: " .. ACHIEVEMENT_DATA_PAGE .. " does not exist or title creation failed")
            return DEFAULT_DATA
        end

        local content = pageTitle:getContent()
        if not content or content == '' then
            mw.log("JSON-DEBUG: Page content is empty")
            return DEFAULT_DATA
        end

        mw.log("JSON-DEBUG: Raw JSON content length: " .. #content)
        local parseSuccess, parsedData = pcall(function()
            return json.decode(content)
        end)

        if not parseSuccess or not parsedData then
            mw.log("JSON-DEBUG: JSON parse failed: " .. tostring(parsedData or 'unknown error'))
            return DEFAULT_DATA
        end

        mw.log("JSON-DEBUG: Successfully loaded achievement data")
        return parsedData
    end)

    if not success or not data then
        mw.log("JSON-DEBUG: Critical error in load process: " .. tostring(data or 'unknown error'))
        data = DEFAULT_DATA
    end

    dataCache = data
    return data
end

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

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        return false
    end

    local key = tostring(pageId)
    if data.user_achievements[key] and #data.user_achievements[key] > 0 then
        return true
    end

    -- Check for legacy "n123" style
    if key:match("^%d+$") then
        local alt = "n" .. key
        if data.user_achievements[alt] and #data.user_achievements[alt] > 0 then
            return true
        end
    end

    -- We removed the forced "true" for test pages to avoid dev-role injection
    return false
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")
        mw.log("ACHIEVEMENT-NAME-ERROR: Received empty achievementType")
        return 'Unknown'
    end

    debugLog("Looking up achievement name for type: '" .. tostring(achievementType) .. "'")

    local data = Achievements.loadData()
    if not data or not data.achievement_types then
        mw.log("ACHIEVEMENT-NAME-ERROR: No achievement data or achievement_types missing")
        return achievementType
    end

    for i, typeData in ipairs(data.achievement_types) do
        if typeData.id == achievementType then
            if typeData.name and typeData.name ~= "" then
                debugLog("Found achievement: " .. typeData.id .. " with name: " .. typeData.name)
                return typeData.name
            else
                mw.log("ACHIEVEMENT-NAME-WARNING: '" .. typeData.id .. "' has no name; using ID")
                return achievementType
            end
        end
    end

    mw.log("ACHIEVEMENT-NAME-ERROR: 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 data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("No achievement data available in getTitleClass")
        return '', ''
    end

    local key = tostring(pageId)
    debugLog("Looking up achievements for ID: " .. key)

    local userAchievements = data.user_achievements[key] or {}
    if #userAchievements == 0 and key:match("^%d+$") then
        local altKey = "n" .. key
        userAchievements = data.user_achievements[altKey] or {}
    end

    if #userAchievements == 0 then
        debugLog("No achievements found for user " .. key)
        return '', ''
    end

    local highestTier = 999
    local highestAchievement = nil

    for _, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id == achType then
                local tier = typeData.tier or 999
                if tier < highestTier then
                    highestTier = tier
                    highestAchievement = typeData
                end
            end
        end
    end

    if not highestAchievement or not highestAchievement.id then
        debugLog("No valid top-tier achievement found for user " .. key)
        return '', ''
    end

    local cssClass = "achievement-" .. highestAchievement.id
    local displayName = highestAchievement.name or highestAchievement.id or "Award"
    debugLog("Using top-tier achievement: " .. cssClass .. " with name: " .. displayName)
    return cssClass, displayName
end

--------------------------------------------------------------------------------
-- Renders a simple "box" with the top-tier achievement for the user
--------------------------------------------------------------------------------
function Achievements.renderAchievementBox(pageId)
    if not pageId or pageId == '' then
        return ''
    end

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        return ''
    end

    local key = tostring(pageId)
    local userAchievements = data.user_achievements[key]
    if (not userAchievements or #userAchievements == 0) and key:match("^%d+$") then
        userAchievements = data.user_achievements["n" .. key]
    end

    if not userAchievements or #userAchievements == 0 then
        return ''
    end

    local highestTier = 999
    local topAch = nil

    for _, achievement in ipairs(userAchievements) do
        local achType = achievement.type
        for _, typeData in ipairs(data.achievement_types) do
            if typeData.id == achType then
                local tier = typeData.tier or 999
                if tier < highestTier then
                    highestTier = tier
                    topAch = typeData
                end
            end
        end
    end

    if topAch then
        return string.format(
            '<div class="achievement-box-simple" data-achievement-type="%s">%s</div>',
            topAch.id,
            htmlEncode(topAch.name or topAch.id or "")
        )
    end

    return ''
end

--------------------------------------------------------------------------------
-- 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("ACHIEVEMENT-DEBUG: Looking for '" .. tostring(achievementType) ..
             "' in page ID: " .. tostring(pageId))

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

    local data = Achievements.loadData()
    if not data or not data.user_achievements then
        debugLog("ACHIEVEMENT-DEBUG: No achievement data loaded")
        return nil
    end

    local key = tostring(pageId)
    local userAchievements = data.user_achievements[key] or {}
    if #userAchievements == 0 and key:match("^%d+$") then
        userAchievements = data.user_achievements["n" .. key] or {}
    end

    for _, achievement in ipairs(userAchievements) do
        if achievement.type == achievementType then
            return achievement
        end
    end

    -- Removed the forced injection code for test pages to avoid dev-role overrides
    debugLog("ACHIEVEMENT-DEBUG: No match found for achievement type: " .. achievementType)
    return nil
end

return Achievements