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.

Verze ze dne 27. 10. 2024. Zobrazit nejnovější verzi.

// ==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.0
// @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);

    // Handle non-exception-throwing cases:
    // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
    // but... JSON.parse(null) returns null, and typeof null === "object",
    // so we must check for that, too. Thankfully, null is falsey, so this suffices:
    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);
          console.log(adObj);
          const score = adObj?.data?.turnScore;
          if (typeof score === "number") {
            GM_setValue("adScore", score);
            console.log("score: " + 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;
        console.log(
          `Received ${key}: ${newValue}, old value: ${oldValue}, changed by remote: ${remote}`
        );
        document.querySelector("#score_value").value = adScore;
      }
    );

    GM_notification("Starting Autodarts Match, please wait");
    document.querySelector("#score_value").focus();
    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 {
        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);
        console.log("aborting autodarts game");
      }

      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) {
          console.log("starting new round dude");
          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) => {
        console.log(`${key}: ${oldValue} => ${newValue}`);
        if (newValue === true) {
          console.log("aborting game dude");
          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);
      console.log("we're here my dude");
      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 x01clicker = () => {
        console.info("RichDarts: x01Clicker interval running");
        const x01Button = document.querySelector('[href="/lobbies/new/x01"]');
        if (x01Button) {
          x01Button.click();
          clearInterval(loopInterval);
        }
        if (location.href === "https://play.autodarts.io/lobbies/new/x01") {
          clearInterval(loopInterval);
        }
      };
      setTimeout(() => {
        if (location.href === "https://play.autodarts.io/") {
          loopInterval = setInterval(x01clicker, 50);
        }
      },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", 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;
  }
})();