carrot-script

Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         carrot-script
// @namespace    https://greasyfork.org/zh-CN/users/1182955
// @version      0.1.1
// @author       meooow25 & RimuruChan
// @description  Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan
// @license      MIT
// @icon         https://aowuucdn.oss-accelerate.aliyuncs.com/codeforces.png
// @homepageURL  https://github.com/RimuruChan/carrot-userscript
// @match        https://codeforces.com/*
// @grant        GM.deleteValue
// @grant        GM_addStyle
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// ==/UserScript==

(function () {
  'use strict';

  var __defProp = Object.defineProperty;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  class Api {
    constructor(fetchFromContentScript2) {
      __publicField(this, "fetchFromContentScript");
      this.fetchFromContentScript = fetchFromContentScript2;
    }
    async fetch(path, queryParams) {
      let queryParamList = [];
      for (const [key, value] of Object.entries(queryParams)) {
        if (value !== void 0) {
          queryParamList.push([key, value]);
        }
      }
      return await this.fetchFromContentScript(path, queryParamList);
    }
    async contestList(gym = void 0) {
      return await this.fetch("contest.list", { gym });
    }
    async contestStandings(contestId, from = void 0, count = void 0, handles = void 0, room = void 0, showUnofficial = void 0) {
      return await this.fetch("contest.standings", {
        contestId,
        from,
        count,
        handles: handles && handles.length ? handles.join(";") : void 0,
        room,
        showUnofficial
      });
    }
    async contestRatingChanges(contestId) {
      return await this.fetch("contest.ratingChanges", { contestId });
    }
    async userRatedList(activeOnly = false) {
      return await this.fetch("user.ratedList", { activeOnly });
    }
  }
  class FFTConv {
    constructor(n) {
      __publicField(this, "n");
      __publicField(this, "wr");
      __publicField(this, "wi");
      __publicField(this, "rev");
      let k = 1;
      while (1 << k < n) {
        k++;
      }
      this.n = 1 << k;
      const n2 = this.n >> 1;
      this.wr = [];
      this.wi = [];
      const ang = 2 * Math.PI / this.n;
      for (let i = 0; i < n2; i++) {
        this.wr[i] = Math.cos(i * ang);
        this.wi[i] = Math.sin(i * ang);
      }
      this.rev = [0];
      for (let i = 1; i < this.n; i++) {
        this.rev[i] = this.rev[i >> 1] >> 1 | (i & 1) << k - 1;
      }
    }
    reverse(a) {
      for (let i = 1; i < this.n; i++) {
        if (i < this.rev[i]) {
          const tmp = a[i];
          a[i] = a[this.rev[i]];
          a[this.rev[i]] = tmp;
        }
      }
    }
    transform(ar, ai) {
      this.reverse(ar);
      this.reverse(ai);
      const wr = this.wr;
      const wi = this.wi;
      for (let len = 2; len <= this.n; len <<= 1) {
        const half = len >> 1;
        const diff = this.n / len;
        for (let i = 0; i < this.n; i += len) {
          let pw = 0;
          for (let j = i; j < i + half; j++) {
            const k = j + half;
            const vr = ar[k] * wr[pw] - ai[k] * wi[pw];
            const vi = ar[k] * wi[pw] + ai[k] * wr[pw];
            ar[k] = ar[j] - vr;
            ai[k] = ai[j] - vi;
            ar[j] += vr;
            ai[j] += vi;
            pw += diff;
          }
        }
      }
    }
    convolve(a, b) {
      if (a.length === 0 || b.length === 0) {
        return [];
      }
      const n = this.n;
      const resLen = a.length + b.length - 1;
      if (resLen > n) {
        throw new Error(
          `a.length + b.length - 1 is ${a.length} + ${b.length} - 1 = ${resLen}, expected <= ${n}`
        );
      }
      const cr = new Array(n).fill(0);
      const ci = new Array(n).fill(0);
      cr.splice(0, a.length, ...a);
      ci.splice(0, b.length, ...b);
      this.transform(cr, ci);
      cr[0] = 4 * cr[0] * ci[0];
      ci[0] = 0;
      for (let i = 1, j = n - 1; i <= j; i++, j--) {
        const ar = cr[i] + cr[j];
        const ai = ci[i] - ci[j];
        const br = ci[j] + ci[i];
        const bi = cr[j] - cr[i];
        cr[i] = ar * br - ai * bi;
        ci[i] = ar * bi + ai * br;
        cr[j] = cr[i];
        ci[j] = -ci[i];
      }
      this.transform(cr, ci);
      const res = [];
      res[0] = cr[0] / (4 * n);
      for (let i = 1, j = n - 1; i <= j; i++, j--) {
        res[i] = cr[j] / (4 * n);
        res[j] = cr[i] / (4 * n);
      }
      res.splice(resLen);
      return res;
    }
  }
  function binarySearch(left, right, predicate) {
    if (left > right) {
      throw new Error(`left ${left} must be <= right ${right}`);
    }
    while (left < right) {
      const mid = Math.floor((left + right) / 2);
      if (predicate(mid)) {
        right = mid;
      } else {
        left = mid + 1;
      }
    }
    return left;
  }
  const DEFAULT_RATING = 1400;
  class Contestant {
    constructor(handle, points, penalty, rating) {
      __publicField(this, "handle");
      __publicField(this, "points");
      __publicField(this, "penalty");
      __publicField(this, "rating");
      __publicField(this, "effectiveRating");
      __publicField(this, "rank");
      __publicField(this, "delta");
      __publicField(this, "performance");
      this.handle = handle;
      this.points = points;
      this.penalty = penalty;
      this.rating = rating;
      this.effectiveRating = rating == null ? DEFAULT_RATING : rating;
      this.rank = null;
      this.delta = null;
      this.performance = null;
    }
  }
  class PredictResult {
    constructor(handle, rating, delta, performance2) {
      __publicField(this, "handle");
      __publicField(this, "rating");
      __publicField(this, "delta");
      __publicField(this, "performance");
      this.handle = handle;
      this.rating = rating;
      this.delta = delta;
      this.performance = performance2;
    }
    get effectiveRating() {
      return this.rating == null ? DEFAULT_RATING : this.rating;
    }
  }
  const MAX_RATING_LIMIT = 6e3;
  const MIN_RATING_LIMIT = -500;
  const RATING_RANGE_LEN = MAX_RATING_LIMIT - MIN_RATING_LIMIT;
  const ELO_OFFSET = RATING_RANGE_LEN;
  const RATING_OFFSET = -MIN_RATING_LIMIT;
  const ELO_WIN_PROB = new Array(2 * RATING_RANGE_LEN + 1);
  for (let i = -RATING_RANGE_LEN; i <= RATING_RANGE_LEN; i++) {
    ELO_WIN_PROB[i + ELO_OFFSET] = 1 / (1 + Math.pow(10, i / 400));
  }
  const fftConv = new FFTConv(ELO_WIN_PROB.length + RATING_RANGE_LEN - 1);
  class RatingCalculator {
    constructor(contestants) {
      __publicField(this, "contestants");
      __publicField(this, "seed");
      __publicField(this, "adjustment");
      this.contestants = contestants;
      this.seed = null;
      this.adjustment = null;
    }
    calculateDeltas(calcPerfs = false) {
      performance.now();
      this.calcSeed();
      this.reassignRanks();
      this.calcDeltas();
      this.adjustDeltas();
      if (calcPerfs) {
        this.calcPerfs();
      }
      performance.now();
    }
    calcSeed() {
      const counts = new Array(RATING_RANGE_LEN).fill(0);
      for (const c of this.contestants) {
        counts[c.effectiveRating + RATING_OFFSET] += 1;
      }
      this.seed = fftConv.convolve(ELO_WIN_PROB, counts);
      for (let i = 0; i < this.seed.length; i++) {
        this.seed[i] += 1;
      }
    }
    getSeed(r, exclude) {
      return this.seed[r + ELO_OFFSET + RATING_OFFSET] - ELO_WIN_PROB[r - exclude + ELO_OFFSET];
    }
    reassignRanks() {
      this.contestants.sort(
        (a, b) => a.points !== b.points ? b.points - a.points : a.penalty - b.penalty
      );
      let lastPoints, lastPenalty, rank;
      for (let i = this.contestants.length - 1; i >= 0; i--) {
        const c = this.contestants[i];
        if (c.points !== lastPoints || c.penalty !== lastPenalty) {
          lastPoints = c.points;
          lastPenalty = c.penalty;
          rank = i + 1;
        }
        c.rank = rank;
      }
    }
    calcDelta(contestant, assumedRating) {
      const c = contestant;
      const seed = this.getSeed(assumedRating, c.effectiveRating);
      const midRank = Math.sqrt(c.rank * seed);
      const needRating = this.rankToRating(midRank, c.effectiveRating);
      const delta = Math.trunc((needRating - assumedRating) / 2);
      return delta;
    }
    calcDeltas() {
      for (const c of this.contestants) {
        c.delta = this.calcDelta(c, c.effectiveRating);
      }
    }
    rankToRating(rank, selfRating) {
      return binarySearch(
        2,
        MAX_RATING_LIMIT,
        (rating) => this.getSeed(rating, selfRating) < rank
      ) - 1;
    }
    adjustDeltas() {
      this.contestants.sort((a, b) => b.effectiveRating - a.effectiveRating);
      const n = this.contestants.length;
      {
        const deltaSum = this.contestants.reduce((a, b) => a + b.delta, 0);
        const inc = Math.trunc(-deltaSum / n) - 1;
        this.adjustment = inc;
        for (const c of this.contestants) {
          c.delta += inc;
        }
      }
      {
        const zeroSumCount = Math.min(4 * Math.round(Math.sqrt(n)), n);
        const deltaSum = this.contestants.slice(0, zeroSumCount).reduce((a, b) => a + b.delta, 0);
        const inc = Math.min(Math.max(Math.trunc(-deltaSum / zeroSumCount), -10), 0);
        this.adjustment += inc;
        for (const c of this.contestants) {
          c.delta += inc;
        }
      }
    }
    calcPerfs() {
      for (const c of this.contestants) {
        if (c.rank === 1) {
          c.performance = Infinity;
        } else {
          c.performance = binarySearch(
            MIN_RATING_LIMIT,
            MAX_RATING_LIMIT,
            (assumedRating) => this.calcDelta(c, assumedRating) + this.adjustment <= 0
          );
        }
      }
    }
  }
  function predict$1(contestants, calcPerfs = false) {
    new RatingCalculator(contestants).calculateDeltas(calcPerfs);
    return contestants.map((c) => new PredictResult(c.handle, c.rating, c.delta, c.performance));
  }
  const _Rank = class _Rank {
    constructor(name, abbr, low, high, colorClass) {
      __publicField(this, "name");
      __publicField(this, "abbr");
      __publicField(this, "low");
      __publicField(this, "high");
      __publicField(this, "colorClass");
      this.name = name;
      this.abbr = abbr;
      this.low = low;
      this.high = high;
      this.colorClass = colorClass;
    }
    static forRating(rating) {
      if (rating == null) {
        return _Rank.UNRATED;
      }
      for (const rank of _Rank.RATED) {
        if (rating < rank.high) {
          return rank;
        }
      }
      return _Rank.RATED[_Rank.RATED.length - 1];
    }
  };
  __publicField(_Rank, "UNRATED");
  __publicField(_Rank, "RATED");
  let Rank = _Rank;
  Rank.UNRATED = new Rank("Unrated", "U", -Infinity, null, null);
  Rank.RATED = [
    new Rank("Newbie", "N", -Infinity, 1200, "user-gray"),
    new Rank("Pupil", "P", 1200, 1400, "user-green"),
    new Rank("Specialist", "S", 1400, 1600, "user-cyan"),
    new Rank("Expert", "E", 1600, 1900, "user-blue"),
    new Rank("Candidate Master", "CM", 1900, 2100, "user-violet"),
    new Rank("Master", "M", 2100, 2300, "user-orange"),
    new Rank("International Master", "IM", 2300, 2400, "user-orange"),
    new Rank("Grandmaster", "GM", 2400, 2600, "user-red"),
    new Rank("International Grandmaster", "IGM", 2600, 3e3, "user-red"),
    new Rank("Legendary Grandmaster", "LGM", 3e3, 4e3, "user-legendary"),
    new Rank("Tourist", "T", 4e3, Infinity, "user-4000")
  ];
  class PredictResponseRow {
    constructor(delta, rank, performance2, newRank, deltaReqForRankUp, nextRank) {
      __publicField(this, "delta");
      __publicField(this, "rank");
      __publicField(this, "performance");
      // For FINAL
      __publicField(this, "newRank");
      // For PREDICTED
      __publicField(this, "deltaReqForRankUp");
      __publicField(this, "nextRank");
      this.delta = delta;
      this.rank = rank;
      this.performance = performance2;
      this.newRank = newRank;
      this.deltaReqForRankUp = deltaReqForRankUp;
      this.nextRank = nextRank;
    }
  }
  const _PredictResponse = class _PredictResponse {
    constructor(predictResults, type, fetchTime) {
      __publicField(this, "rowMap");
      __publicField(this, "type");
      __publicField(this, "fetchTime");
      _PredictResponse.assertTypeOk(type);
      this.rowMap = {};
      this.type = type;
      this.fetchTime = fetchTime;
      this.populateMap(predictResults);
    }
    populateMap(predictResults) {
      for (const result of predictResults) {
        let rank, newRank, deltaReqForRankUp, nextRank;
        switch (this.type) {
          case _PredictResponse.TYPE_PREDICTED:
            rank = Rank.forRating(result.rating);
            const effectiveRank = Rank.forRating(result.effectiveRating);
            deltaReqForRankUp = effectiveRank.high - result.effectiveRating;
            nextRank = Rank.RATED[Rank.RATED.indexOf(effectiveRank) + 1] || null;
            break;
          case _PredictResponse.TYPE_FINAL:
            rank = Rank.forRating(result.rating);
            newRank = Rank.forRating(result.effectiveRating + result.delta);
            break;
          default:
            throw new Error("Unknown prediction type");
        }
        const performance2 = {
          value: result.performance === Infinity ? "Infinity" : result.performance,
          colorClass: Rank.forRating(result.performance).colorClass
        };
        this.rowMap[result.handle] = new PredictResponseRow(
          result.delta,
          rank,
          performance2,
          newRank,
          deltaReqForRankUp,
          nextRank
        );
      }
    }
    static assertTypeOk(type) {
      if (!_PredictResponse.TYPES.includes(type)) {
        throw new Error("Unknown prediction type: " + type);
      }
    }
  };
  __publicField(_PredictResponse, "TYPE_PREDICTED", "PREDICTED");
  __publicField(_PredictResponse, "TYPE_FINAL", "FINAL");
  __publicField(_PredictResponse, "TYPES", [_PredictResponse.TYPE_PREDICTED, _PredictResponse.TYPE_FINAL]);
  let PredictResponse = _PredictResponse;
  class Lock {
    constructor() {
      __publicField(this, "queue");
      __publicField(this, "locked");
      this.queue = [];
      this.locked = false;
    }
    async acquire() {
      if (this.locked) {
        await new Promise((resolve) => {
          this.queue.push(resolve);
        });
      }
      this.locked = true;
    }
    release() {
      if (!this.locked) {
        throw new Error("The lock must be acquired before release");
      }
      this.locked = false;
      if (this.queue.length) {
        const resolve = this.queue.shift();
        resolve();
      }
    }
    async execute(asyncFunc) {
      await this.acquire();
      try {
        return await asyncFunc();
      } finally {
        this.release();
      }
    }
  }
  const REFRESH_INTERVAL = 6 * 60 * 60 * 1e3;
  const CONTESTS$1 = "cache.contests";
  const CONTESTS_TIMESTAMP = "cache.contests.timestamp";
  class Contests {
    constructor(api, storage) {
      __publicField(this, "api");
      __publicField(this, "storage");
      __publicField(this, "lock");
      this.api = api;
      this.storage = storage;
      this.lock = new Lock();
    }
    async getLastAttemptTime() {
      return await this.storage.get(CONTESTS_TIMESTAMP, 0);
    }
    async setLastAttemptTime(time) {
      await this.storage.set(CONTESTS_TIMESTAMP, time);
    }
    async getContestMap() {
      let res = await this.storage.get(CONTESTS$1, {});
      res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), v]));
      return res;
    }
    async setContestMap(contestMap) {
      const obj = Object.fromEntries(contestMap);
      await this.storage.set(CONTESTS$1, obj);
    }
    async maybeRefreshCache() {
      const inner = async () => {
        const now = Date.now();
        const refresh = now - await this.getLastAttemptTime() > REFRESH_INTERVAL;
        if (!refresh) {
          return;
        }
        await this.setLastAttemptTime(now);
        try {
          const contests = await this.api.contestList();
          await this.setContestMap(new Map(contests.map((c) => [c.id, c])));
        } catch (er) {
          console.warn("Unable to fetch contest list: " + er);
        }
      };
      await this.lock.execute(inner);
    }
    async list() {
      return Array.from((await this.getContestMap()).values());
    }
    async hasCached(contestId) {
      return (await this.getContestMap()).has(contestId);
    }
    async getCached(contestId) {
      return (await this.getContestMap()).get(contestId);
    }
    async update(contest) {
      const contestMap = await this.getContestMap();
      contestMap.set(contest.id, contest);
      await this.setContestMap(contestMap);
    }
  }
  const PREFETCH_INTERVAL = 60 * 60 * 1e3;
  const RATINGS_TIMESTAMP = "cache.ratings.timestamp";
  const RATINGS$1 = "cache.ratings";
  class Ratings {
    constructor(api, storage) {
      __publicField(this, "api");
      __publicField(this, "storage");
      __publicField(this, "lock");
      this.api = api;
      this.storage = storage;
      this.lock = new Lock();
    }
    async maybeRefreshCache(contestStartMs) {
      const inner = async () => {
        const timeLeft = contestStartMs - Date.now();
        if (timeLeft > PREFETCH_INTERVAL) {
          return;
        }
        const timeLeftAfterLastFetch = contestStartMs - await this.storage.get(RATINGS_TIMESTAMP, 0);
        if (timeLeftAfterLastFetch > PREFETCH_INTERVAL) {
          await this.cacheRatings();
        }
      };
      await this.lock.execute(inner);
    }
    async fetchCurrentRatings(contestStartMs) {
      if (Date.now() < contestStartMs) {
        throw new Error("getCurrentRatings should be called after contest start");
      }
      await this.maybeRefreshCache(contestStartMs);
      const ratings = await this.storage.get(RATINGS$1);
      return new Map(Object.entries(ratings));
    }
    async cacheRatings() {
      const users = await this.api.userRatedList(false);
      const ratings = Object.fromEntries(users.map((u) => [u.handle, u.rating]));
      await this.storage.set(RATINGS$1, ratings);
      await this.storage.set(RATINGS_TIMESTAMP, Date.now());
    }
  }
  const _Contest = class _Contest {
    constructor(contest, problems, rows, ratingChanges, oldRatings, fetchTime, isRated) {
      __publicField(this, "contest");
      __publicField(this, "problems");
      __publicField(this, "rows");
      __publicField(this, "ratingChanges");
      __publicField(this, "oldRatings");
      __publicField(this, "performances");
      __publicField(this, "fetchTime");
      __publicField(this, "isRated");
      __publicField(this, "startTimeSeconds");
      __publicField(this, "durationSeconds");
      this.contest = contest;
      this.problems = problems;
      this.rows = rows;
      this.ratingChanges = ratingChanges;
      this.oldRatings = oldRatings;
      this.fetchTime = fetchTime;
      this.isRated = isRated;
      this.performances = null;
      this.startTimeSeconds = 0;
      this.durationSeconds = 0;
    }
    toPlainObject() {
      return {
        contest: this.contest,
        problems: this.problems,
        rows: this.rows,
        ratingChanges: this.ratingChanges,
        oldRatings: Array.from(this.oldRatings),
        fetchTime: this.fetchTime,
        isRated: this.isRated
      };
    }
    static fromPlainObject(obj) {
      const c = new _Contest(
        obj.contest,
        obj.problems,
        obj.rows,
        obj.ratingChanges,
        new Map(obj.oldRatings),
        obj.fetchTime,
        obj.isRated
      );
      return c;
    }
  };
  __publicField(_Contest, "IsRated", {
    YES: "YES",
    NO: "NO",
    LIKELY: "LIKELY"
  });
  let Contest = _Contest;
  const MAGIC_CACHE_DURATION = 5 * 60 * 1e3;
  const RATING_PENDING_MAX_DAYS = 3;
  function isOldContest(contest) {
    const daysSinceContestEnd = (Date.now() / 1e3 - contest.startTimeSeconds - contest.durationSeconds) / (60 * 60 * 24);
    return daysSinceContestEnd > RATING_PENDING_MAX_DAYS;
  }
  function isMagicOn() {
    let now = /* @__PURE__ */ new Date();
    return now.getMonth() === 11 && now.getDate() >= 24 || now.getMonth() === 0 && now.getDate() <= 11;
  }
  const MAX_FINISHED_CONTESTS_TO_CACHE = 5;
  const CONTESTS_COMPLETE$1 = "cache.contests_complete";
  const CONTESTS_COMPLETE_IDS = "cache.contests_complete.ids";
  const CONTESTS_COMPLETE_TIMESTAMP = "cache.contests_complete.timestamp";
  class ContestsComplete {
    constructor(api, storage) {
      __publicField(this, "api");
      __publicField(this, "storage");
      this.api = api;
      this.storage = storage;
    }
    async getContests() {
      let res = await this.storage.get(CONTESTS_COMPLETE$1, {});
      res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), Contest.fromPlainObject(v)]));
      return res;
    }
    async setContests(contests) {
      const obj = Object.fromEntries([...contests.entries()].map(([k, v]) => [k, v.toPlainObject()]));
      await this.storage.set(CONTESTS_COMPLETE$1, obj);
    }
    async getContestIds() {
      return await this.storage.get(CONTESTS_COMPLETE_IDS, []);
    }
    async setContestIds(contestIds) {
      await this.storage.set(CONTESTS_COMPLETE_IDS, contestIds);
    }
    async getContestTimestamp() {
      let res = await this.storage.get(CONTESTS_COMPLETE_TIMESTAMP, {});
      res = new Map(Object.entries(res));
      return res;
    }
    async setContestTimestamp(contestTimestamp) {
      const obj = Object.fromEntries(contestTimestamp);
      await this.storage.set(CONTESTS_COMPLETE_TIMESTAMP, obj);
    }
    async fetch(contestId) {
      const cachedContests = await this.getContests();
      if (cachedContests.has(contestId)) {
        console.log("Returning cached contest");
        return cachedContests.get(contestId);
      }
      const { contest, problems, rows } = await this.api.contestStandings(contestId);
      let ratingChanges;
      let oldRatings;
      let isRated = Contest.IsRated.LIKELY;
      if (contest.phase === "FINISHED") {
        try {
          ratingChanges = await this.api.contestRatingChanges(contestId);
          if (ratingChanges) {
            if (ratingChanges.length > 0) {
              isRated = Contest.IsRated.YES;
              oldRatings = adjustOldRatings(contestId, ratingChanges);
            } else {
              ratingChanges = void 0;
            }
          }
        } catch (er) {
          if (er.message.includes("Rating changes are unavailable for this contest")) {
            isRated = Contest.IsRated.NO;
          }
        }
      }
      if (isRated === Contest.IsRated.LIKELY && isOldContest(contest)) {
        isRated = Contest.IsRated.NO;
      }
      const isFinished = isRated === Contest.IsRated.NO || isRated === Contest.IsRated.YES;
      const c = new Contest(contest, problems, rows, ratingChanges, oldRatings, Date.now(), isRated);
      if (isFinished) {
        const contests = await this.getContests();
        contests.set(contestId, c);
        let contestIds = await this.getContestIds();
        contestIds.push(contestId);
        while (contestIds.length > MAX_FINISHED_CONTESTS_TO_CACHE) {
          contests.delete(contestIds.shift());
        }
        if (isMagicOn()) {
          const contestTimestamp = await this.getContestTimestamp();
          for (const [cid, timestamp] of contestTimestamp) {
            if (Date.now() - timestamp > MAGIC_CACHE_DURATION) {
              contestTimestamp.delete(cid);
              contests.delete(cid);
              contestIds = contestIds.filter((c2) => c2 !== cid);
            }
          }
          contestTimestamp.set(contestId, Date.now());
          await this.setContestTimestamp(contestTimestamp);
        }
        await this.setContests(contests);
        await this.setContestIds(contestIds);
      }
      return c;
    }
  }
  const FAKE_RATINGS_SINCE_CONTEST = 1360;
  const NEW_DEFAULT_RATING = 1400;
  function adjustOldRatings(contestId, ratingChanges) {
    const oldRatings = /* @__PURE__ */ new Map();
    if (contestId < FAKE_RATINGS_SINCE_CONTEST) {
      for (const change of ratingChanges) {
        oldRatings.set(change.handle, change.oldRating);
      }
    } else {
      for (const change of ratingChanges) {
        oldRatings.set(change.handle, change.oldRating == 0 ? NEW_DEFAULT_RATING : change.oldRating);
      }
    }
    return oldRatings;
  }
  var _GM_addStyle = /* @__PURE__ */ (() => typeof GM_addStyle != "undefined" ? GM_addStyle : void 0)();
  var _GM_deleteValue = /* @__PURE__ */ (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)();
  var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_listValues = /* @__PURE__ */ (() => typeof GM_listValues != "undefined" ? GM_listValues : void 0)();
  var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  class StorageWrapper {
    constructor(storageName) {
      __publicField(this, "storageName");
      this.storageName = storageName;
    }
    async get(key, defaultValue = void 0) {
      return await _GM_getValue(`${this.storageName}.${key}`, defaultValue);
    }
    async set(key, value) {
      return await _GM_setValue(`${this.storageName}.${key}`, value);
    }
  }
  const LOCAL = new StorageWrapper("LOCAL");
  const SYNC = new StorageWrapper("SYNC");
  function boolSetterGetter(key, defaultValue) {
    return async (value) => {
      if (value === void 0) {
        return await SYNC.get(key, defaultValue);
      }
      return await SYNC.set(key, value);
    };
  }
  const enablePredictDeltas = boolSetterGetter("settings.enablePredictDeltas", true);
  const enableFinalDeltas = boolSetterGetter("settings.enableFetchDeltas", true);
  const enablePrefetchRatings = boolSetterGetter("settings.enablePrefetchRatings", true);
  const showColCurrentPerformance = boolSetterGetter("settings.showColCurrentPerformance", true);
  const showColPredictedDelta = boolSetterGetter("settings.showColPredictedDelta", true);
  const showColRankUpDelta = boolSetterGetter("settings.showColRankUpDelta", true);
  const showColFinalPerformance = boolSetterGetter("settings.showColFinalPerformance", true);
  const showColFinalDelta = boolSetterGetter("settings.showColFinalDelta", true);
  const showColRankChange = boolSetterGetter("settings.showColRankChange", true);
  async function getPrefs() {
    return {
      enablePredictDeltas: await enablePredictDeltas(),
      enableFinalDeltas: await enableFinalDeltas(),
      enablePrefetchRatings: await enablePrefetchRatings(),
      showColCurrentPerformance: await showColCurrentPerformance(),
      showColPredictedDelta: await showColPredictedDelta(),
      showColRankUpDelta: await showColRankUpDelta(),
      showColFinalPerformance: await showColFinalPerformance(),
      showColFinalDelta: await showColFinalDelta(),
      showColRankChange: await showColRankChange()
    };
  }
  const UNRATED_HINTS = ["unrated", "fools", "q#", "kotlin", "marathon", "teams"];
  const EDU_ROUND_RATED_THRESHOLD = 2100;
  const API = new Api(fetchFromContentScript);
  const CONTESTS = new Contests(API, LOCAL);
  const RATINGS = new Ratings(API, LOCAL);
  const CONTESTS_COMPLETE = new ContestsComplete(API, LOCAL);
  const API_PATH = "/api/";
  async function fetchFromContentScript(path, queryParamList) {
    const url = new URL(location.origin + API_PATH + path);
    for (const [key, value] of queryParamList) {
      url.searchParams.append(key, value);
    }
    const resp = await fetch(url);
    const text = await resp.text();
    if (resp.status !== 200) {
      throw new Error(`CF API: HTTP error ${resp.status}: ${text}`);
    }
    let json;
    try {
      json = JSON.parse(text);
    } catch (_) {
      throw new Error(`CF API: Invalid JSON: ${text}`);
    }
    if (json.status !== "OK" || json.result === void 0) {
      throw new Error(`CF API: Error: ${text}`);
    }
    return json.result;
  }
  function isUnratedByName(contestName) {
    const lower = contestName.toLowerCase();
    return UNRATED_HINTS.some((hint) => lower.includes(hint));
  }
  function anyRowHasTeam(rows) {
    return rows.some((row) => row.party.teamId != null || row.party.teamName != null);
  }
  async function getDeltas(contestId) {
    const prefs = await getPrefs();
    return await calcDeltas(contestId, prefs);
  }
  async function calcDeltas(contestId, prefs) {
    if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) {
      return { result: "DISABLED" };
    }
    if (await CONTESTS.hasCached(contestId)) {
      const contest2 = await CONTESTS.getCached(contestId);
      if (isUnratedByName(contest2.name)) {
        return { result: "UNRATED_CONTEST" };
      }
    }
    const contest = await CONTESTS_COMPLETE.fetch(contestId);
    CONTESTS.update(contest.contest);
    if (contest.isRated === Contest.IsRated.NO) {
      return { result: "UNRATED_CONTEST" };
    }
    if (contest.isRated === Contest.IsRated.YES) {
      if (!prefs.enableFinalDeltas) {
        return { result: "DISABLED" };
      }
      return {
        result: "OK",
        prefs,
        predictResponse: getFinal(contest)
      };
    }
    if (isUnratedByName(contest.contest.name)) {
      return { result: "UNRATED_CONTEST" };
    }
    if (anyRowHasTeam(contest.rows)) {
      return { result: "UNRATED_CONTEST" };
    }
    if (!prefs.enablePredictDeltas) {
      return { result: "DISABLED" };
    }
    return {
      result: "OK",
      prefs,
      predictResponse: await getPredicted(contest)
    };
  }
  function predictForRows(rows, ratingBeforeContest) {
    const contestants = rows.map((row) => {
      const handle = row.party.members[0].handle;
      return new Contestant(handle, row.points, row.penalty, ratingBeforeContest.get(handle) ?? null);
    });
    return predict$1(contestants, true);
  }
  function getFinal(contest) {
    if (contest.performances === null) {
      const ratingBeforeContest = new Map(
        contest.ratingChanges.map((c) => [c.handle, contest.oldRatings.get(c.handle)])
      );
      const rows = contest.rows.filter((row) => {
        const handle = row.party.members[0].handle;
        return ratingBeforeContest.has(handle);
      });
      const predictResultsForPerf = predictForRows(rows, ratingBeforeContest);
      contest.performances = new Map(predictResultsForPerf.map((r) => [r.handle, r.performance]));
    }
    const predictResults = [];
    for (const change of contest.ratingChanges) {
      predictResults.push(
        new PredictResult(
          change.handle,
          change.oldRating,
          change.newRating - change.oldRating,
          contest.performances.get(change.handle)
        )
      );
    }
    return new PredictResponse(predictResults, PredictResponse.TYPE_FINAL, contest.fetchTime);
  }
  async function getPredicted(contest) {
    const ratingMap = await RATINGS.fetchCurrentRatings(contest.contest.startTimeSeconds * 1e3);
    const isEduRound = contest.contest.name.toLowerCase().includes("educational");
    let rows = contest.rows;
    if (isEduRound) {
      rows = contest.rows.filter((row) => {
        const handle = row.party.members[0].handle;
        return !ratingMap.has(handle) || ratingMap.get(handle) < EDU_ROUND_RATED_THRESHOLD;
      });
    }
    const predictResults = predictForRows(rows, ratingMap);
    return new PredictResponse(predictResults, PredictResponse.TYPE_PREDICTED, contest.fetchTime);
  }
  async function predictDeltas(contestId) {
    return await getDeltas(contestId);
  }
  async function maybeUpdateContestList() {
    const prefs = await getPrefs();
    if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) {
      return;
    }
    await CONTESTS.maybeRefreshCache();
  }
  async function getNearestUpcomingRatedContestStartTime() {
    let nearest = null;
    const now = Date.now();
    for (const c of await CONTESTS.list()) {
      const start = (c.startTimeSeconds || 0) * 1e3;
      if (start < now || isUnratedByName(c.name)) {
        continue;
      }
      if (nearest === null || start < nearest) {
        nearest = start;
      }
    }
    return nearest;
  }
  async function maybeUpdateRatings() {
    const prefs = await getPrefs();
    if (!prefs.enablePredictDeltas || !prefs.enablePrefetchRatings) {
      return;
    }
    const startTimeMs = await getNearestUpcomingRatedContestStartTime();
    if (startTimeMs !== null) {
      await RATINGS.maybeRefreshCache(startTimeMs);
    }
  }
  const contentCss = ".carrot-display-none {\n  display: none;\n}\n";
  const PING_INTERVAL = 3 * 60 * 1e3;
  const PREDICT_TEXT_ID = "carrot-predict-text";
  const DISPLAY_NONE_CLS = "carrot-display-none";
  const Unicode = {
    BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW: "⮭",
    GREEK_CAPITAL_DELTA: "Δ",
    GREEK_CAPITAL_PI: "Π",
    INFINITY: "∞",
    SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL: "⭜",
    BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL: "⭝"
  };
  const PREDICT_COLUMNS = [
    {
      text: "current performance",
      id: "carrot-current-performance",
      setting: "showColCurrentPerformance"
    },
    {
      text: "predicted delta",
      id: "carrot-predicted-delta",
      setting: "showColPredictedDelta"
    },
    {
      text: "delta required to rank up",
      id: "carrot-rank-up-delta",
      setting: "showColRankUpDelta"
    }
  ];
  const FINAL_COLUMNS = [
    {
      text: "final performance",
      id: "carrot-final-performance",
      setting: "showColFinalPerformance"
    },
    {
      text: "final delta",
      id: "carrot-final-delta",
      setting: "showColFinalDelta"
    },
    {
      text: "rank change",
      id: "carrot-rank-change",
      setting: "showColRankChange"
    }
  ];
  const ALL_COLUMNS = PREDICT_COLUMNS.concat(FINAL_COLUMNS);
  function makeGreySpan(text, title) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    span.style.color = "lightgrey";
    span.textContent = text;
    if (title) {
      span.title = title;
    }
    span.classList.add("small");
    return span;
  }
  function makePerformanceSpan(performance2) {
    const span = document.createElement("span");
    if (performance2.value === "Infinity") {
      span.textContent = Unicode.INFINITY;
    } else {
      span.textContent = performance2.value;
      span.classList.add(performance2.colorClass);
    }
    span.style.fontWeight = "bold";
    span.style.display = "inline-block";
    return span;
  }
  function makeRankSpan(rank) {
    const span = document.createElement("span");
    if (rank.colorClass) {
      span.classList.add(rank.colorClass);
    }
    span.style.verticalAlign = "middle";
    span.textContent = rank.abbr;
    span.title = rank.name;
    span.style.display = "inline-block";
    return span;
  }
  function makeArrowSpan(arrow) {
    const span = document.createElement("span");
    span.classList.add("small");
    span.style.verticalAlign = "middle";
    span.style.paddingLeft = "0.5em";
    span.style.paddingRight = "0.5em";
    span.textContent = arrow;
    return span;
  }
  function makeDeltaSpan(delta) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    span.style.verticalAlign = "middle";
    if (delta > 0) {
      span.style.color = "green";
      span.textContent = `+${delta}`;
    } else {
      span.style.color = "gray";
      span.textContent = delta.toString();
    }
    return span;
  }
  function makeFinalRankUpSpan(rank, newRank, arrow) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    span.appendChild(makeRankSpan(rank));
    span.appendChild(makeArrowSpan(arrow));
    span.appendChild(makeRankSpan(newRank));
    return span;
  }
  function makePredictedRankUpSpan(rank, deltaReqForRankUp, nextRank) {
    const span = document.createElement("span");
    span.style.fontWeight = "bold";
    if (nextRank === null) {
      span.appendChild(makeRankSpan(rank));
      return span;
    }
    span.appendChild(makeDeltaSpan(deltaReqForRankUp));
    span.appendChild(makeArrowSpan(Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL));
    span.appendChild(makeRankSpan(nextRank));
    return span;
  }
  function makePerfHeaderCell() {
    const cell = document.createElement("th");
    cell.classList.add("top");
    cell.style.width = "4em";
    {
      const span = document.createElement("span");
      span.textContent = Unicode.GREEK_CAPITAL_PI;
      span.title = "Performance";
      cell.appendChild(span);
    }
    return cell;
  }
  function makeDeltaHeaderCell(deltaColTitle) {
    const cell = document.createElement("th");
    cell.classList.add("top");
    cell.style.width = "4.5em";
    {
      const span = document.createElement("span");
      span.textContent = Unicode.GREEK_CAPITAL_DELTA;
      span.title = deltaColTitle;
      cell.appendChild(span);
    }
    cell.appendChild(document.createElement("br"));
    {
      const span = document.createElement("span");
      span.classList.add("small");
      span.id = PREDICT_TEXT_ID;
      cell.appendChild(span);
    }
    return cell;
  }
  function makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle) {
    const cell = document.createElement("th");
    cell.classList.add("top", "right");
    cell.style.width = rankUpColWidth;
    {
      const span = document.createElement("span");
      span.textContent = Unicode.BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW;
      span.title = rankUpColTitle;
      cell.appendChild(span);
    }
    return cell;
  }
  function makeDataCell(bottom = false, right = false) {
    const cell = document.createElement("td");
    if (bottom) {
      cell.classList.add("bottom");
    }
    if (right) {
      cell.classList.add("right");
    }
    return cell;
  }
  function populateCells(row, type, rankUpTint, perfCell, deltaCell, rankUpCell) {
    if (row === void 0) {
      perfCell.appendChild(makeGreySpan("N/A", "Not applicable"));
      deltaCell.appendChild(makeGreySpan("N/A", "Not applicable"));
      rankUpCell.appendChild(makeGreySpan("N/A", "Not applicable"));
      return;
    }
    perfCell.appendChild(makePerformanceSpan(row.performance));
    deltaCell.appendChild(makeDeltaSpan(row.delta));
    switch (type) {
      case "FINAL":
        if (row.rank.abbr === row.newRank.abbr) {
          rankUpCell.appendChild(makeGreySpan("N/C", "No change"));
        } else {
          const arrow = row.delta > 0 ? Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL : Unicode.BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL;
          rankUpCell.appendChild(makeFinalRankUpSpan(row.rank, row.newRank, arrow));
        }
        break;
      case "PREDICTED":
        rankUpCell.appendChild(
          makePredictedRankUpSpan(row.rank, row.deltaReqForRankUp, row.nextRank)
        );
        if (row.delta >= row.deltaReqForRankUp) {
          const [color, priority] = rankUpTint;
          rankUpCell.style.setProperty("background-color", color ?? null, priority);
        }
        break;
      default:
        throw new Error("Unknown prediction type");
    }
  }
  function updateStandings(resp) {
    let deltaColTitle, rankUpColWidth, rankUpColTitle, columns;
    switch (resp.type) {
      case "FINAL":
        deltaColTitle = "Final rating change";
        rankUpColWidth = "6.5em";
        rankUpColTitle = "Rank change";
        columns = FINAL_COLUMNS;
        break;
      case "PREDICTED":
        deltaColTitle = "Predicted rating change";
        rankUpColWidth = "7.5em";
        rankUpColTitle = "Rating change for rank up";
        columns = PREDICT_COLUMNS;
        break;
      default:
        throw new Error("Unknown prediction type");
    }
    const rows = Array.from(document.querySelectorAll("table.standings tbody tr"));
    for (const [idx, tableRow] of rows.entries()) {
      tableRow.querySelector("th:last-child, td:last-child").classList.remove("right");
      let perfCell, deltaCell, rankUpCell;
      if (idx === 0) {
        perfCell = makePerfHeaderCell();
        deltaCell = makeDeltaHeaderCell(deltaColTitle);
        rankUpCell = makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle);
      } else if (idx === rows.length - 1) {
        perfCell = makeDataCell(true);
        deltaCell = makeDataCell(true);
        rankUpCell = makeDataCell(true, true);
      } else {
        perfCell = makeDataCell();
        deltaCell = makeDataCell();
        rankUpCell = makeDataCell(false, true);
        const handle = tableRow.querySelector("td.contestant-cell").textContent.trim();
        let rankUpTint;
        if (tableRow.classList.contains("highlighted-row")) {
          rankUpTint = ["#d1eef2", "important"];
        } else {
          rankUpTint = [idx % 2 ? "#ebf8eb" : "#f2fff2", void 0];
        }
        populateCells(resp.rowMap[handle], resp.type, rankUpTint, perfCell, deltaCell, rankUpCell);
      }
      const cells = [perfCell, deltaCell, rankUpCell];
      for (let i = 0; i < cells.length; i++) {
        const cell = cells[i];
        if (idx % 2) {
          cell.classList.add("dark");
        }
        cell.classList.add(columns[i].id, DISPLAY_NONE_CLS);
        tableRow.appendChild(cell);
      }
    }
    return columns;
  }
  function updateColumnVisibility(prefs) {
    for (const col of ALL_COLUMNS) {
      const showCol = prefs[col.setting];
      const func = showCol ? (cell) => cell.classList.remove(DISPLAY_NONE_CLS) : (cell) => cell.classList.add(DISPLAY_NONE_CLS);
      document.querySelectorAll(`.${col.id}`).forEach(func);
    }
  }
  function showFinal() {
    const predictTextSpan = document.getElementById(PREDICT_TEXT_ID);
    predictTextSpan.textContent = "Final";
  }
  function showTimer(fetchTime) {
    const predictTextSpan = document.getElementById(PREDICT_TEXT_ID);
    function update() {
      const secSincePredict = Math.floor((Date.now() - fetchTime) / 1e3);
      if (secSincePredict < 30) {
        predictTextSpan.textContent = "Just now";
      } else if (secSincePredict < 60) {
        predictTextSpan.textContent = "<1m old";
      } else {
        predictTextSpan.textContent = Math.floor(secSincePredict / 60) + "m old";
      }
    }
    update();
    setInterval(update, 1e3);
  }
  async function predict(contestId) {
    const response = await predictDeltas(contestId);
    switch (response.result) {
      case "OK":
        break;
      case "UNRATED_CONTEST":
        console.info("[Carrot] Unrated contest, not displaying delta column.");
        return;
      case "DISABLED":
        console.info("[Carrot] Deltas for this contest are disabled according to user settings.");
        return;
      default:
        throw new Error("Unknown result");
    }
    const columns = updateStandings(response.predictResponse);
    switch (response.predictResponse.type) {
      case "FINAL":
        showFinal();
        break;
      case "PREDICTED":
        showTimer(response.predictResponse.fetchTime);
        break;
      default:
        throw new Error("Unknown prediction type");
    }
    updateColumnVisibility(response.prefs);
    return columns;
  }
  function main() {
    _GM_addStyle(contentCss);
    const matches = location.pathname.match(/contest\/(\d+)\/standings/);
    const contestId = matches ? matches[1] : null;
    if (contestId && document.querySelector("table.standings")) {
      predict(Number.parseInt(contestId)).then((columns) => {
      }).catch((er) => {
        console.error("[Carrot] Predict error: %o", er);
        er.toString();
      });
    }
    const ping = async () => {
      await Promise.all([maybeUpdateContestList(), maybeUpdateRatings()]);
    };
    setInterval(ping, PING_INTERVAL);
  }
  const optionsHtml = '<dialog id="options-dialog">\n  <h1>Options</h1>\n  <div id="options-content">\n    <ul>\n      <li>\n        <input type="checkbox" id="enable-predict-deltas">\n        <label for="enable-predict-deltas">\n          Predict and show deltas for running contests and recently finished contests\n        </label>\n        <ul class="inner-ul">\n          <li>\n            <details>\n              <summary>\n                TL;DR: Disable this if you are on a data capped network\n              </summary>\n              If you are on Codeforces and a contest starts in less than an hour, having this\n              option enabled will prefetch user ratings (around 7MB of data) which is required for\n              delta prediction. This is a one-time fetch for the contest. Disabling this will fetch\n              the ratings when you open the ranklist for the first time.\n            </details>\n            <input type="checkbox" id="enable-prefetch-ratings">\n            <label for="enable-prefetch-ratings">Prefetch ratings</label>\n          </li>\n        </ul>\n      </li>\n      <li>\n        <input type="checkbox" id="enable-final-deltas">\n        <label for="enable-final-deltas">\n          Show final deltas for finished rated contests\n        </label>\n      </li>\n      <button id="close-options">Close</button>\n    </ul>\n  </div>\n</dialog>';
  const optionsCss = "#options-dialog {\n  min-width: 500px;\n  min-height: 180px;\n\n  margin: 0 auto;\n  padding: 10px;\n  border: 1px solid #ccc;\n  border-radius: 5px;\n  background-color: #f9f9f9;\n  top: 50%;\n  left: 50%;\n  -webkit-transform: translateX(-50%) translateY(-50%);\n  -moz-transform: translateX(-50%) translateY(-50%);\n  -ms-transform: translateX(-50%) translateY(-50%);\n  transform: translateX(-50%) translateY(-50%);\n}\n\n#options-dialog ul {\n  list-style-type: none;\n}\n\n#options-dialog li {\n  padding-top: 5px;\n}\n\n#options-dialog .inner-ul {\n  padding-left: 25px;\n}\n\n#options-dialog details {\n  margin-left: 10px;\n}\n\n#options-dialog details > summary {\n  margin-left: -10px;\n  cursor: pointer;\n}\n";
  async function setup() {
    const predict2 = document.querySelector("#enable-predict-deltas");
    const final = document.querySelector("#enable-final-deltas");
    const prefetch = document.querySelector("#enable-prefetch-ratings");
    async function update() {
      predict2.checked = await enablePredictDeltas();
      final.checked = await enableFinalDeltas();
      prefetch.checked = await enablePrefetchRatings();
      prefetch.disabled = !predict2.checked;
    }
    predict2.addEventListener("input", async () => {
      await enablePredictDeltas(predict2.checked);
      await update();
    });
    final.addEventListener("input", async () => {
      await enableFinalDeltas(final.checked);
      await update();
    });
    prefetch.addEventListener("input", async () => {
      await enablePrefetchRatings(prefetch.checked);
      await update();
    });
    await update();
  }
  function initOptions() {
    $("body").append(optionsHtml);
    _GM_addStyle(optionsCss);
    _GM_registerMenuCommand("Open options", () => {
      const dialog = document.querySelector("#options-dialog");
      dialog.showModal();
    });
    _GM_registerMenuCommand("Clear cache", () => {
      const list = _GM_listValues();
      for (const key of list) {
        if (key.startsWith("LOCAL.")) {
          _GM_deleteValue(key);
        }
      }
    });
    $("#close-options").on("click", () => {
      const dialog = document.querySelector("#options-dialog");
      dialog.close();
    });
    setup();
  }
  initOptions();
  main();

})();