bsnes/purify/purify.cpp

572 lines
19 KiB
C++
Raw Normal View History

#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() {
Update to v090 release. byuu says: Most notably, this release adds Nintendo DS emulation. The Nintendo DS module was written entirely by Cydrak, so please give him all of the credit for it. I for one am extremely grateful to be allowed to use his module in bsnes. The Nintendo DS emulator's standalone name is dasShiny. You will need the Nintendo DS firmware, which I cannot provide, in order to use it. It also cannot (currently?) detect the save type used by NDS games. As such, manifest.xml files must be created manually for this purpose. The long-term plan is to create a database of save types for each game. Also, you will need an analog input device for the touch screen for now (joypad axes work well.) There have also been a lot of changes from my end: a unified manifest.xml format across all systems, major improvements to SPC7110 emulation, enhancements to RTC emulation, MSU1 enhancements, icons in the file browser list, improvements to SNES coprocessor memory mapping, cleanups and improvements in the libraries used to build bsnes, etc. I've also included kaijuu (which allows launching game folders directly with bsnes) and purify (which allows opening images that are compressed, have copier headers, and have wrong extensions); both of which are fully GUI-based. This release only loads game folders, not files. Use purify to load ROM files in bsnes. Note that this will likely be the last release for a long time, and that I will probably rename the emulator for the next release, due to how many additional systems it now supports.
2012-08-07 14:08:37 +00:00
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;
}