/*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; } 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)); } 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.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)); } 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; } 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)); } 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)); } 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.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)); } 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 = "