RichDarts - Remote input connection hub for Autodarts

Connect a lidarts.org game with autodarts.io to automatically enter thrown scores into lidarts. This is a proof of concept and may break or don't function as expected, if you need to correct scores you will have to use autodarts to create the correct score. Manual score input is not possible while the script is running. Lidarts example can be used to implement other sites.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        RichDarts - Remote input connection hub for Autodarts
// @namespace   https://greatest.deepsurf.us/en/users/913506-dotty-dev
// @match       *://lidarts.org/game/*
// @match       *://play.autodarts.io/*
// @match       *://login.autodarts.io/*
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_addValueChangeListener
// @grant       GM_removeValueChangeListener
// @grant       GM_openInTab
// @grant       GM_getTab
// @grant       GM_saveTab
// @grant       GM_notification
// @version     2.1.2
// @author      dotty-dev
// @license     MIT
// @description Connect a lidarts.org game with autodarts.io to automatically enter thrown scores into lidarts. This is a proof of concept and may break or don't function as expected, if you need to correct scores you will have to use autodarts to create the correct score. Manual score input is not possible while the script is running. Lidarts example can be used to implement other sites.
// ==/UserScript==
/*jshint esversion: 11 */
/*jshint laxbreak:true */

let websocketListenerActive = false;

function listen(fn) {
  fn = fn || console.log;

  let property = Object.getOwnPropertyDescriptor(
    MessageEvent.prototype,
    "data"
  );

  const data = property.get;

  // wrapper that replaces getter
  function lookAtMessage() {
    let socket = this.currentTarget instanceof WebSocket;

    if (!socket) {
      return data.call(this);
    }

    let msg = data.call(this);

    Object.defineProperty(this, "data", { value: msg }); //anti-loop
    fn({ data: msg, socket: this.currentTarget, event: this });
    return msg;
  }

  property.get = lookAtMessage;

  Object.defineProperty(MessageEvent.prototype, "data", property);
}

function tryParseJSONObject(jsonString) {
  try {
    var o = JSON.parse(jsonString);
    if (o && typeof o === "object") {
      return o;
    }
  } catch (e) {}

  return jsonString;
}

function injectWebsocketListener() {
  if (websocketListenerActive === false) {
    websocketListenerActive = true;
    let observer = new MutationObserver(function () {
      if (document.head) {
        observer.disconnect();
        listen(({ data }) => {
          const adObj = tryParseJSONObject(data);
          const score = adObj?.data?.turnScore;
          if (typeof score === "number") {
            GM_setValue("adScore", score);
          }
        });
      }
    });
    observer.observe(document, { subtree: true, childList: true });
  }
}

(async function () {
  const site = location.host;
  let lidartLegs = 0;
  let gameSettingsAutodarts = {
    baseScore: 501,
    inMode: "Straight",
    outMode: "Double",
    maxRounds: 80,
    bullMode: "25/50",
    bullOff: "Off",
    matchMode: "Off",
    lobby: "Private",
  };
  let connectionEstablished = false;
  GM_deleteValue("autodartsHeartbeat");
  GM_setValue("startNewGame", false);
  GM_setValue("abortGame", false);

  async function lidartsInit() {
    console.info("RichDarts: we're on lidarts");

    const callerMuter = setInterval(() => {
      let muteLink = document.getElementById("mute");
      muteLink.click();
      if (muteLink.style.display == "none") {
        clearInterval(callerMuter);
      }
    }, 500);
    const gameUrl = location.href;
    const apiUrl = gameUrl.replace("/game/", "/api/game/");
    let gameData = await fetch(apiUrl).then((resp) => resp.json());
    gameSettingsAutodarts.baseScore = gameData.type;
    gameSettingsAutodarts.inMode =
      gameData.in_mode === "si"
        ? "Straight"
        : gameData.in_mode === "di"
        ? "Double"
        : "Master";
    gameSettingsAutodarts.outMode =
      gameData.out_mode === "so"
        ? "Straight"
        : gameData.out_mode === "do"
        ? "Double"
        : "Master";

    GM_setValue("lobbySettings", JSON.stringify(gameSettingsAutodarts));
    const bullNotificationElement = document.querySelector(
      "#closest_to_bull_notification_div"
    );
    const waitForBullOff = setInterval(() => {
      if (
        !bullNotificationElement ||
        bullNotificationElement.style.display == "none"
      ) {
        insertRichDartsButton();
        clearInterval(waitForBullOff);
      }
    }, 500);
  }

  function startLidartsLoop(event) {
    event.preventDefault();
    console.warn(`autodartsHeartbeat: ${GM_getValue("autodartsHeartbeat")}`);
    if (connectionEstablished) {
      GM_notification("RichDarts is already connected");
      return;
    } else {
      GM_setValue("isConnectRichDarts", true);
    }
    if (
      parseInt(document.querySelector(".p1_score").textContent) !=
        gameSettingsAutodarts.baseScore &&
      parseInt(document.querySelector(".p2_score").textContent) !=
        gameSettingsAutodarts.baseScore
    ) {
      GM_notification(
        "Score's don't match base score, please make sure to correct autodarts score!"
      );
    }

    const scoreChangeListener = GM_addValueChangeListener(
      "adScore",
      (key, oldValue, newValue, remote) => {
        const adScore = newValue;
        document.querySelector("#score_value").value = adScore;
      }
    );

    GM_notification("Starting Autodarts Match, please wait");
    openAutodartsTab();
    const richDartsButtons = document.querySelectorAll(".richdarts-connect");

    const adHeartbeatCheck = setInterval(() => {
      let heartbeat = GM_getValue("autodartsHeartbeat");
      if (Date.now() - heartbeat < 20000) {
        if (connectionEstablished === false) {
          connectionEstablished = true;
        }
          richDartsButtons.forEach((el) => {
            let crosshairsClassList =
              el.querySelector(".fa-crosshairs").classList;
            crosshairsClassList.add("text-success");
            crosshairsClassList.remove("text-warning");
            el.querySelector(".richdarts-status").innerText =
              "RichDarts Connected";
          });
      } else if (
        Date.now() - heartbeat > 20000 ||
        !!GM_getValue("autodartsHeartbeat")
      ) {
        console.info("RichDarts: Autodarts connection lost. Stopping loop.");
        connectionEstablished = false;
        clearInterval(adHeartbeatCheck);
        richDartsButtons.forEach((el) => {
          el.disabled = false;
          let crosshairsClassList =
            el.querySelector(".fa-crosshairs").classList;
          crosshairsClassList.remove("text-success");
          crosshairsClassList.add("text-danger");
          el.querySelector(".richdarts-status").innerText =
            "RichDarts disconnected";
        });
      } else {
        console.info(`adHeartbeat: ${heartbeat}`);
        richDartsButtons.forEach((el) => {
          el.disabled = true;
          let crosshairsClassList =
            el.querySelector(".fa-crosshairs").classList;
          crosshairsClassList.add("text-warning");
          crosshairsClassList.remove("text-danger");
          el.querySelector(".richdarts-status").innerText =
            "RichDarts Connecting...";
        });
      }

      let gameCompleted =
        document.querySelector("#confirm_completion").style.display != "none";
      let gameAborted =
        document.querySelector("#game-aborted").style.display != "none";

      if (gameCompleted || gameAborted) {
        GM_setValue("abortGame", true);
        GM_removeValueChangeListener(scoreChangeListener);
      }

      let currentLegsPlayed =
        parseInt(document.querySelector(".p1_legs").textContent) +
        parseInt(document.querySelector(".p2_legs").textContent);
      if (currentLegsPlayed > lidartLegs && !(gameCompleted || gameAborted)) {
        lidartLegs = currentLegsPlayed;
        console.info("RichDarts: starting new Autodarts round");
        GM_setValue("startNewRound", true);
      }
    }, 2500);
  }

  function insertRichDartsButton() {
    const richDartsButtonDummy = document.createElement("div");

    richDartsButtonDummy.innerHTML = /*html*/ `
          <button class="btn btn-primary btn-sm richdarts-connect btn-block mb-3 px-0">
            <span class="fa-stack fa-lg">
              <i class="fas fa-square fa-stack-2x"></i>>
              <i class="fas fa-crosshairs fa-stack-1x text-danger"></i>
            </span>
            <span class="richdarts-status">
              Connect RichDarts
            </span>
          </button>
        `;

    document
      .querySelectorAll(".score_input input.score_value")
      .forEach((el) => {
        el.parentElement.insertAdjacentElement(
          "afterend",
          richDartsButtonDummy.querySelector("button").cloneNode(1)
        );
      });

    document.querySelectorAll(".richdarts-connect").forEach((el) => {
      el.addEventListener("click", startLidartsLoop);
    });
  }

  function autodartsApplySettings(buttons) {
    let settingsClicker = setInterval(() => {
      console.info("RichDarts: trying to apply settings");
      setTimeout(() => {
        buttons?.baseScore.click();
      }, 0);
      setTimeout(() => {
        buttons?.inMode.click();
      }, 50);
      setTimeout(() => {
        buttons?.outMode.click();
      }, 100);
      setTimeout(() => {
        buttons?.maxRounds.click();
      }, 150);
      setTimeout(() => {
        buttons?.bullMode.click();
      }, 200);
      setTimeout(() => {
        buttons?.bullOff.click();
      }, 250);
      setTimeout(() => {
        buttons?.matchMode.click();
      }, 300);
      setTimeout(() => {
        buttons?.lobby.click();
      }, 350);

      if (Object.values(buttons).every((el) => el?.dataset.active === "")) {
        console.info("RichDarts: all settings applied, stopping loop");
        clearInterval(settingsClicker);
        buttons.openButton.click();

        let gameStarter = setInterval(() => {
          console.info("RichDarts: looking for game start button");
          document.querySelectorAll("button").forEach((el) => {
            if (el.innerText === "Start game") {
              if (!el.disabled) {
                el.click();
                clearInterval(gameStarter);
                const gameLoadedCheck = setInterval(() => {
                  if (
                    document.getElementById("ad-ext-game-variant")?.innerText ==
                    "X01"
                  ) {
                    clearInterval(gameLoadedCheck);
                    autodartsSendScores();
                  }
                }, 250);
              }
            }
          });
        }, 250);
      }
    }, 250);
  }

  function autodartsSendScores() {
    let abortButton;
    document
      .querySelector("#ad-ext-game-variant")
      ?.closest(".css-0")
      ?.querySelectorAll("button")
      ?.forEach((el) => {
        if (el.innerText === "Abort") {
          abortButton = el;
        }
      });
    let newGame = false;
    GM_deleteValue("isConnectRichDarts");
    console.info("RichDarts: init sending scores...");
    console.info(
      `autodartsHeartbeat is ${!!GM_getValue("autodartsHeartbeat")}`
    );

    injectWebsocketListener();

    const sendHeartbeat = setInterval(() => {
      GM_setValue("autodartsHeartbeat", Date.now());
    }, 2500);

    const startNewRoundListener = GM_addValueChangeListener(
      "startNewRound",
      (key, oldValue, newValue, remote) => {
        if (newValue === true) {
          GM_setValue("startNewRound", false);
          GM_setValue("isConnectRichDarts", true);
          clearInterval(sendHeartbeat);
          newGame = true;
          let finishButton;
          document
            .querySelector("#ad-ext-turn + div")
            .querySelectorAll("button")
            .forEach((el) => {
              if (el.innerText === "Finish") {
                finishButton = el;
              }
            });
          if (finishButton) {
            finishButton.click();
          } else {
            let i = 0;
            const abortGame = setInterval(() => {
              abortButton.click();
              i++;
              if (i === 3) {
                clearInterval(abortGame);
              }
            }, 500);
          }
          restartOrClose();
        }
      }
    );

    const abortGameListener = GM_addValueChangeListener(
      "abortGame",
      (key, oldValue, newValue, remote) => {
        if (newValue === true) {
          GM_setValue("abortGame", false);
          clearInterval(sendHeartbeat);
          let finishButton;
          document
            .querySelector("#ad-ext-turn + div")
            ?.querySelectorAll("button")
            ?.forEach((el) => {
              if (el.innerText === "Finish") {
                finishButton = el;
              }
            });
          if (finishButton) {
            finishButton.click();
            restartOrClose();
          } else {
            let i = 0;
            const abortGame = setInterval(() => {
              abortButton.click();
              i++;
              if (i === 3) {
                clearInterval(abortGame);
                restartOrClose();
              }
            }, 500);
          }
        }
      }
    );

    function restartOrClose() {
      clearInterval(sendHeartbeat);
      GM_removeValueChangeListener(abortGameListener);
      GM_removeValueChangeListener(startNewRoundListener);
      if (newGame) {
        console.info("RichDarts: Started new round");
        // document.querySelector('a').click();
        setTimeout(() => autodartsInit(), 2000);
      } else {
        GM_notification(
          "Autodarts tabs that were opened by RichDarts will be closed"
        );
        setTimeout(() => {
          window.close();
        }, 2000);
      }
    }
  }

  async function autodartsInit() {
    GM_setValue("abortGame", false);
    GM_setValue("startNewRound", false);
    const isConnect = GM_getValue("isConnectRichDarts");
    if (isConnect) {
      gameSettingsAutodarts = JSON.parse(GM_getValue("lobbySettings"));
      let loopInterval = null;
      console.info("RichDarts: we're on autodarts");
      const autodartsNewLobbyButtons = {
        baseScore: undefined,
        inMode: undefined,
        outMode: undefined,
        maxRounds: undefined,
        bullMode: undefined,
        bullOff: undefined,
        matchMode: undefined,
        lobby: undefined,
        openButton: undefined,
      };
      const x01start = () => {
        console.info("RichDarts: x01start interval running");
        if(location.href !== "https://play.autodarts.io/lobbies/new/x01") {
          clearInterval(loopInterval);
          location.href = "https://play.autodarts.io/lobbies/new/x01";
        }
        if (location.href === "https://play.autodarts.io/lobbies/new/x01") {
          clearInterval(loopInterval);
        }
      };
      setTimeout(() => {
        if (location.href !== "https://play.autodarts.io/lobbies/new/x01") {
          loopInterval = setInterval(x01start, 500);
        }
      },500);

      let settingsElsGetter = setInterval(() => {
        if (
          !Object.values(autodartsNewLobbyButtons).some(
            (val) => val === undefined
          )
        ) {
          clearInterval(settingsElsGetter);
          console.info("RichDarts: all Settings loaded");
          console.info(gameSettingsAutodarts);
          console.info(autodartsNewLobbyButtons);
          autodartsApplySettings(autodartsNewLobbyButtons);
        } else {
          console.info("RichDarts: runningElementGetter");
          document.querySelectorAll(".chakra-text").forEach((el) => {
            switch (el.innerText) {
              case "Base score":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.baseScore) {
                    autodartsNewLobbyButtons.baseScore = el;
                  }
                });
                break;
              case "In mode":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.inMode) {
                    autodartsNewLobbyButtons.inMode = el;
                  }
                });
                break;
              case "Out mode":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.outMode) {
                    autodartsNewLobbyButtons.outMode = el;
                  }
                });
                break;
              case "Max rounds":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.maxRounds) {
                    autodartsNewLobbyButtons.maxRounds = el;
                  }
                });
                break;
              case "Bull mode":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.bullMode) {
                    autodartsNewLobbyButtons.bullMode = el;
                  }
                });
                break;
              case "Bull-off":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.bullOff) {
                    autodartsNewLobbyButtons.bullOff = el;
                  }
                });
                break;
              case "Match mode":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.matchMode) {
                    autodartsNewLobbyButtons.matchMode = el;
                  }
                });
                break;
              case "Lobby":
                el.nextSibling.querySelectorAll("button").forEach((el) => {
                  if (el.innerText == gameSettingsAutodarts.lobby) {
                    autodartsNewLobbyButtons.lobby = el;
                  }
                });
                break;
              default:
                break;
            }
          });
          document.querySelectorAll("button.chakra-button").forEach((el) => {
            if (el.innerText === "Open Lobby") {
              autodartsNewLobbyButtons.openButton = el;
              el.dataset.active = "";
            }
          });
        }
      }, 200);
    }
  }

  function openAutodartsTab() {
    console.info("RichDarts: trying to open a new tab");
    GM_openInTab("https://play.autodarts.io/lobbies/new/x01", true);
  }

  switch (site) {
    case "play.autodarts.io":
      autodartsInit();
      break;
    case "login.autodarts.io":
      alert(
        "You need to log in to Autodarts for RichDarts to connect your games."
      );
      break;
    case "lidarts.org":
      if (location.pathname !== "/game/create") {
        lidartsInit();
      }
      break;
  }
})();