From 6face921361f5b5357ed127e1ac3f3ca3696ffd8 Mon Sep 17 00:00:00 2001 From: SwingTheVine Date: Wed, 30 Jul 2025 02:47:03 -0400 Subject: [PATCH] Added image to canvas --- dist/BlueMarble.user.js | 4 +- docs/README.md | 4 +- package-lock.json | 4 +- package.json | 2 +- src/BlueMarble.meta.js | 2 +- src/apiManager.js | 26 ++++++++++- src/main.js | 100 +++++++++++++++++++++++++++++++--------- src/templateManager.js | 25 ++++++---- 8 files changed, 127 insertions(+), 40 deletions(-) diff --git a/dist/BlueMarble.user.js b/dist/BlueMarble.user.js index d09a867..e01bdf7 100644 --- a/dist/BlueMarble.user.js +++ b/dist/BlueMarble.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Blue Marble // @namespace https://github.com/SwingTheVine/ -// @version 0.63.0 +// @version 0.63.20 // @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA. // @author SwingTheVine // @license MPL-2.0 @@ -20,4 +20,4 @@ // Wplace --> https://wplace.live // License --> https://www.mozilla.org/en-US/MPL/2.0/ -(()=>{var t,e,n=t=>{throw TypeError(t)},s=(t,e,s)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),s);t=new WeakSet,e=function(t,e={},n={}){const s=document.createElement(t);this.t?(this.i.appendChild(s),this.o.push(this.i),this.i=s):(this.t=s,this.i=s);for(const[t,n]of Object.entries(e))s[t]=n;for(const[t,e]of Object.entries(n))s[t]=e;return s};var i=GM_info.script.name.toString(),o=GM_info.script.version.toString();function a(t){const e=document.createElement("script");e.setAttribute("bm-s",i),e.setAttribute("bm-q","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement.appendChild(e),e.remove()}a(()=>{const t=document.currentScript,e=(t?.getAttribute("bm-s"),t?.getAttribute("bm-q"),window.fetch);window.fetch=async function(...t){const n=await e.apply(this,t),s=n.clone();if((s.headers.get("content-type")||"").includes("application/json")){let e=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore";s.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:e,jsonData:t},"*")}).catch(t=>{})}return n}}),a(()=>{const t=document.currentScript,e=(t?.getAttribute("bm-s"),t?.getAttribute("bm-q"),maplibregl.Map);maplibregl.Map=function(...t){const n=new e(...t);return window.__bm_interceptedMap,n}});var r=GM_getResourceText("CSS-BM-File");GM_addStyle(r);var h=document.createElement("link");h.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",h.rel="preload",h.as="style",h.onload=function(){this.onload=null,this.rel="stylesheet"},document.head.appendChild(h),new class{constructor(){this.h=null,this.l=null,this.u="#bm-9"}m(t){return this.l=t,this.h=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.u)}),this}p(){return this.h}observe(t,e=!1,n=!1){t.observe(this.l,{childList:e,subtree:n})}};var c=new class{constructor(e,s){var i,o;i=this,(o=t).has(i)?n("Cannot add the same private member more than once"):o instanceof WeakSet?o.add(i):o.set(i,undefined),this.name=e,this.version=s,this.v=null,this.M="bm-e",this.t=null,this.i=null,this.o=[]}C(t){this.v=t}$(){return this.o.length>0&&(this.i=this.o.pop()),this}T(t){t.appendChild(this.t),this.t=null,this.i=null,this.o=[]}S(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"div",{},n)),this}k(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"p",{},n)),this}N(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"small",{},n)),this}I(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"img",{},n)),this}L(n,i={},o=()=>{}){return o(this,s(this,t,e).call(this,"h"+n,{},i)),this}D(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"hr",{},n)),this}H(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"br",{},n)),this}B(n={},i=()=>{}){const o=s(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=s(this,t,e).call(this,"input",{type:"checkbox"},n);return o.insertBefore(a,o.firstChild),this.$(),i(this,o,a),this}R(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"button",{},n)),this}O(n={},i=()=>{}){const o=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${o}`;const a={textContent:"?",className:"bm-t",onclick:()=>{this.P(this.M,o)}};return i(this,s(this,t,e).call(this,"button",a,n)),this}U(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"input",{},n)),this}j(n={},i=()=>{}){const o=n.textContent??"";delete n.textContent;const a=s(this,t,e).call(this,"div"),r=s(this,t,e).call(this,"input",{type:"file",style:"display: none;"},n);this.$();const h=s(this,t,e).call(this,"button",{textContent:o});return this.$(),this.$(),h.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{h.style.maxWidth=`${h.offsetWidth}px`,r.files.length>0?h.textContent=r.files[0].name:h.textContent=o}),i(this,a,r,h),this}_(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"textarea",{},n)),this}P(t,e,n=!1){const s=document.getElementById(t.replace(/^#/,""));s&&(s instanceof HTMLInputElement?s.value=e:n?s.textContent=e:s.innerHTML=e)}G(t,e){let n,s=!1,i=0;t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),t&&e?(e.addEventListener("mousedown",function(o){s=!0,n=o.clientX-t.getBoundingClientRect().left,i=o.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging")}),e.addEventListener("touchstart",function(o){s=!0;const a=o?.touches?.[0];a&&(n=a.clientX-t.getBoundingClientRect().left,i=a.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging"))},{passive:!1}),document.addEventListener("mousemove",function(e){s&&(t.style.left=e.clientX-n+"px",t.style.top=e.clientY-i+"px",t.style.right="")}),document.addEventListener("touchmove",function(e){if(s){const s=e?.touches?.[0];if(!s)return;t.style.left=s.clientX-n+"px",t.style.top=s.clientY-i+"px",e.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",function(){s=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchend",function(){s=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchcancel",function(){s=!1,document.body.style.userSelect="",e.classList.remove("dragging")})):this.W(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`)}X(t){(0,console.info)(`${this.name}: ${t}`),this.P(this.M,"Status: "+t,!0)}W(t){(0,console.error)(`${this.name}: ${t}`),this.P(this.M,"Error: "+t,!0)}}(i,o),l=new class{constructor(){this.Y=null,this.q="bm-r",this.F="div#map canvas.maplibregl-canvas",this.V=window.__bm_interceptedMap,this.Z=this.Z.bind(this),this.A=this.A.bind(this),this.J=this.J.bind(this),this.K=null,this.state=""}tt(){if(document.body.contains(this.Y))return this.Y;document.getElementById(this.q)?.remove();const t=document.querySelector(this.F),e=document.createElement("canvas");return e.id=this.q,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.style?.height,e.style.width=t?.style?.width,e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.Y=e,window.addEventListener("move",this.Z),window.addEventListener("zoom",this.J),window.addEventListener("resize",this.A),this.Y}et(t){this.K=t,this.state="file";const e=URL.createObjectURL(t);window.open(e,"_blank"),setTimeout(()=>URL.revokeObjectURL(e),1e4)}nt(){const t=this.tt(this.q)?.getContext("2d");t&&(t.fillStyle="red",t.fillRect(10,10,100,100))}Z(){this.nt()}J(){this.nt()}A(){const t=document.querySelector(this.F),e=this.tt();e&&t&&(e.style.height=t.style.height,e.style.width=t.style.width,e.height=t.height,e.width=t.width,this.nt())}},d=new class{constructor(){this.st=!1,this.it=[]}ot(t){window.addEventListener("message",e=>{const n=e.data,s=n.jsonData;var i,o;if(n&&"blue-marble"===n.source)switch(n.endpoint.split("?")[0].split("/").filter(t=>t&&isNaN(Number(t))).pop()){case"me":if(s.status&&"2"!=s.status?.toString()[0])return void t.W("The game is down!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(s.level)*Math.pow(30,.65),1/.65)-s.pixelsPainted);t.P("bm-j",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(s.name)}`),t.P("bm-f",`Droplets: ${(new Intl.NumberFormat).format(s.droplets)}`),t.P("bm-a",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const a=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),r=new URLSearchParams(n.endpoint.split("?")[1]),h=[r.get("x"),r.get("y")];if(this.it.length&&(!a.length||!h.length))return void t.W("Coordinates are malformed!\nDid you try clicking the canvas first?");this.it=[...a,...h];const c=(i=a,o=h,[parseInt(i[0])%4*1e3+parseInt(o[0]),parseInt(i[1])%4*1e3+parseInt(o[1])]),l=document.querySelectorAll("span");for(const t of l)if(t.textContent.trim().includes(`${c[0]}, ${c[1]}`)){let e=document.querySelector("#bm-9");const n=`(Tl X: ${a[0]}, Tl Y: ${a[1]}, Px X: ${h[0]}, Px Y: ${h[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-9",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"robots":this.st="false"==s.userscript?.toString().toLowerCase()}})}};c.C(d),c.S({id:"bm-p",style:"top: 10px; right: 75px;"}).S({id:"bm-b"}).S({id:"bm-k"}).$().I({alt:"Blue Marble Icon",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png"}).$().L(1,{textContent:i}).$().$().D().$().S({id:"bm-4"}).k({id:"bm-j",textContent:"Username:"}).$().k({id:"bm-f",textContent:"Droplets:"}).$().k({id:"bm-a",textContent:"Next level in..."}).$().$().D().$().S({id:"bm-3"}).B({id:"bm-g",textContent:"Stealth",checked:!0}).$().O({title:"Waits for the website to make requests, instead of sending requests."}).$().H().$().B({id:"bm-6",textContent:"Possessed",checked:!0}).$().O({title:"Controls the website as if it were possessed."}).$().H().$().S({id:"bm-c"}).R({id:"bm-h",className:"bm-t",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.v?.it;e?.[0]?(t.P("bm-l",e?.[0]||""),t.P("bm-m",e?.[1]||""),t.P("bm-n",e?.[2]||""),t.P("bm-o",e?.[3]||"")):t.W("Coordinates are malformed! Did you try clicking on the canvas first?")}}).$().U({type:"number",id:"bm-l",placeholder:"Tl X",min:0,max:2047,step:1}).$().U({type:"number",id:"bm-m",placeholder:"Tl Y",min:0,max:2047,step:1}).$().U({type:"number",id:"bm-n",placeholder:"Px X",min:0,max:2047,step:1}).$().U({type:"number",id:"bm-o",placeholder:"Px Y",min:0,max:2047,step:1}).$().$().j({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).$().S({id:"bm-0"}).R({id:"bm-i",textContent:"Enable"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2");e?.files[0]?(l.et(e.files[0]),l.nt(),t.X("Drew to canvas!")):t.W("No file selected!")}}).$().R({id:"bm-d",textContent:"Disable"}).$().$()._({id:c.M,placeholder:`Status: Sleeping...\nVersion: ${o}`,readOnly:!0}).$().S({id:"bm-1"}).S().R({id:"bm-7",className:"bm-t",textContent:"✈"}).$().R({id:"bm-8",className:"bm-t",innerHTML:''}).$().R({id:"bm-5",className:"bm-t",innerHTML:"🖌"}).$().$().N({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).$().$().$().T(document.body),c.G("#bm-p","#bm-k"),d.ot(c),function(...t){(0,console.log)(...t)}(`%c${i}%c (${o}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file +(()=>{var t,e,n=t=>{throw TypeError(t)},s=(t,e,s)=>(((t,e)=>{e.has(t)||n("Cannot access private method")})(t,e),s);t=new WeakSet,e=function(t,e={},n={}){const s=document.createElement(t);this.t?(this.i.appendChild(s),this.o.push(this.i),this.i=s):(this.t=s,this.i=s);for(const[t,n]of Object.entries(e))s[t]=n;for(const[t,e]of Object.entries(n))s[t]=e;return s};var i=GM_info.script.name.toString(),o=GM_info.script.version.toString();function a(t){const e=document.createElement("script");e.setAttribute("bm-s",i),e.setAttribute("bm-q","color: cornflowerblue;"),e.textContent=`(${t})();`,document.documentElement.appendChild(e),e.remove()}a(()=>{const t=document.currentScript,e=t?.getAttribute("bm-s")||"Blue Marble",n=t?.getAttribute("bm-q")||"",s=new Map;window.addEventListener("message",t=>{const{source:i,endpoint:o,blobID:a,blobData:r,blink:c}=t.data,l=Date.now()-c;console.groupCollapsed(`%c${e}%c: ${s.size} Recieved IMAGE message about blob "${a}"`,n,""),console.log(`Blob fetch took %c${String(Math.floor(l/6e4)).padStart(2,"0")}:${String(Math.floor(l/1e3)%60).padStart(2,"0")}.${String(l%1e3).padStart(3,"0")}%c MM:SS.mmm`,n,""),console.log(s),console.groupEnd(),"blue-marble"==i&&a&&r&&!o&&s.get(a)(r)});const i=window.fetch;window.fetch=async function(...t){const o=await i.apply(this,t),a=o.clone(),r=(t[0]instanceof Request?t[0]?.url:t[0])||"ignore",c=a.headers.get("content-type")||"";if(c.includes("application/json"))console.log(`%c${e}%c: Sending JSON message about endpoint "${r}"`,n,""),a.json().then(t=>{window.postMessage({source:"blue-marble",endpoint:r,jsonData:t},"*")}).catch(t=>{console.error(`%c${e}%c: Failed to parse JSON: `,n,"",t)});else if(c.includes("image/")&&!r.includes("openfreemap")){const t=Date.now(),i=await a.blob();return console.log(`%c${e}%c: ${s.size} Sending IMAGE message about endpoint "${r}"`,n,""),new Promise(o=>{const c=crypto.randomUUID();s.set(c,t=>{o(new Response(t,{headers:a.headers,status:a.status,statusText:a.statusText})),s.delete(c),console.log(`%c${e}%c: ${s.size} Processed blob "${c}"`,n,"")}),window.postMessage({source:"blue-marble",endpoint:r,blobID:c,blobData:i,blink:t})}).catch(i=>{const o=Date.now();console.error(`%c${e}%c: Failed to Promise blob!`,n,""),console.groupCollapsed(`%c${e}%c: Details of failed blob Promise:`,n,""),console.log(`Endpoint: ${r}\nThere are ${s.size} blobs processing...\nBlink: ${t.toLocaleString()}\nTime Since Blink: ${String(Math.floor(o/6e4)).padStart(2,"0")}:${String(Math.floor(o/1e3)%60).padStart(2,"0")}.${String(o%1e3).padStart(3,"0")} MM:SS.mmm`),console.error("Exception stack:",i),console.groupEnd()})}return o}}),a(()=>{const t=document.currentScript,e=(t?.getAttribute("bm-s"),t?.getAttribute("bm-q"),maplibregl.Map);maplibregl.Map=function(...t){const n=new e(...t);return window.__bm_interceptedMap,n}});var r=GM_getResourceText("CSS-BM-File");GM_addStyle(r);var c=document.createElement("link");c.href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap",c.rel="preload",c.as="style",c.onload=function(){this.onload=null,this.rel="stylesheet"},document.head.appendChild(c),new class{constructor(){this.l=null,this.h=null,this.u="#bm-9"}m(t){return this.h=t,this.l=new MutationObserver(t=>{for(const e of t)for(const t of e.addedNodes)t instanceof HTMLElement&&t.matches?.(this.u)}),this}p(){return this.l}observe(t,e=!1,n=!1){t.observe(this.h,{childList:e,subtree:n})}};var l=new class{constructor(e,s){var i,o;i=this,(o=t).has(i)?n("Cannot add the same private member more than once"):o instanceof WeakSet?o.add(i):o.set(i,undefined),this.name=e,this.version=s,this.v=null,this.$="bm-e",this.t=null,this.i=null,this.o=[]}M(t){this.v=t}S(){return this.o.length>0&&(this.i=this.o.pop()),this}k(t){t.appendChild(this.t),this.t=null,this.i=null,this.o=[]}C(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"div",{},n)),this}T(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"p",{},n)),this}D(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"small",{},n)),this}I(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"img",{},n)),this}N(n,i={},o=()=>{}){return o(this,s(this,t,e).call(this,"h"+n,{},i)),this}B(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"hr",{},n)),this}L(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"br",{},n)),this}H(n={},i=()=>{}){const o=s(this,t,e).call(this,"label",{textContent:n.textContent??""});delete n.textContent;const a=s(this,t,e).call(this,"input",{type:"checkbox"},n);return o.insertBefore(a,o.firstChild),this.S(),i(this,o,a),this}P(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"button",{},n)),this}R(n={},i=()=>{}){const o=n.title??n.textContent??"Help: No info";delete n.textContent,n.title=`Help: ${o}`;const a={textContent:"?",className:"bm-t",onclick:()=>{this.O(this.$,o)}};return i(this,s(this,t,e).call(this,"button",a,n)),this}G(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"input",{},n)),this}j(n={},i=()=>{}){const o=n.textContent??"";delete n.textContent;const a=s(this,t,e).call(this,"div"),r=s(this,t,e).call(this,"input",{type:"file",style:"display: none;"},n);this.S();const c=s(this,t,e).call(this,"button",{textContent:o});return this.S(),this.S(),c.addEventListener("click",()=>{r.click()}),r.addEventListener("change",()=>{c.style.maxWidth=`${c.offsetWidth}px`,r.files.length>0?c.textContent=r.files[0].name:c.textContent=o}),i(this,a,r,c),this}U(n={},i=()=>{}){return i(this,s(this,t,e).call(this,"textarea",{},n)),this}O(t,e,n=!1){const s=document.getElementById(t.replace(/^#/,""));s&&(s instanceof HTMLInputElement?s.value=e:n?s.textContent=e:s.innerHTML=e)}F(t,e){let n,s=!1,i=0;t=document.querySelector("#"==t?.[0]?t:"#"+t),e=document.querySelector("#"==e?.[0]?e:"#"+e),t&&e?(e.addEventListener("mousedown",function(o){s=!0,n=o.clientX-t.getBoundingClientRect().left,i=o.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging")}),e.addEventListener("touchstart",function(o){s=!0;const a=o?.touches?.[0];a&&(n=a.clientX-t.getBoundingClientRect().left,i=a.clientY-t.getBoundingClientRect().top,document.body.style.userSelect="none",e.classList.add("dragging"))},{passive:!1}),document.addEventListener("mousemove",function(e){s&&(t.style.left=e.clientX-n+"px",t.style.top=e.clientY-i+"px",t.style.right="")}),document.addEventListener("touchmove",function(e){if(s){const s=e?.touches?.[0];if(!s)return;t.style.left=s.clientX-n+"px",t.style.top=s.clientY-i+"px",e.preventDefault()}},{passive:!1}),document.addEventListener("mouseup",function(){s=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchend",function(){s=!1,document.body.style.userSelect="",e.classList.remove("dragging")}),document.addEventListener("touchcancel",function(){s=!1,document.body.style.userSelect="",e.classList.remove("dragging")})):this._(`Can not drag! ${t?"":"moveMe"} ${t||e?"":"and "}${e?"":"iMoveThings "}was not found!`)}W(t){(0,console.info)(`${this.name}: ${t}`),this.O(this.$,"Status: "+t,!0)}_(t){(0,console.error)(`${this.name}: ${t}`),this.O(this.$,"Error: "+t,!0)}}(i,o),h=new class{constructor(){this.X=null,this.Y="bm-r",this.q="div#map canvas.maplibregl-canvas",this.A=window.__bm_interceptedMap,this.V=this.V.bind(this),this.J=this.J.bind(this),this.Z=this.Z.bind(this),this.K=null,this.tt=""}et(){if(document.body.contains(this.X))return this.X;document.getElementById(this.Y)?.remove();const t=document.querySelector(this.q),e=document.createElement("canvas");return e.id=this.Y,e.className="maplibregl-canvas",e.style.position="absolute",e.style.top="0",e.style.left="0",e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),e.style.zIndex="8999",e.style.pointerEvents="none",t?.parentElement?.appendChild(e),this.X=e,window.addEventListener("move",this.V),window.addEventListener("zoom",this.Z),window.addEventListener("resize",this.J),this.X}nt(t){this.K=t,this.tt="file";const e=URL.createObjectURL(t);window.open(e,"_blank"),setTimeout(()=>URL.revokeObjectURL(e),1e4)}st(){const t=this.et(this.Y)?.getContext("2d");t&&(t.fillStyle="red",t.fillRect(10,10,100,100))}it(){if("file"==this.tt)return this.K}V(){this.st(),console.log(this.A)}Z(){this.st()}J(){const t=document.querySelector(this.q),e=this.et();e&&t&&(e.style.height=t?.clientHeight*(window.devicePixelRatio||1)+"px",e.style.width=t?.clientWidth*(window.devicePixelRatio||1)+"px",e.height=t?.clientHeight*(window.devicePixelRatio||1),e.width=t?.clientWidth*(window.devicePixelRatio||1),this.st())}},d=new class{constructor(t){this.ot=t,this.rt=!1,this.ct=[]}lt(t){window.addEventListener("message",e=>{const n=e.data,s=n.jsonData;if(!n||"blue-marble"!==n.source)return;if(!n.endpoint)return;const i=n.endpoint?.split("?")[0].split("/").filter(t=>t&&isNaN(Number(t))).filter(t=>t&&!t.includes(".")).pop();switch(console.log(`%cBlue Marble%c: Recieved message about "${i}"`,"color: cornflowerblue;",""),i){case"me":if(s.status&&"2"!=s.status?.toString()[0])return void t._("The game is down!\nCould not fetch userdata.");const e=Math.ceil(Math.pow(Math.floor(s.level)*Math.pow(30,.65),1/.65)-s.pixelsPainted);t.O("bm-j",`Username: ${function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}(s.name)}`),t.O("bm-f",`Droplets: ${(new Intl.NumberFormat).format(s.droplets)}`),t.O("bm-a",`Next level in ${(new Intl.NumberFormat).format(e)} pixel${1==e?"":"s"}`);break;case"pixel":const i=n.endpoint.split("?")[0].split("/").filter(t=>t&&!isNaN(Number(t))),r=new URLSearchParams(n.endpoint.split("?")[1]),c=[r.get("x"),r.get("y")];if(this.ct.length&&(!i.length||!c.length))return void t._("Coordinates are malformed!\nDid you try clicking the canvas first?");this.ct=[...i,...c];const l=(o=i,a=c,[parseInt(o[0])%4*1e3+parseInt(a[0]),parseInt(o[1])%4*1e3+parseInt(a[1])]),h=document.querySelectorAll("span");for(const t of h)if(t.textContent.trim().includes(`${l[0]}, ${l[1]}`)){let e=document.querySelector("#bm-9");const n=`(Tl X: ${i[0]}, Tl Y: ${i[1]}, Px X: ${c[0]}, Px Y: ${c[1]})`;e?e.textContent=n:(e=document.createElement("span"),e.id="bm-9",e.textContent=n,e.style="margin-left: calc(var(--spacing)*3); font-size: small;",t.parentNode.parentNode.parentNode.insertAdjacentElement("afterend",e))}break;case"tiles":const d=n.blobID,u=(n.blobData,this.ot.it());window.postMessage({source:"blue-marble",blobID:d,blobData:u,blink:n.blink});break;case"robots":this.rt="false"==s.userscript?.toString().toLowerCase();break}var o,a})}}(h);l.M(d),l.C({id:"bm-p",style:"top: 10px; right: 75px;"}).C({id:"bm-b"}).C({id:"bm-k"}).S().I({alt:"Blue Marble Icon",src:"https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png"}).S().N(1,{textContent:i}).S().S().B().S().C({id:"bm-4"}).T({id:"bm-j",textContent:"Username:"}).S().T({id:"bm-f",textContent:"Droplets:"}).S().T({id:"bm-a",textContent:"Next level in..."}).S().S().B().S().C({id:"bm-3"}).H({id:"bm-g",textContent:"Stealth",checked:!0}).S().R({title:"Waits for the website to make requests, instead of sending requests."}).S().L().S().H({id:"bm-6",textContent:"Possessed",checked:!0}).S().R({title:"Controls the website as if it were possessed."}).S().L().S().C({id:"bm-c"}).P({id:"bm-h",className:"bm-t",style:"margin-top: 0;",innerHTML:''},(t,e)=>{e.onclick=()=>{const e=t.v?.ct;e?.[0]?(t.O("bm-l",e?.[0]||""),t.O("bm-m",e?.[1]||""),t.O("bm-n",e?.[2]||""),t.O("bm-o",e?.[3]||"")):t._("Coordinates are malformed! Did you try clicking on the canvas first?")}}).S().G({type:"number",id:"bm-l",placeholder:"Tl X",min:0,max:2047,step:1}).S().G({type:"number",id:"bm-m",placeholder:"Tl Y",min:0,max:2047,step:1}).S().G({type:"number",id:"bm-n",placeholder:"Px X",min:0,max:2047,step:1}).S().G({type:"number",id:"bm-o",placeholder:"Px Y",min:0,max:2047,step:1}).S().S().j({id:"bm-2",textContent:"Upload Template",accept:"image/png, image/jpeg, image/webp, image/bmp, image/gif"}).S().C({id:"bm-0"}).P({id:"bm-i",textContent:"Enable"},(t,e)=>{e.onclick=()=>{const e=document.querySelector("#bm-2");e?.files[0]?(h.nt(e.files[0]),h.st(),console.log(h.X),t.W("Drew to canvas!")):t._("No file selected!")}}).S().P({id:"bm-d",textContent:"Disable"}).S().S().U({id:l.$,placeholder:`Status: Sleeping...\nVersion: ${o}`,readOnly:!0}).S().C({id:"bm-1"}).C().P({id:"bm-7",className:"bm-t",textContent:"✈"}).S().P({id:"bm-8",className:"bm-t",innerHTML:''}).S().P({id:"bm-5",className:"bm-t",innerHTML:"🖌"}).S().S().D({textContent:"Made by SwingTheVine",style:"margin-top: auto;"}).S().S().S().k(document.body),l.F("#bm-p","#bm-k"),d.lt(l),function(...t){(0,console.log)(...t)}(`%c${i}%c (${o}) userscript has loaded!`,"color: cornflowerblue;","")})(); \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index fe3a29f..b482d0a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,8 +34,8 @@ Latest Release Software License: MPL-2.0 Contact Me -WakaTime -Total Patches +WakaTime +Total Patches Total Lines of Code Total Comments Compression diff --git a/package-lock.json b/package-lock.json index 56996e7..668bc78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wplace-bluemarble", - "version": "0.60.14", + "version": "0.63.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wplace-bluemarble", - "version": "0.60.14", + "version": "0.63.20", "devDependencies": { "esbuild": "^0.25.0", "terser": "^5.43.1" diff --git a/package.json b/package.json index 35ba23d..348436e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wplace-bluemarble", - "version": "0.63.0", + "version": "0.63.20", "type": "module", "scripts": { "build": "node build/build.js", diff --git a/src/BlueMarble.meta.js b/src/BlueMarble.meta.js index 29100ac..bb45434 100644 --- a/src/BlueMarble.meta.js +++ b/src/BlueMarble.meta.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Blue Marble // @namespace https://github.com/SwingTheVine/ -// @version 0.63.0 +// @version 0.63.20 // @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA. // @author SwingTheVine // @license MPL-2.0 diff --git a/src/apiManager.js b/src/apiManager.js index b776ac0..a721553 100644 --- a/src/apiManager.js +++ b/src/apiManager.js @@ -3,14 +3,17 @@ * @since 0.11.1 */ +import TemplateManager from "./templateManager.js"; import { escapeHTML, serverTPtoDisplayTP } from "./utils.js"; export default class ApiManager { /** Constructor for ApiManager class + * @param {TemplateManager} templateManager * @since 0.11.34 */ - constructor() { + constructor(templateManager) { + this.templateManager = templateManager; this.disableAll = false; // Should the entire userscript be disabled? this.coordsTilePixel = []; // Contains the last detected tile/pixel coordinate pair requested } @@ -33,9 +36,13 @@ export default class ApiManager { // Kills itself if the message was not intended for Blue Marble if (!(data && data['source'] === 'blue-marble')) {return;} + // Kills itself if the message has no endpoint (intended for Blue Marble, but not this function) + if (!data['endpoint']) {return;} + // Trims endpoint to the second to last non-number, non-null directoy. // E.g. "wplace.live/api/pixel/0/0?payload" -> "pixel" - const endpointText = data['endpoint'].split('?')[0].split('/').filter(s => s && isNaN(Number(s))).pop(); + // E.g. "wplace.live/api/files/s0/tiles/0/0/0.png" -> "tiles" + const endpointText = data['endpoint']?.split('?')[0].split('/').filter(s => s && isNaN(Number(s))).filter(s => s && !s.includes('.')).pop(); console.log(`%cBlue Marble%c: Recieved message about "${endpointText}"`, 'color: cornflowerblue;', ''); @@ -97,6 +104,21 @@ export default class ApiManager { } } break; + + case 'tiles': + + const blobUUID = data['blobID']; + const blobData = data['blobData']; + + const gojo = this.templateManager.drawGojo(); + + window.postMessage({ + source: 'blue-marble', + blobID: blobUUID, + blobData: gojo, + blink: data['blink'] + }); + break; case 'robots': // Request to retrieve what script types are allowed this.disableAll = dataJSON['userscript']?.toString().toLowerCase() == 'false'; // Disables Blue Marble if site owner wants userscripts disabled diff --git a/src/main.js b/src/main.js index 436fb7c..5268ad6 100644 --- a/src/main.js +++ b/src/main.js @@ -35,7 +35,25 @@ inject(() => { const script = document.currentScript; // Gets the current script HTML Script Element const name = script?.getAttribute('bm-name') || 'Blue Marble'; // Gets the name value that was passed in. Defaults to "Blue Marble" if nothing was found const consoleStyle = script?.getAttribute('bm-cStyle') || ''; // Gets the console style value that was passed in. Defaults to no styling if nothing was found - + const fetchedBlobQueue = new Map(); // Blobs being processed + + window.addEventListener('message', (event) => { + const { source, endpoint, blobID, blobData, blink } = event.data; + + const elapsed = Date.now() - blink; + + // Since this code does not run in the userscript, we can't use consoleLog(). + console.groupCollapsed(`%c${name}%c: ${fetchedBlobQueue.size} Recieved IMAGE message about blob "${blobID}"`, consoleStyle, ''); + console.log(`Blob fetch took %c${String(Math.floor(elapsed/60000)).padStart(2,'0')}:${String(Math.floor(elapsed/1000) % 60).padStart(2,'0')}.${String(elapsed % 1000).padStart(3,'0')}%c MM:SS.mmm`, consoleStyle, ''); + console.log(fetchedBlobQueue); + console.groupEnd(); + + // The modified blob won't have an endpoint, so we ignore any message without one. + if ((source == 'blue-marble') && !!blobID && !!blobData && !endpoint) { + fetchedBlobQueue.get(blobID)(blobData); + } + }); + // Spys on "spontaneous" fetch requests made by the client const originalFetch = window.fetch; // Saves a copy of the original fetch @@ -45,12 +63,13 @@ inject(() => { const response = await originalFetch.apply(this, args); // Sends a fetch const cloned = response.clone(); // Makes a copy of the response + // Retrieves the endpoint name. Unknown endpoint = "ignore" + const endpointName = ((args[0] instanceof Request) ? args[0]?.url : args[0]) || 'ignore'; + // Check Content-Type to only process JSON const contentType = cloned.headers.get('content-type') || ''; if (contentType.includes('application/json')) { - // Retrieves the endpoint name. Unknown endpoint = "ignore" - let endpointName = ((args[0] instanceof Request) ? args[0]?.url : args[0]) || 'ignore'; // Since this code does not run in the userscript, we can't use consoleLog(). console.log(`%c${name}%c: Sending JSON message about endpoint "${endpointName}"`, consoleStyle, ''); @@ -67,30 +86,67 @@ inject(() => { .catch(err => { console.error(`%c${name}%c: Failed to parse JSON: `, consoleStyle, '', err); }); + } else if (contentType.includes('image/') && (!endpointName.includes('openfreemap'))) { + // Fetch custom for all images but opensourcemap + + const blink = Date.now(); // Current time + + const blob = await cloned.blob(); // The original blob + + // Since this code does not run in the userscript, we can't use consoleLog(). + console.log(`%c${name}%c: ${fetchedBlobQueue.size} Sending IMAGE message about endpoint "${endpointName}"`, consoleStyle, ''); + + // Returns the manipulated blob + return new Promise((resolve) => { + const blobUUID = crypto.randomUUID(); // Generates a random UUID + + // Store the blob while we wait for processing + fetchedBlobQueue.set(blobUUID, (blobProcessed) => { + // The response that triggers when the blob is finished processing + + // Creates a new response + resolve(new Response(blobProcessed, { + headers: cloned.headers, + status: cloned.status, + statusText: cloned.statusText + })); + + // Removes the processed blob from the queue + fetchedBlobQueue.delete(blobUUID); + + // Since this code does not run in the userscript, we can't use consoleLog(). + console.log(`%c${name}%c: ${fetchedBlobQueue.size} Processed blob "${blobUUID}"`, consoleStyle, ''); + }); + + window.postMessage({ + source: 'blue-marble', + endpoint: endpointName, + blobID: blobUUID, + blobData: blob, + blink: blink + }); + }).catch(exception => { + const elapsed = Date.now(); + console.error(`%c${name}%c: Failed to Promise blob!`, consoleStyle, ''); + console.groupCollapsed(`%c${name}%c: Details of failed blob Promise:`, consoleStyle, ''); + console.log(`Endpoint: ${endpointName}\nThere are ${fetchedBlobQueue.size} blobs processing...\nBlink: ${blink.toLocaleString()}\nTime Since Blink: ${String(Math.floor(elapsed/60000)).padStart(2,'0')}:${String(Math.floor(elapsed/1000) % 60).padStart(2,'0')}.${String(elapsed % 1000).padStart(3,'0')} MM:SS.mmm`); + console.error(`Exception stack:`, exception); + console.groupEnd(); + }); + + // cloned.blob().then(blob => { + // window.postMessage({ + // source: 'blue-marble', + // endpoint: endpointName, + // blobData: blob + // }, '*'); + // }); } return response; // Returns the original response }; }); -/** What code to execute instantly in the client (webpage) to spy on the canvas. - * This code will execute outside of TamperMonkey's sandbox. - * @since 0.60.14 - */ -inject(() => { - - const script = document.currentScript; // Gets the current script HTML Script Element - const name = script?.getAttribute('bm-name') || 'Blue Marble'; // Gets the name value that was passed in. Defaults to "Blue Marble" if nothing was found - const consoleStyle = script?.getAttribute('bm-cStyle') || ''; // Gets the console style value that was passed in. Defaults to no styling if nothing was found - - const originalMap = maplibregl.Map; - maplibregl.Map = function (...args) { - const instance = new originalMap(...args); - window.__bm_interceptedMap; - return instance; - } -}); - // Imports the CSS file from dist folder on github const cssOverlay = GM_getResourceText("CSS-BM-File"); GM_addStyle(cssOverlay); @@ -110,7 +166,7 @@ document.head.appendChild(stylesheetLink); const observers = new Observers(); // Constructs a new Observers object const overlay = new Overlay(name, version); // Constructs a new Overlay object const templateManager = new TemplateManager(); // Constructs a new TemplateManager object -const apiManager = new ApiManager(); // Constructs a new ApiManager object +const apiManager = new ApiManager(templateManager); // Constructs a new ApiManager object overlay.setApiManager(apiManager); // Sets the API manager diff --git a/src/templateManager.js b/src/templateManager.js index a757dae..108e799 100644 --- a/src/templateManager.js +++ b/src/templateManager.js @@ -15,7 +15,7 @@ export default class TemplateManager { this.onResize = this.onResize.bind(this); // Binds the handler for `resize` to this class instance's function this.onZoom = this.onZoom.bind(this); // Binds the handler for `zoom` to this class instance's function this.template = null; // The template image. - this.state = ''; // The state of the template ('blob', 'proccessing', 'template', etc.) + this.templateState = ''; // The state of the template ('blob', 'proccessing', 'template', etc.) } /** Retrieves the pixel art canvas. @@ -41,8 +41,10 @@ export default class TemplateManager { canvasTemplateNew.style.position = 'absolute'; canvasTemplateNew.style.top = '0'; canvasTemplateNew.style.left = '0'; - canvasTemplateNew.style.height = canvasMain?.style?.height; - canvasTemplateNew.style.width = canvasMain?.style?.width; + canvasTemplateNew.style.height = `${canvasMain?.clientHeight * (window.devicePixelRatio || 1)}px`; + canvasTemplateNew.style.width = `${canvasMain?.clientWidth * (window.devicePixelRatio || 1)}px`; + canvasTemplateNew.height = canvasMain?.clientHeight * (window.devicePixelRatio || 1); + canvasTemplateNew.width = canvasMain?.clientWidth * (window.devicePixelRatio || 1); canvasTemplateNew.style.zIndex = '8999'; canvasTemplateNew.style.pointerEvents = 'none'; canvasMain?.parentElement?.appendChild(canvasTemplateNew); // Append the newCanvas as a child of the parent of the main canvas @@ -62,7 +64,7 @@ export default class TemplateManager { setTemplateImage(file) { this.template = file; - this.state = 'file'; + this.templateState = 'file'; const url = URL.createObjectURL(file); // Creates a blob URL window.open(url, '_blank'); // Opens a new tab with blob @@ -82,6 +84,13 @@ export default class TemplateManager { } } + drawGojo() { + + if (this.templateState != 'file') {return;} + + return this.template; + } + /** What to do to our canvas when the canvas is panned. * @since 0.60.10 */ @@ -104,10 +113,10 @@ export default class TemplateManager { const canvasMain = document.querySelector(this.canvasMainID); const canvasTemplate = this.getCanvas(); if (!canvasTemplate || !canvasMain) {return;} - canvasTemplate.style.height = canvasMain.style.height; - canvasTemplate.style.width = canvasMain.style.width; - canvasTemplate.height = canvasMain.height; - canvasTemplate.width = canvasMain.width; + canvasTemplate.style.height = `${canvasMain?.clientHeight * (window.devicePixelRatio || 1)}px`; + canvasTemplate.style.width = `${canvasMain?.clientWidth * (window.devicePixelRatio || 1)}px`; + canvasTemplate.height = canvasMain?.clientHeight * (window.devicePixelRatio || 1); + canvasTemplate.width = canvasMain?.clientWidth * (window.devicePixelRatio || 1); this.tempDraw(); } } \ No newline at end of file