Update to v094r14 release.

byuu says:

Man, over five weeks have passed without so much as touching the
codebase ... time is advancing so fast it's positively frightening. Oh
well, little by little, and we'll get there eventually.

Changelog:
- added save state slots (1-5 in the menu)
- added hotkeys settings dialog + mapping system
- added fullscreen toggle (with a cute aspect correction trick)

About three hours of work here.

Short-term:
- add input port changing support
- add other input types (mouse-based, etc)
- add cheat codes
- add timing configuration (video/audio sync)

Long-term:
- add slotted cart loader (SGB, BSX, ST)
- add DIP switch selection window (NSS)
- add cheat code database
- add state manager
- add overscan masking

Not planned:
- video color adjustments (will allow emulated color vs raw color; but
  no more sliders)
- pixel shaders
- ananke integration (will need to make a command-line version to get my
  games in)
- fancy audio adjustment controls (resampler, latency, volume)
- input focus settings
- relocating game library (not hard, just don't feel like it)
- localization support (not enough users)
- window geometry memory
- anything else not in higan v094
This commit is contained in:
Tim Allen 2015-04-13 21:16:33 +10:00
parent b4ba95242f
commit 89d578bc7f
20 changed files with 278 additions and 31 deletions

View File

@ -3,7 +3,7 @@
namespace Emulator {
static const char Name[] = "higan";
static const char Version[] = "094.13";
static const char Version[] = "094.14";
static const char Author[] = "byuu";
static const char License[] = "GPLv3";
static const char Website[] = "http://byuu.org/";

View File

@ -139,9 +139,8 @@ template<> struct stringify<vector<uint8_t>> {
auto data() const -> const char* { return _text.data(); }
auto size() const -> unsigned { return _text.size(); }
stringify(vector<uint8_t> source) {
_text.resize(source.size() + 1);
_text.resize(source.size());
memory::copy(_text.data(), source.data(), source.size());
_text[_text.size()] = 0;
}
};
@ -150,9 +149,8 @@ template<> struct stringify<const vector<uint8_t>&> {
auto data() const -> const char* { return _text.data(); }
auto size() const -> unsigned { return _text.size(); }
stringify(const vector<uint8_t>& source) {
_text.resize(source.size() + 1);
_text.resize(source.size());
memory::copy(_text.data(), source.data(), source.size());
_text[_text.size()] = 0;
}
};

View File

@ -15,8 +15,8 @@ auto activepath() -> string {
auto realpath(rstring name) -> string {
string result;
char path[PATH_MAX] = "";
if(::realpath(name, path)) result = string{path}.pathname();
if(result.empty()) result = activepath();
if(::realpath(name, path)) result = string{path}.transform("\\", "/").pathname();
if(result.empty()) return activepath();
result.transform("\\", "/");
if(result.endsWith("/") == false) result.append("/");
return result;

View File

@ -5,6 +5,9 @@ auto config() -> ConfigurationManager& { return *configurationManager; }
ConfigurationManager::ConfigurationManager() {
configurationManager = this;
userInterface.append(userInterface.showStatusBar, "ShowStatusBar");
append(userInterface, "UserInterface");
video.append(video.driver, "Driver");
video.append(video.synchronize, "Synchronize");
video.append(video.scale, "Scale");

View File

@ -2,6 +2,10 @@ struct ConfigurationManager : Configuration::Document {
ConfigurationManager();
auto quit() -> void;
struct UserInterface : Configuration::Node {
bool showStatusBar = true;
} userInterface;
struct Video : Configuration::Node {
string driver;
bool synchronize = false;

View File

@ -0,0 +1,24 @@
auto InputManager::appendHotkeys() -> void {
{
auto hotkey = new InputHotkey;
hotkey->name = "Toggle Fullscreen";
hotkey->action = [] {
presentation->toggleFullScreen();
};
hotkeys.append(hotkey);
}
Configuration::Node nodeHotkeys;
for(auto& hotkey : hotkeys) {
nodeHotkeys.append(hotkey->assignment, string{hotkey->name}.replace(" ", ""));
}
config.append(nodeHotkeys, "Hotkeys");
}
auto InputManager::pollHotkeys() -> void {
for(auto& hotkey : hotkeys) {
int16 state = hotkey->poll();
if(hotkey->state == 0 && state == 1 && hotkey->action) hotkey->action();
hotkey->state = state;
}
}

View File

@ -1,4 +1,5 @@
#include "../tomoko.hpp"
#include "hotkeys.cpp"
InputManager* inputManager = nullptr;
auto InputMapping::bind() -> void {
@ -85,6 +86,7 @@ InputManager::InputManager() {
config.append(nodeEmulator, string{inputEmulator.name}.replace(" ", ""));
}
appendHotkeys();
config.load({configpath(), "tomoko/input.bml"});
config.save({configpath(), "tomoko/input.bml"});
poll(); //will call bind();
@ -100,6 +102,10 @@ auto InputManager::bind() -> void {
}
}
}
for(auto& hotkey : hotkeys) {
hotkey->bind();
}
}
auto InputManager::poll() -> void {
@ -115,14 +121,19 @@ auto InputManager::poll() -> void {
this->devices = devices;
bind();
}
if(presentation && presentation->focused()) pollHotkeys();
}
auto InputManager::onChange(HID::Device& device, unsigned group, unsigned input, int16 oldValue, int16 newValue) -> void {
if(settingsManager->focused()) {
settingsManager->input.inputEvent(device, group, input, oldValue, newValue);
settingsManager->hotkeys.inputEvent(device, group, input, oldValue, newValue);
}
}
auto InputManager::quit() -> void {
config.save({configpath(), "tomoko/input.bml"});
emulators.reset();
hotkeys.reset();
}

View File

@ -12,6 +12,12 @@ struct InputMapping {
unsigned input = 0;
};
struct InputHotkey : InputMapping {
function<void ()> action;
int16 state = 0;
};
struct InputDevice {
string name;
vector<InputMapping*> mappings; //pointers used so that addresses do not change when arrays are resized
@ -34,8 +40,13 @@ struct InputManager {
auto onChange(HID::Device& device, unsigned group, unsigned input, int16 oldValue, int16 newValue) -> void;
auto quit() -> void;
//hotkeys.cpp
auto appendHotkeys() -> void;
auto pollHotkeys() -> void;
vector<HID::Device*> devices;
vector<InputEmulator> emulators;
vector<InputHotkey*> hotkeys;
Configuration::Document config;
};

View File

@ -67,6 +67,11 @@ Presentation::Presentation() {
config().audio.mute = muteAudio.checked();
program->dsp.setVolume(config().audio.mute ? 0.0 : 1.0);
});
showStatusBar.setText("Show Status Bar").setChecked(config().userInterface.showStatusBar).onToggle([&] {
config().userInterface.showStatusBar = showStatusBar.checked();
statusBar.setVisible(config().userInterface.showStatusBar);
if(visible()) resizeViewport();
});
showConfiguration.setText("Configuration ...").onActivate([&] {
settingsManager->setVisible();
settingsManager->setFocused();
@ -74,26 +79,28 @@ Presentation::Presentation() {
toolsMenu.setText("Tools").setVisible(false);
saveStateMenu.setText("Save State");
saveSlot1.setText("Slot 1").onActivate([&] {});
saveSlot2.setText("Slot 2").onActivate([&] {});
saveSlot3.setText("Slot 3").onActivate([&] {});
saveSlot4.setText("Slot 4").onActivate([&] {});
saveSlot5.setText("Slot 5").onActivate([&] {});
saveSlot1.setText("Slot 1").onActivate([&] { program->saveState(1); });
saveSlot2.setText("Slot 2").onActivate([&] { program->saveState(2); });
saveSlot3.setText("Slot 3").onActivate([&] { program->saveState(3); });
saveSlot4.setText("Slot 4").onActivate([&] { program->saveState(4); });
saveSlot5.setText("Slot 5").onActivate([&] { program->saveState(5); });
loadStateMenu.setText("Load State");
loadSlot1.setText("Slot 1").onActivate([&] {});
loadSlot2.setText("Slot 2").onActivate([&] {});
loadSlot3.setText("Slot 3").onActivate([&] {});
loadSlot4.setText("Slot 4").onActivate([&] {});
loadSlot5.setText("Slot 5").onActivate([&] {});
loadSlot1.setText("Slot 1").onActivate([&] { program->loadState(1); });
loadSlot2.setText("Slot 2").onActivate([&] { program->loadState(2); });
loadSlot3.setText("Slot 3").onActivate([&] { program->loadState(3); });
loadSlot4.setText("Slot 4").onActivate([&] { program->loadState(4); });
loadSlot5.setText("Slot 5").onActivate([&] { program->loadState(5); });
stateManager.setText("State Manager").onActivate([&] {});
cheatEditor.setText("Cheat Editor").onActivate([&] {});
statusBar.setFont(Font::sans(8, "Bold"));
statusBar.setVisible(config().userInterface.showStatusBar);
onClose([&] { program->quit(); });
setTitle({"tomoko v", Emulator::Version});
setResizable(false);
setBackgroundColor({16, 16, 16});
resizeViewport();
}
@ -106,18 +113,62 @@ auto Presentation::resizeViewport() -> void {
height = program->emulator().information.height;
}
if(config().video.scale == "Small" ) width *= 1, height *= 1;
if(config().video.scale == "Normal") width *= 2, height *= 2;
if(config().video.scale == "Large" ) width *= 4, height *= 4;
if(config().video.aspectCorrection) {
if(!program->activeEmulator || program->emulator().information.aspectRatio != 1.0) width = width * 5 / 4;
if(fullScreen() == false) {
bool arc = config().video.aspectCorrection
&& program->activeEmulator
&& program->emulator().information.aspectRatio != 1.0;
if(config().video.scale == "Small" ) width *= 1, height *= 1;
if(config().video.scale == "Normal") width *= 2, height *= 2;
if(config().video.scale == "Large" ) width *= 4, height *= 4;
if(arc) width = width * 8 / 7;
setSize({width, height});
viewport.setGeometry({0, 0, width, height});
setPlacement(0.5, 0.5);
} else {
auto desktop = Desktop::size();
//aspect ratio correction is always enabled in fullscreen mode
//note that below algorithm yields 7:6 ratio on 2560x(1440,1600) monitors
//this is extremely close to the optimum 8:7 ratio
//it is used so that linear interpolation isn't required
//todo: we should handle other resolutions nicely as well
unsigned multiplier = desktop.height() / height;
width *= 1 + multiplier;
height *= multiplier;
signed x = (desktop.width() - width) / 2;
signed y = (desktop.height() - height) / 2;
if(x < 0) x = 0;
if(y < 0) y = 0;
if(width > desktop.width()) width = desktop.width();
if(height > desktop.height()) height = desktop.height();
viewport.setGeometry({x, y, width, height});
}
setSize({width, height});
setPlacement(0.5, 0.5);
if(!program->activeEmulator) drawSplashScreen();
}
auto Presentation::toggleFullScreen() -> void {
if(fullScreen() == false) {
menuBar.setVisible(false);
statusBar.setVisible(false);
setResizable(true);
setFullScreen(true);
} else {
setFullScreen(false);
setResizable(false);
menuBar.setVisible(true);
statusBar.setVisible(config().userInterface.showStatusBar);
}
Application::processEvents();
resizeViewport();
}
auto Presentation::drawSplashScreen() -> void {
uint32* output;
unsigned length;

View File

@ -1,6 +1,7 @@
struct Presentation : Window {
Presentation();
auto resizeViewport() -> void;
auto toggleFullScreen() -> void;
auto drawSplashScreen() -> void;
MenuBar menuBar{this};
@ -28,6 +29,7 @@ struct Presentation : Window {
MenuCheckItem synchronizeVideo{&settingsMenu};
MenuCheckItem synchronizeAudio{&settingsMenu};
MenuCheckItem muteAudio{&settingsMenu};
MenuCheckItem showStatusBar{&settingsMenu};
MenuSeparator settingsMenuSeparator2{&settingsMenu};
MenuItem showConfiguration{&settingsMenu};
Menu toolsMenu{&menuBar};
@ -47,8 +49,8 @@ struct Presentation : Window {
MenuItem stateManager{&toolsMenu};
MenuItem cheatEditor{&toolsMenu};
VerticalLayout layout{this};
Viewport viewport{&layout, Size{~0, ~0}};
FixedLayout layout{this};
Viewport viewport{&layout, Geometry{0, 0, 1, 1}};
StatusBar statusBar{this};
};

View File

@ -51,7 +51,7 @@ auto Program::videoRefresh(const uint32* palette, const uint32* data, unsigned p
time(&current);
if(current != previous) {
previous = current;
presentation->statusBar.setText({"FPS: ", frameCounter});
statusText = {"FPS: ", frameCounter};
frameCounter = 0;
}
}

View File

@ -18,6 +18,7 @@ auto Program::loadMedia(Emulator::Interface& _emulator, Emulator::Interface::Med
mediaPaths(0) = {userpath(), "Emulation/System/", media.name, ".sys/"};
mediaPaths(media.id) = location;
folderPaths.append(location);
setEmulator(&_emulator);
updateVideoPalette();
@ -35,6 +36,8 @@ auto Program::unloadMedia() -> void {
emulator().unload();
setEmulator(nullptr);
mediaPaths.reset();
folderPaths.reset();
presentation->setTitle({"tomoko v", Emulator::Version});
presentation->systemMenu.setVisible(false);

View File

@ -5,6 +5,7 @@
#include <gba/interface/interface.hpp>
#include "interface.cpp"
#include "media.cpp"
#include "state.cpp"
#include "utility.cpp"
Program* program = nullptr;
@ -56,14 +57,15 @@ Program::Program() {
}
auto Program::emulator() -> Emulator::Interface& {
if(activeEmulator == nullptr) throw;
if(!activeEmulator) throw;
return *activeEmulator;
}
auto Program::main() -> void {
updateStatusText();
inputManager->poll();
if(activeEmulator == nullptr || emulator().loaded() == false) {
if(!activeEmulator || emulator().loaded() == false) {
audio.clear();
usleep(20 * 1000);
return;

View File

@ -24,7 +24,13 @@ struct Program : Emulator::Interface::Bind {
auto loadMedia(Emulator::Interface& interface, Emulator::Interface::Media& media, const string& location) -> void;
auto unloadMedia() -> void;
//state.cpp
auto loadState(unsigned slot) -> bool;
auto saveState(unsigned slot) -> bool;
//utility.cpp
auto showMessage(const string& text) -> void;
auto updateStatusText() -> void;
auto updateVideoFilter() -> void;
auto updateVideoPalette() -> void;
@ -32,7 +38,13 @@ struct Program : Emulator::Interface::Bind {
vector<Emulator::Interface*> emulators;
Emulator::Interface* activeEmulator = nullptr;
vector<string> mediaPaths;
vector<string> folderPaths;
string statusText;
string statusMessage;
time_t statusTime = 0;
};
extern Program* program;

View File

@ -0,0 +1,19 @@
auto Program::loadState(unsigned slot) -> bool {
if(!activeEmulator) return false;
auto memory = file::read({folderPaths[0], "higan/state-", slot, ".bst"});
if(memory.size() == 0) return showMessage({"Slot ", slot, " does not exist"}), false;
serializer s(memory.data(), memory.size());
if(emulator().unserialize(s) == false) return showMessage({"Slot ", slot, " state incompatible"}), false;
return showMessage({"Loaded from slot ", slot}), true;
}
auto Program::saveState(unsigned slot) -> bool {
if(!activeEmulator) return false;
serializer s = emulator().serialize();
if(s.size() == 0) return showMessage({"Failed to save state to slot ", slot}), false;
directory::create({folderPaths[0], "higan/"});
if(file::write({folderPaths[0], "higan/state-", slot, ".bst"}, s.data(), s.size()) == false) {
return showMessage({"Unable to write to slot ", slot}), false;
}
return showMessage({"Saved to slot ", slot}), true;
}

View File

@ -1,3 +1,27 @@
auto Program::showMessage(const string& text) -> void {
statusTime = time(0);
statusMessage = text;
}
auto Program::updateStatusText() -> void {
time_t currentTime = time(0);
string text;
if((currentTime - statusTime) <= 2) {
text = statusMessage;
} else if(!activeEmulator || emulator().loaded() == false) {
text = "No cartridge loaded";
} else if(0) {
text = "Paused";
} else {
text = statusText;
}
if(text != presentation->statusBar.text()) {
presentation->statusBar.setText(text);
}
}
auto Program::updateVideoFilter() -> void {
if(config().video.filter == "None") video.set(Video::Filter, Video::FilterNearest);
if(config().video.filter == "Blur") video.set(Video::Filter, Video::FilterLinear);

View File

@ -0,0 +1,62 @@
HotkeySettings::HotkeySettings(TabFrame* parent) : TabFrameItem(parent) {
setIcon(Icon::Device::Keyboard);
setText("Hotkeys");
layout.setMargin(5);
mappingList.setHeaderVisible();
mappingList.onActivate([&] { assignMapping(); });
mappingList.onChange([&] {
eraseButton.setEnabled((bool)mappingList.selected());
});
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(ListViewColumn().setText("Name"));
mappingList.append(ListViewColumn().setText("Mapping").setWidth(~0));
mappingList.append(ListViewColumn().setText("Device"));
for(auto& hotkey : inputManager->hotkeys) {
mappingList.append(ListViewItem().setText(0, hotkey->name));
}
mappingList.resizeColumns();
}
auto HotkeySettings::refreshMappings() -> void {
unsigned position = 0;
for(auto& hotkey : inputManager->hotkeys) {
auto path = hotkey->assignment.split("/");
string assignment = path.takeLast();
string device = path(0);
mappingList.item(position++)->setText(1, assignment).setText(2, device);
}
mappingList.resizeColumns();
}
auto HotkeySettings::assignMapping() -> void {
inputManager->poll(); //clear any pending events first
if(auto item = mappingList.selected()) {
activeMapping = inputManager->hotkeys[item->offset()];
settingsManager->statusBar.setText({"Press a key or button to map [", activeMapping->name, "] ..."});
}
}
auto HotkeySettings::inputEvent(HID::Device& device, unsigned group, unsigned input, int16 oldValue, int16 newValue) -> void {
if(!activeMapping) return;
if(!device.isKeyboard() || oldValue != 0 || newValue != 1) return;
if(activeMapping->bind(device, group, input, oldValue, newValue)) {
activeMapping = nullptr;
settingsManager->statusBar.setText("");
refreshMappings();
}
}

View File

@ -9,10 +9,11 @@ InputSettings::InputSettings(TabFrame* parent) : TabFrameItem(parent) {
emulatorList.onChange([&] { reloadPorts(); });
portList.onChange([&] { reloadDevices(); });
deviceList.onChange([&] { reloadMappings(); });
mappingList.onActivate([&] { assignMapping(); }).onChange([&] {
mappingList.setHeaderVisible();
mappingList.onActivate([&] { assignMapping(); });
mappingList.onChange([&] {
eraseButton.setEnabled((bool)mappingList.selected());
});
mappingList.setHeaderVisible();
resetButton.setText("Reset").onActivate([&] {
if(MessageDialog("Are you sure you want to erase all mappings for this device?").setParent(*settingsManager).question() == 0) {
for(auto& mapping : activeDevice().mappings) mapping->unbind();
@ -25,6 +26,7 @@ InputSettings::InputSettings(TabFrame* parent) : TabFrameItem(parent) {
refreshMappings();
}
});
reloadPorts();
}

View File

@ -1,5 +1,6 @@
#include "../tomoko.hpp"
#include "input.cpp"
#include "hotkeys.cpp"
#include "advanced.cpp"
SettingsManager* settingsManager = nullptr;
@ -14,4 +15,5 @@ SettingsManager::SettingsManager() {
setPlacement(0.0, 1.0);
input.mappingList.resizeColumns();
hotkeys.mappingList.resizeColumns();
}

View File

@ -24,6 +24,22 @@ struct InputSettings : TabFrameItem {
Button eraseButton{&controlLayout, Size{80, 0}};
};
struct HotkeySettings : TabFrameItem {
HotkeySettings(TabFrame*);
auto reloadMappings() -> void;
auto refreshMappings() -> void;
auto assignMapping() -> void;
auto inputEvent(HID::Device& device, unsigned group, unsigned input, int16 oldValue, int16 newValue) -> void;
InputMapping* activeMapping = nullptr;
VerticalLayout layout{this};
ListView mappingList{&layout, Size{~0, ~0}};
HorizontalLayout controlLayout{&layout, Size{~0, 0}};
Widget spacer{&controlLayout, Size{~0, 0}};
Button eraseButton{&controlLayout, Size{80, 0}};
};
struct AdvancedSettings : TabFrameItem {
AdvancedSettings(TabFrame*);
@ -44,6 +60,7 @@ struct SettingsManager : Window {
VerticalLayout layout{this};
TabFrame panelLayout{&layout, Size{~0, ~0}};
InputSettings input{&panelLayout};
HotkeySettings hotkeys{&panelLayout};
AdvancedSettings advanced{&panelLayout};
StatusBar statusBar{this};