Модуль:Песочница/Pok: различия между версиями

мНет описания правки
Нет описания правки
 
(не показано 313 промежуточных версий этого же участника)
Строка 1: Строка 1:
local p = {}
local p = {}
local getArgs = require('Module:Arguments').getArgs
local JsonPaths = require('Module:JsonPaths')


-- Функция для загрузки данных исследований из JSON-файла
local dpOk, dpModule = pcall(require, "Module:GetField")
local function loadResearchData()
local dp = dpOk and dpModule or nil
     return mw.text.jsonDecode(mw.title.new("User:IanComradeBot/prototypes/research.json"):getContent())
 
local switchModeRegistry = {}
local switchModeOrder = {}
 
local function trim(s)
    if not s then return s end
    return (s:gsub("^%s*(.-)%s*$", "%1"))
end
 
local function each_csv_value(str, fn)
     if not str or str == "" then
        return
    end
    for item in string.gmatch(str, "[^,]+") do
        local value = trim(item)
        if value ~= "" then
            fn(value)
        end
    end
end
 
local function load_module_data(page)
    local moduleName = JsonPaths.get(page)
    local ok, data = pcall(mw.loadData, moduleName)
    if not ok then
        return nil
    end
    return data
end
 
local function load_template_content(path)
    local title = mw.title.new("Template:" .. path)
    if not title then
        return nil
    end
    local ok, content = pcall(title.getContent, title)
    if not ok then
        return nil
    end
    return content
end
 
local function lcfirst(s)
    if not s or s == "" then return s end
    return string.lower(s:sub(1, 1)) .. (s:sub(2) or "")
end
 
local function load_entity_data(entityId)
    if not entityId or entityId == "" then
        return nil
    end
 
    local page = "prototype/Entity/" .. entityId .. ".json"
    local moduleName = JsonPaths.get(page)
    local ok, data = pcall(mw.loadData, moduleName)
    if not ok or type(data) ~= "table" then
        return nil
    end
    return data
end
 
local function normalize_component_name(name)
    if type(name) ~= "string" then
        return nil
    end
 
    name = trim(name)
    if name == "" then
        return nil
    end
 
    if name:sub(1, 5) == "type:" then
        return name:sub(6)
    end
    if name:sub(1, 6) == "!type:" then
        return name:sub(7)
    end
 
    return name
end
 
local function collect_entity_components(entity)
    local out = {}
    local seen = {}
 
    if type(entity) ~= "table" then
        return out
    end
 
    local comps = entity.components
    if type(comps) ~= "table" then
        return out
    end
 
    if #comps > 0 then
        for _, v in ipairs(comps) do
            local name = normalize_component_name(v)
            if name and not seen[name] then
                seen[name] = true
                out[#out + 1] = name
            end
        end
    else
        for k in pairs(comps) do
            local name = normalize_component_name(k)
            if name and not seen[name] then
                seen[name] = true
                out[#out + 1] = name
            end
        end
    end
 
    table.sort(out)
    return out
end
 
local function load_entity_components_from_dp(entityId)
    if not dp then
        return nil
    end
 
    local getter = dp.getEntityComponents or dp.collectEntityComponents or dp.getComp
    if type(getter) ~= "function" then
        return nil
    end
 
    local ok, result = pcall(getter, { args = { entityId } })
    if not ok or result == nil or result == "" then
        return nil
    end
 
    if type(result) == "table" then
        return result
    end
 
    if type(result) == "string" then
        local okJson, decoded = pcall(mw.text.jsonDecode, result)
        if okJson and type(decoded) == "table" then
            return decoded
        end
    end
 
    return nil
end
 
local function load_entity_components(entityId)
    local viaDp = load_entity_components_from_dp(entityId)
    if type(viaDp) == "table" and next(viaDp) ~= nil then
        local out = {}
        local seen = {}
        if #viaDp > 0 then
            for _, v in ipairs(viaDp) do
                local name = normalize_component_name(v)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        else
            for k in pairs(viaDp) do
                local name = normalize_component_name(k)
                if name and not seen[name] then
                    seen[name] = true
                    out[#out + 1] = name
                end
            end
        end
        table.sort(out)
        return out
    end
 
    local entity = load_entity_data(entityId)
    return collect_entity_components(entity)
end
 
p.loadEntityData = load_entity_data
p.collectEntityComponents = collect_entity_components
p.loadEntityComponents = load_entity_components
p.entityHasComponent = function(entityOrId, compName)
    if not compName or compName == "" then
        return false
    end
 
    if type(entityOrId) == "string" then
        local entity = load_entity_data(entityOrId)
        if not entity then
            return false
        end
        local comps = collect_entity_components(entity)
        for _, v in ipairs(comps) do
            if v == compName then
                return true
            end
        end
        return false
    end
 
    local comps = collect_entity_components(entityOrId)
    for _, v in ipairs(comps) do
        if v == compName then
            return true
        end
    end
    return false
end
 
local function makeTplCall(tplPath, sw, key, id, extra)
    local tplStr = "{{" .. tplPath .. "|" .. sw .. "|" .. key
    tplStr = tplStr .. "|id=" .. tostring(id)
    if extra and extra ~= "" then tplStr = tplStr .. "|" .. extra end
    tplStr = tplStr .. "}}"
    return tplStr
end
 
local function add_template_param(params, seen, raw)
    local param = trim(raw or "")
    if param == "" or param == "id" or param:match("^%d+$") then
        return
    end
    if not seen[param] then
        seen[param] = true
        params[#params + 1] = param
    end
end
 
local function collect_template_params(content)
    local params = {}
    local seen = {}
 
    if not content or content == "" then
        return params
    end
 
    for param in content:gmatch("{{{%s*([^|}]+)%s*|") do
        add_template_param(params, seen, param)
    end
    for param in content:gmatch("{{{%s*([^|}]+)%s*}}") do
        add_template_param(params, seen, param)
    end
 
    return params
end
 
local function get_template_params(tplPath, content)
    return collect_template_params(content)
end
 
local function sort_entries_by_priority(entries)
    table.sort(entries, function(a, b)
        if a.priority == b.priority then return a.idx < b.idx end
        return a.priority > b.priority
    end)
end
 
local function make_source(kind, name, pathName, tplPath)
    return { kind = kind, name = name, pathName = pathName, tplPath = tplPath }
end
 
local function register_switch_mode(name, cfg)
    switchModeRegistry[name] = cfg or {}
    switchModeOrder[#switchModeOrder + 1] = name
end
 
local function new_switch_state()
    local state = { keyOrder = {}, keyToTemplates = {}, keySources = {} }
    for _, sw in ipairs(switchModeOrder) do
        state.keyOrder[sw] = {}
        state.keyToTemplates[sw] = {}
        state.keySources[sw] = {}
    end
    return state
end
 
local function ensure_switch_key(state, sw, key)
    local byKey = state.keyToTemplates[sw]
    if not byKey[key] then
        byKey[key] = {}
        state.keyOrder[sw][#state.keyOrder[sw] + 1] = key
    end
    return byKey[key]
end
 
local function add_switch_entry(state, sw, key, entry)
    local bucket = ensure_switch_key(state, sw, key)
    entry.idx = #bucket + 1
    bucket[#bucket + 1] = entry
end
 
local function collect_tpl_calls(entries)
    local tplCalls = {}
    local sources = {}
    if #entries > 0 then
        sort_entries_by_priority(entries)
        for _, e in ipairs(entries) do
            tplCalls[#tplCalls + 1] = e.tpl
            sources[#sources + 1] = e.source
        end
    end
    return tplCalls, sources
end
 
local function makeSourceLink(s)
    local className =
        (s.name:sub(1, 1):upper() .. s.name:sub(2)) ..
        (s.kind and (s.kind:sub(1, 1):upper() .. s.kind:sub(2)) or "")
 
    local tplLabel = "Template:" .. s.tplPath
    return "[[" .. tplLabel .. "|" .. className .. "]]"
end
 
local function renderTitleBlock(key, tplCalls, sources, includeHeader, frame, showSource)
    local parts = {}
    if tplCalls and #tplCalls > 0 then
        for i, tpl in ipairs(tplCalls) do
            local add = true
            if frame then
                local expanded = frame:preprocess(tpl)
                add = expanded and trim(expanded) ~= ""
            end
            if add then
                local src = sources and sources[i]
                local line = '<div>' .. tpl .. '</div>'
                if showSource and src then
                    line = line .. '<div class="ts-Сущность-field">' .. makeSourceLink(src) .. '</div>'
                end
                parts[#parts + 1] = '<div class="ts-Сущность">' .. line .. '</div>'
            end
        end
        if #parts == 0 then
            return ""
        end
        if includeHeader then
            table.insert(parts, 1, "<h2>" .. mw.text.encode(key) .. "</h2>")
        end
    end
    return table.concat(parts, "\n")
end
 
local function split_title_key(key)
    local main, sub = (key or ""):match("^([^_]+)_(.+)$")
    if main and sub then
        return main, sub
    end
    return key, nil
end
end


-- Таблица для перевода названий дисциплин
local function renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
local disciplineMapping = {
    local groups = {}
     Arsenal = "Арсенал",
     local groupOrder = {}
    Industrial = "Промышленность",
 
     Experimental = "Экспериментальное",
    for _, key in ipairs(keyOrder or {}) do
    CivilianServices = "Обслуживание персонала"
        local mainTitle, subTitle = split_title_key(key)
}
        if mainTitle and mainTitle ~= "" then
            local group = groups[mainTitle]
            if not group then
                group = { blocks = {} }
                groups[mainTitle] = group
                groupOrder[#groupOrder + 1] = mainTitle
            end
 
            group.blocks[#group.blocks + 1] = {
                subTitle = subTitle,
                entries = keyToTemplates[key] or {}
            }
        end
    end
 
     local out = {}
    for _, mainTitle in ipairs(groupOrder) do
        local group = groups[mainTitle]
        local parts = {}


-- Таблица для цветов по уровням
        if not noHeaders then
local tierColors = {
            parts[#parts + 1] = "<h2>" .. mw.text.encode(mainTitle) .. "</h2>"
    [1] = "#54d554",
        end
    [2] = "#ed9000",
    [3] = "#d72a2a"
}


function p.main(frame)
        for _, block in ipairs(group.blocks or {}) do
    local dataCache = loadResearchData()
            local tplCalls, sources = collect_tpl_calls(block.entries or {})
    local input = frame.args[1] or ""
            local blockText = renderTitleBlock(block.subTitle or mainTitle, tplCalls, sources, false, frame, showSource)
    local manualIcon = frame.args[2] or ""
            if blockText ~= "" then
    local out = ""
                if block.subTitle and not noHeaders then
                    parts[#parts + 1] = "<h3>" .. mw.text.encode(block.subTitle) .. "</h3>"
                end
                parts[#parts + 1] = blockText
            end
        end


    if input == "" then
        if #parts > 0 then
         return '<div style="color:red;">Не указан идентификатор исследования или дисциплина.</div>'
            out[#out + 1] = table.concat(parts, "\n")
         end
     end
     end


     -- Если первый параметр соответствует дисциплине, работаем в режиме категории
     return table.concat(out, "\n")
    if disciplineMapping[input] then
end
        local found = false
 
        for _, research in ipairs(dataCache) do
local function normalizeFilterKey(s)
            if research.technology and research.technology.discipline == input then
    s = trim(s or "")
                found = true
    s = s:gsub("%s*_%s*", "_")
                local tech = research.technology
    return s
                local iconPath = tech.icon and tech.icon.sprite or nil
end
                local tierColor = tierColors[tech.tier] or "#FFFFFF"
                local disciplineName = disciplineMapping[tech.discipline] or "Неизвестная дисциплина"


                -- Формирование строки необходимых исследований (prerequisites)
local function matches_card_list(list, callKey, compositeKey)
                local prerequisites = ""
    if not list then
                if tech.technologyPrerequisites and #tech.technologyPrerequisites > 0 then
        return false
                    prerequisites = '<ul>'
    end
                     for _, prerequisiteId in ipairs(tech.technologyPrerequisites) do
    callKey = normalizeFilterKey(callKey)
                        if prerequisiteId and prerequisiteId ~= "" then
    compositeKey = normalizeFilterKey(compositeKey)
                            local prerequisiteName = ""
    return list[callKey] or list[compositeKey] or false
                            for _, r in ipairs(dataCache) do
end
                                if r.technology and r.technology.id == prerequisiteId then
 
                                    prerequisiteName = r.technology.name
local function buildCardCall(merged, entityId)
                                    break
    local parts = {}
                                end
 
                            end
    if entityId and entityId ~= "" then
                            if prerequisiteName ~= "" then
        parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
                                prerequisites = prerequisites .. '<li>{{#invoke:Ftl|main|translation|' .. prerequisiteName .. '}}</li>'
    end
                             end
 
    if merged.tags and #merged.tags > 0 then
        table.sort(merged.tags)
        parts[#parts + 1] = "тип=" .. table.concat(merged.tags, ", ")
    end
 
    if merged.sections and #merged.sections > 0 then
        table.sort(merged.sections, function(a, b)
            if a == "Сущность" then return true end
            if b == "Сущность" then return false end
            return a < b
        end)
        parts[#parts + 1] = "sections=" .. table.concat(merged.sections, ", ")
        for _, section in ipairs(merged.sections) do
            local labels = merged.labelLists[section]
            if labels and #labels > 0 then
                local enc = {}
                for i = 1, #labels do
                     enc[i] = mw.text.encode(labels[i])
                end
                parts[#parts + 1] = mw.text.encode(section) .. "=" .. table.concat(enc, ", ")
            end
        end
    end
 
    for compositeKey, displayLabel in pairs(merged.labelOverrides or {}) do
        if displayLabel and displayLabel ~= "" then
            parts[#parts + 1] = mw.text.encode(compositeKey .. "_label") .. "=" .. displayLabel
        end
    end
    for compositeKey, content in pairs(merged.contentByKey or {}) do
        if content and content ~= "" then
            parts[#parts + 1] = mw.text.encode(compositeKey) .. "=" .. content
        end
    end
 
    if #parts == 0 then
        return ""
    end
 
    return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}"
end
 
local function cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
    local merged = {
        sections = {},
        sectionsMap = {},
        labelLists = {},
        labelSets = {},
        labelOverrides = {},
        contentByKey = {},
        tags = {},
        tagSet = {}
    }
    local rawContentParts = {}
    for _, callKey in ipairs(keyOrder or {}) do
        local entries = keyToTemplates[callKey] or {}
        if #entries > 0 then
            sort_entries_by_priority(entries)
            for _, e in ipairs(entries) do
                local displayLabel = trim(frame:preprocess(e.tplLabel or "") or "")
                local content = trim(frame:preprocess(e.tplContent or "") or "")
                local tagText = ""
                if e.tplTag then
                    tagText = trim(frame:preprocess(e.tplTag or "") or "")
                end
                local compositeKey = (callKey:find("_", 1, true)) and callKey or ("Сущность_" .. callKey)
                local section = (callKey:find("_", 1, true)) and callKey:match("^([^_]+)") or "Сущность"
 
                local isWhitelisted = cardFilter and matches_card_list(cardFilter.whitelist, callKey, compositeKey) or
                    false
                local isBlacklisted = cardFilter and matches_card_list(cardFilter.blacklist, callKey, compositeKey) or
                    false
                if isWhitelisted and content ~= "" then
                    rawContentParts[#rawContentParts + 1] = content
                end
 
                local allowCardEntry = not isWhitelisted and not isBlacklisted and
                    ((not cardFilter) or (not cardFilter.hasWhitelist) or
                        matches_card_list(cardFilter.cardWhitelist, callKey, compositeKey))
 
                if allowCardEntry and (displayLabel ~= "" or content ~= "") then
                    if not merged.sectionsMap[section] then
                        merged.sectionsMap[section] = true
                        merged.sections[#merged.sections + 1] = section
                    end
                    if displayLabel ~= "" and (not merged.labelOverrides[compositeKey] or merged.labelOverrides[compositeKey] == "") then
                        merged.labelOverrides[compositeKey] = displayLabel
                    end
                    if content ~= "" then
                        local prev = merged.contentByKey[compositeKey]
                        if prev and prev ~= "" then
                            merged.contentByKey[compositeKey] = prev .. "\n" .. content
                        else
                             merged.contentByKey[compositeKey] = content
                         end
                         end
                     end
                     end
                     prerequisites = prerequisites .. '</ul>'
 
                     merged.labelSets[section] = merged.labelSets[section] or {}
                    if not merged.labelSets[section][compositeKey] then
                        merged.labelSets[section][compositeKey] = true
                        local cur = merged.labelLists[section] or {}
                        cur[#cur + 1] = compositeKey
                        merged.labelLists[section] = cur
                    end
                 end
                 end


                 -- Формирование строки открываемых исследований (unlocks)
                 if allowCardEntry and tagText ~= "" then
                local unlocks = ""
                    if not merged.tagSet[tagText] then
                if tech.recipeUnlocks and #tech.recipeUnlocks > 0 then
                        merged.tagSet[tagText] = true
                    unlocks = '<ul>'
                         merged.tags[#merged.tags + 1] = tagText
                    for _, recipe in ipairs(tech.recipeUnlocks) do
                         if recipe and recipe ~= "" then
                            unlocks = unlocks .. '<li>{{#invoke:Entity Lookup|createimagetooltip|Файл:'
                                .. recipe .. '.png|' .. recipe
                                .. '|Мета=32x32px,link=}} {{#invoke:Entity Lookup|getname|'
                                .. recipe .. '}}</li>'
                        end
                     end
                     end
                    unlocks = unlocks .. '</ul>'
                 end
                 end
            end
        end
    end


                local templateArgs = {
    each_csv_value(frame.args.cardTag or "", function(extraTag)
                    id = tech.id,
        if not merged.tagSet[extraTag] then
                    icon = iconPath,
            merged.tagSet[extraTag] = true
                    name = tech.name,
            merged.tags[#merged.tags + 1] = extraTag
                    discipline = tech.discipline,
        end
                    tier = tech.tier,
    end)
                    tierColor = tierColor,
 
                    disciplineName = disciplineName,
    local out = {}
                    cost = tech.cost,
    if #rawContentParts > 0 then
                    unlocks = unlocks
        out[#out + 1] = table.concat(rawContentParts, "\n")
                 }
    end
                 if prerequisites ~= "" then
 
                     templateArgs.prerequisites = prerequisites
    local cardCall = buildCardCall(merged, entityId)
 
    if noHeaders then
        local hasLabel = false
        for _, v in pairs(merged.labelOverrides or {}) do
            if v and v ~= "" then
                hasLabel = true
                 break
            end
        end
        if not hasLabel then
            for _, lst in pairs(merged.labelLists or {}) do
                 if #lst > 0 then
                     hasLabel = true
                    break
                 end
                 end
            end
        end


                out = out .. frame:expandTemplate({
        local hasContent = false
                    title = 'Prototypes/Механика/Исследование',
        for _, v in pairs(merged.contentByKey or {}) do
                    args = templateArgs
            if v and v ~= "" then
                 })
                hasContent = true
                 break
             end
             end
         end
         end
         if not found then
 
             out = out .. '<div style="color:red;">Не найдено исследований для дисциплины "' .. input .. '".</div>'
         if not hasLabel and not hasContent then
             return table.concat(out, "\n")
         end
         end
         return mw.getCurrentFrame():preprocess(out)
    end
     else
 
         -- Режим поиска по ID исследования
    if cardCall ~= "" then
         local tech = nil
        out[#out + 1] = cardCall
         for _, research in ipairs(dataCache) do
    end
             if research.technology and research.technology.id == input then
 
                 tech = research.technology
    return table.concat(out, "\n")
                 break
end
 
register_switch_mode("card", {
    full = true,
    build_entry = function(ctx, key)
        return {
            tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ctx.id, ctx.extra),
            tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ctx.id, ctx.extra),
            tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    build_preview_entry = function(ctx, key)
         return {
            tplLabel = makeTplCall(ctx.tplPath, "cardLabel", key, ""),
            tplContent = makeTplCall(ctx.tplPath, "cardContent", key, ""),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    add_entity_extras = function(state, parsed, ctx)
        if type(parsed) == "table" and parsed.cardTag and parsed.cardTag ~= "" then
            add_switch_entry(state, "card", "cardTag", {
                tplLabel = "",
                tplContent = "",
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                source = ctx.source,
                priority = ctx.priority
            })
        end
    end,
    render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource, cardFilter)
        return cardWrapper(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, cardFilter)
    end
})
 
register_switch_mode("title", {
    full = true,
    build_entry = function(ctx, key)
        return {
            tpl = makeTplCall(ctx.tplPath, "title", key, ctx.id, ctx.extra),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    build_preview_entry = function(ctx, key)
        return {
            tpl = makeTplCall(ctx.tplPath, "title", key, ""),
            source = ctx.source,
            priority = ctx.priority
        }
    end,
    render_full = function(frame, keyOrder, keyToTemplates, keySources, entityId, noHeaders, showSource)
        return renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
    end
})
 
local function getTemplateMeta(frame, tplPath)
    local expanded = frame:expandTemplate {
        title = tplPath,
        args = { "json" }
    }
 
    local ok, data = pcall(mw.text.jsonDecode, expanded)
    if not ok or type(data) ~= "table" then
        return ""
    end
 
    if data.card == nil then
        local cardKeys = {}
        local seen = {}
        for base, labels in pairs(data) do
            if type(base) == "string" and base ~= "card" and base:sub(1, 4) == "card" and type(labels) == "table" then
                for _, lab in ipairs(labels) do
                    if not seen[lab] then
                        seen[lab] = true
                        cardKeys[#cardKeys + 1] = lab
                    end
                end
            end
        end
        data.card = cardKeys
    end
 
    return data
end
 
local function parseListArg(str)
     local res = {}
    if not str or str == "" then return res end
    for item in string.gmatch(str, "[^,]+") do
         local s = normalizeFilterKey(item)
         if s ~= "" then
            local a, b = s:match("^([^_]+)_(.+)$")
            if a and b then
                res[a] = res[a] or {}
                res[a][b] = true
            end
         end
    end
    return res
end
 
local function build_key_filter(args)
    local filter = {}
    filter.blacklist = parseListArg(args.blacklist or "")
    filter.whitelist = parseListArg(args.whitelist or "")
    filter.hasWhitelist = false
    for _, sw in ipairs(switchModeOrder) do
        if filter.whitelist[sw] and next(filter.whitelist[sw]) ~= nil then
            filter.hasWhitelist = true
            break
        end
    end
    if not filter.hasWhitelist and filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil then
        filter.hasWhitelist = true
    end
    return filter
end
 
local function build_render_options(filter)
    return {
        noHeaders = false,
        cardFilter = {
            hasWhitelist = filter.whitelist.cardContent and next(filter.whitelist.cardContent) ~= nil or false,
            cardWhitelist = filter.whitelist.card or {},
            blacklist = filter.blacklist.cardContent or {},
            whitelist = filter.whitelist.cardContent or {}
        }
    }
end
 
local function should_include_key(filter, sw, key)
    if filter.hasWhitelist then
        if filter.whitelist[sw] and filter.whitelist[sw][key] then
             return true
        end
        if sw == "card" and filter.whitelist.cardContent and filter.whitelist.cardContent[key] then
            return true
        end
        return false
    end
    return not (filter.blacklist[sw] and filter.blacklist[sw][key])
end
 
local function parse_csv_set(str)
    local res = {}
    each_csv_value(str, function(name)
        res[name] = true
    end)
    return res
end
 
local function apply_entity_set_filters(foundSet, whitelistSet, blacklistSet)
    local hasWhitelist = next(whitelistSet or {}) ~= nil
 
    if hasWhitelist then
        for name in pairs(foundSet) do
            if not whitelistSet[name] then
                foundSet[name] = nil
            end
        end
    end
 
    for name in pairs(blacklistSet or {}) do
        foundSet[name] = nil
    end
end
 
local function collect_entity_sets(id, prototypeStoreDefs, componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
    local foundComponents, foundPrototypes = {}, {}
 
    local compList = load_entity_components(id)
    if type(compList) == "table" then
        for _, v in ipairs(compList) do
            if type(v) == "string" and v ~= "" then
                 foundComponents[v] = true
            end
        end
    end
 
    local protoStore = prototypeStoreDefs and prototypeStoreDefs[id]
    if type(protoStore) == "table" then
        for protoName in pairs(protoStore) do
            if type(protoName) == "string" and protoName ~= "" then
                 foundPrototypes[protoName] = true
             end
             end
         end
         end
    end
    apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
    apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))
    return foundComponents, foundPrototypes
end


        if not tech then
local function resolve_priority(parsed)
            out = out .. '<div style="color:red;">Исследование с ID "' .. input .. '" не найдено.</div>'
    local basePriority = 1
    if type(parsed) == "table" and parsed.priority ~= nil then
        if type(parsed.priority) == "number" then
            basePriority = parsed.priority
         else
         else
             local tierColor = tierColors[tech.tier] or "#FFFFFF"
             local pnum = tonumber(parsed.priority)
            local disciplineName = disciplineMapping[tech.discipline] or "Неизвестная дисциплина"
            if pnum then
            local iconPath = manualIcon ~= "" and manualIcon or (tech.icon and tech.icon.sprite or nil)
                basePriority = pnum
            end
        end
    end
    return basePriority
end
 
local function get_selective_extra(id, dataPage, paramNames)
    if not dp or type(dp.flattenFieldSelectiveDirect) ~= "function" then
        return ""
    end
    if type(paramNames) ~= "table" or #paramNames == 0 then
        return ""
    end
 
    return dp.flattenFieldSelectiveDirect(id, dataPage, paramNames) or ""
end


            local prerequisites = ""
local function add_card_tag_value(tags, seen, value)
            if tech.technologyPrerequisites and #tech.technologyPrerequisites > 0 then
    value = trim(value or "")
                prerequisites = '<ul>'
    if value == "" or seen[value] then
                for _, prerequisiteId in ipairs(tech.technologyPrerequisites) do
        return
                    if prerequisiteId and prerequisiteId ~= "" then
    end
                        local prerequisiteName = ""
    seen[value] = true
                        for _, research in ipairs(dataCache) do
    tags[#tags + 1] = value
                            if research.technology and research.technology.id == prerequisiteId then
end
                                prerequisiteName = research.technology.name
 
                                break
local function merge_card_tag_text(...)
                            end
    local tags = {}
                        end
    local seen = {}
                        if prerequisiteName ~= "" then
 
                            prerequisites = prerequisites .. '<li>{{#invoke:Ftl|main|translation|' .. prerequisiteName .. '}}</li>'
    for i = 1, select("#", ...) do
        each_csv_value(select(i, ...), function(value)
            add_card_tag_value(tags, seen, value)
        end)
    end
 
    return table.concat(tags, ", ")
end
 
local function add_entries_from_meta(state, parsed, ctx, filter, isPreview)
    for _, sw in ipairs(switchModeOrder) do
        local mode = switchModeRegistry[sw] or {}
        local keys
        if type(mode.get_keys) == "function" then
            keys = mode.get_keys(parsed)
        else
            keys = (type(parsed) == "table" and parsed[sw]) or {}
        end
 
        if type(keys) == "table" then
            for _, key in ipairs(keys) do
                if (not filter) or should_include_key(filter, sw, key) then
                    local buildFn = isPreview and (mode.build_preview_entry or mode.build_entry) or mode.build_entry
                    if type(buildFn) == "function" then
                        local entry = buildFn(ctx, key)
                        if entry then
                            add_switch_entry(state, sw, key, entry)
                         end
                         end
                     end
                     end
                 end
                 end
                prerequisites = prerequisites .. '</ul>'
             end
             end
        end


             local unlocks = ""
        if (not isPreview) and type(mode.add_entity_extras) == "function" then
             if tech.recipeUnlocks and #tech.recipeUnlocks > 0 then
             mode.add_entity_extras(state, parsed, ctx)
                unlocks = '<ul>'
        end
                for _, recipe in ipairs(tech.recipeUnlocks) do
    end
                    if recipe and recipe ~= "" then
end
                        unlocks = unlocks .. '<li>{{#invoke:Entity Lookup|createimagetooltip|Файл:'
 
                            .. recipe .. '.png|' .. recipe
local function extract_whitelist_search_strings(keyFilter)
                            .. '|Мета=32x32px,link=}} {{#invoke:Entity Lookup|getname|'
    if not keyFilter or not keyFilter.hasWhitelist then
                            .. recipe .. '}}</li>'
        return nil
                     end
    end
 
    local strings = {}
    for sw, keys in pairs(keyFilter.whitelist) do
        if type(keys) == "table" then
            for key in pairs(keys) do
                strings[#strings + 1] = key
             end
        end
    end
 
    if #strings == 0 then
        return nil
    end
 
    return strings
end
 
local function content_matches_whitelist(content, searchStrings)
    if not searchStrings then
        return true
    end
    if not content then
        return false
    end
 
    for _, s in ipairs(searchStrings) do
        if string.find(content, s, 1, true) then
            return true
        end
    end
 
    return false
end
 
local function each_entity_data(frame, id, onEntity, onMissing, keyFilter)
    local componentWhitelist = frame.args.componentWhitelist or frame.args.componentwhitelist or ""
    local componentBlacklist = frame.args.componentBlacklist or frame.args.componentblacklist or ""
    local prototypeWhitelist = frame.args.prototypeWhitelist or frame.args.prototypewhitelist or ""
    local prototypeBlacklist = frame.args.prototypeBlacklist or frame.args.prototypeblacklist or ""
 
    local prototypeStoreDefs = load_module_data("prototype_store.json")
    if not prototypeStoreDefs then
        return false
    end
 
    local foundComponents, foundPrototypes = collect_entity_sets(id, prototypeStoreDefs,
        componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
 
    local compWhitelistSet = parse_csv_set(componentWhitelist)
    local compBlacklistSet = parse_csv_set(componentBlacklist)
    local protoWhitelistSet = parse_csv_set(prototypeWhitelist)
    local protoBlacklistSet = parse_csv_set(prototypeBlacklist)
 
    local compHasWhitelist = next(compWhitelistSet) ~= nil
    local protoHasWhitelist = next(protoWhitelistSet) ~= nil
    local anyEntityWhitelist = compHasWhitelist or protoHasWhitelist
 
    local whitelistSearchStrings = extract_whitelist_search_strings(keyFilter)
 
    local function processEntity(kind, name, isStore)
        local pathName = lcfirst(name)
        local tplPath = kind .. "/" .. pathName
        if isStore then
            tplPath = tplPath .. "/store"
        end
 
        local content = load_template_content(tplPath)
        if not content then
            if onMissing then
                onMissing(kind, name, isStore, tplPath)
            end
            return
        end
 
        if not content_matches_whitelist(content, whitelistSearchStrings) then
            return
        end
 
        local parsed = getTemplateMeta(frame, tplPath)
        if type(parsed) ~= "table" then
            parsed = {}
        end
 
        local extra = ""
        local paramNames = get_template_params(tplPath, content)
        if dp then
            local dataPage = tplPath .. ".json"
            extra = get_selective_extra(id, dataPage, paramNames)
        end
 
        onEntity(parsed, {
            tplPath = tplPath,
            id = id,
            extra = extra,
            source = make_source(kind, name, pathName, tplPath),
            priority = resolve_priority(parsed)
        })
    end
 
    for compName in pairs(foundComponents) do
        if not anyEntityWhitelist or compHasWhitelist then
            processEntity("component", compName, false)
        end
    end
    for protoName in pairs(foundPrototypes) do
        if not anyEntityWhitelist or protoHasWhitelist then
            processEntity("prototype", protoName, false)
        end
    end
 
    local componentStoreDefs = load_module_data("component_store.json")
    if type(componentStoreDefs) == "table" and (not anyEntityWhitelist or compHasWhitelist) then
        local compStore = componentStoreDefs[id]
        if type(compStore) == "table" then
            for compName in pairs(compStore) do
                local allowed = true
                if compBlacklistSet[compName] then
                    allowed = false
                elseif compHasWhitelist and not compWhitelistSet[compName] then
                    allowed = false
                end
                if allowed then
                    processEntity("component", compName, true)
                end
            end
        end
    end
 
    if type(prototypeStoreDefs) == "table" and (not anyEntityWhitelist or protoHasWhitelist) then
        local protoStore = prototypeStoreDefs[id]
        if type(protoStore) == "table" then
            for protoName in pairs(protoStore) do
                local allowed = true
                if protoBlacklistSet[protoName] then
                    allowed = false
                elseif protoHasWhitelist and not protoWhitelistSet[protoName] then
                    allowed = false
                end
                if allowed then
                    processEntity("prototype", protoName, true)
                end
            end
        end
    end
 
    return true
end
 
local function collect_card_tag_text(frame, args, id)
    local filter = build_key_filter(args)
    local cardFilter = build_render_options(filter).cardFilter
    local entries = {}
 
    local ok = each_entity_data(frame, id, function(parsed, ctx)
        local keys = parsed.card or {}
 
        if type(keys) == "table" then
            for _, key in ipairs(keys) do
                local compositeKey = (key:find("_", 1, true)) and key or ("Сущность_" .. key)
                local isWhitelisted = matches_card_list(cardFilter.whitelist, key, compositeKey)
                local isBlacklisted = matches_card_list(cardFilter.blacklist, key, compositeKey)
                local allowCardEntry = not isWhitelisted and not isBlacklisted and
                    ((not cardFilter.hasWhitelist) or matches_card_list(cardFilter.cardWhitelist, key, compositeKey))
 
                if allowCardEntry then
                    entries[#entries + 1] = {
                        tplTag = makeTplCall(ctx.tplPath, "cardTag", key, ctx.id, ctx.extra),
                        priority = ctx.priority,
                        idx = #entries + 1
                     }
                 end
                 end
                unlocks = unlocks .. '</ul>'
             end
             end
        end


             local templateArgs = {
        if parsed.cardTag and parsed.cardTag ~= "" then
                 id = tech.id,
             entries[#entries + 1] = {
                icon = iconPath,
                 tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                name = tech.name,
                 priority = ctx.priority,
                discipline = tech.discipline,
                 idx = #entries + 1
                tier = tech.tier,
                tierColor = tierColor,
                disciplineName = disciplineName,  
                 cost = tech.cost,
                 unlocks = unlocks
             }
             }
             if prerequisites ~= "" then
        end
                 templateArgs.prerequisites = prerequisites
    end)
 
    if not ok then
        return trim(args.tag or "")
    end
 
    sort_entries_by_priority(entries)
 
    local tags = {}
    local seen = {}
    for _, entry in ipairs(entries) do
        local tagText = trim(frame:preprocess(entry.tplTag or "") or "")
        add_card_tag_value(tags, seen, tagText)
    end
 
    return merge_card_tag_text(table.concat(tags, ", "), args.tag)
end
 
p.mergeCardTagText = merge_card_tag_text
p.collectCardTagText = collect_card_tag_text
 
local function build_missing_template_error(kind, name, isStore, tplPath)
    local baseType = (kind and (kind:sub(1, 1):upper() .. kind:sub(2)) or "")
    local classType = baseType
    if isStore then
        classType = classType .. "Store"
    end
    local className = name .. baseType
    local tplLabel = "Template:" .. tplPath
    return "{{сущность/infobox/base|тип=" .. classType .. "|название=" .. className .. "|ссылка=" .. tplLabel .. "}}"
end
 
local function renderBlocks(frame, state, renderOptions, entityId, showSource)
    local outLocal = {}
    local noHeaders = renderOptions and renderOptions.noHeaders
    local cardFilter = renderOptions and renderOptions.cardFilter
    for _, sw in ipairs(switchModeOrder) do
        local mode = switchModeRegistry[sw] or {}
        if mode.full then
            local outStr = ""
            if type(mode.render_full) == "function" then
                outStr = mode.render_full(frame, state.keyOrder[sw], state.keyToTemplates[sw], state.keySources[sw],
                    entityId, noHeaders, showSource, cardFilter)
            end
             if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
        else
            for _, key in ipairs(state.keyOrder[sw] or {}) do
                 local entries = state.keyToTemplates[sw][key] or {}
                if type(mode.render_key) == "function" then
                    local outStr = mode.render_key(frame, key, entries, noHeaders, showSource)
                    if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
                end
             end
             end
        end
    end
    return outLocal
end
function p.get(frame)
    local args = getArgs(frame, { removeBlanks = false })
    local id = args[1] or ""
    if id == "" then return "" end
    local showSource = trim(args.showSource or "") == ""
    local filter = build_key_filter(args)
    local renderOptions = build_render_options(filter)


            out = out .. frame:expandTemplate({
    local state = new_switch_state()
                title = 'Prototypes/Механика/Исследование',
    local errors = {}
                args = templateArgs
    local ok = each_entity_data(frame, id, function(parsed, ctx)
             })
        add_entries_from_meta(state, parsed, ctx, filter, false)
    end, function(kind, name, isStore, tplPath)
        if not filter.hasWhitelist then
             errors[#errors + 1] = build_missing_template_error(kind, name, isStore, tplPath)
         end
         end
         return mw.getCurrentFrame():preprocess(out)
    end, filter)
    if not ok then return "" end
 
    local out = {}
 
    if #errors > 0 then
         out[#out + 1] = '{{сущность/infobox|' .. table.concat(errors, "\n") .. '}}'
    end
 
    renderOptions.noHeaders = filter.hasWhitelist
 
    local blocks = renderBlocks(frame, state, renderOptions, id, showSource)
    for _, b in ipairs(blocks) do
        out[#out + 1] = b
     end
     end
    return frame:preprocess(table.concat(out, "\n"))
end
function p.preview(frame)
    local args = getArgs(frame, { removeBlanks = false })
    local tplPath = args[1] or ""
    if tplPath == "" then return "" end
    local showSource = trim(args.nosource or "") == ""
    local previewFilter = build_key_filter(args)
    local renderOptions = build_render_options(previewFilter)
    local content = load_template_content(tplPath)
    if not content then
        return ""
    end
    local parsed = getTemplateMeta(frame, tplPath) or {}
    if type(parsed) ~= "table" then
        parsed = {}
    end
    local state = new_switch_state()
    add_entries_from_meta(state, parsed, {
        tplPath = tplPath,
        id = "",
        extra = "",
        source = make_source("", tplPath, tplPath, tplPath),
        priority = 1
    }, nil, true)
    local hasWhitelist = previewFilter.hasWhitelist
    renderOptions.noHeaders = hasWhitelist
    local out = {}
    local blocks = renderBlocks(frame, state, renderOptions, "", showSource)
    for _, b in ipairs(blocks) do
        out[#out + 1] = b
    end
    return frame:preprocess(table.concat(out, "\n"))
end
end


return p
return p