您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Easily download chunk images from wplace.live using multi-point selection and highlighting
当前为
// ==UserScript== // @name Wplace chunk downloader // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description Easily download chunk images from wplace.live using multi-point selection and highlighting // @author NotNotWaldo // @match https://wplace.live/* // @icon https://www.google.com/s2/favicons?sz=64&domain=wplace.live // @license MIT // @run-at document-end // @grant none // ==/UserScript== // Code below, feel free to read in horror // Global vals because I'm way too lazy // These variables are for handling the highlighting feature let isHightlightOn = false; let downloadingState = false; // the downloading state ensures that the highlight wont be also printed when downloading the images of chunk // just a template for chunk img const chunkTemplateUrl = `https://backend.wplace.live/files/s0/tiles/`; (() => { // array for highlighting chunks let highlightedChunksLinksArr = []; // the coords of chunks that are selected by the points you've set let mlChunkCoords = { firstChunk: { x: null, y: null }, secondChunk: { x: null, y: null }, }; const originalFetch = window.fetch; const mlCoordsOrganizer = async (mlCoords) => { let tempMlChunkCoords = structuredClone(mlChunkCoords); // checks if the second point is empty. If yes, it copies the val of first point onto the second if (mlCoords.secondChunk.x == null) { tempMlChunkCoords.secondChunk.x = mlChunkCoords.firstChunk.x; tempMlChunkCoords.secondChunk.y = mlChunkCoords.firstChunk.y; return tempMlChunkCoords; } // making sure that the coords that will be sent would be appropriate // turns the first point to be the topleft corner and the second the bottom right tempMlChunkCoords.firstChunk.x = Math.min( mlCoords.firstChunk.x, mlCoords.secondChunk.x ); tempMlChunkCoords.secondChunk.x = Math.max( mlCoords.firstChunk.x, mlCoords.secondChunk.x ); tempMlChunkCoords.firstChunk.y = Math.min( mlCoords.firstChunk.y, mlCoords.secondChunk.y ); tempMlChunkCoords.secondChunk.y = Math.max( mlCoords.firstChunk.y, mlCoords.secondChunk.y ); return tempMlChunkCoords; }; window.fetch = async (resource, init) => { const url = new URL( typeof resource === "string" ? resource : resource.url || "" ); const res = await originalFetch(resource, init); const x = url.searchParams.get("x"); const y = url.searchParams.get("y"); // This part is for the wonky feature that highlights a chunk, the reason it is wonky is becuase // it has to wait for the app to reload/refetch the img so we can apply effects // Why wait? because the images are flattened in the <canvas> object, and I have no access // (or I dont know how to get access) to the functions that sets the <canvas> obj // leaving me unable to modify or even force to reload/fetch the image // Detect tile fetches if (url.pathname.endsWith(".png")) { const pathParts = url.pathname.split("/"); const chunkX = pathParts[pathParts.length - 2]; const chunkY = pathParts[pathParts.length - 1].replace(".png", ""); // If this chunk matches our selected one if ( isHightlightOn && highlightedChunksLinksArr.includes( chunkTemplateUrl + chunkX + "/" + chunkY + ".png" ) && !downloadingState ) { // Clone so we don't consume the original stream const cloned = res.clone(); const blob = await cloned.blob(); const imgBitmap = await createImageBitmap(blob); // Draw onto an offscreen canvas const canvas = document.createElement("canvas"); canvas.width = imgBitmap.width; canvas.height = imgBitmap.height; const ctx = canvas.getContext("2d"); // Draw original ctx.drawImage(imgBitmap, 0, 0); // Add blue overlay ctx.fillStyle = "rgba(0, 0, 255, 0.2)"; ctx.fillRect(0, 0, canvas.width, canvas.height); const modifiedBlob = await new Promise((r) => canvas.toBlob(r, "image/png") ); return new Response(modifiedBlob, { status: res.status, statusText: res.statusText, headers: res.headers, }); } } if (x && y) { // Setting important infos const pathnames = url.pathname.split("/"); const chunkX = pathnames[pathnames.length - 2]; const chunkY = pathnames[pathnames.length - 1]; const chunkUrl = `https://backend.wplace.live/files/s0/tiles/${chunkX}/${chunkY}.png`; const parent = document .querySelector(".rounded-t-box") ?.querySelector("div"); if (parent) { const coordsParent = parent.querySelector("h2.mt-0\\.5"); const insertAfterThisObj = coordsParent.querySelector( "span.whitespace-nowrap:nth-child(1)" ); const pixelBtns = parent.querySelector(".hide-scrollbar"); let chunkCoords = parent.querySelector(".chunkCoords"); if (!chunkCoords) { chunkCoords = document.createElement("span"); chunkCoords.className = "chunkCoords whitespace-nowrap"; chunkCoords.textContent = ` | Chunk: ${chunkX}, ${chunkY}`; insertAfterThisObj.after(chunkCoords); } // checks if the buttons are already there if (pixelBtns.children.length > 3) { // remove those buttons <<< WILL BREAK IF THE DEVS ADD NEW BUTTONS TO IT LOL, it has, multiple times, because of me lmao. for (let i = 0; i < 4 && pixelBtns.lastElementChild; i++) { pixelBtns.removeChild(pixelBtns.lastElementChild); } } if (pixelBtns) { pixelBtns.style.flexWrap = "wrap"; pixelBtns.style.overflowX = "visible"; // adds "View chunk img" button const viewImgBtn = document.createElement("button"); viewImgBtn.className = "btn btn-soft"; viewImgBtn.textContent = "View chunk img"; viewImgBtn.addEventListener("click", () => { window.open(chunkUrl, "_blank"); }); // adds "Download chunk png" button const dlBtn = document.createElement("button"); dlBtn.className = "btn btn-soft"; dlBtn.textContent = "Download chunk png"; dlBtn.addEventListener("click", async () => { multipleChunksDlUrl(chunkX, chunkY, chunkX, chunkY); }); const downloadChunksText = document.createElement("span"); downloadChunksText.className = "w-full pt-2"; downloadChunksText.textContent = "Multiple chunks downloader:"; const refreshSetPointsInfo = () => { let infoChildren = mlChunkDlCon.querySelectorAll("div"); infoChildren[0].textContent = `1st X: ${mlChunkCoords.firstChunk.x}, Y: ${mlChunkCoords.firstChunk.y}`; infoChildren[1].textContent = `2nd X: ${mlChunkCoords.secondChunk.x}, Y: ${mlChunkCoords.secondChunk.y}`; }; // This are for the multiple chunks downloader let mlChunkDlCon = document.createElement("div"); mlChunkDlCon.className = "flex sm:rounded-b-box w-full pt- sm:mb-2"; mlChunkDlCon.style.gap = "5px"; mlChunkDlCon.style.flexWrap = "wrap"; mlChunkDlCon.style.overflowX = "visible"; // Yes, these "divs" has their classes set to "btn"... a class for <button> objs... I just dont wanna do css let topLeftCoords = document.createElement("div"); topLeftCoords.className = "btn btn-soft"; topLeftCoords.textContent = `1st X: ${mlChunkCoords.firstChunk.x}, Y: ${mlChunkCoords.firstChunk.y}`; let botRightCoords = document.createElement("div"); botRightCoords.className = "btn btn-soft"; botRightCoords.textContent = `2nd X: ${mlChunkCoords.secondChunk.x}, Y: ${mlChunkCoords.secondChunk.y}`; let setPointBtn = document.createElement("button"); setPointBtn.textContent = "Set point"; setPointBtn.className = "btn btn-soft btn-primary"; mlChunkDlCon.appendChild(setPointBtn); mlChunkDlCon.appendChild(topLeftCoords); mlChunkDlCon.appendChild(botRightCoords); setPointBtn.addEventListener("click", async () => { if (mlChunkCoords.firstChunk.x == null) { // yes I'm lazy mlChunkCoords.firstChunk = { x: chunkX, y: chunkY }; } else if (mlChunkCoords.secondChunk.x == null) { mlChunkCoords.secondChunk = { x: chunkX, y: chunkY }; } else { mlChunkCoords.firstChunk = mlChunkCoords.secondChunk; mlChunkCoords.secondChunk = { x: chunkX, y: chunkY }; } if (isHightlightOn) { highlightedChunksLinksArr.length = 0; let organizedCoords = await mlCoordsOrganizer(mlChunkCoords); highlightedChunksLinksArr.push( ...getLinksFromChunkCoords(organizedCoords) ); } refreshSetPointsInfo(); console.log( `current mlcoords-> firstChunk: {X: ${mlChunkCoords.firstChunk.x}, Y: ${mlChunkCoords.firstChunk.y}}, secondChunk: {X: ${mlChunkCoords.secondChunk.x}, Y: ${mlChunkCoords.secondChunk.y}}` ); }); // creates the "download chunks" button let downloadChunksBtn = document.createElement("button"); downloadChunksBtn.className = "btn btn-soft btn-primary"; downloadChunksBtn.textContent = "Download chunks"; downloadChunksBtn.addEventListener("click", async () => { let tempCoords = structuredClone(mlChunkCoords); if ( mlChunkCoords.firstChunk.x == null && mlChunkCoords.secondChunk.x == null ) {return;} if (mlChunkCoords.secondChunk.x == null) { tempCoords.secondChunk.x = mlChunkCoords.firstChunk.x; tempCoords.secondChunk.y = mlChunkCoords.firstChunk.y; } console.log( `downloading chunks: ${tempCoords.firstChunk.x}, ${tempCoords.firstChunk.y} | ${tempCoords.secondChunk.x}, ${tempCoords.secondChunk.y}` ); // making sure the coords are appropriate before passing it let organizedCoords = await mlCoordsOrganizer(mlChunkCoords); multipleChunksDlUrl( organizedCoords.firstChunk.x, organizedCoords.firstChunk.y, organizedCoords.secondChunk.x, organizedCoords.secondChunk.y ); }); let removePointsBtn = document.createElement("button"); removePointsBtn.className = "btn btn-soft"; removePointsBtn.textContent = "Remove points"; removePointsBtn.addEventListener("click", async () => { mlChunkCoords = { firstChunk: { x: null, y: null }, secondChunk: { x: null, y: null }, }; highlightedChunksLinksArr.length = 0; isHightlightOn = false; refreshSetPointsInfo(); let highlightBtn = mlChunkDlCon.querySelector(".highlight-btn"); // I pray that this does not break :) highlightBtn.textContent = "Highlight chunks"; }); let highlightChunksBtn = document.createElement("button"); highlightChunksBtn.className = "highlight-btn btn btn-soft"; if (!isHightlightOn) { highlightChunksBtn.textContent = "Highlight chunks"; } else { highlightChunksBtn.textContent = "Unhighlight chunks"; } highlightChunksBtn.addEventListener("click", async () => { console.log("Trying to hightlight chunks"); if (mlChunkCoords.firstChunk.x == null) return; if (!isHightlightOn) { let organizedCoords = await mlCoordsOrganizer(mlChunkCoords); console.log(Object.keys(organizedCoords)); highlightedChunksLinksArr.push( ...getLinksFromChunkCoords(organizedCoords) ); console.log(`Turned on hightlight`); isHightlightOn = !isHightlightOn; highlightChunksBtn.textContent = "Unhighlight chunks"; } else { highlightedChunksLinksArr.length = 0; console.log(`Turned off highlight`); isHightlightOn = !isHightlightOn; highlightChunksBtn.textContent = "Highlight chunks"; } }); mlChunkDlCon.appendChild(setPointBtn); mlChunkDlCon.appendChild(topLeftCoords); mlChunkDlCon.appendChild(botRightCoords); mlChunkDlCon.appendChild(downloadChunksBtn); mlChunkDlCon.appendChild(removePointsBtn); mlChunkDlCon.appendChild(highlightChunksBtn); // phew, that was a lot... pixelBtns.appendChild(viewImgBtn); pixelBtns.appendChild(dlBtn); pixelBtns.appendChild(downloadChunksText); pixelBtns.appendChild(mlChunkDlCon); } else { console.error("Btn obj not found"); } } else { console.error("Parent element not found"); } } return res; }; })(); const multipleChunksDlUrl = async ( topleftX, topleftY, botRightX, botRightY, safety = true ) => { let linksResultArr = getLinksFromChunkCoords({ firstChunk: { x: topleftX, y: topleftY }, secondChunk: { x: botRightX, y: botRightY }, }); downloadingState = true; let safetyThreshold = 70; let chunkWidth = 1 + Number(botRightX - topleftX); if (linksResultArr.length > safetyThreshold) { if (safety) { console.warn( `You were about to download ${linksResultArr.length} images but was prevented by this precaution. If you intentionally wanted to download that much, you can edit the script and change the "safetyThreshold" variable's value. Good luck.` ); return; } else { console.log("Better pray to God..."); } } stitchAndDownload( linksResultArr, chunkWidth, `ch(${topleftX}, ${topleftY}, ${botRightX}, ${botRightY})` // the name of the stitched file png ); }; let getLinksFromChunkCoords = (chunkCoords) => { console.log("getting the links from chunk coords."); console.log( "tempChunkCoords: " + `First chunk {x: ${chunkCoords.firstChunk.x}, y: ${chunkCoords.firstChunk.y}}, Second chunk {x: ${chunkCoords.secondChunk.x}, y: ${chunkCoords.secondChunk.y}}` ); let topleftX = chunkCoords.firstChunk.x, topleftY = chunkCoords.firstChunk.y, botRightX = chunkCoords.secondChunk.x, botRightY = chunkCoords.secondChunk.y; if (botRightX == null) { botRightX = topleftX; botRightY = topleftY; } let chunkWidth = 1 + Number(botRightX - topleftX); let chunkHeight = 1 + Number(botRightY - topleftY); console.log("chunkWidth: " + chunkWidth); console.log("chunkHeight: " + chunkHeight); let linksArr = []; for (let j = 0; j < chunkHeight; j++) { for (let i = 0; i < chunkWidth; i++) { // I F*CKING HATE JAVASCRIPT. TF YOU MEAN THAT YOU THINK A F*CKING NUMBER IS A STRING! linksArr.push( chunkTemplateUrl + (Number(i) + Number(topleftX)) + "/" + (Number(j) + Number(topleftY)) + ".png" ); } } return linksArr; }; async function stitchAndDownload(images, width, name) { // Creates a temp 1000x1000 image const createBlank = () => { const c = document.createElement("canvas"); c.width = 1000; c.height = 1000; const ctx = c.getContext("2d"); ctx.fillStyle = "rgba(0,0,0,0)"; // or transparent ctx.fillRect(0, 0, c.width, c.height); return new Promise((resolve) => { c.toBlob((blob) => { const img = new Image(); img.src = URL.createObjectURL(blob); img.onload = () => resolve(img); }); }); }; // loads all images with delay const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function loadImagesWithDelay(images, delay = 200) { const loadedImages = []; for (const src of images) { const img = await new Promise((resolve) => { const image = new Image(); image.crossOrigin = "anonymous"; image.onload = () => resolve(image); image.onerror = async () => { console.warn("Failed to load, using blank:", src); const blank = await createBlank(); resolve(blank); }; image.src = src; }); loadedImages.push(img); // cooldown for lesser chance of being rate-limited by the website // wait before starting the next one await sleep(delay); } return loadedImages; } // usage const loadedImages = await loadImagesWithDelay(images, 200); // # of columns/rows const columns = width; const rows = Math.ceil(loadedImages.length / columns); const imgWidth = loadedImages[0].width; const imgHeight = loadedImages[0].height; const canvas = document.createElement("canvas"); canvas.width = imgWidth * columns; canvas.height = imgHeight * rows; const ctx = canvas.getContext("2d"); // this draw images in order: left to right, top to bottom loadedImages.forEach((img, index) => { const x = (index % columns) * imgWidth; const y = Math.floor(index / columns) * imgHeight; ctx.drawImage(img, x, y); }); // Download stitched image canvas.toBlob((blob) => { const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = `${name}_${Date.now()}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); // to enable the highlight to stay after downloading downloadingState = false; }, "image/png"); } // Sry for horrible coding lmao // No one just can win against Javascript that easily...