Untitled
public
Dec 02, 2023
Never
59
1 // ==UserScript== 2 // @name Garticphone DRAW bot 3 // @namespace http://tampermonkey.net/ 4 // @version 0.1 5 // @license GNU 6 // @description Auto drawing bot! 7 // @author petmshall (peter-marshall5) 8 9 // @match *://garticphone.com/* 10 // @connect garticphone.com 11 // @exclude *://garticphone.com/_next/* 12 13 // @icon https://www.google.com/s2/favicons?domain=garticphone.com 14 15 // @grant unsafeWindow 16 // @grant GM_xmlhttpRequest 17 // @grant GM_log 18 19 // @run-at document-start 20 // ==/UserScript== 21 22 23 24 25 26 function requestText (url) { 27 return fetch(url).then((d) => {return d.text()}) 28 } 29 30 function requestBuffer (url) { 31 return fetch(url).then((d) => {return d.arrayBuffer()}) 32 } 33 34 // Generate decimal to hexadecimal conversion table 35 let hexTable = [] 36 for (let i = 0; i < 256; i++) { 37 let hex = i.toString(16) 38 if (hex.length < 2) { 39 hex = '0' + hex 40 } 41 hexTable.push(hex) 42 } 43 44 function rgbToHex (r, g, b) { 45 return `#${hexTable[r]}${hexTable[g]}${hexTable[b]}` 46 } 47 48 // Check if in a gamemode with animation 49 // Ex. Animation, Background, Solo 50 function isAnimation () { 51 return Boolean(document.getElementsByClassName('note').length) 52 } 53 54 // Proxy to modify client script 55 Node.prototype.appendChild = new Proxy( Node.prototype.appendChild, { 56 async apply (target, thisArg, [element]) { 57 if (element.tagName == "SCRIPT") { 58 if (element.src.indexOf('draw') != -1) { 59 let text = await requestText(element.src) 60 text = editScript(text) 61 let blob = new Blob([text]) 62 element.src = URL.createObjectURL(blob) 63 } 64 } 65 return Reflect.apply( ...arguments ) 66 } 67 }) 68 69 /* stroke configuration note */ 70 /* [toolID, strokeID, [color, 18, 0.6], [x0, y0]. [x1, y1], ..., [xn, yn]] */ 71 72 function editScript (text) { 73 // Find the final draw function 74 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] 75 // find the variable that setData is part of 76 let setDataVar = functionFinalDraw.match(/\w{1,}(?=\.setData)/g)[0] 77 // Expose setData to the script 78 text = text.replace(/\(\(function\(\){if\(!\w{1,}\.disabled\)/, `((function(){;window.setData = ${setDataVar}.setData;if(!${setDataVar}.disabled)`) 79 return text 80 } 81 82 // Stores the current turn in the game 83 let turnNum = null 84 // Stores the websocket that is currently in use 85 let currWs = null 86 87 // Custom websocket class to capture current websocket 88 class customWebSocket extends WebSocket { 89 constructor(...args) { 90 let ws = super(...args) 91 currWs = ws 92 // console.log(ws) 93 ws.addEventListener('message', (e) => { 94 // console.log(e.data) 95 if (e.data && typeof e.data == 'string' && e.data.includes('[')) { 96 let t = JSON.parse(e.data.replace(/[^\[]{0,}/, ''))[2] 97 if (t?.hasOwnProperty('turnNum')) turnNum = t.turnNum 98 } 99 }) 100 return ws 101 } 102 } 103 unsafeWindow.WebSocket = customWebSocket 104 105 let drawEnabled = true 106 107 CanvasRenderingContext2D.prototype.stroke = new Proxy( CanvasRenderingContext2D.prototype.stroke, { 108 async apply (target, thisArg, [element]) { 109 if (drawEnabled) return Reflect.apply( ...arguments ) 110 return 111 } 112 }) 113 114 CanvasRenderingContext2D.prototype.fill = new Proxy( CanvasRenderingContext2D.prototype.fill, { 115 async apply (target, thisArg, [element]) { 116 if (drawEnabled) return Reflect.apply( ...arguments ) 117 return 118 } 119 }) 120 121 CanvasRenderingContext2D.prototype.clearRect = new Proxy( CanvasRenderingContext2D.prototype.clearRect, { 122 async apply (target, thisArg, [element]) { 123 if (drawEnabled) return Reflect.apply( ...arguments ) 124 return 125 } 126 }) 127 128 // Converts an image element to the format that Gartic Phone uses 129 function draw (image, fit='zoom', width=758, height=424, penSize=2) { 130 console.log('[Autodraw] Drawing image') 131 132 let canvas = document.createElement('canvas') 133 canvas.width = width 134 canvas.height = height 135 let ctx = canvas.getContext('2d') 136 ctx.imageSmoothingQuality = 'high' 137 138 // White background 139 ctx.fillStyle = 'white' 140 ctx.fillRect(0, 0, width, height) 141 142 // Calculate the image position and dimensions 143 let imageX = 0 144 let imageY = 0 145 let imageWidth = width 146 let imageHeight = height 147 // Stretch to fit by default (do nothing) 148 if (fit != 'stretch') { 149 const imageAspectRatio = image.width / image.height 150 const canvasAspectRatio = canvas.width / canvas.height 151 if (fit == 'zoom') { 152 // Zoom to fit 153 if (imageAspectRatio > canvasAspectRatio) { 154 imageWidth = image.width * (height / image.height) 155 imageX = (width - imageWidth) / 2 156 } else if (imageAspectRatio < canvasAspectRatio) { 157 imageHeight = image.height * (width / image.width) 158 imageY = (height - imageHeight) / 2 159 } 160 } else { 161 // Shrink to fit 162 if (imageAspectRatio < canvasAspectRatio) { 163 imageWidth = image.width * (height / image.height) 164 imageX = (width - imageWidth) / 2 165 } else if (imageAspectRatio > canvasAspectRatio) { 166 imageHeight = image.height * (width / image.width) 167 imageY = (height - imageHeight) / 2 168 } 169 } 170 } 171 172 // Draw the image on the canvas 173 ctx.drawImage(image, imageX, imageY, imageWidth, imageHeight) 174 175 // Draw the image on the game canvas 176 let gc = document.querySelector('.jsx-187140558') 177 gc.getContext('2d') 178 .drawImage(canvas, 0, 0, gc.width, gc.height) 179 180 // Get RGB data from canvas 181 let data = ctx.getImageData(0, 0, width, 424).data 182 183 let packets = [] 184 let story = [] 185 let strokeId = 0 186 187 if (isAnimation()) { 188 // Gamemodes with animation require different format 189 let pos = 0 190 for (let y = 0; y < height; y++) { 191 for (let x = 0; x < width; x++) { 192 let color = rgbToHex(data[pos], data[pos+1], data[pos+2]) 193 packets.push(`42[2,7,{"t":${turnNum},"d":1,"v":[1,${strokeId},["${color}",${penSize},${data[pos+3]/255}],[${x},${y}]]}]`) 194 story.push([1, strokeId, [color, 2, data[3]/255], [x, y]]) 195 strokeId++ 196 pos += 4 197 } 198 } 199 drawEnabled = false 200 unsafeWindow.setData((function(e){ return story })()) 201 } else { 202 // Other gamemodes 203 let dict = {} 204 let pos = 0 205 for (let y = 0; y < height; y++) { 206 for (let x = 0; x < width; x++) { 207 // let pos = i * 4 208 let color = rgbToHex(data[pos], data[pos+1], data[pos+2]) 209 if (dict[color] == undefined) { 210 // Huge stability improvement 211 // Use unique stroke ID 212 dict[color] = [8, strokeId, [color, data[3]/255], x, y, 1, 1] 213 strokeId++ 214 } else { 215 dict[color].push(x, y, 1, 1) 216 } 217 pos += 4 218 } 219 } 220 221 for (let key in dict) { 222 story.push(dict[key]) 223 let stroke = `42[2,7,{"t":${turnNum},"d":1,"v":`+JSON.stringify(dict[key])+`}]` 224 packets.push(stroke) 225 } 226 drawEnabled = false 227 unsafeWindow.setData((function(e){ return story })()) 228 } 229 230 // Send packets to server 231 drawEnabled = true 232 return sendPackets(packets, story) 233 //.then(() => drawEnabled = true) 234 } 235 236 function sendPackets (packets, story) { 237 console.log('[Autodraw] Sending packets') 238 return new Promise(function(resolve) { 239 let p = 0 240 let sent = 0 241 let pongCount = 2 242 let rateLimitActive = false 243 let pongsRecieved = 0 244 function pongHandler (e) { 245 if (e.data == '3') { 246 pongsRecieved++ 247 console.log('[Autodraw] Pong ' + pongsRecieved + ' / ' + pongCount) 248 if (pongsRecieved >= pongCount) { 249 console.log('[Autodraw] All pongs recieved') 250 currWs.removeEventListener('message', pongHandler) 251 resolve() 252 } 253 } 254 } 255 currWs.addEventListener('message', pongHandler) 256 currWs.send('2') 257 let pingInterval = setInterval(() => { 258 currWs.send('2') 259 pongCount++ 260 }, 10000) 261 function sendChunk () { 262 // Check if websocket is in OPEN state 263 if (currWs.readyState != WebSocket.OPEN) { 264 console.log('[Autodraw] Reconnecting', currWs.readyState) 265 setTimeout(sendChunk, 200) 266 return 267 } 268 269 // Only send data when nothing is buffered 270 if (currWs.bufferedAmount > 0) { 271 // Schedule for next javascript tick 272 setTimeout(sendChunk, 0) 273 return 274 } 275 276 // Limit to 100Kb at a time 277 while (currWs.bufferedAmount < 100000) { 278 currWs.send(packets[p]) 279 280 sent += packets[p].length 281 282 p++ 283 284 if (p >= packets.length) { 285 clearInterval(pingInterval) 286 currWs.send('2') 287 // Exit if the websocket closes 288 console.log('[Autodraw] Finished sending packets') 289 currWs.addEventListener('close', resolve) 290 return 291 } 292 } 293 setTimeout(sendChunk, 0) 294 } 295 sendChunk() 296 }) 297 } 298 299 let doneButton 300 let bottomContainer 301 302 // Fake "Done" button that shows while drawing 303 // Prevents submitting before all packets are sent 304 let fakeButton = document.createElement('button') 305 fakeButton.disabled = true 306 fakeButton.style.display = 'none' 307 fakeButton.innerHTML = '<i class="jsx-3322258600 pencil"></i><strong>Drawing...</strong>' 308 309 function disableButton (e) { 310 if (!doneButton) return e 311 doneButton.style.display = 'none' 312 fakeButton.style.display = '' 313 return e 314 } 315 316 function enableButton (e) { 317 if (!doneButton) return e 318 doneButton.style.display = '' 319 fakeButton.style.display = 'none' 320 return e 321 } 322 323 let currentImage 324 325 function loadImage (objectURL) { 326 // Store an image file 327 console.log('[Autodraw] Selected image') 328 dropPreview.style.display = 'block' 329 dropText.style.display = 'none' 330 currentImage = objectURL 331 dropPreview.src = objectURL 332 } 333 334 function unloadImage () { 335 dropPreview.style.display = 'none' 336 dropText.style.display = 'block' 337 currentImage = null 338 dropPreview.src = 'favicon.ico' 339 } 340 341 function startDrawing () { 342 if (!currentImage) { 343 console.error('[Autodraw] No image loaded') 344 return 345 } 346 if (unsafeWindow.location.href.indexOf('draw') == -1) { 347 console.error('[Autodraw] You are not in the drawing section') 348 return 349 } 350 if (!unsafeWindow.setData) { 351 console.error('[Autodraw] window.setData is missing! (Injector malfunction)') 352 return 353 } 354 disableButton() 355 closeDialog() 356 setTimeout(() => { 357 createImage(currentImage) 358 .then(draw) 359 .then(enableButton) 360 .then(() => { 361 console.log('[Autodraw] Done!') 362 closeDialog() 363 unloadImage() 364 }) 365 }, 500) 366 } 367 368 function pickFile () { 369 return new Promise(function(resolve) { 370 let picker = document.createElement('input') 371 picker.type = 'file' 372 picker.click() 373 picker.oninput = function() { 374 resolve(URL.createObjectURL(picker.files[0])) 375 } 376 }) 377 } 378 379 function createImage (url) { 380 console.log('[Autodraw] Loading image') 381 return new Promise(function(resolve) { 382 let image = document.createElement('img') 383 image.onload = function() { 384 console.log('[Autodraw] Image loaded') 385 resolve(image) 386 } 387 image.src = url 388 }) 389 } 390 391 function injectUI () { 392 // Get the side menu container 393 const redoButton = unsafeWindow.document.querySelector(".tool.redo") 394 if (!redoButton) { 395 return 396 } 397 398 const buttonClass = redoButton.classList[0] 399 if (!buttonClass) { 400 console.log('[Autodraw] Could not find tool button class') 401 } 402 403 const sideMenu = redoButton.parentElement; 404 if (!sideMenu || sideMenu.children.length > 10) { 405 return 406 } 407 sideMenu.style.height = 'unset' 408 409 bottomContainer = document.querySelector('.bottom') 410 411 doneButton = bottomContainer.querySelector('.small') 412 const doneButtonClass = doneButton.classList[0] 413 414 fakeButton.classList = doneButtonClass + ' small' 415 416 // Add the fake button 417 bottomContainer.appendChild(fakeButton) 418 419 // Create the "Add image" button 420 const addImageButton = document.createElement('div') 421 addImageButton.classList = buttonClass + ' tool addimage' 422 addImageButton.style.margin = '6px 0 1px 0' 423 addImageButton.style.backgroundSize = '100%' 424 addImageButton.style.color = '#d16283' 425 426 // Add style 427 const style = document.createElement('style') 428 style.innerText = '.' + buttonClass + `.addimage::after { 429 content: "+"; 430 margin: 2px; 431 flex: 1 1 0%; 432 border-radius: 3px; 433 align-self: stretch; 434 font: 60px Black; 435 transform: translate(0px, -20px); 436 }` 437 unsafeWindow.document.head.appendChild(style) 438 sideMenu.appendChild(addImageButton) 439 440 // Click handler 441 addImageButton.onclick = openDialog 442 } 443 444 function openDialog () { 445 container.style.display = 'flex' 446 setTimeout(() => { 447 container.style.opacity = '1' 448 }, 0) 449 } 450 451 function closeDialog () { 452 container.style.opacity = '0' 453 setTimeout(() => { 454 container.style.display = 'none' 455 }, 200) 456 } 457 458 // Create the UI 459 const container = document.createElement('div') 460 container.style.width = '100%' 461 container.style.height = '100%' 462 container.style.position = 'absolute' 463 container.style.top = '0px' 464 container.style.left = '0px' 465 container.style.background = 'rgba(0,0,0,0.8)' 466 container.style.justifyContent = 'center' 467 container.style.alignItems = 'center' 468 container.style.display = 'none' // Set to "flex" to show 469 container.style.opacity = 0 470 container.style.zIndex = '5' 471 container.classList = 'autodraw-container' 472 const modal = document.createElement('div') 473 modal.style.width = '60%' 474 modal.style.height = '60%' 475 modal.style.background = 'white' 476 modal.style.padding = '25px 30px' 477 modal.style.borderRadius = '12px' 478 modal.style.display = 'flex' 479 modal.style.flexDirection = 'column' 480 modal.style.alignItems = 'center' 481 modal.style.fontFamily = 'Black' 482 container.appendChild(modal) 483 const closeButton = document.createElement('div') 484 closeButton.innerText = '' // "X" symbol 485 closeButton.style.fontFamily = 'ico' // Icon font 486 closeButton.style.fontSize = '24px' 487 closeButton.style.color = 'black' 488 closeButton.style.textAlign = 'right' 489 closeButton.style.margin = '0 0 0 100%' 490 closeButton.style.lineHeight = '5px' // Center in corner 491 closeButton.style.textTransform = 'uppercase' 492 closeButton.style.height = '0px' // Don't offset the next line 493 closeButton.style.cursor = 'pointer' 494 closeButton.onclick = closeDialog 495 modal.appendChild(closeButton) 496 const title = document.createElement('h2') 497 title.classList = 'jsx-143026286' 498 title.innerText = 'Insert Image' 499 title.style.fontFamily = 'Black' 500 title.style.fontSize = '24px' 501 title.style.color = 'rgb(48, 26, 107)' 502 title.style.textAlign = 'center' 503 title.style.lineHeight = '29px' 504 title.style.textTransform = 'uppercase' 505 title.style.display = 'flex' 506 title.style.flexDirection = 'row' 507 modal.appendChild(title) 508 const dropArea = document.createElement('div') 509 dropArea.style.width = '100%' 510 dropArea.style.height = '100%' 511 dropArea.style.alignItems = 'center' 512 dropArea.style.display = 'flex' 513 dropArea.style.justifyContent = 'center' 514 dropArea.style.border = '4px dashed gray' 515 dropArea.style.borderRadius = '17px' 516 dropArea.style.cursor = 'pointer' 517 dropArea.style.overflow = 'hidden' 518 // dropArea.style.margin = '0 0 10px' 519 dropArea.onclick = function() { 520 pickFile().then(loadImage) 521 } 522 dropArea.addEventListener('dragover', (e) => { 523 e.preventDefault() 524 }) 525 dropArea.addEventListener('drop', (e) => { 526 e.preventDefault() 527 loadImage(URL.createObjectURL(e.dataTransfer.files[0])) 528 }) 529 const dropText = document.createElement('div') 530 dropText.style.padding = '20px' 531 dropText.innerText = 'Drag and drop images here or click to choose a file' 532 dropArea.appendChild(dropText) 533 const dropPreview = document.createElement('img') 534 dropPreview.style.display = 'none' 535 dropPreview.style.maxWidth = '95%' 536 dropPreview.style.maxHeight = '95%' 537 dropPreview.style.borderRadius = '6px' 538 dropPreview.style.objectFit = 'cover' 539 dropPreview.src = 'favicon.ico' 540 dropArea.appendChild(dropPreview) 541 modal.appendChild(dropArea) 542 const bottomDiv = document.createElement('div') 543 bottomDiv.style.width = '100%' 544 bottomDiv.style.display = 'flex' 545 bottomDiv.style.flexDirection = 'row' 546 bottomDiv.style.margin = '20px 0 0' 547 bottomDiv.style.justifyContent = 'center' 548 modal.appendChild(bottomDiv) 549 const insertButton = document.createElement('button') 550 insertButton.classList = 'insert-button' 551 insertButton.innerText = 'DRAW IMAGE' 552 insertButton.onclick = function() { 553 startDrawing() 554 } 555 bottomDiv.appendChild(insertButton) 556 const uiStyle = document.createElement('style') 557 uiStyle.innerText = ` 558 .insert-button:hover { 559 background-color: rgb(64, 32, 194); 560 } 561 .insert-button { 562 margin: 0px 8px; 563 cursor: pointer; 564 border: none; 565 background-color: rgb(86, 53, 220); 566 border-radius: 7px; 567 width: 160px; 568 height: 42px; 569 font-family: Black; 570 font-size: 17px; 571 color: rgb(255, 255, 255); 572 text-align: center; 573 text-transform: uppercase; 574 } 575 .autodraw-container { 576 transition: opacity linear 0.2s; 577 }` 578 579 function injectAll () { 580 setInterval(injectUI, 300) 581 582 // Add UI 583 document.body.appendChild(container) 584 document.head.appendChild(uiStyle) 585 } 586 587 unsafeWindow.startDrawing = startDrawing 588 let stateCheck = setInterval(() => { 589 if (unsafeWindow.document.readyState === 'complete') { 590 injectAll() 591 clearInterval(stateCheck) 592 } 593 }, 100);