Объясни пошагово всё, что происходит в этом Google Apps script:

function updateAllStats() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const headerRow = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  
  // Получаем индексы столбцов по названиям
  const columnIndices = {
    name: headerRow.indexOf('name') + 1,
    gameVer: headerRow.indexOf('gameVer') + 1,
    modrinthId: headerRow.indexOf('modrinthId') + 1,
    curseforgeId: headerRow.indexOf('curseforgeId') + 1,
    follows: headerRow.indexOf('follows') + 1,
    mrDownloads: headerRow.indexOf('mrDownloads') + 1,
    cfDownloads: headerRow.indexOf('cfDownloads') + 1,
    popularity: headerRow.indexOf('popularity') + 1,
    actualModVer: headerRow.indexOf('actualModVer') + 1
  };

  const lastRow = sheet.getLastRow();
  
  for (let row = 2; row <= lastRow; row++) {
    const modrinthId = sheet.getRange(row, columnIndices.modrinthId).getValue();
    const curseforgeId = sheet.getRange(row, columnIndices.curseforgeId).getValue();
    const gameVer = sheet.getRange(row, columnIndices.gameVer).getValue(); // Получаем gameVer

    Logger.log(`Processing row ${row}: Modrinth ID = ${modrinthId}, CurseForge ID = ${curseforgeId}, Game Version = ${gameVer}`);

    if ((modrinthId === false || modrinthId === "FALSE") && (curseforgeId === false || curseforgeId === "FALSE")) {
      Logger.log(`Skipping row ${row} due to both IDs being FALSE`);
      continue;
    }

    let follows = sheet.getRange(row, columnIndices.follows).getValue() || 0;
    let mrDownloads = sheet.getRange(row, columnIndices.mrDownloads).getValue() || 0;
    let cfDownloads = sheet.getRange(row, columnIndices.cfDownloads).getValue() || 0;
    let actualModVer = ''; // Инициализируем переменную для actualModVer

    let modrinthSuccess = false; // Флаг успешности получения данных с Modrinth

    // Попытка получить данные с Modrinth
    if (modrinthId && modrinthId !== false && modrinthId !== "FALSE") {
      const modrinthData = fetchModrinthStats(modrinthId, gameVer);
      if (modrinthData && modrinthData.latestVersion) {
        modrinthSuccess = true;
        if (modrinthData.followersCount > 0) {
          follows = modrinthData.followersCount;
        }
        if (modrinthData.downloadsCount > 0) {
          mrDownloads = modrinthData.downloadsCount;
        }
        if (modrinthData.latestVersion) {
          actualModVer = modrinthData.latestVersion;
        }
        Logger.log(`Modrinth data for ID ${modrinthId}: Followers = ${modrinthData.followersCount}, Downloads = ${modrinthData.downloadsCount}, Latest Version = ${modrinthData.latestVersion}`);
      } else {
        Logger.log(`No suitable version found on Modrinth for mod ID ${modrinthId} and game version ${gameVer}`);
      }
    }
    
    // Если данные с Modrinth не получены, пытаемся получить данные с CurseForge
    if (!modrinthSuccess && curseforgeId && curseforgeId !== false && curseforgeId !== "FALSE") {
      const cfData = fetchCurseForgeMod(curseforgeId, gameVer);
      if (cfData) {
        if (cfData.downloadCount > 0) {
          cfDownloads = cfData.downloadCount;
        }
        if (cfData.latestVersion) {
          actualModVer = cfData.latestVersion;
        }
        Logger.log(`CurseForge data for ID ${curseforgeId}: Downloads = ${cfData.downloadCount}, Latest Version = ${cfData.latestVersion}`);
      } else {
        Logger.log(`No suitable version found on CurseForge for mod ID ${curseforgeId} and game version ${gameVer}`);
      }
    }

    // Обновляем значения в таблице
    sheet.getRange(row, columnIndices.mrDownloads).setValue(mrDownloads);
    sheet.getRange(row, columnIndices.follows).setValue(follows); 
    sheet.getRange(row, columnIndices.cfDownloads).setValue(cfDownloads);
    sheet.getRange(row, columnIndices.actualModVer).setValue(actualModVer); // Устанавливаем actualModVer

    const values = [follows, mrDownloads, cfDownloads];
    const nonZeroValues = values.filter(value => value > 0);
    let popularity = 0;

    if (nonZeroValues.length > 0) {
      popularity = nonZeroValues.reduce((acc, val) => acc + val, 0) / nonZeroValues.length;
    }
    
    Logger.log(`Calculated popularity for row ${row}: ${popularity}`);
    sheet.getRange(row, columnIndices.popularity).setValue(popularity);
  }

  sortByColumns(sheet, columnIndices.popularity, columnIndices.name);
}

function compareGameVersions(apiVersion, sheetVersion) {
  // Проверяем на пустые значения
  if (!apiVersion || !sheetVersion) {
    return false;
  }

  // Функция для извлечения числовой части версии
  const extractVersionNumber = (versionStr) => {
    const match = versionStr.match(/^(\d+\.\d+)(\.\d+)?/); // Ищем паттерн вида "число.число" или "число.число.число"
    return match ? match[1] : null;
  };
  
  const apiVersionNumber = extractVersionNumber(apiVersion);
  const sheetVersionNumber = extractVersionNumber(sheetVersion);

  if (!apiVersionNumber || !sheetVersionNumber) {
    return false;
  }

  return apiVersionNumber === sheetVersionNumber;
}


function fetchModrinthStats(projectId, gameVerPrefix, retryCount = 0) {
  const url = `https://api.modrinth.com/v2/project/${projectId}`;
  const options = {
    'muteHttpExceptions': true // Добавляем опцию для подавления исключений
  };
  try {
    const response = UrlFetchApp.fetch(url, options);
    const statusCode = response.getResponseCode();
    if (statusCode !== 200) {
      Logger.log(`Received response code ${statusCode} from Modrinth: ${response.getContentText()}`);
      if (statusCode >= 500 && retryCount < 3) {
        // Подождём и попробуем снова
        Utilities.sleep(1000 * (retryCount + 1)); // Экспоненциальная задержка
        return fetchModrinthStats(projectId, gameVerPrefix, retryCount + 1);
      } else {
        return null; // Возвращаем null при неудаче
      }
    }
    const data = JSON.parse(response.getContentText());
    Logger.log(`Fetched data from Modrinth for ID ${projectId}`);
    
    // Получаем список версий проекта
    const versionsUrl = `https://api.modrinth.com/v2/project/${projectId}/version`;
    const versionsOptions = {
      'muteHttpExceptions': true // Добавляем опцию для подавления исключений
    };
    const versionsResponse = UrlFetchApp.fetch(versionsUrl, versionsOptions);
    const versionsStatusCode = versionsResponse.getResponseCode();
    if (versionsStatusCode !== 200) {
      Logger.log(`Received response code ${versionsStatusCode} from Modrinth versions API: ${versionsResponse.getContentText()}`);
      return null; // Возвращаем null, так как не удалось получить версии
    }
    const versionsData = JSON.parse(versionsResponse.getContentText());

    // Фильтруем версии по gameVerPrefix
    const filteredVersions = versionsData.filter(version => {
      const validGameVersions = version.game_versions.filter(gv => /^\d+\.\d+(\.\d+)?$/.test(gv));
      return validGameVersions.some(gv => compareGameVersions(gv, gameVerPrefix));
    });


    // Сортируем от новых к старым по дате публикации
    filteredVersions.sort((a, b) => new Date(b.date_published) - new Date(a.date_published));

    // Если есть версии после фильтрации, берём первую (самую новую)
    let latestVersion = '';
    if (filteredVersions.length > 0) {
      latestVersion = filteredVersions[0].version_number || '';
    } else {
      return null; // Если нет подходящих версий, возвращаем null
    }

    return {
      downloadsCount: data.downloads || 0,
      followersCount: data.followers || 0,
      latestVersion: latestVersion
    };
  } catch (error) {
    Logger.log(`Error fetching data for Modrinth project ID ${projectId}: ${error.message}\nStack Trace: ${error.stack}`);
    return null; // Возвращаем null при ошибке
  }
}

function fetchCurseForgeMod(projectId, gameVerPrefix) {
  const apiKey = '$2a$10$Urj8j4d0C3ctaItu3WOUNex3LZJ2WBRj0q5mYojEpuGmjYZCUNas. '; // API Key
  const url = `https://api.curseforge.com/v1/mods/${projectId}`;
  const headers = {
    'Accept': 'application/json',
    'x-api-key': apiKey
  };

  const options = {
    'headers': headers,
    'muteHttpExceptions': true // Добавляем опцию для подавления исключений
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const statusCode = response.getResponseCode();
    if (statusCode !== 200) {
      Logger.log(`Received response code ${statusCode} from CurseForge: ${response.getContentText()}`);
      return null; // Возвращаем null при неудаче
    }
    const data = JSON.parse(response.getContentText());
    Logger.log(`Fetched data from CurseForge for ID ${projectId}`);

    // Получаем список файлов модов
    const filesUrl = `https://api.curseforge.com/v1/mods/${projectId}/files`;
    const filesResponse = UrlFetchApp.fetch(filesUrl, options);
    const filesStatusCode = filesResponse.getResponseCode();
    if (filesStatusCode !== 200) {
      Logger.log(`Received response code ${filesStatusCode} from CurseForge files API: ${filesResponse.getContentText()}`);
      return null; // Возвращаем null, так как не удалось получить файлы
    }
    const filesData = JSON.parse(filesResponse.getContentText());

    // Фильтруем файлы по gameVerPrefix
    const filteredFiles = filesData.data.filter(file => {
      const validGameVersions = file.gameVersions.filter(gv => /^\d+\.\d+(\.\d+)?$/.test(gv));
      return validGameVersions.some(gv => compareGameVersions(gv, gameVerPrefix));
    });

    // Сортируем от новых к старым по дате файла
    filteredFiles.sort((a, b) => new Date(b.fileDate) - new Date(a.fileDate));

    let latestVersion = '';
    if (filteredFiles.length > 0) {
      latestVersion = filteredFiles[0].displayName || filteredFiles[0].fileName;
    } else {
      return null; // Если нет подходящих файлов, возвращаем null
    }

    return {
      downloadCount: data.data.downloadCount || 0,
      latestVersion: latestVersion
    };
  } catch (error) {
    Logger.log(`Error fetching data for CurseForge project ID ${projectId}: ${error}`);
    return null; // Возвращаем null при ошибке
  }
}

function sortByColumns(sheet, popularityCol, nameCol) {
  const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
  range.sort([
    { column: popularityCol, ascending: false }, // По популярности
    { column: nameCol, ascending: true }        // По имени мода
  ]);
}