1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-04 18:29:40 +00:00

Merge pull request #461 from TomHarte/Joystick

Introduces Joystick support for the Apple II
This commit is contained in:
Thomas Harte 2018-06-13 19:34:26 -04:00 committed by GitHub
commit 8d8f244bf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 427 additions and 302 deletions

View File

@ -25,14 +25,14 @@ class MultiJoystick: public Inputs::Joystick {
}
}
std::vector<DigitalInput> get_inputs() override {
std::vector<DigitalInput> inputs;
for(const auto &joystick: joysticks_) {
std::vector<DigitalInput> joystick_inputs = joystick->get_inputs();
for(const auto &input: joystick_inputs) {
if(std::find(inputs.begin(), inputs.end(), input) != inputs.end()) {
inputs.push_back(input);
std::vector<Input> &get_inputs() override {
if(inputs.empty()) {
for(const auto &joystick: joysticks_) {
std::vector<Input> joystick_inputs = joystick->get_inputs();
for(const auto &input: joystick_inputs) {
if(std::find(inputs.begin(), inputs.end(), input) != inputs.end()) {
inputs.push_back(input);
}
}
}
}
@ -40,11 +40,18 @@ class MultiJoystick: public Inputs::Joystick {
return inputs;
}
void set_digital_input(const DigitalInput &digital_input, bool is_active) override {
void set_input(const Input &digital_input, bool is_active) override {
for(const auto &joystick: joysticks_) {
joystick->set_digital_input(digital_input, is_active);
joystick->set_input(digital_input, is_active);
}
}
void set_input(const Input &digital_input, float value) override {
for(const auto &joystick: joysticks_) {
joystick->set_input(digital_input, value);
}
}
void reset_all_inputs() override {
for(const auto &joystick: joysticks_) {
joystick->reset_all_inputs();
@ -52,6 +59,7 @@ class MultiJoystick: public Inputs::Joystick {
}
private:
std::vector<Input> inputs;
std::vector<Inputs::Joystick *> joysticks_;
};

View File

@ -21,28 +21,73 @@ class Joystick {
public:
virtual ~Joystick() {}
struct DigitalInput {
/*!
Defines a single input, any individually-measured thing a fire button or
other digital control, an analogue axis, or a button with a symbol on it.
*/
struct Input {
/// Defines the broad type of the input.
enum Type {
Up, Down, Left, Right, Fire,
// Half-axis inputs.
Up, Down, Left, Right,
// Full-axis inputs.
Horizontal, Vertical,
// Fire buttons.
Fire,
// Other labelled keys.
Key
} type;
union {
};
const Type type;
bool is_digital_axis() const {
return type < Type::Horizontal;
}
bool is_analogue_axis() const {
return type >= Type::Horizontal && type < Type::Fire;
}
bool is_axis() const {
return type < Type::Fire;
}
bool is_button() const {
return type >= Type::Fire;
}
enum Precision {
Analogue, Digital
};
Precision precision() const {
return is_analogue_axis() ? Precision::Analogue : Precision::Digital;
}
/*!
Holds extra information pertaining to the input.
@c Type::Key inputs declare the symbol printed on them.
All other types of input have an associated index, indicating whether they
are the zeroth, first, second, third, etc of those things. E.g. a joystick
may have two fire buttons, which will be buttons 0 and 1.
*/
union Info {
struct {
int index;
size_t index;
} control;
struct {
wchar_t symbol;
} key;
} info;
};
Info info;
// TODO: Find a way to make the above safely const; may mean not using a union.
DigitalInput(Type type, int index = 0) : type(type) {
Input(Type type, size_t index = 0) :
type(type) {
info.control.index = index;
}
DigitalInput(wchar_t symbol) : type(Key) {
Input(wchar_t symbol) : type(Key) {
info.key.symbol = symbol;
}
bool operator == (const DigitalInput &rhs) {
bool operator == (const Input &rhs) {
if(rhs.type != type) return false;
if(rhs.type == Key) {
return rhs.info.key.symbol == info.key.symbol;
@ -52,17 +97,123 @@ class Joystick {
}
};
virtual std::vector<DigitalInput> get_inputs() = 0;
/// @returns The list of all inputs defined on this joystick.
virtual std::vector<Input> &get_inputs() = 0;
// Host interface.
virtual void set_digital_input(const DigitalInput &digital_input, bool is_active) = 0;
/*!
Sets the digital value of @c input. This may have direct effect or
influence an analogue value; e.g. if the caller declares that ::Left is
active but this joystick has only an analogue horizontal axis, this will
cause a change to that analogue value.
*/
virtual void set_input(const Input &input, bool is_active) = 0;
/*!
Sets the analogue value of @c input. If the input is actually digital,
or if there is a digital input with a corresponding meaning (e.g. ::Left
versus the horizontal axis), this may cause a digital input to be set.
@c value should be in the range [0.0, 1.0].
*/
virtual void set_input(const Input &input, float value) = 0;
/*!
Sets all inputs to their resting state.
*/
virtual void reset_all_inputs() {
for(const auto &input: get_inputs()) {
set_digital_input(input, false);
if(input.precision() == Input::Precision::Digital)
set_input(input, false);
else
set_input(input, 0.5f);
}
}
};
/*!
ConcreteJoystick is the class that it's expected most machines will actually subclass;
it accepts a set of Inputs at construction and thereby is able to provide the
promised analogue <-> digital mapping of Joystick.
*/
class ConcreteJoystick: public Joystick {
public:
ConcreteJoystick(const std::vector<Input> &inputs) : inputs_(inputs) {
// Size and populate stick_types_, which is used for digital <-> analogue conversion.
for(const auto &input: inputs_) {
const bool is_digital_axis = input.is_digital_axis();
const bool is_analogue_axis = input.is_analogue_axis();
if(is_digital_axis || is_analogue_axis) {
const size_t required_size = static_cast<size_t>(input.info.control.index+1);
if(stick_types_.size() < required_size) {
stick_types_.resize(required_size);
}
stick_types_[static_cast<size_t>(input.info.control.index)] = is_digital_axis ? StickType::Digital : StickType::Analogue;
}
}
}
std::vector<Input> &get_inputs() override final {
return inputs_;
}
void set_input(const Input &input, bool is_active) override final {
// If this is a digital setting to a digital property, just pass it along.
if(input.is_button() || stick_types_[input.info.control.index] == StickType::Digital) {
did_set_input(input, is_active);
return;
}
// Otherwise this is logically to an analogue axis; for now just use some
// convenient hard-coded values. TODO: make these a function of time.
using Type = Joystick::Input::Type;
switch(input.type) {
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.25f : 0.5f); break;
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.75f : 0.5f); break;
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.25f : 0.5f); break;
case Type::Down: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.75f : 0.5f); break;
}
}
void set_input(const Input &input, float value) override final {
// If this is an analogue setting to an analogue property, just pass it along.
if(!input.is_button() && stick_types_[input.info.control.index] == StickType::Analogue) {
did_set_input(input, value);
return;
}
// Otherwise apply a threshold test to convert to digital, with remapping from axes to digital inputs.
using Type = Joystick::Input::Type;
switch(input.type) {
default: did_set_input(input, value > 0.5f); break;
case Type::Horizontal:
did_set_input(Input(Type::Left, input.info.control.index), value <= 0.25f);
did_set_input(Input(Type::Right, input.info.control.index), value >= 0.25f);
break;
case Type::Vertical:
did_set_input(Input(Type::Up, input.info.control.index), value <= 0.25f);
did_set_input(Input(Type::Down, input.info.control.index), value >= 0.25f);
break;
}
}
protected:
virtual void did_set_input(const Input &input, float value) {
}
virtual void did_set_input(const Input &input, bool value) {
}
private:
std::vector<Input> inputs_;
enum class StickType {
Digital,
Analogue
};
std::vector<StickType> stick_types_;
};
}
#endif /* Joystick_hpp */

View File

@ -11,6 +11,7 @@
#include "../../Activity/Source.hpp"
#include "../ConfigurationTarget.hpp"
#include "../CRTMachine.hpp"
#include "../JoystickMachine.hpp"
#include "../KeyboardMachine.hpp"
#include "../Utility/MemoryFuzzer.hpp"
#include "../Utility/StringSerialiser.hpp"
@ -51,6 +52,7 @@ class ConcreteMachine:
public Inputs::Keyboard,
public AppleII::Machine,
public Activity::Source,
public JoystickMachine::Machine,
public AppleII::Card::Delegate {
private:
struct VideoBusHandler : public AppleII::Video::BusHandler {
@ -173,6 +175,56 @@ class ConcreteMachine:
// MARK - quick loading
bool should_load_quickly_ = false;
// MARK - joysticks
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick() :
ConcreteJoystick({
Input(Input::Horizontal),
Input(Input::Vertical),
// The Apple II offers three buttons between two joysticks;
// this emulator puts three buttons on each joystick and
// combines them.
Input(Input::Fire, 0),
Input(Input::Fire, 1),
Input(Input::Fire, 2),
}) {}
void did_set_input(const Input &input, float value) override {
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
}
void did_set_input(const Input &input, bool value) override {
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
buttons[input.info.control.index] = value;
}
}
bool buttons[3] = {false, false, false};
float axes[2] = {0.5f, 0.5f};
};
// On an Apple II, the programmer strobes 0xc070 and that causes each analogue input
// to begin a charge and discharge cycle **if they are not already charging**.
// The greater the analogue input, the faster they will charge and therefore the sooner
// they will discharge.
//
// This emulator models that with analogue_charge_ being essentially the amount of time,
// in charge threshold units, since 0xc070 was last strobed. But if any of the analogue
// inputs were already partially charged then they gain a bias in analogue_biases_.
//
// It's a little indirect, but it means only having to increment the one value in the
// main loop.
float analogue_charge_ = 0.0f;
float analogue_biases_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
bool analogue_channel_is_discharged(size_t channel) {
return static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1] < analogue_charge_ + analogue_biases_[channel];
}
public:
ConcreteMachine():
m6502_(*this),
@ -198,6 +250,10 @@ class ConcreteMachine:
// Also, start with randomised memory contents.
Memory::Fuzz(ram_, sizeof(ram_));
// Add a couple of joysticks.
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
}
~ConcreteMachine() {
@ -377,16 +433,49 @@ class ConcreteMachine:
break;
case 0xc061: // Switch input 0.
*value &= 0x7f;
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2])
*value |= 0x80;
break;
case 0xc062: // Switch input 1.
*value &= 0x7f;
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1])
*value |= 0x80;
break;
case 0xc063: // Switch input 2.
*value &= 0x7f;
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[2] || static_cast<Joystick *>(joysticks_[1].get())->buttons[0])
*value |= 0x80;
break;
case 0xc064: // Analogue input 0.
case 0xc065: // Analogue input 1.
case 0xc066: // Analogue input 2.
case 0xc067: { // Analogue input 3.
const size_t input = address - 0xc064;
*value &= 0x7f;
if(analogue_channel_is_discharged(input)) {
*value |= 0x80;
}
} break;
}
} else {
// Write-only switches.
}
break;
case 0xc070: { // Permit analogue inputs that are currently discharged to begin a charge cycle.
// Ensure those that were still charging retain that state.
for(size_t c = 0; c < 4; ++c) {
if(analogue_channel_is_discharged(c)) {
analogue_biases_[c] = 0.0f;
} else {
analogue_biases_[c] += analogue_charge_;
}
}
analogue_charge_ = 0.0f;
} break;
/* Read-write switches. */
case 0xc050: update_video(); video_->set_graphics_mode(); break;
case 0xc051: update_video(); video_->set_text_mode(); break;
@ -494,6 +583,9 @@ class ConcreteMachine:
}
}
// Update analogue charge level.
analogue_charge_ = std::min(analogue_charge_ + 1.0f / 2820.0f, 1.0f);
return Cycles(1);
}
@ -619,6 +711,11 @@ class ConcreteMachine:
Configurable::append_quick_load_tape_selection(selection_set, true);
return selection_set;
}
// MARK: JoystickMachine
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
return joysticks_;
}
};
}
@ -630,3 +727,4 @@ Machine *Machine::AppleII() {
}
Machine::~Machine() {}

View File

@ -37,30 +37,27 @@ namespace {
namespace Atari2600 {
class Joystick: public Inputs::Joystick {
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick(Bus *bus, std::size_t shift, std::size_t fire_tia_input) :
ConcreteJoystick({
Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
Input(Input::Fire)
}),
bus_(bus), shift_(shift), fire_tia_input_(fire_tia_input) {}
std::vector<DigitalInput> get_inputs() override {
return {
DigitalInput(DigitalInput::Up),
DigitalInput(DigitalInput::Down),
DigitalInput(DigitalInput::Left),
DigitalInput(DigitalInput::Right),
DigitalInput(DigitalInput::Fire)
};
}
void set_digital_input(const DigitalInput &digital_input, bool is_active) override {
void did_set_input(const Input &digital_input, bool is_active) override {
switch(digital_input.type) {
case DigitalInput::Up: bus_->mos6532_.update_port_input(0, 0x10 >> shift_, is_active); break;
case DigitalInput::Down: bus_->mos6532_.update_port_input(0, 0x20 >> shift_, is_active); break;
case DigitalInput::Left: bus_->mos6532_.update_port_input(0, 0x40 >> shift_, is_active); break;
case DigitalInput::Right: bus_->mos6532_.update_port_input(0, 0x80 >> shift_, is_active); break;
case Input::Up: bus_->mos6532_.update_port_input(0, 0x10 >> shift_, is_active); break;
case Input::Down: bus_->mos6532_.update_port_input(0, 0x20 >> shift_, is_active); break;
case Input::Left: bus_->mos6532_.update_port_input(0, 0x40 >> shift_, is_active); break;
case Input::Right: bus_->mos6532_.update_port_input(0, 0x80 >> shift_, is_active); break;
// TODO: latching
case DigitalInput::Fire:
case Input::Fire:
if(is_active)
bus_->tia_input_value_[fire_tia_input_] &= ~0x80;
else

View File

@ -32,30 +32,29 @@ const int sn76489_divider = 2;
namespace Coleco {
namespace Vision {
class Joystick: public Inputs::Joystick {
class Joystick: public Inputs::ConcreteJoystick {
public:
std::vector<DigitalInput> get_inputs() override {
return {
DigitalInput(DigitalInput::Up),
DigitalInput(DigitalInput::Down),
DigitalInput(DigitalInput::Left),
DigitalInput(DigitalInput::Right),
Joystick() :
ConcreteJoystick({
Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
DigitalInput(DigitalInput::Fire, 0),
DigitalInput(DigitalInput::Fire, 1),
Input(Input::Fire, 0),
Input(Input::Fire, 1),
DigitalInput('0'), DigitalInput('1'), DigitalInput('2'),
DigitalInput('3'), DigitalInput('4'), DigitalInput('5'),
DigitalInput('6'), DigitalInput('7'), DigitalInput('8'),
DigitalInput('9'), DigitalInput('*'), DigitalInput('#'),
};
}
Input('0'), Input('1'), Input('2'),
Input('3'), Input('4'), Input('5'),
Input('6'), Input('7'), Input('8'),
Input('9'), Input('*'), Input('#'),
}) {}
void set_digital_input(const DigitalInput &digital_input, bool is_active) override {
void did_set_input(const Input &digital_input, bool is_active) override {
switch(digital_input.type) {
default: return;
case DigitalInput::Key:
case Input::Key:
if(!is_active) keypad_ |= 0xf;
else {
uint8_t mask = 0xf;
@ -78,11 +77,11 @@ class Joystick: public Inputs::Joystick {
}
break;
case DigitalInput::Up: if(is_active) direction_ &= ~0x01; else direction_ |= 0x01; break;
case DigitalInput::Right: if(is_active) direction_ &= ~0x02; else direction_ |= 0x02; break;
case DigitalInput::Down: if(is_active) direction_ &= ~0x04; else direction_ |= 0x04; break;
case DigitalInput::Left: if(is_active) direction_ &= ~0x08; else direction_ |= 0x08; break;
case DigitalInput::Fire:
case Input::Up: if(is_active) direction_ &= ~0x01; else direction_ |= 0x01; break;
case Input::Right: if(is_active) direction_ &= ~0x02; else direction_ |= 0x02; break;
case Input::Down: if(is_active) direction_ &= ~0x04; else direction_ |= 0x04; break;
case Input::Left: if(is_active) direction_ &= ~0x08; else direction_ |= 0x08; break;
case Input::Fire:
switch(digital_input.info.control.index) {
default: break;
case 0: if(is_active) direction_ &= ~0x40; else direction_ |= 0x40; break;

View File

@ -257,31 +257,28 @@ class Vic6560BusHandler {
/*!
Interfaces a joystick to the two VIAs.
*/
class Joystick: public Inputs::Joystick {
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick(UserPortVIA &user_port_via_port_handler, KeyboardVIA &keyboard_via_port_handler) :
ConcreteJoystick({
Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
Input(Input::Fire)
}),
user_port_via_port_handler_(user_port_via_port_handler),
keyboard_via_port_handler_(keyboard_via_port_handler) {}
std::vector<DigitalInput> get_inputs() override {
return {
DigitalInput(DigitalInput::Up),
DigitalInput(DigitalInput::Down),
DigitalInput(DigitalInput::Left),
DigitalInput(DigitalInput::Right),
DigitalInput(DigitalInput::Fire)
};
}
void set_digital_input(const DigitalInput &digital_input, bool is_active) override {
void did_set_input(const Input &digital_input, bool is_active) override {
JoystickInput mapped_input;
switch(digital_input.type) {
default: return;
case DigitalInput::Up: mapped_input = Up; break;
case DigitalInput::Down: mapped_input = Down; break;
case DigitalInput::Left: mapped_input = Left; break;
case DigitalInput::Right: mapped_input = Right; break;
case DigitalInput::Fire: mapped_input = Fire; break;
case Input::Up: mapped_input = Up; break;
case Input::Down: mapped_input = Down; break;
case Input::Left: mapped_input = Left; break;
case Input::Right: mapped_input = Right; break;
case Input::Fire: mapped_input = Fire; break;
}
user_port_via_port_handler_.set_joystick_state(mapped_input, is_active);

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14109" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14109"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -129,22 +129,6 @@
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" enabled="NO" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" enabled="NO" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" enabled="NO" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" enabled="NO" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
@ -155,192 +139,6 @@
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" enabled="NO" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" enabled="NO" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" enabled="NO" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
@ -357,6 +155,23 @@
</items>
</menu>
</menuItem>
<menuItem title="Input" id="5bL-VY-cxd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Input" id="6yv-Cf-E9r">
<items>
<menuItem title="Use Keyboard as Keyboard" state="on" tag="100" keyEquivalent="k" id="TfX-0B-j4U">
<connections>
<action selector="useKeyboardAsKeyboard:" target="-1" id="6fl-fS-Oe9"/>
</connections>
</menuItem>
<menuItem title="Use Keyboard as Joystick" tag="101" enabled="NO" keyEquivalent="j" id="5mn-ch-Xv6">
<connections>
<action selector="useKeyboardAsJoystick:" target="-1" id="Yz7-CL-f0y"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">

View File

@ -256,4 +256,38 @@ class MachineDocument:
@IBAction func cancelCreateMachine(_ sender: NSButton?) {
close()
}
// MARK: Joystick-via-the-keyboard selection
@IBAction func useKeyboardAsKeyboard(_ sender: NSMenuItem?) {
machine.inputMode = .keyboard
}
@IBAction func useKeyboardAsJoystick(_ sender: NSMenuItem?) {
machine.inputMode = .joystick
}
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if let menuItem = item as? NSMenuItem {
switch item.action {
case #selector(self.useKeyboardAsKeyboard):
if machine == nil || !machine.hasKeyboard {
return false
}
menuItem.state = machine.inputMode == .keyboard ? .on : .off
return true
case #selector(self.useKeyboardAsJoystick):
if machine == nil || !machine.hasJoystick {
return false
}
menuItem.state = machine.inputMode == .joystick ? .on : .off
return true
default: break
}
}
return super.validateUserInterfaceItem(item)
}
}

View File

@ -24,6 +24,11 @@ typedef NS_ENUM(NSInteger, CSMachineVideoSignal) {
CSMachineVideoSignalRGB
};
typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
CSMachineKeyboardInputModeKeyboard,
CSMachineKeyboardInputModeJoystick
};
// Deliberately low; to ensure CSMachine has been declared as an @class already.
#import "CSAtari2600.h"
#import "CSZX8081.h"
@ -63,6 +68,11 @@ typedef NS_ENUM(NSInteger, CSMachineVideoSignal) {
- (bool)supportsVideoSignal:(CSMachineVideoSignal)videoSignal;
// Input control.
@property (nonatomic, readonly) BOOL hasKeyboard;
@property (nonatomic, readonly) BOOL hasJoystick;
@property (nonatomic, assign) CSMachineKeyboardInputMode inputMode;
// Special-case accessors; undefined behaviour if accessed for a machine not of the corresponding type.
@property (nonatomic, readonly) CSAtari2600 *atari2600;
@property (nonatomic, readonly) CSZX8081 *zx8081;

View File

@ -69,6 +69,8 @@ struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate, public LockP
_machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(), error));
if(!_machine) return nil;
_inputMode = _machine->keyboard_machine() ? CSMachineKeyboardInputModeKeyboard : CSMachineKeyboardInputModeJoystick;
_delegateMachineAccessLock = [[NSLock alloc] init];
_speakerDelegate.machine = self;
@ -171,7 +173,7 @@ struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate, public LockP
- (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed {
auto keyboard_machine = _machine->keyboard_machine();
if(keyboard_machine) {
if(self.inputMode == CSMachineKeyboardInputModeKeyboard && keyboard_machine) {
// Don't pass anything on if this is not new information.
if(_depressedKeys[key] == !!isPressed) return;
_depressedKeys[key] = !!isPressed;
@ -248,23 +250,27 @@ struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate, public LockP
}
auto joystick_machine = _machine->joystick_machine();
if(joystick_machine) {
if(self.inputMode == CSMachineKeyboardInputModeJoystick && joystick_machine) {
@synchronized(self) {
std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks = joystick_machine->get_joysticks();
if(!joysticks.empty()) {
// Convert to a C++ bool so that the following calls are resolved correctly even if overloaded.
bool is_pressed = !!isPressed;
switch(key) {
case VK_LeftArrow: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Left, isPressed); break;
case VK_RightArrow: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Right, isPressed); break;
case VK_UpArrow: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Up, isPressed); break;
case VK_DownArrow: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Down, isPressed); break;
case VK_Space: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Fire, isPressed); break;
case VK_ANSI_A: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(Inputs::Joystick::DigitalInput::Fire, 0), isPressed); break;
case VK_ANSI_S: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(Inputs::Joystick::DigitalInput::Fire, 1), isPressed); break;
case VK_LeftArrow: joysticks[0]->set_input(Inputs::Joystick::Input::Left, is_pressed); break;
case VK_RightArrow: joysticks[0]->set_input(Inputs::Joystick::Input::Right, is_pressed); break;
case VK_UpArrow: joysticks[0]->set_input(Inputs::Joystick::Input::Up, is_pressed); break;
case VK_DownArrow: joysticks[0]->set_input(Inputs::Joystick::Input::Down, is_pressed); break;
case VK_Space: joysticks[0]->set_input(Inputs::Joystick::Input::Fire, is_pressed); break;
case VK_ANSI_A: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 0), is_pressed); break;
case VK_ANSI_S: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 1), is_pressed); break;
case VK_ANSI_D: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 2), is_pressed); break;
case VK_ANSI_F: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 3), is_pressed); break;
default:
if(characters) {
joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput([characters characterAtIndex:0]), isPressed);
joysticks[0]->set_input(Inputs::Joystick::Input([characters characterAtIndex:0]), is_pressed);
} else {
joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Fire, isPressed);
joysticks[0]->set_input(Inputs::Joystick::Input::Fire, is_pressed);
}
break;
}
@ -392,4 +398,14 @@ struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate, public LockP
return [[CSZX8081 alloc] initWithZX8081:_machine->raw_pointer() owner:self];
}
#pragma mark - Input device queries
- (BOOL)hasJoystick {
return !!_machine->joystick_machine();
}
- (BOOL)hasKeyboard {
return !!_machine->keyboard_machine();
}
@end

View File

@ -464,16 +464,16 @@ int main(int argc, char *argv[]) {
std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks = joystick_machine->get_joysticks();
if(!joysticks.empty()) {
switch(event.key.keysym.scancode) {
case SDL_SCANCODE_LEFT: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Left, is_pressed); break;
case SDL_SCANCODE_RIGHT: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Right, is_pressed); break;
case SDL_SCANCODE_UP: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Up, is_pressed); break;
case SDL_SCANCODE_DOWN: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Down, is_pressed); break;
case SDL_SCANCODE_SPACE: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Fire, is_pressed); break;
case SDL_SCANCODE_A: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(Inputs::Joystick::DigitalInput::Fire, 0), is_pressed); break;
case SDL_SCANCODE_S: joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(Inputs::Joystick::DigitalInput::Fire, 1), is_pressed); break;
case SDL_SCANCODE_LEFT: joysticks[0]->set_input(Inputs::Joystick::Input::Left, is_pressed); break;
case SDL_SCANCODE_RIGHT: joysticks[0]->set_input(Inputs::Joystick::Input::Right, is_pressed); break;
case SDL_SCANCODE_UP: joysticks[0]->set_input(Inputs::Joystick::Input::Up, is_pressed); break;
case SDL_SCANCODE_DOWN: joysticks[0]->set_input(Inputs::Joystick::Input::Down, is_pressed); break;
case SDL_SCANCODE_SPACE: joysticks[0]->set_input(Inputs::Joystick::Input::Fire, is_pressed); break;
case SDL_SCANCODE_A: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 0), is_pressed); break;
case SDL_SCANCODE_S: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 1), is_pressed); break;
default: {
const char *key_name = SDL_GetKeyName(event.key.keysym.sym);
joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(key_name[0]), is_pressed);
joysticks[0]->set_input(Inputs::Joystick::Input(key_name[0]), is_pressed);
} break;
}
}