Module:CommonCargoQuery

From Noita Wiki
Jump to navigation Jump to search

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

local p = {}
local cargo = mw.ext.cargo

-- Scribunto (Lua main): https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual
-- Cargo Lua support: https://www.mediawiki.org/wiki/Extension:Cargo/Other_features#Lua_support

-- Small utility for removing quotes from list of single quoted IN (...) params
function p.StripQuotes(frame)
	return tostring(string.gsub(frame.args["from"], "'", ''))
end

-- Trims whitespace off of the left and right sides of a string
local function trim(str)
   return str:gsub("^%s*(.-)%s*$", "%1")
end

-- Basically is null, is empty, or is whitespace
local function is_empty(str)
	return str == nil or str.len(trim(str)) == 0
end

-- If a given string is empty or nil, return nil, otherwise give back the value
local function nil_or_value(str)
    if is_empty(str) then
    	return nil
    end
    
    return str
end

-- Normally you'd do `table.insert(table, item)` to add to the end, but this
-- does not appear to be working in this Lua implementation so here's a workaround.
local function append_table(tbl, data)
	tbl[#tbl + 1] = data
end

-- Split a string by comma, return each item trimmed in a table.
local function split(str)
    local result = {}
    for token in string.gmatch(str, "[^,]+") do
        append_table(result, trim(token))
    end
    return result
end

-- Processes an argument clause, for example spawnLocation (all of the ones provided).
--
-- frame:    The 'frame' argument provided to the entry point, used to retrieve the args.
-- argname:  The name of the argument, i.e. "spawnLocation" or "name".
-- joinType: How to join the clauses together, can be "AND" or "OR".
-- clause: The clause string, including a %s. For example "spawnLocation HOLDS '%s'", don't forget to escape % with %% for 'LIKE'.
--
-- Returns all the clauses concatenated into a string surrounded by parentheses.
local function processArgClause(frame, argName, joinType, clause)
    local arg = frame.args[argName]
    if is_empty(arg) then return nil end

    local result = {}
    local items = split(arg)
    for _, item in ipairs(items) do
        append_table(result, string.format(clause, item))
    end
    -- Joins everything together with AND or OR between each clause
    return "(" .. table.concat(result, " " .. joinType .. " ") .. ")"
end

-- Creates an entire query to use in the `where` argument of the cargo call.
--
-- frame:        The 'frame' argument provided to the entry point, used to retrieve the args.
-- queryMapping: A table with all the possible arg names, mapped to joinType and clauses. See enemyQueryMapping for an example.
--
-- Returns a string with all clauses joined by the keyword "AND".
local function createQuery(frame, queryMapping)
    local result = {}

    for argName, argData in pairs(queryMapping) do
        local clause = processArgClause(frame, argName, argData.joinType, argData.clause)

        if clause ~= nil then
            append_table(result, clause)
        end
    end
    
    return table.concat(result, " AND ")
end

-- Does all of the stuff the old template would do normally in a generic way.
-- 
-- frame:        The 'frame' argument provided to the entry point, used to retrieve the args.
-- queryMapping: A table with all the possible arg names, mapped to joinType and clauses. See enemyQueryMapping for an example.
-- tableName:    Name of the Cargo table (i.e. "Enemies").
-- args:         The args that would normally have been passed to cargo.
--
-- Returns the page contents to render.
local function doCommonQuery(frame, queryMapping, tableName, args)
	-- Cannot use 'or' here as params provided but not given a value are still in the table (as empty string?)
	local query = nil_or_value(frame.args["where"]) or createQuery(frame, queryMapping)

    if string.len(query) == 0 then
        return "''No query arguments were provided.''"
    end
    
    args.where = query
    local results = cargo.query(tableName, args.fields, args)

    -- Build the result page by instantiating all the row templates with the rows
    local pageElements = {}
    append_table(pageElements, frame.args["intro"] or "")
    
    for _, row in ipairs(results) do
        local rowText = frame:expandTemplate{ title = args.template, args = row }
        append_table(pageElements, rowText)
    end
    if #pageElements == 0 then
        return string.format("''There are no %s of the specified query.''", tableName)
    end

	append_table(pageElements, frame.args["outro"] or "")

    return table.concat(pageElements)
end

--------------------------------------------------------------------------------
---- EnemyQuery ------------------------------------------------ EnemyQuery ----
-- Double % escapes a %, %s is for string
local enemyQueryMapping = {
    ["name"] = {
        joinType = "OR",
--        clause = "name LIKE \"%%%s%%\"" -- Unable to differenciate between similar enemy variants (hurtta/heikkohurtta)
        clause = "name = \"%s\""
    },
    ["spawnLocation"] = {
        joinType = "OR",
        clause = "spawnLocation HOLDS \"%s\""
    },
    ["ngplusSpawnLocation"] = {
        joinType = "OR",
        clause = "ngplusSpawnLocation HOLDS \"%s\""
    },
    ["ngplus2SpawnLocation"] = {
        joinType = "OR",
        clause = "ngplus2SpawnLocation HOLDS \"%s\""
    },
    ["ngplus3SpawnLocation"] = {
        joinType = "OR",
        clause = "ngplus3SpawnLocation HOLDS \"%s\""
    },
    ["faction"] = {
        joinType = "OR",
        clause = "faction=\"%s\""
    },
    ["notFaction"] = {
        joinType = "AND",
        clause = "NOT faction=\"%s\""
    },
    ["category"] = {
        joinType = "OR",
        clause = "category=\"%s\""
    },
    ["bloodMaterial"] = {
        joinType = "OR",
        clause = "blood LIKE \"%%%s%%\""
    },
    ["corpseMaterial"] = {
        joinType = "OR",
        clause = "corpse LIKE \"%%%s%%\""
    },
}

-- EnemyQuery (queries Enemies table)
-- This is a module function, referenced from the second argument to #invoke.
-- i.e. {{#invoke:CommonCargoQuery|EnemyQuery|arg1=...}}
function p.EnemyQuery(frame)
	return doCommonQuery(frame, enemyQueryMapping, "Enemies", {
		fields = "_pageName,image,icon,name,alias",
		limit = 400,
		orderBy = 'faction,_pageName,name',
		template = frame.args["template"] or "EnemyQuery/row"
	})
end

--------------------------------------------------------------------------------
---- SpellQuery ------------------------------------------------ SpellQuery ----
local spellQueryMapping = {
    ["id"] = {
        joinType = "OR",
    	clause = "id=\"%s\""
    },
    ["name"] = {
        joinType = "OR",
        clause = "name=\"%s\""
    },
    ["nameLike"] = {
        joinType = "OR",
        clause = "name LIKE \"%%%s%%\""
    },
    ["names"] = { -- Deprecated - use name instead
        joinType = "OR",
        clause = "name=\"%s\""
    },
    ["type"] = {
    	joinType = "OR",
    	clause = "type=\"%s\""
    },
    ["tier"] = {
    	joinType = "OR",
    	clause = "spellTier HOLDS \"%s\""
    },
    ["tags"] = {
    	joinType = "OR",
    	clause = "tags HOLDS \"%s\""
    },
    ["allOfTags"] = {
    	joinType = "AND",
    	clause = "tags HOLDS \"%s\""
    },
    ["noneOfTags"] = {
    	joinType = "AND",
    	clause = "tags HOLDS NOT \"%s\""
    },
}

-- SpellQuery (queries Spells table)
-- This is a module function, referenced from the second argument to #invoke.
-- i.e. {{#invoke:CommonCargoQuery|SpellQuery|arg1=...}}
-- pass in fields=one,two,three to specify custom field sets to return
function p.SpellQuery(frame)
	return doCommonQuery(frame, spellQueryMapping, "Spells", {
		fields = frame.args["fields"] or "_pageName,image,name,id,description,type",
		limit = 400,
		orderBy = nil_or_value(frame.args["orderBy"]) or 'type,sortKey,_pageName',
		template = frame.args["template"] or "SpellQuery/row2",
	})
end

--------------------------------------------------------------------------------
---- MaterialQuery ------------------------------------------ MaterialQuery ----
local materialQueryMapping = {
    ["name"] = {
        joinType = "OR",
        clause = "name LIKE \"%%%s%%\""
    },
    ["id"] = {
    	joinType = "OR",
    	clause = "id=\"%s\""
    },
    ["type"] = {
    	joinType = "OR",
    	clause = "type=\"%s\""
    },
    ["tags"] = {
    	joinType = "OR",
    	clause = "tags HOLDS \"%s\""
    },
}

-- MaterialQuery (queries Spells table)
-- This is a module function, referenced from the second argument to #invoke.
-- i.e. {{#invoke:CommonCargoQuery|MaterialQuery|arg1=...}}
-- pass in fields=one,two,three to specify custom field sets to return
function p.MaterialQuery(frame)
	return doCommonQuery(frame, materialQueryMapping, "Materials", {
		fields = frame.args["fields"] or "_pageName,image,name,id,icon,pouchIcon",
		limit = frame.args["limit"] or 400,
		orderBy = 'type,name',
		template = frame.args["template"] or "MaterialQuery/row",
	})
end

--------------------------------------------------------------------------------
---- PerkQuery -------------------------------------------------- PerkQuery ----
-- Double % escapes a %, %s is for string
local perkQueryMapping = {
    ["Name"] = {
        joinType = "OR",
--        clause = "Name LIKE \"%%%s%%\"" -- Unable to differenciate between similar enemy variants (hurtta/heikkohurtta)
        clause = "Name = \"%s\""
    },
    ["Categories"] = {
        joinType = "OR",
        clause = "Categories HOLDS \"%s\""
    },
    ["notCategories"] = {
        joinType = "AND",
        clause = "NOT Categories HOLDS \"%s\""
    },
    ["OneOff"] = {
        joinType = "OR",
        clause = "OneOff = \"%s\""
    },
    ["PlayerOnly"] = {
        joinType = "OR",
        clause = "PlayerOnly = \"%s\""
    },
    ["Stackable"] = {
        joinType = "OR",
        clause = "Stackable = \"%s\""
    },
    ["PerkPool"] = {
        joinType = "OR",
        clause = "PerkPool = \"%s\""
    },
    ["MaxStack"] = {
        joinType = "OR",
        clause = "MaxStack = \"%s\""
    },
    ["PoolMax"] = {
        joinType = "OR",
        clause = "PoolMax = \"%s\""
    },
    ["ReappearsAfter"] = {
        joinType = "OR",
        clause = "ReappearsAfter = \"%s\""
    },
}

-- PerkQuery (queries Perks table)
-- This is a module function, referenced from the second argument to #invoke.
-- i.e. {{#invoke:CommonCargoQuery|PerkQuery|arg1=...}}
function p.PerkQuery(frame)
	return doCommonQuery(frame, perkQueryMapping, "Perks", {
		fields = "_pageName,Image,Name",
		limit = 400,
		orderBy = ',_pageName,Name',
		template = "PerkQuery/row"
	})
end

return p