// ==UserScript== // @name RO Unlock Desktop // @namespace romobile-map-vps // @version 2.0.0 // @description หลอก mobile/PWA check ของ RoPlay ให้เล่นบนเบราว์เซอร์เดสก์ท็อปได้ (ใช้เฉพาะตอนทดสอบ) // @author romobile-map-vps // @match https://m.furr-ro.com/* // @run-at document-start // @grant none // @noframes // ==/UserScript== (function () { 'use strict'; try { var s = document.createElement('script'); s.textContent = "(function () {\n'use strict';\n/* =====================================================================\n * unlock-web.js -- ปลดล็อคหน้าเว็บ RoPlay/roBrowser บนเดสก์ท็อป\n *\n * โค้ดนี้เป็นของผู้ใช้ เก็บไว้ตามต้นฉบับ — src/unlock.ts เอาไปห่อเป็น userscript\n * แล้วฉีดเข้า context ของหน้าเว็บให้ ไม่ได้แก้ไขเนื้อโค้ด\n *\n * หลอก mobile/PWA checks เพื่อให้เล่นบนเบราว์เซอร์เดสก์ท็อปได้\n *\n * Helpers (รันใน Console ได้ตลอด):\n * __roCap.unblock() เปิด bypass mobile/PWA\n * __roCap.reloadClient() โหลด RoPlay.js ใหม่หลัง unblock\n * __roCap.openPatched() alias ของ reloadClient()\n * __roCap.useDisplay(w,h,dpr) ปรับขนาดหน้าจอจำลอง\n * __roCap.restoreDisplay() คืนค่าหน้าจอจริง\n * __roCap.displayInfo() ดูข้อมูล canvas/หน้าจอ\n * ===================================================================== */\n(function roUnlockMain() {\n var BYPASS_VERSION = 5;\n\n var cap = (window.__roCap = window.__roCap || { desktopBypass: false });\n\n function setGetter(obj, prop, value) {\n try {\n Object.defineProperty(obj, prop, {\n configurable: true,\n get: function () {\n return value;\n },\n });\n return true;\n } catch (e) {\n return false;\n }\n }\n\n function setNavigator(prop, value) {\n if (setGetter(navigator, prop, value)) return;\n try {\n if (window.Navigator && window.Navigator.prototype)\n setGetter(window.Navigator.prototype, prop, value);\n } catch (e) {}\n }\n\n function setWindow(prop, value) {\n if (setGetter(window, prop, value)) return;\n try {\n if (window.Window && window.Window.prototype) setGetter(window.Window.prototype, prop, value);\n } catch (e) {}\n }\n\n function setScreen(prop, value) {\n if (setGetter(screen, prop, value)) return;\n try {\n if (window.Screen && window.Screen.prototype) setGetter(window.Screen.prototype, prop, value);\n } catch (e) {}\n }\n\n function patchROConfig() {\n if (!window.ROConfig) return false;\n window.ROConfig.allowDevMode = true;\n window.ROConfig.allowDesktopBrowser = true;\n window.ROConfig.allowVmMode = true;\n window.ROConfig.emulatorDebugMode = false;\n return true;\n }\n\n function installDesktopBypass() {\n if (cap.desktopBypass && cap.bypassVersion === BYPASS_VERSION) {\n patchROConfig();\n return;\n }\n cap.desktopBypass = true;\n cap.bypassVersion = BYPASS_VERSION;\n try {\n window.onbeforeunload = null;\n } catch (e) {}\n if (!cap.realDisplay) {\n cap.realDisplay = {\n dpr: window.devicePixelRatio,\n screen: screen,\n width: screen.width,\n height: screen.height,\n availWidth: screen.availWidth,\n availHeight: screen.availHeight,\n };\n }\n\n var ua =\n 'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36';\n setNavigator('userAgent', ua);\n setNavigator('appVersion', ua);\n setNavigator('platform', 'Linux armv8l');\n setNavigator('vendor', 'Google Inc.');\n setNavigator('maxTouchPoints', 5);\n setNavigator('hardwareConcurrency', 8);\n setNavigator('deviceMemory', 8);\n try {\n Object.defineProperty(window, 'ontouchstart', { configurable: true, value: null });\n } catch (e) {}\n try {\n setGetter(navigator, 'standalone', true);\n } catch (e) {}\n try {\n setWindow('devicePixelRatio', 3);\n } catch (e) {}\n try {\n setScreen('width', 1024);\n setScreen('height', 768);\n setScreen('availWidth', 1024);\n setScreen('availHeight', 768);\n if (screen.orientation) {\n setGetter(screen.orientation, 'type', 'landscape-primary');\n setGetter(screen.orientation, 'angle', 90);\n }\n } catch (e) {}\n try {\n var nativeScreen = screen;\n var fakeScreen = new Proxy(nativeScreen, {\n get: function (target, prop) {\n if (prop === 'width' || prop === 'availWidth') return 1024;\n if (prop === 'height' || prop === 'availHeight') return 768;\n return target[prop];\n },\n });\n setWindow('screen', fakeScreen);\n } catch (e) {}\n\n patchWebGLFingerprint();\n\n var nativeMatchMedia = window.matchMedia ? window.matchMedia.bind(window) : null;\n window.matchMedia = function (query) {\n if (query && /display-mode:\\s*(standalone|fullscreen)/i.test(query)) {\n return {\n matches: true,\n media: query,\n onchange: null,\n addListener: function () {},\n removeListener: function () {},\n addEventListener: function () {},\n removeEventListener: function () {},\n dispatchEvent: function () {\n return false;\n },\n };\n }\n return nativeMatchMedia\n ? nativeMatchMedia(query)\n : {\n matches: false,\n media: query,\n onchange: null,\n addListener: function () {},\n removeListener: function () {},\n addEventListener: function () {},\n removeEventListener: function () {},\n dispatchEvent: function () {\n return false;\n },\n };\n };\n\n patchROConfig();\n cap._configPatchTimer = setInterval(patchROConfig, 250);\n setTimeout(function () {\n if (cap._configPatchTimer) clearInterval(cap._configPatchTimer);\n }, 15000);\n\n console.log('%c[roUnlock] desktop/mobile bypass installed', 'color:#0a0');\n }\n\n function patchWebGLFingerprint() {\n var vendor = 'Qualcomm';\n var renderer = 'Adreno (TM) 740';\n\n function patchContext(gl) {\n if (!gl || gl.__roUnlockWebGLContextPatched) return gl;\n var nativeGetParameter = gl.getParameter ? gl.getParameter.bind(gl) : null;\n var nativeGetExtension = gl.getExtension ? gl.getExtension.bind(gl) : null;\n try {\n Object.defineProperty(gl, '__roUnlockWebGLContextPatched', { value: true });\n } catch (e) {}\n if (nativeGetParameter) {\n try {\n Object.defineProperty(gl, 'getParameter', {\n configurable: true,\n value: function (pname) {\n if (pname === 0x1f00 || pname === 0x9245) return vendor;\n if (pname === 0x1f01 || pname === 0x9246) return renderer;\n return nativeGetParameter(pname);\n },\n });\n } catch (e) {}\n }\n if (nativeGetExtension) {\n try {\n Object.defineProperty(gl, 'getExtension', {\n configurable: true,\n value: function (name) {\n var ext = nativeGetExtension(name);\n if (name === 'WEBGL_debug_renderer_info') {\n return ext || { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 };\n }\n return ext;\n },\n });\n } catch (e) {}\n }\n return gl;\n }\n\n function patchProto(proto) {\n if (!proto || proto.__roUnlockWebGLPatched) return;\n var nativeGetParameter = proto.getParameter;\n var nativeGetExtension = proto.getExtension;\n try {\n Object.defineProperty(proto, '__roUnlockWebGLPatched', { value: true });\n } catch (e) {}\n if (nativeGetParameter) {\n proto.getParameter = function (pname) {\n if (pname === 0x1f00 || pname === 0x9245) return vendor;\n if (pname === 0x1f01 || pname === 0x9246) return renderer;\n return nativeGetParameter.apply(this, arguments);\n };\n }\n if (nativeGetExtension) {\n proto.getExtension = function (name) {\n var ext = nativeGetExtension.apply(this, arguments);\n if (name === 'WEBGL_debug_renderer_info') {\n return ext || { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 };\n }\n return ext;\n };\n }\n }\n\n try {\n patchProto(window.WebGLRenderingContext && window.WebGLRenderingContext.prototype);\n } catch (e) {}\n try {\n patchProto(window.WebGL2RenderingContext && window.WebGL2RenderingContext.prototype);\n } catch (e) {}\n try {\n var canvasProto = window.HTMLCanvasElement && window.HTMLCanvasElement.prototype;\n if (canvasProto && !canvasProto.__roUnlockGetContextPatched) {\n var nativeGetContext = canvasProto.getContext;\n Object.defineProperty(canvasProto, '__roUnlockGetContextPatched', { value: true });\n canvasProto.getContext = function (type) {\n var gl = nativeGetContext.apply(this, arguments);\n if (/^(webgl|experimental-webgl|webgl2)$/i.test(type || '')) return patchContext(gl);\n return gl;\n };\n }\n } catch (e) {}\n }\n\n function roPlaySrc() {\n var scripts = document.getElementsByTagName('script');\n for (var i = scripts.length - 1; i >= 0; i--) {\n var src = scripts[i].getAttribute('src') || '';\n if (/RoPlay\\.js/i.test(src))\n return src.replace(/([?&])roUnlockReload=\\d+(&?)/, '$1').replace(/[?&]$/, '');\n }\n return 'RoPlay.js?v=v3187';\n }\n\n function looksBlocked() {\n var text = (document.body && document.body.innerText) || '';\n return (\n /Access unavailable|mobile devices only|smartphone|Add to Home|Home Screen|Desktop access blocked/i.test(\n text\n ) || /ไม่สามารถเข้าถึง|รองรับเฉพาะ|หน้าจอหลัก|ติดตั้ง|มือถือ/i.test(text)\n );\n }\n\n cap.unblock = function () {\n installDesktopBypass();\n patchROConfig();\n console.log(\n '%c[roUnlock] unblock applied. If the page is still blocked, run __roCap.reloadClient()',\n 'color:#0a0'\n );\n };\n\n cap.reloadClient = function () {\n installDesktopBypass();\n patchROConfig();\n if (document.body) {\n Array.prototype.slice\n .call(document.querySelectorAll('#emulator-debug'))\n .forEach(function (n) {\n n.remove();\n });\n if (looksBlocked()) document.body.innerHTML = '';\n }\n var src = roPlaySrc();\n src += (src.indexOf('?') >= 0 ? '&' : '?') + 'roUnlockReload=' + Date.now();\n var script = document.createElement('script');\n script.type = 'text/javascript';\n script.src = src;\n script.onload = function () {\n console.log('%c[roUnlock] reloaded client: ' + src, 'color:#0a0');\n };\n script.onerror = function () {\n console.error('[roUnlock] failed to reload client: ' + src);\n };\n document.body.appendChild(script);\n return src;\n };\n\n cap.openPatched = function () {\n console.log(\n '%c[roUnlock] openPatched now reloads the client in-place; no navigation',\n 'color:#0a0'\n );\n return cap.reloadClient();\n };\n\n cap.useDisplay = function (width, height, dpr) {\n width = Number(width) || 1024;\n height = Number(height) || 768;\n dpr = Number(dpr) || 3;\n try {\n setWindow('devicePixelRatio', dpr);\n } catch (e) {}\n try {\n setScreen('width', width);\n setScreen('height', height);\n setScreen('availWidth', width);\n setScreen('availHeight', height);\n if (screen.orientation) {\n setGetter(\n screen.orientation,\n 'type',\n width >= height ? 'landscape-primary' : 'portrait-primary'\n );\n setGetter(screen.orientation, 'angle', width >= height ? 90 : 0);\n }\n } catch (e) {}\n try {\n var nativeScreen =\n cap.realDisplay && cap.realDisplay.screen ? cap.realDisplay.screen : screen;\n var fakeScreen = new Proxy(nativeScreen, {\n get: function (target, prop) {\n if (prop === 'width' || prop === 'availWidth') return width;\n if (prop === 'height' || prop === 'availHeight') return height;\n return target[prop];\n },\n });\n setWindow('screen', fakeScreen);\n } catch (e) {}\n cap.nudgeRender();\n console.log(\n '%c[roUnlock] display spoof set to ' + width + 'x' + height + ' DPR=' + dpr,\n 'color:#0a0'\n );\n };\n\n cap.restoreDisplay = function () {\n var d = cap.realDisplay;\n if (!d) {\n console.warn(\n '[roUnlock] no real display snapshot; use __roCap.useDisplay(width,height,dpr) instead'\n );\n return false;\n }\n try {\n setWindow('devicePixelRatio', d.dpr);\n } catch (e) {}\n try {\n setWindow('screen', d.screen);\n setScreen('width', d.width);\n setScreen('height', d.height);\n setScreen('availWidth', d.availWidth);\n setScreen('availHeight', d.availHeight);\n } catch (e) {}\n cap.nudgeRender();\n console.log(\n '%c[roUnlock] display spoof restored to real screen ' +\n d.width +\n 'x' +\n d.height +\n ' DPR=' +\n d.dpr,\n 'color:#0a0'\n );\n return true;\n };\n\n cap.nudgeRender = function () {\n try {\n window.dispatchEvent(new Event('resize'));\n } catch (e) {}\n try {\n window.dispatchEvent(new Event('orientationchange'));\n } catch (e) {}\n try {\n Array.prototype.slice.call(document.querySelectorAll('canvas')).forEach(function (canvas) {\n canvas.style.transform = canvas.style.transform || 'translateZ(0)';\n });\n } catch (e) {}\n };\n\n cap.displayInfo = function () {\n var canvases = [];\n try {\n canvases = Array.prototype.slice\n .call(document.querySelectorAll('canvas'))\n .map(function (c, i) {\n return {\n i: i,\n width: c.width,\n height: c.height,\n clientWidth: c.clientWidth,\n clientHeight: c.clientHeight,\n display: getComputedStyle(c).display,\n visibility: getComputedStyle(c).visibility,\n };\n });\n } catch (e) {}\n var info = {\n screen: screen.width + 'x' + screen.height,\n dpr: window.devicePixelRatio,\n inner: window.innerWidth + 'x' + window.innerHeight,\n canvases: canvases,\n };\n console.log('[roUnlock] displayInfo', info);\n return info;\n };\n\n installDesktopBypass();\n\n setTimeout(function () {\n if (looksBlocked()) {\n console.log(\n '%c[roUnlock] mobile/PWA block detected; reloading client in-place with bypass',\n 'color:#fa0'\n );\n cap.reloadClient();\n }\n }, 300);\n\n console.log(\n '%c[roUnlock] installed. Run __roCap.unblock() or __roCap.reloadClient() if needed',\n 'color:#0a0;font-weight:bold'\n );\n})();\n\n})();"; (document.head || document.documentElement).appendChild(s); s.remove(); console.log('[ro-unlock] ฉีด unlock-web เข้าหน้าเว็บแล้ว — เรียก __roCap ใน Console ได้'); } catch (e) { console.error('[ro-unlock] ฉีดเข้าหน้าเว็บไม่สำเร็จ:', e && e.message); } })();