Module:Data

From The Alchemist Code Wiki
Jump to: navigation, search

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

-- <nowiki>
local p = {}
local h = {}
local cargo = require('Module:CargoUtil')
local yesno = require('Module:Yesno')
local dataModel = require("Module:Data/Model")
local util_text = require('Module:TextUtil')
local split = util_text.split
local gsplit = util_text.gsplit
local trim = util_text.trim

local langs = {'english', 'japanese'}
local locPrefixes = {
	['Ability'] = true,
	['Artifact'] = true,
	['ConceptCard'] = true,
	['Item'] = true,
	['Job'] = true,
	['Skill'] = true,
	['Unit'] = true,
}
function h.getEntityType(args)
	local name = args.name or mw.title.getCurrentTitle().prefixedText
	return name:match('Data:Game/MasterParam/([^/]+)/.+')
end

function p.displayLocs(data)
	local locs = data.loc
	if not locs then return end
	local entityType = h.getEntityType(data)
	if not entityType or not locPrefixes[entityType] then return end

	local definitions = mw.loadData('Module:CargoDeclare/'..entityType..'Loc')

	-- HOTFIX: "otherdesc"
	local temp = {}
	for param, obj in pairs(locs) do
		if param == 'otherdesc' then
			param = 'desc_ot'
		end
		for lang, value in pairs(obj) do
			temp[param] = temp[param] or {}
			temp[param][lang] = value
		end
	end
	locs = temp

	-- Build localization wikitable
	local wikitable = mw.html.create('table'):addClass('wikitable')
	local tr = wikitable:tag('tr')
	tr:tag('th'):wikitext('param')
	for _, lang in ipairs(langs) do
		tr:tag('th'):wikitext(lang)
	end
	for _, definition in ipairs(definitions) do
		local field = definition.field
		if field and locs[field] then
			tr = wikitable:tag('tr')
			tr:tag('th'):wikitext(field)
			for _, lang in ipairs(langs) do
				tr:tag('td'):wikitext(locs[field][lang])
			end
		end
	end
	return tostring(wikitable)
end

function p.storeLocs(nocargo, data)
	local locs = data.loc
	if not locs then return end
	local entityType = h.getEntityType(data)
	if not entityType or not locPrefixes[entityType] then return end
	
	local iname = (data.gl or data.jp or {}).iname
	local definitions = mw.loadData('Module:CargoDeclare/'..entityType..'Loc')

	-- HOTFIX: "otherdesc"
	local temp = {}
	for param, obj in pairs(locs) do
		if param == 'otherdesc' then
			param = 'desc_ot'
		end
		for lang, value in pairs(obj) do
			temp[param] = temp[param] or {}
			temp[param][lang] = value
		end
	end
	locs = temp

	local rows = {}
	for _, definition in ipairs(definitions) do
		local field = definition.field
		if field and locs[field] then
			for lang, value in pairs(locs[field]) do
				rows[lang] = rows[lang] or {
					_table = entityType .. 'Loc',
					iname = iname,
					lang = lang,
				}
				rows[lang][field] = value
			end
		end
	end

	local result = {}
	-- Store localization
	if not yesno(nocargo) then
		for _, row in pairs(rows) do
			mw.logObject(row)
			result[#result+1] = cargo.store(row)
		end
	end
	return table.concat(result)
end

function h.getTitlePath()
    local curTitle = mw.title.getCurrentTitle()
    --if curTitle.prefixedText == "Module:Data" then curTitle = mw.title.new("Game/MasterParam/Unit/UN_V2_LOGI", "Data") end
    local titleNodes = {curTitle.subpageText}
    while curTitle.baseText ~= curTitle.text do
        curTitle = curTitle.basePageTitle
        table.insert(titleNodes, 1, curTitle.subpageText)
    end
    return titleNodes
end
-- Generates table for data page
function h.insertQueryResult(result, title, tbl)
    local fields = {}
    local found = {}
    local count = 0
    local hideCols = true
    for i, row in pairs(tbl) do
        count = count + 1
        for col in ipairs(row) do 
            if found[col] == nil then
                table.insert(fields, col)
                found[col] = true
            end
        end
        for col in pairs(row) do 
            if found[col] == nil then
                table.insert(fields, col)
                found[col] = true
                hideCols = false
            end
        end
    end
    table.sort(fields, function(a, b) return a == "server" or a == "lang" or (b ~= "server" and b ~= "lang" and (a or '') < (b or '')) end)
    if count == 0 then return nil end
    if title ~= nil then table.insert(result, "==="..title.."===\n") end
    table.insert(result, '{| class="wikitable"')
    for i, col in ipairs(fields) do
        table.insert(result, "\n|-")
        if not hideCols then
            table.insert(result, "\n! ")
            table.insert(result, col)
        end
        
        -- Merge matching rows
        local allSame = true
        local testVal = tbl[next(tbl)][col]
        for j, row in pairs(tbl) do
            if type(testVal) == 'table' and type(row[col]) == 'table' then
                for k, v in pairs(testVal) do if v ~= row[col][k] then allSame = false end end
                for k, v in pairs(row[col]) do if v ~= testVal[k] then allSame = false end end
            elseif row[col] ~= testVal then allSame = false end
        end
        
        local function writeVal(val)
            if type(val) == "table" then
                table.insert(result, "\n")
                h.insertQueryResult(result, nil, {val})
            else
                table.insert(result, tostring(val or ''))
            end
        end
        
        if allSame then
            table.insert(result, '\n| colspan="'..count..'" | ')
            writeVal(testVal)
        else
            for j, row in pairs(tbl) do
                table.insert(result, "\n| ")
                writeVal(row[col])
            end
        end
    end
    table.insert(result, "\n|}\n")
end
p.insertQueryResult = h.insertQueryResult

-- Will only actually perform the declares on the CargoTable template pages.
function p.init(frame)
    local args = require('Module:Arguments').getArgs(frame, {parentFirst=true})
    return p._init(args)
end
function p._init(args)
    local declareStatements = {}
    local recFunc
    function recFunc(node)
        if node.nodes ~= nil then for k, child in pairs(node.nodes) do recFunc(child) end end
        if node.model ~= nil then for tbl, model in pairs(node.model) do
            local statement = {"", _table = tbl, server = "String"}
            for col, colModel in pairs(model) do
                if type(colModel) == "string" then colModel = {colModel} end
                local modelType = colModel[1]
                if colModel.list ~= nil then modelType = "List ("..colModel.list..") of "..modelType end
                statement[col] = modelType
            end
            declareStatements[tbl] = statement
        end end
    end
    recFunc(dataModel)

    local curTitle = mw.title.getCurrentTitle()
    if args[1] then
        curTitle = mw.title.new(args[1])
    end
    if curTitle.basePageTitle.prefixedText == "Template:CargoTable" then
        local statement = declareStatements[curTitle.subpageText]
        if statement == nil then return "Invalid table "..curTitle.subpageText end
        local tbl = {'{{#cargo_declare:', '}}'}
        for k,v in pairs(statement) do
            if k ~= 1 then
                tbl[#tbl+1] = '| ' .. k..' = '..v
            end
        end
        table.sort(tbl)
        local declare = table.concat(tbl, '\n')
        if yesno(args.debug) then
            return tostring(mw.html.create('pre'):wikitext(declare))
        else
            mw.log(declare)
            return mw.getCurrentFrame():callParserFunction('#cargo_declare', statement)
        end
    end
    if mw.getCurrentFrame():getParent() == nil then
        return nil
    end
    return "asdf"
end

-- Called from Data pages
function p.insert(frame)
    local args = require('Module:Arguments').getArgs(frame, {parentFirst = true})
    return p._insert(args)
end
function p._insert(args)
    local nocargo = args.nocargo
    local curNode = {nodes = {Game = dataModel}}
    local titlePath = h.getTitlePath()
    for i, pathItem in ipairs(titlePath) do
        if curNode == nil then return nil end
        if i == #titlePath then break end
        if curNode.nodes == nil then return nil end
        curNode = curNode.nodes[pathItem]
    end
    
    if curNode.model == nil then return nil end
    -- Now that we know this is a valid page
    local result = {}

    if next(curNode.model) ~= nil then table.insert(result, "==Tables==\nThe following tables have these rows defined by this data entry.\n") end

    -- Decode JSON
    local jsonSets = {}
    for k, v in pairs(args) do
    	if type(v) == 'string' then
	    	v = mw.text.decode(mw.text.unstripNoWiki(v))
	    	local success, result = pcall(mw.text.jsonDecode, v)
	    	if success then
	    		v = result
	    	end
	    	args[k] = v
	    end
    end
    local content_sha256s = {}
    local suffix = '_sha256'
    for server, json in pairs(args) do
        if server == 'gl' or server == 'jp' or server == 'loc' then
            jsonSets[server] = json
        elseif string.sub(server, -string.len(suffix)) == suffix then
            -- TODO: Store this in a cargo table somewhere.
            content_sha256s[string.sub(server, 1, -string.len(suffix))] = json
        end
    end
    
    -- Convert Functions
    local convertFrom = {
        table = function(val, model)
            if model.list ~= nil then
                return table.concat(val, model.list)
            end
            error("JSON data for "..model[1].." is a table but is not modeled to handle it.")
        end,
    }
    
    -- Insert Rows
    local where = '_pageId='..mw.title.getCurrentTitle().id
    local errors = {}
    
    -- Do loc first
    result[#result+1] = p.storeLocs(nocargo, args)
    result[#result+1] = p.displayLocs(args)

    local locSets = jsonSets.loc or {}
    jsonSets.loc = nil
    local expected = 0
    table.insert(result, '\n'..mw.getCurrentFrame():expandTemplate{ title = "CargoTable/Loc" })

    for param, json in pairs(locSets) do
        if type(json) == "table" then
            for lang, value in pairs(json) do
                table.insert(result, cargo.store{
                    _table = 'Loc',
                    lang = lang,
                    param = param,
                    value = value,
                })
                expected = expected + 1
            end
        end
    end
    -- Query loc table
    local rows = cargo.query{
        tables = 'Loc',
        fields = 'lang, param, value',
        where = where,
        orderBy = 'lang'
    }
    -- Check if expected row count matches actual row count.
    local actual = #rows
    if actual ~= expected then
        table.insert(result, string.format('Entry count mismatch in table %q (%s expected, got %s)\n', 'Loc', expected, actual))
        errors.cargoStore = true
    end
    
    -- Generate loc tables from queried data
    local queryResult = {}
    for i, row in ipairs(rows) do
        if row.lang == 'english' and row.param == 'name' then
            local pagename = string.gsub(row.value, "[%[%]]", {
                ["["] = "【",
                ["]"] = "】",
            })
            if pagename ~= row.value then
                row.value = '[[' .. pagename .. '|' .. row.value .. ']]'
            else
                row.value = '[[' .. row.value .. ']]'
            end
        end
        queryResult[i] = row
    end
    h.insertQueryResult(result, tbl, queryResult)
    
    local iname
    for server, json in pairs(jsonSets) do
        if not iname and json.iname then
            iname = json.iname
        end
    end

    local actualTitle = table.concat(titlePath, '/', 1, 3)
    mw.logObject(actualTitle, 'actualTitle')
    if actualTitle == 'Game/MasterParam/Buff' then
    	local definitions = mw.loadData('Module:CargoDeclare/BuffDetail')
        for server, json in pairs(jsonSets) do
            for i=1,11 do
            	local values = {
            		_table = 'BuffDetail',
            	}
                for _, definition in ipairs(definitions) do
                	local field = definition.field
                	if definition.type == 'Integer' then
	                	values[field] = tonumber(json[field..i])
	                else
	                	values[field] = json[field..i]
	                end
                end
            	values.server = server
            	values.buff_iname = iname
            	values.idx = i
                if (values.type or 0) <= 0 then
                    mw.logObject(values, "Skipped insert into BuffDetail values")
                else
                	mw.logObject(values, "#cargo_store")
                    table.insert(result, cargo.store(values))
                end
            end
        end
    end

    if actualTitle == 'Game/MasterParam/Job' then
    	local definitions = mw.loadData('Module:CargoDeclare/JobRank')
    	local jobRanks = {}
        for server, json in pairs(jsonSets) do
        	for i, rank in ipairs(json.ranks) do
        		local values = {
        			_table = 'JobRank',
        		}
                for _, definition in ipairs(definitions) do
                	local field = definition.field
                	if rank[field] ~= nil then
                		if definition.type == 'Integer' then
	                		values[field] = tonumber(rank[field])
		                else
		                	values[field] = rank[field]
		                end
		                rank[field] = nil -- remove
	                end
                end
            	values.server = server
            	values.job_iname = iname
            	values.idx = i
            	mw.logObject(values, "#cargo_store")
                table.insert(result, cargo.store(values))
            	values._table = nil
            	jobRanks[i] = jobRanks[i] or {}
            	jobRanks[i][#jobRanks[i]+1] = values
            end
            json.ranks = nil -- done
        end
		table.insert(result, '=== JobRank ===\n')
        for i, rank in ipairs(jobRanks) do
		    h.insertQueryResult(result, nil, rank)
		end
    end

    -- Now get the data
    for tbl, model in pairs(curNode.model) do
        table.insert(result, mw.getCurrentFrame():expandTemplate{ title = "CargoTable/"..tbl })
        expected = 0
        for server, json in pairs(jsonSets) do
            local statement = {_table = tbl, server = server}
            for col, colModel in pairs(model) do
                if type(colModel) == "string" then colModel = {colModel} end
                -- Extract value
                local jsonVal = json[colModel.from or col]
                json[colModel.from or col] = nil
                -- Convert datatypes here
                local converter = convertFrom[type(jsonVal)]
                if converter ~= nil then jsonVal = converter(jsonVal, colModel) end
                -- Append to statement
                statement[col] = jsonVal
            end
            mw.logObject(statement, 'statement')
            table.insert(result, cargo.store(statement))
            expected = expected + 1
        end

        -- Query the data
        local query = {
            tables = tbl,
            fields = {"server"},
            where = where,
            orderBy = 'server',
        }
        for col in pairs(model) do
            table.insert(query.fields, col)
        end
        local rows = cargo.query(query)

        local actual = #rows
        if actual ~= expected then
            table.insert(result, string.format('Entry count mismatch in table %q (%s expected, got %s)\n', tbl, expected, actual))
            errors.cargoStore = true
        end
        
        -- Generate tables from queried data
        queryResult = {}
        for i, row in ipairs(rows) do
            for col, val in pairs(row) do
                if type(model[col]) == "table" then
                    if model[col].list ~= nil then
                        val = #val == 0 and {} or split(val, model[col].list, true)
                    end
                    if model[col].dataRef ~= nil then
                        if type(val) == "table" then
                            for k, v in pairs(val) do
                                if not v:find('__HIDE__$') then
                                    val[k] = "[[Data:Game/"..model[col].dataRef.."/"..v.."|"..v.."]]"
                                else
                                    val[k] = v
                                end
                            end
                        elseif #val > 0 and not val:find('__HIDE__$') then
                            val = "[[Data:Game/"..model[col].dataRef.."/"..val.."|"..val.."]]"
                        end
                    end
                end
                row[col] = val
            end
            queryResult[i] = row
        end
        h.insertQueryResult(result, tbl, queryResult)
    end

    local hasEffects = false
    local jsonEffects = {}
    for server, json in pairs(jsonSets) do
        if json.effects then
            hasEffects = true
            jsonEffects[server] = {effects = {}}
            for i,effect in ipairs(json.effects) do
                local values = {}
                values._table = 'ConceptCardEffect'
                values.server = server
                values.cc_iname = iname
                jsonEffects[server].effects[i] = {}
                for k,v in pairs(effect) do
                    values[k] = v
                    jsonEffects[server].effects[i][k] = v
                    effect[k] = nil
                end
                table.insert(result, cargo.store(values))
                jsonEffects[server].server = server
            end
            -- A table of failed effect insertions.
            local errorEffects = {}
            for i, effect in ipairs(json.effects) do
                local empty = true
                for k, v in pairs(effect) do empty = false end
                if not empty then
                    errorEffects[#errorEffects+1] = effect
                end
            end
            json.effects = #errorEffects > 0 and errorEffects or nil
        end
    end
    
    if next(jsonEffects) then
        table.insert(result, '===ConceptCardEffect===\n')
        local toInsert = {}
        for k in pairs(jsonEffects) do table.insert(toInsert, k) end
        table.sort(toInsert)
        for i, k in ipairs(toInsert) do toInsert[i] = jsonEffects[k] end
        h.insertQueryResult(result, nil, toInsert)
    end

    local buffDetailRows = cargo.query{
        tables = 'BuffDetail',
        fields = 'buff_iname, server, idx, calc, type, tktag, vini, vmax, vone',
        where = where,
        orderBy = 'idx'
    }
    h.insertQueryResult(result, 'BuffDetail', buffDetailRows)

    
    -- Display unused keys here
    local unused = false
    for server, json in pairs(jsonSets) do if next(json) ~= nil then
        unused = true
        json.server = server
    end end
    if unused then
        table.insert(result, "===Unused Keys===\nThe following keys were not used by inserted into any table and are thus unused.\n")
        local toInsert = {}
        for k in pairs(jsonSets) do table.insert(toInsert, k) end
        table.sort(toInsert)
        for i, k in ipairs(toInsert) do toInsert[i] = jsonSets[k] end
        h.insertQueryResult(result, nil, toInsert)
    end
    
    table.insert(result, '[[Category:Data pages]]') -- Tracking category.
    -- Handle errors encountered
    if errors.cargoStore then
        table.insert(result, '[[Category:Data pages that failed a table insert]]' )
    end
    if hasEffects then
        table.insert(result, '[[Category:Data pages with effects]]')
    end

    return table.concat(result)
end

-- Add functions to model
p.model = (function()
    dataModel.tables = {}
    local recFunc
    function recFunc(node)
        if node.nodes ~= nil then for k, child in pairs(node.nodes) do recFunc(child) end end
        if node.model ~= nil then for tbl, model in pairs(node.model) do dataModel.tables[tbl] = model end end
    end
    recFunc(dataModel)
    
    -----------
    -- Query --
    -----------
    dataModel.query = function(tables, fields, args)
        local query = args
        query.tables = tables
        query.fields = fields
        local result = cargo.query(query)
        local toCast = {}
        if type(query.tables) == 'string' then
            query.tables = split(query.tables, ',', true)
        end
        if type(query.fields) == 'string' then
            query.fields = split(query.fields, ',', true)
        end
        for _, field in ipairs(query.fields) do
            -- Extract alias
            local t = split(field, '=', true)
            local alias = trim(t[2] or t[1])
            
            local fnMatch = string.match(t[1], "([^()]*)[(](.*)[)](.*)")
            if fnMatch ~= nil then
                mw.log(fnMatch)
                toCast[alias] = {"String"}
            else
                -- Extract table
                local t2 = split(t[1], '.', true)
                local tbl = #t2 > 1 and trim(t2[1]) or nil
                local key = trim(t2[#t2])
                if tbl == nil then
                    for _, tblName in ipairs(query.tables) do
                        tblName = trim(tblName)
                        if (dataModel.tables[tblName] or {})[key] ~= nil then
                            tbl = tblName
                            break
                        end
                    end
                end
                local model = (dataModel.tables[tbl] or {})[key]
                toCast[alias] = type(model) == "string" and {model} or model
            end
        end
        local castFuncs = {
            Integer = tonumber
        }
        for i, row in ipairs(result) do
            for k, castTo in pairs(toCast) do
                local castFunc = castFuncs[castTo[1]] or function(v) return v end
                if castTo.list ~= nil then
                    local list = {}
                    if row[k] and row[k] ~= '' then
                        for s in gsplit(row[k], castTo.list, true) do
                            list[#list+1] = castFunc(s)
                        end
                    end
                    row[k] = list
                else
                    row[k] = castFunc(row[k])
                end
            end
        end
        return result
    end
    
    ------------
    -- getLoc --
    ------------
    dataModel.getLoc = function(_pageName, param, lang)
        if _pageName == nil then return '???' end
        local where = "_pageName='".._pageName.."' and param='"..param.."' and value<>'' and lang='"
        local thisLang = cargo.query{tables='Loc',fields='value',where=where..(lang or "english'")}
        if #thisLang == 0 then thisLang = cargo.query{tables='Loc',fields='value',where=where.."english'"} end
        if #thisLang == 0 then thisLang = cargo.query{tables='Loc',fields='value',where=where.."japanese'"} end
        if #thisLang == 0 then thisLang = {{value = ''}} end
        return thisLang[1].value
    end

    -------------
    -- Sources --
    -------------
    dataModel.sources = require("Module:Data/Sources")(dataModel)
    
    return dataModel
end)()

return p