Initial version: Based on last source available in SourceForge, converted to a Maven project format

This commit is contained in:
Brendan Robert 2014-09-07 16:10:04 -05:00
parent c41c87c17a
commit 6341001743
217 changed files with 29994 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

2
.gitignore vendored
View File

@ -10,3 +10,5 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
/jace/nbproject/private/
/jace/target/

3
manifest.mf Normal file
View File

@ -0,0 +1,3 @@
Manifest-Version: 1.0
X-COMMENT: Main-Class will be added automatically by build

48
nbactions.xml Normal file
View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<actions>
<action>
<actionName>run</actionName>
<packagings>
<packaging>jar</packaging>
</packagings>
<goals>
<goal>-X</goal>
<goal>-e</goal>
<goal>process-classes</goal>
<goal>org.codehaus.mojo:exec-maven-plugin:1.2.1:exec</goal>
</goals>
<properties>
<exec.args>-classpath target/jace-2.0-SNAPSHOT.jar jace.Emulator</exec.args>
<exec.executable>java</exec.executable>
</properties>
</action>
<action>
<actionName>debug</actionName>
<packagings>
<packaging>jar</packaging>
</packagings>
<goals>
<goal>process-classes</goal>
<goal>org.codehaus.mojo:exec-maven-plugin:1.2.1:exec</goal>
</goals>
<properties>
<exec.args>-Xdebug -Xrunjdwp:transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath jace.Emulator</exec.args>
<exec.executable>java</exec.executable>
<jpda.listen>true</jpda.listen>
</properties>
</action>
<action>
<actionName>profile</actionName>
<packagings>
<packaging>jar</packaging>
</packagings>
<goals>
<goal>process-classes</goal>
<goal>org.codehaus.mojo:exec-maven-plugin:1.2.1:exec</goal>
</goals>
<properties>
<exec.args>-classpath %classpath jace.Emulator</exec.args>
<exec.executable>java</exec.executable>
</properties>
</action>
</actions>

108
pom.xml Normal file
View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.badvision</groupId>
<artifactId>jace</artifactId>
<version>2.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jace</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mainClass>jace.MainApp</mainClass>
</properties>
<organization>
<!-- Used as the 'Vendor' for JNLP generation -->
<name>Your Organisation</name>
</organization>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>package</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<excludeScope>system</excludeScope>
<excludeGroupIds>junit,org.mockito,org.hamcrest</excludeGroupIds>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/../bin/javafxpackager</executable>
<arguments>
<argument>-createjar</argument>
<argument>-nocss2bin</argument>
<argument>-appclass</argument>
<argument>${mainClass}</argument>
<argument>-srcdir</argument>
<argument>${project.build.directory}/classes</argument>
<argument>-outdir</argument>
<argument>${project.build.directory}</argument>
<argument>-outfile</argument>
<argument>${project.build.finalName}.jar</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>default-cli</id>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/bin/java</executable>
<commandlineArgs>${runfx.args}</commandlineArgs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArguments>
<bootclasspath>${sun.boot.class.path}${path.separator}${java.home}/lib/jfxrt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.16</version>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>${java.home}/lib/jfxrt.jar</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</plugin>
</plugins>
</build>
</project>

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/java/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/java/jace/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace;
import jace.hardware.FloppyDisk;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Generic disk conversion utility, using the FloppyDisk nibblize/denibblize to
* convert between DSK and NIB formats (wherever possible anyway)
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class ConvertDiskImage {
public static void main(String... args) {
if (args.length != 2) {
showHelp();
return;
}
File in = new File(args[0]);
File out = new File(args[1]);
if (!in.exists()) {
showHelp();
System.out.println("Cannot find input file: " + args[0]);
return;
}
if (out.exists()) {
showHelp();
System.out.println("Output file already exists!: " + args[1]);
return;
}
String ext = args[1].substring(args[1].length() - 3);
boolean writeNibblized = false;
boolean writeProdosOrdered = false;
if (ext.equalsIgnoreCase("NIB")) {
System.out.println("Preparing to write NIB image");
writeNibblized = true;
} else if (ext.equalsIgnoreCase(".DO") || ext.equalsIgnoreCase("DSK")) {
System.out.println("Preparing to write DOS 3.3 ordered disk image");
writeNibblized = false;
writeProdosOrdered = false;
} else if (ext.equalsIgnoreCase(".PO")) {
System.out.println("Preparing to write Prodos ordered image");
writeNibblized = false;
writeProdosOrdered = true;
} else {
showHelp();
System.out.println("Could not understand desired output format");
return;
}
// First read in the disk image, this decodes the disk as necessary
FloppyDisk theDisk = null;
try {
theDisk = new FloppyDisk(in);
} catch (IOException ex) {
System.out.println("Couldn't read disk image");
ex.printStackTrace();
return;
}
if (!writeNibblized) {
// Now change the disk image to point to a new file and adjust the sector ordering
System.out.println("Writing disk image with " + (writeProdosOrdered ? "prodos" : "dos 3.3") + " sector ordering");
theDisk.diskPath = out;
theDisk.isNibblizedImage = true;
theDisk.currentSectorOrder = writeProdosOrdered ? FloppyDisk.PRODOS_SECTOR_ORDER : FloppyDisk.DOS_33_SECTOR_ORDER;
theDisk.headerLength = 0;
for (int i = 0; i < FloppyDisk.TRACK_COUNT; i++) {
theDisk.updateTrack(i);
}
} else {
FileOutputStream fos = null;
System.out.println("Writing NIB image");
try {
fos = new FileOutputStream(out);
fos.write(theDisk.nibbles);
fos.close();
} catch (IOException ex) {
System.err.println("Error writing NIB image: " + ex.getMessage());
ex.printStackTrace();
} finally {
try {
fos.close();
} catch (IOException ex) {
System.err.println("Error closing NIB image: " + ex.getMessage());
ex.printStackTrace();
}
}
}
System.out.println("Finished converting disk image.");
}
private static void showHelp() {
for (String s : new String[]{
"ConvertDiskImage",
"----------------",
"Usage: java -cp jace.jar jace.ConvertDiskImage DISK_INPUT_NAME DISK_OUTPUT_NAME",
"where DISK_INPUT_NAME is the path of a valid disk image, ",
"and DISK_OUTPUT_NAME is the path where you want to ",
"save the converted disk image.",
"Supported input formats: ",
" DSK (assumes DO), DO, PO, 2MG (140kb), NIB",
"Supported output formats: ",
" DO/DSK, PO, NIB"
}) {
System.out.println(s);
}
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace;
import jace.apple2e.Apple2e;
import jace.config.Configuration;
import jace.core.Computer;
import jace.ui.AbstractEmulatorFrame;
import jace.ui.EmulatorFrame;
import java.awt.Component;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
/**
* Created on January 15, 2007, 10:10 PM
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Emulator {
public static Emulator instance;
public static Thread mainThread;
public static void main(String... args) {
mainThread = Thread.currentThread();
instance = new Emulator(args);
}
public static AbstractEmulatorFrame getFrame() {
if (instance != null) {
return instance.theApp;
} else {
return null;
}
}
public Apple2e computer;
public AbstractEmulatorFrame theApp;
/**
* Creates a new instance of Emulator
* @param args
*/
public Emulator(String... args) {
computer = new Apple2e();
Configuration.loadSettings();
Map<String, String> settings = new HashMap<>();
if (args != null) {
for (int i = 0; i < args.length; i++) {
if (args[i].startsWith("-")) {
String key = args[i].substring(1);
if ((i + 1) < args.length) {
String val = args[i + 1];
if (!val.startsWith("-")) {
settings.put(key, val);
i++;
} else {
settings.put(key, "true");
}
} else {
settings.put(key, "true");
}
} else {
System.err.println("Did not understand parameter " + args[i] + ", skipping.");
}
}
}
Configuration.applySettings(settings);
// theApp = new MainFrame();
theApp = new EmulatorFrame();
try {
theApp.setIconImage(ImageIO.read(Emulator.class.getClassLoader().getResourceAsStream("jace/data/woz_figure.gif")));
} catch (IOException ex) {
Logger.getLogger(Emulator.class.getName()).log(Level.SEVERE, null, ex);
}
//theApp.setBounds(new Rectangle((140*6),(192*3)));
theApp.setVisible(true);
theApp.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
theApp.setFocusTraversalKeysEnabled(false);
theApp.setTitle("Java Apple Computer Emulator");
theApp.addKeyListener(computer.getKeyboard().getListener());
theApp.addComponentListener(new ComponentListener() {
// theApp.screen.addComponentListener(new ComponentListener() {
@Override
public void componentResized(ComponentEvent e) {
// System.out.println("Screen resized");
resizeVideo();
}
@Override
public void componentMoved(ComponentEvent e) {
resizeVideo();
}
@Override
public void componentShown(ComponentEvent e) {
}
@Override
public void componentHidden(ComponentEvent e) {
}
});
theApp.addWindowListener(new WindowListener() {
@Override
public void windowOpened(WindowEvent e) {
}
@Override
public void windowClosing(WindowEvent e) {
}
@Override
public void windowClosed(WindowEvent e) {
}
@Override
public void windowIconified(WindowEvent e) {
Computer.getComputer().getVideo().suspend();
}
@Override
public void windowDeiconified(WindowEvent e) {
Computer.getComputer().getVideo().resume();
resizeVideo();
}
@Override
public void windowActivated(WindowEvent e) {
resizeVideo();
}
@Override
public void windowDeactivated(WindowEvent e) {
resizeVideo();
}
});
EmulatorUILogic.registerDebugger();
computer.getVideo().setScreen(theApp.getScreenGraphics());
computer.coldStart();
}
public static void resizeVideo() {
AbstractEmulatorFrame window = getFrame();
if (window != null) {
window.resizeVideo();
}
}
public static Component getScreen() {
AbstractEmulatorFrame window = getFrame();
if (window != null) {
return window.getScreen();
}
return null;
}
}

View File

@ -0,0 +1,372 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurationPanel;
import jace.config.InvokableAction;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.Debugger;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import static jace.core.Utility.*;
import jace.library.MediaLibrary;
import jace.ui.AbstractEmulatorFrame;
import jace.ui.DebuggerPanel;
import java.awt.Color;
import java.awt.HeadlessException;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
/**
* This class contains miscellaneous user-invoked actions such as debugger
* operations and running arbitrary files in the emulator. It is possible for
* these methods to be later refactored into more sensible locations. Created on
* April 16, 2007, 10:30 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class EmulatorUILogic {
static Debugger debugger;
static {
debugger = new Debugger() {
@Override
public void updateStatus() {
enableDebug(true);
MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu();
updateCPURegisters(cpu);
}
};
}
public static void updateCPURegisters(MOS65C02 cpu) {
DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel();
debuggerPanel.valueA.setText(Integer.toHexString(cpu.A));
debuggerPanel.valueX.setText(Integer.toHexString(cpu.X));
debuggerPanel.valueY.setText(Integer.toHexString(cpu.Y));
debuggerPanel.valuePC.setText(Integer.toHexString(cpu.getProgramCounter()));
debuggerPanel.valueSP.setText(Integer.toHexString(cpu.getSTACK()));
debuggerPanel.valuePC2.setText(cpu.getFlags());
debuggerPanel.valueINST.setText(cpu.disassemble());
}
public static void enableDebug(boolean b) {
DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel();
debugger.setActive(b);
debuggerPanel.enableDebug.setSelected(b);
debuggerPanel.setBackground(
b ? Color.RED : new Color(0, 0, 0x040));
}
public static void enableTrace(boolean b) {
Computer.getComputer().getCpu().setTraceEnabled(b);
}
public static void stepForward() {
debugger.step = true;
}
static void registerDebugger() {
Computer.getComputer().getCpu().setDebug(debugger);
}
public static Integer getValidAddress(String s) {
try {
int addr = Integer.parseInt(s.toUpperCase(), 16);
if (addr >= 0 && addr < 0x10000) {
return addr;
}
return null;
} catch (NumberFormatException ex) {
return null;
}
}
public static List<RAMListener> watches = new ArrayList<>();
public static void updateWatchList(final DebuggerPanel panel) {
java.awt.EventQueue.invokeLater(() -> {
watches.stream().forEach((oldWatch) -> {
Computer.getComputer().getMemory().removeListener(oldWatch);
});
if (panel == null) {
return;
}
addWatch(panel.textW1, panel.valueW1);
addWatch(panel.textW2, panel.valueW2);
addWatch(panel.textW3, panel.valueW3);
addWatch(panel.textW4, panel.valueW4);
});
}
private static void addWatch(JTextField watch, final JLabel watchValue) {
final Integer address = getValidAddress(watch.getText());
if (address != null) {
//System.out.println("Adding watch for "+Integer.toString(address, 16));
RAMListener newListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(address);
}
@Override
protected void doEvent(RAMEvent e) {
watchValue.setText(Integer.toHexString(e.getNewValue() & 0x0FF));
}
};
Computer.getComputer().getMemory().addListener(newListener);
watches.add(newListener);
// Print out the current value right away
byte b = Computer.getComputer().getMemory().readRaw(address);
watchValue.setText(Integer.toString(b & 0x0ff, 16));
} else {
watchValue.setText("00");
}
}
public static void updateBreakpointList(final DebuggerPanel panel) {
java.awt.EventQueue.invokeLater(() -> {
Integer address;
debugger.getBreakpoints().clear();
if (panel == null) {
return;
}
address = getValidAddress(panel.textBP1.getText());
if (address != null) {
debugger.getBreakpoints().add(address);
}
address = getValidAddress(panel.textBP2.getText());
if (address != null) {
debugger.getBreakpoints().add(address);
}
address = getValidAddress(panel.textBP3.getText());
if (address != null) {
debugger.getBreakpoints().add(address);
}
address = getValidAddress(panel.textBP4.getText());
if (address != null) {
debugger.getBreakpoints().add(address);
}
debugger.updateBreakpoints();
});
}
@InvokableAction(
name = "BRUN file",
category = "file",
description = "Loads a binary file in memory and executes it. File should end with #06xxxx, where xxxx is the start address in hex",
alternatives = "Execute program;Load binary;Load program;Load rom;Play single-load game")
public static void runFile() {
Computer.pause();
JFileChooser select = new JFileChooser();
select.showDialog(Emulator.getFrame(), "Execute binary file");
File binary = select.getSelectedFile();
if (binary == null) {
Computer.resume();
return;
}
runFile(binary);
}
public static void runFile(File binary) {
String fileName = binary.getName().toLowerCase();
try {
if (fileName.contains("#06")) {
String addressStr = fileName.substring(fileName.length() - 4);
int address = Integer.parseInt(addressStr, 16);
brun(binary, address);
} else if (fileName.contains("#fc")) {
gripe("BASIC not supported yet");
}
} catch (NumberFormatException | IOException ex) {
}
Computer.getComputer().getCpu().resume();
}
public static void brun(File binary, int address) throws FileNotFoundException, IOException {
// If it was halted already, then it was initiated outside of an opcode execution
// If it was not yet halted, then it is the case that the CPU is processing another opcode
// So if that is the case, the program counter will need to be decremented here to compensate
// TODO: Find a better mousetrap for this one -- it's an ugly hack
Computer.pause();
FileInputStream in = new FileInputStream(binary);
byte[] data = new byte[in.available()];
in.read(data);
RAM ram = Computer.getComputer().getMemory();
for (int i = 0; i < data.length; i++) {
ram.write(address + i, data[i], false, true);
}
CPU cpu = Computer.getComputer().getCpu();
Computer.getComputer().getCpu().setProgramCounter(address);
Computer.resume();
}
@InvokableAction(
name = "Adjust display",
category = "display",
description = "Adjusts window size to 1:1 aspect ratio for optimal viewing.",
alternatives = "Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;")
static public void scaleIntegerRatio() {
AbstractEmulatorFrame frame = Emulator.getFrame();
if (frame == null) {
return;
}
Computer.pause();
frame.enforceIntegerRatio();
Computer.resume();
}
@InvokableAction(
name = "Toggle Debug",
category = "debug",
description = "Show/hide the debug panel",
alternatives = "Show Debug;Hide Debug")
public static void toggleDebugPanel() {
AbstractEmulatorFrame frame = Emulator.getFrame();
if (frame == null) {
return;
}
frame.setShowDebug(!frame.isShowDebug());
frame.reconfigure();
Emulator.resizeVideo();
}
public static void toggleFullscreen() {
AbstractEmulatorFrame frame = Emulator.getFrame();
if (frame == null) {
return;
}
Computer.pause();
frame.toggleFullscreen();
Computer.resume();
}
@InvokableAction(
name = "Save Raw Screenshot",
category = "general",
description = "Save raw (RAM) format of visible screen",
alternatives = "screendump, raw screenshot")
public static void saveScreenshotRaw() throws FileNotFoundException, IOException {
SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss");
String timestamp = df.format(new Date());
String type;
int start = Computer.getComputer().getVideo().getCurrentWriter().actualWriter().getYOffset(0);
int len = 0;
if (start < 0x02000) {
// Lo-res or double-lores
len = 0x0400;
type = "gr";
} else {
// Hi-res or double-hires
len = 0x02000;
type = "hgr";
}
boolean dres = SoftSwitches._80COL.getState() && (SoftSwitches.DHIRES.getState() || start < 0x02000);
if (dres) {
type = "d" + type;
}
File outFile = new File("screen_" + type + "_a" + Integer.toHexString(start) + "_" + timestamp);
try (FileOutputStream out = new FileOutputStream(outFile)) {
RAM128k ram = (RAM128k) Computer.getComputer().memory;
Computer.pause();
if (dres) {
for (int i = 0; i < len; i++) {
out.write(ram.getAuxVideoMemory().readByte(start + i));
}
}
for (int i = 0; i < len; i++) {
out.write(ram.getMainMemory().readByte(start + i));
}
}
System.out.println("Wrote screenshot to " + outFile.getAbsolutePath());
}
@InvokableAction(
name = "Save Screenshot",
category = "general",
description = "Save image of visible screen",
alternatives = "Save image,save framebuffer,screenshot")
public static void saveScreenshot() throws HeadlessException, IOException {
JFileChooser select = new JFileChooser();
Computer.pause();
BufferedImage i = Computer.getComputer().getVideo().getFrameBuffer();
BufferedImage j = new BufferedImage(i.getWidth(), i.getHeight(), i.getType());
j.getGraphics().drawImage(i, 0, 0, null);
select.showSaveDialog(Emulator.getFrame());
File targetFile = select.getSelectedFile();
if (targetFile == null) {
return;
}
String filename = targetFile.getName();
System.out.println("Writing screenshot to " + filename);
String extension = filename.substring(filename.lastIndexOf(".") + 1);
ImageIO.write(j, extension, targetFile);
}
public static final String CONFIGURATION_DIALOG_NAME = "Configuration";
@InvokableAction(
name = "Configuration",
category = "general",
description = "Edit emulator configuraion",
alternatives = "Reconfigure,Preferences,Settings")
public static void showConfig() {
if (Emulator.getFrame().getModalDialogUI(CONFIGURATION_DIALOG_NAME) == null) {
JPanel ui = new ConfigurationPanel();
Emulator.getFrame().registerModalDialog(ui, CONFIGURATION_DIALOG_NAME, null, false);
}
Emulator.getFrame().showDialog(CONFIGURATION_DIALOG_NAME);
}
public static final String MEDIA_MANAGER_DIALOG_NAME = "Media Manager";
public static final String MEDIA_MANAGER_EDIT_DIALOG_NAME = "Media Details";
@InvokableAction(
name = "Media Manager",
category = "general",
description = "Show the media manager",
alternatives = "Insert disk;Eject disk;Browse;Download;Select")
public static void showMediaManager() {
if (Emulator.getFrame().getModalDialogUI(MEDIA_MANAGER_DIALOG_NAME) == null) {
Emulator.getFrame().registerModalDialog(MediaLibrary.getInstance().buildUserInterface(), MEDIA_MANAGER_DIALOG_NAME, null, false);
}
Emulator.getFrame().showDialog(MEDIA_MANAGER_DIALOG_NAME);
}
public static boolean confirm(String message) {
return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(Emulator.getFrame(), message);
}
}

View File

@ -0,0 +1,38 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jace;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
*
* @author blurry
*/
public class JaceApplication extends Application {
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("fxml/JaceUI.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}

View File

@ -0,0 +1,38 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jace;
import java.awt.Canvas;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.Region;
/**
*
* @author blurry
*/
public class JaceUIController {
@FXML
private ResourceBundle resources;
@FXML
private Canvas displayCanvas;
@FXML
private Region notificationRegion;
@FXML
public void initialize() {
assert displayCanvas != null : "fx:id=\"displayCanvas\" was not injected: check your FXML file 'JaceUI.fxml'.";
assert notificationRegion != null : "fx:id=\"notificationRegion\" was not injected: check your FXML file 'JaceUI.fxml'.";
}
}

View File

@ -0,0 +1,433 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e;
import jace.Emulator;
import jace.cheat.Cheats;
import jace.config.ClassSelection;
import jace.config.ConfigurableField;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Motherboard;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.Stateful;
import jace.core.Video;
import jace.hardware.CardDiskII;
import jace.hardware.CardExt80Col;
import jace.hardware.ConsoleProbe;
import jace.hardware.Joystick;
import jace.hardware.massStorage.CardMassStorage;
import java.awt.Graphics;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Apple2e is a computer with a 65c02 CPU, 128k of bankswitched ram,
* double-hires graphics, and up to seven peripheral I/O cards installed. Pause
* and resume are implemented by the Motherboard class. This class provides
* overall configuration of the computer, but the actual operation of the
* computer and its timing characteristics are managed in the Motherboard class.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public class Apple2e extends Computer {
static int IRQ_VECTOR = 0x003F2;
public Motherboard motherboard;
@ConfigurableField(name = "Slot 1", shortName = "s1card")
public ClassSelection card1 = new ClassSelection(Card.class, null);
@ConfigurableField(name = "Slot 2", shortName = "s2card")
// public Class<? extends Card> card2 = CardSSC.class;
public ClassSelection card2 = new ClassSelection(Card.class, null);
@ConfigurableField(name = "Slot 3", shortName = "s3card")
public ClassSelection card3 = new ClassSelection(Card.class, null);
@ConfigurableField(name = "Slot 4", shortName = "s4card")
public ClassSelection card4 = new ClassSelection(Card.class, null);
@ConfigurableField(name = "Slot 5", shortName = "s5card")
public ClassSelection card5 = new ClassSelection(Card.class, null);
@ConfigurableField(name = "Slot 6", shortName = "s6card")
public ClassSelection card6 = new ClassSelection(Card.class, CardDiskII.class);
@ConfigurableField(name = "Slot 7", shortName = "s7card")
public ClassSelection card7 = new ClassSelection(Card.class, CardMassStorage.class);
@ConfigurableField(name = "Debug rom", shortName = "debugRom", description = "Use debugger //e rom")
public boolean useDebugRom = false;
@ConfigurableField(name = "Console probe", description = "Enable console redirection (experimental!)")
public boolean useConsoleProbe = false;
private ConsoleProbe probe = new ConsoleProbe();
@ConfigurableField(name = "Helpful hints", shortName = "hints")
public boolean enableHints = true;
@ConfigurableField(name = "Renderer", shortName = "video", description = "Video rendering implementation")
public ClassSelection videoRenderer = new ClassSelection(Video.class, VideoNTSC.class);
@ConfigurableField(name = "Aux Ram", shortName = "ram", description = "Aux ram card")
public ClassSelection ramCard = new ClassSelection(RAM128k.class, CardExt80Col.class);
public Joystick joystick1;
public Joystick joystick2;
@ConfigurableField(name = "Activate Cheats", shortName = "cheat", defaultValue = "")
public ClassSelection cheatEngine = new ClassSelection(Cheats.class, null);
public Cheats activeCheatEngine = null;
/**
* Creates a new instance of Apple2e
*/
public Apple2e() {
super();
try {
reconfigure();
// Setup core resources
joystick1 = new Joystick(0);
joystick2 = new Joystick(1);
setCpu(new MOS65C02());
reinitMotherboard();
} catch (Throwable t) {
System.err.println("Unable to initalize virtual machine");
t.printStackTrace(System.err);
}
}
@Override
public String getName() {
return "Computer (Apple //e)";
}
private void reinitMotherboard() {
if (motherboard != null && motherboard.isRunning()) {
motherboard.suspend();
}
motherboard = new Motherboard();
motherboard.reconfigure();
Motherboard.miscDevices.add(joystick1);
Motherboard.miscDevices.add(joystick2);
}
@Override
public void coldStart() {
Computer.pause();
reinitMotherboard();
reboot();
//getMemory().dump();
for (SoftSwitches s : SoftSwitches.values()) {
s.getSwitch().reset();
}
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
getCpu().reset();
for (Card c : getMemory().getAllCards()) {
if (c != null) {
c.reset();
}
}
Computer.resume();
/*
getCpu().resume();
getVideo().resume();
*/
}
public void reboot() {
RAM r = getMemory();
r.write(IRQ_VECTOR, (byte) 0x00, false, true);
r.write(IRQ_VECTOR + 1, (byte) 0x00, false, true);
r.write(IRQ_VECTOR + 2, (byte) 0x00, false, true);
warmStart();
}
@Override
public void warmStart() {
boolean restart = Computer.pause();
for (SoftSwitches s : SoftSwitches.values()) {
s.getSwitch().reset();
}
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
getCpu().reset();
for (Card c : getMemory().getAllCards()) {
if (c != null) {
c.reset();
}
}
getCpu().resume();
Computer.resume();
}
private void insertCard(Class<? extends Card> type, int slot) {
if (getMemory().getCard(slot) != null) {
if (getMemory().getCard(slot).getClass().equals(type)) {
return;
}
getMemory().removeCard(slot);
}
if (type != null) {
try {
getMemory().addCard(type.newInstance(), slot);
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@Override
public final void reconfigure() {
boolean restart = Computer.pause();
super.reconfigure();
RAM128k currentMemory = (RAM128k) getMemory();
if (currentMemory != null && !(currentMemory.getClass().equals(ramCard.getValue()))) {
try {
RAM128k newMemory = (RAM128k) ramCard.getValue().newInstance();
newMemory.copyFrom(currentMemory);
setMemory(newMemory);
} catch (InstantiationException | IllegalAccessException ex) {
}
}
if (getMemory() == null) {
try {
currentMemory = (RAM128k) ramCard.getValue().newInstance();
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
try {
setMemory(currentMemory);
for (SoftSwitches s : SoftSwitches.values()) {
s.getSwitch().register();
}
} catch (Throwable ex) {
}
}
currentMemory.reconfigure();
try {
if (useConsoleProbe) {
probe.init(this);
} else {
probe.shutdown();
}
if (useDebugRom) {
loadRom("jace/data/apple2e_debug.rom");
} else {
loadRom("jace/data/apple2e.rom");
}
if (getVideo() == null || getVideo().getClass() != videoRenderer.getValue()) {
Graphics g = null;
if (getVideo() != null) {
getVideo().suspend();
g = getVideo().getScreen();
}
try {
setVideo((Video) videoRenderer.getValue().newInstance());
getVideo().configureVideoMode();
getVideo().reconfigure();
getVideo().setScreen(g);
Emulator.resizeVideo();
getVideo().resume();
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
}
// Add all new cards
insertCard(card1.getValue(), 1);
insertCard(card2.getValue(), 2);
insertCard(card3.getValue(), 3);
insertCard(card4.getValue(), 4);
insertCard(card5.getValue(), 5);
insertCard(card6.getValue(), 6);
insertCard(card7.getValue(), 7);
if (enableHints) {
enableHints();
} else {
disableHints();
}
getMemory().configureActiveMemory();
if (cheatEngine.getValue() == null) {
if (activeCheatEngine != null) {
activeCheatEngine.detach();
}
activeCheatEngine = null;
} else {
boolean startCheats = true;
if (activeCheatEngine != null) {
if (activeCheatEngine.getClass().equals(cheatEngine.getValue())) {
startCheats = false;
} else {
activeCheatEngine.detach();
activeCheatEngine = null;
}
}
if (startCheats) {
try {
activeCheatEngine = (Cheats) cheatEngine.getValue().newInstance();
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
activeCheatEngine.attach();
}
}
} catch (IOException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
if (restart) {
Computer.resume();
}
}
@Override
protected void doPause() {
if (motherboard == null) {
return;
}
motherboard.pause();
}
@Override
protected void doResume() {
if (motherboard == null) {
return;
}
motherboard.resume();
}
@Override
protected boolean isRunning() {
if (motherboard == null) {
return false;
}
return motherboard.isRunning() && !motherboard.isPaused;
}
private List<RAMListener> hints = new ArrayList<>();
private void enableHints() {
if (hints.isEmpty()) {
hints.add(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0FB63);
}
@Override
protected void doEvent(RAMEvent e) {
if (getCpu().getProgramCounter() != getScopeStart()) {
return;
}
Thread t = new Thread(() -> {
try {
// Give the floppy drive time to start
Thread.sleep(1000);
if (getCpu().getProgramCounter() >> 8 != 0x0c6) {
return;
}
int row = 2;
for (String s : new String[]{
" Welcome to",
" _ __ ___ ____ ",
" | | / /\\ / / ` | |_ ",
" \\_|_| /_/--\\ \\_\\_, |_|__ ",
"",
" Java Apple Computer Emulator",
"",
" Presented by BLuRry",
" http://goo.gl/SnzqG",
"",
"Press F1 to insert disk in Slot 6, D1",
"Press F2 to insert disk in Slot 6, D2",
"Press F3 to insert HDV or 2MG in slot 7",
"Press F4 to open configuration",
"Press F5 to run raw binary program",
"Press F8 to correct the aspect ratio",
"Press F9 to toggle fullscreen",
"Press F10 to open/close the debugger",
"",
" If metacheat is enabled:",
"Press HOME to activate memory heatmap",
"Press END to activate metacheat search"
}) {
int addr = 0x0401 + VideoDHGR.calculateTextOffset(row++);
for (char c : s.toCharArray()) {
getMemory().write(addr++, (byte) (c | 0x080), false, true);
}
}
while (getCpu().getProgramCounter() >> 8 == 0x0c6) {
int x = (int) (Math.random() * 26.0) + 7;
int y = (int) (Math.random() * 4.0) + 3;
int addr = 0x0400 + VideoDHGR.calculateTextOffset(y) + x;
byte old = getMemory().readRaw(addr);
for (char c : "+xX*+".toCharArray()) {
if (getCpu().getProgramCounter() >> 8 != 0x0c6) {
break;
}
getMemory().write(addr, (byte) (c | 0x080), true, true);
Thread.sleep(100);
}
getMemory().write(addr, old, true, true);
}
} catch (InterruptedException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
});
t.setName("Startup Animation");
t.start();
}
});
// Latch to the PRODOS SYNTAX CHECK parser
/*
hints.add(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {setScopeStart(0x0a685);}
@Override
protected void doEvent(RAMEvent e) {
String in = "";
for (int i=0x0200; i < 0x0300; i++) {
char c = (char) (getMemory().readRaw(i) & 0x07f);
if (c == 0x0d) break;
in += c;
}
System.err.println("Intercepted command: "+in);
}
});
*/
}
hints.stream().forEach((hint) -> {
getMemory().addListener(hint);
});
}
private void disableHints() {
hints.stream().forEach((hint) -> {
getMemory().removeListener(hint);
});
}
@Override
public String getShortName() {
return "computer";
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,286 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e;
import jace.core.Card;
import jace.core.Computer;
import jace.core.PagedMemory;
import jace.core.RAM;
import jace.state.Stateful;
import java.io.IOException;
import java.io.InputStream;
/**
* Implementation of a 128k memory space and the MMU found in an Apple //e. The
* MMU behavior is mimicked by configureActiveMemory.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
abstract public class RAM128k extends RAM {
@Stateful
public PagedMemory mainMemory;
@Stateful
public PagedMemory languageCard;
@Stateful
public PagedMemory languageCard2;
public PagedMemory cPageRom;
public PagedMemory rom;
public PagedMemory blank;
public RAM128k() {
super();
mainMemory = new PagedMemory(0xc000, PagedMemory.Type.ram);
rom = new PagedMemory(0x3000, PagedMemory.Type.firmwareMain);
cPageRom = new PagedMemory(0x1000, PagedMemory.Type.slotRom);
languageCard = new PagedMemory(0x3000, PagedMemory.Type.languageCard);
languageCard2 = new PagedMemory(0x1000, PagedMemory.Type.languageCard);
activeRead = new PagedMemory(0x10000, PagedMemory.Type.ram);
activeWrite = new PagedMemory(0x10000, PagedMemory.Type.ram);
blank = new PagedMemory(0x100, PagedMemory.Type.ram);
// Format memory with FF FF 00 00 pattern
for (int i = 0; i < 0x0100; i++) {
blank.get(0)[i] = (byte) 0x0FF;
}
initMemoryPattern(mainMemory);
}
public final void initMemoryPattern(PagedMemory mem) {
// Format memory with FF FF 00 00 pattern
for (int i = 0; i < 0x0100; i++) {
for (int j = 0; j < 0x0c0; j++) {
byte use = (byte) ((i % 4) > 1 ? 0x0FF : 0x00);
mem.get(j)[i] = use;
}
}
}
/**
*
*/
@Override
public void configureActiveMemory() {
log("MMU Switches");
synchronized (this) {
// First off, set up read/write for main memory (might get changed later on)
activeRead.fillBanks(SoftSwitches.RAMRD.getState() ? getAuxMemory() : mainMemory);
activeWrite.fillBanks(SoftSwitches.RAMWRT.getState() ? getAuxMemory() : mainMemory);
// Handle language card softswitches
activeRead.fillBanks(rom);
//activeRead.fillBanks(cPageRom);
for (int i = 0x0c0; i < 0x0d0; i++) {
activeWrite.set(i, null);
}
if (SoftSwitches.LCRAM.getState()) {
if (!SoftSwitches.AUXZP.getState()) {
activeRead.fillBanks(languageCard);
if (!SoftSwitches.LCBANK1.getState()) {
activeRead.fillBanks(languageCard2);
}
} else {
activeRead.fillBanks(getAuxLanguageCard());
if (!SoftSwitches.LCBANK1.getState()) {
activeRead.fillBanks(getAuxLanguageCard2());
}
}
}
if (SoftSwitches.LCWRITE.getState()) {
if (!SoftSwitches.AUXZP.getState()) {
activeWrite.fillBanks(languageCard);
if (!SoftSwitches.LCBANK1.getState()) {
activeWrite.fillBanks(languageCard2);
}
} else {
activeWrite.fillBanks(getAuxLanguageCard());
if (!SoftSwitches.LCBANK1.getState()) {
activeWrite.fillBanks(getAuxLanguageCard2());
}
}
} else {
// Make 0xd000 - 0xffff non-writable!
for (int i = 0x0d0; i < 0x0100; i++) {
activeWrite.set(i, null);
}
}
// Handle 80STORE logic for bankswitching video ram
if (SoftSwitches._80STORE.isOn()) {
activeRead.setBanks(0x04, 0x04, 0x04,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
activeWrite.setBanks(0x04, 0x04, 0x04,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
if (SoftSwitches.HIRES.isOn()) {
activeRead.setBanks(0x020, 0x020, 0x020,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
activeWrite.setBanks(0x020, 0x020, 0x020,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
}
}
// Handle zero-page bankswitching
if (SoftSwitches.AUXZP.getState()) {
// Aux pages 0 and 1
activeRead.setBanks(0, 2, 0, getAuxMemory());
activeWrite.setBanks(0, 2, 0, getAuxMemory());
} else {
// Main pages 0 and 1
activeRead.setBanks(0, 2, 0, mainMemory);
activeWrite.setBanks(0, 2, 0, mainMemory);
}
/*
INTCXROM SLOTC3ROM C1,C2,C4-CF C3
0 0 slot rom
0 1 slot slot
1 - rom rom
*/
if (SoftSwitches.CXROM.getState()) {
// Enable C1-CF to point to rom
activeRead.setBanks(0, 0x0F, 0x0C1, cPageRom);
} else {
// Enable C1-CF to point to slots
for (int slot = 1; slot <= 7; slot++) {
Card c = getCard(slot);
if (c != null) {
// Enable card slot ROM
activeRead.setBanks(0, 1, 0x0C0 + slot, c.getCxRom());
if (getActiveSlot() == slot) {
activeRead.setBanks(0, 8, 0x0C8, c.getC8Rom());
}
} else {
// Disable card slot ROM (TODO: floating bus)
activeRead.set(0x0C0 + slot, blank.get(0));
}
}
if (getActiveSlot() == 0) {
for (int i = 0x0C8; i < 0x0D0; i++) {
activeRead.set(i, blank.get(0));
}
}
if (SoftSwitches.SLOTC3ROM.isOff()) {
// Enable C3 to point to internal ROM
activeRead.setBanks(2, 1, 0x0C3, cPageRom);
}
if (SoftSwitches.INTC8ROM.getState()) {
// Enable C8-CF to point to internal ROM
activeRead.setBanks(7, 8, 0x0C8, cPageRom);
}
}
}
// All ROM reads not intecepted will return 0xFF! (TODO: floating bus)
activeRead.set(0x0c0, blank.get(0));
}
public void log(String message) {
if (Computer.getComputer().cpu != null && Computer.getComputer().cpu.isLogEnabled()) {
String stack = "";
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
stack += e.getClassName() + "." + e.getMethodName() + "(" + e.getLineNumber() + ");";
}
Computer.getComputer().cpu.log(stack);
Computer.getComputer().cpu.log(message + ";" + SoftSwitches.RAMRD + ";" + SoftSwitches.RAMWRT + ";" + SoftSwitches.AUXZP + ";" + SoftSwitches._80STORE + ";" + SoftSwitches.HIRES + ";" + SoftSwitches.PAGE2 + ";" + SoftSwitches.LCBANK1 + ";" + SoftSwitches.LCRAM + ";" + SoftSwitches.LCWRITE);
}
}
/**
*
* @param path
* @throws java.io.IOException
*/
@Override
protected void loadRom(String path) throws IOException {
// Remap writable ram to reflect rom file structure
byte[] ignore = new byte[256];
activeWrite.set(0, ignore); // Ignore first bank of data
for (int i = 1; i < 17; i++) {
activeWrite.set(i, ignore);
}
activeWrite.setBanks(0, cPageRom.getMemory().length, 0x011, cPageRom);
activeWrite.setBanks(0, rom.getMemory().length, 0x020, rom);
//----------------------
InputStream inputRom = getClass().getClassLoader().getResourceAsStream(path);
int read = 0;
int addr = 0;
byte[] in = new byte[1024];
while (addr < 0x00FFFF && (read = inputRom.read(in)) > 0) {
for (int i = 0; i < read; i++) {
write(addr++, in[i], false, false);
}
}
// System.out.println("Finished reading rom with " + inputRom.available() + " bytes left unread!");
//dump();
configureActiveMemory();
}
/**
* @return the mainMemory
*/
public PagedMemory getMainMemory() {
return mainMemory;
}
abstract public PagedMemory getAuxVideoMemory();
abstract public PagedMemory getAuxMemory();
abstract public PagedMemory getAuxLanguageCard();
abstract public PagedMemory getAuxLanguageCard2();
/**
* @return the languageCard
*/
public PagedMemory getLanguageCard() {
return languageCard;
}
/**
* @return the languageCard2
*/
public PagedMemory getLanguageCard2() {
return languageCard2;
}
/**
* @return the cPageRom
*/
public PagedMemory getcPageRom() {
return cPageRom;
}
/**
* @return the rom
*/
public PagedMemory getRom() {
return rom;
}
void copyFrom(RAM128k currentMemory) {
// This is really quick and dirty but should be sufficient to avoid most crashes...
blank = currentMemory.blank;
cPageRom = currentMemory.cPageRom;
rom = currentMemory.rom;
listeners = currentMemory.listeners;
mainMemory = currentMemory.mainMemory;
languageCard = currentMemory.languageCard;
languageCard2 = currentMemory.languageCard2;
cards = currentMemory.cards;
activeSlot = currentMemory.activeSlot;
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e;
import jace.apple2e.softswitch.IntC8SoftSwitch;
import jace.apple2e.softswitch.KeyboardSoftSwitch;
import jace.apple2e.softswitch.Memory2SoftSwitch;
import jace.apple2e.softswitch.MemorySoftSwitch;
import jace.apple2e.softswitch.VideoSoftSwitch;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.SoftSwitch;
/**
* Softswitches reside in the addresses C000-C07f and control everything from
* memory management to speaker sound and keyboard. Other I/O ports (c080-C0ff)
* are managed by any registered Cards. This enumeration serves as a convenient
* way to represent the different softswitches as well as provide a clean
* enumeration.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public enum SoftSwitches {
_80STORE(new MemorySoftSwitch("80Store", 0x0c000, 0x0c001, 0x0c018, RAMEvent.TYPE.WRITE, false)),
RAMRD(new MemorySoftSwitch("AuxRead (RAMRD)", 0x0c002, 0x0c003, 0x0c013, RAMEvent.TYPE.WRITE, false)),
RAMWRT(new MemorySoftSwitch("AuxWrite (RAMWRT)", 0x0c004, 0x0c005, 0x0c014, RAMEvent.TYPE.WRITE, false)),
CXROM(new MemorySoftSwitch("IntCXROM", 0x0c006, 0x0c007, 0x0c015, RAMEvent.TYPE.WRITE, false)),
AUXZP(new MemorySoftSwitch("AuxZeroPage", 0x0c008, 0x0c009, 0x0c016, RAMEvent.TYPE.WRITE, false)),
SLOTC3ROM(new MemorySoftSwitch("C3ROM", 0x0c00a, 0x0c00b, 0x0c017, RAMEvent.TYPE.WRITE, false)),
INTC8ROM(new IntC8SoftSwitch()),
LCBANK1(new MemorySoftSwitch("LangCardBank1",
new int[]{0x0c088, 0x0c089, 0x0c08a, 0x0c08b, 0x0c08c, 0x0c08d, 0x0c08e, 0x0c08f},
new int[]{0x0c080, 0x0c081, 0x0c082, 0x0c083, 0x0c084, 0x0c085, 0x0c086, 0x0c087},
new int[]{0x0c011}, RAMEvent.TYPE.ANY, false)),
LCRAM(new MemorySoftSwitch("LangCardRam/HRAMRD'",
new int[]{0x0c081, 0x0c082, 0x0c085, 0x0c086, 0x0c089, 0x0c08a, 0x0c08d, 0x0c08e},
new int[]{0x0c080, 0x0c083, 0x0c084, 0x0c087, 0x0c088, 0x0c08b, 0x0c08c, 0x0c08f},
new int[]{0x0c012}, RAMEvent.TYPE.ANY, false)),
LCWRITE(new Memory2SoftSwitch("LangCardWrite",
new int[]{0x0c080, 0x0c082, 0x0c084, 0x0c086, 0x0c088, 0x0c08a, 0x0c08c, 0x0c08e},
new int[]{0x0c081, 0x0c083, 0x0c085, 0x0c087, 0x0c089, 0x0c08b, 0x0c08d, 0x0c08f},
null, RAMEvent.TYPE.ANY, true)),
//Renamed as per Sather 5-7
_80COL(new VideoSoftSwitch("80ColumnVideo (80COL/80VID)", 0x0c00c, 0x0c00d, 0x0c01f, RAMEvent.TYPE.WRITE, false)),
ALTCH(new VideoSoftSwitch("Mousetext", 0x0c00e, 0x0c00f, 0x0c01e, RAMEvent.TYPE.WRITE, false)),
TEXT(new VideoSoftSwitch("Text", 0x0c050, 0x0c051, 0x0c01a, RAMEvent.TYPE.ANY, true)),
MIXED(new VideoSoftSwitch("Mixed", 0x0c052, 0x0c053, 0x0c01b, RAMEvent.TYPE.ANY, false)),
PAGE2(new VideoSoftSwitch("Page2", 0x0c054, 0x0c055, 0x0c01c, RAMEvent.TYPE.ANY, false) {
@Override
public void stateChanged() {
// if (Computer.getComputer() == null) {
// return;
// }
// if (Computer.getComputer() == null && Computer.getComputer().getMemory() == null) {
// return;
// }
// if (Computer.getComputer() == null && Computer.getComputer().getVideo() == null) {
// return;
// }
// PAGE2 is a hybrid switch; 80STORE ? memory : video
if (_80STORE.isOn()) {
Computer.getComputer().getMemory().configureActiveMemory();
} else {
Computer.getComputer().getVideo().configureVideoMode();
}
}
}),
HIRES(new VideoSoftSwitch("Hires", 0x0c056, 0x0c057, 0x0c01d, RAMEvent.TYPE.ANY, false)),
DHIRES(new VideoSoftSwitch("Double-hires", 0x0c05f, 0x0c05e, 0x0c07f, RAMEvent.TYPE.ANY, false)),
PB0(new MemorySoftSwitch("Pushbutton0", -1, -1, 0x0c061, RAMEvent.TYPE.ANY, null)),
PB1(new MemorySoftSwitch("Pushbutton1", -1, -1, 0x0c062, RAMEvent.TYPE.ANY, null)),
PB2(new MemorySoftSwitch("Pushbutton2", -1, -1, 0x0c063, RAMEvent.TYPE.ANY, null)),
PDLTRIG(new MemorySoftSwitch(
"PaddleTrigger",
null,
new int[]{0x0c070, 0x0c071, 0x0c072, 0x0c073, 0x0c074, 0x0c075, 0x0c076, 0x0c077,
0x0c078, 0x0c079, 0x0c07a, 0x0c07b, 0x0c07c, 0x0c07d, 0x0c07e, 0x0c07f},
null, RAMEvent.TYPE.ANY, false)),
PDL0(new MemorySoftSwitch("Paddle0", -1, -1, 0x0c064, RAMEvent.TYPE.ANY, false)),
PDL1(new MemorySoftSwitch("Paddle1", -1, -1, 0x0c065, RAMEvent.TYPE.ANY, false)),
PDL2(new MemorySoftSwitch("Paddle2", -1, -1, 0x0c066, RAMEvent.TYPE.ANY, false)),
PDL3(new MemorySoftSwitch("Paddle3", -1, -1, 0x0c067, RAMEvent.TYPE.ANY, false)),
AN0(new MemorySoftSwitch("Annunciator0", 0x0c058, 0x0c059, -1, RAMEvent.TYPE.ANY, false)),
AN1(new MemorySoftSwitch("Annunciator1", 0x0c05a, 0x0c05b, -1, RAMEvent.TYPE.ANY, false)),
AN2(new MemorySoftSwitch("Annunciator2", 0x0c05c, 0x0c05d, -1, RAMEvent.TYPE.ANY, false)),
AN3(new MemorySoftSwitch("Annunciator3", 0x0c05e, 0x0c05f, -1, RAMEvent.TYPE.ANY, false)),
KEYBOARD(new KeyboardSoftSwitch(
"Keyboard",
new int[]{0x0c010, 0x0c11, 0x0c012, 0x0c013, 0x0c014, 0x0c015, 0x0c016, 0x0c017,
0x0c018, 0x0c019, 0x0c01a, 0x0c01b, 0x0c01c, 0x0c01d, 0x0c01e, 0x0c01f},
null,
new int[]{0x0c000, 0x0c001, 0x0c002, 0x0c003, 0x0c004, 0x0c005, 0x0c006, 0x0c007,
0x0c008, 0x0c009, 0x0c00a, 0x0c00b, 0x0c00c, 0x0c00d, 0x0c00e, 0x0c00f, 0x0c010},
RAMEvent.TYPE.WRITE, false)),
//C010 should clear keyboard strobe when read as well
KEYBOARD_STROBE_READ(new SoftSwitch("KeyStrobe_Read", 0x0c010, -1, -1, RAMEvent.TYPE.READ, false) {
@Override
protected byte readSwitch() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void stateChanged() {
KEYBOARD.getSwitch().setState(false);
}
}),
TAPEOUT(new MemorySoftSwitch("TapeOut", 0x0c020, 0x0c020, 0x0c060, RAMEvent.TYPE.ANY, false)),
VBL(new VideoSoftSwitch("VBL", -1, -1, 0x0c019, RAMEvent.TYPE.ANY, false)),
FLOATING_BUS(new SoftSwitch("FloatingBus", null, null, new int[]{0x0C050, 0x0C051, 0x0C052, 0x0C053, 0x0C054}, RAMEvent.TYPE.READ, null) {
@Override
protected byte readSwitch() {
if (Computer.getComputer().getVideo() == null) {
return 0;
}
return Computer.getComputer().getVideo().getFloatingBus();
}
@Override
public void stateChanged() {
}
});
/*
2C:VBL (new MemorySoftSwitch(0x0c070, 0x0*, 0x0c041, RAMEvent.TYPE.ANY, false)),
2C:VBLENABLE (new MemorySoftSwitch(0x0c05a, 0x0c05b, 0x0-, RAMEvent.TYPE.ANY, false)),
2C:XINT (new MemorySoftSwitch(0x0c015 (r), c048-c04f (r/w), 0x0-, 0x0-, RAMEvent.TYPE.ANY, false)),
2C:YINT (new MemorySoftSwitch(0x0c017 (r), c048-c04f (r/w), 0x0-, 0x0-, RAMEvent.TYPE.ANY, false)),
2C:MBUTTON (new MemorySoftSwitch(0x0*, 0x0*, 0x0c063, RAMEvent.TYPE.ANY, false)),
2C:80/40 switch (new MemorySoftSwitch(0x0*, 0x0*, 0x0c060, RAMEvent.TYPE.ANY, false)),
2C:XDirection (new MemorySoftSwitch(0x0*, 0x0*, 0x0c066, RAMEvent.TYPE.ANY, false)),
2C:YDirection (new MemorySoftSwitch(0x0*, 0x0*, 0x0c067, RAMEvent.TYPE.ANY, false)),
*/
private final SoftSwitch softswitch;
/**
* Creates a new instance of SoftSwitches
*/
private SoftSwitches(SoftSwitch softswitch) {
this.softswitch = softswitch;
}
public SoftSwitch getSwitch() {
return softswitch;
}
public boolean getState() {
return softswitch.getState();
}
public final boolean isOn() {
return softswitch.getState();
}
public final boolean isOff() {
return !softswitch.getState();
}
@Override
public String toString() {
return softswitch.toString();
}
}

View File

@ -0,0 +1,369 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e;
import jace.config.ConfigurableField;
import jace.core.Computer;
import jace.core.Device;
import jace.core.Motherboard;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.SoundMixer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import static jace.core.Utility.*;
import java.io.FileNotFoundException;
/**
* Apple // Speaker Emulation Created on May 9, 2007, 9:55 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Speaker extends Device {
static boolean fileOutputActive = false;
static OutputStream out;
public static void toggleFileOutput() {
if (fileOutputActive) {
try {
out.close();
} catch (IOException ex) {
Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
}
out = null;
fileOutputActive = false;
} else {
JFileChooser fileChooser = new JFileChooser();
fileChooser.showSaveDialog(null);
File f = fileChooser.getSelectedFile();
if (f == null) {
return;
}
if (f.exists()) {
int i = JOptionPane.showConfirmDialog(null, "Overwrite existing file?");
if (i != JOptionPane.OK_OPTION && i != JOptionPane.YES_OPTION) {
return;
}
}
try {
out = new FileOutputStream(f);
fileOutputActive = true;
} catch (FileNotFoundException ex) {
Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* Counter tracks the number of cycles between sampling
*/
private double counter = 0;
/**
* Level is the number of cycles the speaker has been on
*/
private int level = 0;
/**
* Idle cycles counts the number of cycles the speaker has not been changed
* (used to deactivate sound when not in use)
*/
private int idleCycles = 0;
/**
* Number of samples in buffer
*/
static int BUFFER_SIZE = (int) (((float) SoundMixer.RATE) * 0.4);
// Number of samples available in output stream before playback happens (avoid extra blocking)
// static int MIN_PLAYBACK_BUFFER = BUFFER_SIZE / 2;
static int MIN_PLAYBACK_BUFFER = 64;
// Number of samples in buffer to wait until playback (avoid underrun)
private int MIN_SAMPLE_PLAYBACK = 64;
/**
* Playback volume (should be < 1423)
*/
@ConfigurableField(name = "Speaker Volume", shortName = "vol", description = "Should be under 1400")
public static int VOLUME = 600;
/**
* Number of idle cycles until speaker playback is deactivated
*/
@ConfigurableField(name = "Idle cycles before sleep", shortName = "idle")
public static int MAX_IDLE_CYCLES = 100000;
/**
* Java sound output
*/
private SourceDataLine sdl;
/**
* Manifestation of the apple speaker softswitch
*/
private boolean speakerBit = false;
//
/**
* Locking semaphore to prevent race conditions when working with buffer or
* related variables
*/
private final Object bufferLock = new Object();
/**
* Double-buffer used for playing processed sound -- as one is played the
* other fills up.
*/
byte[] soundBuffer1;
byte[] soundBuffer2;
int currentBuffer = 1;
int bufferPos = 0;
private double TICKS_PER_SAMPLE = ((double) Motherboard.SPEED) / ((double) SoundMixer.RATE);
private double TICKS_PER_SAMPLE_FLOOR = Math.floor(TICKS_PER_SAMPLE);
Thread playbackThread;
private final RAMListener listener
= new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
public boolean isRelevant(RAMEvent e) {
return true;
}
@Override
protected void doConfig() {
setScopeStart(0x0C030);
setScopeEnd(0x0C03F);
}
@Override
protected void doEvent(RAMEvent e) {
if (e.getType() == RAMEvent.TYPE.WRITE) {
level += 2;
} else {
speakerBit = !speakerBit;
}
resetIdle();
}
};
/**
* Creates a new instance of Speaker
*/
public Speaker() {
configureListener();
reconfigure();
}
/**
* Suspend playback of sound
* @return
*/
@Override
public boolean suspend() {
boolean result = super.suspend();
speakerBit = false;
if (playbackThread != null && playbackThread.isAlive()) {
playbackThread = null;
}
return result;
}
/**
* Start or resume playback of sound
*/
@Override
public void resume() {
sdl = null;
try {
sdl = Motherboard.mixer.getLine(this);
} catch (LineUnavailableException ex) {
System.out.println("ERROR: Could not output sound: " + ex.getMessage());
}
if (sdl != null) {
setRun(true);
counter = 0;
idleCycles = 0;
level = 0;
bufferPos = 0;
if (playbackThread == null || !playbackThread.isAlive()) {
playbackThread = new Thread(new Runnable() {
@Override
public void run() {
int len;
while (isRunning()) {
// Motherboard.requestSpeed(this);
len = bufferPos;
if (len >= MIN_SAMPLE_PLAYBACK) {
byte[] buffer;
synchronized (bufferLock) {
len = bufferPos;
buffer = (currentBuffer == 1) ? soundBuffer1 : soundBuffer2;
currentBuffer = (currentBuffer == 1) ? 2 : 1;
bufferPos = 0;
}
sdl.write(buffer, 0, len);
if (fileOutputActive && out != null) {
try {
out.write(buffer, 0, len);
} catch (IOException ex) {
Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
}
}
} else {
try {
// Wait 12.5 ms, which is 1/8 the total duration of the buffer
Thread.sleep(10);
} catch (InterruptedException ex) {
Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
Motherboard.cancelSpeedRequest(this);
Motherboard.mixer.returnLine(this);
}
});
playbackThread.setName("Speaker playback");
playbackThread.start();
}
}
}
/**
* Reset idle counter whenever sound playback occurs
*/
public void resetIdle() {
idleCycles = 0;
if (!isRunning()) {
resume();
}
}
/**
* Motherboard cycle tick Every 23 ticks a sample will be added to the
* buffer If the buffer is full, this will block until there is room in the
* buffer, thus keeping the emulation in sync with the sound
*/
@Override
public void tick() {
if (!isRunning() || playbackThread == null) {
return;
}
if (idleCycles++ >= MAX_IDLE_CYCLES) {
suspend();
}
if (speakerBit) {
level++;
}
counter += 1.0d;
if (counter >= TICKS_PER_SAMPLE) {
int sample = level * VOLUME;
int bytes = SoundMixer.BITS >> 3;
int shift = SoundMixer.BITS;
// Force emulator to wait until sound buffer has been processed
int wait = 0;
while (bufferPos >= BUFFER_SIZE) {
if (wait++ > 1000) {
Computer.pause();
detach();
Computer.resume();
Motherboard.enableSpeaker = false;
gripe("Sound playback is not working properly. Check your configuration and sound system to ensure they are set up properly.");
return;
}
try {
// Yield to other threads (e.g. sound) so that the buffer can drain
Thread.sleep(5);
} catch (InterruptedException ex) {
}
}
byte[] buf;
synchronized (bufferLock) {
if (currentBuffer == 1) {
buf = soundBuffer1;
} else {
buf = soundBuffer2;
}
int index = bufferPos;
for (int i = 0; i < SoundMixer.BITS; i += 8, index++) {
shift -= 8;
buf[index] = buf[index + bytes] = (byte) ((sample >> shift) & 0x0ff);
}
bufferPos += bytes * 2;
}
// Set level back to 0
level = 0;
// Set counter to 0
counter -= TICKS_PER_SAMPLE_FLOOR;
}
}
/**
* Add a memory event listener for C03x for capturing speaker events
*/
private void configureListener() {
Computer.getComputer().getMemory().addListener(listener);
}
private void removeListener() {
Computer.getComputer().getMemory().removeListener(listener);
}
/**
* Returns "Speaker"
*
* @return "Speaker"
*/
@Override
protected String getDeviceName() {
return "Speaker";
}
@Override
public String getShortName() {
return "spk";
}
@Override
public final void reconfigure() {
if (soundBuffer1 != null && soundBuffer2 != null) {
return;
}
BUFFER_SIZE = 10000 * (SoundMixer.BITS >> 3);
MIN_SAMPLE_PLAYBACK = SoundMixer.BITS * 8;
soundBuffer1 = new byte[BUFFER_SIZE];
soundBuffer2 = new byte[BUFFER_SIZE];
}
@Override
public void attach() {
configureListener();
resume();
}
@Override
public void detach() {
removeListener();
suspend();
}
}

View File

@ -0,0 +1,747 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e;
import jace.core.Computer;
import jace.core.Font;
import jace.core.Palette;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.Video;
import jace.core.VideoWriter;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.util.logging.Logger;
/**
* This is the primary video rendering class, which provides all necessary video
* writers for every display mode as well as managing the display mode (via
* configureVideoMode). The quality of the color rendering is sub-par compared
* to VideoNTSC.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class VideoDHGR extends Video {
// Reorder bits 3,2,1,0 -> 0,3,2,1
// Fixes double-hires color palette
public final static int flipNybble[] = {
0, 2, 4, 6,
8, 10, 12, 14,
1, 3, 5, 7,
9, 11, 13, 15
};
private static final boolean USE_GS_MOUSETEXT = false;
private VideoWriter textPage1;
private VideoWriter textPage2;
private VideoWriter loresPage1;
private VideoWriter loresPage2;
private VideoWriter hiresPage1;
private VideoWriter hiresPage2;
// Special 80-column modes
private VideoWriter text80Page1;
private VideoWriter text80Page2;
private VideoWriter dloresPage1;
private VideoWriter dloresPage2;
private VideoWriter dhiresPage1;
private VideoWriter dhiresPage2;
// Mixed mode
private VideoWriter mixed;
private VideoWriter currentGraphicsWriter = null;
private VideoWriter currentTextWriter = null;
/**
* Creates a new instance of VideoDHGR
*/
public VideoDHGR() {
hiresPage1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (hiresOffset[y] + 0x02000);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayHires(screen, xOffset, y, yGraphicsOffset + 0x02000);
}
};
hiresPage2 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (hiresOffset[y] + 0x04000);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayHires(screen, xOffset, y, yGraphicsOffset + 0x04000);
}
};
dhiresPage1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (hiresOffset[y] + 0x02000);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayDoubleHires(screen, xOffset, y, yGraphicsOffset + 0x02000);
}
@Override
public VideoWriter actualWriter() {
return hiresPage1;
}
};
dhiresPage2 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (hiresOffset[y] + 0x04000);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayDoubleHires(screen, xOffset, y, yGraphicsOffset + 0x04000);
}
@Override
public VideoWriter actualWriter() {
return hiresPage2;
}
};
textPage1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0400);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayText(screen, xOffset, y, yTextOffset + 0x0400);
}
};
textPage2 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0800);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayText(screen, xOffset, y, yTextOffset + 0x0800);
}
};
text80Page1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0400);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayText80(screen, xOffset, y, yTextOffset + 0x0400);
}
@Override
public VideoWriter actualWriter() {
return textPage1;
}
};
text80Page2 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0800);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayText80(screen, xOffset, y, yTextOffset + 0x0800);
}
@Override
public VideoWriter actualWriter() {
return textPage2;
}
};
loresPage1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0400);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayLores(screen, xOffset, y, yTextOffset + 0x0400);
}
@Override
public VideoWriter actualWriter() {
return textPage1;
}
};
loresPage2 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0800);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayLores(screen, xOffset, y, yTextOffset + 0x0800);
}
@Override
public VideoWriter actualWriter() {
return textPage2;
}
};
dloresPage1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0400);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayDoubleLores(screen, xOffset, y, yTextOffset + 0x0400);
}
@Override
public VideoWriter actualWriter() {
return textPage1;
}
};
dloresPage2 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (textOffset[y] + 0x0800);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayDoubleLores(screen, xOffset, y, yTextOffset + 0x0800);
}
@Override
public VideoWriter actualWriter() {
return textPage2;
}
};
mixed = new VideoWriter() {
@Override
public int getYOffset(int y) {
return actualWriter().getYOffset(y);
}
@Override
public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) {
displayMixed(screen, xOffset, y, yTextOffset, yGraphicsOffset);
}
@Override
public void markDirty(int y) {
actualWriter().actualWriter().markDirty(y);
}
@Override
public void clearDirty(int y) {
actualWriter().actualWriter().clearDirty(y);
}
@Override
public boolean isRowDirty(int y) {
return actualWriter().actualWriter().isRowDirty(y);
}
@Override
public VideoWriter actualWriter() {
if (y < 160) {
return currentGraphicsWriter;
}
return currentTextWriter;
}
@Override
public boolean isMixed() {
return true;
}
};
registerDirtyFlagChecks();
}
// color burst per byte (chat mauve compatibility)
boolean[] useColor = new boolean[80];
protected void displayDoubleHires(BufferedImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
return;
}
int b1 = ((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset);
int b2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset);
int b3 = ((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1);
int b4 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1);
int useColOffset = xOffset << 1;
// This shouldn't be necessary but prevents an index bounds exception when graphics modes are flipped (Race condition?)
if (useColOffset >= 77) {
useColOffset = 76;
}
useColor[useColOffset] = (b1 & 0x80) != 0;
useColor[useColOffset + 1] = (b2 & 0x80) != 0;
useColor[useColOffset + 2] = (b3 & 0x80) != 0;
useColor[useColOffset + 3] = (b4 & 0x80) != 0;
int dhgrWord = 0x07f & b1;
dhgrWord |= (0x07f & b2) << 7;
dhgrWord |= (0x07f & b3) << 14;
dhgrWord |= (0x07f & b4) << 21;
showDhgr(screen, times14[xOffset], y, dhgrWord);
}
boolean extraHalfBit = false;
protected void displayHires(BufferedImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
return;
}
int b1 = 0x0ff & ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset);
int b2 = 0x0ff & ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1);
int dhgrWord = hgrToDhgr[(extraHalfBit && xOffset > 0) ? b1 | 0x0100 : b1][b2];
extraHalfBit = (dhgrWord & 0x10000000) != 0;
showDhgr(screen, times14[xOffset], y, dhgrWord & 0xfffffff);
// If you want monochrome, use this instead...
// showBW(screen, times14[xOffset], y, dhgrWord);
}
// Take two consecutive bytes and double them, taking hi-bit into account
// This should yield a 28-bit word of 7 color dhgr pixels
// This looks like crap on text...
static final int[][] hgrToDhgr;
// Take two consecutive bytes and double them, disregarding hi-bit
// Useful for text mode
static final int[][] hgrToDhgrBW;
static final int[] times14;
static final int[] flipBits;
static {
// complete reverse of 8 bits
flipBits = new int[256];
for (int i = 0; i < 256; i++) {
flipBits[i] = (((i * 0x0802 & 0x22110) | (i * 0x8020 & 0x88440)) * 0x10101 >> 16) & 0x0ff;
}
times14 = new int[40];
for (int i = 0; i < 40; i++) {
times14[i] = i * 14;
}
hgrToDhgr = new int[512][256];
hgrToDhgrBW = new int[256][256];
for (int bb1 = 0; bb1 < 512; bb1++) {
for (int bb2 = 0; bb2 < 256; bb2++) {
int value = ((bb1 & 0x0181) >= 0x0101) ? 1 : 0;
int b1 = byteDoubler((byte) (bb1 & 0x07f));
if ((bb1 & 0x080) != 0) {
b1 <<= 1;
}
int b2 = byteDoubler((byte) (bb2 & 0x07f));
if ((bb2 & 0x080) != 0) {
b2 <<= 1;
}
if ((bb1 & 0x040) == 0x040 && (bb2 & 1) != 0) {
b2 |= 1;
}
value |= b1 | (b2 << 14);
if ((bb2 & 0x040) != 0) {
value |= 0x10000000;
}
hgrToDhgr[bb1][bb2] = value;
hgrToDhgrBW[bb1 & 0x0ff][bb2] =
byteDoubler((byte) bb1) | (byteDoubler((byte) bb2) << 14);
}
}
}
protected void displayLores(BufferedImage screen, int xOffset, int y, int rowAddress) {
int c1 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
} else {
c1 >>= 4;
}
DataBuffer b = screen.getRaster().getDataBuffer();
int yOffset = xyOffset[y][times14[xOffset]];
int color = Palette.color[c1].getRGB();
// Unrolled loop, faster
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
}
private void displayDoubleLores(BufferedImage screen, int xOffset, int y, int rowAddress) {
int c1 = ((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset) & 0x0FF;
int c2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
c2 &= 15;
} else {
c1 >>= 4;
c2 >>= 4;
}
DataBuffer b = screen.getRaster().getDataBuffer();
int yOffset = xyOffset[y][times14[xOffset]];
int color = Palette.color[c1].getRGB();
int color2 = Palette.color[c2].getRGB();
// Unrolled loop, faster
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color2);
b.setElem(yOffset++, color2);
b.setElem(yOffset++, color2);
b.setElem(yOffset++, color2);
b.setElem(yOffset++, color2);
b.setElem(yOffset++, color2);
b.setElem(yOffset++, color2);
}
boolean flashInverse = false;
int flashTimer = 0;
int FLASH_SPEED = 16; // UTAIIe:8-13,P7 - FLASH toggles every 16 scans
int[] currentCharMap = CHAR_MAP1;
static final int[] CHAR_MAP1;
static final int[] CHAR_MAP2;
static final int[] CHAR_MAP3;
static {
// Generate screen text lookup maps ahead of time
// ALTCHR clear
// 00-3F - Inverse characters (uppercase only) "@P 0"
// 40-7F - Flashing characters (uppercase only) "@P 0"
// 80-BF - Normal characters (uppercase only) "@P 0"
// C0-DF - Normal characters (repeat 80-9F) "@P"
// E0-FF - Normal characters (lowercase) "`p"
// ALTCHR set
// 00-3f - Inverse characters (uppercase only) "@P 0"
// 40-5f - Mousetext (//gs alts are at 0x46 and 0x47, swap with 0x11 and 0x12 for //e and //c)
// 60-7f - Inverse characters (lowercase only)
// 80-BF - Normal characters (uppercase only)
// C0-DF - Normal characters (repeat 80-9F)
// E0-FF - Normal characters (lowercase)
// MAP1: Normal map, flash inverse = false
CHAR_MAP1 = new int[256];
// MAP2: Normal map, flash inverse = true
CHAR_MAP2 = new int[256];
// MAP3: Alt map, mousetext mode
CHAR_MAP3 = new int[256];
for (int b = 0; b < 256; b++) {
int mod = b % 0x020;
// Inverse
if (b < 0x020) {
CHAR_MAP1[b] = mod + 0x0c0;
CHAR_MAP2[b] = mod + 0x0c0;
CHAR_MAP3[b] = mod + 0x0c0;
} else if (b < 0x040) {
CHAR_MAP1[b] = mod + 0x0a0;
CHAR_MAP2[b] = mod + 0x0a0;
CHAR_MAP3[b] = mod + 0x0a0;
} else if (b < 0x060) {
// Flash/Mouse
CHAR_MAP1[b] = mod + 0x0c0;
CHAR_MAP2[b] = mod + 0x040;
if (!USE_GS_MOUSETEXT && mod == 6) {
CHAR_MAP3[b] = 0x011;
} else if (!USE_GS_MOUSETEXT && mod == 7) {
CHAR_MAP3[b] = 0x012;
} else {
CHAR_MAP3[b] = mod + 0x080;
}
} else if (b < 0x080) {
// Flash/Inverse lowercase
CHAR_MAP1[b] = mod + 0x0a0;
CHAR_MAP2[b] = mod + 0x020;
CHAR_MAP3[b] = mod + 0x0e0;
} else if (b < 0x0a0) {
// Normal uppercase
CHAR_MAP1[b] = mod + 0x040;
CHAR_MAP2[b] = mod + 0x040;
CHAR_MAP3[b] = mod + 0x040;
} else if (b < 0x0c0) {
// Normal uppercase
CHAR_MAP1[b] = mod + 0x020;
CHAR_MAP2[b] = mod + 0x020;
CHAR_MAP3[b] = mod + 0x020;
} else if (b < 0x0e0) {
// Normal uppercase (repeat)
CHAR_MAP1[b] = mod + 0x040;
CHAR_MAP2[b] = mod + 0x040;
CHAR_MAP3[b] = mod + 0x040;
} else {
// Normal lowercase
CHAR_MAP1[b] = mod + 0x060;
CHAR_MAP2[b] = mod + 0x060;
CHAR_MAP3[b] = mod + 0x060;
}
}
}
@Override
public void vblankStart() {
// ALTCHR set only affects character mapping and disables FLASH.
if (SoftSwitches.ALTCH.isOn()) {
currentCharMap = CHAR_MAP3;
} else {
flashTimer--;
if (flashTimer <= 0) {
markFlashDirtyBits();
flashTimer = FLASH_SPEED;
flashInverse = !flashInverse;
if (flashInverse) {
currentCharMap = CHAR_MAP2;
} else {
currentCharMap = CHAR_MAP1;
}
}
}
super.vblankStart();
}
@Override
public void vblankEnd() {
}
private int getFontChar(byte b) {
return currentCharMap[b & 0x0ff];
}
protected void displayText(BufferedImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
return;
}
int yOffset = y & 7;
byte byte2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1);
int c1 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset));
int c2 = getFontChar(byte2);
int b1 = Font.getByte(c1, yOffset);
int b2 = Font.getByte(c2, yOffset);
// Why is this getting inversed now? Bug in hgrToDhgrBW?
// Nick says: are you getting confused because the //e video ROM is inverted? (1=black)
int out = hgrToDhgrBW[b1][b2];
showBW(screen, times14[xOffset], y, out);
}
protected void displayText80(BufferedImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
return;
}
int yOffset = y & 7;
int c1 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset));
int c2 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset));
int c3 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1));
int c4 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1));
int bits = Font.getByte(c1, yOffset) | (Font.getByte(c2, yOffset) << 7)
| (Font.getByte(c3, yOffset) << 14) | (Font.getByte(c4, yOffset) << 21);
showBW(screen, times14[xOffset], y, bits);
}
private void displayMixed(BufferedImage screen, int xOffset, int y, int textOffset, int graphicsOffset) {
mixed.actualWriter().displayByte(screen, xOffset, y, textOffset, graphicsOffset);
}
protected boolean hiresMode = false;
public boolean dhgrMode = false;
@Override
public void configureVideoMode() {
boolean page2 = SoftSwitches.PAGE2.isOn() && SoftSwitches._80STORE.isOff();
dhgrMode = SoftSwitches._80COL.getState() && SoftSwitches.DHIRES.getState() && SoftSwitches.HIRES.getState();
currentTextWriter =
SoftSwitches._80COL.getState()
? page2
? text80Page2 : text80Page1
: page2
? textPage2 : textPage1;
currentGraphicsWriter =
SoftSwitches._80COL.getState() && SoftSwitches.DHIRES.getState()
? SoftSwitches.HIRES.getState()
? page2
? dhiresPage2 : dhiresPage1
: page2
? dloresPage2 : dloresPage1
: SoftSwitches.HIRES.getState()
? page2
? hiresPage2 : hiresPage1
: page2
? loresPage2 : loresPage1;
setCurrentWriter(
SoftSwitches.TEXT.getState() ? currentTextWriter
: SoftSwitches.MIXED.getState() ? mixed
: currentGraphicsWriter);
hiresMode = !SoftSwitches.DHIRES.getState();
}
protected void showDhgr(BufferedImage screen, int xOffset, int y, int dhgrWord) {
//Graphics2D g = (Graphics2D) screen.getGraphics();
DataBuffer b = screen.getRaster().getDataBuffer();
int yOffset = xyOffset[y][xOffset];
try {
for (int i = 0; i < 7; i++) {
int color = Palette.color[flipNybble[dhgrWord & 15]].getRGB();
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
b.setElem(yOffset++, color);
dhgrWord >>= 4;
}
} catch (ArrayIndexOutOfBoundsException ex) {
Logger.getLogger(getClass().getName()).warning("Went out of bounds in video display");
}
}
static final int BLACK = Color.BLACK.getRGB();
static final int WHITE = Color.WHITE.getRGB();
static final int[][] xyOffset;
static {
xyOffset = new int[192][560];
for (int y = 0; y < 192; y++) {
for (int x = 0; x < 560; x++) {
xyOffset[y][x] = y * 560 + x;
}
}
}
protected void showBW(BufferedImage screen, int xOffset, int y, int dhgrWord) {
int color = 0;
// Using the data buffer directly is about 15 times faster than setRGB
// This is because setRGB does extra (useless) color model logic
// For that matter even Graphics.drawLine is faster than setRGB!
DataBuffer b = screen.getRaster().getDataBuffer();
// This is equivilant to y*560 but is 5% faster
// Also, adding xOffset now makes it additionally 5% faster
//int yOffset = ((y << 4) + (y << 5) + (y << 9))+xOffset;
//is this lookup faster?
int yOffset = xyOffset[y][xOffset];
for (int i = 0; i < 28; i++) {
// yOffset++ is used instead of yOffset+i, because it is faster
b.setElem(yOffset++, (dhgrWord & 1) == 1 ? WHITE : BLACK);
dhgrWord >>= 1;
}
}
/**
*
*/
@Override
public void doPostDraw() {
}
/**
*
* @return
*/
@Override
protected String getDeviceName() {
return "DHGR-Capable Video";
}
private void markFlashDirtyBits() {
// TODO: Be smarter about detecting where flash is used... one day...
for (int row = 0; row < 192; row++) {
currentTextWriter.markDirty(row);
}
}
private void registerDirtyFlagChecks() {
((RAM128k) Computer.getComputer().getMemory()).addListener(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0400);
setScopeEnd(0x0bff);
}
@Override
protected void doEvent(RAMEvent e) {
int row = textRowLookup[e.getAddress() & 0x03ff];
// int row = identifyTextRow(e.getAddress() & 0x03ff);
if (row > 23) {
return;
}
VideoWriter tmark = (e.getAddress() < 0x0800) ? textPage1 : textPage2;
row <<= 3;
int yy = row + 8;
for (int y = row; y < yy; y++) {
tmark.markDirty(y);
}
}
});
((RAM128k) Computer.getComputer().getMemory()).addListener(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x2000);
setScopeEnd(0x5fff);
}
@Override
protected void doEvent(RAMEvent e) {
int row = hiresRowLookup[e.getAddress() & 0x01fff];
// int row = identifyHiresRow(e.getAddress() & 0x03fff);
if (row < 0 || row >= 192) {
return;
}
VideoWriter mark = (e.getAddress() < 0x04000) ? hiresPage1 : hiresPage2;
mark.markDirty(row);
}
});
}
@Override
public void reconfigure() {
// Do nothing (for now)
}
@Override
public void attach() {
// Do nothing
}
@Override
public void detach() {
// Do nothing
}
@Override
public void hblankStart(BufferedImage screen, int y, boolean isDirty) {
// Do nothing
}
}

View File

@ -0,0 +1,433 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e;
import jace.config.ConfigurableField;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.Video;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.util.HashSet;
import java.util.Set;
/**
* Provides a clean color monitor simulation, complete with text-friendly
* palette and mixed color/bw (mode 7) rendering. This class extends the
* VideoDHGR class to provide all necessary video writers and other rendering
* mechanics, and then overrides the actual output routines (showBW, showDhgr) with more suitable
* (and much prettier) alternatives. Rather than draw to the video buffer every
* cycle, rendered screen info is pushed into a buffer with mask bits (to
* indicate B&W vs color) And the actual conversion happens at the end of the
* scanline during the HBLANK period. This video rendering was inspired by
* Blargg but was ultimately rewritten from scratch once the color palette was
* implemented.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class VideoNTSC extends VideoDHGR {
@ConfigurableField(name = "Text palette", shortName = "textPalette", defaultValue = "false", description = "Use text-friendly color palette")
public static boolean useTextPalette = false;
static int activePalette[][];
@ConfigurableField(name = "Video 7", shortName = "video7", defaultValue = "true", description = "Enable Video 7 RGB rendering support")
public static boolean enableVideo7 = false;
// Scanline represents 560 bits, divided up into 28-bit words
int[] scanline = new int[20];
static int[] divBy28 = new int[560];
static {
for (int i = 0; i < 560; i++) {
divBy28[i] = i / 28;
}
}
int pos = 0;
int lastKnownY = -1;
boolean colorActive = false;
int rowStart = 0;
@Override
protected void showBW(BufferedImage screen, int xOffset, int y, int dhgrWord) {
if (lastKnownY != y) {
lastKnownY = y;
pos = rowStart = divBy28[xOffset];
colorActive = false;
} else {
if (pos > 20) pos-=20;
}
doDisplay(screen, xOffset, y, dhgrWord);
}
@Override
protected void showDhgr(BufferedImage screen, int xOffset, int y, int dhgrWord) {
if (lastKnownY != y) {
lastKnownY = y;
pos = rowStart = divBy28[xOffset];
colorActive = true;
}
doDisplay(screen, xOffset, y, dhgrWord);
}
@Override
protected void displayLores(BufferedImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
return;
}
if (lastKnownY != y) {
lastKnownY = y;
pos = rowStart = divBy28[xOffset];
colorActive = true;
}
int c1 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
} else {
c1 >>= 4;
}
int c2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1) & 0x0FF;
if ((y & 7) < 4) {
c2 &= 15;
} else {
c2 >>= 4;
}
int pat = c1 | c1 << 4 | c1 << 8 | (c1 & 3) << 12;
pat |= (c2 & 12) << 12 | c2 << 16 | c2 << 20 | c2 << 24;
scanline[pos++] = pat;
}
private void doDisplay(BufferedImage screen, int xOffset, int y, int dhgrWord) {
if (pos >= 20) pos -= 20;
scanline[pos] = dhgrWord;
pos++;
}
@Override
public void hblankStart(BufferedImage screen, int y, boolean isDirty) {
if (isDirty) {
renderScanline(screen, y);
}
lastKnownY = -1;
}
// Offset is based on location in graphics buffer that corresponds with the row and
// a number (0-20) that represents how much of the scanline was rendered
// This is based off the xyOffset but is different because of P
static int pyOffset[][];
static {
pyOffset = new int[192][21];
for (int y = 0; y < 192; y++) {
for (int p = 0; p < 21; p++) {
pyOffset[y][p] = (y * 560) + (p * 28);
}
}
}
private void renderScanline(BufferedImage screen, int y) {
DataBuffer b = screen.getRaster().getDataBuffer();
try {
// This is equivilant to y*560 but is 5% faster
//int yOffset = ((y << 4) + (y << 5) + (y << 9))+xOffset;
// For some reason this jumps up to 40 in the wayout title screen (?)
int p = pyOffset[y][rowStart];
if (rowStart > 0) {
getCurrentWriter().markDirty(y);
}
// Reset scanline position
if (colorActive && (!dhgrMode || !enableVideo7 || graphicsMode.isColor())) {
int byteCounter = 0;
for (int s = rowStart; s < 20; s++) {
int add = 0;
int bits;
if (hiresMode) {
bits = scanline[s] << 2;
if (s > 0) {
bits |= (scanline[s - 1] >> 26) & 3;
}
} else {
bits = scanline[s] << 3;
if (s > 0) {
bits |= (scanline[s - 1] >> 25) & 7;
}
}
if (s < 19) {
add = (scanline[s + 1] & 7);
}
boolean isBW = false;
if (enableVideo7 && dhgrMode && graphicsMode == rgbMode.mix) {
for (int i = 0; i < 28; i++) {
if (i % 7 == 0) {
isBW = !hiresMode && !useColor[byteCounter];
byteCounter++;
}
if (isBW) {
b.setElem(p++, ((bits & 0x8) == 0) ? BLACK : WHITE);
} else {
b.setElem(p++, activePalette[i % 4][bits & 0x07f]);
}
bits >>= 1;
if (i == 20) {
bits |= add << (hiresMode ? 9 : 10);
}
}
} else {
for (int i = 0; i < 28; i++) {
b.setElem(p++, activePalette[i % 4][bits & 0x07f]);
bits >>= 1;
if (i == 20) {
bits |= add << (hiresMode ? 9 : 10);
}
}
}
}
} else {
for (int s = rowStart; s < 20; s++) {
int bits = scanline[s];
for (int i = 0; i < 28; i++) {
b.setElem(p++, ((bits & 1) == 0) ? BLACK : WHITE);
bits >>= 1;
}
}
}
} catch (ArrayIndexOutOfBoundsException ex) {
// Flag this scanline to be written again, something screwed up!
// This only happens during a race condition when the video
// mode changes at just the wrong time.
getCurrentWriter().markDirty(y);
}
}
// y Range [0,1]
public static final double MIN_Y = 0;
public static final double MAX_Y = 1;
// i Range [-0.5957, 0.5957]
public static final double MAX_I = 0.5957;
// q Range [-0.5226, 0.5226]
public static final double MAX_Q = 0.5226;
static final int solidPalette[][] = new int[4][128];
static final int textPalette[][] = new int[4][128];
static final double[][] yiq = {
{0.0, 0.0, 0.0}, //0000 0
{0.25, 0.5, 0.5}, //0001 1
{0.25, -0.5, 0.5}, //0010 2
{0.5, 0.0, 1.0}, //0011 3 +Q
{0.25, -0.5, -0.5}, //0100 4
{0.5, 0.0, 0.0}, //0101 5
{0.5, -1.0, 0.0}, //0110 6 +I
{0.75, -0.5, 0.5}, //0111 7
{0.25, 0.5, -0.5}, //1000 8
{0.5, 1.0, 0.0}, //1001 9 -I
{0.5, 0.0, 0.0}, //1010 a
{0.75, 0.5, 0.5}, //1011 b
{0.5, 0.0, -1.0}, //1100 c -Q
{0.75, 0.5, -0.5}, //1101 d
{0.75, -0.5, -0.5}, //1110 e
{1.0, 0.0, 0.0}, //1111 f
};
static {
int maxLevel = 10;
for (int offset = 0; offset < 4; offset++) {
for (int pattern = 0; pattern < 128; pattern++) {
int level = (pattern & 1)
+ ((pattern >> 1) & 1) * 1
+ ((pattern >> 2) & 1) * 2
+ ((pattern >> 3) & 1) * 4
+ ((pattern >> 4) & 1) * 2
+ ((pattern >> 5) & 1) * 1;
int col = (pattern >> 2) & 0x0f;
for (int rot = 0; rot < offset; rot++) {
col = ((col & 8) >> 3) | ((col << 1) & 0x0f);
}
double y1 = yiq[col][0];
double y2 = ((double) level / (double) maxLevel);
solidPalette[offset][pattern] = (255 << 24) | yiqToRgb(y1, yiq[col][1] * MAX_I, yiq[col][2] * MAX_Q);
textPalette[offset][pattern] = (255 << 24) | yiqToRgb(y2, yiq[col][1] * MAX_I, yiq[col][2] * MAX_Q);
}
}
// Avoid NPE just in case.
activePalette = solidPalette;
}
static public int yiqToRgb(double y, double i, double q) {
int r = (int) (normalize((y + 0.956 * i + 0.621 * q), 0, 1) * 255);
int g = (int) (normalize((y - 0.272 * i - 0.647 * q), 0, 1) * 255);
int b = (int) (normalize((y - 1.105 * i + 1.702 * q), 0, 1) * 255);
return (r << 16) | (g << 8) | b;
}
public static double normalize(double x, double minX, double maxX) {
if (x < minX) {
return minX;
}
if (x > maxX) {
return maxX;
}
return x;
}
@Override
public void reconfigure() {
detach();
activePalette = useTextPalette ? textPalette : solidPalette;
super.reconfigure();
attach();
}
// The following section captures changes to the RGB mode
// The details of this are in Brodener's patent application #4631692
// http://www.freepatentsonline.com/4631692.pdf
// as well as the AppleColor adapter card manual
// http://apple2.info/download/Ext80ColumnAppleColorCardHR.pdf
rgbMode graphicsMode = rgbMode.color;
public static enum rgbMode {
color(true), mix(true), bw(false), _160col(false);
boolean colorMode = false;
rgbMode(boolean c) {
this.colorMode = c;
}
public boolean isColor() {
return colorMode;
}
}
public static enum ModeStateChanges {
SET_AN3, CLEAR_AN3, SET_80, CLEAR_80;
}
boolean f1 = true;
boolean f2 = true;
boolean an3 = true;
public void rgbStateChange(ModeStateChanges state) {
switch (state) {
case CLEAR_80:
break;
case CLEAR_AN3:
an3 = false;
break;
case SET_80:
break;
case SET_AN3:
if (!an3) {
f2 = f1;
f1 = SoftSwitches._80COL.getState();
}
an3 = true;
break;
}
// This is the more technically correct implementation except for two issues:
// 1) 160-column mode isn't implemented so it's not worth bothering to capture that state
// 2) A lot of programs are clueless about RGB modes so it's good to default to normal color mode
// graphicsMode = f1 ? (f2 ? rgbMode.color : rgbMode.mix) : (f2 ? rgbMode._160col : rgbMode.bw);
graphicsMode = f1 ? (f2 ? rgbMode.color : rgbMode.mix) : (f2 ? rgbMode.color : rgbMode.bw);
// System.out.println(state + ": "+ graphicsMode);
}
// These catch changes to the RGB mode to toggle between color, BW and mixed
static Set<RAMListener> rgbStateListeners = new HashSet<>();
static {
rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0c05e);
}
@Override
protected void doEvent(RAMEvent e) {
Video v = Computer.getComputer().getVideo();
if (v instanceof VideoNTSC) {
((VideoNTSC) v).rgbStateChange(ModeStateChanges.CLEAR_AN3);
}
}
});
rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0c05f);
}
@Override
protected void doEvent(RAMEvent e) {
Video v = Computer.getComputer().getVideo();
if (v instanceof VideoNTSC) {
((VideoNTSC) v).rgbStateChange(ModeStateChanges.SET_AN3);
}
}
});
rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0fa62);
}
@Override
protected void doEvent(RAMEvent e) {
Video v = Computer.getComputer().getVideo();
if (v instanceof VideoNTSC) {
// When reset hook is called, reset the graphics mode
// This is useful in case a program is running that
// is totally clueless how to set the RGB state correctly.
((VideoNTSC) v).f1 = true;
((VideoNTSC) v).f2 = true;
((VideoNTSC) v).an3 = false;
((VideoNTSC) v).graphicsMode = rgbMode.color;
}
}
});
rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0c00d);
}
@Override
protected void doEvent(RAMEvent e) {
Video v = Computer.getComputer().getVideo();
if (v instanceof VideoNTSC) {
((VideoNTSC) v).rgbStateChange(ModeStateChanges.SET_80);
}
}
});
}
@Override
public void detach() {
super.detach();
rgbStateListeners.stream().forEach((l) -> {
Computer.getComputer().getMemory().removeListener(l);
});
}
@Override
public void attach() {
super.attach();
rgbStateListeners.stream().forEach((l) -> {
Computer.getComputer().getMemory().addListener(l);
});
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e.softswitch;
import jace.apple2e.SoftSwitches;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.SoftSwitch;
/**
* Very funky softswitch which controls Slot 3 / C8 ROM behavior (and is the
* reason why some cards don't like Slot 3)
* Created on February 1, 2007, 9:40 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class IntC8SoftSwitch extends SoftSwitch {
/**
* Creates a new instance of IntC8SoftSwitch
*/
public IntC8SoftSwitch() {
super("InternalC8Rom", false);
// INTC8Rom should activate whenever C3xx memory is accessed and SLOTC3ROM is off
addListener(
new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0C300);
setScopeEnd(0x0C3FF);
}
@Override
protected void doEvent(RAMEvent e) {
if (SoftSwitches.SLOTC3ROM.isOff()) {
setState(true);
}
}
});
// INTCXRom shoud deactivate whenever CFFF is accessed
addListener(
new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
protected void doConfig() {
setScopeStart(0x0CFFF);
}
@Override
protected void doEvent(RAMEvent e) {
setState(false);
}
});
}
@Override
protected byte readSwitch() {
return 0;
}
@Override
public void stateChanged() {
if (Computer.getComputer().getMemory() != null) {
Computer.getComputer().getMemory().configureActiveMemory();
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e.softswitch;
import jace.core.Keyboard;
import jace.core.RAMEvent;
import jace.core.SoftSwitch;
/**
* Keyboard keypress strobe -- on = key pressed
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class KeyboardSoftSwitch extends SoftSwitch {
public KeyboardSoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) {
super(name,offAddress,onAddress,queryAddress,changeType,initalState);
}
public KeyboardSoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) {
super(name,offAddrs,onAddrs,queryAddrs,changeType,initalState);
}
public void stateChanged() {
Keyboard.clearStrobe();
}
/**
* return current keypress (if high bit set, strobe is not cleared)
* @return
*/
@Override
public byte readSwitch() {
return Keyboard.readState();
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e.softswitch;
import jace.core.RAMEvent.TYPE;
/**
* A softswitch that requires two consecutive accesses to flip
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Memory2SoftSwitch extends MemorySoftSwitch {
public Memory2SoftSwitch(String name, int offAddress, int onAddress, int queryAddress, TYPE changeType, Boolean initalState) {
super(name, offAddress, onAddress, queryAddress, changeType, initalState);
}
public Memory2SoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, TYPE changeType, Boolean initalState) {
super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState);
}
// The switch must be set true two times in a row before it will actually be set.
int count = 0;
@Override
public void setState(boolean newState) {
if (!newState) {
count = 0;
super.setState(newState);
} else {
count++;
if (count >= 2) {
super.setState(newState);
count = 0;
}
}
}
@Override
public String toString() {
return getName()+(getState()?":1":":0")+"~~"+count;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e.softswitch;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.SoftSwitch;
/**
* A memory softswitch is a softswitch which triggers a memory reconfiguration
* after its value is changed.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class MemorySoftSwitch extends SoftSwitch {
public MemorySoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) {
super(name, offAddress, onAddress, queryAddress, changeType, initalState);
}
public MemorySoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) {
super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState);
}
public void stateChanged() {
// System.out.println(getName()+ " was switched to "+getState());
if (Computer.getComputer().getMemory() != null) {
Computer.getComputer().getMemory().configureActiveMemory();
}
}
// Todo: Implement floating bus, maybe?
protected byte readSwitch() {
return (byte) (getState() ? 0x0A0 : 0x020);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.apple2e.softswitch;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.SoftSwitch;
/**
* A video softswitch is a softswitch which triggers a change in video mode when
* it is altered.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class VideoSoftSwitch extends SoftSwitch {
public VideoSoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) {
super(name, offAddress, onAddress, queryAddress, changeType, initalState);
}
public VideoSoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) {
super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState);
}
public void stateChanged() {
// System.out.println("Set "+getName()+" -> "+getState());
if (Computer.getComputer().getVideo() != null) {
Computer.getComputer().getVideo().configureVideoMode();
}
}
protected byte readSwitch() {
// System.out.println("Read "+getName()+" = "+getState());
return (byte) (getState() ? 0x080 : 0x000);
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.applesoft;
import java.util.ArrayList;
import java.util.List;
/**
* A command is a list of parts, either raw bytes (ascii text) or tokens. When
* put together they represent a single basic statement.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Command {
public static enum TOKEN {
END((byte) 0x080, "END"),
FOR((byte) 0x081, "FOR"),
NEXT((byte) 0x082, "NEXT"),
DATA((byte) 0x083, "DATA"),
INPUT((byte) 0x084, "INPUT"),
DEL((byte) 0x085, "DEL"),
DIM((byte) 0x086, "DIM"),
READ((byte) 0x087, "READ"),
GR((byte) 0x088, "GR"),
TEXT((byte) 0x089, "TEXT"),
PR((byte) 0x08A, "PR#"),
IN((byte) 0x08B, "IN#"),
CALL((byte) 0x08C, "CALL"),
PLOT((byte) 0x08D, "PLOT"),
HLIN((byte) 0x08E, "HLIN"),
VLIN((byte) 0x08F, "VLIN"),
HGR2((byte) 0x090, "HGR2"),
HGR((byte) 0x091, "HGR"),
HCOLOR((byte) 0x092, "HCOLOR="),
HPLOT((byte) 0x093, "HPLOT"),
DRAW((byte) 0x094, "DRAW"),
XDRAW((byte) 0x095, "XDRAW"),
HTAB((byte) 0x096, "HTAB"),
HOME((byte) 0x097, "HOME"),
ROT((byte) 0x098, "ROT="),
SCALE((byte) 0x099, "SCALE="),
SHLOAD((byte) 0x09A, "SHLOAD"),
TRACE((byte) 0x09B, "TRACE"),
NOTRACE((byte) 0x09C, "NOTRACE"),
NORMAL((byte) 0x09D, "NORMAL"),
INVERSE((byte) 0x09E, "INVERSE"),
FLASH((byte) 0x09F, "FLASH"),
COLOR((byte) 0x0A0, "COLOR="),
POP((byte) 0x0A1, "POP"),
VTAB((byte) 0x0A2, "VTAB"),
HIMEM((byte) 0x0A3, "HIMEM:"),
LOMEM((byte) 0x0A4, "LOMEM:"),
ONERR((byte) 0x0A5, "ONERR"),
RESUME((byte) 0x0A6, "RESUME"),
RECALL((byte) 0x0A7, "RECALL"),
STORE((byte) 0x0A8, "STORE"),
SPEED((byte) 0x0A9, "SPEED="),
LET((byte) 0x0AA, "LET"),
GOTO((byte) 0x0AB, "GOTO"),
RUN((byte) 0x0AC, "RUN"),
IF((byte) 0x0AD, "IF"),
RESTORE((byte) 0x0AE, "RESTORE"),
AMPERSAND((byte) 0x0AF, "&"),
GOSUB((byte) 0x0B0, "GOSUB"),
RETURN((byte) 0x0B1, "RETURN"),
REM((byte) 0x0B2, "REM"),
STOP((byte) 0x0B3, "STOP"),
ONGOTO((byte) 0x0B4, "ON"),
WAIT((byte) 0x0B5, "WAIT"),
LOAD((byte) 0x0B6, "LOAD"),
SAVE((byte) 0x0B7, "SAVE"),
DEF((byte) 0x0B8, "DEF"),
POKE((byte) 0x0B9, "POKE"),
PRINT((byte) 0x0BA, "PRINT"),
CONT((byte) 0x0BB, "CONT"),
LIST((byte) 0x0BC, "LIST"),
CLEAR((byte) 0x0BD, "CLEAR"),
GET((byte) 0x0BE, "GET"),
NEW((byte) 0x0BF, "NEW"),
TAB((byte) 0x0C0, "TAB("),
TO((byte) 0x0C1, "TO"),
FN((byte) 0x0C2, "FN"),
SPC((byte) 0x0c3, "SPC"),
THEN((byte) 0x0c4, "THEN"),
AT((byte) 0x0c5, "AT"),
NOT((byte) 0x0c6, "NOT"),
STEP((byte) 0x0c7, "STEP"),
PLUS((byte) 0x0c8, "+"),
MINUS((byte) 0x0c9, "-"),
MULTIPLY((byte) 0x0Ca, "*"),
DIVIDE((byte) 0x0Cb, "/"),
POWER((byte) 0x0Cc, "^"),
AND((byte) 0x0Cd, "AND"),
OR((byte) 0x0Ce, "OR"),
GREATER((byte) 0x0CF, ">"),
EQUAL((byte) 0x0d0, "="),
LESS((byte) 0x0d1, "<"),
SGN((byte) 0x0D2, "SGN"),
INT((byte) 0x0D3, "INT"),
ABS((byte) 0x0D4, "ABS"),
USR((byte) 0x0D5, "USR"),
FRE((byte) 0x0D6, "FRE"),
SCREEN((byte) 0x0D7, "SCRN("),
PDL((byte) 0x0D8, "PDL"),
POS((byte) 0x0D9, "POS"),
SQR((byte) 0x0DA, "SQR"),
RND((byte) 0x0DB, "RND"),
LOG((byte) 0x0DC, "LOG"),
EXP((byte) 0x0DD, "EXP"),
COS((byte) 0x0DE, "COS"),
SIN((byte) 0x0DF, "SIN"),
TAN((byte) 0x0E0, "TAN"),
ATN((byte) 0x0E1, "ATN"),
PEEK((byte) 0x0E2, "PEEK"),
LEN((byte) 0x0E3, "LEN"),
STR((byte) 0x0E4, "STR$"),
VAL((byte) 0x0E5, "VAL"),
ASC((byte) 0x0E6, "ASC"),
CHR((byte) 0x0E7, "CHR$"),
LEFT((byte) 0x0E8, "LEFT$"),
RIGHT((byte) 0x0E9, "RIGHT$"),
MID((byte) 0x0EA, "MID$");
private String str;
private byte b;
TOKEN(byte b, String str) {
this.b = b;
this.str = str;
}
@Override
public String toString() {
return str;
}
public static TOKEN fromByte(byte b) {
for (TOKEN t : values()) {
if (t.b == b) {
return t;
}
}
return null;
}
}
public static class ByteOrToken {
byte b;
TOKEN t;
boolean isToken = false;
public ByteOrToken(byte b) {
TOKEN t = TOKEN.fromByte(b);
if (t != null) {
isToken = true;
this.t = t;
} else {
isToken = false;
this.b = b;
}
}
@Override
public String toString() {
return isToken
? " " + t.toString() + " "
: String.valueOf((char) b);
}
}
List<ByteOrToken> parts = new ArrayList<ByteOrToken>();
@Override
public String toString() {
String out = "";
for (ByteOrToken p : parts) {
out += p.toString();
}
return out;
}
}

View File

@ -0,0 +1,141 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.applesoft;
import java.util.ArrayList;
import java.util.List;
/**
* Representation of a line of applesoft basic, having a line number and a list of program commands.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Line {
private static char STATEMENT_BREAK = ':'; // delimits multiple commands, the colon character
private int number;
private Line next;
private Line previous;
private List<Command> commands = new ArrayList<Command>();
private int length;
/**
* @return the number
*/
public int getNumber() {
return number;
}
/**
* @param number the number to set
*/
public void setNumber(int number) {
this.number = number;
}
/**
* @return the next
*/
public Line getNext() {
return next;
}
/**
* @param next the next to set
*/
public void setNext(Line next) {
this.next = next;
}
/**
* @return the previous
*/
public Line getPrevious() {
return previous;
}
/**
* @param previous the previous to set
*/
public void setPrevious(Line previous) {
this.previous = previous;
}
/**
* @return the commands
*/
public List<Command> getCommands() {
return commands;
}
/**
* @param commands the commands to set
*/
public void setCommands(List<Command> commands) {
this.commands = commands;
}
/**
* @return the length
*/
public int getLength() {
return length;
}
/**
* @param length the length to set
*/
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
String out = String.valueOf(getNumber());
boolean isFirst = true;
for (Command c : commands) {
if (!isFirst) out += STATEMENT_BREAK;
out += c.toString();
isFirst = false;
}
return out;
}
static Line fromBinary(byte[] binary, int pos) {
Line l = new Line();
int lineNumber = (binary[pos+2] & 0x0ff) + ((binary[pos+3] & 0x0ff) << 8);
l.setNumber(lineNumber);
pos += 4;
Command c = new Command();
int size = 5;
while (binary[pos] != 0) {
size++;
if (binary[pos] == STATEMENT_BREAK) {
l.commands.add(c);
c = new Command();
} else {
Command.ByteOrToken bt = new Command.ByteOrToken(binary[pos]);
c.parts.add(bt);
}
pos++;
}
l.commands.add(c);
l.length = size;
return l;
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.applesoft;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Decode an applesoft program into a list of program lines
* Right now this is an example/test program but it successfully tokenized the
* souce of Lemonade Stand.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Program {
List<Line> lines = new ArrayList<Line>();
int startingAddress = 0x0801;
public static void main(String... args) {
byte[] source = null;
try {
File f = new File("/home/brobert/Documents/Personal/a2gameserver/lib/data/games/LEMONADE#fc0801");
FileInputStream in = new FileInputStream(f);
source = new byte[(int) f.length()];
in.read(source);
} catch (FileNotFoundException ex) {
Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex);
}
Program test = Program.fromBinary(source);
System.out.println(test.toString());
}
static Program fromBinary(byte[] binary) {
return fromBinary(binary, 0x0801);
}
static Program fromBinary(byte[] binary, int startAddress) {
Program program = new Program();
int currentAddress = startAddress;
int pos = 0;
while (pos < binary.length) {
int nextAddress = (binary[pos] & 0x0ff) + ((binary[pos+1] & 0x0ff) << 8);
if (nextAddress == 0) break;
int length = nextAddress - currentAddress;
Line l = Line.fromBinary(binary, pos);
if (l == null) break;
program.lines.add(l);
if (l.getLength() != length) {
System.out.println("Line "+l.getNumber()+" parsed as "+l.getLength()+" bytes long, but that leaves "+
(length - l.getLength())+" bytes hidden behind next line");
}
pos += length;
currentAddress = nextAddress;
}
return program;
}
@Override
public String toString() {
String out = "";
for (Line l : lines)
out += l.toString() + "\n";
return out;
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.cheat;
import jace.core.Computer;
import jace.core.Device;
import jace.core.RAMListener;
import java.util.HashSet;
import java.util.Set;
/**
* Represents some combination of hacks that can be enabled or disabled
* through the configuration interface.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class Cheats extends Device {
Set<RAMListener> listeners = new HashSet<RAMListener>();
public void addCheat(RAMListener l) {
listeners.add(l);
Computer.getComputer().getMemory().addListener(l);
}
@Override
public void detach() {
for (RAMListener l : listeners) {
Computer.getComputer().getMemory().removeListener(l);
}
listeners.clear();
}
@Override
public void reconfigure() {
detach();
attach();
}
public String getShortName() {
return "cheat";
}
}

View File

@ -0,0 +1,295 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.3" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo">
<NonVisualComponents>
<Component class="javax.swing.ButtonGroup" name="mapType">
</Component>
</NonVisualComponents>
<Properties>
<Property name="defaultCloseOperation" type="int" value="3"/>
</Properties>
<SyntheticProperties>
<SyntheticProperty name="formSizePolicy" type="int" value="1"/>
<SyntheticProperty name="generateCenter" type="boolean" value="false"/>
</SyntheticProperties>
<Events>
<EventHandler event="componentResized" listener="java.awt.event.ComponentListener" parameters="java.awt.event.ComponentEvent" handler="formComponentResized"/>
<EventHandler event="componentShown" listener="java.awt.event.ComponentListener" parameters="java.awt.event.ComponentEvent" handler="formComponentShown"/>
<EventHandler event="componentHidden" listener="java.awt.event.ComponentListener" parameters="java.awt.event.ComponentEvent" handler="formComponentHidden"/>
<EventHandler event="ancestorResized" listener="java.awt.event.HierarchyBoundsListener" parameters="java.awt.event.HierarchyEvent" handler="formAncestorResized"/>
</Events>
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
</AuxValues>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="viewScroll" pref="633" max="32767" attributes="0"/>
<Component id="controlPanel" alignment="0" max="32767" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="viewScroll" pref="492" max="32767" attributes="0"/>
<EmptySpace type="unrelated" max="-2" attributes="0"/>
<Component id="controlPanel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Container class="javax.swing.JScrollPane" name="viewScroll">
<Properties>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[640, 480]"/>
</Property>
</Properties>
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
<SubComponents>
<Container class="jace.cheat.MemorySpyGrid" name="memorySpyGrid">
<Properties>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[580, 480]"/>
</Property>
</Properties>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<EmptySpace min="0" pref="646" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<EmptySpace min="0" pref="490" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
</Layout>
</Container>
</SubComponents>
</Container>
<Container class="javax.swing.JPanel" name="controlPanel">
<Properties>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="org.netbeans.modules.form.compat2.border.LineBorderInfo">
<LineBorder roundedCorners="true"/>
</Border>
</Property>
</Properties>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace min="-2" pref="19" max="-2" attributes="0"/>
<Component id="showLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="mapActivityMode" min="-2" max="-2" attributes="0"/>
<Component id="fastFadeCheckbox" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" max="-2" attributes="0">
<Group type="102" attributes="0">
<Component id="mapValueMode" min="-2" max="-2" attributes="0"/>
<EmptySpace max="32767" attributes="0"/>
<Component id="zoomLabel" min="-2" max="-2" attributes="0"/>
</Group>
<Component id="useColorCheckbox" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="103" groupAlignment="0" max="-2" attributes="0">
<Group type="102" attributes="0">
<EmptySpace min="-2" pref="144" max="-2" attributes="0"/>
<Component id="endLabel" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="102" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="zoomAmount" min="-2" pref="107" max="-2" attributes="0"/>
<EmptySpace max="32767" attributes="0"/>
<Component id="startLabel" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" max="-2" attributes="0">
<Component id="endAddress" pref="60" max="32767" attributes="0"/>
<Component id="startAddress" max="32767" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Component id="updateAddressButton" min="-2" max="-2" attributes="0"/>
<EmptySpace pref="28" max="32767" attributes="0"/>
</Group>
<Group type="102" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="statusLabel" max="32767" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Component id="zoomAmount" alignment="0" pref="0" max="32767" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="startLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="startAddress" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="endAddress" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="endLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="updateAddressButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="useColorCheckbox" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="fastFadeCheckbox" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<Group type="103" groupAlignment="1" attributes="0">
<Group type="103" alignment="1" groupAlignment="3" attributes="0">
<Component id="mapActivityMode" alignment="3" min="-2" pref="22" max="-2" attributes="0"/>
<Component id="showLabel" alignment="3" max="-2" attributes="0"/>
</Group>
<Group type="103" alignment="0" groupAlignment="3" attributes="0">
<Component id="zoomLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="mapValueMode" alignment="3" max="-2" attributes="0"/>
</Group>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Component id="statusLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Component class="javax.swing.JLabel" name="showLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Show"/>
</Properties>
</Component>
<Component class="javax.swing.JSlider" name="zoomAmount">
<Properties>
<Property name="maximum" type="int" value="15"/>
<Property name="minimum" type="int" value="1"/>
<Property name="paintTicks" type="boolean" value="true"/>
<Property name="snapToTicks" type="boolean" value="true"/>
<Property name="value" type="int" value="5"/>
</Properties>
<Events>
<EventHandler event="stateChanged" listener="javax.swing.event.ChangeListener" parameters="javax.swing.event.ChangeEvent" handler="zoomAmountStateChanged"/>
</Events>
</Component>
<Component class="javax.swing.JRadioButton" name="mapActivityMode">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="mapType"/>
</Property>
<Property name="selected" type="boolean" value="true"/>
<Property name="text" type="java.lang.String" value="Activity"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="mapActivityModeActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JRadioButton" name="mapValueMode">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="mapType"/>
</Property>
<Property name="text" type="java.lang.String" value="Value"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="mapValueModeActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="zoomLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Zoom"/>
</Properties>
</Component>
<Component class="javax.swing.JTextField" name="startAddress">
<Properties>
<Property name="text" type="java.lang.String" value="$0000"/>
</Properties>
</Component>
<Component class="javax.swing.JLabel" name="startLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Start"/>
</Properties>
</Component>
<Component class="javax.swing.JTextField" name="endAddress">
<Properties>
<Property name="text" type="java.lang.String" value="$FFFF"/>
</Properties>
</Component>
<Component class="javax.swing.JLabel" name="statusLabel">
<Properties>
<Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor">
<Font name="Courier New" size="14" style="0"/>
</Property>
<Property name="text" type="java.lang.String" value=" "/>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="org.netbeans.modules.form.compat2.border.EtchedBorderInfo">
<EtchetBorder bevelType="0"/>
</Border>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/>
</AuxValues>
</Component>
<Component class="javax.swing.JLabel" name="endLabel">
<Properties>
<Property name="text" type="java.lang.String" value="End"/>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="updateAddressButton">
<Properties>
<Property name="text" type="java.lang.String" value="Update"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="updateAddressButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JCheckBox" name="useColorCheckbox">
<Properties>
<Property name="text" type="java.lang.String" value="Color Gradient"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="useColorCheckboxActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JCheckBox" name="fastFadeCheckbox">
<Properties>
<Property name="text" type="java.lang.String" value="Fast Fade"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="fastFadeCheckboxActionPerformed"/>
</Events>
</Component>
</SubComponents>
</Container>
</SubComponents>
</Form>

View File

@ -0,0 +1,395 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.cheat;
import jace.core.Utility;
import static jace.core.Utility.gripe;
/**
* This represents the window of the memory spy module. The memory view itself
* is implemented in MemorySpyGrid.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class MemorySpy extends javax.swing.JFrame {
static MemorySpy singleton = null;
/**
* Creates new form MemorySpy
*/
public MemorySpy() {
initComponents();
setDefaultCloseOperation(HIDE_ON_CLOSE);
singleton = this;
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
mapType = new javax.swing.ButtonGroup();
viewScroll = new javax.swing.JScrollPane();
memorySpyGrid = new jace.cheat.MemorySpyGrid();
controlPanel = new javax.swing.JPanel();
showLabel = new javax.swing.JLabel();
zoomAmount = new javax.swing.JSlider();
mapActivityMode = new javax.swing.JRadioButton();
mapValueMode = new javax.swing.JRadioButton();
zoomLabel = new javax.swing.JLabel();
startAddress = new javax.swing.JTextField();
startLabel = new javax.swing.JLabel();
endAddress = new javax.swing.JTextField();
statusLabel = new javax.swing.JLabel();
endLabel = new javax.swing.JLabel();
updateAddressButton = new javax.swing.JButton();
useColorCheckbox = new javax.swing.JCheckBox();
fastFadeCheckbox = new javax.swing.JCheckBox();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
addComponentListener(new java.awt.event.ComponentAdapter() {
public void componentResized(java.awt.event.ComponentEvent evt) {
formComponentResized(evt);
}
public void componentShown(java.awt.event.ComponentEvent evt) {
formComponentShown(evt);
}
public void componentHidden(java.awt.event.ComponentEvent evt) {
formComponentHidden(evt);
}
});
addHierarchyBoundsListener(new java.awt.event.HierarchyBoundsListener() {
public void ancestorMoved(java.awt.event.HierarchyEvent evt) {
}
public void ancestorResized(java.awt.event.HierarchyEvent evt) {
formAncestorResized(evt);
}
});
viewScroll.setPreferredSize(new java.awt.Dimension(640, 480));
memorySpyGrid.setPreferredSize(new java.awt.Dimension(580, 480));
javax.swing.GroupLayout memorySpyGridLayout = new javax.swing.GroupLayout(memorySpyGrid);
memorySpyGrid.setLayout(memorySpyGridLayout);
memorySpyGridLayout.setHorizontalGroup(
memorySpyGridLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 631, Short.MAX_VALUE)
);
memorySpyGridLayout.setVerticalGroup(
memorySpyGridLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 490, Short.MAX_VALUE)
);
viewScroll.setViewportView(memorySpyGrid);
controlPanel.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(0, 0, 0), 1, true));
showLabel.setText("Show");
zoomAmount.setMaximum(15);
zoomAmount.setMinimum(1);
zoomAmount.setPaintTicks(true);
zoomAmount.setSnapToTicks(true);
zoomAmount.setValue(5);
zoomAmount.addChangeListener(new javax.swing.event.ChangeListener() {
public void stateChanged(javax.swing.event.ChangeEvent evt) {
zoomAmountStateChanged(evt);
}
});
mapType.add(mapActivityMode);
mapActivityMode.setSelected(true);
mapActivityMode.setText("Activity");
mapActivityMode.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
mapActivityModeActionPerformed(evt);
}
});
mapType.add(mapValueMode);
mapValueMode.setText("Value");
mapValueMode.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
mapValueModeActionPerformed(evt);
}
});
zoomLabel.setText("Zoom");
startAddress.setText("$0000");
startLabel.setText("Start");
endAddress.setText("$FFFF");
statusLabel.setFont(new java.awt.Font("Courier New", 0, 14)); // NOI18N
statusLabel.setText(" ");
statusLabel.setBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED));
endLabel.setText("End");
updateAddressButton.setText("Update");
updateAddressButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
updateAddressButtonActionPerformed(evt);
}
});
useColorCheckbox.setText("Color Gradient");
useColorCheckbox.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
useColorCheckboxActionPerformed(evt);
}
});
fastFadeCheckbox.setText("Fast Fade");
fastFadeCheckbox.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
fastFadeCheckboxActionPerformed(evt);
}
});
javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel);
controlPanel.setLayout(controlPanelLayout);
controlPanelLayout.setHorizontalGroup(
controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(controlPanelLayout.createSequentialGroup()
.addGap(19, 19, 19)
.addComponent(showLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(mapActivityMode)
.addComponent(fastFadeCheckbox))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addGroup(controlPanelLayout.createSequentialGroup()
.addComponent(mapValueMode)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(zoomLabel))
.addComponent(useColorCheckbox))
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addGroup(controlPanelLayout.createSequentialGroup()
.addGap(144, 144, 144)
.addComponent(endLabel))
.addGroup(controlPanelLayout.createSequentialGroup()
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(zoomAmount, javax.swing.GroupLayout.PREFERRED_SIZE, 107, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(startLabel)))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addComponent(endAddress, javax.swing.GroupLayout.DEFAULT_SIZE, 60, Short.MAX_VALUE)
.addComponent(startAddress))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(updateAddressButton)
.addContainerGap(28, Short.MAX_VALUE))
.addGroup(controlPanelLayout.createSequentialGroup()
.addContainerGap()
.addComponent(statusLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addContainerGap())
);
controlPanelLayout.setVerticalGroup(
controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(controlPanelLayout.createSequentialGroup()
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(controlPanelLayout.createSequentialGroup()
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(zoomAmount, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(startLabel)
.addComponent(startAddress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(endAddress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(endLabel)
.addComponent(updateAddressButton)
.addComponent(useColorCheckbox)
.addComponent(fastFadeCheckbox)))
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(mapActivityMode, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(showLabel))
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(zoomLabel)
.addComponent(mapValueMode))))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(statusLabel)
.addContainerGap())
);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(viewScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 633, Short.MAX_VALUE)
.addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(viewScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 492, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap())
);
pack();
}// </editor-fold>//GEN-END:initComponents
private void mapActivityModeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mapActivityModeActionPerformed
memorySpyGrid.setDisplayMode(MemorySpyGrid.Modes.ACTIVITY);
}//GEN-LAST:event_mapActivityModeActionPerformed
private void mapValueModeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mapValueModeActionPerformed
memorySpyGrid.setDisplayMode(MemorySpyGrid.Modes.VALUE);
}//GEN-LAST:event_mapValueModeActionPerformed
private void zoomAmountStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_zoomAmountStateChanged
if (!zoomAmount.getValueIsAdjusting()) {
memorySpyGrid.setZoomAmount(zoomAmount.getValue());
}
}//GEN-LAST:event_zoomAmountStateChanged
private void updateAddressButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_updateAddressButtonActionPerformed
int startAddr = Utility.parseHexInt(startAddress.getText());
int endAddr = Utility.parseHexInt(endAddress.getText());
if (startAddr < 0 || startAddr > 0x0ffff) {
gripe("Start address is out of range!");
return;
}
if (endAddr < 0 || endAddr > 0x0ffff) {
gripe("End address is out of range!");
return;
}
if (startAddr > endAddr) {
gripe("Start address must be smaller than end address");
return;
}
memorySpyGrid.updateRange(startAddr, endAddr);
}//GEN-LAST:event_updateAddressButtonActionPerformed
@Override
public void validate() {
super.validate();
memorySpyGrid.adjustSize();
}
private void formComponentHidden(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentHidden
memorySpyGrid.stopDisplay();
}//GEN-LAST:event_formComponentHidden
private void formComponentShown(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentShown
memorySpyGrid.startDisplay();
}//GEN-LAST:event_formComponentShown
long lastRefresh = -1L;
long refreshDelay = 1000L;
private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized
}//GEN-LAST:event_formComponentResized
private void formAncestorResized(java.awt.event.HierarchyEvent evt) {//GEN-FIRST:event_formAncestorResized
// java.awt.EventQueue.invokeLater(new Runnable() {
// @Override
// public void run() {
// lastRefresh = System.currentTimeMillis() + refreshDelay + 10;
// try {
// Thread.sleep(refreshDelay);
// } catch (InterruptedException ex) {
// Logger.getLogger(MemorySpy.class.getName()).log(Level.SEVERE, null, ex);
// }
// if (System.currentTimeMillis() >= lastRefresh) {
// int newWidth = getParent().getWidth();
// }
// }
// });
// memorySpyGrid.adjustSize();
}//GEN-LAST:event_formAncestorResized
private void useColorCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_useColorCheckboxActionPerformed
memorySpyGrid.setColorGradient(useColorCheckbox.isSelected());
}//GEN-LAST:event_useColorCheckboxActionPerformed
private void fastFadeCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fastFadeCheckboxActionPerformed
memorySpyGrid.setFastFade(fastFadeCheckbox.isSelected());
}//GEN-LAST:event_fastFadeCheckboxActionPerformed
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
/* Set the Nimbus look and feel */
//<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
/* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
* For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new MemorySpy().setVisible(true);
}
});
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JPanel controlPanel;
private javax.swing.JTextField endAddress;
private javax.swing.JLabel endLabel;
private javax.swing.JCheckBox fastFadeCheckbox;
private javax.swing.JRadioButton mapActivityMode;
private javax.swing.ButtonGroup mapType;
private javax.swing.JRadioButton mapValueMode;
private jace.cheat.MemorySpyGrid memorySpyGrid;
private javax.swing.JLabel showLabel;
private javax.swing.JTextField startAddress;
private javax.swing.JLabel startLabel;
public javax.swing.JLabel statusLabel;
private javax.swing.JButton updateAddressButton;
private javax.swing.JCheckBox useColorCheckbox;
private javax.swing.JScrollPane viewScroll;
private javax.swing.JSlider zoomAmount;
private javax.swing.JLabel zoomLabel;
// End of variables declaration//GEN-END:variables
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.3" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
<Properties>
<Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor">
<Color blue="1" green="1" red="1" type="rgb"/>
</Property>
<Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor">
<Color id="Crosshair Cursor"/>
</Property>
</Properties>
<Events>
<EventHandler event="mouseClicked" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="formMouseClicked"/>
<EventHandler event="mouseExited" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="formMouseExited"/>
<EventHandler event="mouseEntered" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="formMouseEntered"/>
<EventHandler event="mouseMoved" listener="java.awt.event.MouseMotionListener" parameters="java.awt.event.MouseEvent" handler="formMouseMoved"/>
</Events>
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
</AuxValues>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<EmptySpace min="0" pref="400" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<EmptySpace min="0" pref="300" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
</Layout>
</Form>

View File

@ -0,0 +1,489 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.cheat;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.RAMListener;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JLabel;
import javax.swing.JViewport;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* This is the memory view of the memory spy module. The window is defined by
* the MemorySpy class. This class registers memory listeners needed to provide
* the relevant memory views.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class MemorySpyGrid extends javax.swing.JPanel {
/**
* Creates new form MemorySpyGrid
*/
public MemorySpyGrid() {
initComponents();
gradient = bwGradient;
}
// This is used primarily during scroll events to repaint the whole area
ChangeListener viewportChangeListener = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
repaint();
}
};
@Override
public void validate() {
super.validate();
if (Computer.getComputer() != null) {
adjustSize();
}
if (getParent() != null) {
JViewport viewport = (JViewport) getParent();
viewport.removeChangeListener(viewportChangeListener);
viewport.addChangeListener(viewportChangeListener);
}
}
@Override
public void paint(Graphics g) {
super.paint(g);
if (mode == Modes.ACTIVITY) {
for (int a = startAddress; a <= endAddress; a++) {
if (!activeRam.contains(a)) {
drawActivity(a, 0, 0, 0, (Graphics2D) g);
} else {
drawActivity(a,
getInt(writeActivity.get(a)),
getInt(readActivity.get(a)),
getInt(pcActivity.get(a)),
(Graphics2D) g);
}
}
} else {
RAM ram = Computer.getComputer().getMemory();
for (int a = startAddress; a <= endAddress; a++) {
drawValue(a, ram.readRaw(a) & 0x0ff, (Graphics2D) g);
}
}
}
Color[] gradient;
static Color[] colorGradient, bwGradient;
static {
colorGradient = new Color[256];
bwGradient = new Color[256];
for (int i = 0; i < 256; i++) {
float hue = ((float) i) / 360.0f;
colorGradient[i] = Color.getHSBColor(0.67f - hue, 1, 1);
bwGradient[i] = new Color(i, i, i);
}
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
setBackground(new java.awt.Color(1, 1, 1));
setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
formMouseClicked(evt);
}
public void mouseExited(java.awt.event.MouseEvent evt) {
formMouseExited(evt);
}
public void mouseEntered(java.awt.event.MouseEvent evt) {
formMouseEntered(evt);
}
});
addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
public void mouseMoved(java.awt.event.MouseEvent evt) {
formMouseMoved(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 400, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 300, Short.MAX_VALUE)
);
}// </editor-fold>//GEN-END:initComponents
boolean mousePresent = false;
int mouseX;
int mouseY;
private void formMouseMoved(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseMoved
mouseX = evt.getX();
mouseY = evt.getY();
}//GEN-LAST:event_formMouseMoved
private void formMouseEntered(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseEntered
mousePresent = true;
}//GEN-LAST:event_formMouseEntered
private void formMouseExited(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseExited
mousePresent = false;
}//GEN-LAST:event_formMouseExited
private void formMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseClicked
int addr = convertXYtoAddr(mouseX, mouseY);
if (addr >= 0) {
MetaCheats.singleton.addWatches(addr, addr);
}
}//GEN-LAST:event_formMouseClicked
private int convertXYtoAddr(int x, int y) {
int offset = x / zoomAmount;
if (offset < 0 || offset > columns) {
return -1;
}
int row = y / zoomAmount;
int addr = startAddress + (row * columns) + offset;
if (addr > endAddress) {
return -1;
}
return addr;
}
private String spaceFill(String s, int size) {
while (s.length() < size) {
s = s + " ";
}
return s;
}
private void updateDetails() {
final JLabel status = MemorySpy.singleton.statusLabel;
if (mousePresent) {
final int addr = convertXYtoAddr(mouseX, mouseY);
if (addr >= 0) {
final int value = Computer.getComputer().getMemory().readRaw(addr) & 0x0ff;
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
Integer lastChange = setBy.get(addr);
Integer lastRead = readBy.get(addr);
char c1 = (char) Math.max(32, value & 0x07f);
char c2 = (char) (64 + (value % 32));
char c3 = (char) (32 + (value % 96));
status.setText("$"
+ spaceFill(Integer.toHexString(addr), 4) + ": "
+ spaceFill(String.valueOf(value), 3) + " ($"
+ spaceFill(Integer.toHexString(value), 2) + ") " + c1 + " " + c2 + " " + c3
+ (lastChange != null
? " Written by $"
+ spaceFill(Integer.toHexString(lastChange), 4)
: "")
+ (lastRead != null
? " Read by $"
+ spaceFill(Integer.toHexString(lastRead), 4)
: ""));
}
});
}
} else {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
status.setText("Nothing selected");
}
});
}
}
// Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables
Modes mode = Modes.ACTIVITY;
void setDisplayMode(Modes newMode) {
stopDisplay();
mode = newMode;
repaint();
startDisplay();
}
int zoomAmount = 5;
void setZoomAmount(int value) {
zoomAmount = value;
recalibrate();
}
int startAddress = 0;
int endAddress = 0x0ffff;
void updateRange(int startAddr, int endAddr) {
startAddress = startAddr;
endAddress = endAddr;
activeRam.clear();
valueActivity.clear();
readActivity.clear();
writeActivity.clear();
pcActivity.clear();
recalibrate();
}
int columns = 1;
int lastKnownWidth = 0;
int lastKnownHeight = 0;
public void adjustSize() {
int newWidth = getParent().getWidth();
if (newWidth == lastKnownWidth && getParent().getHeight() == lastKnownHeight) {
return;
}
lastKnownWidth = newWidth;
lastKnownHeight = getParent().getHeight();
recalibrate();
}
public void recalibrate() {
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
stopDisplay();
int size = endAddress - startAddress + 1;
int width = getParent().getWidth();
columns = ((width / zoomAmount) / 16) * 16;
int height = ((size / columns) + 1) * zoomAmount;
setSize(width, height);
setPreferredSize(new Dimension(width, height));
getParent().validate();
repaint();
startDisplay();
}
});
}
protected void drawValue(int addr, int value, Graphics2D g) {
int offset = addr - startAddress;
int x = zoomAmount * (offset % columns);
int y = zoomAmount * (offset / columns);
value = value & 0x0ff;
g.setColor(gradient[value]);
g.fillRect(x, y, zoomAmount, zoomAmount);
}
protected void drawActivity(int addr, int read, int write, int pc, Graphics2D g) {
int offset = addr - startAddress;
int x = zoomAmount * (offset % columns);
int y = zoomAmount * (offset / columns);
g.setColor(new Color(read & 0x0ff, write & 0x0ff, pc & 0x0ff));
g.fillRect(x, y, zoomAmount, zoomAmount);
}
private int getInt(Integer i) {
if (i != null) {
return i;
}
return 0;
}
boolean isDisplayActive = false;
Thread redrawThread = null;
// 30 fps or so
static int UPDATE_INTERVAL = 10;
static int ACTIVITY_INCREMENT = 150;
static int ACTIVITY_DECREMENT = 5;
static int ACTIVITY_MAX = 255;
RAMListener memoryListener = null;
Map<Integer, Integer> readActivity = new ConcurrentHashMap<Integer, Integer>();
Map<Integer, Integer> writeActivity = new ConcurrentHashMap<Integer, Integer>();
Map<Integer, Integer> pcActivity = new ConcurrentHashMap<Integer, Integer>();
Set<Integer> activeRam = new ConcurrentSkipListSet<Integer>();
Map<Integer, Integer> valueActivity = new ConcurrentHashMap<Integer, Integer>();
Map<Integer, Integer> setBy = new ConcurrentHashMap<Integer, Integer>();
Map<Integer, Integer> readBy = new ConcurrentHashMap<Integer, Integer>();
void startDisplay() {
if (memoryListener != null) {
Computer.getComputer().getMemory().removeListener(memoryListener);
memoryListener = null;
}
isDisplayActive = true;
memoryListener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(startAddress);
setScopeEnd(endAddress);
}
@Override
protected void doEvent(RAMEvent e) {
int addr = e.getAddress();
if (addr < startAddress || addr > endAddress) {
return;
}
int pc = Computer.getComputer().getCpu().programCounter;
if (e.getType() == TYPE.EXECUTE || e.getType() == TYPE.READ_OPERAND) {
if (pcActivity.containsKey(addr)) {
pcActivity.put(addr, Math.min(ACTIVITY_MAX, getInt(pcActivity.get(addr)) + ACTIVITY_INCREMENT));
} else {
pcActivity.put(addr, ACTIVITY_INCREMENT);
}
} else if (e.getType().isRead()) {
if (readActivity.containsKey(addr)) {
readActivity.put(addr, Math.min(ACTIVITY_MAX, getInt(readActivity.get(addr)) + ACTIVITY_INCREMENT));
} else {
readActivity.put(addr, ACTIVITY_INCREMENT);
}
readBy.put(addr, pc);
} else {
if (writeActivity.containsKey(addr)) {
writeActivity.put(addr, Math.min(ACTIVITY_MAX, getInt(writeActivity.get(addr)) + ACTIVITY_INCREMENT));
} else {
writeActivity.put(addr, ACTIVITY_INCREMENT);
}
valueActivity.put(addr, e.getNewValue());
setBy.put(addr, pc);
}
activeRam.add(addr);
}
};
Computer.getComputer().getMemory().addListener(memoryListener);
redrawThread = new Thread(new Runnable() {
@Override
public void run() {
if (mode == Modes.ACTIVITY) {
// Activity heatmap mode
while (isDisplayActive) {
updateDetails();
Graphics2D g = (Graphics2D) getGraphics();
for (Iterator<Integer> i = activeRam.iterator(); i.hasNext();) {
boolean remove = true;
int addr = i.next();
int read = getInt(readActivity.get(addr));
if (read <= 0) {
read = 0;
} else {
remove = false;
readActivity.put(addr, read - ACTIVITY_DECREMENT);
}
int write = getInt(writeActivity.get(addr));
if (write <= 0) {
write = 0;
} else {
remove = false;
writeActivity.put(addr, write - ACTIVITY_DECREMENT);
}
int pc = getInt(pcActivity.get(addr));
if (pc <= 0) {
pc = 0;
} else {
remove = false;
pcActivity.put(addr, pc - ACTIVITY_DECREMENT);
}
if (remove) {
i.remove();
pcActivity.remove(addr);
writeActivity.remove(addr);
readActivity.remove(addr);
}
drawActivity(addr, write, read, pc, g);
}
g.dispose();
try {
Thread.sleep(UPDATE_INTERVAL);
} catch (InterruptedException ex) {
Logger.getLogger(MemorySpyGrid.class.getName()).log(Level.SEVERE, null, ex);
}
}
} else {
// Redraw value, no activity counts needed
while (isDisplayActive) {
updateDetails();
Graphics2D g = (Graphics2D) getGraphics();
for (Iterator<Integer> i = valueActivity.keySet().iterator(); i.hasNext();) {
int addr = i.next();
int value = getInt(valueActivity.get(addr));
i.remove();
drawValue(addr, value, g);
}
g.dispose();
try {
Thread.sleep(UPDATE_INTERVAL);
} catch (InterruptedException ex) {
Logger.getLogger(MemorySpyGrid.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
});
redrawThread.setName("Memory spy redraw");
redrawThread.start();
}
void stopDisplay() {
isDisplayActive = false;
if (memoryListener != null) {
Computer.getComputer().getMemory().removeListener(memoryListener);
memoryListener = null;
}
if (redrawThread != null && redrawThread.isAlive()) {
try {
redrawThread.join();
} catch (InterruptedException ex) {
Logger.getLogger(MemorySpyGrid.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
void setColorGradient(boolean useColor) {
if (useColor) {
gradient = colorGradient;
} else {
gradient = bwGradient;
}
repaint();
}
void setFastFade(boolean fast) {
if (fast) {
ACTIVITY_DECREMENT = 20;
} else {
ACTIVITY_DECREMENT = 5;
}
}
public static enum Modes {
ACTIVITY, VALUE
}
}

View File

@ -0,0 +1,516 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo">
<NonVisualComponents>
<Component class="javax.swing.ButtonGroup" name="searchTypes">
</Component>
<Component class="javax.swing.ButtonGroup" name="valueTypes">
</Component>
</NonVisualComponents>
<Properties>
<Property name="defaultCloseOperation" type="int" value="3"/>
</Properties>
<SyntheticProperties>
<SyntheticProperty name="formSizePolicy" type="int" value="1"/>
<SyntheticProperty name="generateCenter" type="boolean" value="false"/>
</SyntheticProperties>
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="1"/>
</AuxValues>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="tabs" max="32767" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="tabs" max="32767" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Container class="javax.swing.JTabbedPane" name="tabs">
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout"/>
<SubComponents>
<Container class="javax.swing.JPanel" name="cheatPanel">
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout$JTabbedPaneConstraintsDescription">
<JTabbedPaneConstraints tabName="Cheats">
<Property name="tabTitle" type="java.lang.String" value="Cheats"/>
</JTabbedPaneConstraints>
</Constraint>
</Constraints>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="activeCheatsScroll" pref="586" max="32767" attributes="0"/>
<Component id="jSeparator1" alignment="0" max="32767" attributes="0"/>
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Component id="activeCheatsLabel" min="-2" max="-2" attributes="0"/>
<Component id="addNewCheatLabel" alignment="0" min="-2" max="-2" attributes="0"/>
<Group type="102" alignment="0" attributes="0">
<Component id="addByteValueButton" min="-2" max="-2" attributes="0"/>
<EmptySpace type="separate" max="-2" attributes="0"/>
<Component id="addWordValueButton" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Component id="addValueLabel" min="-2" pref="56" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="addValueField" min="-2" pref="79" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Component id="addAddressLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="addAddressField" min="-2" pref="79" max="-2" attributes="0"/>
</Group>
</Group>
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Component id="disableCheatButton" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="enableCheatButton" min="-2" max="-2" attributes="0"/>
<EmptySpace max="32767" attributes="0"/>
<Component id="removeSelectedButton" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="activeCheatsLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="activeCheatsScroll" pref="118" max="32767" attributes="0"/>
<EmptySpace type="unrelated" max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="removeSelectedButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="disableCheatButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="enableCheatButton" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Component id="jSeparator1" min="-2" pref="10" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="addNewCheatLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="addAddressLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="addAddressField" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="addValueLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="addValueField" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="addByteValueButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="addWordValueButton" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Component class="javax.swing.JLabel" name="activeCheatsLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Active Cheats"/>
</Properties>
</Component>
<Container class="javax.swing.JScrollPane" name="activeCheatsScroll">
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
<SubComponents>
<Component class="javax.swing.JTable" name="activeCheatsTable">
<Properties>
<Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.editors2.TableModelEditor">
<Table columnCount="2" rowCount="0">
<Column editable="false" title="Address" type="java.lang.String"/>
<Column editable="false" title="Value" type="java.lang.String"/>
</Table>
</Property>
</Properties>
</Component>
</SubComponents>
</Container>
<Component class="javax.swing.JButton" name="removeSelectedButton">
<Properties>
<Property name="text" type="java.lang.String" value="Remove Selected"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="removeSelectedButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="addNewCheatLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Add a new cheat:"/>
</Properties>
</Component>
<Component class="javax.swing.JSeparator" name="jSeparator1">
</Component>
<Component class="javax.swing.JLabel" name="addValueLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Value"/>
</Properties>
</Component>
<Component class="javax.swing.JLabel" name="addAddressLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Address"/>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="addByteValueButton">
<Properties>
<Property name="text" type="java.lang.String" value="Add Byte Value"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addByteValueButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JButton" name="addWordValueButton">
<Properties>
<Property name="text" type="java.lang.String" value="Add Word Value"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addWordValueButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JTextField" name="addAddressField">
</Component>
<Component class="javax.swing.JTextField" name="addValueField">
</Component>
<Component class="javax.swing.JButton" name="disableCheatButton">
<Properties>
<Property name="text" type="java.lang.String" value="Disable Selected"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="disableCheatButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JButton" name="enableCheatButton">
<Properties>
<Property name="text" type="java.lang.String" value="Enable Selected"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="enableCheatButtonActionPerformed"/>
</Events>
</Component>
</SubComponents>
</Container>
<Container class="javax.swing.JPanel" name="searchPanel">
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout$JTabbedPaneConstraintsDescription">
<JTabbedPaneConstraints tabName="Search">
<Property name="tabTitle" type="java.lang.String" value="Search"/>
</JTabbedPaneConstraints>
</Constraint>
</Constraints>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="resultsScroll" pref="569" max="32767" attributes="0"/>
<Group type="102" alignment="0" attributes="0">
<Component id="addSelected" min="-2" max="-2" attributes="0"/>
<EmptySpace type="separate" max="-2" attributes="0"/>
<Component id="resultsStatusLabel" max="32767" attributes="0"/>
</Group>
<Group type="102" alignment="1" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<Component id="searchForLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="searchForChange" min="-2" max="-2" attributes="0"/>
<Component id="searchForValue" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<EmptySpace min="-2" pref="3" max="-2" attributes="0"/>
<Component id="searchForByte" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<EmptySpace min="-2" pref="6" max="-2" attributes="0"/>
<Component id="searchForWord" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
</Group>
<Group type="102" alignment="0" attributes="0">
<Component id="valueLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="searchNumber" min="-2" pref="63" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="resetButton" min="-2" pref="76" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="searchButton" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<EmptySpace pref="158" max="32767" attributes="0"/>
<Group type="103" groupAlignment="0" max="-2" attributes="0">
<Group type="102" alignment="1" attributes="0">
<Component id="addWatchLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="addWatchesButton" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Component id="addStartLabel" alignment="0" min="-2" max="-2" attributes="0"/>
<Component id="addEndLabel" alignment="0" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="addEndNumber" max="32767" attributes="0"/>
<Component id="addStartNumber" max="32767" attributes="0"/>
</Group>
</Group>
</Group>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="searchForLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="searchForValue" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="searchForByte" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="searchForChange" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="searchForWord" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="addStartLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="addStartNumber" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="searchNumber" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="valueLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="resetButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="searchButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="addEndLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="addEndNumber" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<Group type="102" alignment="0" attributes="0">
<EmptySpace min="-2" pref="10" max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="addWatchesButton" alignment="3" min="-2" pref="24" max="-2" attributes="0"/>
<Component id="addWatchLabel" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Component id="resultsScroll" pref="195" max="32767" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="addSelected" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="resultsStatusLabel" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Component class="javax.swing.JRadioButton" name="searchForValue">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="searchTypes"/>
</Property>
<Property name="selected" type="boolean" value="true"/>
<Property name="text" type="java.lang.String" value="Value"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="searchForValueActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="searchForLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Search for:"/>
</Properties>
</Component>
<Component class="javax.swing.JRadioButton" name="searchForChange">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="searchTypes"/>
</Property>
<Property name="text" type="java.lang.String" value="Change"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="searchForChangeActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JTextField" name="searchNumber">
<Properties>
<Property name="text" type="java.lang.String" value="0"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="searchNumberActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="valueLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Value:"/>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="resetButton">
<Properties>
<Property name="text" type="java.lang.String" value="Reset"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="resetButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JButton" name="searchButton">
<Properties>
<Property name="text" type="java.lang.String" value="Search"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="searchButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="resultsStatusLabel">
<Properties>
<Property name="text" type="java.lang.String" value="No results"/>
<Property name="verticalAlignment" type="int" value="1"/>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="org.netbeans.modules.form.compat2.border.SoftBevelBorderInfo">
<BevelBorder/>
</Border>
</Property>
</Properties>
</Component>
<Container class="javax.swing.JScrollPane" name="resultsScroll">
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
<SubComponents>
<Component class="javax.swing.JTable" name="resultsTable">
<Properties>
<Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.editors2.TableModelEditor">
<Table columnCount="3" rowCount="0">
<Column editable="false" title="Address" type="java.lang.String"/>
<Column editable="false" title="Last Search" type="java.lang.String"/>
<Column editable="false" title="Current Value" type="java.lang.String"/>
</Table>
</Property>
<Property name="columnModel" type="javax.swing.table.TableColumnModel" editor="org.netbeans.modules.form.editors2.TableColumnModelEditor">
<TableColumnModel selectionModel="0">
<Column maxWidth="-1" minWidth="-1" prefWidth="-1" resizable="false">
<Title/>
<Editor/>
<Renderer/>
</Column>
<Column maxWidth="-1" minWidth="-1" prefWidth="-1" resizable="false">
<Title/>
<Editor/>
<Renderer/>
</Column>
<Column maxWidth="-1" minWidth="-1" prefWidth="-1" resizable="false">
<Title/>
<Editor/>
<Renderer/>
</Column>
</TableColumnModel>
</Property>
<Property name="tableHeader" type="javax.swing.table.JTableHeader" editor="org.netbeans.modules.form.editors2.JTableHeaderEditor">
<TableHeader reorderingAllowed="true" resizingAllowed="false"/>
</Property>
</Properties>
</Component>
</SubComponents>
</Container>
<Component class="javax.swing.JButton" name="addSelected">
<Properties>
<Property name="text" type="java.lang.String" value="Add Selected"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addSelectedActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JRadioButton" name="searchForByte">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="valueTypes"/>
</Property>
<Property name="selected" type="boolean" value="true"/>
<Property name="text" type="java.lang.String" value="Byte"/>
</Properties>
</Component>
<Component class="javax.swing.JRadioButton" name="searchForWord">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="valueTypes"/>
</Property>
<Property name="text" type="java.lang.String" value="Word"/>
</Properties>
</Component>
<Component class="javax.swing.JLabel" name="addWatchLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Add Watches:"/>
</Properties>
</Component>
<Component class="javax.swing.JLabel" name="addStartLabel">
<Properties>
<Property name="text" type="java.lang.String" value="Start"/>
</Properties>
</Component>
<Component class="javax.swing.JTextField" name="addStartNumber">
</Component>
<Component class="javax.swing.JTextField" name="addEndNumber">
</Component>
<Component class="javax.swing.JLabel" name="addEndLabel">
<Properties>
<Property name="text" type="java.lang.String" value="End"/>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="addWatchesButton">
<Properties>
<Property name="text" type="java.lang.String" value="Add"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addWatchesButtonActionPerformed"/>
</Events>
</Component>
</SubComponents>
</Container>
</SubComponents>
</Container>
</SubComponents>
</Form>

View File

@ -0,0 +1,623 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.cheat;
import jace.core.Utility;
import static jace.core.Utility.gripe;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* This is the metacheat user interface. The actual logic of metacheat is in the
* Metacheats class.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class MetaCheatForm extends javax.swing.JFrame {
/**
* Creates new form MetaCheatForm
*/
public MetaCheatForm() {
initComponents();
this.setDefaultCloseOperation(HIDE_ON_CLOSE);
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
searchTypes = new javax.swing.ButtonGroup();
valueTypes = new javax.swing.ButtonGroup();
tabs = new javax.swing.JTabbedPane();
cheatPanel = new javax.swing.JPanel();
activeCheatsLabel = new javax.swing.JLabel();
activeCheatsScroll = new javax.swing.JScrollPane();
activeCheatsTable = new javax.swing.JTable();
removeSelectedButton = new javax.swing.JButton();
addNewCheatLabel = new javax.swing.JLabel();
jSeparator1 = new javax.swing.JSeparator();
addValueLabel = new javax.swing.JLabel();
addAddressLabel = new javax.swing.JLabel();
addByteValueButton = new javax.swing.JButton();
addWordValueButton = new javax.swing.JButton();
addAddressField = new javax.swing.JTextField();
addValueField = new javax.swing.JTextField();
disableCheatButton = new javax.swing.JButton();
enableCheatButton = new javax.swing.JButton();
searchPanel = new javax.swing.JPanel();
searchForValue = new javax.swing.JRadioButton();
searchForLabel = new javax.swing.JLabel();
searchForChange = new javax.swing.JRadioButton();
searchNumber = new javax.swing.JTextField();
valueLabel = new javax.swing.JLabel();
resetButton = new javax.swing.JButton();
searchButton = new javax.swing.JButton();
resultsStatusLabel = new javax.swing.JLabel();
resultsScroll = new javax.swing.JScrollPane();
resultsTable = new javax.swing.JTable();
addSelected = new javax.swing.JButton();
searchForByte = new javax.swing.JRadioButton();
searchForWord = new javax.swing.JRadioButton();
addWatchLabel = new javax.swing.JLabel();
addStartLabel = new javax.swing.JLabel();
addStartNumber = new javax.swing.JTextField();
addEndNumber = new javax.swing.JTextField();
addEndLabel = new javax.swing.JLabel();
addWatchesButton = new javax.swing.JButton();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
activeCheatsLabel.setText("Active Cheats");
activeCheatsTable.setModel(new javax.swing.table.DefaultTableModel(
new Object [][] {
},
new String [] {
"Address", "Value"
}
) {
Class[] types = new Class [] {
java.lang.String.class, java.lang.String.class
};
boolean[] canEdit = new boolean [] {
false, false
};
public Class getColumnClass(int columnIndex) {
return types [columnIndex];
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
return canEdit [columnIndex];
}
});
activeCheatsScroll.setViewportView(activeCheatsTable);
removeSelectedButton.setText("Remove Selected");
removeSelectedButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
removeSelectedButtonActionPerformed(evt);
}
});
addNewCheatLabel.setText("Add a new cheat:");
addValueLabel.setText("Value");
addAddressLabel.setText("Address");
addByteValueButton.setText("Add Byte Value");
addByteValueButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
addByteValueButtonActionPerformed(evt);
}
});
addWordValueButton.setText("Add Word Value");
addWordValueButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
addWordValueButtonActionPerformed(evt);
}
});
disableCheatButton.setText("Disable Selected");
disableCheatButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
disableCheatButtonActionPerformed(evt);
}
});
enableCheatButton.setText("Enable Selected");
enableCheatButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
enableCheatButtonActionPerformed(evt);
}
});
javax.swing.GroupLayout cheatPanelLayout = new javax.swing.GroupLayout(cheatPanel);
cheatPanel.setLayout(cheatPanelLayout);
cheatPanelLayout.setHorizontalGroup(
cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(cheatPanelLayout.createSequentialGroup()
.addContainerGap()
.addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(activeCheatsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 569, Short.MAX_VALUE)
.addComponent(jSeparator1)
.addGroup(cheatPanelLayout.createSequentialGroup()
.addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(activeCheatsLabel)
.addComponent(addNewCheatLabel)
.addGroup(cheatPanelLayout.createSequentialGroup()
.addComponent(addByteValueButton)
.addGap(18, 18, 18)
.addComponent(addWordValueButton))
.addGroup(cheatPanelLayout.createSequentialGroup()
.addComponent(addValueLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 56, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(addValueField, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE))
.addGroup(cheatPanelLayout.createSequentialGroup()
.addComponent(addAddressLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(addAddressField, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE)))
.addGap(0, 0, Short.MAX_VALUE))
.addGroup(cheatPanelLayout.createSequentialGroup()
.addComponent(disableCheatButton)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(enableCheatButton)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(removeSelectedButton)))
.addContainerGap())
);
cheatPanelLayout.setVerticalGroup(
cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(cheatPanelLayout.createSequentialGroup()
.addContainerGap()
.addComponent(activeCheatsLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(activeCheatsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 118, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(removeSelectedButton)
.addComponent(disableCheatButton)
.addComponent(enableCheatButton))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(addNewCheatLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(addAddressLabel)
.addComponent(addAddressField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(addValueLabel)
.addComponent(addValueField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(addByteValueButton)
.addComponent(addWordValueButton))
.addContainerGap())
);
tabs.addTab("Cheats", cheatPanel);
searchTypes.add(searchForValue);
searchForValue.setSelected(true);
searchForValue.setText("Value");
searchForValue.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
searchForValueActionPerformed(evt);
}
});
searchForLabel.setText("Search for:");
searchTypes.add(searchForChange);
searchForChange.setText("Change");
searchForChange.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
searchForChangeActionPerformed(evt);
}
});
searchNumber.setText("0");
searchNumber.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
searchNumberActionPerformed(evt);
}
});
valueLabel.setText("Value:");
resetButton.setText("Reset");
resetButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
resetButtonActionPerformed(evt);
}
});
searchButton.setText("Search");
searchButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
searchButtonActionPerformed(evt);
}
});
resultsStatusLabel.setText("No results");
resultsStatusLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP);
resultsStatusLabel.setBorder(new javax.swing.border.SoftBevelBorder(javax.swing.border.BevelBorder.RAISED));
resultsTable.setModel(new javax.swing.table.DefaultTableModel(
new Object [][] {
},
new String [] {
"Address", "Last Search", "Current Value"
}
) {
Class[] types = new Class [] {
java.lang.String.class, java.lang.String.class, java.lang.String.class
};
boolean[] canEdit = new boolean [] {
false, false, false
};
public Class getColumnClass(int columnIndex) {
return types [columnIndex];
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
return canEdit [columnIndex];
}
});
resultsTable.getTableHeader().setResizingAllowed(false);
resultsScroll.setViewportView(resultsTable);
resultsTable.getColumnModel().getColumn(0).setResizable(false);
resultsTable.getColumnModel().getColumn(1).setResizable(false);
resultsTable.getColumnModel().getColumn(2).setResizable(false);
addSelected.setText("Add Selected");
addSelected.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
addSelectedActionPerformed(evt);
}
});
valueTypes.add(searchForByte);
searchForByte.setSelected(true);
searchForByte.setText("Byte");
valueTypes.add(searchForWord);
searchForWord.setText("Word");
addWatchLabel.setText("Add Watches:");
addStartLabel.setText("Start");
addEndLabel.setText("End");
addWatchesButton.setText("Add");
addWatchesButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
addWatchesButtonActionPerformed(evt);
}
});
javax.swing.GroupLayout searchPanelLayout = new javax.swing.GroupLayout(searchPanel);
searchPanel.setLayout(searchPanelLayout);
searchPanelLayout.setHorizontalGroup(
searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(searchPanelLayout.createSequentialGroup()
.addContainerGap()
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(resultsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 569, Short.MAX_VALUE)
.addGroup(searchPanelLayout.createSequentialGroup()
.addComponent(addSelected)
.addGap(18, 18, 18)
.addComponent(resultsStatusLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, searchPanelLayout.createSequentialGroup()
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(searchPanelLayout.createSequentialGroup()
.addComponent(searchForLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(searchForChange)
.addComponent(searchForValue))
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(searchPanelLayout.createSequentialGroup()
.addGap(3, 3, 3)
.addComponent(searchForByte))
.addGroup(searchPanelLayout.createSequentialGroup()
.addGap(6, 6, 6)
.addComponent(searchForWord))))
.addGroup(searchPanelLayout.createSequentialGroup()
.addComponent(valueLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(searchNumber, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(resetButton, javax.swing.GroupLayout.PREFERRED_SIZE, 76, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(searchButton)))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 158, Short.MAX_VALUE)
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, searchPanelLayout.createSequentialGroup()
.addComponent(addWatchLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(addWatchesButton))
.addGroup(searchPanelLayout.createSequentialGroup()
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(addStartLabel)
.addComponent(addEndLabel))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(addEndNumber)
.addComponent(addStartNumber))))))
.addContainerGap())
);
searchPanelLayout.setVerticalGroup(
searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(searchPanelLayout.createSequentialGroup()
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(searchPanelLayout.createSequentialGroup()
.addContainerGap()
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(searchForLabel)
.addComponent(searchForValue)
.addComponent(searchForByte))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(searchForChange)
.addComponent(searchForWord)
.addComponent(addStartLabel)
.addComponent(addStartNumber, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(searchNumber, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(valueLabel)
.addComponent(resetButton)
.addComponent(searchButton)
.addComponent(addEndLabel)
.addComponent(addEndNumber, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
.addGroup(searchPanelLayout.createSequentialGroup()
.addGap(10, 10, 10)
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(addWatchesButton, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(addWatchLabel))))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(resultsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 194, Short.MAX_VALUE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(addSelected)
.addComponent(resultsStatusLabel))
.addContainerGap())
);
tabs.addTab("Search", searchPanel);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(tabs)
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(tabs)
.addContainerGap())
);
pack();
}// </editor-fold>//GEN-END:initComponents
private void searchForValueActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchForValueActionPerformed
// TODO add your handling code here:
}//GEN-LAST:event_searchForValueActionPerformed
private void searchForChangeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchForChangeActionPerformed
// TODO add your handling code here:
}//GEN-LAST:event_searchForChangeActionPerformed
private void searchNumberActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchNumberActionPerformed
// TODO add your handling code here:
}//GEN-LAST:event_searchNumberActionPerformed
private void resetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_resetButtonActionPerformed
MetaCheats.singleton.resetSearch();
resultsStatusLabel.setText("Results cleared.");
}//GEN-LAST:event_resetButtonActionPerformed
private void addSelectedActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addSelectedActionPerformed
List<Integer> addr = new ArrayList<Integer>(MetaCheats.singleton.results.keySet());
int val = Utility.parseHexInt(searchNumber.getText());
for (int i : resultsTable.getSelectedRows()) {
if (searchForByte.isSelected()) {
MetaCheats.singleton.addByteCheat(addr.get(i), val);
} else {
MetaCheats.singleton.addWordCheat(addr.get(i), val);
}
}
}//GEN-LAST:event_addSelectedActionPerformed
private void removeSelectedButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeSelectedButtonActionPerformed
Set<Integer> remove = new HashSet<Integer>();
for (int i : activeCheatsTable.getSelectedRows()) {
String s = String.valueOf(activeCheatsTable.getModel().getValueAt(i, 0));
remove.add(Utility.parseHexInt(s));
}
for (int i : remove) {
MetaCheats.singleton.removeCheat(i);
}
}//GEN-LAST:event_removeSelectedButtonActionPerformed
private void addByteValueButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addByteValueButtonActionPerformed
try {
int addr = Utility.parseHexInt(addAddressField.getText());
int val = Utility.parseHexInt(addValueField.getText());
MetaCheats.singleton.addByteCheat(addr, val);
} catch (NullPointerException e) {
gripe("Please enure that the address and value fields are correctly filled in.");
}
}//GEN-LAST:event_addByteValueButtonActionPerformed
private void addWordValueButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addWordValueButtonActionPerformed
try {
int addr = Utility.parseHexInt(addAddressField.getText());
int val = Utility.parseHexInt(addValueField.getText());
MetaCheats.singleton.addWordCheat(addr, val);
} catch (NullPointerException e) {
gripe("Please enure that the address and value fields are correctly filled in.");
}
}//GEN-LAST:event_addWordValueButtonActionPerformed
private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed
if (searchForChange.isSelected() == searchForValue.isSelected()) {
gripe("Please select if you want to search for a fixed value or a delta change");
return;
}
if (searchForByte.isSelected() == searchForWord.isSelected()) {
gripe("Please select if you want to search for a byte or a word value");
return;
}
try {
int val = Utility.parseHexInt(searchNumber.getText());
MetaCheats.singleton.performSearch(searchForChange.isSelected(), searchForByte.isSelected(), val);
} catch (NullPointerException e) {
gripe("Please enter a value");
}
}//GEN-LAST:event_searchButtonActionPerformed
private void disableCheatButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_disableCheatButtonActionPerformed
for (int i : activeCheatsTable.getSelectedRows()) {
String s = String.valueOf(activeCheatsTable.getModel().getValueAt(i, 0));
MetaCheats.singleton.disableCheat(Utility.parseHexInt(s));
}
}//GEN-LAST:event_disableCheatButtonActionPerformed
private void enableCheatButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableCheatButtonActionPerformed
for (int i : activeCheatsTable.getSelectedRows()) {
String s = String.valueOf(activeCheatsTable.getModel().getValueAt(i, 0));
MetaCheats.singleton.enableCheat(Utility.parseHexInt(s));
}
}//GEN-LAST:event_enableCheatButtonActionPerformed
private void addWatchesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addWatchesButtonActionPerformed
try {
int addrStart = Utility.parseHexInt(addStartNumber.getText());
int addrEnd = Utility.parseHexInt(addEndNumber.getText());
if (addrStart > addrEnd) {
gripe("Start address must be smaller than end address!");
return;
}
MetaCheats.singleton.addWatches(addrStart, addrEnd);
resultsStatusLabel.setText("Added range to watch list.");
} catch (NullPointerException e) {
gripe("Please enure that the start and end fields are correctly filled in.");
}
}//GEN-LAST:event_addWatchesButtonActionPerformed
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
/* Set the Nimbus look and feel */
//<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
/* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
* For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
*/
try {
for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
javax.swing.UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (ClassNotFoundException ex) {
java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (InstantiationException ex) {
java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
//</editor-fold>
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new MetaCheatForm().setVisible(true);
}
});
}
// Variables declaration - do not modify//GEN-BEGIN:variables
public javax.swing.JLabel activeCheatsLabel;
public javax.swing.JScrollPane activeCheatsScroll;
public javax.swing.JTable activeCheatsTable;
public javax.swing.JTextField addAddressField;
public javax.swing.JLabel addAddressLabel;
public javax.swing.JButton addByteValueButton;
public javax.swing.JLabel addEndLabel;
public javax.swing.JTextField addEndNumber;
public javax.swing.JLabel addNewCheatLabel;
public javax.swing.JButton addSelected;
public javax.swing.JLabel addStartLabel;
public javax.swing.JTextField addStartNumber;
public javax.swing.JTextField addValueField;
public javax.swing.JLabel addValueLabel;
public javax.swing.JLabel addWatchLabel;
public javax.swing.JButton addWatchesButton;
public javax.swing.JButton addWordValueButton;
public javax.swing.JPanel cheatPanel;
public javax.swing.JButton disableCheatButton;
public javax.swing.JButton enableCheatButton;
public javax.swing.JSeparator jSeparator1;
public javax.swing.JButton removeSelectedButton;
public javax.swing.JButton resetButton;
public javax.swing.JScrollPane resultsScroll;
public javax.swing.JLabel resultsStatusLabel;
public javax.swing.JTable resultsTable;
public javax.swing.JButton searchButton;
public javax.swing.JRadioButton searchForByte;
public javax.swing.JRadioButton searchForChange;
public javax.swing.JLabel searchForLabel;
public javax.swing.JRadioButton searchForValue;
public javax.swing.JRadioButton searchForWord;
public javax.swing.JTextField searchNumber;
public javax.swing.JPanel searchPanel;
public javax.swing.ButtonGroup searchTypes;
public javax.swing.JTabbedPane tabs;
public javax.swing.JLabel valueLabel;
public javax.swing.ButtonGroup valueTypes;
// End of variables declaration//GEN-END:variables
}

View File

@ -0,0 +1,320 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.cheat;
import jace.apple2e.RAM128k;
import jace.core.Computer;
import jace.core.KeyHandler;
import jace.core.Keyboard;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.table.DefaultTableModel;
/**
* Basic mame-style cheats. The user interface is in MetaCheatForm.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class MetaCheats extends Cheats {
static MetaCheats singleton = null;
// This is used to help the handler exit faster when there is nothing to do
boolean noCheats = true;
public MetaCheats() {
super();
singleton = this;
}
Map<Integer, Integer> holdBytes = new TreeMap<Integer,Integer>();
Map<Integer, Integer> holdWords = new TreeMap<Integer,Integer>();
Set<Integer> disabled = new HashSet<Integer>();
Map<Integer, Integer> results = new TreeMap<Integer,Integer>();
@Override
protected String getDeviceName() {
return "Meta-cheat engine";
}
@Override
public void tick() {
// Do nothing
}
public static int MAX_RESULTS_SHOWN = 256;
public MetaCheatForm form = null;
public boolean isDrawing = false;
public void redrawResults() {
if (isDrawing) {
return;
}
isDrawing = true;
try {
Thread.sleep(10);
} catch (InterruptedException ex) {
Logger.getLogger(MetaCheats.class.getName()).log(Level.SEVERE, null, ex);
}
RAM128k ram = (RAM128k) Computer.getComputer().getMemory();
DefaultTableModel model = (DefaultTableModel) form.resultsTable.getModel();
if (results.size() > MAX_RESULTS_SHOWN) {
model.setRowCount(0);
isDrawing = false;
return;
}
boolean useWord = form.searchForWord.isSelected();
if (model.getRowCount() != results.size()) {
model.setRowCount(0);
List<Integer> iter = new ArrayList<Integer>(results.keySet());
for (Integer i : iter) {
int val = results.get(i);
if (useWord) {
int current = ram.readWordRaw(i) & 0x0ffff;
model.addRow(new Object[]{hex(i, 4), val + " ("+hex(val,4)+")", current + " ("+hex(current,4)+")"});
} else {
int current = ram.readRaw(i) & 0x0ff;
model.addRow(new Object[]{hex(i, 4), val + " ("+hex(val,2)+")", current + " ("+hex(current,2)+")"});
}
}
} else {
List<Integer> iter = new ArrayList<Integer>(results.keySet());
for (int i=0; i < iter.size(); i++) {
int val = results.get(iter.get(i));
if (useWord) {
int current = ram.readWordRaw(iter.get(i)) & 0x0ffff;
model.setValueAt(val + " ("+hex(val,4)+")", i, 1);
model.setValueAt(current + " ("+hex(current,4)+")", i, 2);
} else {
int current = ram.readRaw(iter.get(i)) & 0x0ff;
model.setValueAt(val + " ("+hex(val,2)+")", i, 1);
model.setValueAt(current + " ("+hex(current,2)+")", i, 2);
}
}
}
isDrawing = false;
}
public static String hex(int val, int size) {
String out = Integer.toHexString(val);
while (out.length() < size) {
out = "0"+out;
}
return "$"+out;
}
public void redrawCheats() {
noCheats = holdBytes.isEmpty() && holdWords.isEmpty() && disabled.isEmpty();
DefaultTableModel model = (DefaultTableModel) form.activeCheatsTable.getModel();
model.setRowCount(0);
for (Integer i : holdBytes.keySet()) {
String loc = hex(i, 4);
if (disabled.contains(i)) loc += " (off)";
int val = holdBytes.get(i);
model.addRow(new Object[]{loc, val + " ("+hex(val,2)+")"});
}
for (Integer i : holdWords.keySet()) {
String loc = hex(i, 4);
if (disabled.contains(i)) loc += " (off)";
int val = holdWords.get(i);
model.addRow(new Object[]{loc, val + " ("+hex(val,4)+")"});
}
}
public void showCheatForm() {
if (form == null) {
form = new MetaCheatForm();
}
form.setVisible(true);
}
MemorySpy spy = null;
public void showMemorySpy() {
if (spy == null) {
spy = new MemorySpy();
}
spy.setVisible(true);
}
@Override
public void attach() {
this.addCheat(new RAMListener(RAMEvent.TYPE.READ, RAMEvent.SCOPE.ANY, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
}
@Override
protected void doEvent(RAMEvent e) {
if (noCheats) return;
if (disabled.contains(e.getAddress())) return;
if (holdBytes.containsKey(e.getAddress())) {
e.setNewValue(holdBytes.get(e.getAddress()));
} else if (holdWords.containsKey(e.getAddress())) {
e.setNewValue(holdWords.get(e.getAddress()) & 0x0ff);
} else if (holdWords.containsKey(e.getAddress()-1)) {
if (disabled.contains(e.getAddress()-1)) return;
e.setNewValue((holdWords.get(e.getAddress()-1)>>8) & 0x0ff);
}
}
});
this.addCheat(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ANY, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
}
@Override
protected void doEvent(RAMEvent e) {
if (results.isEmpty()) return;
if (results.size() > MAX_RESULTS_SHOWN || isDrawing) return;
if (results.containsKey(e.getAddress()) || results.containsKey(e.getAddress()-1)) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
redrawResults();
}
});
t.setName("Metacheat results updater");
t.start();
}
}
});
Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_END) {
@Override
public boolean handleKeyUp(KeyEvent e) {
showCheatForm();
return false;
}
@Override
public boolean handleKeyDown(KeyEvent e) {
return false;
}
}, this);
Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_HOME) {
@Override
public boolean handleKeyUp(KeyEvent e) {
showMemorySpy();
return false;
}
@Override
public boolean handleKeyDown(KeyEvent e) {
return false;
}
}, this);
}
@Override
public void detach() {
super.detach();
Keyboard.unregisterAllHandlers(this);
}
public void addByteCheat(int addr, int val) {
holdBytes.put(addr, val);
redrawCheats();
}
public void addWordCheat(int addr, int val) {
holdWords.put(addr, val);
redrawCheats();
}
void removeCheat(int i) {
holdBytes.remove(i);
holdWords.remove(i);
disabled.remove(i);
redrawCheats();
}
void resetSearch() {
results.clear();
redrawResults();
}
void performSearch(boolean useDeltaSearch, boolean searchForByteValues, int val) {
RAM128k ram = (RAM128k) Computer.getComputer().getMemory();
if (results.isEmpty()) {
int max = 0x010000;
if (!searchForByteValues) max--;
for (int i=0; i < max; i++) {
if (i >= 0x0c000 && i <= 0x0cfff) continue;
int v = searchForByteValues ? ram.readRaw(i) & 0x0ff : ram.readWordRaw(i) & 0x0ffff;
if (useDeltaSearch) {
results.put(i, v);
} else if (v == val) {
results.put(i, v);
}
}
} else {
Set<Integer> remove = new HashSet<Integer>();
for (Integer i : results.keySet()) {
int v = searchForByteValues ? ram.readRaw(i) & 0x0ff : ram.readWordRaw(i) & 0x0ffff;
if (useDeltaSearch) {
if (v - results.get(i) != val) {
remove.add(i);
} else {
results.put(i,v);
}
} else {
if (v != val) {
remove.add(i);
} else {
results.put(i,v);
}
}
}
for (Integer i : remove) {
results.remove(i);
}
}
form.resultsStatusLabel.setText("Search found "+results.size()+" result(s).");
redrawResults();
}
void enableCheat(int addr) {
disabled.remove(addr);
redrawCheats();
}
void disableCheat(int addr) {
disabled.add(addr);
redrawCheats();
}
void addWatches(int addrStart, int addrEnd) {
RAM128k ram = (RAM128k) Computer.getComputer().getMemory();
if (form == null) return;
boolean searchForByteValues = form.searchForByte.isSelected();
for (int i = addrStart; i <= addrEnd; i = i + (searchForByteValues ? 1 : 2)) {
int v = searchForByteValues ? ram.readRaw(i) & 0x0ff : ram.readWordRaw(i) & 0x0ffff;
results.put(i,v);
}
redrawResults();
}
}

View File

@ -0,0 +1,455 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.cheat;
import jace.Emulator;
import jace.apple2e.RAM128k;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField;
import jace.core.Computer;
import jace.core.PagedMemory;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import java.awt.Component;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* Prince of Persia game cheats. This would not have been possible without the
* source. I am eternally grateful to Jordan Mechner both for creating this
* game, and for being so kind to release the source code to it so that we can
* learn how it works. Where possible, I've indicated where I found the various
* game variables in the original source so that it might help anyone else
* trying to learn how this game works.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class PrinceOfPersiaCheats extends Cheats implements MouseListener {
@ConfigurableField(category = "Hack", name = "Feather fall", defaultValue = "false", description = "Fall like a feather!")
public static boolean velocityHack;
// Game memory locations
// Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/01%20POP%20Source/Source/GAMEEQ.S
@ConfigurableField(category = "Hack", name = "Invincibility", defaultValue = "false", description = "Warning: will crash game if you are impaled")
public static boolean invincibilityHack;
@ConfigurableField(category = "Hack", name = "Infinite Time", defaultValue = "false", description = "Freeze the clock")
public static boolean timeHack;
@ConfigurableField(category = "Hack", name = "Sleepy Time", defaultValue = "false", description = "Enemies won't react")
public static boolean sleepHack;
@ConfigurableField(category = "Hack", name = "Can haz sword?", defaultValue = "false", description = "Start with sword in level 1")
public static boolean swordHack;
@ConfigurableField(category = "Hack", name = "Mouse", defaultValue = "false", description = "Left click kills/opens, Right click teleports")
public static boolean mouseHack;
boolean mouseRegistered = false;
public static int PREV = 0x02b;
public static int SPREV = 0x02e;
public static int CharPosn = 0x040;
public static int CharX = 0x041;
public static int CharY = 0x042;
public static int CharFace = 0x043;
public static int CharBlockX = 0x44;
public static int CharBlockY = 0x45;
public static int CharAction = 0x46;
public static int CharXVel = 0x47;
public static int CharYVel = 0x48;
public static int CharSeq = 0x49; // Word
public static int CharScrn = 0x4b;
public static int CharRepeat = 0x4c;
public static int CharID = 0x4d;
public static int CharSword = 0x4e;
public static int CharLife = 0x4f;
public static int KidX = 0x051;
public static int KidY = 0x052;
public static int KidFace = 0x53;
public static int KidBlockX = 0x54;
public static int KidBlockY = 0x55;
public static int KidAction = 0x56;
public static int KidScrn = 0x5b;
public static int ShadBlockX = 0x64;
public static int ShadBlockY = 0x65;
public static int ShadLife = 0x06f;
// Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/02%20POP%20Disk%20Routines/CP.525/RYELLOW1.S
public static int deprotectCheckYellow = 0x07c;
public static int NumTrans = 0x096;
public static int OppStrength = 0x0cc;
public static int KidStrength = 0x0ce;
public static int EnemyAlert = 0x0d1;
public static int ChgOppStr = 0x0d2;
// Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/02%20POP%20Disk%20Routines/CP.525/PURPLE.MAIN.S
public static int deprotectCheckPurple = 0x0da;
public static int Heoric = 0x0d3;
public static int InEditor = 0x0202;
public static int MinLeft = 0x0300;
public static int hasSword = 0x030a;
public static int mobtables = 0x0b600;
public static int trloc = mobtables;
public static int trscrn = trloc + 0x020;
public static int trdirec = trscrn + 0x020;
// Blueprint (map level data)0
public static int BlueSpec = 0x0b9d0;
public static int LinkLoc = 0x0bca0;
public static int LinkMap = 0x0bda0;
public static int Map = 0x0bea0;
public static int MapInfo = 0x0bf00;
public static int RedBufs = 0x05e00;
public static int RedBuf = RedBufs + 90;
// Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/01%20POP%20Source/Source/EQ.S
public static int WipeBuf = RedBuf + 90;
public static int MoveBuf = WipeBuf + 30;
// Object types
// Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/01%20POP%20Source/Source/MOVEDATA.S
public static int space = 0;
public static int floor = 1;
public static int spikes = 2;
public static int posts = 3;
public static int gate = 4;
public static int dpressplate = 5;
public static int pressplate = 6;
public static int panelwif = 7;
public static int pillarbottom = 8;
public static int pillartop = 9;
public static int flask = 10;
public static int loose = 11;
public static int panelwof = 12;
public static int mirror = 13;
public static int rubble = 14;
public static int upressplate = 15;
public static int exit = 16;
public static int exit2 = 17;
public static int slicer = 18;
public static int torch = 19;
public static int block = 20;
public static int bones = 21;
public static int sword = 22;
public static int window = 23;
public static int window2 = 24;
public static int archbot = 25;
public static int archtop1 = 26;
public static int archtop2 = 27;
public static int archtop3 = 28;
public static int archtop4 = 29;
// This is the correct value for an open exit door.
public static int ExitOpen = 172;
@Override
protected String getDeviceName() {
return ("Prince of Persia");
}
@Override
public void tick() {
// Do nothing
}
@Override
public void attach() {
if (velocityHack) {
addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(CharYVel);
}
@Override
protected void doEvent(RAMEvent e) {
registerMouse();
if (!SoftSwitches.AUXZP.getState()) {
return;
}
int newVel = e.getNewValue();
if (newVel > 5) {
newVel = 1;
}
e.setNewValue(newVel & 0x0ff);
}
});
}
if (invincibilityHack) {
addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(KidStrength);
}
@Override
protected void doEvent(RAMEvent e) {
registerMouse();
if (!SoftSwitches.AUXZP.getState()) {
return;
}
e.setNewValue(3);
}
});
}
if (sleepHack) {
addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(EnemyAlert);
}
@Override
protected void doEvent(RAMEvent e) {
registerMouse();
if (!SoftSwitches.AUXZP.getState()) {
return;
}
e.setNewValue(0);
}
});
}
if (swordHack) {
addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(hasSword);
}
@Override
protected void doEvent(RAMEvent e) {
registerMouse();
if (!SoftSwitches.AUXZP.getState()) {
return;
}
e.setNewValue(1);
}
});
}
if (timeHack) {
addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(MinLeft);
}
@Override
protected void doEvent(RAMEvent e) {
registerMouse();
if (!SoftSwitches.AUXZP.getState()) {
return;
}
e.setNewValue(0x069);
}
});
}
if (mouseHack) {
if (Emulator.getScreen() != null) {
Emulator.getScreen().addMouseListener(this);
} else {
mouseRegistered = true;
}
} else {
if (Emulator.getScreen() != null) {
Emulator.getScreen().removeMouseListener(this);
}
}
}
@Override
public void detach() {
super.detach();
if (Emulator.getScreen() != null) {
Emulator.getScreen().removeMouseListener(this);
}
mouseRegistered = false;
}
public static int BlueType = 0x0b700;
public void registerMouse() {
if (mouseRegistered) {
Component drawingArea = Emulator.getScreen();
if (drawingArea == null) {
return;
}
Emulator.getScreen().addMouseListener(this);
mouseRegistered = false;
}
}
@Override
public void mouseClicked(MouseEvent me) {
Component drawingArea = Emulator.getScreen();
if (drawingArea == null) {
return;
}
Point currentMouseLocation = MouseInfo.getPointerInfo().getLocation();
Point topLeft = drawingArea.getLocationOnScreen();
Double x = (currentMouseLocation.x - topLeft.x) / drawingArea.getSize().getWidth();
// Offset y by three pixels to account for tiles above
Double y = (currentMouseLocation.y - topLeft.y) / drawingArea.getSize().getHeight() - 0.015625;
// Now we have the x and y coordinates ranging from 0 to 1.0, scale to POP values
int row = y < 0 ? -1 : (int) (y * 3);
int col = (int) (x * 10);
// Do a check if we are at the bottom of the tile, the user might have been clicking on the tile to the right.
// This accounts for the isometric view and allows a little more flexibility, not to mention warping behind gates
// that are on the left edge of the screen!
int yCoor = ((int) (y * 192) % 63);
if (yCoor >= 47) {
double yOffset = 1.0 - (((double) yCoor - 47.0) / 16.0);
int xCoor = ((int) (x * 280) % 28);
double xOffset = ((double) xCoor) / 28.0;
if (xOffset <= yOffset) {
col--;
}
}
// Note: POP uses a 255-pixel horizontal axis, Pixels 0-57 are offscreen to the left
// and 198-255 offscreen to the right.
// System.out.println("Clicked on " + col + "," + row + " -- screen " + (x * 280) + "," + (y * 192));
RAM128k mem = (RAM128k) Computer.getComputer().getMemory();
PagedMemory auxMem = mem.getAuxMemory();
if (me.getButton() == MouseEvent.BUTTON1) {
// Left click hacks
// See if there is an opponent we can kill off.
int opponentX = auxMem.readByte(ShadBlockX);
int opponentY = auxMem.readByte(ShadBlockY);
int opponentLife = auxMem.readByte(ShadLife);
// If there is a guy near where the user clicked and he's alive, then kill 'em.
if (opponentLife != 0 && opponentY == row && Math.abs(col - opponentX) <= 1) {
// System.out.println("Enemy at " + opponentX + "," + opponentY + "; life=" + opponentLife);
// Occasionally, if the code is at the right spot this will cause the special effect of a hit to appear
auxMem.writeByte(ChgOppStr, (byte) -opponentLife);
// And this will kill the dude pretty much right away.
auxMem.writeByte(ShadLife, (byte) 0);
} else if (row >= 0 && col >= 0) {
// Try to perform actions on the block clicked as well as to the left and right of it.
// This opens gates and exits.
performAction(row, col, 1);
performAction(row, col - 1, 1);
performAction(row, col + 1, 1);
}
} else {
// Right/middle click == warp
byte warpX = (byte) (x * 140 + 58);
// This aliases the Y coordinate so the prince is on the floor at the correct spot.
byte warpY = (byte) ((row * 63) + 54);
// System.out.println("Warping to " + warpX + "," + warpY);
auxMem.writeByte(KidX, warpX);
auxMem.writeByte(KidY, warpY);
auxMem.writeByte(KidBlockX, (byte) col);
auxMem.writeByte(KidBlockY, (byte) row);
// Set action to bump into a wall so it can reset the kid's feet on the ground correctly.
// Not sure if this has any real effect but things seem to be working (so I'll just leave this here...)
auxMem.writeByte(KidAction, (byte) 5);
}
}
/**
*
* @param row
* @param col
* @param direction
*/
public void performAction(int row, int col, int direction) {
RAM128k mem = (RAM128k) Computer.getComputer().getMemory();
PagedMemory auxMem = mem.getAuxMemory();
byte currentScrn = auxMem.readByte(KidScrn);
if (col < 0) {
col += 10;
int scrnLeft = auxMem.readByte(Map + ((currentScrn - 1) * 4));
if (scrnLeft == 0) {
return;
}
currentScrn = (byte) scrnLeft;
byte prev = auxMem.readByte(PREV + row);
byte sprev = auxMem.readByte(SPREV + row);
// If the block to the left is gate, let's lie about it being open... for science
// This causes odd-looking screen behavior but it gets the job done.
if (prev == 4) {
// Update the temp variable that represents that object
auxMem.writeByte(SPREV + row, (byte) 255);
// And also update the blueprint
auxMem.writeByte(BlueSpec + ((scrnLeft - 1) * 30) + row * 10 + 9, (byte) 255);
}
// System.out.println("Looking at room to left, row "+row+": "+Integer.toHexString(prev)+","+Integer.toHexString(sprev));
} else if (col >= 10) {
// This code will probably never be called but here just in case.
col -= 10;
int scrnRight = auxMem.readByte(Map + ((currentScrn - 1) * 4) + 1);
if (scrnRight == 0) {
return;
}
currentScrn = (byte) scrnRight;
}
int numTransition = auxMem.readByte(NumTrans);
byte clickedLoc = (byte) (row * 10 + col);
// Figure out what kind of block is there
int blockType = auxMem.readByte(BlueType + (currentScrn - 1) * 30 + row * 10 + col) & 0x01f;
if (blockType == exit2 || blockType == exit) {
// Open the exit by changing the map data and adding the tiles to the move buffer
auxMem.writeByte(BlueSpec + (currentScrn - 1) * 30 + row * 10 + col, (byte) ExitOpen);
direction = 1;
// Tell the graphics engine that this piece has moved.
auxMem.writeByte(MoveBuf + row * 10 + col, (byte) 2);
}
if (blockType == gate || blockType == exit2 || blockType == exit) {
// If the object in question can be opened (exit or gate) add it to the transitional animation buffer
//System.out.print("Triggering screen " + currentScrn + " at pos " + clickedLoc);
boolean addTransition = false;
if (numTransition == 0) {
addTransition = true;
} else {
addTransition = true;
for (int i = 1; i <= numTransition; i++) {
byte scrn = auxMem.readByte(trscrn + i);
byte loc = auxMem.readByte(trloc + i);
if (scrn == currentScrn && loc == clickedLoc) {
// Entry already exists, just change its direction
auxMem.writeByte(trdirec + i, (byte) direction);
addTransition = false;
break;
}
}
if (addTransition && numTransition >= 0x20) {
addTransition = false;
}
}
// If the object was not in the animation buffer, add it.
if (addTransition) {
numTransition++;
auxMem.writeByte(trdirec + numTransition, (byte) direction);
auxMem.writeByte(trscrn + numTransition, currentScrn);
auxMem.writeByte(trloc + numTransition, clickedLoc);
auxMem.writeByte(NumTrans, (byte) numTransition);
}
}
}
@Override
public void mousePressed(MouseEvent me) {
}
@Override
public void mouseReleased(MouseEvent me) {
}
@Override
public void mouseEntered(MouseEvent me) {
}
@Override
public void mouseExited(MouseEvent me) {
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.config.Configuration.ConfigNode;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JCheckBox;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
class BooleanComponent extends JCheckBox implements ActionListener {
ConfigNode node;
String fieldName;
public BooleanComponent(ConfigNode node, String fieldName) {
this.node = node;
this.fieldName = fieldName;
synchronizeValue();
addActionListener(this);
}
public void actionPerformed(ActionEvent e) {
Boolean value = Boolean.valueOf(isSelected());
node.setFieldValue(fieldName, value);
}
public void synchronizeValue() {
try {
Object value = node.getFieldValue(fieldName);
if (value == null) {
setSelected(false);
} else {
setSelected(Boolean.valueOf(String.valueOf(value)).booleanValue());
}
} catch (IllegalArgumentException ex) {
Logger.getLogger(BooleanComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

View File

@ -0,0 +1,137 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.core.Utility;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class ClassSelection extends DynamicSelection<Class> {
Class template = null;
public ClassSelection(Class supertype, Class defaultValue) {
super(defaultValue);
template = supertype;
}
@Override
public LinkedHashMap<Class, String> getSelections() {
LinkedHashMap<Class, String> selections = new LinkedHashMap<Class, String>();
List<? extends Class> allClasses = (List<? extends Class>) Utility.findAllSubclasses(template);
if (!allClasses.contains(null)) {
allClasses.add(null);
}
List<Entry<Class, String>> values = new ArrayList<Map.Entry<Class, String>>();
if (allowNull()) {
values.add(new Entry<Class, String>() {
@Override
public Class getKey() {
return null;
}
@Override
public String getValue() {
return "***Empty***";
}
@Override
public String setValue(String v) {
throw new UnsupportedOperationException("Not supported yet.");
}
});
}
for (final Class c : allClasses) {
Entry<Class, String> entry = new Map.Entry<Class, String>() {
public Class getKey() {
return c;
}
public String getValue() {
if (c == null) {
return "**Empty**";
}
if (c.isAnnotationPresent(Name.class)) {
return ((Name) c.getAnnotation(Name.class)).value();
}
return c.getSimpleName();
}
public String setValue(String value) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String toString() {
return getValue();
}
@Override
public boolean equals(Object obj) {
return super.equals(obj) || obj == getKey() || getKey() != null && getKey().equals(obj);
}
};
values.add(entry);
}
Collections.sort(values, new Comparator<Map.Entry<? extends Class, String>>() {
public int compare(Entry<? extends Class, String> o1, Entry<? extends Class, String> o2) {
if (o1.getKey() == null) {
return -1;
}
if (o2.getKey() == null) {
return 1;
} else {
return (o1.getValue().compareTo(o2.getValue()));
}
}
});
for (Map.Entry<Class, String> entry : values) {
Class key = entry.getKey();
selections.put(key, entry.getValue());
}
return selections;
}
@Override
public boolean allowNull() {
return false;
}
@Override
public void setValue(Class value) {
Object v = value;
if (v != null && v instanceof String) {
super.setValueByMatch((String) v);
return;
}
super.setValue(value);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A configurable field annotation means that an object property can be changed
* by the end-user.
* NOTE: Any field that implements this must be public and serializable!
* If a field is not serializable, it will result in a serialization error
* when the configuration is being saved. There is no way to offer a compiler
* warning to avoid this, unfortunately.
* One way you can work with this constraint when allowing large reconfiguration
* of functionality, such as Cards or other class implementations of hardware,
* is to store the class itself of the component as a configuration value
* and let the Reconfigure method generate a new instance.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigurableField {
public String name();
public String shortName() default "";
public String defaultValue() default "";
public String description() default "";
public String category() default "General";
}

View File

@ -0,0 +1,512 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.core.Computer;
import jace.core.Utility;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
/**
* Manages the configuration state of the emulator components.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Configuration implements Reconfigurable {
public String getName() {
return "Configuration";
}
@Override
public String getShortName() {
return "cfg";
}
public void reconfigure() {
}
public static class ConfigTreeModel implements TreeModel {
public Object getRoot() {
return BASE;
}
public Object getChild(Object parent, int index) {
if (parent instanceof ConfigNode) {
ConfigNode n = (ConfigNode) parent;
return n.children.values().toArray()[index];
} else {
return null;
}
}
public int getChildCount(Object parent) {
if (parent instanceof ConfigNode) {
ConfigNode n = (ConfigNode) parent;
return n.children.size();
} else {
return 0;
}
}
public boolean isLeaf(Object node) {
return getChildCount(node) == 0;
}
public void valueForPathChanged(TreePath path, Object newValue) {
// Do nothing...
}
public int getIndexOfChild(Object parent, Object child) {
if (parent instanceof ConfigNode) {
ConfigNode n = (ConfigNode) parent;
ConfigNode[] c = (ConfigNode[]) n.children.values().toArray(new ConfigNode[0]);
for (int i = 0; i < c.length; i++) {
if (c[i].equals(child)) {
return i;
}
}
}
return -1;
}
public void addTreeModelListener(TreeModelListener l) {
// Do nothing...
}
public void removeTreeModelListener(TreeModelListener l) {
// Do nothing...
}
}
/**
* Represents a serializable configuration node as part of a tree.
* The root node should be a single instance (e.g. Computer)
* The child nodes should be all object instances that stem from each object
* The overall goal of this class is two-fold:
* 1) Provide a navigable manner to inspect configuration
* 2) Provide a simple persistence mechanism to load/store configuration
*/
public static class ConfigNode implements Serializable {
public transient ConfigNode root;
public transient ConfigNode parent;
public transient Reconfigurable subject;
private Map<String, Serializable> settings;
protected Map<String, ConfigNode> children;
private boolean changed = false;
@Override
public String toString() {
if (subject == null) {
return "???";
}
return (changed ? "<html><i>" : "") + subject.getName();
}
public ConfigNode(Reconfigurable subject) {
this(null, subject);
this.root = null;
}
public ConfigNode(ConfigNode parent, Reconfigurable subject) {
this.subject = subject;
this.settings = new TreeMap<String, Serializable>();
this.children = new TreeMap<String, ConfigNode>();
this.parent = parent;
if (this.parent != null) {
this.root = this.parent.root != null ? this.parent.root : this.parent;
}
}
public void setFieldValue(String field, Serializable value) {
if (value != null) {
if (value.equals(getFieldValue(field))) {
return;
}
} else {
if (getFieldValue(field) == null) {
return;
}
}
changed = true;
setRawFieldValue(field, value);
}
public void setRawFieldValue(String field, Serializable value) {
settings.put(field, value);
}
public Serializable getFieldValue(String field) {
return settings.get(field);
}
public Set<String> getAllSettingNames() {
return settings.keySet();
}
}
public final static ConfigNode BASE;
public static Computer emulator = Computer.getComputer();
@ConfigurableField(name = "Autosave Changes", description = "If unchecked, changes are only saved when the Save button is pressed.")
public static boolean saveAutomatically = false;
static {
BASE = new ConfigNode(new Configuration());
buildTree();
}
public static void buildTree() {
buildTree(BASE, new HashSet());
}
private static void buildTree(ConfigNode node, Set visited) {
if (node.subject == null) {
return;
}
for (Field f : node.subject.getClass().getFields()) {
// System.out.println("Evaluating field " + f.getName());
try {
Object o = f.get(node.subject);
if (/*o == null ||*/visited.contains(o)) {
continue;
}
// System.out.println(o.getClass().getName());
// If the object in question is not reconfigurable,
// skip over it and investigate its fields instead
// if (o.getClass().isAssignableFrom(Reconfigurable.class)) {
// if (Reconfigurable.class.isAssignableFrom(o.getClass())) {
if (f.isAnnotationPresent(ConfigurableField.class)) {
if (o != null && ISelection.class.isAssignableFrom(o.getClass())) {
ISelection selection = (ISelection) o;
node.setRawFieldValue(f.getName(), (Serializable) selection.getSelections().get(selection.getValue()));
} else {
node.setRawFieldValue(f.getName(), (Serializable) o);
}
continue;
} else if (o == null) {
continue;
}
if (o instanceof Reconfigurable) {
Reconfigurable r = (Reconfigurable) o;
visited.add(r);
ConfigNode child = node.children.get(f.getName());
if (child == null || !child.subject.equals(o)) {
child = new ConfigNode(node, r);
node.children.put(f.getName(), child);
}
buildTree(child, visited);
} else if (o.getClass().isArray()) {
String fieldName = f.getName();
Class type = o.getClass().getComponentType();
if (!Reconfigurable.class.isAssignableFrom(type)) {
continue;
}
Reconfigurable[] r = (Reconfigurable[]) o;
visited.add(r);
for (int i = 0; i < r.length; i++) {
String childName = fieldName + i;
if (r[i] == null) {
node.children.remove(childName);
continue;
}
ConfigNode grandchild = node.children.get(childName);
if (grandchild == null || !grandchild.subject.equals(r[i])) {
grandchild = new ConfigNode(node, r[i]);
node.children.put(childName, grandchild);
}
buildTree(grandchild, visited);
}
}
} catch (IllegalArgumentException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@InvokableAction(
name="Save settings",
description="Save all configuration settings as defaults",
category="general",
alternatives="save preferences;save defaults"
)
public static void saveSettings() {
FileOutputStream fos = null;
{
ObjectOutputStream oos = null;
try {
applySettings(BASE);
oos = new ObjectOutputStream(new FileOutputStream(getSettingsFile()));
oos.writeObject(BASE);
} catch (FileNotFoundException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
@InvokableAction(
name="Load settings",
description="Load all configuration settings previously saved",
category="general",
alternatives="load preferences;revert settings;revert preferences"
)
public static void loadSettings() {
{
ObjectInputStream ois = null;
FileInputStream fis = null;
try {
ois = new ObjectInputStream(new FileInputStream(getSettingsFile()));
ConfigNode newRoot = (ConfigNode) ois.readObject();
applyConfigTree(newRoot, BASE);
} catch (ClassNotFoundException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} catch (FileNotFoundException ex) {
// Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
// This just means there are no settings to be saved -- just ignore it.
} catch (IOException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
public static void resetToDefaults() {
throw new UnsupportedOperationException("Not yet implemented");
}
public static File getSettingsFile() {
return new File(System.getProperty("user.dir"), ".jace.conf");
}
/**
* Apply settings from node tree to the object model
* This also calls "reconfigure" on objects in sequence
* @param node
* @return True if any settings have changed in the node or any of its descendants
*/
public static boolean applySettings(ConfigNode node) {
boolean resume = false;
if (node == BASE) {
resume = Computer.pause();
}
boolean hasChanged = false;
if (node.changed) {
doApply(node);
hasChanged = true;
}
// Now that the object structure reflects the current configuration,
// process reconfiguration from the children, etc.
for (ConfigNode child : node.children.values()) {
hasChanged |= applySettings(child);
}
if (node.equals(BASE) && hasChanged) {
buildTree();
}
if (resume) {
Computer.resume();
}
return hasChanged;
}
private static void applyConfigTree(ConfigNode newRoot, ConfigNode oldRoot) {
if (oldRoot == null || newRoot == null) {
return;
}
oldRoot.settings = newRoot.settings;
if (oldRoot.subject != null) {
doApply(oldRoot);
buildTree(oldRoot, new HashSet());
}
for (String childName : newRoot.children.keySet()) {
// System.out.println("Applying settings for " + childName);
applyConfigTree(newRoot.children.get(childName), oldRoot.children.get(childName));
}
}
private static void doApply(ConfigNode node) {
List<String> removeList = new ArrayList<String>();
for (String f : node.settings.keySet()) {
try {
Field ff = node.subject.getClass().getField(f);
// System.out.println("Setting " + f + " to " + node.settings.get(f));
Object val = node.settings.get(f);
Class valType = (val != null ? val.getClass() : null);
Class fieldType = ff.getType();
if (ISelection.class.isAssignableFrom(fieldType)) {
ISelection selection = (ISelection) ff.get(node.subject);
try {
selection.setValue(val);
} catch (ClassCastException c) {
selection.setValueByMatch(String.valueOf(val));
}
continue;
}
if (val == null || valType.equals(fieldType)) {
ff.set(node.subject, val);
continue;
}
// System.out.println(fieldType);
val = Utility.deserializeString(String.valueOf(val), fieldType, false);
// System.out.println("Setting "+node.subject.getName()+" property "+ff.getName()+" with value "+String.valueOf(val));
ff.set(node.subject, val);
} catch (NoSuchFieldException ex) {
System.out.println("Setting "+f+" no longer exists, skipping.");
removeList.add(f);
} catch (SecurityException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalArgumentException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
}
for (String f : removeList) {
node.settings.remove(f);
}
try {
// When settings are applied, this could very well change the object structure
// For example, if cards or other pieces of emulation are changed around
// System.out.println("Reconfiguring "+node.subject.getName());
node.subject.reconfigure();
} catch (Exception ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
node.changed = false;
}
public static void applySettings(Map<String, String> settings) {
for (Map.Entry<String, String> setting : settings.entrySet()) {
Map<String, ConfigNode> shortNames = new HashMap<String, ConfigNode>();
buildNodeMap(BASE, shortNames);
String settingName = setting.getKey();
String value = setting.getValue();
String[] parts = settingName.split("\\.");
if (parts.length != 2) {
System.err.println("Unable to parse settting, should be in the form of DEVICE.PROPERTYNAME "+settingName);
continue;
}
String deviceName = parts[0];
String fieldName = parts[1];
ConfigNode n = shortNames.get(deviceName.toLowerCase());
if (n == null) {
System.err.println("Unable to find device named "+deviceName+", try one of these: "+Utility.join(shortNames.keySet(), ", "));
continue;
}
boolean found = false;
List<String> shortFieldNames = new ArrayList<String>();
for (String longName : n.getAllSettingNames()) {
ConfigurableField f = null;
try {
f = n.subject.getClass().getField(longName).getAnnotation(ConfigurableField.class);
} catch (NoSuchFieldException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
String shortName = (f != null && !f.shortName().equals("")) ? f.shortName() : longName;
shortFieldNames.add(shortName);
if (fieldName.equalsIgnoreCase(longName) || fieldName.equalsIgnoreCase(shortName)) {
found = true;
n.setFieldValue(longName, value);
applySettings(n);
n.subject.reconfigure();
buildTree();
System.out.println("Set property "+n.subject.getName()+"."+longName+" to "+value);
break;
}
}
if (!found) {
System.err.println("Unable to find property "+fieldName+" for device "+deviceName+". Try one of these :"+Utility.join(shortFieldNames, ", "));
}
}
}
private static void buildNodeMap(ConfigNode n, Map<String, ConfigNode> shortNames) {
shortNames.put(n.subject.getShortName().toLowerCase(), n);
for (Map.Entry<String, ConfigNode> c : n.children.entrySet()) {
buildNodeMap(c.getValue(), shortNames);
}
}
private static void printTree(ConfigNode n, String prefix, int i) {
for (String setting : n.getAllSettingNames()) {
for (int j=0; j < i; j++) System.out.print(" ");
ConfigurableField f = null;
try {
f = n.subject.getClass().getField(setting).getAnnotation(ConfigurableField.class);
} catch (NoSuchFieldException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
String sn = (f != null && !f.shortName().equals("")) ? f.shortName() : setting;
System.out.println(prefix+">>"+setting+" ("+n.subject.getShortName()+"."+sn+")");
}
for (Map.Entry<String, ConfigNode> c : n.children.entrySet()) {
printTree(c.getValue(), prefix+"."+c.getKey(), i+1);
}
}
}

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.3" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
<Properties>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[650, 480]"/>
</Property>
</Properties>
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
</AuxValues>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="applyButton" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="saveButton" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="revertButton" min="-2" max="-2" attributes="0"/>
</Group>
<Component id="configTreeScrollPane" min="-2" pref="271" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Component id="settingsPanel" pref="438" max="32767" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" attributes="0">
<Component id="configTreeScrollPane" max="32767" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="applyButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="saveButton" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="revertButton" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
<Component id="settingsPanel" pref="0" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Container class="javax.swing.JScrollPane" name="configTreeScrollPane">
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
<SubComponents>
<Component class="javax.swing.JTree" name="configTree">
<Properties>
<Property name="model" type="javax.swing.tree.TreeModel" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
<Connection code="new Configuration.ConfigTreeModel()" type="code"/>
</Property>
</Properties>
<Events>
<EventHandler event="valueChanged" listener="javax.swing.event.TreeSelectionListener" parameters="javax.swing.event.TreeSelectionEvent" handler="configTreeValueChanged"/>
</Events>
</Component>
</SubComponents>
</Container>
<Container class="javax.swing.JPanel" name="settingsPanel">
<Properties>
<Property name="alignmentX" type="float" value="0.0"/>
<Property name="alignmentY" type="float" value="0.0"/>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[375, 480]"/>
</Property>
</Properties>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
</Container>
<Component class="javax.swing.JButton" name="applyButton">
<Properties>
<Property name="text" type="java.lang.String" value="Apply"/>
<Property name="toolTipText" type="java.lang.String" value="Apply current changes without saving"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="applyButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JButton" name="saveButton">
<Properties>
<Property name="text" type="java.lang.String" value="Save"/>
<Property name="toolTipText" type="java.lang.String" value="Apply settings and save"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="saveButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JButton" name="revertButton">
<Properties>
<Property name="text" type="java.lang.String" value="Revert"/>
<Property name="toolTipText" type="java.lang.String" value="Revert all settings to last saved values (hold SHIFT while clicking to revert to defaults)"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="revertButtonActionPerformed"/>
</Events>
</Component>
</SubComponents>
</Form>

View File

@ -0,0 +1,280 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.apple2e.Apple2e;
import jace.config.Configuration.ConfigNode;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.File;
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.JTree;
/**
* Configuration user interface. Not pretty but it works.
* Created on Dec 16, 2010, 3:23:13 PM
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class ConfigurationPanel extends javax.swing.JPanel {
public static void main(String... args) {
new Apple2e();
Apple2e.getComputer().reconfigure();
Configuration.loadSettings();
JFrame f = new JFrame();
f.setContentPane(new ConfigurationPanel());
f.setSize(f.getContentPane().getPreferredSize());
f.validate();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}
/** Creates new form ConfigurationPanel */
public ConfigurationPanel() {
initComponents();
expandAll(configTree);
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
configTreeScrollPane = new javax.swing.JScrollPane();
configTree = new javax.swing.JTree();
settingsPanel = new javax.swing.JPanel();
applyButton = new javax.swing.JButton();
saveButton = new javax.swing.JButton();
revertButton = new javax.swing.JButton();
setPreferredSize(new java.awt.Dimension(650, 480));
configTree.setModel(new Configuration.ConfigTreeModel());
configTree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {
public void valueChanged(javax.swing.event.TreeSelectionEvent evt) {
configTreeValueChanged(evt);
}
});
configTreeScrollPane.setViewportView(configTree);
settingsPanel.setAlignmentX(0.0F);
settingsPanel.setAlignmentY(0.0F);
settingsPanel.setPreferredSize(new java.awt.Dimension(375, 480));
settingsPanel.setLayout(new java.awt.GridBagLayout());
applyButton.setText("Apply");
applyButton.setToolTipText("Apply current changes without saving");
applyButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
applyButtonActionPerformed(evt);
}
});
saveButton.setText("Save");
saveButton.setToolTipText("Apply settings and save");
saveButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
saveButtonActionPerformed(evt);
}
});
revertButton.setText("Revert");
revertButton.setToolTipText("Revert all settings to last saved values (hold SHIFT while clicking to revert to defaults)");
revertButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
revertButtonActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(applyButton)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(saveButton)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(revertButton))
.addComponent(configTreeScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 271, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(settingsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 438, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(configTreeScrollPane)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(applyButton)
.addComponent(saveButton)
.addComponent(revertButton))
.addContainerGap())
.addComponent(settingsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 0, Short.MAX_VALUE)
);
}// </editor-fold>//GEN-END:initComponents
ConfigNode currentNode = null;
private void configTreeValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_configTreeValueChanged
settingsPanel.removeAll();
ConfigNode node = (ConfigNode) evt.getPath().getLastPathComponent();
if (currentNode == node) return;
currentNode = node;
if (node != null && node.subject != null && !node.getAllSettingNames().isEmpty()) {
GridBagLayout l = (GridBagLayout) settingsPanel.getLayout();
GridBagConstraints c = new GridBagConstraints();
int y = 0;
for (String s : node.getAllSettingNames()) {
try {
Field f = node.subject.getClass().getField(s);
ConfigurableField annotation = f.getAnnotation(ConfigurableField.class);
if (annotation == null) continue;
JLabel label = new JLabel(annotation.name());
c.anchor = GridBagConstraints.FIRST_LINE_START;
c.fill = GridBagConstraints.HORIZONTAL;
c.gridwidth = 1;
c.gridy = y++;
c.gridx = 0;
c.weightx = 0.0;
c.insets = new Insets(2, 2, 2, 2);
c.ipady = 2;
settingsPanel.add(label,c);
Component edit = generateEditComponent(node, s);
edit.setSize(edit.getPreferredSize());
c.gridx = 1;
c.weightx = 0.5;
c.ipady = 0;
c.insets = new Insets(0, 2, 2, 2);
c.gridwidth = GridBagConstraints.REMAINDER;
settingsPanel.add(edit,c);
if (!annotation.description().equals("")) {
c.gridy = y++;
c.gridx = 0;
c.gridwidth = 2;
c.weightx = 0.0;
JLabel desc = new JLabel("<html><i>" + annotation.description());
c.insets = new Insets(1, 15, 7, 5);
settingsPanel.add(desc, c);
}
} catch (NoSuchFieldException ex) {
Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
settingsPanel.validate();
settingsPanel.setVisible(true);
settingsPanel.repaint();
}//GEN-LAST:event_configTreeValueChanged
private void applyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_applyButtonActionPerformed
Configuration.applySettings(Configuration.BASE);
configTree.updateUI();
expandAll(configTree);
}//GEN-LAST:event_applyButtonActionPerformed
private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed
Configuration.saveSettings();
configTree.updateUI();
expandAll(configTree);
}//GEN-LAST:event_saveButtonActionPerformed
private void revertButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_revertButtonActionPerformed
if ((evt.getModifiers() & evt.SHIFT_MASK) != 0) {
revertDefaultsActionPerformed(evt);
return;
}
Configuration.loadSettings();
configTree.updateUI();
expandAll(configTree);
}//GEN-LAST:event_revertButtonActionPerformed
private void revertDefaultsActionPerformed(java.awt.event.ActionEvent evt) {
if (JOptionPane.showConfirmDialog(null, "Revert all settings to defaults?", "Revert to defaults?", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION)
return;
Configuration.resetToDefaults();
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton applyButton;
private javax.swing.JTree configTree;
private javax.swing.JScrollPane configTreeScrollPane;
private javax.swing.JButton revertButton;
private javax.swing.JButton saveButton;
private javax.swing.JPanel settingsPanel;
// End of variables declaration//GEN-END:variables
public void expandAll(JTree tree) {
for (int row = 0; row < tree.getRowCount(); tree.expandRow(row++));
}
private Component generateEditComponent(ConfigNode node, String s) {
try {
Field f = node.subject.getClass().getField(s);
if (f.getType().isPrimitive()) {
if (f.getType().equals(Boolean.TYPE)) {
return new BooleanComponent(node, s);
} else if (f.getType().equals(Integer.TYPE)) {
return new IntegerComponent(node, s);
} else if (f.getType().equals(Short.TYPE)) {
return new IntegerComponent(node, s);
} else if (f.getType().equals(Byte.TYPE)) {
return new IntegerComponent(node, s);
} else if (f.getType().equals(Long.TYPE)) {
return new IntegerComponent(node, s);
} else {
return new StringComponent(node, s);
}
} else if (f.getType().equals(String.class)) {
return new StringComponent(node, s);
} else if (f.getType().equals(File.class)) {
return new FileComponent(node, s);
} else if (Class.class.isEnum()) {
// TODO: Add enumeration support!
} else if (ISelection.class.isAssignableFrom(f.getType())) {
return new DynamicSelectComponent(node, s);
}
return new JTextField();
} catch (NoSuchFieldException ex) {
Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.config.Configuration.ConfigNode;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.event.ListDataListener;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
class DynamicSelectComponent extends JComboBox implements ActionListener {
ConfigNode node;
String fieldName;
Serializable currentValue;
@Override
public void actionPerformed(ActionEvent e) {
node.setFieldValue(fieldName, currentValue);
}
public void synchronizeValue() {
try {
Object value = node.getFieldValue(fieldName);
if (value == null) {
getModel().setSelectedItem(null);
setSelectedItem(getModel().getSelectedItem());
} else {
getModel().setSelectedItem(value);
setSelectedItem(getModel().getSelectedItem());
}
} catch (IllegalArgumentException ex) {
Logger.getLogger(StringComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
public DynamicSelectComponent(ConfigNode node, String fieldName) {
try {
this.node = node;
this.fieldName = fieldName;
DynamicSelection sel;
try {
sel = (DynamicSelection) node.subject.getClass().getField(fieldName).get(node.subject);
} catch (IllegalArgumentException ex) {
Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex);
System.err.print("Couldn't get selections for field " + fieldName);
return;
} catch (IllegalAccessException ex) {
Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex);
System.err.print("Couldn't get selections for field " + fieldName);
return;
} catch (NoSuchFieldException ex) {
Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex);
System.err.print("Couldn't get selections for field " + fieldName);
return;
} catch (SecurityException ex) {
Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex);
System.err.print("Couldn't get selections for field " + fieldName);
return;
}
currentValue = node.getFieldValue(fieldName);
final LinkedHashMap selections = sel.getSelections();
addActionListener(this);
ComboBoxModel m;
setModel(new ComboBoxModel() {
Entry value;
public void setSelectedItem(Object anItem) {
if (anItem != null && anItem instanceof Map.Entry) {
value = (Entry) anItem;
currentValue = (Serializable) ((Entry) anItem).getKey();
} else {
for (Map.Entry entry : (Set<Map.Entry>) selections.entrySet()) {
if (entry.getValue().equals(anItem)) {
value = entry;
currentValue = (Serializable) entry.getKey();
}
if (entry.getKey() == null && anItem == null) {
value = entry;
currentValue = (Serializable) entry.getKey();
}
if (entry.getKey() != null && entry.equals(anItem)) {
value = entry;
currentValue = (Serializable) entry.getKey();
}
}
}
}
public Object getSelectedItem() {
return selections.get(currentValue);
}
public int getSize() {
return selections.size();
}
public Object getElementAt(int index) {
for (Map.Entry entry : (Set<Map.Entry>) selections.entrySet()) {
if (index == 0) {
return entry.getValue();
}
index--;
}
return null;
}
public void addListDataListener(ListDataListener l) {
}
public void removeListDataListener(ListDataListener l) {
}
});
synchronizeValue();
} catch (SecurityException ex) {
Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.core.Utility;
import java.util.Iterator;
import java.util.Map;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class DynamicSelection<T> implements ISelection<T> {
public DynamicSelection(T defaultValue) {
setValue(defaultValue);
}
abstract public boolean allowNull();
T currentValue;
@Override
public T getValue() {
if (currentValue != null || !allowNull()) {
return currentValue;
} else {
Iterator<? extends T> i = getSelections().keySet().iterator();
return i.next();
}
}
@Override
public void setValue(T value) {currentValue = value;}
public void setValueByMatch(String search) {
Map<? extends T, String> selections = getSelections();
String match = Utility.findBestMatch(search, selections.values());
if (match != null) {
for (T key : selections.keySet()) {
if (selections.get(key).equals(match)) {
setValue(key);
return;
}
}
} else {
setValue(null);
}
}
}

View File

@ -0,0 +1,287 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.config.Configuration.ConfigNode;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;
/**
* This component provides a text field for the manual input of a file, as well
* as an associated 'Browse' button, allowing the user to browse for a file and
* have its location show up automatically in the text field.
*
* --borrowed and modified by Brendan Robert
*
* @author Eelke Spaak
* @see javax.swing.JFileChooser
*/
class FileComponent extends javax.swing.JPanel implements ActionListener, KeyListener {
ConfigNode node;
String fieldName;
public void actionPerformed(ActionEvent e) {
textField.setBackground(Color.WHITE);
String value = textField.getText();
if (value == null || value.equals("")) {
node.setFieldValue(fieldName, null);
} else {
File f = new File(value);
if (f.exists()) {
node.setFieldValue(fieldName, f);
} else {
textField.setBackground(Color.RED);
}
}
}
public void synchronizeValue() {
try {
Object value = node.getFieldValue(fieldName);
if (value == null) {
setText("");
} else {
setText(String.valueOf(value));
}
} catch (IllegalArgumentException ex) {
Logger.getLogger(StringComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
private int TEXT_FIELD_WIDTH = 150;
/** Creates new form JFileField */
public FileComponent(ConfigNode node, String fieldName) {
this.node = node;
this.fieldName = fieldName;
// if (".".equals(type.value())) {
// fileSelectionMode = JFileChooser.DIRECTORIES_ONLY;
// } else {
// setFileTypeName(type.value());
// setExtensionFilter(type.value());
// }
initComponents();
textField.addActionListener(this);
synchronizeValue();
}
private String extensionFilter;
private String fileTypeName;
private int fileSelectionMode = JFileChooser.FILES_ONLY;
/** This method is called from within the constructor to
* initialize the form.
*/
private void initComponents() {
textField = new javax.swing.JTextField();
browseButton = new javax.swing.JButton();
textField.setPreferredSize(new Dimension(150,20));
textField.addKeyListener(this);
browseButton.setText("...");
browseButton.setPreferredSize(new Dimension(25,20));
browseButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
browseButtonActionPerformed(evt);
}
});
this.add(textField);
this.add(browseButton);
FlowLayout layout = new FlowLayout();
this.setLayout(layout);
this.validate();
}
private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {
File currentDirectory = new File(".");
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(fileSelectionMode);
// Quick-n-dirty implementation of file extension filter since it's new in JDK 1.6
if (extensionFilter != null && fileTypeName != null) {
FileFilter filter = new FileFilter() {
String[] extensions = extensionFilter.toLowerCase().split(",");
@Override
public boolean accept(File f) {
for (int i=0; i < extensions.length; i++) {
if (f.getPath().toLowerCase().endsWith(extensions[i]))
return true;
}
return false;
}
@Override
public String getDescription() {
return fileTypeName;
}
};
chooser.setFileFilter(filter);
}
try {
File f = new File(textField.getText());
if (f.exists()) {
if (f.isDirectory()) {
chooser.setCurrentDirectory(f);
} else {
chooser.setCurrentDirectory(f.getParentFile());
chooser.setSelectedFile(f);
}
} else {
chooser.setCurrentDirectory(currentDirectory);
}
} catch (Exception ignore) {
}
int returnVal = chooser.showOpenDialog(this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
try {
File selectedFile = chooser.getSelectedFile();
if (selectedFile.getCanonicalPath().startsWith(currentDirectory.getCanonicalPath())) {
String use = selectedFile.getCanonicalPath().substring(currentDirectory.getCanonicalPath().length() + 1);
textField.setText(use);
} else {
textField.setText(selectedFile.getPath());
}
node.setFieldValue(fieldName, selectedFile);
} catch (IOException ex) {
Logger.getLogger(FileComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* Returns the value of the text field.
*
* @return the value of the text field
*/
public String getText() {
return textField.getText();
}
/**
* Sets the value of the text field.
*
* @param the value to put in the text field
*/
public void setText(String text) {
textField.setText(text);
}
/**
* Returns the extension filter (a comma-separated string of extensions)
* that the JFileChooser should use when browsing for a file.
*
* @return the extension filter
*/
public String getExtensionFilter() {
return extensionFilter;
}
/**
* Sets the extension filter (a comma-separated string of extensions)
* that the JFileChooser should use when browsing for a file.
*
* @param extensionFilter the extension filter
*/
public void setExtensionFilter(String extensionFilter) {
this.extensionFilter = extensionFilter;
}
/**
* Returns the description of the file types the JFileChooser should be
* browsing for.
*
* @return the file type description
*/
public String getFileTypeName() {
return fileTypeName;
}
/**
* Sets the description of the file types the JFileChooser should be
* browsing for.
*
* @param fileTypeName the file type description
*/
public void setFileTypeName(String fileTypeName) {
this.fileTypeName = fileTypeName;
}
/**
* Returns the file selection mode to be used by the JFileChooser.
*
* @return the type of files to be displayed
* @see javax.swing.JFileChooser#getFileSelectionMode()
*/
public int getFileSelectionMode() {
return fileSelectionMode;
}
/**
* Sets the file selection mode to be used by the JFileChooser.
*
* @param fileSelectionMode the type of files to be displayed
* @see javax.swing.JFileChooser#setFileSelectionMode(int)
*/
public void setFileSelectionMode(int fileSelectionMode) {
this.fileSelectionMode = fileSelectionMode;
}
/**
* Implemented to make layout managers align the JFileField on the baseline
* of the included text field, rather than on the absolute bottom of the
* JPanel.
*
* @param w
* @param h
* @return
*/
// @Override
// public int getBaseline(int w, int h) {
// return textField.getBaseline(w, h);
// }
// Variables declaration - do not modify
private javax.swing.JButton browseButton;
private javax.swing.JTextField textField;
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
actionPerformed(null);
}
// End of variables declaration
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import java.io.Serializable;
import java.util.LinkedHashMap;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public interface ISelection<T> extends Serializable {
public LinkedHashMap<? extends T, String> getSelections();
public T getValue();
public void setValue(T value);
public void setValueByMatch(String value);
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.config.Configuration.ConfigNode;
import java.awt.Color;
import java.awt.event.KeyEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class IntegerComponent extends StringComponent {
@Override
public void keyReleased(KeyEvent e) {
String t = getText();
if (t == null || t.equals("")) {
try {
ConfigurableField f = node.subject.getClass().getField(fieldName).getAnnotation(ConfigurableField.class);
t = f.defaultValue();
if (t == null || t.equals("")) {
t = "0";
}
// setText(t);
} catch (NoSuchFieldException ex) {
Logger.getLogger(IntegerComponent.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(IntegerComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
try {
int i = Integer.parseInt(t);
node.setFieldValue(fieldName, i);
setBackground(Color.white);
} catch (NumberFormatException ex) {
setBackground(Color.red);
}
}
public IntegerComponent(ConfigNode node, String fieldName) {
super(node, fieldName);
setColumns(10);
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A invokable action annotation means that an object method can be called by the end-user.
* This serves as a hook for keybindings as well as semantic navigation potential.
* <br/>
* Name should be short, meaningful, and succinct. e.g. "Insert disk"
* <br/>
* Category can be used to group actions by overall topic, for example an automated table of contents
* <br/>
* Description is descriptive text which provides additional clarity, e.g.
* "This will present you with a file selection dialog to pick a floppy disk image.
* Currently, dos-ordered (DSK, DO), Prodos-ordered (PO), and Nibble (NIB) formats are supported.
* <br/>
* Alternatives should be delimited by semicolons) can provide more powerful search
* For "insert disk", alternatives might be "change disk;switch disk" and
* reboot might have alternatives as "warm start;cold start;boot;restart".
* <hr/>
* NOTE: Any method that implements this must be public and take no parameters!
* If a method signature is not correct, it will result in a runtime exception
* when the action is triggered. There is no way to offer a compiler
* warning to avoid this, unfortunately.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokableAction {
/*
* Should be short and meaningful name for action being invoked, e.g. "Insert disk"
*/
public String name();
/*
* Can be used to group actions by overall topic, for example an automated table of contents
* To be determined...
*/
public String category() default "General";
/*
* More descriptive text which provides additional clarity, e.g.
* "This will present you with a file selection dialog to pick a floppy disk image.
* Currently, dos-ordered (DSK, DO), Prodos-ordered (PO), and Nibble (NIB) formats are supported."
*/
public String description() default "";
/*
* Alternatives should be delimited by semicolons) can provide more powerful search
* For "insert disk", alternatives might be "change disk;switch disk" and
* reboot might have alternatives as "warm start;cold start;boot;restart".
*/
public String alternatives() default "";
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Name {
public String value();
public String description() default "";
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public interface Reconfigurable {
public String getName();
public String getShortName();
public void reconfigure();
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.config;
import jace.config.Configuration.ConfigNode;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JTextField;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
class StringComponent extends JTextField implements KeyListener {
ConfigNode node;
String fieldName;
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
node.setFieldValue(fieldName, getText());
}
public StringComponent(ConfigNode node, String fieldName) {
this.node = node;
this.fieldName = fieldName;
synchronizeValue();
addKeyListener(this);
}
public void synchronizeValue() {
try {
Object value = node.getFieldValue(fieldName);
if (value == null) {
setText("");
} else {
setText(String.valueOf(value));
}
} catch (IllegalArgumentException ex) {
Logger.getLogger(StringComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

View File

@ -0,0 +1,167 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.config.ConfigurableField;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* CPU is a vague abstraction of a CPU. It is defined as something which can be
* debugged or traced. It has a program counter which can be incremented or
* change. Most importantly, it is a device which does something on every clock tick.
* Subclasses should implement "executeOpcode" rather than override the tick method.
* Created on January 4, 2007, 7:27 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class CPU extends Device {
/**
* Creates a new instance of CPU
*/
public CPU() {
}
@Override
public String getShortName() {
return "cpu";
}
private Debugger debugger = null;
@ConfigurableField(name = "Enable trace to STDOUT", shortName = "trace")
public boolean trace = false;
public boolean isTraceEnabled() {
return trace;
}
public void setTraceEnabled(boolean t) {
trace = t;
}
@ConfigurableField(name = "Trace length", shortName = "traceSize", description = "Number of most recent trace lines to keep for debugging errors. Zero == disabled")
public int traceLength = 0;
private ArrayList<String> traceLog = new ArrayList<>();
public boolean isLogEnabled() {
return (traceLength > 0);
}
public void log(String line) {
if (!isLogEnabled()) {
return;
}
while (traceLog.size() >= traceLength) {
traceLog.remove(0);
}
traceLog.add(line);
}
public void dumpTrace() {
Computer.pause();
ArrayList<String> newLog = new ArrayList<>();
ArrayList<String> log = traceLog;
traceLog = newLog;
Computer.resume();
System.out.println("Most recent " + traceLength + " instructions:");
log.stream().forEach((s) -> {
System.out.println(s);
});
traceLog.clear();
}
public void setDebug(Debugger d) {
debugger = d;
suspend();
}
public void clearDebug() {
debugger = null;
resume();
}
//@ConfigurableField(name="Program Counter")
public int programCounter = 0;
public int getProgramCounter() {
return programCounter;
}
public void setProgramCounter(int programCounter) {
this.programCounter = 0x00FFFF & programCounter;
}
public void incrementProgramCounter(int amount) {
this.programCounter += amount;
this.programCounter = 0x00FFFF & this.programCounter;
}
/**
* Process a single tick of the main processor clock. Either we're waiting
* to execute the next instruction, or the next instruction is ready to go
*/
@Override
public void tick() {
try {
if (debugger != null) {
if (!debugger.isActive() && debugger.hasBreakpoints()) {
debugger.getBreakpoints().stream().filter((i) -> (i == getProgramCounter())).forEach((_item) -> {
debugger.setActive(true);
});
}
if (debugger.isActive()) {
debugger.updateStatus();
if (!debugger.takeStep()) {
// If the debugger is active and we aren't ready for the next step, sleep and exit
// Without the sleep, this would constitute a very rapid-fire loop and would eat
// an unnecessary amount of CPU.
try {
Thread.sleep(10);
} catch (InterruptedException ex) {
Logger.getLogger(CPU.class.getName()).log(Level.SEVERE, null, ex);
}
return;
}
}
}
} catch (Throwable t) {
// ignore
}
executeOpcode();
}
/*
* Execute the current opcode at the current program counter
*@return number of cycles to wait until next command can be executed
*/
protected abstract void executeOpcode();
public abstract void reset();
public abstract void generateInterrupt();
abstract public void pushPC();
@Override
public void attach() {
}
@Override
public void detach() {
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.apple2e.SoftSwitches;
/**
* Card is an abstraction of an Apple ][ hardware module which can carry its own
* ROM (both CX, a 256-byte ROM which loads into memory depending on what slot
* the card is in) and the C8 ROM is a 2K ROM loaded at $C800 when the card is
* active.
*
* This class mostly just stubs out common functionality used by many different
* cards and provides a consistent interface for more advanced features like VBL
* synchronization.
* Created on February 1, 2007, 5:35 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class Card extends Device {
private PagedMemory cxRom;
private PagedMemory c8Rom;
private int slot;
private RAMListener ioListener;
private RAMListener firmwareListener;
private RAMListener c8firmwareListener;
/**
* Creates a new instance of Card
*/
public Card() {
cxRom = new PagedMemory(0x0100, PagedMemory.Type.cardFirmware);
c8Rom = new PagedMemory(0x0800, PagedMemory.Type.cardFirmware);
}
@Override
public String getShortName() {
return "s" + getSlot();
}
@Override
public String getName() {
return getDeviceName() + " (slot " + slot + ")";
}
abstract public void reset();
@Override
public void attach() {
ioListener = new RAMListener(
RAMEvent.TYPE.ANY,
RAMEvent.SCOPE.RANGE,
RAMEvent.VALUE.ANY) {
protected void doConfig() {
setScopeStart(slot * 16 + 0x00c080);
setScopeEnd(slot * 16 + 0x00c08F);
}
protected void doEvent(RAMEvent e) {
int address = e.getAddress() & 0x0f;
handleIOAccess(address, e.getType(), e.getNewValue(), e);
}
};
firmwareListener = new RAMListener(
RAMEvent.TYPE.ANY,
RAMEvent.SCOPE.RANGE,
RAMEvent.VALUE.ANY) {
protected void doConfig() {
setScopeStart(slot * 256 + 0x00c000);
setScopeEnd(slot * 256 + 0x00c0ff);
}
protected void doEvent(RAMEvent e) {
Computer.getComputer().getMemory().setActiveCard(slot);
if (SoftSwitches.CXROM.getState()) {
return;
}
handleFirmwareAccess(e.getAddress() & 0x0ff, e.getType(), e.getNewValue(), e);
}
};
c8firmwareListener = new RAMListener(
RAMEvent.TYPE.ANY,
RAMEvent.SCOPE.RANGE,
RAMEvent.VALUE.ANY) {
protected void doConfig() {
setScopeStart(slot * 256 + 0x00c800);
setScopeEnd(slot * 256 + 0x00cfff);
}
protected void doEvent(RAMEvent e) {
if (SoftSwitches.CXROM.getState()
|| Computer.getComputer().getMemory().getActiveSlot() != getSlot()
|| SoftSwitches.INTC8ROM.getState()) {
return;
}
handleC8FirmwareAccess(e.getAddress() - 0x0c800, e.getType(), e.getNewValue(), e);
}
};
Computer.getComputer().getMemory().addListener(ioListener);
Computer.getComputer().getMemory().addListener(firmwareListener);
Computer.getComputer().getMemory().addListener(c8firmwareListener);
}
@Override
public void detach() {
suspend();
Computer.getComputer().getMemory().removeListener(ioListener);
Computer.getComputer().getMemory().removeListener(firmwareListener);
Computer.getComputer().getMemory().removeListener(c8firmwareListener);
}
abstract protected void handleIOAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e);
abstract protected void handleFirmwareAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e);
abstract protected void handleC8FirmwareAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e);
public int getSlot() {
return slot;
}
public void setSlot(int slot) {
this.slot = slot;
}
public PagedMemory getCxRom() {
return cxRom;
}
public PagedMemory getC8Rom() {
return c8Rom;
}
@Override
public void reconfigure() {
boolean restart = suspend();
detach();
if (restart) {
resume();
}
attach();
}
public void notifyVBLStateChanged(boolean state) {
// Do nothing unless overridden
}
public boolean suspendWithCPU() {
return false;
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import jace.config.Reconfigurable;
import jace.library.MediaLibrary;
import jace.state.StateManager;
import java.io.IOException;
/**
* This is a very generic stub of a Computer and provides a generic set of
* overall functionality, namely boot, pause and resume features. What sort of
* memory, video and cpu get used are totally determined by fully-baked
* subclasses.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class Computer implements Reconfigurable {
static private Computer theComputer;
public RAM memory;
public CPU cpu;
public Video video;
public Keyboard keyboard;
public StateManager stateManager;
public MediaLibrary mediaLibrary = MediaLibrary.getInstance();
@ConfigurableField(category = "advanced", name = "State management", shortName = "rewind", description = "This enables rewind support, but consumes a lot of memory when active.")
public boolean enableStateManager;
/**
* Creates a new instance of Computer
*/
public Computer() {
theComputer = this;
keyboard = new Keyboard();
}
public RAM getMemory() {
return memory;
}
public void notifyVBLStateChanged(boolean state) {
for (Card c : getMemory().cards) {
if (c == null) {
continue;
}
c.notifyVBLStateChanged(state);
}
if (state && stateManager != null) {
stateManager.notifyVBLActive();
}
}
public void setMemory(RAM memory) {
if (this.memory != memory) {
if (this.memory != null) {
this.memory.detach();
}
memory.attach();
}
this.memory = memory;
}
public void waitForNextCycle() {
//@TODO IMPLEMENT TIMER SLEEP CODE!
}
public Video getVideo() {
return video;
}
public void setVideo(Video video) {
this.video = video;
}
public CPU getCpu() {
return cpu;
}
public void setCpu(CPU cpu) {
this.cpu = cpu;
}
public void loadRom(String path) throws IOException {
memory.loadRom(path);
}
@InvokableAction(
name = "Cold boot",
description = "Process startup sequence from power-up",
category = "general",
alternatives = "Full reset;reset emulator")
public abstract void coldStart();
@InvokableAction(
name = "Warm boot",
description = "Process user-initatiated reboot (ctrl+apple+reset)",
category = "general",
alternatives = "reboot;reset;three-finger-salute")
public abstract void warmStart();
static public Computer getComputer() {
return theComputer;
}
public Keyboard getKeyboard() {
return this.keyboard;
}
protected abstract boolean isRunning();
protected abstract void doPause();
protected abstract void doResume();
@InvokableAction(name = "Pause", description = "Stops the computer, allowing reconfiguration of core elements", alternatives = "freeze;halt")
public static boolean pause() {
boolean result = false;
if (theComputer != null) {
result = theComputer.isRunning();
theComputer.doPause();
}
return result;
}
@InvokableAction(name = "Resume", description = "Resumes the computer if it was previously paused", alternatives = "unpause;unfreeze;resume")
public static void resume() {
if (theComputer != null) {
theComputer.doResume();
}
}
public void reconfigure() {
if (enableStateManager) {
stateManager = StateManager.getInstance();
} else {
stateManager = null;
StateManager.getInstance().invalidate();
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import java.util.ArrayList;
import java.util.List;
/**
* A debugger has the ability to track a list of breakpoints and step a CPU one
* instruction at a time. This is a very generic abstraction used to describe
* the basic contract of what a debugger should do. EmulatorUILogic creates an
* anonymous subclass that hooks into the actual emulator.
* Created on April 16, 2007, 10:37 PM
*
* @author Administrator
*/
public abstract class Debugger {
public abstract void updateStatus();
private boolean active = false;
public boolean step = false;
public void setActive(boolean state) {
active = state;
}
public boolean isActive() {
return active;
}
private List<Integer> breakpoints = new ArrayList<Integer>();
public List<Integer> getBreakpoints() {
return breakpoints;
}
private boolean hasBreakpoints = false;
boolean hasBreakpoints() {
return hasBreakpoints;
}
public void updateBreakpoints() {
hasBreakpoints = false;
for (Integer i : breakpoints) {
if (i != null) {
hasBreakpoints = true;
}
}
}
boolean takeStep() {
if (step) {
step = false;
return true;
}
return false;
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.state.Stateful;
import jace.config.Reconfigurable;
/**
* Device is a very simple abstraction of any emulation component. A device
* performs some sort of action on every clock tick, unless it is waiting for a
* number of cycles to elapse (waitCycles > 0). A device might also be paused or
* suspended.
*
* Depending on the type of device, some special work might be required to
* attach or detach it to the active emulation (such as what should happen when
* a card is inserted or removed from a slot?)
* Created on May 10, 2007, 5:46 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public abstract class Device implements Reconfigurable {
// Number of cycles to do nothing (for cpu/video cycle accuracy)
@Stateful
private int waitCycles = 0;
@Stateful
private boolean run = true;
@Stateful
public boolean isPaused = false;
public void addWaitCycles(int wait) {
waitCycles += wait;
}
public void setWaitCycles(int wait) {
waitCycles = wait;
}
public void doTick() {
/*
if (waitCycles <= 0)
tick();
else
waitCycles--;
*/
if (!run) {
// System.out.println("Device stopped: " + getName());
isPaused = true;
return;
}
// The following might be as much as 7% faster than the above
// My guess is that the above results in a GOTO
// whereas the following pre-emptive return avoids that
if (waitCycles > 0) {
waitCycles--;
return;
}
// Implicit else...
tick();
}
public boolean isRunning() {
return run;
}
public synchronized void setRun(boolean run) {
// System.out.println(Thread.currentThread().getName() + (run ? " resuming " : " suspending ")+ getDeviceName());
isPaused = false;
this.run = run;
}
protected abstract String getDeviceName();
@Override
public String getName() {
return getDeviceName();
}
public abstract void tick();
public boolean suspend() {
if (isRunning()) {
setRun(false);
return true;
}
return false;
}
public void resume() {
setRun(true);
}
public abstract void attach();
public abstract void detach();
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
/**
* Represents the Apple ][ character font used in text modes.
* Created on January 16, 2007, 8:16 PM
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Font {
static public int[][] font;
static public int getByte(int c, int yOffset) {
return font[c][yOffset];
}
static {
font = new int[256][8];
try {
InputStream in = ClassLoader.getSystemResourceAsStream("jace/data/font.gif");
BufferedImage fontImage = ImageIO.read(in);
for (int i=0; i < 256; i++) {
int x = (i >> 4)*13 + 2;
int y = (i & 15)*13 + 4;
for (int j=0; j < 8; j++) {
int row = 0;
for (int k=0; k < 7; k++) {
int color = fontImage.getRGB((7-k)+x, j+y);
row = (row<<1) | (1-(color&1));
// row = (row<<1) | (color&1);
}
font[i][j]=row;
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
/** Creates a new instance of Font */
private Font() {
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import java.awt.event.KeyEvent;
/**
* Listen for a specific key or set of keys
* If there is a match, the handleKeyUp or handleKeyDown methods will be called.
* This is meant to save a lot of extra conditional logic elsewhere.
*
* The handler methods should return true if they have consumed the key event and do
* not want any other processing to continue for that keypress.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class KeyHandler {
public int key = 0;
public int modifiers = 0;
public KeyHandler(int key, int... modifiers) {
this.key = key;
this.modifiers = 0;
for (int m : modifiers) {
this.modifiers |= m;
}
}
public abstract boolean handleKeyUp(KeyEvent e);
public abstract boolean handleKeyDown(KeyEvent e);
}

View File

@ -0,0 +1,346 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.EmulatorUILogic;
import jace.apple2e.SoftSwitches;
import jace.apple2e.Speaker;
import jace.apple2e.softswitch.KeyboardSoftSwitch;
import jace.config.Reconfigurable;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Keyboard manages all keyboard-related activities. For now, all hotkeys are
* hard-coded. The eventual direction for this class is to only manage key
* handlers for all keys and provide remapping -- but it's not there yet.
* Created on March 29, 2007, 11:32 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Keyboard implements Reconfigurable {
@Override
public String getShortName() {
return "kbd";
}
static byte currentKey = 0;
public static void clearStrobe() {
currentKey = (byte) (currentKey & 0x07f);
}
public static void pressKey(byte key) {
currentKey = (byte) (0x0ff & (0x080 | key));
}
public static byte readState() {
// If strobe was cleared...
if ((currentKey & 0x080) == 0) {
// Call clipboard buffer paste routine
int newKey = Keyboard.getClipboardKeystroke();
if (newKey >= 0) {
pressKey((byte) newKey);
}
}
return currentKey;
}
/**
* Creates a new instance of Keyboard
*/
public Keyboard() {
}
private static Map<Integer, Set<KeyHandler>> keyHandlersByKey = new HashMap<Integer, Set<KeyHandler>>();
private static Map<Object, Set<KeyHandler>> keyHandlersByOwner = new HashMap<Object, Set<KeyHandler>>();
public static void registerKeyHandler(KeyHandler l, Object owner) {
if (!keyHandlersByKey.containsKey(l.key)) {
keyHandlersByKey.put(l.key, new HashSet<KeyHandler>());
}
keyHandlersByKey.get(l.key).add(l);
if (!keyHandlersByOwner.containsKey(owner)) {
keyHandlersByOwner.put(owner, new HashSet<KeyHandler>());
}
keyHandlersByOwner.get(owner).add(l);
}
public static void unregisterAllHandlers(Object owner) {
if (!keyHandlersByOwner.containsKey(owner)) {
return;
}
for (KeyHandler handler : keyHandlersByOwner.get(owner)) {
if (!keyHandlersByKey.containsKey(handler.key)) {
continue;
}
keyHandlersByKey.get(handler.key).remove(handler);
}
}
public static void processKeyDownEvents(KeyEvent e) {
if (keyHandlersByKey.containsKey(e.getKeyCode())) {
for (KeyHandler h : keyHandlersByKey.get(e.getKeyCode())) {
if (h.modifiers != e.getModifiers() && h.modifiers != -1) {
continue;
}
boolean isHandled = h.handleKeyDown(e);
if (isHandled) {
e.consume();
return;
}
}
}
}
public static void processKeyUpEvents(KeyEvent e) {
if (keyHandlersByKey.containsKey(e.getKeyCode())) {
for (KeyHandler h : keyHandlersByKey.get(e.getKeyCode())) {
if (h.modifiers != e.getModifiers() && h.modifiers != -1) {
continue;
}
boolean isHandled = h.handleKeyUp(e);
if (isHandled) {
e.consume();
return;
}
}
}
}
public KeyListener getListener() {
return new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
processKeyDownEvents(e);
if (e.getKeyCode() == 0 || e.isConsumed()) {
return;
}
KeyboardSoftSwitch key =
(KeyboardSoftSwitch) SoftSwitches.KEYBOARD.getSwitch();
char c = e.getKeyChar();
if ((e.getModifiers() & (KeyEvent.ALT_MASK|KeyEvent.META_MASK|KeyEvent.META_DOWN_MASK)) > 0) {
// explicit left and right here because other locations
// can be sent as well, e.g. KEY_LOCATION_STANDARD
if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT) {
pressOpenApple();
} else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT) {
pressSolidApple();
}
}
int code = e.getKeyCode();
switch (code) {
case KeyEvent.VK_LEFT:
case KeyEvent.VK_KP_LEFT:
c = 8;
break;
case KeyEvent.VK_RIGHT:
case KeyEvent.VK_KP_RIGHT:
c = 21;
break;
case KeyEvent.VK_UP:
case KeyEvent.VK_KP_UP:
c = 11;
break;
case KeyEvent.VK_DOWN:
case KeyEvent.VK_KP_DOWN:
c = 10;
break;
case KeyEvent.VK_TAB:
c = 9;
break;
case KeyEvent.VK_ENTER:
c = 13;
break;
case KeyEvent.VK_BACK_SPACE:
c = 127;
break;
default:
if ((e.getModifiers() & KeyEvent.CTRL_DOWN_MASK) > 0) {
c = (char) (code - 'A' + 1);
}
}
if (c < 128) {
pressKey((byte) c);
}
// e.consume();
}
@Override
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
processKeyUpEvents(e);
if (code == 0 || e.isConsumed()) {
return;
}
if (code == KeyEvent.VK_INSERT && e.isShiftDown()) {
doPaste();
}
if (code == KeyEvent.VK_F10) {
EmulatorUILogic.toggleDebugPanel();
}
if ((code == KeyEvent.VK_F12 || code == KeyEvent.VK_PAGE_UP || code == KeyEvent.VK_BACK_SPACE || code == KeyEvent.VK_PAUSE) && ((e.getModifiers() & KeyEvent.CTRL_MASK) > 0)) {
Computer.getComputer().warmStart();
}
if (code == KeyEvent.VK_F1) {
EmulatorUILogic.showMediaManager();
}
if (code == KeyEvent.VK_F4) {
EmulatorUILogic.showConfig();
}
if (code == KeyEvent.VK_F7) {
Speaker.toggleFileOutput();
}
if (code == KeyEvent.VK_F8) {
EmulatorUILogic.scaleIntegerRatio();
}
if (code == KeyEvent.VK_F9) {
EmulatorUILogic.toggleFullscreen();
}
if (code == KeyEvent.VK_PRINTSCREEN || code == KeyEvent.VK_SCROLL_LOCK) {
try {
if (e.isShiftDown()) {
EmulatorUILogic.saveScreenshotRaw();
} else {
EmulatorUILogic.saveScreenshot();
}
} catch (IOException ex) {
Logger.getLogger(Keyboard.class.getName()).log(Level.SEVERE, null, ex);
}
Computer.resume();
}
if ((e.getModifiers() & (KeyEvent.ALT_MASK|KeyEvent.META_MASK|KeyEvent.META_DOWN_MASK)) > 0) {
// explicit left and right here because other locations
// can be sent as well, e.g. KEY_LOCATION_STANDARD
if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT) {
releaseOpenApple();
} else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT) {
releaseSolidApple();
}
}
e.consume();
// e.setKeyChar((char) 0);
// e.setKeyCode(0);
}
private void pressOpenApple() {
Computer.pause();
SoftSwitches.PB0.getSwitch().setState(true);
Computer.resume();
}
private void pressSolidApple() {
Computer.pause();
SoftSwitches.PB1.getSwitch().setState(true);
Computer.resume();
}
private void releaseOpenApple() {
Computer.pause();
SoftSwitches.PB0.getSwitch().setState(false);
Computer.resume();
}
private void releaseSolidApple() {
Computer.pause();
SoftSwitches.PB1.getSwitch().setState(false);
Computer.resume();
}
};
}
public static void doPaste(String text) {
pasteBuffer = new StringReader(text);
}
private static void doPaste() {
try {
Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
String contents = (String) clip.getData(DataFlavor.stringFlavor);
if (contents != null && !"".equals(contents)) {
contents = contents.replaceAll("\\n(\\r)?", (char) 0x0d + "");
pasteBuffer = new StringReader(contents);
}
} catch (UnsupportedFlavorException ex) {
Logger.getLogger(Keyboard.class
.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(Keyboard.class
.getName()).log(Level.SEVERE, null, ex);
}
}
static StringReader pasteBuffer = null;
public static int getClipboardKeystroke() {
if (pasteBuffer == null) {
return -1;
}
try {
int keypress = pasteBuffer.read();
// Handle end of paste buffer
if (keypress == -1) {
pasteBuffer.close();
pasteBuffer = null;
return -1;
}
KeyboardSoftSwitch key =
(KeyboardSoftSwitch) SoftSwitches.KEYBOARD.getSwitch();
return (keypress & 0x0ff);
} catch (IOException ex) {
Logger.getLogger(Keyboard.class
.getName()).log(Level.SEVERE, null, ex);
}
return -1;
}
@Override
public String getName() {
return "Keyboard";
}
@Override
public void reconfigure() {
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.apple2e.SoftSwitches;
import jace.apple2e.Speaker;
import jace.config.ConfigurableField;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Motherboard is the heart of the computer. It can have a list of cards
* inserted (the behavior and number of cards is determined by the Memory class)
* as well as a speaker and any other miscellaneous devices (e.g. joysticks).
* This class provides the real main loop of the emulator, and is responsible
* for all timing as well as the pause/resume features used to prevent resource
* collisions between threads. Created on May 1, 2007, 11:22 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Motherboard extends TimedDevice {
static final Computer computer = Computer.getComputer();
static final CPU cpu = computer.getCpu();
static Motherboard instance;
static final public Set<Device> miscDevices = new HashSet<Device>();
@ConfigurableField(name = "Enable Speaker", shortName = "speaker", defaultValue = "true")
public static boolean enableSpeaker = true;
public static Speaker speaker;
public static SoundMixer mixer = new SoundMixer();
static void vblankEnd() {
SoftSwitches.VBL.getSwitch().setState(true);
computer.notifyVBLStateChanged(true);
}
static void vblankStart() {
SoftSwitches.VBL.getSwitch().setState(false);
computer.notifyVBLStateChanged(false);
}
/**
* Creates a new instance of Motherboard
*/
public Motherboard() {
instance = this;
}
protected String getDeviceName() {
return "Motherboard";
}
@Override
public String getShortName() {
return "mb";
}
@ConfigurableField(category = "advanced", name = "CPU per clock", defaultValue = "1", description = "Number of CPU cycles per clock cycle (normal = 1)")
public static int cpuPerClock = 1;
public int clockCounter = 1;
public Card[] cards;
public void tick() {
try {
clockCounter--;
cpu.doTick();
if (clockCounter > 0) {
return;
}
clockCounter = cpuPerClock;
Computer.getComputer().getVideo().doTick();
// Unrolled loop since this happens so often
if (cards[0] != null) {
cards[0].doTick();
}
if (cards[1] != null) {
cards[1].doTick();
}
if (cards[2] != null) {
cards[2].doTick();
}
if (cards[3] != null) {
cards[3].doTick();
}
if (cards[4] != null) {
cards[4].doTick();
}
if (cards[5] != null) {
cards[5].doTick();
}
if (cards[6] != null) {
cards[6].doTick();
}
for (Device m : miscDevices) {
m.doTick();
}
} catch (Throwable t) {
t.printStackTrace();
}
}
// From the holy word of Sather 3:5 (Table 3.1) :-)
// This average speed averages in the "long" cycles
public static long SPEED = 1020484L; // (NTSC)
//public static long SPEED = 1015625L; // (PAL)
public long defaultCyclesPerSecond() {
return SPEED;
}
public synchronized void reconfigure() {
boolean startAgain = pause();
accelorationRequestors.clear();
super.reconfigure();
Card[] cards = computer.getMemory().getAllCards();
// Now create devices as needed, e.g. sound
Motherboard.miscDevices.add(mixer);
mixer.reconfigure();
if (enableSpeaker) {
try {
if (speaker == null) {
speaker = new Speaker();
} else {
speaker.attach();
}
if (mixer.lineAvailable) {
Motherboard.miscDevices.add(speaker);
}
} catch (Throwable t) {
System.out.println("Unable to initalize sound -- deactivating speaker out");
speaker.detach();
Motherboard.miscDevices.remove(speaker);
}
} else {
if (speaker != null) {
speaker.detach();
Motherboard.miscDevices.remove(speaker);
}
}
if (startAgain && Computer.getComputer().getMemory() != null) {
resume();
}
}
static HashSet<Object> accelorationRequestors = new HashSet<Object>();
static public void requestSpeed(Object requester) {
accelorationRequestors.add(requester);
if (instance != null) {
instance.enableTempMaxSpeed();
}
}
static public void cancelSpeedRequest(Object requester) {
accelorationRequestors.remove(requester);
if (instance != null && accelorationRequestors.isEmpty()) {
instance.disableTempMaxSpeed();
}
}
@Override
public void attach() {
}
Map<Card, Boolean> resume = new HashMap<Card, Boolean>();
@Override
public boolean suspend() {
synchronized (resume) {
resume.clear();
for (Card c : cards) {
if (c == null || !c.suspendWithCPU() || !c.isRunning()) {
continue;
}
resume.put(c, c.suspend());
}
}
return super.suspend();
}
@Override
public void resume() {
cards = computer.getMemory().getAllCards();
super.resume();
synchronized (resume) {
for (Card c : cards) {
if (Boolean.TRUE.equals(resume.get(c))) {
c.resume();
}
}
}
}
@Override
public void detach() {
for (Device d : miscDevices) {
d.suspend();
}
miscDevices.clear();
// halt();
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.state.StateManager;
import jace.state.Stateful;
import java.util.Arrays;
/**
* This represents bank-switchable ram which can reside at fixed portions of the
* computer's memory. This makes it possible to switch out memory pages in a
* very efficient manner so that the MMU abstraction doesn't bury the rest of
* the emulator in messy conditionals.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public class PagedMemory {
public enum Type {
cardFirmware(0x0c800),
languageCard(0x0d000),
firmwareMain(0x0d000),
firmware80column(0x0c300),
slotRom(0x0c100),
ram(0x0000);
protected int baseAddress;
private Type(int newBase) {
baseAddress = newBase;
}
public int getBaseAddress() {
return baseAddress;
}
}
// This is a fixed array, used for internal-only!!
@Stateful
public byte[][] internalMemory = new byte[0][];
@Stateful
public Type type;
/**
* Creates a new instance of PagedMemory
*/
public PagedMemory(int size, Type memType) {
type = memType;
internalMemory = new byte[size >> 8][256];
for (int i = 0; i < size; i += 256) {
byte[] b = new byte[256];
Arrays.fill(b, (byte) 0x00);
internalMemory[i >> 8] = b;
}
}
public PagedMemory(byte[] romData, Type memType) {
type = memType;
loadData(romData);
}
public void loadData(byte[] romData) {
for (int i = 0; i < romData.length; i += 256) {
byte[] b = new byte[256];
for (int j = 0; j < 256; j++) {
b[j] = romData[i + j];
}
internalMemory[i >> 8] = b;
}
}
public void loadData(byte[] romData, int offset, int length) {
for (int i = 0; i < length; i += 256) {
byte[] b = new byte[256];
for (int j = 0; j < 256; j++) {
b[j] = romData[offset + i + j];
}
internalMemory[i >> 8] = b;
}
}
public byte[][] getMemory() {
return internalMemory;
}
public byte[] get(int pageNumber) {
return internalMemory[pageNumber];
}
public void set(int pageNumber, byte[] bank) {
internalMemory[pageNumber] = bank;
}
public byte[] getMemoryPage(int memoryBase) {
int offset = memoryBase - type.baseAddress;
// int page = offset >> 8;
int page = (offset >> 8) & 0x0ff;
// return get(page);
return internalMemory[page];
}
public void setBanks(int sourceStart, int sourceLength, int targetStart, PagedMemory source) {
for (int i = 0; i < sourceLength; i++) {
set(targetStart + i, source.get(sourceStart + i));
}
}
public byte readByte(int address) {
return getMemoryPage(address)[address & 0x0ff];
}
public void writeByte(int address, byte value) {
byte[] page = getMemoryPage(address);
StateManager.markDirtyValue(page);
getMemoryPage(address)[address & 0x0ff] = value;
}
public void fillBanks(PagedMemory source) {
byte[][] sourceMemory = source.getMemory();
int sourceBase = source.type.getBaseAddress() >> 8;
int thisBase = type.getBaseAddress() >> 8;
int start = sourceBase > thisBase ? sourceBase : thisBase;
int sourceEnd = sourceBase + source.getMemory().length;
int thisEnd = thisBase + getMemory().length;
int end = sourceEnd < thisEnd ? sourceEnd : thisEnd;
for (int i = start; i < end; i++) {
set(i - thisBase, sourceMemory[i - sourceBase]);
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import java.awt.Color;
/**
* Fixed color palette -- only used for the older DHGR renderer (the new NTSC renderer uses its own YUV conversion and builds its own palettes)
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Palette {
private Palette() {}
static public final int BLACK = 0;
static public final int VIOLET = 3;
static public final int BLUE = 6;
static public final int ORANGE = 9;
static public final int GREEN = 12;
static public final int WHITE = 15;
static public Color[] color;
static {
color = new Color[16];
color[ 0] = new Color( 0, 0, 0);
color[ 1] = new Color(208, 0, 48);
color[ 2] = new Color( 0, 0,128);
color[ 3] = new Color(255, 0,255);
color[ 4] = new Color( 0,128, 0);
color[ 5] = new Color(128,128,128);
color[ 6] = new Color( 0, 0,255);
color[ 7] = new Color( 96,160,255);
color[ 8] = new Color(128, 80, 0);
color[ 9] = new Color(255,128, 0);
color[10] = new Color(192,192,192);
color[11] = new Color(255,144,128);
color[12] = new Color( 0,255, 0);
color[13] = new Color(255,255, 0);
color[14] = new Color( 64,255,144);
color[15] = new Color(255,255,255);
}
}

View File

@ -0,0 +1,295 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.apple2e.SoftSwitches;
import jace.config.Reconfigurable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* RAM is a 64K address space of paged memory. It also manages sets of memory
* listeners, used by I/O as well as emulator add-ons (and cheats). RAM also
* manages cards in the emulator because they are tied into the MMU memory
* bankswitch logic.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class RAM implements Reconfigurable {
public PagedMemory activeRead;
public PagedMemory activeWrite;
public List<RAMListener> listeners;
public List<RAMListener>[] listenerMap;
public List<RAMListener>[] ioListenerMap;
protected Card[] cards;
// card 0 = 80 column card firmware / system rom
public int activeSlot = 0;
/**
* Creates a new instance of RAM
*/
public RAM() {
listeners = new Vector<RAMListener>();
cards = new Card[8];
refreshListenerMap();
}
public void setActiveCard(int slot) {
if (activeSlot != slot) {
activeSlot = slot;
configureActiveMemory();
} else if (!SoftSwitches.CXROM.getState()) {
configureActiveMemory();
}
}
public int getActiveSlot() {
return activeSlot;
}
public Card[] getAllCards() {
return cards;
}
public Card getCard(int slot) {
if (slot >= 1 && slot <= 7) {
return cards[slot];
}
return null;
}
public void addCard(Card c, int slot) {
cards[slot] = c;
c.setSlot(slot);
c.attach();
}
public void removeCard(Card c) {
c.suspend();
c.detach();
removeCard(c.getSlot());
}
public void removeCard(int slot) {
if (cards[slot] != null) {
cards[slot].suspend();
cards[slot].detach();
}
cards[slot] = null;
}
abstract public void configureActiveMemory();
public byte write(int address, byte b, boolean generateEvent, boolean requireSynchronization) {
byte[] page = activeWrite.getMemoryPage(address);
byte old = 0;
if (page == null) {
if (generateEvent) {
callListener(RAMEvent.TYPE.WRITE, address, old, b, requireSynchronization);
}
} else {
int offset = address & 0x0FF;
old = page[offset];
if (generateEvent) {
b = callListener(RAMEvent.TYPE.WRITE, address, old, b, requireSynchronization);
}
page[offset] = b;
}
return old;
}
public void writeWord(int address, int w, boolean generateEvent, boolean requireSynchronization) {
int lsb = write(address, (byte) (w & 0x0ff), generateEvent, requireSynchronization);
int msb = write(address + 1, (byte) (w >> 8), generateEvent, requireSynchronization);
// int oldValue = msb << 8 + lsb;
}
public byte readRaw(int address) {
// if (address >= 65536) return 0;
return activeRead.getMemoryPage(address)[address & 0x0FF];
}
public byte read(int address, RAMEvent.TYPE eventType, boolean triggerEvent, boolean requireSyncronization) {
// if (address >= 65536) return 0;
byte value = activeRead.getMemoryPage(address)[address & 0x0FF];
// if (triggerEvent || ((address & 0x0FF00) == 0x0C000)) {
if (triggerEvent || (address & 0x0FFF0) == 0x0c030) {
value = callListener(eventType, address, value, value, requireSyncronization);
}
return value;
}
public int readWordRaw(int address) {
int lsb = 0x00ff & readRaw(address);
int msb = (0x00ff & readRaw(address + 1)) << 8;
return msb + lsb;
}
public int readWord(int address, RAMEvent.TYPE eventType, boolean triggerEvent, boolean requireSynchronization) {
int lsb = 0x00ff & read(address, eventType, triggerEvent, requireSynchronization);
int msb = (0x00ff & read(address + 1, eventType, triggerEvent, requireSynchronization)) << 8;
int value = msb + lsb;
// if (generateEvent) {
// callListener(RAMEvent.TYPE.READ, address, value, value);
// }
return value;
}
private void mapListener(RAMListener l, int address) {
if ((address & 0x0FF00) == 0x0C000) {
int index = address & 0x0FF;
List<RAMListener> ioListeners = ioListenerMap[index];
if (ioListeners == null) {
ioListeners = new ArrayList<>();
ioListenerMap[index] = ioListeners;
}
if (!ioListeners.contains(l)) {
ioListeners.add(l);
}
} else {
int index = address >> 8;
List<RAMListener> otherListeners = listenerMap[index];
if (otherListeners == null) {
otherListeners = new ArrayList<>();
listenerMap[index] = otherListeners;
}
if (!otherListeners.contains(l)) {
otherListeners.add(l);
}
}
}
private void addListenerRange(RAMListener l) {
if (l.getScope() == RAMEvent.SCOPE.ADDRESS) {
mapListener(l, l.getScopeStart());
} else {
int start = 0;
int end = 0x0ffff;
if (l.getScope() == RAMEvent.SCOPE.RANGE) {
start = l.getScopeStart();
end = l.getScopeEnd();
}
for (int i = start; i <= end; i++) {
mapListener(l, i);
}
}
}
private void refreshListenerMap() {
listenerMap = new ArrayList[256];
ioListenerMap = new ArrayList[256];
for (RAMListener l : listeners) {
addListenerRange(l);
}
}
public void addListener(final RAMListener l) {
boolean restart = Computer.pause();
if (listeners.contains(l)) {
return;
}
listeners.add(l);
addListenerRange(l);
if (restart) {
Computer.resume();
}
}
public void removeListener(final RAMListener l) {
boolean restart = Computer.pause();
listeners.remove(l);
refreshListenerMap();
if (restart) {
Computer.resume();
}
}
public byte callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue, boolean requireSyncronization) {
List<RAMListener> activeListeners = null;
if (requireSyncronization) {
Computer.getComputer().getCpu().suspend();
}
if ((address & 0x0FF00) == 0x0C000) {
activeListeners = ioListenerMap[address & 0x0FF];
if (activeListeners == null && t.isRead()) {
if (requireSyncronization) {
Computer.getComputer().getCpu().resume();
}
return Computer.getComputer().getVideo().getFloatingBus();
}
} else {
activeListeners = listenerMap[(address >> 8) & 0x0ff];
}
if (activeListeners != null) {
RAMEvent e = new RAMEvent(t, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, address, oldValue, newValue);
for (RAMListener l : activeListeners) {
l.handleEvent(e);
}
if (requireSyncronization) {
Computer.getComputer().getCpu().resume();
}
return (byte) e.getNewValue();
}
if (requireSyncronization) {
Computer.getComputer().getCpu().resume();
}
return (byte) newValue;
}
abstract protected void loadRom(String path) throws IOException;
public void dump() {
for (int i = 0; i < 0x0FFFF; i += 16) {
System.out.print(Integer.toString(i, 16));
System.out.print(":");
String part1 = "";
String part2 = "";
for (int j = 0; j < 16; j++) {
int a = i + j;
int br = 0x0FF & activeRead.getMemory()[i >> 8][i & 0x0ff];
String s1 = Integer.toString(br, 16);
System.out.print(' ');
if (s1.length() == 1) {
System.out.print('0');
}
System.out.print(s1);
/*
try {
int bw = 0;
bw = 0x0FF & activeWrite.getMemory().get(a/256)[a%256];
String s2 = (br == bw) ? "**" : Integer.toString(bw,16);
System.out.print(' ');
if (s2.length()==1) System.out.print('0');
System.out.print(s2);
} catch (NullPointerException ex) {
System.out.print(" --");
}
*/
}
System.out.println();
// System.out.println(Integer.toString(i, 16)+":"+part1+" -> "+part2);
}
}
abstract public void attach();
abstract public void detach();
}

View File

@ -0,0 +1,132 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
/**
* A RAM event is defined as anything that causes a read or write to the
* mainboard RAM of the computer. This could be the result of an indirect
* address fetch (indirect addressing) as well as direct or indexed operator
* addressing modes.
*
* It is also possible to track if the read is an opcode read, indicating that
* the CPU is executing the given memory location at that moment.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class RAMEvent {
public enum TYPE {
READ(true),
READ_DATA(true),
EXECUTE(true),
READ_OPERAND(true),
WRITE(false),
ANY(false);
boolean read = false;
TYPE(boolean r) {
this.read = r;
}
public boolean isRead() {
return read;
}
};
public enum SCOPE {
ADDRESS,
RANGE,
ANY
};
public enum VALUE {
ANY,
RANGE,
EQUALS,
NOT_EQUALS,
CHANGE_BY
};
private TYPE type;
private SCOPE scope;
private VALUE value;
private int address, oldValue, newValue;
/**
* Creates a new instance of RAMEvent
*/
public RAMEvent(TYPE t, SCOPE s, VALUE v, int address, int oldValue, int newValue) {
setType(t);
setScope(s);
setValue(v);
this.setAddress(address);
this.setOldValue(oldValue);
this.setNewValue(newValue);
}
public TYPE getType() {
return type;
}
public final void setType(TYPE type) {
this.type = type;
}
public SCOPE getScope() {
return scope;
}
public final void setScope(SCOPE scope) {
this.scope = scope;
}
public VALUE getValue() {
return value;
}
public final void setValue(VALUE value) {
this.value = value;
}
public int getAddress() {
return address;
}
public final void setAddress(int address) {
this.address = address;
}
public int getOldValue() {
return oldValue;
}
public final void setOldValue(int oldValue) {
this.oldValue = oldValue;
}
public int getNewValue() {
return newValue;
}
public final void setNewValue(int newValue) {
this.newValue = newValue;
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.core.RAMEvent.TYPE;
/**
* A Ram Listener waits for a specific ram event, as specified by access type
* (read/write/execute, etc) or a memory address, or range of addresses. The
* subclass must define the address range (scope start/end) via the doConfig
* method. Ram listeners are used all over the emulator, but especially in cheat
* modules and the softswitch and I/O cards.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class RAMListener {
private RAMEvent.TYPE type;
private RAMEvent.SCOPE scope;
private RAMEvent.VALUE value;
private int scopeStart;
private int scopeEnd;
private int valueStart;
private int valueEnd;
private int valueAmount;
/**
* Creates a new instance of RAMListener
*/
public RAMListener(RAMEvent.TYPE t, RAMEvent.SCOPE s, RAMEvent.VALUE v) {
setType(t);
setScope(s);
setValue(v);
doConfig();
}
public RAMEvent.TYPE getType() {
return type;
}
public final void setType(RAMEvent.TYPE type) {
this.type = type;
}
public RAMEvent.SCOPE getScope() {
return scope;
}
public final void setScope(RAMEvent.SCOPE scope) {
this.scope = scope;
}
public RAMEvent.VALUE getValue() {
return value;
}
public final void setValue(RAMEvent.VALUE value) {
this.value = value;
}
public int getScopeStart() {
return scopeStart;
}
public void setScopeStart(int scopeStart) {
this.scopeStart = scopeStart;
}
public int getScopeEnd() {
return scopeEnd;
}
public void setScopeEnd(int scopeEnd) {
this.scopeEnd = scopeEnd;
}
public int getValueStart() {
return valueStart;
}
public void setValueStart(int valueStart) {
this.valueStart = valueStart;
}
public int getValueEnd() {
return valueEnd;
}
public void setValueEnd(int valueEnd) {
this.valueEnd = valueEnd;
}
public int getValueAmount() {
return valueAmount;
}
public void setValueAmount(int valueAmount) {
this.valueAmount = valueAmount;
}
public boolean isRelevant(RAMEvent e) {
// Skip event if it's not the right type
if (type != TYPE.ANY && e.getType() != TYPE.ANY) {
if ((type != e.getType())) {
if (type == TYPE.READ) {
if (!e.getType().isRead()) {
return false;
}
} else {
return false;
}
}
}
// Skip event if it's not in the scope we care about
if (scope != RAMEvent.SCOPE.ANY) {
if (scope == RAMEvent.SCOPE.ADDRESS && e.getAddress() != scopeStart) {
return false;
} else if (scope == RAMEvent.SCOPE.RANGE && (e.getAddress() < scopeStart || e.getAddress() > scopeEnd)) {
return false;
}
}
// Skip event if the value modification is uninteresting
if (value != RAMEvent.VALUE.ANY) {
if (value == RAMEvent.VALUE.CHANGE_BY && e.getNewValue() - e.getOldValue() != valueAmount) {
return false;
} else if (value == RAMEvent.VALUE.EQUALS && e.getNewValue() != valueAmount) {
return false;
} else if (value == RAMEvent.VALUE.NOT_EQUALS && e.getNewValue() == valueAmount) {
return false;
} else if (value == RAMEvent.VALUE.RANGE && (e.getNewValue() < valueStart || e.getNewValue() > valueEnd)) {
return false;
}
}
// Ok, so we've filtered out the uninteresting stuff
// If we've made it this far then the event is valid.
return true;
}
public void handleEvent(RAMEvent e) {
if (isRelevant(e)) {
doEvent(e);
}
}
abstract protected void doConfig();
abstract protected void doEvent(RAMEvent e);
}

View File

@ -0,0 +1,285 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.state.Stateful;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A softswitch is a hidden bit that lives in the MMU, it can be activated or
* deactivated to change operating characteristics of the computer such as video
* display mode or memory paging model. Other special softswitches access
* keyboard and speaker ports. The underlying mechanic of softswitches is
* managed by the RamListener/Ram model and, in the case of video modes, the
* Video classes.
*
* The implementation of softswitches is in jace.apple2e.SoftSwitches
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
* @see jace.apple2e.SoftSwitches
*/
public abstract class SoftSwitch {
@Stateful
public Boolean state;
private Boolean initalState;
private List<RAMListener> listeners;
private final List<Integer> exclusionActivate = new ArrayList<>();
private final List<Integer> exclusionDeactivate = new ArrayList<>();
private final List<Integer> exclusionQuery = new ArrayList<>();
private String name;
private boolean toggleType = false;
/**
* Creates a new instance of SoftSwitch
*
* @param name
* @param initalState
*/
public SoftSwitch(String name, Boolean initalState) {
this.initalState = initalState;
this.state = initalState;
this.listeners = new ArrayList<>();
this.name = name;
}
public SoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) {
if (onAddress == offAddress && onAddress != -1) {
toggleType = true;
// System.out.println("Switch " + name + " is a toggle type switch!");
}
this.initalState = initalState;
this.state = initalState;
this.listeners = new ArrayList<>();
this.name = name;
int[] onAddresses = null;
int[] offAddresses = null;
int[] queryAddressList = null;
if (onAddress >= 0) {
onAddresses = new int[]{onAddress};
}
if (offAddress >= 0) {
offAddresses = new int[]{offAddress};
}
if (queryAddress >= 0) {
queryAddressList = new int[]{queryAddress};
}
init(offAddresses, onAddresses, queryAddressList, changeType);
}
public SoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) {
this(name, initalState);
init(offAddrs, onAddrs, queryAddrs, changeType);
}
private void init(int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType) {
if (toggleType) {
List<Integer> addrs = new ArrayList<>();
for (int i : onAddrs) {
addrs.add(i);
}
Collections.sort(addrs);
final int beginAddr = addrs.get(0);
final int endAddr = addrs.get(addrs.size() - 1);
for (int i = beginAddr; i < endAddr; i++) {
if (!addrs.contains(i)) {
exclusionActivate.add(i);
}
}
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
setScopeEnd(endAddr);
}
@Override
protected void doEvent(RAMEvent e) {
if (!exclusionActivate.contains(e.getAddress())) {
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
setState(!getState());
}
}
};
addListener(l);
} else {
if (onAddrs != null) {
List<Integer> addrs = new ArrayList<>();
for (int i : onAddrs) {
addrs.add(i);
}
Collections.sort(addrs);
final int beginAddr = addrs.get(0);
final int endAddr = addrs.get(addrs.size() - 1);
for (int i = beginAddr; i < endAddr; i++) {
if (!addrs.contains(i)) {
exclusionActivate.add(i);
}
}
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
setScopeEnd(endAddr);
}
@Override
protected void doEvent(RAMEvent e) {
if (!exclusionActivate.contains(e.getAddress())) {
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
setState(true);
}
}
};
addListener(l);
}
if (offAddrs != null) {
List<Integer> addrs = new ArrayList<>();
for (int i : offAddrs) {
addrs.add(i);
}
final int beginAddr = addrs.get(0);
final int endAddr = addrs.get(addrs.size() - 1);
for (int i = beginAddr; i < endAddr; i++) {
if (!addrs.contains(i)) {
exclusionDeactivate.add(i);
}
}
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
setScopeEnd(endAddr);
}
@Override
protected void doEvent(RAMEvent e) {
if (!exclusionDeactivate.contains(e.getAddress())) {
setState(false);
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" disables switch "+getName());
}
}
};
addListener(l);
}
}
if (queryAddrs != null) {
List<Integer> addrs = new ArrayList<>();
for (int i : queryAddrs) {
addrs.add(i);
}
final int beginAddr = addrs.get(0);
final int endAddr = addrs.get(addrs.size() - 1);
for (int i = beginAddr; i < endAddr; i++) {
if (!addrs.contains(i)) {
exclusionQuery.add(i);
}
}
// RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
RAMListener l = new RAMListener(RAMEvent.TYPE.READ, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
setScopeEnd(endAddr);
}
@Override
protected void doEvent(RAMEvent e) {
if (!exclusionQuery.contains(e.getAddress())) {
e.setNewValue(0x0ff & readSwitch());
// System.out.println("Read from "+Integer.toHexString(e.getAddress())+" returns "+Integer.toHexString(e.getNewValue()));
}
}
};
addListener(l);
}
}
public boolean inhibit() {
return false;
}
abstract protected byte readSwitch();
protected void addListener(RAMListener l) {
listeners.add(l);
}
public String getName() {
return name;
}
public void reset() {
if (initalState != null) {
setState(initalState);
}
}
public void register() {
RAM m = Computer.getComputer().getMemory();
listeners.stream().forEach((l) -> {
m.addListener(l);
});
}
public void unregister() {
RAM m = Computer.getComputer().getMemory();
listeners.stream().forEach((l) -> {
m.removeListener(l);
});
}
public void setState(boolean newState) {
if (inhibit()) {
return;
}
// if (this != SoftSwitches.VBL.getSwitch() &&
// this != SoftSwitches.KEYBOARD.getSwitch())
// System.out.println("Switch "+name+" set to "+newState);
state = newState;
/*
if (queryAddresses != null) {
RAM m = Computer.getComputer().getMemory();
for (int i:queryAddresses) {
byte old = m.read(i, false);
m.write(i, (byte) (old & 0x7f | (state ? 0x080:0x000)), false);
}
}
*/
stateChanged();
}
public final boolean getState() {
if (state == null) {
return false;
}
return state;
}
abstract public void stateChanged();
@Override
public String toString() {
return getName() + (getState() ? ":1" : ":0");
}
}

View File

@ -0,0 +1,264 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.config.ConfigurableField;
import jace.config.DynamicSelection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.Mixer.Info;
import javax.sound.sampled.SourceDataLine;
/**
* Manages sound resources used by various audio devices (such as speaker and
* mockingboard cards.) The plumbing is managed in this class so that the
* consumers do not have to do a lot of work to manage mixer lines or deal with
* how to reuse active lines if needed. It is possible that this class might be
* used to manage volume in the future, but that remains to be seen.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class SoundMixer extends Device {
private final Set<SourceDataLine> availableLines = Collections.synchronizedSet(new HashSet<SourceDataLine>());
private final Map<Object, SourceDataLine> activeLines = Collections.synchronizedMap(new HashMap<Object, SourceDataLine>());
/**
* Bits per sample
*/
@ConfigurableField(name = "Bits per sample", shortName = "bits")
public static int BITS = 16;
/**
* Sample playback rate
*/
@ConfigurableField(name = "Playback Rate", shortName = "freq")
public static int RATE = 48000;
/**
* Sound format used for playback
*/
private AudioFormat af;
/**
* Is sound line available for playback at all?
*/
public boolean lineAvailable;
@ConfigurableField(name = "Audio device", description = "Audio output device")
public static DynamicSelection<String> preferredMixer = new DynamicSelection<String>(null) {
@Override
public boolean allowNull() {
return false;
}
@Override
public LinkedHashMap<? extends String, String> getSelections() {
Info[] mixerInfo = AudioSystem.getMixerInfo();
LinkedHashMap<String, String> out = new LinkedHashMap<>();
for (Info i : mixerInfo) {
out.put(i.getName(), i.getName());
}
return out;
}
};
private Mixer theMixer;
@Override
public String getDeviceName() {
return "Sound Output";
}
@Override
public String getShortName() {
return "mixer";
}
@Override
public synchronized void reconfigure() {
detach();
try {
initMixer();
if (lineAvailable) {
initAudio();
System.out.println("Started sound");
} else {
System.out.println("Sound not stared: Line not available");
}
} catch (LineUnavailableException ex) {
System.out.println("Unable to start sound");
Logger.getLogger(SoundMixer.class.getName()).log(Level.SEVERE, null, ex);
}
attach();
}
/**
* Obtain sound playback line if available
*
* @throws javax.sound.sampled.LineUnavailableException If there is no line
* available
*/
private void initAudio() throws LineUnavailableException {
af = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, RATE, BITS, 2, BITS / 4, RATE, true);
// af = new AudioFormat(RATE, BITS, 2, true, true);
DataLine.Info dli = new DataLine.Info(SourceDataLine.class, af);
lineAvailable = AudioSystem.isLineSupported(dli);
}
public synchronized SourceDataLine getLine(Object requester) throws LineUnavailableException {
if (activeLines.containsKey(requester)) {
return activeLines.get(requester);
}
SourceDataLine sdl = null;
if (availableLines.isEmpty()) {
sdl = getNewLine();
} else {
sdl = availableLines.iterator().next();
availableLines.remove(sdl);
}
activeLines.put(requester, sdl);
sdl.start();
return sdl;
}
public void returnLine(Object requester) {
if (activeLines.containsKey(requester)) {
SourceDataLine sdl = activeLines.remove(requester);
// Calling drain on pulse driver can cause it to freeze up (?)
// sdl.drain();
sdl.stop();
sdl.flush();
availableLines.add(sdl);
}
}
private SourceDataLine getNewLine() throws LineUnavailableException {
SourceDataLine l = null;
// Line.Info[] info = theMixer.getSourceLineInfo();
DataLine.Info dli = new DataLine.Info(SourceDataLine.class, af);
System.out.println("Maximum output lines: " + theMixer.getMaxLines(dli));
System.out.println("Allocated output lines: " + theMixer.getSourceLines().length);
System.out.println("Getting source line from " + theMixer.getMixerInfo().toString() + ": " + af.toString());
try {
l = (SourceDataLine) theMixer.getLine(dli);
} catch (IllegalArgumentException e) {
lineAvailable = false;
throw new LineUnavailableException(e.getMessage());
} catch (LineUnavailableException e) {
lineAvailable = false;
throw e;
}
if (!(l instanceof SourceDataLine)) {
lineAvailable = false;
throw new LineUnavailableException("Line is not an output line!");
}
final SourceDataLine sdl = (SourceDataLine) l;
// sdl.open(af);
// if (false) {
// return sdl;
// }
sdl.open();
sdl.start();
// new Thread(new Runnable() {
// @Override
// public void run() {
// System.out.println("Going into an infinite loop!!!");
// try {
// while (true) {
// sdl.write(new byte[]{randomByte(),randomByte(),randomByte(),randomByte()}, 0, 4);
// }
// } catch (Throwable t) {
// t.printStackTrace();
// }
// System.out.println("Thread dying...");
// }
// }).start();
return sdl;
}
public byte randomByte() {
return (byte) (Math.random() * 256);
}
@Override
public void tick() {
}
@Override
public void attach() {
// if (Motherboard.enableSpeaker)
// Motherboard.speaker.attach();
}
@Override
public void detach() {
availableLines.stream().forEach((line) -> {
line.close();
});
Set requesters = new HashSet(activeLines.keySet());
requesters.stream().map((o) -> {
if (o instanceof Device) {
((Device) o).detach();
}
return o;
}).filter((o) -> (o instanceof Card)).forEach((o) -> {
((Card) o).reconfigure();
});
if (theMixer != null) {
for (Line l : theMixer.getSourceLines()) {
l.close();
}
}
availableLines.clear();
activeLines.clear();
}
private void initMixer() {
Info selected = null;
Info[] mixerInfo = AudioSystem.getMixerInfo();
if (mixerInfo == null || mixerInfo.length == 0) {
theMixer = null;
lineAvailable = false;
System.out.println("No sound mixer is available!");
return;
}
String mixer = preferredMixer.getValue();
selected = mixerInfo[0];
for (Info i : mixerInfo) {
if (i.getName().equalsIgnoreCase(mixer)) {
selected = i;
break;
}
}
theMixer = AudioSystem.getMixer(selected);
for (Line l : theMixer.getSourceLines()) {
l.close();
}
lineAvailable = true;
}
}

View File

@ -0,0 +1,178 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.config.ConfigurableField;
/**
* A timed device is a device which executes so many ticks in a given time
* interval. This is the core of the emulator timing mechanics.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class TimedDevice extends Device {
/**
* Creates a new instance of TimedDevice
*/
public TimedDevice() {
setSpeed(cyclesPerSecond);
}
@ConfigurableField(name = "Speed", description = "(in hertz)")
public long cyclesPerSecond = defaultCyclesPerSecond();
@ConfigurableField(name = "Max speed")
public boolean maxspeed = false;
@Override
public abstract void tick();
private static final double NANOS_PER_SECOND = 1000000000.0;
// current cycle within the period
private int cycleTimer = 0;
// The actual worker that the device runs as
public Thread worker;
public static int TEMP_SPEED_MAX_DURATION = 1000000;
private int tempSpeedDuration = 0;
public boolean hasStopped = true;
@Override
public boolean suspend() {
disableTempMaxSpeed();
boolean result = super.suspend();
if (worker != null && worker.isAlive()) {
try {
worker.interrupt();
worker.join(1000);
} catch (InterruptedException ex) {
}
}
worker = null;
return result;
}
Thread timerThread;
public boolean pause() {
if (!isRunning()) {
return false;
}
isPaused = true;
try {
// KLUDGE: Sleeping to wait for worker thread to hit paused state. We might be inside the worker (?)
Thread.sleep(10);
} catch (InterruptedException ex) {
}
return true;
}
@Override
public void resume() {
super.resume();
isPaused = false;
if (worker != null && worker.isAlive()) {
return;
}
worker = new Thread(() -> {
while (isRunning()) {
hasStopped = false;
doTick();
while (isPaused) {
hasStopped = true;
try {
Thread.sleep(10);
} catch (InterruptedException ex) {
return;
}
}
resync();
}
hasStopped = true;
});
worker.setDaemon(false);
worker.setPriority(Thread.MAX_PRIORITY);
worker.start();
worker.setName("Timed device " + getDeviceName() + " worker");
}
long nanosPerInterval; // How long to wait between pauses
long cyclesPerInterval; // How many cycles to wait until a pause interval
long nextSync; // When was the last pause?
public final void setSpeed(long cyclesPerSecond) {
cyclesPerInterval = cyclesPerSecond / 100L;
nanosPerInterval = (long) ((double) cyclesPerInterval * NANOS_PER_SECOND / (double) cyclesPerSecond);
// System.out.println("Will pause " + nanosPerInterval + " nanos every " + cyclesPerInterval + " cycles");
cycleTimer = 0;
resetSyncTimer();
}
long skip = 0;
long wait = 0;
public final void resetSyncTimer() {
nextSync = System.nanoTime() + nanosPerInterval;
cycleTimer = 0;
}
public void enableTempMaxSpeed() {
tempSpeedDuration = TEMP_SPEED_MAX_DURATION;
}
public void disableTempMaxSpeed() {
tempSpeedDuration = 0;
resetSyncTimer();
}
protected void resync() {
if (++cycleTimer >= cyclesPerInterval) {
if (maxspeed || tempSpeedDuration > 0) {
if (tempSpeedDuration > 0) {
tempSpeedDuration -= cyclesPerInterval;
}
resetSyncTimer();
return;
}
long now = System.nanoTime();
if (now < nextSync) {
cycleTimer = 0;
long currentSyncDiff = nextSync - now;
// Don't bother resynchronizing unless we're off by 10ms
if (currentSyncDiff > 10000000L) {
try {
// System.out.println("Sleeping for " + currentSyncDiff / 1000000 + " milliseconds");
Thread.sleep(currentSyncDiff / 1000000L, (int) (currentSyncDiff % 1000000L));
} catch (InterruptedException ex) {
System.err.println(getDeviceName() + " was trying to sleep for " + (currentSyncDiff / 1000000) + " millis but was woken up");
// Logger.getLogger(TimedDevice.class.getName()).log(Level.SEVERE, null, ex);
}
} else {
// System.out.println("Sleeping for " + currentSyncDiff + " nanoseconds");
// LockSupport.parkNanos(currentSyncDiff);
}
}
nextSync += nanosPerInterval;
}
}
@Override
public void reconfigure() {
if (cyclesPerSecond == 0) {
cyclesPerSecond = defaultCyclesPerSecond();
}
setSpeed(cyclesPerSecond);
}
public abstract long defaultCyclesPerSecond();
}

View File

@ -0,0 +1,552 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.Emulator;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
/**
* This is a set of helper functions which do not belong anywhere else. Functions vary from introspection, discovery, and string/pattern matching.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Utility {
//--------------- Introspection utilities
private static Set<Class> findClasses(String pckgname, Class clazz) {
Set<Class> output = new HashSet<>();
// Code from JWhich
// ======
// Translate the package name into an absolute path
String name = pckgname;
if (!name.startsWith("/")) {
name = "/" + name;
}
name = name.replace('.', '/');
// Get a File object for the package
URL url = Utility.class.getResource(name);
if (url == null || url.getFile().contains("jre/lib")) {
return output;
}
if (url.getProtocol().equalsIgnoreCase("jar")) {
return findClassesInJar(url, clazz);
}
File directory = new File(url.getFile());
// New code
// ======
if (directory.exists()) {
// Get the list of the files contained in the package
for (String filename : directory.list()) {
char firstLetter = filename.charAt(0);
if (firstLetter < 'A' || (firstLetter > 'Z' && firstLetter < 'a') || firstLetter > 'z') {
continue;
}
// we are only interested in .class files
if (filename.endsWith(".class")) {
// removes the .class extension
String classname = filename.substring(0, filename.length() - 6);
try {
// Try to create an instance of the object
String className = pckgname + "." + classname;
// System.out.println("Class: " + className);
Class c = Class.forName(className);
if (clazz.isAssignableFrom(c)) {
output.add(c);
}
} catch (ClassNotFoundException cnfex) {
System.err.println(cnfex);
}
} else {
// System.out.println("Skipping non class: " + filename);
}
}
}
return output;
}
private static Set<Class> findClassesInJar(URL jarLocation, Class clazz) {
Set<Class> output = new HashSet<>();
JarFile jarFile = null;
try {
JarURLConnection conn = (JarURLConnection) jarLocation.openConnection();
jarFile = conn.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
String last = "";
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
if (jarEntry.getName().equals(last)) {
return output;
}
last = jarEntry.getName();
if (jarEntry.getName().endsWith(".class")) {
String className = jarEntry.getName();
className = className.substring(0, className.length() - 6);
className = className.replaceAll("/", "\\.");
if (className.startsWith("com.sun")) {
continue;
}
if (className.startsWith("java")) {
continue;
}
if (className.startsWith("javax")) {
continue;
}
if (className.startsWith("com.oracle")) {
continue;
}
// removes the .class extension
try {
// Try to create an instance of the object
// System.out.println("Class: " + className);
Class c = Class.forName(className);
if (clazz.isAssignableFrom(c)) {
output.add(c);
}
} catch (ClassNotFoundException cnfex) {
System.err.println(cnfex);
} catch (Throwable cnfex) {
// System.err.println(cnfex);
}
} else {
// System.out.println("Skipping non class: " + jarEntry.getName());
}
}
} catch (IOException ex) {
Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
if (jarFile != null) {
jarFile.close();
}
} catch (IOException ex) {
Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex);
}
}
return output;
}
private static final Map<Class, Collection<Class>> classCache = new HashMap<>();
public static List<Class> findAllSubclasses(Class clazz) {
if (classCache.containsKey(clazz)) {
return (List<Class>) classCache.get(clazz);
}
TreeMap<String, Class> allClasses = new TreeMap<>();
for (Package p : Package.getPackages()) {
if (p.getName().startsWith("java")
|| p.getName().startsWith("com.sun")
|| p.getName().startsWith("com.oracle")) {
continue;
}
findClasses(p.getName(), clazz).stream().filter((c) -> !(Modifier.isAbstract(c.getModifiers()))).forEach((c) -> {
allClasses.put(c.getSimpleName(), c);
});
}
List<Class> values = new ArrayList(allClasses.values());
classCache.put(clazz, values);
return values;
}
//------------------------------ String comparators
/**
* Rank two strings similarity in terms of distance The lower the number,
* the more similar these strings are to each other See:
* http://en.wikipedia.org/wiki/Levenshtein_distance#Computing_Levenshtein_distance
*
* @param s
* @param t
* @return Distance (higher is better)
*/
public static int levenshteinDistance(String s, String t) {
if (s == null || t == null || s.length() == 0 || t.length() == 0) {
return -1;
}
s = s.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", "");
t = t.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", "");
int m = s.length();
int n = t.length();
int[][] dist = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
dist[i][0] = i;
}
for (int i = 1; i <= n; i++) {
dist[0][i] = i;
}
for (int j = 1; j <= n; j++) {
for (int i = 1; i <= m; i++) {
if (s.charAt(i - 1) == t.charAt(j - 1)) {
dist[i][j] = dist[i - 1][j - 1];
} else {
int del = dist[i - 1][j] + 1;
int insert = dist[i][j - 1] + 1;
int sub = dist[i - 1][j - 1] + 1;
dist[i][j] = Math.min(Math.min(del, insert), sub);
}
}
}
return Math.max(m, n) - dist[m][n];
}
/**
* Compare strings based on a tally of similar patterns found, using a fixed
* search window The resulting score is heavily penalized if the strings
* differ greatly in length This is not as efficient as levenshtein, so it's
* only used as a tie-breaker.
*
* @param c1
* @param c2
* @param width Search window size
* @return Overall similarity score (higher is beter)
*/
public static double rankMatch(String c1, String c2, int width) {
double score = 0;
String s1 = c1.toLowerCase();
String s2 = c2.toLowerCase();
for (int i = 0; i < s1.length() + 1 - width; i++) {
String m = s1.substring(i, i + width);
int j = 0;
while ((j = s2.indexOf(m, j)) > -1) {
score += width;
j++;
}
}
double l1 = s1.length();
double l2 = s2.length();
// If the two strings are equivilent in length, the score is higher
// If the two strings are different in length, the score is adjusted lower depending on how large the difference is
// This is offset just a hair for tuning purposes
double adjustment = (Math.min(l1, l2) / Math.max(l1, l2)) + 0.1;
return score * adjustment * adjustment;
}
public static String join(Collection c, String d) {
String result = "";
boolean isFirst = true;
for (Object o : c) {
result += (isFirst ? "" : d) + o.toString();
isFirst = false;
}
return result;
}
public static ImageIcon loadIcon(String filename) {
URL imageUrl = Utility.class.getClassLoader().getResource("jace/data/" + filename);
ImageIcon i = new ImageIcon(imageUrl);
return i;
}
public static void runModalProcess(String title, final Runnable runnable) {
final JDialog frame = new JDialog(Emulator.getFrame());
final JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
final JPanel contentPane = new JPanel();
contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
contentPane.setLayout(new BorderLayout());
contentPane.add(new JLabel(title), BorderLayout.NORTH);
contentPane.add(progressBar, BorderLayout.CENTER);
frame.setContentPane(contentPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
new Thread(() -> {
runnable.run();
frame.setVisible(false);
frame.dispose();
}).start();
}
public static class RankingComparator implements Comparator<String> {
String match;
public RankingComparator(String match) {
// Adding a space helps respect word boundaries as part of the match
// In the case of very close matches this is another tie-breaker
// Especially for very small search terms
this.match = match + " ";
}
@Override
public int compare(String o1, String o2) {
double s1 = levenshteinDistance(match, o1);
double s2 = levenshteinDistance(match, o2);
if (s2 == s1) {
s1 = rankMatch(o1, match, 3) + rankMatch(o1, match, 2);
s2 = rankMatch(o2, match, 3) + rankMatch(o2, match, 2);
if (s2 == s1) {
return (o1.compareTo(o2));
} else {
// Normalize result to -1, 0 or 1 so there is no rounding issues!
return (int) Math.signum(s2 - s1);
}
} else {
return (int) (s2 - s1);
}
}
}
/**
* Given a desired search string and a search space of recognized
* selections, identify the best match in the list
*
* @param match String to search for
* @param search Space of all valid results
* @return Best match found, or null if there was nothing close to a match
* found.
*/
public static String findBestMatch(String match, Collection<String> search) {
if (search == null || search.isEmpty()) {
return null;
}
RankingComparator r = new RankingComparator(match);
List<String> candidates = new ArrayList<>(search);
Collections.sort(candidates, r);
// for (String c : candidates) {
// double m2 = rankMatch(c, match, 2);
// double m3 = rankMatch(c, match, 3);
// double m4 = rankMatch(c, match, 4);
// double l = levenshteinDistance(match, c);
// System.out.println(match + "->" + c + ":" + l + " -- "+ m2 + "," + m3 + "," + "(" + (m2 + m3) + ")");
// }
// double score = rankMatch(match, candidates.get(0), 2);
double score = levenshteinDistance(match, candidates.get(0));
if (score > 1) {
return candidates.get(0);
}
return null;
}
public static void printStackTrace() {
System.out.println("CURRENT STACK TRACE:");
for (StackTraceElement s : Thread.currentThread().getStackTrace()) {
System.out.println(s.getClassName() + "." + s.getMethodName() + " (line " + s.getLineNumber() + ") " + (s.isNativeMethod() ? "NATIVE" : ""));
}
System.out.println("END OF STACK TRACE");
}
public static int parseHexInt(Object s) {
if (s == null) {
return -1;
}
if (s instanceof Integer) {
return (Integer) s;
}
String val = String.valueOf(s).trim();
int base = 10;
if (val.startsWith("$")) {
base = 16;
val = val.contains(" ") ? val.substring(1, val.indexOf(' ')) : val.substring(1);
} else if (val.startsWith("0x")) {
base = 16;
val = val.contains(" ") ? val.substring(2, val.indexOf(' ')) : val.substring(2);
}
try {
return Integer.parseInt(val, base);
} catch (NumberFormatException ex) {
gripe("This isn't a valid number: " + val + ". If you put a $ in front of that then I'll know you meant it to be a hex number.");
throw ex;
}
}
public static void gripe(final String message) {
EventQueue.invokeLater(() -> {
JOptionPane.showMessageDialog(Emulator.getFrame(), message, "Error", JOptionPane.ERROR_MESSAGE);
});
}
public static Object findChild(Object object, String fieldName) {
if (object instanceof Map) {
Map map = (Map) object;
for (Object key : map.keySet()) {
if (key.toString().equalsIgnoreCase(fieldName)) {
return map.get(key);
}
}
return null;
}
try {
Field f = object.getClass().getField(fieldName);
return f.get(object);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
for (Method m : object.getClass().getMethods()) {
if (m.getName().equalsIgnoreCase("get" + fieldName) && m.getParameterTypes().length == 0) {
try {
return m.invoke(object, new Object[0]);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) {
}
}
}
}
return null;
}
public static Object setChild(Object object, String fieldName, String value, boolean hex) {
if (object instanceof Map) {
Map map = (Map) object;
for (Object key : map.entrySet()) {
if (key.toString().equalsIgnoreCase(fieldName)) {
map.put(key, value);
return null;
}
}
return null;
}
Field f;
try {
f = object.getClass().getField(fieldName);
} catch (NoSuchFieldException ex) {
System.out.println("Object type " + object.getClass().getName() + " has no field named " + fieldName);
Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex);
return null;
} catch (SecurityException ex) {
Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex);
return null;
}
Object useValue = deserializeString(value, f.getType(), hex);
try {
f.set(object, useValue);
return useValue;
} catch (IllegalArgumentException | IllegalAccessException ex) {
for (Method m : object.getClass().getMethods()) {
if (m.getName().equalsIgnoreCase("set" + fieldName) && m.getParameterTypes().length == 0) {
try {
m.invoke(object, useValue);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) {
}
}
}
}
return useValue;
}
static Map<Class, Map<String, Object>> enumCache = new HashMap<>();
public static Object findClosestEnumConstant(String value, Class type) {
Map<String, Object> enumConstants = enumCache.get(type);
if (enumConstants == null) {
Object[] constants = type.getEnumConstants();
enumConstants = new HashMap<>();
for (Object o : constants) {
enumConstants.put(o.toString(), o);
}
enumCache.put(type, enumConstants);
}
String key = findBestMatch(value, enumConstants.keySet());
if (key == null) {
return null;
}
return enumConstants.get(key);
}
public static Object deserializeString(String value, Class type, boolean hex) {
int radix = hex ? 16 : 10;
if (type.equals(Integer.TYPE) || type == Integer.class) {
value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", "");
try {
return Integer.parseInt(value, radix);
} catch (NumberFormatException ex) {
return null;
}
} else if (type.equals(Short.TYPE) || type == Short.class) {
value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", "");
try {
return Short.parseShort(value, radix);
} catch (NumberFormatException ex) {
return null;
}
} else if (type.equals(Long.TYPE) || type == Long.class) {
value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", "");
try {
return Long.parseLong(value, radix);
} catch (NumberFormatException ex) {
return null;
}
} else if (type.equals(Byte.TYPE) || type == Byte.class) {
try {
value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", "");
return Byte.parseByte(value, radix);
} catch (NumberFormatException ex) {
return null;
}
} else if (type.equals(Boolean.TYPE) || type == Boolean.class) {
return Boolean.valueOf(value);
} else if (type == File.class) {
return new File(String.valueOf(value));
} else if (type.isEnum()) {
value = value.replaceAll("[\\.\\s\\-]", "");
return findClosestEnumConstant(value, type);
}
return null;
}
public static Object getProperty(Object object, String path) {
String[] paths = path.split("\\.");
for (String path1 : paths) {
object = findChild(object, path1);
if (object == null) {
return null;
}
}
return object;
}
public static Object setProperty(Object object, String path, String value, boolean hex) {
String[] paths = path.split("\\.");
for (int i = 0; i < paths.length - 1; i++) {
object = findChild(object, paths[i]);
if (object == null) {
return null;
}
}
return setChild(object, paths[paths.length - 1], value, hex);
}
}

View File

@ -0,0 +1,299 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import jace.state.Stateful;
import jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
/**
* Generic abstraction of a 560x192 video output device which renders 40 columns
* per scanline. This also triggers VBL and updates the physical screen.
* Subclasses are used to manage actual rendering via ScreenWriter
* implementations.
* Created on November 10, 2006, 4:29 PM
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public abstract class Video extends Device {
@Stateful
BufferedImage video;
VideoWriter currentWriter;
Graphics screen;
private byte floatingBus = 0;
private int width = 560;
private int height = 192;
@Stateful
public int x = 0;
@Stateful
public int y = 0;
@Stateful
public int scannerAddress;
@Stateful
public int vPeriod = 0;
@Stateful
public int hPeriod = 0;
static final public int CYCLES_PER_LINE = 65;
static final public int TOTAL_LINES = 262;
static final public int APPLE_CYCLES_PER_LINE = 40;
static final public int APPLE_SCREEN_LINES = 192;
static final public int HBLANK = CYCLES_PER_LINE - APPLE_CYCLES_PER_LINE;
static final public int VBLANK = (TOTAL_LINES - APPLE_SCREEN_LINES) * CYCLES_PER_LINE;
static public int[] textOffset;
static public int[] hiresOffset;
static public int[] textRowLookup;
static public int[] hiresRowLookup;
private boolean screenDirty;
private boolean lineDirty;
private boolean isVblank = false;
static VideoWriter[][] writerCheck = new VideoWriter[40][192];
static {
textOffset = new int[192];
hiresOffset = new int[192];
textRowLookup = new int[0x0400];
hiresRowLookup = new int[0x02000];
for (int i = 0; i < 192; i++) {
textOffset[i] = calculateTextOffset(i >> 3);
hiresOffset[i] = calculateHiresOffset(i);
}
for (int i = 0; i < 0x0400; i++) {
textRowLookup[i] = identifyTextRow(i);
}
for (int i = 0; i < 0x2000; i++) {
hiresRowLookup[i] = identifyHiresRow(i);
}
}
private int forceRedrawRowCount = 0;
Thread updateThread;
/**
* Creates a new instance of Video
*/
public Video() {
suspend();
video = new BufferedImage(560, 192, BufferedImage.TYPE_INT_RGB);
vPeriod = 0;
hPeriod = 0;
forceRefresh();
}
public void setWidth(int w) {
width = w;
}
public int getWidth() {
return width;
}
public void setHeight(int h) {
height = h;
}
public int getHeight() {
return height;
}
public void setScreen(Graphics g) {
screen = g;
}
public Graphics getScreen() {
return screen;
}
public VideoWriter getCurrentWriter() {
return currentWriter;
}
public void setCurrentWriter(VideoWriter currentWriter) {
if (this.currentWriter != currentWriter || currentWriter.isMixed()) {
this.currentWriter = currentWriter;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1;
}
}
@ConfigurableField(category = "video", name = "Min. Screen Refesh", defaultValue = "15", description = "Minimum number of miliseconds to wait before trying to redraw.")
public static int MIN_SCREEN_REFRESH = 15;
public void redraw() {
if (screen == null || video == null) {
return;
}
screenDirty = false;
screen.drawImage(video, 0, 0, width, height, null);
if (Emulator.getFrame() != null) {
Emulator.getFrame().repaintIndicators();
}
}
public void vblankStart() {
if (screenDirty && isRunning()) {
redraw();
}
}
abstract public void vblankEnd();
abstract public void hblankStart(BufferedImage screen, int y, boolean isDirty);
public void setScannerLocation(int loc) {
scannerAddress = loc;
}
@Override
public void tick() {
setFloatingBus(Computer.getComputer().getMemory().readRaw(scannerAddress + x));
if (hPeriod > 0) {
hPeriod--;
if (hPeriod == 0) {
x = -1;
setScannerLocation(currentWriter.getYOffset(y));
}
} else {
if (!isVblank) {
draw();
}
if (x >= APPLE_CYCLES_PER_LINE - 1) {
int yy = y + hblankOffsetY;
if (yy < 0) {
yy += APPLE_SCREEN_LINES;
}
if (yy >= APPLE_SCREEN_LINES) {
yy -= (TOTAL_LINES - APPLE_SCREEN_LINES);
}
setScannerLocation(currentWriter.getYOffset(yy) + hblankOffsetX + (yy < 64 ? 128 : 0));
x = -1;
if (!isVblank) {
if (lineDirty) {
screenDirty = true;
currentWriter.clearDirty(y);
}
hblankStart(video, y, lineDirty);
lineDirty = false;
forceRedrawRowCount--;
}
hPeriod = HBLANK;
y++;
if (y >= APPLE_SCREEN_LINES) {
if (!isVblank) {
y = APPLE_SCREEN_LINES - (TOTAL_LINES - APPLE_SCREEN_LINES);
isVblank = true;
vblankStart();
Motherboard.vblankStart();
} else {
y = 0;
isVblank = false;
vblankEnd();
Motherboard.vblankEnd();
}
}
}
}
x++;
}
abstract public void configureVideoMode();
protected static int byteDoubler(byte b) {
int num =
// Skip hi-bit because it's not used in display
// ((b&0x080)<<7) |
((b & 0x040) << 6)
| ((b & 0x020) << 5)
| ((b & 0x010) << 4)
| ((b & 0x08) << 3)
| ((b & 0x04) << 2)
| ((b & 0x02) << 1)
| (b & 0x01);
return num | (num << 1);
}
@ConfigurableField(name = "Waits per cycle", category = "Advanced", description = "Adjust the delay for the scanner")
public static int waitsPerCycle = 0;
@ConfigurableField(name = "Hblank X offset", category = "Advanced", description = "Adjust where the hblank period starts relative to the start of the line")
public static int hblankOffsetX = -29;
@ConfigurableField(name = "Hblank Y offset", category = "Advanced", description = "Adjust which line the HBLANK starts on (0=current, 1=next, etc)")
public static int hblankOffsetY = 1;
private void draw() {
if (lineDirty || forceRedrawRowCount > 0 || currentWriter.isRowDirty(y)) {
lineDirty = true;
currentWriter.displayByte(video, x, y, textOffset[y], hiresOffset[y]);
}
setWaitCycles(waitsPerCycle);
doPostDraw();
}
static public int calculateHiresOffset(int y) {
return calculateTextOffset(y >> 3) + ((y & 7) << 10);
}
static public int calculateTextOffset(int y) {
return ((y & 7) << 7) + 40 * (y >> 3);
}
static public int identifyTextRow(int y) {
//floor((x-1024)/128) + floor(((x-1024)%128)/40)*8
// Caller must check result is <= 23, if so then they are in a screenhole!
return (y >> 7) + (((y & 0x7f) / 40) << 3);
}
static public int identifyHiresRow(int y) {
int blockOffset = identifyTextRow(y & 0x03ff);
// Caller must check results is > 0, if not then they are in a screenhole!
if (blockOffset > 23) {
return -1;
}
return ((y >> 10) & 7) + (blockOffset << 3);
}
public abstract void doPostDraw();
public byte getFloatingBus() {
return floatingBus;
}
private void setFloatingBus(byte floatingBus) {
this.floatingBus = floatingBus;
}
@InvokableAction(name = "Refresh screen",
category = "display",
description = "Marks screen contents as changed, forcing full screen redraw",
alternatives = "redraw")
public final void forceRefresh() {
lineDirty = true;
screenDirty = true;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1;
}
@Override
public String getShortName() {
return "vid";
}
public BufferedImage getFrameBuffer() {
return video;
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.core;
import java.awt.image.BufferedImage;
/**
* VideoWriter is an abstraction of a graphics display mode that knows how to
* render a scanline a certain way (lo-res, hi-res, text, etc) over a specific
* range of memory (as determined by getYOffset.) Dirty flags are used to mark
* scanlines that were altered and require redraw. This is the key to only
* updating the screen as needed instead of drawing all the time at the expense
* of CPU.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class VideoWriter {
public abstract void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset);
// This is used to support composite mixed-mode writers so that we can talk to the writer being used for a scanline
public VideoWriter actualWriter() {
return this;
}
public abstract int getYOffset(int y);
// Dirty flags allow us to know if a scanline has or has not changed
// Very useful for knowing if we should bother drawing changes
private final boolean[] dirtyFlags = new boolean[192];
public void markDirty(int y) {
actualWriter().dirtyFlags[y] = true;
}
public void clearDirty(int y) {
actualWriter().dirtyFlags[y] = false;
}
public boolean isRowDirty(int y) {
return actualWriter().dirtyFlags[y];
}
public boolean isMixed() {
return false;
}
}

View File

@ -0,0 +1,637 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.Emulator;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.PagedMemory;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.state.Stateful;
import jace.core.Utility;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.ImageIcon;
/**
* Apple Mouse interface implementation. This is fully compatible with several
* applications.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
@Name("Apple Mouse")
public class CardAppleMouse extends Card implements MouseListener {
@Stateful
public int mode;
@Stateful
public boolean active;
@Stateful
public boolean interruptOnMove;
@Stateful
public boolean interruptOnPress;
@Stateful
public boolean interruptOnVBL;
@Stateful
public boolean button0press;
@Stateful
public boolean button1press;
@Stateful
public boolean button0pressLast;
@Stateful
public boolean button1pressLast;
@Stateful
public boolean isInterrupt;
@Stateful
public boolean isVBL;
@Stateful
public int statusByte;
@Stateful
public Point lastMouseLocation;
@Stateful
public Point clampMin = new Point(0, 0x03ff);
@Stateful
public Point clampMax = new Point(0, 0x03ff);
// By default, update 60 times a second -- roughly every VBL period (in theory)
@ConfigurableField(name = "Update frequency", shortName = "updateFreq", category = "Mouse", description = "# of CPU cycles between updates; affects polling and interrupt-based routines")
public static int CYCLES_PER_UPDATE = (int) (1020484L / 60L);
@ConfigurableField(name = "Fullscreen fix", shortName = "fsfix", category = "Mouse", description = "If the mouse pointer is a little off when in fullscreen, this should fix it.")
public boolean fullscreenFix = true;
ImageIcon mouseActive = Utility.loadIcon("input-mouse.png");
@Override
public String getDeviceName() {
return "Apple Mouse";
}
@Override
public void reset() {
mode = 0;
deactivateMouse();
}
/*
* Coded against this information
* http://stason.org/TULARC/pc/apple2/programmer/012-How-do-I-write-programs-which-use-the-mouse.html
*/
@Override
protected void handleFirmwareAccess(int offset, TYPE type, int value, RAMEvent e) {
/*
* Screen holes
* $0478 + slot Low byte of absolute X position
* $04F8 + slot Low byte of absolute Y position
* $0578 + slot High byte of absolute X position
* $05F8 + slot High byte of absolute Y position
* $0678 + slot Reserved and used by the firmware
* $06F8 + slot Reserved and used by the firmware
* $0778 + slot Button 0/1 interrupt status byte
* $07F8 + slot Mode byte
*
* Interrupt status byte:
* Set by READMOUSE
* Bit 7 6 5 4 3 2 1 0
* | | | | | | | |
* | | | | | | | `--- Previously, button 1 was up (0) or down (1)
* | | | | | | `----- Movement interrupt
* | | | | | `------- Button 0/1 interrupt
* | | | | `--------- VBL interrupt
* | | | `----------- Currently, button 1 is up (0) or down (1)
* | | `------------- X/Y moved since last READMOUSE
* | `--------------- Previously, button 0 was up (0) or down (1)
* `----------------- Currently, button 0 is up (0) or down (1)
*
* Mode byte
* Valid after calling SERVEMOUSE, cleared with READMOUSE
* Bit 7 6 5 4 3 2 1 0
* | | | | | | | |
* | | | | | | | `--- Mouse off (0) or on (1)
* | | | | | | `----- Interrupt if mouse is moved
* | | | | | `------- Interrupt if button is pressed
* | | | | `--------- Interrupt on VBL
* | | | `----------- Reserved
* | | `------------- Reserved
* | `--------------- Reserved
* `----------------- Reserved
*/
if (type == RAMEvent.TYPE.EXECUTE) {
// This means the CPU is calling firmware at this location
switch (offset - 0x080) {
case 0:
setMouse();
break;
case 1:
serveMouse();
break;
case 2:
readMouse();
break;
case 3:
clearMouse();
break;
case 4:
posMouse();
break;
case 5:
clampMouse();
break;
case 6:
homeMouse();
break;
case 7:
initMouse();
break;
}
// Always pass back RTS
e.setNewValue(0x060);
} else if (type.isRead()) {
/* Identification bytes
* $Cn05 = $38 $Cn07 = $18 $Cn0B = $01 $Cn0C = $20 $CnFB = $D6
*/
switch (offset) {
case 0x05:
e.setNewValue(0x038);
break;
case 0x07:
e.setNewValue(0x018);
break;
case 0x0B:
e.setNewValue(0x01);
break;
case 0x0C:
e.setNewValue(0x020);
break;
case 0x0FB:
e.setNewValue(0x0D6);
break;
// As per the //gs firmware reference manual
case 0x08:
// Pascal signature byte
e.setNewValue(0x001);
case 0x011:
e.setNewValue(0x000);
break;
// Function call offsets
case 0x12:
e.setNewValue(0x080);
break;
case 0x13:
e.setNewValue(0x081);
break;
case 0x14:
e.setNewValue(0x082);
break;
case 0x15:
e.setNewValue(0x083);
break;
case 0x16:
e.setNewValue(0x084);
break;
case 0x17:
e.setNewValue(0x085);
break;
case 0x18:
e.setNewValue(0x086);
break;
case 0x19:
e.setNewValue(0x087);
break;
default:
e.setNewValue(0x069);
}
// System.out.println("Read mouse firmware at "+Integer.toHexString(e.getAddress())+" == "+Integer.toHexString(e.getNewValue()));
}
}
private MOS65C02 getCPU() {
return (MOS65C02) Computer.getComputer().getCpu();
}
/*
* $Cn12 SETMOUSE Sets mouse mode
* A = mouse operation mode (0-f)
* C = 1 if illegal mode requested
* mode byte updated
*/
private void setMouse() {
mode = getCPU().A & 0x0ff;
if (mode > 0x0f) {
getCPU().C = 1;
return;
} else {
getCPU().C = 0;
}
//Mouse off (0) or on (1)
if ((mode & 1) == 0) {
deactivateMouse();
return;
}
//Interrupt if mouse is moved
interruptOnMove = ((mode & 2) != 0);
//Interrupt if button is pressed
interruptOnPress = ((mode & 4) != 0);
//Interrupt on VBL
interruptOnVBL = ((mode & 8) != 0);
activateMouse();
}
/*
* $Cn13 SERVEMOUSE Services mouse interrupt
* Test for interupt and clear mouse interrupt line
* Return C=0 if mouse interrupt occurred
* Updates screen hole interrupt status bits
*/
private void serveMouse() {
// If any interrupts are registered then
updateMouseState();
if (isInterrupt) {
getCPU().C = 0;
} else {
getCPU().C = 1;
// System.out.println("MOUSE TRIGGERED INTERRUPT!");
}
// isInterrupt = false;
// isVBL=false;
}
/*
* $Cn14 READMOUSE Reads mouse position
* Reads delta (X/Y) positions, updates abolute X/Y pos
* and reads button statuses
* Always returns C=0
* Interrupt status bits cleared
* Screen hole positions for button/movement status bits updated
*/
private void readMouse() {
updateMouseState();
isInterrupt = false;
isVBL = false;
// set screen holes
getCPU().C = 0;
}
/*
* $Cn16 POSMOUSE Sets mouse position to a user-defined pos
* Caller puts new position in screenhole
* Always returns C=0
*/
private void posMouse() {
// Ignore?
getCPU().C = 0;
}
/*
* $Cn17 CLAMPMOUSE Sets mouse bounds in a window
* Sets up clamping window for mouse user
* Power up defaults are 0 - 1023 (0 - 3ff)
* Caller sets:
* A = 0 if setting X, 1 if setting Y
* $0478 = low byte of low clamp.
* $04F8 = low byte of high clamp.
* $0578 = high byte of low clamp.
* $05F8 = high byte of high clamp.
* //gs homes mouse to low address, but //c and //e do not
*/
private void clampMouse() {
RAM128k memory = (RAM128k) Computer.getComputer().memory;
byte clampMinLo = memory.getMainMemory().readByte(0x0478);
byte clampMaxLo = memory.getMainMemory().readByte(0x04F8);
byte clampMinHi = memory.getMainMemory().readByte(0x0578);
byte clampMaxHi = memory.getMainMemory().readByte(0x05F8);
int min = (clampMinLo & 0x0ff) | ((clampMinHi << 8) & 0x0FF00);
int max = (clampMaxLo & 0x0ff) | ((clampMaxHi << 8) & 0x0FF00);
if (getCPU().A == 0) {
setClampWindowX(min, max);
} else if (getCPU().A == 1) {
setClampWindowY(min, max);
}
// System.out.println("Set mouse clamping to:" + clampMin.toString() + ";" + clampMax.toString());
}
/*
* $Cn19 INITMOUSE Resets mouse clamps to default values; sets mouse position to 0,0
* Sets screen holes to default values and sets clamping
* window to default value (000 - 3ff) for both X and Y
* Exit:C=0
* Screen holes are updated
*/
private void initMouse() {
mouseActive.setDescription("Active");
Emulator.getFrame().addIndicator(this, mouseActive, 2000);
setClampWindowX(0, 0x3ff);
setClampWindowY(0, 0x3ff);
clearMouse();
}
/*
* $Cn15 CLEARMOUSE Clears mouse position to 0 (for delta mode)
* Resets buttons, movement and interrupt status bits to 0
* Intended to be used for delta mouse positioning instead of absolute positioning
* Always returns C=0
* Interrupt status bits cleared
* Screen hole positions for button/movement status bits updated
*/
private void clearMouse() {
isVBL = false;
isInterrupt = false;
button0press = false;
button1press = false;
button0pressLast = false;
button1pressLast = false;
homeMouse();
}
/*
* $Cn18 HOMEMOUSE Sets absolute position to upper-left corner of clamping window
* Exit: c=0
* Screen hole positions are updated
*/
private void homeMouse() {
lastMouseLocation = new Point(0, 0);
updateMouseState();
getCPU().C = 0;
}
/*
* This is called whenever the mouse firmware has been activated in software
*/
private void activateMouse() {
active = true;
Component drawingArea = Emulator.getScreen();
if (drawingArea != null) {
drawingArea.addMouseListener(this);
}
}
/*
* This is called whenever there is a hard reset or when the mouse is turned off
*/
private void deactivateMouse() {
active = false;
mode = 0;
interruptOnMove = false;
interruptOnPress = false;
interruptOnVBL = false;
Component drawingArea = Emulator.getScreen();
if (drawingArea != null) {
drawingArea.removeMouseListener(this);
}
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
// No IO access necessary (is there?)
}
private int delay = CYCLES_PER_UPDATE;
@Override
public void tick() {
if (!active) {
return;
}
delay--;
if (delay > 0) {
return;
}
delay = CYCLES_PER_UPDATE;
// If interrupts not used, just move on
if (!interruptOnMove && !interruptOnPress) {
return;
}
if (interruptOnPress) {
if (button0press != button0pressLast || button1press != button1pressLast) {
isInterrupt = true;
getCPU().generateInterrupt();
return;
}
}
if (interruptOnMove) {
Point currentMouseLocation = MouseInfo.getPointerInfo().getLocation();
if (!currentMouseLocation.equals(lastMouseLocation)) {
isInterrupt = true;
getCPU().generateInterrupt();
}
}
}
@Override
public void notifyVBLStateChanged(boolean state) {
// VBL is false when it is the vertical blanking period
if (!state && interruptOnVBL && active) {
isVBL = true;
isInterrupt = true;
getCPU().generateInterrupt();
}
}
private void updateMouseState() {
Component drawingArea = Emulator.getScreen();
if (drawingArea == null) {
return;
}
Graphics2D screen = (Graphics2D) Computer.getComputer().getVideo().getScreen();
// Point currentMouseLocation = MouseInfo.getPointerInfo().getLocation();
// Point topLeft = drawingArea.getLocationOnScreen();
Point currentMouseLocation = Emulator.getFrame().getContentPane().getMousePosition();
if (currentMouseLocation == null) return;
// Point topLeft = drawingArea.getLocationOnScreen();
Point topLeft = new Point(0,0);
Dimension d = drawingArea.getBounds().getSize();
if (screen.getTransform() != null) {
d = new Dimension((int) (screen.getTransform().getScaleX() * d.width),
(int) (screen.getTransform().getScaleY() * d.height));
topLeft.x += screen.getTransform().getTranslateX();
topLeft.y += screen.getTransform().getTranslateY();
}
if (fullscreenFix) {
if (Emulator.getFrame().isFullscreenActive()) {
Toolkit t = Toolkit.getDefaultToolkit();
topLeft.y -= t.getScreenInsets(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()).top;
}
}
// Scale X and Y to the clamping range of the mouse (will this work for most software?)
double width = clampMax.x - clampMin.x;
double x = currentMouseLocation.getX() - topLeft.x;
x *= width;
x /= d.width;
x += clampMin.x;
if (x < clampMin.x) {
x = clampMin.x;
}
if (x > clampMax.x) {
x = clampMax.x;
}
double height = clampMax.y - clampMin.y;
double y = currentMouseLocation.getY() - topLeft.y;
y *= height;
y /= d.height;
y += clampMin.y;
if (y < clampMin.y) {
y = clampMin.y;
}
if (y > clampMax.y) {
y = clampMax.y;
}
PagedMemory m = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory();
int s = getSlot();
/*
* $0478 + slot Low byte of absolute X position
* $04F8 + slot Low byte of absolute Y position
*/
m.writeByte(0x0478 + s, (byte) ((int) x & 0x0ff));
m.writeByte(0x04F8 + s, (byte) ((int) y & 0x0ff));
/*
* $0578 + slot High byte of absolute X position
* $05F8 + slot High byte of absolute Y position
*/
m.writeByte(0x0578 + s, (byte) (((int) x & 0x0ff00) >> 8));
m.writeByte(0x05F8 + s, (byte) (((int) y & 0x0ff00) >> 8));
/*
* $0678 + slot Reserved and used by the firmware
* $06F8 + slot Reserved and used by the firmware
*
* Interrupt status byte:
* Set by READMOUSE
* Bit 7 6 5 4 3 2 1 0
* | | | | | | | |
* | | | | | | | `--- Previously, button 1 was up (0) or down (1)
* | | | | | | `----- Movement interrupt
* | | | | | `------- Button 0/1 interrupt
* | | | | `--------- VBL interrupt
* | | | `----------- Currently, button 1 is up (0) or down (1)
* | | `------------- X/Y moved since last READMOUSE
* | `--------------- Previously, button 0 was up (0) or down (1)
* `----------------- Currently, button 0 is up (0) or down (1)
*/
int status = 0;
boolean mouseMoved = !currentMouseLocation.equals(lastMouseLocation);
if (button1pressLast) {
status |= 1;
}
if (interruptOnMove && mouseMoved) {
status |= 2;
}
if (interruptOnPress && (button0press != button0pressLast || button1press != button1pressLast)) {
status |= 4;
}
if (isVBL) {
status |= 8;
}
if (button1press) {
status |= 16;
}
if (mouseMoved) {
status |= 32;
}
if (button0pressLast) {
status |= 64;
}
if (button0press) {
status |= 128;
}
/*
* $0778 + slot Button 0/1 interrupt status byte
*/
m.writeByte(0x0778 + s, (byte) (status));
/*
* $07F8 + slot Mode byte
*/
m.writeByte(0x07F8 + s, (byte) (mode));
lastMouseLocation = currentMouseLocation;
button0pressLast = button0press;
button1pressLast = button1press;
}
@Override
public void mousePressed(MouseEvent me) {
int button = me.getButton();
if (button == 1 || button == 2) {
button0press = true;
}
if (button == 2 || button == 3) {
button1press = true;
}
}
@Override
public void mouseReleased(MouseEvent me) {
int button = me.getButton();
if (button == 1 || button == 2) {
button0press = false;
}
if (button == 2 || button == 3) {
button1press = false;
}
}
@Override
public void mouseClicked(MouseEvent me) {
}
@Override
public void mouseEntered(MouseEvent me) {
}
@Override
public void mouseExited(MouseEvent me) {
}
private void setClampWindowX(int min, int max) {
// Fix for GEOS clamping funkiness
if (max == 32767) {
max = 560;
}
clampMin.x = min;
clampMax.x = max;
}
private void setClampWindowY(int min, int max) {
// Fix for GEOS clamping funkiness
if (max == 32767) {
max = 192;
}
clampMin.y = min;
clampMax.y = max;
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// Do nothing, there is no need to emulate c8 rom
}
}

View File

@ -0,0 +1,220 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.config.Reconfigurable;
import jace.core.Card;
import jace.core.Motherboard;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Apple Disk ][ interface implementation. This card represents the interface
* side of the Disk ][ controller interface as well as the on-board "boot0" ROM.
* The behavior of the actual drive stepping, reading disk images, and so on is
* performed by DiskIIDrive and FloppyDisk, respectively. This class only serves
* as the I/O interface portion.
* Created on April 21, 2007
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Disk ][ Controller")
public class CardDiskII extends Card implements Reconfigurable, MediaConsumerParent {
DiskIIDrive currentDrive;
DiskIIDrive drive1 = new DiskIIDrive();
DiskIIDrive drive2 = new DiskIIDrive();
@ConfigurableField(category = "Disk", defaultValue = "254", name = "Default volume", description = "Value to use for disk volume number")
static public int DEFAULT_VOLUME_NUMBER = 0x0FE;
@ConfigurableField(category = "Disk", defaultValue = "true", name = "Speed boost", description = "If enabled, emulator will run at max speed during disk access")
static public boolean USE_MAX_SPEED = true;
public CardDiskII() {
try {
loadRom("jace/data/DiskII.rom");
} catch (IOException ex) {
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
}
drive1.setIcon(Utility.loadIcon("disk_ii.png"));
drive2.setIcon(Utility.loadIcon("disk_ii.png"));
reset();
}
@Override
public String getDeviceName() {
return "Disk ][ Controller";
}
@Override
public void reset() {
currentDrive = drive1;
drive1.reset();
drive2.reset();
if (Emulator.getFrame() != null) {
Emulator.getFrame().removeIndicators(this);
}
// Motherboard.cancelSpeedRequest(this);
}
@SuppressWarnings("fallthrough")
@Override
protected void handleIOAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e) {
// handle Disk ][ registers
switch (register) {
case 0x0:
case 0x1:
case 0x2:
case 0x3:
case 0x4:
case 0x5:
case 0x6:
case 0x7:
currentDrive.step(register);
break;
case 0x8:
// drive off
currentDrive.setOn(false);
// Emulator.getFrame().removeIndicator(this, currentDrive == drive1 ? diskDrive1Icon : diskDrive2Icon, false);
break;
case 0x9:
// drive on
currentDrive.setOn(true);
Emulator.getFrame().addIndicator(this, currentDrive.getIcon());
break;
case 0xA:
// drive 1
currentDrive = drive1;
break;
case 0xB:
// drive 2
currentDrive = drive2;
break;
case 0xC:
// read/write latch
currentDrive.write();
e.setNewValue(currentDrive.readLatch());
break;
case 0xF:
// write mode
currentDrive.setWriteMode();
case 0xD:
// set latch
if (e.getType() == RAMEvent.TYPE.WRITE) {
currentDrive.setLatchValue((byte) e.getNewValue());
}
e.setNewValue(currentDrive.readLatch());
break;
case 0xE:
// read mode
currentDrive.setReadMode();
if (currentDrive.disk != null && currentDrive.disk.writeProtected) {
e.setNewValue(0x080);
} else
{
// e.setNewValue((byte) (Math.random() * 256.0));
e.setNewValue(0);
}
break;
}
// even addresses return the latch value
// if (e.getType() == RAMEvent.TYPE.READ) {
// if ((register & 0x1) == 0) {
// e.setNewValue(currentDrive.latch);
// } else {
// // return floating bus value (IIRC)
// }
// }
tweakTiming();
}
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// Do nothing: The ROM does everything
return;
}
public void loadRom(String path) throws IOException {
InputStream romFile = CardDiskII.class.getClassLoader().getResourceAsStream(path);
final int cxRomLength = 0x100;
byte[] romData = new byte[cxRomLength];
try {
if (romFile.read(romData) != cxRomLength) {
throw new IOException("Bad Disk ][ ROM size");
}
getCxRom().loadData(romData);
} catch (IOException ex) {
throw ex;
}
}
@Override
public void tick() {
// Do nothing (if you want 1mhz timing control, you can do that here...)
// drive1.tick();
// drive2.tick();
}
@Override
public void reconfigure() {
super.reconfigure();
}
private void tweakTiming() {
if (drive1.isOn() || drive2.isOn()) {
if (USE_MAX_SPEED) {
Motherboard.requestSpeed(this);
}
} else {
Motherboard.cancelSpeedRequest(this);
}
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// There is no special c8 rom for this card
}
@Override
public void setSlot(int slot) {
super.setSlot(slot);
drive1.getIcon().setDescription("S" + slot + "D1");
drive2.getIcon().setDescription("S" + slot + "D2");
}
public MediaConsumer[] getConsumers() {
return new MediaConsumer[] {drive1, drive2};
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.apple2e.RAM128k;
import jace.core.PagedMemory;
import jace.state.Stateful;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public class CardExt80Col extends RAM128k {
@Stateful
public PagedMemory auxMemory;
@Stateful
public PagedMemory auxLanguageCard;
@Stateful
public PagedMemory auxLanguageCard2;
@Override
public String getName() {
return "Extended 80-col card (128kb)";
}
@Override
public String getShortName() {
return "128kb";
}
public CardExt80Col() {
super();
auxMemory = new PagedMemory(0xc000, PagedMemory.Type.ram);
auxLanguageCard = new PagedMemory(0x3000, PagedMemory.Type.languageCard);
auxLanguageCard2 = new PagedMemory(0x1000, PagedMemory.Type.languageCard);
initMemoryPattern(auxMemory);
}
// This is redundant here, but necessary for Ramworks
@Override
public PagedMemory getAuxVideoMemory() {
return auxMemory;
}
/**
* @return the auxMemory
*/
@Override
public PagedMemory getAuxMemory() {
return auxMemory;
}
/**
* @return the auxLanguageCard
*/
@Override
public PagedMemory getAuxLanguageCard() {
return auxLanguageCard;
}
/**
* @return the auxLanguageCard2
*/
@Override
public PagedMemory getAuxLanguageCard2() {
return auxLanguageCard2;
}
@Override
public void reconfigure() {
// Do nothing
}
@Override
public void attach() {
// Nothing to do...
}
@Override
public void detach() {
// Nothing to do...
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.config.Name;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Partial Hayes Micromodem II implementation, acting more as a bridge to
* provide something similar to the Super Serial support for applications which
* do not support the SSC card but do support Hayes, such as DiversiDial.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Hayes Micromodem II")
public class CardHayesMicromodem extends CardSSC {
@Override
public String getDeviceName() {
return "Hayes Micromodem";
}
public int RING_INDICATOR_REG = 5;
private boolean ringIndicator = false;
public CardHayesMicromodem() {
ACIA_Data = 7;
ACIA_Status = 6;
ACIA_Control = 5;
ACIA_Command = 6;
// set these to high values will essentially NO-OP them.
SW1 = 255;
SW1_SETTING = 255;
SW2_CTS = 255;
RECV_IRQ_ENABLED = false;
TRANS_IRQ_ENABLED = false;
}
@Override
public void clientConnected() {
setRingIndicator(true);
super.clientConnected();
}
@Override
public void clientDisconnected() {
setRingIndicator(false);
super.clientDisconnected();
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
if (register == ACIA_Data) {
super.handleIOAccess(register, type, value, e);
return;
}
if (type.isRead() && register == RING_INDICATOR_REG) {
e.setNewValue(isRingIndicator() ? 0 : 255);
} else if (type.isRead() && register == ACIA_Status) {
e.setNewValue(getStatusValue());
} else if (type == TYPE.WRITE && register == ACIA_Control) {
if ((value & 0x080) == 0) {
System.out.println("Software triggered disconnect");
try {
if (clientSocket != null) {
clientSocket.getOutputStream().write("Disconnected by host\n".getBytes());
}
} catch (IOException ex) {
System.out.println("Client disconnected before host");
// If there's an error, ignore it. That means the client disconnected first.
}
// Hang up
hangUp();
setRingIndicator(false);
} else {
System.out.println("Software answered connect request");
try {
if (clientSocket != null) {
clientSocket.getOutputStream().write("Connected to emulated Apple\n".getBytes());
}
} catch (IOException ex) {
Logger.getLogger(CardHayesMicromodem.class.getName()).log(Level.SEVERE, null, ex);
}
setRingIndicator(false);
}
}
}
@Override
public void loadRom(String path) throws IOException {
// Do nothing -- there is no rom for this card right now.
}
/**
* @return the ringIndicator
*/
public boolean isRingIndicator() {
return ringIndicator;
}
/**
* @param ringIndicator the ringIndicator to set
*/
public void setRingIndicator(boolean ringIndicator) {
this.ringIndicator = ringIndicator;
}
private int getStatusValue() {
int status = 0;
try {
// 0 = receive register full
if (inputAvailable()) {
status |= 0x01;
}
// 1 = transmit register empty -- always :-)
status |= 0x02;
// 2 = No Carrier
if (isRingIndicator() || !isConnected()) {
status |= 0x04;
}
} catch (Throwable ex) {
Logger.getLogger(CardHayesMicromodem.class.getName()).log(Level.SEVERE, null, ex);
}
return status;
}
}

View File

@ -0,0 +1,407 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Motherboard;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.RAMListener;
import jace.core.SoundMixer;
import static jace.core.Utility.*;
import jace.hardware.mockingboard.PSG;
import jace.hardware.mockingboard.R6522;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
/**
* Mockingboard-C implementation (with partial Phasor support). This uses two
* 6522 chips to communicate to two respective AY PSG sound chips. This class
* manages the I/O access as well as the sound playback thread.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Mockingboard")
public class CardMockingboard extends Card implements Runnable {
// If true, emulation will cover 4 AY chips. Otherwise, only 2 AY chips
static final int[] AY_ADDRESSES = new int[]{0, 0x080, 0x010, 0x090};
@ConfigurableField(name = "Volume", shortName = "vol",
category = "Sound",
description = "Mockingboard volume, 100=max, 0=silent")
public int volume = 100;
static public int MAX_AMPLITUDE = 0x007fff;
@ConfigurableField(name = "Phasor mode",
category = "Sound",
description = "If enabled, card will have 4 sound chips instead of 2")
public boolean phasorMode = false;
@ConfigurableField(name = "Clock Rate (hz)",
category = "Sound",
defaultValue = "1020484",
description = "Clock rate of AY oscillators")
public int CLOCK_SPEED = 1020484;
public int SAMPLE_RATE = 48000;
@ConfigurableField(name = "Buffer size",
category = "Sound",
description = "Number of samples to generate on each pass")
public int BUFFER_LENGTH = 64;
// The array of configured AY chips
public PSG[] chips;
// The 6522 controllr chips (always 2)
public R6522[] controllers;
private int ticksBeteenPlayback = 5000;
Lock timerSync = new ReentrantLock();
Condition cpuCountReached = timerSync.newCondition();
Condition playbackFinished = timerSync.newCondition();
@ConfigurableField(name = "Idle sample threshold", description = "Number of samples to wait before suspending sound")
private int MAX_IDLE_SAMPLES = SAMPLE_RATE;
@Override
public String getDeviceName() {
return "Mockingboard";
}
public CardMockingboard() {
controllers = new R6522[2];
for (int i = 0; i < 2; i++) {
//don't ask...
final int j = i;
controllers[i] = new R6522() {
@Override
public void sendOutputA(int value) {
if (activeChip != null) {
activeChip.setBus(value);
} else {
System.out.println("No active AY chip!");
}
}
@Override
public void sendOutputB(int value) {
if (activeChip != null) {
activeChip.setControl(value & 0x07);
} else {
System.out.println("No active AY chip!");
}
}
@Override
public int receiveOutputA() {
return activeChip == null ? 0 : activeChip.bus;
}
@Override
public int receiveOutputB() {
return 0;
}
@Override
public String getShortName() {
return "timer" + j;
}
};
}
}
@Override
public void reset() {
// Reset PSG registers
suspend();
if (chips != null) {
for (PSG psg : chips) {
psg.reset();
}
}
}
RAMListener mainListener = null;
PSG activeChip = null;
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// System.out.println(e.getType().toString() + " event to mockingboard register "+Integer.toHexString(register)+", value "+e.getNewValue());
activeChip = null;
resume();
int chip = 0;
for (PSG psg : chips) {
if (psg.getBaseReg() == (register & 0x0f0)) {
activeChip = psg;
break;
}
chip++;
}
if (activeChip == null) {
System.err.println("Could not determine which PSG to communicate to");
e.setNewValue(Computer.getComputer().getVideo().getFloatingBus());
return;
}
R6522 controller = controllers[chip & 1];
if (e.getType().isRead()) {
int val = controller.readRegister(register & 0x0f);
// System.out.println("Register returns "+Integer.toHexString(val));
e.setNewValue(val);
} else {
controller.writeRegister(register & 0x0f, e.getNewValue());
}
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
// Oddly, all IO is done at the firmware address bank. It's a strange card.
e.setNewValue(Computer.getComputer().getVideo().getFloatingBus());
}
long ticksSinceLastPlayback = 0;
@Override
public void tick() {
for (R6522 c : controllers) {
if (c == null || !c.isRunning()) {
continue;
}
c.tick();
}
if (isRunning() && !pause) {
timerSync.lock();
try {
ticksSinceLastPlayback++;
if (ticksSinceLastPlayback >= ticksBeteenPlayback) {
cpuCountReached.signalAll();
while (isRunning() && ticksSinceLastPlayback >= ticksBeteenPlayback) {
if (!playbackFinished.await(1, TimeUnit.SECONDS)) {
gripe("The mockingboard playback thread has stalled. Disabling mockingboard.");
suspend();
}
}
}
} catch (InterruptedException ex) {
suspend();
// Do nothing, probably suspending CPU
} finally {
timerSync.unlock();
}
}
}
@Override
public void reconfigure() {
boolean restart = suspend();
initPSG();
for (PSG chip : chips) {
chip.setRate(CLOCK_SPEED, SAMPLE_RATE);
chip.reset();
}
super.reconfigure();
if (restart) {
resume();
}
}
///////////////////////////////////////////////////////////
public static int[] VolTable;
int[][] buffers;
int bufferLength = -1;
public void playSound(int[] left, int[] right) {
chips[0].update(left, true, left, false, left, false, BUFFER_LENGTH);
chips[1].update(right, true, right, false, right, false, BUFFER_LENGTH);
if (phasorMode) {
chips[2].update(left, false, left, false, left, false, BUFFER_LENGTH);
chips[3].update(right, false, right, false, right, false, BUFFER_LENGTH);
}
}
public void buildMixerTable() {
VolTable = new int[16];
int numChips = phasorMode ? 4 : 2;
/* calculate the volume->voltage conversion table */
/* The AY-3-8910 has 16 levels, in a logarithmic scale (3dB per step) */
/* The YM2149 still has 16 levels for the tone generators, but 32 for */
/* the envelope generator (1.5dB per step). */
double out = ((double) MAX_AMPLITUDE * (double) volume) / 100.0;
// Reduce max amplitude to reflect post-mixer values so we don't have to scale volume when mixing channels
out = out * 2.0 / 3.0 / numChips;
double delta = 1.15;
for (int i = 15; i > 0; i--) {
VolTable[i] = (int) Math.round(out); /* round to nearest */ // [TC: unsigned int cast]
// out /= 1.188502227; /* = 10 ^ (1.5/20) = 1.5dB */
// out /= 1.15; /* = 10 ^ (3/20) = 3dB */
delta += 0.0225;
out /= delta; // As per applewin's source, the levels don't scale as documented.
}
VolTable[0] = 0;
}
Thread playbackThread = null;
boolean pause = false;
@Override
public void resume() {
pause = false;
if (!isRunning()) {
if (chips == null) {
initPSG();
}
for (R6522 controller : controllers) {
controller.attach();
controller.resume();
}
}
super.resume();
if (playbackThread == null || !playbackThread.isAlive()) {
playbackThread = new Thread(this, "Mockingboard sound playback");
playbackThread.start();
}
}
@Override
public boolean suspend() {
super.suspend();
for (R6522 controller : controllers) {
controller.suspend();
controller.detach();
}
if (playbackThread == null || !playbackThread.isAlive()) {
return false;
}
if (playbackThread != null) {
playbackThread.interrupt();
try {
// Wait for thread to die
playbackThread.join();
} catch (InterruptedException ex) {
}
}
playbackThread = null;
return true;
}
@Override
/**
* This is the audio playback thread
*/
public void run() {
try {
SourceDataLine out = Motherboard.mixer.getLine(this);
int[] leftBuffer = new int[BUFFER_LENGTH];
int[] rightBuffer = new int[BUFFER_LENGTH];
int frameSize = out.getFormat().getFrameSize();
byte[] buffer = new byte[BUFFER_LENGTH * frameSize];
System.out.println("Mockingboard playback started");
int bytesPerSample = frameSize / 2;
buildMixerTable();
ticksBeteenPlayback = (int) ((Motherboard.SPEED * BUFFER_LENGTH) / SAMPLE_RATE);
ticksSinceLastPlayback = 0;
int zeroSamples = 0;
while (isRunning()) {
Motherboard.requestSpeed(this);
playSound(leftBuffer, rightBuffer);
int p = 0;
for (int idx = 0; idx < BUFFER_LENGTH; idx++) {
int sampleL = leftBuffer[idx];
int sampleR = rightBuffer[idx];
// Convert left + right samples into buffer format
if (sampleL == 0 && sampleR == 0) {
zeroSamples++;
} else {
zeroSamples = 0;
}
for (int shift = SoundMixer.BITS - 8, index = 0; shift >= 0; shift -= 8, index++) {
buffer[p + index] = (byte) (sampleR >> shift);
buffer[p + index + bytesPerSample] = (byte) (sampleL >> shift);
}
p += frameSize;
}
try {
timerSync.lock();
ticksSinceLastPlayback -= ticksBeteenPlayback;
} finally {
timerSync.unlock();
}
out.write(buffer, 0, buffer.length);
if (zeroSamples >= MAX_IDLE_SAMPLES) {
zeroSamples = 0;
pause = true;
Motherboard.cancelSpeedRequest(this);
while (pause && isRunning()) {
try {
Thread.sleep(50);
timerSync.lock();
playbackFinished.signalAll();
} catch (InterruptedException ex) {
return;
} catch (IllegalMonitorStateException ex) {
// Do nothing
} finally {
try {
timerSync.unlock();
} catch (IllegalMonitorStateException ex) {
// Do nothing -- this is probably caused by a suspension event
}
}
}
}
try {
timerSync.lock();
playbackFinished.signalAll();
while (isRunning() && ticksSinceLastPlayback < ticksBeteenPlayback) {
cpuCountReached.await();
}
} catch (InterruptedException ex) {
// Do nothing, probably killing playback thread on purpose
} finally {
timerSync.unlock();
}
}
} catch (LineUnavailableException ex) {
Logger.getLogger(CardMockingboard.class
.getName()).log(Level.SEVERE, null, ex);
} finally {
Motherboard.cancelSpeedRequest(this);
System.out.println("Mockingboard playback stopped");
Motherboard.mixer.returnLine(this);
}
}
private void initPSG() {
int max = phasorMode ? 4 : 2;
chips = new PSG[max];
for (int i = 0; i < max; i++) {
chips[i] = new PSG(AY_ADDRESSES[i], CLOCK_SPEED, SAMPLE_RATE, "AY" + i);
}
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// There is no c8 rom access to emulate
}
// This fixes freezes when resizing the window, etc.
@Override
public boolean suspendWithCPU() {
return true;
}
}

View File

@ -0,0 +1,234 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Motherboard;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.state.Stateful;
import jace.core.Utility;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
/**
* This card strives to be a clone of the Applied Engineering RamFactor card
* http://www.downloads.reactivemicro.com/Public/Apple%20II%20Items/Hardware/RAMFactor/RAMFactor%20v1.5.pdf
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
@Name("RamFactor")
public class CardRamFactor extends Card {
int ADDR1 = 0;
int ADDR2 = 1;
int ADDR3 = 2;
int DATA = 3;
int BANK_SELECT = 0x0f;
@ConfigurableField(category = "memory", name = "Ram size", description = "Size of card ram in KB", shortName = "size", defaultValue = "8192")
public int RAM_SIZE = 8192;
int actualSize = RAM_SIZE * 1024;
// Important: pointer of current ram read/write for slinky access
@Stateful
int addressPointer = 0x0ffffff;
@Stateful
int firmwareBank = 0;
@ConfigurableField(category = "performance", name = "Speed Boost", description = "Boost emulator speed when RAM in use", shortName = "boostSpeed", defaultValue = "false")
public boolean speedBoost = false;
@Override
public String getDeviceName() {
return "RamFactor";
}
ImageIcon indicator;
public CardRamFactor() {
indicator=Utility.loadIcon("ram.png");
try {
loadRom("jace/data/RAMFactor14.rom");
} catch (IOException ex) {
Logger.getLogger(CardRamFactor.class.getName()).log(Level.SEVERE, null, ex);
}
allocateMemory(actualSize);
updateFirmwareMemory();
}
@Override
public void reset() {
firmwareBank = 0;
updateFirmwareMemory();
}
@Override
public void reconfigure() {
actualSize = RAM_SIZE * 1024;
allocateMemory(actualSize);
updateFirmwareMemory();
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
Emulator.getFrame().addIndicator(this, indicator);
value &= 0x0ff;
switch (register) {
case 0:
case 4:
// Lo-byte of pointer
if (type.isRead()) {
e.setNewValue(addressPointer & 0x0ff);
} else {
addressPointer = (addressPointer & 0x0ffff00) | value;
if (RAM_SIZE <= 1024) {
addressPointer |= 0x0f00000;
}
}
break;
case 1:
case 5:
// Mid-byte of pointer
if (type.isRead()) {
e.setNewValue((addressPointer >> 8) & 0x0ff);
} else {
addressPointer = (addressPointer & 0x0ff00ff) | (value << 8);
if (RAM_SIZE <= 1024) {
addressPointer |= 0x0f00000;
}
}
break;
case 2:
case 6:
// Hi-byte of pointer
if (type.isRead()) {
if (RAM_SIZE <= 1024) {
e.setNewValue(0x0f0 | ((addressPointer >> 16) & 0x0ff));
} else {
e.setNewValue((addressPointer >> 16) & 0x0ff);
}
} else {
addressPointer = (addressPointer & 0x00ffff) | (value << 16);
if (RAM_SIZE <= 1024) {
addressPointer |= 0x0f00000;
}
}
break;
case 3:
case 7:
if (type.isRead()) {
e.setNewValue(readMemory(addressPointer));
} else {
writeMemory(addressPointer, (byte) value);
}
addressPointer++;
// Keep the pointer in range
addressPointer &= 0x0ffffff;
break;
case 15: {
// Firmware bank select
if (type == TYPE.WRITE) {
firmwareBank = value;
updateFirmwareMemory();
}
}
default:
if (type.isRead()) {
e.setNewValue(0x0ff);
}
break;
}
}
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
if (speedBoost) {
Motherboard.requestSpeed(this);
}
}
@Override
public void tick() {
// Do nothing
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
if (speedBoost) {
Motherboard.requestSpeed(this);
}
}
@Stateful
public byte[] cardRam;
int ADDRESS_MASK = 0x07FFFFF;
private byte readMemory(int i) {
while (i >= cardRam.length) {
i -= cardRam.length;
}
return cardRam[i];
}
private void writeMemory(int i, byte newValue) {
while (i >= cardRam.length) {
i -= cardRam.length;
}
cardRam[i] = newValue;
}
private void allocateMemory(int size) {
if (cardRam != null && cardRam.length == size) return;
cardRam = new byte[size];
Arrays.fill(cardRam, (byte) 0);
}
@Override
public void setSlot(int slot) {
super.setSlot(slot);
indicator.setDescription("Slot "+getSlot());
// Rom has different images for each slot
updateFirmwareMemory();
}
final int cxRomLength = 0x02000;
byte[] romData = new byte[cxRomLength];
public void loadRom(String path) throws IOException {
InputStream romFile = CardRamFactor.class.getClassLoader().getResourceAsStream(path);
try {
if (romFile.read(romData) != cxRomLength) {
throw new IOException("Bad RamFactor rom size");
}
updateFirmwareMemory();
} catch (IOException ex) {
throw ex;
}
}
private void updateFirmwareMemory() {
int romOffset = 0;
if ((firmwareBank&1) == 1) {
romOffset = 0x01000;
}
getCxRom().loadData(romData, romOffset + getSlot()*0x0100, 256);
getC8Rom().loadData(romData, romOffset + 0x0800, 0x0800);
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.apple2e.RAM128k;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Computer;
import jace.core.PagedMemory;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.Stateful;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
/**
* Emulates the Ramworks Basic and Ramworks III cards
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
@Name("Ramworks III Memory Expansion")
public class CardRamworks extends RAM128k {
public static int BANK_SELECT = 0x0c073;
@Stateful
public int currentBank = 0;
@Stateful
public List<Map<BankType, PagedMemory>> memory;
public Map<BankType, PagedMemory> nullBank = generateBank();
@ConfigurableField(
category = "memory",
defaultValue = "3072",
name = "Memory Size",
description = "Size in KB. Should be a multiple of 64 and not exceed 8192. The real card cannot support more than 3072k")
public int memorySize = 3072;
public int maxBank = memorySize / 64;
private Map<BankType, PagedMemory> generateBank() {
Map<BankType, PagedMemory> memoryBank = new EnumMap<BankType, PagedMemory>(BankType.class);
memoryBank.put(BankType.MAIN_MEMORY, new PagedMemory(0xc000, PagedMemory.Type.ram));
memoryBank.put(BankType.LANGUAGE_CARD_1, new PagedMemory(0x3000, PagedMemory.Type.languageCard));
memoryBank.put(BankType.LANGUAGE_CARD_2, new PagedMemory(0x1000, PagedMemory.Type.languageCard));
return memoryBank;
}
public static enum BankType {
MAIN_MEMORY, LANGUAGE_CARD_1, LANGUAGE_CARD_2
};
public CardRamworks() {
super();
memory = new ArrayList<Map<BankType, PagedMemory>>(maxBank);
reconfigure();
}
private PagedMemory getAuxBank(BankType type, int bank) {
if (bank >= maxBank) {
return nullBank.get(type);
}
Map<BankType, PagedMemory> memoryBank = memory.get(bank);
if (memoryBank == null) {
memoryBank = generateBank();
memory.set(bank, memoryBank);
}
return memoryBank.get(type);
}
@Override
public PagedMemory getAuxVideoMemory() {
return getAuxBank(BankType.MAIN_MEMORY, 0);
}
PagedMemory lastAux = null;
@Override
public PagedMemory getAuxMemory() {
return getAuxBank(BankType.MAIN_MEMORY, currentBank);
}
@Override
public PagedMemory getAuxLanguageCard() {
return getAuxBank(BankType.LANGUAGE_CARD_1, currentBank);
}
@Override
public PagedMemory getAuxLanguageCard2() {
return getAuxBank(BankType.LANGUAGE_CARD_2, currentBank);
}
@Override
public String getName() {
return "Ramworks III";
}
@Override
public String getShortName() {
return "Ramworks3";
}
@Override
public void reconfigure() {
boolean resume = Computer.pause();
maxBank = memorySize / 64;
if (maxBank < 1) maxBank = 1;
if (maxBank > 128) maxBank = 128;
for (int i = memory.size(); i < maxBank; i++) {
memory.add(null);
}
configureActiveMemory();
if (resume) {
Computer.resume();
}
}
RAMListener bankSelectListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(BANK_SELECT);
setScopeEnd(BANK_SELECT);
}
@Override
protected void doEvent(RAMEvent e) {
currentBank = e.getNewValue();
configureActiveMemory();
}
};
@Override
public void attach() {
addListener(bankSelectListener);
}
@Override
public void detach() {
removeListener(bankSelectListener);
}
}

View File

@ -0,0 +1,490 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.config.Reconfigurable;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
/**
* Super Serial Card with serial-over-tcp/ip support. This is fully compatible
* with the SSC ROM and supported applications.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Super Serial Card")
public class CardSSC extends Card implements Reconfigurable, Runnable {
@ConfigurableField(name = "TCP/IP Port", shortName = "port")
public static short IP_PORT = 1977;
protected ServerSocket socket;
protected Socket clientSocket;
protected Thread listenThread;
private int lastInputByte = 0;
private boolean FULL_ECHO = true;
private boolean RECV_ACTIVE = true;
private boolean TRANS_ACTIVE = true;
// private boolean RECV_STRIP_LF = true;
// private boolean TRANS_ADD_LF = true;
@ConfigurableField(name = "Strip LF (recv)", shortName = "stripLF", defaultValue = "false", description = "Strip incoming linefeeds")
public boolean RECV_STRIP_LF = false;
@ConfigurableField(name = "Add LF (send)", shortName = "addLF", defaultValue = "false", description = "Append linefeeds after outgoing carriage returns")
public boolean TRANS_ADD_LF = false;
private boolean DTR = true;
public int SW1 = 0x01; // Read = Jumper block SW1
//Bit 0 = !SW1-6
//Bit 1 = !SW1-5
//Bit 4 = !SW1-4
//Bit 5 = !SW1-3
//Bit 6 = !SW1-2
//Bit 7 = !SW1-1
// 19200 baud (SW1-1,2,3,4 off)
// Communications mode (SW1-5,6 on)
public int SW1_SETTING = 0x0F0;
public int SW2_CTS = 0x02; // Read = Jumper block SW2 and CTS
//Bit 0 = !CTS
//SW2-6 = Allow interrupts (disable in ][, ][+)
//Bit 1 = !SW2-5 -- Generate LF after CR
//Bit 2 = !SW2-4
//Bit 3 = !SW2-3
//Bit 5 = !SW2-2
//Bit 7 = !SW2-1
// 1 stop bit (SW2-1 on)
// 8 data bits (SW2-2 on)
// No parity (SW2-3 don't care, SW2-4 off)
private static int SW2_SETTING = 0x04;
public int ACIA_Data = 0x08; // Read=Receive / Write=transmit
public int ACIA_Status = 0x09; // Read=Status / Write=Reset
public int ACIA_Command = 0x0A;
public int ACIA_Control = 0x0B;
public boolean PORT_CONNECTED = false;
public boolean RECV_IRQ_ENABLED = false;
public boolean TRANS_IRQ_ENABLED = false;
public boolean IRQ_TRIGGERED = false;
// Bitmask for stop bits (FF = 8, 7F = 7, etc)
private int DATA_BITS = 0x07F;
public String getDeviceName() {
return "Super Serial Card";
}
ImageIcon activityIndicator;
@Override
public void setSlot(int slot) {
try {
loadRom("jace/data/SSC.rom");
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
super.setSlot(slot);
activityIndicator = Utility.loadIcon("network-wired.png");
activityIndicator.setDescription("Slot " + slot);
}
@Override
public void run() {
while (socket != null && !socket.isClosed()) {
try {
Logger.getLogger(CardSSC.class.getName()).log(Level.INFO, "Slot " + getSlot() + " listening on port " + IP_PORT, (Throwable) null);
// System.out.println("Waiting for connect");
while ((clientSocket = socket.accept()) != null) {
clientConnected();
clientSocket.setTcpNoDelay(true);
while (isConnected()) {
try {
Thread.sleep(livenessCheck / 2);
} catch (InterruptedException ex) {
// Do nothing
}
}
clientDisconnected();
hangUp();
}
} catch (SocketTimeoutException ex) {
// Do nothing
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.FINE, null, ex);
}
}
socket = null;
}
// Called when a client first connects via telnet
public void clientConnected() {
System.err.println("Client connected");
}
// Called when a client disconnects
public void clientDisconnected() {
System.out.println("Client disconnected");
}
public void loadRom(String path) throws IOException {
// Load rom file, first 0x0700 bytes are C8 rom, last 0x0100 bytes are CX rom
// CF00-CFFF are unused by the SSC
InputStream romFile = CardSSC.class.getClassLoader().getResourceAsStream(path);
final int cxRomLength = 0x0100;
final int c8RomLength = 0x0700;
byte[] romxData = new byte[cxRomLength];
byte[] rom8Data = new byte[c8RomLength];
try {
if (romFile.read(rom8Data) != c8RomLength) {
throw new IOException("Bad SSC rom size");
}
getC8Rom().loadData(rom8Data);
if (romFile.read(romxData) != cxRomLength) {
throw new IOException("Bad SSC rom size");
}
getCxRom().loadData(romxData);
} catch (IOException ex) {
throw ex;
}
}
@Override
public void reset() {
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
suspend();
resume();
}
});
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
try {
int newValue = -1;
switch (type) {
case EXECUTE:
case READ_OPERAND:
case READ_DATA:
case READ:
if (register == SW1) {
newValue = SW1_SETTING;
}
if (register == SW2_CTS) {
newValue = SW2_SETTING & 0x0FE;
// if port is connected and ready to send another byte, set CTS bit on
newValue |= (PORT_CONNECTED && inputAvailable()) ? 0x00 : 0x01;
}
if (register == ACIA_Data) {
Emulator.getFrame().addIndicator(this, activityIndicator);
newValue = getInputByte();
if (RECV_IRQ_ENABLED) {
triggerIRQ();
}
}
if (register == ACIA_Status) {
newValue = 0;
// 0 = Parity error (1)
// 1 = Framing error (1)
// 2 = Overrun error (1)
// 3 = ACIA Receive Register full (1)
if (inputAvailable()) {
newValue |= 0x08;
}
// 4 = ACIA Transmit Register empty (1)
if (true) {
newValue |= 0x010;
}
// 5 = Data Carrier Detect (DCD) true (0)
// 6 = Data Set Ready (DSR) true (0)
// 7 = Interrupt (IRQ) has occurred
if (IRQ_TRIGGERED) {
newValue |= 0x080;
}
IRQ_TRIGGERED = false;
}
if (register == ACIA_Command) {
newValue = 0;
// 0 = DTR Enable (1) / Disable (0) receiver and IRQ
// 1 = Allow IRQ (1) when status bit 3 is true
if (RECV_IRQ_ENABLED) {
newValue |= 2;
}
// 2,3 = Control transmit IRQ, RTS level and transmitter
newValue |= 12;
// 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0)
if (FULL_ECHO) {
newValue |= 16;
}
// 5 = Control parity
}
if (register == ACIA_Control) {
// 0-3 = Baud Rate
// 4 = Use baud rate generator (1) / Use external clock (0)
// 5-6 = Number of data bits (00 = 8, 10 = 7, 01 = 6, 11 = 5)
// 7 = Number of stop bits (0 = 1 stop bit, 1 = 1-1/2 (with 5 data bits no parity), 1 (8 data plus parity) or 2)
newValue = 0;
}
break;
case WRITE:
if (register == ACIA_Data) {
Emulator.getFrame().addIndicator(this, activityIndicator);
sendOutputByte(value & 0x0FF);
if (TRANS_IRQ_ENABLED) {
triggerIRQ();
}
}
if (register == ACIA_Command) {
// 0 = DTR Enable (1) / Disable (0) receiver and IRQ
DTR = ((value & 1) == 0);
// 0 = Allow IRQ (0) when status bit 3 is true
if ((value & 2) == 0) {
RECV_IRQ_ENABLED = !DTR;
} else {
RECV_IRQ_ENABLED = false;
}
// 2,3 = Control transmit IRQ, RTS level and transmitter
// 0 0 = Transmit interrupt off, RTS high, Transmitter off
// 1 0 = Transmit interrupt ON, RTS low, Transmitter on
// 0 1 = Transmit interrupt off, RTS low, Transmitter on
// 1 1 = Transmit interrupt off, RTS low, Transmit BRK
switch ((value >> 2) & 3) {
case 0:
TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = false;
break;
case 1:
TRANS_IRQ_ENABLED = true;
TRANS_ACTIVE = true;
break;
case 2:
TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = true;
break;
case 3:
TRANS_IRQ_ENABLED = false;
TRANS_ACTIVE = true;
break;
}
// 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0)
FULL_ECHO = ((value & 16) > 0);
System.out.println("Echo set to " + FULL_ECHO);
// 5 = Control parity
}
if (register == ACIA_Control) {
// 0-3 = Baud Rate
// 4 = Use baud rate generator (1) / Use external clock (0)
// 5-6 = Number of data bits (00 = 8, 01 = 7, 10 = 6, 11 = 5)
// 7 = Number of stop bits (0 = 1 stop bit, 1 = 1-1/2 (with 5 data bits no parity), 1 (8 data plus parity) or 2)
int bits = (value & 127) >> 5;
System.out.println("Data bits set to " + (8 - bits));
switch (bits) {
case 0:
DATA_BITS = 0x0FF;
break;
case 1:
DATA_BITS = 0x07F;
break;
case 2:
DATA_BITS = 0x03F;
break;
case 3:
DATA_BITS = 0x01F;
break;
}
}
break;
}
if (newValue > -1) {
e.setNewValue(newValue);
value = newValue;
}
// System.out.println("SSC I/O "+type+", register "+register+", value "+value);
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
}
@Override
public void tick() {
// Do nothing
}
public boolean inputAvailable() throws IOException {
if (isConnected() && clientSocket != null && clientSocket.getInputStream() != null) {
return clientSocket.getInputStream().available() > 0;
} else {
return false;
}
}
private int getInputByte() throws IOException {
if (inputAvailable()) {
int in = clientSocket.getInputStream().read() & DATA_BITS;
// System.out.write(in & 0x07f);
if (RECV_STRIP_LF && in == 10 && lastInputByte == 13) {
in = clientSocket.getInputStream().read() & DATA_BITS;
// System.out.write(in & 0x07f);
}
// System.out.flush();
lastInputByte = in;
}
return lastInputByte;
}
long lastSuccessfulWrite = -1L;
private void sendOutputByte(int i) throws IOException {
if (clientSocket != null && clientSocket.isConnected()) {
try {
// System.out.write(i & 0x07f);
clientSocket.getOutputStream().write(i & DATA_BITS);
if (TRANS_ADD_LF && (i & DATA_BITS) == 13) {
// System.out.write(10);
clientSocket.getOutputStream().write(10);
}
clientSocket.getOutputStream().flush();
lastSuccessfulWrite = System.currentTimeMillis();
} catch (IOException e) {
lastSuccessfulWrite = -1L;
hangUp();
}
} else {
lastSuccessfulWrite = -1L;
}
}
private void setCTS(boolean b) throws InterruptedException {
PORT_CONNECTED = b;
if (b == false) {
reset();
}
}
private boolean getCTS() throws InterruptedException {
return PORT_CONNECTED;
}
private void triggerIRQ() {
IRQ_TRIGGERED = true;
Computer.getComputer().getCpu().generateInterrupt();
}
public void hangUp() {
lastInputByte = 0;
lastSuccessfulWrite = -1L;
if (clientSocket != null && clientSocket.isConnected()) {
try {
clientSocket.shutdownInput();
clientSocket.shutdownOutput();
clientSocket.close();
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
}
clientSocket = null;
}
/**
* Detach from server socket port and ensure that the card's resources are
* no longer in use
*/
@Override
public boolean suspend() {
if (socket != null) {
try {
socket.close();
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
}
hangUp();
if (listenThread != null && listenThread.isAlive()) {
try {
listenThread.join();
} catch (InterruptedException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
}
listenThread = null;
socket = null;
return super.suspend();
}
@Override
public void resume() {
if (!isRunning()) {
super.resume();
RECV_IRQ_ENABLED = false;
TRANS_IRQ_ENABLED = false;
IRQ_TRIGGERED = false;
try {
socket = new ServerSocket(IP_PORT);
socket.setReuseAddress(true);
socket.setSoTimeout(0);
//socket.setReuseAddress(true);
listenThread = new Thread(this);
listenThread.setDaemon(false);
listenThread.setName("SSC port listener");
listenThread.start();
} catch (IOException ex) {
suspend();
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
ex.printStackTrace();
}
}
}
@ConfigurableField(category = "Advanced", name = "Liveness check interval", description = "How often the connection is polled for signs of life (in milliseconds)")
public int livenessCheck = 10000;
public boolean isConnected() {
if (clientSocket == null || !clientSocket.isConnected()) {
return false;
}
if (lastSuccessfulWrite == -1 || System.currentTimeMillis() > (lastSuccessfulWrite + livenessCheck)) {
try {
sendOutputByte(0);
return true;
} catch (IOException e) {
return false;
}
} else {
return true;
}
}
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// Do nothing -- the card rom does everything
return;
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// There is no special c8 rom behavior for this card
}
}

View File

@ -0,0 +1,329 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.Emulator;
import jace.apple2e.MOS65C02;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Motherboard;
import jace.core.PagedMemory;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
/**
* Implementation of the Thunderclock Plus with some limitations:
*
* The apple cannot set time. The firmware will act like it is working but
* nothing will actually happen when a time set command is sent.
*
* Though the interrupt features are implemented, they have not been tested.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("ThunderClock Plus")
public class CardThunderclock extends Card {
ImageIcon clockIcon;
ImageIcon clockFixIcon;
long lastShownIcon = -1;
// Only mention that the clock is read if it hasn't been checked for over 30 seconds
// This is to avoid showing it all the time in programs that poll it constantly
long MIN_WAIT = 30000;
@ConfigurableField(category = "OS", name = "Patch Prodos Year", description = "If enabled, the Prodos clock driver will be patched to use the current year.")
public boolean attemptYearPatch = true;
public CardThunderclock() {
try {
loadRom("jace/data/thunderclock_plus.rom");
} catch (IOException ex) {
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
}
clockIcon = Utility.loadIcon("clock.png");
clockFixIcon = Utility.loadIcon("clock_fix.png");
clockFixIcon.setDescription("Fixed");
}
// Raw format: 40 bits, in BCD form (it actually streams out in the reverse order of this, bit 0 first)
// The data format is fully elaborated in the datasheet of the calendar/clock chip: NEC uPD1990AC
// month (1-12) -- hex
// day of week (0-6)
// day of month, tens digit (0-3)
// day of month, ones digit (0-9)
// hour, tens digit (0-2)
// hour, ones digit (0-9)
// minute, tens digit (0-5)
// minute, ones digit (0-9)
// second, tens digit (0-5)
// second, ones digit (0-9)
@Override
public void reset() {
irqAsserted = false;
irqEnabled = false;
ticks = 0;
timerRate = 0;
}
public boolean strobe = false;
public boolean clock = false;
public boolean shiftMode = false;
public boolean irqEnabled = false;
public boolean irqAsserted = false;
public boolean timerEnabled = false;
public int timerRate = 0;
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
// Data is read via bit-banging the status register
// Nibbles are sent lowest significant bit first.
// Commands are sent to the register followed by a strobe pulse on bit 2 on and off
// So senting the time read command would be a string of bytes: 0x018, 0x01c and then 0x018 again
//
// Time read is signaled by 0x018 followed by a register shift command 0x08
// When register shift is active, a clock signal is used to move to the next bit.
//
// A bit is placed in data-in (bit 0)
// Then the clock is raised (bit 1 set) and then lowered (bit 1 unset)
// After this, the next time the register is read it will have the next bit
// of the register in the hibit (bit 7)
//
// Reg 0: Command register
// data in = 0x01
// clock = 0x02
// strobe = 0x04
// register hold = 0x0
// register shift = 0x08
// time set = 0x010
// time read = 0x018
// Timer modes = 0x020 (64hz), 0x028 (256hz), 0x030 (2048hz)
// Interrupt enable = 0x040 (IRQ assert is read as 0x020 in the status register)
// data out = 0x080
if (type.isRead() && register == 0) {
e.setNewValue((peekBit()) | (irqAsserted ? 0x020 : 0));
return;
}
if (register == 8) {
irqAsserted = false;
return;
} else if (register != 0) {
return;
}
boolean isClock = (value & 0x02) != 0;
boolean isStrobe = (value & 0x04) != 0;
boolean isShift = (value & 0x08) != 0;
boolean isRead = (value & 0x18) != 0;
if (!isClock && clock) {
if (buffer != null) {
buffer.pop();
}
}
if (!isStrobe && strobe) {
shiftMode = isShift;
if (isRead) {
if (attemptYearPatch) {
performProdosPatch();
}
getTime();
clockIcon.setDescription("Slot " + getSlot());
long now = System.currentTimeMillis();
if ((now - lastShownIcon) > MIN_WAIT) {
Emulator.getFrame().addIndicator(this, clockIcon, 3000);
}
lastShownIcon = now;
}
shiftMode = isShift;
}
timerEnabled = (value & 0x020) != 0;
ticks = 0;
if (timerEnabled) {
switch (value & 0x038) {
case 0x020:
timerRate = (int) (Motherboard.SPEED / 64);
break;
case 0x028:
timerRate = (int) (Motherboard.SPEED / 256);
break;
case 0x030:
timerRate = (int) (Motherboard.SPEED / 2048);
break;
default:
timerEnabled = false;
timerRate = 0;
}
} else {
timerRate = 0;
}
irqEnabled = (value & 0x040) != 0;
clock = isClock;
strobe = isStrobe;
}
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// Firmware ROM is used -- only I/O port was needed for proper emulation
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// C8 access is used to read the clock directly
}
@Override
protected String getDeviceName() {
return "Thunderclock Plus";
}
int ticks = 0;
@Override
public void tick() {
if (timerEnabled) {
ticks++;
if (ticks >= timerRate) {
ticks = 0;
irqAsserted = true;
if (irqEnabled) {
Computer.getComputer().getCpu().generateInterrupt();
}
}
}
}
private void getTime() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(System.currentTimeMillis());
clearBuffer();
pushNibble(cal.get(Calendar.MONTH) + 1);
pushNibble(cal.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY);
pushNibble(cal.get(Calendar.DAY_OF_MONTH) / 10);
pushNibble(cal.get(Calendar.DAY_OF_MONTH) % 10);
pushNibble(cal.get(Calendar.HOUR_OF_DAY) / 10);
pushNibble(cal.get(Calendar.HOUR_OF_DAY) % 10);
pushNibble(cal.get(Calendar.MINUTE) / 10);
pushNibble(cal.get(Calendar.MINUTE) % 10);
pushNibble(cal.get(Calendar.SECOND) / 10);
pushNibble(cal.get(Calendar.SECOND) % 10);
}
Stack<Boolean> buffer;
private void clearBuffer() {
if (buffer == null) {
buffer = new Stack<Boolean>();
} else {
buffer.clear();
}
}
private void pushNibble(int value) {
for (int i = 0; i < 4; i++) {
boolean val = (value & 8) != 0;
buffer.push(val);
value <<= 1;
}
}
private int peekBit() {
if (buffer == null || buffer.isEmpty()) {
return 0;
}
return buffer.peek() ? 0x080 : 0;
}
public void loadRom(String path) throws IOException {
InputStream romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path);
final int cxRomLength = 0x0100;
final int c8RomLength = 0x0700;
byte[] romxData = new byte[cxRomLength];
byte[] rom8Data = new byte[c8RomLength];
try {
if (romFile.read(romxData) != cxRomLength) {
throw new IOException("Bad Thunderclock rom size");
}
getCxRom().loadData(romxData);
romFile.close();
romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path);
if (romFile.read(rom8Data) != c8RomLength) {
throw new IOException("Bad Thunderclock rom size");
}
getC8Rom().loadData(rom8Data);
romFile.close();
} catch (IOException ex) {
throw ex;
}
}
static byte[] DRIVER_PATTERN = {
(byte) 0x00, (byte) 0x01f, (byte) 0x03b, (byte) 0x05a,
(byte) 0x078, (byte) 0x097, (byte) 0x0b5, (byte) 0x0d3,
(byte) 0x0f2
};
static int DRIVER_OFFSET = -26;
int patchLoc = -1;
/**
* Scan active memory for the Prodos clock driver and patch the internal
* code to use a fixed value for the present year. This means Prodos will
* always tell time correctly.
*/
private void performProdosPatch() {
PagedMemory ram = Computer.getComputer().getMemory().activeRead;
if (patchLoc > 0) {
// We've already patched, just validate
if (ram.readByte(patchLoc) == (byte) MOS65C02.OPCODE.LDA_IMM.getCode()) {
return;
}
}
int match = 0;
int matchStart = 0;
for (int addr = 0x08000; addr < 0x010000; addr++) {
if (ram.readByte(addr) == DRIVER_PATTERN[match]) {
match++;
if (match == DRIVER_PATTERN.length) {
break;
}
} else {
match = 0;
matchStart = addr;
}
}
if (match != DRIVER_PATTERN.length) {
return;
}
patchLoc = matchStart + DRIVER_OFFSET;
ram.writeByte(patchLoc, (byte) MOS65C02.OPCODE.LDA_IMM.getCode());
int year = Calendar.getInstance().get(Calendar.YEAR) % 100;
ram.writeByte(patchLoc + 1, (byte) year);
ram.writeByte(patchLoc + 2, (byte) MOS65C02.OPCODE.NOP.getCode());
ram.writeByte(patchLoc + 3, (byte) MOS65C02.OPCODE.NOP.getCode());
Emulator.getFrame().addIndicator(this, clockFixIcon, 4000);
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.apple2e.SoftSwitches;
import jace.core.Computer;
import jace.core.Keyboard;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import java.awt.Rectangle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Attempt at adding accessibility by redirecting screen/keyboard traffic to
* stdout/stdin of the console. Doesn't work well, unfortunately.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class ConsoleProbe {
public static boolean enabled = true;
public String[] lastScreen = new String[24];
public List<Rectangle> regions = new ArrayList<Rectangle>();
private RAMListener textListener;
public static long lastChange;
public static long updateDelay = 100L;
public static boolean readerActive = false;
public Computer computer;
private Thread keyReaderThread;
public void init(final Computer c) {
computer = c;
enabled = true;
keyReaderThread = new Thread(new KeyReader());
keyReaderThread.setName("Console probe key reader");
keyReaderThread.start();
textListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0400);
setScopeEnd(0x0BFF);
}
@Override
protected void doEvent(RAMEvent e) {
if (e.getAddress() < 0x0800 && SoftSwitches.PAGE2.isOn()) {
return;
}
if (SoftSwitches.TEXT.isOff()) {
if (SoftSwitches.MIXED.isOn()) {
handleMixedMode();
}
} else {
handleTextMode();
}
}
private void handleMixedMode() {
handleTextMode();
}
private void handleTextMode() {
lastChange = System.currentTimeMillis();
if (readerActive) {
return;
}
Thread t = new Thread(new ScreenReader());
t.start();
}
};
c.getMemory().addListener(textListener);
}
public static synchronized void performRead() {
}
public void shutdown() {
enabled = false;
if (textListener != null) {
computer.getMemory().removeListener(textListener);
}
if (keyReaderThread != null && keyReaderThread.isAlive()) {
try {
keyReaderThread.join();
} catch (InterruptedException ex) {
Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public static class ScreenReader implements Runnable {
public void run() {
readerActive = true;
try {
// Keep sleeping until there have been no more screen changes during the specified delay period
// It is possible that the lastChange will keep being updated while in this loop
// That is both expected and the reason this is a loop!
long delay = 0;
while (System.currentTimeMillis() - lastChange <= updateDelay) {
delay = updateDelay - System.currentTimeMillis() - lastChange;
if (delay > 0) {
Thread.sleep(delay);
}
}
} catch (InterruptedException ex) {
Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex);
}
// Signal that we're off to go read the screen (and any additional update will need to spawn the thread again)
readerActive = false;
performRead();
}
}
public static class KeyReader implements Runnable {
public Computer c;
public void run() {
while (true) {
try {
while (enabled && (System.in.available() == 0 || Keyboard.readState() < 0)) {
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
System.out.println(ex.getMessage());
Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (!enabled) {
return;
}
int ch = System.in.read();
if (ch == 10) {
ch = 13;
}
Keyboard.pressKey((byte) ch);
} catch (IOException ex) {
System.out.println(ex.getMessage());
Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.apple2e.MOS65C02;
import jace.core.Computer;
import jace.core.Keyboard;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import java.awt.Toolkit;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Attempt to simplify what ConsoleProbe was attempting. Still not ready for any
* real use.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class ConsoleProbeSimple {
RAMListener cout;
public static int COUT = 0xFDED;
public void init(final Computer c) {
Thread t = new Thread(new KeyReader());
t.start();
cout = new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(COUT);
}
@Override
protected void doEvent(RAMEvent e) {
MOS65C02 cpu = (MOS65C02) c.getCpu();
int ch = cpu.A & 0x07f;
if (ch == 13) {
System.out.println();
} else if (ch < ' ') {
if (ch == 7) {
Toolkit.getDefaultToolkit().beep();
} else {
System.out.println("CHR" + ch);
}
} else {
System.out.print((char) ch);
}
}
};
c.getMemory().addListener(cout);
}
public static class KeyReader implements Runnable {
public Computer c;
public void run() {
while (true) {
try {
while (System.in.available() == 0 || Keyboard.readState() < 0) {
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
System.out.println(ex.getMessage());
Logger.getLogger(ConsoleProbeSimple.class.getName()).log(Level.SEVERE, null, ex);
}
}
int ch = System.in.read();
if (ch == 10) {
ch = 13;
}
Keyboard.pressKey((byte) ch);
} catch (IOException ex) {
System.out.println(ex.getMessage());
Logger.getLogger(ConsoleProbeSimple.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}

View File

@ -0,0 +1,279 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.library.MediaConsumer;
import jace.library.MediaEntry;
import jace.library.MediaEntry.MediaFile;
import jace.state.StateManager;
import jace.state.Stateful;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.locks.LockSupport;
import javax.swing.ImageIcon;
/**
* This implements the mechanical part of the disk drive and tracks changes to
* disk images. The actual handling of disk images is performed in the
* FloppyDisk class. The apple interface card portion is managed in the
* CardDiskII class. Useful reading:
* http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public class DiskIIDrive implements MediaConsumer {
FloppyDisk disk;
// Number of milliseconds to wait between last write and update to disk image
public static long WRITE_UPDATE_DELAY = 1000;
// Flag to halt if any writes to floopy occur when updating physical disk image
boolean diskUpdatePending = false;
// Last time of write operation
long lastWriteTime;
// Managed thread to update disk image in background
Thread writerThread;
private final byte[][] driveHeadStepDelta = {
{0, 0, 1, 1, 0, 0, 1, 1, -1, -1, 0, 0, -1, -1, 0, 0}, // phase 0
{0, -1, 0, -1, 1, 0, 1, 0, 0, -1, 0, -1, 1, 0, 1, 0}, // phase 1
{0, 0, -1, -1, 0, 0, -1, -1, 1, 1, 0, 0, 1, 1, 0, 0}, // phase 2
{0, 1, 0, 1, -1, 0, -1, 0, 0, 1, 0, 1, -1, 0, -1, 0}}; // phase 3
@Stateful
public int halfTrack;
@Stateful
public int trackStartOffset;
@Stateful
public int nibbleOffset;
@Stateful
public boolean writeMode;
@Stateful
public boolean driveOn;
@Stateful
public int magnets;
@Stateful
public byte latch;
@Stateful
public int spinCount;
Set<Integer> dirtyTracks;
public void reset() {
driveOn = false;
magnets = 0;
dirtyTracks = new HashSet<Integer>();
diskUpdatePending = false;
}
void step(int register) {
// switch drive head stepper motor magnets on/off
int magnet = (register >> 1) & 0x3;
magnets &= ~(1 << magnet);
magnets |= ((register & 0x1) << magnet);
// step the drive head according to stepper magnet changes
if (driveOn) {
int delta = driveHeadStepDelta[halfTrack & 0x3][magnets];
if (delta != 0) {
int newHalfTrack = halfTrack + delta;
if (newHalfTrack < 0) {
newHalfTrack = 0;
} else if (newHalfTrack > FloppyDisk.HALF_TRACK_COUNT) {
newHalfTrack = FloppyDisk.HALF_TRACK_COUNT;
}
if (newHalfTrack != halfTrack) {
halfTrack = newHalfTrack;
trackStartOffset = (halfTrack >> 1) * FloppyDisk.TRACK_NIBBLE_LENGTH;
if (trackStartOffset >= FloppyDisk.DISK_NIBBLE_LENGTH) {
trackStartOffset = FloppyDisk.DISK_NIBBLE_LENGTH - FloppyDisk.TRACK_NIBBLE_LENGTH;
}
nibbleOffset = 0;
//System.out.printf("new half track %d\n", currentHalfTrack);
}
}
}
}
void setOn(boolean b) {
driveOn = b;
}
boolean isOn() {
return driveOn;
}
byte readLatch() {
byte result = 0x07f;
if (!writeMode) {
spinCount = (spinCount + 1) & 0x0F;
if (spinCount > 0) {
if (disk != null) {
result = disk.nibbles[trackStartOffset + nibbleOffset++];
} else {
result = (byte) 0x0ff;
}
}
if (nibbleOffset >= FloppyDisk.TRACK_NIBBLE_LENGTH) {
nibbleOffset = 0;
}
} else {
spinCount = (spinCount + 1) & 0x0F;
if (spinCount > 0) {
result = (byte) 0x080;
}
}
return result;
}
void write() {
if (writeMode) {
while (diskUpdatePending) {
// If another thread requested writes to block (e.g. because of disk activity), wait for it to finish!
LockSupport.parkNanos(1000);
}
if (disk != null) {
// Do nothing if write-protection is enabled!
if (getMediaEntry() == null || !getMediaEntry().writeProtected) {
dirtyTracks.add(trackStartOffset / FloppyDisk.TRACK_NIBBLE_LENGTH);
disk.nibbles[trackStartOffset + nibbleOffset++] = latch;
triggerDiskUpdate();
StateManager.markDirtyValue(disk.nibbles);
}
}
if (nibbleOffset >= FloppyDisk.TRACK_NIBBLE_LENGTH) {
nibbleOffset = 0;
}
}
}
void setLatchValue(byte value) {
if (writeMode) {
latch = value;
} else {
latch = (byte) 0xFF;
}
}
void setReadMode() {
writeMode = false;
}
void setWriteMode() {
writeMode = true;
}
private void updateDisk() {
// Signal disk update is underway
diskUpdatePending = true;
// Update all tracks as necessary
if (disk != null) {
for (Integer track : dirtyTracks) {
disk.updateTrack(track);
}
}
// Empty out dirty list
dirtyTracks.clear();
// Signal disk update is completed
diskUpdatePending = false;
}
private void triggerDiskUpdate() {
lastWriteTime = System.currentTimeMillis();
if (writerThread == null || !writerThread.isAlive()) {
writerThread = new Thread(new Runnable() {
@Override
public void run() {
long diff = 0;
// Wait until there have been no virtual writes for specified delay time
while ((diff = System.currentTimeMillis() - lastWriteTime) < WRITE_UPDATE_DELAY) {
// Sleep for difference of time
LockSupport.parkNanos(diff * 1000);
// Note: In the meantime, there could have been another disk write,
// in which case this loop will repeat again as needed.
}
updateDisk();
}
});
writerThread.start();
}
}
void insertDisk(File diskPath) throws IOException {
disk = new FloppyDisk(diskPath);
dirtyTracks = new HashSet<Integer>();
// Emulator state has changed significantly, reset state manager
StateManager.getInstance().invalidate();
}
private ImageIcon icon;
public ImageIcon getIcon() {
return icon;
}
public void setIcon(ImageIcon i) {
icon = i;
}
private MediaEntry currentMediaEntry;
private MediaFile currentMediaFile;
public void eject() {
if (disk == null) {
return;
}
waitForPendingWrites();
disk = null;
dirtyTracks = new HashSet<Integer>();
// Emulator state has changed significantly, reset state manager
StateManager.getInstance().invalidate();
}
public void insertMedia(MediaEntry e, MediaFile f) throws IOException {
if (!isAccepted(e, f)) {
return;
}
eject();
insertDisk(f.path);
currentMediaEntry = e;
currentMediaFile = f;
}
public MediaEntry getMediaEntry() {
return currentMediaEntry;
}
public MediaFile getMediaFile() {
return currentMediaFile;
}
public boolean isAccepted(MediaEntry e, MediaFile f) {
if (f == null) return false;
System.out.println("Type is accepted: "+f.path+"; "+e.type.toString()+": "+e.type.is140kb);
return e.type.is140kb;
}
private void waitForPendingWrites() {
while (diskUpdatePending || !dirtyTracks.isEmpty()) {
// If the current disk has unsaved changes, wait!!!
LockSupport.parkNanos(1000);
}
}
}

View File

@ -0,0 +1,420 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.state.StateManager;
import jace.state.Stateful;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Representation of a 140kb floppy disk image. This also performs conversions as
* needed. Internally, the emulator will always use a "nibblized" disk
* representation during active use. So if any sort of dsk/do/po image is loaded
* it will be converted first. If changes are made to the disk then the tracks
* will be converted back into de-nibblized form prior to saving. The
* DiskIIDrive class managed disk changes, this class is more an interface to
* load/save various disk formats and hold the active disk image while it is in
* use.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public class FloppyDisk {
@Stateful
boolean writeProtected;
@Stateful
public int headerLength = 0;
@Stateful
public boolean isNibblizedImage;
@Stateful
public int volumeNumber;
static final public int TRACK_NIBBLE_LENGTH = 0x1A00;
static final public int TRACK_COUNT = 35;
static final public int SECTOR_COUNT = 16;
static final public int HALF_TRACK_COUNT = TRACK_COUNT * 2;
static final public int DISK_NIBBLE_LENGTH = TRACK_NIBBLE_LENGTH * TRACK_COUNT;
static final public int DISK_PLAIN_LENGTH = 143360;
static final public int DISK_2MG_NON_NIB_LENGTH = DISK_PLAIN_LENGTH + 0x040;
static final public int DISK_2MG_NIB_LENGTH = DISK_NIBBLE_LENGTH + 0x040;
@Stateful
public byte[] nibbles = new byte[DISK_NIBBLE_LENGTH];
// Denotes the mapping of physical order (array index) to the dos 3.3 logical order (value)
public static int[] DOS_33_SECTOR_ORDER = {
0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04,
0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F
};
// Denotes the mapping of physical order (array index) to the Prodos logical order (value)
// Borrowed from KEGS -- thanks KEGS team!
public static int[] PRODOS_SECTOR_ORDER = {
0x00, 0x08, 0x01, 0x09, 0x02, 0x0a, 0x03, 0x0b,
0x04, 0x0c, 0x05, 0x0d, 0x06, 0x0e, 0x07, 0x0f
};
// Sector ordering used for current disk
@Stateful
public int[] currentSectorOrder;
// Location of image
@Stateful
public File diskPath;
static int[] NIBBLE_62 = {
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff};
static int[] NIBBLE_62_REVERSE;
static {
NIBBLE_62_REVERSE = new int[256];
for (int i = 0; i < NIBBLE_62.length; i++) {
NIBBLE_62_REVERSE[NIBBLE_62[i] & 0x0ff] = 0x0ff & i;
}
}
private static boolean DEBUG = false;
public FloppyDisk() throws IOException {
// This constructor is only used for disk conversion...
}
/**
*
* @param diskFile
* @throws IOException
*/
public FloppyDisk(File diskFile) throws IOException {
FileInputStream input = new FileInputStream(diskFile);
String name = diskFile.getName().toUpperCase();
readDisk(input, name.endsWith(".PO"));
writeProtected = !diskFile.canWrite();
diskPath = diskFile;
}
// brendanr: refactored to use input stream
public void readDisk(InputStream diskFile, boolean prodosOrder) throws IOException {
isNibblizedImage = true;
volumeNumber = CardDiskII.DEFAULT_VOLUME_NUMBER;
headerLength = 0;
try {
int bytesRead = diskFile.read(nibbles);
if (bytesRead == DISK_2MG_NIB_LENGTH) {
bytesRead -= 0x040;
// Try to pick up volume number from 2MG header.
volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254;
nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length);
headerLength = 0x040;
}
if (bytesRead == DISK_2MG_NON_NIB_LENGTH) {
bytesRead -= 0x040;
// Try to pick up correct sector ordering and volume from 2MG header.
prodosOrder = (nibbles[12] == 01);
volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254;
nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length);
headerLength = 0x040;
}
currentSectorOrder = prodosOrder ? PRODOS_SECTOR_ORDER : DOS_33_SECTOR_ORDER;
if (bytesRead == DISK_PLAIN_LENGTH) {
isNibblizedImage = false;
nibbles = nibblize(nibbles);
if (nibbles.length != DISK_NIBBLE_LENGTH) {
throw new IOException("Nibblized version is wrong size (expected-actual = " + (DISK_NIBBLE_LENGTH - nibbles.length) + ")");
}
} else if (bytesRead != DISK_NIBBLE_LENGTH) {
throw new IOException("Bad NIB size " + bytesRead + "; JACE only recognizes plain images " + DISK_PLAIN_LENGTH + " or nibble images " + DISK_NIBBLE_LENGTH + " sizes");
}
} catch (IOException ex) {
throw ex;
}
StateManager.markDirtyValue(nibbles);
StateManager.markDirtyValue(currentSectorOrder);
}
/*
* Convert a block-format disk to a 6-by-2 nibblized encoding scheme (raw NIB disk format)
*/
public byte[] nibblize(byte[] nibbles) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (int track = 0; track < TRACK_COUNT; track++) {
for (int sector = 0; sector < SECTOR_COUNT; sector++) {
// 15 junk bytes
writeJunkBytes(output, 15);
// Address block
writeAddressBlock(output, track, sector);
// 4 junk bytes
writeJunkBytes(output, 4);
// Data block
nibblizeBlock(output, track, currentSectorOrder[sector], nibbles);
// 34 junk bytes
writeJunkBytes(output, 34);
}
}
return output.toByteArray();
}
private void writeJunkBytes(ByteArrayOutputStream output, int i) {
for (int b = 0; b < i; b++) {
output.write(0x0FF);
}
}
private void writeAddressBlock(ByteArrayOutputStream output, int track, int sector) throws IOException {
output.write(0x0d5);
output.write(0x0aa);
output.write(0x096);
int checksum = 00;
// volume
checksum ^= volumeNumber;
output.write(getOddEven(volumeNumber));
// track
checksum ^= track;
output.write(getOddEven(track));
// sector
checksum ^= sector;
output.write(getOddEven(sector));
// checksum
output.write(getOddEven(checksum & 0x0ff));
output.write(0x0de);
output.write(0x0aa);
output.write(0x0eb);
}
private byte[] getOddEven(int i) {
byte[] out = new byte[2];
out[0] = (byte) (0xAA | (i >> 1));
out[1] = (byte) (0xAA | i);
return out;
}
private int decodeOddEven(byte b1, byte b2) {
// return (((b1 ^ 0x0AA) << 1) & 0x0ff) | ((b2 ^ 0x0AA) & 0x0ff);
int result = ((((b1 << 1) | 1) & b2) & 0x0ff);
return result;
}
private void nibblizeBlock(ByteArrayOutputStream output, int track, int sector, byte[] nibbles) {
int offset = ((track * SECTOR_COUNT) + sector) * 256;
int[] temp = new int[342];
for (int i = 0; i < 256; i++) {
temp[i] = (nibbles[offset + i] & 0x0ff) >> 2;
}
int hi = 0x001;
int med = 0x0AB;
int low = 0x055;
for (int i = 0; i < 0x56; i++) {
int value = ((nibbles[offset + hi] & 1) << 5)
| ((nibbles[offset + hi] & 2) << 3)
| ((nibbles[offset + med] & 1) << 3)
| ((nibbles[offset + med] & 2) << 1)
| ((nibbles[offset + low] & 1) << 1)
| ((nibbles[offset + low] & 2) >> 1);
temp[i + 256] = value;
hi = (hi - 1) & 0x0ff;
med = (med - 1) & 0x0ff;
low = (low - 1) & 0x0ff;
}
output.write(0x0d5);
output.write(0x0aa);
output.write(0x0ad);
int last = 0;
for (int i = temp.length - 1; i > 255; i--) {
int value = temp[i] ^ last;
output.write(NIBBLE_62[value]);
last = temp[i];
}
for (int i = 0; i < 256; i++) {
int value = temp[i] ^ last;
output.write(NIBBLE_62[value]);
last = temp[i];
}
// Last data byte used as checksum
output.write(NIBBLE_62[last]);
output.write(0x0de);
output.write(0x0aa);
output.write(0x0eb);
}
public void updateTrack(Integer track) {
// If disk is nibble image, write nibbles directly
if (isNibblizedImage) {
updateNibblizedTrack(track);
}
// Otherwise denibblize and write out
if (!isNibblizedImage) {
updateDenibblizedTrack(track);
}
}
void updateNibblizedTrack(Integer track) {
try {
RandomAccessFile disk = new RandomAccessFile(diskPath, "rws");
// Locate start of track
disk.seek(headerLength + track * TRACK_NIBBLE_LENGTH);
// Update that section of the disk image
disk.write(nibbles, track * TRACK_NIBBLE_LENGTH, TRACK_NIBBLE_LENGTH);
disk.close();
} catch (FileNotFoundException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
static public boolean CHECK_NIB_SECTOR_PATTERN_ON_WRITE = true;
void updateDenibblizedTrack(Integer track) {
try {
byte[] trackNibbles = new byte[TRACK_NIBBLE_LENGTH];
byte[] trackData = new byte[SECTOR_COUNT * 256];
// Copy track into temporary buffer
// System.out.println("Nibblized track "+track);
// System.out.printf("%04d:",0);
for (int i = 0, pos = track * TRACK_NIBBLE_LENGTH; i < TRACK_NIBBLE_LENGTH; i++, pos++) {
trackNibbles[i] = nibbles[pos];
// System.out.print(Integer.toString(nibbles[pos] & 0x0ff, 16)+" ");
// if (i % 16 == 15) {
// System.out.println();
// System.out.printf("%04d:",i+1);
// }
}
// System.out.println();
int pos = 0;
for (int i = 0; i < SECTOR_COUNT; i++) {
// Loop through number of sectors
pos = locatePattern(pos, trackNibbles, 0x0d5, 0x0aa, 0x096);
// Locate track number
int trackVerify = decodeOddEven(trackNibbles[pos + 5], trackNibbles[pos + 6]);
// Locate sector number
int sector = decodeOddEven(trackNibbles[pos + 7], trackNibbles[pos + 8]);
// System.out.println("Writing track " + track + ", getting address block for T" + trackVerify + ".S" + sector + " found at NIB offset "+pos);
// Skip to end of address block
pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa /*, 0x0eb this is sometimes being written as FF??*/);
// Locate start of sector data
pos = locatePattern(pos, trackNibbles, 0x0d5, 0x0aa, 0x0ad);
// Determine offset in output data for sector
//int offset = reverseLoopkup(currentSectorOrder, sector) * 256;
int offset = currentSectorOrder[sector] * 256;
// System.out.println("Sector "+sector+" maps to physical sector "+reverseLoopkup(currentSectorOrder, sector));
// Decode sector data
denibblizeSector(trackNibbles, pos + 3, trackData, offset);
// Skip to end of sector
pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa, 0x0eb);
}
// Write track to disk
RandomAccessFile disk;
try {
disk = new RandomAccessFile(diskPath, "rws");
disk.seek(headerLength + track * 256 * SECTOR_COUNT);
disk.write(trackData);
disk.close();
} catch (FileNotFoundException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (Throwable ex) {
Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
private int locatePattern(int pos, byte[] data, int... pattern) throws Throwable {
int max = data.length;
while (!matchPattern(pos, data, pattern)) {
pos = (pos + 1) % data.length;
max--;
if (max < 0) {
throw new Throwable("Could not match pattern!");
}
}
// System.out.print("Found pattern at "+pos+": ");
// for (int i : pattern) {System.out.print(Integer.toString( i & 0x0ff, 16)+" ");}
// System.out.println();
return pos;
}
private boolean matchPattern(int pos, byte[] data, int... pattern) {
int matched = 0;
for (int i : pattern) {
int d = data[pos] & 0x0ff;
if (d != i) {
if (matched > 1) {
System.out.println("Warning: Issue when interpreting nibbilized disk data: at position " + pos + " pattern byte " + Integer.toString(i, 16) + " doesn't match " + Integer.toString(d, 16));
}
return false;
}
pos = (pos + 1) % data.length;
matched++;
}
return true;
}
private void denibblizeSector(byte[] source, int pos, byte[] trackData, int offset) {
int[] temp = new int[342];
int current = pos;
int last = 0;
// Un-encode raw data, leaving with pre-nibblized bytes
for (int i = temp.length - 1; i > 255; i--) {
int t = NIBBLE_62_REVERSE[0x0ff & source[current++]];
temp[i] = t ^ last;
last ^= t;
}
for (int i = 0; i < 256; i++) {
int t = NIBBLE_62_REVERSE[0x0ff & source[current++]];
temp[i] = t ^ last;
last ^= t;
}
// Now decode the pre-nibblized bytes
int p = temp.length - 1;
for (int i = 0; i < 256; i++) {
int a = (temp[i] << 2);
a = a + ((temp[p] & 1) << 1) + ((temp[p] & 2) >> 1);
trackData[i + offset] = (byte) a;
temp[p] = temp[p] >> 2;
p--;
if (p < 256) {
p = temp.length - 1;
}
}
}
private int reverseLoopkup(int[] table, int value) {
for (int i = 0; i < table.length; i++) {
if (table[i] == value) {
return i;
}
}
return -1;
}
}

View File

@ -0,0 +1,279 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.apple2e.SoftSwitches;
import jace.apple2e.softswitch.MemorySoftSwitch;
import jace.config.ConfigurableField;
import jace.core.Computer;
import jace.core.Device;
import jace.core.KeyHandler;
import jace.core.Keyboard;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.Stateful;
import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Simple implementation of joystick support that supports mouse or keyboard.
* Actual joystick support isn't offered by Java at this moment in time
* unfortunately.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public class Joystick extends Device {
@ConfigurableField(name = "Enabled", shortName = "enabled", description = "If unchecked, then there is no joystick support.")
public boolean enabled;
@ConfigurableField(name = "Center Mouse", description = "Moves mouse back to the center of the screen, can get annoying.")
public boolean centerMouse;
@ConfigurableField(name = "Use keyboard", shortName = "useKeys", description = "Arrow keys will control joystick instead of the mouse.")
public boolean useKeyboard;
@ConfigurableField(name = "Hog keypresses", shortName = "hog", description = "Key presses will not be sent to emulator.")
public boolean hogKeyboard;
public int port;
@Stateful
public int x = 0;
@Stateful
public int y = 0;
private int joyX = 0;
private int joyY = 0;
MemorySoftSwitch xSwitch;
MemorySoftSwitch ySwitch;
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Point lastMouseLocation;
Robot robot;
Point centerPoint;
public Joystick(int port) {
centerPoint = new Point(screenSize.width / 2, screenSize.height / 2);
this.port = port;
if (port == 0) {
xSwitch = (MemorySoftSwitch) SoftSwitches.PDL0.getSwitch();
ySwitch = (MemorySoftSwitch) SoftSwitches.PDL1.getSwitch();
} else {
xSwitch = (MemorySoftSwitch) SoftSwitches.PDL2.getSwitch();
ySwitch = (MemorySoftSwitch) SoftSwitches.PDL3.getSwitch();
}
lastMouseLocation = MouseInfo.getPointerInfo().getLocation();
try {
robot = new Robot();
} catch (AWTException ex) {
Logger.getLogger(Joystick.class.getName()).log(Level.SEVERE, null, ex);
}
}
public boolean leftPressed = false;
public boolean rightPressed = false;
public boolean upPressed = false;
public boolean downPressed = false;
private void readJoystick() {
if (useKeyboard) {
joyX = leftPressed ? (rightPressed ? 128 : 0) : (rightPressed ? 255 : 128);
joyY = upPressed ? (downPressed ? 128 : 0) : (downPressed ? 255 : 128);
} else {
Point l = MouseInfo.getPointerInfo().getLocation();
if (l.x < lastMouseLocation.x) {
joyX = 0;
} else if (l.x > lastMouseLocation.x) {
joyX = 255;
} else {
joyX = 128;
}
if (l.y < lastMouseLocation.y) {
joyY = 0;
} else if (l.y > lastMouseLocation.y) {
joyY = 255;
} else {
joyY = 128;
}
if (centerMouse) {
lastMouseLocation = centerPoint;
robot.mouseMove(centerPoint.x, centerPoint.y);
} else {
if (l.x <= 20) {
robot.mouseMove(20, l.y);
l = MouseInfo.getPointerInfo().getLocation();
}
if ((l.x + 21) == screenSize.getWidth()) {
robot.mouseMove((int) (screenSize.getWidth() - 20), l.y);
l = MouseInfo.getPointerInfo().getLocation();
}
if (l.y <= 20) {
robot.mouseMove(l.x, 20);
l = MouseInfo.getPointerInfo().getLocation();
}
if ((l.y + 21) == screenSize.getHeight()) {
robot.mouseMove(l.x, (int) (screenSize.getHeight() - 20));
l = MouseInfo.getPointerInfo().getLocation();
}
lastMouseLocation = l;
}
}
}
@Override
protected String getDeviceName() {
return "Joystick (port " + port + ")";
}
@Override
public String getShortName() {
return "joy" + port;
}
@Override
public void tick() {
boolean finished = true;
if (x > 0) {
if (--x == 0) {
xSwitch.setState(false);
} else {
finished = false;
}
}
if (y > 0) {
if (--y == 0) {
ySwitch.setState(false);
} else {
finished = false;
}
}
if (finished) {
setRun(false);
}
}
@Override
public void attach() {
registerListeners();
}
@Override
public void detach() {
removeListeners();
}
public void reconfigure() {
x = 0;
y = 0;
if (enabled) {
registerListeners();
} else {
removeListeners();
}
}
RAMListener listener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0C070);
setScopeEnd(0x0C07f);
}
@Override
protected void doEvent(RAMEvent e) {
setRun(true);
readJoystick();
// if (x <= 0) {
xSwitch.setState(true);
x = 10 + joyX * 11;
// }
// if (y <= 0) {
ySwitch.setState(true);
y = 10 + joyY * 11;
// }
}
};
private void registerListeners() {
Computer.getComputer().getMemory().addListener(listener);
if (useKeyboard) {
System.out.println("Registering key handlers");
Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_LEFT, -1) {
@Override
public boolean handleKeyUp(KeyEvent e) {
leftPressed = false;
return hogKeyboard;
}
@Override
public boolean handleKeyDown(KeyEvent e) {
leftPressed = true;
return hogKeyboard;
}
}, this);
Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_RIGHT, -1) {
@Override
public boolean handleKeyUp(KeyEvent e) {
rightPressed = false;
return hogKeyboard;
}
@Override
public boolean handleKeyDown(KeyEvent e) {
rightPressed = true;
return hogKeyboard;
}
}, this);
Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_UP, -1) {
@Override
public boolean handleKeyUp(KeyEvent e) {
upPressed = false;
return hogKeyboard;
}
@Override
public boolean handleKeyDown(KeyEvent e) {
upPressed = true;
return hogKeyboard;
}
}, this);
Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_DOWN, -1) {
@Override
public boolean handleKeyUp(KeyEvent e) {
downPressed = false;
return hogKeyboard;
}
@Override
public boolean handleKeyDown(KeyEvent e) {
downPressed = true;
return hogKeyboard;
}
}, this);
}
}
private void removeListeners() {
Computer.getComputer().getMemory().removeListener(listener);
Keyboard.unregisterAllHandlers(this);
}
}

View File

@ -0,0 +1,551 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
/**
* Partial implementation of Passport midi card, supporting midi output routed
* to the java midi synth for playback. Compatible with Ultima V. Card
* operational notes taken from the Passport MIDI interface manual
* ftp://ftp.apple.asimov.net/pub/apple_II/documentation/hardware/misc/passport_midi.pdf
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name(value = "Passport Midi Interface", description = "MIDI sound card")
public class PassportMidiInterface extends Card {
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// There is no rom on this card, so nothing to do here
}
// MIDI timing: 31250 BPS, 8-N-1 (roughly 3472k per second)
public static enum TIMER_MODE {
continuous, singleShot, freqComparison, pulseComparison
};
public static class PTMTimer {
// Configuration values
public boolean prescaledTimer = false; // Only available on Timer 3
public boolean enableClock = false; // False == use CX clock input
public boolean dual8BitMode = false;
public TIMER_MODE mode = TIMER_MODE.continuous;
public boolean irqEnabled = false;
public boolean counterOutputEnable = false;
// Set by data latches
public Long duration = 0L;
// Run values
public boolean irqRequested = true;
public Long value = 0L;
}
// I/O registers
// --- 6840 PTM
public static final int TIMER_CONTROL_1 = 0;
public static final int TIMER_CONTROL_2 = 1;
public static final int TIMER1_MSB = 2;
public static final int TIMER1_LSB = 3;
public static final int TIMER2_MSB = 4;
public static final int TIMER2_LSB = 5;
// (Most likely not used)
public static final int TIMER3_MSB = 6;
public static final int TIMER3_LSB = 7;
// --- 6850 ACIA registers (write)
public static final int ACIA_CONTROL = 8;
public static final int ACIA_SEND = 9;
// --- 6850 ACIA registers (read)
public static final int ACIA_STATUS = 8;
public static final int ACIA_RECV = 9;
// --- Drums
public static final int DRUM_SYNC_SET = 0x0e;
public static final int DRUM_SYNC_CLEAR = 0x0f;
//---------------------------------------------------------
// PTM control values (register 1,2 and 3)
public static final int PTM_START_TIMERS = 0;
public static final int PTM_STOP_TIMERS = 1;
public static final int PTM_RESET = 67;
// PTM select values (register 2 only) -- modifies what Reg 1 points to
public static final int PTM_SELECT_REG_1 = 1;
public static final int PTM_SELECT_REG_3 = 0;
// PTM select values (register 3 only)
public static final int TIMER_3_PRESCALED = 1;
public static final int TIMER_3_NOT_PRESCALED = 0;
// PTM bit values
public static final int PTM_CLOCK_SOURCE = 2; // Bit 1
// 0 = external, 2 = internal clock
public static final int PTM_LATCH_IS_16_BIT = 4; // Bit 2
// 0 = 16-bit, 4 = dual 8-bit
// Bits 3-5
// 5 4 3
public static final int PTM_CONTINUOUS = 0; // 0 x 0
public static final int PTM_SINGLE_SHOT = 32; // 1 x 0
public static final int PTM_FREQ_COMP = 8; // x 0 1
public static final int PTM_PULSE_COMP = 24; // x 1 1
public static final int PTM_IRQ_ENABLED = 64; // Bit 6
// 64 = IRQ Enabled, 0 = IRQ Masked
public static final int PTM_OUTPUT_ENABLED = 128; // Bit 7
// 128 = Timer output enabled, 0 = disabled
// ACIA control values
// Reset == Master reset + even parity + 2 stop bits + 8 bit + No interrupts (??)
public static final int ACIA_RESET = 19;
public static final int ACIA_MASK_INTERRUPTS = 17;
public static final int ACIA_OFF = 21;
// Counter * 1 + RTS = low, transmit interrupt enabled
public static final int ACIA_INT_ON_SEND = 49;
// Counter * 1 + RTS = high, transmit interrupt disabled + Interrupt on receive
public static final int ACIA_INT_ON_RECV = 145;
// Counter * 1 + RTS = low, transmit interrupt enabled + Interrupt on receive
public static final int ACIA_INT_ON_SEND_AND_RECV = 177;
// ACIA control register values
// --- Bits 1 and 0 control counter divide select
public static final int ACIA_COUNTER_1 = 0;
public static final int ACIA_COUNTER_16 = 1;
public static final int ACIA_COUNTER_64 = 2;
public static final int ACIA_MASTER_RESET = 3;
// Midi is always transmitted 8-N-1
public static final int ACIA_ODD_PARITY = 4; // 4 = odd, 0 = even
public static final int ACIA_STOP_BITS_1 = 8; // 8 = 1 stop bit, 0 = 2 stop bits
public static final int ACIA_WORD_LENGTH_8 = 16; // 16 = 8-bit, 0 = 7-bit
// --- Bits 5 and 6 control interrupts
// 6 5
// 0 0 RTS = low, transmit interrupt disabled
// 0 1 RTS = low, transmit interrupt enabled
// 1 0 RTS = high, transmit interrupt disabled
// 1 1 RTS = low, Transmit break, trasmit interrupt disabled
public static final int ACIA_RECV_INTERRUPT = 128; // 128 = interrupt on receive, 0 = no interrupt
// PTM configuration
private boolean ptmTimer3Selected = false; // When true, reg 1 points at timer 3
private boolean ptmTimersActive = false; // When true, timers run constantly
private PTMTimer[] ptmTimer = {
new PTMTimer(),
new PTMTimer(),
new PTMTimer()
};
private boolean ptmStatusReadSinceIRQ = false;
// ---------------------- ACIA CONFIGURATION
private boolean aciaInterruptOnSend = false;
private boolean aciaInterruptOnReceive = false;
// ---------------------- ACIA STATUS BITS
// True when MIDI IN receives a byte
private boolean receivedACIAByte = false;
// True when data is not transmitting (always true because we aren't really doing wire transmission);
private boolean transmitACIAEmpty = true;
// True if another byte is received before the previous byte was processed
private boolean receiverACIAOverrun = false;
// True if ACIA generated interrupt request
private boolean irqRequestedACIA = false;
//--- the synth
private Synthesizer synth;
@Override
public void reset() {
// TODO: Deactivate card
suspend();
}
@Override
public boolean suspend() {
// TODO: Deactivate card
suspendACIA();
return super.suspend();
}
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// No firmware, so do nothing
return;
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
switch (type) {
case READ_DATA:
int returnValue = 0;
switch (register) {
case ACIA_STATUS:
returnValue = getACIAStatus();
break;
case ACIA_RECV:
returnValue = getACIARecieve();
break;
//TODO: Implement PTM registers
case TIMER_CONTROL_1:
// Technically it's not supposed to return anything...
returnValue = getPTMStatus();
break;
case TIMER_CONTROL_2:
returnValue = getPTMStatus();
break;
case TIMER1_LSB:
returnValue = (int) (ptmTimer[0].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false;
}
break;
case TIMER1_MSB:
returnValue = (int) (ptmTimer[0].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false;
}
break;
case TIMER2_LSB:
returnValue = (int) (ptmTimer[1].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false;
}
break;
case TIMER2_MSB:
returnValue = (int) (ptmTimer[1].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false;
}
break;
case TIMER3_LSB:
returnValue = (int) (ptmTimer[2].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false;
}
break;
case TIMER3_MSB:
returnValue = (int) (ptmTimer[2].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false;
}
break;
default:
System.out.println("Passport midi read unrecognized, port " + register);
}
e.setNewValue(returnValue);
// System.out.println("Passport I/O read register " + register + " == " + returnValue);
break;
case WRITE:
int v = e.getNewValue() & 0x0ff;
// System.out.println("Passport I/O write register " + register + " == " + v);
switch (register) {
case ACIA_CONTROL:
processACIAControl(v);
break;
case ACIA_SEND:
processACIASend(v);
break;
case TIMER_CONTROL_1:
if (ptmTimer3Selected) {
// System.out.println("Configuring timer 3");
ptmTimer[2].prescaledTimer = ((v & TIMER_3_PRESCALED) != 0);
processPTMConfiguration(ptmTimer[2], v);
} else {
// System.out.println("Configuring timer 1");
if ((v & PTM_STOP_TIMERS) == 0) {
startPTM();
} else {
stopPTM();
}
processPTMConfiguration(ptmTimer[0], v);
}
break;
case TIMER_CONTROL_2:
// System.out.println("Configuring timer 2");
ptmTimer3Selected = ((v & PTM_SELECT_REG_1) == 0);
processPTMConfiguration(ptmTimer[1], v);
break;
case TIMER1_LSB:
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff00) | v;
break;
case TIMER1_MSB:
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff) | (v << 8);
break;
case TIMER2_LSB:
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff00) | v;
break;
case TIMER2_MSB:
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff) | (v << 8);
break;
case TIMER3_LSB:
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | v;
break;
case TIMER3_MSB:
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | (v << 8);
break;
default:
System.out.println("Passport midi write unrecognized, port " + register);
}
break;
}
}
@Override
public void tick() {
if (ptmTimersActive) {
for (PTMTimer t : ptmTimer) {
// if (t.duration == 0) {
// continue;
// }
t.value--;
if (t.value < 0) {
// TODO: interrupt dual 8-bit mode, whatver that is!
if (t.irqEnabled) {
// System.out.println("Timer generating interrupt!");
t.irqRequested = true;
Computer.getComputer().getCpu().generateInterrupt();
ptmStatusReadSinceIRQ = false;
}
if (t.mode == TIMER_MODE.continuous || t.mode == TIMER_MODE.freqComparison) {
t.value = t.duration;
}
}
}
}
}
@Override
public String getDeviceName() {
return "Passport MIDI Controller";
}
//------------------------------------------------------ PTM
private void processPTMConfiguration(PTMTimer timer, int val) {
timer.enableClock = (val & PTM_CLOCK_SOURCE) != 0;
timer.dual8BitMode = (val & PTM_LATCH_IS_16_BIT) != 0;
switch (val & 56) {
// Evaluate bits 3, 4 and 5 to determine mode
case PTM_CONTINUOUS:
timer.mode = TIMER_MODE.continuous;
break;
case PTM_PULSE_COMP:
timer.mode = TIMER_MODE.pulseComparison;
break;
case PTM_FREQ_COMP:
timer.mode = TIMER_MODE.freqComparison;
break;
case PTM_SINGLE_SHOT:
timer.mode = TIMER_MODE.singleShot;
break;
default:
timer.mode = TIMER_MODE.continuous;
break;
}
timer.irqEnabled = (val & PTM_IRQ_ENABLED) != 0;
timer.counterOutputEnable = (val & PTM_OUTPUT_ENABLED) != 0;
}
private void stopPTM() {
// System.out.println("Passport timers halted");
ptmTimersActive = false;
}
private void startPTM() {
// System.out.println("Passport timers started");
ptmTimersActive = true;
ptmTimer[0].irqRequested = false;
ptmTimer[1].irqRequested = false;
ptmTimer[2].irqRequested = false;
ptmTimer[0].value = ptmTimer[0].duration;
ptmTimer[1].value = ptmTimer[1].duration;
ptmTimer[2].value = ptmTimer[2].duration;
}
// Bits 0, 1 and 2 == IRQ requested from timer 1, 2 or 3
// Bit 7 = Any IRQ
private int getPTMStatus() {
int status = 0;
for (int i = 0; i < 3; i++) {
PTMTimer t = ptmTimer[i];
if (t.irqRequested && t.irqEnabled) {
ptmStatusReadSinceIRQ = true;
status |= (1 << i);
status |= 128;
}
}
return status;
}
//------------------------------------------------------ ACIA
/*
ACIA status register
Bit 0 = Receive data register full
Bit 1 = Transmit data register empty
Bits 2 and 3 pertain to modem (DCD and CTS, so ignore)
Bit 4 = Framing error
Bit 5 = Receiver overrun
Bit 6 = Partity error (not used by MIDI)
Bit 7 = Interrupt request
*/
private int getACIAStatus() {
int status = 0;
if (receivedACIAByte) {
status |= 1;
}
if (transmitACIAEmpty) {
status |= 2;
}
if (receiverACIAOverrun) {
status |= 32;
}
if (irqRequestedACIA) {
status |= 128;
}
return status;
}
// TODO: Implement MIDI IN... some day
private int getACIARecieve() {
return 0;
}
private void processACIAControl(int value) {
if ((value & 0x03) == ACIA_MASTER_RESET) {
resume();
}
}
ShortMessage currentMessage;
int currentMessageStatus;
int currentMessageData1;
int currentMessageData2;
int messageSize = 255;
int currentMessageReceived = 0;
private void processACIASend(int value) {
if (!isRunning()) {
// System.err.println("ACIA not active!");
return;
} else {
// System.out.println("ACIA send "+value);
}
// First off try to finish off previous command already in play
boolean sendMessage = false;
if (currentMessage != null) {
if ((value & 0x080) > 0) {
// Any command byte received means we finished receiving another command
// and valid or not, process it as-is
if (currentMessage != null) {
sendMessage = true;
}
// If there is no current message, then we'll pick this up afterwards...
} else {
// If we receive a data byte ( < 128 ) then check if we have the right size
// if so, then the command was completely received, and it's time to send it.
currentMessageReceived++;
if (currentMessageReceived >= messageSize) {
sendMessage = true;
}
if (currentMessageReceived == 1) {
currentMessageData1 = value;
} else {
// Possibly redundant, but there's no reason a message should be longer than this...
currentMessageData2 = value;
sendMessage = true;
}
}
}
// If we have a command to send, then do it
if (sendMessage == true) {
if (synth != null && synth.isOpen()) {
// Send message
try {
// System.out.println("Sending MIDI message "+currentMessageStatus+","+currentMessageData1+","+currentMessageData2);
currentMessage.setMessage(currentMessageStatus, currentMessageData1, currentMessageData2);
synth.getReceiver().send(currentMessage, -1L);
} catch (InvalidMidiDataException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
} catch (MidiUnavailableException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}
}
currentMessage = null;
}
// Do we have a new command byte?
if ((value & 0x080) > 0) {
// Start a new message
currentMessage = new ShortMessage();
currentMessageStatus = value;
currentMessageData1 = 0;
currentMessageData2 = 0;
try {
currentMessage.setMessage(currentMessageStatus, 0, 0);
messageSize = currentMessage.getLength();
} catch (InvalidMidiDataException ex) {
messageSize = 0;
}
currentMessageReceived = 0;
}
}
@Override
public void resume() {
if (isRunning() && synth != null && synth.isOpen()) {
return;
}
try {
MidiDevice.Info[] devices = MidiSystem.getMidiDeviceInfo();
if (devices.length == 0) {
System.out.println("No MIDI devices found");
} else {
for (MidiDevice.Info dev : devices) {
System.out.println("MIDI Device found: " + dev);
if (dev.getName().contains("Java Sound")) {
if (dev instanceof Synthesizer) {
synth = (Synthesizer) dev;
break;
}
}
}
}
if (synth == null) {
synth = MidiSystem.getSynthesizer();
}
if (synth != null) {
System.out.println("Selected MIDI device: " + synth.getDeviceInfo().getName());
synth.open();
super.resume();
}
} catch (MidiUnavailableException ex) {
System.out.println("Could not open MIDI synthesizer");
ex.printStackTrace();
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void suspendACIA() {
// TODO: Stop ACIA thread...
if (synth != null && synth.isOpen()) {
synth.close();
synth = null;
}
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.apple2e.MOS65C02;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAM;
import java.io.IOException;
/**
* Helper functions for prodos drivers
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class ProdosDriver {
public static int MLI_COMMAND = 0x042;
public static int MLI_UNITNUMBER = 0x043;
public static int MLI_BUFFER_ADDRESS = 0x044;
public static int MLI_BLOCK_NUMBER = 0x046;
public static enum MLI_RETURN {
NO_ERROR(0), IO_ERROR(0x027), NO_DEVICE(0x028), WRITE_PROTECTED(0x02B);
public int intValue;
MLI_RETURN(int val) {
intValue = val;
}
}
public static enum MLI_COMMAND_TYPE {
STATUS(0x0), READ(0x01), WRITE(0x02), FORMAT(0x03);
public int intValue;
MLI_COMMAND_TYPE(int val) {
intValue = val;
}
public static MLI_COMMAND_TYPE fromInt(int value) {
for (MLI_COMMAND_TYPE c : values()) {
if (c.intValue == value) {
return c;
}
}
return null;
}
}
abstract public boolean changeUnit(int unitNumber);
abstract public int getSize();
abstract public boolean isWriteProtected();
abstract public void mliFormat() throws IOException;
abstract public void mliRead(int block, int bufferAddress) throws IOException;
abstract public void mliWrite(int block, int bufferAddress) throws IOException;
abstract public Card getOwner();
public void handleMLI() {
int returnCode = prodosMLI().intValue;
MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu();
cpu.A = returnCode;
// Clear carry flag if no error, otherwise set carry flag
cpu.C = (returnCode == 0x00) ? 00 : 01;
}
private MLI_RETURN prodosMLI() {
try {
RAM memory = Computer.getComputer().getMemory();
int cmd = memory.readRaw(MLI_COMMAND);
MLI_COMMAND_TYPE command = MLI_COMMAND_TYPE.fromInt(cmd);
int unit = (memory.readWordRaw(MLI_UNITNUMBER) & 0x080) > 0 ? 1 : 0;
if (changeUnit(unit) == false) {
return MLI_RETURN.NO_DEVICE;
}
int block = memory.readWordRaw(MLI_BLOCK_NUMBER);
int bufferAddress = memory.readWordRaw(MLI_BUFFER_ADDRESS);
// System.out.println(getOwner().getName()+" MLI Call "+command+", unit "+unit+" Block "+block+" --> "+Integer.toHexString(bufferAddress));
if (command == null) {
System.out.println(getOwner().getName()+" Mass storage given bogus command (" + Integer.toHexString(cmd) + "), returning I/O error");
return MLI_RETURN.IO_ERROR;
}
switch (command) {
case STATUS:
int blocks = getSize();
MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu();
cpu.X = blocks & 0x0ff;
cpu.Y = (blocks >> 8) & 0x0ff;
if (isWriteProtected()) {
return MLI_RETURN.WRITE_PROTECTED;
}
break;
case FORMAT:
mliFormat();
case READ:
mliRead(block, bufferAddress);
break;
case WRITE:
mliWrite(block, bufferAddress);
break;
default:
System.out.println(getOwner().getName()+" MLI given bogus command (" + Integer.toHexString(cmd) + " = " + command.name() + "), returning I/O error");
return MLI_RETURN.IO_ERROR;
}
return MLI_RETURN.NO_ERROR;
} catch (UnsupportedOperationException ex) {
return MLI_RETURN.WRITE_PROTECTED;
} catch (IOException ex) {
System.out.println(getOwner().getName()+" Encountered IO Error, returning error: " + ex.getMessage());
return MLI_RETURN.IO_ERROR;
}
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware;
import jace.apple2e.MOS65C02;
import jace.core.Computer;
import jace.core.RAM;
import jace.hardware.massStorage.CardMassStorage;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Generic abstraction of a smartport device.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class SmartportDriver {
public static enum ERROR_CODE {
NO_ERROR(0), INVALID_COMMAND(0x01), BAD_PARAM_COUNT(0x04), INVALID_UNIT(0x011), INVALID_CODE(0x021), BAD_BLOCK_NUMBER(0x02d);
int intValue;
ERROR_CODE(int c) {
intValue = c;
}
}
public void handleSmartport() {
int returnCode = callSmartport().intValue;
MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu();
cpu.A = returnCode;
// Clear carry flag if no error, otherwise set carry flag
cpu.C = (returnCode == 0x00) ? 00 : 01;
}
private ERROR_CODE callSmartport() {
MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu();
RAM ram = Computer.getComputer().getMemory();
int callAddress = cpu.popWord() + 1;
int command = ram.readRaw(callAddress);
boolean extendedCall = command >= 0x040;
// command &= 0x0f;
// Modify stack so that RTS goes to the right place after the smartport device call
//cpu.pushWord(callAddress + (extendedCall ? 5 : 3));
// Kludge due to the CPU not getting the faked RTS opcode
cpu.setProgramCounter(callAddress + (extendedCall ? 5 : 3));
// Calculate parameter address block
int parmAddr = 0;
if (!extendedCall) {
parmAddr = ram.readWordRaw(callAddress + 1);
} else {
// Extended calls -- not gonna happen on this platform anyway
int parmAddrLo = ram.readWordRaw(callAddress + 1);
int parmAddrHi = ram.readWordRaw(callAddress + 3);
parmAddr = parmAddrHi << 16 | parmAddrLo;
}
// Now process command
System.out.println("Received command " + command + " with address block " + Integer.toHexString(parmAddr));
byte numParms = ram.readRaw(parmAddr);
int[] params = new int[16];
for (int i = 0; i < 16; i++) {
int value = 0x0ff & ram.readRaw(parmAddr + i);
params[i] = value;
System.out.print(Integer.toHexString(value) + " ");
}
System.out.println();
int unitNumber = params[1];
if (!changeUnit(unitNumber)) {
System.out.println("Invalid unit: "+unitNumber);
return ERROR_CODE.INVALID_UNIT;
}
int dataBuffer = params[2] | (params[3] << 8);
try {
switch (command) {
case 0: //Status
return returnStatus(dataBuffer, params);
case 1: //Read Block
int blockNum = params[4] | (params[5] << 8) | (params[6] << 16);
read(blockNum, dataBuffer);
return ERROR_CODE.NO_ERROR;
// System.out.println("reading "+blockNum+" to $"+Integer.toHexString(dataBuffer));
case 2: //Write Block
blockNum = params[4] | (params[5] << 8) | (params[6] << 16);
write(blockNum, dataBuffer);
return ERROR_CODE.NO_ERROR;
case 3: //Format
case 4: //Control
case 5: //Init
case 6: //Open
case 7: //Close
case 8: //Read
case 9: //Write
default:
System.out.println("Unimplemented command "+command);
return ERROR_CODE.INVALID_COMMAND;
}
} catch (IOException ex) {
Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
return ERROR_CODE.INVALID_CODE;
}
}
abstract public boolean changeUnit(int unitNumber);
abstract public void read(int blockNum, int buffer) throws IOException;
abstract public void write(int blockNum, int buffer) throws IOException;
abstract public ERROR_CODE returnStatus(int dataBuffer, int[] params);
}

View File

@ -0,0 +1,260 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import jace.Emulator;
import jace.apple2e.MOS65C02;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Motherboard;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.hardware.ProdosDriver;
import jace.hardware.SmartportDriver;
import jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Hard disk and 800k floppy (smartport) controller card. HDV and 2MG images are
* both supported.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Mass Storage Device")
public class CardMassStorage extends Card implements MediaConsumerParent {
MassStorageDrive drive1;
MassStorageDrive drive2;
public CardMassStorage() {
drive1 = new MassStorageDrive();
drive2 = new MassStorageDrive();
drive1.setIcon(Utility.loadIcon("drive-harddisk.png"));
drive2.setIcon(Utility.loadIcon("drive-harddisk.png"));
currentDrive = drive1;
}
@Override
public void setSlot(int slot) {
super.setSlot(slot);
drive1.getIcon().setDescription("S" + getSlot() + "D1");
drive2.getIcon().setDescription("S" + getSlot() + "D2");
}
@Override
public String getDeviceName() {
return "Mass Storage Device";
}
// boot0 stores cards*16 of boot device here
static int SLT16 = 0x02B;
// "rom" offset where device driver is called by MLI
// static int DEVICE_DRIVER_OFFSET = 0x042;
static int DEVICE_DRIVER_OFFSET = 0x0A;
byte[] cardSignature = new byte[]{
(byte) 0x0a9 /*NOP*/, 0x020, (byte) 0x0a9, 0x00,
(byte) 0x0a9, 0x03 /*currentDisk cards*/, (byte) 0x0a9, 0x03c /*currentDisk cards*/,
(byte) 0xd0, 0x07, 0x60, (byte) 0x0b0,
0x01 /*firmware cards*/, 0x18, (byte) 0x0b0, 0x5a
};
Card theCard = this;
public MassStorageDrive currentDrive;
public IDisk getCurrentDisk() {
if (currentDrive != null) {
return currentDrive.getCurrentDisk();
}
return null;
}
ProdosDriver driver = new ProdosDriver() {
@Override
public boolean changeUnit(int unit) {
currentDrive = unit == 0 ? drive1 : drive2;
return getCurrentDisk() != null;
}
@Override
public int getSize() {
return getCurrentDisk() != null ? getCurrentDisk().getSize() : 0;
}
@Override
public boolean isWriteProtected() {
return getCurrentDisk() != null ? getCurrentDisk().isWriteProtected() : true;
}
@Override
public void mliFormat() throws IOException {
getCurrentDisk().mliFormat();
}
@Override
public void mliRead(int block, int bufferAddress) throws IOException {
getCurrentDisk().mliRead(block, bufferAddress);
}
@Override
public void mliWrite(int block, int bufferAddress) throws IOException {
getCurrentDisk().mliWrite(block, bufferAddress);
}
@Override
public Card getOwner() {
return theCard;
}
};
@Override
public void reconfigure() {
try {
detach();
int pc = Computer.getComputer().getCpu().getProgramCounter();
if (drive1.getCurrentDisk() != null && getSlot() == 7 && (pc == 0x0c65e || pc == 0x0c661)) {
// If the computer is in a loop trying to boot from cards 6, fast-boot from here instead
// This is a convenience to boot a hard-drive if the emulator has started waiting for a currentDisk
currentDrive = drive1;
getCurrentDisk().boot0(getSlot());
Card[] cards = Computer.getComputer().getMemory().getAllCards();
Motherboard.cancelSpeedRequest(cards[6]);
}
attach();
} catch (IOException ex) {
Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
}
}
@Override
public void reset() {
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
// There is no c8 rom for this card
}
@Override
protected void handleFirmwareAccess(int offset, TYPE type, int value, RAMEvent e) {
MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu();
// System.out.println(e.getType()+" "+Integer.toHexString(e.getAddress())+" from instruction at "+Integer.toHexString(cpu.getProgramCounter()));
if (type.isRead()) {
Emulator.getFrame().addIndicator(this, currentDrive.getIcon());
if (drive1.getCurrentDisk() == null && drive2.getCurrentDisk() == null) {
e.setNewValue(0);
return;
}
if (type == TYPE.EXECUTE) {
// Virtual functions, handle accordingly
String error;
if (offset == 0x00) {
// NOP unless otherwise specified
e.setNewValue(0x0ea);
try {
if (drive1.getCurrentDisk() != null) {
currentDrive = drive1;
getCurrentDisk().boot0(getSlot());
} else {
// Patch for crash on start when no image is mounted
e.setNewValue(0x060);
}
return;
} catch (IOException ex) {
Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
error = ex.getMessage();
// Jump to the basic interpreter for now
cpu.setProgramCounter(0x0dfff);
int address = 0x0480;
for (char c : error.toCharArray()) {
Computer.getComputer().getMemory().write(address++, (byte) (c + 0x080), false, false);
}
}
} else {
if (offset == DEVICE_DRIVER_OFFSET) {
driver.handleMLI();
} else if (offset == DEVICE_DRIVER_OFFSET + 3) {
smartport.handleSmartport();
} else {
System.out.println("Call to unknown handler " + Integer.toString(e.getAddress(), 16) + "-- returning");
}
/* act like RTS was called */
e.setNewValue(0x060);
}
}
if (offset < 16) {
e.setNewValue(cardSignature[offset]);
} else {
switch (offset) {
// Disk capacity = 65536 blocks
case 0x0FC:
e.setNewValue(0x0ff);
break;
case 0x0FD:
e.setNewValue(0x07f);
break;
// Status bits
case 0x0FE:
e.setNewValue(0x0D7);
break;
case 0x0FF:
e.setNewValue(DEVICE_DRIVER_OFFSET);
}
}
}
}
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
// Ignore IO registers
}
@Override
public void tick() {
// Nothing is done per CPU cycle
}
SmartportDriver smartport = new SmartportDriver() {
@Override
public boolean changeUnit(int unitNumber) {
currentDrive = unitNumber == 1 ? drive1 : drive2;
return getCurrentDisk() != null;
}
@Override
public void read(int blockNum, int buffer) throws IOException {
getCurrentDisk().mliRead(blockNum, buffer);
}
@Override
public void write(int blockNum, int buffer) throws IOException {
getCurrentDisk().mliWrite(blockNum, buffer);
}
@Override
public ERROR_CODE returnStatus(int dataBuffer, int[] params) {
return ERROR_CODE.NO_ERROR;
}
};
public MediaConsumer[] getConsumers() {
return new MediaConsumer[]{drive1, drive2};
}
}

View File

@ -0,0 +1,275 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Prodos directory node
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class DirectoryNode extends DiskNode implements FileFilter {
// public static int FILE_ENTRY_SIZE = 38;
public static int FILE_ENTRY_SIZE = 0x027;
public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, int baseBlock) throws IOException {
setBaseBlock(baseBlock);
init(ownerFilesystem, physicalDir);
}
public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir) throws IOException {
init(ownerFilesystem, physicalDir);
}
private void init(ProdosVirtualDisk ownerFilesystem, File physicalFile) throws IOException {
setPhysicalFile(physicalFile);
setType(EntryType.SUBDIRECTORY);
setName(physicalFile.getName());
setOwnerFilesystem(ownerFilesystem);
}
@Override
public void doDeallocate() {
}
@Override
public void doAllocate() {
File[] files = physicalFile.listFiles(this);
int numEntries = files.length;
int numBlocks = 1;
// First block has 12 entries, subsequent blocks have 13 entries
if (numEntries > 12) {
numBlocks += (numEntries - 12) / 13;
}
for (File f : files) {
addFile(f);
}
Collections.sort(children, new Comparator<DiskNode>() {
public int compare(DiskNode o1, DiskNode o2) {
return o1.getName().compareTo(o2.getName());
}
});
}
@Override
public void doRefresh() {
}
@Override
/**
* Checks contents of subdirectory for changes as well as directory itself (super class)
*/
public boolean checkFile() throws IOException {
boolean success = true;
if (!super.checkFile()) {
return false;
}
HashSet<String> realFiles = new HashSet<String>();
File[] realFileList = physicalFile.listFiles(this);
for (File f : realFileList) {
realFiles.add(f.getName());
}
for (Iterator<DiskNode> i = getChildren().iterator(); i.hasNext(); ) {
DiskNode node = i.next();
if (realFiles.contains(node.getPhysicalFile().getName())) {
realFiles.remove(node.getPhysicalFile().getName());
} else {
i.remove();
success = false;
}
if (node.isAllocated()) {
if (!(node instanceof DirectoryNode) && !node.checkFile()) {
success = false;
}
}
}
if (!realFiles.isEmpty()) {
success = false;
// New files showed up -- deal with them!
for (String fileName : realFiles) {
addFile(new File(physicalFile, fileName));
}
}
return success;
}
@Override
public void readBlock(int block, byte[] buffer) throws IOException {
checkFile();
if (block == 0) {
generateHeader(buffer);
for (int i=0; i < 12 && i < children.size(); i++)
generateFileEntry(buffer, 4 + (i+1) * FILE_ENTRY_SIZE, i);
} else {
int start = (block * 13) - 1;
int end = start + 13;
int offset = 4;
for (int i=start; i < end && i < children.size(); i++) {
// TODO: Add any parts that are not file entries.
generateFileEntry(buffer, offset, i);
offset += FILE_ENTRY_SIZE;
}
}
}
public boolean accept(File file) {
if (file.getName().endsWith("~")) return false;
char c = file.getName().charAt(0);
if (c == '.' || c == '~') {
return false;
}
if (file.isHidden()) {
return false;
}
return true;
}
/**
* Generate the directory header found in the base block of a directory
* @param buffer where to write data
*/
@SuppressWarnings("static-access")
private void generateHeader(byte[] buffer) {
// System.out.println("Generating directory header");
// Previous block = 0
generateWord(buffer, 0,0);
// Next block
int nextBlock = 0;
if (!additionalNodes.isEmpty())
nextBlock = additionalNodes.get(0).baseBlock;
generateWord(buffer, 0x02, nextBlock);
// Directory header + name length
// Volumme header = 0x0f0; Subdirectory header = 0x0e0
buffer[4]= (byte) ((baseBlock == 0x02 ? 0x0f0 : 0x0E0) + getName().length());
generateName(buffer, 5, this);
for (int i=0x014 ; i <= 0x01b; i++)
buffer[i] = 0;
generateTimestamp(buffer, 0x01c, getPhysicalFile().lastModified());
// Prodos 1.9
buffer[0x020] = 0x019;
// Minimum version = 0 (no min)
buffer[0x021] = 0x000;
// Directory may be read/written to, may not be destroyed or renamed
buffer[0x022] = 0x03;
// Entry size
buffer[0x023] = (byte) FILE_ENTRY_SIZE;
// Entries per block
buffer[0x024] = (byte) 0x0d;
// Directory items count
generateWord(buffer, 0x025, children.size());
// Volume bitmap pointer
generateWord(buffer, 0x027, ownerFilesystem.freespaceBitmap.baseBlock);
// Total number of blocks
generateWord(buffer, 0x029, ownerFilesystem.MAX_BLOCK);
}
/**
* Generate the entry of a directory
* @param buffer where to write data
* @param offset starting offset in buffer to write
* @param fileNumber number of file (indexed in Children array) to write
*/
private void generateFileEntry(byte[] buffer, int offset, int fileNumber) throws IOException {
// System.out.println("Generating entry for "+children.get(fileNumber).getName());
DiskNode child = children.get(fileNumber);
// Entry Type and length
buffer[offset] = (byte) ((child.getType().code << 4) + child.getName().length());
// Name
generateName(buffer, offset+1, child);
// File type
buffer[offset + 0x010] = (byte) ((child instanceof DirectoryNode) ? 0x0f : ((FileNode) child).fileType);
// Key pointer
generateWord(buffer, offset + 0x011, child.getBaseBlock());
// Blocks used -- will report only one unless file is actually allocated
// child.allocate();
generateWord(buffer, offset + 0x013, 1 + child.additionalNodes.size());
// EOF
// TODO: Verify this is the right thing to do -- is EOF total length or a modulo?
int length = ((int) child.physicalFile.length()) & 0x0ffffff;
generateWord(buffer, offset + 0x015, length & 0x0ffff);
buffer[offset + 0x017] = (byte) ((length >> 16) & 0x0ff);
// Creation date
generateTimestamp(buffer, offset + 0x018, child.physicalFile.lastModified());
// Version = 1.9
buffer[offset + 0x01c] = 0x19;
// Minimum version = 0
buffer[offset + 0x01d] = 0;
// Access = all granted
buffer[offset + 0x01e] = (byte) 0x0ff;
// AUX type
if (child instanceof FileNode)
generateWord(buffer, offset + 0x01f, ((FileNode) child).loadAddress);
// Modification date
generateTimestamp(buffer, offset + 0x021, child.physicalFile.lastModified());
// Key pointer for directory
generateWord(buffer, offset + 0x025, getBaseBlock());
}
private void generateTimestamp(byte[] buffer, int offset, long date) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(date);
// yyyyyyym mmmddddd - Byte 0,1
// ---hhhhh --mmmmmm - Byte 2,3
// buffer[offset+1] = (byte) (((c.get(Calendar.YEAR) - 1990) << 1) + ((c.get(Calendar.MONTH)>> 3) & 1));
buffer[offset+0] = 0;
buffer[offset+1] = 0;
buffer[offset+2] = 0;
buffer[offset+3] = 0;
// buffer[offset+2] = (byte) ((c.get(Calendar.MONTH)>> 3) & 1);
// buffer[offset+3] = (byte) (((c.get(Calendar.MONTH)&7) + c.get(Calendar.DAY_OF_MONTH)) & 0x0ff);
// buffer[offset+0] = (byte) c.get(Calendar.HOUR_OF_DAY);
// buffer[offset+1] = (byte) c.get(Calendar.MINUTE);
}
private void generateWord(byte[] buffer, int i, int value) {
// Little endian format
buffer[i] = (byte) (value & 0x0ff);
buffer[i+1] = (byte) ((value >> 8) & 0x0ff);
}
private void generateName(byte[] buffer, int offset, DiskNode node) {
for (int i=0; i < node.getName().length(); i++) {
buffer[offset+i] = (byte) node.getName().charAt(i);
}
}
private void addFile(File file) {
try {
if (file.isDirectory()) {
addChild(new DirectoryNode(getOwnerFilesystem(), file));
} else {
addChild(new FileNode(getOwnerFilesystem(), file));
}
} catch (IOException ex) {
Logger.getLogger(DirectoryNode.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

View File

@ -0,0 +1,259 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Prodos file/directory node abstraction. This provides a lot of the glue for
* maintaining some sort of state across the virtual prodos volume and the
* physical disk folder or file represented by this node.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class DiskNode {
public enum EntryType {
DELETED(0),
SEEDLING(1),
SAPLING(2),
TREE(3),
SUBDIRECTORY(0x0D),
SUBDIRECTORY_HEADER(0x0E),
VOLUME_HEADER(0x0F);
public int code;
EntryType(int c) {
code = c;
}
}
boolean allocated = false;
long allocationTime = -1L;
long lastCheckTime = -1L;
int baseBlock = -1;
List<DiskNode> additionalNodes;
ProdosVirtualDisk ownerFilesystem;
File physicalFile;
DiskNode parent;
List<DiskNode> children;
private EntryType type;
private String name;
public DiskNode() {
additionalNodes = new ArrayList<DiskNode>();
children = new ArrayList<DiskNode>();
}
public boolean checkFile() throws IOException {
allocate();
if (physicalFile == null) {
return false;
}
if (physicalFile.lastModified() != lastCheckTime) {
lastCheckTime = physicalFile.lastModified();
refresh();
return false;
}
return true;
}
public void allocate() throws IOException {
if (!allocated) {
doAllocate();
allocationTime = System.currentTimeMillis();
allocated = true;
ownerFilesystem.allocateEntry(this);
}
}
public void deallocate() {
if (allocated) {
ownerFilesystem.deallocateEntry(this);
doDeallocate();
allocationTime = -1L;
allocated = false;
additionalNodes.clear();
// NOTE: This is recursive!
for (DiskNode node : getChildren()) {
node.deallocate();
}
}
}
public void refresh() {
ownerFilesystem.deallocateEntry(this);
doRefresh();
allocationTime = System.currentTimeMillis();
allocated = true;
ownerFilesystem.allocateEntry(this);
}
/**
* @return the allocated
*/
public boolean isAllocated() {
return allocated;
}
/**
* @return the allocationTime
*/
public long getAllocationTime() {
return allocationTime;
}
/**
* @return the lastCheckTime
*/
public long getLastCheckTime() {
return lastCheckTime;
}
/**
* @return the baseBlock
*/
public int getBaseBlock() {
return baseBlock;
}
/**
* @param baseBlock the baseBlock to set
*/
public void setBaseBlock(int baseBlock) {
this.baseBlock = baseBlock;
}
/**
* @return the ownerFilesystem
*/
public ProdosVirtualDisk getOwnerFilesystem() {
return ownerFilesystem;
}
/**
* @param ownerFilesystem the ownerFilesystem to set
* @throws IOException
*/
public void setOwnerFilesystem(ProdosVirtualDisk ownerFilesystem) throws IOException {
this.ownerFilesystem = ownerFilesystem;
if (baseBlock == -1) {
setBaseBlock(ownerFilesystem.getNextFreeBlock());
}
ownerFilesystem.allocateEntry(this);
}
/**
* @return the physicalFile
*/
public File getPhysicalFile() {
return physicalFile;
}
/**
* @param physicalFile the physicalFile to set
*/
public void setPhysicalFile(File physicalFile) {
this.physicalFile = physicalFile;
setName(physicalFile.getName());
}
/**
* @return the parent
*/
public DiskNode getParent() {
return parent;
}
/**
* @param parent the parent to set
*/
public void setParent(DiskNode parent) {
this.parent = parent;
}
/**
* @return the children
*/
public List<DiskNode> getChildren() {
return children;
}
/**
* @param children the children to set
*/
public void setChildren(List<DiskNode> children) {
this.children = children;
}
public void addChild(DiskNode child) {
children.add(child);
}
public void removeChild(DiskNode child) {
children.remove(child);
}
/**
* @return the type
*/
public EntryType getType() {
return type;
}
/**
* @param type the type to set
*/
public void setType(EntryType type) {
this.type = type;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
if (name.length() > 15) {
name = name.substring(0, 15);
}
this.name = name.toUpperCase();
}
public abstract void doDeallocate();
public abstract void doAllocate() throws IOException;
public abstract void doRefresh();
public abstract void readBlock(int sequence, byte[] buffer) throws IOException;
public void readBlock(byte[] buffer) throws IOException {
checkFile();
readBlock(0, buffer);
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* Representation of a prodos file with a known file type and having a known
* size (either seedling, sapling or tree)
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class FileNode extends DiskNode {
public enum FileType {
UNKNOWN(0x00, 0x0000),
ADB(0x019, 0x0000),
AWP(0x01a, 0x0000),
ASP(0x01b, 0x0000),
BAD(0x01, 0x0000),
BIN(0x06, 0x0300),
CLASS(0xED, 0x0000),
BAS(0xfc, 0x0801),
CMD(0x0f0, 0x0000),
INT(0xfa, 0x0801),
IVR(0xfb, 0x0000),
PAS(0xef, 0x0000),
REL(0x0Fe, 0x0000),
SHK(0x0e0, 0x08002),
SDK(0x0e0, 0x08002),
SYS(0x0ff, 0x02000),
SYSTEM(0x0ff, 0x02000),
TXT(0x04, 0x0000),
U01(0x0f1, 0x0000),
U02(0x0f2, 0x0000),
U03(0x0f3, 0x0000),
U04(0x0f4, 0x0000),
U05(0x0f5, 0x0000),
U06(0x0f6, 0x0000),
U07(0x0f7, 0x0000),
U08(0x0f8, 0x0000),
VAR(0x0FD, 0x0000);
public int code = 0;
public int defaultLoadAddress = 0;
FileType(int code, int addr) {
this.code = code;
this.defaultLoadAddress = addr;
}
}
public int fileType = 0x00;
public int loadAddress = 0x00;
public static int SEEDLING_MAX_SIZE = ProdosVirtualDisk.BLOCK_SIZE;
public static int SAPLING_MAX_SIZE = ProdosVirtualDisk.BLOCK_SIZE * 128;
@Override
public EntryType getType() {
long fileSize = getPhysicalFile().length();
if (fileSize <= SEEDLING_MAX_SIZE) {
setType(EntryType.SEEDLING);
return EntryType.SEEDLING;
} else if (fileSize <= SAPLING_MAX_SIZE) {
setType(EntryType.SAPLING);
return EntryType.SAPLING;
}
setType(EntryType.TREE);
return EntryType.TREE;
}
@Override
public void setName(String name) {
String[] parts = name.split("\\.");
FileType t = null;
int offset = 0;
if (parts.length > 1) {
String extension = parts[parts.length - 1].toUpperCase();
String[] extParts = extension.split("#");
if (extParts.length == 2) {
offset = Integer.parseInt(extParts[1], 16);
extension = extParts[0];
}
try {
t = FileType.valueOf(extension);
} catch (IllegalArgumentException ex) {
System.out.println("Not sure what extension " + extension + " is!");
}
name = "";
for (int i = 0; i < parts.length - 1; i++) {
name += (i > 0 ? "." + parts[i] : parts[i]);
}
if (extParts[extParts.length - 1].equals("SYSTEM")) {
name += ".SYSTEM";
}
}
if (t == null) {
t = FileType.UNKNOWN;
}
if (offset == 0) {
offset = t.defaultLoadAddress;
}
fileType = t.code;
loadAddress = offset;
// Pass usable name (stripped of file extension and other type info) as name
super.setName(name);
}
public FileNode(ProdosVirtualDisk ownerFilesystem, File file) throws IOException {
setOwnerFilesystem(ownerFilesystem);
setPhysicalFile(file);
}
@Override
public void doDeallocate() {
}
@Override
public void doAllocate() throws IOException {
int dataBlocks = (int) ((getPhysicalFile().length() / ProdosVirtualDisk.BLOCK_SIZE) + 1);
int treeBlocks = 0;
if (dataBlocks > 1 && dataBlocks < 257) {
treeBlocks = 1;
} else {
treeBlocks = 1 + (dataBlocks / 256);
}
for (int i = 1; i < dataBlocks + treeBlocks; i++) {
new SubNode(i, this);
}
}
@Override
public void doRefresh() {
}
@Override
public void readBlock(int block, byte[] buffer) throws IOException {
// System.out.println("Read block "+block+" of file "+getName());
switch (this.getType()) {
case SEEDLING:
readFile(buffer, 0);
break;
case SAPLING:
if (block > 0) {
readFile(buffer, (block - 1));
} else {
// Generate seedling index block
generateIndex(buffer, 0, 256);
}
break;
case TREE:
int dataBlocks = (int) ((getPhysicalFile().length() / ProdosVirtualDisk.BLOCK_SIZE) + 1);
int treeBlocks = (dataBlocks / 256);
if (block == 0) {
generateIndex(buffer, 0, treeBlocks);
} else if (block < treeBlocks) {
int start = treeBlocks + (block - 1 * 256);
int end = Math.min(start + 256, treeBlocks);
generateIndex(buffer, treeBlocks, end);
} else {
readFile(buffer, (block - treeBlocks));
}
break;
}
}
private void readFile(byte[] buffer, int start) throws IOException {
FileInputStream f = new FileInputStream(physicalFile);
f.skip(start * ProdosVirtualDisk.BLOCK_SIZE);
f.read(buffer, 0, ProdosVirtualDisk.BLOCK_SIZE);
f.close();
}
private void generateIndex(byte[] buffer, int indexStart, int indexLimit) {
int pos = 0;
for (int i = indexStart; pos < 256 && i < indexLimit && i < additionalNodes.size(); i++, pos++) {
buffer[pos] = (byte) (additionalNodes.get(i).baseBlock & 0x0ff);
buffer[pos + 256] = (byte) ((additionalNodes.get(i).baseBlock >> 8) & 0x0ff);
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import java.io.IOException;
/**
* Maintain freespace and node allocation
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class FreespaceBitmap extends DiskNode {
int size = (ProdosVirtualDisk.MAX_BLOCK + 1) / 8 / ProdosVirtualDisk.BLOCK_SIZE;
public FreespaceBitmap(ProdosVirtualDisk fs, int start) throws IOException {
setBaseBlock(start);
setOwnerFilesystem(fs);
for (int i=1; i < size; i++) {
new SubNode(i, this, start+i);
}
}
@Override
public void doDeallocate() {
//
}
@Override
public void doAllocate() {
///
}
@Override
public void doRefresh() {
//
}
@Override
public void readBlock(int sequence, byte[] buffer) throws IOException {
int startBlock = sequence * ProdosVirtualDisk.BLOCK_SIZE * 8;
int endBlock = (sequence+1)* ProdosVirtualDisk.BLOCK_SIZE * 8;
int bitCounter=0;
int pos=0;
int value=0;
for (int i=startBlock; i < endBlock; i++) {
if (!getOwnerFilesystem().isAllocated(i)) {
value++;
}
bitCounter++;
if (bitCounter < 8) {
value *= 2;
} else {
bitCounter = 0;
buffer[pos++]=(byte) value;
value = 0;
}
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import java.io.IOException;
/**
* Generic representation of a mass storage disk, either an image or a virtual volume.
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public interface IDisk {
public static int BLOCK_SIZE = 512;
public static int MAX_BLOCK = 65535;
public void mliFormat() throws IOException;
public void mliRead(int block, int bufferAddress) throws IOException;
public void mliWrite(int block, int bufferAddress) throws IOException;
public void boot0(int slot) throws IOException;
// Return size in 512k blocks
public int getSize();
public void eject();
public boolean isWriteProtected();
}

View File

@ -0,0 +1,172 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import jace.apple2e.MOS65C02;
import jace.core.Computer;
import jace.core.RAM;
import static jace.hardware.ProdosDriver.*;
import jace.hardware.ProdosDriver.MLI_COMMAND_TYPE;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Representation of a hard drive or 800k disk image used by CardMassStorage
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class LargeDisk implements IDisk {
RandomAccessFile diskImage;
File diskPath;
// Offset in input file where data can be found
private int dataOffset = 0;
private int physicalBlocks = 0;
private int logicalBlocks = 0;
public LargeDisk(File f) {
try {
readDiskImage(f);
} catch (IOException ex) {
Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void mliFormat() throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
public void mliRead(int block, int bufferAddress) throws IOException {
RAM memory = Computer.getComputer().getMemory();
if (block < physicalBlocks) {
diskImage.seek((block * BLOCK_SIZE) + dataOffset);
for (int i = 0; i < BLOCK_SIZE; i++) {
memory.write(bufferAddress + i, diskImage.readByte(), true, false);
}
} else {
for (int i = 0; i < BLOCK_SIZE; i++) {
memory.write(bufferAddress + i, (byte) 0, true, false);
}
}
}
public void mliWrite(int block, int bufferAddress) throws IOException {
if (block < physicalBlocks) {
RAM memory = Computer.getComputer().getMemory();
diskImage.seek((block * BLOCK_SIZE) + dataOffset);
byte[] buf = new byte[BLOCK_SIZE];
for (int i = 0; i < BLOCK_SIZE; i++) {
buf[i]=memory.readRaw(bufferAddress + i);
}
diskImage.write(buf);
}
}
public void boot0(int slot) throws IOException {
Computer.getComputer().getCpu().suspend();
mliRead(0, 0x0800);
byte slot16 = (byte) (slot << 4);
((MOS65C02) Computer.getComputer().getCpu()).X = slot16;
RAM memory = Computer.getComputer().getMemory();
memory.write(CardMassStorage.SLT16, slot16, false, false);
memory.write(MLI_COMMAND, (byte) MLI_COMMAND_TYPE.READ.intValue, false, false);
memory.write(MLI_UNITNUMBER, slot16, false, false);
// Write location to block read routine to zero page
memory.writeWord(0x048, 0x0c000 + CardMassStorage.DEVICE_DRIVER_OFFSET + (slot * 0x0100), false, false);
((MOS65C02) Computer.getComputer().getCpu()).setProgramCounter(0x0800);
Computer.getComputer().getCpu().resume();
}
public File getPhysicalPath() {
return diskPath;
}
public void setPhysicalPath(File f) throws IOException {
diskPath = f;
}
private boolean read2mg(File f) {
boolean result = false;
FileInputStream fis = null;
try {
fis = new FileInputStream(getPhysicalPath());
if (fis.read() == 0x32 && fis.read() == 0x49 && fis.read() == 0x4D && fis.read() == 0x47) {
System.out.println("Disk is 2MG");
// todo: read header
dataOffset = 64;
physicalBlocks = (int) (f.length() / BLOCK_SIZE);
logicalBlocks = physicalBlocks;
result = true;
}
} catch (FileNotFoundException ex) {
Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
fis.close();
} catch (IOException ex) {
Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
return result;
}
private void readHdv(File f) {
System.out.println("Disk is HDV");
dataOffset = 0;
physicalBlocks = (int) (f.length() / BLOCK_SIZE);
logicalBlocks = physicalBlocks;
}
private void readDiskImage(File f) throws FileNotFoundException, IOException {
eject();
setPhysicalPath(f);
if (!read2mg(f)) {
readHdv(f);
}
diskImage = new RandomAccessFile(f, "rwd");
}
@Override
public void eject() {
if (diskImage != null) {
try {
diskImage.close();
diskImage = null;
setPhysicalPath(null);
} catch (IOException ex) {
Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@Override
public boolean isWriteProtected() {
return diskPath == null || !diskPath.canWrite();
}
@Override
public int getSize() {
return physicalBlocks;
}
}

View File

@ -0,0 +1,92 @@
/*
* Copyright (C) 2013 brobert.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import jace.library.MediaConsumer;
import jace.library.MediaEntry;
import jace.library.MediaEntry.MediaFile;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ImageIcon;
/**
*
* @author brobert
*/
public class MassStorageDrive implements MediaConsumer {
IDisk disk = null;
ImageIcon icon = null;
public ImageIcon getIcon() {
return icon;
}
public void setIcon(ImageIcon i) {
icon = i;
}
MediaEntry currentEntry;
MediaFile currentFile;
public void insertMedia(MediaEntry e, MediaFile f) throws IOException {
eject();
currentEntry = e;
currentFile = f;
disk= readDisk(currentFile.path);
}
public MediaEntry getMediaEntry() {
return currentEntry;
}
public MediaFile getMediaFile() {
return currentFile;
}
public boolean isAccepted(MediaEntry e, MediaFile f) {
return e.type.isProdosOrdered;
}
public void eject() {
if (disk != null) {
disk.eject();
disk = null;
}
}
private IDisk readDisk(File f) {
if (f.isFile()) {
return new LargeDisk(f);
} else if (f.isDirectory()) {
try {
return new ProdosVirtualDisk(f);
} catch (IOException ex) {
System.out.println("Unable to open virtual disk: " + ex.getMessage());
Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
}
}
return null;
}
public IDisk getCurrentDisk() {
return disk;
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import jace.EmulatorUILogic;
import jace.apple2e.MOS65C02;
import jace.core.Computer;
import jace.core.RAM;
import static jace.hardware.ProdosDriver.*;
import jace.hardware.ProdosDriver.MLI_COMMAND_TYPE;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Representation of a Prodos Volume which maps to a physical folder on the
* actual hard drive. This is used by CardMassStorage in the event the disk path
* is a folder and not a disk image. FreespaceBitmap and the various Node
* classes are used to represent the filesystem structure.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class ProdosVirtualDisk implements IDisk {
public static int VOLUME_START = 2;
public static int FREESPACE_BITMAP_START = 6;
byte[] ioBuffer;
File physicalRoot;
Map<Integer, DiskNode> physicalMap;
DirectoryNode rootDirectory;
FreespaceBitmap freespaceBitmap;
public ProdosVirtualDisk(File rootPath) throws IOException {
ioBuffer = new byte[BLOCK_SIZE];
setPhysicalPath(rootPath);
}
public void mliRead(int block, int bufferAddress) throws IOException {
// System.out.println("Read block " + block + " to " + Integer.toHexString(bufferAddress));
DiskNode node = physicalMap.get(block);
RAM memory = Computer.getComputer().getMemory();
Arrays.fill(ioBuffer, (byte) (block & 0x0ff));
if (node == null) {
System.out.println("Reading unknown block?!");
for (int i = 0; i < BLOCK_SIZE; i++) {
memory.write(bufferAddress + i, (byte) 0, false, false);
}
} else {
// if (node.getPhysicalFile() == null) {
// System.out.println("reading block "+block+ " from directory structure to "+Integer.toHexString(bufferAddress));
// } else {
// System.out.println("reading block "+block+ " from "+node.getPhysicalFile().getName()+" to "+Integer.toHexString(bufferAddress));
// }
node.readBlock(ioBuffer);
for (int i = 0; i < BLOCK_SIZE; i++) {
memory.write(bufferAddress + i, ioBuffer[i], false, false);
}
}
// for (int i=0; i < 512; i++) {
// if (i % 32 == 0 && i > 0) System.out.println();
// System.out.print(((ioBuffer[i]&0x0ff)<16 ? "0" : "") + Integer.toHexString(ioBuffer[i] & 0x0ff) + " ");
// }
// System.out.println();
}
public void mliWrite(int block, int bufferAddress) throws IOException {
System.out.println("Write block " + block + " to " + Integer.toHexString(bufferAddress));
throw new IOException("Write not implemented yet!");
// DiskNode node = physicalMap.get(block);
// RAM memory = Computer.getComputer().getMemory();
// if (node == null) {
// // CAPTURE WRITES TO UNUSED BLOCKS
// } else {
// node.readBlock(block, ioBuffer);
// for (int i=0; i < BLOCK_SIZE; i++) {
// memory.write(bufferAddress+i, ioBuffer[i], false);
// }
// }
}
public void mliFormat() {
throw new UnsupportedOperationException("Formatting for this type of media is not supported!");
}
public File locateFile(File rootPath, String string) {
File mostLikelyMatch = null;
for (File f : rootPath.listFiles()) {
if (f.getName().equalsIgnoreCase(string)) {
return f;
}
// This is not sufficient, a more deterministic approach should be taken
if (string.toUpperCase().startsWith(f.getName().toUpperCase())) {
if (mostLikelyMatch == null || f.getName().length() > mostLikelyMatch.getName().length()) {
mostLikelyMatch = f;
}
}
}
return mostLikelyMatch;
}
public int getNextFreeBlock() throws IOException {
// Don't allocate Zero block for anything!
// for (int i = 0; i < MAX_BLOCK; i++) {
for (int i = 2; i < MAX_BLOCK; i++) {
if (!physicalMap.containsKey(i)) {
return i;
}
}
throw new IOException("Virtual Disk Full!");
}
// Mark space occupied by node
public void allocateEntry(DiskNode node) {
physicalMap.put(node.baseBlock, node);
for (DiskNode sub : node.additionalNodes) {
physicalMap.put(sub.getBaseBlock(), sub);
}
}
// Mark space occupied by nodes as free (remove allocation mapping)
public void deallocateEntry(DiskNode node) {
// Only de-map nodes if the allocation table is actually pointing to the nodes!
if (physicalMap.get(node.baseBlock) != null && physicalMap.get(node.baseBlock).equals(node)) {
physicalMap.remove(node.baseBlock);
}
for (DiskNode sub : node.additionalNodes) {
if (physicalMap.get(sub.getBaseBlock()) != null && physicalMap.get(sub.baseBlock).equals(sub)) {
physicalMap.remove(sub.getBaseBlock());
}
}
}
// Is the specified block in use?
public boolean isAllocated(int i) {
return (physicalMap.containsKey(i));
}
@Override
public void boot0(int slot) throws IOException {
File prodos = locateFile(physicalRoot, "PRODOS.SYS");
if (prodos == null || !prodos.exists()) {
throw new IOException("Unable to locate PRODOS.SYS");
}
Computer.getComputer().getCpu().suspend();
byte slot16 = (byte) (slot << 4);
((MOS65C02) Computer.getComputer().getCpu()).X = slot16;
RAM memory = Computer.getComputer().getMemory();
memory.write(CardMassStorage.SLT16, slot16, false, false);
memory.write(MLI_COMMAND, (byte) MLI_COMMAND_TYPE.READ.intValue, false, false);
memory.write(MLI_UNITNUMBER, slot16, false, false);
// Write location to block read routine to zero page
memory.writeWord(0x048, 0x0c000 + CardMassStorage.DEVICE_DRIVER_OFFSET + (slot * 0x0100), false, false);
EmulatorUILogic.brun(prodos, 0x02000);
Computer.getComputer().getCpu().resume();
}
public File getPhysicalPath() {
return physicalRoot;
}
public void setPhysicalPath(File f) throws IOException {
if (physicalRoot != null && physicalRoot.equals(f)) {
return;
}
physicalRoot = f;
physicalMap = new HashMap<Integer, DiskNode>();
if (!physicalRoot.exists() || !physicalRoot.isDirectory()) {
try {
throw new IOException("Root path must be a directory that exists!");
} catch (IOException ex) {
Logger.getLogger(ProdosVirtualDisk.class.getName()).log(Level.SEVERE, null, ex);
}
}
// Root directory ALWAYS starts on block 2!
rootDirectory = new DirectoryNode(this, physicalRoot, VOLUME_START);
rootDirectory.setName("VIRTUAL");
allocateEntry(rootDirectory);
freespaceBitmap = new FreespaceBitmap(this, FREESPACE_BITMAP_START);
allocateEntry(freespaceBitmap);
}
public void eject() {
// Nothing to do here...
}
@Override
public boolean isWriteProtected() {
return true;
}
@Override
public int getSize() {
return 0x0ffff;
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package jace.hardware.massStorage;
import java.io.IOException;
/**
* Subnode is a generic node (block) used up by a file. Thes nodes do nothing
* more than just occupy space in the freespace bitmap. The file node itself
* keeps track of its subnodes and figures out what each subnode should
* "contain". The subnodes themselves don't track anything though.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class SubNode extends DiskNode {
int sequenceNumber;
private int seq;
public SubNode(int seq, DiskNode parent) throws IOException {
init(seq, parent);
}
public SubNode(int seq, DiskNode parent, int baseBlock) throws IOException {
setBaseBlock(baseBlock);
init(seq, parent);
}
private void init(int seq, DiskNode parent) throws IOException {
sequenceNumber = seq;
setParent(parent);
setOwnerFilesystem(parent.getOwnerFilesystem());
parent.additionalNodes.add(this);
}
@Override
public void doDeallocate() {
}
@Override
public void doAllocate() {
}
@Override
public void doRefresh() {
}
@Override
public void readBlock(int sequence, byte[] buffer) throws IOException {
parent.readBlock(sequenceNumber, buffer);
}
}

Some files were not shown because too many files have changed in this diff Show More