mirror of https://github.com/bsnes-emu/bsnes.git
Update to v106r35 release.
byuu says: Changelog: - sfc/ppu-fast: fixed overscan crash - sfc/ppu-fast: fixed direct color mode - sfc: reconnected MSU1 support - higan: game.sfc/msu1/data.rom, game.sfc/msu1/track-#.pcm - bsnes: game.msu, game-#.pcm - bsnes: added cheat code editor - bsnes: added cheat code database support - sfc/ppu-fast: clear overscan lines when overscan disabled - sfc: output 223/239 lines instead of 224/240 lines - bsnes: fix aspect correction calculation - bsnes: crop line 224 when overscan masking is enabled - bsnes: exposed Expansion Port menu; but hid “21fx” from the list of devices - bsnes: tools menu is hidden until a game is loaded - ruby/input/keyboard/quartz: fixed compilation error So only bsnes the automated overscan cropping option. In higan, you can crop however many lines you like from the top or bottom of the image. But for bsnes, it automatically eats sixteen lines. My view right now is that if bsnes is meant to be the casual gaming emulator, that it should eat line 224 in this mode. Most games show content here, but because of the way the SNES PPU works, the very last line ends up on its very own tile row (line 0 isn't rendered), if the scroll registers don't account for it. There's a small number of games that will draw junk data to the very last scanline of the frame as a result of this. So I chose, at least for now, to hide it. Users can obviously disable overscan cropping to see this scanline. I'm open to being convinced not to do this, if someone has a compelling reason. We're pretty much screwed one way or the other with no overscan masking. If we output 239 lines, then most games will render 7 blank lines + 224 drawn lines + 8 blank lines, and the black top and bottom aren't centered. But if we output 240 lines to get 8 + 224 + 8, then games that do use overscan will have a blank line at the very bottom of the window. I'm also trying out a modified cheat code file format. It's been forever since I bothered to look at it, and the “cartridge” parent node doesn't match what I'm doing with trying to rename “cartridge” to “game” in manifests. And indeed, the idea of requiring a root node is rather superfluous for a cheat code file. Current format looks like this: cheat description: foo code: 7e2000=20+7e2001=30?40 enabled cheat description: bar code: 7e4000=80 Open to discussing this, and I'd like to sync up with Snes9X before they push out a new release, and I'll agree to finalize and never change this format again. I chose to use .cht for the extension when using game files (eg gamename.cht)
This commit is contained in:
parent
8c337d4ac6
commit
77ac5f9e88
|
@ -12,7 +12,7 @@ using namespace nall;
|
||||||
|
|
||||||
namespace Emulator {
|
namespace Emulator {
|
||||||
static const string Name = "higan";
|
static const string Name = "higan";
|
||||||
static const string Version = "106.34";
|
static const string Version = "106.35";
|
||||||
static const string Author = "byuu";
|
static const string Author = "byuu";
|
||||||
static const string License = "GPLv3";
|
static const string License = "GPLv3";
|
||||||
static const string Website = "https://byuu.org/";
|
static const string Website = "https://byuu.org/";
|
||||||
|
|
|
@ -89,7 +89,7 @@ private:
|
||||||
auto loadSPC7110(Markup::Node) -> void;
|
auto loadSPC7110(Markup::Node) -> void;
|
||||||
auto loadSDD1(Markup::Node) -> void;
|
auto loadSDD1(Markup::Node) -> void;
|
||||||
auto loadOBC1(Markup::Node) -> void;
|
auto loadOBC1(Markup::Node) -> void;
|
||||||
auto loadMSU1(Markup::Node) -> void;
|
auto loadMSU1() -> void;
|
||||||
|
|
||||||
//save.cpp
|
//save.cpp
|
||||||
auto saveCartridge(Markup::Node) -> void;
|
auto saveCartridge(Markup::Node) -> void;
|
||||||
|
|
|
@ -67,7 +67,8 @@ auto Cartridge::loadCartridge(Markup::Node node) -> void {
|
||||||
if(auto node = board["processor(identifier=SPC7110)"]) loadSPC7110(node);
|
if(auto node = board["processor(identifier=SPC7110)"]) loadSPC7110(node);
|
||||||
if(auto node = board["processor(identifier=SDD1)"]) loadSDD1(node);
|
if(auto node = board["processor(identifier=SDD1)"]) loadSDD1(node);
|
||||||
if(auto node = board["processor(identifier=OBC1)"]) loadOBC1(node);
|
if(auto node = board["processor(identifier=OBC1)"]) loadOBC1(node);
|
||||||
if(auto node = board["processor(identifier=MSU1)"]) loadMSU1(node);
|
|
||||||
|
if(auto fp = platform->open(pathID(), "msu1/data.rom", File::Read)) loadMSU1();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Cartridge::loadCartridgeGameBoy(Markup::Node node) -> void {
|
auto Cartridge::loadCartridgeGameBoy(Markup::Node node) -> void {
|
||||||
|
@ -642,11 +643,9 @@ auto Cartridge::loadOBC1(Markup::Node node) -> void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//processor(identifier=MSU1)
|
//file::exists("msu1/data.rom")
|
||||||
auto Cartridge::loadMSU1(Markup::Node node) -> void {
|
auto Cartridge::loadMSU1() -> void {
|
||||||
has.MSU1 = true;
|
has.MSU1 = true;
|
||||||
|
|
||||||
for(auto map : node.find("map")) {
|
bus.map({&MSU1::readIO, &msu1}, {&MSU1::writeIO, &msu1}, "00-3f,80-bf:2000-2007");
|
||||||
loadMap(map, {&MSU1::readIO, &msu1}, {&MSU1::writeIO, &msu1});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ auto MSU1::Enter() -> void {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto MSU1::main() -> void {
|
auto MSU1::main() -> void {
|
||||||
double left = 0.0;
|
double left = 0.0;
|
||||||
double right = 0.0;
|
double right = 0.0;
|
||||||
|
|
||||||
if(io.audioPlay) {
|
if(io.audioPlay) {
|
||||||
|
@ -72,9 +72,7 @@ auto MSU1::power() -> void {
|
||||||
|
|
||||||
auto MSU1::dataOpen() -> void {
|
auto MSU1::dataOpen() -> void {
|
||||||
dataFile.reset();
|
dataFile.reset();
|
||||||
auto document = Markup::Node(); //todo: fix this
|
string name = {"msu1/data.rom"};
|
||||||
string name = document["board/msu1/rom/name"].text();
|
|
||||||
if(!name) name = "msu1.rom";
|
|
||||||
if(dataFile = platform->open(ID::SuperFamicom, name, File::Read)) {
|
if(dataFile = platform->open(ID::SuperFamicom, name, File::Read)) {
|
||||||
dataFile->seek(io.dataReadOffset);
|
dataFile->seek(io.dataReadOffset);
|
||||||
}
|
}
|
||||||
|
@ -82,13 +80,7 @@ auto MSU1::dataOpen() -> void {
|
||||||
|
|
||||||
auto MSU1::audioOpen() -> void {
|
auto MSU1::audioOpen() -> void {
|
||||||
audioFile.reset();
|
audioFile.reset();
|
||||||
auto document = Markup::Node(); //todo: fix this
|
string name = {"msu1/track-", io.audioTrack, ".pcm"};
|
||||||
string name = {"track-", io.audioTrack, ".pcm"};
|
|
||||||
for(auto track : document.find("board/msu1/track")) {
|
|
||||||
if(track["number"].natural() != io.audioTrack) continue;
|
|
||||||
name = track["name"].text();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if(audioFile = platform->open(ID::SuperFamicom, name, File::Read)) {
|
if(audioFile = platform->open(ID::SuperFamicom, name, File::Read)) {
|
||||||
if(audioFile->size() >= 8) {
|
if(audioFile->size() >= 8) {
|
||||||
uint32 header = audioFile->readm(4);
|
uint32 header = audioFile->readm(4);
|
||||||
|
@ -164,7 +156,7 @@ auto MSU1::writeIO(uint24 addr, uint8 data) -> void {
|
||||||
if(io.audioError) break;
|
if(io.audioError) break;
|
||||||
io.audioPlay = data.bit(0);
|
io.audioPlay = data.bit(0);
|
||||||
io.audioRepeat = data.bit(1);
|
io.audioRepeat = data.bit(1);
|
||||||
bool audioResume = data.bit(2);
|
boolean audioResume = data.bit(2);
|
||||||
if(!io.audioPlay && audioResume) {
|
if(!io.audioPlay && audioResume) {
|
||||||
io.audioResumeTrack = io.audioTrack;
|
io.audioResumeTrack = io.audioTrack;
|
||||||
io.audioResumeOffset = io.audioPlayOffset;
|
io.audioResumeOffset = io.audioPlayOffset;
|
||||||
|
|
|
@ -40,11 +40,11 @@ private:
|
||||||
uint32 audioResumeTrack;
|
uint32 audioResumeTrack;
|
||||||
uint32 audioResumeOffset;
|
uint32 audioResumeOffset;
|
||||||
|
|
||||||
bool audioError;
|
boolean audioError;
|
||||||
bool audioPlay;
|
boolean audioPlay;
|
||||||
bool audioRepeat;
|
boolean audioRepeat;
|
||||||
bool audioBusy;
|
boolean audioBusy;
|
||||||
bool dataBusy;
|
boolean dataBusy;
|
||||||
} io;
|
} io;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,11 @@ auto MSU1::serialize(serializer& s) -> void {
|
||||||
s.integer(io.audioResumeTrack);
|
s.integer(io.audioResumeTrack);
|
||||||
s.integer(io.audioResumeOffset);
|
s.integer(io.audioResumeOffset);
|
||||||
|
|
||||||
s.integer(io.audioError);
|
s.boolean(io.audioError);
|
||||||
s.integer(io.audioPlay);
|
s.boolean(io.audioPlay);
|
||||||
s.integer(io.audioRepeat);
|
s.boolean(io.audioRepeat);
|
||||||
s.integer(io.audioBusy);
|
s.boolean(io.audioBusy);
|
||||||
s.integer(io.dataBusy);
|
s.boolean(io.dataBusy);
|
||||||
|
|
||||||
dataOpen();
|
dataOpen();
|
||||||
audioOpen();
|
audioOpen();
|
||||||
|
|
|
@ -120,9 +120,9 @@ auto Interface::title() -> string {
|
||||||
auto Interface::videoInformation() -> VideoInformation {
|
auto Interface::videoInformation() -> VideoInformation {
|
||||||
VideoInformation vi;
|
VideoInformation vi;
|
||||||
vi.width = 256;
|
vi.width = 256;
|
||||||
vi.height = 240;
|
vi.height = 239;
|
||||||
vi.internalWidth = 512;
|
vi.internalWidth = 512;
|
||||||
vi.internalHeight = 480;
|
vi.internalHeight = 478;
|
||||||
vi.aspectCorrection = 8.0 / 7.0;
|
vi.aspectCorrection = 8.0 / 7.0;
|
||||||
if(Region::NTSC()) vi.refreshRate = system.cpuFrequency() / (262.0 * 1364.0);
|
if(Region::NTSC()) vi.refreshRate = system.cpuFrequency() / (262.0 * 1364.0);
|
||||||
if(Region::PAL()) vi.refreshRate = system.cpuFrequency() / (312.0 * 1364.0);
|
if(Region::PAL()) vi.refreshRate = system.cpuFrequency() / (312.0 * 1364.0);
|
||||||
|
@ -249,12 +249,11 @@ auto Interface::get(const string& name) -> any {
|
||||||
auto Interface::set(const string& name, const any& value) -> bool {
|
auto Interface::set(const string& name, const any& value) -> bool {
|
||||||
if(name == "Blur Emulation" && value.is<bool>()) {
|
if(name == "Blur Emulation" && value.is<bool>()) {
|
||||||
settings.blurEmulation = value.get<bool>();
|
settings.blurEmulation = value.get<bool>();
|
||||||
system.configureVideoEffects();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(name == "Color Emulation" && value.is<bool>()) {
|
if(name == "Color Emulation" && value.is<bool>()) {
|
||||||
settings.colorEmulation = value.get<bool>();
|
settings.colorEmulation = value.get<bool>();
|
||||||
system.configureVideoPalette();
|
Emulator::video.setPalette();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(name == "Scanline Emulation" && value.is<bool>()) return settings.scanlineEmulation = value.get<bool>(), true;
|
if(name == "Scanline Emulation" && value.is<bool>()) return settings.scanlineEmulation = value.get<bool>(), true;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
auto PPU::Line::renderBackground(PPU::IO::Background& self, uint source) -> void {
|
auto PPU::Line::renderBackground(PPU::IO::Background& self, uint source) -> void {
|
||||||
if(io.displayDisable) return;
|
|
||||||
if(!self.aboveEnable && !self.belowEnable) return;
|
if(!self.aboveEnable && !self.belowEnable) return;
|
||||||
if(self.tileMode == TileMode::Mode7) return renderMode7(self, source);
|
if(self.tileMode == TileMode::Mode7) return renderMode7(self, source);
|
||||||
if(self.tileMode == TileMode::Inactive) return;
|
if(self.tileMode == TileMode::Inactive) return;
|
||||||
|
|
|
@ -3,7 +3,7 @@ uint PPU::Line::count = 0;
|
||||||
|
|
||||||
auto PPU::Line::flush() -> void {
|
auto PPU::Line::flush() -> void {
|
||||||
if(Line::count) {
|
if(Line::count) {
|
||||||
#pragma omp parallel for
|
#pragma omp parallel for if(Line::count >= 8)
|
||||||
for(uint y = 0; y < Line::count; y++) {
|
for(uint y = 0; y < Line::count; y++) {
|
||||||
ppu.lines[Line::start + y].render();
|
ppu.lines[Line::start + y].render();
|
||||||
}
|
}
|
||||||
|
@ -13,15 +13,21 @@ auto PPU::Line::flush() -> void {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto PPU::Line::render() -> void {
|
auto PPU::Line::render() -> void {
|
||||||
bool hires = io.pseudoHires || io.bgMode == 5 || io.bgMode == 6;
|
auto output = ppu.output + y * 1024;
|
||||||
|
if(ppu.interlace() && ppu.field()) output += 512;
|
||||||
|
auto width = !ppu.hires() ? 256 : 512;
|
||||||
|
|
||||||
if(!io.displayDisable) {
|
if(io.displayDisable) {
|
||||||
auto aboveColor = cgram[0];
|
memory::fill<uint32>(output, width);
|
||||||
auto belowColor = hires ? cgram[0] : io.col.fixedColor;
|
return;
|
||||||
for(uint x : range(256)) {
|
}
|
||||||
above[x] = {Source::COL, 0, aboveColor};
|
|
||||||
below[x] = {Source::COL, 0, belowColor};
|
bool hires = io.pseudoHires || io.bgMode == 5 || io.bgMode == 6;
|
||||||
}
|
auto aboveColor = cgram[0];
|
||||||
|
auto belowColor = hires ? cgram[0] : io.col.fixedColor;
|
||||||
|
for(uint x : range(256)) {
|
||||||
|
above[x] = {Source::COL, 0, aboveColor};
|
||||||
|
below[x] = {Source::COL, 0, belowColor};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBackground(io.bg1, Source::BG1);
|
renderBackground(io.bg1, Source::BG1);
|
||||||
|
@ -29,20 +35,10 @@ auto PPU::Line::render() -> void {
|
||||||
renderBackground(io.bg3, Source::BG3);
|
renderBackground(io.bg3, Source::BG3);
|
||||||
renderBackground(io.bg4, Source::BG4);
|
renderBackground(io.bg4, Source::BG4);
|
||||||
renderObject(io.obj);
|
renderObject(io.obj);
|
||||||
|
|
||||||
auto output = ppu.output + y * 1024;
|
|
||||||
if(ppu.interlace() && ppu.field()) output += 512;
|
|
||||||
auto width = !ppu.hires() ? 256 : 512;
|
|
||||||
auto luma = io.displayBrightness << 15;
|
|
||||||
|
|
||||||
if(io.displayDisable) {
|
|
||||||
for(uint x : range(width)) output[x] = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWindow(io.col.window, io.col.window.aboveMask, windowAbove);
|
renderWindow(io.col.window, io.col.window.aboveMask, windowAbove);
|
||||||
renderWindow(io.col.window, io.col.window.belowMask, windowBelow);
|
renderWindow(io.col.window, io.col.window.belowMask, windowBelow);
|
||||||
|
|
||||||
|
auto luma = io.displayBrightness << 15;
|
||||||
if(width == 256) for(uint x : range(width)) {
|
if(width == 256) for(uint x : range(width)) {
|
||||||
*output++ = luma | pixel(x, above[x], below[x]);
|
*output++ = luma | pixel(x, above[x], below[x]);
|
||||||
} else if(!hires) for(uint x : range(256)) {
|
} else if(!hires) for(uint x : range(256)) {
|
||||||
|
@ -83,10 +79,13 @@ auto PPU::Line::blend(uint x, uint y, bool halve) const -> uint15 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto PPU::Line::directColor(uint palette, uint tile) const -> uint15 {
|
auto PPU::Line::directColor(uint paletteIndex, uint paletteColor) const -> uint15 {
|
||||||
return (palette << 7 & 0x6000) + (tile >> 0 & 0x1000)
|
//paletteIndex = bgr
|
||||||
+ (palette << 4 & 0x0380) + (tile >> 5 & 0x0040)
|
//paletteColor = BBGGGRRR
|
||||||
+ (palette << 2 & 0x001c) + (tile >> 9 & 0x0002);
|
//output = 0 BBb00 GGGg0 RRRr0
|
||||||
|
return (paletteColor << 2 & 0x001c) + (paletteIndex << 1 & 0x0002) //R
|
||||||
|
+ (paletteColor << 4 & 0x0380) + (paletteIndex << 5 & 0x0040) //G
|
||||||
|
+ (paletteColor << 7 & 0x6000) + (paletteIndex << 10 & 0x1000); //B
|
||||||
}
|
}
|
||||||
|
|
||||||
auto PPU::Line::plotAbove(uint x, uint source, uint priority, uint color) -> void {
|
auto PPU::Line::plotAbove(uint x, uint source, uint priority, uint color) -> void {
|
||||||
|
|
|
@ -51,7 +51,7 @@ auto PPU::Line::renderMode7(PPU::IO::Background& self, uint source) -> void {
|
||||||
mosaicCounter = 1 + io.mosaicSize;
|
mosaicCounter = 1 + io.mosaicSize;
|
||||||
mosaicPalette = palette;
|
mosaicPalette = palette;
|
||||||
mosaicPriority = priority;
|
mosaicPriority = priority;
|
||||||
if(io.col.directColor) {
|
if(io.col.directColor && source == Source::BG1) {
|
||||||
mosaicColor = directColor(0, palette);
|
mosaicColor = directColor(0, palette);
|
||||||
} else {
|
} else {
|
||||||
mosaicColor = cgram[palette];
|
mosaicColor = cgram[palette];
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
auto PPU::Line::renderObject(PPU::IO::Object& self) -> void {
|
auto PPU::Line::renderObject(PPU::IO::Object& self) -> void {
|
||||||
if(io.displayDisable) return;
|
|
||||||
if(!self.aboveEnable && !self.belowEnable) return;
|
if(!self.aboveEnable && !self.belowEnable) return;
|
||||||
|
|
||||||
bool windowAbove[256];
|
bool windowAbove[256];
|
||||||
|
|
|
@ -44,9 +44,13 @@ auto PPU::main() -> void {
|
||||||
scanline();
|
scanline();
|
||||||
uint y = vcounter();
|
uint y = vcounter();
|
||||||
step(512);
|
step(512);
|
||||||
if(y >= 1 && y <= vdisp()) {
|
if(y >= 1 && y <= 239) {
|
||||||
memcpy(&lines[y].io, &io, sizeof(io));
|
if(io.displayDisable || y >= vdisp()) {
|
||||||
memcpy(&lines[y].cgram, &cgram, sizeof(cgram));
|
lines[y].io.displayDisable = true;
|
||||||
|
} else {
|
||||||
|
memcpy(&lines[y].io, &io, sizeof(io));
|
||||||
|
memcpy(&lines[y].cgram, &cgram, sizeof(cgram));
|
||||||
|
}
|
||||||
if(!Line::count) Line::start = y;
|
if(!Line::count) Line::start = y;
|
||||||
Line::count++;
|
Line::count++;
|
||||||
}
|
}
|
||||||
|
@ -78,13 +82,12 @@ auto PPU::scanline() -> void {
|
||||||
|
|
||||||
auto PPU::refresh() -> void {
|
auto PPU::refresh() -> void {
|
||||||
auto output = this->output;
|
auto output = this->output;
|
||||||
if(!overscan()) output -= 14 * 512;
|
if(!overscan()) output -= 12 * 512;
|
||||||
auto pitch = 512 << !interlace();
|
auto pitch = 512 << !interlace();
|
||||||
auto width = 256 << hires();
|
auto width = 256 << hires();
|
||||||
auto height = 240 << interlace();
|
auto height = 239 << interlace();
|
||||||
if(!hires()) Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, false);
|
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation && hires());
|
||||||
Emulator::video.refresh(output, pitch * sizeof(uint32), width, height);
|
Emulator::video.refresh(output, pitch * sizeof(uint32), width, height);
|
||||||
if(!hires()) Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto PPU::load(Markup::Node node) -> bool {
|
auto PPU::load(Markup::Node node) -> bool {
|
||||||
|
@ -94,7 +97,7 @@ auto PPU::load(Markup::Node node) -> bool {
|
||||||
auto PPU::power(bool reset) -> void {
|
auto PPU::power(bool reset) -> void {
|
||||||
create(Enter, system.cpuFrequency());
|
create(Enter, system.cpuFrequency());
|
||||||
PPUcounter::reset();
|
PPUcounter::reset();
|
||||||
memory::fill<uint32>(output, 512 * 480);
|
memory::fill<uint32>(output, 512 * 478);
|
||||||
|
|
||||||
function<auto (uint24, uint8) -> uint8> reader{&PPU::readIO, this};
|
function<auto (uint24, uint8) -> uint8> reader{&PPU::readIO, this};
|
||||||
function<auto (uint24, uint8) -> void> writer{&PPU::writeIO, this};
|
function<auto (uint24, uint8) -> void> writer{&PPU::writeIO, this};
|
||||||
|
|
|
@ -263,7 +263,7 @@ public:
|
||||||
auto render() -> void;
|
auto render() -> void;
|
||||||
auto pixel(uint x, Pixel above, Pixel below) const -> uint15;
|
auto pixel(uint x, Pixel above, Pixel below) const -> uint15;
|
||||||
auto blend(uint x, uint y, bool halve) const -> uint15;
|
auto blend(uint x, uint y, bool halve) const -> uint15;
|
||||||
alwaysinline auto directColor(uint palette, uint tile) const -> uint15;
|
alwaysinline auto directColor(uint paletteIndex, uint paletteColor) const -> uint15;
|
||||||
alwaysinline auto plotAbove(uint x, uint source, uint priority, uint color) -> void;
|
alwaysinline auto plotAbove(uint x, uint source, uint priority, uint color) -> void;
|
||||||
alwaysinline auto plotBelow(uint x, uint source, uint priority, uint color) -> void;
|
alwaysinline auto plotBelow(uint x, uint source, uint priority, uint color) -> void;
|
||||||
|
|
||||||
|
|
|
@ -72,11 +72,9 @@ auto PPU::main() -> void {
|
||||||
|
|
||||||
step(14);
|
step(14);
|
||||||
obj.tilefetch();
|
obj.tilefetch();
|
||||||
} else {
|
|
||||||
step(1052 + 14 + 136);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
step(lineclocks() - 28 - 1052 - 14 - 136);
|
step(lineclocks() - hcounter());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto PPU::load(Markup::Node node) -> bool {
|
auto PPU::load(Markup::Node node) -> bool {
|
||||||
|
@ -223,10 +221,11 @@ auto PPU::frame() -> void {
|
||||||
|
|
||||||
auto PPU::refresh() -> void {
|
auto PPU::refresh() -> void {
|
||||||
auto output = this->output;
|
auto output = this->output;
|
||||||
if(!overscan()) output -= 14 * 512;
|
if(!overscan()) output -= 12 * 512;
|
||||||
auto pitch = 512;
|
auto pitch = 512;
|
||||||
auto width = 512;
|
auto width = 512;
|
||||||
auto height = 480;
|
auto height = 478;
|
||||||
|
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
|
||||||
Emulator::video.refresh(output, pitch * sizeof(uint32), width, height);
|
Emulator::video.refresh(output, pitch * sizeof(uint32), width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ System system;
|
||||||
Scheduler scheduler;
|
Scheduler scheduler;
|
||||||
Random random;
|
Random random;
|
||||||
Cheat cheat;
|
Cheat cheat;
|
||||||
#include "video.cpp"
|
|
||||||
#include "serialization.cpp"
|
#include "serialization.cpp"
|
||||||
|
|
||||||
auto System::run() -> void {
|
auto System::run() -> void {
|
||||||
|
@ -91,8 +90,7 @@ auto System::unload() -> void {
|
||||||
auto System::power(bool reset) -> void {
|
auto System::power(bool reset) -> void {
|
||||||
Emulator::video.reset();
|
Emulator::video.reset();
|
||||||
Emulator::video.setInterface(interface);
|
Emulator::video.setInterface(interface);
|
||||||
configureVideoPalette();
|
Emulator::video.setPalette();
|
||||||
configureVideoEffects();
|
|
||||||
|
|
||||||
Emulator::audio.reset();
|
Emulator::audio.reset();
|
||||||
Emulator::audio.setInterface(interface);
|
Emulator::audio.setInterface(interface);
|
||||||
|
|
|
@ -14,10 +14,6 @@ struct System {
|
||||||
auto unload() -> void;
|
auto unload() -> void;
|
||||||
auto power(bool reset) -> void;
|
auto power(bool reset) -> void;
|
||||||
|
|
||||||
//video.cpp
|
|
||||||
auto configureVideoPalette() -> void;
|
|
||||||
auto configureVideoEffects() -> void;
|
|
||||||
|
|
||||||
//serialization.cpp
|
//serialization.cpp
|
||||||
auto serialize() -> serializer;
|
auto serialize() -> serializer;
|
||||||
auto unserialize(serializer&) -> bool;
|
auto unserialize(serializer&) -> bool;
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
auto System::configureVideoPalette() -> void {
|
|
||||||
Emulator::video.setPalette();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto System::configureVideoEffects() -> void {
|
|
||||||
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
|
|
||||||
}
|
|
|
@ -5,7 +5,8 @@ include sfc/GNUmakefile
|
||||||
include gb/GNUmakefile
|
include gb/GNUmakefile
|
||||||
include processor/GNUmakefile
|
include processor/GNUmakefile
|
||||||
|
|
||||||
ui_objects := ui-bsnes ui-program ui-input ui-presentation ui-settings ui-resource
|
ui_objects := ui-bsnes ui-program ui-input ui-presentation
|
||||||
|
ui_objects += ui-settings ui-tools ui-resource
|
||||||
ui_objects += ruby hiro
|
ui_objects += ruby hiro
|
||||||
ui_objects += $(if $(call streq,$(platform),windows),ui-windows)
|
ui_objects += $(if $(call streq,$(platform),windows),ui-windows)
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ obj/ui-program.o: $(ui)/program/program.cpp
|
||||||
obj/ui-input.o: $(ui)/input/input.cpp
|
obj/ui-input.o: $(ui)/input/input.cpp
|
||||||
obj/ui-presentation.o: $(ui)/presentation/presentation.cpp
|
obj/ui-presentation.o: $(ui)/presentation/presentation.cpp
|
||||||
obj/ui-settings.o: $(ui)/settings/settings.cpp
|
obj/ui-settings.o: $(ui)/settings/settings.cpp
|
||||||
|
obj/ui-tools.o: $(ui)/tools/tools.cpp
|
||||||
obj/ui-resource.o: $(ui)/resource/resource.cpp
|
obj/ui-resource.o: $(ui)/resource/resource.cpp
|
||||||
|
|
||||||
obj/ui-windows.o:
|
obj/ui-windows.o:
|
||||||
|
|
|
@ -15,6 +15,7 @@ extern unique_pointer<Emulator::Interface> emulator;
|
||||||
#include "input/input.hpp"
|
#include "input/input.hpp"
|
||||||
#include "presentation/presentation.hpp"
|
#include "presentation/presentation.hpp"
|
||||||
#include "settings/settings.hpp"
|
#include "settings/settings.hpp"
|
||||||
|
#include "tools/tools.hpp"
|
||||||
#include "resource/resource.hpp"
|
#include "resource/resource.hpp"
|
||||||
|
|
||||||
auto locate(string name) -> string;
|
auto locate(string name) -> string;
|
||||||
|
|
|
@ -20,15 +20,18 @@ Presentation::Presentation() {
|
||||||
});
|
});
|
||||||
controllerPort1.setText("Controller Port 1");
|
controllerPort1.setText("Controller Port 1");
|
||||||
controllerPort2.setText("Controller Port 2");
|
controllerPort2.setText("Controller Port 2");
|
||||||
|
expansionPort.setText("Expansion Port");
|
||||||
for(auto& port : emulator->ports) {
|
for(auto& port : emulator->ports) {
|
||||||
Menu* menu = nullptr;
|
Menu* menu = nullptr;
|
||||||
if(port.name == "Controller Port 1") menu = &controllerPort1;
|
if(port.name == "Controller Port 1") menu = &controllerPort1;
|
||||||
if(port.name == "Controller Port 2") menu = &controllerPort2;
|
if(port.name == "Controller Port 2") menu = &controllerPort2;
|
||||||
|
if(port.name == "Expansion Port") menu = &expansionPort;
|
||||||
if(!menu) continue;
|
if(!menu) continue;
|
||||||
|
|
||||||
Group devices;
|
Group devices;
|
||||||
for(auto& device : port.devices) {
|
for(auto& device : port.devices) {
|
||||||
if(device.name == "None") continue;
|
if(port.name != "Expansion Port" && device.name == "None") continue;
|
||||||
|
if(port.name == "Expansion Port" && device.name == "21fx") continue;
|
||||||
MenuRadioItem item{menu};
|
MenuRadioItem item{menu};
|
||||||
item.setText(device.name).onActivate([=] {
|
item.setText(device.name).onActivate([=] {
|
||||||
auto path = string{"Emulator/", port.name}.replace(" ", "");
|
auto path = string{"Emulator/", port.name}.replace(" ", "");
|
||||||
|
@ -101,14 +104,14 @@ Presentation::Presentation() {
|
||||||
pathSettings.setText("Paths ...").onActivate([&] { settingsWindow->show(2); });
|
pathSettings.setText("Paths ...").onActivate([&] { settingsWindow->show(2); });
|
||||||
advancedSettings.setText("Advanced ...").onActivate([&] { settingsWindow->show(3); });
|
advancedSettings.setText("Advanced ...").onActivate([&] { settingsWindow->show(3); });
|
||||||
|
|
||||||
toolsMenu.setText("Tools");
|
toolsMenu.setText("Tools").setVisible(false);
|
||||||
saveState.setText("Save State").setEnabled(false);
|
saveState.setText("Save State");
|
||||||
saveState1.setText("Slot 1").onActivate([&] { program->saveState(1); });
|
saveState1.setText("Slot 1").onActivate([&] { program->saveState(1); });
|
||||||
saveState2.setText("Slot 2").onActivate([&] { program->saveState(2); });
|
saveState2.setText("Slot 2").onActivate([&] { program->saveState(2); });
|
||||||
saveState3.setText("Slot 3").onActivate([&] { program->saveState(3); });
|
saveState3.setText("Slot 3").onActivate([&] { program->saveState(3); });
|
||||||
saveState4.setText("Slot 4").onActivate([&] { program->saveState(4); });
|
saveState4.setText("Slot 4").onActivate([&] { program->saveState(4); });
|
||||||
saveState5.setText("Slot 5").onActivate([&] { program->saveState(5); });
|
saveState5.setText("Slot 5").onActivate([&] { program->saveState(5); });
|
||||||
loadState.setText("Load State").setEnabled(false);
|
loadState.setText("Load State");
|
||||||
loadState1.setText("Slot 1").onActivate([&] { program->loadState(1); });
|
loadState1.setText("Slot 1").onActivate([&] { program->loadState(1); });
|
||||||
loadState2.setText("Slot 2").onActivate([&] { program->loadState(2); });
|
loadState2.setText("Slot 2").onActivate([&] { program->loadState(2); });
|
||||||
loadState3.setText("Slot 3").onActivate([&] { program->loadState(3); });
|
loadState3.setText("Slot 3").onActivate([&] { program->loadState(3); });
|
||||||
|
@ -117,6 +120,7 @@ Presentation::Presentation() {
|
||||||
pauseEmulation.setText("Pause Emulation").onToggle([&] {
|
pauseEmulation.setText("Pause Emulation").onToggle([&] {
|
||||||
if(pauseEmulation.checked()) audio->clear();
|
if(pauseEmulation.checked()) audio->clear();
|
||||||
});
|
});
|
||||||
|
cheatEditor.setText("Cheat Editor ...").onActivate([&] { toolsWindow->show(0); });
|
||||||
|
|
||||||
helpMenu.setText("Help");
|
helpMenu.setText("Help");
|
||||||
about.setText("About ...").onActivate([&] {
|
about.setText("About ...").onActivate([&] {
|
||||||
|
@ -212,8 +216,8 @@ auto Presentation::resizeViewport() -> void {
|
||||||
return clearViewport();
|
return clearViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
double width = 224 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
||||||
double height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0);
|
uint height = (settings["View/OverscanCropping"].boolean() ? 223.0 : 239.0);
|
||||||
|
|
||||||
if(settings["View/IntegralScaling"].boolean()) {
|
if(settings["View/IntegralScaling"].boolean()) {
|
||||||
uint widthMultiplier = windowWidth / width;
|
uint widthMultiplier = windowWidth / width;
|
||||||
|
@ -226,8 +230,8 @@ auto Presentation::resizeViewport() -> void {
|
||||||
viewportWidth, viewportHeight
|
viewportWidth, viewportHeight
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
double widthMultiplier = windowWidth / width;
|
double widthMultiplier = (double)windowWidth / width;
|
||||||
double heightMultiplier = windowHeight / height;
|
double heightMultiplier = (double)windowHeight / height;
|
||||||
double multiplier = min(widthMultiplier, heightMultiplier);
|
double multiplier = min(widthMultiplier, heightMultiplier);
|
||||||
uint viewportWidth = width * multiplier;
|
uint viewportWidth = width * multiplier;
|
||||||
uint viewportHeight = height * multiplier;
|
uint viewportHeight = height * multiplier;
|
||||||
|
@ -241,8 +245,8 @@ auto Presentation::resizeViewport() -> void {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Presentation::resizeWindow() -> void {
|
auto Presentation::resizeWindow() -> void {
|
||||||
double width = 224 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
||||||
double height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0);
|
uint height = (settings["View/OverscanCropping"].boolean() ? 223.0 : 239.0);
|
||||||
|
|
||||||
uint multiplier = 2;
|
uint multiplier = 2;
|
||||||
if(settings["View/Size"].text() == "Small" ) multiplier = 2;
|
if(settings["View/Size"].text() == "Small" ) multiplier = 2;
|
||||||
|
|
|
@ -29,6 +29,7 @@ struct Presentation : Window {
|
||||||
MenuSeparator portSeparator{&systemMenu};
|
MenuSeparator portSeparator{&systemMenu};
|
||||||
Menu controllerPort1{&systemMenu};
|
Menu controllerPort1{&systemMenu};
|
||||||
Menu controllerPort2{&systemMenu};
|
Menu controllerPort2{&systemMenu};
|
||||||
|
Menu expansionPort{&systemMenu};
|
||||||
MenuSeparator quitSeparator{&systemMenu};
|
MenuSeparator quitSeparator{&systemMenu};
|
||||||
MenuItem quit{&systemMenu};
|
MenuItem quit{&systemMenu};
|
||||||
Menu settingsMenu{&menuBar};
|
Menu settingsMenu{&menuBar};
|
||||||
|
@ -64,6 +65,8 @@ struct Presentation : Window {
|
||||||
MenuItem loadState4{&loadState};
|
MenuItem loadState4{&loadState};
|
||||||
MenuItem loadState5{&loadState};
|
MenuItem loadState5{&loadState};
|
||||||
MenuCheckItem pauseEmulation{&toolsMenu};
|
MenuCheckItem pauseEmulation{&toolsMenu};
|
||||||
|
MenuSeparator toolsSeparator{&toolsMenu};
|
||||||
|
MenuItem cheatEditor{&toolsMenu};
|
||||||
Menu helpMenu{&menuBar};
|
Menu helpMenu{&menuBar};
|
||||||
MenuItem about{&helpMenu};
|
MenuItem about{&helpMenu};
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,10 @@ auto Program::load() -> void {
|
||||||
presentation->setTitle(emulator->title());
|
presentation->setTitle(emulator->title());
|
||||||
presentation->resetSystem.setEnabled(true);
|
presentation->resetSystem.setEnabled(true);
|
||||||
presentation->unloadGame.setEnabled(true);
|
presentation->unloadGame.setEnabled(true);
|
||||||
presentation->saveState.setEnabled(true);
|
presentation->toolsMenu.setVisible(true);
|
||||||
presentation->loadState.setEnabled(true);
|
presentation->pauseEmulation.setChecked(false);
|
||||||
presentation->resizeViewport();
|
presentation->resizeViewport();
|
||||||
|
toolsWindow->cheatEditor.loadCheats();
|
||||||
|
|
||||||
string locations = superNintendo.location;
|
string locations = superNintendo.location;
|
||||||
if(auto location = gameBoy.location) locations.append("|", location);
|
if(auto location = gameBoy.location) locations.append("|", location);
|
||||||
|
@ -93,6 +94,8 @@ auto Program::save() -> void {
|
||||||
|
|
||||||
auto Program::unload() -> void {
|
auto Program::unload() -> void {
|
||||||
if(!emulator->loaded()) return;
|
if(!emulator->loaded()) return;
|
||||||
|
toolsWindow->cheatEditor.saveCheats();
|
||||||
|
toolsWindow->setVisible(false);
|
||||||
emulator->unload();
|
emulator->unload();
|
||||||
superNintendo = {};
|
superNintendo = {};
|
||||||
gameBoy = {};
|
gameBoy = {};
|
||||||
|
@ -102,7 +105,6 @@ auto Program::unload() -> void {
|
||||||
presentation->setTitle({"bsnes v", Emulator::Version});
|
presentation->setTitle({"bsnes v", Emulator::Version});
|
||||||
presentation->resetSystem.setEnabled(false);
|
presentation->resetSystem.setEnabled(false);
|
||||||
presentation->unloadGame.setEnabled(false);
|
presentation->unloadGame.setEnabled(false);
|
||||||
presentation->saveState.setEnabled(false);
|
presentation->toolsMenu.setVisible(false);
|
||||||
presentation->loadState.setEnabled(false);
|
|
||||||
presentation->clearViewport();
|
presentation->clearViewport();
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,15 @@ auto Program::open(uint id, string name, vfs::file::mode mode, bool required) ->
|
||||||
return vfs::fs::file::open(path("Saves", superNintendo.location, ".srm"), mode);
|
return vfs::fs::file::open(path("Saves", superNintendo.location, ".srm"), mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(id == 1 && name == "msu1/data.rom") {
|
||||||
|
return vfs::fs::file::open({Location::notsuffix(superNintendo.location), ".msu"}, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(id == 1 && name.match("msu1/track-*.pcm")) {
|
||||||
|
name.trimLeft("msu1/track-", 1L);
|
||||||
|
return vfs::fs::file::open({Location::notsuffix(superNintendo.location), name}, mode);
|
||||||
|
}
|
||||||
|
|
||||||
//Game Boy
|
//Game Boy
|
||||||
|
|
||||||
if(id == 2 && name == "manifest.bml" && mode == vfs::file::mode::read) {
|
if(id == 2 && name == "manifest.bml" && mode == vfs::file::mode::read) {
|
||||||
|
@ -211,8 +220,8 @@ auto Program::videoRefresh(const uint32* data, uint pitch, uint width, uint heig
|
||||||
|
|
||||||
pitch >>= 2;
|
pitch >>= 2;
|
||||||
if(presentation->overscanCropping.checked()) {
|
if(presentation->overscanCropping.checked()) {
|
||||||
if(height == 240) data += 8 * pitch, height -= 16;
|
if(height == 239) data += 8 * pitch, height -= 16;
|
||||||
if(height == 480) data += 16 * pitch, height -= 32;
|
if(height == 478) data += 16 * pitch, height -= 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(video->lock(output, length, width, height)) {
|
if(video->lock(output, length, width, height)) {
|
||||||
|
|
|
@ -56,6 +56,8 @@ Program::Program(string_vector arguments) {
|
||||||
|
|
||||||
new InputManager;
|
new InputManager;
|
||||||
new SettingsWindow;
|
new SettingsWindow;
|
||||||
|
new CheatDatabase;
|
||||||
|
new ToolsWindow;
|
||||||
new AboutWindow;
|
new AboutWindow;
|
||||||
|
|
||||||
arguments.takeLeft(); //ignore program location in argument parsing
|
arguments.takeLeft(); //ignore program location in argument parsing
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
CheatDatabase::CheatDatabase() {
|
||||||
|
cheatDatabase = this;
|
||||||
|
|
||||||
|
layout.setMargin(5);
|
||||||
|
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, 400});
|
||||||
|
setAlignment({0.5, 1.0});
|
||||||
|
setDismissable();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatDatabase::findCheats() -> void {
|
||||||
|
auto sha256 = emulator->sha256();
|
||||||
|
|
||||||
|
auto document = BML::unserialize(string::read(locate("cheats.bml")));
|
||||||
|
for(auto game : document.find("cartridge")) {
|
||||||
|
if(game["sha256"].text() != sha256) continue;
|
||||||
|
|
||||||
|
cheatList.reset();
|
||||||
|
for(auto cheat : game.find("cheat")) {
|
||||||
|
cheatList.append(ListViewItem()
|
||||||
|
.setCheckable()
|
||||||
|
.setText(cheat["description"].text())
|
||||||
|
.setProperty("code", cheat["code"].text())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle(game["name"].text());
|
||||||
|
setVisible();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog().setParent(*toolsWindow).setText("Sorry, no cheats were found for this game.").information();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatDatabase::addCheats() -> void {
|
||||||
|
for(auto item : cheatList.items()) {
|
||||||
|
if(!item.checked()) continue;
|
||||||
|
|
||||||
|
string code = item.property("code").replace("/", "=", 1L).replace("/", "?", 1L);
|
||||||
|
string description = item.text();
|
||||||
|
if(!toolsWindow->cheatEditor.addCode(false, code, description)) {
|
||||||
|
MessageDialog().setParent(*this).setText("Free slots exhausted. Not all cheats could be added.").warning();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setVisible(false);
|
||||||
|
toolsWindow->cheatEditor.doRefresh();
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
CheatEditor::CheatEditor(TabFrame* parent) : TabFrameItem(parent) {
|
||||||
|
setIcon(Icon::Edit::Replace);
|
||||||
|
setText("Cheat Editor");
|
||||||
|
|
||||||
|
layout.setMargin(5);
|
||||||
|
cheatList.append(TableViewHeader().setVisible()
|
||||||
|
.append(TableViewColumn())
|
||||||
|
.append(TableViewColumn().setText("Slot").setForegroundColor({0, 128, 0}).setAlignment(1.0))
|
||||||
|
.append(TableViewColumn().setText("Code(s)"))
|
||||||
|
.append(TableViewColumn().setText("Description").setExpandable())
|
||||||
|
);
|
||||||
|
for(uint slot : range(Slots)) {
|
||||||
|
cheatList.append(TableViewItem()
|
||||||
|
.append(TableViewCell().setCheckable())
|
||||||
|
.append(TableViewCell().setText(1 + slot))
|
||||||
|
.append(TableViewCell())
|
||||||
|
.append(TableViewCell())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cheatList.onChange([&] { doChangeSelected(); });
|
||||||
|
cheatList.onToggle([&](auto cell) {
|
||||||
|
cheats[cell.parent().offset()].enabled = cell.checked();
|
||||||
|
this->synchronizeCodes();
|
||||||
|
});
|
||||||
|
codeLabel.setText("Code(s):");
|
||||||
|
codeValue.setEnabled(false).onChange([&] { doModify(); });
|
||||||
|
descriptionLabel.setText("Description:");
|
||||||
|
descriptionValue.setEnabled(false).onChange([&] { doModify(); });
|
||||||
|
findCodesButton.setText("Find Codes ...").onActivate([&] { cheatDatabase->findCheats(); });
|
||||||
|
resetButton.setText("Reset").onActivate([&] { doReset(); });
|
||||||
|
eraseButton.setText("Erase").onActivate([&] { doErase(); });
|
||||||
|
|
||||||
|
//do not display "Find Codes" button if there is no cheat database to look up codes in
|
||||||
|
if(!file::exists(locate("cheats.bml"))) findCodesButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::doChangeSelected() -> void {
|
||||||
|
if(auto item = cheatList.selected()) {
|
||||||
|
auto& cheat = cheats[item.offset()];
|
||||||
|
codeValue.setEnabled(true).setText(cheat.code);
|
||||||
|
descriptionValue.setEnabled(true).setText(cheat.description);
|
||||||
|
eraseButton.setEnabled(true);
|
||||||
|
} else {
|
||||||
|
codeValue.setEnabled(false).setText("");
|
||||||
|
descriptionValue.setEnabled(false).setText("");
|
||||||
|
eraseButton.setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::doModify() -> void {
|
||||||
|
if(auto item = cheatList.selected()) {
|
||||||
|
auto& cheat = cheats[item.offset()];
|
||||||
|
cheat.code = codeValue.text();
|
||||||
|
cheat.description = descriptionValue.text();
|
||||||
|
doRefresh();
|
||||||
|
synchronizeCodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::doRefresh() -> void {
|
||||||
|
for(uint slot : range(Slots)) {
|
||||||
|
auto& cheat = cheats[slot];
|
||||||
|
if(cheat.code || cheat.description) {
|
||||||
|
auto codes = cheat.code.split("+");
|
||||||
|
if(codes.size() > 1) codes[0].append("+...");
|
||||||
|
cheatList.item(slot).cell(0).setChecked(cheat.enabled);
|
||||||
|
cheatList.item(slot).cell(2).setText(codes[0]);
|
||||||
|
cheatList.item(slot).cell(3).setText(cheat.description).setForegroundColor({0, 0, 0});
|
||||||
|
} else {
|
||||||
|
cheatList.item(slot).cell(0).setChecked(false);
|
||||||
|
cheatList.item(slot).cell(2).setText("");
|
||||||
|
cheatList.item(slot).cell(3).setText("<empty>").setForegroundColor({128, 128, 128});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cheatList.resizeColumns();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::doReset(bool force) -> void {
|
||||||
|
if(force || MessageDialog().setParent(*toolsWindow).setText("Permamently erase all cheats?").question() == "Yes") {
|
||||||
|
for(auto& cheat : cheats) cheat = {};
|
||||||
|
for(auto& item : cheatList.items()) item.cell(0).setChecked(false);
|
||||||
|
doChangeSelected();
|
||||||
|
doRefresh();
|
||||||
|
synchronizeCodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::doErase() -> void {
|
||||||
|
if(auto item = cheatList.selected()) {
|
||||||
|
cheats[item.offset()] = {};
|
||||||
|
codeValue.setText("");
|
||||||
|
descriptionValue.setText("");
|
||||||
|
doRefresh();
|
||||||
|
synchronizeCodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::synchronizeCodes() -> void {
|
||||||
|
string_vector codes;
|
||||||
|
for(auto& cheat : cheats) {
|
||||||
|
if(!cheat.enabled || !cheat.code) continue;
|
||||||
|
codes.append(cheat.code);
|
||||||
|
}
|
||||||
|
emulator->cheatSet(codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::addCode(bool enabled, string code, string description) -> bool {
|
||||||
|
for(auto& cheat : cheats) {
|
||||||
|
if(cheat.code || cheat.description) continue;
|
||||||
|
cheat.enabled = enabled;
|
||||||
|
cheat.code = code;
|
||||||
|
cheat.description = description;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::loadCheats() -> void {
|
||||||
|
doReset(true);
|
||||||
|
auto location = program->path("Cheats", program->superNintendo.location, ".cht");
|
||||||
|
auto document = BML::unserialize(string::read(location));
|
||||||
|
for(auto cheat : document.find("cheat")) {
|
||||||
|
if(!addCode((bool)cheat["enabled"], cheat["code"].text(), cheat["description"].text())) break;
|
||||||
|
}
|
||||||
|
doRefresh();
|
||||||
|
synchronizeCodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CheatEditor::saveCheats() -> void {
|
||||||
|
string document;
|
||||||
|
for(auto& cheat : cheats) {
|
||||||
|
if(!cheat.code && !cheat.description) continue;
|
||||||
|
document.append("cheat\n");
|
||||||
|
document.append(" description: ", cheat.description, "\n");
|
||||||
|
document.append(" code: ", cheat.code, "\n");
|
||||||
|
if(cheat.enabled)
|
||||||
|
document.append(" enabled\n");
|
||||||
|
document.append("\n");
|
||||||
|
}
|
||||||
|
auto location = program->path("Cheats", program->superNintendo.location, ".cht");
|
||||||
|
if(document) {
|
||||||
|
file::write(location, document);
|
||||||
|
} else {
|
||||||
|
file::remove(location);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
#include "../bsnes.hpp"
|
||||||
|
#include "cheat-database.cpp"
|
||||||
|
#include "cheat-editor.cpp"
|
||||||
|
unique_pointer<CheatDatabase> cheatDatabase;
|
||||||
|
unique_pointer<ToolsWindow> toolsWindow;
|
||||||
|
|
||||||
|
ToolsWindow::ToolsWindow() {
|
||||||
|
toolsWindow = this;
|
||||||
|
|
||||||
|
layout.setMargin(5);
|
||||||
|
|
||||||
|
setTitle("Tools");
|
||||||
|
setSize({600, 400});
|
||||||
|
setAlignment({1.0, 1.0});
|
||||||
|
setDismissable();
|
||||||
|
|
||||||
|
onSize([&] {
|
||||||
|
cheatEditor.cheatList.resizeColumns();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolsWindow::setVisible(bool visible) -> ToolsWindow& {
|
||||||
|
return Window::setVisible(visible), *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ToolsWindow::show(uint index) -> void {
|
||||||
|
panel.item(index)->setSelected();
|
||||||
|
setVisible();
|
||||||
|
setFocused();
|
||||||
|
doSize();
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
struct CheatDatabase : Window {
|
||||||
|
CheatDatabase();
|
||||||
|
auto findCheats() -> void;
|
||||||
|
auto addCheats() -> void;
|
||||||
|
|
||||||
|
public:
|
||||||
|
VerticalLayout layout{this};
|
||||||
|
ListView cheatList{&layout, Size{~0, ~0}};
|
||||||
|
HorizontalLayout controlLayout{&layout, Size{~0, 0}};
|
||||||
|
Button selectAllButton{&controlLayout, Size{100, 0}};
|
||||||
|
Button unselectAllButton{&controlLayout, Size{100, 0}};
|
||||||
|
Widget spacer{&controlLayout, Size{~0, 0}};
|
||||||
|
Button addCheatsButton{&controlLayout, Size{100, 0}};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CheatEditor : TabFrameItem {
|
||||||
|
enum : uint { Slots = 128 };
|
||||||
|
|
||||||
|
CheatEditor(TabFrame*);
|
||||||
|
auto doChangeSelected() -> void;
|
||||||
|
auto doModify() -> void;
|
||||||
|
auto doRefresh() -> void;
|
||||||
|
auto doReset(bool force = false) -> void;
|
||||||
|
auto doErase() -> void;
|
||||||
|
auto synchronizeCodes() -> void;
|
||||||
|
auto addCode(bool enabled, string code, string description) -> bool;
|
||||||
|
auto loadCheats() -> void;
|
||||||
|
auto saveCheats() -> void;
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct Cheat {
|
||||||
|
bool enabled = false;
|
||||||
|
string code;
|
||||||
|
string description;
|
||||||
|
};
|
||||||
|
Cheat cheats[Slots];
|
||||||
|
|
||||||
|
VerticalLayout layout{this};
|
||||||
|
TableView cheatList{&layout, Size{~0, ~0}};
|
||||||
|
HorizontalLayout codeLayout{&layout, Size{~0, 0}};
|
||||||
|
Label codeLabel{&codeLayout, Size{70, 0}};
|
||||||
|
LineEdit codeValue{&codeLayout, Size{~0, 0}};
|
||||||
|
HorizontalLayout descriptionLayout{&layout, Size{~0, 0}};
|
||||||
|
Label descriptionLabel{&descriptionLayout, Size{70, 0}};
|
||||||
|
LineEdit descriptionValue{&descriptionLayout, Size{~0, 0}};
|
||||||
|
HorizontalLayout controlLayout{&layout, Size{~0, 0}};
|
||||||
|
Button findCodesButton{&controlLayout, Size{120, 0}};
|
||||||
|
Widget spacer{&controlLayout, Size{~0, 0}};
|
||||||
|
Button resetButton{&controlLayout, Size{80, 0}};
|
||||||
|
Button eraseButton{&controlLayout, Size{80, 0}};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToolsWindow : Window {
|
||||||
|
ToolsWindow();
|
||||||
|
auto setVisible(bool visible = true) -> ToolsWindow&;
|
||||||
|
auto show(uint index) -> void;
|
||||||
|
|
||||||
|
public:
|
||||||
|
VerticalLayout layout{this};
|
||||||
|
TabFrame panel{&layout, Size{~0, ~0}};
|
||||||
|
CheatEditor cheatEditor{&panel};
|
||||||
|
};
|
||||||
|
|
||||||
|
extern unique_pointer<CheatDatabase> cheatDatabase;
|
||||||
|
extern unique_pointer<ToolsWindow> toolsWindow;
|
|
@ -18,9 +18,9 @@ CheatEditor::CheatEditor(TabFrame* parent) : TabFrameItem(parent) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
cheatList.onChange([&] { doChangeSelected(); });
|
cheatList.onChange([&] { doChangeSelected(); });
|
||||||
cheatList.onToggle([&](TableViewCell cell) {
|
cheatList.onToggle([&](auto cell) {
|
||||||
cheats[cell.parent().offset()].enabled = cell.checked();
|
cheats[cell.parent().offset()].enabled = cell.checked();
|
||||||
synchronizeCodes();
|
this->synchronizeCodes();
|
||||||
});
|
});
|
||||||
codeLabel.setText("Code(s):");
|
codeLabel.setText("Code(s):");
|
||||||
codeValue.onChange([&] { doModify(); });
|
codeValue.onChange([&] { doModify(); });
|
||||||
|
@ -69,7 +69,7 @@ auto CheatEditor::doRefresh() -> void {
|
||||||
} else {
|
} else {
|
||||||
cheatList.item(slot).cell(0).setChecked(false);
|
cheatList.item(slot).cell(0).setChecked(false);
|
||||||
cheatList.item(slot).cell(2).setText("");
|
cheatList.item(slot).cell(2).setText("");
|
||||||
cheatList.item(slot).cell(3).setText("(empty)").setForegroundColor({128, 128, 128});
|
cheatList.item(slot).cell(3).setText("<empty>").setForegroundColor({128, 128, 128});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,15 +77,9 @@ auto CheatEditor::doRefresh() -> void {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto CheatEditor::doReset(bool force) -> void {
|
auto CheatEditor::doReset(bool force) -> void {
|
||||||
if(force || MessageDialog().setParent(*toolsManager).setText("Permanently erase all slots?").question() == "Yes") {
|
if(force || MessageDialog().setParent(*toolsManager).setText("Permanently erase all cheats?").question() == "Yes") {
|
||||||
for(auto& cheat : cheats) {
|
for(auto& cheat : cheats) cheat = {};
|
||||||
cheat.enabled = false;
|
for(auto& item : cheatList.items()) item.cell(0).setChecked(false);
|
||||||
cheat.code = "";
|
|
||||||
cheat.description = "";
|
|
||||||
}
|
|
||||||
for(auto& item : cheatList.items()) {
|
|
||||||
item.cell(0).setChecked(false);
|
|
||||||
}
|
|
||||||
doChangeSelected();
|
doChangeSelected();
|
||||||
doRefresh();
|
doRefresh();
|
||||||
synchronizeCodes();
|
synchronizeCodes();
|
||||||
|
@ -95,9 +89,7 @@ auto CheatEditor::doReset(bool force) -> void {
|
||||||
auto CheatEditor::doErase() -> void {
|
auto CheatEditor::doErase() -> void {
|
||||||
if(auto item = cheatList.selected()) {
|
if(auto item = cheatList.selected()) {
|
||||||
auto& cheat = cheats[item.offset()];
|
auto& cheat = cheats[item.offset()];
|
||||||
cheat.enabled = false;
|
cheats[item.offset()] = {};
|
||||||
cheat.code = "";
|
|
||||||
cheat.description = "";
|
|
||||||
codeValue.setText("");
|
codeValue.setText("");
|
||||||
descriptionValue.setText("");
|
descriptionValue.setText("");
|
||||||
doRefresh();
|
doRefresh();
|
||||||
|
|
|
@ -70,7 +70,7 @@ auto StateManager::doRefresh() -> void {
|
||||||
description.resize(description.length());
|
description.resize(description.length());
|
||||||
stateList.item(slot).cell(1).setText(description).setForegroundColor({0, 0, 0});
|
stateList.item(slot).cell(1).setText(description).setForegroundColor({0, 0, 0});
|
||||||
} else {
|
} else {
|
||||||
stateList.item(slot).cell(1).setText("(empty)").setForegroundColor({128, 128, 128});
|
stateList.item(slot).cell(1).setText("<empty>").setForegroundColor({128, 128, 128});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ auto StateManager::doSave() -> void {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto StateManager::doReset() -> void {
|
auto StateManager::doReset() -> void {
|
||||||
if(MessageDialog().setParent(*toolsManager).setText("Permanently erase all slots?").question() == "Yes") {
|
if(MessageDialog().setParent(*toolsManager).setText("Permanently erase all states?").question() == "Yes") {
|
||||||
for(auto slot : range(Slots)) file::remove(program->stateName(1 + slot, true));
|
for(auto slot : range(Slots)) file::remove(program->stateName(1 + slot, true));
|
||||||
doRefresh();
|
doRefresh();
|
||||||
doUpdateControls();
|
doUpdateControls();
|
||||||
|
|
|
@ -144,7 +144,7 @@ struct InputKeyboardQuartz {
|
||||||
|
|
||||||
hid->setVendorID(HID::Keyboard::GenericVendorID);
|
hid->setVendorID(HID::Keyboard::GenericVendorID);
|
||||||
hid->setProductID(HID::Keyboard::GenericProductID);
|
hid->setProductID(HID::Keyboard::GenericProductID);
|
||||||
hid->setPath(0);
|
hid->setPathID(0);
|
||||||
for(auto& key : keys) {
|
for(auto& key : keys) {
|
||||||
hid->buttons().append(key.name);
|
hid->buttons().append(key.name);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue