Update to v106r25 release.

byuu says:

Changelog:

  - bsnes:
      - added full input mapping support (multi-mapping, digital+analog
        inputs, rumble, hotkeys, etc)
      - can now load multi-part games (eg Super Game Boy) from the
        command-line
      - added recent games menu with list clear function; supports
        multi-part games (sorting logic incomplete)
      - added automatic binding of gamepads on new configuration files
      - added view scaling support with aspect correction, overscan
        cropping, and integral scaling modes
      - added video shader support
      - added status bar (can be hidden)
      - added save states (both menu and hotkeys)
      - added fullscreen mode support
      - added support for loading compressed (ZIP) archives for any
        supported media type (SNES, GB, etc)
      - added frame counter
      - added auto-memory saving
      - added pause / block-input modes when main window loses focus
      - added --fullscreen command-line option to start bsnes in
        fullscreen mode
      - added input settings panel
      - added hotkeys settings panel
      - added path settings panel (paths aren't actually used set, but
        can be assigned)
  - higan: fixed macOS install rule [Sintendo]
  - higan: minor UI code cleanups
  - nall: renamed Processor to Architecture to fix macOS builds
    [Sintendo]

Yeah, you read right: recent games menu, path settings. And dynamic rate
control + screensaver suppression is on the todo list. I'm not fucking
around this time. I really want to make something special here.
This commit is contained in:
Tim Allen 2018-05-23 13:45:24 +10:00
parent a73a94f331
commit 3353efd3a1
26 changed files with 1410 additions and 193 deletions

View File

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

View File

@ -1,3 +1,3 @@
higan
tomoko
bsnes
tomoko

View File

@ -0,0 +1,66 @@
auto InputManager::bindHotkeys() -> void {
static int stateSlot = 1;
hotkeys.append(InputHotkey("Toggle Fullscreen Mode").onPress([] {
presentation->toggleFullscreenMode();
}));
hotkeys.append(InputHotkey("Toggle Mouse Capture").onPress([] {
input->acquired() ? input->release() : input->acquire();
}));
hotkeys.append(InputHotkey("Save State").onPress([&] {
program->saveState(stateSlot);
}));
hotkeys.append(InputHotkey("Load State").onPress([&] {
program->loadState(stateSlot);
}));
hotkeys.append(InputHotkey("Increment State Slot").onPress([&] {
if(--stateSlot < 1) stateSlot = 5;
program->showMessage({"Selected state slot ", stateSlot});
}));
hotkeys.append(InputHotkey("Decrement State Slot").onPress([&] {
if(++stateSlot > 5) stateSlot = 1;
program->showMessage({"Selected state slot ", stateSlot});
}));
hotkeys.append(InputHotkey("Fast Forward").onPress([] {
video->setBlocking(false);
audio->setBlocking(false);
}).onRelease([] {
video->setBlocking(settings["Video/Blocking"].boolean());
audio->setBlocking(settings["Audio/Blocking"].boolean());
}));
hotkeys.append(InputHotkey("Pause Emulation").onPress([] {
presentation->pauseEmulation.setChecked(!presentation->pauseEmulation.checked());
}));
hotkeys.append(InputHotkey("Reset Emulation").onPress([] {
presentation->resetSystem.doActivate();
}));
hotkeys.append(InputHotkey("Quit Emulator").onPress([] {
program->quit();
}));
for(auto& hotkey : hotkeys) {
hotkey.path = string{"Hotkey/", hotkey.name}.replace(" ", "");
hotkey.assignment = settings(hotkey.path).text();
hotkey.bind();
}
}
auto InputManager::pollHotkeys() -> void {
if(!program->focused()) return;
for(auto& hotkey : hotkeys) {
auto state = hotkey.poll();
if(hotkey.state == 0 && state == 1 && hotkey.press) hotkey.press();
if(hotkey.state == 1 && state == 0 && hotkey.release) hotkey.release();
hotkey.state = state;
}
}

View File

@ -1,15 +1,183 @@
#include "../bsnes.hpp"
#include "hotkeys.cpp"
unique_pointer<InputManager> inputManager;
auto InputMapping::bind() -> void {
mappings.reset();
for(auto& item : assignment.split(logic() == Logic::AND ? "&" : "|")) {
auto token = item.split("/");
if(token.size() < 3) continue; //skip invalid mappings
uint64 id = token[0].natural();
uint group = token[1].natural();
uint input = token[2].natural();
string qualifier = token(3, "None");
Mapping mapping;
for(auto& device : inputManager->devices) {
if(id != device->id()) continue;
mapping.device = device;
mapping.group = group;
mapping.input = input;
mapping.qualifier = Qualifier::None;
if(qualifier == "Lo") mapping.qualifier = Qualifier::Lo;
if(qualifier == "Hi") mapping.qualifier = Qualifier::Hi;
if(qualifier == "Rumble") mapping.qualifier = Qualifier::Rumble;
break;
}
if(!mapping.device) continue;
mappings.append(mapping);
}
settings[path].setValue(assignment);
}
auto InputMapping::bind(string mapping) -> void {
if(assignment.split(logic() == Logic::AND ? "&" : "|").find(mapping)) return; //ignore if already in mappings list
if(!assignment || assignment == "None") { //create new mapping
assignment = mapping;
} else { //add additional mapping
assignment.append(logic() == Logic::AND ? "&" : "|");
assignment.append(mapping);
}
bind();
}
auto InputMapping::bind(shared_pointer<HID::Device> device, uint group, uint input, int16 oldValue, int16 newValue) -> bool {
if(device->isNull() || (device->isKeyboard() && device->group(group).input(input).name() == "Escape")) {
return unbind(), true;
}
string encoding = {"0x", hex(device->id()), "/", group, "/", input};
if(isDigital()) {
if((device->isKeyboard() && group == HID::Keyboard::GroupID::Button)
|| (device->isMouse() && group == HID::Mouse::GroupID::Button)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Button)) {
if(newValue) {
return bind(encoding), true;
}
}
if((device->isJoypad() && group == HID::Joypad::GroupID::Axis)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Hat)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Trigger)) {
if(newValue < -16384 && group != HID::Joypad::GroupID::Trigger) { //triggers are always hi
return bind({encoding, "/Lo"}), true;
}
if(newValue > +16384) {
return bind({encoding, "/Hi"}), true;
}
}
}
if(isAnalog()) {
if((device->isMouse() && group == HID::Mouse::GroupID::Axis)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Axis)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Hat)) {
if(newValue < -16384 || newValue > +16384) {
return bind(encoding), true;
}
}
}
if(isRumble()) {
if(device->isJoypad() && group == HID::Joypad::GroupID::Button) {
if(newValue) {
return bind({encoding, "/Rumble"}), true;
}
}
}
return false;
}
auto InputMapping::unbind() -> void {
mappings.reset();
settings[path].setValue(assignment = "None");
}
auto InputMapping::poll() -> int16 {
return 0;
int16 result;
for(auto& mapping : mappings) {
auto& device = mapping.device;
auto& group = mapping.group;
auto& input = mapping.input;
auto& qualifier = mapping.qualifier;
auto value = device->group(group).input(input).value();
if(isDigital()) {
boolean output;
if((device->isKeyboard() && group == HID::Keyboard::GroupID::Button)
|| (device->isMouse() && group == HID::Mouse::GroupID::Button)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Button)) {
output = value != 0;
}
if((device->isJoypad() && group == HID::Joypad::GroupID::Axis)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Hat)
|| (device->isJoypad() && group == HID::Joypad::GroupID::Trigger)) {
if(qualifier == Qualifier::Lo) output = value < -16384;
if(qualifier == Qualifier::Hi) output = value > +16384;
}
if(logic() == Logic::AND && output == 0) return 0;
if(logic() == Logic::OR && output == 1) return 1;
}
if(isAnalog()) {
//logic does not apply to analog inputs ... always combinatorial
if(device->isMouse() && group == HID::Mouse::GroupID::Axis) result += value;
if(device->isJoypad() && group == HID::Joypad::GroupID::Axis) result += value >> 8;
if(device->isJoypad() && group == HID::Joypad::GroupID::Hat) result += value < 0 ? -1 : value > 0 ? +1 : 0;
}
}
if(isDigital() && logic() == Logic::AND) return 1;
return result;
}
auto InputMapping::rumble(bool enable) -> void {
for(auto& mapping : mappings) {
::input->rumble(mapping.device->id(), enable);
}
}
auto InputMapping::displayName() -> string {
if(!mappings) return "None";
string path;
for(auto& mapping : mappings) {
path.append(mapping.device->name());
if(mapping.device->name() != "Keyboard" && mapping.device->name() != "Mouse") {
//show device IDs to distinguish between multiple joypads
path.append("(", hex(mapping.device->id()), ")");
}
if(mapping.device->name() != "Keyboard") {
//keyboards only have one group; no need to append group name
path.append(".", mapping.device->group(mapping.group).name());
}
path.append(".", mapping.device->group(mapping.group).input(mapping.input).name());
if(mapping.qualifier == Qualifier::Lo) path.append(".Lo");
if(mapping.qualifier == Qualifier::Hi) path.append(".Hi");
if(mapping.qualifier == Qualifier::Rumble) path.append(".Rumble");
path.append(logic() == Logic::AND ? " and " : " or ");
}
return path.trimRight(logic() == Logic::AND ? " and " : " or ", 1L);
}
//
InputManager::InputManager() {
inputManager = this;
frequency = 5;
input->onChange({&InputManager::onChange, this});
frequency = max(1u, settings["Input/Frequency"].natural());
for(auto& port : emulator->ports) {
InputPort inputPort{port.id, port.name};
@ -19,7 +187,8 @@ InputManager::InputManager() {
InputMapping inputMapping;
inputMapping.name = input.name;
inputMapping.type = input.type;
inputMapping.path = string{inputPort.name, "/", inputDevice.name, "/", inputMapping.name}.replace(" ", "");
inputMapping.path = string{"Emulator/", inputPort.name, "/", inputDevice.name, "/", inputMapping.name}.replace(" ", "");
inputMapping.assignment = settings(inputMapping.path).text();
inputDevice.mappings.append(inputMapping);
}
inputPort.devices.append(inputDevice);
@ -27,7 +196,21 @@ InputManager::InputManager() {
ports.append(inputPort);
}
input->onChange({&InputManager::onChange, this});
bindHotkeys();
}
auto InputManager::bind() -> void {
for(auto& port : ports) {
for(auto& device : port.devices) {
for(auto& mapping : device.mappings) {
mapping.bind();
}
}
}
for(auto& hotkey : hotkeys) {
hotkey.bind();
}
}
auto InputManager::poll() -> void {
@ -36,10 +219,25 @@ auto InputManager::poll() -> void {
if(thisPoll - lastPoll < frequency) return;
lastPoll = thisPoll;
devices = input->poll();
auto devices = input->poll();
bool changed = devices.size() != this->devices.size();
if(!changed) {
for(auto n : range(devices)) {
changed = devices[n] != this->devices[n];
if(changed) break;
}
}
if(changed) {
this->devices = devices;
bind();
}
}
auto InputManager::onChange(shared_pointer<HID::Device> device, uint group, uint input, int16_t oldValue, int16_t newValue) -> void {
if(settingsWindow->focused()) {
settingsWindow->input.inputEvent(device, group, input, oldValue, newValue);
settingsWindow->hotkeys.inputEvent(device, group, input, oldValue, newValue);
}
}
auto InputManager::mapping(uint port, uint device, uint input) -> maybe<InputMapping&> {
@ -54,3 +252,10 @@ auto InputManager::mapping(uint port, uint device, uint input) -> maybe<InputMap
}
return nothing;
}
auto InputManager::findMouse() -> shared_pointer<HID::Device> {
for(auto& device : devices) {
if(device->isMouse()) return device;
}
return {};
}

View File

@ -1,10 +1,43 @@
struct InputMapping {
auto bind() -> void;
auto bind(string mapping) -> void;
auto bind(shared_pointer<HID::Device> device, uint group, uint input, int16 oldValue, int16 newValue) -> bool;
auto unbind() -> void;
auto poll() -> int16;
auto rumble(bool enable) -> void;
auto displayName() -> string;
auto isDigital() const -> bool { return type == 0; }
auto isAnalog() const -> bool { return type == 1; }
auto isRumble() const -> bool { return type == 2; }
string path; //configuration file key path
string name; //input name (human readable)
uint type = 0;
string assignment = "None";
enum class Logic : uint { AND, OR };
enum class Qualifier : uint { None, Lo, Hi, Rumble };
virtual auto logic() const -> Logic { return Logic::OR; }
struct Mapping {
shared_pointer<HID::Device> device;
uint group = 0;
uint input = 0;
Qualifier qualifier = Qualifier::None;
};
vector<Mapping> mappings;
};
struct InputHotkey : InputMapping {
InputHotkey(string name) { this->name = name; }
auto& onPress(function<void()> press) { return this->press = press, *this; }
auto& onRelease(function<void()> release) { return this->release = release, *this; }
//auto logic() const -> Logic override { return Logic::AND; }
function<auto() -> void> press;
function<auto() -> void> release;
int16 state = 0;
};
struct InputDevice {
@ -21,13 +54,20 @@ struct InputPort {
struct InputManager {
InputManager();
auto bind() -> void;
auto poll() -> void;
auto onChange(shared_pointer<HID::Device> device, uint group, uint input, int16_t oldValue, int16_t newValue) -> void;
auto mapping(uint port, uint device, uint input) -> maybe<InputMapping&>;
auto findMouse() -> shared_pointer<HID::Device>;
private:
//hotkeys.cpp
auto bindHotkeys() -> void;
auto pollHotkeys() -> void;
public:
vector<shared_pointer<HID::Device>> devices;
vector<InputPort> ports;
vector<InputHotkey> hotkeys;
uint64 lastPoll; //time in milliseconds since last call to poll()
uint64 frequency; //minimum time in milliseconds before poll() can be called again

View File

@ -7,19 +7,17 @@ Presentation::Presentation() {
presentation = this;
systemMenu.setText("System");
load.setText("Load Game ...").onActivate([&] {
BrowserDialog dialog;
dialog.setTitle("Load Super Nintendo");
dialog.setPath(settings["Path/Recent/SuperNintendo"].text());
dialog.setFilters({string{"Super Nintendo Games|*.sfc:*.smc"}});
if(auto location = dialog.openFile()) {
settings["Path/Recent/SuperNintendo"].setValue(Location::path(location));
program->load(location);
}
loadGame.setText("Load Game ...").onActivate([&] {
program->load();
});
reset.setText("Reset").setEnabled(false).onActivate([&] {
loadRecentGame.setText("Load Recent Game");
updateRecentGames();
resetSystem.setText("Reset System").setEnabled(false).onActivate([&] {
if(emulator->loaded()) emulator->reset();
});
unloadGame.setText("Unload Game").setEnabled(false).onActivate([&] {
program->unload();
});
controllerPort1.setText("Controller Port 1");
controllerPort2.setText("Controller Port 2");
for(auto& port : emulator->ports) {
@ -33,28 +31,79 @@ Presentation::Presentation() {
if(device.name == "None") continue;
MenuRadioItem item{menu};
item.setText(device.name).onActivate([=] {
auto path = string{"Emulator/", port.name}.replace(" ", "");
settings(path).setValue(device.name);
emulator->connect(port.id, device.id);
});
devices.append(item);
}
auto path = string{"Emulator/", port.name}.replace(" ", "");
auto device = settings(path).text();
bool found = false;
if(devices.objectCount() > 1) {
for(auto item : devices.objects<MenuRadioItem>()) {
if(item.text() == device) item.setChecked(), found = true;
}
}
//select the first device when a new settings file is being created
if(devices.objectCount() && !found) {
devices.objects<MenuRadioItem>()(0).doActivate();
}
}
quit.setText("Quit").onActivate([&] { program->quit(); });
settingsMenu.setText("Settings");
viewMenu.setText("View");
smallView.setText("Small").onActivate([&] {});
mediumView.setText("Medium").onActivate([&] {});
largeView.setText("Large").onActivate([&] {});
aspectCorrection.setText("Aspect Correction").onToggle([&] {});
integralScaling.setText("Integral Scaling").onToggle([&] {});
smallView.setText("Small").onActivate([&] {
settings["View/Size"].setValue("Small");
resizeWindow();
});
mediumView.setText("Medium").onActivate([&] {
settings["View/Size"].setValue("Medium");
resizeWindow();
});
largeView.setText("Large").onActivate([&] {
settings["View/Size"].setValue("Large");
resizeWindow();
});
aspectCorrection.setText("Aspect Correction").setChecked(settings["View/AspectCorrection"].boolean()).onToggle([&] {
settings["View/AspectCorrection"].setValue(aspectCorrection.checked());
resizeWindow();
});
overscanCropping.setText("Overscan Cropping").setChecked(settings["View/OverscanCropping"].boolean()).onToggle([&] {
settings["View/OverscanCropping"].setValue(overscanCropping.checked());
resizeWindow();
});
integralScaling.setText("Integral Scaling").setChecked(settings["View/IntegralScaling"].boolean()).onToggle([&] {
settings["View/IntegralScaling"].setValue(integralScaling.checked());
resizeViewport();
});
shaderMenu.setText("Shader");
updateShaders();
muteAudio.setText("Mute Audio").setChecked(settings["Audio/Mute"].boolean()).onToggle([&] {
settings["Audio/Mute"].setValue(muteAudio.checked());
});
configuration.setText("Configuration ...").onActivate([&] { settingsWindow->setVisible().setFocused(); });
showStatusBar.setText("Show Status Bar").setChecked(settings["UserInterface/ShowStatusBar"].boolean()).onToggle([&] {
settings["UserInterface/ShowStatusBar"].setValue(showStatusBar.checked());
statusBar.setVisible(showStatusBar.checked());
});
inputSettings.setText("Input ...").onActivate([&] { settingsWindow->show(0); });
hotkeySettings.setText("Hotkeys ...").onActivate([&] { settingsWindow->show(1); });
pathSettings.setText("Paths ...").onActivate([&] { settingsWindow->show(2); });
toolsMenu.setText("Tools");
saveState.setText("Save State").setEnabled(false);
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);
loadState1.setText("Slot 1").onActivate([&] { program->loadState(1); });
loadState2.setText("Slot 2").onActivate([&] { program->loadState(2); });
loadState3.setText("Slot 3").onActivate([&] { program->loadState(3); });
loadState4.setText("Slot 4").onActivate([&] { program->loadState(4); });
loadState5.setText("Slot 5").onActivate([&] { program->loadState(5); });
pauseEmulation.setText("Pause Emulation").onToggle([&] {
if(pauseEmulation.checked()) audio->clear();
});
@ -65,33 +114,43 @@ Presentation::Presentation() {
});
viewport.setDroppable().onDrop([&](auto locations) {
if(!file::exists(locations(0))) return;
program->load(locations(0));
program->gameQueue = locations;
program->load();
presentation->setFocused();
});
statusBar.setFont(Font().setBold());
statusBar.setVisible(settings["UserInterface/ShowStatusBar"].boolean());
onClose([&] {
program->quit();
});
onSize([&] {
resizeViewport();
});
setTitle({"bsnes v", Emulator::Version});
setBackgroundColor({0, 0, 0});
setSize({512, 480});
resizeWindow();
setCentered();
#if defined(PLATFORM_WINDOWS)
Application::Windows::onModalChange([](bool modal) { if(modal && audio) audio->clear(); });
Application::Windows::onModalChange([](bool modal) {
if(modal && audio) audio->clear();
});
#endif
#if defined(PLATFORM_MACOS)
Application::Cocoa::onAbout([&] { about.doActivate(); });
Application::Cocoa::onActivate([&] { setFocused(); });
Application::Cocoa::onPreferences([&] { settingsWindow->setVisible().setFocused(); });
Application::Cocoa::onPreferences([&] { settingsWindow->show(0); });
Application::Cocoa::onQuit([&] { doClose(); });
#endif
}
auto Presentation::clearViewport() -> void {
if(!video) return;
uint32_t* output;
uint length;
uint width = viewport.geometry().width();
@ -105,3 +164,168 @@ auto Presentation::clearViewport() -> void {
video->output();
}
}
auto Presentation::resizeViewport() -> void {
uint windowWidth = geometry().width();
uint windowHeight = geometry().height();
double width = 224 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
double height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0);
if(settings["View/IntegralScaling"].boolean()) {
uint widthMultiplier = windowWidth / width;
uint heightMultiplier = windowHeight / height;
uint multiplier = min(widthMultiplier, heightMultiplier);
uint viewportWidth = width * multiplier;
uint viewportHeight = height * multiplier;
viewport.setGeometry({
(windowWidth - viewportWidth) / 2, (windowHeight - viewportHeight) / 2,
viewportWidth, viewportHeight
});
} else {
double widthMultiplier = windowWidth / width;
double heightMultiplier = windowHeight / height;
double multiplier = min(widthMultiplier, heightMultiplier);
uint viewportWidth = width * multiplier;
uint viewportHeight = height * multiplier;
viewport.setGeometry({
(windowWidth - viewportWidth) / 2, (windowHeight - viewportHeight) / 2,
viewportWidth, viewportHeight
});
}
clearViewport();
}
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 multiplier = 2;
if(settings["View/Size"].text() == "Small" ) multiplier = 2;
if(settings["View/Size"].text() == "Medium") multiplier = 3;
if(settings["View/Size"].text() == "Large" ) multiplier = 4;
setSize({width * multiplier, height * multiplier});
resizeViewport();
}
auto Presentation::toggleFullscreenMode() -> void {
if(!fullScreen()) {
statusBar.setVisible(false);
menuBar.setVisible(false);
setFullScreen(true);
video->setExclusive(settings["Video/Exclusive"].boolean());
if(video->exclusive()) setVisible(false);
if(!input->acquired()) input->acquire();
} else {
if(input->acquired()) input->release();
if(video->exclusive()) setVisible(true);
video->setExclusive(false);
setFullScreen(false);
menuBar.setVisible(true);
statusBar.setVisible(settings["UserInterface/ShowStatusBar"].boolean());
}
resizeViewport();
}
auto Presentation::updateRecentGames() -> void {
loadRecentGame.reset();
for(auto index : range(5)) {
MenuItem item;
if(auto game = settings[string{"Game/Recent/", 1 + index}].text()) {
string displayName;
for(auto part : game.split(":")) {
displayName.append(Location::prefix(part), " + ");
}
displayName.trimRight(" + ", 1L);
item.setText(displayName).onActivate([=] {
program->gameQueue = game.split(":");
program->load();
});
} else {
item.setText("<empty>").setEnabled(false);
}
loadRecentGame.append(item);
if(item.text() == "<empty>") item.setEnabled(false); //todo: temporary hack; fix hiro/GTK!!
}
loadRecentGame.append(MenuSeparator());
loadRecentGame.append(MenuItem().setText("Clear List").onActivate([&] {
settings("Game/Recent/1").setValue("");
settings("Game/Recent/2").setValue("");
settings("Game/Recent/3").setValue("");
settings("Game/Recent/4").setValue("");
settings("Game/Recent/5").setValue("");
updateRecentGames();
}));
}
auto Presentation::addRecentGame(string location) -> void {
auto game1 = settings["Game/Recent/1"].text();
auto game2 = settings["Game/Recent/2"].text();
auto game3 = settings["Game/Recent/3"].text();
auto game4 = settings["Game/Recent/4"].text();
auto game5 = settings["Game/Recent/5"].text();
if(game1 == location);
else if(game2 == location) swap(game1, game2);
else if(game3 == location) swap(game1, game3);
else if(game4 == location) swap(game1, game4);
else if(game5 == location) swap(game1, game5);
else {
game5 = game4;
game4 = game3;
game3 = game2;
game2 = game1;
game1 = location;
}
settings("Game/Recent/1").setValue(game1);
settings("Game/Recent/2").setValue(game2);
settings("Game/Recent/3").setValue(game3);
settings("Game/Recent/4").setValue(game4);
settings("Game/Recent/5").setValue(game5);
updateRecentGames();
}
auto Presentation::updateShaders() -> void {
shaderMenu.reset();
Group shaders;
MenuRadioItem none{&shaderMenu};
none.setText("None").onActivate([&] {
settings["Video/Shader"].setValue("None");
program->updateVideoShader();
});
shaders.append(none);
MenuRadioItem blur{&shaderMenu};
blur.setText("Blur").onActivate([&] {
settings["Video/Shader"].setValue("Blur");
program->updateVideoShader();
});
shaders.append(blur);
auto location = locate("shaders/");
if(settings["Video/Driver"].text() == "OpenGL") {
for(auto shader : directory::folders(location, "*.shader")) {
if(shaders.objectCount() == 2) shaderMenu.append(MenuSeparator());
MenuRadioItem item{&shaderMenu};
item.setText(string{shader}.trimRight(".shader", 1L)).onActivate([=] {
settings["Video/Shader"].setValue({location, shader});
program->updateVideoShader();
});
shaders.append(item);
}
}
if(settings["Video/Shader"].text() == "None") none.setChecked();
if(settings["Video/Shader"].text() == "Blur") blur.setChecked();
for(auto item : shaders.objects<MenuRadioItem>()) {
if(settings["Video/Shader"].text() == string{location, item.text(), ".shader/"}) {
item.setChecked();
}
}
}

View File

@ -11,11 +11,20 @@ struct AboutWindow : Window {
struct Presentation : Window {
Presentation();
auto clearViewport() -> void;
auto resizeViewport() -> void;
auto resizeWindow() -> void;
auto toggleFullscreenMode() -> void;
auto clearRecentGames() -> void;
auto updateRecentGames() -> void;
auto addRecentGame(string location) -> void;
auto updateShaders() -> void;
MenuBar menuBar{this};
Menu systemMenu{&menuBar};
MenuItem load{&systemMenu};
MenuItem reset{&systemMenu};
MenuItem loadGame{&systemMenu};
Menu loadRecentGame{&systemMenu};
MenuItem resetSystem{&systemMenu};
MenuItem unloadGame{&systemMenu};
MenuSeparator portSeparator{&systemMenu};
Menu controllerPort1{&systemMenu};
Menu controllerPort2{&systemMenu};
@ -28,19 +37,36 @@ struct Presentation : Window {
MenuItem largeView{&viewMenu};
MenuSeparator viewSeparator{&viewMenu};
MenuCheckItem aspectCorrection{&viewMenu};
MenuCheckItem overscanCropping{&viewMenu};
MenuCheckItem integralScaling{&viewMenu};
Menu shaderMenu{&settingsMenu};
MenuCheckItem muteAudio{&settingsMenu};
MenuSeparator muteSeparator{&settingsMenu};
MenuItem configuration{&settingsMenu};
MenuCheckItem showStatusBar{&settingsMenu};
MenuSeparator settingsSeparator{&settingsMenu};
MenuItem inputSettings{&settingsMenu};
MenuItem hotkeySettings{&settingsMenu};
MenuItem pathSettings{&settingsMenu};
Menu toolsMenu{&menuBar};
Menu saveState{&toolsMenu};
MenuItem saveState1{&saveState};
MenuItem saveState2{&saveState};
MenuItem saveState3{&saveState};
MenuItem saveState4{&saveState};
MenuItem saveState5{&saveState};
Menu loadState{&toolsMenu};
MenuItem loadState1{&loadState};
MenuItem loadState2{&loadState};
MenuItem loadState3{&loadState};
MenuItem loadState4{&loadState};
MenuItem loadState5{&loadState};
MenuCheckItem pauseEmulation{&toolsMenu};
Menu helpMenu{&menuBar};
MenuItem about{&helpMenu};
FixedLayout layout{this};
Viewport viewport{&layout, Geometry{0, 0, 512, 480}};
Viewport viewport{&layout, Geometry{0, 0, 1, 1}};
StatusBar statusBar{this};
};
extern unique_pointer<AboutWindow> aboutWindow;

View File

@ -1,63 +1,88 @@
auto Program::load(string location) -> void {
if(!file::exists(location)) return;
auto Program::load() -> void {
unload();
//Heuristics::SuperFamicom() call will remove copier header from rom if present
auto rom = file::read(location);
auto heuristics = Heuristics::SuperFamicom(rom, location);
context.game.manifest = heuristics.manifest();
context.game.document = BML::unserialize(context.game.manifest);
context.game.location = location;
uint offset = 0;
if(auto size = heuristics.programRomSize()) {
context.game.program.resize(size);
memory::copy(&context.game.program[0], &rom[offset], size);
offset += size;
}
if(auto size = heuristics.dataRomSize()) {
context.game.data.resize(size);
memory::copy(&context.game.data[0], &rom[offset], size);
offset += size;
}
if(auto size = heuristics.expansionRomSize()) {
context.game.expansion.resize(size);
memory::copy(&context.game.expansion[0], &rom[offset], size);
offset += size;
}
if(auto size = heuristics.firmwareRomSize()) {
context.game.firmware.resize(size);
memory::copy(&context.game.firmware[0], &rom[offset], size);
offset += size;
}
auto type = Location::suffix(location).trimLeft(".", 1L);
for(auto& media : emulator->media) {
if(media.type != type) continue;
if(media.type != "sfc") continue;
Emulator::audio.reset(2, audio->frequency());
if(emulator->load(media.id)) {
gameQueue = {};
connectDevices();
emulator->power();
presentation->setTitle(emulator->title());
presentation->reset.setEnabled(true);
presentation->resetSystem.setEnabled(true);
presentation->unloadGame.setEnabled(true);
presentation->saveState.setEnabled(true);
presentation->loadState.setEnabled(true);
string locations = superNintendo.location;
if(auto location = gameBoy.location) locations.append(":", location);
presentation->addRecentGame(locations);
}
break;
}
}
auto Program::loadFile(string location) -> vector<uint8_t> {
if(Location::suffix(location) == ".zip") {
Decode::ZIP archive;
if(archive.open(location)) {
for(auto& file : archive.file) {
auto type = Location::suffix(file.name);
if(type == ".sfc" || type == ".smc" || type == ".gb" || type == ".gbc" || type == ".bs" || type == ".st") {
return archive.extract(file);
}
}
}
} else {
return file::read(location);
}
}
auto Program::loadSuperNintendo(string location) -> void {
auto rom = loadFile(location);
if(!rom) return;
//Heuristics::SuperFamicom() call will remove copier header from rom if present
auto heuristics = Heuristics::SuperFamicom(rom, location);
superNintendo.manifest = heuristics.manifest();
superNintendo.document = BML::unserialize(superNintendo.manifest);
superNintendo.location = location;
uint offset = 0;
if(auto size = heuristics.programRomSize()) {
superNintendo.program.resize(size);
memory::copy(&superNintendo.program[0], &rom[offset], size);
offset += size;
}
if(auto size = heuristics.dataRomSize()) {
superNintendo.data.resize(size);
memory::copy(&superNintendo.data[0], &rom[offset], size);
offset += size;
}
if(auto size = heuristics.expansionRomSize()) {
superNintendo.expansion.resize(size);
memory::copy(&superNintendo.expansion[0], &rom[offset], size);
offset += size;
}
if(auto size = heuristics.firmwareRomSize()) {
superNintendo.firmware.resize(size);
memory::copy(&superNintendo.firmware[0], &rom[offset], size);
offset += size;
}
}
auto Program::loadGameBoy(string location) -> void {
if(!file::exists(location)) return;
auto rom = loadFile(location);
if(!rom) return;
auto rom = file::read(location);
auto heuristics = Heuristics::GameBoy(rom, location);
context.gameBoy.manifest = heuristics.manifest();
context.gameBoy.document = BML::unserialize(context.game.manifest);
context.gameBoy.location = location;
gameBoy.manifest = heuristics.manifest();
gameBoy.document = BML::unserialize(gameBoy.manifest);
gameBoy.location = location;
context.gameBoy.program = rom;
gameBoy.program = rom;
}
auto Program::save() -> void {
@ -68,9 +93,14 @@ auto Program::save() -> void {
auto Program::unload() -> void {
if(!emulator->loaded()) return;
emulator->unload();
context = {};
superNintendo = {};
gameBoy = {};
bsMemory = {};
sufamiTurbo[0] = {};
sufamiTurbo[1] = {};
presentation->setTitle({"bsnes v", Emulator::Version});
presentation->reset.setEnabled(false);
presentation->resetSystem.setEnabled(false);
presentation->unloadGame.setEnabled(false);
presentation->saveState.setEnabled(false);
presentation->loadState.setEnabled(false);
presentation->clearViewport();

View File

@ -23,153 +23,153 @@ auto Program::open(uint id, string name, vfs::file::mode mode, bool required) ->
//Super Famicom
if(id == 1 && name == "manifest.bml" && mode == vfs::file::mode::read) {
return vfs::memory::file::open(context.game.manifest.data<uint8_t>(), context.game.manifest.size());
return vfs::memory::file::open(superNintendo.manifest.data<uint8_t>(), superNintendo.manifest.size());
}
if(id == 1 && name == "program.rom" && mode == vfs::file::mode::read) {
return vfs::memory::file::open(context.game.program.data(), context.game.program.size());
return vfs::memory::file::open(superNintendo.program.data(), superNintendo.program.size());
}
if(id == 1 && name == "data.rom" && mode == vfs::file::mode::read) {
return vfs::memory::file::open(context.game.data.data(), context.game.data.size());
return vfs::memory::file::open(superNintendo.data.data(), superNintendo.data.size());
}
if(id == 1 && name == "expansion.rom" && mode == vfs::file::mode::read) {
return vfs::memory::file::open(context.game.expansion.data(), context.game.expansion.size());
return vfs::memory::file::open(superNintendo.expansion.data(), superNintendo.expansion.size());
}
if(id == 1 && name == "arm6.program.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0x28000) {
return vfs::memory::file::open(&context.game.firmware.data()[0x00000], 0x20000);
if(superNintendo.firmware.size() == 0x28000) {
return vfs::memory::file::open(&superNintendo.firmware.data()[0x00000], 0x20000);
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Program,architecture=ARM6)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Program,architecture=ARM6)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".program.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "arm6.data.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0x28000) {
return vfs::memory::file::open(&context.game.firmware.data()[0x20000], 0x08000);
if(superNintendo.firmware.size() == 0x28000) {
return vfs::memory::file::open(&superNintendo.firmware.data()[0x20000], 0x08000);
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Data,architecture=ARM6)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Data,architecture=ARM6)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".data.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "hg51bs169.data.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0xc00) {
return vfs::memory::file::open(context.game.firmware.data(), context.game.firmware.size());
if(superNintendo.firmware.size() == 0xc00) {
return vfs::memory::file::open(superNintendo.firmware.data(), superNintendo.firmware.size());
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Data,architecture=HG51BS169)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Data,architecture=HG51BS169)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".data.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "lr35902.boot.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0x100) {
return vfs::memory::file::open(context.game.firmware.data(), context.game.firmware.size());
if(superNintendo.firmware.size() == 0x100) {
return vfs::memory::file::open(superNintendo.firmware.data(), superNintendo.firmware.size());
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Boot,architecture=LR35902)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Boot,architecture=LR35902)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".boot.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "upd7725.program.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0x2000) {
return vfs::memory::file::open(&context.game.firmware.data()[0x0000], 0x1800);
if(superNintendo.firmware.size() == 0x2000) {
return vfs::memory::file::open(&superNintendo.firmware.data()[0x0000], 0x1800);
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Program,architecture=uPD7725)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Program,architecture=uPD7725)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".program.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "upd7725.data.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0x2000) {
return vfs::memory::file::open(&context.game.firmware.data()[0x1800], 0x0800);
if(superNintendo.firmware.size() == 0x2000) {
return vfs::memory::file::open(&superNintendo.firmware.data()[0x1800], 0x0800);
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Data,architecture=uPD7725)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Data,architecture=uPD7725)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".data.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "upd96050.program.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0xd000) {
return vfs::memory::file::open(&context.game.firmware.data()[0x0000], 0xc000);
if(superNintendo.firmware.size() == 0xd000) {
return vfs::memory::file::open(&superNintendo.firmware.data()[0x0000], 0xc000);
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Program,architecture=uPD96050)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Program,architecture=uPD96050)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".program.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "upd96050.data.rom" && mode == vfs::file::mode::read) {
if(context.game.firmware.size() == 0xd000) {
return vfs::memory::file::open(&context.game.firmware.data()[0xc000], 0x1000);
if(superNintendo.firmware.size() == 0xd000) {
return vfs::memory::file::open(&superNintendo.firmware.data()[0xc000], 0x1000);
}
if(auto memory = context.game.document["game/board/memory(type=ROM,content=Data,architecture=uPD96050)"]) {
if(auto memory = superNintendo.document["game/board/memory(type=ROM,content=Data,architecture=uPD96050)"]) {
string location = locate({"firmware/", memory["identifier"].text().downcase(), ".data.rom"});
return vfs::fs::file::open(location, mode);
}
}
if(id == 1 && name == "save.ram") {
string location = {Location::path(context.game.location), Location::prefix(context.game.location), ".srm"};
string location = {Location::notsuffix(superNintendo.location), ".srm"};
return vfs::fs::file::open(location, mode);
}
if(id == 1 && name == "download.ram") {
string location = {Location::path(context.game.location), Location::prefix(context.game.location), ".psr"};
string location = {Location::notsuffix(superNintendo.location), ".psr"};
return vfs::fs::file::open(location, mode);
}
if(id == 1 && name == "time.rtc") {
string location = {Location::path(context.game.location), Location::prefix(context.game.location), ".rtc"};
string location = {Location::notsuffix(superNintendo.location), ".rtc"};
return vfs::fs::file::open(location, mode);
}
if(id == 1 && name == "arm6.data.ram") {
string location = {Location::path(context.game.location), Location::prefix(context.game.location), ".srm"};
string location = {Location::notsuffix(superNintendo.location), ".srm"};
return vfs::fs::file::open(location, mode);
}
if(id == 1 && name == "hg51bs169.data.ram") {
string location = {Location::path(context.game.location), Location::prefix(context.game.location), ".srm"};
string location = {Location::notsuffix(superNintendo.location), ".srm"};
return vfs::fs::file::open(location, mode);
}
if(id == 1 && name == "upd7725.data.ram") {
string location = {Location::path(context.game.location), Location::prefix(context.game.location), ".srm"};
string location = {Location::notsuffix(superNintendo.location), ".srm"};
return vfs::fs::file::open(location, mode);
}
if(id == 1 && name == "upd96050.data.ram") {
string location = {Location::path(context.game.location), Location::prefix(context.game.location), ".srm"};
string location = {Location::notsuffix(superNintendo.location), ".srm"};
return vfs::fs::file::open(location, mode);
}
//Game Boy
if(id == 2 && name == "manifest.bml" && mode == vfs::file::mode::read) {
return vfs::memory::file::open(context.gameBoy.manifest.data<uint8_t>(), context.gameBoy.manifest.size());
return vfs::memory::file::open(gameBoy.manifest.data<uint8_t>(), gameBoy.manifest.size());
}
if(id == 2 && name == "program.rom" && mode == vfs::file::mode::read) {
return vfs::memory::file::open(context.gameBoy.program.data(), context.gameBoy.program.size());
return vfs::memory::file::open(gameBoy.program.data(), gameBoy.program.size());
}
if(id == 2 && name == "save.ram") {
string location = {Location::path(context.gameBoy.location), Location::prefix(context.gameBoy.location), ".sav"};
string location = {Location::path(gameBoy.location), Location::prefix(gameBoy.location), ".sav"};
return vfs::fs::file::open(location, mode);
}
if(id == 2 && name == "time.rtc") {
string location = {Location::path(context.gameBoy.location), Location::prefix(context.gameBoy.location), ".rtc"};
string location = {Location::path(gameBoy.location), Location::prefix(gameBoy.location), ".rtc"};
return vfs::fs::file::open(location, mode);
}
@ -178,17 +178,35 @@ auto Program::open(uint id, string name, vfs::file::mode mode, bool required) ->
auto Program::load(uint id, string name, string type, string_vector options) -> Emulator::Platform::Load {
if(id == 1 && name == "Super Famicom" && type == "sfc") {
return {id, "Auto"};
if(gameQueue) {
superNintendo.location = gameQueue.takeLeft();
} else {
BrowserDialog dialog;
dialog.setTitle("Load Super Nintendo");
dialog.setPath(settings["Path/Recent/SuperNintendo"].text());
dialog.setFilters({string{"Super Nintendo Games|*.sfc:*.smc:*.zip"}});
superNintendo.location = dialog.openFile();
}
if(file::exists(superNintendo.location)) {
settings["Path/Recent/SuperNintendo"].setValue(Location::path(superNintendo.location));
loadSuperNintendo(superNintendo.location);
return {id, ""};
}
}
if(id == 2 && name == "Game Boy" && type == "gb") {
BrowserDialog dialog;
dialog.setTitle("Load Game Boy");
dialog.setPath(settings["Path/Recent/GameBoy"].text());
dialog.setFilters({string{"Game Boy Games|*.gb:*.gbc"}});
if(auto location = dialog.openFile()) {
settings["Path/Recent/GameBoy"].setValue(Location::path(location));
loadGameBoy(location);
if(gameQueue) {
gameBoy.location = gameQueue.takeLeft();
} else {
BrowserDialog dialog;
dialog.setTitle("Load Game Boy");
dialog.setPath(settings["Path/Recent/GameBoy"].text());
dialog.setFilters({string{"Game Boy Games|*.gb:*.gbc:*.zip"}});
gameBoy.location = dialog.openFile();
}
if(file::exists(gameBoy.location)) {
settings["Path/Recent/GameBoy"].setValue(Location::path(gameBoy.location));
loadGameBoy(gameBoy.location);
return {id, ""};
}
}
@ -200,8 +218,13 @@ auto Program::videoRefresh(const uint32* data, uint pitch, uint width, uint heig
uint32_t* output;
uint length;
pitch >>= 2;
if(presentation->overscanCropping.checked()) {
data += 16 * pitch;
height -= 32;
}
if(video->lock(output, length, width, height)) {
pitch >>= 2;
length >>= 2;
for(auto y : range(height)) {
@ -211,6 +234,17 @@ auto Program::videoRefresh(const uint32* data, uint pitch, uint width, uint heig
video->unlock();
video->output();
}
static uint frameCounter = 0;
static uint64 previous, current;
frameCounter++;
current = chrono::timestamp();
if(current != previous) {
previous = current;
statusText = {"FPS: ", frameCounter};
frameCounter = 0;
}
}
auto Program::audioSample(const double* samples, uint channels) -> void {
@ -223,9 +257,19 @@ auto Program::audioSample(const double* samples, uint channels) -> void {
}
auto Program::inputPoll(uint port, uint device, uint input) -> int16 {
inputManager->poll();
if(auto mapping = inputManager->mapping(port, device, input)) {
return mapping->poll();
if(focused() || settingsWindow->input.allowInput().checked()) {
inputManager->poll();
if(auto mapping = inputManager->mapping(port, device, input)) {
return mapping->poll();
}
}
return 0;
}
auto Program::inputRumble(uint port, uint device, uint input, bool enable) -> void {
if(focused() || settingsWindow->input.allowInput().checked() || !enable) {
if(auto mapping = inputManager->mapping(port, device, input)) {
return mapping->rumble(enable);
}
}
}

View File

@ -1,6 +1,7 @@
#include "../bsnes.hpp"
#include "interface.cpp"
#include "game.cpp"
#include "state.cpp"
#include "utility.cpp"
unique_pointer<Program> program;
@ -34,21 +35,38 @@ Program::Program(string_vector arguments) {
arguments.takeLeft(); //ignore program location in argument parsing
for(auto& argument : arguments) {
if(file::exists(argument)) {
load(argument);
if(argument == "--fullscreen") {
presentation->toggleFullscreenMode();
} else if(file::exists(argument)) {
gameQueue.append(argument);
}
}
if(gameQueue) load();
Application::onMain({&Program::main, this});
}
auto Program::main() -> void {
updateMessage();
inputManager->poll();
inputManager->pollHotkeys();
if(emulator->loaded() && !presentation->pauseEmulation.checked()) {
emulator->run();
} else {
if(!emulator->loaded()
|| presentation->pauseEmulation.checked()
|| (!focused() && settingsWindow->input.pauseEmulation.checked())
) {
audio->clear();
usleep(20 * 1000);
return;
}
emulator->run();
if(settings["Emulator/AutoSaveMemory/Enable"].boolean()) {
auto currentTime = chrono::timestamp();
if(currentTime - autoSaveTime >= settings["Emulator/AutoSaveMemory/Interval"].natural()) {
autoSaveTime = currentTime;
emulator->save();
}
}
}

View File

@ -10,37 +10,61 @@ struct Program : Emulator::Platform {
auto videoRefresh(const uint32* data, uint pitch, uint width, uint height) -> void override;
auto audioSample(const double* samples, uint channels) -> void override;
auto inputPoll(uint port, uint device, uint input) -> int16 override;
auto inputRumble(uint port, uint device, uint input, bool enable) -> void override;
//game.cpp
auto load(string location) -> void;
auto load() -> void;
auto loadFile(string location) -> vector<uint8_t>;
auto loadSuperNintendo(string location) -> void;
auto loadGameBoy(string location) -> void;
auto save() -> void;
auto unload() -> void;
//state.cpp
auto loadState(uint slot) -> bool;
auto saveState(uint slot) -> bool;
//utility.cpp
auto initializeVideoDriver() -> void;
auto initializeAudioDriver() -> void;
auto initializeInputDriver() -> void;
auto updateVideoShader() -> void;
auto connectDevices() -> void;
auto showMessage(string text) -> void;
auto updateMessage() -> void;
auto focused() -> bool;
private:
struct Context {
struct Game {
string location;
string manifest;
Markup::Node document;
vector<uint8_t> program;
vector<uint8_t> data;
vector<uint8_t> expansion;
vector<uint8_t> firmware;
} game;
public:
struct SuperNintendo {
string location;
string manifest;
Markup::Node document;
vector<uint8_t> program;
vector<uint8_t> data;
vector<uint8_t> expansion;
vector<uint8_t> firmware;
} superNintendo;
struct GameBoy {
string location;
string manifest;
Markup::Node document;
vector<uint8_t> program;
} gameBoy;
} context;
struct GameBoy {
string location;
string manifest;
Markup::Node document;
vector<uint8_t> program;
} gameBoy;
struct BSMemory {
} bsMemory;
struct SufamiTurbo {
} sufamiTurbo[2];
string_vector gameQueue;
uint64 autoSaveTime;
string statusText;
string statusMessage;
uint64 statusTime;
};
extern unique_pointer<Program> program;

View File

@ -0,0 +1,18 @@
auto Program::loadState(uint slot) -> bool {
if(!emulator->loaded()) return false;
string location = {Location::notsuffix(superNintendo.location), ".bs", slot};
if(!file::exists(location)) return showMessage({"Slot ", slot, " state does not exist"}), false;
auto memory = file::read(location);
serializer s{memory.data(), memory.size()};
if(!emulator->unserialize(s)) return showMessage({"Slot ", slot, " state is incompatible"}), false;
return showMessage({"Loaded state from slot ", slot}), true;
}
auto Program::saveState(uint slot) -> bool {
if(!emulator->loaded()) return false;
string location = {Location::notsuffix(superNintendo.location), ".bs", slot};
serializer s = emulator->serialize();
if(!s.size()) return showMessage({"Failed to save state to slot ", slot}), false;
if(!file::write(location, s.data(), s.size())) return showMessage({"Unable to write state to slot ", slot}), false;
return showMessage({"Saved state to slot ", slot}), true;
}

View File

@ -8,6 +8,7 @@ auto Program::initializeVideoDriver() -> void {
video = Video::create("None");
}
presentation->clearViewport();
updateVideoShader();
}
auto Program::initializeAudioDriver() -> void {
@ -35,10 +36,69 @@ auto Program::initializeAudioDriver() -> void {
}
auto Program::initializeInputDriver() -> void {
input = Input::create(settings["Input/Driver"].boolean());
input = Input::create(settings["Input/Driver"].text());
input->setContext(presentation->viewport.handle());
if(!input->ready()) {
MessageDialog().setText("Failed to initialize input driver").warning();
input = Input::create("None");
}
}
auto Program::updateVideoShader() -> void {
if(settings["Video/Driver"].text() == "OpenGL"
&& settings["Video/Shader"].text() != "None"
&& settings["Video/Shader"].text() != "Blur"
) {
video->setSmooth(false);
video->setShader(settings["Video/Shader"].text());
} else {
video->setSmooth(settings["Video/Shader"].text() == "Blur");
video->setShader("");
}
}
auto Program::connectDevices() -> void {
for(auto& port : emulator->ports) {
auto path = string{"Emulator/", port.name}.replace(" ", "");
auto name = settings(path).text();
for(auto& device : port.devices) {
if(device.name == name) {
emulator->connect(port.id, device.id);
break;
}
}
}
}
auto Program::showMessage(string text) -> void {
statusTime = chrono::timestamp();
statusMessage = text;
}
auto Program::updateMessage() -> void {
uint64 currentTime = chrono::timestamp();
string text;
if((currentTime - statusTime) <= 2) {
text = statusMessage;
} else if(!emulator->loaded()) {
text = "No game loaded";
} else if(presentation->pauseEmulation.checked()) {
text = "Paused";
} else if(!focused() && settingsWindow->input.pauseEmulation.checked()) {
text = "Paused";
} else {
text = statusText;
}
if(text != presentation->statusBar.text()) {
presentation->statusBar.setText(text);
}
}
auto Program::focused() -> bool {
//exclusive mode creates its own top-level window: presentation window will not have focus
if(video->exclusive()) return true;
if(presentation && presentation->focused()) return true;
return false;
}

View File

@ -0,0 +1,72 @@
HotkeySettings::HotkeySettings(TabFrame* parent) : TabFrameItem(parent) {
setIcon(Icon::Device::Keyboard);
setText("Hotkeys");
layout.setMargin(5);
mappingList.onActivate([&] { assignMapping(); });
mappingList.onChange([&] { eraseButton.setEnabled((bool)mappingList.selected()); });
resetButton.setText("Reset").onActivate([&] {
if(MessageDialog("Are you sure you want to erase all hotkey mappings?").setParent(*settingsWindow).question() == "Yes") {
for(auto& mapping : inputManager->hotkeys) mapping.unbind();
refreshMappings();
}
});
eraseButton.setText("Erase").onActivate([&] {
if(auto item = mappingList.selected()) {
inputManager->hotkeys[item.offset()].unbind();
refreshMappings();
}
});
reloadMappings();
refreshMappings();
}
auto HotkeySettings::reloadMappings() -> void {
mappingList.reset();
mappingList.append(TableViewHeader().setVisible()
.append(TableViewColumn().setText("Name"))
.append(TableViewColumn().setText("Mapping").setExpandable())
);
for(auto& hotkey : inputManager->hotkeys) {
mappingList.append(TableViewItem()
.append(TableViewCell().setText(hotkey.name))
.append(TableViewCell())
);
}
mappingList.resizeColumns();
}
auto HotkeySettings::refreshMappings() -> void {
uint index = 0;
for(auto& hotkey : inputManager->hotkeys) {
mappingList.item(index++).cell(1).setText(hotkey.displayName());
}
mappingList.resizeColumns();
}
auto HotkeySettings::assignMapping() -> void {
inputManager->poll(); //clear any pending events first
if(auto item = mappingList.selected()) {
activeMapping = inputManager->hotkeys[item.offset()];
settingsWindow->layout.setEnabled(false);
settingsWindow->statusBar.setText({"Press a key or button to map [", activeMapping->name, "] ..."});
}
}
auto HotkeySettings::inputEvent(shared_pointer<HID::Device> device, uint group, uint input, int16 oldValue, int16 newValue) -> void {
if(!activeMapping) return;
if(device->isMouse()) return;
if(activeMapping->bind(device, group, input, oldValue, newValue)) {
activeMapping.reset();
settingsWindow->statusBar.setText("Mapping assigned.");
refreshMappings();
timer.onActivate([&] {
timer.setEnabled(false);
settingsWindow->statusBar.setText();
settingsWindow->layout.setEnabled();
}).setInterval(200).setEnabled();
}
}

View File

@ -0,0 +1,155 @@
InputSettings::InputSettings(TabFrame* parent) : TabFrameItem(parent) {
setIcon(Icon::Device::Joypad);
setText("Input");
layout.setMargin(5);
defocusLabel.setText("When Focus is Lost:");
pauseEmulation.setText("Pause Emulation").onActivate([&] {
settings["Input/Defocus"].setValue("Pause");
});
blockInput.setText("Block Input").onActivate([&] {
settings["Input/Defocus"].setValue("Block");
});
allowInput.setText("Allow Input").onActivate([&] {
settings["Input/Defocus"].setValue("Allow");
});
if(settings["Input/Defocus"].text() == "Pause") pauseEmulation.setChecked();
if(settings["Input/Defocus"].text() == "Block") blockInput.setChecked();
if(settings["Input/Defocus"].text() == "Allow") allowInput.setChecked();
portLabel.setText("Port:");
portList.onChange([&] { reloadDevices(); });
deviceLabel.setText("Device:");
deviceList.onChange([&] { reloadMappings(); });
mappingList.onActivate([&] { assignMapping(); });
mappingList.onChange([&] { updateControls(); });
assignMouse1.onActivate([&] { assignMouseInput(0); });
assignMouse2.onActivate([&] { assignMouseInput(1); });
assignMouse3.onActivate([&] { assignMouseInput(2); });
resetButton.setText("Reset").onActivate([&] {
if(MessageDialog("Are you sure you want to erase all mappings for this device?").setParent(*settingsWindow).question() == "Yes") {
for(auto& mapping : activeDevice().mappings) mapping.unbind();
refreshMappings();
}
});
eraseButton.setText("Erase").onActivate([&] {
if(auto mapping = mappingList.selected()) {
activeDevice().mappings[mapping.offset()].unbind();
refreshMappings();
}
});
reloadPorts();
}
auto InputSettings::updateControls() -> void {
eraseButton.setEnabled((bool)mappingList.selected());
assignMouse1.setVisible(false);
assignMouse2.setVisible(false);
assignMouse3.setVisible(false);
if(auto mapping = mappingList.selected()) {
auto& input = activeDevice().mappings[mapping.offset()];
if(input.isDigital()) {
assignMouse1.setVisible().setText("Mouse Left");
assignMouse2.setVisible().setText("Mouse Middle");
assignMouse3.setVisible().setText("Mouse Right");
} else if(input.isAnalog()) {
assignMouse1.setVisible().setText("Mouse X-axis");
assignMouse2.setVisible().setText("Mouse Y-axis");
}
}
}
auto InputSettings::activePort() -> InputPort& {
return inputManager->ports[portList.selected().offset()];
}
auto InputSettings::activeDevice() -> InputDevice& {
auto index = deviceList.selected().property("index").natural();
return activePort().devices[index];
}
auto InputSettings::reloadPorts() -> void {
portList.reset();
for(auto& port : inputManager->ports) {
if(port.name == "Expansion Port") continue;
portList.append(ComboButtonItem().setText(port.name));
}
reloadDevices();
}
auto InputSettings::reloadDevices() -> void {
deviceList.reset();
uint index = 0;
for(auto& device : activePort().devices) {
if(device.mappings) { //only display devices that have configurable inputs
deviceList.append(ComboButtonItem().setText(device.name).setProperty("index", index));
}
index++;
}
reloadMappings();
}
auto InputSettings::reloadMappings() -> void {
mappingList.reset();
mappingList.append(TableViewHeader().setVisible()
.append(TableViewColumn().setText("Name"))
.append(TableViewColumn().setText("Mapping").setExpandable())
);
for(auto& mapping : activeDevice().mappings) {
mappingList.append(TableViewItem()
.append(TableViewCell().setText(mapping.name))
.append(TableViewCell())
);
}
refreshMappings();
updateControls();
}
auto InputSettings::refreshMappings() -> void {
uint index = 0;
for(auto& mapping : activeDevice().mappings) {
mappingList.item(index++).cell(1).setText(mapping.displayName());
}
mappingList.resizeColumns();
}
auto InputSettings::assignMapping() -> void {
inputManager->poll(); //clear any pending events first
if(auto mapping = mappingList.selected()) {
activeMapping = activeDevice().mappings[mapping.offset()];
settingsWindow->layout.setEnabled(false);
settingsWindow->statusBar.setText({"Press a key or button to map [", activeMapping->name, "] ..."});
}
}
auto InputSettings::assignMouseInput(uint id) -> void {
if(auto mapping = mappingList.selected()) {
activeMapping = activeDevice().mappings[mapping.offset()];
if(auto mouse = inputManager->findMouse()) {
if(activeMapping->isDigital()) {
return inputEvent(mouse, HID::Mouse::GroupID::Button, id, 0, 1, true);
} else if(activeMapping->isAnalog()) {
return inputEvent(mouse, HID::Mouse::GroupID::Axis, id, 0, +32767, true);
}
}
}
}
auto InputSettings::inputEvent(shared_pointer<HID::Device> device, uint group, uint input, int16 oldValue, int16 newValue, bool allowMouseInput) -> void {
if(!activeMapping) return;
if(device->isMouse() && !allowMouseInput) return;
if(activeMapping->bind(device, group, input, oldValue, newValue)) {
activeMapping.reset();
settingsWindow->statusBar.setText("Mapping assigned.");
refreshMappings();
timer.onActivate([&] {
timer.setEnabled(false);
settingsWindow->statusBar.setText();
settingsWindow->layout.setEnabled();
}).setInterval(200).setEnabled();
}
}

View File

@ -0,0 +1,96 @@
PathSettings::PathSettings(TabFrame* parent) : TabFrameItem(parent) {
setIcon(Icon::Emblem::Folder);
setText("Paths");
layout.setMargin(5);
gamesLabel.setText("Games:");
gamesPath.setEditable(false);
gamesAssign.setText("Assign ...").onActivate([&] {
if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) {
settings["Path/Games"].setValue(location);
refreshPaths();
}
});
gamesReset.setText("Reset").onActivate([&] {
settings["Path/Games"].setValue("");
refreshPaths();
});
patchesLabel.setText("Patches:");
patchesPath.setEditable(false);
patchesAssign.setText("Assign ...").onActivate([&] {
if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) {
settings["Path/Patches"].setValue(location);
refreshPaths();
}
});
patchesReset.setText("Reset").onActivate([&] {
settings["Path/Patches"].setValue("");
refreshPaths();
});
savesLabel.setText("Saves:");
savesPath.setEditable(false);
savesAssign.setText("Assign ...").onActivate([&] {
if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) {
settings["Path/Saves"].setValue(location);
refreshPaths();
}
});
savesReset.setText("Reset").onActivate([&] {
settings["Path/Saves"].setValue("");
refreshPaths();
});
statesLabel.setText("States:");
statesPath.setEditable(false);
statesAssign.setText("Assign ...").onActivate([&] {
if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) {
settings["Path/States"].setValue(location);
refreshPaths();
}
});
statesReset.setText("Reset").onActivate([&] {
settings["Path/States"].setValue("");
refreshPaths();
});
cheatsLabel.setText("Cheats:");
cheatsPath.setEditable(false);
cheatsAssign.setText("Assign ...").onActivate([&] {
if(auto location = BrowserDialog().setParent(*settingsWindow).selectFolder()) {
settings["Path/Cheats"].setValue(location);
refreshPaths();
}
});
cheatsReset.setText("Reset").onActivate([&] {
settings["Path/Cheats"].setValue("");
refreshPaths();
});
refreshPaths();
}
auto PathSettings::refreshPaths() -> void {
if(auto location = settings["Path/Games"].text()) {
gamesPath.setText(location).setForegroundColor();
} else {
gamesPath.setText("<last recently used>").setForegroundColor({128, 128, 128});
}
if(auto location = settings["Path/Patches"].text()) {
patchesPath.setText(location).setForegroundColor();
} else {
patchesPath.setText("<same as loaded game>").setForegroundColor({128, 128, 128});
}
if(auto location = settings["Path/Saves"].text()) {
savesPath.setText(location).setForegroundColor();
} else {
savesPath.setText("<same as loaded game>").setForegroundColor({128, 128, 128});
}
if(auto location = settings["Path/States"].text()) {
statesPath.setText(location).setForegroundColor();
} else {
statesPath.setText("<same as loaded game>").setForegroundColor({128, 128, 128});
}
if(auto location = settings["Path/Cheats"].text()) {
cheatsPath.setText(location).setForegroundColor();
} else {
cheatsPath.setText("<same as loaded game>").setForegroundColor({128, 128, 128});
}
}

View File

@ -1,4 +1,7 @@
#include "../bsnes.hpp"
#include "input.cpp"
#include "hotkeys.cpp"
#include "paths.cpp"
Settings settings;
unique_pointer<SettingsWindow> settingsWindow;
@ -11,9 +14,12 @@ Settings::Settings() {
};
set("Video/Driver", Video::safestDriver());
set("Video/Exclusive", false);
set("Video/Blocking", false);
set("Video/Shader", "Blur");
set("Audio/Driver", Audio::safestDriver());
set("Audio/Exclusive", false);
set("Audio/Blocking", true);
set("Audio/Device", "");
set("Audio/Frequency", 48000.0);
@ -21,12 +27,29 @@ Settings::Settings() {
set("Audio/Mute", false);
set("Input/Driver", Input::safestDriver());
set("Input/Frequency", 5);
set("Input/Defocus", "Pause");
set("View/Size", "Small");
set("View/AspectCorrection", true);
set("View/OverscanCropping", true);
set("View/IntegralScaling", true);
set("Path/Games", "");
set("Path/Patches", "");
set("Path/Saves", "");
set("Path/States", "");
set("Path/Cheats", "");
set("Path/Recent/SuperNintendo", Path::user());
set("Path/Recent/GameBoy", Path::user());
set("Path/Recent/BSMemory", Path::user());
set("Path/Recent/SufamiTurbo", Path::user());
set("UserInterface/ShowStatusBar", true);
set("Emulator/AutoSaveMemory/Enable", true);
set("Emulator/AutoSaveMemory/Interval", 30);
set("Crashed", false);
}
@ -38,9 +61,30 @@ SettingsWindow::SettingsWindow() {
settingsWindow = this;
layout.setMargin(5);
statusBar.setFont(Font().setBold());
setTitle("Settings");
setSize({600, 400});
setAlignment({0.0, 1.0});
setDismissable();
onSize([&] {
input.mappingList.resizeColumns();
hotkeys.mappingList.resizeColumns();
});
}
auto SettingsWindow::setVisible(bool visible) -> SettingsWindow& {
if(visible) {
input.refreshMappings();
hotkeys.refreshMappings();
}
return Window::setVisible(visible), *this;
}
auto SettingsWindow::show(uint index) -> void {
panel.item(index)->setSelected();
setVisible();
setFocused();
doSize();
}

View File

@ -3,12 +3,109 @@ struct Settings : Markup::Node {
auto save() -> void;
};
struct SettingsWindow : Window {
SettingsWindow();
struct InputSettings : TabFrameItem {
InputSettings(TabFrame*);
auto updateControls() -> void;
auto activePort() -> InputPort&;
auto activeDevice() -> InputDevice&;
auto reloadPorts() -> void;
auto reloadDevices() -> void;
auto reloadMappings() -> void;
auto refreshMappings() -> void;
auto assignMapping() -> void;
auto assignMouseInput(uint id) -> void;
auto inputEvent(shared_pointer<HID::Device> device, uint group, uint input, int16 oldValue, int16 newValue, bool allowMouseInput = false) -> void;
public:
maybe<InputMapping&> activeMapping;
Timer timer;
VerticalLayout layout{this};
HorizontalLayout defocusLayout{&layout, Size{~0, 0}};
Label defocusLabel{&defocusLayout, Size{0, 0}};
RadioLabel pauseEmulation{&defocusLayout, Size{0, 0}};
RadioLabel blockInput{&defocusLayout, Size{0, 0}};
RadioLabel allowInput{&defocusLayout, Size{0, 0}};
Group defocusGroup{&pauseEmulation, &blockInput, &allowInput};
HorizontalLayout selectionLayout{&layout, Size{~0, 0}};
Label portLabel{&selectionLayout, Size{0, 0}};
ComboButton portList{&selectionLayout, Size{~0, 0}};
Label deviceLabel{&selectionLayout, Size{0, 0}};
ComboButton deviceList{&selectionLayout, Size{~0, 0}};
TableView mappingList{&layout, Size{~0, ~0}};
HorizontalLayout controlLayout{&layout, Size{~0, 0}};
Button assignMouse1{&controlLayout, Size{100, 0}};
Button assignMouse2{&controlLayout, Size{100, 0}};
Button assignMouse3{&controlLayout, Size{100, 0}};
Widget controlSpacer{&controlLayout, Size{~0, 0}};
Button resetButton{&controlLayout, Size{80, 0}};
Button eraseButton{&controlLayout, Size{80, 0}};
};
struct HotkeySettings : TabFrameItem {
HotkeySettings(TabFrame*);
auto reloadMappings() -> void;
auto refreshMappings() -> void;
auto assignMapping() -> void;
auto inputEvent(shared_pointer<HID::Device> device, uint group, uint input, int16 oldValue, int16 newValue) -> void;
public:
maybe<InputMapping&> activeMapping;
Timer timer;
VerticalLayout layout{this};
TableView mappingList{&layout, Size{~0, ~0}};
HorizontalLayout controlLayout{&layout, Size{~0, 0}};
Widget controlSpacer{&controlLayout, Size{~0, 0}};
Button resetButton{&controlLayout, Size{80, 0}};
Button eraseButton{&controlLayout, Size{80, 0}};
};
struct PathSettings : TabFrameItem {
PathSettings(TabFrame*);
auto refreshPaths() -> void;
public:
VerticalLayout layout{this};
HorizontalLayout gamesLayout{&layout, Size{~0, 0}};
Label gamesLabel{&gamesLayout, Size{55, 0}};
LineEdit gamesPath{&gamesLayout, Size{~0, 0}};
Button gamesAssign{&gamesLayout, Size{80, 0}};
Button gamesReset{&gamesLayout, Size{80, 0}};
HorizontalLayout patchesLayout{&layout, Size{~0, 0}};
Label patchesLabel{&patchesLayout, Size{55, 0}};
LineEdit patchesPath{&patchesLayout, Size{~0, 0}};
Button patchesAssign{&patchesLayout, Size{80, 0}};
Button patchesReset{&patchesLayout, Size{80, 0}};
HorizontalLayout savesLayout{&layout, Size{~0, 0}};
Label savesLabel{&savesLayout, Size{55, 0}};
LineEdit savesPath{&savesLayout, Size{~0, 0}};
Button savesAssign{&savesLayout, Size{80, 0}};
Button savesReset{&savesLayout, Size{80, 0}};
HorizontalLayout statesLayout{&layout, Size{~0, 0}};
Label statesLabel{&statesLayout, Size{55, 0}};
LineEdit statesPath{&statesLayout, Size{~0, 0}};
Button statesAssign{&statesLayout, Size{80, 0}};
Button statesReset{&statesLayout, Size{80, 0}};
HorizontalLayout cheatsLayout{&layout, Size{~0, 0}};
Label cheatsLabel{&cheatsLayout, Size{55, 0}};
LineEdit cheatsPath{&cheatsLayout, Size{~0, 0}};
Button cheatsAssign{&cheatsLayout, Size{80, 0}};
Button cheatsReset{&cheatsLayout, Size{80, 0}};
};
struct SettingsWindow : Window {
SettingsWindow();
auto setVisible(bool visible = true) -> SettingsWindow&;
auto show(uint index) -> void;
public:
VerticalLayout layout{this};
TabFrame panel{&layout, Size{~0, ~0}};
TabFrameItem placeholder{&panel};
InputSettings input{&panel};
HotkeySettings hotkeys{&panel};
PathSettings paths{&panel};
StatusBar statusBar{this};
};
extern Settings settings;

View File

@ -84,9 +84,10 @@ ifeq ($(shell id -un),root)
else ifeq ($(platform),windows)
else ifeq ($(platform),macos)
mkdir -p ~/Library/Application\ Support/$(name)/
mkdir -p ~/Emulation/System/
mkdir -p ~/Library/Application\ Support/$(name)/systems/
mkdir -p ~/Emulation/
cp -R out/$(name).app /Applications/$(name).app
cp -R systems/* ~/Library/Application\ Support/$(name)/
cp -R systems/* ~/Library/Application\ Support/$(name)/systems/
else ifneq ($(filter $(platform),linux bsd),)
mkdir -p $(prefix)/bin/
mkdir -p $(prefix)/share/applications/

View File

@ -6,8 +6,7 @@ unique_pointer<InputManager> inputManager;
auto InputMapping::bind() -> void {
mappings.reset();
auto list = assignment.split(logic() == Logic::AND ? "&" : "|");
for(auto& item : list) {
for(auto& item : assignment.split(logic() == Logic::AND ? "&" : "|")) {
auto token = item.split("/");
if(token.size() < 3) continue; //skip invalid mappings
@ -102,6 +101,11 @@ auto InputMapping::bind(shared_pointer<HID::Device> device, uint group, uint inp
return false;
}
auto InputMapping::unbind() -> void {
mappings.reset();
settings[path].setValue(assignment = "None");
}
auto InputMapping::poll() -> int16 {
if(!mappings) return 0;
@ -148,18 +152,11 @@ auto InputMapping::poll() -> int16 {
}
auto InputMapping::rumble(bool enable) -> void {
if(!mappings) return;
for(auto& mapping : mappings) {
::input->rumble(mapping.device->id(), enable);
}
}
auto InputMapping::unbind() -> void {
mappings.reset();
assignment = "None";
settings[path].setValue(assignment);
}
//create a human-readable string from mappings list for display in the user interface
auto InputMapping::displayName() -> string {
if(!mappings) return "None";

View File

@ -2,9 +2,9 @@ struct InputMapping {
auto bind() -> void;
auto bind(string mapping) -> void;
auto bind(shared_pointer<HID::Device> device, uint group, uint input, int16 oldValue, int16 newValue) -> bool;
auto unbind() -> void;
auto poll() -> int16;
auto rumble(bool enable) -> void;
auto unbind() -> void;
auto isDigital() const -> bool { return type == 0; }
auto isAnalog() const -> bool { return type == 1; }
@ -20,6 +20,7 @@ struct InputMapping {
enum class Logic : uint { AND, OR };
enum class Qualifier : uint { None, Lo, Hi, Rumble };
virtual auto logic() const -> Logic { return Logic::OR; }
struct Mapping {
shared_pointer<HID::Device> device;
uint group = 0;

View File

@ -358,7 +358,6 @@ auto Presentation::loadShaders() -> void {
if(settings["Video/Shader"].text() == "None") videoShaderNone.setChecked();
if(settings["Video/Shader"].text() == "Blur") videoShaderBlur.setChecked();
for(auto radioItem : videoShaders.objects<MenuRadioItem>()) {
if(settings["Video/Shader"].text() == string{pathname, radioItem.text(), ".shader/"}) {
radioItem.setChecked();

View File

@ -4,9 +4,7 @@ HotkeySettings::HotkeySettings(TabFrame* parent) : TabFrameItem(parent) {
layout.setMargin(5);
mappingList.onActivate([&] { assignMapping(); });
mappingList.onChange([&] {
eraseButton.setEnabled((bool)mappingList.selected());
});
mappingList.onChange([&] { eraseButton.setEnabled((bool)mappingList.selected()); });
resetButton.setText("Reset").onActivate([&] {
if(MessageDialog("Are you sure you want to erase all hotkey mappings?").setParent(*settingsManager).question() == "Yes") {
for(auto& mapping : inputManager->hotkeys) mapping->unbind();
@ -40,10 +38,9 @@ auto HotkeySettings::reloadMappings() -> void {
}
auto HotkeySettings::refreshMappings() -> void {
uint position = 0;
uint index = 0;
for(auto& hotkey : inputManager->hotkeys) {
mappingList.item(position).cell(1).setText(hotkey->displayName());
position++;
mappingList.item(index++).cell(1).setText(hotkey->displayName());
}
mappingList.resizeColumns();
}

View File

@ -106,10 +106,9 @@ auto InputSettings::reloadMappings() -> void {
}
auto InputSettings::refreshMappings() -> void {
uint position = 0;
uint index = 0;
for(auto& mapping : activeDevice().mappings) {
mappingList.item(position).cell(1).setText(mapping.displayName());
position++;
mappingList.item(index++).cell(1).setText(mapping.displayName());
}
mappingList.resizeColumns();
}

View File

@ -7,7 +7,7 @@ namespace nall {
enum class Platform : uint { Windows, MacOS, Linux, BSD, Unknown };
enum class API : uint { Windows, Posix, Unknown };
enum class DisplayServer : uint { Windows, Quartz, Xorg, Unknown };
enum class Processor : uint { x86, amd64, ARM, PPC32, PPC64, Unknown };
enum class Architecture : uint { x86, amd64, ARM, PPC32, PPC64, Unknown };
enum class Endian : uint { LSB, MSB, Unknown };
enum class Build : uint { Debug, Stable, Size, Release, Performance };
@ -15,7 +15,7 @@ namespace nall {
static inline constexpr auto platform() -> Platform;
static inline constexpr auto api() -> API;
static inline constexpr auto display() -> DisplayServer;
static inline constexpr auto processor() -> Processor;
static inline constexpr auto architecture() -> Architecture;
static inline constexpr auto endian() -> Endian;
static inline constexpr auto build() -> Build;
}
@ -112,29 +112,29 @@ namespace nall {
#include <sys/endian.h>
#endif
/* Processor detection */
/* Architecture detection */
namespace nall {
#if defined(__i386__) || defined(_M_IX86)
#define PROCESSOR_X86
constexpr auto processor() -> Processor { return Processor::x86; }
#define ARCHITECTURE_X86
constexpr auto architecture() -> Architecture { return Architecture::x86; }
#elif defined(__amd64__) || defined(_M_AMD64)
#define PROCESSOR_AMD64
constexpr auto processor() -> Processor { return Processor::amd64; }
#define ARCHITECTURE_AMD64
constexpr auto architecture() -> Architecture { return Architecture::amd64; }
#elif defined(__arm__)
#define PROCESSOR_ARM
constexpr auto processor() -> Processor { return Processor::ARM; }
#define ARCHITECTURE_ARM
constexpr auto architecture() -> Architecture { return Architecture::ARM; }
#elif defined(__ppc64__) || defined(_ARCH_PPC64)
#define PROCESSOR_PPC64
constexpr auto processor() -> Processor { return Processor::PPC64; }
#define ARCHITECTURE_PPC64
constexpr auto architecture() -> Architecture { return Architecture::PPC64; }
#elif defined(__ppc__) || defined(_ARCH_PPC) || defined(_M_PPC)
#define PROCESSOR_PPC32
constexpr auto processor() -> Processor { return Processor::PPC32; }
#define ARCHITECTURE_PPC32
constexpr auto architecture() -> Architecture { return Architecture::PPC32; }
#else
#warning "unable to detect processor"
#define PROCESSOR_UNKNOWN
constexpr auto processor() -> Processor { return Processor::Unknown; }
#warning "unable to detect architecture"
#define ARCHITECTURE_UNKNOWN
constexpr auto architecture() -> Architecture { return Architecture::Unknown; }
#endif
}

View File

@ -69,4 +69,8 @@ inline auto suffix(string_view self) -> string {
return ""; //no suffix found
}
inline auto notsuffix(string_view self) -> string {
return {path(self), prefix(self)};
}
}}