diff options
| -rw-r--r-- | app/mac/CMakeLists.txt | 7 | ||||
| -rw-r--r-- | app/mac/controller-adapter-gamepad.hh | 18 | ||||
| -rw-r--r-- | app/mac/controller-adapter-gamepad.mm | 29 | ||||
| -rw-r--r-- | app/mac/controller-adapter-keyboard.hh | 15 | ||||
| -rw-r--r-- | app/mac/controller-adapter-keyboard.mm | 44 | ||||
| -rw-r--r-- | app/mac/controller-adapter.hh | 16 | ||||
| -rw-r--r-- | app/mac/main.mm | 10 | ||||
| -rw-r--r-- | app/mac/scene.hh | 9 | ||||
| -rw-r--r-- | app/mac/scene.mm | 171 | ||||
| -rw-r--r-- | app/mac/view-controller.hh | 3 | ||||
| -rw-r--r-- | app/mac/view-controller.mm | 83 | ||||
| -rw-r--r-- | lib/nes/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | lib/nes/app/CMakeLists.txt | 7 | ||||
| -rw-r--r-- | lib/nes/app/application.cc | 38 | ||||
| -rw-r--r-- | lib/nes/app/application.hh | 37 | ||||
| -rw-r--r-- | lib/nes/app/input/CMakeLists.txt | 8 | ||||
| -rw-r--r-- | lib/nes/app/input/input-device-controller.hh | 38 | ||||
| -rw-r--r-- | lib/nes/app/input/input-device-keyboard.hh | 16 | ||||
| -rw-r--r-- | lib/nes/app/input/input-device.hh | 44 | ||||
| -rw-r--r-- | lib/nes/app/input/input-manager.cc | 102 | ||||
| -rw-r--r-- | lib/nes/app/input/input-manager.hh | 45 | ||||
| -rw-r--r-- | lib/nes/common/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | lib/nes/common/list.hh | 130 |
23 files changed, 622 insertions, 252 deletions
diff --git a/app/mac/CMakeLists.txt b/app/mac/CMakeLists.txt index e0d42d0..f529e3d 100644 --- a/app/mac/CMakeLists.txt +++ b/app/mac/CMakeLists.txt @@ -21,9 +21,4 @@ target_sources( scene.hh scene.mm view-controller.hh - view-controller.mm - controller-adapter.hh - controller-adapter-gamepad.hh - controller-adapter-gamepad.mm - controller-adapter-keyboard.hh - controller-adapter-keyboard.mm)
\ No newline at end of file + view-controller.mm)
\ No newline at end of file diff --git a/app/mac/controller-adapter-gamepad.hh b/app/mac/controller-adapter-gamepad.hh deleted file mode 100644 index c0caeed..0000000 --- a/app/mac/controller-adapter-gamepad.hh +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#import "controller-adapter.hh" - -#import <GameController/GameController.h> - -NS_ASSUME_NONNULL_BEGIN - -@interface ControllerAdapterGamepad : NSObject <ControllerAdapter> - -- (instancetype)initWithProfile:(GCExtendedGamepad*)profile; -- (instancetype)init NS_UNAVAILABLE; - -@property (readonly) GCExtendedGamepad* profile; - -@end - -NS_ASSUME_NONNULL_END
\ No newline at end of file diff --git a/app/mac/controller-adapter-gamepad.mm b/app/mac/controller-adapter-gamepad.mm deleted file mode 100644 index f1d8db8..0000000 --- a/app/mac/controller-adapter-gamepad.mm +++ /dev/null @@ -1,29 +0,0 @@ -#include "controller-adapter-gamepad.hh" - -@implementation ControllerAdapterGamepad - -- (instancetype)initWithProfile:(GCExtendedGamepad*)profile -{ - self = [super init]; - if (self) - { - _profile = profile; - } - return self; -} - -- (nes::button_mask)readPressedButtons -{ - auto res = nes::button_mask{}; - if ([[[_profile dpad] up] isPressed]) { res.add(nes::buttons::up); } - if ([[[_profile dpad] down] isPressed]) { res.add(nes::buttons::down); } - if ([[[_profile dpad] left] isPressed]) { res.add(nes::buttons::left); } - if ([[[_profile dpad] right] isPressed]) { res.add(nes::buttons::right); } - if ([[_profile buttonA] isPressed]) { res.add(nes::buttons::a); } - if ([[_profile buttonB] isPressed]) { res.add(nes::buttons::b); } - if ([[_profile leftTrigger] isPressed]) { res.add(nes::buttons::select); } - if ([[_profile rightTrigger] isPressed]) { res.add(nes::buttons::start); } - return res; -} - -@end
\ No newline at end of file diff --git a/app/mac/controller-adapter-keyboard.hh b/app/mac/controller-adapter-keyboard.hh deleted file mode 100644 index 7c3ef84..0000000 --- a/app/mac/controller-adapter-keyboard.hh +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#import "controller-adapter.hh" - -#import <GameController/GameController.h> - -NS_ASSUME_NONNULL_BEGIN - -@interface ControllerAdapterKeyboard : NSObject <ControllerAdapter> - -@property (readonly, nullable) GCKeyboard* keyboard; - -@end - -NS_ASSUME_NONNULL_END
\ No newline at end of file diff --git a/app/mac/controller-adapter-keyboard.mm b/app/mac/controller-adapter-keyboard.mm deleted file mode 100644 index ad3dda8..0000000 --- a/app/mac/controller-adapter-keyboard.mm +++ /dev/null @@ -1,44 +0,0 @@ -#import "controller-adapter-keyboard.hh" - -@implementation ControllerAdapterKeyboard - -- (instancetype)init -{ - self = [super init]; - if (self) - { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleKeyboardConnected:) - name:GCKeyboardDidConnectNotification - object:nil]; - } - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)handleKeyboardConnected:(NSNotification*)notification -{ - _keyboard = [GCKeyboard coalescedKeyboard]; -} - -- (nes::button_mask)readPressedButtons -{ - auto profile = [[self keyboard] keyboardInput]; - - auto res = nes::button_mask{}; - if ([[profile buttonForKeyCode:GCKeyCodeUpArrow] isPressed]) { res.add(nes::buttons::up); } - if ([[profile buttonForKeyCode:GCKeyCodeDownArrow] isPressed]) { res.add(nes::buttons::down); } - if ([[profile buttonForKeyCode:GCKeyCodeLeftArrow] isPressed]) { res.add(nes::buttons::left); } - if ([[profile buttonForKeyCode:GCKeyCodeRightArrow] isPressed]) { res.add(nes::buttons::right); } - if ([[profile buttonForKeyCode:GCKeyCodeKeyZ] isPressed]) { res.add(nes::buttons::a); } - if ([[profile buttonForKeyCode:GCKeyCodeKeyX] isPressed]) { res.add(nes::buttons::b); } - if ([[profile buttonForKeyCode:GCKeyCodeReturnOrEnter] isPressed]) { res.add(nes::buttons::start); } - if ([[profile buttonForKeyCode:GCKeyCodeSpacebar] isPressed]) { res.add(nes::buttons::select); } - return res; -} - -@end
\ No newline at end of file diff --git a/app/mac/controller-adapter.hh b/app/mac/controller-adapter.hh deleted file mode 100644 index 09e8b6d..0000000 --- a/app/mac/controller-adapter.hh +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#import "nes/sys/types/button-mask.hh" - -#import <Foundation/Foundation.h> - -NS_ASSUME_NONNULL_BEGIN - -/// Adapts inputs from a GCPhysicalInputProfile to a NES button mask. -@protocol ControllerAdapter <NSObject> - -- (nes::button_mask)readPressedButtons; - -@end - -NS_ASSUME_NONNULL_END
\ No newline at end of file diff --git a/app/mac/main.mm b/app/mac/main.mm index 4e96cea..55e00b1 100644 --- a/app/mac/main.mm +++ b/app/mac/main.mm @@ -5,14 +5,6 @@ int main(int argc, char** argv) { - if (argc != 2) - { - std::cerr << "Usage: " << argv[0] << " <rom-path>" << std::endl; - return 1; - } - - auto filePath = [NSString stringWithUTF8String:argv[1]]; - auto app = [NSApplication sharedApplication]; [app setActivationPolicy:NSApplicationActivationPolicyRegular]; @@ -33,7 +25,7 @@ int main(int argc, char** argv) [mainMenu addItem:item]; [app setMainMenu:mainMenu]; - auto viewController = [[ViewController alloc] initWithFilePath:filePath]; + auto viewController = [[ViewController alloc] init]; auto window = [NSWindow windowWithContentViewController:viewController]; [window setTitle:@"NES"]; [window setContentAspectRatio:[viewController minimumDisplaySize]]; diff --git a/app/mac/scene.hh b/app/mac/scene.hh index ba8eb31..e4f0a45 100644 --- a/app/mac/scene.hh +++ b/app/mac/scene.hh @@ -1,22 +1,15 @@ #pragma once -#import "controller-adapter.hh" - #import <SpriteKit/SpriteKit.h> +#import <GameController/GameController.h> NS_ASSUME_NONNULL_BEGIN @interface Scene : SKScene -- (instancetype)initWithFilePath:(NSString*)filePath NS_DESIGNATED_INITIALIZER; - + (instancetype)sceneWithSize:(CGSize)size NS_UNAVAILABLE; - (instancetype)initWithSize:(CGSize)size NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE; -- (instancetype)init NS_UNAVAILABLE; - -@property (nonatomic, nullable) id<ControllerAdapter> primaryController; -@property (nonatomic, nullable) id<ControllerAdapter> secondaryController; @end diff --git a/app/mac/scene.mm b/app/mac/scene.mm index 97caadf..de9c587 100644 --- a/app/mac/scene.mm +++ b/app/mac/scene.mm @@ -1,13 +1,14 @@ #import "scene.hh" -#import "nes/sys/nes.hh" +#import "nes/app/application.hh" +#import "nes/app/input/input-device-keyboard.hh" +#import "nes/app/input/input-device-controller.hh" #import "nes/common/display.hh" #import <memory> #import <optional> -#import <iostream> namespace { - class display_impl : public nes::display + class display_impl final : public nes::display { SKSpriteNode* node_; NSMutableData* buffer_front_; @@ -63,16 +64,97 @@ namespace return [NSMutableData dataWithLength:length]; } }; + + class input_device_keyboard_impl final : public nes::input_device_keyboard + { + GCKeyboardInput* profile_; + + public: + explicit input_device_keyboard_impl(GCKeyboardInput* profile) + : profile_{ profile } + { + } + + auto set_profile(GCKeyboardInput* profile) -> void { profile_ = profile; } + + auto read_key_up() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeUpArrow] isPressed]; } + auto read_key_down() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeDownArrow] isPressed]; } + auto read_key_left() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeLeftArrow] isPressed]; } + auto read_key_right() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeRightArrow] isPressed]; } + auto read_key_a() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeKeyZ] isPressed]; } + auto read_key_b() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeKeyX] isPressed]; } + auto read_key_select() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeSpacebar] isPressed]; } + auto read_key_start() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeReturnOrEnter] isPressed]; } + auto read_key_escape() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeEscape] isPressed]; } + }; + + class input_device_controller_impl final : public nes::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_key_up() -> bool override { return [[[profile_ dpad] up] isPressed]; } + auto read_key_down() -> bool override { return [[[profile_ dpad] down] isPressed]; } + auto read_key_left() -> bool override { return [[[profile_ dpad] left] isPressed]; } + auto read_key_right() -> bool override { return [[[profile_ dpad] right] isPressed]; } + auto read_key_a() -> bool override { return [[profile_ buttonA] isPressed]; } + auto read_key_b() -> bool override { return [[profile_ buttonB] isPressed]; } + auto read_key_select() -> bool override { return [[profile_ leftTrigger] isPressed]; } + auto read_key_start() -> bool override { return [[profile_ rightTrigger] isPressed]; } + }; } // namespace @implementation Scene { std::unique_ptr<display_impl> _display; - std::unique_ptr<nes::nes> _nes; + std::unique_ptr<input_device_keyboard_impl> _keyboard; + std::vector<std::unique_ptr<input_device_controller_impl>> _controllers; + std::unique_ptr<nes::application> _app; std::optional<NSTimeInterval> _lastTimestamp; } -- (instancetype)initWithFilePath:(NSString*)filePath +- (instancetype)init { self = [super initWithSize:CGSizeMake(nes::display::width, nes::display::height)]; if (self) @@ -84,19 +166,42 @@ namespace [self setScaleMode:SKSceneScaleModeAspectFit]; [self addChild:node]; - auto cartridge = nes::cartridge::from_file([filePath UTF8String]); - if (cartridge.get_status() != nes::status::success) - { - std::cerr << "Unable to load cartridge: " << to_string(cartridge.get_status()) << std::endl; - std::abort(); - } - _display = std::make_unique<display_impl>(node); - _nes = std::make_unique<nes::nes>(std::move(cartridge), *self->_display); + _keyboard = std::make_unique<input_device_keyboard_impl>([[GCKeyboard coalescedKeyboard] keyboardInput]); + _app = std::make_unique<nes::application>(*_display, *_keyboard); + + for (GCController* controller in [GCController controllers]) + { + auto gamepad = [controller extendedGamepad]; + if (gamepad) + { + auto adapter = std::make_unique<input_device_controller_impl>(gamepad); + _app->add_controller(*adapter); + _controllers.emplace_back(std::move(adapter)); + } + } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleKeyboardConnected:) + name:GCKeyboardDidConnectNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleControllerConnected:) + name:GCControllerDidConnectNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleControllerDisconnected:) + name:GCControllerDidDisconnectNotification + object:nil]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void)keyUp:(NSEvent*)event { // Handled by Game Controller @@ -109,20 +214,42 @@ namespace - (void)update:(NSTimeInterval)currentTime { - _nes->ref_controller_1().set_pressed(_primaryController ? [_primaryController readPressedButtons] : nes::button_mask{}); - _nes->ref_controller_2().set_pressed(_secondaryController ? [_secondaryController readPressedButtons] : nes::button_mask{}); - auto const lastTimestamp = _lastTimestamp.value_or(currentTime); _lastTimestamp = currentTime; auto deltaMicroseconds = static_cast<std::uint64_t>((currentTime - lastTimestamp) * 1000. * 1000.); deltaMicroseconds = std::min(deltaMicroseconds, std::uint64_t{ 50000 }); - _nes->step(std::chrono::microseconds{ deltaMicroseconds }); + _app->frame(std::chrono::microseconds{ deltaMicroseconds }); +} - if (_nes->get_status() != nes::status::success) - { - std::cerr << "Invalid state: " << to_string(_nes->get_status()) << std::endl; - std::abort(); - } +- (void)handleKeyboardConnected:(NSNotification*)notification +{ + _keyboard->set_profile([[GCKeyboard coalescedKeyboard] keyboardInput]); +} + +- (void)handleControllerConnected:(NSNotification*)notification +{ + auto controller = (GCController*)[notification object]; + auto gamepad = [controller extendedGamepad]; + if (gamepad) + { + auto adapter = std::make_unique<input_device_controller_impl>(gamepad); + _app->add_controller(*adapter); + _controllers.emplace_back(std::move(adapter)); + } +} + +- (void)handleControllerDisconnected:(NSNotification*)notification +{ + auto controller = (GCController*)[notification object]; + for (auto i = _controllers.size(); i > 0; --i) + { + auto& adapter = *_controllers[i - 1]; + if (adapter.get_controller() == controller) + { + _app->remove_controller(adapter); + _controllers.erase(_controllers.begin() + static_cast<int>(i - 1)); + } + } } @end
\ No newline at end of file diff --git a/app/mac/view-controller.hh b/app/mac/view-controller.hh index 2eecd4e..bed3207 100644 --- a/app/mac/view-controller.hh +++ b/app/mac/view-controller.hh @@ -7,12 +7,9 @@ NS_ASSUME_NONNULL_BEGIN @interface ViewController : GCEventViewController -- (instancetype)initWithFilePath:(NSString*)filePath NS_DESIGNATED_INITIALIZER; - - (instancetype)initWithNibName:(nullable NSString*)nibNameOrNil bundle:(nullable NSBundle*)nibBundleOrNil NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE; -- (instancetype)init NS_UNAVAILABLE; @property (readonly) NSSize minimumDisplaySize; @property (readonly) NSSize preferredDisplaySize; diff --git a/app/mac/view-controller.mm b/app/mac/view-controller.mm index eb81b6b..8695634 100644 --- a/app/mac/view-controller.mm +++ b/app/mac/view-controller.mm @@ -1,54 +1,23 @@ #import "view-controller.hh" -#import "controller-adapter-keyboard.hh" -#import "controller-adapter-gamepad.hh" #import "scene.hh" @implementation ViewController { Scene* _scene; NSSize _sceneSize; - ControllerAdapterKeyboard* _keyboardController; - NSMutableArray<ControllerAdapterGamepad*>* _gamepadControllers; } -- (instancetype)initWithFilePath:(NSString*)filePath +- (instancetype)init { self = [super initWithNibName:nil bundle:nil]; if (self) { - _scene = [[Scene alloc] initWithFilePath:filePath]; + _scene = [[Scene alloc] init]; _sceneSize = [_scene size]; - - _keyboardController = [[ControllerAdapterKeyboard alloc] init]; - _gamepadControllers = [NSMutableArray array]; - for (GCController* controller in [GCController controllers]) - { - auto gamepad = [controller extendedGamepad]; - if (gamepad) - { - auto adapter = [[ControllerAdapterGamepad alloc] initWithProfile:gamepad]; - [_gamepadControllers addObject:adapter]; - } - } - [self updateControllers]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleControllerConnected:) - name:GCControllerDidConnectNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleControllerDisconnected:) - name:GCControllerDidDisconnectNotification - object:nil]; } return self; } -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - - (void)loadView { auto view = [[SKView alloc] init]; @@ -59,54 +28,6 @@ [self setView:view]; } -- (void)updateControllers -{ - // Prefer controllers, then fallback to keyboard input. - if ([_gamepadControllers count] >= 1) - { - [_scene setPrimaryController:[_gamepadControllers objectAtIndex:0]]; - if ([_gamepadControllers count] >= 2) - { - [_scene setSecondaryController:[_gamepadControllers objectAtIndex:1]]; - } - else - { - [_scene setSecondaryController:_keyboardController]; - } - } - else - { - [_scene setPrimaryController:_keyboardController]; - [_scene setSecondaryController:nil]; - } -} - -- (void)handleControllerConnected:(NSNotification*)notification -{ - auto controller = (GCController*)[notification object]; - auto gamepad = [controller extendedGamepad]; - if (gamepad) - { - auto adapter = [[ControllerAdapterGamepad alloc] initWithProfile:gamepad]; - [_gamepadControllers addObject:adapter]; - } - [self updateControllers]; -} - -- (void)handleControllerDisconnected:(NSNotification*)notification -{ - auto controller = (GCController*)[notification object]; - for (auto i = NSUInteger{ [_gamepadControllers count] }; i > 0; --i) - { - auto adapter = [_gamepadControllers objectAtIndex:(i - 1)]; - if ([[adapter profile] controller] == controller) - { - [_gamepadControllers removeObjectAtIndex:(i - 1)]; - } - } - [self updateControllers]; -} - - (NSSize)minimumDisplaySize { return _sceneSize; 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 |
