From c87d450c463d8beefd8c46b64e40e75541b8805f Mon Sep 17 00:00:00 2001
From: ichijyo-hotaru <90632312+ichijyo-hotaru@users.noreply.github.com>
Date: Mon, 20 Sep 2021 20:26:04 +0900
Subject: [PATCH] support dynamic patcher (#224)
* support dynamic patcher
* fix typo
* fix createUI()
* delete sample html
* merge dynamicHex and dynamicString patch
* fix dynamic patch constructor
* use strict equality operator
---
css/style.css | 12 +++
js/dllpatcher.js | 260 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 272 insertions(+)
diff --git a/css/style.css b/css/style.css
index 4d7c548..e2bfbf9 100644
--- a/css/style.css
+++ b/css/style.css
@@ -312,3 +312,15 @@ input[type=radio] {
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;
+}
\ No newline at end of file
diff --git a/js/dllpatcher.js b/js/dllpatcher.js
index 725cad7..7f114da 100644
--- a/js/dllpatcher.js
+++ b/js/dllpatcher.js
@@ -103,6 +103,263 @@ class StandardPatch {
}
}
+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 = $('
', {'class' : 'patch'});
+ this.checkbox = $('
')[0];
+ this.ui.append(this.checkbox);
+ this.ui.append('
');
+ if(this.tooltip) {
+ this.ui.append('
' + this.tooltip + '
');
+ }
+ parent.append(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(var i = 0; i < this.patches.length; i++) {
+ if (Array.isArray(this.patches[i].offset)) {
+ this.patches[i].offset.forEach((offset) => {
+ if (this.target === 'string') {
+ replace(file, offset,
+ new TextEncoder().encode(featureOn? this.patches[i].on : this.patches[i].off));
+ } else {
+ this.patches[i].on = this.patches[i].on.map((patch, idx) => patch === 'XX' ? file[offset + idx] : patch);
+ this.patches[i].off = this.patches[i].off.map((patch, idx) => patch === 'XX' ? file[offset + idx] : patch);
+ replace(file, offset,
+ featureOn? this.patches[i].on : this.patches[i].off)
+ }
+ }
+ );
+ } else {
+ if (this.target === 'string') {
+ replace(file, this.patches[i].offset,
+ new TextEncoder().encode(featureOn? this.patches[i].on : this.patches[i].off));
+ } else {
+ this.patches[i].on = this.patches[i].on.map((patch, idx) => patch === 'XX' ? file[this.patches[i].offset + idx] : patch);
+ this.patches[i].off = this.patches[i].off.map((patch, idx) => patch === 'XX' ? file[this.patches[i].offset + idx] : patch);
+ replace(file, this.patches[i].offset,
+ featureOn? this.patches[i].on : this.patches[i].off);
+ }
+ }
+ }
+ }
+
+ checkPatch(file, updateUiFlag = false) {
+ var patchStatus = "";
+ if (updateUiFlag) {
+ var listUi = $('
');
+ this.ui.append(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.append('
0x' + offOffset.toString(16) + ' ' + patch.off + ' will be replaced with '+ patch.on +'');
+ } else {
+ listUi.append('
0x' + offOffset.toString(16) + ' will be replaced');
+ }
+ }
+ if(patchStatus === "") {
+ patchStatus = "off";
+ }
+ } else if(onOffset > 0) {
+ if (updateUiFlag) {
+ if (this.target === 'string') {
+ listUi.append('
0x' + onOffset.toString(16) + ' ' + patch.on + ' will be replaced with '+ patch.off +'');
+ } else {
+ listUi.append('
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 = "";
+ if (updateUiFlag) {
+ var listUi = $('
');
+ this.ui.append(listUi);
+ }
+ for(var i = 0; i < this.patches.length; i++) {
+ var patch = this.patches[i];
+ var offOffset = this.searchPatchOffsetAll(file, patch.off);
+ var onOffset = this.searchPatchOffsetAll(file, patch.on);
+ this.patches[i].offset = offOffset.length === 0 ? onOffset : offOffset;
+
+ if(offOffset.length > 0) {
+ if (updateUiFlag) {
+ offOffset.forEach((offset) => {
+ listUi.append('
0x' + offset.toString(16) + ' will be replaced');
+ });
+ }
+ if(patchStatus === "") {
+ patchStatus = "off";
+ }
+ } else if(onOffset.length > 0) {
+ if (updateUiFlag) {
+ onOffset.forEach((offset) => {
+ listUi.append('
0x' + offset.toString(16) + ' will be replaced');
+ });
+ }
+ if(patchStatus === "") {
+ patchStatus = "on";
+ }
+ } else {
+ return "patch string not found";
+ }
+ }
+ return patchStatus;
+ }
+
+ searchPatchOffset(file, search, offset) {
+ if (this.target === 'string') {
+ var searchBytes = new TextEncoder().encode(search);
+ } else {
+ var 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) {
+ if (this.target === 'string') {
+ var searchBytes = new TextEncoder().encode(search);
+ } else {
+ var 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
@@ -447,6 +704,9 @@ class Patcher {
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));
}