mirror of https://github.com/bsnes-emu/bsnes.git
Update to v094r10 release.
byuu says: This starts the tomoko UI. So far I have basic library loading and video+audio output. Basically just enough to take the below screenshot. (aside from Library, the menus are empty stubs.) The .sys (system) game folders are now going under ~/Emulation/System, to avoid needing root privileges to stick them into /usr/share. The game library now shows all bootable media types, and the drop-down subtype is gone. I'm going to display a separate modal dialog for loading slotted games this time around. Much cleaner this way, less clutter. tomoko's starting off a lot cleaner than ethos was, and I'm scaling back the number of abstracted classes. What was Utility, Interface, etc are now being merged all into Program. Of course, the real hell is the input system. That has so many layers of bullshit that there's really no sane way to write it.
This commit is contained in:
parent
a512d14628
commit
80c1c9c2ef
|
@ -6,7 +6,7 @@ gb := gb
|
||||||
gba := gba
|
gba := gba
|
||||||
|
|
||||||
profile := accuracy
|
profile := accuracy
|
||||||
target := higan
|
target := tomoko
|
||||||
|
|
||||||
# arch := x86
|
# arch := x86
|
||||||
# console := true
|
# console := true
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
namespace Emulator {
|
namespace Emulator {
|
||||||
static const char Name[] = "higan";
|
static const char Name[] = "higan";
|
||||||
static const char Version[] = "094.09";
|
static const char Version[] = "094.10";
|
||||||
static const char Author[] = "byuu";
|
static const char Author[] = "byuu";
|
||||||
static const char License[] = "GPLv3";
|
static const char License[] = "GPLv3";
|
||||||
static const char Website[] = "http://byuu.org/";
|
static const char Website[] = "http://byuu.org/";
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
higan
|
higan
|
||||||
|
tomoko
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
#include "higan.hpp"
|
|
||||||
|
|
||||||
struct Presentation : Window {
|
|
||||||
MenuBar menuBar{this};
|
|
||||||
Menu menuSystem{&menuBar};
|
|
||||||
StatusBar statusBar{this};
|
|
||||||
|
|
||||||
Presentation() {
|
|
||||||
menuSystem.setText("System");
|
|
||||||
onClose(&Application::quit);
|
|
||||||
|
|
||||||
setBackgroundColor({0, 0, 0});
|
|
||||||
setTitle({Emulator::Name, " v", Emulator::Version});
|
|
||||||
setSize({640, 480});
|
|
||||||
setCentered();
|
|
||||||
setVisible();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#include <nall/main.hpp>
|
|
||||||
auto nall::main(lstring args) -> void {
|
|
||||||
Application::setName("higan");
|
|
||||||
new Presentation;
|
|
||||||
Application::run();
|
|
||||||
}
|
|
|
@ -1,13 +1,15 @@
|
||||||
name := higan
|
name := tomoko
|
||||||
|
|
||||||
processors := arm gsu hg51b lr35902 r6502 r65816 spc700 upd96050
|
processors := arm gsu hg51b lr35902 r6502 r65816 spc700 upd96050
|
||||||
include processor/GNUmakefile
|
include processor/GNUmakefile
|
||||||
|
|
||||||
include fc/GNUmakefile
|
include fc/GNUmakefile
|
||||||
include sfc/GNUmakefile
|
include sfc/GNUmakefile
|
||||||
include gb/GNUmakefile
|
include gb/GNUmakefile
|
||||||
include gba/GNUmakefile
|
include gba/GNUmakefile
|
||||||
|
|
||||||
ui_objects := ui-higan
|
ui_objects := ui-tomoko ui-program
|
||||||
|
ui_objects += ui-library ui-presentation
|
||||||
ui_objects += ruby hiro
|
ui_objects += ruby hiro
|
||||||
|
|
||||||
# platform
|
# platform
|
||||||
|
@ -38,16 +40,33 @@ obj/ruby.o: ruby/ruby.cpp $(call rwildcard,ruby/)
|
||||||
obj/hiro.o: hiro/hiro.cpp $(call rwildcard,hiro/)
|
obj/hiro.o: hiro/hiro.cpp $(call rwildcard,hiro/)
|
||||||
$(compiler) $(hiroflags) -c $< -o $@
|
$(compiler) $(hiroflags) -c $< -o $@
|
||||||
|
|
||||||
obj/ui-higan.o: $(ui)/higan.cpp $(call rwildcard,$(ui)/)
|
obj/ui-tomoko.o: $(ui)/tomoko.cpp $(call rwildcard,$(ui)/)
|
||||||
|
obj/ui-program.o: $(ui)/program/program.cpp $(call rwildcard,$(ui)/)
|
||||||
|
obj/ui-library.o: $(ui)/library/library.cpp $(call rwildcard,$(ui)/)
|
||||||
|
obj/ui-presentation.o: $(ui)/presentation/presentation.cpp $(call rwildcard,$(ui)/)
|
||||||
|
|
||||||
# build
|
# targets
|
||||||
build: $(objects)
|
build: $(objects)
|
||||||
$(strip $(compiler) -o out/$(name) $(objects) $(link))
|
$(strip $(compiler) -o out/$(name) $(objects) $(link))
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
ifeq ($(shell id -un),root)
|
||||||
|
$(error "make install should not be run as root")
|
||||||
|
else ifeq ($(platform),windows)
|
||||||
|
else ifeq ($(platform),macosx)
|
||||||
|
else
|
||||||
cp out/$(name) $(prefix)/bin/$(name)
|
cp out/$(name) $(prefix)/bin/$(name)
|
||||||
cp data/higan.png $(prefix)/share/icons/higan.png
|
cp data/higan.png $(prefix)/share/icons/$(name).png
|
||||||
|
mkdir -p ~/Emulation/System/
|
||||||
|
cp -R profile/* ~/Emulation/System/
|
||||||
|
endif
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
|
ifeq ($(shell id -un),root)
|
||||||
|
$(error "make uninstall should not be run as root")
|
||||||
|
else ifeq ($(platform),windows)
|
||||||
|
else ifeq ($(platform),macosx)
|
||||||
|
else
|
||||||
if [ -f $(prefix)/bin/$(name) ]; then rm $(prefix)/bin/$(name); fi
|
if [ -f $(prefix)/bin/$(name) ]; then rm $(prefix)/bin/$(name); fi
|
||||||
if [ -f $(prefix)/share/icons/higan.png ]; then rm $(prefix)/share/icons/higan.png
|
if [ -f $(prefix)/share/icons/$(name).png ]; then rm $(prefix)/share/icons/$(name).png; fi
|
||||||
|
endif
|
|
@ -0,0 +1,35 @@
|
||||||
|
LibraryBrowser::LibraryBrowser(TabFrame& parent, Emulator::Interface::Media& media) : TabFrameItem{&parent} {
|
||||||
|
this->media = media;
|
||||||
|
setText(media.name);
|
||||||
|
layout.setMargin(5);
|
||||||
|
gameList.onActivate([&] {
|
||||||
|
libraryManager->setVisible(false);
|
||||||
|
program->loadMedia({userpath(), "Emulation/", this->media.name, "/", gameList.selected()->text(), ".", this->media.type, "/"});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LibraryBrowser::reload() -> void {
|
||||||
|
string path = {userpath(), "Emulation/", media.name};
|
||||||
|
directory::create(path);
|
||||||
|
|
||||||
|
gameList.reset();
|
||||||
|
gameList.append(ListViewColumn());
|
||||||
|
bool first = true;
|
||||||
|
auto folders = directory::folders(path, {"*.", media.type});
|
||||||
|
for(auto& folder : folders) {
|
||||||
|
ListViewItem item;
|
||||||
|
item.setIcon(0, Icon::Emblem::Program);
|
||||||
|
item.setText(folder.rtrim({".", media.type, "/"}));
|
||||||
|
gameList.append(item);
|
||||||
|
if(first) {
|
||||||
|
first = false;
|
||||||
|
item.setFocused();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LibraryBrowser::select() -> void {
|
||||||
|
reload();
|
||||||
|
setSelected();
|
||||||
|
gameList.setFocused();
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
#include "../tomoko.hpp"
|
||||||
|
#include "browser.cpp"
|
||||||
|
#include "manager.cpp"
|
|
@ -0,0 +1,21 @@
|
||||||
|
struct LibraryBrowser : TabFrameItem {
|
||||||
|
LibraryBrowser(TabFrame& parent, Emulator::Interface::Media& media);
|
||||||
|
auto reload() -> void;
|
||||||
|
auto select() -> void;
|
||||||
|
|
||||||
|
Emulator::Interface::Media media;
|
||||||
|
|
||||||
|
VerticalLayout layout{this};
|
||||||
|
ListView gameList{&layout, Size{~0, ~0}};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LibraryManager : Window {
|
||||||
|
LibraryManager();
|
||||||
|
auto show(const string& type) -> void;
|
||||||
|
|
||||||
|
VerticalLayout layout{this};
|
||||||
|
TabFrame libraryFrame{&layout, Size{~0, ~0}};
|
||||||
|
vector<LibraryBrowser*> libraryBrowsers;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern LibraryManager* libraryManager;
|
|
@ -0,0 +1,29 @@
|
||||||
|
LibraryManager* libraryManager = nullptr;
|
||||||
|
|
||||||
|
LibraryManager::LibraryManager() {
|
||||||
|
libraryManager = this;
|
||||||
|
|
||||||
|
layout.setMargin(5);
|
||||||
|
|
||||||
|
for(auto& emulator : program->emulators) {
|
||||||
|
for(auto& media : emulator->media) {
|
||||||
|
if(media.bootable == false) continue;
|
||||||
|
auto browser = new LibraryBrowser(libraryFrame, media);
|
||||||
|
libraryBrowsers.append(browser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle("Library");
|
||||||
|
setSize({640, 800});
|
||||||
|
setPosition({0, 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto LibraryManager::show(const string& type) -> void {
|
||||||
|
for(auto& browser : libraryBrowsers) {
|
||||||
|
if(type != browser->media.type) continue;
|
||||||
|
browser->select();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible();
|
||||||
|
setFocused();
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
#include "../tomoko.hpp"
|
||||||
|
Presentation* presentation = nullptr;
|
||||||
|
|
||||||
|
Presentation::Presentation() {
|
||||||
|
presentation = this;
|
||||||
|
|
||||||
|
libraryMenu.setText("Library");
|
||||||
|
for(auto& emulator : program->emulators) {
|
||||||
|
for(auto& media : emulator->media) {
|
||||||
|
if(media.bootable == false) continue;
|
||||||
|
auto item = new MenuItem{&libraryMenu};
|
||||||
|
item->setText({media.name, " ..."}).onActivate([=] {
|
||||||
|
libraryManager->show(media.type);
|
||||||
|
});
|
||||||
|
loadBootableMedia.append(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
superFamicomMenu.setText("Super Famicom");
|
||||||
|
|
||||||
|
settingsMenu.setText("Settings");
|
||||||
|
|
||||||
|
toolsMenu.setText("Tools");
|
||||||
|
|
||||||
|
statusBar.setFont(Font::sans(8, "Bold"));
|
||||||
|
|
||||||
|
onClose(&Application::quit);
|
||||||
|
|
||||||
|
setTitle({"tomoko v", Emulator::Version});
|
||||||
|
setResizable(false);
|
||||||
|
setSize({640, 480});
|
||||||
|
setCentered();
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
struct Presentation : Window {
|
||||||
|
Presentation();
|
||||||
|
|
||||||
|
MenuBar menuBar{this};
|
||||||
|
Menu libraryMenu{&menuBar};
|
||||||
|
vector<MenuItem*> loadBootableMedia;
|
||||||
|
Menu superFamicomMenu{&menuBar};
|
||||||
|
Menu settingsMenu{&menuBar};
|
||||||
|
Menu toolsMenu{&menuBar};
|
||||||
|
|
||||||
|
VerticalLayout layout{this};
|
||||||
|
Viewport viewport{&layout, Size{~0, ~0}};
|
||||||
|
|
||||||
|
StatusBar statusBar{this};
|
||||||
|
};
|
||||||
|
|
||||||
|
extern Presentation* presentation;
|
|
@ -0,0 +1,78 @@
|
||||||
|
//request from emulation core to load non-volatile media folder
|
||||||
|
auto Program::loadRequest(unsigned id, string name, string type) -> void {
|
||||||
|
}
|
||||||
|
|
||||||
|
//request from emulation core to load non-volatile media file
|
||||||
|
auto Program::loadRequest(unsigned id, string path) -> void {
|
||||||
|
string location = {mediaPaths(emulator().group(id)), path};
|
||||||
|
if(!file::exists(location)) return;
|
||||||
|
mmapstream stream{location};
|
||||||
|
return emulator().load(id, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::saveRequest(unsigned id, string path) -> void {
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::videoColor(unsigned source, uint16 alpha, uint16 red, uint16 green, uint16 blue) -> uint32 {
|
||||||
|
alpha >>= 8;
|
||||||
|
red >>= 8;
|
||||||
|
green >>= 8;
|
||||||
|
blue >>= 8;
|
||||||
|
return alpha << 24 | red << 16 | green << 8 | blue << 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::videoRefresh(const uint32* palette, const uint32* data, unsigned pitch, unsigned width, unsigned height) -> void {
|
||||||
|
uint32* output;
|
||||||
|
unsigned length;
|
||||||
|
|
||||||
|
if(video.lock(output, length, width, height)) {
|
||||||
|
pitch >>= 2, length >>= 2;
|
||||||
|
|
||||||
|
for(auto y : range(height)) {
|
||||||
|
const uint32* sp = data + y * pitch;
|
||||||
|
uint32* dp = output + y * length;
|
||||||
|
for(auto x : range(width)) {
|
||||||
|
*dp++ = palette[*sp++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video.unlock();
|
||||||
|
video.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned frameCounter = 0;
|
||||||
|
static time_t previous, current;
|
||||||
|
frameCounter++;
|
||||||
|
|
||||||
|
time(¤t);
|
||||||
|
if(current != previous) {
|
||||||
|
previous = current;
|
||||||
|
presentation->statusBar.setText({"FPS: ", frameCounter});
|
||||||
|
frameCounter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::audioSample(int16 lsample, int16 rsample) -> void {
|
||||||
|
signed samples[] = {lsample, rsample};
|
||||||
|
dsp.sample(samples);
|
||||||
|
while(dsp.pending()) {
|
||||||
|
dsp.read(samples);
|
||||||
|
audio.sample(samples[0], samples[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::inputPoll(unsigned port, unsigned device, unsigned input) -> int16 {
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::inputRumble(unsigned port, unsigned device, unsigned input, bool enable) -> void {
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::dipSettings(const Markup::Node& node) -> unsigned {
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::path(unsigned group) -> string {
|
||||||
|
return mediaPaths(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::notify(string text) -> void {
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
auto Program::loadMedia(string location) -> void {
|
||||||
|
location.transform("\\", "/");
|
||||||
|
if(!directory::exists(location)) return;
|
||||||
|
|
||||||
|
string type = suffixname(location).ltrim(".");
|
||||||
|
for(auto& emulator : emulators) {
|
||||||
|
for(auto& media : emulator->media) {
|
||||||
|
if(media.bootable == false) continue;
|
||||||
|
if(media.type != type) continue;
|
||||||
|
return loadMedia(*emulator, media, location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::loadMedia(Emulator::Interface& _emulator, Emulator::Interface::Media& media, const string& location) -> void {
|
||||||
|
mediaPaths(0) = {userpath(), "Emulation/System/", media.name, ".sys/"};
|
||||||
|
mediaPaths(media.id) = location;
|
||||||
|
|
||||||
|
setEmulator(_emulator);
|
||||||
|
emulator().paletteUpdate(Emulator::Interface::PaletteMode::Standard);
|
||||||
|
emulator().load(media.id);
|
||||||
|
emulator().power();
|
||||||
|
|
||||||
|
presentation->setTitle(emulator().title());
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
#include "../tomoko.hpp"
|
||||||
|
#include <fc/interface/interface.hpp>
|
||||||
|
#include <sfc/interface/interface.hpp>
|
||||||
|
#include <gb/interface/interface.hpp>
|
||||||
|
#include <gba/interface/interface.hpp>
|
||||||
|
#include "interface.cpp"
|
||||||
|
#include "media.cpp"
|
||||||
|
Program* program = nullptr;
|
||||||
|
|
||||||
|
Program::Program() {
|
||||||
|
program = this;
|
||||||
|
Application::onMain({&Program::main, this});
|
||||||
|
|
||||||
|
emulators.append(new Famicom::Interface);
|
||||||
|
emulators.append(new SuperFamicom::Interface);
|
||||||
|
emulators.append(new GameBoy::Interface);
|
||||||
|
emulators.append(new GameBoyAdvance::Interface);
|
||||||
|
for(auto& emulator : emulators) emulator->bind = this;
|
||||||
|
|
||||||
|
new LibraryManager;
|
||||||
|
new Presentation;
|
||||||
|
|
||||||
|
presentation->setVisible();
|
||||||
|
|
||||||
|
video.driver("XShm");
|
||||||
|
video.set(Video::Handle, presentation->viewport.handle());
|
||||||
|
video.set(Video::Synchronize, false);
|
||||||
|
if(!video.init()) { video.driver("None"); video.init(); }
|
||||||
|
|
||||||
|
audio.driver("OpenAL");
|
||||||
|
audio.set(Audio::Handle, presentation->viewport.handle());
|
||||||
|
audio.set(Audio::Synchronize, false);
|
||||||
|
audio.set(Audio::Frequency, 96000u);
|
||||||
|
audio.set(Audio::Latency, 80u);
|
||||||
|
if(!audio.init()) { audio.driver("None"); audio.init(); }
|
||||||
|
|
||||||
|
input.driver("XInput");
|
||||||
|
input.set(Input::Handle, presentation->viewport.handle());
|
||||||
|
if(!input.init()) { input.driver("None"); input.init(); }
|
||||||
|
|
||||||
|
dsp.setPrecision(16);
|
||||||
|
dsp.setBalance(0.0);
|
||||||
|
dsp.setVolume(1.0);
|
||||||
|
dsp.setFrequency(32040);
|
||||||
|
dsp.setResampler(DSP::ResampleEngine::Sinc);
|
||||||
|
dsp.setResamplerFrequency(96000);
|
||||||
|
|
||||||
|
uint32* output;
|
||||||
|
unsigned length;
|
||||||
|
if(video.lock(output, length, 640, 480)) {
|
||||||
|
for(auto y : range(480)) {
|
||||||
|
uint32* dp = output + y * (length >> 2);
|
||||||
|
for(auto x : range(640)) {
|
||||||
|
*dp++ = 0xff401010;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video.unlock();
|
||||||
|
video.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::emulator() -> Emulator::Interface& {
|
||||||
|
if(activeEmulator == nullptr) throw;
|
||||||
|
return *activeEmulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::main() -> void {
|
||||||
|
if(activeEmulator == nullptr || emulator().loaded() == false) {
|
||||||
|
audio.clear();
|
||||||
|
usleep(20 * 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emulator().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Program::setEmulator(Emulator::Interface& emulator) -> void {
|
||||||
|
activeEmulator = &emulator;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
struct Program : Emulator::Interface::Bind {
|
||||||
|
//program.cpp
|
||||||
|
Program();
|
||||||
|
auto emulator() -> Emulator::Interface&;
|
||||||
|
auto main() -> void;
|
||||||
|
auto setEmulator(Emulator::Interface&) -> void;
|
||||||
|
|
||||||
|
//interface.cpp
|
||||||
|
auto loadRequest(unsigned id, string name, string type) -> void override;
|
||||||
|
auto loadRequest(unsigned id, string path) -> void override;
|
||||||
|
auto saveRequest(unsigned id, string path) -> void override;
|
||||||
|
auto videoColor(unsigned source, uint16 alpha, uint16 red, uint16 green, uint16 blue) -> uint32 override;
|
||||||
|
auto videoRefresh(const uint32* palette, const uint32* data, unsigned pitch, unsigned width, unsigned height) -> void override;
|
||||||
|
auto audioSample(int16 lsample, int16 rsample) -> void override;
|
||||||
|
auto inputPoll(unsigned port, unsigned device, unsigned input) -> int16 override;
|
||||||
|
auto inputRumble(unsigned port, unsigned device, unsigned input, bool enable) -> void override;
|
||||||
|
auto dipSettings(const Markup::Node& node) -> unsigned override;
|
||||||
|
auto path(unsigned group) -> string override;
|
||||||
|
auto notify(string text) -> void override;
|
||||||
|
|
||||||
|
//media.cpp
|
||||||
|
auto loadMedia(string location) -> void;
|
||||||
|
auto loadMedia(Emulator::Interface& interface, Emulator::Interface::Media& media, const string& location) -> void;
|
||||||
|
|
||||||
|
DSP dsp;
|
||||||
|
|
||||||
|
vector<Emulator::Interface*> emulators;
|
||||||
|
Emulator::Interface* activeEmulator = nullptr;
|
||||||
|
vector<string> mediaPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern Program* program;
|
|
@ -0,0 +1,8 @@
|
||||||
|
#include "tomoko.hpp"
|
||||||
|
|
||||||
|
#include <nall/main.hpp>
|
||||||
|
auto nall::main(lstring args) -> void {
|
||||||
|
Application::setName("tomoko");
|
||||||
|
new Program;
|
||||||
|
Application::run();
|
||||||
|
}
|
|
@ -6,3 +6,7 @@
|
||||||
using namespace nall;
|
using namespace nall;
|
||||||
using namespace ruby;
|
using namespace ruby;
|
||||||
using namespace hiro;
|
using namespace hiro;
|
||||||
|
|
||||||
|
#include "program/program.hpp"
|
||||||
|
#include "library/library.hpp"
|
||||||
|
#include "presentation/presentation.hpp"
|
Loading…
Reference in New Issue