bsnes/snespurify/snespurify.cpp

401 lines
13 KiB
C++
Executable File

#include "snespurify.hpp"
MainWindow mainWindow;
void MainWindow::create() {
font.setSize(8);
setWidgetFont(font);
setTitle("SNES Purify v11");
navigationLabel.setText("Path to scan:");
navigationScan.setEnabled(false);
navigationScan.setText("Scan");
navigationBrowse.setText("Browse ...");
listView.setHeaderText("Filename", "Problem");
listView.setHeaderVisible();
listView.setCheckable();
controlSelectAll.setText("Select All");
controlClearAll.setText("Clear All");
controlDeleteFiles.setText("Delete ZIP, IPS, UPS files after conversion");
controlCorrect.setEnabled(false);
controlCorrect.setText("Correct");
layout.setMargin(5);
navigationLayout.append(navigationLabel, 0, 0, 5);
navigationLayout.append(navigationPath, ~0, 0, 5);
navigationLayout.append(navigationScan, 80, 0, 5);
navigationLayout.append(navigationBrowse, 80, 0);
layout.append(navigationLayout, 5);
layout.append(listView, ~0, ~0, 5);
controlLayout.append(controlSelectAll, 80, 0, 5);
controlLayout.append(controlClearAll, 80, 0, 5);
controlLayout.append(controlDeleteFiles, 0, 0, 5);
controlLayout.append(spacer, ~0, 0);
controlLayout.append(controlCorrect, 80, 0);
layout.append(controlLayout);
append(layout);
onClose = &OS::quit;
navigationPath.onChange = [this] {
navigationScan.setEnabled(navigationPath.text() != "");
};
navigationPath.onActivate = navigationScan.onTick = { &MainWindow::onScan, this };
navigationBrowse.onTick = [this] {
string path = OS::folderSelect(*this, "");
if(path != "") {
navigationPath.setText(path);
navigationScan.setEnabled(true);
navigationScan.onTick();
}
};
controlSelectAll.onTick = [this] {
for(unsigned n = 0; n < fileList.size(); n++) listView.setChecked(n, true);
};
controlClearAll.onTick = [this] {
for(unsigned n = 0; n < fileList.size(); n++) listView.setChecked(n, false);
};
controlCorrect.onTick = { &MainWindow::onCorrect, this };
setGeometry({ 128, 128, 640, 360 });
setVisible();
}
void MainWindow::onScan() {
string path = navigationPath.text();
path.transform("\\", "/");
if(path.endswith("/") == false) path.append("/");
listView.reset();
fileList.reset();
onScanFolder(path);
foreach(file, fileList) {
listView.append(notdir(file.name), file.problem);
}
controlSelectAll.onTick();
listView.autoSizeColumns();
controlCorrect.setEnabled(fileList.size() > 0);
}
void MainWindow::onScanFolder(const string &path) {
lstring contents = directory::contents(path);
foreach(filename, contents) {
if(filename.endswith("/")) onScanFolder({ path, filename });
else analyzeFile({ path, filename });
}
}
void MainWindow::analyzeFile(const string &filename) {
File file;
file.name = filename;
if(filename.iendswith(".zip")) {
//verify there is exactly one SNES image inside the archive
zip archive;
if(archive.open(filename)) {
unsigned fileCount = 0, archiveFileSize = 0;
string archiveFileName;
foreach(archiveFile, archive.file) if(isImageName(archiveFile.name)) {
//make sure file isn't already decompressed
string basename = { nall::basename(filename), correctExtension(archiveFile.name) };
if(file::exists(basename)) { fileCount = 0; break; } //zero fileCount to prevent multi-archive repeated extractions
fileCount++;
archiveFileName = archiveFile.name;
archiveFileSize = archiveFile.size;
}
archive.close();
if(fileCount == 1) {
file.problem.append("Compressed with ZIP; ");
if(isSnesImageName(archiveFileName) && (archiveFileSize & 0x7fff) == 512) {
file.problem.append("Contains copier header; ");
}
}
}
}
if(isBadImageName(filename)) {
file.problem.append("Incorrect extension; ");
}
if(isSnesImageName(filename) && (file::size(filename) & 0x7fff) == 512) {
file.problem.append("Contains copier header; ");
}
if((isImageName(filename) || filename.iendswith(".zip")) && !file::exists({ nall::basename(filename), ".bps" })) {
if(file::exists({ nall::basename(filename), ".ups" })) {
file.problem.append("Deprecated UPS patch; ");
} else if(file::exists({ nall::basename(filename), ".ips" })) {
file.problem.append("Deprecated IPS patch; ");
}
}
file.problem.rtrim<1>("; ");
if(file.problem != "") fileList.append(file);
}
bool MainWindow::isSnesCartImageName(const string &filename) {
if(filename.iendswith(".sfc")) return true;
if(filename.iendswith(".048")) return true;
if(filename.iendswith(".058")) return true;
if(filename.iendswith(".068")) return true;
if(filename.iendswith(".078")) return true;
if(filename.iendswith(".aus")) return true;
if(filename.iendswith(".dx2")) return true;
if(filename.iendswith(".eur")) return true;
if(filename.iendswith(".fig")) return true;
if(filename.iendswith(".gd3")) return true;
if(filename.iendswith(".gd7")) return true;
if(filename.iendswith(".jap")) return true;
if(filename.iendswith(".mgd")) return true;
if(filename.iendswith(".mgh")) return true;
if(filename.iendswith(".smc")) return true;
if(filename.iendswith(".swc")) return true;
if(filename.iendswith(".ufo")) return true;
if(filename.iendswith(".usa")) return true;
return false;
}
bool MainWindow::isSnesBsImageName(const string &filename) {
if(filename.iendswith(".bs")) return true;
if(filename.iendswith(".bsx")) return true;
return false;
}
bool MainWindow::isSnesStImageName(const string &filename) {
if(filename.iendswith(".st")) return true;
return false;
}
bool MainWindow::isSnesImageName(const string &filename) {
if(isSnesCartImageName(filename)) return true;
if(isSnesBsImageName(filename)) return true;
if(isSnesStImageName(filename)) return true;
return false;
}
bool MainWindow::isGameBoyClassicImageName(const string &filename) {
if(filename.iendswith(".gb")) return true;
if(filename.iendswith(".sgb")) return true;
return false;
}
bool MainWindow::isGameBoyColorImageName(const string &filename) {
if(filename.iendswith(".gbc")) return true;
return false;
}
bool MainWindow::isGameBoyImageName(const string &filename) {
if(isGameBoyClassicImageName(filename)) return true;
if(isGameBoyColorImageName(filename)) return true;
return false;
}
bool MainWindow::isImageName(const string &filename) {
if(isSnesImageName(filename)) return true;
if(isGameBoyImageName(filename)) return true;
return false;
}
bool MainWindow::isGoodImageName(const string &filename) {
if(filename.endswith(".sfc")) return true;
if(filename.endswith(".bs")) return true;
if(filename.endswith(".st")) return true;
if(filename.endswith(".gb")) return true;
if(filename.endswith(".gbc")) return true;
return false;
}
bool MainWindow::isBadImageName(const string &filename) {
return (isGoodImageName(filename) == false && isImageName(filename) == true);
}
string MainWindow::correctExtension(const string &filename) {
if(isSnesCartImageName(filename)) return ".sfc";
if(isSnesBsImageName(filename)) return ".bs";
if(isSnesStImageName(filename)) return ".st";
if(isGameBoyClassicImageName(filename)) return ".gb";
if(isGameBoyColorImageName(filename)) return ".gbc";
return { ".", extension(filename) }; //should never occur
}
void MainWindow::onCorrect() {
navigationPath.setEnabled(false);
navigationScan.setEnabled(false);
navigationBrowse.setEnabled(false);
listView.setEnabled(false);
controlSelectAll.setEnabled(false);
controlClearAll.setEnabled(false);
controlDeleteFiles.setEnabled(false);
controlCorrect.setEnabled(false);
OS::processEvents();
MessageWindow::information(*this,
"Corrections may take a long time to complete.\n"
"The GUI will be unresponsive during this time, so please be patient.\n"
"You will be notified when all corrections have been completed."
);
OS::processEvents();
errors.reset();
foreach(file, fileList, n) {
if(listView.checked(n) == false) continue;
if(file.problem.position("Compressed with ZIP")) problemDecompressZip(file);
if(file.problem.position("Incorrect extension")) problemCorrectExtension(file);
if(file.problem.position("Deprecated UPS patch")) problemCreatePatchFromUPS(file);
if(file.problem.position("Deprecated IPS patch")) problemCreatePatchFromIPS(file);
if(file.problem.position("Contains copier header")) problemRemoveCopierHeader(file);
}
if(errors.size() == 0) {
MessageWindow::information(*this, "Corrections completed!");
} else {
file fp;
if(fp.open({ navigationPath.text(), "errors.txt" }, file::mode::write)) {
foreach(error, errors) fp.print(error, "\n");
fp.close();
}
MessageWindow::information(*this, {
"Corrections completed, but there were errors.\n"
"Please see ", navigationPath.text(), "errors.txt for more details.\n"
});
}
listView.reset();
listView.autoSizeColumns();
fileList.reset();
navigationPath.setEnabled(true);
navigationScan.setEnabled(true);
navigationBrowse.setEnabled(true);
listView.setEnabled(true);
controlSelectAll.setEnabled(true);
controlClearAll.setEnabled(true);
controlDeleteFiles.setEnabled(true);
}
void MainWindow::problemDecompressZip(File &item) {
zip archive;
if(archive.open(item.name)) {
foreach(archiveFile, archive.file) {
if(isImageName(archiveFile.name)) {
uint8_t *data;
unsigned size;
if(archive.extract(archiveFile, data, size)) {
string targetFileName = { nall::basename(item.name), correctExtension(archiveFile.name) };
if(file::write(targetFileName, data, size) == false) {
errors.append({ "Failed to write file: ", item.name });
} else {
if(controlDeleteFiles.checked()) unlink(item.name);
item.name = targetFileName;
}
} else {
errors.append({ "Failed to decompress file: ", item.name });
}
break;
}
}
}
}
void MainWindow::problemCorrectExtension(File &item) {
string filename = { nall::basename(item.name), correctExtension(item.name) };
if(rename(item.name, filename) == -1) {
errors.append({ "Failed to rename file: ", item.name });
} else {
item.name = filename;
}
}
void MainWindow::problemCreatePatchFromUPS(File &item) {
string upsPatchName = { nall::basename(item.name), ".ups" };
string bpsPatchName = { nall::basename(item.name), ".bps" };
ups oldPatch;
filemap patchFile, sourceFile;
patchFile.open(upsPatchName, filemap::mode::read);
sourceFile.open(item.name, filemap::mode::read);
uint8_t *targetData = 0;
unsigned targetSize = 0;
oldPatch.apply(patchFile.data(), patchFile.size(), sourceFile.data(), sourceFile.size(), targetData, targetSize);
targetData = new uint8_t[targetSize];
if(oldPatch.apply(patchFile.data(), patchFile.size(), sourceFile.data(), sourceFile.size(), targetData, targetSize)
== ups::result::success) {
bpslinear newPatch;
newPatch.source(item.name);
newPatch.target(targetData, targetSize);
if(newPatch.create(bpsPatchName)) {
patchFile.close();
if(controlDeleteFiles.checked()) unlink(upsPatchName);
} else {
errors.append({ "Failed to create BPS patch: ", bpsPatchName });
}
} else {
errors.append({ "Invalid UPS patch: ", upsPatchName });
}
}
void MainWindow::problemCreatePatchFromIPS(File &item) {
string ipsPatchName = { nall::basename(item.name), ".ips" };
string bpsPatchName = { nall::basename(item.name), ".bps" };
ips oldPatch;
oldPatch.source(item.name);
oldPatch.modify(ipsPatchName);
if(oldPatch.apply()) {
bpslinear newPatch;
newPatch.source(item.name);
newPatch.target(oldPatch.data, oldPatch.size);
if(newPatch.create(bpsPatchName)) {
if(controlDeleteFiles.checked()) unlink(ipsPatchName);
} else {
errors.append({ "Failed to create BPS patch: ", bpsPatchName });
}
} else {
errors.append({ "Invalid IPS patch: ", ipsPatchName });
}
}
/*
void MainWindow::problemCreateUpsPatch(File &item) {
string ipsPatchName = { nall::basename(item.name), ".ips" };
ips oldPatch;
oldPatch.source(item.name);
oldPatch.modify(ipsPatchName);
if(oldPatch.apply()) {
ups newPatch;
if(newPatch.create(
oldPatch.sourceData, oldPatch.sourceSize,
oldPatch.data, oldPatch.size,
string(nall::basename(item.name), ".ups")
) != ups::result::success) {
errors.append({ "Failed to convert IPS patch: ", ipsPatchName });
} else {
if(controlDeleteFiles.checked()) unlink(ipsPatchName);
}
}
}
*/
void MainWindow::problemRemoveCopierHeader(File &item) {
uint8_t *data;
unsigned size;
if(file::read(item.name, data, size)) {
if((size & 0x7fff) == 512) {
if(file::write(item.name, data + 512, size - 512) == false) {
errors.append({ "Failed to write file: ", item.name });
}
}
}
}
int main() {
mainWindow.create();
OS::main();
return 0;
}