Update to v103r10 release.

byuu says:

Changelog:

  - tomoko: video scaling options are now resolutions in the
    configuration file, eg "640x480", "960x720", "1280x960"
  - tomoko: main window is now always resizable instead of fixed width
    (also supports maximizing)
  - tomoko: added support for non-integral scaling in windowed mode
  - tomoko: made the quick/managed state messaging more consistent
  - tomoko: hide "Find Codes ..." button from the cheat editor window if
    the cheat database is not present
  - tomoko: per-game cheats.bml file now goes into the higan/ subfolder
    instead of the root folder

So the way the new video system works is you have the following options
on the video settings panel:

Windowed mode: { Aspect correction, Integral scaling, Adaptive }

Fullscreen mode: { Aspect correction, Integral scaling } (and one day,
hopefully Exclusive will be added here)

Whenever you adjust the overscan masking, or you change any of the
windowed or fullscreen mode settings, or you choose a different video
scale from the main menu, or you load a new game, or you unload a game,
or you rotate the display of an emulated system, the resizeViewport
logic will be invoked. This logic will remember the last option you
chose for video scale, and base the new window size on that value as an
upper limit of the new window size.

If you are in windowed mode and have adaptive enabled, it will shrink
the window to fit the contents of the emulated system's video output.
Otherwise, if you are not in integral scaling mode, it will scale the
video as large as possible to fit into the video scaled size you have
selected. Otherwise, it will perform an integral scale and center the
video inside of the viewport.

If you are in fullscreen mode, it's much the same, only there is no
adaptive mode.

A major problem with Xorg is that it's basically impossible to change
the resizability attribute of a window post-creation. You can do it, but
all kinds of crazy issues start popping up. Like if you toggle
fullscreen, then you'll find that the window won't grow past a certain
fairly small size that it's already at, and cannot be shrunk. And the
multipliers will stop expanding the window as large as they should. And
sometimes the UI elements won't be placed in the correct position, or
the video will draw over them. It's a big mess. So I have to keep the
main window always resizable. Also, note that this is not a limitation
of hiro. It's just totally broken in Xorg itself. No amount of fiddling
has ever allowed this to work reliably for me on either GTK+ 2 or Qt 4.

So what this means is ... the adaptive mode window is also resizable.
What happens here is, whenever you drag the corners of the main window
to resize it, or toggle the maximize window button, higan will bypass
the video scale resizing code and instead act as though the adaptive
scaling mode were disabled. So if integral scaling is checked, it'll
begin scaling in integral mode. Otherwise, it'll begin scaling in
non-integral mode.

And because of this flexibility, it no longer made sense for the video
scale menu to be a radio box. I know, it sucks to not see what the
active selection is anymore, but ... say you set the scale to small,
then you accidentally resized the window a little, but want it snapped
back to the proper small resolution dimensions. If it were a radio item,
you couldn't reselect the same option again, because it's already active
and events don't propagate in said case. By turning them into regular
menu options, the video scale menu can be used to restore window sizing.

Errata:

On Windows, the main window blinks a few times on first load. The fix
for that is a safeguard in the video settings code, roughly like so ...
but note you'd need to make a few other changes for this to work against
v103r10:

    auto VideoSettings::updateViewport(bool firstRun) -> void {
      settings["Video/Overscan/Horizontal"].setValue(horizontalMaskSlider.position());
      settings["Video/Overscan/Vertical"].setValue(verticalMaskSlider.position());
      settings["Video/Windowed/AspectCorrection"].setValue(windowedModeAspectCorrection.checked());
      settings["Video/Windowed/IntegralScaling"].setValue(windowedModeIntegralScaling.checked());
      settings["Video/Windowed/AdaptiveSizing"].setValue(windowedModeAdaptiveSizing.checked());
      settings["Video/Fullscreen/AspectCorrection"].setValue(fullscreenModeAspectCorrection.checked());
      settings["Video/Fullscreen/IntegralScaling"].setValue(fullscreenModeIntegralScaling.checked());
      horizontalMaskValue.setText({horizontalMaskSlider.position()});
      verticalMaskValue.setText({verticalMaskSlider.position()});
      if(!firstRun) presentation->resizeViewport();
    }

That'll get it down to one blink, as with v103 official. Not sure I can
eliminate that one extra blink.

I forgot to remove the setResizable toggle on fullscreen mode exit. On
Windows, the main window will end up unresizable after toggling
fullscreen. I missed that one because like I said, toggling resizability
is totally broken on Xorg. You can fix that with the below change:

    auto Presentation::toggleFullScreen() -> void {
      if(!fullScreen()) {
        menuBar.setVisible(false);
        statusBar.setVisible(false);
      //setResizable(true);
        setFullScreen(true);
        if(!input->acquired()) input->acquire();
      } else {
        if(input->acquired()) input->release();
        setFullScreen(false);
      //setResizable(false);
        menuBar.setVisible(true);
        statusBar.setVisible(settings["UserInterface/ShowStatusBar"].boolean());
      }
      resizeViewport();
    }

Windows is stealing focus on calls to resizeViewport(), so we need to
deal with that somehow ...

I'm not really concerned about the behavior of shrinking the viewport
below the smallest multiplier for a given system. It might make sense to
snap it to the window size and forego all other scaling, but honestly
... meh. I don't really care. Nobody sane is going to play like that.
This commit is contained in:
Tim Allen 2017-07-07 13:38:46 +10:00
parent 7af270aa59
commit cbbf5ec114
10 changed files with 100 additions and 76 deletions

View File

@ -12,7 +12,7 @@ using namespace nall;
namespace Emulator {
static const string Name = "higan";
static const string Version = "103.09";
static const string Version = "103.10";
static const string Author = "byuu";
static const string License = "GPLv3";
static const string Website = "http://byuu.org/";

View File

@ -1,7 +1,7 @@
//DSP clock (~24576khz) / 12 (~2048khz) is fed into the SMP
//from here, the wait states value is really a clock divider of {2, 4, 8, 16}
//because dividers of 8 and 16 are not evenly divislbe into 12, the SMP glitches
//in these two cases, the SMP ends up consuming 10 and 20 cycles instead
//due to an unknown hardware issue, clock dividers of 8 and 16 are glitchy
//the SMP ends up consuming 10 and 20 clocks per opcode cycle instead
//this causes unpredictable behavior on real hardware
//sometimes the SMP will run far slower than expected
//other times (and more likely), the SMP will deadlock until the system is reset

View File

@ -30,14 +30,15 @@ Settings::Settings() {
set("Video/Overscan/Vertical", 8);
set("Video/Windowed/AspectCorrection", true);
set("Video/Windowed/Adaptive", false);
set("Video/Windowed/Multiplier", "Small");
set("Video/Windowed/Multiplier/Small", 2);
set("Video/Windowed/Multiplier/Medium", 3);
set("Video/Windowed/Multiplier/Large", 4);
set("Video/Windowed/IntegralScaling", true);
set("Video/Windowed/AdaptiveSizing", false);
set("Video/Windowed/Scale", "Small");
set("Video/Windowed/Scale/Small", "640x480");
set("Video/Windowed/Scale/Medium", "960x720");
set("Video/Windowed/Scale/Large", "1280x960");
set("Video/Fullscreen/AspectCorrection", true);
set("Video/Fullscreen/Adaptive", false);
set("Video/Fullscreen/IntegralScaling", true);
set("Audio/Driver", ruby::Audio::optimalDriver());
set("Audio/Device", "");

View File

@ -37,7 +37,7 @@ auto InputManager::appendHotkeys() -> void {
hotkey->name = "Decrement Quick State";
hotkey->press = [&] {
if(--quickStateSlot < 1) quickStateSlot = 5;
program->showMessage({"Selected quick slot ", quickStateSlot});
program->showMessage({"Selected quick state slot ", quickStateSlot});
};
hotkeys.append(hotkey);
}
@ -46,7 +46,7 @@ auto InputManager::appendHotkeys() -> void {
hotkey->name = "Increment Quick State";
hotkey->press = [&] {
if(++quickStateSlot > 5) quickStateSlot = 1;
program->showMessage({"Selected quick slot ", quickStateSlot});
program->showMessage({"Selected quick state slot ", quickStateSlot});
};
hotkeys.append(hotkey);
}

View File

@ -47,19 +47,16 @@ Presentation::Presentation() {
settingsMenu.setText("Settings");
videoScaleMenu.setText("Video Scale");
if(settings["Video/Windowed/Multiplier"].text() == "Small") videoScaleSmall.setChecked();
if(settings["Video/Windowed/Multiplier"].text() == "Medium") videoScaleMedium.setChecked();
if(settings["Video/Windowed/Multiplier"].text() == "Large") videoScaleLarge.setChecked();
videoScaleSmall.setText("Small").onActivate([&] {
settings["Video/Windowed/Multiplier"].setValue("Small");
settings["Video/Windowed/Scale"].setValue("Small");
resizeViewport();
});
videoScaleMedium.setText("Medium").onActivate([&] {
settings["Video/Windowed/Multiplier"].setValue("Medium");
settings["Video/Windowed/Scale"].setValue("Medium");
resizeViewport();
});
videoScaleLarge.setText("Large").onActivate([&] {
settings["Video/Windowed/Multiplier"].setValue("Large");
settings["Video/Windowed/Scale"].setValue("Large");
resizeViewport();
});
videoEmulationMenu.setText("Video Emulation");
@ -122,13 +119,13 @@ Presentation::Presentation() {
});
toolsMenu.setText("Tools").setVisible(false);
saveStateMenu.setText("Save Quickstate");
saveQuickStateMenu.setText("Save Quick State");
saveSlot1.setText("Slot 1").onActivate([&] { program->saveState(1); });
saveSlot2.setText("Slot 2").onActivate([&] { program->saveState(2); });
saveSlot3.setText("Slot 3").onActivate([&] { program->saveState(3); });
saveSlot4.setText("Slot 4").onActivate([&] { program->saveState(4); });
saveSlot5.setText("Slot 5").onActivate([&] { program->saveState(5); });
loadStateMenu.setText("Load Quickstate");
loadQuickStateMenu.setText("Load Quick State");
loadSlot1.setText("Slot 1").onActivate([&] { program->loadState(1); });
loadSlot2.setText("Slot 2").onActivate([&] { program->loadState(2); });
loadSlot3.setText("Slot 3").onActivate([&] { program->loadState(3); });
@ -155,10 +152,15 @@ Presentation::Presentation() {
program->loadMedium();
});
onClose([&] { program->quit(); });
onSize([&] {
resizeViewport(true);
});
onClose([&] {
program->quit();
});
setTitle({"higan v", Emulator::Version});
setResizable(false);
setBackgroundColor({0, 0, 0});
resizeViewport();
setCentered();
@ -235,10 +237,16 @@ auto Presentation::clearViewport() -> void {
}
}
auto Presentation::resizeViewport() -> void {
//onSize is true only for events generated from window resizing
//it will suppress automatic viewport scaling, and disable adaptive scaling
//it does this so that the main window can always be resizable
auto Presentation::resizeViewport(bool onSize) -> void {
//clear video area before resizing to avoid seeing distorted video momentarily
clearViewport();
uint viewportWidth = geometry().width();
uint viewportHeight = geometry().height();
double emulatorWidth = 320;
double emulatorHeight = 240;
double aspectCorrection = 1.0;
@ -257,30 +265,37 @@ auto Presentation::resizeViewport() -> void {
if(!fullScreen()) {
if(settings["Video/Windowed/AspectCorrection"].boolean()) emulatorWidth *= aspectCorrection;
uint viewportMultiplier = 2;
if(settings["Video/Windowed/Multiplier"].text() == "Small") viewportMultiplier = settings["Video/Windowed/Multiplier/Small"].natural();
if(settings["Video/Windowed/Multiplier"].text() == "Medium") viewportMultiplier = settings["Video/Windowed/Multiplier/Medium"].natural();
if(settings["Video/Windowed/Multiplier"].text() == "Large") viewportMultiplier = settings["Video/Windowed/Multiplier/Large"].natural();
uint viewportWidth = 320 * viewportMultiplier;
uint viewportHeight = 240 * viewportMultiplier;
uint multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight);
if(!settings["Video/Windowed/Adaptive"].boolean()) {
if(!onSize) {
string viewportScale = "640x480";
if(settings["Video/Windowed/Scale"].text() == "Small") viewportScale = settings["Video/Windowed/Scale/Small"].text();
if(settings["Video/Windowed/Scale"].text() == "Medium") viewportScale = settings["Video/Windowed/Scale/Medium"].text();
if(settings["Video/Windowed/Scale"].text() == "Large") viewportScale = settings["Video/Windowed/Scale/Large"].text();
auto resolution = viewportScale.isplit("x", 1L);
viewportWidth = resolution(0).natural();
viewportHeight = resolution(1).natural();
}
if(settings["Video/Windowed/AdaptiveSizing"].boolean() && !onSize) {
uint multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight);
emulatorWidth *= multiplier;
emulatorHeight *= multiplier;
setSize({viewportWidth, viewportHeight});
viewport.setGeometry({
(viewportWidth - emulatorWidth) / 2, (viewportHeight - emulatorHeight) / 2,
emulatorWidth, emulatorHeight
});
setSize({viewportWidth = emulatorWidth, viewportHeight = emulatorHeight});
} else if(settings["Video/Windowed/IntegralScaling"].boolean()) {
uint multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight);
emulatorWidth *= multiplier;
emulatorHeight *= multiplier;
if(!onSize) setSize({viewportWidth, viewportHeight});
} else {
setSize({emulatorWidth * multiplier, emulatorHeight * multiplier});
viewport.setGeometry({0, 0, emulatorWidth * multiplier, emulatorHeight * multiplier});
double multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight);
emulatorWidth *= multiplier;
emulatorHeight *= multiplier;
if(!onSize) setSize({viewportWidth, viewportHeight});
}
} else {
if(settings["Video/Fullscreen/AspectCorrection"].boolean()) emulatorWidth *= aspectCorrection;
uint viewportWidth = geometry().width();
uint viewportHeight = geometry().height();
if(!settings["Video/Fullscreen/Adaptive"].boolean()) {
if(settings["Video/Fullscreen/IntegralScaling"].boolean()) {
uint multiplier = min(viewportWidth / emulatorWidth, viewportHeight / emulatorHeight);
emulatorWidth *= multiplier;
emulatorHeight *= multiplier;
@ -289,12 +304,13 @@ auto Presentation::resizeViewport() -> void {
emulatorWidth *= multiplier;
emulatorHeight *= multiplier;
}
viewport.setGeometry({
(viewportWidth - emulatorWidth) / 2, (viewportHeight - emulatorHeight) / 2,
emulatorWidth, emulatorHeight
});
}
viewport.setGeometry({
(viewportWidth - emulatorWidth) / 2, (viewportHeight - emulatorHeight) / 2,
emulatorWidth, emulatorHeight
});
//clear video area again to ensure entire viewport area has been painted in
clearViewport();
}

View File

@ -12,7 +12,7 @@ struct Presentation : Window {
Presentation();
auto updateEmulator() -> void;
auto clearViewport() -> void;
auto resizeViewport() -> void;
auto resizeViewport(bool onSize = false) -> void;
auto toggleFullScreen() -> void;
auto loadShaders() -> void;
@ -27,10 +27,10 @@ struct Presentation : Window {
MenuItem unloadSystem{&systemMenu};
Menu settingsMenu{&menuBar};
Menu videoScaleMenu{&settingsMenu};
MenuRadioItem videoScaleSmall{&videoScaleMenu};
MenuRadioItem videoScaleMedium{&videoScaleMenu};
MenuRadioItem videoScaleLarge{&videoScaleMenu};
Group videoScales{&videoScaleSmall, &videoScaleMedium, &videoScaleLarge};
MenuItem videoScaleSmall{&videoScaleMenu};
MenuItem videoScaleMedium{&videoScaleMenu};
MenuItem videoScaleLarge{&videoScaleMenu};
//Group videoScales{&videoScaleSmall, &videoScaleMedium, &videoScaleLarge};
Menu videoEmulationMenu{&settingsMenu};
MenuCheckItem blurEmulation{&videoEmulationMenu};
MenuCheckItem colorEmulation{&videoEmulationMenu};
@ -48,18 +48,18 @@ struct Presentation : Window {
MenuSeparator showConfigurationSeparator{&settingsMenu};
MenuItem showConfiguration{&settingsMenu};
Menu toolsMenu{&menuBar};
Menu saveStateMenu{&toolsMenu};
MenuItem saveSlot1{&saveStateMenu};
MenuItem saveSlot2{&saveStateMenu};
MenuItem saveSlot3{&saveStateMenu};
MenuItem saveSlot4{&saveStateMenu};
MenuItem saveSlot5{&saveStateMenu};
Menu loadStateMenu{&toolsMenu};
MenuItem loadSlot1{&loadStateMenu};
MenuItem loadSlot2{&loadStateMenu};
MenuItem loadSlot3{&loadStateMenu};
MenuItem loadSlot4{&loadStateMenu};
MenuItem loadSlot5{&loadStateMenu};
Menu saveQuickStateMenu{&toolsMenu};
MenuItem saveSlot1{&saveQuickStateMenu};
MenuItem saveSlot2{&saveQuickStateMenu};
MenuItem saveSlot3{&saveQuickStateMenu};
MenuItem saveSlot4{&saveQuickStateMenu};
MenuItem saveSlot5{&saveQuickStateMenu};
Menu loadQuickStateMenu{&toolsMenu};
MenuItem loadSlot1{&loadQuickStateMenu};
MenuItem loadSlot2{&loadQuickStateMenu};
MenuItem loadSlot3{&loadQuickStateMenu};
MenuItem loadSlot4{&loadQuickStateMenu};
MenuItem loadSlot5{&loadQuickStateMenu};
MenuSeparator toolsMenuSeparator{&toolsMenu};
MenuItem cheatEditor{&toolsMenu};
MenuItem stateManager{&toolsMenu};

View File

@ -14,7 +14,7 @@ auto Program::loadState(uint slot, bool managed) -> bool {
if(memory.size() == 0) return showMessage({"Slot ", slot, " ", type, " state does not exist"}), false;
serializer s(memory.data(), memory.size());
if(emulator->unserialize(s) == false) return showMessage({"Slot ", slot, " ", type, " state incompatible"}), false;
return showMessage({"Loaded from ", type, " slot ", slot}), true;
return showMessage({"Loaded ", type, " state from slot ", slot}), true;
}
auto Program::saveState(uint slot, bool managed) -> bool {
@ -22,10 +22,10 @@ auto Program::saveState(uint slot, bool managed) -> bool {
string type = managed ? "managed" : "quick";
auto location = stateName(slot, managed);
serializer s = emulator->serialize();
if(s.size() == 0) return showMessage({"Failed to save state to slot ", slot}), false;
if(s.size() == 0) return showMessage({"Failed to save ", type, " state to slot ", slot}), false;
directory::create(Location::path(location));
if(file::write(location, s.data(), s.size()) == false) {
return showMessage({"Unable to write to ", type, " slot ", slot}), false;
return showMessage({"Unable to write ", type, " state to slot ", slot}), false;
}
return showMessage({"Saved to ", type, " slot ", slot}), true;
return showMessage({"Saved ", type, " state to slot ", slot}), true;
}

View File

@ -27,11 +27,12 @@ struct VideoSettings : TabFrameItem {
Label windowedModeLabel{&layout, Size{~0, 0}, 2};
HorizontalLayout windowedModeLayout{&layout, Size{~0, 0}};
CheckLabel windowedModeAspectCorrection{&windowedModeLayout, Size{0, 0}};
CheckLabel windowedModeAdaptive{&windowedModeLayout, Size{0, 0}};
CheckLabel windowedModeIntegralScaling{&windowedModeLayout, Size{0, 0}};
CheckLabel windowedModeAdaptiveSizing{&windowedModeLayout, Size{0, 0}};
Label fullscreenModeLabel{&layout, Size{~0, 0}, 2};
HorizontalLayout fullscreenModeLayout{&layout, Size{~0, 0}};
CheckLabel fullscreenModeAspectCorrection{&fullscreenModeLayout, Size{0, 0}};
CheckLabel fullscreenModeAdaptive{&fullscreenModeLayout, Size{0, 0}};
CheckLabel fullscreenModeIntegralScaling{&fullscreenModeLayout, Size{0, 0}};
auto updateColor() -> void;
auto updateOverscan() -> void;

View File

@ -24,12 +24,13 @@ VideoSettings::VideoSettings(TabFrame* parent) : TabFrameItem(parent) {
verticalMaskSlider.setLength(25).setPosition(settings["Video/Overscan/Vertical"].natural()).onChange([&] { updateOverscan(); });
windowedModeLabel.setFont(Font().setBold()).setText("Windowed Mode");
windowedModeAspectCorrection.setText("Correct aspect ratio").setChecked(settings["Video/Windowed/AspectCorrection"].boolean()).onToggle([&] { updateViewport(); });
windowedModeAdaptive.setText("Resize window to viewport").setChecked(settings["Video/Windowed/Adaptive"].boolean()).onToggle([&] { updateViewport(); });
windowedModeAspectCorrection.setText("Aspect correction").setChecked(settings["Video/Windowed/AspectCorrection"].boolean()).onToggle([&] { updateViewport(); });
windowedModeIntegralScaling.setText("Integral scaling").setChecked(settings["Video/Windowed/IntegralScaling"].boolean()).onToggle([&] { updateViewport(); });
windowedModeAdaptiveSizing.setText("Adaptive sizing").setChecked(settings["Video/Windowed/AdaptiveSizing"].boolean()).onToggle([&] { updateViewport(); });
fullscreenModeLabel.setFont(Font().setBold()).setText("Fullscreen Mode");
fullscreenModeAspectCorrection.setText("Correct aspect ratio").setChecked(settings["Video/Fullscreen/AspectCorrection"].boolean()).onToggle([&] { updateViewport(); });
fullscreenModeAdaptive.setText("Resize viewport to window").setChecked(settings["Video/Fullscreen/Adaptive"].boolean()).onToggle([&] { updateViewport(); });
fullscreenModeAspectCorrection.setText("Aspect correction").setChecked(settings["Video/Fullscreen/AspectCorrection"].boolean()).onToggle([&] { updateViewport(); });
fullscreenModeIntegralScaling.setText("Integral scaling").setChecked(settings["Video/Fullscreen/IntegralScaling"].boolean()).onToggle([&] { updateViewport(); });
updateColor();
updateOverscan();
@ -56,8 +57,9 @@ auto VideoSettings::updateOverscan() -> void {
auto VideoSettings::updateViewport() -> void {
settings["Video/Windowed/AspectCorrection"].setValue(windowedModeAspectCorrection.checked());
settings["Video/Windowed/Adaptive"].setValue(windowedModeAdaptive.checked());
settings["Video/Windowed/IntegralScaling"].setValue(windowedModeIntegralScaling.checked());
settings["Video/Windowed/AdaptiveSizing"].setValue(windowedModeAdaptiveSizing.checked());
settings["Video/Fullscreen/AspectCorrection"].setValue(fullscreenModeAspectCorrection.checked());
settings["Video/Fullscreen/Adaptive"].setValue(fullscreenModeAdaptive.checked());
settings["Video/Fullscreen/IntegralScaling"].setValue(fullscreenModeIntegralScaling.checked());
presentation->resizeViewport();
}

View File

@ -27,6 +27,9 @@ CheatEditor::CheatEditor(TabFrame* parent) : TabFrameItem(parent) {
findCodesButton.setText("Find Codes ...").onActivate([&] { cheatDatabase->findCodes(); });
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 {
@ -128,7 +131,7 @@ auto CheatEditor::addCode(const string& code, const string& description, bool en
auto CheatEditor::loadCheats() -> void {
doReset(true);
auto contents = string::read({program->mediumPaths(1), "cheats.bml"});
auto contents = string::read({program->mediumPaths(1), "higan/cheats.bml"});
auto document = BML::unserialize(contents);
for(auto cheat : document["cartridge"].find("cheat")) {
if(!addCode(cheat["code"].text(), cheat["description"].text(), (bool)cheat["enabled"])) break;
@ -149,9 +152,10 @@ auto CheatEditor::saveCheats() -> void {
count++;
}
if(count) {
file::write({program->mediumPaths(1), "cheats.bml"}, document);
directory::create({program->mediumPaths(1), "higan/"});
file::write({program->mediumPaths(1), "higan/cheats.bml"}, document);
} else {
file::remove({program->mediumPaths(1), "cheats.bml"});
file::remove({program->mediumPaths(1), "higan/cheats.bml"});
}
doReset(true);
}