Module:Cargo orders

From Space Station 14 Wiki
Module documentation
View or edit this documentation (about module documentation)

Implements {{Cargo orders}}.


local p = {} --p stands for package
local getArgs = require('Module:Arguments').getArgs
local yesNo = require('Module:Yesno')
local lookup_item_name = require('Module:Item').lookup_item_name
local format_probability = require('Module:Utils/number').format_probability

local orders_table = mw.loadJsonData("Module:Cargo orders/data/auto/orders.json")

local current_frame = mw.getCurrentFrame()

-- =======================

local function assert_not_nil(value, error_message)
    if value == nil then
        if error_message == nil then
            error("value is nil")
        else
            error(error_message)
        end
    end
end

local function starts_with(str, substr)
    return string.sub(str, 1, string.len(substr)) == substr
end

local function ends_with(str, substr)
    local substr_length = string.len(substr)
    return string.sub(str, string.len(str) - substr_length + 1, string.len(str)) == substr
end

local function starts_with_insensitive(str, substr)
    return starts_with(string.lower(str), string.lower(substr))
end

local function ends_with_insensitive(str, substr)
    return ends_with(string.lower(str), string.lower(substr))
end

local function table_filter(tbl, filterFn)
    local out = {}

    for k, v in pairs(tbl) do
        if filterFn(v, k, tbl) then out[k] = v end
    end

    return out
end

local function num_table_reduce(tbl, reduceFn, initial_value)
    local out = initial_value

    for i, v in ipairs(tbl) do
        out = reduceFn(out, v, i, tbl)
    end

    return out
end

local function table_find(tbl, findFn)
    for k, v in pairs(tbl) do
        if findFn(v, k, tbl) then return v end
    end
end

local function table_contains(tbl, findFn)
    return table_find(tbl, findFn) ~= nil
end

local function table_contains_value(tbl, value)
    return table_find(tbl, function(iter_value)
        return iter_value == value
    end) ~= nil
end

local function table_find_key(tbl, findFn)
    for k, v in pairs(tbl) do
        if findFn(v, k, tbl) then return k end
    end
end

local function table_find_index(tbl, findFn)
    for i, v in ipairs(tbl) do
        if findFn(v, i, tbl) then return i end
    end
end

local function table_map(tbl, mapFn)
    local res = {}
    for k, v in pairs(tbl) do
        table.insert(res, mapFn(v, k, tbl))
    end
    return res
end

local function numeric_table_length(t)
    local count = 0
    for _ in ipairs(t) do count = count + 1 end
    return count
end

local function table_length(t)
    local count = 0
    for _ in pairs(t) do count = count + 1 end
    return count
end

-- Returns keys of a table.
local function table_keys(tbl)
    local arr = {}
    for key, _ in pairs(tbl) do
        table.insert(arr, key)
    end
    return arr
end

-- Sorts a table into a new table.
-- An alternative to cases such as when table.sort is not working,
-- like when trying to sort JSON tables (thanks fuckass lua).
function table_to_sorted(tbl, sortfn)
    local keys = {}

    for key, _ in pairs(tbl) do
        table.insert(keys, key)
    end

    table.sort(keys, function(keyA, keyB) return sortfn(tbl[keyA], tbl[keyB]) end)

    local t2 = {}

    for _, key in ipairs(keys) do
        table.insert(t2, tbl[key])
    end

    return t2
end

-- Given a table and a property key, attempts to retrieve said property.
-- If property does not exists, creates it with by assigning a value from `create_value_fn`.
-- The created value is then returned.
function get_table_prop_or_create(tbl, prop_key, create_value_fn)
    local value = tbl[prop_key]
    if value == nil then
        value = create_value_fn()
        tbl[prop_key] = value
    end

    return value;
end

-- ===========================

-- Compares two item IDs (to use in sort functions).
local function compare_items_by_lookup_name_or_id(id1, id2)
    return (lookup_item_name(string.lower(id1), true) or id1)
        < (lookup_item_name(string.lower(id2), true) or id2)
end

-- Formats entity ID.
local function format_id(id, amount, maxAmount)
    local amount_res = maxAmount and (amount .. "-" .. maxAmount) or amount

    return current_frame:expandTemplate { title = "item", args = { id, amount_res } }
end

-- Formats a table of "contents" entries that all have an "orGroup" property.
-- "orGroup" behaves like a weight table so we need to treat it like one.
local function format_or_group(group_name, entries)
    if #entries == 0 then
        error("failed to format 'orGroup': no entries provided")
    end

    -- calculate cumulative weight
    local cumulative_weight = 0
    for _, entry in ipairs(entries) do
        local weight = entry.prob or 1
        cumulative_weight = cumulative_weight + weight
    end

    -- generate entries
    local table_el = mw.html.create("table")
        :addClass("wikitable pool-section")

    local caption_el = table_el:tag("caption")
        :addClass("pool-section-caption")
        :wikitext(group_name)

    -- sort by weight then a-z
    local entries_sorted = table_to_sorted(entries, function(a, b)
        local a_prob = a.prob or 1
        local b_prob = b.prob or 1

        if a_prob == b_prob then
            return compare_items_by_lookup_name_or_id(a.id, b.id)
        else
            return a_prob > b_prob
        end
    end)

    for _, entry in ipairs(entries_sorted) do
        local prob = (entry.prob or 1) / cumulative_weight;

        local row = table_el:tag("tr")
            :addClass("pool-section-row")

        row:tag("td")
            :node(format_id(entry.id, entry.amount or 1, entry.maxAmount))

        row:tag("td")
            :wikitext(format_probability(prob, 3))
    end

    return table_el
end

-- Formats "contents" field to wikitext.
local function format_contents(contents)
    if contents == nil then
        return nil
    end

    local contents_el = mw.html.create("div")
        :addClass("pool")

    local entries_without_orGroup = {}
    local entries_grouped_by_orGroup = num_table_reduce(
        contents,
        function(accum, entry)
            local orGroup = entry.orGroup
            if not orGroup then
                table.insert(entries_without_orGroup, entry)
                return accum
            end

            local accumGroup = get_table_prop_or_create(accum, orGroup, function() return {} end)
            table.insert(accumGroup, entry)

            return accum
        end,
        {}
    )

    -- ungrouped go first
    local ungrouped_list_el = contents_el:tag("ul")

    -- sort by prob then by name (or ID if name lookup fails)
    table.sort(entries_without_orGroup, function(a, b)
        local a_prob = a.prob or 1
        local b_prob = b.prob or 1

        if a_prob == b_prob then
            return compare_items_by_lookup_name_or_id(a.id, b.id)
        else
            return a_prob > b_prob
        end
    end)

    for _, entry in ipairs(entries_without_orGroup) do
        local entry_li_el = ungrouped_list_el:tag("li")
            :node(format_id(entry.id, entry.amount or 1, entry.maxAmount))

        if entry.prob then
            entry_li_el
                :wikitext(" [" .. format_probability(entry.prob) .. "]")
        end
    end

    -- grouped go second
    for group_name, group in pairs(entries_grouped_by_orGroup) do
        contents_el:node(format_or_group(group_name, group))
    end

    return contents_el
end

-- ===========================

function p.main(frame)
    local args = getArgs(frame)

    -- =======================

    local orders_grouped_by_category = num_table_reduce(
        orders_table,
        function(accum, order)
            local category_orders = get_table_prop_or_create(accum, order.category, function() return {} end)
            table.insert(category_orders, order);

            return accum
        end,
        {}
    )

    local categories_sorted = table_keys(orders_grouped_by_category)
    table.sort(categories_sorted)

    -- =========

    local container = mw.html.create('div')
        :addClass("cargo-orders-container")

    local orders_section = mw.html.create('h2')
        :wikitext("Orders")
    container:node(orders_section)

    for _, category in ipairs(categories_sorted) do
        local category_section = mw.html.create('h3')
            :wikitext(category)
        container:node(category_section)

        local category_table = mw.html.create('table')
            :addClass("wikitable cargo-orders-table")
        container:node(category_table)

        local category_table_h = category_table:tag('tr')
        category_table_h:tag('th'):wikitext("Product")
        category_table_h:tag('th'):wikitext("Unit cost")
        category_table_h:tag('th'):wikitext("Description")
        category_table_h:tag('th'):wikitext("Contents")

        local orders = orders_grouped_by_category[category]
        -- sort by name if possible, otherwise fallback to ID
        table.sort(orders, function(a, b)
            return compare_items_by_lookup_name_or_id(a.product, b.product)
        end)

        for _, order in ipairs(orders_grouped_by_category[category]) do
            local row = category_table:tag('tr')
            row:tag('td'):wikitext(frame:expandTemplate { title = "item", args = { order.product, capitalize = true } })
            row:tag('td'):wikitext(frame:expandTemplate { title = "spesos", args = { order.cost } })
            row:tag('td'):wikitext(order.description)
            row:tag('td'):node(format_contents(order.contents))
        end
    end

    return container
end

return p