bsnes-hd/bsnes/target-bsnes/tools/cheat-editor.cpp
2020-01-12 22:12:37 +01:00

464 lines
14 KiB
C++

auto CheatDatabase::create() -> void {
layout.setPadding(5_sx);
selectAllButton.setText("Select All").onActivate([&] {
for(auto item : cheatList.items()) item.setChecked(true);
});
unselectAllButton.setText("Unselect All").onActivate([&] {
for(auto item : cheatList.items()) item.setChecked(false);
});
addCheatsButton.setText("Add Cheats").onActivate([&] {
addCheats();
});
setSize({800_sx, 400_sx});
setAlignment({0.5, 1.0});
setDismissable();
}
auto CheatDatabase::findCheats() -> void {
//hack to locate Super Game Boy cheat codes
auto sha256a = emulator->hashes()(0, "none");
auto sha256b = emulator->hashes()(1, "none");
auto document = BML::unserialize(string::read(locate("Database/Cheat Codes.bml")));
for(auto game : document.find("cartridge")) {
if(game["sha256"].text() != sha256a
&& game["sha256"].text() != sha256b) continue;
cheatList.reset();
for(auto cheat : game.find("cheat")) {
//convert old cheat format (address/data and address/compare/data)
//to new cheat format (address=data and address=compare?data)
auto codes = cheat["code"].text().split("+").strip();
for(auto& code : codes) {
code.replace("/", "=", 1L);
code.replace("/", "?", 1L);
}
auto code = codes.merge("+");
cheatList.append(ListViewItem()
.setCheckable()
.setText(cheat["description"].text())
.setAttribute("code", code)
);
}
setTitle(game["name"].text());
setVisible();
return;
}
MessageDialog().setAlignment(*toolsWindow).setText("Sorry, no cheats were found for this game.").information();
}
auto CheatDatabase::addCheats() -> void {
for(auto item : cheatList.items()) {
if(item.checked()) {
cheatEditor.addCheat({item.text(), item.attribute("code"), false});
}
}
setVisible(false);
}
//
auto CheatWindow::create() -> void {
layout.setPadding(5_sx);
tableLayout.setSize({2, 2});
tableLayout.cell(0).setAlignment({1.0, 0.5});
tableLayout.cell(2).setAlignment({1.0, 0.0});
nameLabel.setText("Name:");
nameValue.onActivate([&] { if(acceptButton.enabled()) acceptButton.doActivate(); });
nameValue.onChange([&] { doChange(); });
codeLabel.setText("Code(s):");
codeValue.setFont(Font().setFamily(Font::Mono));
codeValue.onChange([&] { doChange(); });
enableOption.setText("Enable");
acceptButton.onActivate([&] { doAccept(); });
cancelButton.setText("Cancel").onActivate([&] { setVisible(false); });
setSize({400_sx, layout.minimumSize().height() + 100_sx});
setDismissable();
}
auto CheatWindow::show(Cheat cheat) -> void {
nameValue.setText(cheat.name);
codeValue.setText(cheat.code.split("+").strip().merge("\n"));
enableOption.setChecked(cheat.enable);
doChange();
setTitle(!cheat.name ? "Add Cheat" : "Edit Cheat");
setAlignment(*toolsWindow);
setVisible();
setFocused();
nameValue.setFocused();
acceptButton.setText(!cheat.name ? "Add" : "Edit");
}
auto CheatWindow::doChange() -> void {
bool valid = true;
nameValue.setBackgroundColor(nameValue.text().strip() ? Color{} : (valid = false, Color{255, 224, 224}));
codeValue.setBackgroundColor(codeValue.text().strip() ? Color{} : (valid = false, Color{255, 224, 224}));
acceptButton.setEnabled(valid);
}
auto CheatWindow::doAccept() -> void {
auto codes = codeValue.text().downcase().transform("+", "\n").split("\n").strip();
string invalid; //if empty after below for-loop, code is considered valid
for(auto& code : codes) {
if(!program.gameBoy.program) {
if(!cheatEditor.decodeSNES(code)) {
invalid =
"Invalid code(s), please only use codes in the following format:\n"
"\n"
"Game Genie (eeee-eeee)\n"
"Pro Action Replay (aaaaaadd)\n"
"higan (aaaaaa=dd)\n"
"higan (aaaaaa=cc?dd)";
}
} else {
if(!cheatEditor.decodeGB(code)) {
invalid =
"Invalid code(s), please only use codes in the following format:\n"
"\n"
"Game Genie (eee-eee)\n"
"Game Genie (eee-eee-eee)\n"
"GameShark (01ddaaaa)\n"
"higan (aaaa=dd)\n"
"higan (aaaa=cc?dd)";
}
}
}
if(invalid) return (void)MessageDialog().setAlignment(*toolsWindow).setText(invalid).error();
Cheat cheat = {nameValue.text().strip(), codes.merge("+"), enableOption.checked()};
if(acceptButton.text() == "Add") {
cheatEditor.addCheat(cheat);
} else {
cheatEditor.editCheat(cheat);
}
setVisible(false);
}
//
auto CheatEditor::create() -> void {
setCollapsible();
setVisible(false);
cheatList.setBatchable();
cheatList.setHeadered();
cheatList.setSortable();
cheatList.onActivate([&](auto cell) {
//kind of a hack: toggling a cheat code twice quickly (onToggle) will call onActivate.
//do not trigger the CheatWindow unless it's been at least two seconds since a cheat code was last toggled on or off.
if(chrono::timestamp() - activateTimeout < 2) return;
editButton.doActivate();
});
cheatList.onChange([&] {
auto batched = cheatList.batched();
editButton.setEnabled(batched.size() == 1);
removeButton.setEnabled(batched.size() >= 1);
});
cheatList.onToggle([&](TableViewCell cell) {
activateTimeout = chrono::timestamp();
if(auto item = cell->parentTableViewItem()) {
cheats[item->offset()].enable = cell.checked();
synchronizeCodes();
}
});
cheatList.onSort([&](TableViewColumn column) {
column.setSorting(column.sorting() == Sort::Ascending ? Sort::Descending : Sort::Ascending);
//cheatList.sort();
});
cheatList.onSize([&] {
cheatList.resizeColumns();
});
findCheatsButton.setText("Find Cheats ...").onActivate([&] {
cheatDatabase.findCheats();
});
enableCheats.setText("Enable Cheats").setToolTip(
"Master enable for all cheat codes.\n"
"When unchecked, no cheat codes will be active.\n\n"
"Use this to bypass game areas that have problems with cheats."
).setChecked(settings.emulator.cheats.enable).onToggle([&] {
settings.emulator.cheats.enable = enableCheats.checked();
if(!enableCheats.checked()) {
program.showMessage("All cheat codes disabled");
} else {
program.showMessage("Active cheat codes enabled");
}
synchronizeCodes();
});
addButton.setText("Add").onActivate([&] {
cheatWindow.show();
});
editButton.setText("Edit").onActivate([&] {
if(auto item = cheatList.selected()) {
cheatWindow.show(cheats[item.offset()]);
}
});
removeButton.setText("Remove").onActivate([&] {
removeCheats();
});
//hide the "Find Cheats" button if the cheat code database isn't found
if(!file::exists(locate("Database/Cheat Codes.bml"))) findCheatsButton.setVisible(false);
}
auto CheatEditor::refresh() -> void {
cheatList.reset();
cheatList.append(TableViewColumn());
cheatList.append(TableViewColumn().setText("Name").setSorting(Sort::Ascending).setExpandable());
for(auto& cheat : cheats) {
TableViewItem item{&cheatList};
item.append(TableViewCell().setCheckable().setChecked(cheat.enable));
item.append(TableViewCell().setText(cheat.name));
}
cheatList.resizeColumns().doChange();
}
auto CheatEditor::addCheat(Cheat cheat) -> void {
cheats.append(cheat);
cheats.sort();
refresh();
for(uint index : range(cheats.size())) {
if(cheats[index] == cheat) { cheatList.item(index).setSelected(); break; }
}
cheatList.doChange();
synchronizeCodes();
}
auto CheatEditor::editCheat(Cheat cheat) -> void {
if(auto item = cheatList.selected()) {
cheats[item.offset()] = cheat;
cheats.sort();
refresh();
for(uint index : range(cheats.size())) {
if(cheats[index] == cheat) { cheatList.item(index).setSelected(); break; }
}
cheatList.doChange();
synchronizeCodes();
}
}
auto CheatEditor::removeCheats() -> void {
if(auto batched = cheatList.batched()) {
if(MessageDialog("Are you sure you want to permanently remove the selected cheat(s)?")
.setAlignment(*toolsWindow).question() == "Yes") {
for(auto& item : reverse(batched)) cheats.remove(item.offset());
cheats.sort();
refresh();
synchronizeCodes();
}
}
}
auto CheatEditor::loadCheats() -> void {
cheats.reset();
auto location = program.cheatPath();
auto document = BML::unserialize(string::read(location));
for(auto cheat : document.find("cheat")) {
cheats.append({cheat["name"].text(), cheat["code"].text(), (bool)cheat["enable"]});
}
cheats.sort();
refresh();
synchronizeCodes();
}
auto CheatEditor::saveCheats() -> void {
string document;
for(auto cheat : cheats) {
document.append("cheat\n");
document.append(" name: ", cheat.name, "\n");
document.append(" code: ", cheat.code, "\n");
if(cheat.enable)
document.append(" enable\n");
document.append("\n");
}
auto location = program.cheatPath();
if(document) {
file::write(location, document);
} else {
file::remove(location);
}
}
auto CheatEditor::synchronizeCodes() -> void {
vector<string> codes;
if(enableCheats.checked()) {
for(auto& cheat : cheats) {
if(cheat.enable) codes.append(cheat.code);
}
}
emulator->cheats(codes);
}
//
auto CheatEditor::decodeSNES(string& code) -> bool {
//Game Genie
if(code.size() == 9 && code[4] == '-') {
//strip '-'
code = {code.slice(0, 4), code.slice(5, 4)};
//validate
for(uint n : code) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
//decode
code.transform("df4709156bc8a23e", "0123456789abcdef");
uint32_t r = toHex(code);
//abcd efgh ijkl mnop qrst uvwx
//ijkl qrst opab cduv wxef ghmn
uint address =
(!!(r & 0x002000) << 23) | (!!(r & 0x001000) << 22)
| (!!(r & 0x000800) << 21) | (!!(r & 0x000400) << 20)
| (!!(r & 0x000020) << 19) | (!!(r & 0x000010) << 18)
| (!!(r & 0x000008) << 17) | (!!(r & 0x000004) << 16)
| (!!(r & 0x800000) << 15) | (!!(r & 0x400000) << 14)
| (!!(r & 0x200000) << 13) | (!!(r & 0x100000) << 12)
| (!!(r & 0x000002) << 11) | (!!(r & 0x000001) << 10)
| (!!(r & 0x008000) << 9) | (!!(r & 0x004000) << 8)
| (!!(r & 0x080000) << 7) | (!!(r & 0x040000) << 6)
| (!!(r & 0x020000) << 5) | (!!(r & 0x010000) << 4)
| (!!(r & 0x000200) << 3) | (!!(r & 0x000100) << 2)
| (!!(r & 0x000080) << 1) | (!!(r & 0x000040) << 0);
uint data = r >> 24;
code = {hex(address, 6L), "=", hex(data, 2L)};
return true;
}
//Pro Action Replay
if(code.size() == 8) {
//validate
for(uint n : code) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
//decode
uint32_t r = toHex(code);
uint address = r >> 8;
uint data = r & 0xff;
code = {hex(address, 6L), "=", hex(data, 2L)};
return true;
}
//higan: address=data
if(code.size() == 9 && code[6] == '=') {
string nibbles = {code.slice(0, 6), code.slice(7, 2)};
//validate
for(uint n : nibbles) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
//already in decoded form
return true;
}
//higan: address=compare?data
if(code.size() == 12 && code[6] == '=' && code[9] == '?') {
string nibbles = {code.slice(0, 6), code.slice(7, 2), code.slice(10, 2)};
//validate
for(uint n : nibbles) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
//already in decoded form
return true;
}
//unrecognized code format
return false;
}
auto CheatEditor::decodeGB(string& code) -> bool {
auto nibble = [&](const string& s, uint index) -> uint {
if(index >= s.size()) return 0;
if(s[index] >= '0' && s[index] <= '9') return s[index] - '0';
return s[index] - 'a' + 10;
};
//Game Genie
if(code.size() == 7 && code[3] == '-') {
code = {code.slice(0, 3), code.slice(4, 3)};
//validate
for(uint n : code) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
uint data = nibble(code, 0) << 4 | nibble(code, 1) << 0;
uint address = (nibble(code, 5) ^ 15) << 12 | nibble(code, 2) << 8 | nibble(code, 3) << 4 | nibble(code, 4) << 0;
code = {hex(address, 4L), "=", hex(data, 2L)};
return true;
}
//Game Genie
if(code.size() == 11 && code[3] == '-' && code[7] == '-') {
code = {code.slice(0, 3), code.slice(4, 3), code.slice(8, 3)};
//validate
for(uint n : code) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
uint data = nibble(code, 0) << 4 | nibble(code, 1) << 0;
uint address = (nibble(code, 5) ^ 15) << 12 | nibble(code, 2) << 8 | nibble(code, 3) << 4 | nibble(code, 4) << 0;
uint8_t t = nibble(code, 6) << 4 | nibble(code, 8) << 0;
t = t >> 2 | t << 6;
uint compare = t ^ 0xba;
code = {hex(address, 4L), "=", hex(compare, 2L), "?", hex(data, 2L)};
return true;
}
//GameShark
if(code.size() == 8) {
//validate
for(uint n : code) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
//first two characters are the code type / VRAM bank, which is almost always 01.
//other values are presumably supported, but I have no info on them, so they're not supported.
if(code[0] != '0') return false;
if(code[1] != '1') return false;
uint data = toHex(code.slice(2, 2));
uint16_t address = toHex(code.slice(4, 4));
address = address >> 8 | address << 8;
code = {hex(address, 4L), "=", hex(data, 2L)};
return true;
}
//higan: address=data
if(code.size() == 7 && code[4] == '=') {
string nibbles = {code.slice(0, 4), code.slice(5, 2)};
//validate
for(uint n : nibbles) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
//already in decoded form
return true;
}
//higan: address=compare?data
if(code.size() == 10 && code[4] == '=' && code[7] == '?') {
string nibbles = {code.slice(0, 4), code.slice(5, 2), code.slice(8, 2)};
//validate
for(uint n : nibbles) {
if(n >= '0' && n <= '9') continue;
if(n >= 'a' && n <= 'f') continue;
return false;
}
//already in decoded form
return true;
}
//unrecognized code format
return false;
}