bsnes/purify/purify.cpp

572 lines
19 KiB
C++

#include <nall/config.hpp>
#include <nall/directory.hpp>
#include <nall/file.hpp>
#include <nall/invoke.hpp>
#include <nall/string.hpp>
#include <nall/zip.hpp>
#include <nall/emulation/famicom.hpp>
#include <nall/emulation/super-famicom.hpp>
#include <nall/emulation/satellaview.hpp>
#include <nall/emulation/sufami-turbo.hpp>
#include <nall/emulation/game-boy.hpp>
#include <nall/emulation/game-boy-advance.hpp>
using namespace nall;
#include <phoenix/phoenix.hpp>
using namespace phoenix;
#include "resource/resource.hpp"
#include "resource/resource.cpp"
struct Settings : configuration {
bool ui; //true if in user-interface mode (windows visible); false if in command-line mode
bool play; //true if emulator should be launched after game conversion
lstring extensions;
string emulator;
string path;
string recent;
Settings() {
ui = false;
play = false;
extensions = {".fc", ".nes", ".sfc", ".smc", ".swc", ".fig", ".bs", ".st", ".gb", ".gbc", ".sgb", ".gba"};
directory::create({configpath(), "purify/"});
append(emulator = "bsnes", "emulator");
append(path = {configpath(), "Emulation/"}, "path");
append(recent = {userpath(), "Desktop/"}, "recent");
load({configpath(), "purify/settings.cfg"});
save({configpath(), "purify/settings.cfg"});
}
~Settings() {
save({configpath(), "purify/settings.cfg"});
}
} settings;
void play(const string &pathname) {
settings.play = false;
invoke(settings.emulator, pathname);
}
bool valid_extension(string name) {
name.rtrim<1>("/");
for(auto &extension : settings.extensions) {
if(name.iendswith(extension)) return true;
}
return false;
}
void create_famicom(const string &filename, uint8_t *data, unsigned size) {
FamicomCartridge information(data, size);
if(information.markup.empty()) return;
string name = {nall::basename(notdir(filename)), ".fc/"};
print(name, "\n");
string path = {settings.path, "Famicom/", name};
directory::create(path, 0755);
//skip iNES header
data += 16, size -= 16;
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
if(information.prgrom > 0) file::write({path, "program.rom"}, data, information.prgrom);
if(information.chrrom > 0) file::write({path, "character.rom"}, data + information.prgrom, information.chrrom);
if(!file::exists({path, "save.rwm"})) file::copy({nall::basename(filename), ".sav"}, {path, "save.rwm"});
if(settings.play) play(path);
}
void create_super_famicom(const string &filename, uint8_t *data, unsigned size) {
SuperFamicomCartridge information(data, size);
if(information.markup.empty()) return;
string name = {nall::basename(notdir(filename)), ".sfc/"};
print(name, "\n");
string path = {settings.path, "Super Famicom/", name};
directory::create(path, 0755);
//skip copier header
if((size & 0x7fff) == 512) data += 512, size -= 512;
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
if(information.markup.position("<spc7110>") && size >= 0x100000) {
file::write({path, "program.rom"}, data, 0x100000);
file::write({path, "data.rom"}, data + 0x100000, size - 0x100000);
} else {
file::write({path, "program.rom"}, data, size);
}
if(!file::exists({path, "save.rwm"})) file::copy({nall::basename(filename), ".srm"}, {path, "save.rwm"});
//firmware
string firmwareID = "<firmware name=\"";
if(auto position = information.markup.position(firmwareID)) {
string firmware = substr(information.markup, position() + firmwareID.length());
if(auto position = firmware.position("\"")) {
firmware[position()] = 0;
if(file::copy({dir(filename), firmware}, {path, firmware}) == false) {
print("Warning: required firmware \"", firmware, "\" not found!\n");
}
}
}
if(settings.play) play(path);
}
void create_satellaview(const string &filename, uint8_t *data, unsigned size) {
SatellaviewCartridge information(data, size);
if(information.markup.empty()) return;
string name = {nall::basename(notdir(filename)), ".bs/"};
print(name, "\n");
string path = {settings.path, "BS-X Satellaview/", name};
directory::create(path, 0755);
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
file::write({path, "program.rom"}, data, size);
if(settings.play) play(path);
}
void create_sufami_turbo(const string &filename, uint8_t *data, unsigned size) {
SufamiTurboCartridge information(data, size);
if(information.markup.empty()) return;
string name = {nall::basename(notdir(filename)), ".st/"};
print(name, "\n");
string path = {settings.path, "Sufami Turbo/", name};
directory::create(path, 0755);
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
file::write({path, "program.rom"}, data, size);
if(!file::exists({path, "save.rwm"})) file::copy({nall::basename(filename), ".srm"}, {path, "save.rwm"});
}
void create_game_boy(const string &filename, uint8_t *data, unsigned size) {
GameBoyCartridge information(data, size);
if(information.markup.empty()) return;
string system = information.info.cgb ? "Game Boy Color/" : "Game Boy/";
string extension = information.info.cgb ? ".gbc/" : ".gb/";
string name = {nall::basename(notdir(filename)), extension};
print(name, "\n");
string path = {settings.path, system, name};
directory::create(path, 0755);
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
file::write({path, "program.rom"}, data, size);
if(!file::exists({path, "save.rwm"})) file::copy({nall::basename(filename), ".sav"}, {path, "save.rwm"});
if(settings.play) play(path);
}
void create_game_boy_advance(const string &filename, uint8_t *data, unsigned size) {
GameBoyAdvanceCartridge information(data, size);
if(information.markup.empty()) return;
string name = {nall::basename(notdir(filename)), ".gba/"};
print(name, "\n");
string path = {settings.path, "Game Boy Advance/", name};
directory::create(path, 0755);
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
file::write({path, "program.rom"}, data, size);
if(!file::exists({path, "save.rwm"})) file::copy({nall::basename(filename), ".sav"}, {path, "save.rwm"});
if(settings.play) play(path);
}
void create_data(const string &name, uint8_t *data, unsigned size) {
if(name.iendswith(".fc")
|| name.iendswith(".nes")
) return create_famicom(name, data, size);
if(name.iendswith(".sfc")
|| name.iendswith(".smc")
|| name.iendswith(".swc")
|| name.iendswith(".fig")
) return create_super_famicom(name, data, size);
if(name.iendswith(".bs")
) return create_satellaview(name, data, size);
if(name.iendswith(".st")
) return create_sufami_turbo(name, data, size);
if(name.iendswith(".gb")
|| name.iendswith(".gbc")
|| name.iendswith(".sgb")
) return create_game_boy(name, data, size);
if(name.iendswith(".gba")
) return create_game_boy_advance(name, data, size);
}
void create_file(const string &filename) {
auto buffer = file::read(filename);
if(buffer.size() == 0) return;
return create_data(filename, buffer.data(), buffer.size());
}
void create_zip(const string &filename) {
zip archive;
if(archive.open(filename) == false) return;
for(auto &file : archive.file) {
if(file.data == nullptr || file.size == 0) return;
if(valid_extension(file.name) == false) continue;
auto buffer = archive.extract(file);
if(buffer.size() == 0) continue;
string name = {nall::basename(filename), ".", extension(file.name)};
return create_data(name, buffer.data(), buffer.size());
}
}
void create_manifest(const string &path) {
string name = path;
name.rtrim<1>("/");
name = {notdir(name), "/"};
//Famicom manifests cannot be generated from PRG+CHR ROMs alone
//In the future, a games database may enable manifest generation
if(path.iendswith(".sfc/") && file::exists({path, "program.rom"})) {
print(name, "\n");
auto buffer = file::read({path, "program.rom"});
if(file::exists({path, "data.rom"})) { //SPC7110 ROMs consist of program.rom + data.rom
auto prom = buffer;
auto drom = file::read({path, "data.rom"});
buffer.resize(prom.size() + drom.size());
memcpy(buffer.data() + prom.size(), drom.data(), drom.size());
}
SuperFamicomCartridge information(buffer.data(), buffer.size());
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
}
if(path.iendswith(".bs/") && file::exists({path, "program.rom"})) {
print(name, "\n");
auto buffer = file::read({path, "program.rom"});
SatellaviewCartridge information(buffer.data(), buffer.size());
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
}
if(path.iendswith(".st/") && file::exists({path, "program.rom"})) {
print(name, "\n");
auto buffer = file::read({path, "program.rom"});
SufamiTurboCartridge information(buffer.data(), buffer.size());
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
}
if((path.iendswith(".gb/") || path.iendswith(".gbc/")) && file::exists({path, "program.rom"})) {
print(name, "\n");
auto buffer = file::read({path, "program.rom"});
GameBoyCartridge information(buffer.data(), buffer.size());
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
}
if(path.iendswith(".gba/") && file::exists({path, "program.rom"})) {
print(name, "\n");
auto buffer = file::read({path, "program.rom"});
GameBoyAdvanceCartridge information(buffer.data(), buffer.size());
file::write({path, "manifest.xml"}, (const uint8_t*)information.markup(), information.markup.length());
}
}
//$ purify synchronize
//this function recursively scans directories; as purify uses nested folders for different systems
void synchronize_manifests(string pathname) {
pathname.transform("\\", "/");
if(pathname.endswith("/") == false) pathname.append("/");
lstring folders = directory::folders(pathname);
for(auto &folder : folders) {
if(valid_extension(folder)) {
create_manifest({pathname, folder});
} else {
synchronize_manifests({pathname, folder});
}
}
}
void create_folder(string pathname) {
pathname.transform("\\", "/");
if(pathname.endswith("/") == false) pathname.append("/");
if(pathname == settings.path) {
print(
"You cannot use the same path for both the source and destination directories.\n"
"Please choose a different output path in settings.cfg.\n"
);
return;
}
lstring files = directory::contents(pathname);
for(auto &name : files) {
if(name.iendswith(".zip")) {
create_zip({pathname, name});
} else {
create_file({pathname, name});
}
}
}
struct Progress : Window {
VerticalLayout layout;
Label title;
HorizontalLayout fileLayout;
Label fileLabel;
Label fileName;
HorizontalLayout progressLayout;
ProgressBar progressBar;
Button stopButton;
bool quit;
void convert(const string &pathname) {
fileName.setText("Initializing ...");
progressBar.setPosition(0);
stopButton.setEnabled(true);
quit = false;
setVisible(true);
setModal(true);
lstring files = directory::contents(pathname);
for(unsigned n = 0; n < files.size() && quit == false; n++) {
auto &filename = files(n);
if(!filename.iendswith(".zip") && !valid_extension(filename)) continue;
OS::processEvents();
double position = (double)n / (double)files.size() * 100.0 + 0.5;
progressBar.setPosition((unsigned)position);
string name = filename;
name.rtrim<1>("/");
fileName.setText(notdir(name));
if(filename.iendswith(".zip")) {
create_zip({pathname, filename});
} else {
create_file({pathname, filename});
}
}
if(quit == false) {
fileName.setText("All games have been converted.");
progressBar.setPosition(100);
} else {
fileName.setText("Process aborted. Not all games have been converted.");
}
stopButton.setEnabled(false);
setModal(false);
}
Progress() {
setTitle("purify");
layout.setMargin(5);
title.setFont("Sans, 16, Bold");
title.setText("Conversion Progress");
fileLabel.setFont("Sans, 8, Bold");
fileLabel.setText("Filename:");
stopButton.setText("Stop");
append(layout);
layout.append(title, {~0, 0}, 5);
layout.append(fileLayout, {~0, 0}, 5);
fileLayout.append(fileLabel, {0, 0}, 5);
fileLayout.append(fileName, {~0, 0});
layout.append(progressLayout, {~0, 0});
progressLayout.append(progressBar, {~0, 0}, 5);
progressLayout.append(stopButton, {80, 0});
setGeometry({192, 192, 560, layout.minimumGeometry().height});
stopButton.onActivate = [&] { quit = true; };
}
} *progress = nullptr;
struct Application : Window {
VerticalLayout layout;
Label title;
HorizontalLayout purifyLayout;
Button playButton;
Button convertButton;
HorizontalLayout configLayout;
Button emulatorButton;
Button pathButton;
HorizontalLayout gridLayout;
VerticalLayout labelLayout;
HorizontalLayout emulatorLayout;
Label emulatorName;
Label emulatorValue;
HorizontalLayout pathLayout;
Label pathName;
Label pathValue;
VerticalLayout synchronizeLayout;
Button synchronizeButton;
Application() {
setTitle("purify v01");
setGeometry({128, 128, 600, 200});
layout.setMargin(5);
title.setFont("Sans, 16, Bold");
title.setText("Choose Action");
playButton.setImage({resource::play, sizeof resource::play});
playButton.setText("Play Game");
convertButton.setImage({resource::convert, sizeof resource::convert});
convertButton.setText("Convert Games");
emulatorButton.setImage({resource::emulator, sizeof resource::emulator});
emulatorButton.setText("Choose Emulator");
pathButton.setImage({resource::path, sizeof resource::path});
pathButton.setText("Choose Output Path");
emulatorName.setFont("Sans, 8, Bold");
emulatorName.setText("Emulator:");
emulatorValue.setText(settings.emulator);
pathName.setFont("Sans, 8, Bold");
pathName.setText("Output Path:");
pathValue.setText(settings.path);
synchronizeButton.setImage({resource::synchronize, sizeof resource::synchronize});
synchronizeButton.setText("Update Manifests");
Font font("Sans, 8, Bold");
unsigned width = max(font.geometry("Emulator:").width, font.geometry("Output Path:").width);
append(layout);
layout.append(title, {~0, 0}, 5);
layout.append(purifyLayout, {~0, ~0}, 5);
purifyLayout.append(playButton, {~0, ~0}, 5);
purifyLayout.append(convertButton, {~0, ~0});
layout.append(configLayout, {~0, ~0}, 5);
configLayout.append(emulatorButton, {~0, ~0}, 5);
configLayout.append(pathButton, {~0, ~0});
layout.append(gridLayout, {~0, 0});
gridLayout.append(labelLayout, {~0, 0}, 5);
labelLayout.append(emulatorLayout, {~0, 0}, 5);
emulatorLayout.append(emulatorName, {width, 0}, 5);
emulatorLayout.append(emulatorValue, {~0, 0});
labelLayout.append(pathLayout, {~0, 0});
pathLayout.append(pathName, {width, 0}, 5);
pathLayout.append(pathValue, {~0, 0});
gridLayout.append(synchronizeLayout, {0, 0});
synchronizeLayout.append(synchronizeButton, {0, 0});
onClose = &OS::quit;
playButton.onActivate = {&Application::playAction, this};
convertButton.onActivate = {&Application::convertAction, this};
emulatorButton.onActivate = {&Application::emulatorAction, this};
pathButton.onActivate = {&Application::pathAction, this};
synchronizeButton.onActivate = [&] {
if(MessageWindow::question(*this,
"This will update all manifest.xml files located in your output path.\n"
"This process may take a few minutes to complete.\n"
"The user interface will not be responsive during this time.\n\n"
"Would you like to proceed?"
) == MessageWindow::Response::No) return;
layout.setEnabled(false);
OS::processEvents();
synchronize_manifests(settings.path);
layout.setEnabled(true);
MessageWindow::information(*this, "Process completed. All identified manifests have been updated.");
};
setVisible();
}
void playAction() {
string filters = {settings.extensions.concatenate(","), ",.zip"};
filters.replace(".", "*.");
string filename = DialogWindow::fileOpen(*this, settings.recent, string{"Game Images (", filters, ")"});
if(!filename.empty()) {
setVisible(false);
settings.recent = dir(filename);
settings.play = true;
if(filename.iendswith(".zip")) {
create_zip(filename);
} else {
create_file(filename);
}
exit(0);
}
}
void convertAction() {
string pathname = DialogWindow::folderSelect(*this, settings.recent);
if(pathname.empty()) return;
if(pathname == settings.path) {
MessageWindow::critical(*this,
"You cannot use the same path for both the source and destination directories.\n\n"
"Please choose a different output path."
);
return;
}
settings.recent = pathname;
progress->convert(pathname);
}
void emulatorAction() {
string filter = {
"Emulators ", Intrinsics::platform() == Intrinsics::Platform::Windows
? "(*.exe)"
: "(*)"
};
string filename = DialogWindow::fileOpen(*this, settings.recent, filter);
if(!filename.empty()) {
settings.recent = dir(filename);
emulatorValue.setText(settings.emulator = filename);
}
}
void pathAction() {
string pathname = DialogWindow::folderSelect(*this, settings.recent);
if(!pathname.empty()) {
settings.recent = pathname;
pathValue.setText(settings.path = pathname);
}
}
} *application = nullptr;
int main(int argc, char **argv) {
#if defined(PLATFORM_WINDOWS)
utf8_args(argc, argv);
#endif
lstring args;
for(unsigned n = 1; n < argc; n++) {
string argument = argv[n];
argument.replace("~/", userpath());
args.append(argument);
}
if(args.size() == 1 && args[0] == "synchronize") {
synchronize_manifests(settings.path);
return 0;
}
if(args.size() == 1 && directory::exists(args[0])) {
create_folder(args[0]);
return 0;
}
if(args.size() == 1 && istrend(args[0], ".zip")) {
settings.play = true;
create_zip(args[0]);
return 0;
}
if(args.size() == 1 && file::exists(args[0]) && valid_extension(args[0])) {
settings.play = true;
create_file(args[0]);
return 0;
}
settings.ui = true;
progress = new Progress;
application = new Application;
OS::main();
return 0;
}