* double-click a cheat finder result to add a new cheat code
* fixed v108.1 regression not enabling coprocessor LLE when requested
* add "[HLE] " title bar indicator for HLE mode
* default to LLE mode for coprocessors
* simplify game titles in main window (eg omit SGB BIOS name)
* add more GUI tooltips to explain options
* pause emulator during modal loops (helps Windows menubar navigation)
* add support for decoding Game Genie + Pro Action Replay SNES cheats
* add support for decoding Game Genie + GameShark Game Boy cheats
* add tool-tip explanation to verified/unverified status bar icon
This commit is contained in:
byuu 2019-08-05 09:27:51 +09:00
parent 24dce7dd92
commit e030428054
24 changed files with 326 additions and 62 deletions

View File

@ -56,12 +56,13 @@ Standard Features
- Sprite limit disable support
- Cubic audio interpolation support
- Optional high-level emulation of most SNES coprocessors
- SuperFX overclocking of up to 800%
- CPU, SA1, and SuperFX overclocking support
- Frame advance support
- Screenshot support
- Cheat code search support
- Movie recording and playback support
- Rewind support
- HiDPI support
Links
-----

View File

@ -31,7 +31,7 @@ using namespace nall;
namespace Emulator {
static const string Name = "bsnes";
static const string Version = "108.4";
static const string Version = "108.5";
static const string Author = "byuu";
static const string License = "GPLv3";
static const string Website = "https://byuu.org";

View File

@ -61,6 +61,7 @@ struct Interface {
virtual auto hashes() -> vector<string> { return {}; }
virtual auto manifests() -> vector<string> { return {}; }
virtual auto titles() -> vector<string> { return {}; }
virtual auto title() -> string { return {}; }
virtual auto load() -> bool { return false; }
virtual auto save() -> void {}
virtual auto unload() -> void {}

View File

@ -37,6 +37,17 @@ auto Cartridge::titles() const -> vector<string> {
return titles;
}
auto Cartridge::title() const -> string {
if(slotGameBoy.label) return slotGameBoy.label;
if(has.MCC && slotBSMemory.label) return slotBSMemory.label;
if(slotBSMemory.label) return {game.label, " + ", slotBSMemory.label};
if(slotSufamiTurboA.label && slotSufamiTurboB.label) return {slotSufamiTurboA.label, " + ", slotSufamiTurboB.label};
if(slotSufamiTurboA.label) return slotSufamiTurboA.label;
if(slotSufamiTurboB.label) return slotSufamiTurboB.label;
if(has.Cx4 || has.DSP1 || has.DSP2 || has.DSP4 || has.ST0010) return {"[HLE] ", game.label};
return game.label;
}
auto Cartridge::load() -> bool {
information = {};
has = {};

View File

@ -5,6 +5,7 @@ struct Cartridge {
auto hashes() const -> vector<string>;
auto manifests() const -> vector<string>;
auto titles() const -> vector<string>;
auto title() const -> string;
auto load() -> bool;
auto save() -> void;

View File

@ -428,7 +428,7 @@ auto Cartridge::loadHitachiDSP(Markup::Node node, uint roms) -> void {
}
}
if(configuration.hacks.coprocessors.hle) {
if(configuration.hacks.coprocessor.preferHLE) {
has.Cx4 = true;
for(auto map : node.find("map")) {
loadMap(map, {&Cx4::read, &cx4}, {&Cx4::write, &cx4});
@ -503,7 +503,7 @@ auto Cartridge::loaduPD7725(Markup::Node node) -> void {
}
}
if(failed || configuration.hacks.coprocessors.hle) {
if(failed || configuration.hacks.coprocessor.preferHLE) {
auto manifest = BML::serialize(game.document);
if(manifest.find("identifier: DSP1")) { //also matches DSP1B
has.DSP1 = true;
@ -583,7 +583,7 @@ auto Cartridge::loaduPD96050(Markup::Node node) -> void {
}
}
if(failed || configuration.hacks.coprocessors.hle) {
if(failed || configuration.hacks.coprocessor.preferHLE) {
auto manifest = BML::serialize(game.document);
if(manifest.find("identifier: ST010")) {
has.ST0010 = true;

View File

@ -1,5 +1,5 @@
auto SA1::BWRAM::conflict() const -> bool {
if(configuration.hacks.coprocessors.delayedSync) return false;
if(configuration.hacks.coprocessor.delayedSync) return false;
if((cpu.r.mar & 0x40e000) == 0x006000) return true; //00-3f,80-bf:6000-7fff
if((cpu.r.mar & 0xf00000) == 0x400000) return true; //40-4f:0000-ffff

View File

@ -1,5 +1,5 @@
auto SA1::IRAM::conflict() const -> bool {
if(configuration.hacks.coprocessors.delayedSync) return false;
if(configuration.hacks.coprocessor.delayedSync) return false;
if((cpu.r.mar & 0x40f800) == 0x003000) return cpu.refresh() == 0; //00-3f,80-bf:3000-37ff
return false;

View File

@ -1,5 +1,5 @@
auto SA1::ROM::conflict() const -> bool {
if(configuration.hacks.coprocessors.delayedSync) return false;
if(configuration.hacks.coprocessor.delayedSync) return false;
if((cpu.r.mar & 0x408000) == 0x008000) return true; //00-3f,80-bf:8000-ffff
if((cpu.r.mar & 0xc00000) == 0xc00000) return true; //c0-ff:0000-ffff

View File

@ -36,7 +36,7 @@ auto CPU::step() -> void {
overclocking.counter += Clocks;
if(overclocking.counter < overclocking.target) {
if constexpr(Synchronize) {
if(configuration.hacks.coprocessors.delayedSync) return;
if(configuration.hacks.coprocessor.delayedSync) return;
synchronizeCoprocessors();
}
return;
@ -64,7 +64,7 @@ auto CPU::step() -> void {
}
if constexpr(Synchronize) {
if(configuration.hacks.coprocessors.delayedSync) return;
if(configuration.hacks.coprocessor.delayedSync) return;
synchronizeCoprocessors();
}
}

View File

@ -25,8 +25,8 @@ auto Configuration::process(Markup::Node document, bool load) -> void {
bind(boolean, "Hacks/PPU/Mode7/Mosaic", hacks.ppu.mode7.mosaic);
bind(boolean, "Hacks/DSP/Fast", hacks.dsp.fast);
bind(boolean, "Hacks/DSP/Cubic", hacks.dsp.cubic);
bind(boolean, "Hacks/Coprocessors/HLE", hacks.coprocessors.hle);
bind(boolean, "Hacks/Coprocessors/DelayedSync", hacks.coprocessors.delayedSync);
bind(boolean, "Hacks/Coprocessor/DelayedSync", hacks.coprocessor.delayedSync);
bind(boolean, "Hacks/Coprocessor/PreferHLE", hacks.coprocessor.preferHLE);
bind(natural, "Hacks/SA1/Overclock", hacks.sa1.overclock);
bind(natural, "Hacks/SuperFX/Overclock", hacks.superfx.overclock);

View File

@ -42,10 +42,10 @@ struct Configuration {
bool fast = true;
bool cubic = false;
} dsp;
struct Coprocessors {
struct Coprocessor {
bool delayedSync = true;
bool hle = true;
} coprocessors;
bool preferHLE = false;
} coprocessor;
struct SA1 {
uint overclock = 100;
} sa1;

View File

@ -70,6 +70,10 @@ auto Interface::titles() -> vector<string> {
return cartridge.titles();
}
auto Interface::title() -> string {
return cartridge.title();
}
auto Interface::load() -> bool {
return system.load(this);
}

View File

@ -40,6 +40,7 @@ struct Interface : Emulator::Interface {
auto hashes() -> vector<string> override;
auto manifests() -> vector<string> override;
auto titles() -> vector<string> override;
auto title() -> string override;
auto load() -> bool override;
auto save() -> void override;
auto unload() -> void override;

View File

@ -277,12 +277,23 @@ auto Presentation::updateStatusIcon() -> void {
icon.allocate(16, StatusHeight);
icon.fill(0xff202020);
if(emulator->loaded()) {
image emblem{program.verified() ? (image)Icon::Emblem::Program : (image)Icon::Emblem::Binary};
if(emulator->loaded() && program.verified()) {
image emblem{Icon::Emblem::Program};
icon.impose(image::blend::sourceAlpha, 0, (StatusHeight - 16) / 2, emblem, 0, 0, 16, 16);
statusIcon.setIcon(icon).setToolTip(
"This is a known clean game image.\n"
"PCB emulation is 100% accurate."
);
} else if(emulator->loaded()) {
image emblem{Icon::Emblem::Binary};
icon.impose(image::blend::sourceAlpha, 0, (StatusHeight - 16) / 2, emblem, 0, 0, 16, 16);
statusIcon.setIcon(icon).setToolTip(
"This is not a verified game image.\n"
"PCB emulation is relying on heuristics."
);
} else {
statusIcon.setIcon(icon).setToolTip();
}
statusIcon.setIcon(icon);
}
auto Presentation::resizeWindow() -> void {

View File

@ -10,8 +10,8 @@ auto Program::load() -> void {
emulator->configure("Hacks/PPU/Mode7/Mosaic", settings.emulator.hack.ppu.mode7.mosaic);
emulator->configure("Hacks/DSP/Fast", settings.emulator.hack.dsp.fast);
emulator->configure("Hacks/DSP/Cubic", settings.emulator.hack.dsp.cubic);
emulator->configure("Hacks/Coprocessor/DelayedSync", settings.emulator.hack.coprocessors.delayedSync);
emulator->configure("Hacks/Coprocessor/HLE", settings.emulator.hack.coprocessors.hle);
emulator->configure("Hacks/Coprocessor/DelayedSync", settings.emulator.hack.coprocessor.delayedSync);
emulator->configure("Hacks/Coprocessor/PreferHLE", settings.emulator.hack.coprocessor.preferHLE);
emulator->configure("Hacks/SuperFX/Overclock", settings.emulator.hack.superfx.overclock);
if(!emulator->load()) return;
@ -45,7 +45,7 @@ auto Program::load() -> void {
appliedPatch() ? " and patch applied" : ""
});
presentation.setFocused();
presentation.setTitle(emulator->titles().merge(" + "));
presentation.setTitle(emulator->title());
presentation.resetSystem.setEnabled(true);
presentation.unloadGame.setEnabled(true);
presentation.toolsMenu.setVisible(true);

View File

@ -2,7 +2,7 @@ auto Program::hackCompatibility() -> void {
bool fastPPU = emulatorSettings.fastPPU.checked();
bool fastPPUNoSpriteLimit = emulatorSettings.noSpriteLimit.checked();
bool fastDSP = emulatorSettings.fastDSP.checked();
bool coprocessorsDelayedSync = emulatorSettings.coprocessorsDelayedSyncOption.checked();
bool coprocessorDelayedSync = emulatorSettings.coprocessorDelayedSyncOption.checked();
auto title = superFamicom.title;
if(title == "AIR STRIKE PATROL" || title == "DESERT FIGHTER") fastPPU = false;
@ -17,7 +17,7 @@ auto Program::hackCompatibility() -> void {
emulator->configure("Hacks/PPU/Mode7/Mosaic", settings.emulator.hack.ppu.mode7.mosaic);
emulator->configure("Hacks/DSP/Fast", fastDSP);
emulator->configure("Hacks/DSP/Cubic", settings.emulator.hack.dsp.cubic);
emulator->configure("Hacks/Coprocessors/DelayedSync", coprocessorsDelayedSync);
emulator->configure("Hacks/Coprocessor/DelayedSync", coprocessorDelayedSync);
}
auto Program::hackPatchMemory(vector<uint8_t>& data) -> void {

View File

@ -75,15 +75,19 @@ auto Program::create() -> void {
auto Program::main() -> void {
updateStatus();
video.poll();
if(Application::modal()) {
audio.clear();
return;
}
inputManager.poll();
inputManager.pollHotkeys();
if(inactive()) {
audio.clear();
if(!Application::modal()) {
usleep(20 * 1000);
viewportRefresh();
}
return;
}

View File

@ -74,11 +74,14 @@ auto EmulatorSettings::create() -> void {
emulator->configure("Hacks/DSP/Cubic", settings.emulator.hack.dsp.cubic);
});
coprocessorLabel.setText("Coprocessors").setFont(Font().setBold());
coprocessorsDelayedSyncOption.setText("Fast mode").setChecked(settings.emulator.hack.coprocessors.delayedSync).onToggle([&] {
settings.emulator.hack.coprocessors.delayedSync = coprocessorsDelayedSyncOption.checked();
coprocessorDelayedSyncOption.setText("Fast mode").setChecked(settings.emulator.hack.coprocessor.delayedSync).onToggle([&] {
settings.emulator.hack.coprocessor.delayedSync = coprocessorDelayedSyncOption.checked();
});
coprocessorsHLEOption.setText("Prefer HLE").setChecked(settings.emulator.hack.coprocessors.hle).onToggle([&] {
settings.emulator.hack.coprocessors.hle = coprocessorsHLEOption.checked();
coprocessorPreferHLEOption.setText("Prefer HLE").setChecked(settings.emulator.hack.coprocessor.preferHLE).setToolTip(
"When checked, less accurate HLE emulation will always be used when available.\n"
"When unchecked, HLE will only be used when LLE firmware is missing."
).onToggle([&] {
settings.emulator.hack.coprocessor.preferHLE = coprocessorPreferHLEOption.checked();
});
hacksNote.setText("Note: some hack setting changes do not take effect until after reloading games.");
}

View File

@ -118,8 +118,8 @@ auto Settings::process(bool load) -> void {
bind(boolean, "Emulator/Hack/PPU/Mode7/Mosaic", emulator.hack.ppu.mode7.mosaic);
bind(boolean, "Emulator/Hack/DSP/Fast", emulator.hack.dsp.fast);
bind(boolean, "Emulator/Hack/DSP/Cubic", emulator.hack.dsp.cubic);
bind(boolean, "Emulator/Hack/Coprocessors/DelayedSync", emulator.hack.coprocessors.delayedSync);
bind(boolean, "Emulator/Hack/Coprocessors/HLE", emulator.hack.coprocessors.hle);
bind(boolean, "Emulator/Hack/Coprocessor/DelayedSync", emulator.hack.coprocessor.delayedSync);
bind(boolean, "Emulator/Hack/Coprocessor/PreferHLE", emulator.hack.coprocessor.preferHLE);
bind(natural, "Emulator/Hack/SA1/Overclock", emulator.hack.sa1.overclock);
bind(natural, "Emulator/Hack/SuperFX/Overclock", emulator.hack.superfx.overclock);
bind(boolean, "Emulator/Cheats/Enable", emulator.cheats.enable);

View File

@ -109,10 +109,10 @@ struct Settings : Markup::Node {
bool fast = true;
bool cubic = false;
} dsp;
struct Coprocessors {
struct Coprocessor {
bool delayedSync = true;
bool hle = true;
} coprocessors;
bool preferHLE = false;
} coprocessor;
struct SA1 {
uint overclock = 100;
} sa1;
@ -341,9 +341,9 @@ public:
CheckLabel fastDSP{&dspLayout, Size{0, 0}};
CheckLabel cubicInterpolation{&dspLayout, Size{0, 0}};
Label coprocessorLabel{this, Size{~0, 0}, 2};
HorizontalLayout coprocessorsLayout{this, Size{~0, 0}};
CheckLabel coprocessorsDelayedSyncOption{&coprocessorsLayout, Size{0, 0}};
CheckLabel coprocessorsHLEOption{&coprocessorsLayout, Size{0, 0}};
HorizontalLayout coprocessorLayout{this, Size{~0, 0}};
CheckLabel coprocessorDelayedSyncOption{&coprocessorLayout, Size{0, 0}};
CheckLabel coprocessorPreferHLEOption{&coprocessorLayout, Size{0, 0}};
Label hacksNote{this, Size{~0, 0}};
};

View File

@ -29,9 +29,12 @@ auto CheatDatabase::findCheats() -> void {
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 code = cheat["code"].text();
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())
@ -98,7 +101,35 @@ auto CheatWindow::doChange() -> void {
}
auto CheatWindow::doAccept() -> void {
Cheat cheat = {nameValue.text().strip(), codeValue.text().split("\n").strip().merge("+"), enableOption.checked()};
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 {
@ -260,3 +291,174 @@ auto CheatEditor::synchronizeCodes() -> void {
}
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;
}

View File

@ -3,6 +3,28 @@ auto CheatFinder::create() -> void {
setVisible(false);
searchList.setHeadered();
searchList.onActivate([&](auto cell) {
if(auto item = searchList.selected()) {
uint address = toHex(item.cell(0).text().trimLeft("0x", 1L));
string data = item.cell(1).text().trimLeft("0x", 1L).split(" ", 1L).first();
string code;
if(data.size() == 2) {
code.append(hex(address + 0, 6L), "=", data.slice(0, 2), "\n");
}
if(data.size() == 4) {
code.append(hex(address + 0, 6L), "=", data.slice(2, 2), "\n");
code.append(hex(address + 1, 6L), "=", data.slice(0, 2), "\n");
}
if(data.size() == 6) {
code.append(hex(address + 0, 6L), "=", data.slice(4, 2), "\n");
code.append(hex(address + 1, 6L), "=", data.slice(2, 2), "\n");
code.append(hex(address + 2, 6L), "=", data.slice(0, 2), "\n");
}
toolsWindow.show(1);
cheatEditor.addButton.doActivate();
cheatWindow.codeValue.setText(code).doChange();
}
});
searchValue.onActivate([&] { eventScan(); });
searchLabel.setText("Value:");
searchSize.append(ComboButtonItem().setText("Byte"));
@ -34,7 +56,7 @@ auto CheatFinder::restart() -> void {
auto CheatFinder::refresh() -> void {
searchList.reset();
searchList.append(TableViewColumn().setText("Address"));
searchList.append(TableViewColumn().setText("Value").setExpandable());
searchList.append(TableViewColumn().setText("Value"));
for(auto& candidate : candidates) {
TableViewItem item{&searchList};

View File

@ -88,6 +88,9 @@ struct CheatEditor : VerticalLayout {
auto saveCheats() -> void;
auto synchronizeCodes() -> void;
auto decodeSNES(string& code) -> bool;
auto decodeGB(string& code) -> bool;
public:
vector<Cheat> cheats;
uint64_t activateTimeout = 0;