Gartic Phone Draw Bot

Auto drawing bot!

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==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)
})