summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorHannes Schulze <haschu0103@outlook.de>2025-01-30 16:43:06 +0100
committerHannes Schulze <haschu0103@outlook.de>2025-01-31 00:02:26 +0100
commit307e07d6513f9524b7fcab5134494bd175465cd1 (patch)
treece025db2ea998bdcd8980a0ed443083f0daf8d35 /lib
parent5959349f341a2380c0c3f41ec40a1e8e0d969d00 (diff)
handle input devices in the c++ application
Diffstat (limited to 'lib')
-rw-r--r--lib/nes/CMakeLists.txt3
-rw-r--r--lib/nes/app/CMakeLists.txt7
-rw-r--r--lib/nes/app/application.cc38
-rw-r--r--lib/nes/app/application.hh37
-rw-r--r--lib/nes/app/input/CMakeLists.txt8
-rw-r--r--lib/nes/app/input/input-device-controller.hh38
-rw-r--r--lib/nes/app/input/input-device-keyboard.hh16
-rw-r--r--lib/nes/app/input/input-device.hh44
-rw-r--r--lib/nes/app/input/input-manager.cc102
-rw-r--r--lib/nes/app/input/input-manager.hh45
-rw-r--r--lib/nes/common/CMakeLists.txt1
-rw-r--r--lib/nes/common/list.hh130
12 files changed, 468 insertions, 1 deletions
diff --git a/lib/nes/CMakeLists.txt b/lib/nes/CMakeLists.txt
index 78c09fe..f24c27f 100644
--- a/lib/nes/CMakeLists.txt
+++ b/lib/nes/CMakeLists.txt
@@ -1,2 +1,3 @@
add_subdirectory(common)
-add_subdirectory(sys) \ No newline at end of file
+add_subdirectory(sys)
+add_subdirectory(app) \ No newline at end of file
diff --git a/lib/nes/app/CMakeLists.txt b/lib/nes/app/CMakeLists.txt
new file mode 100644
index 0000000..18b2a2c
--- /dev/null
+++ b/lib/nes/app/CMakeLists.txt
@@ -0,0 +1,7 @@
+target_sources(
+ nes
+ PRIVATE
+ application.hh
+ application.cc)
+
+add_subdirectory(input) \ No newline at end of file
diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc
new file mode 100644
index 0000000..db43854
--- /dev/null
+++ b/lib/nes/app/application.cc
@@ -0,0 +1,38 @@
+#include "nes/app/application.hh"
+#include "nes/app/input/input-device-keyboard.hh"
+#include "nes/app/input/input-device-controller.hh"
+#include <iostream>
+
+namespace nes
+{
+ namespace
+ {
+ constexpr auto file = std::string_view{ "/Users/hannes/Documents/temp/supermario.nes" };
+ } // namespace
+
+ application::application(display& display, input_device_keyboard& keyboard)
+ : display_{ display }
+ , input_manager_{ keyboard }
+ , console_{ cartridge::from_file(file), display_ }
+ {
+ if (console_.get_status() != status::success)
+ {
+ std::cerr << "Unable to load cartridge: " << to_string(console_.get_status()) << std::endl;
+ std::abort();
+ }
+ }
+
+ auto application::frame(std::chrono::microseconds const elapsed_time) -> void
+ {
+ console_.ref_controller_1().set_pressed(input_manager_.get_input_1().read_mask());
+ console_.ref_controller_2().set_pressed(input_manager_.get_input_2().read_mask());
+
+ console_.step(elapsed_time);
+
+ if (console_.get_status() != status::success)
+ {
+ std::cerr << "Invalid state: " << to_string(console_.get_status()) << std::endl;
+ std::abort();
+ }
+ }
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/application.hh b/lib/nes/app/application.hh
new file mode 100644
index 0000000..9f6bd16
--- /dev/null
+++ b/lib/nes/app/application.hh
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "nes/app/input/input-manager.hh"
+#include "nes/sys/nes.hh"
+#include <chrono>
+
+namespace nes
+{
+ class display;
+
+ /// Entrypoint for the application.
+ ///
+ /// This class takes care of presenting a UI screen or the emulator, handling any errors and implementing the main
+ /// application logic.
+ ///
+ /// It delegates some platform-specific tasks to interfaces.
+ class application
+ {
+ display& display_;
+ input_manager input_manager_;
+ nes console_;
+
+ public:
+ explicit application(display&, input_device_keyboard&);
+
+ application(application const&) = delete;
+ application(application&&) = delete;
+ auto operator=(application const&) -> application& = delete;
+ auto operator=(application&&) -> application& = delete;
+
+ /// Request a new frame for the application.
+ auto frame(std::chrono::microseconds elapsed_time) -> void;
+
+ auto add_controller(input_device_controller& c) -> void { input_manager_.add_controller(c); }
+ auto remove_controller(input_device_controller& c) -> void { input_manager_.remove_controller(c); }
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/CMakeLists.txt b/lib/nes/app/input/CMakeLists.txt
new file mode 100644
index 0000000..7150745
--- /dev/null
+++ b/lib/nes/app/input/CMakeLists.txt
@@ -0,0 +1,8 @@
+target_sources(
+ nes
+ PRIVATE
+ input-device.hh
+ input-device-keyboard.hh
+ input-device-controller.hh
+ input-manager.hh
+ input-manager.cc) \ No newline at end of file
diff --git a/lib/nes/app/input/input-device-controller.hh b/lib/nes/app/input/input-device-controller.hh
new file mode 100644
index 0000000..6adb79c
--- /dev/null
+++ b/lib/nes/app/input/input-device-controller.hh
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "nes/app/input/input-device.hh"
+#include "nes/common/list.hh"
+#include <string_view>
+
+namespace nes
+{
+ /// A game controller input device.
+ class input_device_controller : public input_device, public list<input_device_controller>::node
+ {
+ public:
+ enum class index
+ {
+ unused = -1,
+ player_1 = 0,
+ player_2 = 1,
+ };
+
+ /// Indicates whether the controller can be trusted to actually exist.
+ ///
+ /// If this returns false, the controller is not automatically selected on startup if there is no other
+ /// controller already selected.
+ virtual auto is_reliable() const -> bool = 0;
+
+ /// Human-readable name for the controller.
+ virtual auto get_name() const -> std::string_view = 0;
+
+ /// Get the (persistent) player index for the controller, if already set before.
+ virtual auto get_index() const -> index = 0;
+
+ /// Set the (persistent) player index for the controller.
+ virtual auto set_index(index) -> void = 0;
+
+ protected:
+ explicit input_device_controller() = default;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-device-keyboard.hh b/lib/nes/app/input/input-device-keyboard.hh
new file mode 100644
index 0000000..d61417d
--- /dev/null
+++ b/lib/nes/app/input/input-device-keyboard.hh
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "nes/app/input/input-device.hh"
+
+namespace nes
+{
+ /// A keyboard input device.
+ class input_device_keyboard : public input_device
+ {
+ public:
+ virtual auto read_key_escape() -> bool = 0;
+
+ protected:
+ explicit input_device_keyboard() = default;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-device.hh b/lib/nes/app/input/input-device.hh
new file mode 100644
index 0000000..9ac7423
--- /dev/null
+++ b/lib/nes/app/input/input-device.hh
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "nes/sys/types/button-mask.hh"
+
+namespace nes
+{
+ /// An abstract input device which can be used to play games.
+ class input_device
+ {
+ public:
+ virtual ~input_device() = default;
+
+ input_device(input_device const&) = delete;
+ input_device(input_device&&) = delete;
+ auto operator=(input_device const&) -> input_device& = delete;
+ auto operator=(input_device&&) -> input_device& = delete;
+
+ virtual auto read_key_up() -> bool = 0;
+ virtual auto read_key_down() -> bool = 0;
+ virtual auto read_key_left() -> bool = 0;
+ virtual auto read_key_right() -> bool = 0;
+ virtual auto read_key_a() -> bool = 0;
+ virtual auto read_key_b() -> bool = 0;
+ virtual auto read_key_select() -> bool = 0;
+ virtual auto read_key_start() -> bool = 0;
+
+ auto read_mask() -> button_mask
+ {
+ auto res = button_mask{};
+ if (read_key_up()) { res.add(buttons::up); }
+ if (read_key_down()) { res.add(buttons::down); }
+ if (read_key_left()) { res.add(buttons::left); }
+ if (read_key_right()) { res.add(buttons::right); }
+ if (read_key_a()) { res.add(buttons::a); }
+ if (read_key_b()) { res.add(buttons::b); }
+ if (read_key_select()) { res.add(buttons::select); }
+ if (read_key_start()) { res.add(buttons::start); }
+ return res;
+ }
+
+ protected:
+ explicit input_device() = default;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-manager.cc b/lib/nes/app/input/input-manager.cc
new file mode 100644
index 0000000..1c9db9e
--- /dev/null
+++ b/lib/nes/app/input/input-manager.cc
@@ -0,0 +1,102 @@
+#include "nes/app/input/input-manager.hh"
+
+namespace nes
+{
+ namespace
+ {
+ class input_device_dummy final : public input_device
+ {
+ public:
+ explicit input_device_dummy() = default;
+
+ static auto get_instance() -> input_device_dummy&
+ {
+ static auto res = input_device_dummy{};
+ return res;
+ }
+
+ auto read_key_up() -> bool override { return false; }
+ auto read_key_down() -> bool override { return false; }
+ auto read_key_left() -> bool override { return false; }
+ auto read_key_right() -> bool override { return false; }
+ auto read_key_a() -> bool override { return false; }
+ auto read_key_b() -> bool override { return false; }
+ auto read_key_select() -> bool override { return false; }
+ auto read_key_start() -> bool override { return false; }
+ };
+ } // namespace
+
+ input_manager::input_manager(input_device_keyboard& keyboard)
+ : keyboard_{ &keyboard }
+ {
+ fill_unassigned_inputs();
+ }
+
+ auto input_manager::add_controller(input_device_controller& controller) -> void
+ {
+ controllers_.append(controller);
+ if (controller.is_reliable())
+ {
+ auto const index = controller.get_index();
+ if (index != input_device_controller::index::unused)
+ {
+ // This controller was previously used and assigned an index -> replace the current device with this
+ // (known) controller.
+ auto const i = static_cast<std::size_t>(index);
+ if (inputs_[i].controller)
+ {
+ inputs_[i].controller->set_index(input_device_controller::index::unused);
+ }
+
+ inputs_[i].controller = &controller;
+ inputs_[i].device = &controller;
+ }
+
+ // There may be another free controller now.
+ fill_unassigned_inputs();
+ }
+ }
+
+ auto input_manager::remove_controller(input_device_controller& controller) -> void
+ {
+ controllers_.remove(controller);
+ fill_unassigned_inputs();
+ }
+
+ auto input_manager::fill_unassigned_inputs() -> void
+ {
+ auto it = controllers_.begin();
+ input_device* fallback = keyboard_;
+
+ for (auto i = std::size_t{ 0 }; i < input_count; ++i)
+ {
+ if (inputs_[i].controller != nullptr) { continue; }
+
+ // 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)
+ {
+ break;
+ }
+ ++it;
+ }
+
+ if (it != controllers_.end())
+ {
+ // We've found an unused controller -> assign it.
+ inputs_[i].controller = *it;
+ inputs_[i].controller->set_index(static_cast<input_device_controller::index>(i));
+ inputs_[i].device = *it;
+ }
+ else
+ {
+ // We're all out of controllers -> fall back to the keyboard (if this is the first missing controller)
+ // or nothing.
+ inputs_[i].device = fallback;
+ fallback = &input_device_dummy::get_instance(); // Only use the keyboard as a fallback once.
+ }
+ }
+ }
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-manager.hh b/lib/nes/app/input/input-manager.hh
new file mode 100644
index 0000000..10308c7
--- /dev/null
+++ b/lib/nes/app/input/input-manager.hh
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "nes/app/input/input-device-keyboard.hh"
+#include "nes/app/input/input-device-controller.hh"
+#include "nes/common/list.hh"
+
+namespace nes
+{
+ /// Manages input devices.
+ ///
+ /// The main purpose of this class is to assign newly plugged in controllers as the primary/secondary input and fall
+ /// back to the keyboard if needed.
+ class input_manager
+ {
+ static constexpr auto input_count = std::size_t{ 2 };
+
+ input_device_keyboard* keyboard_;
+ list<input_device_controller> controllers_;
+ struct
+ {
+ input_device* device{ nullptr };
+ input_device_controller* controller{ nullptr };
+ } inputs_[input_count]{};
+
+ public:
+ explicit input_manager(input_device_keyboard&);
+
+ input_manager(input_manager const&) = delete;
+ input_manager(input_manager&&) = delete;
+ auto operator=(input_manager const&) -> input_manager& = delete;
+ auto operator=(input_manager&&) -> input_manager& = delete;
+
+ auto get_input_1() const -> input_device& { return *inputs_[0].device; }
+ auto get_input_2() const -> input_device& { return *inputs_[1].device; }
+
+ auto get_keyboard() const -> input_device_keyboard& { return *keyboard_; }
+ auto get_controllers() const -> list<input_device_controller> const& { return controllers_; }
+
+ auto add_controller(input_device_controller&) -> void;
+ auto remove_controller(input_device_controller&) -> void;
+
+ private:
+ auto fill_unassigned_inputs() -> void;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/common/CMakeLists.txt b/lib/nes/common/CMakeLists.txt
index 69ae435..553d468 100644
--- a/lib/nes/common/CMakeLists.txt
+++ b/lib/nes/common/CMakeLists.txt
@@ -3,4 +3,5 @@ target_sources(
PRIVATE
debug.hh
rgb.hh
+ list.hh
display.hh) \ No newline at end of file
diff --git a/lib/nes/common/list.hh b/lib/nes/common/list.hh
new file mode 100644
index 0000000..5f0d258
--- /dev/null
+++ b/lib/nes/common/list.hh
@@ -0,0 +1,130 @@
+#pragma once
+
+namespace nes
+{
+ /// An intrusive doubly-linked list.
+ template<typename T>
+ class list
+ {
+ public:
+ /// Base class of nodes in the list.
+ class node
+ {
+ friend list;
+
+ list* list_{ nullptr };
+ T* prev_{ nullptr };
+ T* next_{ nullptr };
+
+ public:
+ virtual ~node() = default;
+
+ node(node const&) = delete;
+ node(node&&) = delete;
+ auto operator=(node const&) -> node& = delete;
+ auto operator=(node&&) -> node& = delete;
+
+ protected:
+ explicit node() = default;
+ };
+
+ /// An iterator for the linked list.
+ class iterator
+ {
+ friend list;
+
+ T* current_{ nullptr };
+
+ public:
+ auto operator==(iterator const& other) -> bool { return current_ == other.current_; }
+ auto operator!=(iterator const& other) -> bool { return current_ != other.current_; }
+
+ auto operator*() -> T* { return current_; }
+
+ auto operator++() -> iterator&
+ {
+ if (current_)
+ {
+ node& current_node = *current_;
+ current_ = current_node.next_;
+ }
+ return *this;
+ }
+
+ auto operator++(int) -> iterator
+ {
+ auto res = *this;
+ ++*this;
+ return res;
+ }
+
+ private:
+ explicit iterator() = default;
+ explicit iterator(T* current)
+ : current_{ current }
+ {
+ }
+ };
+
+ private:
+ T* head_{ nullptr };
+ T* tail_{ nullptr };
+
+ public:
+ explicit list() = default;
+
+ ~list()
+ {
+ while (begin() != end()) { remove(begin()); }
+ }
+
+ list(list const&) = delete;
+ list(list&&) = delete;
+ auto operator=(list const&) -> list& = delete;
+ auto operator=(list&&) -> list& = delete;
+
+ auto begin() const -> iterator { return iterator{ head_ }; }
+ auto end() const -> iterator { return iterator{}; }
+
+ /// Add an item to the end of the list, removing it from any other list it may be in.
+ auto append(T& item) -> void
+ {
+ node& item_node = item;
+ if (item_node.list_) { item_node.list_->remove(item); }
+
+ item_node.list_ = this;
+ if (tail_)
+ {
+ item_node.prev_ = tail_;
+ tail_->next_ = &item;
+ }
+ else
+ {
+ head_ = &item;
+ }
+ tail_ = &item;
+ }
+
+ /// Remove an item from the list.
+ auto remove(T& item) -> void
+ {
+ node& item_node = item;
+ if (!item_node.list_) { return; }
+
+ if (head_ == &item) { head_ = item_node.next_; }
+ if (tail_ == &item) { tail_ = item_node.prev_; }
+ if (item_node.prev_) { item_node.prev_->next_ = item_node.next_; }
+ if (item_node.next_) { item_node.next_->prev_ = item_node.prev_; }
+
+ item_node.next_ = nullptr;
+ item_node.prev_ = nullptr;
+ item_node.list_ = nullptr;
+ }
+
+ /// Remove an item from the list by its iterator.
+ auto remove(iterator it) -> void
+ {
+ if (it != end()) { remove(**it); }
+ }
+ };
+} // namespace nes \ No newline at end of file