Module:DataTableParserV2
From Dune Awakening DB
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 wikitable.
--------------------------------------------------------------------------------
local function loadBuildingData(frame)
local edQuery = [[
{{#get_external_data: source=externaldb
|from=data_buildings
|data=ID=id,
BuildingType=building_type,
Name=name,
Description=description,
PowerCost=power_cost,
GeneratesPower=generates_power,
StorageSlots=storage_slots,
StorageCapacity=storage_capacity,
SchematicRequirement=schematic_requirement,
JourneyRequirement=journey_requirement,
Health=health,
PlacedWith=placed_with,
AdditionalNotes=additional_notes,
RecipeToBuild=recipe_to_build,
ImageFile=image_file,
IconFile=icon_file,
Category1=category_1,
Category2=category_2,
Category3=category_3,
Gallery1=gallery_1,
Gallery2=gallery_2,
Gallery3=gallery_3,
Gallery4=gallery_4,
YoutubeVideoLink=youtube_video_link
|cache=yes
|where=name='{{PAGENAME}}'
|limit=1
}}
{| class="wikitable"
! ID
! Building Type
! Name
! Description
! Power Cost
! Generates Power
! Storage Slots
! Storage Capacity
! Schematic Requirement
! Journey Requirement
! Health
! Placed With
! Additional Notes
! Recipe To Build
! Image File
! Icon File
! Category 1
! Category 2
! Category 3
! Gallery 1
! Gallery 2
! Gallery 3
! Gallery 4
! YouTube Video Link
{{#for_external_table:|
{{!}}-
{{!}} {{{ID}}}
{{!}} {{{BuildingType}}}
{{!}} {{{Name}}}
{{!}} {{{Description}}}
{{!}} {{{PowerCost}}}
{{!}} {{{GeneratesPower}}}
{{!}} {{{StorageSlots}}}
{{!}} {{{StorageCapacity}}}
{{!}} {{{SchematicRequirement}}}
{{!}} {{{JourneyRequirement}}}
{{!}} {{{Health}}}
{{!}} {{{PlacedWith}}}
{{!}} {{{AdditionalNotes}}}
{{!}} {{{RecipeToBuild}}}
{{!}} {{{ImageFile}}}
{{!}} {{{IconFile}}}
{{!}} {{{Category1}}}
{{!}} {{{Category2}}}
{{!}} {{{Category3}}}
{{!}} {{{Gallery1}}}
{{!}} {{{Gallery2}}}
{{!}} {{{Gallery3}}}
{{!}} {{{Gallery4}}}
{{!}} {{{YoutubeVideoLink}}}
}}
|}
]]
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 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
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
if not colMap["Name"] then
return nil, "Error: 'Name' column not found in building data"
end
for _, fields in ipairs(rows) do
if fields[colMap["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 colMap = refiningData.colMap
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[colMap["Refiner"]] 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[colMap["Output"]] or ""
local ingredients = recipe[colMap["Ingredients"]] or ""
local time = recipe[colMap["Time"]] or ""
local recipeQty = recipe[colMap["Recipe"]] 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
-- Add debug information if debug flag is set
if frame.args.debug then
return p.debugBuildingData(frame)
end
local building, colMap = p.getBuildingData(buildingName, frame)
if not building then
return "Error: Building '" .. buildingName .. "' not found. Try adding '|debug=1' to see more information."
end
-- Check for required fields
local requiredFields = {"Name", "Description"}
for _, field in ipairs(requiredFields) do
if not colMap[field] then
return "Error: Required field '" .. field .. "' missing in database schema"
end
if not building[colMap[field]] or building[colMap[field]] == "" or building[colMap[field]] == "-" then
return "Error: Required field '" .. field .. "' is empty for building '" .. buildingName .. "'"
end
end
-- Prepare params with default values and better error handling
local params = {
Name = building[colMap["Name"]] or buildingName,
Tier = building[colMap["BuildingType"]] or "Unknown",
Description = "",
JourneyRequirement = "",
Health = building[colMap["Health"]] or "0",
EnergyConsumption = building[colMap["PowerCost"]] or "0",
GeneratesPower = building[colMap["GeneratesPower"]] or "0",
StorageSlots = building[colMap["StorageSlots"]] or "0",
StorageVolume = building[colMap["StorageCapacity"]] or "0",
Components = "",
PlacedWith = "",
AdditionalNotes = "",
ImageFile = building[colMap["ImageFile"]] or "",
PrimarySource = "Crafting"
}
-- Process fields that need special formatting
if colMap["Description"] and building[colMap["Description"]] then
params.Description = p.iconize({args = {[1] = building[colMap["Description"]]}})
end
if colMap["JourneyRequirement"] and building[colMap["JourneyRequirement"]] then
params.JourneyRequirement = p.iconize({args = {[1] = building[colMap["JourneyRequirement"]]}})
end
if colMap["RecipeToBuild"] and building[colMap["RecipeToBuild"]] then
params.Components = p.formatComponent(building[colMap["RecipeToBuild"]])
end
if colMap["PlacedWith"] and building[colMap["PlacedWith"]] then
params.PlacedWith = p.iconize({args = {[1] = building[colMap["PlacedWith"]]}})
end
if colMap["AdditionalNotes"] and building[colMap["AdditionalNotes"]] then
params.AdditionalNotes = p.iconize({args = {[1] = building[colMap["AdditionalNotes"]]}})
end
-- Safety check for functions that might fail
local success, result = pcall(function()
-- Append additional columns by calling other functions:
local refiningRecipes = p.getRefiningRecipes(frame) or "No refining recipes found"
local youtubeEmbed = p.getYoutubeEmbed(frame) or "No video available"
local category3 = p.getCategory3(frame) or "Buildings"
local relatedBuildings = p.relatedBuildings(frame) or "No related buildings found"
return {
RefiningRecipes = refiningRecipes,
YoutubeEmbed = youtubeEmbed,
Category3 = category3,
RelatedBuildings = relatedBuildings
}
end)
if success then
-- Add the results to params
for key, value in pairs(result) do
params[key] = value
end
else
-- Log the error but continue with empty values
mw.log("Error getting additional data: " .. (result or "unknown error"))
params.RefiningRecipes = "Error loading recipes"
params.YoutubeEmbed = "Error loading video"
params.Category3 = "Buildings"
params.RelatedBuildings = "Error loading related buildings"
end
-- Construct the template call
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}}"
-- Add debug info at the bottom if requested
if frame.args.verbose then
templateCall = templateCall .. "\n\n<div style='display:none'>Debug info: " ..
"Building name: " .. buildingName ..
"</div>"
end
-- Process the template
local result = frame:preprocess(templateCall)
if result == templateCall then
-- If preprocessing didn't change anything, the template might not exist
return "Error: Template 'BuildingRefinerDisplayV2' might not exist."
end
return result
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 building, colMap = p.getBuildingData(buildingName, frame)
if not building then
return "Error loading data"
end
local youtubeUrl = building[colMap["YoutubeVideoLink"]] or ""
if youtubeUrl == "" or youtubeUrl == "-" then
return "Coming Soon"
end
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: getCategory3
-- Returns the Category3 field for the current building.
--------------------------------------------------------------------------------
function p.getCategory3(frame)
local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
local building, colMap = p.getBuildingData(buildingName, frame)
if not building then
return "Buildings"
end
local cat3 = building[colMap["Category3"]] or ""
if cat3 == "-" or cat3 == "" then
return "Buildings"
end
if cat3:find("%[%[") then
cat3 = cat3:match("%[%[(.-)%]%]")
end
return mw.text.trim(cat3)
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 building, colMap = p.getBuildingData(buildingName, frame)
if not building then
return "Error loading building data"
end
local targetCategory = building[colMap["Category3"]] 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)
-- Load all building rows (assuming the externaldata query returns all rows)
local allData, err = loadBuildingData(frame)
if not allData then
return "Error loading building data"
end
local rows = allData.rows
local relatedBuildings = {}
for _, row in ipairs(rows) do
if row[colMap["Name"]] ~= buildingName then
local category = row[colMap["Category3"]] 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 _, b in ipairs(relatedBuildings) do
local name = b[colMap["Name"]] or ""
local tier = b[colMap["Tier"]] or ""
local description = b[colMap["Description"]] or ""
if tier == "-" then tier = "" end
if description == "-" then description = "" end
local nameWithIcon = "[[" .. name .. "]]"
local iconFile = b[colMap["ImageFile"]] 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
--------------------------------------------------------------------------------
-- Function: debugBuildingData
-- Better error logging
--------------------------------------------------------------------------------
function p.debugBuildingData(frame)
local buildingName = frame.args[1] or mw.title.getCurrentTitle().text
local buildingData, err = loadBuildingData(frame)
if not buildingData then
return "Error: " .. (err or "Unknown error")
end
local output = "Debug information for '" .. buildingName .. "':\n\n"
output = output .. "Raw data from database:\n"
output = output .. "<pre>"
-- Output column map
output = output .. "Column mapping:\n"
for name, idx in pairs(buildingData.colMap) do
output = output .. name .. " => " .. idx .. "\n"
end
-- Output row data
output = output .. "\nRows (" .. #buildingData.rows .. " found):\n"
for i, row in ipairs(buildingData.rows) do
output = output .. "Row " .. i .. ":\n"
for j, cell in ipairs(row) do
local colName = "Unknown"
for name, idx in pairs(buildingData.colMap) do
if idx == j then
colName = name
break
end
end
output = output .. " " .. colName .. " (" .. j .. "): '" .. cell .. "'\n"
end
end
output = output .. "</pre>"
return output
end
return p
