Module:Sandbox/KickahaOta/QueryDistances

From Path of Exile Wiki
Revision as of 20:06, 19 December 2021 by KickahaOta (talk | contribs) (KickahaOta moved page Module:Sandbox/KickahaOta/CustomCalc to Module:Sandbox/KickahaOta/QueryDistances without leaving a redirect: Better name)
Jump to navigation Jump to search
Module documentation[create] [purge]
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

local m_calculate = {}

function m_calculate.calculate_distances(query_results, args)
--[[
Computes and inserts a new column into a cargo query results table. This column will contain the distance between each
  row in the table and a specified origin row. (The distance value will be an integer. The specified origin row
  will have a distance value of 0; the rows connected to that row will have a distance value of 1; the rows
  connected to those rows will have a distance value of 2; and so on.)

Takes 2 arguments:
    query_results: table; required. Table returned from m_cargo.query
    args - table; required. Association table of arguments:
        id_field - string; required. The name of the field containing a row's id for connection purposes.
            Example: "passive_skills.id".
        connection_ids_field - string; required. The name of the field containing the ids of the rows that a given row connects to.
            Example: "passive_skills.connections".
            The contents of this field can either be an array of ids or a string containing a delimited list of ids.
        connection_ids_delimiter - string; the delimeter separating ids in the connection_ids_field field.
            Example: ",".
            This parameter is required if the connection_ids_field field contains a string. It is optional and ignored
              if the field contains an array.
        origin_row_id - string; required. The id of a row that should be used as the starting point of the calculation.
            Example: "AscendancyBerserkerStart" to start at the starting node in the Berserker's Ascendancy tree.
        distance_field - string; required. the name of the field that should be set to each row's calculated distance from
              the starting point. 
            Examples: "distance"; "passive_skills.connection_distance".
        max_distance - integer; optional. The longest route we should calculate.
            For instance, if max_distance = 10, then we will only calculate distances to rows that can be reached from the
              origin row through 10 or fewer connections. Any nodes that can't be reached within this limit will wind up with
              the distance field set to nil.
            By default, max_distance is 99. This typically means that any rows that have any connection at all to the
              origin row will have their distance fields set to non-nil.
            Example: 10
       test - boolean; optional. If true, does logging. Defaults to false.
       Any additional arguments accepted by remove_unwanted_rows.
Returns: A table that is a copy of the table in query_results, except that each row will have the field identified by
  distance_field set to the calculated distance. The field will be created if it does not already exist in each row.
]]

    query_results = query_results or error("No results table supplied")
    local param_test = args or error("No args supplied")
    param_test = args.id_field or error("no id_field supplied")
    param_test = args.connection_ids_field or error("no connection_ids_field supplied")
    param_test = args.distance_field or error("no distance_field supplied")
    param_test = args.origin_row_id or error("no originidrow supplied")
    args.max_distance = args.max_distance or 99
    test = args.test or false

    
-- Start by creating three tables:
--   calc_results: A copy of query_results, where we'll eventually insert the computed distances into each row.
--   calc_results_id_map: A table that maps each connection ID to the index of the row in calc_results with that ID. 
--   calculables: A queue table containing the indexes of rows in calcResults where we now know the distance for that row, but we
--     haven't yet used that knowledge to set the distances of the other rows connected to it. At first, this should contain
--     a single index -- the index of the row identified by origin_row_id.
    
    local calc_results = {}
    local calc_results_id_map = {}
    local calculables = {}
    
    for i, row in ipairs(query_results) do
        calc_results[i] = row
        local rowid = calc_results[i][args.id_field]
        if rowid == nil then
            error("No id_field")
        end
        if calc_results_id_map[rowid] ~= nil then
            if test then mw.log(string.format("Warning: id_field not unique: id_field='%s', rowid='%s', previous rowid='%s'", args.id_field, rowid, calc_results_id_map[rowid])) end
        else
            calc_results_id_map[rowid] = i
        end
        if rowid == args.origin_row_id then
            calc_results[i][args.distance_field] = 0
            table.insert(calculables, i)
        else
            calc_results[i][args.distance_field] = nil
        end
    end
    
    if #calculables == 0 then
        error("Origin row not found")
    elseif #calculables > 1 then
        error("Multiple origin rows found")
    end
    
-- Now we start processing by grabbing the next row from the queue
    while #calculables > 0 do
        local idx = table.remove(calculables, 1)
        local id = calc_results[idx][args.id_field]
        local distance_to_this=calc_results[idx][args.distance_field]
        
        if test then mw.log(string.format("Processing node %s with distance %d", id, distance_to_this)) end

        -- Check each connected node to see if our current node is the most efficient route we've found to that other node
        local outgoing_ids = calc_results[idx][args.connection_ids_field]
        if type(outgoing_ids) == "string" then
            param_test = args.connection_ids_delimiter or error("no connection_ids_delimiter supplied")
            outgoing_ids = m_util.string.split(outgoing_ids, args.connection_ids_delimiter)
        end
        if type(outgoing_ids) ~= "table" then
            error(string.format("Unexpected outgoing id data type '%s' in field '%s' for id '%s'", type(outgoing_ids), args.connection_ids_field, calc_results[idx][args.id_field]))
        end
        
        for _,outgoing_id in ipairs(outgoing_ids) do
            local idx_connected_row = calc_results_id_map[outgoing_id]
            if idx_connected_row == nil then
                if test then mw.log(string.format("Outgoing connection %s for row %s not found, ignoring", outgoing_id, calc_results[idx][args.id_field])) end
            else
                connected_row_current_distance = calc_results[idx_connected_row][args.distance_field]
                -- Does the connected node have no distance computed yet? Then the route through the current node is the first
                --   route we've found to the connected node; so by definition it's the most efficient we've found.
                -- Does the connected node have a distance computed that's higher than our current node's distance + 1? Then
                --   we've found a more efficient route than the previous one(s) we found, and we should update it.
                -- Does the connected node have a distance computed that's lower than that? Then we already found a more
                --   efficient route to that node, and we should leave it alone.
                local is_better = false
                if (connected_row_current_distance == nil) then
                    is_better = true
                    if test then mw.log(string.format("Found first route to %s with distance %d", outgoing_id, distance_to_this + 1)) end
                elseif (connected_row_current_distance > distance_to_this + 1) then
                    is_better = true
                    if test then mw.log(string.format("Found better route to %s, reducing distance from %d to %d", outgoing_id, connected_row_current_distance, distance_to_this + 1)) end
                else
                    if test then mw.log(string.format("Found route to %s with distance %d, but prior route has better distance %d", outgoing_id, distance_to_this + 1, connected_row_current_distance)) end
                end
                if is_better then
                    calc_results[idx_connected_row][args.distance_field] = distance_to_this + 1
                -- Add the connected node to the calculables queue, so that we (re)check the nodes connected to it
                    if (distance_to_this + 1) < args.max_distance then
                        table.insert(calculables, idx_connected_row)
                    else
                        if test then mw.log(string.format("Not checking %s because max_distance reached", outgoing_id)) end
                    end
                end
            end
        end
    end
    return m_calculate.remove_unwanted_rows(calc_results,args)	
end

function m_calculate.remove_unwanted_rows(calc_results,args)
    calc_results = calc_results or error("No results table supplied")
    local param_test = args or error("No args supplied")
    param_test = args.distance_field or error("no distance_field supplied")
    param_test = args.id_field or error("no id_field specified")
    test = args.test_remove or false

    local stripped_calc_results = {}
    for i, row in ipairs(calc_results) do
        local rowid = row[args.id_field] or tostring(i)
        local keep = true
        if args.remove_nil_distances and row[args.distance_field] == nil then
            keep = false
            if test then mw.log(string.format("Removing row %s with nil distance", rowid)) end
        end
        if keep and (args.remove_origin or args.remove_zero) and row[args.distance_field] == 0 then
            keep = false
            if test then mw.log(string.format("Removing row %s with zero distance", rowid)) end
        end
        if keep and args.field_required_values then
            local field_required_values = {}
            if type(args.field_required_values) == 'table' then
                field_required_values = args.field_required_values
            elseif type(args.field.required_values) == 'string' then
                field_required_values = m_util.string.splitargs(args.field_required_values)
            else
                error(string.format("Unexpected field_required_values type '%s'", tostring(type(args.field_required_values))))
            end
            for key, val in pairs(field_required_values) do
                if tostring(row[key]) ~= tostring(val) then
                    if test and keep then 
                        mw.log(string.format("Removing row %s with %s value %s (not %s)", 
                            rowid, key, tostring(row[key]), tostring(val)))
                    end
                    keep = false
                    break
                end
            end
        end
        if keep and args.target_ids then
            keep = false
            for _, target_id in ipairs(args.target_ids) do
                if tostring(rowid) == tostring(target_id) then
                    keep = true
                    break
                end
            end
            if test and not keep then mw.log(string.format("Removing row %s not in target ID list", tostring(rowid))) end
        end	
        if keep then
            stripped_calc_results[#stripped_calc_results+1] = row
            if test then mw.log(string.format("Keeping row %s with distance %s", tostring(rowid), tostring(row[args.distance_field]))) end
        end
    end
    return stripped_calc_results
end	

function m_calculate.testascendancy(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {where='passive_skills.ascendancy_class="Berserker"', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="AscendancyBerserkerStart",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

function m_calculate.testwitch(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'CONCAT("")=distance'},
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="witch595",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
        max_distance = 10,
        remove_nil_distances=true,
        remove_origin=true,
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

function m_calculate.testmarauder(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'passive_skills.is_notable', 'CONCAT("")=distance'},
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="marauder594",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
        max_distance = 30,
        field_required_values = {
            ["passive_skills.is_notable"] = 1,
        }
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

function m_calculate.testtargets(test_calc, test_remove)
    local results = m_cargo.query(
        {'passive_skills'},
        {'passive_skills.name', 'passive_skills.id', 'passive_skills.connections', 'passive_skills.ascendancy_class', 'passive_skills.is_notable', 'CONCAT("")=distance'},
        {where='id NOT LIKE "%jewel_slot%" AND connections HOLDS LIKE "%" AND (ascendancy_class="" OR ascendancy_class IS NULL)', order_by='passive_skills.id'}
    )
    local calc_args = {
        id_field = "passive_skills.id",
        connection_ids_field="passive_skills.connections",
        connection_ids_delimiter=",",
        origin_row_id="melee_damage687",
        distance_field="distance",
        test = test_calc or false,
        test_remove = test_remove or false,
        max_distance = 30,
        target_ids = { "marauder594", "witch595", "ranger596", "duelist597", "templar598", "six704", "seven1490" },
    }
    local calc_results = m_calculate.calculate_distances(results, calc_args)
    return calc_results
end

return m_calculate