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
This commit is contained in:
ichijyo-hotaru 2021-09-20 20:26:04 +09:00 committed by GitHub
parent 7ee6560d4b
commit c87d450c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 272 additions and 0 deletions

View File

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

View File

@ -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 = $('<div>', {'class' : 'patch'});
this.checkbox = $('<input type="checkbox" id="' + id + '">')[0];
this.ui.append(this.checkbox);
this.ui.append('<label for="' + id + '">' + label + '</label>');
if(this.tooltip) {
this.ui.append('<div class="tooltip">' + this.tooltip + '</div>');
}
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 = $('<ul />');
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('<li class="patch-off">0x' + offOffset.toString(16) + ' <b>' + patch.off + '</b> will be replaced with <b>'+ patch.on +'</b></li>');
} else {
listUi.append('<li class="patch-off">0x' + offOffset.toString(16) + ' will be replaced</li>');
}
}
if(patchStatus === "") {
patchStatus = "off";
}
} else if(onOffset > 0) {
if (updateUiFlag) {
if (this.target === 'string') {
listUi.append('<li class="patch-on">0x' + onOffset.toString(16) + ' <b>' + patch.on + '</b> will be replaced with <b>'+ patch.off +'</b></li>');
} else {
listUi.append('<li class="patch-on">0x' + onOffset.toString(16) + ' will be replaced</li>');
}
}
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 = $('<ul />');
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('<li class="patch-off">0x' + offset.toString(16) + ' will be replaced</li>');
});
}
if(patchStatus === "") {
patchStatus = "off";
}
} else if(onOffset.length > 0) {
if (updateUiFlag) {
onOffset.forEach((offset) => {
listUi.append('<li class="patch-on">0x' + offset.toString(16) + ' will be replaced</li>');
});
}
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));
}