mirror of https://github.com/bsnes-emu/bsnes.git
Update to v106r35 release.
byuu says: Changelog: - sfc/ppu-fast: fixed overscan crash - sfc/ppu-fast: fixed direct color mode - sfc: reconnected MSU1 support - higan: game.sfc/msu1/data.rom, game.sfc/msu1/track-#.pcm - bsnes: game.msu, game-#.pcm - bsnes: added cheat code editor - bsnes: added cheat code database support - sfc/ppu-fast: clear overscan lines when overscan disabled - sfc: output 223/239 lines instead of 224/240 lines - bsnes: fix aspect correction calculation - bsnes: crop line 224 when overscan masking is enabled - bsnes: exposed Expansion Port menu; but hid “21fx” from the list of devices - bsnes: tools menu is hidden until a game is loaded - ruby/input/keyboard/quartz: fixed compilation error So only bsnes the automated overscan cropping option. In higan, you can crop however many lines you like from the top or bottom of the image. But for bsnes, it automatically eats sixteen lines. My view right now is that if bsnes is meant to be the casual gaming emulator, that it should eat line 224 in this mode. Most games show content here, but because of the way the SNES PPU works, the very last line ends up on its very own tile row (line 0 isn't rendered), if the scroll registers don't account for it. There's a small number of games that will draw junk data to the very last scanline of the frame as a result of this. So I chose, at least for now, to hide it. Users can obviously disable overscan cropping to see this scanline. I'm open to being convinced not to do this, if someone has a compelling reason. We're pretty much screwed one way or the other with no overscan masking. If we output 239 lines, then most games will render 7 blank lines + 224 drawn lines + 8 blank lines, and the black top and bottom aren't centered. But if we output 240 lines to get 8 + 224 + 8, then games that do use overscan will have a blank line at the very bottom of the window. I'm also trying out a modified cheat code file format. It's been forever since I bothered to look at it, and the “cartridge” parent node doesn't match what I'm doing with trying to rename “cartridge” to “game” in manifests. And indeed, the idea of requiring a root node is rather superfluous for a cheat code file. Current format looks like this: cheat description: foo code: 7e2000=20+7e2001=30?40 enabled cheat description: bar code: 7e4000=80 Open to discussing this, and I'd like to sync up with Snes9X before they push out a new release, and I'll agree to finalize and never change this format again. I chose to use .cht for the extension when using game files (eg gamename.cht)
This commit is contained in:
parent
8c337d4ac6
commit
77ac5f9e88
|
@ -12,7 +12,7 @@ using namespace nall;
|
|||
|
||||
namespace Emulator {
|
||||
static const string Name = "higan";
|
||||
static const string Version = "106.34";
|
||||
static const string Version = "106.35";
|
||||
static const string Author = "byuu";
|
||||
static const string License = "GPLv3";
|
||||
static const string Website = "https://byuu.org/";
|
||||
|
|
|
@ -89,7 +89,7 @@ private:
|
|||
auto loadSPC7110(Markup::Node) -> void;
|
||||
auto loadSDD1(Markup::Node) -> void;
|
||||
auto loadOBC1(Markup::Node) -> void;
|
||||
auto loadMSU1(Markup::Node) -> void;
|
||||
auto loadMSU1() -> void;
|
||||
|
||||
//save.cpp
|
||||
auto saveCartridge(Markup::Node) -> void;
|
||||
|
|
|
@ -67,7 +67,8 @@ auto Cartridge::loadCartridge(Markup::Node node) -> void {
|
|||
if(auto node = board["processor(identifier=SPC7110)"]) loadSPC7110(node);
|
||||
if(auto node = board["processor(identifier=SDD1)"]) loadSDD1(node);
|
||||
if(auto node = board["processor(identifier=OBC1)"]) loadOBC1(node);
|
||||
if(auto node = board["processor(identifier=MSU1)"]) loadMSU1(node);
|
||||
|
||||
if(auto fp = platform->open(pathID(), "msu1/data.rom", File::Read)) loadMSU1();
|
||||
}
|
||||
|
||||
auto Cartridge::loadCartridgeGameBoy(Markup::Node node) -> void {
|
||||
|
@ -642,11 +643,9 @@ auto Cartridge::loadOBC1(Markup::Node node) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
//processor(identifier=MSU1)
|
||||
auto Cartridge::loadMSU1(Markup::Node node) -> void {
|
||||
//file::exists("msu1/data.rom")
|
||||
auto Cartridge::loadMSU1() -> void {
|
||||
has.MSU1 = true;
|
||||
|
||||
for(auto map : node.find("map")) {
|
||||
loadMap(map, {&MSU1::readIO, &msu1}, {&MSU1::writeIO, &msu1});
|
||||
}
|
||||
bus.map({&MSU1::readIO, &msu1}, {&MSU1::writeIO, &msu1}, "00-3f,80-bf:2000-2007");
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ auto MSU1::Enter() -> void {
|
|||
}
|
||||
|
||||
auto MSU1::main() -> void {
|
||||
double left = 0.0;
|
||||
double left = 0.0;
|
||||
double right = 0.0;
|
||||
|
||||
if(io.audioPlay) {
|
||||
|
@ -72,9 +72,7 @@ auto MSU1::power() -> void {
|
|||
|
||||
auto MSU1::dataOpen() -> void {
|
||||
dataFile.reset();
|
||||
auto document = Markup::Node(); //todo: fix this
|
||||
string name = document["board/msu1/rom/name"].text();
|
||||
if(!name) name = "msu1.rom";
|
||||
string name = {"msu1/data.rom"};
|
||||
if(dataFile = platform->open(ID::SuperFamicom, name, File::Read)) {
|
||||
dataFile->seek(io.dataReadOffset);
|
||||
}
|
||||
|
@ -82,13 +80,7 @@ auto MSU1::dataOpen() -> void {
|
|||
|
||||
auto MSU1::audioOpen() -> void {
|
||||
audioFile.reset();
|
||||
auto document = Markup::Node(); //todo: fix this
|
||||
string name = {"track-", io.audioTrack, ".pcm"};
|
||||
for(auto track : document.find("board/msu1/track")) {
|
||||
if(track["number"].natural() != io.audioTrack) continue;
|
||||
name = track["name"].text();
|
||||
break;
|
||||
}
|
||||
string name = {"msu1/track-", io.audioTrack, ".pcm"};
|
||||
if(audioFile = platform->open(ID::SuperFamicom, name, File::Read)) {
|
||||
if(audioFile->size() >= 8) {
|
||||
uint32 header = audioFile->readm(4);
|
||||
|
@ -164,7 +156,7 @@ auto MSU1::writeIO(uint24 addr, uint8 data) -> void {
|
|||
if(io.audioError) break;
|
||||
io.audioPlay = data.bit(0);
|
||||
io.audioRepeat = data.bit(1);
|
||||
bool audioResume = data.bit(2);
|
||||
boolean audioResume = data.bit(2);
|
||||
if(!io.audioPlay && audioResume) {
|
||||
io.audioResumeTrack = io.audioTrack;
|
||||
io.audioResumeOffset = io.audioPlayOffset;
|
||||
|
|
|
@ -40,11 +40,11 @@ private:
|
|||
uint32 audioResumeTrack;
|
||||
uint32 audioResumeOffset;
|
||||
|
||||
bool audioError;
|
||||
bool audioPlay;
|
||||
bool audioRepeat;
|
||||
bool audioBusy;
|
||||
bool dataBusy;
|
||||
boolean audioError;
|
||||
boolean audioPlay;
|
||||
boolean audioRepeat;
|
||||
boolean audioBusy;
|
||||
boolean dataBusy;
|
||||
} io;
|
||||
};
|
||||
|
||||
|
|
|
@ -13,11 +13,11 @@ auto MSU1::serialize(serializer& s) -> void {
|
|||
s.integer(io.audioResumeTrack);
|
||||
s.integer(io.audioResumeOffset);
|
||||
|
||||
s.integer(io.audioError);
|
||||
s.integer(io.audioPlay);
|
||||
s.integer(io.audioRepeat);
|
||||
s.integer(io.audioBusy);
|
||||
s.integer(io.dataBusy);
|
||||
s.boolean(io.audioError);
|
||||
s.boolean(io.audioPlay);
|
||||
s.boolean(io.audioRepeat);
|
||||
s.boolean(io.audioBusy);
|
||||
s.boolean(io.dataBusy);
|
||||
|
||||
dataOpen();
|
||||
audioOpen();
|
||||
|
|
|
@ -120,9 +120,9 @@ auto Interface::title() -> string {
|
|||
auto Interface::videoInformation() -> VideoInformation {
|
||||
VideoInformation vi;
|
||||
vi.width = 256;
|
||||
vi.height = 240;
|
||||
vi.height = 239;
|
||||
vi.internalWidth = 512;
|
||||
vi.internalHeight = 480;
|
||||
vi.internalHeight = 478;
|
||||
vi.aspectCorrection = 8.0 / 7.0;
|
||||
if(Region::NTSC()) vi.refreshRate = system.cpuFrequency() / (262.0 * 1364.0);
|
||||
if(Region::PAL()) vi.refreshRate = system.cpuFrequency() / (312.0 * 1364.0);
|
||||
|
@ -249,12 +249,11 @@ auto Interface::get(const string& name) -> any {
|
|||
auto Interface::set(const string& name, const any& value) -> bool {
|
||||
if(name == "Blur Emulation" && value.is<bool>()) {
|
||||
settings.blurEmulation = value.get<bool>();
|
||||
system.configureVideoEffects();
|
||||
return true;
|
||||
}
|
||||
if(name == "Color Emulation" && value.is<bool>()) {
|
||||
settings.colorEmulation = value.get<bool>();
|
||||
system.configureVideoPalette();
|
||||
Emulator::video.setPalette();
|
||||
return true;
|
||||
}
|
||||
if(name == "Scanline Emulation" && value.is<bool>()) return settings.scanlineEmulation = value.get<bool>(), true;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
auto PPU::Line::renderBackground(PPU::IO::Background& self, uint source) -> void {
|
||||
if(io.displayDisable) return;
|
||||
if(!self.aboveEnable && !self.belowEnable) return;
|
||||
if(self.tileMode == TileMode::Mode7) return renderMode7(self, source);
|
||||
if(self.tileMode == TileMode::Inactive) return;
|
||||
|
|
|
@ -3,7 +3,7 @@ uint PPU::Line::count = 0;
|
|||
|
||||
auto PPU::Line::flush() -> void {
|
||||
if(Line::count) {
|
||||
#pragma omp parallel for
|
||||
#pragma omp parallel for if(Line::count >= 8)
|
||||
for(uint y = 0; y < Line::count; y++) {
|
||||
ppu.lines[Line::start + y].render();
|
||||
}
|
||||
|
@ -13,15 +13,21 @@ auto PPU::Line::flush() -> void {
|
|||
}
|
||||
|
||||
auto PPU::Line::render() -> void {
|
||||
bool hires = io.pseudoHires || io.bgMode == 5 || io.bgMode == 6;
|
||||
auto output = ppu.output + y * 1024;
|
||||
if(ppu.interlace() && ppu.field()) output += 512;
|
||||
auto width = !ppu.hires() ? 256 : 512;
|
||||
|
||||
if(!io.displayDisable) {
|
||||
auto aboveColor = cgram[0];
|
||||
auto belowColor = hires ? cgram[0] : io.col.fixedColor;
|
||||
for(uint x : range(256)) {
|
||||
above[x] = {Source::COL, 0, aboveColor};
|
||||
below[x] = {Source::COL, 0, belowColor};
|
||||
}
|
||||
if(io.displayDisable) {
|
||||
memory::fill<uint32>(output, width);
|
||||
return;
|
||||
}
|
||||
|
||||
bool hires = io.pseudoHires || io.bgMode == 5 || io.bgMode == 6;
|
||||
auto aboveColor = cgram[0];
|
||||
auto belowColor = hires ? cgram[0] : io.col.fixedColor;
|
||||
for(uint x : range(256)) {
|
||||
above[x] = {Source::COL, 0, aboveColor};
|
||||
below[x] = {Source::COL, 0, belowColor};
|
||||
}
|
||||
|
||||
renderBackground(io.bg1, Source::BG1);
|
||||
|
@ -29,20 +35,10 @@ auto PPU::Line::render() -> void {
|
|||
renderBackground(io.bg3, Source::BG3);
|
||||
renderBackground(io.bg4, Source::BG4);
|
||||
renderObject(io.obj);
|
||||
|
||||
auto output = ppu.output + y * 1024;
|
||||
if(ppu.interlace() && ppu.field()) output += 512;
|
||||
auto width = !ppu.hires() ? 256 : 512;
|
||||
auto luma = io.displayBrightness << 15;
|
||||
|
||||
if(io.displayDisable) {
|
||||
for(uint x : range(width)) output[x] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
renderWindow(io.col.window, io.col.window.aboveMask, windowAbove);
|
||||
renderWindow(io.col.window, io.col.window.belowMask, windowBelow);
|
||||
|
||||
auto luma = io.displayBrightness << 15;
|
||||
if(width == 256) for(uint x : range(width)) {
|
||||
*output++ = luma | pixel(x, above[x], below[x]);
|
||||
} else if(!hires) for(uint x : range(256)) {
|
||||
|
@ -83,10 +79,13 @@ auto PPU::Line::blend(uint x, uint y, bool halve) const -> uint15 {
|
|||
}
|
||||
}
|
||||
|
||||
auto PPU::Line::directColor(uint palette, uint tile) const -> uint15 {
|
||||
return (palette << 7 & 0x6000) + (tile >> 0 & 0x1000)
|
||||
+ (palette << 4 & 0x0380) + (tile >> 5 & 0x0040)
|
||||
+ (palette << 2 & 0x001c) + (tile >> 9 & 0x0002);
|
||||
auto PPU::Line::directColor(uint paletteIndex, uint paletteColor) const -> uint15 {
|
||||
//paletteIndex = bgr
|
||||
//paletteColor = BBGGGRRR
|
||||
//output = 0 BBb00 GGGg0 RRRr0
|
||||
return (paletteColor << 2 & 0x001c) + (paletteIndex << 1 & 0x0002) //R
|
||||
+ (paletteColor << 4 & 0x0380) + (paletteIndex << 5 & 0x0040) //G
|
||||
+ (paletteColor << 7 & 0x6000) + (paletteIndex << 10 & 0x1000); //B
|
||||
}
|
||||
|
||||
auto PPU::Line::plotAbove(uint x, uint source, uint priority, uint color) -> void {
|
||||
|
|
|
@ -51,7 +51,7 @@ auto PPU::Line::renderMode7(PPU::IO::Background& self, uint source) -> void {
|
|||
mosaicCounter = 1 + io.mosaicSize;
|
||||
mosaicPalette = palette;
|
||||
mosaicPriority = priority;
|
||||
if(io.col.directColor) {
|
||||
if(io.col.directColor && source == Source::BG1) {
|
||||
mosaicColor = directColor(0, palette);
|
||||
} else {
|
||||
mosaicColor = cgram[palette];
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
auto PPU::Line::renderObject(PPU::IO::Object& self) -> void {
|
||||
if(io.displayDisable) return;
|
||||
if(!self.aboveEnable && !self.belowEnable) return;
|
||||
|
||||
bool windowAbove[256];
|
||||
|
|
|
@ -44,9 +44,13 @@ auto PPU::main() -> void {
|
|||
scanline();
|
||||
uint y = vcounter();
|
||||
step(512);
|
||||
if(y >= 1 && y <= vdisp()) {
|
||||
memcpy(&lines[y].io, &io, sizeof(io));
|
||||
memcpy(&lines[y].cgram, &cgram, sizeof(cgram));
|
||||
if(y >= 1 && y <= 239) {
|
||||
if(io.displayDisable || y >= vdisp()) {
|
||||
lines[y].io.displayDisable = true;
|
||||
} else {
|
||||
memcpy(&lines[y].io, &io, sizeof(io));
|
||||
memcpy(&lines[y].cgram, &cgram, sizeof(cgram));
|
||||
}
|
||||
if(!Line::count) Line::start = y;
|
||||
Line::count++;
|
||||
}
|
||||
|
@ -78,13 +82,12 @@ auto PPU::scanline() -> void {
|
|||
|
||||
auto PPU::refresh() -> void {
|
||||
auto output = this->output;
|
||||
if(!overscan()) output -= 14 * 512;
|
||||
if(!overscan()) output -= 12 * 512;
|
||||
auto pitch = 512 << !interlace();
|
||||
auto width = 256 << hires();
|
||||
auto height = 240 << interlace();
|
||||
if(!hires()) Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, false);
|
||||
auto height = 239 << interlace();
|
||||
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation && hires());
|
||||
Emulator::video.refresh(output, pitch * sizeof(uint32), width, height);
|
||||
if(!hires()) Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
|
||||
}
|
||||
|
||||
auto PPU::load(Markup::Node node) -> bool {
|
||||
|
@ -94,7 +97,7 @@ auto PPU::load(Markup::Node node) -> bool {
|
|||
auto PPU::power(bool reset) -> void {
|
||||
create(Enter, system.cpuFrequency());
|
||||
PPUcounter::reset();
|
||||
memory::fill<uint32>(output, 512 * 480);
|
||||
memory::fill<uint32>(output, 512 * 478);
|
||||
|
||||
function<auto (uint24, uint8) -> uint8> reader{&PPU::readIO, this};
|
||||
function<auto (uint24, uint8) -> void> writer{&PPU::writeIO, this};
|
||||
|
|
|
@ -263,7 +263,7 @@ public:
|
|||
auto render() -> void;
|
||||
auto pixel(uint x, Pixel above, Pixel below) const -> uint15;
|
||||
auto blend(uint x, uint y, bool halve) const -> uint15;
|
||||
alwaysinline auto directColor(uint palette, uint tile) const -> uint15;
|
||||
alwaysinline auto directColor(uint paletteIndex, uint paletteColor) const -> uint15;
|
||||
alwaysinline auto plotAbove(uint x, uint source, uint priority, uint color) -> void;
|
||||
alwaysinline auto plotBelow(uint x, uint source, uint priority, uint color) -> void;
|
||||
|
||||
|
|
|
@ -72,11 +72,9 @@ auto PPU::main() -> void {
|
|||
|
||||
step(14);
|
||||
obj.tilefetch();
|
||||
} else {
|
||||
step(1052 + 14 + 136);
|
||||
}
|
||||
|
||||
step(lineclocks() - 28 - 1052 - 14 - 136);
|
||||
step(lineclocks() - hcounter());
|
||||
}
|
||||
|
||||
auto PPU::load(Markup::Node node) -> bool {
|
||||
|
@ -223,10 +221,11 @@ auto PPU::frame() -> void {
|
|||
|
||||
auto PPU::refresh() -> void {
|
||||
auto output = this->output;
|
||||
if(!overscan()) output -= 14 * 512;
|
||||
if(!overscan()) output -= 12 * 512;
|
||||
auto pitch = 512;
|
||||
auto width = 512;
|
||||
auto height = 480;
|
||||
auto height = 478;
|
||||
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
|
||||
Emulator::video.refresh(output, pitch * sizeof(uint32), width, height);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ System system;
|
|||
Scheduler scheduler;
|
||||
Random random;
|
||||
Cheat cheat;
|
||||
#include "video.cpp"
|
||||
#include "serialization.cpp"
|
||||
|
||||
auto System::run() -> void {
|
||||
|
@ -91,8 +90,7 @@ auto System::unload() -> void {
|
|||
auto System::power(bool reset) -> void {
|
||||
Emulator::video.reset();
|
||||
Emulator::video.setInterface(interface);
|
||||
configureVideoPalette();
|
||||
configureVideoEffects();
|
||||
Emulator::video.setPalette();
|
||||
|
||||
Emulator::audio.reset();
|
||||
Emulator::audio.setInterface(interface);
|
||||
|
|
|
@ -14,10 +14,6 @@ struct System {
|
|||
auto unload() -> void;
|
||||
auto power(bool reset) -> void;
|
||||
|
||||
//video.cpp
|
||||
auto configureVideoPalette() -> void;
|
||||
auto configureVideoEffects() -> void;
|
||||
|
||||
//serialization.cpp
|
||||
auto serialize() -> serializer;
|
||||
auto unserialize(serializer&) -> bool;
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
auto System::configureVideoPalette() -> void {
|
||||
Emulator::video.setPalette();
|
||||
}
|
||||
|
||||
auto System::configureVideoEffects() -> void {
|
||||
Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
|
||||
}
|
|
@ -5,7 +5,8 @@ include sfc/GNUmakefile
|
|||
include gb/GNUmakefile
|
||||
include processor/GNUmakefile
|
||||
|
||||
ui_objects := ui-bsnes ui-program ui-input ui-presentation ui-settings ui-resource
|
||||
ui_objects := ui-bsnes ui-program ui-input ui-presentation
|
||||
ui_objects += ui-settings ui-tools ui-resource
|
||||
ui_objects += ruby hiro
|
||||
ui_objects += $(if $(call streq,$(platform),windows),ui-windows)
|
||||
|
||||
|
@ -51,6 +52,7 @@ obj/ui-program.o: $(ui)/program/program.cpp
|
|||
obj/ui-input.o: $(ui)/input/input.cpp
|
||||
obj/ui-presentation.o: $(ui)/presentation/presentation.cpp
|
||||
obj/ui-settings.o: $(ui)/settings/settings.cpp
|
||||
obj/ui-tools.o: $(ui)/tools/tools.cpp
|
||||
obj/ui-resource.o: $(ui)/resource/resource.cpp
|
||||
|
||||
obj/ui-windows.o:
|
||||
|
|
|
@ -15,6 +15,7 @@ extern unique_pointer<Emulator::Interface> emulator;
|
|||
#include "input/input.hpp"
|
||||
#include "presentation/presentation.hpp"
|
||||
#include "settings/settings.hpp"
|
||||
#include "tools/tools.hpp"
|
||||
#include "resource/resource.hpp"
|
||||
|
||||
auto locate(string name) -> string;
|
||||
|
|
|
@ -20,15 +20,18 @@ Presentation::Presentation() {
|
|||
});
|
||||
controllerPort1.setText("Controller Port 1");
|
||||
controllerPort2.setText("Controller Port 2");
|
||||
expansionPort.setText("Expansion Port");
|
||||
for(auto& port : emulator->ports) {
|
||||
Menu* menu = nullptr;
|
||||
if(port.name == "Controller Port 1") menu = &controllerPort1;
|
||||
if(port.name == "Controller Port 2") menu = &controllerPort2;
|
||||
if(port.name == "Expansion Port") menu = &expansionPort;
|
||||
if(!menu) continue;
|
||||
|
||||
Group devices;
|
||||
for(auto& device : port.devices) {
|
||||
if(device.name == "None") continue;
|
||||
if(port.name != "Expansion Port" && device.name == "None") continue;
|
||||
if(port.name == "Expansion Port" && device.name == "21fx") continue;
|
||||
MenuRadioItem item{menu};
|
||||
item.setText(device.name).onActivate([=] {
|
||||
auto path = string{"Emulator/", port.name}.replace(" ", "");
|
||||
|
@ -101,14 +104,14 @@ Presentation::Presentation() {
|
|||
pathSettings.setText("Paths ...").onActivate([&] { settingsWindow->show(2); });
|
||||
advancedSettings.setText("Advanced ...").onActivate([&] { settingsWindow->show(3); });
|
||||
|
||||
toolsMenu.setText("Tools");
|
||||
saveState.setText("Save State").setEnabled(false);
|
||||
toolsMenu.setText("Tools").setVisible(false);
|
||||
saveState.setText("Save State");
|
||||
saveState1.setText("Slot 1").onActivate([&] { program->saveState(1); });
|
||||
saveState2.setText("Slot 2").onActivate([&] { program->saveState(2); });
|
||||
saveState3.setText("Slot 3").onActivate([&] { program->saveState(3); });
|
||||
saveState4.setText("Slot 4").onActivate([&] { program->saveState(4); });
|
||||
saveState5.setText("Slot 5").onActivate([&] { program->saveState(5); });
|
||||
loadState.setText("Load State").setEnabled(false);
|
||||
loadState.setText("Load State");
|
||||
loadState1.setText("Slot 1").onActivate([&] { program->loadState(1); });
|
||||
loadState2.setText("Slot 2").onActivate([&] { program->loadState(2); });
|
||||
loadState3.setText("Slot 3").onActivate([&] { program->loadState(3); });
|
||||
|
@ -117,6 +120,7 @@ Presentation::Presentation() {
|
|||
pauseEmulation.setText("Pause Emulation").onToggle([&] {
|
||||
if(pauseEmulation.checked()) audio->clear();
|
||||
});
|
||||
cheatEditor.setText("Cheat Editor ...").onActivate([&] { toolsWindow->show(0); });
|
||||
|
||||
helpMenu.setText("Help");
|
||||
about.setText("About ...").onActivate([&] {
|
||||
|
@ -212,8 +216,8 @@ auto Presentation::resizeViewport() -> void {
|
|||
return clearViewport();
|
||||
}
|
||||
|
||||
double width = 224 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
||||
double height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0);
|
||||
uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
||||
uint height = (settings["View/OverscanCropping"].boolean() ? 223.0 : 239.0);
|
||||
|
||||
if(settings["View/IntegralScaling"].boolean()) {
|
||||
uint widthMultiplier = windowWidth / width;
|
||||
|
@ -226,8 +230,8 @@ auto Presentation::resizeViewport() -> void {
|
|||
viewportWidth, viewportHeight
|
||||
});
|
||||
} else {
|
||||
double widthMultiplier = windowWidth / width;
|
||||
double heightMultiplier = windowHeight / height;
|
||||
double widthMultiplier = (double)windowWidth / width;
|
||||
double heightMultiplier = (double)windowHeight / height;
|
||||
double multiplier = min(widthMultiplier, heightMultiplier);
|
||||
uint viewportWidth = width * multiplier;
|
||||
uint viewportHeight = height * multiplier;
|
||||
|
@ -241,8 +245,8 @@ auto Presentation::resizeViewport() -> void {
|
|||
}
|
||||
|
||||
auto Presentation::resizeWindow() -> void {
|
||||
double width = 224 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
||||
double height = (settings["View/OverscanCropping"].boolean() ? 224.0 : 240.0);
|
||||
uint width = 256 * (settings["View/AspectCorrection"].boolean() ? 8.0 / 7.0 : 1.0);
|
||||
uint height = (settings["View/OverscanCropping"].boolean() ? 223.0 : 239.0);
|
||||
|
||||
uint multiplier = 2;
|
||||
if(settings["View/Size"].text() == "Small" ) multiplier = 2;
|
||||
|
|
|
@ -29,6 +29,7 @@ struct Presentation : Window {
|
|||
MenuSeparator portSeparator{&systemMenu};
|
||||
Menu controllerPort1{&systemMenu};
|
||||
Menu controllerPort2{&systemMenu};
|
||||
Menu expansionPort{&systemMenu};
|
||||
MenuSeparator quitSeparator{&systemMenu};
|
||||
MenuItem quit{&systemMenu};
|
||||
Menu settingsMenu{&menuBar};
|
||||
|
@ -64,6 +65,8 @@ struct Presentation : Window {
|
|||
MenuItem loadState4{&loadState};
|
||||
MenuItem loadState5{&loadState};
|
||||
MenuCheckItem pauseEmulation{&toolsMenu};
|
||||
MenuSeparator toolsSeparator{&toolsMenu};
|
||||
MenuItem cheatEditor{&toolsMenu};
|
||||
Menu helpMenu{&menuBar};
|
||||
MenuItem about{&helpMenu};
|
||||
|
||||
|
|
|
@ -12,9 +12,10 @@ auto Program::load() -> void {
|
|||
presentation->setTitle(emulator->title());
|
||||
presentation->resetSystem.setEnabled(true);
|
||||
presentation->unloadGame.setEnabled(true);
|
||||
presentation->saveState.setEnabled(true);
|
||||
presentation->loadState.setEnabled(true);
|
||||
presentation->toolsMenu.setVisible(true);
|
||||
presentation->pauseEmulation.setChecked(false);
|
||||
presentation->resizeViewport();
|
||||
toolsWindow->cheatEditor.loadCheats();
|
||||
|
||||
string locations = superNintendo.location;
|
||||
if(auto location = gameBoy.location) locations.append("|", location);
|
||||
|
@ -93,6 +94,8 @@ auto Program::save() -> void {
|
|||
|
||||
auto Program::unload() -> void {
|
||||
if(!emulator->loaded()) return;
|
||||
toolsWindow->cheatEditor.saveCheats();
|
||||
toolsWindow->setVisible(false);
|
||||
emulator->unload();
|
||||
superNintendo = {};
|
||||
gameBoy = {};
|
||||
|
@ -102,7 +105,6 @@ auto Program::unload() -> void {
|
|||
presentation->setTitle({"bsnes v", Emulator::Version});
|
||||
presentation->resetSystem.setEnabled(false);
|
||||
presentation->unloadGame.setEnabled(false);
|
||||
presentation->saveState.setEnabled(false);
|
||||
presentation->loadState.setEnabled(false);
|
||||
presentation->toolsMenu.setVisible(false);
|
||||
presentation->clearViewport();
|
||||
}
|
||||
|
|
|
@ -146,6 +146,15 @@ auto Program::open(uint id, string name, vfs::file::mode mode, bool required) ->
|
|||
return vfs::fs::file::open(path("Saves", superNintendo.location, ".srm"), mode);
|
||||
}
|
||||
|
||||
if(id == 1 && name == "msu1/data.rom") {
|
||||
return vfs::fs::file::open({Location::notsuffix(superNintendo.location), ".msu"}, mode);
|
||||
}
|
||||
|
||||
if(id == 1 && name.match("msu1/track-*.pcm")) {
|
||||
name.trimLeft("msu1/track-", 1L);
|
||||
return vfs::fs::file::open({Location::notsuffix(superNintendo.location), name}, mode);
|
||||
}
|
||||
|
||||
//Game Boy
|
||||
|
||||
if(id == 2 && name == "manifest.bml" && mode == vfs::file::mode::read) {
|
||||
|
@ -211,8 +220,8 @@ auto Program::videoRefresh(const uint32* data, uint pitch, uint width, uint heig
|
|||
|
||||
pitch >>= 2;
|
||||
if(presentation->overscanCropping.checked()) {
|
||||
if(height == 240) data += 8 * pitch, height -= 16;
|
||||
if(height == 480) data += 16 * pitch, height -= 32;
|
||||
if(height == 239) data += 8 * pitch, height -= 16;
|
||||
if(height == 478) data += 16 * pitch, height -= 32;
|
||||
}
|
||||
|
||||
if(video->lock(output, length, width, height)) {
|
||||
|
|
|
@ -56,6 +56,8 @@ Program::Program(string_vector arguments) {
|
|||
|
||||
new InputManager;
|
||||
new SettingsWindow;
|
||||
new CheatDatabase;
|
||||
new ToolsWindow;
|
||||
new AboutWindow;
|
||||
|
||||
arguments.takeLeft(); //ignore program location in argument parsing
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
CheatDatabase::CheatDatabase() {
|
||||
cheatDatabase = this;
|
||||
|
||||
layout.setMargin(5);
|
||||
selectAllButton.setText("Select All").onActivate([&] {
|
||||
for(auto item : cheatList.items()) item.setChecked(true);
|
||||
});
|
||||
unselectAllButton.setText("Unselect All").onActivate([&] {
|
||||
for(auto item : cheatList.items()) item.setChecked(false);
|
||||
});
|
||||
addCheatsButton.setText("Add Cheats").onActivate([&] {
|
||||
addCheats();
|
||||
});
|
||||
|
||||
setSize({800, 400});
|
||||
setAlignment({0.5, 1.0});
|
||||
setDismissable();
|
||||
}
|
||||
|
||||
auto CheatDatabase::findCheats() -> void {
|
||||
auto sha256 = emulator->sha256();
|
||||
|
||||
auto document = BML::unserialize(string::read(locate("cheats.bml")));
|
||||
for(auto game : document.find("cartridge")) {
|
||||
if(game["sha256"].text() != sha256) continue;
|
||||
|
||||
cheatList.reset();
|
||||
for(auto cheat : game.find("cheat")) {
|
||||
cheatList.append(ListViewItem()
|
||||
.setCheckable()
|
||||
.setText(cheat["description"].text())
|
||||
.setProperty("code", cheat["code"].text())
|
||||
);
|
||||
}
|
||||
|
||||
setTitle(game["name"].text());
|
||||
setVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
MessageDialog().setParent(*toolsWindow).setText("Sorry, no cheats were found for this game.").information();
|
||||
}
|
||||
|
||||
auto CheatDatabase::addCheats() -> void {
|
||||
for(auto item : cheatList.items()) {
|
||||
if(!item.checked()) continue;
|
||||
|
||||
string code = item.property("code").replace("/", "=", 1L).replace("/", "?", 1L);
|
||||
string description = item.text();
|
||||
if(!toolsWindow->cheatEditor.addCode(false, code, description)) {
|
||||
MessageDialog().setParent(*this).setText("Free slots exhausted. Not all cheats could be added.").warning();
|
||||
break;
|
||||
}
|
||||
}
|
||||
setVisible(false);
|
||||
toolsWindow->cheatEditor.doRefresh();
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
CheatEditor::CheatEditor(TabFrame* parent) : TabFrameItem(parent) {
|
||||
setIcon(Icon::Edit::Replace);
|
||||
setText("Cheat Editor");
|
||||
|
||||
layout.setMargin(5);
|
||||
cheatList.append(TableViewHeader().setVisible()
|
||||
.append(TableViewColumn())
|
||||
.append(TableViewColumn().setText("Slot").setForegroundColor({0, 128, 0}).setAlignment(1.0))
|
||||
.append(TableViewColumn().setText("Code(s)"))
|
||||
.append(TableViewColumn().setText("Description").setExpandable())
|
||||
);
|
||||
for(uint slot : range(Slots)) {
|
||||
cheatList.append(TableViewItem()
|
||||
.append(TableViewCell().setCheckable())
|
||||
.append(TableViewCell().setText(1 + slot))
|
||||
.append(TableViewCell())
|
||||
.append(TableViewCell())
|
||||
);
|
||||
}
|
||||
cheatList.onChange([&] { doChangeSelected(); });
|
||||
cheatList.onToggle([&](auto cell) {
|
||||
cheats[cell.parent().offset()].enabled = cell.checked();
|
||||
this->synchronizeCodes();
|
||||
});
|
||||
codeLabel.setText("Code(s):");
|
||||
codeValue.setEnabled(false).onChange([&] { doModify(); });
|
||||
descriptionLabel.setText("Description:");
|
||||
descriptionValue.setEnabled(false).onChange([&] { doModify(); });
|
||||
findCodesButton.setText("Find Codes ...").onActivate([&] { cheatDatabase->findCheats(); });
|
||||
resetButton.setText("Reset").onActivate([&] { doReset(); });
|
||||
eraseButton.setText("Erase").onActivate([&] { doErase(); });
|
||||
|
||||
//do not display "Find Codes" button if there is no cheat database to look up codes in
|
||||
if(!file::exists(locate("cheats.bml"))) findCodesButton.setVisible(false);
|
||||
}
|
||||
|
||||
auto CheatEditor::doChangeSelected() -> void {
|
||||
if(auto item = cheatList.selected()) {
|
||||
auto& cheat = cheats[item.offset()];
|
||||
codeValue.setEnabled(true).setText(cheat.code);
|
||||
descriptionValue.setEnabled(true).setText(cheat.description);
|
||||
eraseButton.setEnabled(true);
|
||||
} else {
|
||||
codeValue.setEnabled(false).setText("");
|
||||
descriptionValue.setEnabled(false).setText("");
|
||||
eraseButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
auto CheatEditor::doModify() -> void {
|
||||
if(auto item = cheatList.selected()) {
|
||||
auto& cheat = cheats[item.offset()];
|
||||
cheat.code = codeValue.text();
|
||||
cheat.description = descriptionValue.text();
|
||||
doRefresh();
|
||||
synchronizeCodes();
|
||||
}
|
||||
}
|
||||
|
||||
auto CheatEditor::doRefresh() -> void {
|
||||
for(uint slot : range(Slots)) {
|
||||
auto& cheat = cheats[slot];
|
||||
if(cheat.code || cheat.description) {
|
||||
auto codes = cheat.code.split("+");
|
||||
if(codes.size() > 1) codes[0].append("+...");
|
||||
cheatList.item(slot).cell(0).setChecked(cheat.enabled);
|
||||
cheatList.item(slot).cell(2).setText(codes[0]);
|
||||
cheatList.item(slot).cell(3).setText(cheat.description).setForegroundColor({0, 0, 0});
|
||||
} else {
|
||||
cheatList.item(slot).cell(0).setChecked(false);
|
||||
cheatList.item(slot).cell(2).setText("");
|
||||
cheatList.item(slot).cell(3).setText("<empty>").setForegroundColor({128, 128, 128});
|
||||
}
|
||||
}
|
||||
cheatList.resizeColumns();
|
||||
}
|
||||
|
||||
auto CheatEditor::doReset(bool force) -> void {
|
||||
if(force || MessageDialog().setParent(*toolsWindow).setText("Permamently erase all cheats?").question() == "Yes") {
|
||||
for(auto& cheat : cheats) cheat = {};
|
||||
for(auto& item : cheatList.items()) item.cell(0).setChecked(false);
|
||||
doChangeSelected();
|
||||
doRefresh();
|
||||
synchronizeCodes();
|
||||
}
|
||||
}
|
||||
|
||||
auto CheatEditor::doErase() -> void {
|
||||
if(auto item = cheatList.selected()) {
|
||||
cheats[item.offset()] = {};
|
||||
codeValue.setText("");
|
||||
descriptionValue.setText("");
|
||||
doRefresh();
|
||||
synchronizeCodes();
|
||||
}
|
||||
}
|
||||
|
||||
auto CheatEditor::synchronizeCodes() -> void {
|
||||
string_vector codes;
|
||||
for(auto& cheat : cheats) {
|
||||
if(!cheat.enabled || !cheat.code) continue;
|
||||
codes.append(cheat.code);
|
||||
}
|
||||
emulator->cheatSet(codes);
|
||||
}
|
||||
|
||||
auto CheatEditor::addCode(bool enabled, string code, string description) -> bool {
|
||||
for(auto& cheat : cheats) {
|
||||
if(cheat.code || cheat.description) continue;
|
||||
cheat.enabled = enabled;
|
||||
cheat.code = code;
|
||||
cheat.description = description;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto CheatEditor::loadCheats() -> void {
|
||||
doReset(true);
|
||||
auto location = program->path("Cheats", program->superNintendo.location, ".cht");
|
||||
auto document = BML::unserialize(string::read(location));
|
||||
for(auto cheat : document.find("cheat")) {
|
||||
if(!addCode((bool)cheat["enabled"], cheat["code"].text(), cheat["description"].text())) break;
|
||||
}
|
||||
doRefresh();
|
||||
synchronizeCodes();
|
||||
}
|
||||
|
||||
auto CheatEditor::saveCheats() -> void {
|
||||
string document;
|
||||
for(auto& cheat : cheats) {
|
||||
if(!cheat.code && !cheat.description) continue;
|
||||
document.append("cheat\n");
|
||||
document.append(" description: ", cheat.description, "\n");
|
||||
document.append(" code: ", cheat.code, "\n");
|
||||
if(cheat.enabled)
|
||||
document.append(" enabled\n");
|
||||
document.append("\n");
|
||||
}
|
||||
auto location = program->path("Cheats", program->superNintendo.location, ".cht");
|
||||
if(document) {
|
||||
file::write(location, document);
|
||||
} else {
|
||||
file::remove(location);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
#include "../bsnes.hpp"
|
||||
#include "cheat-database.cpp"
|
||||
#include "cheat-editor.cpp"
|
||||
unique_pointer<CheatDatabase> cheatDatabase;
|
||||
unique_pointer<ToolsWindow> toolsWindow;
|
||||
|
||||
ToolsWindow::ToolsWindow() {
|
||||
toolsWindow = this;
|
||||
|
||||
layout.setMargin(5);
|
||||
|
||||
setTitle("Tools");
|
||||
setSize({600, 400});
|
||||
setAlignment({1.0, 1.0});
|
||||
setDismissable();
|
||||
|
||||
onSize([&] {
|
||||
cheatEditor.cheatList.resizeColumns();
|
||||
});
|
||||
}
|
||||
|
||||
auto ToolsWindow::setVisible(bool visible) -> ToolsWindow& {
|
||||
return Window::setVisible(visible), *this;
|
||||
}
|
||||
|
||||
auto ToolsWindow::show(uint index) -> void {
|
||||
panel.item(index)->setSelected();
|
||||
setVisible();
|
||||
setFocused();
|
||||
doSize();
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
struct CheatDatabase : Window {
|
||||
CheatDatabase();
|
||||
auto findCheats() -> void;
|
||||
auto addCheats() -> void;
|
||||
|
||||
public:
|
||||
VerticalLayout layout{this};
|
||||
ListView cheatList{&layout, Size{~0, ~0}};
|
||||
HorizontalLayout controlLayout{&layout, Size{~0, 0}};
|
||||
Button selectAllButton{&controlLayout, Size{100, 0}};
|
||||
Button unselectAllButton{&controlLayout, Size{100, 0}};
|
||||
Widget spacer{&controlLayout, Size{~0, 0}};
|
||||
Button addCheatsButton{&controlLayout, Size{100, 0}};
|
||||
};
|
||||
|
||||
struct CheatEditor : TabFrameItem {
|
||||
enum : uint { Slots = 128 };
|
||||
|
||||
CheatEditor(TabFrame*);
|
||||
auto doChangeSelected() -> void;
|
||||
auto doModify() -> void;
|
||||
auto doRefresh() -> void;
|
||||
auto doReset(bool force = false) -> void;
|
||||
auto doErase() -> void;
|
||||
auto synchronizeCodes() -> void;
|
||||
auto addCode(bool enabled, string code, string description) -> bool;
|
||||
auto loadCheats() -> void;
|
||||
auto saveCheats() -> void;
|
||||
|
||||
public:
|
||||
struct Cheat {
|
||||
bool enabled = false;
|
||||
string code;
|
||||
string description;
|
||||
};
|
||||
Cheat cheats[Slots];
|
||||
|
||||
VerticalLayout layout{this};
|
||||
TableView cheatList{&layout, Size{~0, ~0}};
|
||||
HorizontalLayout codeLayout{&layout, Size{~0, 0}};
|
||||
Label codeLabel{&codeLayout, Size{70, 0}};
|
||||
LineEdit codeValue{&codeLayout, Size{~0, 0}};
|
||||
HorizontalLayout descriptionLayout{&layout, Size{~0, 0}};
|
||||
Label descriptionLabel{&descriptionLayout, Size{70, 0}};
|
||||
LineEdit descriptionValue{&descriptionLayout, Size{~0, 0}};
|
||||
HorizontalLayout controlLayout{&layout, Size{~0, 0}};
|
||||
Button findCodesButton{&controlLayout, Size{120, 0}};
|
||||
Widget spacer{&controlLayout, Size{~0, 0}};
|
||||
Button resetButton{&controlLayout, Size{80, 0}};
|
||||
Button eraseButton{&controlLayout, Size{80, 0}};
|
||||
};
|
||||
|
||||
struct ToolsWindow : Window {
|
||||
ToolsWindow();
|
||||
auto setVisible(bool visible = true) -> ToolsWindow&;
|
||||
auto show(uint index) -> void;
|
||||
|
||||
public:
|
||||
VerticalLayout layout{this};
|
||||
TabFrame panel{&layout, Size{~0, ~0}};
|
||||
CheatEditor cheatEditor{&panel};
|
||||
};
|
||||
|
||||
extern unique_pointer<CheatDatabase> cheatDatabase;
|
||||
extern unique_pointer<ToolsWindow> toolsWindow;
|
|
@ -18,9 +18,9 @@ CheatEditor::CheatEditor(TabFrame* parent) : TabFrameItem(parent) {
|
|||
);
|
||||
}
|
||||
cheatList.onChange([&] { doChangeSelected(); });
|
||||
cheatList.onToggle([&](TableViewCell cell) {
|
||||
cheatList.onToggle([&](auto cell) {
|
||||
cheats[cell.parent().offset()].enabled = cell.checked();
|
||||
synchronizeCodes();
|
||||
this->synchronizeCodes();
|
||||
});
|
||||
codeLabel.setText("Code(s):");
|
||||
codeValue.onChange([&] { doModify(); });
|
||||
|
@ -69,7 +69,7 @@ auto CheatEditor::doRefresh() -> void {
|
|||
} else {
|
||||
cheatList.item(slot).cell(0).setChecked(false);
|
||||
cheatList.item(slot).cell(2).setText("");
|
||||
cheatList.item(slot).cell(3).setText("(empty)").setForegroundColor({128, 128, 128});
|
||||
cheatList.item(slot).cell(3).setText("<empty>").setForegroundColor({128, 128, 128});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,15 +77,9 @@ auto CheatEditor::doRefresh() -> void {
|
|||
}
|
||||
|
||||
auto CheatEditor::doReset(bool force) -> void {
|
||||
if(force || MessageDialog().setParent(*toolsManager).setText("Permanently erase all slots?").question() == "Yes") {
|
||||
for(auto& cheat : cheats) {
|
||||
cheat.enabled = false;
|
||||
cheat.code = "";
|
||||
cheat.description = "";
|
||||
}
|
||||
for(auto& item : cheatList.items()) {
|
||||
item.cell(0).setChecked(false);
|
||||
}
|
||||
if(force || MessageDialog().setParent(*toolsManager).setText("Permanently erase all cheats?").question() == "Yes") {
|
||||
for(auto& cheat : cheats) cheat = {};
|
||||
for(auto& item : cheatList.items()) item.cell(0).setChecked(false);
|
||||
doChangeSelected();
|
||||
doRefresh();
|
||||
synchronizeCodes();
|
||||
|
@ -95,9 +89,7 @@ auto CheatEditor::doReset(bool force) -> void {
|
|||
auto CheatEditor::doErase() -> void {
|
||||
if(auto item = cheatList.selected()) {
|
||||
auto& cheat = cheats[item.offset()];
|
||||
cheat.enabled = false;
|
||||
cheat.code = "";
|
||||
cheat.description = "";
|
||||
cheats[item.offset()] = {};
|
||||
codeValue.setText("");
|
||||
descriptionValue.setText("");
|
||||
doRefresh();
|
||||
|
|
|
@ -70,7 +70,7 @@ auto StateManager::doRefresh() -> void {
|
|||
description.resize(description.length());
|
||||
stateList.item(slot).cell(1).setText(description).setForegroundColor({0, 0, 0});
|
||||
} else {
|
||||
stateList.item(slot).cell(1).setText("(empty)").setForegroundColor({128, 128, 128});
|
||||
stateList.item(slot).cell(1).setText("<empty>").setForegroundColor({128, 128, 128});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ auto StateManager::doSave() -> void {
|
|||
}
|
||||
|
||||
auto StateManager::doReset() -> void {
|
||||
if(MessageDialog().setParent(*toolsManager).setText("Permanently erase all slots?").question() == "Yes") {
|
||||
if(MessageDialog().setParent(*toolsManager).setText("Permanently erase all states?").question() == "Yes") {
|
||||
for(auto slot : range(Slots)) file::remove(program->stateName(1 + slot, true));
|
||||
doRefresh();
|
||||
doUpdateControls();
|
||||
|
|
|
@ -144,7 +144,7 @@ struct InputKeyboardQuartz {
|
|||
|
||||
hid->setVendorID(HID::Keyboard::GenericVendorID);
|
||||
hid->setProductID(HID::Keyboard::GenericProductID);
|
||||
hid->setPath(0);
|
||||
hid->setPathID(0);
|
||||
for(auto& key : keys) {
|
||||
hid->buttons().append(key.name);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue