diff --git a/css/_style.css b/css/_style.css new file mode 100644 index 0000000..de44412 --- /dev/null +++ b/css/_style.css @@ -0,0 +1,52 @@ +:root { + --background-color: #1c1c1c; + --primary: #e3e3e3; +} + +body { + background-color: var(--background-color); + margin: 0; +} + +main { + display: grid; + min-height: 100vh; + place-items: center; +} + +header { + color: var(--primary); +} + +/*#file { + display: block; + border: 1px solid white; + border-radius: 100vw; + + width: 400px; + height: 400px; + + background-color: white; +}*/ + +#input { + display: none; +} + +#upload { + display: block; + border: 1px solid var(--primary); + border-radius: 100vw; + + width: 400px; + height: 400px; + + background-color: var(--primary); + + cursor: pointer; +} + +#upload > span { + font-size: 2em; + color: var(--background-color); +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..fec9729 --- /dev/null +++ b/css/style.css @@ -0,0 +1,190 @@ +:root { + --col-dark: #1C1C1C; + --col-light: #E3E3E3; +} + +@font-face { + font-family: 'Lato'; + src: url('../fonts/Lato-Regular.ttf'); + src: url('../fonts/Lato-Regular.ttf') format('truetype'); +} + +html, body { + margin: 0; +} + +body { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + width: 100vw; + height: 100vh; + + overflow: hidden; + + background: var(--col-dark); +} + +section { + color: var(--col-light); + font-family: Lato; +} + +a { + color: var(--col-light); + text-decoration: none; +} + +a[rel="author"] { + justify-self: end; +} +a[rel="author"]::before { + content: " - "; +} + +main { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + gap: 1rem; +} + +section { + display: grid; + place-content: center; + place-items: center; + margin: 0 4rem; +} + +section * { + margin: .2rem 0; +} + +.btn { + cursor: pointer; +} + +.btn:focus { + outline: none; +} + +.btn.cbtn { + display: grid; + place-content: center; + place-items: center; + position: relative; + + padding: 2.2rem; + + width: 0; + height: 0; + + border: none; + background: none; + font: unset; + + border-radius: 100vw; +} +.btn.cbtn > i { + display: grid; + place-items: center; +} +.btn.cbtn > i::after { + content: ""; + display: grid; + place-content: center; + position: absolute; + + z-index: 1000; + + overflow: hidden; + + padding: 0rem; + + background-color: var(--col-light); + color: var(--col-dark); + + width: 0; + height: 0; + + border-radius: 100vw; + transition: padding .2s, background-color .2s, color .2s; +} +.btn.cbtn:hover > i::after, +.btn.cbtn.dragover > i::after { + padding: var(--padding); +} + +@keyframes cbtn-click { + from { + padding: 0rem; + opacity: 1; + } + to { + padding: calc(var(--padding) * 1.3); + opacity: 0; + } +} +.btn.cbtn > .click { + position: absolute; + padding: 0rem; + opacity: 1; + z-index: 10000; + + border-radius: 100vw; + width: 0; + height: 0; + + /*box-shadow: inset 0 0 2px #FFF;*/ + background: #DDD8; + border: 4px solid #fff; + + animation: cbtn-click .2s; +} + +.btn.cbtn { + --padding: 5vmin; + padding: var(--padding); + font-size: 3vmin; +} + +.btn.cbtn.medium { + --padding: 5vmin; + padding: var(--padding); + font-size: 3vmin; +} + +.btn.cbtn.big { + --padding: 30vmin; + padding: var(--padding); + font-size: 8vmin; +} + +.btn.cbtn.overlap { + margin: -1vmin; +} + +footer { + display: grid; + grid-auto-columns: min-content; + grid-auto-flow: column; + padding: 3vmin; +} + +#input { + display: none; +} + +#button_text { + font-style: normal; +} + +#button_text::before, #button_text::after { + content: "Drag & Drop Yuzu Executable here"; + text-align: center; +} + +.dragover { + --col-dark: #1C1C1C; + --col-light: #82DD90; +} \ No newline at end of file diff --git a/fonts/Lato-Regular.ttf b/fonts/Lato-Regular.ttf new file mode 100644 index 0000000..33eba8b Binary files /dev/null and b/fonts/Lato-Regular.ttf differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..be8d47c --- /dev/null +++ b/index.html @@ -0,0 +1,32 @@ + + + + + + YUZU Patcher + + + + + + +
+
+

A yuzu Emulator fork patcher

+

+ yuzu forks often change the application and engine name given to Vulkan. +
+ This has the side effect of not applying a driver level patch by AMD for RDNA3 graphic cards, causing visual issues. +
+ This tool replaces the application and engine name given to Vulkan with yuzu's. +

+ + + + + +
+
+ + + \ No newline at end of file diff --git a/js/FileSaver.js b/js/FileSaver.js new file mode 100644 index 0000000..64fd250 --- /dev/null +++ b/js/FileSaver.js @@ -0,0 +1,172 @@ +/* +* FileSaver.js +* A saveAs() FileSaver implementation. +* +* By Eli Grey, http://eligrey.com +* +* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) +* source : http://purl.eligrey.com/github/FileSaver.js +*/ + +// The one and only way of getting global scope in all environments +// https://stackoverflow.com/q/3277182/1008999 +var _global = typeof window === 'object' && window.window === window + ? window : typeof self === 'object' && self.self === self + ? self : typeof global === 'object' && global.global === global + ? global + : this + +function bom (blob, opts) { + if (typeof opts === 'undefined') opts = { autoBom: false } + else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object') + opts = { autoBom: !opts } + } + + // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type }) + } + return blob +} + +function download (url, name, opts) { + var xhr = new XMLHttpRequest() + xhr.open('GET', url) + xhr.responseType = 'blob' + xhr.onload = function () { + saveAs(xhr.response, name, opts) + } + xhr.onerror = function () { + console.error('could not download file') + } + xhr.send() +} + +function corsEnabled (url) { + var xhr = new XMLHttpRequest() + // use sync to avoid popup blocker + xhr.open('HEAD', url, false) + try { + xhr.send() + } catch (e) {} + return xhr.status >= 200 && xhr.status <= 299 +} + +// `a.click()` doesn't work for all browsers (#465) +function click (node) { + try { + node.dispatchEvent(new MouseEvent('click')) + } catch (e) { + var evt = document.createEvent('MouseEvents') + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, + 20, false, false, false, false, 0, null) + node.dispatchEvent(evt) + } +} + +// Detect WebView inside a native macOS app by ruling out all browsers +// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too +// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos +var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent) + +var saveAs = _global.saveAs || ( + // probably in some web worker + (typeof window !== 'object' || window !== _global) + ? function saveAs () { /* noop */ } + + // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : ('download' in HTMLAnchorElement.prototype && !isMacOSWebView) + ? function saveAs (blob, name, opts) { + var URL = _global.URL || _global.webkitURL + // Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561) + var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a') + name = name || blob.name || 'download' + + a.download = name + a.rel = 'noopener' // tabnabbing + + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob + if (a.origin !== location.origin) { + corsEnabled(a.href) + ? download(blob, name, opts) + : click(a, a.target = '_blank') + } else { + click(a) + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob) + setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s + setTimeout(function () { click(a) }, 0) + } + } + + // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator + ? function saveAs (blob, name, opts) { + name = name || blob.name || 'download' + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts) + } else { + var a = document.createElement('a') + a.href = blob + a.target = '_blank' + setTimeout(function () { click(a) }) + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name) + } + } + + // Fallback to using FileReader and a popup + : function saveAs (blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank') + if (popup) { + popup.document.title = + popup.document.body.innerText = 'downloading...' + } + + if (typeof blob === 'string') return download(blob, name, opts) + + var force = blob.type === 'application/octet-stream' + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent) + + if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader() + reader.onloadend = function () { + var url = reader.result + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;') + if (popup) popup.location.href = url + else location = url + popup = null // reverse-tabnabbing #460 + } + reader.readAsDataURL(blob) + } else { + var URL = _global.URL || _global.webkitURL + var url = URL.createObjectURL(blob) + if (popup) popup.location = url + else location.href = url + popup = null // reverse-tabnabbing #460 + setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s + } + } +) + +_global.saveAs = saveAs.saveAs = saveAs + +if (typeof module !== 'undefined') { + module.exports = saveAs; +} \ No newline at end of file diff --git a/js/button.js b/js/button.js new file mode 100644 index 0000000..26675b1 --- /dev/null +++ b/js/button.js @@ -0,0 +1,14 @@ +addEventListener("load", () => { + const onAnimationFinished = (e) => { + e.target.remove(); + }; + const onClick = (e, btn) => { + const clickElement = document.createElement("div"); + clickElement.className = "click"; + clickElement.addEventListener("animationend", onAnimationFinished); + btn.appendChild(clickElement); + }; + for (const btn of document.getElementsByClassName("btn")) { + btn.addEventListener("click", (e) => onClick(e, btn) ); + }; +}); \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..7dd5457 --- /dev/null +++ b/js/main.js @@ -0,0 +1,109 @@ +const VK_PATTERN = (new TextEncoder()).encode("vkDestroyInstance"); +const YUZU_PATTERN = (new TextEncoder()).encode("yuzu Emulator"); + +addEventListener("load", () => { + const input = document.getElementById("input"); + const button = document.getElementById("upload"); + + input.addEventListener("change", (e) => { + if (e.target.files.length) { + handleFile(e.target.files[0]); + } + }); + + button.addEventListener("click", () => input.click()); + button.addEventListener("drop", (e) => { + e.preventDefault(); + button.classList.remove("dragover"); + if (e.dataTransfer.files.length) { + handleFile(e.dataTransfer.files[0]); + } + }); + button.addEventListener("dragover", (e) => { + e.preventDefault(); + button.classList.add("dragover"); + }); + button.addEventListener("dragleave", (e) => { + button.classList.remove("dragover"); + }); +}); + +/** + * @param {File} file + */ +function handleFile(file) { + const filename = file.name ?? "yuzu.exe"; + const reader = new FileReader(); + reader.onload = (e) => { + const buff = e.target.result; + const view = new DataView(buff); + + const patternLen = VK_PATTERN.byteLength; + const buffLen = buff.byteLength; + + // Find the pattern + let foundIndex = -1; + for (let i = 0; i <= buffLen - patternLen; i++) { + let found = true; + for (let j = 0; j < patternLen; j++) { + if (view.getUint8(i + j) !== VK_PATTERN[j]) { + found = false; + break; + } + } + + if (found) { + foundIndex = i; + break; + } + } + + if (foundIndex == -1) { + console.error("Could not find pattern in file"); + alert("Could not find pattern in file"); + return; + } + + // Walk backwards until we hit the start of the Emulator string + let startIndex = -1; + let emuLen = 0; + for (let i = foundIndex - 2;; i--) { // skip over first 0 byte right before current index + if (view.getUint8(i) == 0) { + startIndex = i + 1; + break; + } + emuLen++; + } + + const yuzuLen = YUZU_PATTERN.byteLength; + if (emuLen < yuzuLen) { + console.error("Cannot patch emulator with smaller default name then yuzu"); + alert("Cannot patch emulator with smaller default name then yuzu"); + return; + } + + if (emuLen == yuzuLen) { + let isYuzu = true; + for (let i = 0; i < emuLen; i++) { + if (view.getUint8(startIndex + i) != YUZU_PATTERN[i]) { + isYuzu = false; + break; + } + } + + if (isYuzu) { + console.error("Either this is an already patched fork or its real yuzu"); + alert("Either this is an already patched fork or its real yuzu"); + return; + } + } + + for (let i = 0; i < emuLen; i++) { + view.setUint8(startIndex + i, YUZU_PATTERN[i] || 0); + } + + const blobPatched = new Blob([buff]); + saveAs(blobPatched, filename.replace(".exe", "_patched.exe")); + } + reader.readAsArrayBuffer(file); +} \ No newline at end of file