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:
Tim Allen 2018-06-03 23:14:42 +10:00
parent 8c337d4ac6
commit 77ac5f9e88
31 changed files with 414 additions and 124 deletions

View File

@ -12,7 +12,7 @@ using namespace nall;
namespace Emulator {
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 License = "GPLv3";
static const string Website = "https://byuu.org/";

View File

@ -89,7 +89,7 @@ private:
auto loadSPC7110(Markup::Node) -> void;
auto loadSDD1(Markup::Node) -> void;
auto loadOBC1(Markup::Node) -> void;
auto loadMSU1(Markup::Node) -> void;
auto loadMSU1() -> void;
//save.cpp
auto saveCartridge(Markup::Node) -> void;

View File

@ -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=SDD1)"]) loadSDD1(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 {
@ -642,11 +643,9 @@ auto Cartridge::loadOBC1(Markup::Node node) -> void {
}
}
//processor(identifier=MSU1)
auto Cartridge::loadMSU1(Markup::Node node) -> void {
//file::exists("msu1/data.rom")
auto Cartridge::loadMSU1() -> void {
has.MSU1 = true;
for(auto map : node.find("map")) {
loadMap(map, {&MSU1::readIO, &msu1}, {&MSU1::writeIO, &msu1});
}
bus.map({&MSU1::readIO, &msu1}, {&MSU1::writeIO, &msu1}, "00-3f,80-bf:2000-2007");
}

View File

@ -11,7 +11,7 @@ auto MSU1::Enter() -> void {
}
auto MSU1::main() -> void {
double left = 0.0;
double left = 0.0;
double right = 0.0;
if(io.audioPlay) {
@ -72,9 +72,7 @@ auto MSU1::power() -> void {
auto MSU1::dataOpen() -> void {
dataFile.reset();
auto document = Markup::Node(); //todo: fix this
string name = document["board/msu1/rom/name"].text();
if(!name) name = "msu1.rom";
string name = {"msu1/data.rom"};
if(dataFile = platform->open(ID::SuperFamicom, name, File::Read)) {
dataFile->seek(io.dataReadOffset);
}
@ -82,13 +80,7 @@ auto MSU1::dataOpen() -> void {
auto MSU1::audioOpen() -> void {
audioFile.reset();
auto document = Markup::Node(); //todo: fix this
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;
}
string name = {"msu1/track-", io.audioTrack, ".pcm"};
if(audioFile = platform->open(ID::SuperFamicom, name, File::Read)) {
if(audioFile->size() >= 8) {
uint32 header = audioFile->readm(4);
@ -164,7 +156,7 @@ auto MSU1::writeIO(uint24 addr, uint8 data) -> void {
if(io.audioError) break;
io.audioPlay = data.bit(0);
io.audioRepeat = data.bit(1);
bool audioResume = data.bit(2);
boolean audioResume = data.bit(2);
if(!io.audioPlay && audioResume) {
io.audioResumeTrack = io.audioTrack;
io.audioResumeOffset = io.audioPlayOffset;

View File

@ -40,11 +40,11 @@ private:
uint32 audioResumeTrack;
uint32 audioResumeOffset;
bool audioError;
bool audioPlay;
bool audioRepeat;
bool audioBusy;
bool dataBusy;
boolean audioError;
boolean audioPlay;
boolean audioRepeat;
boolean audioBusy;
boolean dataBusy;
} io;
};

View File

@ -13,11 +13,11 @@ auto MSU1::serialize(serializer& s) -> void {
s.integer(io.audioResumeTrack);
s.integer(io.audioResumeOffset);
s.integer(io.audioError);
s.integer(io.audioPlay);
s.integer(io.audioRepeat);
s.integer(io.audioBusy);
s.integer(io.dataBusy);
s.boolean(io.audioError);
s.boolean(io.audioPlay);
s.boolean(io.audioRepeat);
s.boolean(io.audioBusy);
s.boolean(io.dataBusy);
dataOpen();
audioOpen();

View File

@ -120,9 +120,9 @@ auto Interface::title() -> string {
auto Interface::videoInformation() -> VideoInformation {
VideoInformation vi;
vi.width = 256;
vi.height = 240;
vi.height = 239;
vi.internalWidth = 512;
vi.internalHeight = 480;
vi.internalHeight = 478;
vi.aspectCorrection = 8.0 / 7.0;
if(Region::NTSC()) vi.refreshRate = system.cpuFrequency() / (262.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 {
if(name == "Blur Emulation" && value.is<bool>()) {
settings.blurEmulation = value.get<bool>();
system.configureVideoEffects();
return true;
}
if(name == "Color Emulation" && value.is<bool>()) {
settings.colorEmulation = value.get<bool>();
system.configureVideoPalette();
Emulator::video.setPalette();
return true;
}
if(name == "Scanline Emulation" && value.is<bool>()) return settings.scanlineEmulation = value.get<bool>(), true;

View File

@ -1,5 +1,4 @@
auto PPU::Line::renderBackground(PPU::IO::Background& self, uint source) -> void {
if(io.displayDisable) return;
if(!self.aboveEnable && !self.belowEnable) return;
if(self.tileMode == TileMode::Mode7) return renderMode7(self, source);
if(self.tileMode == TileMode::Inactive) return;

View File

@ -3,7 +3,7 @@ uint PPU::Line::count = 0;
auto PPU::Line::flush() -> void {
if(Line::count) {
#pragma omp parallel for
#pragma omp parallel for if(Line::count >= 8)
for(uint y = 0; y < Line::count; y++) {
ppu.lines[Line::start + y].render();
}
@ -13,15 +13,21 @@ auto PPU::Line::flush() -> 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) {
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};
}
if(io.displayDisable) {
memory::fill<uint32>(output, width);
return;
}
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);
@ -29,20 +35,10 @@ auto PPU::Line::render() -> void {
renderBackground(io.bg3, Source::BG3);
renderBackground(io.bg4, Source::BG4);
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.belowMask, windowBelow);
auto luma = io.displayBrightness << 15;
if(width == 256) for(uint x : range(width)) {
*output++ = luma | pixel(x, above[x], below[x]);
} 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 {
return (palette << 7 & 0x6000) + (tile >> 0 & 0x1000)
+ (palette << 4 & 0x0380) + (tile >> 5 & 0x0040)
+ (palette << 2 & 0x001c) + (tile >> 9 & 0x0002);
auto PPU::Line::directColor(uint paletteIndex, uint paletteColor) const -> uint15 {
//paletteIndex = bgr
//paletteColor = BBGGGRRR
//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 {

View File

@ -51,7 +51,7 @@ auto PPU::Line::renderMode7(PPU::IO::Background& self, uint source) -> void {
mosaicCounter = 1 + io.mosaicSize;
mosaicPalette = palette;
mosaicPriority = priority;
if(io.col.directColor) {
if(io.col.directColor && source == Source::BG1) {
mosaicColor = directColor(0, palette);
} else {
mosaicColor = cgram[palette];

View File

@ -1,5 +1,4 @@
auto PPU::Line::renderObject(PPU::IO::Object& self) -> void {
if(io.displayDisable) return;
if(!self.aboveEnable && !self.belowEnable) return;
bool windowAbove[256];

View File

@ -44,9 +44,13 @@ auto PPU::main() -> void {
scanline();
uint y = vcounter();
step(512);
if(y >= 1 && y <= vdisp()) {
memcpy(&lines[y].io, &io, sizeof(io));
memcpy(&lines[y].cgram, &cgram, sizeof(cgram));
if(y >= 1 && y <= 239) {
if(io.displayDisable || y >= vdisp()) {
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;
Line::count++;
}
@ -78,13 +82,12 @@ auto PPU::scanline() -> void {
auto PPU::refresh() -> void {
auto output = this->output;
if(!overscan()) output -= 14 * 512;
if(!overscan()) output -= 12 * 512;
auto pitch = 512 << !interlace();
auto width = 256 << hires();
auto height = 240 << interlace();
if(!hires()) Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, false);
auto height = 239 << interlace();
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation && hires());
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 {
@ -94,7 +97,7 @@ auto PPU::load(Markup::Node node) -> bool {
auto PPU::power(bool reset) -> void {
create(Enter, system.cpuFrequency());
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) -> void> writer{&PPU::writeIO, this};

View File

@ -263,7 +263,7 @@ public:
auto render() -> void;
auto pixel(uint x, Pixel above, Pixel below) 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 plotBelow(uint x, uint source, uint priority, uint color) -> void;

View File

@ -72,11 +72,9 @@ auto PPU::main() -> void {
step(14);
obj.tilefetch();
} else {
step(1052 + 14 + 136);
}
step(lineclocks() - 28 - 1052 - 14 - 136);
step(lineclocks() - hcounter());
}
auto PPU::load(Markup::Node node) -> bool {
@ -223,10 +221,11 @@ auto PPU::frame() -> void {
auto PPU::refresh() -> void {
auto output = this->output;
if(!overscan()) output -= 14 * 512;
if(!overscan()) output -= 12 * 512;
auto pitch = 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);
}

View File

@ -6,7 +6,6 @@ System system;
Scheduler scheduler;
Random random;
Cheat cheat;
#include "video.cpp"
#include "serialization.cpp"
auto System::run() -> void {
@ -91,8 +90,7 @@ auto System::unload() -> void {
auto System::power(bool reset) -> void {
Emulator::video.reset();
Emulator::video.setInterface(interface);
configureVideoPalette();
configureVideoEffects();
Emulator::video.setPalette();
Emulator::audio.reset();
Emulator::audio.setInterface(interface);

View File

@ -14,10 +14,6 @@ struct System {
auto unload() -> void;
auto power(bool reset) -> void;
//video.cpp
auto configureVideoPalette() -> void;
auto configureVideoEffects() -> void;
//serialization.cpp
auto serialize() -> serializer;
auto unserialize(serializer&) -> bool;

View File

@ -1,7 +0,0 @@
auto System::configureVideoPalette() -> void {
Emulator::video.setPalette();
}
auto System::configureVideoEffects() -> void {
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
}

View File

@ -5,7 +5,8 @@ include sfc/GNUmakefile
include gb/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 += $(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-presentation.o: $(ui)/presentation/presentation.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-windows.o:

View File

@ -15,6 +15,7 @@ extern unique_pointer<Emulator::Interface> emulator;
#include "input/input.hpp"
#include "presentation/presentation.hpp"
#include "settings/settings.hpp"
#include "tools/tools.hpp"
#include "resource/resource.hpp"
auto locate(string name) -> string;

View File

@ -20,15 +20,18 @@ Presentation::Presentation() {
});
controllerPort1.setText("Controller Port 1");
controllerPort2.setText("Controller Port 2");
expansionPort.setText("Expansion Port");
for(auto& port : emulator->ports) {
Menu* menu = nullptr;
if(port.name == "Controller Port 1") menu = &controllerPort1;
if(port.name == "Controller Port 2") menu = &controllerPort2;
if(port.name == "Expansion Port") menu = &expansionPort;
if(!menu) continue;
Group 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};
item.setText(device.name).onActivate([=] {
auto path = string{"Emulator/", port.name}.replace(" ", "");
@ -101,14 +104,14 @@ Presentation::Presentation() {
pathSettings.setText("Paths ...").onActivate([&] { settingsWindow->show(2); });
advancedSettings.setText("Advanced ...").onActivate([&] { settingsWindow->show(3); });
toolsMenu.setText("Tools");
saveState.setText("Save State").setEnabled(false);
toolsMenu.setText("Tools").setVisible(false);
saveState.setText("Save State");
saveState1.setText("Slot 1").onActivate([&] { program->saveState(1); });
saveState2.setText("Slot 2").onActivate([&] { program->saveState(2); });
saveState3.setText("Slot 3").onActivate([&] { program->saveState(3); });
saveState4.setText("Slot 4").onActivate([&] { program->saveState(4); });
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); });
loadState2.setText("Slot 2").onActivate([&] { program->loadState(2); });
loadState3.setText("Slot 3").onActivate([&] { program->loadState(3); });
@ -117,6 +120,7 @@ Presentation::Presentation() {
pauseEmulation.setText("Pause Emulation").onToggle([&] {
if(pauseEmulation.checked()) audio->clear();
});
cheatEditor.setText("Cheat Editor ...").onActivate([&] { toolsWindow->show(0); });
helpMenu.setText("Help");
about.setText("About ...").onActivate([&] {
@ -212,8 +216,8 @@ auto Presentation::resizeViewport() -> void {
return clearViewport();
}
double width = 224 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
double height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0);
uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
uint height = (settings["View/OverscanCropping"].boolean() ? 223.0 : 239.0);
if(settings["View/IntegralScaling"].boolean()) {
uint widthMultiplier = windowWidth / width;
@ -226,8 +230,8 @@ auto Presentation::resizeViewport() -> void {
viewportWidth, viewportHeight
});
} else {
double widthMultiplier = windowWidth / width;
double heightMultiplier = windowHeight / height;
double widthMultiplier = (double)windowWidth / width;
double heightMultiplier = (double)windowHeight / height;
double multiplier = min(widthMultiplier, heightMultiplier);
uint viewportWidth = width * multiplier;
uint viewportHeight = height * multiplier;
@ -241,8 +245,8 @@ auto Presentation::resizeViewport() -> void {
}
auto Presentation::resizeWindow() -> void {
double width = 224 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
double height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0);
uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
uint height = (settings["View/OverscanCropping"].boolean() ? 223.0 : 239.0);
uint multiplier = 2;
if(settings["View/Size"].text() == "Small" ) multiplier = 2;

View File

@ -29,6 +29,7 @@ struct Presentation : Window {
MenuSeparator portSeparator{&systemMenu};
Menu controllerPort1{&systemMenu};
Menu controllerPort2{&systemMenu};
Menu expansionPort{&systemMenu};
MenuSeparator quitSeparator{&systemMenu};
MenuItem quit{&systemMenu};
Menu settingsMenu{&menuBar};
@ -64,6 +65,8 @@ struct Presentation : Window {
MenuItem loadState4{&loadState};
MenuItem loadState5{&loadState};
MenuCheckItem pauseEmulation{&toolsMenu};
MenuSeparator toolsSeparator{&toolsMenu};
MenuItem cheatEditor{&toolsMenu};
Menu helpMenu{&menuBar};
MenuItem about{&helpMenu};

View File

@ -12,9 +12,10 @@ auto Program::load() -> void {
presentation->setTitle(emulator->title());
presentation->resetSystem.setEnabled(true);
presentation->unloadGame.setEnabled(true);
presentation->saveState.setEnabled(true);
presentation->loadState.setEnabled(true);
presentation->toolsMenu.setVisible(true);
presentation->pauseEmulation.setChecked(false);
presentation->resizeViewport();
toolsWindow->cheatEditor.loadCheats();
string locations = superNintendo.location;
if(auto location = gameBoy.location) locations.append("|", location);
@ -93,6 +94,8 @@ auto Program::save() -> void {
auto Program::unload() -> void {
if(!emulator->loaded()) return;
toolsWindow->cheatEditor.saveCheats();
toolsWindow->setVisible(false);
emulator->unload();
superNintendo = {};
gameBoy = {};
@ -102,7 +105,6 @@ auto Program::unload() -> void {
presentation->setTitle({"bsnes v", Emulator::Version});
presentation->resetSystem.setEnabled(false);
presentation->unloadGame.setEnabled(false);
presentation->saveState.setEnabled(false);
presentation->loadState.setEnabled(false);
presentation->toolsMenu.setVisible(false);
presentation->clearViewport();
}

View File

@ -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);
}
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
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;
if(presentation->overscanCropping.checked()) {
if(height == 240) data += 8 * pitch, height -= 16;
if(height == 480) data += 16 * pitch, height -= 32;
if(height == 239) data += 8 * pitch, height -= 16;
if(height == 478) data += 16 * pitch, height -= 32;
}
if(video->lock(output, length, width, height)) {

View File

@ -56,6 +56,8 @@ Program::Program(string_vector arguments) {
new InputManager;
new SettingsWindow;
new CheatDatabase;
new ToolsWindow;
new AboutWindow;
arguments.takeLeft(); //ignore program location in argument parsing

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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;

View File

@ -18,9 +18,9 @@ CheatEditor::CheatEditor(TabFrame* parent) : TabFrameItem(parent) {
);
}
cheatList.onChange([&] { doChangeSelected(); });
cheatList.onToggle([&](TableViewCell cell) {
cheatList.onToggle([&](auto cell) {
cheats[cell.parent().offset()].enabled = cell.checked();
synchronizeCodes();
this->synchronizeCodes();
});
codeLabel.setText("Code(s):");
codeValue.onChange([&] { doModify(); });
@ -69,7 +69,7 @@ auto CheatEditor::doRefresh() -> void {
} 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.item(slot).cell(3).setText("<empty>").setForegroundColor({128, 128, 128});
}
}
@ -77,15 +77,9 @@ auto CheatEditor::doRefresh() -> void {
}
auto CheatEditor::doReset(bool force) -> void {
if(force || MessageDialog().setParent(*toolsManager).setText("Permanently erase all slots?").question() == "Yes") {
for(auto& cheat : cheats) {
cheat.enabled = false;
cheat.code = "";
cheat.description = "";
}
for(auto& item : cheatList.items()) {
item.cell(0).setChecked(false);
}
if(force || MessageDialog().setParent(*toolsManager).setText("Permanently erase all cheats?").question() == "Yes") {
for(auto& cheat : cheats) cheat = {};
for(auto& item : cheatList.items()) item.cell(0).setChecked(false);
doChangeSelected();
doRefresh();
synchronizeCodes();
@ -95,9 +89,7 @@ auto CheatEditor::doReset(bool force) -> void {
auto CheatEditor::doErase() -> void {
if(auto item = cheatList.selected()) {
auto& cheat = cheats[item.offset()];
cheat.enabled = false;
cheat.code = "";
cheat.description = "";
cheats[item.offset()] = {};
codeValue.setText("");
descriptionValue.setText("");
doRefresh();

View File

@ -70,7 +70,7 @@ auto StateManager::doRefresh() -> void {
description.resize(description.length());
stateList.item(slot).cell(1).setText(description).setForegroundColor({0, 0, 0});
} 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 {
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));
doRefresh();
doUpdateControls();

View File

@ -144,7 +144,7 @@ struct InputKeyboardQuartz {
hid->setVendorID(HID::Keyboard::GenericVendorID);
hid->setProductID(HID::Keyboard::GenericProductID);
hid->setPath(0);
hid->setPathID(0);
for(auto& key : keys) {
hid->buttons().append(key.name);
}