Jupaoqq / Geoguessr Yandex + Baidu Plugin Battle Royale

// ==UserScript==
// @name          Geoguessr Yandex + Baidu Plugin Battle Royale
// @namespace     Jupaoqq
// @description   WARNING: PLEASE DISABLE THIS SCRIPT FOR NORMAL BR MAPS - Play Geoguessr with Yandex and Baidu Panoramas. A big shout out to kommu, who made the original Yandex script, and @MrAmericanMike who helped kommu coding this script and to @Alok who had the idea and help us to test the script
// @version       1.0.1ba
// @include       https://www.geoguessr.com/*
// @run-at        document-start
// @license       MIT
// ==/UserScript==

const YANDEX_API_KEY = "4ca8ac1d-2ecb-4f1d-9bae-fa012e2136ff";
const BAIDU_API_KEY = "8dQ9hZPGEQnqg9r0O1C8Ate2N6P8Zk92";
let BR = false;

function myLog(...args) {
  console.log(...args);
}
function myHighlight(...args) {
  console.log(`%c${[...args]}`, "color: dodgerblue; font-size: 24px;");
}

myLog("Geoguessr Yandex Plugin");

const MAP_NAME_INCLUDES = "Yandex";
const BAIDU_MAP_NAME_INCLUDES = "baidu";
const PANORAMA_MAP_NAME_INCLUDES = "Yandex Air Panorama";

const MAPS_API_URL = "https://maps.googleapis.com/maps/api/js?";

let PLAYER = null;
let ROUND = 0;
let YANDEX_INJECTED = false;
let NEW_ROUND_LOADED = false;
let COMPASS = null;
let PANORAMA_MAP = false;
let CURRENT_ROUND_DATA = null;


let BAIDU_INJECTED = false;
// let COMPASS_HIDDEN = false;

/**
 * Resolves succesfully after detecting that Google Maps API was loaded in a page
 *
 * @returns Promise
 */
function gmp() {
  return new Promise((resolve, reject) => {
    let scriptObserver = new MutationObserver((mutations, observer) => {
      for (let mutation of mutations) {
        for (let node of mutation.addedNodes) {
          if (node.tagName === "SCRIPT" && node.src.startsWith(MAPS_API_URL)) {
            scriptObserver.disconnect();
            scriptObserver = undefined;
            myLog("Detected Google Maps API");
            node.onload = () => resolve();
          }
        }
      }
    });

    let bodyDone = false;
    let headDone = false;

    let injectorObserver = new MutationObserver((mutations, observer) => {
      if (!bodyDone && document.body) {
        bodyDone = true;
        myLog("Body Observer Injected");
        scriptObserver && scriptObserver.observe(document.body, {
          childList: true
        });
      }
      if (!headDone && document.head) {
        headDone = true;
        myLog("Head Observer Injected");
        scriptObserver && scriptObserver.observe(document.head, {
          childList: true
        });
      }
      if (headDone && bodyDone) {
        myLog("Body and Head Observers Injected");
        observer.disconnect();
      }
    });

    injectorObserver.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  });
}

/**
 * Once the Google Maps API was loaded we can do more stuff
 */
gmp().then(() => {
  launchObserver();
});

/**
 * This observer stays alive while the script is running
 */
function launchObserver() {
  myHighlight("Main Observer");
  const OBSERVER = new MutationObserver((mutations, observer) => {
    detectGamePage();
  });
  OBSERVER.observe(document.head, { attributes: true, childList: true, subtree: true });
}

/**
 * Detects if the current page contains /game/ or /challenge/ in it
 * If it doesn't it sets:
 * ROUND = 0
 * PLAYER = null
 * COMPASS = null
 */
function detectGamePage() {
  const PATHNAME = window.location.pathname;
  if (PATHNAME.startsWith("/game/") || PATHNAME.startsWith("/challenge/")){
    myLog("Game page");
    BR = false;
    checkRound();
  }
  else if (PATHNAME.startsWith("/battle-royale/")){
    myLog("BR");
    BR = true;
    checkRound();
  }
  else {
    myLog("Not a Game page");
    ROUND = 0;
    PLAYER = null;
    COMPASS = null;
    BAIDU_INJECTED = false;
  }
}

/**
 * Checks for round changes
 */
function checkRound() {
    if (!BR){
  let currentRound = getRoundFromPage();
  if (ROUND != currentRound) {
    myHighlight("New round");
    ROUND = currentRound;
    NEW_ROUND_LOADED = true;
    COMPASS = null;
    getMapData();
  }
    }
    else{
    myHighlight("New round2");
    NEW_ROUND_LOADED = true;
    COMPASS = null;
    getMapData();
    }
}

/**
 * Gets data for the current map and checks if name contains `MAP_NAME_INCLUDES`
 * If so it continues calling other functions, otherway it does nothing else
 */
function getMapData() {
  getSeed().then((data) => {
    // myHighlight("Seed data");
    // myLog(data);
    if (BR)
    {
        if (!CURRENT_ROUND_DATA)
        {
            CURRENT_ROUND_DATA = data
            hideGoogleCanvas();
            injectBaiduCanvas();
            if (PLAYER) {
                goToBaiduLocation(data);
            }
            else {
                myLog("Ready to inject baidu player");
                injectBaiduPlayer(data);
            }
        }

        if (!(data.currentRoundNumber === CURRENT_ROUND_DATA.currentRoundNumber))
        {
            CURRENT_ROUND_DATA = data
            hideGoogleCanvas();
            injectBaiduCanvas();
            if (PLAYER) {
                goToBaiduLocation(data);
            }
            else {
                myLog("Ready to inject baidu player");
                injectBaiduPlayer(data);
            }
        }
            else{

            }
    }
      else
      {
    if (data.mapName.includes(MAP_NAME_INCLUDES)) {
        PANORAMA_MAP = (data.mapName.includes(PANORAMA_MAP_NAME_INCLUDES));
        myHighlight("Yandex Map");
        hideGoogleCanvas();
        hideDefaultZoomControls();
        injectYandexCanvas();
        CURRENT_ROUND_DATA = data;
        if (PLAYER) {
            goToLocation(data);
        }
        else {
            injectYandexScript().then(() => {
                myLog("Ready to inject player");
                injectYandexPlayer(data);
            }).catch((error) => {
                myLog(error);
            });
        }
    }
    else if (data.mapName.includes(BAIDU_MAP_NAME_INCLUDES)) {
        // if (!COMPASS_HIDDEN)
        // {
        //     hideDefaultCompass();
        // }
        PANORAMA_MAP = (data.mapName.includes(PANORAMA_MAP_NAME_INCLUDES));
        myLog(data);
        hideGoogleCanvas();
        hideDefaultZoomControls();
        injectBaiduCanvas();
        CURRENT_ROUND_DATA = data;
        if (PLAYER) {
            myLog("PLAYER");
            myLog(PLAYER);
            goToBaiduLocation(data);
        }
        else {
            myLog("Ready to inject baidu player");
            injectBaiduPlayer(data);
        }
    }
      }

  }).catch((error) => {
    myLog(error);
  });
}

/**
 * Gets the seed data for the current game
 *
 * @returns Promise with seed data as object
 */
function getSeed() {
  myLog("getSeed called");
  return new Promise((resolve, reject) => {
    let token = getToken();
    let URL;
    let cred = ""

    const PATHNAME = window.location.pathname;

    if (PATHNAME.startsWith("/game/")) {
      URL = `https://www.geoguessr.com/api/v3/games/${token}`;
    }
    else if (PATHNAME.startsWith("/challenge/")) {
      URL = `https://www.geoguessr.com/api/v3/challenges/${token}/game`;
    }
     else if (PATHNAME.startsWith("/battle-royale/")) {
      URL = `https://game-server.geoguessr.com/api/battle-royale/${token}`;
    }
    if (BR)
    {
        fetch(URL, {
    // Include credentials to GET from the endpoint
    credentials: 'include'
  })
      .then((response) => response.json())
      .then((data) => {
        resolve(data);
      })
      .catch((error) => {
        reject(error);
      });
    }
      else{
    fetch(URL)
      .then((response) => response.json())
      .then((data) => {
        resolve(data);
      })
      .catch((error) => {
        reject(error);
      });
  }
  });
}

/**
 * Gets the token from the current URL
 *
 * @returns token
 */
function getToken() {
  const PATHNAME = window.location.pathname;
  if (PATHNAME.startsWith("/game/")) {
    return PATHNAME.replace("/game/", "");
  }
  else if (PATHNAME.startsWith("/challenge/")) {
    return PATHNAME.replace("/challenge/", "");
  }
  else if (PATHNAME.startsWith("/battle-royale/")) {
    return PATHNAME.replace("/battle-royale/", "");
  }
}

/**
 * Gets the round number from the ongoing game from the page itself
 *
 * @returns Round number
 */
function getRoundFromPage() {
  const roundData = document.querySelector("div[data-qa='round-number']");
  if (roundData) {
    let roundElement = roundData.querySelector("div:last-child");
    if (roundElement) {
      let round = parseInt(roundElement.innerText.charAt(0));
      if (!isNaN(round) && round >= 1 && round <= 5) {
        return round;
      }
    }
  }
  else {
    return ROUND;
  }
}

/**
 * Hides Google Canvas
 */
function hideGoogleCanvas() {
  let GOOGLE_MAPS_CANVAS = ""
  if (!BR)
  {
  GOOGLE_MAPS_CANVAS = document.querySelector(".game-layout__panorama-canvas");
  }
    else{
   GOOGLE_MAPS_CANVAS = document.querySelector(".br-game-layout__panorama-canvas");
    }
  GOOGLE_MAPS_CANVAS.style.display = "none";
  myLog("Google Canvas hidden");
}

/**
 * Hides default zoom controls and Yandex GOTO link
 */
function hideDefaultZoomControls() {
  let style = `
	.ymaps-2-1-79-panorama-gotoymaps {display: none !important;}
	.game-layout__controls {bottom: 8rem !important; left: 1rem !important;}
	.game-layout__controls .styles_controlGroup__2pd1f, .styles_horizontalControls__ewOLi {display: none !important;}
	`;

  let style_element = document.createElement("style");
  style_element.innerHTML = style;
  document.body.appendChild(style_element);
}

/**
 * Injects Yandex Canvas
 */
function injectYandexCanvas() {
  const GAME_CANVAS = document.querySelector(".game-layout__panorama");
  GAME_CANVAS.id = "yandex_player";
  myLog("Yandex Canvas injected");
}

/**
 * Injects Yandex Script
 */
function injectYandexScript() {
  return new Promise((resolve, reject) => {
    if (!YANDEX_INJECTED) {
      if (YANDEX_API_KEY === "") {
        let canvas = document.getElementById("yandex_player");
        canvas.innerHTML = `
			<div style="text-align: center;">
			<h1 style="margin-top: 80px; font-size: 48px;">YOU NEED YANDEX API KEY<h1>
			<p><a target="_blank" href="https://yandex.com/dev/maps/jsapi/doc/2.1/quick-start/index.html?from=techmapsmain">Get it here</a></p>
			<br/>
			<p>After that you need to add that key into this script in</p>
			<code>const YANDEX_API_KEY = "";</code>
			</div>
			`;
        reject();
      }
      else {
        const SCRIPT = document.createElement("script");
        SCRIPT.type = "text/javascript";
        SCRIPT.async = true;
        SCRIPT.onload = () => {
          ymaps.ready(() => {
            YANDEX_INJECTED = true;
            myHighlight("Yandex API Loaded");
            resolve();
          });
        }
        SCRIPT.src = `https://api-maps.yandex.ru/2.1/?lang=en_US&apikey=${YANDEX_API_KEY}`;
        document.body.appendChild(SCRIPT);
      }
    }
    else {
      resolve();
    }
  });
}

/**
 * Injects Yandex Player and calls handleReturnToStart
 */
function injectYandexPlayer(data) {
  const lat = data.rounds[data.round - 1].lat;
  const lng = data.rounds[data.round - 1].lng;

  if (PLAYER === null) {
    let options = {
      "direction": [0, 16],
      "span": [10, 67],
      "controls": ["zoomControl"]
    };
    if (PANORAMA_MAP) {
      options.layer = 'yandex#airPanorama';
    }
    ymaps.panorama.createPlayer("yandex_player", [lat, lng], options)
      .done((player) => {
        PLAYER = player;
        PLAYER.events.add("directionchange", (e) => {
          updateCompass(data);
        });
        myHighlight("Player injected");
      });
  }
  else {
    goToLocation(data);
  }
}







function injectBaiduCanvas() {
    let GAME_CANVAS = ""
    if (!BR)
    {
    GAME_CANVAS = document.querySelector(".game-layout__canvas");
    }
    else
    {
    GAME_CANVAS = document.querySelector(".br-game-layout__canvas");
    }
    GAME_CANVAS.id = "baidu_player";
    myLog("Baidu Canvas injected");
  }


/**
 * Injects Baidu
 */
 function injectBaiduScript() {
    // return new Promise(function(resolve, reject) {
    // let canvas = document.getElementById("baidu_player");
    // canvas.innerHTML = `
    //     <div id="PanoramaMap"> </div>
    // `;
    // var script = document.createElement('script')
    // script.type = 'text/javascript'
    // script.src = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_API_KEY}&callback=init`;
    // script.onerror = reject;
    // document.head.appendChild(script)
    // myLog("BMap2");
    // window.init = () => {
    //     resolve(window.BMap)
    // }
    // })
// }
    return new Promise((resolve, reject) => {
      if (!BAIDU_INJECTED) {
        if (BAIDU_API_KEY === "") {
          let canvas = document.getElementById("baidu_player");
          canvas.innerHTML = `
              <div style="text-align: center;">
              <h1 style="margin-top: 80px; font-size: 48px;">YOU NEED Baidu API KEY<h1>
              <br/>
              <p>After that you need to add that key into this script in</p>
              <code>const YANDEX_API_KEY = "";</code>
              </div>
              `;
          reject();
        }
        else {
          const SCRIPT = document.createElement("script");
          SCRIPT.type = "text/javascript";
          SCRIPT.async = true;
          SCRIPT.src = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_API_KEY}&callback=init`;
          document.body.appendChild(SCRIPT);

          let canvas = document.createElement("game-layout__canvas_new");
            if (BR)
            {
                canvas.innerHTML = `
                 <div id="PanoramaMap" class="br-game-layout__panorama" style="zIndex: 99999,position: "absolute", top: 0, left: 0, width: '100%', height: '100%',"> </div>
            `;
            }
            else{
          canvas.innerHTML = `
                 <div id="PanoramaMap" class="game-layout__panorama" style="zIndex: 99999,position: "absolute", top: 0, left: 0, width: '100%', height: '100%',"> </div>
            `;
            }

          var div = document.getElementById("baidu_player");
          div.appendChild(canvas);

          SCRIPT.addEventListener('load', () => {
            BAIDU_INJECTED = true;
                myHighlight("Baidu API Loaded");
                // resolve(BMap);
                setTimeout(function(){resolve(BMap);}.bind(this), 3000);
          })


        }
      }
      else {
        resolve();
      }
    });
  }

  /**
   * Injects Baidu Player and calls handleReturnToStart
   */
  function injectBaiduPlayer(data) {
      let lat = 0;
      let lng = 0;
    if (BR)
    {
        lat = data.rounds[data.currentRoundNumber - 1].lat;
        lng = data.rounds[data.currentRoundNumber - 1].lng;
    }
      else
      {
       lat = data.rounds[data.round - 1].lat;
       lng = data.rounds[data.round - 1].lng;
      }

    if (PLAYER === null) {
        injectBaiduScript().then(BMap => {

        PLAYER = new BMap.Panorama('PanoramaMap');
        PLAYER.setPov({heading: -40, pitch: 6});
        PLAYER.setPosition(new BMap.Point(lng, lat));
        handleBaiduReturnToStart()
        });
    }
    else {
        myLog("No");
      goToBaiduLocation(data);
    }
  }


/**
 * Goes to location when PLAYER already exists
 *
 * @param {*} data
 */
function goToLocation(data) {
  myLog("Going to location");
  const lat = data.rounds[data.round - 1].lat;
  const lng = data.rounds[data.round - 1].lng;
  let options = {};
  if (PANORAMA_MAP) {
    options.layer = 'yandex#airPanorama';
  }
  PLAYER.moveTo([lat, lng], options);
  PLAYER.setDirection([0, 16]);
  PLAYER.setSpan([10, 67]);
}

/**
 * Updates the compass to match Yandex Panorama facing
 */
function updateCompass() {
  if (!COMPASS) {
    let compass = document.querySelector("img.compass__indicator");
    if (compass != null) {
      COMPASS = compass;
      let direction = PLAYER.getDirection()[0] * -1;
      COMPASS.setAttribute("style", `transform: rotate(${direction}deg);`);
    }
    handleReturnToStart();
  }
  else {
    let direction = PLAYER.getDirection()[0] * -1;
    COMPASS.setAttribute("style", `transform: rotate(${direction}deg);`);
  }
}

/**
 * Injects behavior to go back to start when the return to start flag is clicked
 *
 * @param {*} data
 */
function handleReturnToStart() {
  let flag = document.querySelector("button[data-qa='return-to-start']");
  if (flag != null) {
    myLog("Return to start attached");
    let FLAG = flag;
    FLAG.addEventListener("click", () => { goToLocation(CURRENT_ROUND_DATA); });
  }
}






/**
 * Goes to location when PLAYER already exists
 *
 * @param {*} data
 */
 function goToBaiduLocation(data) {
    myLog("Going to location");
      let lat = 0;
      let lng = 0;
     if (BR)
     {
         lat = data.rounds[data.currentRoundNumber - 1].lat;
         lng = data.rounds[data.currentRoundNumber - 1].lng;
     }
     else
     {
         lat = data.rounds[data.round - 1].lat;
         lng = data.rounds[data.round - 1].lng;
     }
    PLAYER.setPov({heading: -40, pitch: 6});
    PLAYER.setPosition(new BMap.Point(lng, lat));

  }


  function handleBaiduReturnToStart() {
    let flag = document.querySelector("button[data-qa='return-to-start']");
    if (flag != null) {
      myLog("Return to start attached");
      let FLAG = flag;
      FLAG.addEventListener("click", () => { goToBaiduLocation(CURRENT_ROUND_DATA); });
    }
  }

//   function hideDefaultCompass() {
//     var comp = document.querySelector(".compass__indicator");
//     comp.style.display = "none";
//     var comp2 = document.querySelector(".compass__circle");
//     comp2.style.display = "none";
//     COMPASS_HIDDEN = true;
//   }