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

Нет описания правки
Нет описания правки
 
(не показаны 264 промежуточные версии этого же участника)
Строка 1: Строка 1:
-- Загрузка данных
local p = {}
local latheData    = mw.loadData("Module:IanComradeBot/prototypes/lathe.json/data")
local getArgs = require('Module:Arguments').getArgs
local recipeData  = mw.loadData("Module:IanComradeBot/prototypes/lathe/recipes.json/data")
local JsonPaths = require('Module:JsonPaths')
local researchData = mw.loadData("Module:IanComradeBot/prototypes/research.json/data")
 
local materialData = mw.loadData("Module:IanComradeBot/prototypes/materials.json/data")
local dpOk, dpModule = pcall(require, "Module:GetField")
local chemData    = mw.loadData("Module:IanComradeBot/chem prototypes.json/data")
local dp = dpOk and dpModule or nil
 
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)
if type(materialData) ~= "table" or not next(materialData) then
    local moduleName = JsonPaths.get(page)
     mw.log("Материалов не загружено или получены пустые данные. Проверьте Module:IanComradeBot/prototypes/materials.json/data")
    local ok, data = pcall(mw.loadData, moduleName)
    if not ok then
        return nil
    end
     return data
end
end


local p = {}
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 function format_seconds_to_short_string(input_seconds)
     local out = {}
     local minutes = math.floor(input_seconds / 60)
     local seen = {}
     local seconds = input_seconds % 60


     local minutes_part = minutes > 0 and (minutes .. " мин.") or nil
     if type(entity) ~= "table" then
     local seconds_part = seconds > 0 and (seconds .. " сек.") or nil
        return out
     end


     if minutes_part and seconds_part then
    local comps = entity.components
         return minutes_part .. " " .. seconds_part
     if type(comps) ~= "table" then
     elseif seconds_part then
         return out
         return seconds_part
    end
    elseif minutes_part then
 
         return minutes_part
     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
     else
         return '0 сек.'
         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
     end
    table.sort(out)
    return out
end
end


-- Функция для сортировки рецептов
local function load_entity_components_from_dp(entityId)
local function sortRecipesByPriority(recipes)
     if not dp then
     table.sort(recipes, function(a, b)
        return nil
         local priority = { Static = 1, EMAG = 3 }
    end
         local aPriority = priority[a.discipline] or 2
 
         local bPriority = priority[b.discipline] or 2
    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


        if a.isEmag ~= b.isEmag then
local function load_entity_components(entityId)
             return not a.isEmag
    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
         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 aPriority == bPriority then
    if type(entityOrId) == "string" then
             if a.tier == b.tier then
        local entity = load_entity_data(entityOrId)
                 return a.discipline < b.discipline
        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 a.tier < b.tier
         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


         return aPriority < bPriority
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)
end
end


function p.main(frame)
local function make_source(kind, name, pathName, tplPath)
     -- Подключение CSS
    return { kind = kind, name = name, pathName = pathName, tplPath = tplPath }
     local cssLink = frame:extensionTag('templatestyles', '', {
end
         src = 'Шаблон:Prototypes/Машина/Станок/styles.css'
 
     })
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
 
local function renderGroupedTitleBlocks(frame, keyOrder, keyToTemplates, noHeaders, showSource)
    local groups = {}
    local groupOrder = {}
 
    for _, key in ipairs(keyOrder or {}) do
        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
            parts[#parts + 1] = "<h2>" .. mw.text.encode(mainTitle) .. "</h2>"
        end
 
        for _, block in ipairs(group.blocks or {}) do
            local tplCalls, sources = collect_tpl_calls(block.entries or {})
            local blockText = renderTitleBlock(block.subTitle or mainTitle, tplCalls, sources, false, frame, showSource)
            if blockText ~= "" then
                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 #parts > 0 then
            out[#out + 1] = table.concat(parts, "\n")
        end
    end
 
    return table.concat(out, "\n")
end
 
local function normalizeFilterKey(s)
    s = trim(s or "")
    s = s:gsub("%s*_%s*", "_")
    return s
end
 
local function matches_card_list(list, callKey, compositeKey)
    if not list then
        return false
    end
    callKey = normalizeFilterKey(callKey)
    compositeKey = normalizeFilterKey(compositeKey)
    return list[callKey] or list[compositeKey] or false
end
 
local function buildCardCall(merged, entityId)
    local parts = {}
 
    if entityId and entityId ~= "" then
        parts[#parts + 1] = "id=" .. mw.text.encode(entityId)
    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


    local latheId = frame.args[1] or ""
     if #parts == 0 then
     if latheId == "" then
         return ""
         return '<div style="color:red;">Не указан ID станка.</div>'
     end
     end


     local lathe = nil
    return "{{карточка/сущность|" .. table.concat(parts, "|") .. "}}"
     for _, data in ipairs(latheData or {}) do
end
         if data.id == latheId then
 
             lathe = data
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
 
                    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
 
                if allowCardEntry and tagText ~= "" then
                    if not merged.tagSet[tagText] then
                        merged.tagSet[tagText] = true
                        merged.tags[#merged.tags + 1] = tagText
                    end
                end
            end
        end
    end
 
    each_csv_value(frame.args.cardTag or "", function(extraTag)
        if not merged.tagSet[extraTag] then
            merged.tagSet[extraTag] = true
            merged.tags[#merged.tags + 1] = extraTag
        end
    end)
 
    local out = {}
    if #rawContentParts > 0 then
        out[#out + 1] = table.concat(rawContentParts, "\n")
    end
 
    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
 
        local hasContent = false
        for _, v in pairs(merged.contentByKey or {}) do
            if v and v ~= "" then
                hasContent = true
                break
            end
        end
 
        if not hasLabel and not hasContent then
            return table.concat(out, "\n")
        end
    end
 
    if cardCall ~= "" then
        out[#out + 1] = cardCall
    end
 
    return table.concat(out, "\n")
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
             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
     end
     end


     if not lathe then
     for name in pairs(blacklistSet or {}) do
         return '<div style="color:red;">Станок с ID "' .. latheId .. '" не найден.</div>'
         foundSet[name] = nil
     end
     end
end
local function collect_entity_sets(id, prototypeStoreDefs, componentWhitelist, componentBlacklist, prototypeWhitelist, prototypeBlacklist)
    local foundComponents, foundPrototypes = {}, {}


     -- Построение словаря материалов по их ID
     local compList = load_entity_components(id)
     local materialMapping = {}
     if type(compList) == "table" then
    for _, material in ipairs(materialData or {}) do
        for _, v in ipairs(compList) do
        if material.id then
            if type(v) == "string" and v ~= "" then
            -- Приоритет: stackEntity > id > name
                foundComponents[v] = true
            materialMapping[ material.id ] = material.stackEntity or material.id or material.name
            end
         end
         end
     end
     end


     local chemMapping = {}
     local protoStore = prototypeStoreDefs and prototypeStoreDefs[id]
     for id, chem in pairs(chemData or {}) do
     if type(protoStore) == "table" then
        chemMapping[id] = chem.name
        for protoName in pairs(protoStore) do
            if type(protoName) == "string" and protoName ~= "" then
                foundPrototypes[protoName] = true
            end
        end
     end
     end


     local out = cssLink
     apply_entity_set_filters(foundComponents, parse_csv_set(componentWhitelist), parse_csv_set(componentBlacklist))
     local recipes = {}
     apply_entity_set_filters(foundPrototypes, parse_csv_set(prototypeWhitelist), parse_csv_set(prototypeBlacklist))


     local function getRecipeDetails(recipeId)
     return foundComponents, foundPrototypes
         for _, recipe in ipairs(recipeData or {}) do
end
             if recipe.id == recipeId then
 
                 return recipe
local function resolve_priority(parsed)
    local basePriority = 1
    if type(parsed) == "table" and parsed.priority ~= nil then
         if type(parsed.priority) == "number" then
             basePriority = parsed.priority
        else
            local pnum = tonumber(parsed.priority)
            if pnum then
                 basePriority = pnum
             end
             end
         end
         end
        return nil
     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 function add_card_tag_value(tags, seen, value)
    value = trim(value or "")
    if value == "" or seen[value] then
        return
    end
    seen[value] = true
    tags[#tags + 1] = value
end
local function merge_card_tag_text(...)
    local tags = {}
    local seen = {}
    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


    local function findInResearch(recipeId)
        if type(keys) == "table" then
        for _, research in ipairs(researchData or {}) do
            for _, key in ipairs(keys) do
            if research.technology and research.technology.recipeUnlocks then
                if (not filter) or should_include_key(filter, sw, key) then
                for _, unlock in ipairs(research.technology.recipeUnlocks) do
                    local buildFn = isPreview and (mode.build_preview_entry or mode.build_entry) or mode.build_entry
                     if unlock == recipeId then
                     if type(buildFn) == "function" then
                         return {
                         local entry = buildFn(ctx, key)
                            name = research.technology.name,
                        if entry then
                             tier = research.technology.tier,
                             add_switch_entry(state, sw, key, entry)
                            discipline = research.technology.discipline
                         end
                         }
                     end
                     end
                 end
                 end
             end
             end
         end
         end
        if (not isPreview) and type(mode.add_entity_extras) == "function" then
            mode.add_entity_extras(state, parsed, ctx)
        end
    end
end
local function extract_whitelist_search_strings(keyFilter)
    if not keyFilter or not keyFilter.hasWhitelist then
        return nil
    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
         return nil
     end
     end


     -- Обработка staticRecipes
     return strings
     if lathe.Lathe and lathe.Lathe.staticRecipes then
end
         for _, recipeId in ipairs(lathe.Lathe.staticRecipes) do
 
            local recipe = getRecipeDetails(recipeId)
local function content_matches_whitelist(content, searchStrings)
             if recipe and recipe.result then
    if not searchStrings then
                table.insert(recipes, {
        return true
                    result = recipe.result,
    end
                    completetime = recipe.completetime,
     if not content then
                    materials = recipe.materials,
         return false
                    discipline = "Static",
    end
                    tier = 0
 
                })
    for _, s in ipairs(searchStrings) do
            elseif recipe and recipe.resultReagents then
        if string.find(content, s, 1, true) then
                for reagent, amount in pairs(recipe.resultReagents) do
             return true
                    local reagentName = chemMapping[reagent] or reagent
        end
                    table.insert(recipes, {
    end
                        result = reagentName .. "|amount=" .. amount .. "ед.|mode-chem=1",
 
                        completetime = recipe.completetime,
    return false
                        materials = recipe.materials,
end
                        discipline = "Static",
 
                        tier = 0
local function each_entity_data(frame, id, onEntity, onMissing, keyFilter)
                    })
    local componentWhitelist = frame.args.componentWhitelist or frame.args.componentwhitelist or ""
                    break -- обрабатываем только первый реагент
    local componentBlacklist = frame.args.componentBlacklist or frame.args.componentblacklist or ""
                end
    local prototypeWhitelist = frame.args.prototypeWhitelist or frame.args.prototypewhitelist or ""
            else
    local prototypeBlacklist = frame.args.prototypeBlacklist or frame.args.prototypeblacklist or ""
                out = out .. '<div style="color:red;">Ошибка: Рецепт с ID "' .. recipeId .. '" не найден или поля result/resultReagents отсутствуют.</div>'
 
    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
             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
     end
     end


     -- Обработка dynamicRecipes
     local componentStoreDefs = load_module_data("component_store.json")
     if lathe.Lathe and lathe.Lathe.dynamicRecipes then
     if type(componentStoreDefs) == "table" and (not anyEntityWhitelist or compHasWhitelist) then
         for _, recipeId in ipairs(lathe.Lathe.dynamicRecipes) do
         local compStore = componentStoreDefs[id]
            local recipe = getRecipeDetails(recipeId)
        if type(compStore) == "table" then
            if recipe then
            for compName in pairs(compStore) do
                local researchInfo = findInResearch(recipeId)
                local allowed = true
                 if researchInfo then
                if compBlacklistSet[compName] then
                     table.insert(recipes, {
                    allowed = false
                        result = recipe.result,
                 elseif compHasWhitelist and not compWhitelistSet[compName] then
                        completetime = recipe.completetime,
                     allowed = false
                        materials = recipe.materials,
                end
                        discipline = researchInfo.discipline,
                if allowed then
                        tier = researchInfo.tier,
                    processEntity("component", compName, true)
                        researchName = researchInfo.name
                    })
                 end
                 end
             end
             end
Строка 170: Строка 994:
     end
     end


    -- Обработка emagStaticRecipes
     if type(prototypeStoreDefs) == "table" and (not anyEntityWhitelist or protoHasWhitelist) then
     if lathe.EmagLatheRecipes and lathe.EmagLatheRecipes.emagStaticRecipes then
         local protoStore = prototypeStoreDefs[id]
         for _, recipeId in ipairs(lathe.EmagLatheRecipes.emagStaticRecipes) do
        if type(protoStore) == "table" then
            local recipe = getRecipeDetails(recipeId)
            for protoName in pairs(protoStore) do
            if recipe then
                local allowed = true
                 table.insert(recipes, {
                if protoBlacklistSet[protoName] then
                     result = recipe.result,
                    allowed = false
                    completetime = recipe.completetime,
                 elseif protoHasWhitelist and not protoWhitelistSet[protoName] then
                    materials = recipe.materials,
                     allowed = false
                     discipline = "Static",
                end
                    tier = 0,
                if allowed then
                    isEmag = true
                     processEntity("prototype", protoName, true)
                 })
                 end
             end
             end
         end
         end
     end
     end


     -- Обработка emagDynamicRecipes
     return true
     if lathe.EmagLatheRecipes and lathe.EmagLatheRecipes.emagDynamicRecipes then
end
        for _, recipeId in ipairs(lathe.EmagLatheRecipes.emagDynamicRecipes) do
 
            local recipe = getRecipeDetails(recipeId)
local function collect_card_tag_text(frame, args, id)
            if recipe then
    local filter = build_key_filter(args)
                 local researchInfo = findInResearch(recipeId)
     local cardFilter = build_render_options(filter).cardFilter
                 if researchInfo then
    local entries = {}
                     table.insert(recipes, {
 
                         result = recipe.result,
    local ok = each_entity_data(frame, id, function(parsed, ctx)
                        completetime = recipe.completetime,
        local keys = parsed.card or {}
                        materials = recipe.materials,
 
                        discipline = researchInfo.discipline,
        if type(keys) == "table" then
                        tier = researchInfo.tier,
            for _, key in ipairs(keys) do
                         researchName = researchInfo.name,
                local compositeKey = (key:find("_", 1, true)) and key or ("Сущность_" .. key)
                         isEmag = true
                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
             end
             end
         end
         end
        if parsed.cardTag and parsed.cardTag ~= "" then
            entries[#entries + 1] = {
                tplTag = makeTplCall(ctx.tplPath, "cardTag", "cardTag", ctx.id, ctx.extra),
                priority = ctx.priority,
                idx = #entries + 1
            }
        end
    end)
    if not ok then
        return trim(args.tag or "")
     end
     end


     sortRecipesByPriority(recipes)
     sort_entries_by_priority(entries)


     -- Таблица для перевода названий дисциплин
     local tags = {}
     local disciplineMapping = {
     local seen = {}
        Arsenal          = "Арсенал",
    for _, entry in ipairs(entries) do
         Industrial        = "Промышленность",
         local tagText = trim(frame:preprocess(entry.tplTag or "") or "")
        Experimental      = "Экспериментальное",
         add_card_tag_value(tags, seen, tagText)
         CivilianServices  = "Обслуживание персонала"
     end
     }


     -- Таблица для цветов по уровням
     return merge_card_tag_text(table.concat(tags, ", "), args.tag)
    local tierColors = {
end
        [1] = "#54d554",
        [2] = "#ed9000",
        [3] = "#d72a2a"
    }


    local materialUseMultiplier = (lathe.Lathe and lathe.Lathe.materialUseMultiplier) or 1
p.mergeCardTagText = merge_card_tag_text
    local timeMultiplier        = (lathe.Lathe and lathe.Lathe.timeMultiplier) or 1
p.collectCardTagText = collect_card_tag_text


     for _, recipe in ipairs(recipes) do
local function build_missing_template_error(kind, name, isStore, tplPath)
         local scaledTime = format_seconds_to_short_string(recipe.completetime * timeMultiplier)
     local baseType = (kind and (kind:sub(1, 1):upper() .. kind:sub(2)) or "")
        out = out .. '{{Шаблон:Prototypes/Машина/Станок|product=' .. recipe.result
    local classType = baseType
        out = out .. '|complete-time=' .. scaledTime
    if isStore then
        out = out .. '|materials='
         classType = classType .. "Store"
    end
    local className = name .. baseType
    local tplLabel = "Template:" .. tplPath
    return "{{сущность/infobox/base|тип=" .. classType .. "|название=" .. className .. "|ссылка=" .. tplLabel .. "}}"
end


        local materials_str = ""
local function renderBlocks(frame, state, renderOptions, entityId, showSource)
        if recipe.materials and type(recipe.materials) == "table" and next(recipe.materials) then
    local outLocal = {}
            for material, amount in pairs(recipe.materials) do
    local noHeaders = renderOptions and renderOptions.noHeaders
                local stackEntity = materialMapping[material] or material
    local cardFilter = renderOptions and renderOptions.cardFilter
                local scaledAmount = (amount * materialUseMultiplier) / 100
    for _, sw in ipairs(switchModeOrder) do
                 materials_str = materials_str .. '<b>[[File:' .. stackEntity .. '.png|32x32px|link=]] '
        local mode = switchModeRegistry[sw] or {}
                              .. scaledAmount .. ' {{#invoke:Entity Lookup|getname|' .. stackEntity .. '}}</b> '
        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
             end
            if outStr and outStr ~= "" then outLocal[#outLocal + 1] = outStr end
         else
         else
             materials_str = 'Нет данных о материалах'
             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


        out = out .. materials_str
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 "") == ""
        if recipe.discipline ~= "Static" then
            local tierColor = tierColors[recipe.tier] or "#FFFFFF"
            local disciplineName = disciplineMapping[recipe.discipline] or "Неизвестная дисциплина"


            out = out .. '|info=<div style="font-weight:600;"><span style="margin:8px;">[[File:'
    local filter = build_key_filter(args)
                .. recipe.discipline .. '.png|16x16px|link=]]</span> [[Руководство по исследованию и разработке|'
    local renderOptions = build_render_options(filter)
                .. disciplineName .. ']], уровень: <span style="color: ' .. tierColor .. '">'
 
                .. recipe.tier .. '</span> </div>'
    local state = new_switch_state()
    local errors = {}
    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
    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
    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


         -- Пометка при взломе EMAG
    local showSource = trim(args.nosource or "") == ""
        if recipe.isEmag then
    local previewFilter = build_key_filter(args)
            out = out .. '|mode-emag=1'
    local renderOptions = build_render_options(previewFilter)
        end
 
    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
        if recipe.discipline ~= "Static" then
    renderOptions.noHeaders = hasWhitelist
            out = out .. '|mode-research=1'
        end


         out = out .. '}}'
    local out = {}
    local blocks = renderBlocks(frame, state, renderOptions, "", showSource)
    for _, b in ipairs(blocks) do
         out[#out + 1] = b
     end
     end


     return mw.getCurrentFrame():preprocess(out)
     return frame:preprocess(table.concat(out, "\n"))
end
end


return p
return p