From 79553ab1fa3d707612e80dd03bf95ca65fa0c411 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Tue, 4 Feb 2025 23:11:08 +0100 Subject: refactor file browser --- app/mac/CMakeLists.txt | 4 +- app/mac/impl/CMakeLists.txt | 13 + app/mac/impl/display-spritekit.hh | 24 ++ app/mac/impl/display-spritekit.mm | 50 +++ app/mac/impl/file-browser-posix.cc | 129 +++++++ app/mac/impl/file-browser-posix.hh | 30 ++ app/mac/impl/input-device-gc-gamepad.hh | 24 ++ app/mac/impl/input-device-gc-gamepad.mm | 60 +++ app/mac/impl/input-device-gc-keyboard.hh | 30 ++ app/mac/impl/input-device-gc-keyboard.mm | 119 ++++++ app/mac/impl/input-device-serial-controller.cc | 96 +++++ app/mac/impl/input-device-serial-controller.hh | 29 ++ app/mac/scene.mm | 503 +------------------------ lib/nes/app/application.cc | 18 +- lib/nes/app/file-browser.hh | 20 +- lib/nes/app/input/input-buffer.cc | 2 +- lib/nes/app/input/input-manager.cc | 7 +- lib/nes/app/ui/action.hh | 26 +- lib/nes/app/ui/screen-browser.cc | 28 +- lib/nes/common/CMakeLists.txt | 3 +- lib/nes/common/box.hh | 29 +- lib/nes/common/debug.hh | 18 +- lib/nes/common/list.hh | 24 +- lib/nes/common/path-builder.cc | 14 +- lib/nes/common/path-builder.hh | 21 +- lib/nes/common/path-view.cc | 4 +- lib/nes/common/span.hh | 33 +- lib/nes/common/status.hh | 42 +++ lib/nes/common/string-builder.cc | 116 +++--- lib/nes/common/string-builder.hh | 37 +- lib/nes/sys/cartridge.cc | 3 +- lib/nes/sys/cartridge.hh | 2 +- lib/nes/sys/cpu.hh | 2 +- lib/nes/sys/mapper.hh | 2 +- lib/nes/sys/types/CMakeLists.txt | 3 +- lib/nes/sys/types/status.hh | 33 -- 36 files changed, 944 insertions(+), 654 deletions(-) create mode 100644 app/mac/impl/CMakeLists.txt create mode 100644 app/mac/impl/display-spritekit.hh create mode 100644 app/mac/impl/display-spritekit.mm create mode 100644 app/mac/impl/file-browser-posix.cc create mode 100644 app/mac/impl/file-browser-posix.hh create mode 100644 app/mac/impl/input-device-gc-gamepad.hh create mode 100644 app/mac/impl/input-device-gc-gamepad.mm create mode 100644 app/mac/impl/input-device-gc-keyboard.hh create mode 100644 app/mac/impl/input-device-gc-keyboard.mm create mode 100644 app/mac/impl/input-device-serial-controller.cc create mode 100644 app/mac/impl/input-device-serial-controller.hh create mode 100644 lib/nes/common/status.hh delete mode 100644 lib/nes/sys/types/status.hh diff --git a/app/mac/CMakeLists.txt b/app/mac/CMakeLists.txt index e20edb5..e7471d1 100644 --- a/app/mac/CMakeLists.txt +++ b/app/mac/CMakeLists.txt @@ -21,4 +21,6 @@ target_sources( scene.hh scene.mm view-controller.hh - view-controller.mm) \ No newline at end of file + view-controller.mm) + +add_subdirectory(impl) \ No newline at end of file diff --git a/app/mac/impl/CMakeLists.txt b/app/mac/impl/CMakeLists.txt new file mode 100644 index 0000000..f8ff091 --- /dev/null +++ b/app/mac/impl/CMakeLists.txt @@ -0,0 +1,13 @@ +target_sources( + nes_app_mac + PRIVATE + display-spritekit.hh + display-spritekit.mm + input-device-gc-gamepad.hh + input-device-gc-gamepad.mm + input-device-gc-keyboard.hh + input-device-gc-keyboard.mm + input-device-serial-controller.hh + input-device-serial-controller.cc + file-browser-posix.hh + file-browser-posix.cc) diff --git a/app/mac/impl/display-spritekit.hh b/app/mac/impl/display-spritekit.hh new file mode 100644 index 0000000..64e3b03 --- /dev/null +++ b/app/mac/impl/display-spritekit.hh @@ -0,0 +1,24 @@ +#pragma once + +#import "nes/common/display.hh" + +#import + +namespace nes::app::mac +{ + /// Display implementation rendering contents into a SpriteKit node. + class display_spritekit final : public display + { + SKSpriteNode* node_; + NSMutableData* buffer_front_; + NSMutableData* buffer_back_; + u8* bytes_back_; + + public: + explicit display_spritekit(SKSpriteNode* node); + + virtual auto switch_buffers() -> void override; + virtual auto set(u32 x, u32 y, rgb value) -> void override; + virtual auto get(u32 x, u32 y) const -> rgb override; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/display-spritekit.mm b/app/mac/impl/display-spritekit.mm new file mode 100644 index 0000000..b6123d6 --- /dev/null +++ b/app/mac/impl/display-spritekit.mm @@ -0,0 +1,50 @@ +#import "impl/display-spritekit.hh" + +namespace nes::app::mac +{ + namespace + { + auto make_buffer() -> NSMutableData* + { + auto const length = NSUInteger{ display::width * display::height * 4 }; + return [NSMutableData dataWithLength:length]; + } + } // namespace + + display_spritekit::display_spritekit(SKSpriteNode* node) + : node_{ node } + , buffer_front_{ make_buffer() } + , buffer_back_{ make_buffer() } + , bytes_back_{ static_cast([buffer_back_ mutableBytes]) } + { + } + + auto display_spritekit::switch_buffers() -> void + { + std::swap(buffer_back_, buffer_front_); + bytes_back_ = static_cast([buffer_back_ mutableBytes]); + + auto texture = [SKTexture textureWithData:buffer_front_ size:CGSizeMake(width, height) flipped:YES]; + [texture setFilteringMode:SKTextureFilteringNearest]; + [node_ setTexture:texture]; + } + + auto display_spritekit::set(u32 const x, u32 const y, rgb const value) -> void + { + auto const offset = (y * width + x) * 4; + bytes_back_[offset + 0] = value.r; + bytes_back_[offset + 1] = value.g; + bytes_back_[offset + 2] = value.b; + bytes_back_[offset + 3] = 255; + } + + auto display_spritekit::get(u32 const x, u32 const y) const -> rgb + { + auto const offset = (y * width + x) * 4; + auto res = rgb{}; + res.r = bytes_back_[offset + 0]; + res.g = bytes_back_[offset + 1]; + res.b = bytes_back_[offset + 2]; + return res; + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/file-browser-posix.cc b/app/mac/impl/file-browser-posix.cc new file mode 100644 index 0000000..4c717fd --- /dev/null +++ b/app/mac/impl/file-browser-posix.cc @@ -0,0 +1,129 @@ +#include "impl/file-browser-posix.hh" +#include +#include +#include +#include + +namespace nes::app::mac +{ + file_browser_posix::file_browser_posix() + { + reopen_directory(path_.get_path()); + } + + file_browser_posix::~file_browser_posix() + { + if (it_) { closedir(it_); } + } + + auto file_browser_posix::seek(u32 const pos) -> void + { + if (it_pos_ > pos) + { + reopen_directory(path_.get_path()); + } + + while (it_pos_ < pos) + { + if (!read_next(nullptr)) { break; } + } + } + + auto file_browser_posix::read_next(item* out_item) -> bool + { + if (!it_) { return false; } + + while (true) + { + errno = 0; + auto const entry = readdir(it_); + if (!entry) + { + if (errno) { perror("file_browser_posix: readdir"); } + return false; + } + + auto const name = std::string_view{ entry->d_name }; + if (name.length() > max_name_length) { continue; } + if (name[0] == '.') { continue; } + + auto type = item_type::directory; + if (entry->d_type == DT_DIR) { type = item_type::directory; } + else if (entry->d_type == DT_REG) { type = item_type::file; } + else { continue; } + + it_pos_ += 1; + if (out_item) { *out_item = item{ type, name }; } + return true; + } + } + + auto file_browser_posix::navigate_up() -> status + { + auto new_path = path_; + if (auto const s = new_path.pop(); s != status::success) { return s; } + if (auto const s = reopen_directory(new_path.get_path()); s != status::success) { return s; } + path_ = new_path; + return status::success; + } + + auto file_browser_posix::navigate(std::string_view const item) -> status + { + auto new_path = path_; + if (auto const s = new_path.push(item); s != status::success) { return s; } + if (auto const s = reopen_directory(new_path.get_path()); s != status::success) { return s; } + path_ = new_path; + return status::success; + } + + auto file_browser_posix::load(std::string_view const item, span const buffer, u32* out_length) -> status + { + auto file_path = path_; + if (auto const s = file_path.push(item); s != status::success) { return s; } + + auto const file_path_str = std::string{ file_path.get_path() }; + auto const fd = open(file_path_str.c_str(), O_RDONLY); + if (fd == -1) + { + perror("file_browser_posix: open"); + return status::error_system_error; + } + + auto const res = read(fd, buffer.get_data(), buffer.get_length()); + if (res < 0) + { + perror("file_browser_posix: read"); + close(fd); + return status::error_system_error; + } + if (out_length) { *out_length = static_cast(res); } + + auto remaining = char{}; + if (read(fd, &remaining, 1) != 0) + { + // File was too long. + close(fd); + return status::error_invalid_ines_data; + } + + close(fd); + + return status::success; + } + + auto file_browser_posix::reopen_directory(std::string_view const path) -> status + { + auto const path_str = std::string{ path }; + auto const new_it = opendir(path_str.c_str()); + if (!new_it) + { + perror("file_browser_posix: opendir"); + return status::error_system_error; + } + + if (it_) { closedir(it_); } + it_ = new_it; + it_pos_ = 0; + return status::success; + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/file-browser-posix.hh b/app/mac/impl/file-browser-posix.hh new file mode 100644 index 0000000..b760e53 --- /dev/null +++ b/app/mac/impl/file-browser-posix.hh @@ -0,0 +1,30 @@ +#pragma once + +#include "nes/app/file-browser.hh" +#include "nes/common/path-builder.hh" +#include + +namespace nes::app::mac +{ + /// File browser implementation using the POSIX directory API. + class file_browser_posix final : public file_browser + { + path_buffer<1024> path_; + DIR* it_{ nullptr }; + u32 it_pos_{ 0 }; + + public: + explicit file_browser_posix(); + ~file_browser_posix() override; + + auto get_path() const -> path_view override { return path_.get_components(); } + auto seek(u32) -> void override; + auto read_next(item* out_item) -> bool override; + auto navigate_up() -> status override; + auto navigate(std::string_view) -> status override; + auto load(std::string_view, span, u32* out_length) -> status override; + + private: + auto reopen_directory(std::string_view path) -> status; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-gamepad.hh b/app/mac/impl/input-device-gc-gamepad.hh new file mode 100644 index 0000000..b4b4a4e --- /dev/null +++ b/app/mac/impl/input-device-gc-gamepad.hh @@ -0,0 +1,24 @@ +#pragma once + +#import "nes/app/input/input-device-controller.hh" + +#import + +namespace nes::app::mac +{ + /// Input device wrapping a GCExtendedGamepad. + class input_device_gc_gamepad final : public input_device_controller + { + GCExtendedGamepad* profile_; + + public: + explicit input_device_gc_gamepad(GCExtendedGamepad* profile); + + auto is_reliable() const -> bool override { return true; } + auto get_controller() const -> GCController* { return [profile_ controller]; } + auto get_name() const -> std::string_view override; + auto get_index() const -> index override; + auto set_index(index) -> void override; + auto read_buttons() -> sys::button_mask override; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-gamepad.mm b/app/mac/impl/input-device-gc-gamepad.mm new file mode 100644 index 0000000..d2f84ec --- /dev/null +++ b/app/mac/impl/input-device-gc-gamepad.mm @@ -0,0 +1,60 @@ +#import "impl/input-device-gc-gamepad.hh" + +namespace nes::app::mac +{ + input_device_gc_gamepad::input_device_gc_gamepad(GCExtendedGamepad* profile) + : profile_{ profile } + { + } + + auto input_device_gc_gamepad::get_name() const -> std::string_view + { + return [[get_controller() vendorName] UTF8String]; + } + + auto input_device_gc_gamepad::get_index() const -> index + { + switch ([get_controller() playerIndex]) + { + case GCControllerPlayerIndex1: + return index::player_1; + case GCControllerPlayerIndex2: + return index::player_2; + case GCControllerPlayerIndex3: + case GCControllerPlayerIndex4: + case GCControllerPlayerIndexUnset: + break; + } + return index::unused; + } + + auto input_device_gc_gamepad::set_index(index const idx) -> void + { + switch (idx) + { + case index::unused: + [get_controller() setPlayerIndex:GCControllerPlayerIndexUnset]; + break; + case index::player_1: + [get_controller() setPlayerIndex:GCControllerPlayerIndex1]; + break; + case index::player_2: + [get_controller() setPlayerIndex:GCControllerPlayerIndex2]; + break; + } + } + + auto input_device_gc_gamepad::read_buttons() -> sys::button_mask + { + auto res = sys::button_mask{}; + if ([[[profile_ dpad] up] isPressed]) { res.add(sys::buttons::up); } + if ([[[profile_ dpad] down] isPressed]) { res.add(sys::buttons::down); } + if ([[[profile_ dpad] left] isPressed]) { res.add(sys::buttons::left); } + if ([[[profile_ dpad] right] isPressed]) { res.add(sys::buttons::right); } + if ([[profile_ buttonA] isPressed]) { res.add(sys::buttons::a); } + if ([[profile_ buttonB] isPressed]) { res.add(sys::buttons::b); } + if ([[profile_ leftTrigger] isPressed]) { res.add(sys::buttons::select); } + if ([[profile_ rightTrigger] isPressed]) { res.add(sys::buttons::start); } + return res; + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-keyboard.hh b/app/mac/impl/input-device-gc-keyboard.hh new file mode 100644 index 0000000..c879dcc --- /dev/null +++ b/app/mac/impl/input-device-gc-keyboard.hh @@ -0,0 +1,30 @@ +#pragma once + +#import "nes/app/input/input-device-keyboard.hh" +#import "nes/app/input/input-buffer.hh" +#import + +#import + +namespace nes::app::mac +{ + /// Input device wrapping a GCKeyboard. + class input_device_gc_keyboard final : public input_device_keyboard + { + GCKeyboardInput* profile_{ nil }; + input_buffer buffer_; + + public: + explicit input_device_gc_keyboard(GCKeyboardInput* profile); + ~input_device_gc_keyboard() override; + + auto set_profile(GCKeyboardInput* profile) -> void; + + auto read_key(key const key) -> bool override { return buffer_.read_key(key); } + auto poll_event() -> input_event override { return buffer_.poll_event(); } + + private: + auto handle_key_changed(GCKeyCode key_code, BOOL pressed) -> void; + auto convert_key_code(GCKeyCode key_code) -> std::optional; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-keyboard.mm b/app/mac/impl/input-device-gc-keyboard.mm new file mode 100644 index 0000000..34ab98d --- /dev/null +++ b/app/mac/impl/input-device-gc-keyboard.mm @@ -0,0 +1,119 @@ +#import "impl/input-device-gc-keyboard.hh" + +namespace nes::app::mac +{ + input_device_gc_keyboard::input_device_gc_keyboard(GCKeyboardInput* profile) + { + set_profile(profile); + } + + input_device_gc_keyboard::~input_device_gc_keyboard() + { + set_profile(nil); + } + + auto input_device_gc_keyboard::set_profile(GCKeyboardInput* profile) -> void + { + [profile_ setKeyChangedHandler:nil]; + profile_ = profile; + [profile_ setKeyChangedHandler:^(GCKeyboardInput*, GCControllerButtonInput*, GCKeyCode key_code, BOOL pressed) { + handle_key_changed(key_code, pressed); + }]; + } + + auto input_device_gc_keyboard::handle_key_changed(GCKeyCode const key_code, BOOL const pressed) -> void + { + auto const key = convert_key_code(key_code); + if (key) + { + if (pressed) + { + buffer_.key_down(*key); + } + else + { + buffer_.key_up(*key); + } + } + } + + auto input_device_gc_keyboard::convert_key_code(GCKeyCode const key_code) -> std::optional + { + if (key_code == GCKeyCodeZero) { return key::digit_0; } + if (key_code == GCKeyCodeOne) { return key::digit_1; } + if (key_code == GCKeyCodeTwo) { return key::digit_2; } + if (key_code == GCKeyCodeThree) { return key::digit_3; } + if (key_code == GCKeyCodeFour) { return key::digit_4; } + if (key_code == GCKeyCodeFive) { return key::digit_5; } + if (key_code == GCKeyCodeSix) { return key::digit_6; } + if (key_code == GCKeyCodeSeven) { return key::digit_7; } + if (key_code == GCKeyCodeEight) { return key::digit_8; } + if (key_code == GCKeyCodeNine) { return key::digit_9; } + if (key_code == GCKeyCodeKeyA) { return key::letter_a; } + if (key_code == GCKeyCodeKeyB) { return key::letter_b; } + if (key_code == GCKeyCodeKeyC) { return key::letter_c; } + if (key_code == GCKeyCodeKeyD) { return key::letter_d; } + if (key_code == GCKeyCodeKeyE) { return key::letter_e; } + if (key_code == GCKeyCodeKeyF) { return key::letter_f; } + if (key_code == GCKeyCodeKeyG) { return key::letter_g; } + if (key_code == GCKeyCodeKeyH) { return key::letter_h; } + if (key_code == GCKeyCodeKeyI) { return key::letter_i; } + if (key_code == GCKeyCodeKeyJ) { return key::letter_j; } + if (key_code == GCKeyCodeKeyK) { return key::letter_k; } + if (key_code == GCKeyCodeKeyL) { return key::letter_l; } + if (key_code == GCKeyCodeKeyM) { return key::letter_m; } + if (key_code == GCKeyCodeKeyN) { return key::letter_n; } + if (key_code == GCKeyCodeKeyO) { return key::letter_o; } + if (key_code == GCKeyCodeKeyP) { return key::letter_p; } + if (key_code == GCKeyCodeKeyQ) { return key::letter_q; } + if (key_code == GCKeyCodeKeyR) { return key::letter_r; } + if (key_code == GCKeyCodeKeyS) { return key::letter_s; } + if (key_code == GCKeyCodeKeyT) { return key::letter_t; } + if (key_code == GCKeyCodeKeyU) { return key::letter_u; } + if (key_code == GCKeyCodeKeyV) { return key::letter_v; } + if (key_code == GCKeyCodeKeyW) { return key::letter_w; } + if (key_code == GCKeyCodeKeyX) { return key::letter_x; } + if (key_code == GCKeyCodeKeyY) { return key::letter_y; } + if (key_code == GCKeyCodeKeyZ) { return key::letter_z; } + if (key_code == GCKeyCodeEscape) { return key::escape; } + if (key_code == GCKeyCodeHyphen) { return key::dash; } + if (key_code == GCKeyCodeEqualSign) { return key::equal; } + if (key_code == GCKeyCodeDeleteOrBackspace) { return key::backspace; } + if (key_code == GCKeyCodeTab) { return key::tab; } + if (key_code == GCKeyCodeReturnOrEnter) { return key::enter; } + if (key_code == GCKeyCodeSemicolon) { return key::semicolon; } + if (key_code == GCKeyCodeQuote) { return key::apostrophe; } + if (key_code == GCKeyCodeBackslash) { return key::backslash; } + if (key_code == GCKeyCodeComma) { return key::comma; } + if (key_code == GCKeyCodePeriod) { return key::period; } + if (key_code == GCKeyCodeSlash) { return key::slash; } + if (key_code == GCKeyCodeSpacebar) { return key::space; } + if (key_code == GCKeyCodeCapsLock) { return key::caps_lock; } + if (key_code == GCKeyCodeOpenBracket) { return key::bracket_open; } + if (key_code == GCKeyCodeCloseBracket) { return key::bracket_close; } + if (key_code == GCKeyCodeLeftControl) { return key::control_left; } + if (key_code == GCKeyCodeRightControl) { return key::control_right; } + if (key_code == GCKeyCodeLeftShift) { return key::shift_left; } + if (key_code == GCKeyCodeRightShift) { return key::shift_right; } + if (key_code == GCKeyCodeLeftAlt) { return key::alt_left; } + if (key_code == GCKeyCodeRightAlt) { return key::alt_right; } + if (key_code == GCKeyCodeLeftArrow) { return key::arrow_left; } + if (key_code == GCKeyCodeRightArrow) { return key::arrow_right; } + if (key_code == GCKeyCodeUpArrow) { return key::arrow_up; } + if (key_code == GCKeyCodeDownArrow) { return key::arrow_down; } + if (key_code == GCKeyCodeF1) { return key::f1; } + if (key_code == GCKeyCodeF2) { return key::f2; } + if (key_code == GCKeyCodeF3) { return key::f3; } + if (key_code == GCKeyCodeF4) { return key::f4; } + if (key_code == GCKeyCodeF5) { return key::f5; } + if (key_code == GCKeyCodeF6) { return key::f6; } + if (key_code == GCKeyCodeF7) { return key::f7; } + if (key_code == GCKeyCodeF8) { return key::f8; } + if (key_code == GCKeyCodeF9) { return key::f9; } + if (key_code == GCKeyCodeF10) { return key::f10; } + if (key_code == GCKeyCodeF11) { return key::f11; } + if (key_code == GCKeyCodeF12) { return key::f12; } + + return std::nullopt; + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-serial-controller.cc b/app/mac/impl/input-device-serial-controller.cc new file mode 100644 index 0000000..7a493a7 --- /dev/null +++ b/app/mac/impl/input-device-serial-controller.cc @@ -0,0 +1,96 @@ +#include "impl/input-device-serial-controller.hh" +#include +#include +#include +#include +#include +#include + +namespace nes::app::mac +{ + input_device_serial_controller::input_device_serial_controller(char const* path) + : path_{ path } + { + fd_ = open(path, O_RDONLY | O_NOCTTY | O_NDELAY | O_NONBLOCK); + if (fd_ == -1) + { + perror("input_device_serial_controller: open"); + return; + } + + if (!set_up()) { close(); } + } + + input_device_serial_controller::~input_device_serial_controller() + { + close(); + } + + auto input_device_serial_controller::is_reliable() const -> bool + { + return fd_ != -1; + } + + auto input_device_serial_controller::read_buttons() -> sys::button_mask + { + if (fd_ == -1) { return sys::button_mask{}; } + + // Poll for any updates until we can't read anymore. + while (true) + { + auto buf = u8{}; + auto const res = read(fd_, &buf, 1); + if (res == -1) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } + + perror("read from controller"); + close(); + break; + } + + current_ = sys::button_mask::from_raw_value(buf); + } + + return current_; + } + + + auto input_device_serial_controller::set_up() -> bool + { + if (ioctl(fd_, TIOCEXCL) == -1) + { + perror("input_device_serial_controller: ioctl"); + return false; + } + + auto attr = termios{}; + if (tcgetattr(fd_, &attr) == -1) + { + perror("input_device_serial_controller: tcgetattr"); + return false; + } + + cfsetispeed(&attr, B57600); + cfsetospeed(&attr, B57600); + attr.c_cflag = attr.c_cflag | CLOCAL | CREAD; + attr.c_cflag = (attr.c_cflag & static_cast(~CSIZE)) | CS8; + + if (tcsetattr(fd_, TCSANOW, &attr) == -1) + { + perror("input_device_serial_controller: tcsetattr"); + return false; + } + + return true; + } + + auto input_device_serial_controller::close() -> void + { + if (fd_ != -1) + { + ::close(fd_); + fd_ = -1; + } + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-serial-controller.hh b/app/mac/impl/input-device-serial-controller.hh new file mode 100644 index 0000000..2022ed2 --- /dev/null +++ b/app/mac/impl/input-device-serial-controller.hh @@ -0,0 +1,29 @@ +#pragma once + +#include "nes/app/input/input-device-controller.hh" + +namespace nes::app::mac +{ + /// Input device for an NES controller plugged in via serial port. + class input_device_serial_controller final : public input_device_controller + { + char const* path_; + int fd_{ -1 }; + sys::button_mask current_{}; + index index_{ index::unused }; + + public: + explicit input_device_serial_controller(char const* path); + ~input_device_serial_controller() override; + + auto is_reliable() const -> bool override; + auto get_name() const -> std::string_view override { return path_; } + auto get_index() const -> index override { return index_; } + auto set_index(index const v) -> void override { index_ = v; } + auto read_buttons() -> sys::button_mask override; + + private: + auto set_up() -> bool; + auto close() -> void; + }; +} // namespace nes::app:mac \ No newline at end of file diff --git a/app/mac/scene.mm b/app/mac/scene.mm index 901af64..3650156 100644 --- a/app/mac/scene.mm +++ b/app/mac/scene.mm @@ -1,4 +1,9 @@ #import "scene.hh" +#import "impl/display-spritekit.hh" +#import "impl/file-browser-posix.hh" +#import "impl/input-device-gc-keyboard.hh" +#import "impl/input-device-gc-gamepad.hh" +#import "impl/input-device-serial-controller.hh" #import "nes/app/application.hh" #import "nes/app/input/input-device-keyboard.hh" #import "nes/app/input/input-device-controller.hh" @@ -8,494 +13,20 @@ #import "nes/common/path-view.hh" #import "nes/common/path-builder.hh" #import -#import #import -#import -#import -#import -#import -#import namespace { - class display_impl final : public nes::display - { - SKSpriteNode* node_; - NSMutableData* buffer_front_; - NSMutableData* buffer_back_; - nes::u8* bytes_back_; - - public: - explicit display_impl(SKSpriteNode* node) - : node_{ node } - , buffer_front_{ make_buffer() } - , buffer_back_{ make_buffer() } - , bytes_back_{ static_cast([buffer_back_ mutableBytes]) } - { - } - - ~display_impl() = default; - - virtual auto switch_buffers() -> void override - { - auto tmp = buffer_back_; - buffer_back_ = buffer_front_; - buffer_front_ = tmp; - bytes_back_ = static_cast([buffer_back_ mutableBytes]); - - auto texture = [SKTexture textureWithData:buffer_front_ size:CGSizeMake(width, height) flipped:YES]; - [texture setFilteringMode:SKTextureFilteringNearest]; - [node_ setTexture:texture]; - } - - virtual auto set(nes::u32 x, nes::u32 y, nes::rgb value) -> void override - { - auto const offset = (y * width + x) * 4; - bytes_back_[offset + 0] = value.r; - bytes_back_[offset + 1] = value.g; - bytes_back_[offset + 2] = value.b; - bytes_back_[offset + 3] = 255; - } - - virtual auto get(nes::u32 x, nes::u32 y) const -> nes::rgb override - { - auto const offset = (y * width + x) * 4; - auto res = nes::rgb{}; - res.r = bytes_back_[offset + 0]; - res.g = bytes_back_[offset + 1]; - res.b = bytes_back_[offset + 2]; - return res; - } - - private: - static auto make_buffer() -> NSMutableData* - { - auto const length = NSUInteger{ width * height * 4 }; - return [NSMutableData dataWithLength:length]; - } - }; - - class input_device_keyboard_impl final : public nes::app::input_device_keyboard - { - GCKeyboardInput* profile_{ nil }; - nes::app::input_buffer buffer_; - - public: - explicit input_device_keyboard_impl(GCKeyboardInput* profile) - { - set_profile(profile); - } - - ~input_device_keyboard_impl() - { - set_profile(nil); - } - - auto set_profile(GCKeyboardInput* profile) -> void - { - [profile_ setKeyChangedHandler:nil]; - profile_ = profile; - [profile_ setKeyChangedHandler:^(GCKeyboardInput*, GCControllerButtonInput*, GCKeyCode key_code, BOOL pressed) { - handle_key_changed(key_code, pressed); - }]; - } - - auto read_key(nes::app::key const key) -> bool override { return buffer_.read_key(key); } - auto poll_event() -> nes::app::input_event override { return buffer_.poll_event(); } - - private: - auto handle_key_changed(GCKeyCode const key_code, BOOL const pressed) -> void - { - auto const key = convert_key_code(key_code); - if (key) - { - if (pressed) - { - buffer_.key_down(*key); - } - else - { - buffer_.key_up(*key); - } - } - } - - auto convert_key_code(GCKeyCode const key_code) -> std::optional - { - if (key_code == GCKeyCodeZero) { return nes::app::key::digit_0; } - if (key_code == GCKeyCodeOne) { return nes::app::key::digit_1; } - if (key_code == GCKeyCodeTwo) { return nes::app::key::digit_2; } - if (key_code == GCKeyCodeThree) { return nes::app::key::digit_3; } - if (key_code == GCKeyCodeFour) { return nes::app::key::digit_4; } - if (key_code == GCKeyCodeFive) { return nes::app::key::digit_5; } - if (key_code == GCKeyCodeSix) { return nes::app::key::digit_6; } - if (key_code == GCKeyCodeSeven) { return nes::app::key::digit_7; } - if (key_code == GCKeyCodeEight) { return nes::app::key::digit_8; } - if (key_code == GCKeyCodeNine) { return nes::app::key::digit_9; } - if (key_code == GCKeyCodeKeyA) { return nes::app::key::letter_a; } - if (key_code == GCKeyCodeKeyB) { return nes::app::key::letter_b; } - if (key_code == GCKeyCodeKeyC) { return nes::app::key::letter_c; } - if (key_code == GCKeyCodeKeyD) { return nes::app::key::letter_d; } - if (key_code == GCKeyCodeKeyE) { return nes::app::key::letter_e; } - if (key_code == GCKeyCodeKeyF) { return nes::app::key::letter_f; } - if (key_code == GCKeyCodeKeyG) { return nes::app::key::letter_g; } - if (key_code == GCKeyCodeKeyH) { return nes::app::key::letter_h; } - if (key_code == GCKeyCodeKeyI) { return nes::app::key::letter_i; } - if (key_code == GCKeyCodeKeyJ) { return nes::app::key::letter_j; } - if (key_code == GCKeyCodeKeyK) { return nes::app::key::letter_k; } - if (key_code == GCKeyCodeKeyL) { return nes::app::key::letter_l; } - if (key_code == GCKeyCodeKeyM) { return nes::app::key::letter_m; } - if (key_code == GCKeyCodeKeyN) { return nes::app::key::letter_n; } - if (key_code == GCKeyCodeKeyO) { return nes::app::key::letter_o; } - if (key_code == GCKeyCodeKeyP) { return nes::app::key::letter_p; } - if (key_code == GCKeyCodeKeyQ) { return nes::app::key::letter_q; } - if (key_code == GCKeyCodeKeyR) { return nes::app::key::letter_r; } - if (key_code == GCKeyCodeKeyS) { return nes::app::key::letter_s; } - if (key_code == GCKeyCodeKeyT) { return nes::app::key::letter_t; } - if (key_code == GCKeyCodeKeyU) { return nes::app::key::letter_u; } - if (key_code == GCKeyCodeKeyV) { return nes::app::key::letter_v; } - if (key_code == GCKeyCodeKeyW) { return nes::app::key::letter_w; } - if (key_code == GCKeyCodeKeyX) { return nes::app::key::letter_x; } - if (key_code == GCKeyCodeKeyY) { return nes::app::key::letter_y; } - if (key_code == GCKeyCodeKeyZ) { return nes::app::key::letter_z; } - if (key_code == GCKeyCodeEscape) { return nes::app::key::escape; } - if (key_code == GCKeyCodeHyphen) { return nes::app::key::dash; } - if (key_code == GCKeyCodeEqualSign) { return nes::app::key::equal; } - if (key_code == GCKeyCodeDeleteOrBackspace) { return nes::app::key::backspace; } - if (key_code == GCKeyCodeTab) { return nes::app::key::tab; } - if (key_code == GCKeyCodeReturnOrEnter) { return nes::app::key::enter; } - if (key_code == GCKeyCodeSemicolon) { return nes::app::key::semicolon; } - if (key_code == GCKeyCodeQuote) { return nes::app::key::apostrophe; } - if (key_code == GCKeyCodeBackslash) { return nes::app::key::backslash; } - if (key_code == GCKeyCodeComma) { return nes::app::key::comma; } - if (key_code == GCKeyCodePeriod) { return nes::app::key::period; } - if (key_code == GCKeyCodeSlash) { return nes::app::key::slash; } - if (key_code == GCKeyCodeSpacebar) { return nes::app::key::space; } - if (key_code == GCKeyCodeCapsLock) { return nes::app::key::caps_lock; } - if (key_code == GCKeyCodeOpenBracket) { return nes::app::key::bracket_open; } - if (key_code == GCKeyCodeCloseBracket) { return nes::app::key::bracket_close; } - if (key_code == GCKeyCodeLeftControl) { return nes::app::key::control_left; } - if (key_code == GCKeyCodeRightControl) { return nes::app::key::control_right; } - if (key_code == GCKeyCodeLeftShift) { return nes::app::key::shift_left; } - if (key_code == GCKeyCodeRightShift) { return nes::app::key::shift_right; } - if (key_code == GCKeyCodeLeftAlt) { return nes::app::key::alt_left; } - if (key_code == GCKeyCodeRightAlt) { return nes::app::key::alt_right; } - if (key_code == GCKeyCodeLeftArrow) { return nes::app::key::arrow_left; } - if (key_code == GCKeyCodeRightArrow) { return nes::app::key::arrow_right; } - if (key_code == GCKeyCodeUpArrow) { return nes::app::key::arrow_up; } - if (key_code == GCKeyCodeDownArrow) { return nes::app::key::arrow_down; } - if (key_code == GCKeyCodeF1) { return nes::app::key::f1; } - if (key_code == GCKeyCodeF2) { return nes::app::key::f2; } - if (key_code == GCKeyCodeF3) { return nes::app::key::f3; } - if (key_code == GCKeyCodeF4) { return nes::app::key::f4; } - if (key_code == GCKeyCodeF5) { return nes::app::key::f5; } - if (key_code == GCKeyCodeF6) { return nes::app::key::f6; } - if (key_code == GCKeyCodeF7) { return nes::app::key::f7; } - if (key_code == GCKeyCodeF8) { return nes::app::key::f8; } - if (key_code == GCKeyCodeF9) { return nes::app::key::f9; } - if (key_code == GCKeyCodeF10) { return nes::app::key::f10; } - if (key_code == GCKeyCodeF11) { return nes::app::key::f11; } - if (key_code == GCKeyCodeF12) { return nes::app::key::f12; } - - return std::nullopt; - } - }; - - class input_device_controller_impl final : public nes::app::input_device_controller - { - GCExtendedGamepad* profile_; - - public: - explicit input_device_controller_impl(GCExtendedGamepad* profile) - : profile_{ profile } - { - } - - auto is_reliable() const -> bool override { return true; } - auto get_controller() const -> GCController* { return [profile_ controller]; } - auto get_name() const -> std::string_view override { return [[get_controller() vendorName] UTF8String]; } - - auto get_index() const -> index override - { - switch ([get_controller() playerIndex]) - { - case GCControllerPlayerIndex1: - return index::player_1; - case GCControllerPlayerIndex2: - return index::player_2; - case GCControllerPlayerIndex3: - case GCControllerPlayerIndex4: - case GCControllerPlayerIndexUnset: - break; - } - return index::unused; - } - - auto set_index(index idx) -> void override - { - switch (idx) - { - case index::unused: - [get_controller() setPlayerIndex:GCControllerPlayerIndexUnset]; - break; - case index::player_1: - [get_controller() setPlayerIndex:GCControllerPlayerIndex1]; - break; - case index::player_2: - [get_controller() setPlayerIndex:GCControllerPlayerIndex2]; - break; - } - } - - auto read_buttons() -> nes::sys::button_mask override - { - auto res = nes::sys::button_mask{}; - if ([[[profile_ dpad] up] isPressed]) { res.add(nes::sys::buttons::up); } - if ([[[profile_ dpad] down] isPressed]) { res.add(nes::sys::buttons::down); } - if ([[[profile_ dpad] left] isPressed]) { res.add(nes::sys::buttons::left); } - if ([[[profile_ dpad] right] isPressed]) { res.add(nes::sys::buttons::right); } - if ([[profile_ buttonA] isPressed]) { res.add(nes::sys::buttons::a); } - if ([[profile_ buttonB] isPressed]) { res.add(nes::sys::buttons::b); } - if ([[profile_ leftTrigger] isPressed]) { res.add(nes::sys::buttons::select); } - if ([[profile_ rightTrigger] isPressed]) { res.add(nes::sys::buttons::start); } - return res; - } - }; - - class input_device_controller_serial : public nes::app::input_device_controller - { - char const* path_; - int fd_{ 0 }; - nes::sys::button_mask current_{}; - index index_{ index::unused }; - - public: - explicit input_device_controller_serial(char const* path) - : path_{ path } - { - fd_ = open(path, O_RDONLY | O_NOCTTY | O_NDELAY | O_NONBLOCK); - if (fd_ == -1) - { - perror("open controller"); - return; - } - - if (!setup()) { close(); } - } - - ~input_device_controller_serial() override - { - close(); - } - - auto read_buttons() -> nes::sys::button_mask override - { - if (fd_ == -1) { return nes::sys::button_mask{}; } - - // Poll for any updates until we can't read anymore. - while (true) - { - auto buf = nes::u8{}; - auto res = read(fd_, &buf, 1); - if (res == -1) - { - if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } - - perror("read from controller"); - close(); - break; - } - - current_ = nes::sys::button_mask::from_raw_value(buf); - } - - return current_; - } - - auto is_reliable() const -> bool override { return fd_ != -1; } - auto get_name() const -> std::string_view override { return path_; } - auto set_index(index const index) -> void override { index_ = index; } - auto get_index() const -> index override { return index_; } - - private: - auto close() -> void - { - if (fd_ != -1) - { - ::close(fd_); - fd_ = -1; - } - } - - auto setup() -> bool - { - if (ioctl(fd_, TIOCEXCL) == -1) - { - perror("ioctl for controller"); - return false; - } - - auto attr = termios{}; - if (tcgetattr(fd_, &attr) == -1) - { - perror("tcgetattr for controller"); - return false; - } - - cfsetispeed(&attr, B57600); - cfsetospeed(&attr, B57600); - attr.c_cflag = attr.c_cflag | CLOCAL | CREAD; - attr.c_cflag = (attr.c_cflag & static_cast(~CSIZE)) | CS8; - - if (tcsetattr(fd_, TCSANOW, &attr) == -1) - { - perror("tcsetattr for controller"); - return false; - } - - return true; - } - }; - - class file_browser_impl final : public nes::app::file_browser - { - nes::path_buffer<256> path_; - DIR* it_{ nullptr }; - nes::u32 it_pos_{ 0 }; - - public: - explicit file_browser_impl() - { - reopen_dir(); - } - - ~file_browser_impl() override - { - closedir(it_); - } - - auto get_path() const -> nes::path_view override - { - return path_.get_components(); - } - - auto seek(nes::u32 const position) -> void override - { - if (it_pos_ > position) - { - reopen_dir(); - } - - while (it_pos_ < position) - { - if (!read_next(nullptr)) { break; } - } - } - - auto read_next(item* out_item) -> bool override - { - if (!it_) { return false; } - - while (true) - { - errno = 0; - auto entry = readdir(it_); - if (!entry) - { - if (errno) - { - perror("readdir"); - std::abort(); - } - - return false; - } - - auto const name = std::string_view{ entry->d_name }; - if (name.length() > max_name_length) { continue; } - if (name[0] == '.') { continue; } - - auto type = item_type::directory; - if (entry->d_type == DT_DIR) { type = item_type::directory; } - else if (entry->d_type == DT_REG) { type = item_type::file; } - else { continue; } - - it_pos_ += 1; - if (out_item) { *out_item = item{ type, name }; } - return true; - } - } - - auto navigate_up() -> void override - { - if (!path_.pop()) { return; } - - reopen_dir(); - } - - auto navigate(std::string_view const item) -> void override - { - if (!path_.push(item)) { std::abort(); } - - reopen_dir(); - } - - auto load(std::string_view const item, nes::span const buffer) -> nes::u32 override - { - auto file_path = path_; - if (!file_path.push(item)) { std::abort(); } - - auto const path = std::string{ file_path.get_path() }; - auto const fd = open(path.c_str(), O_RDONLY); - if (fd == -1) - { - perror("open"); - std::abort(); - } - - auto const res = read(fd, buffer.get_data(), buffer.get_length()); - if (res < 0) - { - perror("read"); - std::abort(); - } - - auto remaining = char{}; - if (read(fd, &remaining, 1) != 0) - { - std::cerr << "Expected EOF" << std::endl; - std::abort(); - } - - close(fd); - - return static_cast(res); - } - - private: - auto reopen_dir() -> void - { - if (it_) { closedir(it_); } - - auto const path = std::string{ path_.get_path() }; - it_ = opendir(path.c_str()); - if (!it_) - { - perror("opendir"); - std::abort(); - } - it_pos_ = 0; - } - }; + constexpr auto const serial_controller_path = "/dev/cu.usbserial-FTB6SPL3"; } // namespace @implementation Scene { - std::unique_ptr _display; - std::unique_ptr _fileBrowser; - std::unique_ptr _keyboard; - std::unique_ptr _serialController; - std::vector> _controllers; + std::unique_ptr _display; + std::unique_ptr _fileBrowser; + std::unique_ptr _serialController; + std::unique_ptr _keyboard; + std::vector> _controllers; std::unique_ptr _app; std::optional _lastTimestamp; } @@ -512,10 +43,10 @@ namespace [self setScaleMode:SKSceneScaleModeAspectFit]; [self addChild:node]; - _display = std::make_unique(node); - _fileBrowser = std::make_unique(); - _keyboard = std::make_unique([[GCKeyboard coalescedKeyboard] keyboardInput]); - _serialController = std::make_unique("/dev/cu.usbserial-FTB6SPL3"); + _display = std::make_unique(node); + _fileBrowser = std::make_unique(); + _serialController = std::make_unique(serial_controller_path); + _keyboard = std::make_unique([[GCKeyboard coalescedKeyboard] keyboardInput]); _app = std::make_unique(*_display, *_keyboard, *_fileBrowser); _app->add_controller(*_serialController); @@ -525,7 +56,7 @@ namespace auto gamepad = [controller extendedGamepad]; if (gamepad) { - auto adapter = std::make_unique(gamepad); + auto adapter = std::make_unique(gamepad); _app->add_controller(*adapter); _controllers.emplace_back(std::move(adapter)); } @@ -582,7 +113,7 @@ namespace auto gamepad = [controller extendedGamepad]; if (gamepad) { - auto adapter = std::make_unique(gamepad); + auto adapter = std::make_unique(gamepad); _app->add_controller(*adapter); _controllers.emplace_back(std::move(adapter)); } diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc index b528e61..6b7253e 100644 --- a/lib/nes/app/application.cc +++ b/lib/nes/app/application.cc @@ -46,7 +46,7 @@ namespace nes::app // display_proxy::switch_buffers). console_->step(elapsed_time); - if (console_->get_status() != sys::status::success) + if (console_->get_status() != status::success) { std::cerr << "Invalid state: " << to_string(console_->get_status()) << std::endl; std::abort(); @@ -93,10 +93,15 @@ namespace nes::app case action::type::launch_game: { u8 buffer[sys::cartridge::max_file_size]; - auto const length = file_browser_.load(a.get_file_name(), buffer); + auto length = u32{ 0 }; + if (auto const s = file_browser_.load(a.get_file_name(), buffer, &length); s != status::success) + { + std::cerr << "Unable to open cartridge file: " << to_string(console_->get_status()) << std::endl; + std::abort(); + } - console_.emplace(display_, span{ buffer, length }); - if (console_->get_status() != sys::status::success) + console_.emplace(display_, span{ buffer, length }); + if (console_->get_status() != status::success) { std::cerr << "Unable to load cartridge: " << to_string(console_->get_status()) << std::endl; std::abort(); @@ -105,6 +110,11 @@ namespace nes::app display_.screen = nullptr; break; } + case action::type::show_error: + { + // TODO + break; + } } } } // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/file-browser.hh b/lib/nes/app/file-browser.hh index 6ce55aa..ef5343c 100644 --- a/lib/nes/app/file-browser.hh +++ b/lib/nes/app/file-browser.hh @@ -2,6 +2,8 @@ #include "nes/common/types.hh" #include "nes/common/span.hh" +#include "nes/common/status.hh" +#include namespace nes { @@ -10,6 +12,7 @@ namespace nes namespace nes::app { + /// A file browser that has a current directory whose items are iterated. class file_browser { public: @@ -46,12 +49,23 @@ namespace nes::app auto operator=(file_browser const&) -> file_browser& = delete; auto operator=(file_browser&&) -> file_browser& = delete; + /// Get the path components of the current working directory. virtual auto get_path() const -> path_view = 0; + + /// Go to directory entry at the given index. virtual auto seek(u32) -> void = 0; + + /// Read the directory entry at the current index. virtual auto read_next(item* out_item) -> bool = 0; - virtual auto navigate_up() -> void = 0; - virtual auto navigate(std::string_view) -> void = 0; - virtual auto load(std::string_view, span) -> u32 = 0; + + /// Change the current working directory to the parent directory. + virtual auto navigate_up() -> status = 0; + + /// Change the current working directory to an item. + virtual auto navigate(std::string_view) -> status = 0; + + /// Load a file in the current working directory into memory. + virtual auto load(std::string_view, span, u32* out_length) -> status = 0; protected: explicit file_browser() = default; diff --git a/lib/nes/app/input/input-buffer.cc b/lib/nes/app/input/input-buffer.cc index e8da51c..a9ccf2b 100644 --- a/lib/nes/app/input/input-buffer.cc +++ b/lib/nes/app/input/input-buffer.cc @@ -23,7 +23,7 @@ namespace nes::app auto it = event_queue_.begin(); if (it == event_queue_.end()) { return input_event{}; } - auto const event = *it; + auto const event = it; auto const k = get_key(*event); auto const pressed = event->pressed; event_queue_.remove(it); diff --git a/lib/nes/app/input/input-manager.cc b/lib/nes/app/input/input-manager.cc index 1af0f5b..ceb6690 100644 --- a/lib/nes/app/input/input-manager.cc +++ b/lib/nes/app/input/input-manager.cc @@ -68,8 +68,7 @@ namespace nes::app // This input is not assigned -> use the next available controller if available. while (it != controllers_.end()) { - auto controller = *it; - if (controller->is_reliable() && controller->get_index() == input_device_controller::index::unused) + if (it->is_reliable() && it->get_index() == input_device_controller::index::unused) { break; } @@ -79,9 +78,9 @@ namespace nes::app if (it != controllers_.end()) { // We've found an unused controller -> assign it. - inputs_[i].controller = *it; + inputs_[i].controller = &*it; inputs_[i].controller->set_index(static_cast(i)); - inputs_[i].device = *it; + inputs_[i].device = &*it; } else { diff --git a/lib/nes/app/ui/action.hh b/lib/nes/app/ui/action.hh index b701606..66f77f8 100644 --- a/lib/nes/app/ui/action.hh +++ b/lib/nes/app/ui/action.hh @@ -1,5 +1,6 @@ #pragma once +#include "nes/common/status.hh" #include namespace nes::app @@ -16,28 +17,41 @@ namespace nes::app go_to_settings, go_to_help, launch_game, + show_error, }; private: type type_{ type::none }; std::string_view file_name_; + status error_{ status::success }; public: explicit action() = default; - static auto go_to_title() -> action { return action{ type::go_to_title, "" }; } - static auto go_to_browser() -> action { return action{ type::go_to_browser, "" }; } - static auto go_to_settings() -> action { return action{ type::go_to_settings, "" }; } - static auto go_to_help() -> action { return action{ type::go_to_help, "" }; } - static auto launch_game(std::string_view const file_name) -> action { return action{ type::launch_game, file_name }; } + static auto go_to_title() -> action { return action{ type::go_to_title, "", status::success }; } + static auto go_to_browser() -> action { return action{ type::go_to_browser, "", status::success }; } + static auto go_to_settings() -> action { return action{ type::go_to_settings, "", status::success }; } + static auto go_to_help() -> action { return action{ type::go_to_help, "", status::success }; } + + static auto launch_game(std::string_view const file_name) -> action + { + return action{ type::launch_game, file_name, status::success }; + } + + static auto show_error(status const error) + { + return action{ type::show_error, "", error }; + } auto get_type() const -> type { return type_; } auto get_file_name() const -> std::string_view { return file_name_; } + auto get_error() const -> status { return error_; } private: - explicit action(type const type, std::string_view const file_name) + explicit action(type const type, std::string_view const file_name, status const error) : type_{ type } , file_name_{ file_name } + , error_{ error } { } }; diff --git a/lib/nes/app/ui/screen-browser.cc b/lib/nes/app/ui/screen-browser.cc index 6a18d3c..14ddc23 100644 --- a/lib/nes/app/ui/screen-browser.cc +++ b/lib/nes/app/ui/screen-browser.cc @@ -99,7 +99,10 @@ namespace nes::app { if (navigation_steps_ > 0) { - file_browser_.navigate_up(); + if (auto const s = file_browser_.navigate_up(); s != status::success) + { + return action::show_error(s); + } refresh(); navigation_steps_ -= 1; } @@ -115,22 +118,25 @@ namespace nes::app { if (selected->is_parent) { - file_browser_.navigate_up(); + if (auto const s = file_browser_.navigate_up(); s != status::success) + { + return action::show_error(s); + } refresh(); if (navigation_steps_ > 0) { navigation_steps_ -= 1; } } - else + else if (selected->data.get_type() == file_browser::item_type::directory) { - switch (selected->data.get_type()) + if (auto const s = file_browser_.navigate(selected->data.get_name()); s != status::success) { - case file_browser::item_type::directory: - file_browser_.navigate(selected->data.get_name()); - refresh(); - navigation_steps_ += 1; - break; - case file_browser::item_type::file: - return action::launch_game(selected->data.get_name()); + return action::show_error(s); } + refresh(); + navigation_steps_ += 1; + } + else if (selected->data.get_type() == file_browser::item_type::file) + { + return action::launch_game(selected->data.get_name()); } } } diff --git a/lib/nes/common/CMakeLists.txt b/lib/nes/common/CMakeLists.txt index a7c1d76..a5703ff 100644 --- a/lib/nes/common/CMakeLists.txt +++ b/lib/nes/common/CMakeLists.txt @@ -13,4 +13,5 @@ target_sources( path-view.cc path-builder.hh path-builder.cc - span.hh) \ No newline at end of file + span.hh + status.hh) \ No newline at end of file diff --git a/lib/nes/common/box.hh b/lib/nes/common/box.hh index 3a65613..7bf2848 100644 --- a/lib/nes/common/box.hh +++ b/lib/nes/common/box.hh @@ -1,6 +1,7 @@ #pragma once #include "nes/common/types.hh" +#include "nes/common/debug.hh" namespace nes { @@ -28,10 +29,30 @@ namespace nes auto operator=(box const&) -> box& = delete; auto operator=(box&&) -> box& = delete; - auto operator*() -> T& { return *get(); } - auto operator*() const -> T const& { return *get(); } - auto operator->() -> T* { return get(); } - auto operator->() const -> T const* { return get(); } + auto operator*() -> T& + { + NES_ASSERT(has_value() && "expected box to be non-empty"); + return *get(); + } + + auto operator*() const -> T const& + { + NES_ASSERT(has_value() && "expected box to be non-empty"); + return *get(); + } + + auto operator->() -> T* + { + NES_ASSERT(has_value() && "expected box to be non-empty"); + return get(); + } + + auto operator->() const -> T const* + { + NES_ASSERT(has_value() && "expected box to be non-empty"); + return get(); + } + operator bool() const { return has_value(); } auto has_value() const -> bool { return initialized_; } diff --git a/lib/nes/common/debug.hh b/lib/nes/common/debug.hh index 409d55a..30370e9 100644 --- a/lib/nes/common/debug.hh +++ b/lib/nes/common/debug.hh @@ -5,11 +5,25 @@ #include #define NES_DEBUG_LOG(name, ...) \ - spdlog::info("[" #name "] " __VA_ARGS__) + spdlog::info("[" #name "] " __VA_ARGS__) #else #define NES_DEBUG_LOG(name, fmt, ...) \ - (void)0 + (void)0 + +#endif + +#ifdef NDEBUG + +#define NES_ASSERT(...) \ + (void)0 + +#else + +#include + +#define NES_ASSERT(...) \ + assert(__VA_ARGS__) #endif \ No newline at end of file diff --git a/lib/nes/common/list.hh b/lib/nes/common/list.hh index b1fddf0..46f7027 100644 --- a/lib/nes/common/list.hh +++ b/lib/nes/common/list.hh @@ -1,5 +1,7 @@ #pragma once +#include "nes/common/debug.hh" + namespace nes { /// An intrusive doubly-linked list. @@ -39,15 +41,23 @@ namespace nes auto operator==(iterator const& other) -> bool { return current_ == other.current_; } auto operator!=(iterator const& other) -> bool { return current_ != other.current_; } - auto operator*() const -> T* { return current_; } + auto operator*() const -> T& + { + NES_ASSERT(current_ != nullptr && "invalid iterator"); + return *current_; + } + + auto operator->() const -> T* + { + NES_ASSERT(current_ != nullptr && "invalid iterator"); + return current_; + } auto operator++() -> iterator& { - if (current_) - { - node& current_node = *current_; - current_ = current_node.next_; - } + NES_ASSERT(current_ != nullptr && "invalid iterator"); + node& current_node = *current_; + current_ = current_node.next_; return *this; } @@ -126,7 +136,7 @@ namespace nes /// Remove an item from the list by its iterator. auto remove(iterator it) -> bool { - if (it != end()) { return remove(**it); } + if (it != end()) { return remove(*it); } return false; } }; diff --git a/lib/nes/common/path-builder.cc b/lib/nes/common/path-builder.cc index 794d565..b66226f 100644 --- a/lib/nes/common/path-builder.cc +++ b/lib/nes/common/path-builder.cc @@ -9,28 +9,28 @@ namespace nes if (buffer_.get_length() > 0) { buffer_[length_++] = '/'; } } - auto path_builder::push(std::string_view component) -> bool + auto path_builder::push(std::string_view component) -> status { // Trim leading and trailing slashes while (!component.empty() && component[0] == '/') { component = component.substr(1); } while (!component.empty() && component[component.length() - 1] == '/') { component = component.substr(0, component.length() - 1); } - if (component.empty() || std::find(component.begin(), component.end(), '/') != component.end()) { return false; } + if (component.empty() || std::find(component.begin(), component.end(), '/') != component.end()) { return status::error_invalid_path; } // Check if there is enough remaining capacity for "/" auto const add_leading_slash = length_ == 0 || buffer_[length_ - 1] != '/'; auto const length = component.length() + (add_leading_slash ? 1 : 0); - if (length_ + length > buffer_.get_length()) { return false; } + if (length_ + length > buffer_.get_length()) { return status::error_buffer_overflow; } // Append "/" if (add_leading_slash) { buffer_[length_++] = '/'; } for (auto i = u32{ 0 }; i < component.length(); ++i) { buffer_[length_++] = component[i]; } - return true; + return status::success; } - auto path_builder::pop() -> bool + auto path_builder::pop() -> status { - if (length_ == 0 || buffer_[length_ - 1] == '/') { return false; } + if (length_ == 0 || buffer_[length_ - 1] == '/') { return status::error_invalid_path; } // Find the start of the last component. auto last_start = length_ - 1; @@ -41,6 +41,6 @@ namespace nes // Drop everything including the trailing slash. length_ = last_start; - return true; + return status::success; } } // namespace nes \ No newline at end of file diff --git a/lib/nes/common/path-builder.hh b/lib/nes/common/path-builder.hh index b64fb28..e7980ea 100644 --- a/lib/nes/common/path-builder.hh +++ b/lib/nes/common/path-builder.hh @@ -3,6 +3,7 @@ #include "nes/common/types.hh" #include "nes/common/span.hh" #include "nes/common/path-view.hh" +#include "nes/common/status.hh" #include namespace nes @@ -10,6 +11,9 @@ namespace nes /// A mutable path with a fixed-length buffer. class path_builder { + template + friend class path_buffer; + span buffer_; u32 length_{ 0 }; @@ -19,8 +23,8 @@ namespace nes auto get_components() const -> path_view { return path_view{ get_path() }; } auto get_path() const -> std::string_view { return std::string_view{ buffer_.get_data(), length_ }; } - auto push(std::string_view) -> bool; - auto pop() -> bool; + auto push(std::string_view) -> status; + auto pop() -> status; }; /// A path builder with a buffer embedded into it. @@ -34,5 +38,18 @@ namespace nes : path_builder{ buffer_ } { } + + path_buffer(path_buffer const& other) + : path_builder{ buffer_ } + { + std::copy_n(other.buffer_, Capacity, buffer_); + length_ = other.length_; + } + + auto operator=(path_buffer const& other) + { + std::copy_n(other.buffer_, Capacity, buffer_); + length_ = other.length_; + } }; } // namespace nes \ No newline at end of file diff --git a/lib/nes/common/path-view.cc b/lib/nes/common/path-view.cc index 47f198b..743c060 100644 --- a/lib/nes/common/path-view.cc +++ b/lib/nes/common/path-view.cc @@ -1,10 +1,12 @@ #include "nes/common/path-view.hh" +#include "nes/common/debug.hh" namespace nes { auto path_view::iterator::operator*() const -> std::string_view { - return current_ ? std::string_view{ current_, component_length_ } : ""; + NES_ASSERT(current_ != nullptr && component_length_ > 0 && "invalid iterator"); + return std::string_view{ current_, component_length_ }; } auto path_view::iterator::operator++() -> iterator& diff --git a/lib/nes/common/span.hh b/lib/nes/common/span.hh index 16eb129..6779c37 100644 --- a/lib/nes/common/span.hh +++ b/lib/nes/common/span.hh @@ -1,6 +1,7 @@ #pragma once #include "nes/common/types.hh" +#include "nes/common/debug.hh" namespace nes { @@ -21,6 +22,9 @@ namespace nes { using type = T; }; + + template + using remove_const_type = typename remove_const::type; } // namespace detail /// Implementation of a container similar to std::span in C++20. @@ -54,7 +58,7 @@ namespace nes static_assert(Length == ActualLength || Length == detail::span_dynamic_length); } - span(span::type, Length> const other) + span(span, Length> const other) : span{ other.data_, other.length_, {} } { } @@ -65,12 +69,31 @@ namespace nes auto begin() const -> iterator { return data_; } auto end() const -> iterator { return data_ + length_; } - auto subspan(u32 const first, u32 const length) const -> span { return span{ data_ + first, length }; } - auto subspan(u32 const first) const -> span { return subspan(length_ - first); } + auto subspan(u32 const first, u32 const length) const -> span + { + NES_ASSERT(first + length <= get_length() && "subspan out of bounds"); + return span{ data_ + first, length }; + } + + auto subspan(u32 const first) const -> span + { + NES_ASSERT(first <= get_length() && "subspan out of bounds"); + return subspan(length_ - first); + } + template - auto subspan(u32 const first) const -> span { return span{ data_ + first, OtherLength, {} }; } + auto subspan(u32 const first) const -> span + { + static_assert(OtherLength < Length); + NES_ASSERT(first + OtherLength <= get_length() && "subspan out of bounds"); + return span{ data_ + first, OtherLength, {} }; + } - auto operator[](u32 const index) const -> T& { return data_[index]; } + auto operator[](u32 const index) const -> T& + { + NES_ASSERT(index < get_length() && "index out of bounds"); + return data_[index]; + } private: explicit span(T* data, u32 const length, detail::span_unchecked) diff --git a/lib/nes/common/status.hh b/lib/nes/common/status.hh new file mode 100644 index 0000000..4b23b60 --- /dev/null +++ b/lib/nes/common/status.hh @@ -0,0 +1,42 @@ +#pragma once + +namespace nes +{ + /// A status code. + enum class status + { + success, + error_system_error, + error_invalid_ines_data, + error_unsupported_mapper, + error_unsupported_instruction, + error_buffer_overflow, + error_invalid_path, + error_invalid_format_string, + }; + + constexpr auto to_string(status const status) -> char const* + { + switch (status) + { + case status::success: + return "Success"; + case status::error_system_error: + return "System error"; + case status::error_invalid_ines_data: + return "Invalid iNES data"; + case status::error_unsupported_mapper: + return "Unsupported mapper"; + case status::error_unsupported_instruction: + return "Unsupported instruction"; + case status::error_buffer_overflow: + return "Buffer overflow"; + case status::error_invalid_path: + return "Invalid path"; + case status::error_invalid_format_string: + return "Invalid format string"; + } + + return "(invalid)"; + } +} // namespace nes \ No newline at end of file diff --git a/lib/nes/common/string-builder.cc b/lib/nes/common/string-builder.cc index 087f0d4..7747aaa 100644 --- a/lib/nes/common/string-builder.cc +++ b/lib/nes/common/string-builder.cc @@ -7,70 +7,73 @@ namespace nes { } - auto string_builder::append_char(char const value) -> string_builder& + auto string_builder::append_char(char const value) -> status { - if (length_ == buffer_.get_length()) - { - is_good_ = false; - return *this; - } + if (length_ == buffer_.get_length()) { return status::error_buffer_overflow; } buffer_[length_] = value; length_ += 1; - return *this; + return status::success; } - auto string_builder::append_string(std::string_view const value) -> string_builder& + auto string_builder::append_string(std::string_view const value) -> status { + auto copy = *this; for (auto const c : value) { - append_char(c); + if (auto const s = copy.append_char(c); s != status::success) { return s; } } - return *this; + *this = copy; + return status::success; } - auto string_builder::append_int(i64 value, number_format const format) -> string_builder& + auto string_builder::append_int(i64 value, number_format const format) -> status { + auto copy = *this; if (value < 0) { - append_char('-'); + if (auto const s = copy.append_char('-'); s != status::success) { return s; } value += 1; // to prevent lossy conversion to unsigned auto abs_value = static_cast(-value); abs_value += 1; // to correct the previous offset - append_int(abs_value, format); + if (auto const s = copy.append_int(abs_value, format); s != status::success) { return s; } } else { - append_int(static_cast(value), format); + if (auto const s = copy.append_int(static_cast(value), format); s != status::success) { return s; } } - return *this; + *this = copy; + return status::success; } - auto string_builder::append_int(u64 value, number_format const format) -> string_builder& + auto string_builder::append_int(u64 value, number_format const format) -> status { constexpr auto max_string_length = u32{ 64 }; // worst case is a 64-bit integer in binary representation constexpr auto digits = "0123456789abcdef"; + auto copy = *this; auto base = u64{ 10 }; + auto prefix = ""; switch (format) { case number_format::binary: - append_string("0b"); + prefix = "0b"; base = 2; break; case number_format::octal: - append_string("0"); + prefix = "0"; base = 8; break; case number_format::decimal: break; case number_format::hexadecimal: - append_string("0x"); + prefix = "0x"; base = 16; break; } + if (auto const s = copy.append_string(prefix); s != status::success) { return s; } char buf[max_string_length + 1] = {}; // null-terminated auto pos = max_string_length; @@ -89,19 +92,21 @@ namespace nes buf[pos] = '0'; } - append_string(&buf[pos]); - return *this; + if (const auto s = copy.append_string(&buf[pos]); s != status::success) { return s; } + + *this = copy; + return status::success; } - auto string_builder::append_bool(bool const value) -> string_builder& + auto string_builder::append_bool(bool const value) -> status { - append_string(value ? "true" : "false"); - return *this; + return append_string(value ? "true" : "false"); } - auto string_builder::append_format( - std::string_view const fmt, format_arg const* args, u32 const arg_count) -> string_builder& + auto string_builder::append_format(std::string_view const fmt, format_arg const* args, u32 const arg_count) -> status { + auto copy = *this; + // Go through the format string, inserting the arguments for the templates as necessary. auto current_arg = u32{ 0 }; for (auto i = u32{ 0 }; i < fmt.length(); ++i) @@ -112,7 +117,7 @@ namespace nes if (fmt[i] == '{') { // escaped '{' - append_char('{'); + if (auto const s = copy.append_char('{'); s != status::success) { return s; } } else { @@ -128,21 +133,20 @@ namespace nes if (fmt[i] != '}') { // unmatched '{' - is_good_ = false; - break; + return status::error_invalid_format_string; } // end of template auto const p = std::string_view{ params, params_length }; if (current_arg < arg_count) { - append_format_arg(args[current_arg], p); + if (auto const s = copy.append_format_arg(args[current_arg], p); s != status::success) { return s; } current_arg += 1; } else { // missing argument in parameter pack - is_good_ = false; + return status::error_invalid_format_string; } } } @@ -153,39 +157,41 @@ namespace nes if (fmt[i] == '}') { // escaped '}' - append_char('}'); + if (auto const s = copy.append_char('}'); s != status::success) { return s; } } else { // unmatched '}' - is_good_ = false; - break; + return status::error_invalid_format_string; } } else { // normal character in the template string - append_char(fmt[i]); + if (auto const s = copy.append_char(fmt[i]); s != status::success) { return s; } } } if (current_arg < arg_count) { // not all supplied arguments were used - is_good_ = false; + return status::error_invalid_format_string; } - return *this; + *this = copy; + return status::success; } - auto string_builder::append_format_arg(format_arg const arg, std::string_view const params) -> string_builder& + auto string_builder::append_format_arg(format_arg const arg, std::string_view const params) -> status { auto const parse_params = [&] { if (!params.empty()) { - is_good_ = false; + return status::error_invalid_format_string; } + + return status::success; }; auto const parse_params_integer = [&](number_format& out_format) @@ -208,47 +214,43 @@ namespace nes } else { - out_format = number_format::decimal; - is_good_ = false; + return status::error_invalid_format_string; } + + return status::success; }; switch (arg.type) { case format_arg_type::string: { - parse_params(); - append_string(arg.string); - break; + if (auto const s = parse_params(); s != status::success) { return s; } + return append_string(arg.string); } case format_arg_type::character: { - parse_params(); - append_char(arg.character); - break; + if (auto const s = parse_params(); s != status::success) { return s; } + return append_char(arg.character); } case format_arg_type::signed_integer: { auto format = number_format{}; - parse_params_integer(format); - append_int(arg.signed_integer, format); - break; + if (auto const s = parse_params_integer(format); s != status::success) { return s; } + return append_int(arg.signed_integer, format); } case format_arg_type::unsigned_integer: { auto format = number_format{}; - parse_params_integer(format); - append_int(arg.unsigned_integer, format); - break; + if (auto const s = parse_params_integer(format); s != status::success) { return s; } + return append_int(arg.unsigned_integer, format); } case format_arg_type::boolean: { - parse_params(); - append_bool(arg.character); - break; + if (auto const s = parse_params(); s != status::success) { return s; } + return append_bool(arg.character); } } - return *this; + return status::error_invalid_format_string; } } // namespace nes \ No newline at end of file diff --git a/lib/nes/common/string-builder.hh b/lib/nes/common/string-builder.hh index 256c267..0298b8e 100644 --- a/lib/nes/common/string-builder.hh +++ b/lib/nes/common/string-builder.hh @@ -2,6 +2,7 @@ #include "nes/common/types.hh" #include "nes/common/span.hh" +#include "nes/common/status.hh" #include namespace nes @@ -17,9 +18,11 @@ namespace nes /// Builds a string into a fixed-length buffer. class string_builder { + template + friend class string_buffer; + span buffer_; u32 length_{ 0 }; - bool is_good_{ true }; enum class format_arg_type { @@ -65,14 +68,11 @@ namespace nes /// Get the resulting string. auto get_result() const -> std::string_view { return std::string_view{ buffer_.get_data(), length_ }; } - /// Returns false after an invalid format string or buffer overflow. - auto is_good() const -> bool { return is_good_; } - - auto append_char(char) -> string_builder&; - auto append_string(std::string_view) -> string_builder&; - auto append_int(i64, number_format = number_format::decimal) -> string_builder&; - auto append_int(u64, number_format = number_format::decimal) -> string_builder&; - auto append_bool(bool) -> string_builder&; + auto append_char(char) -> status; + auto append_string(std::string_view) -> status; + auto append_int(i64, number_format = number_format::decimal) -> status; + auto append_int(u64, number_format = number_format::decimal) -> status; + auto append_bool(bool) -> status; /// Print a templated string. /// @@ -82,7 +82,7 @@ namespace nes /// /// Curly braces can be escaped by repeating them twice like {{this}}. template - auto append_format(std::string_view const fmt, Args... args) -> string_builder& + auto append_format(std::string_view const fmt, Args... args) -> status { constexpr u32 arg_count = sizeof...(Args); // Add another argument at the end so we don't have an empty array expression. @@ -92,8 +92,8 @@ namespace nes } private: - auto append_format(std::string_view fmt, format_arg const* args, u32 arg_count) -> string_builder&; - auto append_format_arg(format_arg, std::string_view params) -> string_builder&; + auto append_format(std::string_view fmt, format_arg const* args, u32 arg_count) -> status; + auto append_format_arg(format_arg, std::string_view params) -> status; }; /// A string builder with a buffer embedded into it. @@ -107,5 +107,18 @@ namespace nes : string_builder{ buffer_ } { } + + string_buffer(string_buffer const& other) + : string_builder{ buffer_ } + { + std::copy_n(other.buffer_, Capacity, buffer_); + length_ = other.length_; + } + + auto operator=(string_buffer const& other) + { + std::copy_n(other.buffer_, Capacity, buffer_); + length_ = other.length_; + } }; } // namespace nes \ No newline at end of file diff --git a/lib/nes/sys/cartridge.cc b/lib/nes/sys/cartridge.cc index 14fff3d..0729107 100644 --- a/lib/nes/sys/cartridge.cc +++ b/lib/nes/sys/cartridge.cc @@ -1,6 +1,5 @@ #include "nes/sys/cartridge.hh" -#include "nes/sys/types/status.hh" -#include +#include "nes/common/status.hh" #include namespace nes::sys diff --git a/lib/nes/sys/cartridge.hh b/lib/nes/sys/cartridge.hh index 5383b56..4534e1e 100644 --- a/lib/nes/sys/cartridge.hh +++ b/lib/nes/sys/cartridge.hh @@ -1,7 +1,7 @@ #pragma once -#include "nes/sys/types/status.hh" #include "nes/sys/mapper.hh" +#include "nes/common/status.hh" #include "nes/common/types.hh" #include "nes/common/span.hh" diff --git a/lib/nes/sys/cpu.hh b/lib/nes/sys/cpu.hh index 5d6466f..99753a7 100644 --- a/lib/nes/sys/cpu.hh +++ b/lib/nes/sys/cpu.hh @@ -2,7 +2,7 @@ #include "nes/sys/types/cycle-count.hh" #include "nes/sys/types/address.hh" -#include "nes/sys/types/status.hh" +#include "nes/common/status.hh" #include "nes/common/types.hh" namespace nes::sys diff --git a/lib/nes/sys/mapper.hh b/lib/nes/sys/mapper.hh index 9ad88e2..48363dd 100644 --- a/lib/nes/sys/mapper.hh +++ b/lib/nes/sys/mapper.hh @@ -1,7 +1,7 @@ #pragma once #include "nes/sys/types/address.hh" -#include "nes/sys/types/status.hh" +#include "nes/common/status.hh" #include "nes/common/types.hh" #include "nes/common/span.hh" diff --git a/lib/nes/sys/types/CMakeLists.txt b/lib/nes/sys/types/CMakeLists.txt index f51b0ae..863cf65 100644 --- a/lib/nes/sys/types/CMakeLists.txt +++ b/lib/nes/sys/types/CMakeLists.txt @@ -4,5 +4,4 @@ target_sources( address.hh button-mask.hh cycle-count.hh - snapshot.hh - status.hh) \ No newline at end of file + snapshot.hh) \ No newline at end of file diff --git a/lib/nes/sys/types/status.hh b/lib/nes/sys/types/status.hh deleted file mode 100644 index 9b1e824..0000000 --- a/lib/nes/sys/types/status.hh +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -namespace nes::sys -{ - /// A status code. - enum class status - { - success, - error_system_error, - error_invalid_ines_data, - error_unsupported_mapper, - error_unsupported_instruction, - }; - - constexpr auto to_string(status const status) -> char const* - { - switch (status) - { - case status::success: - return "Success"; - case status::error_system_error: - return "System error"; - case status::error_invalid_ines_data: - return "Invalid iNES data"; - case status::error_unsupported_mapper: - return "Unsupported mapper"; - case status::error_unsupported_instruction: - return "Unsupported instruction"; - } - - return "(invalid)"; - } -} // namespace nes::sys \ No newline at end of file -- cgit v1.3.1