mirror of https://github.com/bsnes-emu/bsnes.git
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:
parent
a73a94f331
commit
3353efd3a1
|
@ -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/";
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
higan
|
||||
tomoko
|
||||
bsnes
|
||||
tomoko
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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)};
|
||||
}
|
||||
|
||||
}}
|
||||
|
|
Loading…
Reference in New Issue