1
0
mirror of https://github.com/Raymonf/whack.git synced 2024-09-23 17:38:20 +02:00

Add MercuryPatcher

This commit is contained in:
Raymonf 2022-11-10 01:21:38 -05:00
parent 2ce8d58ed8
commit 9780ac7eb4
No known key found for this signature in database
GPG Key ID: 438459BF619B037A
10 changed files with 1351 additions and 0 deletions

21
MercuryPatcher/LICENSE Normal file
View File

@ -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.

4
MercuryPatcher/README.md Normal file
View File

@ -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.

BIN
MercuryPatcher/css/file.eot Normal file

Binary file not shown.

View File

@ -0,0 +1,11 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="file-binary" horiz-adv-x="768" d="M0 0v896h576l192-192v-704h-768zM704 640l-192 192h-448v-768h640v576zM320 448h-192v256h192v-256zM256 640h-64v-128h64v128zM256 192h64v-64h-192v64h64v128h-64v64h128v-192zM512 512h64v-64h-192v64h64v128h-64v64h128v-192zM576 128h-192v256h192v-256zM512 320h-64v-128h64v128z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 777 B

BIN
MercuryPatcher/css/file.ttf Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,359 @@
@font-face {
font-family: 'file';
src: url('file.eot?abyff3');
src: url('file.eot?abyff3#iefix') format('embedded-opentype'),
url('file.ttf?abyff3') format('truetype'),
url('file.woff?abyff3') format('woff'),
url('file.svg?abyff3#file') format('svg');
font-weight: normal;
font-style: normal;
}
.tagline {
font-family: monospace;
}
.icons, .subsection {
display: grid;
grid-template-columns: repeat(auto-fit, 180px);
grid-auto-flow: dense;
align-items: stretch;
margin: 0 auto;
text-align: center;
justify-content: center;
}
.subsection {
grid-column: 1 / -1;
background-color: #a2a2a2;
margin: unset;
/* I don't understand anything */
margin-right: -15px;
padding-right: 15px;
}
.gameicon,
.patchContainer {
border-radius: 2px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
color: inherit;
transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
background: #fff;
}
.gameicon {
display: flex;
flex-direction: column;
text-decoration: none;
margin: 15px;
width: 144px;
padding: 10px;
justify-content: center;
}
label.gameicon {
cursor: pointer;
background: #fffcf0;
}
input.sectionToggle,
input.sectionToggle + div {
display: none;
}
input:checked.sectionToggle + div {
display: grid;
}
.gameicon:hover,
.dragover {
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
.gameicon img {
margin-bottom: 10px;
border-radius: 2px;
width: 128px;
height: 128px;
box-shadow: 0 2px 1px rgba(0, 0, 0, .24), 0 0px 1px rgba(0, 0, 0, 0.48);
align-self: center;
margin-top: 5px;
}
.gameicon>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;
}

34
MercuryPatcher/index.html Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Mercury Patcher</title>
<link rel='stylesheet' href='css/style.css'>
<script type='text/javascript' src='js/dllpatcher.js'></script>
<script type="text/javascript">
window.addEventListener("load", function () {
new PatchContainer([
new Patcher('Mercury-Win64-Shipping.exe', "3.07.01", [
{
name: 'Fully disable lockout',
patches: [
{offset : 0x4EF970, off: [0xE8, 0x2B, 0x5A, 0xE4, 0xFF, 0x45, 0x33, 0xC0, 0x41, 0x8D, 0x50, 0x04, 0x48, 0x8B, 0xCB, 0xE8, 0x0C, 0xAC, 0xED, 0xFF, 0x84, 0xC0, 0x75, 0x12, 0xBA, 0x06], on : [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xBA, 0x00]},
{offset : 0x4EF9A6, off: [0xBA, 0x02], on : [0xBA, 0x00]},
{offset : 0x4EF9F2, off: [0x48, 0x8D, 0x55, 0x20, 0x48], on : [0xE9, 0x79, 0xFF, 0xFF, 0xFF]},
]
},
{
name: 'Force default engine locale (ignore router country language)',
patches: [
{offset : 0x558154, off: [0x74, 0x07], on : [0x90, 0x90]},
]
},
]),
]);
});
</script>
</head>
<body>
<h1>Mercury Patcher</h1>
</body>
</html>

View File

@ -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) + ' <b>' + patch.off + '</b> will be replaced with <b>'+ patch.on +'</b>'));
} 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) + ' <b>' + patch.on + '</b> will be replaced with <b>'+ patch.off +'</b>'));
} 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 = "<h3>" + header + "</h3>";
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 = "<strong>Choose a file</strong> 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 = '<h3>' + header + '</h3>';
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 = '<strong>Choose a file</strong> 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 + "<br/>";
success = false;
} else {
this.validPatches++;
}
}
return success;
}
}
window.Patcher = Patcher;
window.PatchContainer = PatchContainer;
})(window, document);

View File

@ -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