AppleIIGo/Source/AppleIIGo.java

882 lines
21 KiB
Java

/**
* AppleIIGo
* The Java Apple II Emulator
* Copyright 2015 by Nick Westgate (Nick.Westgate@gmail.com)
* Copyright 2006 by Marc S. Ressl (mressl@gmail.com)
* Released under the GNU General Public License version 2
* See http://www.gnu.org/licenses/
*
* Change list:
*
* Version 1.0.10 - changes by Nick:
* - fixed disk stepping bug for Mabel's Mansion using my code from AppleWin
* - patch loaded ROM's empty slots with faux-floating bus data so Mabel's Mansion works
* - revert CPU status bug introduced in 1.0.9 - V and R used the same bit
* - fixed BRK bug by adding extra PC increment
* - NOTE: decimal mode arithmetic fails some tests and should be fixed someday
*
* Version 1.0.9 - changes by Nick:
* - fixed disk speed-up bug (Sherwood Forest reads with the drive motor off)
* - added check for 2IMG header ID
* - fixed processor status bugs in BRK, PLP, RTI, NMI, IRQ
*
* Version 1.0.8 - changes by Nick:
* - implemented disk writing (only in memory, not persisted)
* - added support for .2MG (2IMG) disk images, including lock flag and volume number
* - support meta tag for write protect in disk filename eg: NotWritable_Meta_DW0.dsk
*
* Version 1.0.7 - changes by Nick:
* - fixed disk emulation bug (sense write protect entered write mode)
* - now honour diskWritable parameter (but writing is not implemented)
* - support meta tag for volume number in disk filename eg: Vol2_Meta_DV2.dsk
* - added isPaddleEnabled parameter
* - exposed setPaddleEnabled(boolean value), setPaddleInverted(boolean value)
* - paddle values are now 255 at startup (ie. correct if disabled/not present)
* - minor AppleSpeaker fix (SourceDataLine.class) thanks to William Halliburton
*
* Version 1.0.6 - changes by Nick:
* - exposed F3/F4 disk swapping method: cycleDisk(int driveNumber)
* - exposed reset() method
* - exposed setSpeed(int value) method
*
* Version 1.0.5 - changes by Nick:
* - added support for .NIB (nibble) disk images (also inside ZIP archives)
* - added disk speedup hacks for DOS (expect ~2x faster reads)
*
* Version 1.0.4 - changes by Nick:
* - added support for .PO (ProDOS order) disk images (also inside ZIP archives)
* - added Command key for Closed-Apple on Mac OS X
* - added Home and End keys for Open-Apple and Closed-Apple on full keyboards
*
* Version 1.0.3 - changes by Nick:
* - fixed paddle values for scaled display window
* - added "digital" joystick support via numeric keypad arrows
* - added Left-Alt and Right-Alt keys for Open-Apple and Closed-Apple
* - changed reset key from Home to Ctrl-F12 and Ctrl-Pause/Break
*
* Version 1.0.2 - changes by Nick:
* - improved sound sync by moving AppleSpeaker into the main thread
* - added version (F1)
* - added multiple disks & swapping (F3, F4)
* - added ZIP archive support
* - fixed HTTP disk image access bug
*/
import java.applet.Applet;
import java.awt.Graphics;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* AppleIIGo class<p>
* Connects EmAppleII, AppleCanvas
*/
public class AppleIIGo extends Applet implements KeyListener, ComponentListener,
MouseListener, MouseMotionListener {
private static final long serialVersionUID = -3302282815441501352L;
final String version = "1.0.10";
final String versionString = "AppleIIGo Version " + version;
final String metaStart = "_meta_";
// Class instances
private EmAppleII apple;
private AppleDisplay display;
private DiskII disk;
// Machine variables
private boolean isCpuPaused;
private boolean isCpuDebugEnabled;
// Keyboard variables
private boolean keyboardUppercaseOnly;
// Paddle variables
private boolean isPaddleEnabled;
private boolean isPaddleInverted;
// Disk variables - TODO: refactor into a class
private String diskDriveResource[] = new String[2];
private boolean diskWritable;
private String[][] diskImageNames = {{}, {}};
private int diskImageNumber[] = {0, 0};
/**
* Debug
*/
private void debug(String message) {
// System.out.println(message);
}
/**
* Parameters
*/
private String getAppletParameter(String parameter, String defaultValue) {
String value = getParameter(parameter);
if ((value == null) || (value.length() == 0))
return defaultValue;
return value;
}
/**
* On applet initialization
*/
public void init() {
debug("init()");
// Activate listeners
addKeyListener(this);
addMouseListener(this);
addMouseMotionListener(this);
if (getAppletParameter("displayFocusOnStart", "true").equals("true"))
addComponentListener(this);
// Initialize Apple II emulator
apple = new EmAppleII();
loadRom(getAppletParameter("cpuRom", ""));
apple.setCpuSpeed(new Integer(getAppletParameter("cpuSpeed", "1000")).intValue());
isCpuPaused = getAppletParameter("cpuPaused", "false").equals("true");
isCpuDebugEnabled = getAppletParameter("cpuDebugEnabled", "false").equals("true");
apple.setStepMode(getAppletParameter("cpuStepMode", "false").equals("true"));
// Keyboard
keyboardUppercaseOnly = getAppletParameter("keyboardUppercaseOnly", "true").equals("true");
// Paddles
isPaddleEnabled = getAppletParameter("paddleEnabled", "true").equals("true");
isPaddleInverted = getAppletParameter("paddleInverted", "false").equals("true");
// Display
display = new AppleDisplay(this, apple);
display.setScale(new Float(getAppletParameter("displayScale", "1")).floatValue());
display.setRefreshRate(new Integer(getAppletParameter("displayRefreshRate", "10")).intValue());
display.setColorMode(new Integer(getAppletParameter("displayColorMode", "0")).intValue());
display.setStatMode(getAppletParameter("displayStatMode", "false").equals("true"));
display.setGlare(getAppletParameter("displayGlare", "false").equals("true"));
// Speaker
apple.speaker = new AppleSpeaker(apple);
apple.speaker.setVolume(new Integer(getAppletParameter("speakerVolume", "6")).intValue());
// Peripherals
disk = new DiskII(apple);
apple.setPeripheral(disk, 6);
// Initialize disk drives
diskWritable = getAppletParameter("diskWritable", "false").equals("true");
diskImageNames[0] = getAppletParameter("diskDrive1", "").split("[|]");
diskImageNumber[0] = 0;
diskImageNames[1] = getAppletParameter("diskDrive2", "").split("[|]");
diskImageNumber[1] = 0;
mountDisk(0, diskImageNames[0][diskImageNumber[0]]);
mountDisk(1, diskImageNames[1][diskImageNumber[1]]);
// Start CPU
if (!isCpuPaused)
resume();
}
/**
* Start applet
*/
public void start() {
debug("AppleIIGo Version " + version);
debug("start()");
}
/**
* Stop applet
*/
public void stop() {
debug("stop()");
}
/**
* On applet destruction
*/
public void destroy() {
debug("destroy()");
unmountDisk(0);
unmountDisk(1);
}
// Public Java interface
/**
* Javascript interface
*/
public void focus() {
debug("focus()");
requestFocus();
}
/**
* Pause emulator
*/
public void pause() {
debug("pause()");
isCpuPaused = true;
apple.setPaused(true);
display.setPaused(true);
apple.speaker.setPaused(true);
}
/**
* Resume emulator
*/
public void resume() {
debug("resume()");
isCpuPaused = false;
apple.speaker.setPaused(false);
display.setPaused(false);
apple.setPaused(false);
}
/**
* Restarts emulator
*/
public void restart() {
debug("restart()");
apple.restart();
}
public void reset() {
debug("reset()");
apple.reset();
}
public void setSpeed(int value) {
debug("setSpeed(" + value + ")");
try
{
pause();
this.wait(1000);
}
catch (Throwable e)
{
}
apple.setCpuSpeed(value);
resume();
}
public void cycleDisk(int driveNumber)
{
debug("cycleDisk(" + driveNumber + ")");
if (diskImageNames[driveNumber].length > 1) {
diskImageNumber[driveNumber] = ++diskImageNumber[driveNumber] % diskImageNames[driveNumber].length;
mountDisk(driveNumber, diskImageNames[driveNumber][diskImageNumber[driveNumber]]);
}
}
/**
* Open input stream
*/
private DataInputStream openInputStream(String resource) {
return openInputStream(resource, null);
}
private DataInputStream openInputStream(String resource, StringBuffer OutFilename) {
InputStream is = null;
if (OutFilename != null)
{
OutFilename.setLength(0);
int slashPos = resource.lastIndexOf('/');
int backslashPos = resource.lastIndexOf('\\');
int index = Math.max(slashPos, backslashPos);
OutFilename.append(resource.substring((index > 0) ? index : 0));
}
try {
URL codeBase = getCodeBase();
URL url = new URL(codeBase, resource);
debug("resource: " + url.toString());
is = url.openStream();
if (resource.toLowerCase().endsWith(".gz"))
{
is = new GZIPInputStream(is);
}
else if (resource.toLowerCase().endsWith(".zip"))
{
is = new ZipInputStream(is);
ZipEntry entry = ((ZipInputStream)is).getNextEntry();
if (OutFilename != null)
{
OutFilename.setLength(0);
OutFilename.append(entry.getName());
}
}
} catch (Exception e) {
debug("Exeption: " + e.getLocalizedMessage());
}
if (is == null)
{
debug("failed");
return null;
}
else
{
debug("ok");
return new DataInputStream(is);
}
}
/**
* Open output stream
*/
private OutputStream openOutputStream(String resource) {
OutputStream os = null;
try {
if (!(resource.substring(0, 6).equals("http://")))
os = new FileOutputStream(resource);
} catch (Exception e) {
}
return os;
}
/**
* Load ROM
*/
public boolean loadRom(String resource) {
debug("loadRom(resource: " + resource + ")");
boolean success = false;
try {
DataInputStream is = openInputStream(resource);
success = apple.loadRom(is);
is.close();
} catch (Exception e) {
debug("Exeption: " + e.getLocalizedMessage());
}
return success;
}
/**
* Mount a disk
*/
public boolean mountDisk(int drive, String resource) {
debug("mountDisk(drive: " + drive + ", resource: " + resource + ")");
boolean success = false;
if ((drive < 0) || (drive > 1))
return success;
try {
unmountDisk(drive);
diskDriveResource[drive] = resource;
StringBuffer diskname = new StringBuffer();
DataInputStream is = openInputStream(resource, diskname);
int diskVolumeNumber = DiskII.DEFAULT_VOLUME;
boolean diskWritableOverride = diskWritable;
// handle disk meta tag for disk volume (etc?)
// could break this out into a method, but then multiple tags ...?
String lowerDiskname = diskname.toString().toLowerCase();
int metaIndex = lowerDiskname.indexOf(metaStart);
if (metaIndex != -1)
{
metaIndex += metaStart.length();
int command = 0;
int operand = 0;
boolean execute = false;
while (metaIndex < lowerDiskname.length())
{
char c = lowerDiskname.charAt(metaIndex++);
switch (c)
{
case '0': case '1':case '2':case '3':case '4':
case '5': case '6':case '7':case '8':case '9':
{
operand = 10 * operand + (c - '0');
break;
}
case '.': // end meta
metaIndex = lowerDiskname.length();
execute = true;
break;
case '_': // end word
execute = true;
break;
default:
{
if (c >= 'a' && c <= 'z')
{
command = (command << 16) + c;
execute = (command & 0xFFFF0000) != 0;
}
break;
}
}
if (execute)
{
switch (command)
{
case ('d' << 16) + 'v':
diskVolumeNumber = operand;
break;
case ('d' << 16) + 'w':
diskWritableOverride = (operand != 0);
break;
}
command = 0;
operand = 0;
}
}
}
success = disk.readDisk(drive, is, diskname.toString(), !diskWritableOverride, diskVolumeNumber);
is.close();
showStatus("Drive " + (drive + 1) + ": " + resource);
} catch (Exception e) {
debug("Exeption: " + e.getLocalizedMessage());
}
return success;
}
/**
* Unmount a disk
*/
public void unmountDisk(int drive) {
debug("unmountDisk(drive: " + drive + ")");
if ((drive < 0) || (drive > 1))
return;
if (!diskWritable)
return;
// TODO: only for local disk cache when it's working
//try {
//OutputStream os = openOutputStream(diskDriveResource[drive]);
//disk.writeDisk(drive, os);
//os.close();
//} catch (Exception e) {
//}
}
/**
* Set color mode
*/
public void setColorMode(int value) {
debug("setColorMode(value: " + value + ")");
display.setColorMode(value);
}
/**
* Set paddle enabled/disabled
*/
public void setPaddleEnabled(boolean value) {
debug("setPaddleEnabled(value: " + value + ")");
isPaddleEnabled = value;
if (!value)
{
apple.paddle.setPaddlePos(0, Paddle.PADDLE_HIGH);
apple.paddle.setPaddlePos(1, Paddle.PADDLE_HIGH);
apple.paddle.setButton(0, false);
apple.paddle.setButton(1, false);
}
}
/**
* Set paddle inverted/normal
*/
public void setPaddleInverted(boolean value) {
debug("setPaddleInverted(value: " + value + ")");
isPaddleInverted = value;
}
/**
* Get disk activity
*/
public boolean getDiskActivity() {
return (!isCpuPaused && disk.isMotorOn());
}
public int getSizeX()
{
return display.getSizeX();
}
public int getSizeY()
{
return display.getSizeY();
}
/**
* KeyListener event handling
*/
public void keyTyped(KeyEvent e) {
// Send to emulator
int key = e.getKeyChar();
if (key == 10)
apple.setKeyLatch(13);
else if (key < 128) {
if (keyboardUppercaseOnly && (key >= 97) && (key <= 122))
key -= 32;
apple.setKeyLatch(key);
}
}
public void keyPressed(KeyEvent e) {
switch(e.getKeyCode()) {
case KeyEvent.VK_META:
apple.paddle.setButton(1, true);
break;
case KeyEvent.VK_ALT:
if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT)
{
apple.paddle.setButton(0, true);
}
else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT)
{
apple.paddle.setButton(1, true);
}
break;
case KeyEvent.VK_BACK_SPACE:
case KeyEvent.VK_LEFT:
if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_NUMPAD)
{
handleKeypadLeft();
}
else
{
apple.setKeyLatch(8);
}
break;
case KeyEvent.VK_RIGHT:
if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_NUMPAD)
{
handleKeypadRight();
}
else
{
apple.setKeyLatch(21);
}
break;
case KeyEvent.VK_UP:
if (e.isControlDown())
{
apple.speaker.setVolume(apple.speaker.getVolume() + 1);
}
else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_NUMPAD)
{
handleKeypadUp();
}
else
{
apple.setKeyLatch(11);
}
break;
case KeyEvent.VK_DOWN:
if (e.isControlDown())
{
apple.speaker.setVolume(apple.speaker.getVolume() - 1);
}
else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_NUMPAD)
{
handleKeypadDown();
}
else
{
apple.setKeyLatch(10);
}
break;
case KeyEvent.VK_ESCAPE:
apple.setKeyLatch(27);
break;
case KeyEvent.VK_DELETE:
apple.setKeyLatch(127);
break;
case KeyEvent.VK_HOME:
if (e.isControlDown())
restart();
else
apple.paddle.setButton(0, true);
break;
case KeyEvent.VK_END:
apple.paddle.setButton(1, true);
break;
case KeyEvent.VK_F1:
showStatus("AppleIIGo Version " + version);
break;
case KeyEvent.VK_F3:
cycleDisk(0);
break;
case KeyEvent.VK_F4:
cycleDisk(1);
break;
case KeyEvent.VK_F5:
if (isCpuDebugEnabled)
display.setStatMode(!display.getStatMode());
break;
case KeyEvent.VK_F6:
if (isCpuDebugEnabled)
apple.setStepMode(!apple.getStepMode());
break;
case KeyEvent.VK_F7:
if (isCpuDebugEnabled) {
apple.setStepMode(apple.getStepMode());
apple.stepInstructions(1);
}
break;
case KeyEvent.VK_F8:
if (isCpuDebugEnabled) {
apple.setStepMode(apple.getStepMode());
apple.stepInstructions(128);
}
break;
case KeyEvent.VK_CANCEL: // Pause/Break sends this (as Mac OS swallows Ctrl-F12)
case KeyEvent.VK_F12:
if (e.isControlDown())
reset();
break;
case KeyEvent.VK_KP_LEFT:
handleKeypadLeft();
break;
case KeyEvent.VK_KP_RIGHT:
handleKeypadRight();
break;
case KeyEvent.VK_KP_UP:
handleKeypadUp();
break;
case KeyEvent.VK_KP_DOWN:
handleKeypadDown();
break;
}
}
public void keyReleased(KeyEvent e) {
switch(e.getKeyCode()) {
case KeyEvent.VK_META:
apple.paddle.setButton(1, false);
break;
case KeyEvent.VK_ALT:
if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT)
{
apple.paddle.setButton(0, false);
}
else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT)
{
apple.paddle.setButton(1, false);
}
break;
case KeyEvent.VK_LEFT:
case KeyEvent.VK_RIGHT:
if (e.getKeyLocation() != KeyEvent.KEY_LOCATION_NUMPAD)
break;
// else fall through
case KeyEvent.VK_KP_LEFT:
case KeyEvent.VK_KP_RIGHT:
handleKeypadCentreX();
break;
case KeyEvent.VK_UP:
case KeyEvent.VK_DOWN:
if (e.getKeyLocation() != KeyEvent.KEY_LOCATION_NUMPAD)
break;
// else fall through
case KeyEvent.VK_KP_UP:
case KeyEvent.VK_KP_DOWN:
handleKeypadCentreY();
break;
case KeyEvent.VK_HOME:
if (!e.isControlDown())
{
apple.paddle.setButton(0, false);
}
case KeyEvent.VK_END:
apple.paddle.setButton(1, false);
}
}
private void handleKeypadCentreX() {
if (isPaddleEnabled)
{
if (isPaddleInverted) {
apple.paddle.setPaddlePos(1, 127);
} else {
apple.paddle.setPaddlePos(0, 127);
}
}
}
private void handleKeypadCentreY() {
if (isPaddleEnabled)
{
if (isPaddleInverted) {
apple.paddle.setPaddlePos(0, 127);
} else {
apple.paddle.setPaddlePos(1, 127);
}
}
}
private void handleKeypadLeft() {
if (isPaddleEnabled)
{
if (isPaddleInverted) {
apple.paddle.setPaddlePos(1, 255);
} else {
apple.paddle.setPaddlePos(0, 0);
}
}
}
private void handleKeypadRight() {
if (isPaddleEnabled)
{
if (isPaddleInverted) {
apple.paddle.setPaddlePos(1, 0);
} else {
apple.paddle.setPaddlePos(0, 255);
}
}
}
private void handleKeypadUp() {
if (isPaddleEnabled)
{
if (isPaddleInverted) {
apple.paddle.setPaddlePos(0, 255);
} else {
apple.paddle.setPaddlePos(1, 0);
}
}
}
private void handleKeypadDown() {
if (isPaddleEnabled)
{
if (isPaddleInverted) {
apple.paddle.setPaddlePos(0, 0);
} else {
apple.paddle.setPaddlePos(1, 255);
}
}
}
/**
* ComponentListener event handling
*/
public void componentHidden(ComponentEvent e) {
}
public void componentMoved(ComponentEvent e) {
}
public void componentResized(ComponentEvent e) {
}
public void componentShown(ComponentEvent e) {
debug("componentShown()");
removeComponentListener(this);
requestFocus();
}
/**
* MouseListener, MouseMotionListener event handling
*/
public void mouseClicked(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
try {
getAppletContext().showDocument(new URL("javascript:flipMouseOver();"));
} catch (MalformedURLException ex) {
}
}
public void mouseExited(MouseEvent e) {
try {
getAppletContext().showDocument(new URL("javascript:flipMouseOut();"));
} catch (MalformedURLException ex) {
}
}
public void mousePressed(MouseEvent e) {
int modifiers = e.getModifiers();
if (isPaddleEnabled)
{
if ((modifiers & InputEvent.BUTTON1_MASK) != 0)
apple.paddle.setButton(0, true);
if ((modifiers & InputEvent.BUTTON3_MASK) != 0)
apple.paddle.setButton(1, true);
}
}
public void mouseReleased(MouseEvent e) {
int modifiers = e.getModifiers();
if (isPaddleEnabled)
{
if ((modifiers & InputEvent.BUTTON1_MASK) != 0)
apple.paddle.setButton(0, false);
if ((modifiers & InputEvent.BUTTON3_MASK) != 0)
apple.paddle.setButton(1, false);
}
}
public void mouseDragged(MouseEvent e) {
mouseMoved(e);
}
public void mouseMoved(MouseEvent e) {
float scale = display.getScale();
if (isPaddleEnabled)
{
if (isPaddleInverted) {
apple.paddle.setPaddlePos(0, (int) (255 - e.getY() * 256 / (192 * scale)));
apple.paddle.setPaddlePos(1, (int) (255 - e.getX() * 256 / (280 * scale)));
} else {
apple.paddle.setPaddlePos(0, (int) (e.getX() * 256 / (280 * scale)));
apple.paddle.setPaddlePos(1, (int) (e.getY() * 256 / (192 * scale)));
}
}
}
/**
* Applet paint function
*/
public void paint(Graphics g) {
display.paint(g);
}
/**
* Applet update function
*/
public void update(Graphics g) {
display.paint(g);
}
}