JackboxDrawer

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

当前为 2021-04-22 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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.4.3
// @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(`
ignore = 0
oldStringify = JSON.stringify
JSON.stringify = function(arg) {
  if (ignore > 0) {
    console.log('ignoring ' + arg.params)
    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()
      }
    },
    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()
      }
    },
    canSubmitNormally: function() {
      return document.getElementsByClassName("button choice-button btn btn-lg")[0].innerText == "SUBMIT"
    },
    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 > 142) {
            alert("Please update the JackboxDrawer Greasemonkey script!")
        } else if (version < 142) {
            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
    }
    
    submit = true
    if (gameID == "champd_up") {
      if (currentGame.canSubmitNormally()) {
        window.eval("ignore = 1")
      } else {
        submit = false
      }
    }
    
    //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')
  
    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.
    
    if (submit)
    	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)