Support EBP patches
This commit is contained in:
parent
bf438bde16
commit
d39682952d
17 changed files with 307 additions and 19 deletions
|
@ -38,6 +38,7 @@
|
|||
<data android:pathPattern="/.*\\.ups" />
|
||||
<data android:pathPattern="/.*\\.bps" />
|
||||
<data android:pathPattern="/.*\\.ppf" />
|
||||
<data android:pathPattern="/.*\\.ebp" />
|
||||
<data android:pathPattern="/.*\\.dps" />
|
||||
<data android:pathPattern="/.*\\.xdelta" />
|
||||
<data android:pathPattern="/.*\\.xdelta3" />
|
||||
|
|
BIN
app/src/main/assets/patch/ebp/wrong1.ips
Normal file
BIN
app/src/main/assets/patch/ebp/wrong1.ips
Normal file
Binary file not shown.
BIN
app/src/main/assets/patch/ebp/wrong2.ips
Normal file
BIN
app/src/main/assets/patch/ebp/wrong2.ips
Normal file
Binary file not shown.
BIN
app/src/main/assets/patch/ebp/wrong3.ips
Normal file
BIN
app/src/main/assets/patch/ebp/wrong3.ips
Normal file
Binary file not shown.
BIN
app/src/main/assets/patch/ebp/wrong4.ips
Normal file
BIN
app/src/main/assets/patch/ebp/wrong4.ips
Normal file
Binary file not shown.
BIN
app/src/main/assets/patch/ebp/wrong5.ips
Normal file
BIN
app/src/main/assets/patch/ebp/wrong5.ips
Normal file
Binary file not shown.
BIN
app/src/main/assets/patch/ebp/wrong6.ips
Normal file
BIN
app/src/main/assets/patch/ebp/wrong6.ips
Normal file
Binary file not shown.
|
@ -139,6 +139,14 @@ public class Utils {
|
|||
}
|
||||
}
|
||||
|
||||
public static String bytesToHexString(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i = 0; i < bytes.length ;i++) {
|
||||
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static boolean isPatch(File file) {
|
||||
String[] patches = {"ips", "ups", "bps", "ppf", "dps", "xdelta", "xdelta3", "vcdiff"};
|
||||
String ext = FilenameUtils.getExtension(file.getName()).toLowerCase(Locale.getDefault());
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.apache.commons.io.FileUtils;
|
|||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.emunix.unipatcher.patch.BPS;
|
||||
import org.emunix.unipatcher.patch.DPS;
|
||||
import org.emunix.unipatcher.patch.EBP;
|
||||
import org.emunix.unipatcher.patch.IPS;
|
||||
import org.emunix.unipatcher.patch.PPF;
|
||||
import org.emunix.unipatcher.patch.Patch;
|
||||
|
@ -118,6 +119,8 @@ public class WorkerService extends IntentService {
|
|||
patcher = new BPS(this, patchFile, romFile, outputFile);
|
||||
else if ("ppf".equals(ext))
|
||||
patcher = new PPF(this, patchFile, romFile, outputFile);
|
||||
else if ("ebp".equals(ext))
|
||||
patcher = new EBP(this, patchFile, romFile, outputFile);
|
||||
else if ("dps".equals(ext))
|
||||
patcher = new DPS(this, patchFile, romFile, outputFile);
|
||||
else if ("xdelta".equals(ext) || "xdelta3".equals(ext) || "vcdiff".equals(ext))
|
||||
|
@ -218,7 +221,7 @@ public class WorkerService extends IntentService {
|
|||
startForeground(notify.getID(), notify.getNotifyBuilder().build());
|
||||
|
||||
try {
|
||||
worker.deleteSnesSmcHeader(this, romFile);
|
||||
worker.deleteSnesSmcHeader(this, romFile, true);
|
||||
} catch (RomException | IOException e) {
|
||||
if (Utils.getFreeSpace(romFile.getParentFile()) == 0) {
|
||||
errorMsg = getString(R.string.notify_error_not_enough_space);
|
||||
|
|
275
app/src/main/java/org/emunix/unipatcher/patch/EBP.java
Normal file
275
app/src/main/java/org/emunix/unipatcher/patch/EBP.java
Normal file
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
This file based on source code of EBPatcher by Marc Gagné (https://github.com/Lyrositor/EBPatcher)
|
||||
|
||||
Copyright (C) 2016 Boris Timofeev
|
||||
|
||||
This file is part of UniPatcher.
|
||||
|
||||
UniPatcher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
UniPatcher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with UniPatcher. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.emunix.unipatcher.patch;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.emunix.unipatcher.R;
|
||||
import org.emunix.unipatcher.Utils;
|
||||
import org.emunix.unipatcher.tools.RomException;
|
||||
import org.emunix.unipatcher.tools.SnesSmcHeader;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class EBP extends Patch {
|
||||
|
||||
private static final byte[] MAGIC_NUMBER = {0x50, 0x41, 0x54, 0x43, 0x48}; // "PATCH"
|
||||
private static final byte[] EARTH_BOUND = {0x45, 0x41, 0x52, 0x54, 0x48, 0x20, 0x42, 0x4f, 0x55, 0x4e, 0x44};
|
||||
private static final String EB_CLEAN_MD5 = "a864b2e5c141d2dec1c4cbed75a42a85";
|
||||
private static final int EB_CLEAN_ROM_SIZE = 0x300000;
|
||||
private static final HashMap<String, String> EB_WRONG_MD5;
|
||||
|
||||
static {
|
||||
EB_WRONG_MD5 = new HashMap<>();
|
||||
EB_WRONG_MD5.put("8c28ce81c7d359cf9ccaa00d41f8ad33", "patch/ebp/wrong1.ips");
|
||||
EB_WRONG_MD5.put("b2dcafd3252cc4697bf4b89ea3358cd5", "patch/ebp/wrong2.ips");
|
||||
EB_WRONG_MD5.put("0b8c04fc0182e380ff0e3fe8fdd3b183", "patch/ebp/wrong3.ips");
|
||||
EB_WRONG_MD5.put("2225f8a979296b7dcccdda17b6a4f575", "patch/ebp/wrong4.ips");
|
||||
EB_WRONG_MD5.put("eb83b9b6ea5692cefe06e54ea3ec9394", "patch/ebp/wrong5.ips");
|
||||
EB_WRONG_MD5.put("cc9fa297e7bf9af21f7f179e657f1aa1", "patch/ebp/wrong6.ips");
|
||||
}
|
||||
|
||||
public EBP(Context context, File patch, File rom, File output) {
|
||||
super(context, patch, rom, output);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() throws PatchException, IOException {
|
||||
File cleanRom = File.createTempFile("rom", null, context.getCacheDir());
|
||||
File ipsPatch = File.createTempFile("patch", null, context.getCacheDir());
|
||||
try {
|
||||
Utils.copyFile(context, romFile, cleanRom);
|
||||
prepareCleanRom(cleanRom);
|
||||
|
||||
EBPtoIPS(patchFile, ipsPatch);
|
||||
|
||||
IPS ips = new IPS(context, ipsPatch, cleanRom, outputFile);
|
||||
ips.apply();
|
||||
} finally {
|
||||
FileUtils.deleteQuietly(ipsPatch);
|
||||
FileUtils.deleteQuietly(cleanRom);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareCleanRom(File file) throws IOException, PatchException {
|
||||
// delete smc header
|
||||
SnesSmcHeader smc = new SnesSmcHeader();
|
||||
try {
|
||||
smc.deleteSnesSmcHeader(context, file, false);
|
||||
} catch (RomException e) {
|
||||
// no header
|
||||
}
|
||||
|
||||
// check rom size and remove unused expanded space
|
||||
if (file.length() < EB_CLEAN_ROM_SIZE)
|
||||
throw new PatchException(context.getString(R.string.notify_error_rom_not_compatible_with_patch));
|
||||
if (file.length() > EB_CLEAN_ROM_SIZE && checkExpanded(file))
|
||||
removeExpanded(file);
|
||||
|
||||
// try to fix the ROM if it's incorrect
|
||||
if (!checkMD5(file))
|
||||
repairRom(file);
|
||||
|
||||
// if we couldn't fix the ROM, try to remove a 0xff byte at the end.
|
||||
if (!checkMD5(file)) {
|
||||
int length = (int)file.length();
|
||||
byte[] buffer = new byte[length];
|
||||
FileInputStream in = new FileInputStream(file);
|
||||
int count = in.read(buffer);
|
||||
in.close();
|
||||
if (count != file.length())
|
||||
throw new IOException("Unable read file");
|
||||
if (buffer[length - 1] == 0xff)
|
||||
buffer[length - 1] = 0;
|
||||
|
||||
if (checkMD5(buffer)) {
|
||||
RandomAccessFile f = new RandomAccessFile(file, "rw");
|
||||
f.seek(length - 1);
|
||||
f.write(0);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!checkMD5(file) || !checkEarthBound(file)) {
|
||||
throw new PatchException(context.getString(R.string.notify_error_rom_not_compatible_with_patch));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkExpanded(File file) throws IOException {
|
||||
byte[] byteArray = new byte[EB_CLEAN_ROM_SIZE];
|
||||
FileInputStream f = new FileInputStream(file);
|
||||
int count = f.read(byteArray);
|
||||
IOUtils.closeQuietly(f);
|
||||
if (count < EB_CLEAN_ROM_SIZE)
|
||||
throw new IOException("Unable to read 0x300000 bytes from ROM");
|
||||
// ExHiROM expanded ROMs have two bytes different from LoROM.
|
||||
byteArray[0xffd5] = 0x31;
|
||||
byteArray[0xffd7] = 0x0c;
|
||||
|
||||
// If the normal area is unmodified, then the expanded area is unused and can be deleted.
|
||||
return checkMD5(byteArray);
|
||||
}
|
||||
|
||||
private void removeExpanded(File file) throws IOException {
|
||||
if (file.length() > 0x400000) {
|
||||
RandomAccessFile f = new RandomAccessFile(file, "rw");
|
||||
f.seek(0xffd5);
|
||||
f.write(0x31);
|
||||
f.seek(0xffd7);
|
||||
f.write(0x0c);
|
||||
f.close();
|
||||
}
|
||||
FileChannel fc = new FileOutputStream(file, true).getChannel();
|
||||
fc.truncate(EB_CLEAN_ROM_SIZE);
|
||||
fc.close();
|
||||
}
|
||||
|
||||
private void repairRom(File file) throws IOException, PatchException {
|
||||
String md5 = calculateMD5(file);
|
||||
if (EB_WRONG_MD5.containsKey(md5)) {
|
||||
|
||||
// copy patch from assets
|
||||
InputStream in = context.getAssets().open(EB_WRONG_MD5.get(md5));
|
||||
File patch = File.createTempFile("patch", null, context.getCacheDir());
|
||||
FileUtils.copyToFile(in, patch);
|
||||
IOUtils.closeQuietly(in);
|
||||
|
||||
// fix rom
|
||||
File tmpFile = File.createTempFile("rom", null, context.getCacheDir());
|
||||
FileUtils.copyFile(file, tmpFile);
|
||||
IPS ips = new IPS(context, patch, tmpFile, file);
|
||||
ips.apply();
|
||||
|
||||
FileUtils.deleteQuietly(tmpFile);
|
||||
FileUtils.deleteQuietly(patch);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkMD5(byte[] array) throws IOException {
|
||||
try {
|
||||
MessageDigest md5Digest = MessageDigest.getInstance("MD5");
|
||||
md5Digest.update(array);
|
||||
String md5 = Utils.bytesToHexString(md5Digest.digest());
|
||||
return md5.equals(EB_CLEAN_MD5);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkMD5(File file) throws IOException {
|
||||
String md5 = calculateMD5(file);
|
||||
return md5.equals(EB_CLEAN_MD5);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String calculateMD5(File file) throws IOException {
|
||||
FileInputStream f = new FileInputStream(file);
|
||||
try {
|
||||
MessageDigest md5Digest = MessageDigest.getInstance("MD5");
|
||||
byte[] byteArray = new byte[32768];
|
||||
int count;
|
||||
while ((count = f.read(byteArray)) != -1)
|
||||
md5Digest.update(byteArray, 0, count);
|
||||
return Utils.bytesToHexString(md5Digest.digest());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
} finally {
|
||||
IOUtils.closeQuietly(f);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkEarthBound(File file) throws IOException {
|
||||
byte[] buffer = new byte[11];
|
||||
RandomAccessFile f = new RandomAccessFile(file, "r");
|
||||
f.seek(0xffc0);
|
||||
f.read(buffer);
|
||||
f.close();
|
||||
return Arrays.equals(EARTH_BOUND, buffer);
|
||||
}
|
||||
|
||||
private void EBPtoIPS(File ebpFile, File ipsFile) throws IOException, PatchException {
|
||||
BufferedInputStream ebp = null;
|
||||
BufferedOutputStream ips = null;
|
||||
try {
|
||||
ebp = new BufferedInputStream(new FileInputStream(ebpFile));
|
||||
ips = new BufferedOutputStream(new FileOutputStream(ipsFile));
|
||||
|
||||
int size;
|
||||
byte[] buffer = new byte[65536];
|
||||
|
||||
if (ebpFile.length() < 14) {
|
||||
throw new PatchException(context.getString(R.string.notify_error_patch_corrupted));
|
||||
}
|
||||
|
||||
// check magic string
|
||||
byte[] magic = new byte[5];
|
||||
size = ebp.read(magic);
|
||||
if (size != 5 || !Arrays.equals(magic, MAGIC_NUMBER))
|
||||
throw new PatchException(context.getString(R.string.notify_error_not_ebp_patch));
|
||||
|
||||
ips.write(magic);
|
||||
|
||||
while (true) {
|
||||
size = ebp.read(buffer, 0, 3);
|
||||
if (size < 3)
|
||||
throw new PatchException(context.getString(R.string.notify_error_patch_corrupted));
|
||||
ips.write(buffer, 0, 3);
|
||||
if (buffer[0] == 0x45 && buffer[1] == 0x4f && buffer[2] == 0x46) // EOF
|
||||
break;
|
||||
size = ebp.read(buffer, 0, 2);
|
||||
if (size < 2)
|
||||
throw new PatchException(context.getString(R.string.notify_error_patch_corrupted));
|
||||
ips.write(buffer, 0, 2);
|
||||
size = (((int)buffer[0] & 0xff) << 8) + ((int)buffer[1] & 0xff);
|
||||
if (size != 0) {
|
||||
int c = ebp.read(buffer, 0, size);
|
||||
if (c < size)
|
||||
throw new PatchException(context.getString(R.string.notify_error_patch_corrupted));
|
||||
ips.write(buffer, 0, size);
|
||||
} else {
|
||||
size = ebp.read(buffer, 0, 3);
|
||||
if (size < 3)
|
||||
throw new PatchException(context.getString(R.string.notify_error_patch_corrupted));
|
||||
ips.write(buffer, 0, 3);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
IOUtils.closeQuietly(ips);
|
||||
IOUtils.closeQuietly(ebp);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,9 +39,9 @@ public class SnesSmcHeader {
|
|||
return (romSize & 0x7fff) == 512;
|
||||
}
|
||||
|
||||
public void deleteSnesSmcHeader(Context context, File romfile) throws IOException, RomException {
|
||||
public void deleteSnesSmcHeader(Context context, File romfile, boolean saveHeader) throws IOException, RomException {
|
||||
if (!isHasSmcHeader(romfile)) {
|
||||
throw new RomException();
|
||||
throw new RomException("ROM don't have SMC header");
|
||||
}
|
||||
|
||||
FileInputStream inputRom = null;
|
||||
|
@ -51,17 +51,19 @@ public class SnesSmcHeader {
|
|||
|
||||
try {
|
||||
tmpfile = File.createTempFile(romfile.getName(), null, romfile.getParentFile());
|
||||
File headerfile = new File(romfile.getPath()+".smc_header");
|
||||
|
||||
inputRom = new FileInputStream(romfile);
|
||||
outputRom = new FileOutputStream(tmpfile);
|
||||
outputHeader = new FileOutputStream(headerfile);
|
||||
|
||||
// write smc header in a file
|
||||
byte[] header = new byte[HEADER_SIZE];
|
||||
int length;
|
||||
length = inputRom.read(header);
|
||||
outputHeader.write(header, 0, length);
|
||||
if (saveHeader) {
|
||||
File headerfile = new File(romfile.getPath()+".smc_header");
|
||||
outputHeader = new FileOutputStream(headerfile);
|
||||
outputHeader.write(header, 0, length);
|
||||
}
|
||||
|
||||
// write headerless rom in tmp file
|
||||
byte[] buffer = new byte[32768];
|
||||
|
|
|
@ -420,8 +420,8 @@ public class FilePickerActivity extends AppCompatActivity implements FilePickerA
|
|||
fis.close();
|
||||
|
||||
String crc32 = Long.toHexString(crc32Digest.getValue());
|
||||
String md5 = bytesToHexString(md5Digest.digest());
|
||||
String sha1 = bytesToHexString(sha1Digest.digest());
|
||||
String md5 = Utils.bytesToHexString(md5Digest.digest());
|
||||
String sha1 = Utils.bytesToHexString(sha1Digest.digest());
|
||||
|
||||
HashMap<String, String> checksum = new HashMap<>();
|
||||
checksum.put(CRC32, crc32);
|
||||
|
@ -429,13 +429,5 @@ public class FilePickerActivity extends AppCompatActivity implements FilePickerA
|
|||
checksum.put(SHA1, sha1);
|
||||
return checksum;
|
||||
}
|
||||
|
||||
private String bytesToHexString(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int i = 0; i < bytes.length ;i++) {
|
||||
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<body>
|
||||
<p>UniPatcher is a ROM patcher that supports IPS, UPS, BPS, PPF, DPS and XDelta3 patch types.</p>
|
||||
<p>UniPatcher is a ROM patcher that supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patch types.</p>
|
||||
<p>Additional features:</p>
|
||||
<ul>
|
||||
<li>Fix checksum in Sega Mega Drive ROMs</li>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<body>
|
||||
|
||||
<h4>0.11 (December, 2016)</h4>
|
||||
<ul>
|
||||
<li>Support EBP patches (for EarthBound game)</li>
|
||||
</ul>
|
||||
|
||||
<h4>0.10.1 (December 6, 2016)</h4>
|
||||
<ul>
|
||||
<li>Added dark theme</li>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<p>UniPatcher is an Android tool for applying patches to ROM images of various video game consoles.</p>
|
||||
|
||||
<h4>What patch formats are supported?</h4>
|
||||
<p>The app supports IPS, UPS, BPS, PPF, DPS and XDelta3 patches.</p>
|
||||
<p>The app supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patches.</p>
|
||||
|
||||
<h4>Can I hack or crack Android game using this app?</h4>
|
||||
<p>No. UniPatcher is not designed to hack the Android games.</p>
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
<string name="notify_error_not_ups_patch">Not a valid UPS patch</string>
|
||||
<string name="notify_error_not_bps_patch">Not a valid BPS patch</string>
|
||||
<string name="notify_error_not_ppf_patch">Not a valid PPF patch</string>
|
||||
<string name="notify_error_not_ebp_patch">Not a valid EBP patch</string>
|
||||
<string name="notify_error_not_dps_patch">Not a valid DPS patch</string>
|
||||
<string name="notify_error_not_xdelta3_patch">Not a valid XDelta3 patch</string>
|
||||
<string name="notify_error_xdelta1_unsupported">I am not able to work with XDelta1 patches</string>
|
||||
|
@ -123,7 +124,7 @@
|
|||
<string name="send_feedback_error_no_email_apps">There are no email clients installed</string>
|
||||
|
||||
<string name="share_dialog_title">Share UniPatcher</string>
|
||||
<string name="share_text">Download the best ROM patcher for Android. It supports IPS, UPS, BPS, PPF, DPS and XDelta3 patch types.\n\n</string>
|
||||
<string name="share_text">Download the best ROM patcher for Android. It supports IPS, UPS, BPS, PPF, DPS, EBP and XDelta3 patch types.\n\n</string>
|
||||
|
||||
<!-- Navigation list -->
|
||||
<string name="nav_apply_patch">Apply patch</string>
|
||||
|
|
|
@ -12,6 +12,7 @@ Utility to apply patches to game rom.
|
|||
• BPS
|
||||
• PPF
|
||||
• DPS
|
||||
• EBP
|
||||
• XDelta 3
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue