// ==UserScript==
// @name Gartic Phone Draw Bot
// @namespace http://tampermonkey.net/
// @version 0.1
// @license GNU
// @description Auto drawing bot!
// @author petmshall (peter-marshall5)
// @match *://garticphone.com/*
// @connect garticphone.com
// @exclude *://garticphone.com/_next/*
// @icon https://www.google.com/s2/favicons?domain=garticphone.com
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM_log
// @run-at document-start
// ==/UserScript==
function requestText (url) {
return fetch(url).then((d) => {return d.text()})
}
function requestBuffer (url) {
return fetch(url).then((d) => {return d.arrayBuffer()})
}
// Generate decimal to hexadecimal conversion table
let hexTable = []
for (let i = 0; i < 256; i++) {
let hex = i.toString(16)
if (hex.length < 2) {
hex = '0' + hex
}
hexTable.push(hex)
}
function rgbToHex (r, g, b) {
return `#${hexTable[r]}${hexTable[g]}${hexTable[b]}`
}
// Check if in a gamemode with animation
// Ex. Animation, Background, Solo
function isAnimation () {
return Boolean(document.getElementsByClassName('note').length)
}
// Proxy to modify client script
Node.prototype.appendChild = new Proxy( Node.prototype.appendChild, {
async apply (target, thisArg, [element]) {
if (element.tagName == "SCRIPT") {
if (element.src.indexOf('draw') != -1) {
let text = await requestText(element.src)
text = editScript(text)
let blob = new Blob([text])
element.src = URL.createObjectURL(blob)
}
}
return Reflect.apply( ...arguments )
}
})
/* stroke configuration note */
/* [toolID, strokeID, [color, 18, 0.6], [x0, y0]. [x1, y1], ..., [xn, yn]] */
function editScript (text) {
// Find the final draw function
let functionFinalDraw = text.match(/function\s\w{1,}\(\w{0,}\){[^\{]+{[^\}]{0,}return\[\]\.concat\(Object\(\w{0,}\.*\w{0,}\)\(\w{0,}\),\[\w{0,}\]\)[^\}]{0,}}[^\}]{0,}}/g)[0]
// find the variable that setData is part of
let setDataVar = functionFinalDraw.match(/\w{1,}(?=\.setData)/g)[0]
// Expose setData to the script
text = text.replace(/\(\(function\(\){if\(!\w{1,}\.disabled\)/, `((function(){;window.setData = ${setDataVar}.setData;if(!${setDataVar}.disabled)`)
return text
}
// Stores the current turn in the game
let turnNum = null
// Stores the websocket that is currently in use
let currWs = null
// Custom websocket class to capture current websocket
class customWebSocket extends WebSocket {
constructor(...args) {
let ws = super(...args)
currWs = ws
// console.log(ws)
ws.addEventListener('message', (e) => {
// console.log(e.data)
if (e.data && typeof e.data == 'string' && e.data.includes('[')) {
let t = JSON.parse(e.data.replace(/[^\[]{0,}/, ''))[2]
if (t?.hasOwnProperty('turnNum')) turnNum = t.turnNum
}
})
return ws
}
}
unsafeWindow.WebSocket = customWebSocket
let drawEnabled = true
CanvasRenderingContext2D.prototype.stroke = new Proxy( CanvasRenderingContext2D.prototype.stroke, {
async apply (target, thisArg, [element]) {
if (drawEnabled) return Reflect.apply( ...arguments )
return
}
})
CanvasRenderingContext2D.prototype.fill = new Proxy( CanvasRenderingContext2D.prototype.fill, {
async apply (target, thisArg, [element]) {
if (drawEnabled) return Reflect.apply( ...arguments )
return
}
})
CanvasRenderingContext2D.prototype.clearRect = new Proxy( CanvasRenderingContext2D.prototype.clearRect, {
async apply (target, thisArg, [element]) {
if (drawEnabled) return Reflect.apply( ...arguments )
return
}
})
// Converts an image element to the format that Gartic Phone uses
function draw (image, fit='zoom', width=758, height=424, penSize=2) {
console.log('[Autodraw] Drawing image')
let canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
let ctx = canvas.getContext('2d')
ctx.imageSmoothingQuality = 'high'
// White background
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, width, height)
// Calculate the image position and dimensions
let imageX = 0
let imageY = 0
let imageWidth = width
let imageHeight = height
// Stretch to fit by default (do nothing)
if (fit != 'stretch') {
const imageAspectRatio = image.width / image.height
const canvasAspectRatio = canvas.width / canvas.height
if (fit == 'zoom') {
// Zoom to fit
if (imageAspectRatio > canvasAspectRatio) {
imageWidth = image.width * (height / image.height)
imageX = (width - imageWidth) / 2
} else if (imageAspectRatio < canvasAspectRatio) {
imageHeight = image.height * (width / image.width)
imageY = (height - imageHeight) / 2
}
} else {
// Shrink to fit
if (imageAspectRatio < canvasAspectRatio) {
imageWidth = image.width * (height / image.height)
imageX = (width - imageWidth) / 2
} else if (imageAspectRatio > canvasAspectRatio) {
imageHeight = image.height * (width / image.width)
imageY = (height - imageHeight) / 2
}
}
}
// Draw the image on the canvas
ctx.drawImage(image, imageX, imageY, imageWidth, imageHeight)
// Draw the image on the game canvas
let gc = document.querySelector('.jsx-187140558')
gc.getContext('2d')
.drawImage(canvas, 0, 0, gc.width, gc.height)
// Get RGB data from canvas
let data = ctx.getImageData(0, 0, width, 424).data
let packets = []
let story = []
let strokeId = 0
if (isAnimation()) {
// Gamemodes with animation require different format
let pos = 0
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let color = rgbToHex(data[pos], data[pos+1], data[pos+2])
packets.push(`42[2,7,{"t":${turnNum},"d":1,"v":[1,${strokeId},["${color}",${penSize},${data[pos+3]/255}],[${x},${y}]]}]`)
story.push([1, strokeId, [color, 2, data[3]/255], [x, y]])
strokeId++
pos += 4
}
}
drawEnabled = false
unsafeWindow.setData((function(e){ return story })())
} else {
// Other gamemodes
let dict = {}
let pos = 0
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// let pos = i * 4
let color = rgbToHex(data[pos], data[pos+1], data[pos+2])
if (dict[color] == undefined) {
// Huge stability improvement
// Use unique stroke ID
dict[color] = [8, strokeId, [color, data[3]/255], x, y, 1, 1]
strokeId++
} else {
dict[color].push(x, y, 1, 1)
}
pos += 4
}
}
for (let key in dict) {
story.push(dict[key])
let stroke = `42[2,7,{"t":${turnNum},"d":1,"v":`+JSON.stringify(dict[key])+`}]`
packets.push(stroke)
}
drawEnabled = false
unsafeWindow.setData((function(e){ return story })())
}
// Send packets to server
drawEnabled = true
return sendPackets(packets, story)
//.then(() => drawEnabled = true)
}
function sendPackets (packets, story) {
console.log('[Autodraw] Sending packets')
return new Promise(function(resolve) {
let p = 0
let sent = 0
let pongCount = 2
let rateLimitActive = false
let pongsRecieved = 0
function pongHandler (e) {
if (e.data == '3') {
pongsRecieved++
console.log('[Autodraw] Pong ' + pongsRecieved + ' / ' + pongCount)
if (pongsRecieved >= pongCount) {
console.log('[Autodraw] All pongs recieved')
currWs.removeEventListener('message', pongHandler)
resolve()
}
}
}
currWs.addEventListener('message', pongHandler)
currWs.send('2')
let pingInterval = setInterval(() => {
currWs.send('2')
pongCount++
}, 10000)
function sendChunk () {
// Check if websocket is in OPEN state
if (currWs.readyState != WebSocket.OPEN) {
console.log('[Autodraw] Reconnecting', currWs.readyState)
setTimeout(sendChunk, 200)
return
}
// Only send data when nothing is buffered
if (currWs.bufferedAmount > 0) {
// Schedule for next javascript tick
setTimeout(sendChunk, 0)
return
}
// Limit to 100Kb at a time
while (currWs.bufferedAmount < 100000) {
currWs.send(packets[p])
sent += packets[p].length
p++
if (p >= packets.length) {
clearInterval(pingInterval)
currWs.send('2')
// Exit if the websocket closes
console.log('[Autodraw] Finished sending packets')
currWs.addEventListener('close', resolve)
return
}
}
setTimeout(sendChunk, 0)
}
sendChunk()
})
}
let doneButton
let bottomContainer
// Fake "Done" button that shows while drawing
// Prevents submitting before all packets are sent
let fakeButton = document.createElement('button')
fakeButton.classList = 'jsx-4289504161 small'
fakeButton.disabled = true
fakeButton.style.display = 'none'
fakeButton.innerHTML = '<i class="jsx-3322258600 pencil"></i><strong>Drawing...</strong>'
function disableButton (e) {
if (!doneButton) return e
doneButton.style.display = 'none'
fakeButton.style.display = ''
return e
}
function enableButton (e) {
if (!doneButton) return e
doneButton.style.display = ''
fakeButton.style.display = 'none'
return e
}
let currentImage
function loadImage (objectURL) {
// Store an image file
console.log('[Autodraw] Selected image')
dropPreview.style.display = 'block'
dropText.style.display = 'none'
currentImage = objectURL
dropPreview.src = objectURL
}
function unloadImage () {
dropPreview.style.display = 'none'
dropText.style.display = 'block'
currentImage = null
dropPreview.src = 'favicon.ico'
}
function startDrawing () {
if (!currentImage) {
console.error('[Autodraw] No image loaded')
return
}
if (unsafeWindow.location.href.indexOf('draw') == -1) {
console.error('[Autodraw] You are not in the drawing section')
return
}
if (!unsafeWindow.setData) {
console.error('[Autodraw] window.setData is missing! (Injector malfunction)')
return
}
disableButton()
closeDialog()
setTimeout(() => {
createImage(currentImage)
.then(draw)
.then(enableButton)
.then(() => {
console.log('[Autodraw] Done!')
closeDialog()
unloadImage()
})
}, 500)
}
function pickFile () {
return new Promise(function(resolve) {
let picker = document.createElement('input')
picker.type = 'file'
picker.click()
picker.oninput = function() {
resolve(URL.createObjectURL(picker.files[0]))
}
})
}
function createImage (url) {
console.log('[Autodraw] Loading image')
return new Promise(function(resolve) {
let image = document.createElement('img')
image.onload = function() {
console.log('[Autodraw] Image loaded')
resolve(image)
}
image.src = url
})
}
function injectUI () {
// Get the side menu container
const sideMenu = document.querySelector('.jsx-2643802174.tools > .jsx-2643802174')
if (!sideMenu) {
return
}
if (sideMenu.childElementCount > 10) {
return
}
sideMenu.style.height = 'unset'
doneButton = document.querySelector('button.jsx-4289504161.small')
bottomContainer = document.querySelector('.jsx-2849961842.bottom')
// Add the fake button
bottomContainer.appendChild(fakeButton)
// Create the "Add image" button
const addImageButton = document.createElement('div')
addImageButton.classList = 'jsx-2643802174 tool image'
addImageButton.style.margin = '6px 0 1px 0'
addImageButton.style.backgroundSize = '100%'
addImageButton.style.color = '#d16283'
// Add style
const style = document.createElement('style')
style.innerText = `.jsx-2643802174.tool.image::after {
content: "+";
margin: 2px;
flex: 1 1 0%;
border-radius: 3px;
align-self: stretch;
font: 60px Black;
transform: translate(0px, -20px);
}`
document.head.appendChild(style)
sideMenu.appendChild(addImageButton)
// Click handler
addImageButton.onclick = openDialog
}
function openDialog () {
container.style.display = 'flex'
setTimeout(() => {
container.style.opacity = '1'
}, 0)
}
function closeDialog () {
container.style.opacity = '0'
setTimeout(() => {
container.style.display = 'none'
}, 200)
}
// Create the UI
const container = document.createElement('div')
container.style.width = '100%'
container.style.height = '100%'
container.style.position = 'absolute'
container.style.top = '0px'
container.style.left = '0px'
container.style.background = 'rgba(0,0,0,0.8)'
container.style.justifyContent = 'center'
container.style.alignItems = 'center'
container.style.display = 'none' // Set to "flex" to show
container.style.opacity = 0
container.style.zIndex = '5'
container.classList = 'autodraw-container'
const modal = document.createElement('div')
modal.style.width = '60%'
modal.style.height = '60%'
modal.style.background = 'white'
modal.style.padding = '25px 30px'
modal.style.borderRadius = '12px'
modal.style.display = 'flex'
modal.style.flexDirection = 'column'
modal.style.alignItems = 'center'
modal.style.fontFamily = 'Black'
container.appendChild(modal)
const closeButton = document.createElement('div')
closeButton.innerText = '' // "X" symbol
closeButton.style.fontFamily = 'ico' // Icon font
closeButton.style.fontSize = '24px'
closeButton.style.color = 'black'
closeButton.style.textAlign = 'right'
closeButton.style.margin = '0 0 0 100%'
closeButton.style.lineHeight = '5px' // Center in corner
closeButton.style.textTransform = 'uppercase'
closeButton.style.height = '0px' // Don't offset the next line
closeButton.style.cursor = 'pointer'
closeButton.onclick = closeDialog
modal.appendChild(closeButton)
const title = document.createElement('h2')
title.classList = 'jsx-143026286'
title.innerText = 'Insert Image'
title.style.fontFamily = 'Black'
title.style.fontSize = '24px'
title.style.color = 'rgb(48, 26, 107)'
title.style.textAlign = 'center'
title.style.lineHeight = '29px'
title.style.textTransform = 'uppercase'
title.style.display = 'flex'
title.style.flexDirection = 'row'
modal.appendChild(title)
const dropArea = document.createElement('div')
dropArea.style.width = '100%'
dropArea.style.height = '100%'
dropArea.style.alignItems = 'center'
dropArea.style.display = 'flex'
dropArea.style.justifyContent = 'center'
dropArea.style.border = '4px dashed gray'
dropArea.style.borderRadius = '17px'
dropArea.style.cursor = 'pointer'
dropArea.style.overflow = 'hidden'
// dropArea.style.margin = '0 0 10px'
dropArea.onclick = function() {
pickFile().then(loadImage)
}
dropArea.addEventListener('dragover', (e) => {
e.preventDefault()
})
dropArea.addEventListener('drop', (e) => {
e.preventDefault()
loadImage(URL.createObjectURL(e.dataTransfer.files[0]))
})
const dropText = document.createElement('div')
dropText.style.padding = '20px'
dropText.innerText = 'Drag and drop images here or click to choose a file'
dropArea.appendChild(dropText)
const dropPreview = document.createElement('img')
dropPreview.style.display = 'none'
dropPreview.style.maxWidth = '95%'
dropPreview.style.maxHeight = '95%'
dropPreview.style.borderRadius = '6px'
dropPreview.style.objectFit = 'cover'
dropPreview.src = 'favicon.ico'
dropArea.appendChild(dropPreview)
modal.appendChild(dropArea)
const bottomDiv = document.createElement('div')
bottomDiv.style.width = '100%'
bottomDiv.style.display = 'flex'
bottomDiv.style.flexDirection = 'row'
bottomDiv.style.margin = '20px 0 0'
bottomDiv.style.justifyContent = 'center'
modal.appendChild(bottomDiv)
const insertButton = document.createElement('button')
insertButton.classList = 'insert-button'
insertButton.innerText = 'DRAW IMAGE'
insertButton.onclick = function() {
startDrawing()
}
bottomDiv.appendChild(insertButton)
const uiStyle = document.createElement('style')
uiStyle.innerText = `
.insert-button:hover {
background-color: rgb(64, 32, 194);
}
.insert-button {
margin: 0px 8px;
cursor: pointer;
border: none;
background-color: rgb(86, 53, 220);
border-radius: 7px;
width: 160px;
height: 42px;
font-family: Black;
font-size: 17px;
color: rgb(255, 255, 255);
text-align: center;
text-transform: uppercase;
}
.autodraw-container {
transition: opacity linear 0.2s;
}`
unsafeWindow.startDrawing = startDrawing
document.addEventListener('DOMContentLoaded', () => {
setInterval(injectUI, 300)
// Add UI
document.body.appendChild(container)
document.head.appendChild(uiStyle)
})