From 9780ac7eb45a0789cb9182d5c2d56d5b106ab3bb Mon Sep 17 00:00:00 2001 From: Raymonf Date: Thu, 10 Nov 2022 01:21:38 -0500 Subject: [PATCH] Add MercuryPatcher --- MercuryPatcher/LICENSE | 21 + MercuryPatcher/README.md | 4 + MercuryPatcher/css/file.eot | Bin 0 -> 1324 bytes MercuryPatcher/css/file.svg | 11 + MercuryPatcher/css/file.ttf | Bin 0 -> 1160 bytes MercuryPatcher/css/file.woff | Bin 0 -> 1236 bytes MercuryPatcher/css/style.css | 359 +++++++++++++ MercuryPatcher/index.html | 34 ++ MercuryPatcher/js/dllpatcher.js | 921 ++++++++++++++++++++++++++++++++ README.md | 1 + 10 files changed, 1351 insertions(+) create mode 100644 MercuryPatcher/LICENSE create mode 100644 MercuryPatcher/README.md create mode 100644 MercuryPatcher/css/file.eot create mode 100644 MercuryPatcher/css/file.svg create mode 100644 MercuryPatcher/css/file.ttf create mode 100644 MercuryPatcher/css/file.woff create mode 100644 MercuryPatcher/css/style.css create mode 100644 MercuryPatcher/index.html create mode 100644 MercuryPatcher/js/dllpatcher.js diff --git a/MercuryPatcher/LICENSE b/MercuryPatcher/LICENSE new file mode 100644 index 0000000..8864d4a --- /dev/null +++ b/MercuryPatcher/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MercuryPatcher/README.md b/MercuryPatcher/README.md new file mode 100644 index 0000000..fae109d --- /dev/null +++ b/MercuryPatcher/README.md @@ -0,0 +1,4 @@ +# MercuryPatcher +A tool to easily apply known hex edits to the Mercury binary, derived from [BemaniPatcher](https://github.com/mon/BemaniPatcher). + +Currently only has patches for 3.07.01. diff --git a/MercuryPatcher/css/file.eot b/MercuryPatcher/css/file.eot new file mode 100644 index 0000000000000000000000000000000000000000..32d36bea1a0a4638af681468c19bf9508739275d GIT binary patch literal 1324 zcmaJ>O-vI(7=5$51zNON1u;s@5^U5&D_B8IG}(h8K_MD6Bp!?`Wob#;-KIYY7!G?< zV@%}Q8wXDwIO&bU#)BtL9z2_PBAz{&Qs2xjg_6dvotgK&d9&ZlH$T^6z*_?l(9mfD zn?f?PPda(24Xu&hDID(ul2}C!9!e}8Tndtyhl4!oD8fdC%vmgush~=&)R>q+2IES5 zd^AZJupd5jj6Z&Q=;dr&!7KWo>7OXsYh^Bo zQ`G|crfZj+#>4nu`ac=tb=j-dI8T}(b7Ml+e6a~;S!-y!WIlwg_>HUx14(gcg46Ca z3)q7mv;tT<3p)>zFLkD@R5TfJgnePFE7b@L+##PpA2(McXYQfZY_;T|HWsa4!v@D4 z{jPwZYTyXR!*Tnrk{s&(&-KY5I&`12h~p$uxPUaS;yS;S8XZpQmq$~hVl0*I9UID~ zvO{B&*-4gk8=Z;zpjm+x>|2_@?+1ZJW918|#!?KU1WgKpCWP$q_oakC{+od$+IcJN zq#G{>?)^c(At#6*v*+YFX}4;|JU7f&Xk4$#KbIkm2|mm~sECC?U!x>gSdzl zH6q3Xu5!};ayTHYlh}^FE6__SjG+-PMp!4Fim*Xkk8q4PV<^HsRG9A6?r~1u6k^2)YvYQ ZTf#c)8TCxfMbA`*S#NLL(XBbe{{RSDyr2L8 literal 0 HcmV?d00001 diff --git a/MercuryPatcher/css/file.svg b/MercuryPatcher/css/file.svg new file mode 100644 index 0000000..55f885b --- /dev/null +++ b/MercuryPatcher/css/file.svg @@ -0,0 +1,11 @@ + + + +Generated by IcoMoon + + + + + + + \ No newline at end of file diff --git a/MercuryPatcher/css/file.ttf b/MercuryPatcher/css/file.ttf new file mode 100644 index 0000000000000000000000000000000000000000..805576d9cfe7a52a902418525f0d9918edd5e23f GIT binary patch literal 1160 zcmaJ=O=uHA6#iy+(>AH5Mq49VWJ@jT!I~Od6%_U$g{noUQt%*ln{JarvK#UPsr0ZX z1rhX`TMwQ*^rW{Q77w0!^59wUR6Kik?)l}cP;(7`5Z9r zlh2hX416)al5de;DpeY-Bmwz0U!&!q;Nl8il7AvUQE^*Uq>&?!$Zg-Pcv#9n|caEF!%`A`{;JMT{Y_vi|)^?#d-bf3J7M6^l1ig>yAq4<^5k9 zl2uHYA-hQ8Br>>wDO|-hu9TU`X3WbInF%qO$q!DBiR@$9vzZkju8^e}t5Z@o2li$ga zR~_@a;d#Zz{i^flHl*=@dnXWQB;rU+sWEg zD>ln+jYT~++C&*HYR6{I2Y$mYdcIe48{V3|x@j*If<^W=jd@kO!RkdW*4Egnps=Sv)l#TZ@E~@ZZj(Z?Tk->2@sN{> z2=>~W2TvY)(pwLUhV+*4FN9}ioinmIxME}0%(EQiuMXx5@Hhswz9VnL9m&tu1-VuHE z;LTRa_m%*2gZ`9g`a|Pki6pr%^v{a+e6&87D)lvTzvv$qO>!uQ1?E{qIIQ$Mo>aUw z*0scUl%_Ercom=AMb>^Mx_LYKXEm(V+3)-PJXyvvo5ltbtg~tE(0?6U<0o>46zmjZ zpwkoV0=uxHb_Ca=l2K<$E8?h8RUsN?Oz<#A$r^@@>$EAJl*!M4vx|1C-6qGm8XN5& zL$;ow0vrq&%<5OG#GDCSyP--S)c-#xx5H(vCU=p<5oB;0qqu-eTq!d$l(Ei@WJZi^ zCfA!C%w=+e*~#1_b#xyc(=nKCf|Oo&=AB+}fI6n^}* zA|dwAt+YM5`KbTqZ}i!+L43b^l6QMx)sE$Du{$N~SDioG5a9tIoPc7Gh$FEh)8rDA zr1-ARU|G#bU`)MOK7@ba4x7f7Ue`b``M8Ejyr8i~Jf*Qs+|W3|f7hVKJ>>6boWd!* zlzX+4WYUbp*d#7$Y!Tnm*d~6eaRMjcXxu}7PU94=;_XTytb}2J6%-JnLJbkj`_div { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; +} + +.fileInput { + display: none +} + +.fileLabel { + cursor: pointer; +} + +.error { + color: red; +} + +.success { + color: DarkGreen; +} + +.success.hidden { + display: none; +} + +.patchContainer { + background-color: white; + padding: 20px; + max-width: 650px; + margin: 0 auto 2em; + position: relative; +} + +.tooltip { + visibility: hidden; + font-size: 14px; + margin-left: 8px; + padding: 8px; + border-radius: 4px; + position: relative; + background: grey; + white-space: nowrap; + cursor: pointer; +} + +.tooltip:hover, +.tooltip:focus, +.tooltip:active { + visibility: visible; + color: white; + border: none; + margin-top: 0px; + position: absolute; + padding: 3px 16px; + white-space: normal; + max-width: 300px; + z-index: 11; +} + +.tooltip:before { + visibility: visible; + content: '?'; + font-size: 18px; + margin-right: 8px; + background: gray; + border-radius: 50%; + padding: 2px 9px; + margin-left: -8px; + color: white; + cursor: pointer; + z-index: 10; +} + +.tooltip:hover:before, +.tooltip:focus:before, +.tooltip:active:before { + color: black; + display: none; +} + +.danger { + background: #ff6000 !important; +} + +.danger:before { + content: '!!'; + background: #ff6000; +} + +body { + margin: 40px auto; + max-width: 1300px; + line-height: 1.6; + font-size: 18px; + color: #000; + padding: 0 10px; + background: #e2e1e0; + font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; +} + +h1, +h2, +h3 { + line-height: 1.2; +} + +h4, +h5 { + line-height: 1; + margin: 10px auto; +} + +h1 { + text-align: center; +} + +h1 a { + color: inherit; + text-decoration: inherit; +} + +button, .fileLabel > strong { + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.25, 0.5, 0.5, 1); + position: relative; + padding: 0 16px; + height: 36px; + border: none; + border-radius: 2px; + outline: none; + font-size: 0.875rem; + font-weight: 500; + letter-spacing: 0.04em; + line-height: 2.25rem; + color: rgba(0, 0, 0, 0.73); + transition-property: box-shadow, background; + background-color: #40b31a; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25), 0px 0px 2px rgba(0, 0, 0, 0.125); + color: white; + cursor: pointer; +} + +button:disabled { + background-color: rgba(0, 0, 0, 0.12); + color: rgba(0, 0, 0, 0.38); + box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); + cursor: default; +} + +button:hover:enabled, .fileLabel > strong:hover { + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 0px 4px rgba(0, 0, 0, 0.125); + background-color: #5dbe3c; +} + +.matchPercent { + font-size: 15px; + font-style: italic; + color: red; +} + +.matchSuccess { + font-size: 15px; + font-style: italic; + color: green; +} + +li > button { + height: 24px; + padding: 0 7px; + line-height: 0; +} + +.patches { + margin: 1em auto; +} + +input[type=checkbox], +input[type=radio] { + vertical-align: middle; + position: relative; + bottom: 1px; +} + +input[type=radio] { + bottom: 2px; +} + +.patches label { + margin-left: 4px; +} + +.dragover>* { + filter: blur(5px); +} + +.dragover::before { + content: ''; + display: block; + height: 100%; + position: absolute; + top: 0; + left: 0; + width: 100%; + text-align: center; + z-index: 10; + outline: dashed 10px; +} + +.dragover::after { + content: "\e900"; + font-family: 'file' !important; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + display: block; + height: auto; + position: absolute; + top: calc(50% - 3rem); + left: 0; + width: 100%; + text-align: center; + z-index: 11; + font-size: 8rem; +} + +.tooltip:not(:hover) { + font-size: 0px; + padding: 0; + margin-left: 16px; +} + +.tooltip { + visibility: hidden; + font-size: 14px; + margin-left: 8px; + padding: 8px; + border-radius: 4px; + position: relative; + background: gray; + white-space: nowrap; + cursor: pointer; + display: inline-block; +} + +.patchPreviewLabel { + cursor: pointer; +} +.patchPreviewToggle { + display: none; +} +.patchPreview { + display: none; +} +input[type=checkbox]:checked + .patchPreview { + display: block; +} +input[type=checkbox] ~ ul > li.patch-off { + display: none; +} +input[type=checkbox] ~ ul > li.patch-on { + display: list-item; +} +input[type=checkbox]:checked ~ ul > li.patch-on { + display: none; +} +input[type=checkbox]:checked ~ ul > li.patch-off { + display: list-item; +} diff --git a/MercuryPatcher/index.html b/MercuryPatcher/index.html new file mode 100644 index 0000000..a236cc5 --- /dev/null +++ b/MercuryPatcher/index.html @@ -0,0 +1,34 @@ + + + + + Mercury Patcher + + + + + +

Mercury Patcher

+ + diff --git a/MercuryPatcher/js/dllpatcher.js b/MercuryPatcher/js/dllpatcher.js new file mode 100644 index 0000000..853165f --- /dev/null +++ b/MercuryPatcher/js/dllpatcher.js @@ -0,0 +1,921 @@ +/*jshint esversion: 6 */ +(function(window, document) { +"use strict"; + +// form labels often need unique IDs - this can be used to generate some +window.Patcher_uniqueid = 0; +var createID = function() { + window.Patcher_uniqueid++; + return "dllpatch_" + window.Patcher_uniqueid; +}; + +var bytesMatch = function(buffer, offset, bytes) { + for(var i = 0; i < bytes.length; i++) { + if(buffer[offset+i] != bytes[i]) + return false; + } + return true; +}; + +var replace = function(buffer, offset, bytes) { + for(var i = 0; i < bytes.length; i++) { + buffer[offset+i] = bytes[i]; + } +}; + +var whichBytesMatch = function(buffer, offset, bytesArray) { + for(var i = 0; i < bytesArray.length; i++) { + if(bytesMatch(buffer, offset, bytesArray[i])) + return i; + } + return -1; +}; + +// shorthand functions +var createElementClass = function(elName, className, textContent, innerHTML) { + var el = document.createElement(elName); + el.className = className || ''; + el.textContent = textContent || ''; // optional + // overrides textContent with HTML if provided + if(innerHTML) { + el.innerHTML = innerHTML; + } + return el; +}; + +var createInput = function(type, id, className) { + var el = document.createElement('input'); + el.type = type; + el.id = id; + el.className = className || ''; + return el; +}; + +var createLabel = function(labelText, htmlFor, className) { + var el = document.createElement('label'); + el.textContent = labelText; + el.htmlFor = htmlFor; + el.className = className || ''; + return el; +}; + +// Each unique kind of patch should have createUI, validatePatch, applyPatch, +// updateUI + +class StandardPatch { + constructor(options) { + this.name = options.name; + this.patches = options.patches; + this.tooltip = options.tooltip; + this.danger = options.danger; + } + + createUI(parent) { + var id = createID(); + var label = this.name; + var patch = createElementClass('div', 'patch'); + this.checkbox = createInput('checkbox', id); + patch.appendChild(this.checkbox); + patch.appendChild(createLabel(label, id)); + if(this.tooltip) { + patch.appendChild(createElementClass('div', 'tooltip', this.tooltip)); + } + if(this.danger) { + patch.appendChild(createElementClass('div', 'danger tooltip', this.danger)); + } + parent.appendChild(patch); + } + + updateUI(file) { + this.checkbox.checked = this.checkPatchBytes(file) === "on"; + } + + validatePatch(file) { + var status = this.checkPatchBytes(file); + if(status === "on") { + console.log('"' + this.name + '"', "is enabled!"); + } else if(status === "off") { + console.log('"' + this.name + '"', "is disabled!"); + } else { + return '"' + this.name + '" is neither on nor off! Have you got the right file?'; + } + } + + applyPatch(file) { + this.replaceAll(file, this.checkbox.checked); + } + + replaceAll(file, featureOn) { + for(var i = 0; i < this.patches.length; i++) { + replace(file, this.patches[i].offset, + featureOn? this.patches[i].on : this.patches[i].off); + } + } + + checkPatchBytes(file) { + var patchStatus = ""; + for(var i = 0; i < this.patches.length; i++) { + var patch = this.patches[i]; + if(bytesMatch(file, patch.offset, patch.off)) { + if(patchStatus === "") { + patchStatus = "off"; + } else if(patchStatus != "off"){ + return "on/off mismatch within patch"; + } + } else if(bytesMatch(file, patch.offset, patch.on)) { + if(patchStatus === "") { + patchStatus = "on"; + } else if(patchStatus != "on"){ + return "on/off mismatch within patch"; + } + } else { + return "patch neither on nor off"; + } + } + return patchStatus; + } +} + +class DynamicPatch { + constructor(options) { + this.name = options.name; + this.patches = options.patches; + this.tooltip = options.tooltip; + this.danger = options.danger; + this.mode = options.mode; + this.target = options.target; + } + + createUI(parent) { + var id = createID(); + var label = this.name; + this.ui = createElementClass('div', 'patch'); + this.checkbox = createInput('checkbox', id); + this.ui.appendChild(this.checkbox); + this.ui.appendChild(createLabel(label, id)); + if(this.tooltip) { + this.ui.appendChild(createElementClass('div', 'tooltip', this.tooltip)); + } + if(this.danger) { + this.ui.appendChild(createElementClass('div', 'danger tooltip', this.danger)); + } + parent.appendChild(this.ui); + } + + updateUI(file) { + if (this.mode === 'all') { + this.checkbox.checked = this.checkPatchAll(file, true) === "on"; + } else { + this.checkbox.checked = this.checkPatch(file, true) === "on"; + } + } + + validatePatch(file) { + var status = this.mode === 'all' ? this.checkPatchAll(file) : this.checkPatch(file); + + if(status === "on") { + console.log('"' + this.name + '"', "is enabled!"); + } else if(status === "off") { + console.log('"' + this.name + '"', "is disabled!"); + } else { + return '"' + this.name + '" is neither on nor off! Have you got the right file?'; + } + } + + applyPatch(file) { + this.replaceAll(file, this.checkbox.checked); + } + + replaceAll(file, featureOn) { + for(let patch of this.patches) { + if (Array.isArray(patch.offset)) { + for(const offset of patch.offset) { + if (this.target === 'string') { + replace(file, offset, + new TextEncoder().encode(featureOn? patch.on : patch.off)); + } else { + patch.on = patch.on.map((patch, idx) => patch === 'XX' ? file[offset + idx] : patch); + patch.off = patch.off.map((patch, idx) => patch === 'XX' ? file[offset + idx] : patch); + replace(file, offset, + featureOn? patch.on : patch.off); + } + } + } else { + if (this.target === 'string') { + replace(file, patch.offset, + new TextEncoder().encode(featureOn? patch.on : patch.off)); + } else { + patch.on = patch.on.map((patch, idx) => patch === 'XX' ? file[patch.offset + idx] : patch); + patch.off = patch.off.map((patch, idx) => patch === 'XX' ? file[patch.offset + idx] : patch); + replace(file, patch.offset, + featureOn? patch.on : patch.off); + } + } + } + } + + checkPatch(file, updateUiFlag = false) { + var patchStatus = ""; + let listUi; + if (updateUiFlag) { + listUi = document.createElement('ul'); + this.ui.appendChild(listUi); + } + for(var i = 0; i < this.patches.length; i++) { + var patch = this.patches[i]; + var offOffset = this.searchPatchOffset(file, patch.off, i); + var onOffset = this.searchPatchOffset(file, patch.on, i); + this.patches[i].offset = offOffset === -1 ? onOffset : offOffset; + if(offOffset > 0) { + if (updateUiFlag) { + if (this.target === 'string') { + listUi.appendChild(createElementClass('li', 'patch-off', null, '0x' + offOffset.toString(16) + ' ' + patch.off + ' will be replaced with '+ patch.on +'')); + } else { + listUi.appendChild(createElementClass('li', 'patch-off', '0x' + offOffset.toString(16) + ' will be replaced')); + } + } + if(patchStatus === "") { + patchStatus = "off"; + } + } else if(onOffset > 0) { + if (updateUiFlag) { + if (this.target === 'string') { + listUi.appendChild(createElementClass('li', 'patch-on', null, '0x' + onOffset.toString(16) + ' ' + patch.on + ' will be replaced with '+ patch.off +'')); + } else { + listUi.appendChild(createElementClass('li', 'patch-on', '0x' + onOffset.toString(16) + ' will be replaced')); + } + } + if(patchStatus === "") { + patchStatus = "on"; + } + } else if (this.mode === 'all') { + continue; + } else { + return "patch string not found"; + } + } + return patchStatus; + } + + checkPatchAll(file, updateUiFlag = false) { + var patchStatus = ""; + let listUi; + if (updateUiFlag) { + listUi = document.createElement('ul'); + this.ui.appendChild(listUi); + } + for(let patch of this.patches) { + var offOffset = this.searchPatchOffsetAll(file, patch.off); + var onOffset = this.searchPatchOffsetAll(file, patch.on); + patch.offset = offOffset.length === 0 ? onOffset : offOffset; + + if(offOffset.length > 0) { + if (updateUiFlag) { + for(const offset of offOffset) { + listUi.appendChild(createElementClass('li', 'patch-off', '0x' + offset.toString(16) + ' will be replaced')); + } + } + if(patchStatus === "") { + patchStatus = "off"; + } + } else if(onOffset.length > 0) { + if (updateUiFlag) { + for(const offset of onOffset) { + listUi.appendChild(createElementClass('li', 'patch-on', '0x' + offset.toString(16) + ' will be replaced')); + } + } + if(patchStatus === "") { + patchStatus = "on"; + } + } else { + return "patch string not found"; + } + } + return patchStatus; + } + + searchPatchOffset(file, search, offset) { + let searchBytes; + if (this.target === 'string') { + searchBytes = new TextEncoder().encode(search); + } else { + searchBytes = search; + } + + Uint8Array.prototype.indexOfArr = function(searchElements, fromIndex) { + fromIndex = fromIndex || 0; + + var index = Array.prototype.indexOf.call(this, searchElements[0], fromIndex); + if(searchElements.length === 1 || index === -1) { + return { + match: false, + index: -1, + }; + } + + for(var i = index, j = 0; j < searchElements.length && i < this.length; i++, j++) { + if (this.target !== 'string' && searchElements[j] === 'XX') { + continue; + } + if(this[i] !== searchElements[j]) { + return { + match: false, + index, + }; + } + } + return { + match: true, + index, + }; + }; + + var idx = 0; + var foundCount = 0; + for (var i = 0; i < file.length; i++) { + var result = file.indexOfArr(searchBytes, idx); + if (result.match) { + if (offset === foundCount) { + return result.index; + } + foundCount++; + } else if (result.index === -1) { + break; + } + idx = result.index + 1; + } + return -1; + } + + searchPatchOffsetAll(file, search) { + let searchBytes; + if (this.target === 'string') { + searchBytes = new TextEncoder().encode(search); + } else { + searchBytes = search; + } + + Uint8Array.prototype.indexOfArr = function(searchElements, fromIndex) { + fromIndex = fromIndex || 0; + + var index = Array.prototype.indexOf.call(this, searchElements[0], fromIndex); + if(searchElements.length === 1 || index === -1) { + return { + match: false, + index: -1, + }; + } + + for(var i = index, j = 0; j < searchElements.length && i < this.length; i++, j++) { + if (this.target !== 'string' && searchElements[j] === 'XX') { + continue; + } + if(this[i] !== searchElements[j]) { + return { + match: false, + index, + }; + } + } + + return { + match: true, + index, + }; + }; + + var idx = 0; + var foundOffsetArray = []; + for (var i = 0; i < file.length; i++) { + var result = file.indexOfArr(searchBytes, idx); + if (result.match) { + foundOffsetArray.push(result.index); + } else if (result.index === -1) { + break; + } + idx = result.index + 1; + } + return foundOffsetArray; + } +} + +// Each unique kind of patch should have createUI, validatePatch, applyPatch, +// updateUI + +// The DEFAULT state is always the 1st element in the patches array +class UnionPatch { + constructor(options) { + this.name = options.name; + this.offset = options.offset; + this.patches = options.patches; + this.tooltip = options.tooltip; + this.danger = options.danger; + } + + createUI(parent) { + this.radios = []; + var radio_id = createID(); + + var container = createElementClass('div', 'patch-union'); + container.appendChild(createElementClass('span', 'patch-union-title', this.name + ':')); + if(this.tooltip) { + container.appendChild(createElementClass('div', 'tooltip', this.tooltip)); + } + if(this.danger) { + container.appendChild(createElementClass('div', 'danger tooltip', this.danger)); + } + container.appendChild(document.createElement('span')); + + for(var i = 0; i < this.patches.length; i++) { + var patch = this.patches[i]; + var id = createID(); + var label = patch.name; + var patchDiv = createElementClass('div', 'patch'); + var radio = createInput('radio', id); + radio.name = radio_id; + this.radios.push(radio); + + patchDiv.appendChild(radio); + patchDiv.appendChild(createLabel(label, id)); + if(patch.tooltip) { + patchDiv.appendChild(createElementClass('div', 'tooltip', patch.tooltip)); + } + if(patch.danger) { + patchDiv.appendChild(createElementClass('div', 'danger tooltip', patch.danger)); + } + container.appendChild(patchDiv); + } + parent.appendChild(container); + } + + updateUI(file) { + for(var i = 0; i < this.patches.length; i++) { + if(bytesMatch(file, this.offset, this.patches[i].patch)) { + this.radios[i].checked = true; + return; + } + } + // Default fallback + this.radios[0].checked = true; + } + + validatePatch(file) { + for(var i = 0; i < this.patches.length; i++) { + if(bytesMatch(file, this.offset, this.patches[i].patch)) { + console.log(this.name, "has", this.patches[i].name, "enabled"); + return; + } + } + return '"' + this.name + '" doesn\'t have a valid patch! Have you got the right file?'; + } + + applyPatch(file) { + var patch = this.getSelected(); + replace(file, this.offset, patch.patch); + } + + getSelected() { + for(var i = 0; i < this.patches.length; i++) { + if(this.radios[i].checked) { + return this.patches[i]; + } + } + return null; + } +} + +// Each unique kind of patch should have createUI, validatePatch, applyPatch, +// updateUI +class NumberPatch { + constructor(options) { + this.name = options.name; + this.tooltip = options.tooltip; + this.danger = options.danger; + + this.offset = options.offset; + this.size = options.size; + this.min = options.min; + this.max = options.max; + } + + createUI(parent) { + var id = createID(); + var label = this.name; + var patch = createElementClass('div', 'patch'); + + patch.appendChild(createLabel(label, id)); + + this.number = createInput('number', id); + if (this.min !== null) { + this.number.min = this.min; + } + if (this.max) { + this.number.max = this.max; + } + + patch.appendChild(this.number); + + + if (this.tooltip) { + patch.appendChild(createElementClass('div', 'tooltip', this.tooltip)); + } + if (this.danger) { + patch.appendChild(createElementClass('div', 'danger tooltip', this.danger)); + } + parent.appendChild(patch); + + } + + updateUI(file) { + // This converts bytes from the file to big endian by shifting each + // byte `i` bytes to the left then doing a bitwise OR to add the less + // significant bytes that were gathered at earlier iterations of loop + var val = 0; + for (var i = 0; i < this.size; i++) { + val = (file[this.offset + i] << (8 * i)) | val; + } + + this.number.value = val; + } + + validatePatch(file) { + return; + } + + applyPatch(file) { + // Convert user inputted number to little endian + const view = new DataView(new ArrayBuffer(this.size * 2)); + view.setInt32(1, this.number.value, true); + + for (var i = 0; i < this.size; i++) { + var val = view.getInt32(1); + + // Shift off less significant bytes + val = val >> ((this.size - 1 - i) * 8); + + // Mask off more significant bytes + val = val & 0xFF; + + // Write this byte + file[this.offset + i] = val; + } + } +} + +var loadPatch = function(_this, self, patcher) { + patcher.loadPatchUI(); + patcher.updatePatchUI(); + patcher.container.style.display = ''; + var successStr = patcher.filename; + if (typeof _this.description === "string") { + successStr += "(" + patcher.description + ")"; + } + self.successDiv.innerHTML = successStr + " loaded successfully!"; +}; + +class PatchContainer { + constructor(patchers) { + this.patchers = patchers; + this.createUI(); + } + + getSupportedDLLs() { + var dlls = []; + for (var i = 0; i < this.patchers.length; i++) { + var name = this.patchers[i].filename; + if (dlls.indexOf(name) === -1) { + dlls.push(name); + } + } + return dlls; + } + + createUI() { + var self = this; + var container = createElementClass('div', 'patchContainer'); + var header = this.getSupportedDLLs().join(", "); + container.innerHTML = "

" + header + "

"; + + var supportedDlls = document.createElement('ul'); + this.forceLoadTexts = []; + this.forceLoadButtons = []; + this.matchSuccessText = []; + for (var i = 0; i < this.patchers.length; i++) { + var checkboxId = createID(); + + var listItem = document.createElement('li'); + listItem.appendChild(createLabel(this.patchers[i].description, checkboxId, 'patchPreviewLabel')); + var matchPercent = createElementClass('span', 'matchPercent'); + this.forceLoadTexts.push(matchPercent); + listItem.appendChild(matchPercent); + var matchSuccess = createElementClass('span', 'matchSuccess'); + this.matchSuccessText.push(matchSuccess); + listItem.appendChild(matchSuccess); + var forceButton = createElementClass('button', '', 'Force load?'); + forceButton.style.display = 'none'; + this.forceLoadButtons.push(forceButton); + listItem.appendChild(forceButton); + + var input = createInput('checkbox', checkboxId, 'patchPreviewToggle'); + listItem.appendChild(input); + var patchPreviews = createElementClass('ul', 'patchPreview'); + for (var j = 0; j < this.patchers[i].mods.length; j++) { + var patchName = this.patchers[i].mods[j].name; + patchPreviews.appendChild(createElementClass('li', null, patchName)); + } + listItem.appendChild(patchPreviews); + + supportedDlls.appendChild(listItem); + } + + ["dragover", "dragenter"].forEach(function(n){ + document.documentElement.addEventListener(n,function (e) { + container.classList.add("dragover"); + e.preventDefault(); + e.stopPropagation(); + }); + }); + ["dragleave", "dragend", "drop"].forEach(function(n){ + document.documentElement.addEventListener(n,function (e) { + container.classList.remove("dragover"); + e.preventDefault(); + e.stopPropagation(); + }); + }); + + container.addEventListener("drop", function (e) { + var files = e.dataTransfer.files; + if (files && files.length > 0) + self.loadFile(files[0]); + }); + + var filepickerId = createID(); + this.fileInput = createInput('file', filepickerId, 'fileInput'); + var label = createLabel('', filepickerId, 'fileLabel'); + label.innerHTML = "Choose a file or drag and drop."; + + this.fileInput.addEventListener("change", function (e) { + if (this.files && this.files.length > 0) + self.loadFile(this.files[0]); + }); + + this.successDiv = createElementClass('div', 'success'); + this.errorDiv = createElementClass('div', 'error'); + + container.appendChild(this.fileInput); + container.appendChild(label); + + container.appendChild(createElementClass('h4', null, 'Supported Versions:')); + container.appendChild(createElementClass('h5', null, 'Click name to preview patches')); + container.appendChild(supportedDlls); + container.appendChild(this.successDiv); + container.appendChild(this.errorDiv); + document.body.appendChild(container); + } + + loadFile(file) { + var reader = new FileReader(); + var self = this; + + reader.onload = function (e) { + var found = false; + // clear logs + self.errorDiv.textContent = ''; + self.successDiv.textContent = ''; + for (var i = 0; i < self.patchers.length; i++) { + // reset text and buttons + self.forceLoadButtons[i].style.display = 'none'; + self.forceLoadTexts[i].textContent = ''; + self.matchSuccessText[i].textContent = ''; + var patcher = self.patchers[i]; + // remove the previous UI to clear the page + patcher.destroyUI(); + // patcher UI elements have to exist to load the file + patcher.createUI(); + patcher.container.style.display = 'none'; + patcher.loadBuffer(e.target.result); + if (patcher.validatePatches()) { + found = true; + loadPatch(this, self, patcher); + // show patches matched for 100% - helps identify which version is loaded + var valid = patcher.validPatches; + self.matchSuccessText[i].textContent = ' ' + valid + ' of ' + valid + ' patches matched (100%) '; + } + } + + if (!found) { + // let the user force a match + for (let i = 0; i < self.patchers.length; i++) { + const patcher = self.patchers[i]; + + const valid = patcher.validPatches; + const percent = (valid / patcher.totalPatches * 100).toFixed(1); + + self.forceLoadTexts[i].textContent = ' ' + valid + ' of ' + patcher.totalPatches + ' patches matched (' + percent + '%) '; + self.forceLoadButtons[i].style.display = ''; + self.forceLoadButtons[i].onclick = function(i) { + // reset old text + for(var j = 0; j < self.patchers.length; j++) { + self.forceLoadButtons[j].style.display = 'none'; + self.forceLoadTexts[j].textContent = ''; + } + + + loadPatch(this, self, self.patchers[i]); + }.bind(this, i); + } + self.errorDiv.innerHTML = "No patch set was a 100% match."; + } + }; + + reader.readAsArrayBuffer(file); + } +} + +class Patcher { + constructor(fname, description, args) { + this.mods = []; + for(var i = 0; i < args.length; i++) { + var mod = args[i]; + if(mod.type) { + if(mod.type === "union") { + this.mods.push(new UnionPatch(mod)); + } + if(mod.type === "number") { + this.mods.push(new NumberPatch(mod)); + } + if(mod.type === "dynamic") { + this.mods.push(new DynamicPatch(mod)); + } + } else { // standard patch + this.mods.push(new StandardPatch(mod)); + } + } + + this.filename = fname; + this.description = description; + this.multiPatcher = true; + + if (!this.description) { + // old style patcher, use the old method to generate the UI + this.multiPatcher = false; + this.createUI(); + this.loadPatchUI(); + } + } + + createUI() { + var self = this; + this.container = createElementClass('div', 'patchContainer'); + var header = this.filename; + if(this.description === "string") { + header += ' (' + this.description + ')'; + } + this.container.innerHTML = '

' + header + '

'; + + this.successDiv = createElementClass('div', 'success'); + this.errorDiv = createElementClass('div', 'error'); + this.patchDiv = createElementClass('div', 'patches'); + + var saveButton = document.createElement('button'); + saveButton.disabled = true; + saveButton.textContent = 'Load file First'; + saveButton.addEventListener('click', this.saveDll.bind(this)); + this.saveButton = saveButton; + + if (!this.multiPatcher) { + ["dragover", "dragenter"].forEach(function(n){ + document.documentElement.addEventListener(n,function(e) { + self.container.classList.add('dragover'); + e.preventDefault(); + return true; + }); + }); + ["dragleave", "dragend", "drop"].forEach(function(n){ + document.documentElement.addEventListener(n,function(e) { + self.container.classList.remove('dragover'); + e.preventDefault(); + return true; + }); + }); + + this.container.addEventListener('drop', function(e) { + var files = e.dataTransfer.files; + if(files && files.length > 0) + self.loadFile(files[0]); + }); + + var filepickerId = createID(); + this.fileInput = createInput('file', filepickerId, 'fileInput'); + var label = createLabel('', filepickerId, 'fileLabel'); + label.innerHTML = 'Choose a file or drag and drop.'; + + this.fileInput.addEventListener('change', function(e) { + if(this.files && this.files.length > 0) + self.loadFile(this.files[0]); + }); + + this.container.appendChild(this.fileInput); + this.container.appendChild(label); + } + + this.container.appendChild(this.successDiv); + this.container.appendChild(this.errorDiv); + this.container.appendChild(this.patchDiv); + this.container.appendChild(saveButton); + document.body.appendChild(this.container); + } + + destroyUI() { + if (this.hasOwnProperty("container")) + this.container.remove(); + } + + loadBuffer(buffer) { + this.dllFile = new Uint8Array(buffer); + if(this.validatePatches()) { + this.successDiv.classList.remove("hidden"); + this.successDiv.innerHTML = "File loaded successfully!"; + } else { + this.successDiv.classList.add("hidden"); + } + // Update save button regardless + this.saveButton.disabled = false; + this.saveButton.textContent = 'Save Patched File'; + this.errorDiv.innerHTML = this.errorLog; + } + + loadFile(file) { + var reader = new FileReader(); + var self = this; + + reader.onload = function(e) { + self.loadBuffer(e.target.result); + self.updatePatchUI(); + }; + + reader.readAsArrayBuffer(file); + } + + downloadURI(uri, filename) { + // http://stackoverflow.com/a/18197341 + var element = document.createElement('a'); + element.setAttribute('href', uri); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + } + + saveDll() { + if(!this.dllFile || !this.mods || !this.filename) + return; + + for(var i = 0; i < this.mods.length; i++) { + this.mods[i].applyPatch(this.dllFile); + } + + var blob = new Blob([this.dllFile], {type: "application/octet-stream"}); + var uri = URL.createObjectURL(blob); + this.downloadURI(uri, this.filename); + URL.revokeObjectURL(uri); + } + + loadPatchUI() { + for(var i = 0; i < this.mods.length; i++) { + this.mods[i].createUI(this.patchDiv); + } + } + + updatePatchUI() { + for(var i = 0; i < this.mods.length; i++) { + this.mods[i].updateUI(this.dllFile); + } + } + + validatePatches() { + this.errorLog = ""; + var success = true; + this.validPatches = 0; + this.totalPatches = this.mods.length; + for(var i = 0; i < this.mods.length; i++) { + var error = this.mods[i].validatePatch(this.dllFile); + if(error) { + this.errorLog += error + "
"; + success = false; + } else { + this.validPatches++; + } + } + return success; + } +} + +window.Patcher = Patcher; +window.PatchContainer = PatchContainer; + +})(window, document); diff --git a/README.md b/README.md index f068ef1..9437931 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A collection of tools and other hacky (read: ugly) things I've made for myself, * galliumhook - injects `mercuryhook.dll` (whatever that may be) and forces created windows to be 1080x1920 regardless of screen resolution * WTT - translation tool that exports message tables to and imports from toml files [(readme)](WTT/README.md) +* MercuryPatcher - web UI that applies some patches, derived from BemaniPatcher [(go there)](https://raymonf.github.io/whack/MercuryPatcher/) ### Changelog