Модуль:Serialization/EntityTableSelector: различия между версиями

Материал из Space Station 14 Вики
Нет описания правки
Нет описания правки
Метка: ручная отмена
(не показано 6 промежуточных версий этого же участника)
Строка 230: Строка 230:


     return table.concat(result)
     return table.concat(result)
end
local function renderPlainEntry(entry)
    if type(entry) ~= "table" or trim(entry.id) == "" then
        return ""
    end
    return formatContent(entry)
end
end


Строка 242: Строка 250:
         if selector then
         if selector then
             local rendered = renderSelector(selector, visited, true, depth)
             local rendered = renderSelector(selector, visited, true, depth)
            if rendered ~= "" then
                result[#result + 1] = rendered
            end
        else
            local rendered = renderPlainEntry(child)
             if rendered ~= "" then
             if rendered ~= "" then
                 result[#result + 1] = rendered
                 result[#result + 1] = rendered
Строка 293: Строка 306:


     local title = ""
     local title = ""
    local hasWeight = rawget(selector, "weight") ~= nil


     if selector.weight ~= nil and selector.weight ~= "default" then
     if hasWeight and selector.weight ~= "default" then
         title = "Группа предметов " .. normalizePercent(selector.weight, false)
         title = "Группа предметов " .. normalizePercent(selector.weight, false)
     elseif selector.weight == nil then
     elseif not hasWeight then
         title = "Может выпасть лишь один из:"
         title = "Может выпасть лишь один из:"
     end
     end
Строка 383: Строка 397:
     local selector = normalizeSelector(data)
     local selector = normalizeSelector(data)
     if not selector then
     if not selector then
        local plainEntry = renderPlainEntry(data)
        if plainEntry ~= "" then
            return frame:preprocess(plainEntry)
        end
         local plainEntries = renderPlainEntries(data)
         local plainEntries = renderPlainEntries(data)
         if plainEntries ~= "" then
         if plainEntries ~= "" then

Версия от 09:33, 28 марта 2026

Для документации этого модуля может быть создана страница Модуль:Serialization/EntityTableSelector/doc

local p = {}

local getArgs = require('Module:Arguments').getArgs
local JsonPaths = require('Module:JsonPaths')

local entityTableDataCache

local function trim(value)
    return mw.text.trim(value or "")
end

local function deepCopy(source)
    if type(source) ~= "table" then
        return source
    end

    local copy = {}
    for key, value in pairs(source) do
        copy[key] = deepCopy(value)
    end

    return copy
end

local function deepMerge(target, source)
    if type(target) ~= "table" or type(source) ~= "table" then
        return
    end

    for key, value in pairs(source) do
        if type(value) == "table" and type(target[key]) == "table" then
            deepMerge(target[key], value)
        else
            target[key] = deepCopy(value)
        end
    end
end

local function loadEntityTableData()
    if entityTableDataCache ~= nil then
        return entityTableDataCache
    end

    local moduleName = JsonPaths.get("prototype/entityTable.json")
    local ok, data = pcall(mw.loadData, moduleName)
    if not ok or type(data) ~= "table" then
        entityTableDataCache = {}
        return entityTableDataCache
    end

    entityTableDataCache = data
    return entityTableDataCache
end

local function resolveEntry(data, id)
    if type(data) ~= "table" or id == nil or id == "" then
        return nil
    end

    local base = type(data["default"]) == "table" and deepCopy(data["default"]) or nil
    local idsTable = type(data.id) == "table" and data.id or nil
    local specific = idsTable and idsTable[id] or data[id]

    if type(specific) ~= "table" then
        return base
    end

    if base then
        deepMerge(base, specific)
        return base
    end

    return deepCopy(specific)
end

local function cloneSelector(value)
    if type(value) ~= "table" then
        return nil
    end

    local selector = deepCopy(value)
    selector["!type"] = selector["!type"] or ""
    return selector
end

local function normalizeSelector(value)
    if type(value) ~= "table" then
        return nil
    end

    if type(value["!type"]) == "string" and value["!type"] ~= "" then
        return cloneSelector(value)
    end

    for key, inner in pairs(value) do
        if type(key) == "string" and string.sub(key, 1, 6) == "!type:" and type(inner) == "table" then
            local selector = deepCopy(inner)
            selector["!type"] = string.sub(key, 7)
            return selector
        end
    end

    if type(value.table) == "table" then
        return normalizeSelector(value.table)
    end

    return nil
end

local function normalizePercent(value, multiplyByHundred)
    if type(value) ~= "number" then
        return ""
    end

    local percent = multiplyByHundred and (value * 100) or value
    if percent == math.floor(percent) then
        percent = math.floor(percent)
    end

    return tostring(percent) .. "%"
end

local function parseRange(rangeText)
    if type(rangeText) ~= "string" then
        return nil
    end

    local min, max = rangeText:match("(%d+),%s*(%d+)")
    min = tonumber(min)
    max = tonumber(max)

    if not min or not max then
        return nil
    end

    return min + 1, max + 1
end

local function getSelectorValue(selector)
    selector = normalizeSelector(selector) or selector
    if type(selector) ~= "table" then
        return nil
    end

    if selector.value ~= nil then
        return selector.value
    end

    if type(selector.range) == "string" then
        local min, max = parseRange(selector.range)
        if min and max then
            return tostring(min) .. "-" .. tostring(max)
        end
    end

    return nil
end

local function formatAmount(amountSelector)
    local value = getSelectorValue(amountSelector)
    if value == nil or value == 1 or value == "1" then
        return ""
    end

    return " [" .. tostring(value) .. "]"
end

local function formatRolls(rollSelector)
    local value = getSelectorValue(rollSelector)
    if value == nil then
        return ""
    end

    return "[" .. tostring(value) .. "]"
end

local function formatContent(selector)
    if type(selector) ~= "table" or trim(selector.id) == "" then
        return "Ошибка: отсутствует id у элемента."
    end

    local id = selector.id
    local amount = formatAmount(selector.amount)
    local prob = ""

    if type(selector.prob) == "number" then
        prob = normalizePercent(selector.prob, true)
    elseif type(selector.weight) == "number" then
        prob = normalizePercent(selector.weight, false)
    end

    return "{{предмет|" .. id .. "|" .. amount .. " <span>" .. prob .. "</span>|wrapper=1|repository=1}}"
end

local renderSelector
local renderTableById

local function hasNestedCollapsible(content)
    if type(content) ~= "string" or content == "" then
        return false
    end

    return string.find(content, "{{LinkCard/Сollapsible|", 1, true) ~= nil
end

local function wrapCollapsible(title, content, depth)
    if trim(title) == "" then
        return content or ""
    end

    local prefix = "{{LinkCard/Сollapsible|"
    if (depth or 0) > 0 and not hasNestedCollapsible(content) then
        prefix = prefix .. "развернуть=1|"
    end

    return prefix .. "название=" .. title .. "|содержание=" .. (content or "") .. "}}"
end

local function renderPlainEntries(entries)
    if type(entries) ~= "table" then
        return ""
    end

    local result = {}
    for _, entry in ipairs(entries) do
        if type(entry) == "table" and trim(entry.id) ~= "" then
            result[#result + 1] = formatContent(entry)
        end
    end

    return table.concat(result)
end

local function renderPlainEntry(entry)
    if type(entry) ~= "table" or trim(entry.id) == "" then
        return ""
    end

    return formatContent(entry)
end

local function renderChildren(children, visited, depth)
    if type(children) ~= "table" then
        return ""
    end

    local result = {}
    for _, child in ipairs(children) do
        local selector = normalizeSelector(child)
        if selector then
            local rendered = renderSelector(selector, visited, true, depth)
            if rendered ~= "" then
                result[#result + 1] = rendered
            end
        else
            local rendered = renderPlainEntry(child)
            if rendered ~= "" then
                result[#result + 1] = rendered
            end
        end
    end

    return table.concat(result)
end

local function renderAllSelector(selector, visited, depth)
    return renderChildren(selector.children, visited, depth)
end

local function renderNestedSelector(selector, visited, wrapped, depth)
    if trim(selector.tableId) == "" then
        return ""
    end

    local suffix = ""

    if wrapped then
        local rolls = formatRolls(selector.rolls)
        local prob = ""

        if rolls ~= "" then
            suffix = suffix .. ", максимум может выпасть: " .. rolls
        end

        if type(selector.prob) == "number" then
            prob = " <div>" .. normalizePercent(selector.prob, true) .. "</div>"
        elseif type(selector.weight) == "number" then
            prob = " <div>" .. normalizePercent(selector.weight, false) .. "</div>"
        end

        suffix = suffix .. prob
    end

    local content = renderTableById(selector.tableId, visited, depth)
    if wrapped and suffix ~= "" then
        return wrapCollapsible("Группа предметов" .. suffix, content, depth)
    end

    return content
end

local function renderGroupSelector(selector, visited, depth)
    if type(selector.children) ~= "table" then
        return ""
    end

    local title = ""
    local hasWeight = rawget(selector, "weight") ~= nil

    if hasWeight and selector.weight ~= "default" then
        title = "Группа предметов " .. normalizePercent(selector.weight, false)
    elseif not hasWeight then
        title = "Может выпасть лишь один из:"
    end

    local result = {}
    for _, child in ipairs(selector.children) do
        local childSelector = normalizeSelector(child)
        if childSelector then
            if childSelector["!type"] == "AllSelector" then
                local rendered = renderAllSelector(childSelector, visited, (depth or 0) + 1)
                if rendered ~= "" then
                    result[#result + 1] = wrapCollapsible("Выпадают только вместе:", rendered, (depth or 0) + 1)
                end
            elseif childSelector["!type"] == "GroupSelector" then
                result[#result + 1] = renderGroupSelector(childSelector, visited, (depth or 0) + 1)
            elseif childSelector["!type"] == "NestedSelector" then
                result[#result + 1] = renderNestedSelector(childSelector, visited, true, (depth or 0) + 1)
            elseif childSelector["!type"] == "EntSelector" then
                result[#result + 1] = formatContent(childSelector)
            end
        end
    end

    return wrapCollapsible(title, table.concat(result), depth)
end

renderTableById = function(tableId, visited, depth)
    visited = visited or {}
    tableId = trim(tableId)

    if tableId == "" then
        return ""
    end

    if visited[tableId] then
        return ""
    end
    visited[tableId] = true

    local data = loadEntityTableData()
    local entry = resolveEntry(data, tableId)
    local selector = entry and normalizeSelector(entry.table) or nil

    if not selector then
        visited[tableId] = nil
        return "Таблица не найдена."
    end

    local result = renderSelector(selector, visited, false, depth)
    visited[tableId] = nil
    return result
end

renderSelector = function(selector, visited, wrapped, depth)
    selector = normalizeSelector(selector)
    if not selector then
        return ""
    end

    if selector["!type"] == "EntSelector" then
        return formatContent(selector)
    elseif selector["!type"] == "AllSelector" then
        return renderAllSelector(selector, visited, depth)
    elseif selector["!type"] == "GroupSelector" then
        return renderGroupSelector(selector, visited, depth)
    elseif selector["!type"] == "NestedSelector" then
        return renderNestedSelector(selector, visited, wrapped ~= false, depth)
    end

    return ""
end

function p.main(frame)
    local args = getArgs(frame, { removeBlanks = false })
    local jsonText = mw.text.unstripNoWiki(args[1] or args.json or "")

    if trim(jsonText) == "" then
        return ""
    end

    local ok, data = pcall(mw.text.jsonDecode, jsonText)
    if not ok or type(data) ~= "table" then
        return ""
    end

    local selector = normalizeSelector(data)
    if not selector then
        local plainEntry = renderPlainEntry(data)
        if plainEntry ~= "" then
            return frame:preprocess(plainEntry)
        end

        local plainEntries = renderPlainEntries(data)
        if plainEntries ~= "" then
            return frame:preprocess(plainEntries)
        end

        return ""
    end

    return frame:preprocess(renderSelector(selector, {}, false, 0))
end

return p