JackboxDrawer

Allows custom brush sizes, colors, and even importing images into Jackbox's drawing games!

目前為 2020-10-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         JackboxDrawer
// @description  Allows custom brush sizes, colors, and even importing images into Jackbox's drawing games!
// @namespace    ipodtouch0218/JackboxDrawer
// @version      1.3.1
// @include      *://jackbox.tv/*
// ==/UserScript==

//Catch outgoing messages through stringify and replace drawing data.
//Has to be done through eval to break through GreaseMonkey's sandboxing.
window.eval(`
oldStringify = JSON.stringify;
JSON.stringify = function(arg) {
  if (typeof(ignore) == 'undefined' || ignore > 0) {
    console.log('ignoring ' + arg.params);
    ignore = 1;
    ignore--;
    return oldStringify(arg);
  }
  if (typeof(arg.params) == 'undefined' || arg.params == null) {
    return oldStringify(arg);
  }
  data = arg.params;
  if (typeof(tempvar) == 'undefined' || tempvar === null) {
    //No custom code ready, most likely a vanilla subimssion. Ignore this one.
    return oldStringify(arg);
  }
  eval(tempvar);
  tempvar = null;
  return oldStringify(arg);
}
`);

//Handle games and their differences.
var games = {
  "drawful_1": {
    submitDrawing: function() {
      document.getElementById("drawful-submitdrawing").click();
    },
    isInDrawingMode: function() {
      return !document.getElementsByClassName("state-draw")[0].getAttribute("class").includes("pt-page-off");
    },
    getSketchpad: function() {
      return document.getElementsByClassName("sketchpad")[0];
    }
  },
  "drawful_2": {
    submitDrawing: function() {
      document.getElementById("drawful-submitdrawing").click();
    },
    isInDrawingMode: function() {
      return !document.getElementsByClassName("state-draw")[0].getAttribute("class").includes("pt-page-off");
    },
    getSketchpad: function() {
      return document.getElementsByClassName("sketchpad")[0];
    }
  },
  "bidiots": {
    submitDrawing: function() {
      document.getElementById("auction-submitdrawing").click();
    },
    isInDrawingMode: function() {
      return !document.getElementById("state-draw").getAttribute("class").includes("pt-page-off");
    },
    getSketchpad: function() {
      return document.getElementById("auction-sketchpad");
    }
  },
  "tee_ko": {
    submitDrawing: function() {
      document.getElementById("awshirt-submitdrawing").click();
    },
    isInDrawingMode: function() {
      return !document.getElementById("state-draw").getAttribute("class").includes("pt-page-off");
    },
    getSketchpad: function() {
      return document.getElementsByClassName("awshirt-sketchpad")[0];
    }
  },
  "push_the_button": {
    submitDrawing: function() {
      document.getElementById("submitdrawing").click();
    },
    isInDrawingMode: function() {
      return document.getElementsByClassName("Draw")[0] != null;
    },
    getSketchpad: function() {
      return document.getElementById("fullLayer");
    }
  },
  "trivia_murder_party_1": {
    submitDrawing: function() {
      document.getElementById("enter-single-drawing-submit").click();
    },
    isInDrawingMode: function() {
      return !document.getElementById("state-enter-single-drawing").getAttribute("class").includes("pt-page-off");
    },
    getSketchpad: function() {
      return document.getElementById("sketchpad");
    }
  },
  "champd_up": {
    submitDrawing: function() {
      if (document.getElementsByClassName("button choice-button btn btn-lg")[0].innerText == "SUBMIT") {
        document.getElementsByClassName("button choice-button btn btn-lg")[0].click();
      } else {
        document.getElementById("undoButton").click();
        alert("Submitted!");
      }
    },
    submitName: function() {
      btn = document.getElementsByClassName("button choice-button btn btn-lg")[0];
      if (btn.getAttribute("data-action") == "name") {
        btn.click();
        document.getElementsByClassName("swal2-input")[0].value = "test";
        document.getElementsByClassName("swal2-confirm swal2-styled")[0].click();
      }
    },
    isInDrawingMode: function() {
      return document.getElementsByClassName("Draw")[0] != null;
    },
    getSketchpad: function() {
      return document.getElementsByClassName("sketchpad fullLayer")[0];
    }
  }
}
//Keeps track of the game we're currently playing.
gameID = null;

function updateGame(id) {
  gameID = id;
  if (typeof(socket) !== 'undefined' && socket !== null) {
    //Update the drawing app on what game we're playing.
    socket.send("updategame:" + id); 
  }
}

//Is ran every time the document changes. Useful for finding which game we're currently playing.
var callback = function(mutationsList, observer) {
  if (document.getElementById("page-drawful") !== null) {
    //Drawful 1 and 2 actually share the same ID, but have different graphcis modes.
    //Luckily, the drawing div has "drawful2-page" as a class in Drawful 2.
    if (document.getElementsByClassName("state-draw")[0].getAttribute("class").includes("drawful2-page")) {
      updateGame("drawful_2");
    } else {
      updateGame("drawful_1");
    }
  } else if (document.getElementById("page-auction") !== null) {
    updateGame("bidiots");
  } else if (document.getElementById("page-awshirt") !== null) {
    //Fun fact. Tee KO is actually internally called "awshirt" both on the website and in the game files.
    updateGame("tee_ko");
  } else if (document.getElementsByClassName("Push The Button")[0] != null) {
    //Yes, the class name has spaces.
    updateGame("push_the_button");
  } else if (document.getElementById("page-triviadeath") !== null) {
    updateGame("trivia_murder_party_1");
  } else if (document.getElementsByClassName("worldchamps")[0] != null) {
    updateGame("champd_up");
  }
};

//Initiate the DOM observer to run "callback" every time it changes.
var observer = new MutationObserver(callback);
var targetNode = document.getElementById('content-region');
var config = { attributes: false, childList: true, subtree: true };
observer.observe(targetNode, config);

//Info related to communicating with the Java app.
var socket = null;
var open = false;
var firsttry = false;

//We want to automatically attempt reconnects if the connection is dropped, use setInterval with some
//checks to make sure we don't make multiple connections.
setInterval(function() {
  if (open || socket !== null) {
    return;
  }
  socket = new WebSocket("ws://127.0.0.1:2460");
  
  socket.onopen = function(e) {
    alert("Connection established with JackboxDrawer program.");
    open = true;
    callback(null,null);
  };
  
  socket.onmessage = function(event) {
    
    //Check for the proper version.
    if (event.data.startsWith("version")) {
        var version = event.data.split(":")[1];
        if (version > 131) {
            alert("Please update the JackboxDrawer Greasemonkey script!");
        } else if (version < 131) {
            alert("Please update the JackboxDrawer Java program!\nThe download can be found here: https://github.com/ipodtouch0218/JackboxDrawer/releases");
        }
        return;
    }
    
    //Save incoming code from the websocket in "tempvar". Needs to be eval'd to get through Greasemonkey's sandboxing.
    //We could also use window.wrappedJSObject but this is what I thought of first. Either way, potental security breach right here.
    window.eval("var tempvar = `" + event.data.replace("SUBMITNAME;","") + "`");
    
    //Check to make sure we can actually DRAW right now.
    //If not, even attempting to submit a drawing can easily crash our webpage.
    
    var currentGame = games[gameID];
    if (typeof (currentGame) === 'undefined' || currentGame === null) {
      alert("Game not supported!");
      return;
    }
    
    if (!currentGame.isInDrawingMode()) {
      alert("Cannot submit: Not in drawing mode!");
      window.eval("var tempvar = null;");
      return;
    }
    
    if (gameID == "champd_up") {
      if (event.data.startsWith("SUBMITNAME;")) {
        currentGame.submitName();
        return;
      }
    }
    
    //Find the current sketchpad. We need it for later...
    var sketchpad = currentGame.getSketchpad();
    if (sketchpad === null) {
      //Couldn't find it, we're probably can't draw right now. Somehow, the previous checks failed.
      return;
    }
    
    //Simulate drawing on the sketchpad with mouse events. We can't access the sketchpad's info directly
    //as it's kept track of internally, and the game never attempts to send any data if it's blank.
    var rect = sketchpad.getBoundingClientRect();
    var mouseEvent = document.createEvent('MouseEvents');
  
    console.log(gameID);
    if (gameID == "champd_up") {
      eval("ignore = 1");
    }
  
    mouseEvent.clientX = rect.x + rect.width / 2;
    mouseEvent.clientY = rect.y + rect.height / 2;
    mouseEvent.initEvent("mousedown", true, false);
    sketchpad.dispatchEvent(mouseEvent);
    mouseEvent.clientX += 2;
    mouseEvent.initEvent("mousemove", true, false);
    sketchpad.dispatchEvent(mouseEvent);
    mouseEvent.initEvent("mouseup", true, false);
    sketchpad.dispatchEvent(mouseEvent);
    
    //Submit drawing and get ready to switch-a-roo.
    
    currentGame.submitDrawing();
  };
  
  //Socket died, my dude.
  socket.onclose = function(event) {
    if (!open) return;
    alert("Connection lost with JackboxDrawer program. Retrying...");
    open = false;
    socket = null;
  };
  
  //Socket died PAINFULLY, my dude.
  socket.onerror = function(error) {
    if (!firsttry) {
      alert("Failed to connect to JackboxDrawer. \nI will attempt to reconnect in the background...");
      firsttry = true;
    }
    socket = null;
  }
}, 1000);