Модуль:Cron/GenerateStats

Материал из ЧТМ
Версия от 02:55, 6 июня 2026; Lord (обсуждение | вклад) (добавлен БЛОК СБОРКИ ДЕТАЛЬНЫХ ДАННЫХ ДЛЯ КАСТОМНЫХ СТРАНИЦ (Module:Stats/*))
Перейти к навигации Перейти к поиску
Документация Документация

Модуль:Cron/GenerateStats представляет собой служебный скрипт на языке Lua (Scribunto) для ЧТМ Вики, предназначенный для агрегации, расчёта и кэширования полной статистической базы данных проекта.

Основная задача модуля — собрать данные обо всех сыгранных матчах, игроках, командах и наградах за все сезоны, провести ресурсоёмкие вычисления (такие как расчёт игровых серий, вычисление лидеров, распределение медалей и интеграция рейтинга) и экспортировать финальный структурированный результат в формате JSON. Полученный JSON-кэш используется другими модулями для быстрой отрисовки страниц и таблиц без повторного обхода всей базы данных при каждом просмотре.


1. Зависимости модуля

Для работы скрипт импортирует следующие компоненты:

  • Module:Config — содержит общую конфигурацию проекта (список сезонов years, настройки эры матчей eras, правила для плей-офф и корректировки).
  • Module:Data/Teams — справочник футбольных/спортивных команд.
  • Module:StatEngine/Pure — базовый движок сбора статистики. Метод Harvester.run_all_time производит первичный обход баз данных матчей.
  • Module:StatEngine/TournamentAwards — логика определения лучших игроков турниров, распределения номинаций (лучший бомбардир, ассистент) и обработки игровых дней (matchdays).
  • Module:Megarating — предоставляет исторические данные рейтинга игроков через метод get_public_history().
  • Module:StatEngine/StreaksCore — специализированный компонент для вычисления последовательных серий (голевых, беспроигрышных, вратарских "сухих" и т. д.).
  • Module:Автоматическая статистика — содержит метаданные и структуру строк (row_defs) для формирования индивидуальных профилей игроков.

2. Внутренние (вспомогательные) функции

  • plural(n, w1, w2, w5)
    Реализует алгоритм русской локализации числительных (согласование существительных с числами).

    • Параметры: n (число), w1 ("день"), w2 ("дня"), w5 ("дней").
    • Возвращает: соответствующую форму слова в зависимости от значения n.
  • date_diff(date1, date2)
    Вычисляет разницу во времени между двумя датами в формате YYYY-MM-DD.

    • Возвращает:
      1. Количество дней (число) для сравнения интервалов.
      2. Текстовое представление разницы на русском языке (например, "1 год 2 месяца 5 дней").
  • apply_adj(s, m, v)
    Применяет ручные корректировки показателей игрока, если данные за старые периоды неполные или требуют точечного исправления.

    • Параметры: s — таблица статистики игрока, m — идентификатор метрики (например, "goals", "clean_sheets"), v — числовое значение корректировки (прибавляется к текущему).

3. Алгоритм работы Cron.main

При вызове функции Cron.main(frame) выполняются следующие шаги:

Шаг 3.1. Загрузка и первичный сбор данных

  1. Из Module:Config считывается массив активных сезонов. Скрипт поочерёдно загружает модули данных вида Module:Data/<Год> через безопасный вызов pcall.
  2. Функция StatEngine.Harvester.run_all_time собирает агрегированную сырую статистику по игрокам, командам и их связкам (GrandStats).

Шаг 3.2. Применение ручных корректировок

  • На основе конфигурационных параметров Config.award_adjustments и индивидуальных настроек метрик Config.metrics вносятся исправления в показатели игроков за конкретные годы (например, добавляются голы или ассисты, не зафиксированные в стандартных протоколах).

Шаг 3.3. Поматчевый хронологический анализ

  • Все матчи из всех сезонных баз данных объединяются в плоский список и сортируются строго по дате и ID матча.
  • Происходит последовательный обход матчей с помощью StreaksEngine для расчёта игровых серий.
  • Накапливаются данные о дебютных матчах игроков (navbox_data.debuts).
  • Для сезонов начиная с 2022 года формируется детальная поматчевая история игрока (player_matches_list), включающая:
    • Статус (старт/замена, роль вратаря или полевого).
    • Забитые мячи (включая тип: головой, автогол, с пенальти).
    • Голевые передачи.
    • Участие в сериях послематчевых пенальти и отражённые удары.
    • События матча (карточки, выносы из пустых ворот и т.д.).
  • Фиксируются перерывы в карьере игроков (вычисляется наибольший интервал без матчей через date_diff).
  • Выявляются победители турниров, обладатели индивидуальных наград финалов (Золотой мяч, Золотая бутса, лучший ассистент, приз Эльнура и др.).

Шаг 3.4. Анализ игровых дней (Matchdays)

  • Для сезонов с 2022 года с помощью TournamentAwards.evaluateMatchdayPrizes рассчитываются индивидуальные достижения игроков в рамках отдельных туров/игровых дней (призы за MVP тура, лучший вратарь, результативность и т.д.). Эти данные суммируются в профилях игроков.

Шаг 3.5. Определение лидеров сезона и распределение медалей

  • По каждой метрике из списка автоматической статистики определяется список лидеров за каждый год.
  • С помощью TournamentAwards.getGenericMetric рассчитываются "медали" (золотые, серебряные, бронзовые, деревянные) для игроков как за отдельные сезоны, так и в суммарном рейтинге за всё время. Медали присваивают игрокам соответствующие цветовые маркеры (color).

Шаг 3.6. Интеграция рейтинга

  • Данные о динамике Мегарейтинга (mr_hist) распределяются по годам в профили игроков.

Шаг 3.7. Подсчёт глобальных рекордов

  • Вычисляются максимальные (или минимальные для GAA) показатели среди всех игроков:
    • За всю историю проекта (PA_GlobalRecords.AllTime).
    • В рамках одного конкретного турнира (PA_GlobalRecords.PerTournament).

Шаг 3.8. Минимизация размера JSON

  • Для снижения нагрузки на парсер MediaWiki производится очистка структуры данных. Из таблиц игроков удаляются пустые поля, неиспользуемые годы и метрики, у которых все показатели (значение, числитель, знаменатель, медали) равны нулю.

Шаг 3.9. Сериализация и вывод

  • Собранная таблица ExportData преобразуется в JSON-строку при помощи встроенной функции mw.text.jsonEncode и возвращается вызывающей стороне.

4. Структура экспортируемого JSON-кэша

Финальный JSON содержит две основные ветки: Global (глобальные метаданные) и Players (индивидуальные профили).

{
  "Global": {
    "navbox_data": {
      "champs": { "2022": ["Игрок А", "Игрок Б"], "2023": [...] },
      "superchamps": [ [2022, "Игрок А"] ],
      "zshar": [ [2022, "Игрок Б"] ],
      "zbashmak": [ [2022, "Игрок В", 15, "TEAM_CODE"] ],
      "elnur": [],
      "final_goals": { "Игрок А": 5 },
      "final_assists": { "Игрок Б": 3 },
      "loyalty": [ [2022, "Игрок А", "Команда Х"] ],
      "debuts": { "Игрок А": 2021 },
      "champ_counts": { "Игрок А": 3 },
      "global_goals": { "Игрок А": 120 },
      "team_matches": { "2022": { "Игрок А": { "TEAM_CODE": 10 } } }
    },
    "PA_GlobalRecords": {
      "AllTime": { "goals": 150, "max_streaks_goals": 8, "...": 0 },
      "PerTournament": { "goals": 25, "...": 0 }
    },
    "PA_CustomStats": {
      "club100": [ { "name": "Игрок А", "year": 2022 } ],
      "stadiums": { "Стадион 1": { "Игрок А": 45 } },
      "finals": { "2022": { "goals_count": {}, "superchampions": [], "...": "" } }
    },
    "PA_Leaders": {
      "goals": { "2022": ["Игрок А"], "2023": ["Игрок Б", "Игрок В"] }
    },
    "PA_TeamLeaders": {
      "TEAM_CODE": { "players": ["Игрок А"], "goals": 80 }
    },
    "PA_StadiumLeaders": {
      "Стадион 1": { "players": ["Игрок А"], "goals": 45 }
    },
    "PA_ShoeTeams": { "2022": { "Игрок А": "TEAM_CODE" } },
    "PA_AssistTeams": { "2022": { "Игрок Б": "TEAM_CODE" } }
  },
  "Players": {
    "Имя Игрока": {
      "name": "Имя Игрока",
      "AS_compiled": {
        "played_years": { "2022": true, "2023": true },
        "last_played_year": 2023,
        "played_before_2022": false,
        "metrics": {
          "goals": {
            "total_val": 42,
            "total_num": 0, "total_den": 0, "color": "gold",
            "years": {
              "2022": { "val": 20, "num": 0, "den": 0, "color": "silver" },
              "2023": { "val": 22, "num": 0, "den": 0, "color": "gold" }
            }
          }
        },
        "player_matches_list": [
          {
            "year": 2022, "num_hist": 123, "date": "2022-05-12", "stage": "Группа",
            "team1": "Команда А", "team2": "Команда Б", "score1": 3, "score2": 1,
            "role_team": 1, "goals": 2, "assists": 1, "is_mvp": true, "events": ["Гол головой"]
          }
        ]
      },
      "PA_Player": {
        "goals": 42, "matches": 50, "champs": 2, "champs_years": [2022, 2023],
        "max_streaks": { "goals": 5, "unbeaten": 12 },
        "tournaments": {
          "2022": { "goals": 20, "matches": 24, "playoff_goals": 4 }
        }
      },
      "mr_history": { "total_pct": 74.5, "years": { "2022": { "pct": 72.0, "pts": 150 } } }
    }
  }
}


5. Использование в других модулях

Чтобы получить доступ к сгенерированной статистике без повторного обсчёта, другие Lua-скрипты используют декодирование JSON-строки, возвращаемой данным модулем.

Пример интеграции в стороннем модуле:

local CronStats = require('Module:Cron/GenerateStats')

local p = {}

function p.showPlayerGoals(frame)
    -- Получаем кэш в формате JSON
    local raw_json = CronStats.main(frame)
    local data = mw.text.jsonDecode(raw_json)
    
    local player_name = frame.args[1] or "Герыч"
    local player_data = data.Players[player_name]
    
    if player_data and player_data.PA_Player then
        return player_name .. " забил всего голов: " .. tostring(player_data.PA_Player.goals)
    else
        return "Игрок не найден или у него нет статистики."
    end
end

return p

Пожалуйста, добавляйте категории на страницу документации.

-- ==========================================
-- Модуль:Cron/GenerateStats
-- Назначение: Генерация полного кэша всей базы данных (JSON)
-- ==========================================
local Cron = {}

local Config = require('Module:Config')
local Teams = require('Module:Data/Teams')
local StatEngine = require('Module:StatEngine/Pure')
local TournamentAwards = require('Module:StatEngine/TournamentAwards')
local ModuleMegarating = require('Module:Megarating')
local StreaksCore = require('Module:StatEngine/StreaksCore')
local AS = require('Module:Автоматическая статистика')

local function plural(n, w1, w2, w5)
    local n10 = n % 10; local n100 = n % 100
    if n100 >= 11 and n100 <= 19 then return w5 end
    if n10 == 1 then return w1 end
    if n10 >= 2 and n10 <= 4 then return w2 end
    return w5
end

local function date_diff(date1, date2)
    local y1, m1, d1 = date1:match("(%d+)%-(%d+)%-(%d+)")
    local y2, m2, d2 = date2:match("(%d+)%-(%d+)%-(%d+)")
    if not y1 or not y2 then return 0, "" end
    y1, m1, d1 = tonumber(y1), tonumber(m1), tonumber(d1)
    y2, m2, d2 = tonumber(y2), tonumber(m2), tonumber(d2)

    local dy = y2 - y1; local dm = m2 - m1; local dd = d2 - d1
    if dd < 0 then dm = dm - 1; dd = dd + 30 end
    if dm < 0 then dy = dy - 1; dm = dm + 12 end

    local total_days = dy * 365 + dm * 30 + dd
    local parts = {}
    if dy > 0 then table.insert(parts, dy .. " " .. plural(dy, "год", "года", "лет")) end
    if dm > 0 then table.insert(parts, dm .. " " .. plural(dm, "месяц", "месяца", "месяцев")) end
    if dd > 0 then table.insert(parts, dd .. " " .. plural(dd, "день", "дня", "дней")) end

    return total_days, table.concat(parts, " ")
end

local function apply_adj(s, m, v)
    if m == "goals" then s.goals.total = (s.goals.total or 0) + v
    elseif m == "head_goals" then s.goals.head = (s.goals.head or 0) + v
    elseif m == "heel_goals" then s.goals.heel = (s.goals.heel or 0) + v
    elseif m == "free_kick_goals" then s.goals.free_kick = (s.goals.free_kick or 0) + v
    elseif m == "goalie_goals" then s.goals.goalie = (s.goals.goalie or 0) + v
    elseif m == "mega_tricks" then s.goals.hat_trick = (s.goals.hat_trick or 0) + v
    elseif m == "assists" then s.assists.total = (s.assists.total or 0) + v
    elseif m == "assist_mega_tricks" then s.assists.hat_trick = (s.assists.hat_trick or 0) + v
    elseif m == "clearances" then s.clearances = (s.clearances or 0) + v
    elseif m == "mvp" then s.mvp.is_mvp = (s.mvp.is_mvp or 0) + v
    elseif m == "mvp_goalie" then s.mvp.is_goalie_mvp = (s.mvp.is_goalie_mvp or 0) + v
    elseif m == "pens_scored" then s.penalties.in_game.g = (s.penalties.in_game.g or 0) + v
    elseif m == "pens_missed" then s.penalties.in_game.u = (s.penalties.in_game.u or 0) + v
    elseif m == "pens_saved" then s.penalties.saved_as_goalie = (s.penalties.saved_as_goalie or 0) + v
    elseif m == "caused_pens" then s.penalties.caused_pens = (s.penalties.caused_pens or 0) + v
    elseif m == "clean_sheets" then s.clean_sheets = (s.clean_sheets or 0) + v
    elseif m == "yellow_cards" then s.cards.yellow = (s.cards.yellow or 0) + v
    elseif m == "red_cards" then s.cards.red = (s.cards.red or 0) + v
    elseif m == "own_goals" then s.own_goals = (s.own_goals or 0) + v
    elseif m == "matches" then s.matches_total = (s.matches_total or 0) + v
    elseif m == "field_matches" then s.matches_field = (s.matches_field or 0) + v
    elseif m == "goalie_matches" then s.matches_goalie = (s.matches_goalie or 0) + v
    elseif m == "matchday_prizes" then s.matchday_prizes = (s.matchday_prizes or 0) + v
    elseif m == "matchday_places" then s.matchday_places = (s.matchday_places or 0) + v
    end
end

function Cron.main(frame)
    local databases = {}
    for _, year in ipairs(Config.years) do
        local success, year_db = pcall(require, 'Module:Data/' .. year)
        if success and type(year_db) == "table" then databases[year] = year_db end
    end

    local GrandStats = StatEngine.Harvester.run_all_time(databases, { 
        need_players = true, need_teams = true, need_combos = true, keep_years = true, keep_matches = false 
    })

    if Config.award_adjustments then
        for year, year_adj in pairs(Config.award_adjustments) do
            for award_type, combos in pairs(year_adj) do
                for combo_key, adj in pairs(combos) do
                    local p_name, t_code = string.match(combo_key, "^(.*)_(.*)$")
                    if p_name and t_code and adj.total then
                        local add = adj.total
                        if GrandStats.Players[p_name] then
                            if award_type == "goals" then GrandStats.Players[p_name].goals.total = GrandStats.Players[p_name].goals.total + add
                            elseif award_type == "assists" then GrandStats.Players[p_name].assists.total = GrandStats.Players[p_name].assists.total + add end
                        end
                        if GrandStats.Years[year] and GrandStats.Years[year].Players[p_name] then
                            if award_type == "goals" then GrandStats.Years[year].Players[p_name].goals.total = GrandStats.Years[year].Players[p_name].goals.total + add
                            elseif award_type == "assists" then GrandStats.Years[year].Players[p_name].assists.total = GrandStats.Years[year].Players[p_name].assists.total + add end
                        end
                        if GrandStats.PlayerTeam[combo_key] then
                            if award_type == "goals" then GrandStats.PlayerTeam[combo_key].goals.total = GrandStats.PlayerTeam[combo_key].goals.total + add
                            elseif award_type == "assists" then GrandStats.PlayerTeam[combo_key].assists.total = GrandStats.PlayerTeam[combo_key].assists.total + add end
                        end
                        if GrandStats.Years[year] and GrandStats.Years[year].PlayerTeam[combo_key] then
                            if award_type == "goals" then GrandStats.Years[year].PlayerTeam[combo_key].goals.total = GrandStats.Years[year].PlayerTeam[combo_key].goals.total + add
                            elseif award_type == "assists" then GrandStats.Years[year].PlayerTeam[combo_key].assists.total = GrandStats.Years[year].PlayerTeam[combo_key].assists.total + add end
                        end
                    end
                end
            end
        end
    end

    for metric_id, metric_cfg in pairs(Config.metrics) do
        if metric_cfg.adjustments and metric_cfg.adjustments.players then
            for p_name_adj, p_adjs in pairs(metric_cfg.adjustments.players) do
                for y, val in pairs(p_adjs) do
                    if type(y) == "number" and metric_id ~= "plus_minus" then
                        if not GrandStats.Players[p_name_adj] then 
                            GrandStats.Players[p_name_adj] = StatEngine.Harvester.create_empty_stats()
                            GrandStats.Players[p_name_adj].name = p_name_adj
                        end
                        if not GrandStats.Years[y] then GrandStats.Years[y] = { Players = {}, Teams = {}, PlayerTeam = {} } end
                        if not GrandStats.Years[y].Players[p_name_adj] then
                            GrandStats.Years[y].Players[p_name_adj] = StatEngine.Harvester.create_empty_stats()
                            GrandStats.Years[y].Players[p_name_adj].name = p_name_adj
                        end
                        apply_adj(GrandStats.Players[p_name_adj], metric_id, val)
                        apply_adj(GrandStats.Years[y].Players[p_name_adj], metric_id, val)
                    end
                end
            end
        end
    end

    local GlobalCache = {
        navbox_data = { champs = {}, superchamps = {}, zshar = {}, zbashmak = {}, elnur = {}, final_goals = {}, final_assists = {}, loyalty = {}, debuts = {}, champ_counts = {}, global_goals = {}, team_matches = {} },
        PA_GlobalRecords = { AllTime = {}, PerTournament = {} },
        PA_CustomStats = { club100 = {}, stadiums = {}, finals = {} },
        PA_Leaders = {}, PA_TeamLeaders = {}, PA_StadiumLeaders = {},
        PA_ShoeTeams = {}, PA_AssistTeams = {},
        MegaratingHistory = {}
    }

    local hardcoded_loyalty = { {2010, "Герыч", "Мьянма"}, {2014, "Антон", "Доминика"}, {2018, "Диман", "Доминика"}, {2018, "Антон", "Доминика"}, {2018, "Диман", "Теркс и Кайкос"}, {2018, "Антон", "Теркс и Кайкос"} }
    for _, l in ipairs(hardcoded_loyalty) do table.insert(GlobalCache.navbox_data.loyalty, l) end

    local PlayersCache = {}
    local function get_pc(name)
        if not PlayersCache[name] then
            PlayersCache[name] = {
                name = name,
                AS_compiled = {
                    metrics = {}, played_years = {}, last_played_year = 0, played_before_2022 = false,
                    player_matches_list = {}
                },
                PA_Player = nil,
                mr_history = nil
            }
            for _, row in ipairs(AS.row_defs) do
                PlayersCache[name].AS_compiled.metrics[row.id] = { total_val = 0, total_num = 0, total_den = 0, years = {} }
            end
        end
        return PlayersCache[name]
    end

    local PA_Players = {}
    local function get_p(name)
        if not PA_Players[name] then
            PA_Players[name] = {
                name = name,
                champs = 0, champs_years = {}, champ_captain = 0, champ_captain_years = {},
                champ_streak = 0, cur_champ_streak = 0, last_champ_year = nil, champ_streak_end_year = nil,
                champ_team_streak = 0, cur_champ_team_streak = 0, last_champ_team = nil, champ_team_streak_code = "", champ_team_streak_end_year = nil,
                superchamps = 0, superchamps_years = {}, golden_goals = 0, golden_goals_years = {}, final_gold_goals_years = {},
                final_goals_real = 0, final_assists_real = 0, final_ga_real = 0,
                max_final_goals = 0, max_final_goals_occurences = {}, finals_played = 0, mvp_final = 0, mvp_final_years = {},
                goals = 0, max_goals_team = 0, max_goals_team_code = "", max_goals_match = 0, max_goals_match_occurences = {},
                mega_tricks = 0, max_mega_tricks_team = 0, max_mega_tricks_team_code = "",
                classic_hat_tricks = 0, pokers = 0, pentas = 0, hexas = 0, pens_scored = 0, pens_in_game_scored = 0,
                max_assists_match = 0, max_assists_match_occurences = {}, pm = 0, md_prizes = 0, md_places = 0,
                assists = 0, heel_goals = 0, golden_spheres = 0, golden_spheres_years = {}, total_spheres = 0, 
                golden_shoes = 0, golden_shoes_years = {}, total_shoes = 0, mvp = 0, max_team_mvp = 0, max_team_mvp_code = "",
                mvp_goalie = 0, mr_points_pct = 0, pens_saved = 0, matches = 0, field_matches = 0, goalie_matches = 0, goalie_goals = 0, head_goals = 0, clearances = 0,
                max_streaks = {}, cur_streaks = {}, last_played_hist = nil, cur_team = nil, max_streaks_team_matches_code = "", max_streaks_goalie_clean_code = "",
                max_gap_days = 0, max_gap_str = "", last_date = nil,
                wins = { goals = 0, goals_years = {}, mega_tricks = 0, mega_tricks_years = {}, pm = 0, pm_years = {}, mvp = 0, mvp_years = {}, mvp_goalie = 0, mvp_goalie_years = {}, head_goals = 0, head_goals_years = {}, clearances = 0, clearances_years = {}, gaa = 0, gaa_years = {} },
                tournaments = {}
            }
        end
        return PA_Players[name]
    end

    local function get_t(p, year)
        if not p.tournaments[year] then
            p.tournaments[year] = {
                year = year, goals = 0, max_goals_team = 0, max_goals_team_code = "",
                mega_tricks = 0, pm = 0, matches = 0, pts_pct = 0, md_prizes = 0, md_places = 0,
                heel_goals = 0, mvp = 0, max_team_mvp = 0, max_team_mvp_code = "",
                playoff_wins = 0, head_goals = 0, clearances = 0, gaa = 0, playoff_goals = 0, winning_goals = 0, avg_goals = 0,
                max_team_assists = 0, max_team_assists_code = "", mvp_goalie = 0, mr_points_pct = 0, mr_points = 0, assists = 0, avg_assists = 0,
                ga = 0, max_team_ga = 0, max_team_ga_code = "", goalie_matches = 0, field_matches = 0, max_mega_tricks_team = 0, max_mega_tricks_team_code = ""
            }
        end
        return p.tournaments[year]
    end

    local flat_matches = {}
    local global_goals = {}
    for _, year in ipairs(Config.years) do
        local year_db = databases[year]
        if year_db then
            GlobalCache.PA_CustomStats.finals[year] = {}
            for id, m in pairs(year_db) do table.insert(flat_matches, { year = year, id = id, m = m, num = m.num_hist or 0 }) end
        end
    end
    table.sort(flat_matches, function(a, b)
        if a.year ~= b.year then return a.year < b.year end
        return tostring(a.id) < tostring(b.id)
    end)

    local StreaksEngine = StreaksCore.new()
    local current_scorer = nil
    local consecutive_goals = 0
    local N = GlobalCache.navbox_data

    for _, item in ipairs(flat_matches) do
        local year = item.year; local match = item.m; local match_id = item.id
        local is_playoff = Config.playoff_stages[match.stage] == true
        local is_final = (match.stage == "Финал" or match.stage == "финал")

        StreaksEngine:process_match(year, match, match.num_hist)
        local p_teams = StatEngine.Harvester.get_all_teams(match)

        for p_name, _ in pairs(p_teams) do if not N.debuts[p_name] then N.debuts[p_name] = year end end
        if year >= 2022 then
            local p_teams_starters = {}
            if match.squad1 then
                if type(match.squad1.starters) == "table" then for _, p_n in ipairs(match.squad1.starters) do p_teams_starters[p_n] = match.team1 end end
                if type(match.squad1.substitutes) == "table" then for _, p_n in ipairs(match.squad1.substitutes) do p_teams_starters[p_n] = match.team1 end end
            end
            if match.squad2 then
                if type(match.squad2.starters) == "table" then for _, p_n in ipairs(match.squad2.starters) do p_teams_starters[p_n] = match.team2 end end
                if type(match.squad2.substitutes) == "table" then for _, p_n in ipairs(match.squad2.substitutes) do p_teams_starters[p_n] = match.team2 end end
            end
            for p_n, t_code in pairs(p_teams_starters) do
                N.team_matches[year] = N.team_matches[year] or {}; N.team_matches[year][p_n] = N.team_matches[year][p_n] or {}
                N.team_matches[year][p_n][t_code] = (N.team_matches[year][p_n][t_code] or 0) + 1
            end
            
            for p_name, t_id in pairs(p_teams) do
                local pc = get_pc(p_name)
                local m_row = {
                    year = year, num_hist = match.num_hist or 0, match_num_id = tonumber(match_id:match("%-(%d+)")) or 0,
                    date = match.date or "", stage = match.stage or "", letter = match.letter,
                    team1 = match.team1, team2 = match.team2, score1 = match.score1, score2 = match.score2,
                    aet = match.aet, shootout1 = match.shootout_score1, shootout2 = match.shootout_score2,
                    wikilink = match.wikilink, role_team = t_id, is_official = true, goals = 0, assists = 0, 
                    is_mvp = (match.mvp and match.mvp.player == p_name), is_goalie = false, field = false, events = {}
                }
                if (t_id == 1 and match.squad1 and match.squad1.full_match_goalie == p_name) or (t_id == 2 and match.squad2 and match.squad2.full_match_goalie == p_name) or (t_id == 0 and match.neutral_gk and Config.utils.has_value(match.neutral_gk.starters, p_name)) then
                    m_row.is_goalie = true else m_row.field = true end
                
                local clearances = 0
                if match.clearances then for _, cl in ipairs(match.clearances) do if cl.player == p_name then clearances = clearances + 1 end end end
                if type(match.goals) == "table" then
                    for _, g in ipairs(match.goals) do
                        if g.scorer == p_name then
                            m_row.goals = m_row.goals + 1
                            if g.goal_type == "голова" or g.goal_type2 == "голова" then table.insert(m_row.events, "Гол головой") end
                            if g.goal_type == "пенальти" or g.goal_type2 == "пенальти" then table.insert(m_row.events, "Гол с пенальти") end
                        end
                        if g.assist == p_name then m_row.assists = m_row.assists + 1 end
                        if g.own_scorer == p_name then table.insert(m_row.events, "Автогол") end
                    end
                end
                if clearances > 0 then table.insert(m_row.events, clearances == 1 and "Вынос из пустых" or (clearances .. " выноса из пустых")) end
                if type(match.subs) == "table" then
                    for _, s in ipairs(match.subs) do
                        local sub_s = s.score or ""
                        if m_row.role_team == 2 and sub_s ~= "" then local p1, p2 = sub_s:match("(%d+):(%d+)"); if p1 and p2 then sub_s = p2 .. ":" .. p1 end end
                        if s.player_in == p_name and s.player_out ~= "none" then table.insert(m_row.events, "Вышел на замену при счёте " .. sub_s) end
                        if s.player_out == p_name then table.insert(m_row.events, "Заменён при счёте " .. sub_s) end
                    end
                end
                if type(match.shootout) == "table" then
                    local taken, scored, saves = 0, 0, 0; local saved_by, misses = nil, {}
                    for _, sh in ipairs(match.shootout) do
                        if sh.taker == p_name then taken = taken + 1; if sh.result == "гол" then scored = scored + 1 elseif sh.result == "вратарь" then saved_by = sh.goalie elseif sh.result == "штанга" or sh.result == "перекладина" then table.insert(misses, "1 штанга") end end
                        if sh.goalie == p_name and sh.result == "вратарь" then saves = saves + 1 end
                    end
                    if taken > 0 or saves > 0 then
                        local lines = {}
                        if taken > 0 then
                            local txt = scored .. " из " .. taken; local m_details = {}
                            if saved_by then table.insert(m_details, "1 отбил [[" .. saved_by .. "]]") end
                            for _, m_item in ipairs(misses) do table.insert(m_details, m_item) end
                            if #m_details > 0 then txt = txt .. " (" .. table.concat(m_details, ", ") .. ")" end
                            table.insert(lines, txt)
                        end
                        if saves > 0 then table.insert(lines, saves .. " сэйв" .. (saves > 1 and "а" or "")) end
                        if #lines > 0 then table.insert(m_row.events, "Серия пенальти:<br>" .. table.concat(lines, ", ")) end
                    end
                end
                if m_row.is_goalie and m_row.role_team ~= 0 and not m_row.field then table.insert(m_row.events, "Вратарь") end
                table.insert(pc.AS_compiled.player_matches_list, m_row)
            end
        end

        if match.date then
            for p_name, _ in pairs(p_teams) do
                local p = get_p(p_name)
                if p.last_date then
                    local days, str = date_diff(p.last_date, match.date)
                    if days > p.max_gap_days then p.max_gap_days = days; p.max_gap_str = str end
                end
                p.last_date = match.date
            end
        end

        local match_g = {}; local match_a = {}
        if match.goals then
            for _, g in ipairs(match.goals) do
                if g.scorer and not g.own_scorer then
                    if g.scorer == current_scorer then
                        consecutive_goals = consecutive_goals + 1
                        if consecutive_goals == 3 then get_p(g.scorer).classic_hat_tricks = get_p(g.scorer).classic_hat_tricks + 1 end
                    else current_scorer = g.scorer; consecutive_goals = 1 end
                else current_scorer = nil; consecutive_goals = 0 end

                if g.scorer then
                    local p = get_p(g.scorer); local t = get_t(p, year)
                    global_goals[g.scorer] = (global_goals[g.scorer] or 0) + 1
                    if global_goals[g.scorer] == 100 then table.insert(GlobalCache.PA_CustomStats.club100, {name = g.scorer, year = year}) end
                    if match.stadium then
                        GlobalCache.PA_CustomStats.stadiums[match.stadium] = GlobalCache.PA_CustomStats.stadiums[match.stadium] or {}
                        GlobalCache.PA_CustomStats.stadiums[match.stadium][g.scorer] = (GlobalCache.PA_CustomStats.stadiums[match.stadium][g.scorer] or 0) + 1
                    end

                    match_g[g.scorer] = (match_g[g.scorer] or 0) + 1
                    local mg = match_g[g.scorer]
                    if mg > p.max_goals_match then p.max_goals_match = mg; p.max_goals_match_occurences = {year}
                    elseif mg == p.max_goals_match and mg > 0 then table.insert(p.max_goals_match_occurences, year) end
                    
                    if is_final then
                        if mg > p.max_final_goals then p.max_final_goals = mg; p.max_final_goals_occurences = {year}
                        elseif mg == p.max_final_goals and mg > 0 then table.insert(p.max_final_goals_occurences, year) end
                    end
                    if is_playoff then t.playoff_goals = t.playoff_goals + 1 end

                    N.global_goals[g.scorer] = (N.global_goals[g.scorer] or 0) + 1
                    if is_final and not g.own_scorer then N.final_goals[g.scorer] = (N.final_goals[g.scorer] or 0) + 1 end
                end

                if g.assist then
                    local p = get_p(g.assist)
                    match_a[g.assist] = (match_a[g.assist] or 0) + 1
                    local ma = match_a[g.assist]
                    if ma > p.max_assists_match then p.max_assists_match = ma; p.max_assists_match_occurences = {year}
                    elseif ma == p.max_assists_match and ma > 0 then table.insert(p.max_assists_match_occurences, year) end
                    if is_final then N.final_assists[g.assist] = (N.final_assists[g.assist] or 0) + 1 end
                end
            end
        end

        if is_final then
            local function add_to_nav(list, val)
                if type(val) == "table" then for _, v in ipairs(val) do table.insert(list, {year, v}) end elseif val then table.insert(list, {year, val}) end
            end
            add_to_nav(N.superchamps, match.superchampions); add_to_nav(N.zshar, match.golden_sphere); add_to_nav(N.elnur, match.elnur_award)
            
            local match_res = StatEngine.Harvester.evaluate_match(match)
            if match_res then
                local w_team = (match_res[1].pts > match_res[2].pts) and 1 or ((match_res[2].pts > match_res[1].pts) and 2 or nil)
                if w_team then
                    N.champs[year] = {}
                    local w_sq = (w_team == 1) and match.squad1 or match.squad2
                    if w_sq then
                        if type(w_sq.starters) == "table" then for _, p_n in ipairs(w_sq.starters) do N.champ_counts[p_n] = (N.champ_counts[p_n] or 0) + 1; table.insert(N.champs[year], p_n) end end
                        if type(w_sq.substitutes) == "table" then for _, p_n in ipairs(w_sq.substitutes) do N.champ_counts[p_n] = (N.champ_counts[p_n] or 0) + 1; table.insert(N.champs[year], p_n) end end
                    else
                        local seen = {}
                        if type(match.goals) == "table" then for _, g in ipairs(match.goals) do if g.team == w_team and g.scorer and not seen[g.scorer] then seen[g.scorer]=true; N.champ_counts[g.scorer]=(N.champ_counts[g.scorer] or 0)+1; table.insert(N.champs[year], g.scorer) end end end
                    end
                end
            end

            local f = GlobalCache.PA_CustomStats.finals[year]
            f.goals_count = match_g; f.assists_count = match_a

            if match.goals then
                for _, g in ipairs(match.goals) do
                    if g.scorer then get_p(g.scorer).final_goals_real = get_p(g.scorer).final_goals_real + 1 end
                    if g.assist then get_p(g.assist).final_assists_real = get_p(g.assist).final_assists_real + 1 end
                end
            end

            local function parse_aw(aw_data)
                if type(aw_data) == "table" then return aw_data end
                if type(aw_data) == "string" then return {aw_data} end return {}
            end
            
            f.superchampions = parse_aw(match.superchampions); f.golden_sphere = parse_aw(match.golden_sphere); f.silver_sphere = parse_aw(match.silver_sphere)
            f.bronze_sphere = parse_aw(match.bronze_sphere); f.wooden_sphere = parse_aw(match.wooden_sphere)
            f.golden_shoe = parse_aw(match.golden_shoe); f.silver_shoe = parse_aw(match.silver_shoe)
            f.bronze_shoe = parse_aw(match.bronze_shoe); f.wooden_shoe = parse_aw(match.wooden_shoe)
            f.golden_assistant = parse_aw(match.golden_assistant); f.silver_assistant = parse_aw(match.silver_assistant)
            f.bronze_assistant = parse_aw(match.bronze_assistant); f.wooden_assistant = parse_aw(match.wooden_assistant)
            f.elnur_award = parse_aw(match.elnur_award); f.best_goal = parse_aw(match.best_goal); f.mvp = match.mvp and match.mvp.player or nil
            
            f.has_shootout = (match.shootout_score1 ~= nil); f.shootout_winner = nil
            if match.shootout and f.has_shootout then
                local w_team = match.shootout_score1 > match.shootout_score2 and 1 or 2
                local w_score = 0; local max_score = math.max(match.shootout_score1, match.shootout_score2)
                for _, shot in ipairs(match.shootout) do
                    if shot.team == w_team and shot.result == "гол" then
                        w_score = w_score + 1; if w_score == max_score then f.shootout_winner = shot.taker; break end
                    end
                end
            end

            for _, p_n in ipairs(f.superchampions or {}) do get_p(p_n).superchamps = get_p(p_n).superchamps + 1; table.insert(get_p(p_n).superchamps_years, year) end
            for _, p_n in ipairs(f.golden_sphere or {}) do get_p(p_n).golden_spheres = get_p(p_n).golden_spheres + 1; table.insert(get_p(p_n).golden_spheres_years, year); get_p(p_n).total_spheres = get_p(p_n).total_spheres + 1 end
            for _, p_n in ipairs(f.silver_sphere or {}) do get_p(p_n).total_spheres = get_p(p_n).total_spheres + 1 end
            for _, p_n in ipairs(f.bronze_sphere or {}) do get_p(p_n).total_spheres = get_p(p_n).total_spheres + 1 end
            for _, p_n in ipairs(f.wooden_sphere or {}) do get_p(p_n).total_spheres = get_p(p_n).total_spheres + 1 end
            for _, p_n in ipairs(f.golden_shoe or {}) do get_p(p_n).golden_shoes = get_p(p_n).golden_shoes + 1; table.insert(get_p(p_n).golden_shoes_years, year); get_p(p_n).total_shoes = get_p(p_n).total_shoes + 1 end
            for _, p_n in ipairs(f.silver_shoe or {}) do get_p(p_n).total_shoes = get_p(p_n).total_shoes + 1 end
            for _, p_n in ipairs(f.bronze_shoe or {}) do get_p(p_n).total_shoes = get_p(p_n).total_shoes + 1 end
            for _, p_n in ipairs(f.wooden_shoe or {}) do get_p(p_n).total_shoes = get_p(p_n).total_shoes + 1 end

            if f.mvp then get_p(f.mvp).mvp_final = get_p(f.mvp).mvp_final + 1; table.insert(get_p(f.mvp).mvp_final_years, year) end
            if f.shootout_winner then get_p(f.shootout_winner).golden_goals = get_p(f.shootout_winner).golden_goals + 1; table.insert(get_p(f.shootout_winner).golden_goals_years, year) end

            local m_res = StatEngine.Harvester.evaluate_match(match)
            local win_team = (m_res and m_res[1].pts > m_res[2].pts) and 1 or ((m_res and m_res[2].pts > m_res[1].pts) and 2 or 0)
            local cap1 = match.squad1 and match.squad1.captain; local cap2 = match.squad2 and match.squad2.captain
            
            for p_n, t_id in pairs(p_teams) do
                local p = get_p(p_n)
                p.finals_played = p.finals_played + 1
                if t_id == win_team then
                    p.champs = p.champs + 1; table.insert(p.champs_years, year)
                    if (t_id == 1 and cap1 == p_n) or (t_id == 2 and cap2 == p_n) then p.champ_captain = p.champ_captain + 1; table.insert(p.champ_captain_years, year) end
                    local prev_idx = 0
                    for i, y in ipairs(Config.years) do if y == year then prev_idx = i - 1 break end end
                    local prev_year = Config.years[prev_idx]
                    local cur_t_code = (t_id == 1) and match.team1 or match.team2
                    if p.last_champ_year == prev_year then
                        p.cur_champ_streak = p.cur_champ_streak + 1
                        if p.last_champ_team == cur_t_code then p.cur_champ_team_streak = p.cur_champ_team_streak + 1 else p.cur_champ_team_streak = 1 end
                    else p.cur_champ_streak = 1; p.cur_champ_team_streak = 1 end
                    p.last_champ_year = year; p.last_champ_team = cur_t_code
                    if p.cur_champ_streak > p.champ_streak then p.champ_streak = p.cur_champ_streak; p.champ_streak_end_year = year end
                    if p.cur_champ_team_streak > p.champ_team_streak then p.champ_team_streak = p.cur_champ_team_streak; p.champ_team_streak_code = cur_t_code; p.champ_team_streak_end_year = year end
                end
            end
        end
    end

    StreaksEngine:finish()
    local PA_GlobalStreaks = StreaksEngine:get_results()
    for name, ep in pairs(PA_GlobalStreaks.players) do
        local p = get_p(name)
        p.max_streaks.team_scored = ep.team_scored or 0; p.max_streaks.matches_played = ep.seq or 0
        p.max_streaks.goals = ep.goals or 0; p.max_streaks.double = ep.double or 0
        p.max_streaks.hat_trick = ep.hat_trick or 0; p.max_streaks.poker = ep.poker or 0
        p.max_streaks.both_ga = ep.both_ga or 0; p.max_streaks.ga = ep.ga or 0
        p.max_streaks.missed_matches = ep.missed_matches or 0; p.max_streaks.unbeaten = ep.unbeaten or 0
        p.max_streaks.win = ep.win or 0
    end
    for name, et in pairs(PA_GlobalStreaks.team_streaks) do
        local p = get_p(name); p.max_streaks.team_matches = et.len or 0; p.max_streaks_team_matches_code = et.team or ""
    end
    for name, egk in pairs(PA_GlobalStreaks.goalie_clean) do
        local p = get_p(name); p.max_streaks.goalie_clean = egk.len or 0; p.max_streaks_goalie_clean_code = egk.team or ""
    end

    local md_cache = {} 
    for year, year_db in pairs(databases) do
        
        if year >= 2022 then
            local mds = {}
            for _, match in pairs(year_db) do if match.matchday then mds[match.matchday] = true end end
            for md, _ in pairs(mds) do
                if not md_cache[year] then md_cache[year] = {} end
                if not md_cache[year][md] then md_cache[year][md] = TournamentAwards.evaluateMatchdayPrizes(year_db, md) end
                
                local prizes = md_cache[year][md]
                
                for p_n, summary in pairs(prizes.summary) do
                    local gp = GrandStats.Players[p_n]
                    if gp then
                        gp.matchday_prizes = (gp.matchday_prizes or 0) + summary.prizes
                        gp.matchday_places = (gp.matchday_places or 0) + summary.places
                    end
                    local gpy = GrandStats.Years[year] and GrandStats.Years[year].Players[p_n]
                    if gpy then
                        gpy.matchday_prizes = (gpy.matchday_prizes or 0) + summary.prizes
                        gpy.matchday_places = (gpy.matchday_places or 0) + summary.places
                    end
                end

                for _, cat in ipairs({"mvp", "scorer", "assistant", "productive", "effective", "useful", "goalie_good", "goalie_bad"}) do
                    if prizes[cat] then
                        for _, p in ipairs(prizes[cat]) do
                            local pl = get_p(p.name); local t = get_t(pl, year)
                            if p.prize then pl.md_prizes = pl.md_prizes + 1; t.md_prizes = t.md_prizes + 1 end
                            if p.prize_place then pl.md_places = pl.md_places + 1; t.md_places = t.md_places + 1 end
                        end
                    end
                end
            end
        end

        for p_name, p_data in pairs(GrandStats.Players) do
            local pc = get_pc(p_name)
            local p_year = GrandStats.Years[year] and GrandStats.Years[year].Players[p_name]
            
            local has_played = false
            if p_year then
                if p_year.matches_total > 0 or year < Config.eras.matches then has_played = true end
            end

            if has_played then
                pc.AS_compiled.played_years[year] = true
                if year > pc.AS_compiled.last_played_year then pc.AS_compiled.last_played_year = year end
                if year < 2022 then pc.AS_compiled.played_before_2022 = true end
            end

            if p_year then
                local map = {
                    matches = p_year.matches_total, field_matches = p_year.matches_field, goals = p_year.goals.total, assists = p_year.assists.total,
                    mvp = p_year.mvp.is_mvp, plus_minus = p_year.plus_minus or 0,
                    mega_tricks = p_year.goals.hat_trick + p_year.goals.poker + p_year.goals.penta + p_year.goals.hexa,
                    assist_mega_tricks = p_year.assists.hat_trick + p_year.assists.poker + p_year.assists.penta,
                    head_goals = p_year.goals.head, heel_goals = p_year.goals.heel, free_kick_goals = p_year.goals.free_kick, goalie_goals = p_year.goals.goalie,
                    clearances = p_year.clearances, pens_scored = p_year.penalties.in_game.g + p_year.penalties.shootout.g,
                    pens_missed = (p_year.penalties.in_game.u - p_year.penalties.in_game.g) + (p_year.penalties.shootout.u - p_year.penalties.shootout.g),
                    pens_saved = p_year.penalties.saved_as_goalie, caused_pens = p_year.penalties.caused_pens,
                    mvp_goalie = p_year.mvp.is_goalie_mvp, clean_sheets = p_year.clean_sheets,
                    yellow_cards = p_year.cards.yellow, red_cards = p_year.cards.red, own_goals = p_year.own_goals,
                    matchday_prizes = p_year.matchday_prizes or 0,
                    matchday_places = p_year.matchday_places or 0
                }
                for k, v in pairs(map) do 
                    if pc.AS_compiled.metrics[k] then 
                        if not pc.AS_compiled.metrics[k].years[year] then pc.AS_compiled.metrics[k].years[year] = { val=0, num=0, den=0, color="" } end
                        pc.AS_compiled.metrics[k].years[year].val = pc.AS_compiled.metrics[k].years[year].val + v 
                    end 
                end
                if pc.AS_compiled.metrics.avg_goals then 
                    if not pc.AS_compiled.metrics.avg_goals.years[year] then pc.AS_compiled.metrics.avg_goals.years[year] = { val=0, num=0, den=0, color="" } end
                    pc.AS_compiled.metrics.avg_goals.years[year].num = pc.AS_compiled.metrics.avg_goals.years[year].num + p_year.avg.goals_num; pc.AS_compiled.metrics.avg_goals.years[year].den = pc.AS_compiled.metrics.avg_goals.years[year].den + p_year.avg.goals_den 
                end
                if pc.AS_compiled.metrics.avg_assists then 
                    if not pc.AS_compiled.metrics.avg_assists.years[year] then pc.AS_compiled.metrics.avg_assists.years[year] = { val=0, num=0, den=0, color="" } end
                    pc.AS_compiled.metrics.avg_assists.years[year].num = pc.AS_compiled.metrics.avg_assists.years[year].num + p_year.avg.assists_num; pc.AS_compiled.metrics.avg_assists.years[year].den = pc.AS_compiled.metrics.avg_assists.years[year].den + p_year.avg.assists_den 
                end
            end
        end

        local shoes = TournamentAwards.getTournamentAwards(year, year_db, "goals")
        GlobalCache.PA_ShoeTeams[year] = {}; for _, p in ipairs(shoes) do if p.rank <= 4 and not GlobalCache.PA_ShoeTeams[year][p.player] then GlobalCache.PA_ShoeTeams[year][p.player] = p.team_code end end
        for _, w in ipairs(shoes) do if w.rank == 1 then table.insert(N.zbashmak, {year, w.player, w.total, w.team_code}) end end

        local assists = TournamentAwards.getTournamentAwards(year, year_db, "assists")
        GlobalCache.PA_AssistTeams[year] = {}; for _, p in ipairs(assists) do if p.rank <= 4 and not GlobalCache.PA_AssistTeams[year][p.player] then GlobalCache.PA_AssistTeams[year][p.player] = p.team_code end end

        local can_give_medals = true
        if year == Config.years[#Config.years] and Config.is_latest_finished == false then can_give_medals = false end

        if can_give_medals then
            for _, row in ipairs(AS.row_defs) do
                if year >= row.era then
                    local m_conf = Config.metrics[row.id]
                    if m_conf and not m_conf.no_medals then
                        local players_for_medal = GrandStats.Years[year].Players
                        if row.id == "avg_goals" or row.id == "avg_assists" then
                            players_for_medal = {}
                            for p_n, d in pairs(GrandStats.Years[year].Players) do if d.matches_field >= 20 then players_for_medal[p_n] = d end end
                        elseif row.id == "gaa" then
                            players_for_medal = {}
                            for p_n, d in pairs(GrandStats.Years[year].Players) do if d.matches_goalie >= 5 then players_for_medal[p_n] = d end end
                        end
                        
                        local award_list_best = TournamentAwards.getGenericMetric(row.id, players_for_medal, { worst = false })
                        local award_list_worst = {}
                        if row.id == "plus_minus" then award_list_worst = TournamentAwards.getGenericMetric(row.id, players_for_medal, { worst = true }) end

                        local function apply_medals(list)
                            for _, p_aw in ipairs(list) do
                                if type(p_aw.rank) == "number" then
                                    local pc = get_pc(p_aw.player)
                                    if pc.AS_compiled.played_years[year] then
                                        if not pc.AS_compiled.metrics[row.id].years[year] then pc.AS_compiled.metrics[row.id].years[year] = { val=0, num=0, den=0, color="" } end
                                        local current_color = pc.AS_compiled.metrics[row.id].years[year].color
                                        if not current_color or current_color == "" or p_aw.rank == 1 then
                                            pc.AS_compiled.metrics[row.id].years[year].color = p_aw.color or ""
                                        end
                                    end
                                end
                            end
                        end
                        apply_medals(award_list_best)
                        apply_medals(award_list_worst)
                    end
                end
            end
        end
    end

    local mr_hist = ModuleMegarating.get_public_history()

    for p_name, gs_p in pairs(GrandStats.Players) do
        local p = get_p(p_name)
        p.goals = gs_p.goals.total; p.assists = gs_p.assists.total; p.matches = gs_p.matches_total
        p.field_matches = gs_p.matches_field; p.goalie_matches = gs_p.matches_goalie
        p.pm = gs_p.plus_minus; p.clearances = gs_p.clearances; p.head_goals = gs_p.goals.head
        p.heel_goals = gs_p.goals.heel; p.goalie_goals = gs_p.goals.goalie
        p.pens_scored = gs_p.penalties.in_game.g + gs_p.penalties.shootout.g
        p.pens_in_game_scored = gs_p.penalties.in_game.g; p.pens_saved = gs_p.penalties.saved_as_goalie
        p.mvp = gs_p.mvp.is_mvp; p.mvp_goalie = gs_p.mvp.is_goalie_mvp
        p.mega_tricks = gs_p.goals.hat_trick + gs_p.goals.poker + gs_p.goals.penta + gs_p.goals.hexa
        p.pokers = gs_p.goals.poker + gs_p.goals.penta + gs_p.goals.hexa
        p.pentas = gs_p.goals.penta + gs_p.goals.hexa; p.hexas = gs_p.goals.hexa
        p.final_ga_real = p.final_goals_real + p.final_assists_real
        p.golden_goals = p.golden_goals + gs_p.megarating.final_gold_goals
        p.avg_goals = (gs_p.avg.goals_den >= 20) and (gs_p.avg.goals_num / gs_p.avg.goals_den) or 0
        p.avg_assists = (gs_p.avg.assists_den >= 20) and (gs_p.avg.assists_num / gs_p.avg.assists_den) or 0
        p.pts_pct = (p.matches >= 20 and gs_p.megarating.mr_matches > 0) and (gs_p.megarating.mr_points / (gs_p.megarating.mr_matches * 3) * 100) or 0
        p.gaa = (p.goalie_matches >= 20) and (gs_p.weighted_ga / p.goalie_matches) or 9999
    end

    for year, year_stats in pairs(GrandStats.Years) do
        for p_name, gs_t in pairs(year_stats.Players) do
            local p = get_p(p_name); local t = get_t(p, year)
            t.goals = gs_t.goals.total; t.assists = gs_t.assists.total; t.ga = t.goals + t.assists
            t.matches = gs_t.matches_total; t.field_matches = gs_t.matches_field; t.goalie_matches = gs_t.matches_goalie
            t.pm = gs_t.plus_minus; t.clearances = gs_t.clearances
            t.head_goals = gs_t.goals.head; t.heel_goals = gs_t.goals.heel
            t.mvp = gs_t.mvp.is_mvp; t.mvp_goalie = gs_t.mvp.is_goalie_mvp
            t.mega_tricks = gs_t.goals.hat_trick + gs_t.goals.poker + gs_t.goals.penta + gs_t.goals.hexa
            t.winning_goals = gs_t.advanced.winning_goals
            t.playoff_wins = gs_t.megarating.playoff_wins.r16 + gs_t.megarating.playoff_wins.qf + gs_t.megarating.playoff_wins.sf + (gs_t.megarating.is_champion and 1 or 0)
            if gs_t.megarating and gs_t.megarating.final_gold_goals and gs_t.megarating.final_gold_goals > 0 then
                for _ = 1, gs_t.megarating.final_gold_goals do table.insert(p.golden_goals_years, year) end
                table.insert(p.final_gold_goals_years, year)
            end
            t.avg_goals = (gs_t.avg.goals_den >= 20) and (gs_t.avg.goals_num / gs_t.avg.goals_den) or 0
            t.avg_assists = (gs_t.avg.assists_den >= 20) and (gs_t.avg.assists_num / gs_t.avg.assists_den) or 0
            t.pts_pct = (t.matches >= 20 and gs_t.megarating.mr_matches > 0) and (gs_t.megarating.mr_points / (gs_t.megarating.mr_matches * 3) * 100) or 0
            t.gaa = (t.goalie_matches >= 5) and (gs_t.weighted_ga / t.goalie_matches) or 0
        end
    end

    for combo, c_data in pairs(GrandStats.PlayerTeam) do
        local pl = c_data.name; local t = c_data.team; local goals = c_data.goals.total
        if t ~= "Нейтрал" and goals > 0 then
            GlobalCache.PA_TeamLeaders[t] = GlobalCache.PA_TeamLeaders[t] or {players = {}, goals = 0}
            if goals > GlobalCache.PA_TeamLeaders[t].goals then GlobalCache.PA_TeamLeaders[t].goals = goals; GlobalCache.PA_TeamLeaders[t].players = {pl}
            elseif goals == GlobalCache.PA_TeamLeaders[t].goals then table.insert(GlobalCache.PA_TeamLeaders[t].players, pl) end
        end
    end

    for p_name, hp in pairs(mr_hist.players) do
        local pc = get_pc(p_name)
        pc.mr_history = hp
        local p = get_p(p_name)
        p.mr_points_pct = hp.total_pct
        for year, ydata in pairs(hp.years) do local t = get_t(p, year); t.mr_points_pct = ydata.pct; t.mr_points = ydata.pts end
    end

    local function evaluate_leader(year, metric_func)
        local max_val = -9999; local leaders = {}
        for pl, p_stats in pairs(GrandStats.Years[year].Players) do
            local val = metric_func(p_stats)
            if val and val > 0 then
                if val > max_val then max_val = val; leaders = {pl}
                elseif val == max_val then table.insert(leaders, pl) end
            end
        end
        return leaders
    end
    local function evaluate_min_leader(year, metric_func, min_req)
        local min_val = 9999; local leaders = {}
        for pl, p_stats in pairs(GrandStats.Years[year].Players) do
            if min_req(p_stats) then
                local val = metric_func(p_stats)
                if val then
                    if val < min_val then min_val = val; leaders = {pl}
                    elseif val == min_val then table.insert(leaders, pl) end
                end
            end
        end
        return leaders
    end
    for _, year in ipairs(Config.years) do
        if GrandStats.Years[year] then
            GlobalCache.PA_Leaders.goals = GlobalCache.PA_Leaders.goals or {}; GlobalCache.PA_Leaders.goals[year] = evaluate_leader(year, function(s) return s.goals.total end)
            GlobalCache.PA_Leaders.assists = GlobalCache.PA_Leaders.assists or {}; GlobalCache.PA_Leaders.assists[year] = evaluate_leader(year, function(s) return s.assists.total end)
            GlobalCache.PA_Leaders.matches = GlobalCache.PA_Leaders.matches or {}; GlobalCache.PA_Leaders.matches[year] = evaluate_leader(year, function(s) return s.matches_total end)
            GlobalCache.PA_Leaders.mvp = GlobalCache.PA_Leaders.mvp or {}; GlobalCache.PA_Leaders.mvp[year] = evaluate_leader(year, function(s) return s.mvp.is_mvp end)
            GlobalCache.PA_Leaders.mvp_goalie = GlobalCache.PA_Leaders.mvp_goalie or {}; GlobalCache.PA_Leaders.mvp_goalie[year] = evaluate_leader(year, function(s) return s.mvp.is_goalie_mvp end)
            GlobalCache.PA_Leaders.mega_tricks = GlobalCache.PA_Leaders.mega_tricks or {}; GlobalCache.PA_Leaders.mega_tricks[year] = evaluate_leader(year, function(s) return (s.goals.hat_trick + s.goals.poker + s.goals.penta + s.goals.hexa) end)
            GlobalCache.PA_Leaders.head_goals = GlobalCache.PA_Leaders.head_goals or {}; GlobalCache.PA_Leaders.head_goals[year] = evaluate_leader(year, function(s) return s.goals.head end)
            GlobalCache.PA_Leaders.pens_saved = GlobalCache.PA_Leaders.pens_saved or {}; GlobalCache.PA_Leaders.pens_saved[year] = evaluate_leader(year, function(s) return s.penalties.saved_as_goalie end)
            GlobalCache.PA_Leaders.clearances = GlobalCache.PA_Leaders.clearances or {}; GlobalCache.PA_Leaders.clearances[year] = evaluate_leader(year, function(s) return s.clearances end)
            GlobalCache.PA_Leaders.playoff_wins = GlobalCache.PA_Leaders.playoff_wins or {}; GlobalCache.PA_Leaders.playoff_wins[year] = evaluate_leader(year, function(s) return s.megarating.playoff_wins.r16 + s.megarating.playoff_wins.qf + s.megarating.playoff_wins.sf + (s.megarating.is_champion and 1 or 0) end)
            GlobalCache.PA_Leaders.plus_minus = GlobalCache.PA_Leaders.plus_minus or {}; GlobalCache.PA_Leaders.plus_minus[year] = evaluate_leader(year, function(s) return s.plus_minus end)
            GlobalCache.PA_Leaders.pens_scored = GlobalCache.PA_Leaders.pens_scored or {}; GlobalCache.PA_Leaders.pens_scored[year] = evaluate_leader(year, function(s) return s.penalties.in_game.g + s.penalties.shootout.g end)
            GlobalCache.PA_Leaders.avg_goals = GlobalCache.PA_Leaders.avg_goals or {}; GlobalCache.PA_Leaders.avg_goals[year] = evaluate_leader(year, function(s) return s.avg.goals_den >= 20 and s.avg.goals_num / s.avg.goals_den or 0 end)
            GlobalCache.PA_Leaders.gaa = GlobalCache.PA_Leaders.gaa or {}; GlobalCache.PA_Leaders.gaa[year] = evaluate_min_leader(year, function(s) return s.weighted_ga / s.matches_goalie end, function(s) return s.matches_goalie and s.matches_goalie >= 5 end)
        end
    end

    for st, p_list in pairs(GlobalCache.PA_CustomStats.stadiums) do
        local max = 0; local best = {}
        for pl, goals in pairs(p_list) do
            if goals > max then max = goals; best = {pl}
            elseif goals == max then table.insert(best, pl) end
        end
        if max > 0 then GlobalCache.PA_StadiumLeaders[st] = {players = best, goals = max} end
    end

    for _, p in pairs(PA_Players) do
        for year, list in pairs(GlobalCache.PA_Leaders.goals or {}) do if Config.utils.has_value(list, p.name) then p.wins.goals = p.wins.goals + 1; table.insert(p.wins.goals_years, year) end end
        for year, list in pairs(GlobalCache.PA_Leaders.mega_tricks or {}) do if Config.utils.has_value(list, p.name) then p.wins.mega_tricks = p.wins.mega_tricks + 1; table.insert(p.wins.mega_tricks_years, year) end end
        for year, list in pairs(GlobalCache.PA_Leaders.plus_minus or {}) do if Config.utils.has_value(list, p.name) then p.wins.pm = p.wins.pm + 1; table.insert(p.wins.pm_years, year) end end
        for year, list in pairs(GlobalCache.PA_Leaders.mvp or {}) do if Config.utils.has_value(list, p.name) then p.wins.mvp = p.wins.mvp + 1; table.insert(p.wins.mvp_years, year) end end
        for year, list in pairs(GlobalCache.PA_Leaders.mvp_goalie or {}) do if Config.utils.has_value(list, p.name) then p.wins.mvp_goalie = p.wins.mvp_goalie + 1; table.insert(p.wins.mvp_goalie_years, year) end end
        for year, list in pairs(GlobalCache.PA_Leaders.head_goals or {}) do if Config.utils.has_value(list, p.name) then p.wins.head_goals = p.wins.head_goals + 1; table.insert(p.wins.head_goals_years, year) end end
        for year, list in pairs(GlobalCache.PA_Leaders.clearances or {}) do if Config.utils.has_value(list, p.name) then p.wins.clearances = p.wins.clearances + 1; table.insert(p.wins.clearances_years, year) end end
        for year, list in pairs(GlobalCache.PA_Leaders.gaa or {}) do if Config.utils.has_value(list, p.name) then p.wins.gaa = p.wins.gaa + 1; table.insert(p.wins.gaa_years, year) end end
    end

    local function get_val(p, key)
        if string.sub(key, 1, 12) == "max_streaks_" then return p.max_streaks[string.sub(key, 13)] or 0 end
        if string.sub(key, 1, 5) == "wins_" then return p.wins[string.sub(key, 6)] or 0 end
        return p[key]
    end

    local metrics_all = { {"champs", false}, {"champ_captain", false}, {"champ_streak", false}, {"champ_team_streak", false}, {"superchamps", false}, {"golden_goals", false}, {"final_goals_real", false}, {"final_assists_real", false}, {"final_ga_real", false}, {"max_final_goals", false}, {"finals_played", false}, {"goals", false}, {"max_goals_team", false}, {"max_goals_match", false}, {"mega_tricks", false}, {"max_mega_tricks_team", false}, {"classic_hat_tricks", false}, {"pokers", false}, {"pentas", false}, {"hexas", false}, {"pens_scored", false}, {"pens_in_game_scored", false}, {"max_assists_match", false}, {"pm", false}, {"pts_pct", false}, {"md_prizes", false}, {"md_places", false}, {"assists", false}, {"avg_assists", false}, {"heel_goals", false}, {"golden_spheres", false}, {"total_spheres", false}, {"golden_shoes", false}, {"total_shoes", false}, {"avg_goals", false}, {"gaa", true}, {"mvp", false}, {"max_team_mvp", false}, {"mvp_goalie", false}, {"mr_points_pct", false}, {"pens_saved", false}, {"matches", false}, {"field_matches", false}, {"goalie_matches", false}, {"head_goals", false}, {"clearances", false}, {"max_gap_days", false} }
    for _, row in ipairs(metrics_all) do
        local key = row[1]; local is_min = row[2]; local best = is_min and 9999 or -9999
        for _, p in pairs(PA_Players) do
            local v = get_val(p, key)
            if v and v ~= 0 and (not is_min or v < 9999) then
                if (is_min and v < best) or (not is_min and v > best) then best = v end
            end
        end
        GlobalCache.PA_GlobalRecords.AllTime[key] = best
    end

    local streak_wins_keys = {"max_streaks_team_scored", "max_streaks_matches_played", "max_streaks_goals", "max_streaks_double", "max_streaks_hat_trick", "max_streaks_poker", "max_streaks_both_ga", "max_streaks_ga", "max_streaks_missed_matches", "max_streaks_goalie_clean", "max_streaks_team_matches", "max_streaks_unbeaten", "max_streaks_win", "wins_goals", "wins_mega_tricks", "wins_pm", "wins_mvp", "wins_mvp_goalie", "wins_head_goals", "wins_clearances", "wins_gaa"}
    for _, key in ipairs(streak_wins_keys) do
        local best = -9999
        for _, p in pairs(PA_Players) do
            local v = get_val(p, key)
            if v and v ~= 0 and v > best then best = v end
        end
        GlobalCache.PA_GlobalRecords.AllTime[key] = best
    end

    local metrics_tourney = { {"goals", false}, {"max_goals_team", false}, {"mega_tricks", false}, {"pm", false}, {"matches", false}, {"pts_pct", false}, {"md_prizes", false}, {"md_places", false}, {"heel_goals", false}, {"mvp", false}, {"max_team_mvp", false}, {"playoff_wins", false}, {"head_goals", false}, {"clearances", false}, {"gaa", true}, {"playoff_goals", false}, {"winning_goals", false}, {"avg_goals", false}, {"max_team_assists", false}, {"mvp_goalie", false}, {"mr_points", false}, {"assists", false}, {"avg_assists", false}, {"ga", false}, {"max_team_ga", false} }
    for _, row in ipairs(metrics_tourney) do
        local key = row[1]; local is_min = row[2]; local best = is_min and 9999 or -9999
        for _, p in pairs(PA_Players) do
            for _, t in pairs(p.tournaments) do
                local v = t[key]
                if v and v ~= 0 and (not is_min or v < 9999) then
                    if key == "gaa" and t.goalie_matches < 5 then v = nil end
                    if (key == "avg_goals" or key == "avg_assists") and t.field_matches < 20 then v = nil end
                    if key == "pts_pct" and t.matches < 20 then v = nil end
                    if v and ((is_min and v < best) or (not is_min and v > best)) then best = v end
                end
            end
        end
        GlobalCache.PA_GlobalRecords.PerTournament[key] = best
    end

    for y, p_data in pairs(N.team_matches) do
        for p_name_nav, t_data in pairs(p_data) do
            for t, count in pairs(t_data) do if count >= 7 then table.insert(N.loyalty, {y, p_name_nav, t}) end end
        end
    end

    -- Суммарные расчеты AS
    for p_name, pc in pairs(PlayersCache) do
        for _, row in ipairs(AS.row_defs) do
            local m = pc.AS_compiled.metrics[row.id]
            for year, y_data in pairs(m.years) do
                m.total_val = m.total_val + y_data.val
                m.total_num = m.total_num + y_data.num
                m.total_den = m.total_den + y_data.den
            end
        end
    end

    -- Общие медали
    for _, row in ipairs(AS.row_defs) do
        local m_conf = Config.metrics[row.id]
        if m_conf and not m_conf.no_medals then
            local players_for_medal = GrandStats.Players
            if row.id == "avg_goals" or row.id == "avg_assists" then
                players_for_medal = {}
                for p_n, d in pairs(GrandStats.Players) do if d.matches_field >= 20 then players_for_medal[p_n] = d end end
            elseif row.id == "gaa" then
                players_for_medal = {}
                for p_n, d in pairs(GrandStats.Players) do if d.matches_goalie >= 20 then players_for_medal[p_n] = d end end
            end
            
            local award_list_best = TournamentAwards.getGenericMetric(row.id, players_for_medal, { worst = false })
            local award_list_worst = {}
            if row.id == "plus_minus" then award_list_worst = TournamentAwards.getGenericMetric(row.id, players_for_medal, { worst = true }) end
            
            local function apply_total_medals(list)
                for _, p_aw in ipairs(list) do
                    if type(p_aw.rank) == "number" then
                        local pc = get_pc(p_aw.player)
                        if pc.AS_compiled.metrics[row.id] then
                            local current_color = pc.AS_compiled.metrics[row.id].color
                            if not current_color or current_color == "" or p_aw.rank == 1 then
                                pc.AS_compiled.metrics[row.id].color = p_aw.color or ""
                            end
                        end
                    end
                end
            end
            apply_total_medals(award_list_best)
            apply_total_medals(award_list_worst)
        end
    end

    -- Чистка массива от мусора для минимизации JSON
    for p_name, pc in pairs(PlayersCache) do
        pc.PA_Player = PA_Players[p_name]
        for _, row in ipairs(AS.row_defs) do
            local m = pc.AS_compiled.metrics[row.id]
            if m then
                local keep_m = false
                for y, y_data in pairs(m.years) do
                    if y_data.val ~= 0 or y_data.num ~= 0 or y_data.den ~= 0 or (y_data.color and y_data.color ~= "") then
                        keep_m = true
                    else m.years[y] = nil end
                end
                if m.total_val == 0 and m.total_num == 0 and m.total_den == 0 and (not m.color or m.color == "") and not keep_m then
                    pc.AS_compiled.metrics[row.id] = nil
                end
            end
        end
    end
    
	-- =================================================================
    -- БЛОК СБОРКИ ДЕТАЛЬНЫХ ДАННЫХ ДЛЯ КАСТОМНЫХ СТРАНИЦ (Module:Stats/*)
    -- =================================================================
    local StatsPages = { Goals = {}, Penalties = {}, MegaTricks = { goals = {}, assists = {} } }
    
    -- 1. Сборка для Module:Stats/Goals
    local g_scorers, g_own = {}, {}
    for p_name, gp in pairs(GrandStats.Players) do
        if gp.goals.total > 0 then
            g_scorers[p_name] = { total = gp.goals.total, pen_total = gp.penalties.in_game.g, years = {} }
            for y, y_data in pairs(GrandStats.Years) do if y_data.Players[p_name] and y_data.Players[p_name].goals.total > 0 then g_scorers[p_name].years[y] = y_data.Players[p_name].goals.total end end
        end
        if gp.own_goals > 0 then
            g_own[p_name] = { total = gp.own_goals, years = {} }
            for y, y_data in pairs(GrandStats.Years) do if y_data.Players[p_name] and y_data.Players[p_name].own_goals > 0 then g_own[p_name].years[y] = y_data.Players[p_name].own_goals end end
        end
    end
    StatsPages.Goals = { scorers = g_scorers, own_scorers = g_own }

    -- 2. Сборка для Module:Stats/Penalties
    local pen_players, pen_tournaments = {}, {}
    for _, y in ipairs(Config.years) do pen_tournaments[y] = { all = {u=0,g=0,k=0,o=0,w=0,p=0,c=0}, ingame = {u=0,g=0,k=0,o=0,w=0,p=0,c=0} } end
    for p_name, gp in pairs(GrandStats.Players) do
        local ig, sh = gp.penalties.in_game, gp.penalties.shootout
        if ig.u > 0 or sh.u > 0 or gp.penalties.saved_as_goalie > 0 or gp.penalties.caused_pens > 0 then
            local p_node = { all = {u=0,g=0,k=0,o=0,w=0,p=0,c=0, years={}}, ingame = {u=0,g=0,k=0,o=0,w=0,p=0,c=0, years={}}, saves = {total=gp.penalties.saved_as_goalie, years={}}, fouls = {total=gp.penalties.caused_pens, years={}} }
            for _, y in ipairs(Config.years) do
                p_node.all.years[y] = {u=0,g=0,k=0,o=0,w=0,p=0,c=0}; p_node.ingame.years[y] = {u=0,g=0,k=0,o=0,w=0,p=0,c=0}; p_node.saves.years[y] = 0; p_node.fouls.years[y] = 0
                local yp = GrandStats.Years[y] and GrandStats.Years[y].Players[p_name]
                if yp then
                    local y_ig, y_sh = yp.penalties.in_game, yp.penalties.shootout
                    if y_ig.u > 0 then for k,v in pairs(y_ig) do p_node.ingame[k]=p_node.ingame[k]+v; p_node.ingame.years[y][k]=p_node.ingame.years[y][k]+v; pen_tournaments[y].ingame[k]=pen_tournaments[y].ingame[k]+v end end
                    if y_ig.u > 0 or y_sh.u > 0 then
                        for k,v in pairs(y_ig) do p_node.all[k]=p_node.all[k]+v; p_node.all.years[y][k]=p_node.all.years[y][k]+v; pen_tournaments[y].all[k]=pen_tournaments[y].all[k]+v end
                        for k,v in pairs(y_sh) do p_node.all[k]=p_node.all[k]+v; p_node.all.years[y][k]=p_node.all.years[y][k]+v; pen_tournaments[y].all[k]=pen_tournaments[y].all[k]+v end
                    end
                    p_node.saves.years[y] = yp.penalties.saved_as_goalie; p_node.fouls.years[y] = yp.penalties.caused_pens
                end
            end
            pen_players[p_name] = p_node
        end
    end
    StatsPages.Penalties = { players = pen_players, tournaments = pen_tournaments }

    -- 3. Сборка для Module:Stats/MegaTricks
    local mt_data = {
        goals = { players = {}, po_data = { all={t3={},t4={},t5={},t6={}}, qf={t3={},t4={},t5={},t6={}}, sf={t3={},t4={},t5={},t6={}}, final={t3={},t4={},t5={},t6={}} } },
        assists = { players = {}, po_data = { all={t3={},t4={},t5={},t6={}}, qf={t3={},t4={},t5={},t6={}}, sf={t3={},t4={},t5={},t6={}}, final={t3={},t4={},t5={},t6={}} } }
    }
    for _, item in ipairs(flat_matches) do
        local year, match, is_po = item.year, item.m, Config.playoff_stages[item.m.stage]
        local function process_mt(mode, ext)
            for p_name, p_stats in pairs(ext) do
                local s = (mode == "goals") and p_stats.goals or p_stats.assists
                if s and s.total and s.total >= 3 then
                    local t3, t4, t5, t6, t = 0, 0, 0, 0, s.total
                    if mode == "goals" and year >= Config.eras.mega_tricks then if t == 3 then t3=1 elseif t == 4 then t4=1 elseif t == 5 then t5=1 elseif t >= 6 then t6=1 end
                    elseif mode == "assists" and year >= Config.eras.assist_mega_tricks then if t == 3 then t3=1 elseif t == 4 then t4=1 elseif t >= 5 then t5=1 end end
                    
                    if t3+t4+t5+t6 > 0 then
                        local md = mt_data[mode]
                        if not md.players[p_name] then md.players[p_name] = { total=0, t3=0, t4=0, t5=0, t6=0, grp=0, po=0, po_t4=0, po_t5=0, po_t6=0, year_details={} } end
                        local p = md.players[p_name]
                        p.total=p.total+1; p.t3=p.t3+t3; p.t4=p.t4+t4; p.t5=p.t5+t5; p.t6=p.t6+t6
                        if not p.year_details[year] then p.year_details[year] = {total=0, t3=0, t4=0, t5=0, t6=0, grp=0, po=0, po_t4=0, po_t5=0, po_t6=0} end
                        local yd = p.year_details[year]
                        yd.total=yd.total+1; yd.t3=yd.t3+t3; yd.t4=yd.t4+t4; yd.t5=yd.t5+t5; yd.t6=yd.t6+t6
                        
                        if is_po then
                            p.po=p.po+1; p.po_t4=p.po_t4+t4; p.po_t5=p.po_t5+t5; p.po_t6=p.po_t6+t6
                            yd.po=yd.po+1; yd.po_t4=yd.po_t4+t4; yd.po_t5=yd.po_t5+t5; yd.po_t6=yd.po_t6+t6
                            local function add_po(scope)
                                local sd = md.po_data[scope]
                                if t3>0 then if scope=="final" then table.insert(sd.t3, {name=p_name, year=year}) else sd.t3[p_name]=(sd.t3[p_name] or 0)+1 end end
                                if t4>0 then if scope=="final" then table.insert(sd.t4, {name=p_name, year=year}) else sd.t4[p_name]=(sd.t4[p_name] or 0)+1 end end
                                if t5>0 then if scope=="final" then table.insert(sd.t5, {name=p_name, year=year}) else sd.t5[p_name]=(sd.t5[p_name] or 0)+1 end end
                                if t6>0 then if scope=="final" then table.insert(sd.t6, {name=p_name, year=year}) else sd.t6[p_name]=(sd.t6[p_name] or 0)+1 end end
                            end
                            add_po("all")
                            if match.stage == "1/4 финала" or match.stage == "Полуфинал" or match.stage == "Финал" then add_po("qf") end
                            if match.stage == "Полуфинал" or match.stage == "Финал" then add_po("sf") end
                            if match.stage == "Финал" then add_po("final") end
                        else
                            p.grp=p.grp+1; yd.grp=yd.grp+1
                        end
                    end
                end
            end
        end
        process_mt("goals", StatEngine.Harvester.extract_goals(item.id, match)); process_mt("assists", StatEngine.Harvester.extract_goals(item.id, match))
    end
    StatsPages.MegaTricks = mt_data

    local ExportData = { Global = GlobalCache, Players = PlayersCache, StatsPages = StatsPages }
    return mw.text.jsonEncode(ExportData)
end

return Cron