diff --git a/README.md b/README.md index 18cc5e7..4a5e8a3 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,25 @@ 1. [What Is It?](#what-is-it) 2. [License](#license) -3. [Compiling](#compiling) -4. [Running](#running) +3. [Requirements](#requirements) + 1. [Linux](#linux) + 2. [Windows](#windows) +4. [Compiling From Source](#compiling-from-source) +5. [Running](#running) 1. [Specifying a System ROM](#specifying-a-system-rom) 2. [Trace Mode](#trace-mode) -5. [Cassette Tapes](#cassette-tapes) +6. [Cassette Tapes](#cassette-tapes) 1. [Reading](#reading) 2. [Writing](#writing) -6. [Disk Drives](#disk-drives) +7. [Disk Drives](#disk-drives) 1. [Loading a Disk Image](#loading-a-disk-image) 2. [Saving a Disk Image](#saving-a-disk-image) -7. [Configuration File](#configuration-file) -8. [Keyboard](#keyboard) +8. [Joysticks](#joysticks) +9. [Configuration File](#configuration-file) +10. [Keyboard](#keyboard) 1. [Emulated Keyboard](#emulated-keyboard) 2. [Pass-through Keyboard](#pass-through-keyboard) -9. [Current Status](#current-status) +11. [Current Status](#current-status) ## What Is It? @@ -53,36 +57,129 @@ Third Party Licenses and Attributions below for more information on those software components. -## Compiling +## Requirements -You will need a copy of the Java Development Kit (JDK) version 8 or greater -installed in to compile the JAR file. I strongly recommend using an open-source -licensed JDK build (GPL v2 with Classpath Exception), available at -[https://adoptopenjdk.net](https://adoptopenjdk.net) and install OpenJDK 8 or -OpenJDK 11. +The project needs several different packages installed in order to run the +emulator properly. Please see the platform specific steps below for +more information. -To build the project, switch to the root of the source directory, and type: +### Linux + +At a minimum, you will need to install the Java Runtime Environment (JRE) 17 or +higher. Additionally, if you wish to use joysticks, you will need to install the +`libjinput` API bindings, add your username to the `group` file, as well as fix a +potential API binding bug. + +1. *Required* - a Java Runtime Environment (JRE) version 17 or higher. The simplest way to +do this is to install OpenJDK 17 or higher. On Ubuntu or Debian systems, this can +be done with : + ```bash + sudo apt update + sudo apt install openjdk-17-jre + ``` + +2. *Optional* - for proper joystick support you will need to install the `jinput` joystick +library API on the system with: + + ```bash + sudo apt install libjinput-java libjinput-jni + ``` + + Once installed, you may need to correct a missing file problem with the `libjinput-jni` + installation. The emulator dependencies require a shared object file called + `libjinput-linux64.so` to be present in the `/usr/lib/jni` directory. However, the + `libjinput-jni` package may only install a file called `libjinput.so`. The + solution is to create a symbolic link from `libjinput.so` to `linjinput-linux64.so`. + First, check to see if the `libjinput-linux64.so` file already exists with: + + ```bash + ls -l /usr/lib/jni + ``` + + If the `libjinput-linux64.so` file is *NOT* listed, you will need to create a symbolic + link with the following command: + + ```bash + sudo ln -s /usr/lib/jni/libjinput.so /usr/lib/jni/libjinput-linux64.so + ``` + + Finally, you will need to add yourself to the `input` group so that the emulator can read + joystick information: + + ```bash + sudo usermod -a -G input + ``` + + You will need to end your current session and restart in order for the group information + to be updated. In some cases, you may need to reboot for the group change to take effect. + +To run the emulator, see the section called [Running](#running) below. + +### Windows + +At a minimum, you will need to install the Java Runtime Environment (JRE) 17 or +higher. + +1. *Required* - a Java Runtime Environment (JRE) version 17 or higher. I recommend using + Eclipse Temurin JRE (formerly AdoptJDK) as the software + is licensed under the GNU license version 2 with classpath exception. The latest + JRE builds are available at [https://adoptium.net/en-GB/temurin/releases](https://adoptium.net/en-GB/temurin/releases) + (make sure you select _JRE_ as the type you wish to download). Run the installer + and follow the directions as required to install the runtime. + +To run the emulator, see the section called [Running](#running) below. + +## Compiling From Source + +_Note this section is optional - this is only if you want to compile the project +yourself from source code._ If you want to build the emulator from source code, +you will need a copy of the Java Development Kit (JDK) version 17 or greater +installed in to compile the JAR file. + +### Linux + +For most Linux distributions there is likely an `openjdk-17-jdk` package that will do +this for you automatically. On Ubuntu and Debian based systems, this is typically: + +```bash +sudo apt update +sudo apt install openjdk-17-jdk +``` + +Next, to build the project, switch to the root of the source directory, and type: + ```bash ./gradlew build ``` -On Windows, switch to the root of the source directory, and type: +The compiled Jar file will be placed in the `build/libs` directory. Note that +for some components such as joystick detection and control to work correctly, +operating-specific steps may be required. See the _Requirements_ section above +to install necessary sub-systems. + + +### Windows + +For Windows, I recommend using Eclipse Temurin JDK (formerly AdoptJDK) as the software +is licensed under the GNU license version 2 with classpath exception. The latest +JDK builds are available at [https://adoptium.net/en-GB/temurin/releases](https://adoptium.net/en-GB/temurin/releases) +(make sure you select _JDK_ as the type you wish to download). + +Next, switch to the root of the source directory, and type: ```bash gradlew.bat build ``` -The compiled Jar file will be placed in the `build/libs` directory. +The compiled Jar file will be placed in the `build/libs` directory. Note that +for some components such as joystick detection and control to work correctly, +operating-specific steps may be required. See the _Requirements_ section above +to install necessary sub-systems. ## Running -For the emulator to run, you will need to have the Java 8 Runtime Environment (JRE) -installed on your computer. See [Oracle's Java SE Runtime Environment Download](https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html) -page for more information on installing the JRE. Alternatively, you can -use [OpenJDK](https://openjdk.java.net/install/). - Simply double-clicking the jar file will start the emulator running. By default, the emulator will be in paused mode until you attach a system ROM to it. You can do so by clicking *ROM*, *Load System ROM*. You can @@ -192,6 +289,22 @@ select a location on your computer where the disk file will be saved to. Once entered, the contents of the drive will be saved to the virtual disk file, and can be loaded from the host computer in a future session. +## Joysticks + +Joystick control is still experimental and only currently available under +Linux. When running the emulator, to set the joystick to be used, +click on *Joystick* and then *Left Joystick* or *Right Joystick* to select +which device should be used as the left or right joystick. If no USB +joystick devices are found, then *No joystick* will be the only option +available. + +To troubleshoot joystick configuration, when the emulator starts, a +list of detected joysticks will be printed to the console. The +`Joystick #0` device will always be `No Joystick`. Other enumerated +joysticks will be printed after this one. The name of the detected joystick +can be used in the configuration file below to automatically associate +the left or right joystick device during emulator startup. + ## Configuration File @@ -205,19 +318,22 @@ Super Extended Color Basic ROM file). Megabug). * `cassetteROM` - the full path to the ROM file used in the cassette recorder. * `drive0Image` - the `DSK` image to be used in drive 0. -* `drive1Image` - the `DSK` image to be used in drive 0. -* `drive2Image` - the `DSK` image to be used in drive 0. -* `drive3Image` - the `DSK` image to be used in drive 0. +* `drive1Image` - the `DSK` image to be used in drive 1. +* `drive2Image` - the `DSK` image to be used in drive 2. +* `drive3Image` - the `DSK` image to be used in drive 3. +* `leftJoystick` - the name of the detected joystick to use as the left joystick. +* `rightJoystick` - the name of the detected joystick to use as the right joystick. Leaving any one of the keys out will result in the emulator ignoring that particular -ROM image. An example YAML configuration file that specifies ROMs to use for the -system, cartridge slot, cassette, and drive 0 is as follows: +configuration option. An example YAML configuration file that specifies ROMs to use for the +system, cartridge slot, cassette, joystick, and drive 0 is as follows: ``` systemROM: "C:\Users\basic3.rom" cartridgeROM: "C:\disk11.rom" cassetteROM: "C:\Users\zaxxon.cas" drive0Image: "C:\megabug.dsk" +leftJoystick: "usb gamepad" ``` If you start the emulator without command-line arguments, it will look for a configuration file named diff --git a/build.gradle b/build.gradle index 2ad8b5b..23cb0b0 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,9 @@ dependencies { implementation 'org.apache.commons:commons-lang3:3.20.0' implementation 'com.beust:jcommander:1.82' implementation 'org.yaml:snakeyaml:2.5' + implementation 'net.java.jinput:jinput:2.0.10' + runtimeOnly 'net.java.jinput:jinput-platform:2.0.7:natives-linux' + implementation 'net.java.jutils:jutils:1.0.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:5.21.0' } diff --git a/src/main/java/ca/craigthomas/yacoco3e/components/Emulator.java b/src/main/java/ca/craigthomas/yacoco3e/components/Emulator.java index f7d3bba..bbd378a 100644 --- a/src/main/java/ca/craigthomas/yacoco3e/components/Emulator.java +++ b/src/main/java/ca/craigthomas/yacoco3e/components/Emulator.java @@ -6,6 +6,8 @@ import ca.craigthomas.yacoco3e.datatypes.*; import ca.craigthomas.yacoco3e.listeners.*; +import net.java.games.input.Controller; +import net.java.games.input.ControllerEnvironment; import javax.swing.*; import java.awt.*; @@ -13,6 +15,7 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.TimerTask; import java.util.Timer; import java.util.logging.Logger; @@ -31,6 +34,8 @@ public class Emulator extends Thread private IOController io; private Cassette cassette; private Memory memory; + private int leftJoystickNumber; + private int rightJoystickNumber; // The Canvas on which all the drawing will take place private Canvas canvas; @@ -39,6 +44,8 @@ public class Emulator extends Thread private JFrame container; private JMenuBar menuBar; + Controller [] controllers; + /* State variables */ public boolean trace; private boolean verbose; @@ -117,6 +124,7 @@ private Emulator(Builder builder) { io = new IOController(memory, new RegisterSet(), keyboard, screen, cassette, builder.useDAC); cpu = new CPU(io); io.setCPU(cpu); + trace = builder.trace; verbose = builder.verbose; status = EmulatorStatus.STOPPED; @@ -129,11 +137,9 @@ private Emulator(Builder builder) { } } } catch (Exception e) { - System.out.println("Nimbus LAF not available"); + LOGGER.warning("Nimbus LAF not available"); } - initEmulatorJFrame(); - // Check to see if we specified a configuration file ConfigFile builderConfig = ConfigFile.parseConfigFile(builder.configFile); @@ -142,6 +148,9 @@ private Emulator(Builder builder) { // Attempt to load the assets for the emulator loadAssets(builderConfig, commandLineConfig); + + // Initialize the main emulator JFrame + initEmulatorJFrame(); } /** @@ -197,6 +206,7 @@ public void loadFromConfigFile(ConfigFile config) { } } + // Load drive images String drive0 = config.getDrive0Image(); if (drive0 != null) { JV1Disk disk = new JV1Disk(); @@ -204,6 +214,29 @@ public void loadFromConfigFile(ConfigFile config) { io.disk[0].loadFromVirtualDisk(disk); } } + + // Enumerate joystick controllers and set current joystick types + controllers = ControllerEnvironment.getDefaultEnvironment().getControllers(); + LOGGER.info("Joystick #0, Name: 'No Joystick', Type: None"); + leftJoystickNumber = 0; + rightJoystickNumber = 0; + int index = 1; + for (Controller controller : controllers) { + Controller.Type controllerType = controller.getType(); + if (controllerType != Controller.Type.MOUSE && controllerType != Controller.Type.KEYBOARD) { + LOGGER.info("Joystick #" + index + ", Name: '" + controller.getName() + "', Type: " + controller.getType()); + if (controller.getName().stripTrailing().equals(config.getLeftJoystick())) { + leftJoystickNumber = index; + } + if (controller.getName().stripTrailing().equals(config.getRightJoystick())) { + rightJoystickNumber = index; + } + index++; + } + } + io.setJoystickControllers(controllers); + io.setLeftJoystickNumber(leftJoystickNumber); + io.setRightJoystickNumber(rightJoystickNumber); } /** @@ -344,6 +377,74 @@ private void initEmulatorJFrame() { menuBar.add(keyboardMenu); + // Joystick menu + JMenu joystickMenu = new JMenu("Joystick"); + joystickMenu.setMnemonic(KeyEvent.VK_J); + + JMenu leftJoystickMenuItem = new JMenu("Left Joystick"); + leftJoystickMenuItem.setMnemonic(KeyEvent.VK_L); + + JRadioButtonMenuItem leftJoystickNoneMenuItem = new JRadioButtonMenuItem("No Joystick"); + leftJoystickNoneMenuItem.setSelected(leftJoystickNumber == 0); + leftJoystickMenuItem.add(leftJoystickNoneMenuItem); + + // Enumerate all joystick devices and add menu items for each + ArrayList options = new ArrayList<>(); + options.add(leftJoystickNoneMenuItem); + + // Loop through all the joystick controllers and generate menu items for each + int index = 1; + for (Controller controller : controllers) { + Controller.Type controllerType = controller.getType(); + if (controllerType != Controller.Type.MOUSE && controllerType != Controller.Type.KEYBOARD) { + JRadioButtonMenuItem leftJoystickControllerMenuItem = new JRadioButtonMenuItem(controller.getName()); + leftJoystickControllerMenuItem.setSelected(leftJoystickNumber == index); + leftJoystickMenuItem.add(leftJoystickControllerMenuItem); + options.add(leftJoystickControllerMenuItem); + index++; + } + } + + // Reset the joystick index and add the list of joystick menu items to the menu + index = 0; + for (JRadioButtonMenuItem button : options) { + button.addActionListener(new SetLeftJoystickMenuItemActionListener(this, options.toArray(new JRadioButtonMenuItem[0]), index)); + index++; + } + + joystickMenu.add(leftJoystickMenuItem); + + JMenu rightJoystickMenuItem = new JMenu("Right Joystick"); + rightJoystickMenuItem.setMnemonic(KeyEvent.VK_R); + + JRadioButtonMenuItem rightJoystickNoneMenuItem = new JRadioButtonMenuItem("No Joystick"); + rightJoystickNoneMenuItem.setSelected(rightJoystickNumber == 0); + rightJoystickMenuItem.add(rightJoystickNoneMenuItem); + + options = new ArrayList<>(); + options.add(rightJoystickNoneMenuItem); + + index = 1; + for (Controller controller : controllers) { + Controller.Type controllerType = controller.getType(); + if (controllerType != Controller.Type.MOUSE && controllerType != Controller.Type.KEYBOARD) { + JRadioButtonMenuItem rightJoystickControllerMenuItem = new JRadioButtonMenuItem(controller.getName()); + rightJoystickControllerMenuItem.setSelected(rightJoystickNumber == index); + rightJoystickMenuItem.add(rightJoystickControllerMenuItem); + options.add(rightJoystickControllerMenuItem); + } + } + + // Reset the joystick index and add the list of joystick menu items to the menu + index = 0; + for (JRadioButtonMenuItem button : options) { + button.addActionListener(new SetRightJoystickMenuItemActionListener(this, options.toArray(new JRadioButtonMenuItem[0]), index)); + index++; + } + + joystickMenu.add(rightJoystickMenuItem); + menuBar.add(joystickMenu); + // Debug menu JMenu debugMenu = new JMenu("Debugging"); debugMenu.setMnemonic(KeyEvent.VK_U); @@ -403,6 +504,24 @@ public void switchKeyListener(Keyboard newKeyboard) { io.setKeyboard(keyboard); } + /** + * Switches out the existing left joystick for a new one. + * + * @param newJoystickNumber the new joystick device number to use (0 for none) + */ + public void switchLeftJoystick(int newJoystickNumber) { + io.setLeftJoystickNumber(newJoystickNumber); + } + + /** + * Switches out the existing right joystick for a new one. + * + * @param newJoystickNumber the new joystick device number to use (0 for none) + */ + public void switchRightJoystick(int newJoystickNumber) { + io.setRightJoystickNumber(newJoystickNumber); + } + /** * Will redraw the contents of the screen to the emulator window. Optionally, if * isInTraceMode is True, will also draw the contents of the overlayScreen to the screen. @@ -430,6 +549,7 @@ public void start() { screenRefreshTimer = new Timer(); screenRefreshTimerTask = new TimerTask() { public void run() { + io.pollJoysticks(); screen.refreshScreen(); refreshScreen(); } diff --git a/src/main/java/ca/craigthomas/yacoco3e/components/IOController.java b/src/main/java/ca/craigthomas/yacoco3e/components/IOController.java index a631870..7747334 100644 --- a/src/main/java/ca/craigthomas/yacoco3e/components/IOController.java +++ b/src/main/java/ca/craigthomas/yacoco3e/components/IOController.java @@ -6,6 +6,10 @@ import ca.craigthomas.yacoco3e.datatypes.*; import ca.craigthomas.yacoco3e.datatypes.screen.ScreenMode.Mode; +import net.java.games.input.Component; +import net.java.games.input.Controller; + +import java.util.logging.Logger; import static ca.craigthomas.yacoco3e.datatypes.RegisterSet.*; @@ -109,6 +113,15 @@ public class IOController public volatile int tickRefreshAmount; + // Joystick controllers + public int leftJoystickNumber; + public int rightJoystickNumber; + public Controller [] controllers; + public Controller leftJoystick; + public Controller rightJoystick; + + /* A logger for the IO controller */ + private final static Logger LOGGER = Logger.getLogger(IOController.class.getName()); public IOController(Memory memory, RegisterSet registerSet, Keyboard keyboard, Screen screen, Cassette cassette, boolean useDAC) { ioMemory = new short[IO_ADDRESS_SIZE]; @@ -118,6 +131,10 @@ public IOController(Memory memory, RegisterSet registerSet, Keyboard keyboard, S this.lowResolutionDisplayActive = false; this.screen = screen; this.cassette = cassette; + this.leftJoystick = null; + this.leftJoystickNumber = 0; + this.rightJoystick = null; + this.rightJoystickNumber = 0; /* Screen controls */ samControlBits = new UnsignedByte(); @@ -126,10 +143,10 @@ public IOController(Memory memory, RegisterSet registerSet, Keyboard keyboard, S deviceSelectorSwitch = new DeviceSelectorSwitch(); /* PIAs */ - pia1a = new PIA1a(keyboard, deviceSelectorSwitch); - pia1b = new PIA1b(keyboard, deviceSelectorSwitch); - pia2a = new PIA2a(cassette, useDAC); pia2b = new PIA2b(this); + pia2a = new PIA2a(cassette, useDAC); + pia1b = new PIA1b(keyboard, deviceSelectorSwitch); + pia1a = new PIA1a(keyboard, deviceSelectorSwitch, pia2a); /* Display registers */ verticalOffsetRegister1 = new UnsignedByte(0x04); @@ -206,6 +223,51 @@ public void setKeyboard(Keyboard keyboard) { this.keyboard = keyboard; } + /** + * Creates a back-reference to the list of joystick controllers. + * + * @param controllers the list of controllers on the system + */ + public void setJoystickControllers(Controller [] controllers) { + this.controllers = controllers; + } + + /** + * Sets the left joystick device based on the controller number (0 = none). + * + * @param controllerNumber the device controller number to use + */ + public void setLeftJoystickNumber(int controllerNumber) { + this.leftJoystickNumber = controllerNumber; + if (controllerNumber == 0) { + this.leftJoystick = null; + LOGGER.info("Set left joystick to 'No Joystick'"); + } else { + if (controllerNumber <= controllers.length) { + this.leftJoystick = controllers[controllerNumber - 1]; + LOGGER.info("Set left joystick to '" + this.leftJoystick.getName() + "'"); + } + } + } + + /** + * Sets the right joystick device based on the controller number (0 = none). + * + * @param controllerNumber the device controller number to use + */ + public void setRightJoystickNumber(int controllerNumber) { + this.rightJoystickNumber = controllerNumber; + if (controllerNumber == 0) { + this.rightJoystick = null; + LOGGER.info("Set right joystick to 'No Joystick'"); + } else { + if (controllerNumber <= controllers.length) { + this.rightJoystick = controllers[controllerNumber - 1]; + LOGGER.info("Set right joystick to '" + this.rightJoystick.getName() + "'"); + } + } + } + /** * Reads a byte from RAM, bypassing the MMU, and reading only from * the physical RAM array. This method should be used by devices @@ -1279,9 +1341,78 @@ public void nonMaskableInterrupt() { cpu.scheduleNMI(); } + /** + * Sends a shutdown signal to the PIA2. + */ public void shutdown() { if (pia2a != null) { pia2a.shutdown(); } } + + /** + * Polls the available joysticks for state data. There are only 2 things we are + * concerned with: + * + * Analog data: + * x-axis - is usually named 'x' and will range from -1.0 to 1.0 + * y-axis - is usually named 'y' and will range from -1.0 to 1.0 + * + * Non-analog data: + * any button - when depressed will read as true, when released is false + * + * Essentially, the polling routine will read the x-axis and y-axis data + * and return them as floats. Any other non-analog input, if it is pressed, + * will trigger a fire button. + */ + public void pollJoysticks() { + if (leftJoystick != null) { + leftJoystick.poll(); + Component [] components = leftJoystick.getComponents(); + float x = 0.0f; + float y = 0.0f; + boolean fire = false; + for (Component component : components) { + if (component.getName().equals("x") && component.isAnalog()) { + x = component.getPollData(); + } + if (component.getName().equals("y") && component.isAnalog()) { + y = component.getPollData(); + } + if (!component.isAnalog()) { + fire |= component.getPollData() == 1.0f; + } + } + + // Scale output to 4.5 volts and set the current state + x = ((x + 1.0f) / 2.0f) * 4.5f; + y = ((y + 1.0f) / 2.0f) * 4.5f; + + pia1a.setLeftJoystickState(x, y, fire); + } + + if (rightJoystick != null) { + rightJoystick.poll(); + Component [] components = rightJoystick.getComponents(); + float x = 0.0f; + float y = 0.0f; + boolean fire = false; + for (Component component : components) { + if (component.getName().equals("x") && component.isAnalog()) { + x = component.getPollData(); + } + if (component.getName().equals("y") && component.isAnalog()) { + y = component.getPollData(); + } + if (!component.isAnalog()) { + fire |= component.getPollData() == 1.0f; + } + } + + // Scale output to 4.5 volts and set the current state + x = ((x + 1.0f) / 2.0f) * 4.5f; + y = ((y + 1.0f) / 2.0f) * 4.5f; + pia1a.setRightJoystickState(x, y, fire); + } + } } diff --git a/src/main/java/ca/craigthomas/yacoco3e/components/PIA1a.java b/src/main/java/ca/craigthomas/yacoco3e/components/PIA1a.java index 38df443..76d9c60 100644 --- a/src/main/java/ca/craigthomas/yacoco3e/components/PIA1a.java +++ b/src/main/java/ca/craigthomas/yacoco3e/components/PIA1a.java @@ -14,23 +14,87 @@ public class PIA1a extends PIA protected Keyboard keyboard; protected DeviceSelectorSwitch deviceSelectorSwitch; protected int timerValue; + protected boolean leftJoystickFire; + protected float leftJoystickX; + protected float leftJoystickY; + protected boolean rightJoystickFire; + protected float rightJoystickX; + protected float rightJoystickY; + protected PIA2a pia2a; - public PIA1a(Keyboard newKeyboard, DeviceSelectorSwitch newDeviceSelectorSwitch) { + public PIA1a(Keyboard newKeyboard, DeviceSelectorSwitch newDeviceSelectorSwitch, PIA2a pia2a) { super(); keyboard = newKeyboard; timerValue = 0; deviceSelectorSwitch = newDeviceSelectorSwitch; + leftJoystickFire = false; + leftJoystickX = 2.25f; + leftJoystickY = 2.25f; + rightJoystickFire = false; + rightJoystickX = 2.25f; + rightJoystickY = 2.25f; + this.pia2a = pia2a; + } + + public void setLeftJoystickState(float x, float y, boolean fire) { + leftJoystickFire = fire; + leftJoystickX = x; + leftJoystickY = y; + } + + public void setRightJoystickState(float x, float y, boolean fire) { + rightJoystickFire = fire; + rightJoystickX = x; + rightJoystickY = y; } /** * In PIA 1 side A, the data register is connected to the keyboard. The high * byte pattern of the keyboard is returned as the contents of the data register. + * Also mix in the joystick fire buttons as appropriate. * * @return the high byte of the keyboard matrix */ @Override public UnsignedByte getDataRegister() { - return keyboard.getHighByte(); + // Check to see if we should output CA2 low on read + if (!controlRegister.isMasked(0x8)) { + deviceSelectorSwitch.setCA2(false); + } + + // Check to see if our joystick values are higher than PIA2 voltage + boolean fireComparator = false; + switch (deviceSelectorSwitch.switchPosition) { + case 0: + fireComparator = rightJoystickX > pia2a.getVoltage(); + break; + + case 1: + fireComparator = rightJoystickY > pia2a.getVoltage(); + break; + + case 2: + fireComparator = leftJoystickX > pia2a.getVoltage(); + break; + + case 3: + fireComparator = leftJoystickY > pia2a.getVoltage(); + break; + } + + UnsignedByte result = keyboard.getHighByte(); + result.and(leftJoystickFire ? ~0x2 : ~0x0); + result.and(rightJoystickFire ? ~0x1 : ~0x0); + result.and(~0x80); + result.or(fireComparator ? 0x80 : 0x0); + + // Check to see if we should transition CA2 high again + if (!controlRegister.isMasked(0x8)) { + if (controlRegister.isMasked(0x4)) { + deviceSelectorSwitch.setCA2(true); + } + } + return result; } /** diff --git a/src/main/java/ca/craigthomas/yacoco3e/components/PIA2a.java b/src/main/java/ca/craigthomas/yacoco3e/components/PIA2a.java index 060f723..3cafe47 100644 --- a/src/main/java/ca/craigthomas/yacoco3e/components/PIA2a.java +++ b/src/main/java/ca/craigthomas/yacoco3e/components/PIA2a.java @@ -90,4 +90,8 @@ public void setControlRegister(UnsignedByte newControlRegister) { cassette.setMotorOn(controlRegister.isMasked(0x08)); } + + public float getVoltage() { + return voltage; + } } diff --git a/src/main/java/ca/craigthomas/yacoco3e/datatypes/ConfigFile.java b/src/main/java/ca/craigthomas/yacoco3e/datatypes/ConfigFile.java index bfaf791..e5043aa 100644 --- a/src/main/java/ca/craigthomas/yacoco3e/datatypes/ConfigFile.java +++ b/src/main/java/ca/craigthomas/yacoco3e/datatypes/ConfigFile.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Craig Thomas + * Copyright (C) 2023-2025 Craig Thomas * This project uses an MIT style license - see LICENSE for details. */ package ca.craigthomas.yacoco3e.datatypes; @@ -20,11 +20,13 @@ public class ConfigFile private String drive2Image; private String drive3Image; private String cassetteROM; + private String rightJoystick; + private String leftJoystick; public ConfigFile() { } public ConfigFile(String system, String cartridge, String drive0, String drive1, String drive2, - String drive3, String cassette) { + String drive3, String cassette, String leftJoy, String rightJoy) { systemROM = system; cartridgeROM = cartridge; drive0Image = drive0; @@ -32,6 +34,8 @@ public ConfigFile(String system, String cartridge, String drive0, String drive1, drive2Image = drive2; drive3Image = drive3; cassetteROM = cassette; + leftJoystick = leftJoy; + rightJoystick = rightJoy; } public ConfigFile(String system, String cartridge, String cassette) { @@ -54,7 +58,8 @@ public boolean hasCassetteROM() { public boolean isEmpty() { return (systemROM == null) && (cartridgeROM == null) && (cassetteROM == null) && (drive0Image == null) && - (drive1Image == null) && (drive2Image == null) && (drive3Image == null); + (drive1Image == null) && (drive2Image == null) && (drive3Image == null) && (leftJoystick == null) && + (rightJoystick == null); } public String getSystemROM() { @@ -113,6 +118,22 @@ public void setCassetteROM(String cassetteROM) { this.cassetteROM = cassetteROM; } + public String getRightJoystick() { + return rightJoystick; + } + + public void setRightJoystick(String rightJoystick) { + this.rightJoystick = rightJoystick; + } + + public String getLeftJoystick() { + return leftJoystick; + } + + public void setLeftJoystick(String leftJoystick) { + this.leftJoystick = leftJoystick; + } + /** * Parses a configuration file. Must contain valid YAML. * diff --git a/src/main/java/ca/craigthomas/yacoco3e/listeners/SetLeftJoystickMenuItemActionListener.java b/src/main/java/ca/craigthomas/yacoco3e/listeners/SetLeftJoystickMenuItemActionListener.java new file mode 100644 index 0000000..52e0acb --- /dev/null +++ b/src/main/java/ca/craigthomas/yacoco3e/listeners/SetLeftJoystickMenuItemActionListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2017-2025 Craig Thomas + * This project uses an MIT style license - see LICENSE for details. + */ +package ca.craigthomas.yacoco3e.listeners; + +import ca.craigthomas.yacoco3e.components.Emulator; +import ca.craigthomas.yacoco3e.components.PassthroughKeyboard; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * An ActionListener that will set which keyboard type is active. + */ +public class SetLeftJoystickMenuItemActionListener extends AbstractFileChooserListener implements ActionListener +{ + private final Emulator emulator; + private final JRadioButtonMenuItem [] options; + private final int selection; + + public SetLeftJoystickMenuItemActionListener(Emulator emulator, JRadioButtonMenuItem [] options, int selection) { + super(); + this.emulator = emulator; + this.options = options; + this.selection = selection; + } + + @Override + public void actionPerformed(ActionEvent e) { + setLeftJoystick(); + } + + public void setLeftJoystick() { + for (int i=0; i < options.length; i++) { + options[i].setSelected(false); + if (i == selection) { + options[i].setSelected(true); + emulator.switchLeftJoystick(i); + } + } + } +} diff --git a/src/main/java/ca/craigthomas/yacoco3e/listeners/SetPassthroughKeyboardActionListener.java b/src/main/java/ca/craigthomas/yacoco3e/listeners/SetPassthroughKeyboardActionListener.java index 8c7a96f..5e60c92 100644 --- a/src/main/java/ca/craigthomas/yacoco3e/listeners/SetPassthroughKeyboardActionListener.java +++ b/src/main/java/ca/craigthomas/yacoco3e/listeners/SetPassthroughKeyboardActionListener.java @@ -12,7 +12,7 @@ import java.awt.event.ActionListener; /** - * An ActionListener that will quit the emulator. + * An ActionListener that will set which keyboard type is active. */ public class SetPassthroughKeyboardActionListener extends AbstractFileChooserListener implements ActionListener { diff --git a/src/main/java/ca/craigthomas/yacoco3e/listeners/SetRightJoystickMenuItemActionListener.java b/src/main/java/ca/craigthomas/yacoco3e/listeners/SetRightJoystickMenuItemActionListener.java new file mode 100644 index 0000000..4eb13a2 --- /dev/null +++ b/src/main/java/ca/craigthomas/yacoco3e/listeners/SetRightJoystickMenuItemActionListener.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017-2025 Craig Thomas + * This project uses an MIT style license - see LICENSE for details. + */ +package ca.craigthomas.yacoco3e.listeners; + +import ca.craigthomas.yacoco3e.components.Emulator; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * An ActionListener that will set which keyboard type is active. + */ +public class SetRightJoystickMenuItemActionListener extends AbstractFileChooserListener implements ActionListener +{ + private final Emulator emulator; + private final JRadioButtonMenuItem [] options; + private final int selection; + + public SetRightJoystickMenuItemActionListener(Emulator emulator, JRadioButtonMenuItem [] options, int selection) { + super(); + this.emulator = emulator; + this.options = options; + this.selection = selection; + } + + @Override + public void actionPerformed(ActionEvent e) { + setRightJoystick(); + } + + public void setRightJoystick() { + for (int i=0; i < options.length; i++) { + options[i].setSelected(false); + if (i == selection) { + options[i].setSelected(true); + emulator.switchRightJoystick(i); + } + } + } +} diff --git a/src/test/java/ca/craigthomas/yacoco3e/components/ConfigFileTest.java b/src/test/java/ca/craigthomas/yacoco3e/components/ConfigFileTest.java index 5e991fa..af1417e 100644 --- a/src/test/java/ca/craigthomas/yacoco3e/components/ConfigFileTest.java +++ b/src/test/java/ca/craigthomas/yacoco3e/components/ConfigFileTest.java @@ -63,6 +63,8 @@ public void testSettersAndGetters() { configFile.setDrive1Image("1"); configFile.setDrive2Image("2"); configFile.setDrive3Image("3"); + configFile.setLeftJoystick("left joystick"); + configFile.setRightJoystick("right joystick"); assertEquals("system", configFile.getSystemROM()); assertEquals("cartridge", configFile.getCartridgeROM()); assertEquals("cassette", configFile.getCassetteROM()); @@ -70,6 +72,8 @@ public void testSettersAndGetters() { assertEquals("1", configFile.getDrive1Image()); assertEquals("2", configFile.getDrive2Image()); assertEquals("3", configFile.getDrive3Image()); + assertEquals("left joystick", configFile.getLeftJoystick()); + assertEquals("right joystick", configFile.getRightJoystick()); } @Test @@ -106,6 +110,14 @@ public void testIsEmptyTrueWhenAtLeastOneExists() { configFile.setDrive3Image("not empty"); assertFalse(configFile.isEmpty()); configFile = new ConfigFile(); + + configFile.setLeftJoystick("not empty"); + assertFalse(configFile.isEmpty()); + configFile = new ConfigFile(); + + configFile.setRightJoystick("not empty"); + assertFalse(configFile.isEmpty()); + configFile = new ConfigFile(); } @Test @@ -118,7 +130,7 @@ public void testConstructor1() { @Test public void testConstructor2() { - configFile = new ConfigFile("system", "cartridge", "0", "1", "2", "3", "cassette"); + configFile = new ConfigFile("system", "cartridge", "0", "1", "2", "3", "cassette", "left joystick", "right joystick"); configFile.setSystemROM("system"); configFile.setCartridgeROM("cartridge"); configFile.setCassetteROM("cassette"); @@ -126,6 +138,8 @@ public void testConstructor2() { assertEquals("1", configFile.getDrive1Image()); assertEquals("2", configFile.getDrive2Image()); assertEquals("3", configFile.getDrive3Image()); + assertEquals("left joystick", configFile.getLeftJoystick()); + assertEquals("right joystick", configFile.getRightJoystick()); } @Test @@ -139,6 +153,8 @@ public void testParseConfigFileCorrectFormat() throws NullPointerException { assertEquals("1", configFile.getDrive1Image()); assertEquals("2", configFile.getDrive2Image()); assertEquals("3", configFile.getDrive3Image()); + assertEquals("left joystick", configFile.getLeftJoystick()); + assertEquals("right joystick", configFile.getRightJoystick()); } @Test diff --git a/src/test/resources/test_config.yml b/src/test/resources/test_config.yml index 62bc803..843b176 100644 --- a/src/test/resources/test_config.yml +++ b/src/test/resources/test_config.yml @@ -4,4 +4,6 @@ cassetteROM: "cassette" drive0Image: "0" drive1Image: "1" drive2Image: "2" -drive3Image: "3" \ No newline at end of file +drive3Image: "3" +leftJoystick: "left joystick" +rightJoystick: "right joystick" \ No newline at end of file