Actions

Module

Module:DataTableParserV2

From Dune Awakening DB

Revision as of 13:37, 26 March 2025 by Operator (talk | contribs)

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

-- Module:DataTableParserV2
-- Handles display and formatting of building data for Templates using External Data.
-- Data is fetched from your SQL tables (data_buildings and data_refining_recipes)
-- via the ExternalData extension rather than from a wiki table page.

local p = {}

--------------------------------------------------
-- Helper: Get icon file reference for a resource
--------------------------------------------------
local function getResourceIcon(resourceName)
    if not resourceName or resourceName == "" then
        return ""
    end
    local fileName = resourceName:gsub("%s+", "_") .. "_-_Icon.png"
    local fileTitle = mw.title.new("File:" .. fileName)
    if fileTitle and fileTitle.exists then
        return "[[File:" .. fileName .. "|20px]]"
    else
        return ""
    end
end

---------------------------------------------
-- Function: iconize
-- Adds icons to resource links in text
---------------------------------------------
function p.iconize(frame)
    local text = frame.args[1] or ""
    text = text:gsub("%[%[([^%]]+)%]%]", function(resourceName)
        local icon = getResourceIcon(resourceName)
        if icon ~= "" then
            return icon .. " [[" .. resourceName .. "]]"
        else
            return "[[" .. resourceName .. "]]"
        end
    end)
    return text
end

------------------------------------------------------
-- Function: formatComponent
-- Formats a recipe component by adding icons to resource links.
------------------------------------------------------
function p.formatComponent(text)
    if not text or text == "" then
        return ""
    end
    local components = mw.text.split(text, ";")
    local formatted = {}
    for i, component in ipairs(components) do
        component = mw.text.trim(component)
        local itemName, quantity = component:match("%[%[([^%]]+)%]%] x (%d+)")
        if itemName and quantity then
            local icon = getResourceIcon(itemName)
            table.insert(formatted, icon .. " [[" .. itemName .. "]] x " .. quantity)
        else
            table.insert(formatted, component)
        end
    end
    return table.concat(formatted, "<br>")
end

--------------------------------------------------------------------------------
-- Function: loadBuildingData
-- Uses externaldata to fetch building data from data_buildings as a wiki table.
--------------------------------------------------------------------------------
local function loadBuildingData(frame)
    local edQuery = [[
{{#get_external_data: source=externaldb
 |from=data_buildings
 |data=Name=name,Tier=building_type,Description=description,JourneyRequirement=journey_requirement,Health=health,PowerCost=power_cost,GeneratesPower=generates_power,StorageSlots=storage_slots,StorageVolume=storage_capacity,RecipeToBuild=recipe_to_build,PlacedWith=placed_with,ImageFile=image_file,AdditionalNotes=additional_notes
 |cache=yes
 |where=name='{{PAGENAME}}'
 |limit=1
}}
{| class="wikitable"
! Name
! Tier
! Description
! JourneyRequirement
! Health
! PowerCost
! GeneratesPower
! StorageSlots
! StorageVolume
! RecipeToBuild
! PlacedWith
! ImageFile
! AdditionalNotes
{{#for_external_table:|
{{!}}-
{{!}} {{{Name}}}
{{!}} {{{Tier}}}
{{!}} {{{Description}}}
{{!}} {{{JourneyRequirement}}}
{{!}} {{{Health}}}
{{!}} {{{PowerCost}}}
{{!}} {{{GeneratesPower}}}
{{!}} {{{StorageSlots}}}
{{!}} {{{StorageVolume}}}
{{!}} {{{RecipeToBuild}}}
{{!}} {{{PlacedWith}}}
{{!}} {{{ImageFile}}}
{{!}} {{{AdditionalNotes}}}
}}
|}
]]
    local content = frame:preprocess(edQuery)
    if not content or content == "" then
        return nil, "Error: No building data returned from external source!"
    end

    local lines = mw.text.split(content, "\n")
    local filtered = {}
    for _, line in ipairs(lines) do
        line = mw.text.trim(line)
        if line ~= "" and not line:match("^__") and not line:match("<noindex>") and not line:match("</noindex>") then
            table.insert(filtered, line)
        end
    end
    if #filtered < 2 then
        return nil, "Error: No valid building data found!"
    end

    local inTable = false
    local headers = {}  -- store table headers
    local colMap = {}   -- map header name to column index
    local data = {}     -- array of row arrays
    local currentRow = {}
    local readingHeader = true

    for _, line in ipairs(filtered) do
        if line:match("^{|") then
            inTable = true
        elseif line:match("^|%-") then
            if not readingHeader and #currentRow > 0 then
                table.insert(data, currentRow)
            end
            currentRow = {}
            readingHeader = false
        elseif line:match("^!") then
            local header = line:gsub("^!+%s*", "")
            header = mw.text.trim(header)
            table.insert(headers, header)
        elseif line:match("^|%+") then
            -- skip table caption
        elseif line:match("^|}") then
            if not readingHeader and #currentRow > 0 then
                table.insert(data, currentRow)
            end
            inTable = false
        elseif inTable and line:match("^|[^%-+}]") then
            local cell = line:gsub("^|+%s*", "")
            cell = mw.text.trim(cell)
            if not readingHeader then
                table.insert(currentRow, cell)
            end
        end
    end

    for i, h in ipairs(headers) do
        colMap[mw.text.trim(h)] = i
    end

    return { rows = data, colMap = colMap }, nil
end

--------------------------------------------------------------------------------
-- Function: loadRefiningData
-- Uses externaldata to fetch refining recipes from data_refining_recipes.
--------------------------------------------------------------------------------
local function loadRefiningData(frame)
    local edQuery = [[
{{#get_external_data: source=externaldb
 |from=data_refining_recipes
 |data=Refiner=Refiner,Output=Output,Ingredients=Ingredients,Time=Time,Recipe=Recipe
 |cache=yes
 |where=Refiner='{{PAGENAME}}'
}}
{| class="wikitable"
! Refiner
! Output
! Ingredients
! Time
! Recipe
{{#for_external_table:|
{{!}}-
{{!}} {{{Refiner}}}
{{!}} {{{Output}}}
{{!}} {{{Ingredients}}}
{{!}} {{{Time}}}
{{!}} {{{Recipe}}}
}}
|}
]]
    local content = frame:preprocess(edQuery)
    if not content or content == "" then
        return nil, "Error: No refining data returned from external source!"
    end

    local lines = mw.text.split(content, "\n")
    local filtered = {}
    for _, line in ipairs(lines) do
        line = mw.text.trim(line)
        if line ~= "" and not line:match("^__") and not line:match("<noindex>") and not line:match("</noindex>") then
            table.insert(filtered, line)
        end
    end
    if #filtered < 2 then
        return nil, "Error: No valid refining data found!"
    end

    local inTable = false
    local headers = {}
    local colMap = {}
    local data = {}
    local currentRow = {}
    local readingHeader = true

    for _, line in ipairs(filtered) do
        if line:match("^{|") then
            inTable = true
        elseif line:match("^|%-") then
            if not readingHeader and #currentRow > 0 then
                table.insert(data, currentRow)
            end
            currentRow = {}
            readingHeader = false
        elseif line:match("^!") then
            local header = line:gsub("^!+%s*", "")
            header = mw.text.trim(header)
            table.insert(headers, header)
        elseif line:match("^|%+") then
            -- skip caption
        elseif line:match("^|}") then
            if not readingHeader and #currentRow > 0 then
                table.insert(data, currentRow)
            end
            inTable = false
        elseif inTable and line:match("^|[^%-+}]") then
            local cell = line:gsub("^|+%s*", "")
            cell = mw.text.trim(cell)
            if not readingHeader then
                table.insert(currentRow, cell)
            end
        end
    end

    for i, h in ipairs(headers) do
        colMap[mw.text.trim(h)] = i
    end

    if #data == 0 then
        return nil, "Error: No data rows found in refining data!"
    end

    return { rows = data, colMap = colMap }, nil
end

--------------------------------------------------------------------------------
-- Function: getBuildingData
-- Returns the row (and colMap) for a given building name from external data.
--------------------------------------------------------------------------------
function p.getBuildingData(buildingName, frame)
    local buildingData, err = loadBuildingData(frame)
    if not buildingData then
        return nil, err
    end

    local colMap = buildingData.colMap
    local rows = buildingData.rows

    local nameCol = colMap["Name"]
    if not nameCol then
        return nil, "Error: 'Name' column not found in building data"
    end

    for _, fields in ipairs(rows) do
        local name = fields[nameCol]
        if name == buildingName then
            return fields, colMap
        end
    end
    return nil, "Building '" .. buildingName .. "' not found"
end

--------------------------------------------------------------------------------
-- Function: getRefiningRecipes
-- Returns a table of refining recipes matching the building name.
--------------------------------------------------------------------------------
function p.getRefiningRecipes(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local refiningData, err = loadRefiningData(frame)
    if not refiningData then
        return "Error loading refining data: " .. (err or "Unknown error")
    end

    local rows = refiningData.rows
    local resourceClassCol = 1  -- Resource Class (if applicable)
    local nameCol = 2           -- Output Name
    local timeCol = 4           -- Time
    local ingredientsCol = 3    -- Ingredients
    local recipeCol = 5         -- Recipe (if needed)

    local function normalizeText(text)
        if not text then return "" end
        text = text:gsub('["\']', '')
        text = text:gsub("%[%[([^%]]+)%]%]", "%1")
        return mw.text.trim(text)
    end

    local matchingRecipes = {}
    for _, row in ipairs(rows) do
        local refinerNeeded = row[1] or ""
        if normalizeText(refinerNeeded) == normalizeText(buildingName) then
            table.insert(matchingRecipes, row)
        end
    end

    if #matchingRecipes == 0 then
        return '<tr><td colspan="3" style="text-align:center;">No refining recipes found for this building.</td></tr>'
    end

    local output = "<tr>\n" ..
                   "<th style=\"text-align:left;\">Output</th>\n" ..
                   "<th style=\"text-align:left;\">Ingredients</th>\n" ..
                   "<th style=\"text-align:left;\">Craft Time</th>\n" ..
                   "</tr>\n"
    for _, recipe in ipairs(matchingRecipes) do
        local outputItem = recipe[nameCol] or ""
        local ingredients = recipe[ingredientsCol] or ""
        local time = recipe[timeCol] or ""
        local recipeQty = recipe[recipeCol] or ""
        local qty = "1"
        if recipeQty ~= "" then
            local extractQty = recipeQty:match("x%s*(%d+)")
            if extractQty then
                qty = extractQty
            end
        end
        local formattedOutput = p.iconize({args = {[1] = outputItem}})
        if qty ~= "1" then
            formattedOutput = formattedOutput .. " × " .. qty
        end
        local formattedIngredients = ""
        if ingredients ~= "" then
            local ingredientsList = mw.text.split(ingredients, ";")
            for i, ingredient in ipairs(ingredientsList) do
                local trimmedIngredient = mw.text.trim(ingredient)
                if trimmedIngredient ~= "" then
                    local formattedIngredient = p.iconize({args = {[1] = trimmedIngredient}})
                    if i > 1 then
                        formattedIngredients = formattedIngredients .. "<br>"
                    end
                    formattedIngredients = formattedIngredients .. formattedIngredient
                end
            end
        end
        output = output .. "<tr>\n" ..
                 "<td style=\"text-align:left;\">" .. formattedOutput .. "</td>\n" ..
                 "<td style=\"text-align:left;\">" .. formattedIngredients .. "</td>\n" ..
                 "<td style=\"text-align:left;\">" .. time .. "</td>\n" ..
                 "</tr>\n"
    end

    return output
end

--------------------------------------------------------------------------------
-- Function: formatBuilding
-- Formats building data as a template call to BuildingRefinerDisplayV2.
--------------------------------------------------------------------------------
function p.formatBuilding(frame)
    local buildingName = frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text
    if not buildingName then
        return "Error: No building name provided"
    end

    local building, colMap = p.getBuildingData(buildingName, frame)
    if not building then
        return "Error: Building '" .. buildingName .. "' not found"
    end

    local params = {
        Name = building[colMap["Name"]] or "",
        Tier = building[colMap["Tier"]] or "",
        Description = p.iconize({args = {[1] = building[colMap["Description"]] or ""}}),
        JourneyRequirement = p.iconize({args = {[1] = building[colMap["JourneyRequirement"]] or ""}}),
        Health = building[colMap["Health"]] or "",
        EnergyConsumption = building[colMap["PowerCost"]] or "",
        GeneratesPower = building[colMap["GeneratesPower"]] or "",
        StorageSlots = building[colMap["StorageSlots"]] or "",
        StorageVolume = building[colMap["StorageVolume"]] or "",
        Components = p.formatComponent(building[colMap["RecipeToBuild"]] or ""),
        PlacedWith = p.iconize({args = {[1] = building[colMap["PlacedWith"]] or ""}}),
        ImageFile = building[colMap["ImageFile"]] or "",
        AdditionalNotes = p.iconize({args = {[1] = building[colMap["AdditionalNotes"]] or ""}}),
        PrimarySource = "Crafting"
    }

    local templateCall = "{{BuildingRefinerDisplayV2"
    for key, value in pairs(params) do
        if value and value ~= "" then
            templateCall = templateCall .. "\n|" .. key .. "=" .. value
        end
    end
    templateCall = templateCall .. "\n}}"
    return frame:preprocess(templateCall)
end

--------------------------------------------------------------------------------
-- Function: displayBuilding
-- A wrapper function that returns the fully formatted building display.
--------------------------------------------------------------------------------
function p.displayBuilding(frame)
    return p.formatBuilding(frame)
end

--------------------------------------------------------------------------------
-- Function: getYoutubeEmbed
-- Returns a processed YouTube embed tag.
--------------------------------------------------------------------------------
function p.getYoutubeEmbed(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local width = frame.args.width or 400
    local height = frame.args.height or 300
    local dimensions = width .. "x" .. height

    local buildingData, err = loadBuildingData(frame)
    if not buildingData then
        return "Error loading data"
    end

    local rows = buildingData.rows
    local building = nil
    for _, row in ipairs(rows) do
        if row[1] == buildingName then
            building = row
            break
        end
    end

    if not building or not building[12] or building[12] == "-" or building[12] == "" then
        return "Coming Soon"
    end

    local youtubeUrl = building[12]
    local videoId = youtubeUrl:match("v=([%w-_]+)")
    if not videoId then
        return "Coming Soon"
    end

    local ytMarkup = string.format('<youtube dimensions="%s" alignment="center">%s</youtube>', dimensions, videoId)
    return frame:preprocess(ytMarkup)
end

--------------------------------------------------------------------------------
-- Function: breadcrumb
-- Builds a breadcrumb trail for the current building.
--------------------------------------------------------------------------------
function p.breadcrumb(frame)
    local buildingData, err = loadBuildingData(frame)
    if not buildingData then
        return err
    end

    local rows = buildingData.rows
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text

    for _, row in ipairs(rows) do
        if row[1] == buildingName then
            local cat1 = row[11] or ""  -- adjust index if needed
            local cat2 = row[12] or ""
            local cat3 = row[13] or ""
            local function cleanCategory(cat)
                if not cat or cat == "-" then return "" end
                if cat:find("%[%[Category:") then
                    cat = cat:match("%[%[Category:%s*(.-)%s*%]%]")
                elseif cat:find("%[%[") then
                    cat = cat:match("%[%[(.-)%]%]")
                end
                return mw.text.trim(cat)
            end
            cat1 = cleanCategory(cat1)
            cat2 = cleanCategory(cat2)
            cat3 = cleanCategory(cat3)
            local breadcrumbParts = {}
            if cat1 ~= "" then table.insert(breadcrumbParts, "[[" .. cat1 .. "]]") end
            if cat2 ~= "" then table.insert(breadcrumbParts, "[[" .. cat2 .. "]]") end
            if cat3 ~= "" then table.insert(breadcrumbParts, "[[" .. cat3 .. "]]") end
            table.insert(breadcrumbParts, "'''" .. buildingName .. "'''")
            return table.concat(breadcrumbParts, " > ")
        end
    end

    return "Breadcrumb not available"
end

--------------------------------------------------------------------------------
-- Function: getCategory3
-- Returns the Category3 field for the current building.
--------------------------------------------------------------------------------
function p.getCategory3(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local buildingData, err = loadBuildingData(frame)
    if not buildingData then
        return "Buildings"
    end
    local rows = buildingData.rows
    for _, row in ipairs(rows) do
        if row[1] == buildingName then
            local cat3 = row[13] or ""
            if cat3 == "-" or cat3 == "" then
                return "Buildings"
            end
            if cat3:find("%[%[") then
                cat3 = cat3:match("%[%[(.-)%]%]")
            end
            return mw.text.trim(cat3)
        end
    end
    return "Buildings"
end

--------------------------------------------------------------------------------
-- Function: relatedBuildings
-- Finds and formats related buildings based on Category3.
--------------------------------------------------------------------------------
function p.relatedBuildings(frame)
    local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
    local buildingData, err = loadBuildingData(frame)
    if not buildingData then
        return "Error loading building data"
    end
    local rows = buildingData.rows
    local targetCategory = nil
    for _, row in ipairs(rows) do
        if row[1] == buildingName then
            targetCategory = row[13] or ""
            if targetCategory == "-" or targetCategory == "" then
                return "No related buildings found"
            end
            if targetCategory:find("%[%[") then
                targetCategory = targetCategory:match("%[%[(.-)%]%]")
            end
            targetCategory = mw.text.trim(targetCategory)
            break
        end
    end
    if not targetCategory then
        return "Building not found"
    end

    local relatedBuildings = {}
    for _, row in ipairs(rows) do
        if row[1] ~= buildingName then
            local category = row[13] or ""
            if category ~= "-" and category ~= "" then
                if category:find("%[%[") then
                    category = category:match("%[%[(.-)%]%]")
                end
                category = mw.text.trim(category)
                if category == targetCategory then
                    table.insert(relatedBuildings, row)
                end
            end
        end
    end

    if #relatedBuildings == 0 then
        return "No other " .. targetCategory .. " found"
    end

    local output = '<table class="infobox-dune" style="width:100%">\n'
    output = output .. "<tr>\n"
    output = output .. "<th style=\"text-align:left;\">Name</th>\n"
    output = output .. "<th style=\"text-align:left;\">Tier</th>\n"
    output = output .. "<th style=\"text-align:left;\">Description</th>\n"
    output = output .. "</tr>\n"
    for _, building in ipairs(relatedBuildings) do
        local name = building[1] or ""
        local tier = building[2] or ""
        local description = building[3] or ""
        if tier == "-" then tier = "" end
        if description == "-" then description = "" end
        local nameWithIcon = "[[" .. name .. "]]"
        local iconFile = building[11] or ""
        if iconFile ~= "-" and iconFile ~= "" then
            nameWithIcon = "[[File:" .. iconFile .. "|20px]] [[" .. name .. "]]"
        end
        output = output .. "<tr>\n"
        output = output .. "<td>" .. nameWithIcon .. "</td>\n"
        output = output .. "<td>" .. tier .. "</td>\n"
        output = output .. "<td>" .. description .. "</td>\n"
        output = output .. "</tr>\n"
    end
    output = output .. "</table>"
    return output
end

return p