This commit is contained in:
Martin Haye 2018-01-10 07:10:51 -08:00
commit 0a829d9878
315 changed files with 142063 additions and 5 deletions

View File

@ -571,6 +571,7 @@ public class AppleImageEditor extends ImageEditor implements EventHandler<MouseE
try (FileOutputStream outStream = new FileOutputStream(out)) {
outStream.write(AppleNTSCGraphics.getAppleHGRBinary(getPlatformData()));
outStream.flush();
outStream.close();
} catch (IOException ex) {
Logger.getLogger(AppleImageEditor.class.getName()).log(Level.SEVERE, null, ex);
}

View File

@ -150,12 +150,20 @@ public class AppleNTSCGraphics {
}
static byte[] getAppleHGRBinary(PlatformData platformData) {
byte[] output = new byte[0x02000];
boolean dhgr = platformData.getWidth() > 40;
byte[] output = new byte[dhgr ? 0x04000 : 0x02000];
int counter = 0;
for (int y = 0; y < platformData.getHeight(); y++) {
int offset = calculateHiresOffset(y);
for (int x = 0; x < platformData.getWidth(); x++) {
output[offset + x] = platformData.getValue()[counter++];
if (dhgr) {
for (int x = 0; x < 40; x++) {
output[0x02000 + offset + x] = platformData.getValue()[counter++];
output[offset + x] = platformData.getValue()[counter++];
}
} else {
for (int x = 0; x < platformData.getWidth(); x++) {
output[offset + x] = platformData.getValue()[counter++];
}
}
}
return output;
@ -166,7 +174,7 @@ public class AppleNTSCGraphics {
for (int y = 0; y < data.getHeight(); y++) {
int pos = calculateHiresOffset(y) + 0x02000;
listing.append("\n").append(Integer.toHexString(pos)).append(":");
for (int x = 1; x < data.getWidth(); x+=2) {
for (int x = 0; x < data.getWidth(); x+=2) {
int val = data.getValue()[data.getWidth() * y + x] & 0x0ff;
listing.append(Integer.toHexString(val)).append(" ");
}
@ -174,7 +182,7 @@ public class AppleNTSCGraphics {
for (int y = 0; y < data.getHeight(); y++) {
int pos = calculateHiresOffset(y) + 0x04000;
listing.append("\n").append(Integer.toHexString(pos)).append(":");
for (int x = 0; x < data.getWidth(); x+=2) {
for (int x = 1; x < data.getWidth(); x+=2) {
int val = data.getValue()[data.getWidth() * y + x] & 0x0ff;
listing.append(Integer.toHexString(val)).append(" ");
}

View File

@ -0,0 +1,4 @@
*.zip
Lawless Legends/*
Lawless Legends.app/*
*.2mg

View File

@ -0,0 +1,6 @@
java -jar packr.jar --platform mac --jdk https://www.dropbox.com/s/nnodj0om38bnypk/mac_jre_1.8.151.zip?dl=1 --resources ../jace/target/LawlessLegends.jar --executable LawlessLegends --classpath LawlessLegends.jar --mainclass jace.LawlessLegends --vmargs Xmx1G --output "Lawless Legends.app" --icon game_icon.icns
java -jar packr.jar --platform windows64 --jdk https://www.dropbox.com/s/i4gxp4bhz97j9wx/win_jre_1.8.151.zip?dl=1 --resources ../jace/target/LawlessLegends.jar --executable LawlessLegends --classpath LawlessLegends.jar --mainclass jace.LawlessLegends --vmargs Xmx1G --output "Lawless Legends"
copy /BY game.2mg "Lawless Legends"
copy /BY game.2mg "Lawless Legends.app\Contents\Resources"

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@ -0,0 +1,36 @@
Java Apple Computer Emulator
====
Download:
* [See releases page for most recent](https://github.com/badvision/jace/releases)
To Run:
* See [run.sh](run.sh)
or `java -jar Jace.jar`
To Build:
* See [build.sh](build.sh)
Jace is a java-based Apple //e emulator with many compelling features:
* NEW: Built-in IDE for writing basic and assembly programs, using ACME to compile and execute directly without leaving the emulator.
* Disk and Mass-storage (hard drive, 3.5 floppy) images
* Joystick and Mouse are fully supported (Joystick can be emulated with either keyboard or mouse, Java doesn't have native joystick support
* All graphics modes are supported, including Apple RGB "Mixed" and B&W modes.
* Fullscreen and windowed modes supported
* PassPort MIDI support for Ultima V
* MetaCheat allows quick and easy ability to find and alter active memory
* MetaCheat also provides a live heat-map showing all RAM activity, color coded to indicate read, write or CPU execution -- Perfect for reverse engineers
* Optional Debugging rom (][DB) can be enabled for a more powerful monitor
* Super serial emulated as a tcp/ip port
* Mockingboard and Phasor support
* Highly flexible configuration allows any combination of cards and many extra options. You can even alter configuration while the emulation is running!
![Airheart](https://sites.google.com/site/brendanrobert/_/rsrc/1327073239228/projects/jace/airheart.png?height=250&width=400)
![Color swatches](https://sites.google.com/site/brendanrobert/_/rsrc/1327073239228/projects/jace/colors.png?height=223&width=400)
![Desktop II](https://sites.google.com/site/brendanrobert/_/rsrc/1327992588666/projects/jace/AppleIIDesktop.png?height=265&width=400)
More information here: https://sites.google.com/site/brendanrobert/projects/jace

View File

@ -0,0 +1,101 @@
#!/bin/sh
# Building Jace requies:
#
# * Maven
# * Java 1.8 JDK
#
# On OSX the easiest way to install Maven is to use brew
#
# brew install maven
#
#
# Troubleshooting:
# ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.3:compile (default-compile) on project jace: Fatal error compiling: invalid target release: 1.8 -> [Help 1]
# org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.3:compile (default-compile) on project jace: Fatal error compiling
#
# Cause: You probably have the Java 1.8 RUNTIME installed but Maven is (trying to) use the Java 1.7 COMPILER.
# OR : You probably have Java 1.7 installed but Maven is (trying to) use Java 1.8
# Reference: http://stackoverflow.com/questions/24705877/cant-get-maven-to-recognize-java-1-8
#
# 0. Here is some information to clear up the confusion about Java:
#
# The JRE (runtime) is needed to RUN Java programs.
# The JDK (compiler) is needed to COMPILTE Java programs.
#
# Solution:
#
# 1. Check which verison of Java that Maven is using
#
# mvn -version
#
# 2. Check which version of the Java JRE is installed:
#
# java -version
#
# You should see something like this:
#
# java version "1.8.0_66"
# Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
#
# 3. Check which version of the Java JDK is installed:
#
# javac -version
#
# If you see something like this:
#
# javac 1.7.0_75
#
# Then you will need to proceed to the next step, else you can skip it.
#
# 4. Install Java 1.8 JDK (if necessary)
#
# You can download the JDK either via the GUI or the command line:
# http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
#
# To download from the command line:
#
# For OSX
# curl -L -O -H "Cookie: oraclelicense=accept-securebackup-cookie" -k "https://edelivery.oracle.com/otn-pub/java/jdk/8u66-b17/jdk-8u66-macosx-x64.dmg"
# open jdk-8u66-macosx-x64.dmg
# Double-click on the .pkg
#
# For Linux
# curl -L -O -H "Cookie: oraclelicense=accept-securebackup-cookie" -k "https://edelivery.oracle.com/otn-pub/java/jdk/8u20-b26/jdk-8u20-linux-i586.tar.gz"
#
# Reference:
# Commands / shell script to download JDK / JRE / Java binaries from Oracle website from terminal / shell / command line / command prompt.
# https://gist.github.com/P7h/9741922
#
# 5. Lastly, verify that JAVA_HOME is set:
#
# echo ${JAVA_HOME}
#
# If it is blank (or not set), set it via:
#
# export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
#
# Then you can (finally!) build JACE. Whew!
#
# Note: Changing the maven project file 'pom.xml' to use Java 1.7 *won't* work:
# <plugin>
# <groupId>org.apache.maven.plugins</groupId>
# <artifactId>maven-compiler-plugin</artifactId>
# <version>3.3</version>
# <configuration>
# <source>1.7</source>
# <target>1.7</target>
#
# As the source code is using Java 1.8 langauge features.
if [[ -z "$JAVA_HOME" ]]; then
echo "WARNING: JAVA_HOME was not set"
echo "... Defaulting to Java 1.8..."
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
echo "... JAVA_HOME=${JAVA_HOME}"
fi
#mvn clean install -X
mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V

View File

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

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<!--
This file contains additional configuration written by modules in the NetBeans IDE.
The configuration is intended to be shared among all the users of project and
therefore it is assumed to be part of version control checkout.
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
-->
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
<!--
Properties that influence various parts of the IDE, especially code formatting and the like.
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
That way multiple projects can share the same settings (useful for formatting rules for example).
Any value defined here will override the pom.xml file value but is only applicable to the current project.
-->
<org-netbeans-modules-html-editor-lib.default-html-public-id>HTML5</org-netbeans-modules-html-editor-lib.default-html-public-id>
<netbeans.hint.jdkPlatform>JDK_1.8</netbeans.hint.jdkPlatform>
</properties>
</project-shared-configuration>

View File

@ -0,0 +1,50 @@
<?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 %classpath jace.LawlessLegends</exec.args>
<exec.executable>java</exec.executable>
</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.LawlessLegends</exec.args>
<exec.executable>java</exec.executable>
<jpda.listen>true</jpda.listen>
</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>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath jace.LawlessLegends</exec.args>
<exec.executable>java</exec.executable>
<jpda.listen>true</jpda.listen>
</properties>
</action>
</actions>

View File

@ -0,0 +1,104 @@
<?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.8bitbunch</groupId>
<artifactId>lawlesslegends</artifactId>
<version>2.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>lawlesslegends</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mainClass>jace.LawlessLegends</mainClass>
<netbeans.hint.license>apache20</netbeans.hint.license>
</properties>
<organization>
<!-- Used as the 'Vendor' for JNLP generation -->
<name>org.badvision</name>
</organization>
<build>
<finalName>LawlessLegends</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</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.4.0</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/../bin/javapackager</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>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<version>3.5</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xerial.thirdparty</groupId>
<artifactId>nestedvm</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,4 @@
#!/bin/sh
java -jar target/Jace.jar

View File

@ -0,0 +1,127 @@
/*
* 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;
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;
try {
theDisk = new FloppyDisk(in, null);
} catch (IOException ex) {
System.out.println("Couldn't read disk image");
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());
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException ex) {
System.err.println("Error closing NIB image: " + ex.getMessage());
}
}
}
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,86 @@
/*
* 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.config.Configuration;
import jace.lawless.LawlessComputer;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 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 EmulatorUILogic logic = new EmulatorUILogic();
public static Thread mainThread;
// public static void main(String... args) {
// mainThread = Thread.currentThread();
// instance = new Emulator(args);
// }
public static LawlessComputer computer;
/**
* Creates a new instance of Emulator
* @param args
*/
public Emulator(List<String> args) {
instance = this;
computer = new LawlessComputer();
Configuration.buildTree();
Configuration.loadSettings();
mainThread = Thread.currentThread();
Map<String, String> settings = new LinkedHashMap<>();
if (args != null) {
for (int i = 0; i < args.size(); i++) {
if (args.get(i).startsWith("-")) {
String key = args.get(i).substring(1);
if ((i + 1) < args.size()) {
String val = args.get(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.get(i) + ", skipping.");
}
}
}
Configuration.applySettings(settings);
// EmulatorUILogic.registerDebugger();
// computer.coldStart();
}
public static void resizeVideo() {
// AbstractEmulatorFrame window = getFrame();
// if (window != null) {
// window.resizeVideo();
// }
}
}

View File

@ -0,0 +1,548 @@
/**
* 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.ConfigurableField;
import jace.config.ConfigurationUIController;
import jace.config.InvokableAction;
import jace.config.Reconfigurable;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.Debugger;
import jace.core.RAM;
import jace.core.RAMListener;
import static jace.core.Utility.*;
import jace.ide.IdeController;
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
/**
* 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 implements Reconfigurable {
static Debugger debugger;
static {
debugger = new Debugger() {
@Override
public void updateStatus() {
enableDebug(true);
MOS65C02 cpu = (MOS65C02) Emulator.computer.getCpu();
updateCPURegisters(cpu);
}
};
}
@ConfigurableField(
category = "General",
name = "Show Drives"
)
public boolean showDrives = false;
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) {
Emulator.computer.getCpu().setTraceEnabled(b);
}
public static void stepForward() {
debugger.step = true;
}
static void registerDebugger() {
Emulator.computer.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) -> {
// Emulator.computer.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));
// }
// };
// Emulator.computer.getMemory().addListener(newListener);
// watches.add(newListener);
// // Print out the current value right away
// byte b = Emulator.computer.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",
defaultKeyMapping = "ctrl+shift+b")
public static void runFile() {
Emulator.computer.pause();
FileChooser select = new FileChooser();
File binary = select.showOpenDialog(LawlessLegends.getApplication().primaryStage);
if (binary == null) {
Emulator.computer.resume();
return;
}
runFileNamed(binary);
}
public static void runFileNamed(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) {
}
Emulator.computer.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
Emulator.computer.pause();
FileInputStream in = new FileInputStream(binary);
byte[] data = new byte[in.available()];
in.read(data);
RAM ram = Emulator.computer.getMemory();
for (int i = 0; i < data.length; i++) {
ram.write(address + i, data[i], false, true);
}
CPU cpu = Emulator.computer.getCpu();
Emulator.computer.getCpu().setProgramCounter(address);
Emulator.computer.resume();
}
@InvokableAction(
name = "Toggle Debug",
category = "debug",
description = "Show/hide the debug panel",
alternatives = "Show Debug;Hide Debug",
defaultKeyMapping = "ctrl+shift+d")
public static void toggleDebugPanel() {
// AbstractEmulatorFrame frame = Emulator.getFrame();
// if (frame == null) {
// return;
// }
// frame.setShowDebug(!frame.isShowDebug());
// frame.reconfigure();
// Emulator.resizeVideo();
}
@InvokableAction(
name = "Toggle fullscreen",
category = "general",
description = "Activate/deactivate fullscreen mode",
alternatives = "fullscreen,maximize",
defaultKeyMapping = "ctrl+shift+f")
public static void toggleFullscreen() {
Platform.runLater(() -> {
Stage stage = LawlessLegends.getApplication().primaryStage;
stage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH);
stage.setFullScreen(!stage.isFullScreen());
LawlessLegends.getApplication().controller.setAspectRatioEnabled(stage.isFullScreen());
});
}
@InvokableAction(
name = "Save Raw Screenshot",
category = "general",
description = "Save raw (RAM) format of visible screen",
alternatives = "screendump, raw screenshot",
defaultKeyMapping = "ctrl+shift+z")
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 = Emulator.computer.getVideo().getCurrentWriter().actualWriter().getYOffset(0);
int len;
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) Emulator.computer.memory;
Emulator.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",
defaultKeyMapping = "ctrl+shift+s")
public static void saveScreenshot() throws IOException {
FileChooser select = new FileChooser();
Emulator.computer.pause();
Image i = Emulator.computer.getVideo().getFrameBuffer();
// BufferedImage bufImageARGB = SwingFXUtils.fromFXImage(i, null);
File targetFile = select.showSaveDialog(LawlessLegends.getApplication().primaryStage);
if (targetFile == null) {
return;
}
String filename = targetFile.getName();
System.out.println("Writing screenshot to " + filename);
String extension = filename.substring(filename.lastIndexOf(".") + 1);
// BufferedImage bufImageRGB = new BufferedImage(bufImageARGB.getWidth(), bufImageARGB.getHeight(), BufferedImage.OPAQUE);
//
// Graphics2D graphics = bufImageRGB.createGraphics();
// graphics.drawImage(bufImageARGB, 0, 0, null);
//
// ImageIO.write(bufImageRGB, extension, targetFile);
// graphics.dispose();
}
public static final String CONFIGURATION_DIALOG_NAME = "Configuration";
@InvokableAction(
name = "Configuration",
category = "general",
description = "Edit emulator configuraion",
alternatives = "Reconfigure,Preferences,Settings",
defaultKeyMapping = {"f4", "ctrl+shift+c"})
public static void showConfig() {
FXMLLoader fxmlLoader = new FXMLLoader(EmulatorUILogic.class.getResource("/fxml/Configuration.fxml"));
fxmlLoader.setResources(null);
try {
Stage configWindow = new Stage();
AnchorPane node = (AnchorPane) fxmlLoader.load();
ConfigurationUIController controller = fxmlLoader.getController();
controller.initialize();
Scene s = new Scene(node);
configWindow.setScene(s);
configWindow.show();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
@InvokableAction(
name = "Open IDE",
category = "development",
description = "Open new IDE window for Basic/Assembly/Plasma coding",
alternatives = "dev,development,acme,assembler,editor",
defaultKeyMapping = {"ctrl+shift+i"})
public static void showIDE() {
FXMLLoader fxmlLoader = new FXMLLoader(EmulatorUILogic.class.getResource("/fxml/editor.fxml"));
fxmlLoader.setResources(null);
try {
Stage editorWindow = new Stage();
AnchorPane node = (AnchorPane) fxmlLoader.load();
IdeController controller = fxmlLoader.getController();
controller.initialize();
Scene s = new Scene(node);
editorWindow.setScene(s);
editorWindow.show();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
static int size = -1;
@InvokableAction(
name = "Resize window",
category = "general",
description = "Resize the screen to 1x/1.5x/2x/3x video size",
alternatives = "Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;",
defaultKeyMapping = {"ctrl+shift+a"})
public static void scaleIntegerRatio() {
Platform.runLater(() -> {
if (LawlessLegends.getApplication() == null
|| LawlessLegends.getApplication().primaryStage == null) {
return;
}
Stage stage = LawlessLegends.getApplication().primaryStage;
size++;
if (size > 3) {
size = 0;
}
if (stage.isFullScreen()) {
LawlessLegends.getApplication().controller.toggleAspectRatio();
} else {
int width = 0, height = 0;
switch (size) {
case 0: // 1x
width = 560;
height = 384;
break;
case 1: // 1.5x
width = 840;
height = 576;
break;
case 2: // 2x
width = 560 * 2;
height = 384 * 2;
break;
case 3: // 3x (retina) 2880x1800
width = 560 * 3;
height = 384 * 3;
break;
default: // 2x
width = 560 * 2;
height = 384 * 2;
}
double vgap = stage.getScene().getY();
double hgap = stage.getScene().getX();
stage.setWidth(hgap * 2 + width);
stage.setHeight(vgap + height);
}
});
}
public static boolean confirm(String message) {
// return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(Emulator.getFrame(), message);
return false;
}
static final Map<Object, Set<Label>> INDICATORS = new HashMap<>();
static public void addIndicator(Object owner, Label icon) {
addIndicator(owner, icon, 250);
}
static public void addIndicator(Object owner, Label icon, long TTL) {
if (LawlessLegends.getApplication() == null) {
return;
}
synchronized (INDICATORS) {
Set<Label> ind = INDICATORS.get(owner);
if (ind == null) {
ind = new HashSet<>();
INDICATORS.put(owner, ind);
}
ind.add(icon);
LawlessLegends.getApplication().controller.addIndicator(icon);
}
}
static public void removeIndicator(Object owner, Label icon) {
if (LawlessLegends.singleton == null) {
return;
}
synchronized (INDICATORS) {
Set<Label> ind = INDICATORS.get(owner);
if (ind != null) {
ind.remove(icon);
}
LawlessLegends.singleton.controller.removeIndicator(icon);
}
}
static public void removeIndicators(Object owner) {
if (LawlessLegends.singleton == null) {
return;
}
synchronized (INDICATORS) {
Set<Label> ind = INDICATORS.get(owner);
if (ind == null) {
return;
}
ind.stream().forEach((icon) -> {
LawlessLegends.singleton.controller.removeIndicator(icon);
});
INDICATORS.remove(owner);
}
}
static public void addMouseListener(EventHandler<MouseEvent> handler) {
if (LawlessLegends.singleton != null) {
LawlessLegends.singleton.controller.addMouseListener(handler);
}
}
static public void removeMouseListener(EventHandler<MouseEvent> handler) {
if (LawlessLegends.singleton != null) {
LawlessLegends.singleton.controller.removeMouseListener(handler);
}
}
public static void simulateCtrlAppleReset() {
Computer computer = LawlessLegends.singleton.controller.computer;
computer.keyboard.openApple(true);
computer.warmStart();
Platform.runLater(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(EmulatorUILogic.class.getName()).log(Level.SEVERE, null, ex);
}
computer.keyboard.openApple(false);
});
}
public static void notify(String message) {
if (LawlessLegends.singleton != null) {
LawlessLegends.singleton.controller.displayNotification(message);
}
}
@Override
public String getName() {
return "Jace User Interface";
}
@Override
public String getShortName() {
return "UI";
}
@Override
public void reconfigure() {
}
}

View File

@ -0,0 +1,300 @@
/*
* 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 com.sun.glass.ui.Application;
import jace.core.Card;
import jace.core.Computer;
import jace.library.MediaCache;
import jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
import jace.library.MediaEntry;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.When;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
*
* @author blurry
*/
public class JaceUIController {
@FXML
private URL location;
@FXML
private AnchorPane rootPane;
@FXML
private StackPane stackPane;
@FXML
private HBox notificationBox;
@FXML
private ImageView appleScreen;
Computer computer;
private BooleanProperty aspectRatioCorrectionEnabled = new SimpleBooleanProperty(false);
@FXML
void initialize() {
assert rootPane != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'JaceUI.fxml'.";
assert stackPane != null : "fx:id=\"stackPane\" was not injected: check your FXML file 'JaceUI.fxml'.";
assert notificationBox != null : "fx:id=\"notificationBox\" was not injected: check your FXML file 'JaceUI.fxml'.";
assert appleScreen != null : "fx:id=\"appleScreen\" was not injected: check your FXML file 'JaceUI.fxml'.";
NumberBinding aspectCorrectedWidth = rootPane.heightProperty().multiply(3.0).divide(2.0);
NumberBinding width = new When(
aspectRatioCorrectionEnabled.and(aspectCorrectedWidth.lessThan(rootPane.widthProperty()))
).then(aspectCorrectedWidth).otherwise(rootPane.widthProperty());
appleScreen.fitWidthProperty().bind(width);
appleScreen.fitHeightProperty().bind(rootPane.heightProperty());
appleScreen.setVisible(false);
rootPane.setOnDragEntered(this::processDragEnteredEvent);
rootPane.setOnDragExited(this::processDragExitedEvent);
rootPane.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null)));
}
public void toggleAspectRatio() {
setAspectRatioEnabled(aspectRatioCorrectionEnabled.not().get());
}
public void setAspectRatioEnabled(boolean enabled) {
aspectRatioCorrectionEnabled.set(enabled);
}
public void connectComputer(Computer computer, Stage primaryStage) {
if (computer == null) {
return;
}
this.computer = computer;
Platform.runLater(() -> {
if (computer.getKeyboard() != null) {
EventHandler<KeyEvent> keyboardHandler = computer.getKeyboard().getListener();
primaryStage.setOnShowing(evt -> computer.getKeyboard().resetState());
rootPane.setOnKeyPressed(keyboardHandler);
rootPane.setOnKeyReleased(keyboardHandler);
rootPane.setFocusTraversable(true);
}
appleScreen.setImage(computer.getVideo().getFrameBuffer());
appleScreen.setVisible(true);
rootPane.requestFocus();
});
}
private void processDragEnteredEvent(DragEvent evt) {
MediaEntry media = null;
if (evt.getDragboard().hasFiles()) {
media = MediaCache.getMediaFromFile(getDraggedFile(evt.getDragboard().getFiles()));
} else if (evt.getDragboard().hasUrl()) {
media = MediaCache.getMediaFromUrl(evt.getDragboard().getUrl());
} else if (evt.getDragboard().hasString()) {
String path = evt.getDragboard().getString();
try {
URI.create(path);
media = MediaCache.getMediaFromUrl(path);
} catch (IllegalArgumentException ex) {
File f = new File(path);
if (f.exists()) {
media = MediaCache.getMediaFromFile(f);
}
}
}
if (media != null) {
evt.acceptTransferModes(TransferMode.LINK, TransferMode.COPY);
startDragEvent(media);
}
}
private void processDragExitedEvent(DragEvent evt) {
endDragEvent();
}
private File getDraggedFile(List<File> files) {
if (files == null || files.isEmpty()) {
return null;
}
for (File f : files) {
if (f.exists()) {
return f;
}
}
return null;
}
HBox drivePanel;
private void startDragEvent(MediaEntry media) {
List<MediaConsumer> consumers = getMediaConsumers();
drivePanel = new HBox();
consumers.stream()
.filter((consumer) -> (consumer.isAccepted(media, media.files.get(0))))
.forEach((consumer) -> {
Label icon = consumer.getIcon().orElse(null);
if (icon == null) {
return;
}
icon.setTextFill(Color.WHITE);
icon.setPadding(new Insets(2.0));
drivePanel.getChildren().add(icon);
icon.setOnDragOver(event -> {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
});
icon.setOnDragDropped(event -> {
System.out.println("Dropping media on " + icon.getText());
try {
computer.pause();
consumer.insertMedia(media, media.files.get(0));
computer.resume();
event.setDropCompleted(true);
event.consume();
} catch (IOException ex) {
Logger.getLogger(JaceUIController.class.getName()).log(Level.SEVERE, null, ex);
}
endDragEvent();
});
});
stackPane.getChildren().add(drivePanel);
drivePanel.setLayoutX(10);
drivePanel.setLayoutY(10);
}
private void endDragEvent() {
stackPane.getChildren().remove(drivePanel);
drivePanel.getChildren().stream().forEach((n) -> {
n.setOnDragDropped(null);
});
}
private List<MediaConsumer> getMediaConsumers() {
List<MediaConsumer> consumers = new ArrayList<>();
consumers.add(Emulator.computer.getUpgradeHandler());
if (Emulator.logic.showDrives) {
for (Optional<Card> card : computer.memory.getAllCards()) {
card.filter(c -> c instanceof MediaConsumerParent).ifPresent(parent -> {
consumers.addAll(Arrays.asList(((MediaConsumerParent) parent).getConsumers()));
});
}
}
return consumers;
}
Map<Label, Long> iconTTL = new ConcurrentHashMap<>();
void addIndicator(Label icon) {
addIndicator(icon, 250);
}
void addIndicator(Label icon, long TTL) {
if (!iconTTL.containsKey(icon)) {
Application.invokeLater(() -> {
if (!notificationBox.getChildren().contains(icon)) {
notificationBox.getChildren().add(icon);
}
});
}
trackTTL(icon, TTL);
}
void removeIndicator(Label icon) {
Application.invokeLater(() -> {
notificationBox.getChildren().remove(icon);
iconTTL.remove(icon);
});
}
ScheduledExecutorService notificationExecutor = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture ttlCleanupTask = null;
private void trackTTL(Label icon, long TTL) {
iconTTL.put(icon, System.currentTimeMillis() + TTL);
if (ttlCleanupTask == null || ttlCleanupTask.isCancelled()) {
ttlCleanupTask = notificationExecutor.scheduleWithFixedDelay(this::processTTL, 1, 100, TimeUnit.MILLISECONDS);
}
}
private void processTTL() {
Long now = System.currentTimeMillis();
iconTTL.keySet().stream()
.filter((icon) -> (iconTTL.get(icon) <= now))
.forEach((icon) -> {
removeIndicator(icon);
});
if (iconTTL.isEmpty()) {
ttlCleanupTask.cancel(true);
ttlCleanupTask = null;
}
}
public void addMouseListener(EventHandler<MouseEvent> handler) {
appleScreen.addEventHandler(MouseEvent.ANY, handler);
}
public void removeMouseListener(EventHandler<MouseEvent> handler) {
appleScreen.removeEventHandler(MouseEvent.ANY, handler);
}
Label currentNotification = null;
public void displayNotification(String message) {
Label oldNotification = currentNotification;
Label notification = new Label(message);
currentNotification = notification;
notification.setEffect(new DropShadow(2.0, Color.BLACK));
notification.setTextFill(Color.WHITE);
notification.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 80, 0.7), new CornerRadii(5.0), new Insets(-5.0))));
Application.invokeLater(() -> {
stackPane.getChildren().remove(oldNotification);
stackPane.getChildren().add(notification);
});
notificationExecutor.schedule(() -> {
Application.invokeLater(() -> {
stackPane.getChildren().remove(notification);
});
}, 4, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,165 @@
/*
* 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 jace.config.Configuration;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.Utility;
import jace.hardware.CardDiskII;
import jace.hardware.CardMockingboard;
import jace.hardware.CardRamFactor;
import jace.hardware.CardRamworks;
import jace.hardware.PassportMidiInterface;
import jace.hardware.massStorage.CardMassStorage;
import jace.lawless.LawlessHacks;
import jace.lawless.LawlessImageTool;
import jace.lawless.LawlessVideo;
import jace.ui.MetacheatUI;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
*
* @author blurry
*/
public class LawlessLegends extends Application {
static LawlessLegends singleton;
public Stage primaryStage;
public JaceUIController controller;
static boolean romStarted = false;
@Override
public void start(Stage stage) throws Exception {
singleton = this;
primaryStage = stage;
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/JaceUI.fxml"));
fxmlLoader.setResources(null);
try {
AnchorPane node = (AnchorPane) fxmlLoader.load();
controller = fxmlLoader.getController();
controller.initialize();
Scene s = new Scene(node);
s.setFill(Color.BLACK);
primaryStage.setScene(s);
primaryStage.setTitle("Lawless Legends");
Utility.loadIcon("game_icon.png").ifPresent(primaryStage.getIcons()::add);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
primaryStage.show();
new Thread(() -> {
new Emulator(getParameters().getRaw());
reconnectUIHooks();
EmulatorUILogic.scaleIntegerRatio();
while (Emulator.computer.getVideo() == null || Emulator.computer.getVideo().getFrameBuffer() == null) {
Thread.yield();
}
configureEmulatorForGame();
bootWatchdog();
}).start();
primaryStage.setOnCloseRequest(event -> {
Emulator.computer.deactivate();
Platform.exit();
System.exit(0);
});
}
public void reconnectUIHooks() {
controller.connectComputer(Emulator.computer, primaryStage);
}
public static LawlessLegends getApplication() {
return singleton;
}
Stage cheatStage;
private MetacheatUI cheatController;
public MetacheatUI showMetacheat() {
if (cheatController == null) {
cheatStage = new Stage(StageStyle.DECORATED);
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/Metacheat.fxml"));
fxmlLoader.setResources(null);
try {
VBox node = fxmlLoader.load();
cheatController = fxmlLoader.getController();
Scene s = new Scene(node);
cheatStage.setScene(s);
cheatStage.setTitle("Jace: MetaCheat");
Utility.loadIcon("woz_figure.gif").ifPresent(cheatStage.getIcons()::add);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
cheatStage.show();
return cheatController;
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
/**
* Start the computer and make sure it runs through the expected rom routine
* for cold boot
*/
private void bootWatchdog() {
romStarted = false;
RAMListener startListener = Emulator.computer.getMemory().
observe(RAMEvent.TYPE.EXECUTE, 0x0c700, (e) -> {
romStarted = true;
});
Emulator.computer.coldStart();
try {
Thread.sleep(10000);
if (!romStarted) {
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Boot not detected, performing a cold start");
Emulator.computer.coldStart();
}
} catch (InterruptedException ex) {
Logger.getLogger(LawlessLegends.class.getName()).log(Level.SEVERE, null, ex);
}
Emulator.computer.getMemory().removeListener(startListener);
}
private void configureEmulatorForGame() {
Emulator.computer.enableHints = false;
Emulator.computer.clockEnabled = true;
Emulator.computer.joy1enabled = false;
Emulator.computer.joy2enabled = false;
Emulator.computer.enableStateManager = false;
Emulator.computer.ramCard.setValue(CardRamworks.class);
Emulator.computer.videoRenderer.setValue(LawlessVideo.class);
Emulator.computer.card7.setValue(CardMassStorage.class);
Emulator.computer.card6.setValue(CardDiskII.class);
Emulator.computer.card5.setValue(CardRamFactor.class);
Emulator.computer.card4.setValue(CardMockingboard.class);
Emulator.computer.card2.setValue(PassportMidiInterface.class);
Emulator.computer.cheatEngine.setValue(LawlessHacks.class);
Configuration.buildTree();
Emulator.computer.reconfigure();
((LawlessImageTool) Emulator.computer.getUpgradeHandler()).loadGame();
}
}

View File

@ -0,0 +1,487 @@
/*
* 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.LawlessLegends;
import jace.apple2e.softswitch.VideoSoftSwitch;
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.core.Utility;
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.NoSlotClock;
import jace.hardware.massStorage.CardMassStorage;
import jace.lawless.LawlessComputer;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
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;
@ConfigurableField(name = "Slot 1", shortName = "s1card")
public ClassSelection card1 = new ClassSelection(Card.class, null);
@ConfigurableField(name = "Slot 2", shortName = "s2card")
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);
@ConfigurableField(name = "Joystick 1 Enabled", shortName = "joy1", description = "If unchecked, then there is no joystick support.", enablesDevice = true)
public boolean joy1enabled = true;
@ConfigurableField(name = "Joystick 2 Enabled", shortName = "joy2", description = "If unchecked, then there is no joystick support.", enablesDevice = true)
public boolean joy2enabled = false;
@ConfigurableField(name = "No-Slot Clock Enabled", shortName = "clock", description = "If checked, no-slot clock will be enabled", enablesDevice = true)
public boolean clockEnabled = true;
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;
public NoSlotClock clock;
/**
* Creates a new instance of Apple2e
*/
public Apple2e() {
super();
try {
reconfigure();
setCpu(new MOS65C02(this));
reinitMotherboard();
} catch (Throwable t) {
System.err.println("Unable to initalize virtual machine");
t.printStackTrace(System.err);
}
}
@Override
public String getName() {
return "Computer (Apple //e)";
}
protected void reinitMotherboard() {
if (motherboard != null && motherboard.isRunning()) {
motherboard.suspend();
}
setMotherboard(new Motherboard(this, motherboard));
reconfigure();
motherboard.reconfigure();
}
@Override
public void coldStart() {
pause();
reinitMotherboard();
for (SoftSwitches s : SoftSwitches.values()) {
s.getSwitch().reset();
}
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset);
}
reboot();
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 = pause();
for (SoftSwitches s : SoftSwitches.values()) {
if (! (s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset();
}
}
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
getCpu().reset();
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset);
}
getCpu().resume();
resume();
}
public Cheats getActiveCheatEngine() {
return activeCheatEngine;
}
private void insertCard(Class<? extends Card> type, int slot) throws NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
if (getMemory().getCard(slot).isPresent()) {
if (getMemory().getCard(slot).get().getClass().equals(type)) {
return;
}
getMemory().removeCard(slot);
}
if (type != null) {
try {
Card card = type.getConstructor(Computer.class).newInstance(this);
getMemory().addCard(card, slot);
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@Override
public final void reconfigure() {
boolean restart = pause();
if (Utility.isHeadlessMode()) {
joy1enabled = false;
joy2enabled = false;
}
super.reconfigure();
RAM128k currentMemory = (RAM128k) getMemory();
if (currentMemory != null && ramCard.getValue() != null && !(currentMemory.getClass().equals(ramCard.getValue()))) {
try {
RAM128k newMemory = (RAM128k) ramCard.getValue().getConstructor(Computer.class).newInstance(this);
newMemory.copyFrom(currentMemory);
setMemory(newMemory);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (getMemory() == null) {
try {
currentMemory = (RAM128k) ramCard.getValue().getConstructor(Computer.class).newInstance(this);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
try {
setMemory(currentMemory);
for (SoftSwitches s : SoftSwitches.values()) {
s.getSwitch().register(this);
}
} catch (Throwable ex) {
}
}
currentMemory.reconfigure();
if (motherboard != null) {
if (joy1enabled) {
if (joystick1 == null) {
joystick1 = new Joystick(0, this);
motherboard.miscDevices.add(joystick1);
joystick1.attach();
}
} else if (joystick1 != null) {
joystick1.detach();
motherboard.miscDevices.remove(joystick1);
joystick1 = null;
}
if (joy2enabled) {
if (joystick2 == null) {
joystick2 = new Joystick(1, this);
motherboard.miscDevices.add(joystick2);
joystick2.attach();
}
} else if (joystick2 != null) {
joystick2.detach();
motherboard.miscDevices.remove(joystick2);
joystick2 = null;
}
if (clockEnabled) {
if (clock == null) {
clock = new NoSlotClock(this);
motherboard.miscDevices.add(clock);
clock.attach();
}
} else if (clock != null) {
motherboard.miscDevices.remove(clock);
clock.detach();
clock = null;
}
}
try {
if (useConsoleProbe) {
probe.init(this);
} else {
probe.shutdown();
}
if (useDebugRom) {
loadRom("jace/data/apple2e_debug.rom");
} else {
loadRom("jace/data/apple2e.rom");
}
RAM128k ram = (RAM128k) getMemory();
ram.activeRead.writeByte(0x0fffc, (byte) 0x000);
ram.activeRead.writeByte(0x0fffd, (byte) 0x0c7);
if (getVideo() == null || getVideo().getClass() != videoRenderer.getValue()) {
if (getVideo() != null) {
getVideo().suspend();
}
try {
setVideo((Video) videoRenderer.getValue().getConstructor(Computer.class).newInstance(this));
getVideo().configureVideoMode();
getVideo().reconfigure();
Emulator.resizeVideo();
if (LawlessLegends.getApplication() != null) {
LawlessLegends.getApplication().reconnectUIHooks();
}
getVideo().resume();
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
}
try {
// 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);
} catch (NoSuchMethodException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
if (enableHints) {
enableHints();
} else {
disableHints();
}
getMemory().configureActiveMemory();
if (cheatEngine.getValue() == null) {
if (activeCheatEngine != null) {
activeCheatEngine.detach();
motherboard.miscDevices.remove(activeCheatEngine);
}
activeCheatEngine = null;
} else {
boolean startCheats = true;
if (activeCheatEngine != null) {
if (activeCheatEngine.getClass().equals(cheatEngine.getValue())) {
startCheats = false;
} else {
activeCheatEngine.detach();
activeCheatEngine = null;
motherboard.miscDevices.remove(activeCheatEngine);
}
}
if (startCheats) {
try {
activeCheatEngine = (Cheats) cheatEngine.getValue().getConstructor(Computer.class).newInstance(this);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
activeCheatEngine.attach();
motherboard.miscDevices.add(activeCheatEngine);
}
}
} catch (IOException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
if (restart) {
resume();
}
}
@Override
protected void doPause() {
if (motherboard == null) {
return;
}
motherboard.pause();
}
@Override
protected void doResume() {
if (motherboard == null) {
return;
}
motherboard.resume();
}
// public boolean isRunning() {
// if (motherboard == null) {
// return false;
// }
// return motherboard.isRunning() && !motherboard.isPaused;
// }
private List<RAMListener> hints = new ArrayList<>();
ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1);
Runnable drawHints = () -> {
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",
"",
"To insert a disk, please drag it over",
"this window and drop on the desired",
"drive icon.",
"",
"Press CTRL+SHIFT+C for configuration.",
"Press CTRL+SHIFT+I for IDE window.",
"",
"O-A: Alt/Option",
"C-A: Shortcut/Command",
"Reset: Delete/Backspace"
}) {
int addr = 0x0401 + VideoDHGR.calculateTextOffset(row++);
for (char c : s.toCharArray()) {
getMemory().write(addr++, (byte) (c | 0x080), false, true);
}
}
};
int animAddr, animCycleNumber;
byte animOldValue;
final String animation = "+xX*+-";
ScheduledFuture animationSchedule;
Runnable doAnimation = () -> {
if (animAddr == 0 || animCycleNumber >= animation.length()) {
if (animAddr > 0) {
getMemory().write(animAddr, animOldValue, true, true);
}
int animX = (int) (Math.random() * 24.0) + 7;
int animY = (int) (Math.random() * 3.0) + 3;
animAddr = 0x0400 + VideoDHGR.calculateTextOffset(animY) + animX;
animOldValue = getMemory().readRaw(animAddr);
animCycleNumber = 0;
}
if (getCpu().getProgramCounter() >> 8 == 0x0c6) {
getMemory().write(animAddr, (byte) (animation.charAt(animCycleNumber) | 0x080), true, true);
animCycleNumber++;
} else {
getMemory().write(animAddr, animOldValue, true, true);
animationSchedule.cancel(false);
animAddr = 0;
}
};
private void enableHints() {
if (hints.isEmpty()) {
hints.add(getMemory().observe(RAMEvent.TYPE.EXECUTE, 0x0FB63, (e)->{
animationTimer.schedule(drawHints, 1, TimeUnit.SECONDS);
animationSchedule =
animationTimer.scheduleAtFixedRate(doAnimation, 1250, 100, TimeUnit.MILLISECONDS);
}));
// 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);
}
});
*/
}
}
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,364 @@
/*
* 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.CPU;
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;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 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 {
Logger LOG = Logger.getLogger(RAM128k.class.getName());
Map<String, PagedMemory> banks;
private Map<String, PagedMemory> getBanks() {
if (banks == null) {
banks = new HashMap<>();
banks.put("main", mainMemory);
banks.put("lc", languageCard);
banks.put("lc2", languageCard2);
banks.put("//e rom (80-col)", cPageRom);
banks.put("//e rom", rom);
banks.put("blank", blank);
banks.put("aux", getAuxMemory());
banks.put("aux lc", getAuxLanguageCard());
banks.put("aux lc2", getAuxLanguageCard2());
cards[1].ifPresent(c -> banks.put("card1a", c.getCxRom()));
cards[1].ifPresent(c -> banks.put("card1b", c.getC8Rom()));
cards[2].ifPresent(c -> banks.put("card2a", c.getCxRom()));
cards[2].ifPresent(c -> banks.put("card2b", c.getC8Rom()));
cards[3].ifPresent(c -> banks.put("card3a", c.getCxRom()));
cards[3].ifPresent(c -> banks.put("card3b", c.getC8Rom()));
cards[4].ifPresent(c -> banks.put("card4a", c.getCxRom()));
cards[4].ifPresent(c -> banks.put("card4b", c.getC8Rom()));
cards[5].ifPresent(c -> banks.put("card5a", c.getCxRom()));
cards[5].ifPresent(c -> banks.put("card5b", c.getC8Rom()));
cards[6].ifPresent(c -> banks.put("card6a", c.getCxRom()));
cards[6].ifPresent(c -> banks.put("card6b", c.getC8Rom()));
cards[7].ifPresent(c -> banks.put("card7a", c.getCxRom()));
cards[7].ifPresent(c -> banks.put("card7b", c.getC8Rom()));
}
return banks;
}
@Override
public void performExtendedCommand(int param) {
switch (param) {
case 0xda:
// 64 da : Dump all memory mappings
System.out.println("Active banks");
for (int i = 0; i < 256; i++) {
byte[] read = activeRead.get(i);
byte[] write = activeWrite.get(i);
String readBank = getBanks().keySet().stream().filter(bank->{
PagedMemory mem = getBanks().get(bank);
for (byte[] page : mem.getMemory()) {
if (page == read) {
return true;
}
}
return false;
}).findFirst().orElse("unknown");
String writeBank = getBanks().keySet().stream().filter(bank->{
PagedMemory mem = getBanks().get(bank);
for (byte[] page : mem.getMemory()) {
if (page == write) {
return true;
}
}
return false;
}).findFirst().orElse("unknown");
LOG.log(Level.INFO,"Bank {0}\t{1}\t{2}", new Object[]{Integer.toHexString(i), readBank, writeBank});
}
default:
}
}
@Stateful
public PagedMemory mainMemory;
@Stateful
public PagedMemory languageCard;
@Stateful
public PagedMemory languageCard2;
public PagedMemory cPageRom;
public PagedMemory rom;
public PagedMemory blank;
public RAM128k(Computer computer) {
super(computer);
mainMemory = new PagedMemory(0xc000, PagedMemory.Type.RAM, computer);
rom = new PagedMemory(0x3000, PagedMemory.Type.FIRMWARE_MAIN, computer);
cPageRom = new PagedMemory(0x1000, PagedMemory.Type.SLOW_ROM, computer);
languageCard = new PagedMemory(0x3000, PagedMemory.Type.LANGUAGE_CARD, computer);
languageCard2 = new PagedMemory(0x1000, PagedMemory.Type.LANGUAGE_CARD, computer);
activeRead = new PagedMemory(0x10000, PagedMemory.Type.RAM, computer);
activeWrite = new PagedMemory(0x10000, PagedMemory.Type.RAM, computer);
blank = new PagedMemory(0x100, PagedMemory.Type.RAM, computer);
// 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;
}
}
}
private final Semaphore configurationSemaphone = new Semaphore(1, true);
/**
*
*/
@Override
public void configureActiveMemory() {
try {
log("MMU Switches");
configurationSemaphone.acquire();
// 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.isOn()) {
if (SoftSwitches.AUXZP.isOff()) {
activeRead.fillBanks(languageCard);
if (SoftSwitches.LCBANK1.isOff()) {
activeRead.fillBanks(languageCard2);
}
} else {
activeRead.fillBanks(getAuxLanguageCard());
if (SoftSwitches.LCBANK1.isOff()) {
activeRead.fillBanks(getAuxLanguageCard2());
}
}
}
if (SoftSwitches.LCWRITE.isOn()) {
if (SoftSwitches.AUXZP.isOff()) {
activeWrite.fillBanks(languageCard);
if (SoftSwitches.LCBANK1.isOff()) {
activeWrite.fillBanks(languageCard2);
}
} else {
activeWrite.fillBanks(getAuxLanguageCard());
if (SoftSwitches.LCBANK1.isOff()) {
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++) {
PagedMemory page = getCard(slot).map(Card::getCxRom).orElse(blank);
activeRead.setBanks(0, 1, 0x0c0 + slot, page);
}
if (getActiveSlot() == 0) {
for (int i = 0x0C8; i < 0x0D0; i++) {
activeRead.set(i, blank.get(0));
}
} else {
getCard(getActiveSlot()).ifPresent(c -> activeRead.setBanks(0, 8, 0x0c8, c.getC8Rom()));
}
if (SoftSwitches.SLOTC3ROM.isOff()) {
// Enable C3 to point to internal ROM
activeRead.setBanks(2, 1, 0x0C3, cPageRom);
}
if (SoftSwitches.INTC8ROM.isOn()) {
// 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));
configurationSemaphone.release();
} catch (InterruptedException ex) {
Logger.getLogger(RAM128k.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void log(String message) {
CPU cpu = computer.getCpu();
if (cpu != null && cpu.isLogEnabled()) {
String stack = "";
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
stack += e.getClassName() + "." + e.getMethodName() + "(" + e.getLineNumber() + ");";
}
cpu.log(stack);
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,194 @@
/*
* 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.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){
@Override
public void stateChanged() {
super.stateChanged();
computer.getVideo().forceRefresh();
}
}),
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 == null) {
// return;
// }
// if (computer == null && computer.getMemory() == null) {
// return;
// }
// if (computer == null && computer.getVideo() == null) {
// return;
// }
// PAGE2 is a hybrid switch; 80STORE ? memory : video
if (_80STORE.isOn()) {
computer.getMemory().configureActiveMemory();
} else {
computer.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 SoftSwitch("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) {
@Override
protected byte readSwitch() {
setState(true);
return computer.getVideo().getFloatingBus();
}
@Override
public void stateChanged() {
}
}),
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() {
return computer.getVideo().getFloatingBus();
}
@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.getVideo() == null) {
return 0;
}
return computer.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,315 @@
/*
* 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.LawlessLegends;
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.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.stage.FileChooser;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
/**
* 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 {
FileChooser fileChooser = new FileChooser();
File f = fileChooser.showSaveDialog(LawlessLegends.getApplication().primaryStage);
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) (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;
/**
* 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 = 2000000;
/**
* 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[] primaryBuffer;
byte[] secondaryBuffer;
int bufferPos = 0;
Timer playbackTimer;
private final double TICKS_PER_SAMPLE = ((double) Motherboard.SPEED) / SoundMixer.RATE;
private final double TICKS_PER_SAMPLE_FLOOR = Math.floor(TICKS_PER_SAMPLE);
private RAMListener listener = null;
/**
* Creates a new instance of Speaker
*
* @param computer
*/
public Speaker(Computer computer) {
super(computer);
}
/**
* Suspend playback of sound
*
* @return
*/
@Override
public boolean suspend() {
boolean result = super.suspend();
playbackTimer.cancel();
speakerBit = false;
sdl = null;
computer.getMotherboard().cancelSpeedRequest(this);
computer.mixer.returnLine(this);
return result;
}
/**
* Start or resume playback of sound
*/
@Override
public void resume() {
if (sdl != null && isRunning()) {
return;
}
try {
if (sdl == null || !sdl.isOpen()) {
sdl = computer.mixer.getLine(this);
}
sdl.start();
setRun(true);
counter = 0;
idleCycles = 0;
level = 0;
bufferPos = 0;
playbackTimer = new Timer();
playbackTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
playCurrentBuffer();
}
}, 10, 30);
} catch (LineUnavailableException ex) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, "ERROR: Could not output sound", ex);
}
}
public void playCurrentBuffer() {
byte[] buffer;
int len;
synchronized (bufferLock) {
len = bufferPos;
buffer = primaryBuffer;
primaryBuffer = secondaryBuffer;
bufferPos = 0;
}
secondaryBuffer = buffer;
sdl.write(buffer, 0, len);
}
/**
* 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() || sdl == 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;
while (bufferPos >= primaryBuffer.length) {
Thread.yield();
}
synchronized (bufferLock) {
int index = bufferPos;
for (int i = 0; i < SoundMixer.BITS; i += 8, index++) {
shift -= 8;
primaryBuffer[index] = primaryBuffer[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;
}
}
private void toggleSpeaker(RAMEvent e) {
if (e.getType() == RAMEvent.TYPE.WRITE) {
level += 2;
} else {
speakerBit = !speakerBit;
}
resetIdle();
}
/**
* Add a memory event listener for C03x for capturing speaker events
*/
private void configureListener() {
listener = computer.getMemory().observe(RAMEvent.TYPE.ANY, 0x0c030, 0x0c03f, this::toggleSpeaker);
}
private void removeListener() {
computer.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 (primaryBuffer != null && secondaryBuffer != null) {
return;
}
BUFFER_SIZE = 20000 * (SoundMixer.BITS >> 3);
primaryBuffer = new byte[BUFFER_SIZE];
secondaryBuffer = new byte[BUFFER_SIZE];
}
@Override
public void attach() {
configureListener();
resume();
}
@Override
public void detach() {
removeListener();
suspend();
super.detach();
}
}

View File

@ -0,0 +1,733 @@
/*
* 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.Video;
import static jace.core.Video.hiresOffset;
import static jace.core.Video.hiresRowLookup;
import static jace.core.Video.textRowLookup;
import jace.core.VideoWriter;
import java.util.logging.Logger;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
/**
* 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[] FLIP_NYBBLE = {
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 final VideoWriter mixed;
private VideoWriter currentGraphicsWriter = null;
private VideoWriter currentTextWriter = null;
/**
* Creates a new instance of VideoDHGR
*
* @param computer
*/
public VideoDHGR(Computer computer) {
super(computer);
hiresPage1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
return (hiresOffset[y] + 0x02000);
}
@Override
public void displayByte(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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(WritableImage 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.getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset );
int b2 = ((RAM128k) computer.getMemory()).getMainMemory() .readByte(rowAddress + xOffset );
int b3 = ((RAM128k) computer.getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1);
int b4 = ((RAM128k) computer.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, TIMES_14[xOffset], y, dhgrWord);
}
boolean extraHalfBit = false;
protected void displayHires(WritableImage 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.getMemory()).getMainMemory().readByte(rowAddress + xOffset);
int b2 = 0x0ff & ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1);
int dhgrWord = HGR_TO_DHGR[(extraHalfBit && xOffset > 0) ? b1 | 0x0100 : b1][b2];
extraHalfBit = (dhgrWord & 0x10000000) != 0;
showDhgr(screen, TIMES_14[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[][] HGR_TO_DHGR;
// Take two consecutive bytes and double them, disregarding hi-bit
// Useful for text mode
static final int[][] HGR_TO_DHGR_BW;
static final int[] TIMES_14;
static final int[] FLIP_BITS;
static {
// complete reverse of 8 bits
FLIP_BITS = new int[256];
for (int i = 0; i < 256; i++) {
FLIP_BITS[i] = (((i * 0x0802 & 0x22110) | (i * 0x8020 & 0x88440)) * 0x10101 >> 16) & 0x0ff;
}
TIMES_14 = new int[40];
for (int i = 0; i < 40; i++) {
TIMES_14[i] = i * 14;
}
HGR_TO_DHGR = new int[512][256];
HGR_TO_DHGR_BW = 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;
}
HGR_TO_DHGR[bb1][bb2] = value;
HGR_TO_DHGR_BW[bb1 & 0x0ff][bb2]
= byteDoubler((byte) bb1) | (byteDoubler((byte) bb2) << 14);
}
}
}
protected void displayLores(WritableImage screen, int xOffset, int y, int rowAddress) {
int c1 = ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
} else {
c1 >>= 4;
}
Color color = Palette.color[c1];
// Unrolled loop, faster
PixelWriter writer = screen.getPixelWriter();
int xx = xOffset * 14;
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
}
protected void displayDoubleLores(WritableImage screen, int xOffset, int y, int rowAddress) {
int c1 = ((RAM128k) computer.getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset) & 0x0FF;
int c2 = ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
c2 &= 15;
} else {
c1 >>= 4;
c2 >>= 4;
}
PixelWriter writer = screen.getPixelWriter();
// int yOffset = xyOffset[y][times14[xOffset]];
Color color = Palette.color[FLIP_NYBBLE[c1]];
// Unrolled loop, faster
int xx = xOffset * 14;
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
color = Palette.color[c2];
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
writer.setColor(xx++, y, color);
}
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(WritableImage 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.getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1);
int c1 = getFontChar(((RAM128k) computer.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 = HGR_TO_DHGR_BW[b1][b2];
showBW(screen, TIMES_14[xOffset], y, out);
}
protected void displayText80(WritableImage 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.getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset));
int c2 = getFontChar(((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset));
int c3 = getFontChar(((RAM128k) computer.getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1));
int c4 = getFontChar(((RAM128k) computer.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, TIMES_14[xOffset], y, bits);
}
private void displayMixed(WritableImage 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(WritableImage screen, int xOffset, int y, int dhgrWord) {
PixelWriter writer = screen.getPixelWriter();
try {
for (int i = 0; i < 7; i++) {
Color color;
if (!dhgrMode && hiresMode) {
color = Palette.color[dhgrWord & 15];
} else {
color = Palette.color[FLIP_NYBBLE[dhgrWord & 15]];
}
writer.setColor(xOffset++, y, color);
writer.setColor(xOffset++, y, color);
writer.setColor(xOffset++, y, color);
writer.setColor(xOffset++, y, color);
dhgrWord >>= 4;
}
} catch (ArrayIndexOutOfBoundsException ex) {
Logger.getLogger(getClass().getName()).warning("Went out of bounds in video display");
}
}
static final Color BLACK = Color.BLACK;
static Color WHITE = Color.WHITE;
static final int[][] XY_OFFSET;
static {
XY_OFFSET = new int[192][560];
for (int y = 0; y < 192; y++) {
for (int x = 0; x < 560; x++) {
XY_OFFSET[y][x] = y * 560 + x;
}
}
}
protected void showBW(WritableImage screen, int xOffset, int y, int dhgrWord) {
// 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!
// 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;
int xx = xOffset;
PixelWriter writer = screen.getPixelWriter();
for (int i = 0; i < 28; i++) {
if (xx < 560) {
// yOffset++ is used instead of yOffset+i, because it is faster
writer.setColor(xx++, y, (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 registerTextDirtyFlag(RAMEvent e) {
int row = textRowLookup[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);
}
}
private void registerHiresDirtyFlag(RAMEvent e) {
int row = hiresRowLookup[e.getAddress() & 0x01fff];
if (row < 0 || row >= 192) {
return;
}
VideoWriter mark = (e.getAddress() < 0x04000) ? hiresPage1 : hiresPage2;
mark.markDirty(row);
}
private void registerDirtyFlagChecks() {
computer.getMemory().observe(RAMEvent.TYPE.WRITE, 0x0400, 0x0bff, this::registerTextDirtyFlag);
computer.getMemory().observe(RAMEvent.TYPE.WRITE, 0x02000, 0x05fff, this::registerHiresDirtyFlag);
}
@Override
public void reconfigure() {
// Do nothing (for now)
}
@Override
public void attach() {
// Do nothing
}
@Override
public void hblankStart(WritableImage screen, int y, boolean isDirty) {
// Do nothing
}
}

View File

@ -0,0 +1,454 @@
/*
* 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.EmulatorUILogic;
import static jace.apple2e.VideoDHGR.BLACK;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
/**
* 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 boolean useTextPalette = true;
int activePalette[][] = TEXT_PALETTE;
@ConfigurableField(name = "Video 7", shortName = "video7", defaultValue = "true", description = "Enable Video 7 RGB rendering support")
public boolean enableVideo7 = true;
// Scanline represents 560 bits, divided up into 28-bit words
int[] scanline = new int[20];
static public int[] divBy28 = new int[560];
static {
for (int i = 0; i < 560; i++) {
divBy28[i] = i / 28;
}
}
protected boolean[] colorActive = new boolean[80];
int rowStart = 0;
public VideoNTSC(Computer computer) {
super(computer);
registerStateListeners();
}
public static enum VideoMode {
Color("Color"),
TextFriendly("Text-friendly color"),
Mode7("Mode7 Mixed RGB"),
Mode7TextFriendly("Mode7 with Text-friendly palette"),
Monochrome("Mono"),
Greenscreen("Green"),
Amber("Amber");
String name;
VideoMode(String n) {
name = n;
}
}
static int currentMode = -1;
@InvokableAction(name = "Toggle video mode",
category = "video",
alternatives = "mode,color,b&w,monochrome",
defaultKeyMapping = {"ctrl+shift+g"})
public static void changeVideoMode() {
VideoNTSC thiss = (VideoNTSC) Emulator.computer.video;
currentMode++;
if (currentMode >= VideoMode.values().length) {
currentMode = 0;
}
thiss.monochomeMode = false;
WHITE = Color.WHITE;
switch (VideoMode.values()[currentMode]) {
case Amber:
thiss.monochomeMode = true;
WHITE = Color.web("ff8000");
break;
case Greenscreen:
thiss.monochomeMode = true;
WHITE = Color.web("0ccc68");
break;
case Monochrome:
thiss.monochomeMode = true;
break;
case Color:
thiss.useTextPalette = false;
thiss.enableVideo7 = false;
break;
case Mode7:
thiss.useTextPalette = false;
thiss.enableVideo7 = true;
break;
case Mode7TextFriendly:
thiss.useTextPalette = true;
thiss.enableVideo7 = true;
break;
case TextFriendly:
thiss.useTextPalette = true;
thiss.enableVideo7 = false;
break;
}
thiss.activePalette = thiss.useTextPalette ? TEXT_PALETTE : SOLID_PALETTE;
EmulatorUILogic.notify("Video mode: "+VideoMode.values()[currentMode].name);
forceRefresh();
}
@Override
protected void showBW(WritableImage screen, int x, int y, int dhgrWord) {
int pos = divBy28[x];
if (rowStart < 0) {
rowStart = pos;
}
colorActive[pos * 4] = colorActive[pos * 4 + 1] = colorActive[pos * 4 + 2] = colorActive[pos * 4 + 3] = false;
scanline[pos] = dhgrWord;
}
@Override
protected void showDhgr(WritableImage screen, int x, int y, int dhgrWord) {
int pos = divBy28[x];
if (rowStart < 0) {
rowStart = pos;
}
colorActive[pos * 4] = colorActive[pos * 4 + 1] = colorActive[pos * 4 + 2] = colorActive[pos * 4 + 3] = true;
scanline[pos] = dhgrWord;
}
@Override
protected void displayLores(WritableImage screen, int xOffset, int y, int rowAddress) {
int data = ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
int pos = xOffset >> 1;
if (rowStart < 0) {
rowStart = pos;
}
colorActive[xOffset * 2] = true;
colorActive[xOffset * 2 + 1] = true;
if ((xOffset & 1) == 0) {
int pat = scanline[pos] & 0x0fffc000;
if ((y & 7) < 4) {
data &= 15;
} else {
data >>= 4;
}
pat |= data | data << 4 | data << 8 | (data & 3) << 12;
scanline[pos] = pat;
} else {
int pat = scanline[pos] & 0x03fff;
if ((y & 7) < 4) {
data &= 15;
} else {
data >>= 4;
}
pat |= (data & 12) << 12 | data << 16 | data << 20 | data << 24;
scanline[pos] = pat;
}
}
@Override
protected void displayDoubleLores(WritableImage screen, int xOffset, int y, int rowAddress) {
int pos = xOffset >> 1;
if (rowStart < 0) {
rowStart = pos;
}
colorActive[xOffset * 2] = colorActive[xOffset * 2 + 1] = true;
int c1 = ((RAM128k) computer.getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
} else {
c1 >>= 4;
}
int c2 = ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c2 &= 15;
} else {
c2 >>= 4;
}
if ((xOffset & 0x01) == 0) {
int pat = c1 | (c1 & 7) << 4;
pat |= c2 << 7 | (c2 & 7) << 11;
scanline[pos] = pat;
} else {
int pat = scanline[pos];
pat |= (c1 & 12) << 12 | c1 << 16 | (c1 & 1) << 20;
pat |= (c2 & 12) << 19 | c2 << 23 | (c2 & 1) << 27;
scanline[pos] = pat;
}
}
@Override
public void hblankStart(WritableImage screen, int y, boolean isDirty) {
if (isDirty) {
renderScanline(screen, y);
}
}
// 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);
}
}
}
boolean monochomeMode = false;
private void renderScanline(WritableImage screen, int y) {
int p = 0;
if (rowStart != 0) {
// getCurrentWriter().markDirty(y);
p = rowStart * 28;
if (rowStart < 0) {
return;
}
}
PixelWriter writer = screen.getPixelWriter();
// Reset scanline position
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;
boolean mixed = enableVideo7 && dhgrMode && graphicsMode == rgbMode.MIX;
for (int i = 0; i < 28; i++) {
if (i % 7 == 0) {
isBW = monochomeMode || !colorActive[byteCounter] || (mixed && !hiresMode && !useColor[byteCounter]);
byteCounter++;
}
if (isBW) {
writer.setColor(p++, y, ((bits & 0x8) == 0) ? BLACK : WHITE);
} else {
writer.setArgb(p++, y, activePalette[i % 4][bits & 0x07f]);
}
bits >>= 1;
if (i == 20) {
bits |= add << (hiresMode ? 9 : 10);
}
}
// } else {
// for (int i = 0; i < 28; i++) {
// writer.setArgb(p++, y, activePalette[i % 4][bits & 0x07f]);
// bits >>= 1;
// if (i == 20) {
// bits |= add << (hiresMode ? 9 : 10);
// }
// }
// }
}
Arrays.fill(scanline, 0);
rowStart = -1;
}
// 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 SOLID_PALETTE[][] = new int[4][128];
static final int[][] TEXT_PALETTE = new int[4][128];
static final double[][] YIQ_VALUES = {
{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_VALUES[col][0];
double y2 = (level / (double) maxLevel);
SOLID_PALETTE[offset][pattern] = yiqToRgb(y1, YIQ_VALUES[col][1] * MAX_I, YIQ_VALUES[col][2] * MAX_Q);
TEXT_PALETTE[offset][pattern] = yiqToRgb(y2, YIQ_VALUES[col][1] * MAX_I, YIQ_VALUES[col][2] * MAX_Q);
}
}
}
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 (255 << 24) | (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() {
activePalette = useTextPalette ? TEXT_PALETTE : SOLID_PALETTE;
super.reconfigure();
}
// 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.MIX;
public static enum rgbMode {
COLOR(true), MIX(true), BW(false), COL_160(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 = false;
public void rgbStateChange() {
// 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);
}
// These catch changes to the RGB mode to toggle between color, BW and mixed
Set<RAMListener> rgbStateListeners = new HashSet<>();
private void registerStateListeners() {
if (!rgbStateListeners.isEmpty() || computer.getVideo() != this) {
return;
}
RAM memory = computer.getMemory();
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.ANY, 0x0c05e, (e) -> {
an3 = false;
rgbStateChange();
}));
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.ANY, 0x0c05f, (e) -> {
if (!an3) {
f2 = f1;
f1 = SoftSwitches._80COL.getState();
}
an3 = true;
rgbStateChange();
}));
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.EXECUTE, 0x0fa62, (e) -> {
// 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.
f1 = true;
f2 = true;
an3 = false;
graphicsMode = rgbMode.COLOR;
rgbStateChange();
}));
}
@Override
public void detach() {
rgbStateListeners.stream().forEach((l) -> {
computer.getMemory().removeListener(l);
});
rgbStateListeners.clear();
super.detach();
}
@Override
public void attach() {
super.attach();
registerStateListeners();
}
}

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.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) {
@Override
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.getMemory() != null) {
computer.getMemory().configureActiveMemory();
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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);
}
@Override
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,58 @@
/*
* 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;
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);
}
@Override
public void stateChanged() {
// System.out.println(getName()+ " was switched to "+getState());
if (computer.getMemory() != null) {
computer.getMemory().configureActiveMemory();
}
}
// Todo: Implement floating bus, maybe?
@Override
protected byte readSwitch() {
byte value = computer.getVideo().getFloatingBus();
if (getState()) {
return (byte) (value | 0x080);
} else {
return (byte) (value & 0x07f);
}
}
}

View File

@ -0,0 +1,53 @@
/*
* 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;
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);
}
@Override
public void stateChanged() {
// System.out.println("Set "+getName()+" -> "+getState());
if (computer.getVideo() != null) {
computer.getVideo().configureVideoMode();
}
}
@Override
protected byte readSwitch() {
// System.out.println("Read "+getName()+" = "+getState());
return (byte) (getState() ? 0x080 : 0x000);
}
}

View File

@ -0,0 +1,72 @@
package jace.applesoft;
import jace.Emulator;
import jace.ide.Program;
import jace.ide.CompileResult;
import jace.ide.LanguageHandler;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
*
* @author blurry
*/
public class ApplesoftHandler implements LanguageHandler<ApplesoftProgram> {
@Override
public String getNewDocumentContent() {
return ApplesoftProgram.fromMemory(Emulator.computer.getMemory()).toString();
}
@Override
public CompileResult<ApplesoftProgram> compile(Program program) {
final ApplesoftProgram result = ApplesoftProgram.fromString(program.getValue());
final Map<Integer, String> warnings = new LinkedHashMap<>();
// int lineNumber = 1;
// for (Line l : result.lines) {
// warnings.put(lineNumber++, l.toString());
// }
return new CompileResult<ApplesoftProgram>() {
@Override
public boolean isSuccessful() {
return result != null;
}
@Override
public ApplesoftProgram getCompiledAsset() {
return result;
}
@Override
public Map<Integer, String> getErrors() {
return Collections.EMPTY_MAP;
}
@Override
public Map<Integer, String> getWarnings() {
return warnings;
}
@Override
public List<String> getOtherMessages() {
return Collections.EMPTY_LIST;
}
@Override
public List<String> getRawOutput() {
return Collections.EMPTY_LIST;
}
};
}
@Override
public void execute(CompileResult<ApplesoftProgram> lastResult) {
lastResult.getCompiledAsset().run();
}
@Override
public void clean(CompileResult<ApplesoftProgram> lastResult) {
}
}

View File

@ -0,0 +1,257 @@
/*
* 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 jace.Emulator;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* Decode an applesoft program into a list of program lines Right now this is an
* example/test program but it successfully tokenized the source of Lemonade
* Stand.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class ApplesoftProgram {
List<Line> lines = new ArrayList<>();
public static final int START_OF_PROG_POINTER = 0x067;
public static final int END_OF_PROG_POINTER = 0x0AF;
public static final int VARIABLE_TABLE = 0x069;
public static final int ARRAY_TABLE = 0x06b;
public static final int VARIABLE_TABLE_END = 0x06d;
public static final int STRING_TABLE = 0x06f;
public static final int HIMEM = 0x073;
public static final int BASIC_RUN = 0x0e000;
public static final int RUNNING_FLAG = 0x076;
public static final int NOT_RUNNING = 0x0FF;
public static final int GOTO_CMD = 0x0D944; //actually starts at D93E
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(ApplesoftProgram.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(ApplesoftProgram.class.getName()).log(Level.SEVERE, null, ex);
}
ApplesoftProgram test = ApplesoftProgram.fromBinary(Arrays.asList(toObjects(source)));
System.out.println(test.toString());
}
public static Byte[] toObjects(byte[] bytesPrim) {
Byte[] bytes = new Byte[bytesPrim.length];
Arrays.setAll(bytes, n -> bytesPrim[n]);
return bytes;
}
public static ApplesoftProgram fromMemory(RAM memory) {
int startAddress = memory.readWordRaw(START_OF_PROG_POINTER);
int nextCheck = memory.readWordRaw(startAddress);
int pos = startAddress;
List<Byte> bytes = new ArrayList<>();
while (nextCheck != 0) {
while (pos < nextCheck + 2) {
bytes.add(memory.readRaw(pos++));
}
nextCheck = memory.readWordRaw(nextCheck);
}
return fromBinary(bytes, startAddress);
}
public static ApplesoftProgram fromBinary(List<Byte> binary) {
return fromBinary(binary, 0x0801);
}
public static ApplesoftProgram fromBinary(List<Byte> binary, int startAddress) {
ApplesoftProgram program = new ApplesoftProgram();
int currentAddress = startAddress;
int pos = 0;
while (pos < binary.size()) {
int nextAddress = (binary.get(pos) & 0x0ff) + ((binary.get(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 = "";
out = lines.stream().map((l) -> l.toString() + "\n").reduce(out, String::concat);
return out;
}
public static ApplesoftProgram fromString(String programSource) {
ApplesoftProgram program = new ApplesoftProgram();
for (String line : programSource.split("\\n")) {
if (line.trim().isEmpty()) {
continue;
}
program.lines.add(Line.fromString(line));
}
//correct line linkage
for (int i = 0; i < program.lines.size(); i++) {
if (i > 0) {
program.lines.get(i).setPrevious(program.lines.get(i - 1));
}
if (i < program.lines.size() - 1) {
program.lines.get(i).setNext(program.lines.get(i + 1));
}
}
return program;
}
public void run() {
RAM memory = Emulator.computer.memory;
Emulator.computer.pause();
int programStart = memory.readWordRaw(START_OF_PROG_POINTER);
int programEnd = programStart + getProgramSize();
if (isProgramRunning()) {
whenReady(()->{
relocateVariables(programEnd);
injectProgram();
});
} else {
injectProgram();
clearVariables(programEnd);
}
Emulator.computer.resume();
}
private void injectProgram() {
RAM memory = Emulator.computer.memory;
int pos = memory.readWordRaw(START_OF_PROG_POINTER);
for (Line line : lines) {
int nextPos = pos + line.getLength();
memory.writeWord(pos, nextPos, false, true);
pos += 2;
memory.writeWord(pos, line.getNumber(), false, true);
pos += 2;
boolean isFirst = true;
for (Command command : line.getCommands()) {
if (!isFirst) {
memory.write(pos++, (byte) ':', false, true);
}
isFirst = false;
for (Command.ByteOrToken part : command.parts) {
memory.write(pos++, part.getByte(), false, true);
}
}
memory.write(pos++, (byte) 0, false, true);
}
memory.write(pos++, (byte) 0, false, true);
memory.write(pos++, (byte) 0, false, true);
memory.write(pos++, (byte) 0, false, true);
memory.write(pos++, (byte) 0, false, true);
}
private boolean isProgramRunning() {
RAM memory = Emulator.computer.memory;
return (memory.readRaw(RUNNING_FLAG) & 0x0FF) != NOT_RUNNING;
}
/**
* If the program is running, wait until it advances to the next line
*/
private void whenReady(Runnable r) {
RAM memory = Emulator.computer.memory;
memory.addListener(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(GOTO_CMD);
}
@Override
protected void doEvent(RAMEvent e) {
r.run();
memory.removeListener(this);
}
});
}
/**
* Rough approximation of the CLEAR command at $D66A.
* http://www.txbobsc.com/scsc/scdocumentor/D52C.html
* @param programEnd Program ending address
*/
private void clearVariables(int programEnd) {
RAM memory = Emulator.computer.memory;
memory.writeWord(ARRAY_TABLE, programEnd, false, true);
memory.writeWord(VARIABLE_TABLE, programEnd, false, true);
memory.writeWord(VARIABLE_TABLE_END, programEnd, false, true);
memory.writeWord(END_OF_PROG_POINTER, programEnd, false, true);
}
/**
* Move variables around to accommodate bigger program
* @param programEnd Program ending address
*/
private void relocateVariables(int programEnd) {
RAM memory = Emulator.computer.memory;
int currentEnd = memory.readWordRaw(END_OF_PROG_POINTER);
memory.writeWord(END_OF_PROG_POINTER, programEnd, false, true);
if (programEnd > currentEnd) {
int diff = programEnd - currentEnd;
int himem = memory.readWordRaw(HIMEM);
for (int i=himem - diff; i >= programEnd; i--) {
memory.write(i+diff, memory.readRaw(i), false, true);
}
memory.writeWord(VARIABLE_TABLE, memory.readWordRaw(VARIABLE_TABLE) + diff, false, true);
memory.writeWord(ARRAY_TABLE, memory.readWordRaw(ARRAY_TABLE) + diff, false, true);
memory.writeWord(VARIABLE_TABLE_END, memory.readWordRaw(VARIABLE_TABLE_END) + diff, false, true);
memory.writeWord(STRING_TABLE, memory.readWordRaw(STRING_TABLE) + diff, false, true);
}
}
private int getProgramSize() {
int size = lines.stream().collect(Collectors.summingInt(Line::getLength)) + 4;
return size;
}
}

View File

@ -0,0 +1,230 @@
/*
* 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;
import java.util.stream.Collectors;
/**
* 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$");
static TOKEN findMatch(String search, int start) {
for (TOKEN t : values()) {
int i = start;
boolean found = true;
for (int j = 0; j < t.toString().length(); j++) {
while (i+j < search.length() && search.charAt(j + i) == ' ') {
i++;
}
if (i + j >= search.length()
|| (search.charAt(i + j) != t.toString().charAt(j))) {
found = false;
break;
}
}
if (found) {
return t;
}
}
return null;
}
private String str;
public byte code;
TOKEN(byte b, String str) {
this.code = b;
this.str = str;
}
@Override
public String toString() {
return str;
}
public static TOKEN fromByte(byte b) {
for (TOKEN t : values()) {
if (t.code == 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;
}
}
public ByteOrToken(TOKEN token) {
isToken = true;
this.t = token;
}
@Override
public String toString() {
return isToken
? " " + t.toString() + " "
: String.valueOf((char) b);
}
public byte getByte() {
if (isToken) {
return t.code;
} else {
return b;
}
}
}
List<ByteOrToken> parts = new ArrayList<>();
@Override
public String toString() {
return parts.stream().map(ByteOrToken::toString).collect(Collectors.joining());
}
}

View File

@ -0,0 +1,214 @@
/*
* 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 jace.applesoft.Command.TOKEN;
import static java.lang.Character.isDigit;
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 final char STATEMENT_BREAK = ':'; // delimits multiple commands, the colon character
private int number = -1;
private Line next;
private Line previous;
private List<Command> commands = new ArrayList<>();
private int length = 0;
/**
* @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(List<Byte> binary, int pos) {
Line l = new Line();
int lineNumber = (binary.get(pos + 2) & 0x0ff) + ((binary.get(pos + 3) & 0x0ff) << 8);
l.setNumber(lineNumber);
pos += 4;
Command c = new Command();
int size = 5;
while (binary.get(pos) != 0) {
size++;
if (binary.get(pos) == STATEMENT_BREAK) {
l.commands.add(c);
c = new Command();
} else {
Command.ByteOrToken bt = new Command.ByteOrToken(binary.get(pos));
c.parts.add(bt);
}
pos++;
}
l.commands.add(c);
l.length = size;
return l;
}
static Line fromString(String lineString) {
Line l = new Line();
boolean inString = false;
boolean hasLineNumber = false;
boolean isComment = false;
Command currentCommand = new Command();
l.commands.add(currentCommand);
l.length = 5; // 4 pointer bytes + 1 null byte at the end
String upperLineString = lineString.toUpperCase();
for (int i = 0; i < lineString.length(); i++) {
if (!hasLineNumber) {
int lineNumber = 0;
for (; i < lineString.length() && isDigit(lineString.charAt(i)); i++) {
lineNumber = lineNumber * 10 + lineString.charAt(i) - '0';
}
i--;
l.setNumber(lineNumber);
hasLineNumber = true;
} else if (inString || isComment) {
if (!isComment && lineString.charAt(i) == '"') {
inString = false;
}
currentCommand.parts.add(new Command.ByteOrToken((byte) lineString.charAt(i)));
l.length++;
} else if (lineString.charAt(i) == '"') {
inString = true;
currentCommand.parts.add(new Command.ByteOrToken((byte) lineString.charAt(i)));
l.length++;
} else if (lineString.charAt(i) == STATEMENT_BREAK) {
currentCommand = new Command();
l.commands.add(currentCommand);
l.length++;
} else if (lineString.charAt(i) == '?') {
Command.ByteOrToken part = new Command.ByteOrToken(TOKEN.PRINT);
currentCommand.parts.add(part);
l.length++;
} else if (lineString.charAt(i) == ' ') {
continue;
} else {
TOKEN match = Command.TOKEN.findMatch(upperLineString, i);
if (match != null) {
Command.ByteOrToken part = new Command.ByteOrToken(match);
currentCommand.parts.add(part);
if (match == TOKEN.REM || match == TOKEN.DATA) {
isComment = true;
}
for (int j=0; j < match.toString().length(); j++, i++) {
while (i < lineString.length() && lineString.charAt(i) == ' ') {
i++;
}
}
if (!isComment) {
i--;
}
l.length++;
} else {
if (lineString.charAt(i) != ' ') {
currentCommand.parts.add(new Command.ByteOrToken((byte) upperLineString.charAt(i)));
l.length++;
}
}
}
}
return l;
}
}

View File

@ -0,0 +1,161 @@
package jace.assembly;
import jace.ide.CompileResult;
import jace.ide.Program;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
*
* @author blurry
*/
public class AcmeCompiler implements CompileResult<File> {
boolean successful = false;
File compiledAsset = null;
Map<Integer, String> errors = new LinkedHashMap<>();
Map<Integer, String> warnings = new LinkedHashMap<>();
List<String> otherWarnings = new ArrayList<>();
List<String> rawOutput = new ArrayList<>();
@Override
public boolean isSuccessful() {
return successful;
}
@Override
public File getCompiledAsset() {
return compiledAsset;
}
@Override
public Map<Integer, String> getErrors() {
return errors;
}
@Override
public Map<Integer, String> getWarnings() {
return warnings;
}
@Override
public List<String> getOtherMessages() {
return otherWarnings;
}
@Override
public List<String> getRawOutput() {
return rawOutput;
}
public void compile(Program proxy) {
File workingDirectory = proxy.getFile()
.map(file -> file.getParentFile())
.orElse(new File(System.getProperty("user.dir")));
File sourceFile = new File(workingDirectory, "_acme_tmp_" + ((int) (Math.random() * 1024.0)) + ".a");
try (FileWriter writer = new FileWriter(sourceFile)) {
writer.append(proxy.getValue());
writer.flush();
writer.close();
invokeAcme(sourceFile, workingDirectory);
} catch (Exception ex) {
compilationFailure(ex);
} finally {
sourceFile.delete();
}
}
private void compilationFailure(Exception ex) {
Logger.getLogger(AcmeCompiler.class.getName()).log(Level.SEVERE, null, ex);
rawOutput = Arrays.asList(ex.getStackTrace()).stream().map(element -> element.toString()).collect(Collectors.toList());
otherWarnings = rawOutput;
}
PrintStream systemOut = System.out;
PrintStream systemErr = System.err;
ByteArrayOutputStream baosOut;
PrintStream out;
ByteArrayOutputStream baosErr;
PrintStream err;
private String normalizeWindowsPath(String path) {
if (path.contains("\\")) {
char firstLetter = path.toLowerCase().charAt(0);
return "/"+firstLetter+path.substring(1).replaceAll("\\\\", "/");
} else {
return path;
}
}
private void invokeAcme(File sourceFile, File workingDirectory) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IOException {
String oldPath = System.getProperty("user.dir");
redirectSystemOutput();
try {
compiledAsset = File.createTempFile(sourceFile.getName(), "bin", sourceFile.getParentFile());
System.setProperty("user.dir", workingDirectory.getAbsolutePath());
AcmeCrossAssembler acme = new AcmeCrossAssembler();
String[] params = {"--outfile", normalizeWindowsPath(compiledAsset.getAbsolutePath()), "-f", "cbm", "--maxerrors","16",normalizeWindowsPath(sourceFile.getAbsolutePath())};
int status = acme.run("Acme", params);
successful = status == 0;
if (!successful) {
compiledAsset.delete();
compiledAsset = null;
}
} finally {
restoreSystemOutput();
System.setProperty("user.dir", oldPath);
}
rawOutput.add("Error output:");
extractOutput(baosErr.toString());
rawOutput.add("");
rawOutput.add("------------------------------");
rawOutput.add("Standard output:");
extractOutput(baosOut.toString());
}
public void extractOutput(String output) throws NumberFormatException {
for (String line : output.split("\\n")) {
rawOutput.add(line);
int lineNumberStart = line.indexOf(", line") + 6;
if (lineNumberStart > 6) {
int lineNumberEnd = line.indexOf(' ', lineNumberStart+1);
int actualLineNumber = Integer.parseUnsignedInt(line.substring(lineNumberStart, lineNumberEnd).trim());
String message = line.substring(lineNumberEnd).trim();
if (line.startsWith("Error")) {
errors.put(actualLineNumber, message);
} else {
warnings.put(actualLineNumber, message);
}
} else {
if (line.trim().length() > 1) {
otherWarnings.add(line);
}
}
}
}
public void restoreSystemOutput() {
System.setOut(systemOut);
System.setErr(systemErr);
}
public void redirectSystemOutput() {
baosOut = new ByteArrayOutputStream();
out = new PrintStream(baosOut);
baosErr = new ByteArrayOutputStream();
err = new PrintStream(baosErr);
System.setOut(out);
System.setErr(err);
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,66 @@
package jace.assembly;
import jace.Emulator;
import jace.core.RAM;
import jace.ide.CompileResult;
import jace.ide.LanguageHandler;
import jace.ide.Program;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author blurry
*/
public class AssemblyHandler implements LanguageHandler<File> {
@Override
public String getNewDocumentContent() {
return "\t\t*= $300;\n\t\t!cpu 65c02;\n;--- Insert your code here ---\n";
}
@Override
public CompileResult<File> compile(Program proxy) {
AcmeCompiler compiler = new AcmeCompiler();
compiler.compile(proxy);
return compiler;
}
@Override
public void execute(CompileResult<File> lastResult) {
if (lastResult.isSuccessful()) {
try {
boolean resume = false;
if (Emulator.computer.isRunning()) {
resume = true;
Emulator.computer.pause();
}
RAM memory = Emulator.computer.getMemory();
FileInputStream input = new FileInputStream(lastResult.getCompiledAsset());
int startLSB = input.read();
int startMSB = input.read();
int pos = startLSB + startMSB << 8;
Emulator.computer.getCpu().JSR(pos);
int next;
while ((next=input.read()) != -1) {
memory.write(pos++, (byte) next, false, true);
}
if (resume) {
Emulator.computer.resume();
}
} catch (IOException ex) {
Logger.getLogger(AssemblyHandler.class.getName()).log(Level.SEVERE, null, ex);
}
}
clean(lastResult);
}
@Override
public void clean(CompileResult<File> lastResult) {
if (lastResult.getCompiledAsset() != null) {
lastResult.getCompiledAsset().delete();
}
}
}

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.cheat;
import jace.apple2e.MOS65C02;
import jace.config.InvokableAction;
import jace.core.Computer;
import jace.core.Device;
import jace.core.RAMEvent;
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 {
boolean cheatsActive = true;
Set<RAMListener> listeners = new HashSet<>();
public Cheats(Computer computer) {
super(computer);
}
@InvokableAction(name = "Toggle Cheats", alternatives = "cheat", defaultKeyMapping = "ctrl+shift+m")
public void toggleCheats() {
cheatsActive = !cheatsActive;
if (cheatsActive) {
attach();
} else {
detach();
}
}
public RAMListener bypassCode(int address, int addressEnd) {
int noOperation = MOS65C02.COMMAND.NOP.ordinal();
return addCheat(RAMEvent.TYPE.READ, (e) -> e.setNewValue(noOperation), address, addressEnd);
}
public RAMListener forceValue(int value, int... address) {
return addCheat(RAMEvent.TYPE.ANY, (e) -> e.setNewValue(value), address);
}
public RAMListener forceValue(int value, boolean auxFlag, int... address) {
return addCheat(RAMEvent.TYPE.ANY, auxFlag, (e) -> e.setNewValue(value), address);
}
public RAMListener addCheat(RAMEvent.TYPE type, RAMEvent.RAMEventHandler handler, int... address) {
RAMListener listener;
if (address.length == 1) {
listener = computer.getMemory().observe(type, address[0], handler);
} else {
listener = computer.getMemory().observe(type, address[0], address[1], handler);
}
listeners.add(listener);
return listener;
}
public RAMListener addCheat(RAMEvent.TYPE type, boolean auxFlag, RAMEvent.RAMEventHandler handler, int... address) {
RAMListener listener;
if (address.length == 1) {
listener = computer.getMemory().observe(type, address[0], auxFlag, handler);
} else {
listener = computer.getMemory().observe(type, address[0], address[1], auxFlag, handler);
}
listeners.add(listener);
return listener;
}
@Override
public void attach() {
registerListeners();
}
@Override
public void detach() {
unregisterListeners();
super.detach();
}
public abstract void registerListeners();
protected void unregisterListeners() {
listeners.stream().forEach((l) -> {
computer.getMemory().removeListener(l);
});
listeners.clear();
}
public void removeListener(RAMListener l) {
computer.getMemory().removeListener(l);
listeners.remove(l);
}
@Override
public void reconfigure() {
unregisterListeners();
if (cheatsActive) {
registerListeners();
}
}
@Override
public String getShortName() {
return "cheat";
}
}

View File

@ -0,0 +1,124 @@
package jace.cheat;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.util.Callback;
import javax.script.ScriptException;
/**
*
* @author blurry
*/
public class DynamicCheat extends RAMListener {
int id;
IntegerProperty addr;
StringProperty expression;
BooleanProperty active;
StringProperty name;
Callback<RAMEvent, Integer> expressionCallback;
public DynamicCheat(int address, String expr) {
super(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY);
id = (int) (Math.random() * 10000000);
addr = new SimpleIntegerProperty(address);
expression = new SimpleStringProperty(expr);
active = new SimpleBooleanProperty(false);
name = new SimpleStringProperty("Untitled");
expression.addListener((param, oldValue, newValue) -> {
expressionCallback = parseExpression(newValue);
});
expressionCallback = parseExpression(expr);
doConfig();
}
@Override
protected void doConfig() {
if (addr != null) {
setScopeStart(addr.getValue());
}
}
@Override
protected void doEvent(RAMEvent e) {
if (active.get() && expressionCallback != null) {
Integer newVal = expressionCallback.call(e);
if (newVal != null) {
e.setNewValue(newVal);
} else {
active.set(false);
expressionCallback = null;
}
}
}
public BooleanProperty activeProperty() {
return active;
}
public StringProperty nameProperty() {
return name;
}
public IntegerProperty addressProperty() {
return addr;
}
public StringProperty expressionProperty() {
return expression;
}
private Callback<RAMEvent, Integer> parseExpression(String expr) {
String functionName = "processCheat" + id;
String functionBody = "function " + functionName + "(old,val){" + (expr.contains("return") ? expr : "return " + expr) + "}";
try {
MetaCheat.NASHORN_ENGINE.eval(functionBody);
return (RAMEvent e) -> {
try {
Object result = MetaCheat.NASHORN_INVOCABLE.invokeFunction(functionName, e.getOldValue(), e.getNewValue());
if (result instanceof Number) {
return ((Number) result).intValue();
} else {
System.err.println("Not able to handle non-numeric return value: " + result.getClass());
return null;
}
} catch (ScriptException | NoSuchMethodException ex) {
return null;
}
};
} catch (ScriptException ex) {
return null;
}
}
public static String escape(String in) {
return in.replaceAll(";", "~~").replaceAll("\n","\\n");
}
public static String unescape(String in) {
return in.replaceAll("~~", ";").replaceAll("\\n", "\n");
}
public static final String DELIMITER = ";";
public String serialize() {
return escape(name.get()) + DELIMITER
+ escape("$"+Integer.toHexString(addr.get())) + DELIMITER
+ escape(expression.get());
}
static public DynamicCheat deserialize(String in) {
String[] parts = in.split(DELIMITER);
String name = unescape(parts[0]);
Integer addr = Integer.parseInt(parts[1].substring(1), 16);
String expr = unescape(parts[2]);
DynamicCheat out = new DynamicCheat(addr, expr);
out.name.set(name);
return out;
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.cheat;
import java.util.ArrayList;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
/**
*
* @author blurry
*/
public class MemoryCell implements Comparable<MemoryCell> {
public static ChangeListener<MemoryCell> listener;
public int address;
public IntegerProperty value = new SimpleIntegerProperty();
public IntegerProperty readCount = new SimpleIntegerProperty();
public IntegerProperty execCount = new SimpleIntegerProperty();
public IntegerProperty writeCount = new SimpleIntegerProperty();
public BooleanBinding hasCount = readCount.add(execCount).add(writeCount).greaterThan(0);
public ObservableList<Integer> readInstructions = FXCollections.observableList(new ArrayList<>());
public ObservableList<String> readInstructionsDisassembly = FXCollections.observableArrayList(new ArrayList<>());
public ObservableList<Integer> writeInstructions = FXCollections.observableList(new ArrayList<>());
public ObservableList<String> writeInstructionsDisassembly = FXCollections.observableArrayList(new ArrayList<>());
public ObservableList<String> execInstructionsDisassembly = FXCollections.observableArrayList(new ArrayList<>());
private int x;
private int y;
private int width;
private int height;
public static void setListener(ChangeListener<MemoryCell> l) {
listener = l;
}
public MemoryCell() {
ChangeListener<Number> changeListener = (ObservableValue<? extends Number> val, Number oldVal, Number newVal) -> {
if (listener != null) {
listener.changed(null, this, this);
}
};
value.addListener(changeListener);
}
public void setRect(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@Override
public int compareTo(MemoryCell o) {
return address - o.address;
}
public boolean hasCounts() {
return hasCount.get();
}
}

View File

@ -0,0 +1,445 @@
package jace.cheat;
import jace.Emulator;
import jace.LawlessLegends;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.State;
import jace.ui.MetacheatUI;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class MetaCheat extends Cheats {
static final ScriptEngine NASHORN_ENGINE = new ScriptEngineManager().getEngineByName("nashorn");
static Invocable NASHORN_INVOCABLE = (Invocable) NASHORN_ENGINE;
public static enum SearchType {
VALUE, TEXT, CHANGE
}
public static enum SearchChangeType {
NO_CHANGE, ANY_CHANGE, LESS, GREATER, AMOUNT
}
public static class SearchResult {
int address;
int lastObservedValue = 0;
private SearchResult(int address, int val) {
this.address = address;
lastObservedValue = val;
}
@Override
public String toString() {
return Integer.toHexString(address) + ": " + lastObservedValue + " (" + Integer.toHexString(lastObservedValue) + ")";
}
public int getAddress() {
return address;
}
}
MetacheatUI ui;
public int fadeRate = 1;
public int lightRate = 30;
public int historyLength = 10;
private int startAddress = 0;
private int endAddress = 0x0ffff;
private final StringProperty startAddressProperty = new SimpleStringProperty(Integer.toHexString(startAddress));
private final StringProperty endAddressProperty = new SimpleStringProperty(Integer.toHexString(endAddress));
private boolean byteSized = true;
private SearchType searchType = SearchType.VALUE;
private SearchChangeType searchChangeType = SearchChangeType.NO_CHANGE;
private final BooleanProperty signedProperty = new SimpleBooleanProperty(false);
private final StringProperty searchValueProperty = new SimpleStringProperty("0");
private final StringProperty changeByProperty = new SimpleStringProperty("0");
private final ObservableList<DynamicCheat> cheatList = FXCollections.observableArrayList();
private final ObservableList<SearchResult> resultList = FXCollections.observableArrayList();
private final ObservableList<State> snapshotList = FXCollections.observableArrayList();
public MetaCheat(Computer computer) {
super(computer);
addNumericValidator(startAddressProperty);
addNumericValidator(endAddressProperty);
addNumericValidator(searchValueProperty);
addNumericValidator(changeByProperty);
startAddressProperty.addListener((prop, oldVal, newVal) -> {
startAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
});
endAddressProperty.addListener((prop, oldVal, newVal) -> {
endAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
});
}
private void addNumericValidator(StringProperty stringProperty) {
stringProperty.addListener((ObservableValue<? extends String> prop, String oldVal, String newVal) -> {
if (newVal == null || newVal.isEmpty()) {
return;
}
if (!newVal.matches("(\\+|-)?(x|$)?[0-9a-fA-F]*")) {
stringProperty.set("");
}
});
}
public int parseInt(String s) throws NumberFormatException {
if (s == null || s.isEmpty()) {
return 0;
}
if (s.matches("(\\+|-)?[0-9]+")) {
return Integer.parseInt(s);
} else {
String upper = s.toUpperCase();
boolean positive = !upper.startsWith("-");
for (int i = 0; i < upper.length(); i++) {
char c = upper.charAt(i);
if ((c >= '0' && c <= '9') || (c >= 'A' & c <= 'F')) {
int value = Integer.parseInt(s.substring(i), 16);
if (!positive) {
value *= -1;
}
return value;
}
}
}
throw new NumberFormatException("Could not interpret int value " + s);
}
@Override
public void registerListeners() {
}
public void addCheat(DynamicCheat cheat) {
cheatList.add(cheat);
computer.getMemory().addListener(cheat);
cheat.addressProperty().addListener((prop, oldVal, newVal) -> {
computer.getMemory().removeListener(cheat);
cheat.doConfig();
computer.getMemory().addListener(cheat);
});
}
public void removeCheat(DynamicCheat cheat) {
cheat.active.set(false);
computer.getMemory().removeListener(cheat);
cheatList.remove(cheat);
}
@Override
protected void unregisterListeners() {
super.unregisterListeners();
cheatList.stream().forEach(computer.getMemory()::removeListener);
}
@Override
protected String getDeviceName() {
return "MetaCheat";
}
@Override
public void detach() {
super.detach();
ui.detach();
}
@Override
public void attach() {
ui = LawlessLegends.getApplication().showMetacheat();
ui.registerMetacheatEngine(this);
super.attach();
}
public int getStartAddress() {
return startAddress;
}
public int getEndAddress() {
return endAddress;
}
public void setByteSized(boolean b) {
byteSized = b;
}
public void setSearchType(SearchType searchType) {
this.searchType = searchType;
}
public void setSearchChangeType(SearchChangeType searchChangeType) {
this.searchChangeType = searchChangeType;
}
public Property<Boolean> signedProperty() {
return signedProperty;
}
public Property<String> searchValueProperty() {
return searchValueProperty;
}
public Property<String> searchChangeByProperty() {
return changeByProperty;
}
public ObservableList<DynamicCheat> getCheats() {
return cheatList;
}
public ObservableList<SearchResult> getSearchResults() {
return resultList;
}
public ObservableList<State> getSnapshots() {
return snapshotList;
}
public Property<String> startAddressProperty() {
return startAddressProperty;
}
public Property<String> endAddressProperty() {
return endAddressProperty;
}
public void newSearch() {
RAM memory = Emulator.computer.getMemory();
resultList.clear();
int compare = parseInt(searchValueProperty.get());
for (int i = 0; i < 0x10000; i++) {
boolean signed = signedProperty.get();
int val
= byteSized
? signed ? memory.readRaw(i) : memory.readRaw(i) & 0x0ff
: signed ? memory.readWordRaw(i) : memory.readWordRaw(i) & 0x0ffff;
if (!searchType.equals(SearchType.VALUE) || val == compare) {
SearchResult result = new SearchResult(i, val);
resultList.add(result);
}
}
}
public void performSearch() {
RAM memory = Emulator.computer.getMemory();
boolean signed = signedProperty.get();
resultList.removeIf((SearchResult result) -> {
int val = byteSized
? signed ? memory.readRaw(result.address) : memory.readRaw(result.address) & 0x0ff
: signed ? memory.readWordRaw(result.address) : memory.readWordRaw(result.address) & 0x0ffff;
int last = result.lastObservedValue;
result.lastObservedValue = val;
switch (searchType) {
case VALUE:
int compare = parseInt(searchValueProperty.get());
return compare != val;
case CHANGE:
switch (searchChangeType) {
case AMOUNT:
int amount = parseInt(searchChangeByProperty().getValue());
return (val - last) != amount;
case GREATER:
return val <= last;
case ANY_CHANGE:
return val == last;
case LESS:
return val >= last;
case NO_CHANGE:
return val != last;
}
break;
case TEXT:
break;
}
return false;
});
}
RAMListener memoryViewListener = null;
private final Map<Integer, MemoryCell> memoryCells = new ConcurrentHashMap<>();
public MemoryCell getMemoryCell(int address) {
return memoryCells.get(address);
}
public void initMemoryView() {
RAM memory = Emulator.computer.getMemory();
for (int addr = getStartAddress(); addr <= getEndAddress(); addr++) {
if (getMemoryCell(addr) == null) {
MemoryCell cell = new MemoryCell();
cell.address = addr;
cell.value.set(memory.readRaw(addr));
memoryCells.put(addr, cell);
}
}
if (memoryViewListener == null) {
memoryViewListener = memory.observe(RAMEvent.TYPE.ANY, startAddress, endAddress, this::processMemoryEvent);
listeners.add(memoryViewListener);
}
}
int fadeCounter = 0;
int FADE_TIMER_VALUE = (int) (Emulator.computer.getMotherboard().cyclesPerSecond / 60);
@Override
public void tick() {
computer.cpu.performSingleTrace();
if (fadeCounter-- <= 0) {
fadeCounter = FADE_TIMER_VALUE;
memoryCells.values().stream()
.filter((cell) -> cell.hasCounts())
.forEach((cell) -> {
if (cell.execCount.get() > 0) {
cell.execCount.set(Math.max(0, cell.execCount.get() - fadeRate));
}
if (cell.readCount.get() > 0) {
cell.readCount.set(Math.max(0, cell.readCount.get() - fadeRate));
}
if (cell.writeCount.get() > 0) {
cell.writeCount.set(Math.max(0, cell.writeCount.get() - fadeRate));
}
if (MemoryCell.listener != null) {
MemoryCell.listener.changed(null, cell, cell);
}
});
}
}
AtomicInteger pendingInspectorUpdates = new AtomicInteger(0);
public void onInspectorChanged() {
pendingInspectorUpdates.set(0);
}
private void processMemoryEvent(RAMEvent e) {
MemoryCell cell = getMemoryCell(e.getAddress());
if (cell != null) {
CPU cpu = Emulator.computer.getCpu();
int pc = cpu.getProgramCounter();
String trace = cpu.getLastTrace();
switch (e.getType()) {
case EXECUTE:
cell.execInstructionsDisassembly.add(trace);
if (cell.execInstructionsDisassembly.size() > historyLength) {
cell.execInstructionsDisassembly.remove(0);
}
case READ_OPERAND:
cell.execCount.set(Math.min(255, cell.execCount.get() + lightRate));
break;
case WRITE:
cell.writeCount.set(Math.min(255, cell.writeCount.get() + lightRate));
if (ui.isInspecting(cell.address)) {
if (pendingInspectorUpdates.incrementAndGet() < 5) {
Platform.runLater(() -> {
pendingInspectorUpdates.decrementAndGet();
cell.writeInstructions.add(pc);
cell.writeInstructionsDisassembly.add(trace);
if (cell.writeInstructions.size() > historyLength) {
cell.writeInstructions.remove(0);
cell.writeInstructionsDisassembly.remove(0);
}
});
}
} else {
cell.writeInstructions.add(cpu.getProgramCounter());
cell.writeInstructionsDisassembly.add(cpu.getLastTrace());
if (cell.writeInstructions.size() > historyLength) {
cell.writeInstructions.remove(0);
cell.writeInstructionsDisassembly.remove(0);
}
}
break;
default:
cell.readCount.set(Math.min(255, cell.readCount.get() + lightRate));
if (ui.isInspecting(cell.address)) {
if (pendingInspectorUpdates.incrementAndGet() < 5) {
Platform.runLater(() -> {
pendingInspectorUpdates.decrementAndGet();
cell.readInstructions.add(pc);
cell.readInstructionsDisassembly.add(trace);
if (cell.readInstructions.size() > historyLength) {
cell.readInstructions.remove(0);
cell.readInstructionsDisassembly.remove(0);
}
});
}
} else {
cell.readInstructions.add(cpu.getProgramCounter());
cell.readInstructionsDisassembly.add(cpu.getLastTrace());
if (cell.readInstructions.size() > historyLength) {
cell.readInstructions.remove(0);
cell.readInstructionsDisassembly.remove(0);
}
}
}
cell.value.set(e.getNewValue());
}
}
public void saveCheats(File saveFile) {
FileWriter writer = null;
try {
writer = new FileWriter(saveFile);
for (DynamicCheat cheat : cheatList) {
writer.write(cheat.serialize());
writer.write("\n");
}
writer.close();
} catch (IOException ex) {
Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
writer.close();
} catch (IOException ex) {
Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public void loadCheats(File saveFile) {
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(saveFile));
StringBuilder guts = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
DynamicCheat cheat = DynamicCheat.deserialize(line);
addCheat(cheat);
}
in.close();
} catch (IOException ex) {
Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
in.close();
} catch (IOException ex) {
Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}

View File

@ -0,0 +1,204 @@
package jace.cheat;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.config.ConfigurableField;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.RAMEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.MouseButton;
public class MontezumasRevengeCheats extends Cheats {
@ConfigurableField(category = "Hack", name = "Repulsive", defaultValue = "false", description = "YOU STINK!")
public static boolean repulsiveHack = false;
@ConfigurableField(category = "Hack", name = "Feather Fall", defaultValue = "false", description = "Falling will not result in death")
public static boolean featherFall = false;
@ConfigurableField(category = "Hack", name = "Moon Jump", defaultValue = "false", description = "Wheeee!")
public static boolean moonJump = false;
@ConfigurableField(category = "Hack", name = "Infinite Lives", defaultValue = "false", description = "Game on!")
public static boolean infiniteLives = false;
@ConfigurableField(category = "Hack", name = "Score hack", defaultValue = "false", description = "Change the score")
public static boolean scoreHack = false;
@ConfigurableField(category = "Hack", name = "Snake Charmer", defaultValue = "false", description = "Disable collision detection with enemies")
public static boolean snakeCharmer = false;
@ConfigurableField(category = "Hack", name = "Teleport", defaultValue = "false", description = "Click to teleport!")
public static boolean mouseHack = false;
@ConfigurableField(category = "Hack", name = "Safe Passage", defaultValue = "false", description = "Deadly floors and doors disabled!")
public static boolean safePassage = false;
public static int X_MAX = 80;
public static int Y_MAX = 160;
public static int MAX_VEL = 4;
public static int MOON_JUMP_VELOCITY = -14;
public static int ROOM_LEVEL = 0x0d1;
public static int LIVES = 0x0e0;
public static int SCORE = 0x0e8;
public static int SCORE_END = 0x0ea;
public static int FLOOR_TIMER = 0x030a;
public static int HAZARD_TIMER = 0x030b;
public static int HAZARD_FLAG = 0x030f;
public static int PLAYER_X = 0x01508;
public static int PLAYER_Y = 0x01510;
public static int Y_VELOCITY = 0x01550;
public static int CHAR_STATE = 0x01570;
public static int lastX = 0;
public MontezumasRevengeCheats(Computer computer) {
super(computer);
}
double mouseX;
double mouseY;
EventHandler<javafx.scene.input.MouseEvent> listener = (event) -> {
Node source = (Node) event.getSource();
mouseX = event.getSceneX() / source.getBoundsInLocal().getWidth();
mouseY = event.getSceneY() / source.getBoundsInLocal().getHeight();
if (event.isPrimaryButtonDown()) {
mouseClicked(event.getButton());
}
};
@Override
public void registerListeners() {
RAM memory = Emulator.computer.memory;
if (repulsiveHack) {
addCheat(RAMEvent.TYPE.WRITE, this::repulsiveBehavior, 0x1508, 0x1518);
}
if (featherFall) {
addCheat(RAMEvent.TYPE.WRITE, this::featherFallBehavior, PLAYER_Y);
// Bypass the part that realizes you should die when you hit the floor
bypassCode(0x6bb3, 0x6bb4);
}
if (moonJump) {
addCheat(RAMEvent.TYPE.WRITE, this::moonJumpBehavior, Y_VELOCITY);
}
if (infiniteLives) {
forceValue(11, LIVES);
}
if (safePassage) {
//blank out pattern for floors/doors
for (int addr = 0x0b54; addr <= 0xb5f; addr++) {
memory.write(addr, (byte) 0, false, false);
memory.write(addr + 0x0400, (byte) 0, false, false);
}
memory.write(0x0b50, (byte) 0b11010111, false, false);
memory.write(0x0b51, (byte) 0b00010000, false, false);
memory.write(0x0b52, (byte) 0b10001000, false, false);
memory.write(0x0b53, (byte) 0b10101010, false, false);
memory.write(0x0f50, (byte) 0b10101110, false, false);
memory.write(0x0f51, (byte) 0b00001000, false, false);
memory.write(0x0f52, (byte) 0b10000100, false, false);
memory.write(0x0f53, (byte) 0b11010101, false, false);
forceValue(32, FLOOR_TIMER);
forceValue(32, HAZARD_TIMER);
forceValue(1, HAZARD_FLAG);
}
if (scoreHack) {
// Score: 900913
forceValue(0x90, SCORE);
forceValue(0x09, SCORE + 1);
forceValue(0x13, SCORE + 2);
}
if (snakeCharmer) {
// Skip the code that determines you're touching an enemy
bypassCode(0x07963, 0x07964);
}
if (mouseHack) {
EmulatorUILogic.addMouseListener(listener);
}
}
@Override
protected void unregisterListeners() {
super.unregisterListeners();
EmulatorUILogic.removeMouseListener(listener);
}
private void repulsiveBehavior(RAMEvent e) {
int playerX = computer.getMemory().readRaw(PLAYER_X);
int playerY = computer.getMemory().readRaw(PLAYER_Y);
for (int num = 7; num > 0; num--) {
int monsterX = computer.getMemory().readRaw(PLAYER_X + num);
int monsterY = computer.getMemory().readRaw(PLAYER_Y + num);
if (monsterX != 0 && monsterY != 0) {
if (Math.abs(monsterY - playerY) < 19) {
if (Math.abs(monsterX - playerX) < 7) {
int movement = Math.max(1, Math.abs(lastX - playerX));
if (monsterX > playerX) {
monsterX += movement;
} else {
monsterX -= movement;
if (monsterX <= 0) {
monsterX = 80;
}
}
computer.getMemory().write(PLAYER_X + num, (byte) monsterX, false, false);
}
}
}
}
lastX = playerX;
}
private void featherFallBehavior(RAMEvent yCoordChangeEvent) {
if (yCoordChangeEvent.getNewValue() != yCoordChangeEvent.getOldValue()) {
int yVel = computer.getMemory().readRaw(Y_VELOCITY);
if (yVel > MAX_VEL) {
computer.getMemory().write(Y_VELOCITY, (byte) MAX_VEL, false, false);
}
}
}
private void moonJumpBehavior(RAMEvent velocityChangeEvent) {
if (inStartingSequence()) {
return;
}
if (velocityChangeEvent.getNewValue() < 0
&& velocityChangeEvent.getNewValue() < velocityChangeEvent.getOldValue()) {
velocityChangeEvent.setNewValue(MOON_JUMP_VELOCITY);
}
}
private boolean inStartingSequence() {
int roomLevel = computer.getMemory().readRaw(ROOM_LEVEL);
return roomLevel == -1;
}
@Override
public String getName() {
return "Montezuma's Revenge";
}
@Override
protected String getDeviceName() {
return "Montezuma's Revenge";
}
@Override
public void tick() {
}
private void mouseClicked(MouseButton button) {
byte newX = (byte) (mouseX * X_MAX);
byte newY = (byte) (mouseY * Y_MAX);
computer.memory.write(PLAYER_X, newX, false, false);
computer.memory.write(PLAYER_Y, newY, false, false);
}
}

View File

@ -0,0 +1,353 @@
/*
* 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.EmulatorUILogic;
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 javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.MouseButton;
/**
* 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 {
@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;
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 final int trloc = mobtables;
public static final 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 final int RedBufs = 0x05e00;
public static final int RedBuf = RedBufs + 90;
// Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/01%20POP%20Source/Source/EQ.S
public static final int WipeBuf = RedBuf + 90;
public static final 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;
public PrinceOfPersiaCheats(Computer computer) {
super(computer);
}
double mouseX;
double mouseY;
EventHandler<javafx.scene.input.MouseEvent> listener = (event) -> {
Node source = (Node) event.getSource();
mouseX = event.getSceneX() / source.getBoundsInLocal().getWidth();
mouseY = event.getSceneY() / source.getBoundsInLocal().getHeight();
if (event.isPrimaryButtonDown()) {
mouseClicked(event.getButton());
}
};
@Override
protected String getDeviceName() {
return ("Prince of Persia");
}
@Override
public void tick() {
// Do nothing
}
@Override
public void registerListeners() {
if (velocityHack) {
addCheat(RAMEvent.TYPE.READ_DATA, true, this::velocityHackBehavior, CharYVel);
}
if (invincibilityHack) {
forceValue(3, true, KidStrength);
}
if (sleepHack) {
forceValue(0, true, EnemyAlert);
}
if (swordHack) {
forceValue(1, true, hasSword);
}
if (timeHack) {
forceValue(0x69, true, MinLeft);
}
if (mouseHack) {
EmulatorUILogic.addMouseListener(listener);
} else {
EmulatorUILogic.removeMouseListener(listener);
}
}
@Override
public void unregisterListeners() {
super.unregisterListeners();
EmulatorUILogic.removeMouseListener(listener);
}
public static int BlueType = 0x0b700;
private void velocityHackBehavior(RAMEvent velocityChangeEvent) {
int newVel = velocityChangeEvent.getNewValue();
if (newVel > 5) {
newVel = 1;
}
velocityChangeEvent.setNewValue(newVel & 0x0ff);
}
public void mouseClicked(MouseButton button) {
Double x = mouseX;
// Offset y by three pixels to account for tiles above
Double y = mouseY - 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 - ((yCoor - 47.0) / 16.0);
int xCoor = ((int) (x * 280) % 28);
double xOffset = 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.getMemory();
PagedMemory auxMem = mem.getAuxMemory();
if (button == MouseButton.PRIMARY) {
// 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.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 = true;
if (numTransition > 0) {
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);
}
}
}
}

View File

@ -0,0 +1,138 @@
/*
* 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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
*
* @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<>();
Set<? extends Class> allClasses = Utility.findAllSubclasses(template);
if (!allClasses.contains(null)) {
allClasses.add(null);
}
List<Entry<Class, String>> values = new ArrayList<>();
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>() {
@Override
public Class getKey() {
return c;
}
@Override
public String getValue() {
if (c == null) {
return "**Empty**";
}
if (c.isAnnotationPresent(Name.class)) {
return ((Name) c.getAnnotation(Name.class)).value();
}
return c.getSimpleName();
}
@Override
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, (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()));
}
});
values.stream().forEach((entry) -> {
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,48 @@
/*
* 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";
public boolean enablesDevice() default false;
}

View File

@ -0,0 +1,632 @@
/*
* 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.Emulator;
import jace.EmulatorUILogic;
import jace.core.Computer;
import jace.core.Keyboard;
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.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
/**
* Manages the configuration state of the emulator components.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Configuration implements Reconfigurable {
private static Method findAnyMethodByName(Class<? extends Reconfigurable> aClass, String m) {
for (Method method : aClass.getMethods()) {
if (method.getName().equals(m)) {
return method;
}
}
return null;
}
static ConfigurableField getConfigurableFieldInfo(Reconfigurable subject, String settingName) {
Field f;
try {
f = subject.getClass().getField(settingName);
} catch (NoSuchFieldException | SecurityException ex) {
return null;
}
ConfigurableField annotation = f.getAnnotation(ConfigurableField.class);
return annotation;
}
public static String getShortName(ConfigurableField f, String longName) {
return (f != null && !f.shortName().equals("")) ? f.shortName() : longName;
}
public static InvokableAction getInvokableActionInfo(Reconfigurable subject, String actionName) {
for (Method m : subject.getClass().getMethods()) {
if (m.getName().equals(actionName) && m.isAnnotationPresent(InvokableAction.class)) {
return m.getAnnotation(InvokableAction.class);
}
}
return null;
}
public static Optional<ImageView> getChangedIcon() {
return Utility.loadIcon("icon_exclaim.gif").map(ImageView::new);
}
@Override
public String getName() {
return "Configuration";
}
@Override
public String getShortName() {
return "cfg";
}
@Override
public void reconfigure() {
}
/**
* 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 extends TreeItem implements Serializable {
public transient ConfigNode root;
public transient ConfigNode parent;
private transient ObservableList<ConfigNode> children;
public transient Reconfigurable subject;
private transient boolean changed = true;
public Map<String, Serializable> settings = new TreeMap<>();
public Map<String, String[]> hotkeys = new TreeMap<>();
public String name;
private String id;
private void writeObject(java.io.ObjectOutputStream out)
throws IOException {
out.writeObject(id);
out.writeObject(name);
out.writeObject(settings);
out.writeObject(hotkeys);
out.writeObject(children.toArray());
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
children = super.getChildren();
id = (String) in.readObject();
name = (String) in.readObject();
settings = (Map) in.readObject();
hotkeys = (Map) in.readObject();
Object[] nodeArray = (Object[]) in.readObject();
for (Object child : nodeArray) {
children.add((ConfigNode) child);
}
}
private void readObjectNoData()
throws ObjectStreamException {
name = "Bad read";
}
@Override
public String toString() {
return name;
}
public ConfigNode(Reconfigurable subject) {
this(null, subject);
this.root = null;
this.setExpanded(true);
}
public ConfigNode(ConfigNode parent, Reconfigurable subject) {
this(parent, subject, subject.getName());
}
public ConfigNode(ConfigNode parent, Reconfigurable subject, String id) {
super();
this.id = id;
this.name = subject.getName();
this.subject = subject;
this.children = getChildren();
this.parent = parent;
if (this.parent != null) {
this.root = this.parent.root != null ? this.parent.root : this.parent;
}
setValue(toString());
}
public void setFieldValue(String field, Serializable value) {
setChanged(true);
if (value != null) {
if (value.equals(getFieldValue(field))) {
return;
}
} else {
if (getFieldValue(field) == null) {
return;
}
}
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();
}
@Override
public ObservableList<ConfigNode> getChildren() {
return super.getChildren();
}
private boolean removeChild(String childName) {
ConfigNode child = findChild(childName);
return children.remove(child);
}
private ConfigNode findChild(String id) {
for (ConfigNode node : children) {
if (id.equalsIgnoreCase(node.id)) {
return node;
}
}
return null;
}
private void putChild(String id, ConfigNode newChild) {
removeChild(id);
int index = 0;
for (ConfigNode node : children) {
int compare = node.toString().compareToIgnoreCase(id);
if (compare >= 0) {
break;
} else {
index++;
}
}
children.add(index, newChild);
}
private void setChanged(boolean b) {
changed = b;
if (!changed) {
setGraphic(null);
} else {
getChangedIcon().ifPresent(this::setGraphic);
}
}
}
public static ConfigNode BASE;
public static EmulatorUILogic ui = Emulator.logic;
public static Computer emulator = Emulator.computer;
@ConfigurableField(name = "Autosave Changes", description = "If unchecked, changes are only saved when the Save button is pressed.")
public static boolean saveAutomatically = false;
public static void buildTree() {
BASE = new ConfigNode(new Configuration());
buildTree(BASE, new LinkedHashSet());
}
private static void buildTree(ConfigNode node, Set visited) {
if (node.subject == null) {
return;
}
for (Method m : node.subject.getClass().getMethods()) {
if (!m.isAnnotationPresent(InvokableAction.class)) {
continue;
}
InvokableAction action = m.getDeclaredAnnotation(InvokableAction.class);
node.hotkeys.put(m.getName(), action.defaultKeyMapping());
}
for (Field f : node.subject.getClass().getFields()) {
// System.out.println("Evaluating field " + f.getName());
try {
Object o = f.get(node.subject);
if (!f.getType().isPrimitive() && f.getType() != String.class && visited.contains(o)) {
continue;
}
visited.add(o);
// 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;
}
if (o == null) {
continue;
}
if (o instanceof Reconfigurable) {
Reconfigurable r = (Reconfigurable) o;
ConfigNode child = node.findChild(r.getName());
if (child == null || !child.subject.equals(o)) {
child = new ConfigNode(node, r);
node.putChild(f.getName(), child);
}
buildTree(child, visited);
} else if (o.getClass().isArray()) {
String fieldName = f.getName();
Class type = o.getClass().getComponentType();
// System.out.println("Evaluating " + node.subject.getShortName() + "." + fieldName + "; type is " + type.toGenericString());
List<Reconfigurable> children = new ArrayList<>();
if (!Reconfigurable.class.isAssignableFrom(type)) {
// System.out.println("Looking at type " + type.getName() + " to see if optional");
if (Optional.class.isAssignableFrom(type)) {
Type genericTypes = f.getGenericType();
// System.out.println("Looking at generic parmeters " + genericTypes.getTypeName() + " for reconfigurable class, type " + genericTypes.getClass().getName());
if (genericTypes instanceof GenericArrayType) {
GenericArrayType aType = (GenericArrayType) genericTypes;
ParameterizedType pType = (ParameterizedType) aType.getGenericComponentType();
if (pType.getActualTypeArguments().length != 1) {
continue;
}
Type genericType = pType.getActualTypeArguments()[0];
// System.out.println("Looking at type " + genericType.getTypeName() + " to see if reconfigurable");
if (!Reconfigurable.class.isAssignableFrom((Class) genericType)) {
continue;
}
} else {
continue;
}
for (Optional<Reconfigurable> child : (Optional<Reconfigurable>[]) o) {
if (child.isPresent()) {
children.add(child.get());
} else {
children.add(null);
}
}
}
} else {
children = Arrays.asList((Reconfigurable[]) o);
}
for (int i = 0; i < children.size(); i++) {
Reconfigurable child = children.get(i);
String childId = fieldName + i;
if (child == null) {
node.removeChild(childId);
continue;
}
ConfigNode grandchild = node.findChild(childId);
if (grandchild == null || !grandchild.subject.equals(child)) {
grandchild = new ConfigNode(node, child, childId);
node.putChild(childId, grandchild);
}
buildTree(grandchild, visited);
}
}
} catch (IllegalArgumentException | 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",
defaultKeyMapping = "meta+ctrl+s"
)
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",
defaultKeyMapping = "meta+ctrl+r"
)
public static void loadSettings() {
{
boolean successful = false;
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(getSettingsFile()));
ConfigNode newRoot = (ConfigNode) ois.readObject();
applyConfigTree(newRoot, BASE);
successful = true;
} catch (FileNotFoundException ex) {
// This just means there are no settings to be saved -- just ignore it.
} catch (ClassNotFoundException | IOException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
if (ois != null) {
ois.close();
}
if (!successful) {
applySettings(BASE);
}
} 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 = Emulator.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.getChildren()) {
hasChanged |= applySettings(child);
}
if (node.equals(BASE) && hasChanged) {
buildTree();
}
if (resume) {
Emulator.computer.resume();
}
return hasChanged;
}
private static void applyConfigTree(ConfigNode newRoot, ConfigNode oldRoot) {
if (oldRoot == null || newRoot == null) {
return;
}
oldRoot.settings = newRoot.settings;
oldRoot.hotkeys = newRoot.hotkeys;
if (oldRoot.subject != null) {
doApply(oldRoot);
buildTree(oldRoot, new HashSet());
}
newRoot.getChildren().stream().forEach((child) -> {
String childName = child.toString();
ConfigNode oldChild = oldRoot.findChild(childName);
if (oldChild == null) {oldChild = oldRoot.findChild(child.id);}
// System.out.println("Applying settings for " + childName);
applyConfigTree(child, oldChild);
});
}
private static void doApply(ConfigNode node) {
List<String> removeList = new ArrayList<>();
Keyboard.unregisterAllHandlers(node.subject);
node.hotkeys.keySet().stream().forEach((m) -> {
Method method = findAnyMethodByName(node.subject.getClass(), m);
if (method != null) {
InvokableAction action = method.getAnnotation(InvokableAction.class);
for (String code : node.hotkeys.get(m)) {
Keyboard.registerInvokableAction(action, node.subject, method, code);
}
}
});
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 | IllegalArgumentException | IllegalAccessException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
}
removeList.stream().forEach((f) -> {
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.setChanged(false);
}
public static void applySettings(Map<String, String> settings) {
for (Map.Entry<String, String> setting : settings.entrySet()) {
Map<String, ConfigNode> shortNames = new HashMap<>();
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<>();
for (String longName : n.getAllSettingNames()) {
ConfigurableField f = getConfigurableFieldInfo(n.subject, longName);
String shortName = getShortName(f, 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) {
// System.out.println("Encountered " + n.subject.getShortName().toLowerCase());
shortNames.put(n.subject.getShortName().toLowerCase(), n);
n.getChildren().stream().forEach((c) -> {
buildNodeMap(c, shortNames);
});
}
private static void printTree(ConfigNode n, String prefix, int i) {
n.getAllSettingNames().stream().forEach((setting) -> {
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 | 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 + ")");
});
n.getChildren().stream().forEach((c) -> {
printTree(c, prefix + "." + c.toString(), i + 1);
});
}
}

View File

@ -0,0 +1,296 @@
package jace.config;
import jace.config.Configuration.ConfigNode;
import java.io.File;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.StringConverter;
public class ConfigurationUIController {
public static final String DELIMITER = "~!~";
@FXML
private ResourceBundle resources;
@FXML
private URL location;
@FXML
private VBox settingsVbox;
@FXML
private SplitPane splitPane;
@FXML
private ScrollPane settingsScroll;
@FXML
private TreeView<ConfigNode> deviceTree;
@FXML
private ScrollPane treeScroll;
@FXML
void reloadConfig(MouseEvent event) {
Configuration.loadSettings();
resetDeviceTree();
}
@FXML
void saveConfig(MouseEvent event) {
applyConfig(event);
Configuration.saveSettings();
}
@FXML
void applyConfig(MouseEvent event) {
Configuration.applySettings(Configuration.BASE);
resetDeviceTree();
}
@FXML
void cancelConfig(MouseEvent event) {
Configuration.buildTree();
resetDeviceTree();
}
@FXML
public void initialize() {
assert settingsVbox != null : "fx:id=\"settingsVbox\" was not injected: check your FXML file 'Configuration.fxml'.";
assert splitPane != null : "fx:id=\"splitPane\" was not injected: check your FXML file 'Configuration.fxml'.";
assert settingsScroll != null : "fx:id=\"settingsScroll\" was not injected: check your FXML file 'Configuration.fxml'.";
assert deviceTree != null : "fx:id=\"deviceTree\" was not injected: check your FXML file 'Configuration.fxml'.";
assert treeScroll != null : "fx:id=\"treeScroll\" was not injected: check your FXML file 'Configuration.fxml'.";
resetDeviceTree();
deviceTree.getSelectionModel().selectedItemProperty().addListener(this::selectionChanged);
deviceTree.maxWidthProperty().bind(treeScroll.widthProperty());
}
private void resetDeviceTree() {
Set<String> expanded = new HashSet<>();
String current = getCurrentNodePath();
getExpandedNodes("", deviceTree.getRoot(), expanded);
deviceTree.setRoot(Configuration.BASE);
setExpandedNodes("", deviceTree.getRoot(), expanded);
setCurrentNodePath(current);
}
private void getExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
if (root == null) return;
root.getChildren().stream().filter((item) -> (item.isExpanded())).forEach((item) -> {
String name = prefix+item.toString();
expanded.add(name);
getExpandedNodes(name+DELIMITER, item, expanded);
});
}
private void setExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
if (root == null) return;
root.getChildren().stream().forEach((item) -> {
String name = prefix+item.toString();
if (expanded.contains(name)) {
item.setExpanded(true);
}
setExpandedNodes(name+DELIMITER, item, expanded);
});
}
private String getCurrentNodePath() {
TreeItem<ConfigNode> current = deviceTree.getSelectionModel().getSelectedItem();
if (current == null) return null;
String out = current.toString();
while (current.getParent() != null) {
out = current.getParent().toString()+DELIMITER+current;
current = current.getParent();
}
return out;
}
private void setCurrentNodePath(String value) {
if (value == null) return;
String[] parts = value.split(Pattern.quote(DELIMITER));
TreeItem<ConfigNode> current = deviceTree.getRoot();
for (String part : parts) {
for (TreeItem child : current.getChildren()) {
if (child.toString().equals(part)) {
current = child;
}
}
}
deviceTree.getSelectionModel().select(current);
}
private void selectionChanged(
ObservableValue<? extends TreeItem<ConfigNode>> observable,
TreeItem<ConfigNode> oldValue,
TreeItem<ConfigNode> newValue) {
clearForm();
buildForm((ConfigNode) newValue);
}
private void clearForm() {
settingsVbox.getChildren().clear();
}
private void buildForm(ConfigNode node) {
if (node == null) {
return;
}
node.hotkeys.forEach((name, values) -> {
settingsVbox.getChildren().add(buildKeyShortcutRow(node, name, values));
});
node.settings.forEach((name, value) -> {
settingsVbox.getChildren().add(buildSettingRow(node, name, value));
});
}
private Node buildSettingRow(ConfigNode node, String settingName, Serializable value) {
ConfigurableField fieldInfo = Configuration.getConfigurableFieldInfo(node.subject, settingName);
if (fieldInfo == null) {
return null;
}
HBox row = new HBox();
row.getStyleClass().add("setting-row");
Label label = new Label(fieldInfo.name());
label.getStyleClass().add("setting-label");
label.setMinWidth(150.0);
Node widget = buildEditField(node, settingName, value);
label.setLabelFor(widget);
row.getChildren().add(label);
row.getChildren().add(widget);
return row;
}
private Node buildKeyShortcutRow(ConfigNode node, String actionName, String[] values) {
InvokableAction actionInfo = Configuration.getInvokableActionInfo(node.subject, actionName);
if (actionInfo == null) {
return null;
}
HBox row = new HBox();
row.getStyleClass().add("setting-row");
Label label = new Label(actionInfo.name());
label.getStyleClass().add("setting-keyboard-shortcut");
label.setMinWidth(150.0);
String value = Arrays.stream(values).collect(Collectors.joining(" or "));
Text widget = new Text(value);
widget.setWrappingWidth(180.0);
widget.getStyleClass().add("setting-keyboard-value");
widget.setOnMouseClicked((event) -> {
editKeyboardShortcut(node, actionName, widget);
});
label.setLabelFor(widget);
row.getChildren().add(label);
row.getChildren().add(widget);
return row;
}
private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
throw new UnsupportedOperationException("Not supported yet.");
}
private Node buildEditField(ConfigNode node, String settingName, Serializable value) {
Field field;
try {
field = node.subject.getClass().getField(settingName);
} catch (NoSuchFieldException | SecurityException ex) {
return null;
}
Class type = field.getType();
if (type == java.lang.String.class) {
return buildTextField(node, settingName, value, null);
} else if (type.isPrimitive()) {
if (type == Integer.TYPE || type == Short.TYPE || type == Byte.TYPE) {
return buildTextField(node, settingName, value, "-?[0-9]+");
} else if (type == Float.TYPE || type == Double.TYPE) {
return buildTextField(node, settingName, value, "-?[0-9]*(\\.[0-9]+)?");
} else if (type == Boolean.TYPE) {
return buildBooleanField(node, settingName, value);
} else {
return buildTextField(node, settingName, value, null);
}
} else if (type.equals(File.class)) {
// TODO: Add file support!
} else if (Class.class.isEnum()) {
// TODO: Add enumeration support!
} else if (ISelection.class.isAssignableFrom(type)) {
return buildDynamicSelectComponent(node, settingName, value);
}
return null;
}
private Node buildTextField(ConfigNode node, String settingName, Serializable value, String validationPattern) {
TextField widget = new TextField(String.valueOf(value));
widget.textProperty().addListener((e) -> {
node.setFieldValue(settingName, widget.getText());
});
return widget;
}
private Node buildBooleanField(ConfigNode node, String settingName, Serializable value) {
CheckBox widget = new CheckBox();
widget.setSelected(value.equals(Boolean.TRUE));
widget.selectedProperty().addListener((e) -> {
node.setFieldValue(settingName, widget.isSelected());
});
return widget;
}
private Node buildDynamicSelectComponent(ConfigNode node, String settingName, Serializable value) {
try {
DynamicSelection sel = (DynamicSelection) node.subject.getClass().getField(settingName).get(node.subject);
ChoiceBox widget = new ChoiceBox(FXCollections.observableList(new ArrayList(sel.getSelections().keySet())));
widget.setMinWidth(175.0);
widget.setConverter(new StringConverter() {
@Override
public String toString(Object object) {
return (String) sel.getSelections().get(object);
}
@Override
public Object fromString(String string) {
return sel.findValueByMatch(string);
}
});
Object selected = value == null ? null : widget.getConverter().fromString(String.valueOf(value));
if (selected == null) {
widget.getSelectionModel().selectFirst();
} else {
widget.setValue(selected);
}
widget.valueProperty().addListener((Observable e) -> {
node.setFieldValue(settingName, widget.getConverter().toString(widget.getValue()));
});
return widget;
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
Logger.getLogger(ConfigurationUIController.class.getName()).log(Level.SEVERE, null, ex);
return null;
}
}
}

View File

@ -0,0 +1,69 @@
/*
* 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();
if (i.hasNext()) {
return i.next();
} else {
return null;
}
}
}
@Override
public void setValue(T value) {currentValue = value;}
@Override
public void setValueByMatch(String search) {
setValue(findValueByMatch(search));
}
public T findValueByMatch(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)) {
return key;
}
}
}
return null;
}
}

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,87 @@
/*
* 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 "";
/*
* If true, the key event will be consumed and not processed by any other event handlers
* If the corresponding method returns a boolean, that value will be used instead.
* True = consume (stop processing keystroke), false = pass-through to other handlers
*/
public boolean consumeKeyEvent() default true;
/*
* If false (default) event is only triggered on press, not release. If true,
* method is notified on press and on release
*/
public boolean notifyOnRelease() default false;
/*
* Standard keyboard mapping
*/
public String[] defaultKeyMapping();
}

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,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.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 {
private static final Logger LOG = Logger.getLogger(CPU.class.getName());
public CPU(Computer computer) {
super(computer);
}
@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> oldLog = traceLog;
traceLog = newLog;
computer.resume();
LOG.log(Level.INFO, "Most recent {0} instructions:", traceLength);
oldLog.stream().forEach(LOG::info);
oldLog.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() {
}
abstract public void JSR(int pointer);
boolean singleTraceEnabled = false;
public String lastTrace = "START";
public void performSingleTrace() {
singleTraceEnabled = true;
}
public boolean isSingleTraceEnabled() {
return singleTraceEnabled;
}
public String getLastTrace() {
return lastTrace;
}
public void captureSingleTrace(String trace) {
lastTrace = trace;
singleTraceEnabled = false;
}
abstract public void clearState();
}

View File

@ -0,0 +1,150 @@
/*
* 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 final PagedMemory cxRom;
private final PagedMemory c8Rom;
private int slot;
private RAMListener ioListener;
private RAMListener firmwareListener;
private RAMListener c8firmwareListener;
/**
* Creates a new instance of Card
*
* @param computer
*/
public Card(Computer computer) {
super(computer);
cxRom = new PagedMemory(0x0100, PagedMemory.Type.CARD_FIRMWARE, computer);
c8Rom = new PagedMemory(0x0800, PagedMemory.Type.CARD_FIRMWARE, computer);
}
@Override
public String getShortName() {
return "s" + getSlot();
}
@Override
public String getName() {
return getDeviceName() + " (slot " + slot + ")";
}
abstract public void reset();
@Override
public void attach() {
registerListeners();
}
@Override
public void detach() {
suspend();
unregisterListeners();
super.detach();
}
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();
unregisterListeners();
if (restart) {
resume();
}
registerListeners();
}
public void notifyVBLStateChanged(boolean state) {
// Do nothing unless overridden
}
public boolean suspendWithCPU() {
return false;
}
protected void registerListeners() {
RAM memory = computer.getMemory();
int baseIO = 0x0c080 + slot * 16;
int baseRom = 0x0c000 + slot * 256;
ioListener = memory.observe(RAMEvent.TYPE.ANY, baseIO, baseIO + 15, (e) -> {
int address = e.getAddress() & 0x0f;
handleIOAccess(address, e.getType(), e.getNewValue(), e);
});
firmwareListener = memory.observe(RAMEvent.TYPE.ANY, baseRom, baseRom + 255, (e) -> {
computer.getMemory().setActiveCard(slot);
// Sather 6-4: Writes will still go through even when CXROM inhibits slot ROM
if (SoftSwitches.CXROM.isOff() || !e.getType().isRead()) {
handleFirmwareAccess(e.getAddress() & 0x0ff, e.getType(), e.getNewValue(), e);
}
});
c8firmwareListener = memory.observe(RAMEvent.TYPE.ANY, 0xc800, 0xcfff, (e) -> {
if (SoftSwitches.CXROM.isOff() && SoftSwitches.INTC8ROM.isOff()
&& computer.getMemory().getActiveSlot() == slot) {
handleC8FirmwareAccess(e.getAddress() - 0x0c800, e.getType(), e.getNewValue(), e);
}
});
}
protected void unregisterListeners() {
computer.getMemory().removeListener(ioListener);
computer.getMemory().removeListener(firmwareListener);
computer.getMemory().removeListener(c8firmwareListener);
}
}

View File

@ -0,0 +1,212 @@
/*
* 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.state.StateManager;
import java.io.IOException;
import java.util.Optional;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
/**
* 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 {
public RAM memory;
public CPU cpu;
public Video video;
public Keyboard keyboard;
public StateManager stateManager;
public Motherboard motherboard;
public boolean romLoaded;
@ConfigurableField(category = "advanced", name = "State management", shortName = "rewind", description = "This enables rewind support, but consumes a lot of memory when active.")
public boolean enableStateManager;
public final SoundMixer mixer;
final private BooleanProperty runningProperty = new SimpleBooleanProperty(false);
/**
* Creates a new instance of Computer
*/
public Computer() {
keyboard = new Keyboard(this);
mixer = new SoundMixer(this);
romLoaded = false;
}
public RAM getMemory() {
return memory;
}
public Motherboard getMotherboard() {
return motherboard;
}
ChangeListener<Boolean> runningPropertyListener = (prop, oldVal, newVal) -> runningProperty.set(newVal);
public void setMotherboard(Motherboard m) {
if (motherboard != null && motherboard.isRunning()) {
motherboard.suspend();
}
motherboard = m;
}
public BooleanProperty getRunningProperty() {
return runningProperty;
}
public boolean isRunning() {
return getRunningProperty().get();
}
public void notifyVBLStateChanged(boolean state) {
for (Optional<Card> c : getMemory().cards) {
c.ifPresent(card -> card.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);
romLoaded = true;
}
public void deactivate() {
if (cpu != null) {
cpu.suspend();
}
if (motherboard != null) {
motherboard.suspend();
}
if (video != null) {
video.suspend();
}
if (mixer != null) {
mixer.detach();
}
}
@InvokableAction(
name = "Cold boot",
description = "Process startup sequence from power-up",
category = "general",
alternatives = "Full reset;reset emulator",
consumeKeyEvent = true,
defaultKeyMapping = {"Ctrl+Shift+Backspace", "Ctrl+Shift+Delete"})
public void invokeColdStart() {
if (!romLoaded) {
Thread delayedStart = new Thread(() -> {
while (!romLoaded) {
Thread.yield();
}
coldStart();
});
delayedStart.start();
} else {
coldStart();
}
}
public abstract void coldStart();
@InvokableAction(
name = "Warm boot",
description = "Process user-initatiated reboot (ctrl+apple+reset)",
category = "general",
alternatives = "reboot;reset;three-finger-salute",
defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"})
public void invokeWarmStart() {
warmStart();
}
public abstract void warmStart();
public Keyboard getKeyboard() {
return this.keyboard;
}
protected abstract void doPause();
protected abstract void doResume();
@InvokableAction(name = "Pause", description = "Stops the computer, allowing reconfiguration of core elements", alternatives = "freeze;halt", defaultKeyMapping = {"meta+pause", "alt+pause"})
public boolean pause() {
boolean result = getRunningProperty().get();
doPause();
getRunningProperty().set(false);
return result;
}
@InvokableAction(name = "Resume", description = "Resumes the computer if it was previously paused", alternatives = "unpause;unfreeze;resume", defaultKeyMapping = {"meta+shift+pause", "alt+shift+pause"})
public void resume() {
doResume();
getRunningProperty().set(true);
}
@Override
public void reconfigure() {
mixer.reconfigure();
if (enableStateManager) {
stateManager = StateManager.getInstance(this);
} else {
stateManager = null;
StateManager.getInstance(this).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 final List<Integer> breakpoints = new ArrayList<>();
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,129 @@
/*
* 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;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
/**
* 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 {
protected Computer computer;
private Device() {
}
public Device(Computer computer) {
this.computer = computer;
}
// Number of cycles to do nothing (for cpu/video cycle accuracy)
@Stateful
private int waitCycles = 0;
@Stateful
private final BooleanProperty run = new SimpleBooleanProperty(true);
@Stateful
public boolean isPaused = false;
public BooleanProperty getRunningProperty() {
return run;
}
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.get()) {
// 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.get();
}
public synchronized void setRun(boolean run) {
// System.out.println(Thread.currentThread().getName() + (run ? " resuming " : " suspending ")+ getDeviceName());
isPaused = false;
this.run.set(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);
waitCycles = 0;
}
public abstract void attach();
public void detach() {
Keyboard.unregisterAllHandlers(this);
}
}

View File

@ -0,0 +1,74 @@
/*
* 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.io.InputStream;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.paint.Color;
/**
* 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 boolean initialized = false;
static public int getByte(int c, int yOffset) {
if (!initialized) {
initalize();
}
return font[c][yOffset];
}
private static void initalize() {
initialized = true;
font = new int[256][8];
Thread fontLoader = new Thread(() -> {
InputStream in = Font.class.getClassLoader().getResourceAsStream("jace/data/font.png");
Image image = new Image(in);
PixelReader reader = image.getPixelReader();
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++) {
Color color = reader.getColor((7 - k) + x, j + y);
boolean on = color.getRed() != 0;
row = (row << 1) | (on ? 0 : 1);
}
font[i][j] = row;
}
}
});
fontLoader.start();
}
/**
* Creates a new instance of Font
*/
private Font() {
}
}

View File

@ -0,0 +1,87 @@
/*
* 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 javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.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 KeyCombination keyCodeCombination;
public KeyCode key;
public KeyHandler(String comboText) {
KeyCode testCode = KeyCode.getKeyCode(comboText);
if (testCode != null) {
key = testCode;
} else {
init(KeyCodeCombination.valueOf(comboText));
}
}
public KeyHandler(KeyCodeCombination keyCodeCombo) {
init(keyCodeCombo);
}
private void init(KeyCombination keyCodeCombo) {
keyCodeCombination = keyCodeCombo;
if (keyCodeCombo instanceof KeyCodeCombination) {
key = ((KeyCodeCombination) keyCodeCombo).getCode();
}
}
public boolean match(KeyEvent e) {
if (keyCodeCombination != null) {
return keyCodeCombination.match(e);
} else {
return e.getCode().equals(key);
}
}
public abstract boolean handleKeyUp(KeyEvent e);
public abstract boolean handleKeyDown(KeyEvent e);
public String getComboName() {
if (keyCodeCombination != null) {
return keyCodeCombination.getName();
} else {
return key.getName();
}
}
public String getKeyName() {
if (key != null) {
return key.getName();
} else {
return null;
}
}
}

View File

@ -0,0 +1,380 @@
/*
* 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 jace.apple2e.SoftSwitches;
import jace.config.InvokableAction;
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.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
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;
import javafx.event.EventHandler;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.WindowEvent;
/**
* 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 {
public void resetState() {
clearStrobe();
openApple(false);
solidApple(false);
}
private Computer computer;
public Keyboard(Computer computer) {
this.computer = computer;
}
@Override
public String getShortName() {
return "kbd";
}
static byte currentKey = 0;
public boolean shiftPressed = false;
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<KeyCode, Set<KeyHandler>> keyHandlersByKey = new HashMap<>();
private static Map<Object, Set<KeyHandler>> keyHandlersByOwner = new HashMap<>();
public static void registerInvokableAction(InvokableAction action, Object owner, Method method, String code) {
boolean isStatic = Modifier.isStatic(method.getModifiers());
registerKeyHandler(new KeyHandler(code) {
@Override
public boolean handleKeyUp(KeyEvent e) {
Emulator.computer.getKeyboard().shiftPressed = e.isShiftDown();
if (action == null || !action.notifyOnRelease()) {
return false;
}
// System.out.println("Key up: "+method.toString());
Object returnValue = null;
try {
if (method.getParameterCount() > 0) {
returnValue = method.invoke(isStatic ? null : owner, false);
} else {
returnValue = method.invoke(isStatic ? null : owner);
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(Keyboard.class.getName()).log(Level.SEVERE, null, ex);
}
if (returnValue != null) {
return (Boolean) returnValue;
}
return action.consumeKeyEvent();
}
@Override
public boolean handleKeyDown(KeyEvent e) {
// System.out.println("Key down: "+method.toString());
Emulator.computer.getKeyboard().shiftPressed = e.isShiftDown();
Object returnValue = null;
try {
if (method.getParameterCount() > 0) {
returnValue = method.invoke(isStatic ? null : owner, true);
} else {
returnValue = method.invoke(isStatic ? null : owner);
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
Logger.getLogger(Keyboard.class.getName()).log(Level.SEVERE, null, ex);
}
if (returnValue != null) {
return (Boolean) returnValue;
}
return action != null ? action.consumeKeyEvent() : null;
}
}, owner);
}
public static void registerKeyHandler(KeyHandler l, Object owner) {
if (!keyHandlersByKey.containsKey(l.key)) {
keyHandlersByKey.put(l.key, new HashSet<>());
}
keyHandlersByKey.get(l.key).add(l);
if (!keyHandlersByOwner.containsKey(owner)) {
keyHandlersByOwner.put(owner, new HashSet<>());
}
keyHandlersByOwner.get(owner).add(l);
// System.out.println("Registered handler for "+l.getComboName()+"; code is "+l.getKeyName());
}
public static void unregisterAllHandlers(Object owner) {
if (!keyHandlersByOwner.containsKey(owner)) {
return;
}
keyHandlersByOwner.get(owner).stream().filter((handler) -> !(!keyHandlersByKey.containsKey(handler.key))).forEach((handler) -> {
keyHandlersByKey.get(handler.key).remove(handler);
});
keyHandlersByOwner.remove(owner);
}
public static void processKeyDownEvents(KeyEvent e) {
if (keyHandlersByKey.containsKey(e.getCode())) {
for (KeyHandler h : keyHandlersByKey.get(e.getCode())) {
if (!h.match(e)) {
continue;
}
boolean isHandled = h.handleKeyDown(e);
if (isHandled) {
e.consume();
return;
}
}
}
}
public static void processKeyUpEvents(KeyEvent e) {
if (keyHandlersByKey.containsKey(e.getCode())) {
for (KeyHandler h : keyHandlersByKey.get(e.getCode())) {
if (!h.match(e)) {
continue;
}
boolean isHandled = h.handleKeyUp(e);
if (isHandled) {
e.consume();
return;
}
}
}
}
public EventHandler<KeyEvent> getListener() {
return (KeyEvent event) -> {
if (event.getEventType() == KeyEvent.KEY_PRESSED) {
keyPressed(event);
} else if (event.getEventType() == KeyEvent.KEY_RELEASED) {
keyReleased(event);
}
};
}
public void keyPressed(KeyEvent e) {
processKeyDownEvents(e);
if (e.isConsumed()) {
return;
}
char c = 255;
if (e.getText().length() > 0) {
c = e.getText().charAt(0);
}
switch (e.getCode()) {
case LEFT:
case KP_LEFT:
c = 8;
break;
case RIGHT:
case KP_RIGHT:
c = 21;
break;
case UP:
case KP_UP:
c = 11;
break;
case DOWN:
case KP_DOWN:
c = 10;
break;
case ESCAPE:
c = 27;
break;
case TAB:
c = 9;
break;
case ENTER:
c = 13;
break;
case BACK_SPACE:
c = 127;
break;
default:
}
Emulator.computer.getKeyboard().shiftPressed = e.isShiftDown();
if (e.isShiftDown()) {
c = fixShiftedChar(c);
}
if (e.isControlDown()) {
if (c == 255) {
return;
}
c = (char) (c & 0x01f);
}
if (c < 128) {
pressKey((byte) c);
}
}
private char fixShiftedChar(char c) {
if (c >= 'a' && c <= 'z') {
return (char) (c - 32);
} else {
switch (c) {
case '0': return ')';
case '1': return '!';
case '2': return '@';
case '3': return '#';
case '4': return '$';
case '5': return '%';
case '6': return '^';
case '7': return '&';
case '8': return '*';
case '9': return '(';
case '-': return '_';
case '=': return '+';
case '[': return '{';
case ']': return '}';
case '\\': return '|';
case ';': return ':';
case '\'': return '"';
case ',': return '<';
case '.': return '>';
case '/': return '?';
case '`': return '~';
}
}
return c;
}
public void keyReleased(KeyEvent e) {
KeyCode code = e.getCode();
processKeyUpEvents(e);
if (code == null || e.isConsumed()) {
return;
}
e.consume();
}
@InvokableAction(name = "Open Apple Key", alternatives = "OA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = "Alt", consumeKeyEvent = false)
public void openApple(boolean pressed) {
computer.pause();
SoftSwitches.PB0.getSwitch().setState(pressed);
computer.resume();
}
@InvokableAction(name = "Closed Apple Key", alternatives = "CA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = {"Shortcut","Meta","Command"}, consumeKeyEvent = false)
public void solidApple(boolean pressed) {
computer.pause();
SoftSwitches.PB1.getSwitch().setState(pressed);
computer.resume();
}
public static void pasteFromString(String text) {
text = text.replaceAll("\\r?\\n|\\r", (char) 0x0d + "");
pasteBuffer = new StringReader(text);
}
@InvokableAction(name = "Paste clipboard", alternatives = "paste", category = "Keyboard", notifyOnRelease = false, defaultKeyMapping = {"Ctrl+Shift+V","Shift+Insert"}, consumeKeyEvent = true)
public static void pasteFromClipboard() {
try {
Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
String contents = (String) clip.getData(DataFlavor.stringFlavor);
if (contents != null && !"".equals(contents)) {
contents = contents.replaceAll("\\r?\\n|\\r", (char) 0x0d + "");
pasteBuffer = new StringReader(contents);
}
} catch (UnsupportedFlavorException | 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;
}
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,209 @@
/*
* 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.HashSet;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 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 {
final public Set<Device> miscDevices = new LinkedHashSet<>();
@ConfigurableField(name = "Enable Speaker", shortName = "speaker", defaultValue = "true")
public static boolean enableSpeaker = true;
public Speaker speaker;
void vblankEnd() {
SoftSwitches.VBL.getSwitch().setState(true);
computer.notifyVBLStateChanged(true);
}
void vblankStart() {
SoftSwitches.VBL.getSwitch().setState(false);
computer.notifyVBLStateChanged(false);
}
/**
* Creates a new instance of Motherboard
* @param computer
* @param oldMotherboard
*/
public Motherboard(Computer computer, Motherboard oldMotherboard) {
super(computer);
if (oldMotherboard != null) {
miscDevices.addAll(oldMotherboard.miscDevices);
speaker = oldMotherboard.speaker;
}
}
@Override
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;
@Override
public void tick() {
Optional<Card>[] cards = computer.getMemory().getAllCards();
try {
clockCounter--;
computer.getCpu().doTick();
if (clockCounter > 0) {
return;
}
clockCounter = cpuPerClock;
computer.getVideo().doTick();
for (Optional<Card> card : cards) {
card.ifPresent(c -> c.doTick());
}
miscDevices.stream().forEach((m) -> {
m.doTick();
});
} catch (Throwable t) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, t);
}
}
// 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)
@Override
public long defaultCyclesPerSecond() {
return SPEED;
}
@Override
public synchronized void reconfigure() {
boolean startAgain = pause();
accelorationRequestors.clear();
super.reconfigure();
// Now create devices as needed, e.g. sound
if (enableSpeaker) {
try {
if (speaker == null) {
speaker = new Speaker(computer);
if (computer.mixer.lineAvailable) {
speaker.attach();
miscDevices.add(speaker);
} else {
System.out.print("No lines available! Speaker not running.");
}
}
speaker.reconfigure();
} catch (Throwable t) {
System.out.println("Unable to initalize sound -- deactivating speaker out");
speaker.detach();
miscDevices.remove(speaker);
}
} else {
System.out.println("Speaker not enabled, leaving it off.");
if (speaker != null) {
speaker.detach();
miscDevices.remove(speaker);
}
}
if (startAgain && computer.getMemory() != null) {
resume();
}
}
static HashSet<Object> accelorationRequestors = new HashSet<>();
public void requestSpeed(Object requester) {
accelorationRequestors.add(requester);
enableTempMaxSpeed();
}
public void cancelSpeedRequest(Object requester) {
accelorationRequestors.remove(requester);
if (accelorationRequestors.isEmpty()) {
disableTempMaxSpeed();
}
}
@Override
public void attach() {
}
final Set<Card> resume = new HashSet<>();
@Override
public boolean suspend() {
synchronized (resume) {
resume.clear();
for (Optional<Card> c : computer.getMemory().getAllCards()) {
if (!c.isPresent()) {
continue;
}
if (!c.get().suspendWithCPU() || !c.get().isRunning()) {
continue;
}
if (c.get().suspend()) {
resume.add(c.get());
}
}
}
return super.suspend();
}
@Override
public void resume() {
super.resume();
synchronized (resume) {
resume.stream().forEach((c) -> {
c.resume();
});
}
}
@Override
public void detach() {
System.out.println("Detaching motherboard");
miscDevices.stream().forEach((d) -> {
d.suspend();
d.detach();
});
miscDevices.clear();
// halt();
super.detach();
}
}

View File

@ -0,0 +1,148 @@
/*
* 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 {
CARD_FIRMWARE(0x0c800),
LANGUAGE_CARD(0x0d000),
FIRMWARE_MAIN(0x0d000),
FIRMWARE_80COL(0x0c300),
SLOW_ROM(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
*/
Computer computer;
public PagedMemory(int size, Type memType, Computer computer) {
this.computer = computer;
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, computer);
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 javafx.scene.paint.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] = Color.rgb(0, 0, 0);
color[ 1] = Color.rgb(208, 0, 48);
color[ 2] = Color.rgb( 0, 0,128);
color[ 3] = Color.rgb(255, 0,255);
color[ 4] = Color.rgb( 0,128, 0);
color[ 5] = Color.rgb(128,128,128);
color[ 6] = Color.rgb( 0, 0,255);
color[ 7] = Color.rgb( 96,160,255);
color[ 8] = Color.rgb(128, 80, 0);
color[ 9] = Color.rgb(255,128, 0);
color[10] = Color.rgb(192,192,192);
color[11] = Color.rgb(255,144,128);
color[12] = Color.rgb( 0,255, 0);
color[13] = Color.rgb(255,255, 0);
color[14] = Color.rgb( 64,255,144);
color[15] = Color.rgb(255,255,255);
}
}

View File

@ -0,0 +1,340 @@
/*
* 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.Optional;
/**
* 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;
public Optional<Card>[] cards;
// card 0 = 80 column card firmware / system rom
public int activeSlot = 0;
protected final Computer computer;
/**
* Creates a new instance of RAM
*
* @param computer
*/
public RAM(Computer computer) {
this.computer = computer;
listeners = new ArrayList<>();
cards = new Optional[8];
for (int i = 0; i < 8; i++) {
cards[i] = Optional.empty();
}
refreshListenerMap();
}
public void setActiveCard(int slot) {
if (activeSlot != slot) {
activeSlot = slot;
configureActiveMemory();
} else if (!SoftSwitches.CXROM.getState()) {
configureActiveMemory();
}
}
public int getActiveSlot() {
return activeSlot;
}
public Optional<Card>[] getAllCards() {
return cards;
}
public Optional<Card> getCard(int slot) {
if (slot >= 1 && slot <= 7) {
return cards[slot];
}
return Optional.empty();
}
public void addCard(Card c, int slot) {
removeCard(slot);
cards[slot] = Optional.of(c);
c.setSlot(slot);
c.attach();
}
public void removeCard(Card c) {
c.suspend();
c.detach();
removeCard(c.getSlot());
}
public void removeCard(int slot) {
cards[slot].ifPresent(Card::suspend);
cards[slot].ifPresent(Card::detach);
cards[slot] = Optional.empty();
}
abstract public void configureActiveMemory();
public void write(int address, byte b, boolean generateEvent, boolean requireSynchronization) {
byte[] page = activeWrite.getMemoryPage(address);
if (page == null) {
if (generateEvent) {
callListener(RAMEvent.TYPE.WRITE, address, 0, b, requireSynchronization);
}
} else {
int offset = address & 0x0FF;
byte old = page[offset];
if (generateEvent) {
page[offset] = callListener(RAMEvent.TYPE.WRITE, address, old, b, requireSynchronization);
} else {
page[offset] = b;
}
}
}
public void writeWord(int address, int w, boolean generateEvent, boolean requireSynchronization) {
write(address, (byte) (w & 0x0ff), generateEvent, requireSynchronization);
write(address + 1, (byte) (w >> 8), generateEvent, requireSynchronization);
}
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;
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];
listeners.stream().forEach((l) -> {
addListenerRange(l);
});
}
public RAMListener observe(RAMEvent.TYPE type, int address, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(address);
}
@Override
protected void doEvent(RAMEvent e) {
handler.handleEvent(e);
}
});
}
public RAMListener observe(RAMEvent.TYPE type, int address, boolean auxFlag, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(address);
}
@Override
protected void doEvent(RAMEvent e) {
if (isAuxFlagCorrect(e, auxFlag)) {
handler.handleEvent(e);
}
}
});
}
public RAMListener observe(RAMEvent.TYPE type, int addressStart, int addressEnd, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(addressStart);
setScopeEnd(addressEnd);
}
@Override
protected void doEvent(RAMEvent e) {
handler.handleEvent(e);
}
});
}
public RAMListener observe(RAMEvent.TYPE type, int addressStart, int addressEnd, boolean auxFlag, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(addressStart);
setScopeEnd(addressEnd);
}
@Override
protected void doEvent(RAMEvent e) {
if (isAuxFlagCorrect(e, auxFlag)) {
handler.handleEvent(e);
}
}
});
}
private boolean isAuxFlagCorrect(RAMEvent e, boolean auxFlag) {
if (e.getAddress() < 0x0100) {
if (SoftSwitches.AUXZP.getState() != auxFlag) {
return false;
}
} else if (SoftSwitches.RAMRD.getState() != auxFlag) {
return false;
}
return true;
}
public RAMListener addListener(final RAMListener l) {
boolean restart = computer.pause();
if (listeners.contains(l)) {
return l;
}
listeners.add(l);
addListenerRange(l);
if (restart) {
computer.resume();
}
return l;
}
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;
if (requireSyncronization) {
computer.getCpu().suspend();
}
if ((address & 0x0FF00) == 0x0C000) {
activeListeners = ioListenerMap[address & 0x0FF];
if (activeListeners == null && t.isRead()) {
if (requireSyncronization) {
computer.getCpu().resume();
}
return computer.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);
activeListeners.stream().forEach((l) -> {
l.handleEvent(e);
});
if (requireSyncronization) {
computer.getCpu().resume();
}
return (byte) e.getNewValue();
}
if (requireSyncronization) {
computer.getCpu().resume();
}
return (byte) newValue;
}
abstract protected void loadRom(String path) throws IOException;
abstract public void attach();
abstract public void detach();
abstract public void performExtendedCommand(int i);
}

View File

@ -0,0 +1,142 @@
/*
* 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 {
static public interface RAMEventHandler {
public void handleEvent(RAMEvent e);
}
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
* @param t
* @param s
* @param v
* @param address
* @param oldValue
* @param newValue
*/
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,170 @@
/*
* 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 implements RAMEvent.RAMEventHandler {
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
* @param t
* @param s
* @param v
*/
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;
}
@Override
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,291 @@
/*
* 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;
protected Computer computer;
/**
* 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 (e.getType().isRead()) {
e.setNewValue(computer.getVideo().getFloatingBus());
}
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(Computer computer) {
this.computer = computer;
RAM m = computer.getMemory();
listeners.stream().forEach((l) -> {
m.addListener(l);
});
}
public void unregister() {
RAM m = computer.getMemory();
listeners.stream().forEach((l) -> {
m.removeListener(l);
});
this.computer = null;
}
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.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,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.core;
import jace.config.ConfigurableField;
import jace.config.DynamicSelection;
import jace.config.Reconfigurable;
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 java.util.regex.Pattern;
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<>());
private final Map<Object, SourceDataLine> activeLines = Collections.synchronizedMap(new HashMap<>());
/**
* 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;
@ConfigurableField(name = "Mute", shortName = "mute")
public static boolean MUTE = false;
/**
* 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;
public SoundMixer(Computer computer) {
super(computer);
}
@Override
public String getDeviceName() {
return "Sound Output";
}
@Override
public String getShortName() {
return "mixer";
}
@Override
public synchronized void reconfigure() {
if (MUTE) {
detach();
} else if (isConfigDifferent()) {
detach();
try {
initMixer();
if (lineAvailable) {
initAudio();
} 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();
}
}
private AudioFormat getAudioFormat() {
return new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, RATE, BITS, 2, BITS / 4, RATE, true);
}
/**
* Obtain sound playback line if available
*
* @throws javax.sound.sampled.LineUnavailableException If there is no line
* available
*/
private void initAudio() throws LineUnavailableException {
af = getAudioFormat();
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;
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();
if (sdl.isRunning()) {
sdl.flush();
sdl.stop();
}
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 = l;
sdl.open();
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) -> {
((Reconfigurable) o).reconfigure();
});
if (theMixer != null) {
for (Line l : theMixer.getSourceLines()) {
// if (l.isOpen()) {
// l.close();
// }
}
}
availableLines.clear();
activeLines.clear();
super.detach();
}
private void initMixer() {
Info selected;
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;
}
String oldPreferredMixer = null;
private boolean isConfigDifferent() {
boolean changed = false;
AudioFormat newAf = getAudioFormat();
changed |= (af == null || !newAf.matches(af));
if (oldPreferredMixer == null) {
changed |= preferredMixer.getValue() != null;
} else {
changed |= !oldPreferredMixer.matches(Pattern.quote(preferredMixer.getValue()));
}
oldPreferredMixer = preferredMixer.getValue();
return changed;
}
}

View File

@ -0,0 +1,182 @@
/*
* 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
* @param computer
*/
public TimedDevice(Computer computer) {
super(computer);
setSpeed(cyclesPerSecond);
}
@ConfigurableField(name = "Speed", description = "(Percentage)")
public int speedRatio = 100;
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) (cyclesPerInterval * NANOS_PER_SECOND / 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() {
cyclesPerSecond = defaultCyclesPerSecond() * speedRatio / 100;
if (cyclesPerSecond == 0) {
cyclesPerSecond = defaultCyclesPerSecond();
}
setSpeed(cyclesPerSecond);
}
public abstract long defaultCyclesPerSecond();
}

View File

@ -0,0 +1,475 @@
/*
* 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.io.File;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.reflections.Reflections;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
/**
* 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 {
static Reflections reflections = new Reflections("jace");
public static Set<Class> findAllSubclasses(Class clazz) {
return reflections.getSubTypesOf(clazz);
}
//------------------------------ 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<String> c, String d) {
return c.stream().collect(Collectors.joining(d));
}
private static boolean isHeadless = false;
public static void setHeadlessMode(boolean headless) {
isHeadless = headless;
}
public static boolean isHeadlessMode() {
return isHeadless;
}
public static Optional<Image> loadIcon(String filename) {
if (isHeadless) {
return Optional.empty();
}
InputStream stream = Utility.class.getClassLoader().getResourceAsStream("jace/data/" + filename);
return Optional.of(new Image(stream));
}
public static Optional<Label> loadIconLabel(String filename) {
if (isHeadless) {
return Optional.empty();
}
Image img = loadIcon(filename).get();
Label label = new Label() {
@Override
public boolean equals(Object obj) {
if (obj instanceof Label) {
Label l2 = (Label) obj;
return super.equals(l2) || l2.getText().equals(getText());
} else {
return super.equals(obj);
}
}
@Override
public int hashCode() {
return getText().hashCode();
}
};
label.setGraphic(new ImageView(img));
label.setAlignment(Pos.CENTER);
label.setContentDisplay(ContentDisplay.TOP);
label.setTextFill(Color.WHITE);
DropShadow shadow = new DropShadow(5.0, Color.BLACK);
label.setEffect(shadow);
return Optional.of(label);
}
public static void confirm(String title, String message, Runnable accept) {
Platform.runLater(() -> {
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setContentText(message);
confirm.setTitle(title);
Optional<ButtonType> response = confirm.showAndWait();
response.ifPresent(b -> {
if (b.getButtonData().isDefaultButton()) {
(new Thread(accept)).start();
}
});
});
}
// 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) {
Platform.runLater(() -> {
Alert errorAlert = new Alert(Alert.AlertType.ERROR);
errorAlert.setContentText(message);
errorAlert.setTitle("Error");
errorAlert.show();
});
}
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,301 @@
/*
* 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 jace.state.Stateful;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
/**
* 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
WritableImage video;
WritableImage visible;
VideoWriter currentWriter;
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
*
* @param computer
*/
public Video(Computer computer) {
super(computer);
suspend();
video = new WritableImage(560, 192);
visible = new WritableImage(560, 192);
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 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;
Runnable redrawScreen = () -> {
if (visible != null && video != null) {
// if (computer.getRunningProperty().get()) {
screenDirty = false;
visible.getPixelWriter().setPixels(0, 0, 560, 192, video.getPixelReader(), 0, 0);
// }
}
};
public void redraw() {
javafx.application.Platform.runLater(redrawScreen);
}
public void vblankStart() {
if (screenDirty && isRunning()) {
redraw();
}
}
abstract public void vblankEnd();
abstract public void hblankStart(WritableImage screen, int y, boolean isDirty);
public void setScannerLocation(int loc) {
scannerAddress = loc;
}
@Override
public void tick() {
setScannerLocation(currentWriter.getYOffset(y));
setFloatingBus(computer.getMemory().readRaw(scannerAddress + x));
if (hPeriod > 0) {
hPeriod--;
if (hPeriod == 0) {
x = -1;
}
} else {
if (!isVblank && x < APPLE_CYCLES_PER_LINE) {
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);
}
x = hblankOffsetX - 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();
computer.getMotherboard().vblankStart();
} else {
y = 0;
isVblank = false;
vblankEnd();
computer.getMotherboard().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",
defaultKeyMapping = {"ctrl+shift+r"})
public static final void forceRefresh() {
if (Emulator.computer != null && Emulator.computer.video != null) {
Emulator.computer.video._forceRefresh();
}
}
private void _forceRefresh() {
lineDirty = true;
screenDirty = true;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1;
}
@Override
public String getShortName() {
return "vid";
}
public Image getFrameBuffer() {
return visible;
}
}

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 javafx.scene.image.WritableImage;
/**
* 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(WritableImage 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,664 @@
/*
* 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.EmulatorUILogic;
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 javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
/**
* 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 {
@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 Point2D lastMouseLocation;
@Stateful
public Rectangle2D clampWindow = new Rectangle2D(0, 0, 0x03ff, 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;
@ConfigurableField(name = "Blazing Paddles fix", shortName = "bpfix", category = "Mouse", description = "Use different clamping values to make Blazing Paddles work more reliably.")
public boolean blazingPaddles = false;
Label mouseActive = Utility.loadIconLabel("input-mouse.png").orElse(null);
public boolean movedSinceLastTick = false;
public boolean movedSinceLastRead = false;
public CardAppleMouse(Computer computer) {
super(computer);
}
@Override
public String getDeviceName() {
return "Apple Mouse";
}
@Override
public void reset() {
mode = 0;
clampWindow = new Rectangle2D(0, 0, 0x03ff, 0x03ff);
deactivateMouse();
}
EventHandler<MouseEvent> mouseHandler = this::processMouseEvent;
private void processMouseEvent(MouseEvent event) {
if (event.getEventType() == MouseEvent.MOUSE_MOVED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
Node source = (Node) event.getSource();
updateLocation(event.getSceneX(), event.getSceneY(), source.getBoundsInLocal());
event.consume();
}
if (event.getEventType() == MouseEvent.MOUSE_PRESSED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
mousePressed(event);
event.consume();
} else if (event.getEventType() == MouseEvent.MOUSE_RELEASED) {
mouseReleased(event);
event.consume();
}
}
private void updateLocation(double x, double y, Bounds bounds) {
double scaledX = x / bounds.getWidth();
double scaledY = y / bounds.getHeight();
lastMouseLocation = new Point2D(scaledX, scaledY);
movedSinceLastTick = true;
movedSinceLastRead = true;
}
/*
* 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;
case 8:
getMouseClamp();
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;
case 0x1A:
e.setNewValue(0x088);
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.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;
}
//Interrupt on VBL
interruptOnVBL = ((mode & 8) != 0);
//Mouse off (0) or on (1)
if ((mode & 1) == 0) {
deactivateMouse();
interruptOnMove = false;
interruptOnPress = false;
return;
}
//Interrupt if mouse is moved
interruptOnMove = ((mode & 2) != 0);
//Interrupt if button is pressed
interruptOnPress = ((mode & 4) != 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.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 (min >= 32768) {
min -= 65536;
}
if (max >= 32768) {
max -= 65536;
}
if (getCPU().A == 0) {
if (blazingPaddles) {
min = -1;
max = 281;
}
setClampWindowX(min, max);
} else {
if (blazingPaddles) {
min = -1;
max = 193;
}
setClampWindowY(min, max);
}
}
/*
* $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.setText("Active");
EmulatorUILogic.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 Point2D(0, 0);
updateMouseState();
getCPU().C = 0;
}
/**
* Described in Apple Mouse technical note #7
* Cn1A: Read mouse clamping values
* Register number is stored in $478 and ranges from x47 to x4e
* Return value should be stored in $5782
* Values should be returned in this order:
* MinXH, MinYH, MinXL, MinYL, MaxXH, MaxYH, MaxXL, MaxYL
*/
private void getMouseClamp() {
byte reg = computer.getMemory().readRaw(0x0478);
byte val = 0;
switch (reg - 0x047) {
case 0:
val = (byte) ((int) clampWindow.getMinX() >> 8);
break;
case 1:
val = (byte) ((int) clampWindow.getMinY() >> 8);
break;
case 2:
val = (byte) ((int) clampWindow.getMinX() & 255);
break;
case 3:
val = (byte) ((int) clampWindow.getMinY() & 255);
break;
case 4:
val = (byte) ((int) clampWindow.getMaxX() >> 8);
break;
case 5:
val = (byte) ((int) clampWindow.getMaxY() >> 8);
break;
case 6:
val = (byte) ((int) clampWindow.getMaxX() & 255);
break;
case 7:
val = (byte) ((int) clampWindow.getMaxY() & 255);
break;
}
computer.getMemory().write(0x0578, val, false, false);
}
/*
* This is called whenever the mouse firmware has been activated in software
*/
private void activateMouse() {
active = true;
EmulatorUILogic.addMouseListener(mouseHandler);
}
/*
* 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;
EmulatorUILogic.removeMouseListener(mouseHandler);
}
@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) {
if (movedSinceLastTick) {
isInterrupt = true;
getCPU().generateInterrupt();
}
}
movedSinceLastTick = false;
}
@Override
public void notifyVBLStateChanged(boolean state) {
// VBL is false when it is the vertical blanking period
if (!state && interruptOnVBL) {
isVBL = true;
isInterrupt = true;
getCPU().generateInterrupt();
}
}
private void updateMouseState() {
double x = lastMouseLocation.getX();
x *= clampWindow.getWidth();
x += clampWindow.getMinX();
x = Math.min(Math.max(x, clampWindow.getMinX()), clampWindow.getMaxX());
double y = lastMouseLocation.getY();
y *= clampWindow.getHeight();
y += clampWindow.getMinY();
y = Math.min(Math.max(y, clampWindow.getMinY()), clampWindow.getMaxY());
PagedMemory m = ((RAM128k) computer.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;
if (button1pressLast) {
status |= 1;
}
if (interruptOnMove && movedSinceLastRead) {
status |= 2;
}
if (interruptOnPress && (button0press != button0pressLast || button1press != button1pressLast)) {
status |= 4;
}
if (isVBL) {
status |= 8;
}
if (button1press) {
status |= 16;
}
if (movedSinceLastRead) {
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));
button0pressLast = button0press;
button1pressLast = button1press;
movedSinceLastRead = false;
}
public void mousePressed(MouseEvent me) {
MouseButton button = me.getButton();
if (button == MouseButton.PRIMARY || button == MouseButton.MIDDLE) {
button0press = true;
}
if (button == MouseButton.SECONDARY || button == MouseButton.MIDDLE) {
button1press = true;
}
}
public void mouseReleased(MouseEvent me) {
MouseButton button = me.getButton();
if (button == MouseButton.PRIMARY || button == MouseButton.MIDDLE) {
button0press = false;
}
if (button == MouseButton.SECONDARY || button == MouseButton.MIDDLE) {
button1press = false;
}
}
private void setClampWindowX(int min, int max) {
// Fix for GEOS clamping funkiness
if (max == 32767) {
max = 560;
}
clampWindow = new Rectangle2D(min, clampWindow.getMinY(), max, clampWindow.getMaxY());
}
private void setClampWindowY(int min, int max) {
// Fix for GEOS clamping funkiness
if (max == 32767) {
max = 192;
}
clampWindow = new Rectangle2D(clampWindow.getMinX(), min, clampWindow.getMaxX(), 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,235 @@
/*
* 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.EmulatorUILogic;
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 jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
import java.io.File;
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(computer);
DiskIIDrive drive2 = new DiskIIDrive(computer);
@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;
@ConfigurableField(category = "Disk", defaultValue = "", shortName = "d1", name = "Drive 1 disk image", description = "Path of disk 1")
public String disk1;
@ConfigurableField(category = "Disk", defaultValue = "", shortName = "d2", name = "Drive 2 disk image", description = "Path of disk 2")
public String disk2;
public CardDiskII(Computer computer) {
super(computer);
try {
loadRom("jace/data/DiskII.rom");
} catch (IOException ex) {
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
}
drive1.setIcon(Utility.loadIconLabel("disk_ii.png"));
drive2.setIcon(Utility.loadIconLabel("disk_ii.png"));
reset();
}
@Override
public String getDeviceName() {
return "Disk ][ Controller";
}
@Override
public void reset() {
currentDrive = drive1;
drive1.reset();
drive2.reset();
EmulatorUILogic.removeIndicators(drive1);
EmulatorUILogic.removeIndicators(drive2);
// 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);
currentDrive.removeIndicator();
break;
case 0x9:
// drive on
currentDrive.setOn(true);
currentDrive.addIndicator();
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
}
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();
try {
if (disk1 != null && !disk1.isEmpty()) {
drive1.insertDisk(new File(disk1));
disk1 = null;
}
if (disk2 != null && !disk2.isEmpty()) {
drive2.insertDisk(new File(disk2));
disk2 = null;
}
} catch (IOException ex) {
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void tweakTiming() {
if ((drive1.isOn() && drive1.disk != null) || (drive2.isOn() && drive2.disk != null)) {
if (USE_MAX_SPEED) {
computer.getMotherboard().requestSpeed(this);
}
} else {
computer.getMotherboard().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().ifPresent(icon->icon.setText("S" + slot + "D1"));
drive2.getIcon().ifPresent(icon->icon.setText("S" + slot + "D2"));
}
@Override
public MediaConsumer[] getConsumers() {
return new MediaConsumer[]{drive1, drive2};
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.Computer;
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(Computer computer) {
super(computer);
auxMemory = new PagedMemory(0xc000, PagedMemory.Type.RAM, computer);
auxLanguageCard = new PagedMemory(0x3000, PagedMemory.Type.LANGUAGE_CARD, computer);
auxLanguageCard2 = new PagedMemory(0x1000, PagedMemory.Type.LANGUAGE_CARD, computer);
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,147 @@
/*
* 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.Computer;
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(Computer computer) {
super(computer);
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,428 @@
/*
* 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.LockSupport;
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
@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 = 2;
// The array of configured AY chips
public PSG[] chips;
// The 6522 controllr chips (always 2)
public R6522[] controllers;
static private int ticksBetweenPlayback = 200;
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(Computer computer) {
super(computer);
controllers = new R6522[2];
for (int i = 0; i < 2; i++) {
//don't ask...
final int j = i;
controllers[i] = new R6522(computer) {
int controller = j;
@Override
public void sendOutputA(int value) {
chips[j].setBus(value);
if (phasorMode) {
chips[j + 2].setBus(value);
}
}
@Override
public void sendOutputB(int value) {
if (phasorMode) {
if ((chips[j].mask & value) != 0) {
chips[j].setControl(value & 0x07);
}
if ((chips[j + 2].mask & value) != 0) {
chips[j + 2].setControl(value & 0x07);
}
} else {
chips[j].setControl(value & 0x07);
}
}
@Override
public int receiveOutputA() {
return chips[j] == null ? 0 : chips[j].bus;
}
@Override
public int receiveOutputB() {
return 0;
}
@Override
public String getShortName() {
return "timer" + j;
}
};
}
}
@Override
public void reset() {
suspend();
}
RAMListener mainListener = null;
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
resume();
int chip = 0;
for (PSG psg : chips) {
if (psg.getBaseReg() == (register & 0x0f0)) {
break;
}
chip++;
}
if (chip >= 2) {
System.err.println("Could not determine which PSG to communicate to");
e.setNewValue(computer.getVideo().getFloatingBus());
return;
}
R6522 controller = controllers[chip & 1];
if (e.getType().isRead()) {
int val = controller.readRegister(register & 0x0f);
e.setNewValue(val);
// System.out.println("Read "+Integer.toHexString(register)+" == "+val);
} else {
controller.writeRegister(register & 0x0f, e.getNewValue());
// System.out.println("Write "+Integer.toHexString(register)+" == "+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.
// System.out.println("MB I/O Access "+type.name()+" "+register+":"+value);
e.setNewValue(computer.getVideo().getFloatingBus());
}
long ticksSinceLastPlayback = 0;
@Override
public void tick() {
for (R6522 c : controllers) {
if (c == null || !c.isRunning()) {
continue;
}
c.tick();
}
if (isRunning() && !pause) {
// buildMixerTable();
timerSync.lock();
try {
ticksSinceLastPlayback++;
if (ticksSinceLastPlayback >= ticksBetweenPlayback) {
cpuCountReached.signalAll();
while (isRunning() && ticksSinceLastPlayback >= ticksBetweenPlayback) {
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(phasorMode ? CLOCK_SPEED * 2 : 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 = (MAX_AMPLITUDE * 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) (out / Math.pow(Math.sqrt(2),(15-i)));
// 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 (PSG psg : chips) {
psg.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SAMPLE_RATE);
psg.reset();
}
}
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 = computer.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();
ticksBetweenPlayback = (int) ((Motherboard.SPEED * BUFFER_LENGTH) / SAMPLE_RATE);
System.out.println("Ticks between playback: "+ticksBetweenPlayback);
ticksSinceLastPlayback = 0;
int zeroSamples = 0;
setRun(true);
LockSupport.parkNanos(5000);
while (isRunning()) {
while (isRunning() && !computer.isRunning()) {
Thread.currentThread().yield();
}
if (isRunning()) {
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 -= ticksBetweenPlayback;
} finally {
timerSync.unlock();
}
out.write(buffer, 0, buffer.length);
if (zeroSamples >= MAX_IDLE_SAMPLES) {
zeroSamples = 0;
pause = true;
computer.getMotherboard().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 < ticksBetweenPlayback) {
computer.getMotherboard().requestSpeed(this);
cpuCountReached.await();
computer.getMotherboard().cancelSpeedRequest(this);
}
} 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 {
computer.getMotherboard().cancelSpeedRequest(this);
System.out.println("Mockingboard playback stopped");
computer.mixer.returnLine(this);
}
}
private void initPSG() {
if (phasorMode) {
chips = new PSG[4];
chips[0] = new PSG(0x10, CLOCK_SPEED * 2, SAMPLE_RATE, "AY1", 8);
chips[1] = new PSG(0x80, CLOCK_SPEED * 2, SAMPLE_RATE, "AY2", 8);
chips[2] = new PSG(0x10, CLOCK_SPEED * 2, SAMPLE_RATE, "AY3", 16);
chips[3] = new PSG(0x80, CLOCK_SPEED * 2, SAMPLE_RATE, "AY4", 16);
} else {
chips = new PSG[2];
chips[0] = new PSG(0, CLOCK_SPEED, SAMPLE_RATE, "AY1", 255);
chips[1] = new PSG(0x80, CLOCK_SPEED, SAMPLE_RATE, "AY2", 255);
}
}
@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,237 @@
/*
* 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.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.state.Stateful;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.scene.control.Label;
/**
* 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";
}
Optional<Label> indicator;
public CardRamFactor(Computer computer) {
super(computer);
indicator = Utility.loadIconLabel("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) {
computer.getMotherboard().requestSpeed(this);
}
}
@Override
public void tick() {
// Do nothing
}
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
if (speedBoost) {
computer.getMotherboard().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.ifPresent(icon->
icon.setText("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,147 @@
/*
* 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.class);
memoryBank.put(BankType.MAIN_MEMORY, new PagedMemory(0xc000, PagedMemory.Type.RAM, computer));
memoryBank.put(BankType.LANGUAGE_CARD_1, new PagedMemory(0x3000, PagedMemory.Type.LANGUAGE_CARD, computer));
memoryBank.put(BankType.LANGUAGE_CARD_2, new PagedMemory(0x1000, PagedMemory.Type.LANGUAGE_CARD, computer));
return memoryBank;
}
public static enum BankType {
MAIN_MEMORY, LANGUAGE_CARD_1, LANGUAGE_CARD_2
};
public CardRamworks(Computer computer) {
super(computer);
memory = new ArrayList<>(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;
} else if (maxBank > 128) {
maxBank = 128;
}
for (int i = memory.size(); i < maxBank; i++) {
memory.add(null);
}
configureActiveMemory();
if (resume) {
computer.resume();
}
}
private RAMListener bankSelectListener;
@Override
public void attach() {
bankSelectListener = computer.getMemory().observe(RAMEvent.TYPE.WRITE, BANK_SELECT, (e) -> {
currentBank = e.getNewValue();
configureActiveMemory();
});
}
@Override
public void detach() {
removeListener(bankSelectListener);
}
}

View File

@ -0,0 +1,509 @@
/*
* 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.EmulatorUILogic;
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.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.scene.control.Label;
/**
* 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 {
@ConfigurableField(name = "TCP/IP Port", shortName = "port")
public short IP_PORT = 1977;
protected ServerSocket socket;
protected Socket clientSocket;
protected BufferedReader socketInput;
protected Thread listenThread;
private int lastInputByte = 0;
private boolean FULL_ECHO = true;
private final 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 final 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 CardSSC(Computer computer) {
super(computer);
}
@Override
public String getDeviceName() {
return "Super Serial Card";
}
Label 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);
Utility.loadIconLabel("network-wired.png").ifPresent(icon->{
activityIndicator = icon;
activityIndicator.setText("Slot " + slot);
});
}
boolean newInputAvailable = false;
public void socketMonitor() {
try {
socket = new ServerSocket(IP_PORT);
socket.setReuseAddress(true);
socket.setSoTimeout(0);
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
suspend();
return;
}
while (socket != null && !socket.isClosed()) {
try {
Logger.getLogger(CardSSC.class.getName()).log(Level.INFO, "Slot " + getSlot() + " listening on port " + IP_PORT, (Throwable) null);
while ((clientSocket = socket.accept()) != null) {
socketInput = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
clientConnected();
clientSocket.setTcpNoDelay(true);
while (isConnected()) {
try {
Thread.sleep(10);
if (socketInput.ready()) {
newInputAvailable = true;
}
} catch (InterruptedException ex) {
// Do nothing
}
}
clientDisconnected();
hangUp();
socketInput = null;
}
Thread.yield();
} 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() {
Thread resetThread = new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
suspend();
resume();
});
resetThread.start();
}
@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) {
EmulatorUILogic.addIndicator(this, activityIndicator);
newValue = getInputByte();
}
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 (newInputAvailable || inputAvailable()) {
newValue |= 0x08;
}
// 4 = ACIA Transmit Register empty (1)
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 = DTR ? 1 : 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) {
EmulatorUILogic.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);
}
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
}
@Override
public void tick() {
if (RECV_IRQ_ENABLED && newInputAvailable) {
newInputAvailable = false;
triggerIRQ();
}
}
public boolean inputAvailable() throws IOException {
if (isConnected() && clientSocket != null && socketInput != null) {
// return socketInput.available() > 0;
return socketInput.ready();
} else {
return false;
}
}
private int getInputByte() throws IOException {
if (inputAvailable()) {
int in = socketInput.read() & DATA_BITS;
if (RECV_STRIP_LF && in == 10 && lastInputByte == 13) {
in = socketInput.read() & DATA_BITS;
}
lastInputByte = in;
}
return lastInputByte;
}
long lastSuccessfulWrite = -1L;
private void sendOutputByte(int i) throws IOException {
if (clientSocket != null && clientSocket.isConnected()) {
try {
clientSocket.getOutputStream().write(i & DATA_BITS);
if (TRANS_ADD_LF && (i & DATA_BITS) == 13) {
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.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
*
* @return
*/
@Override
public boolean suspend() {
synchronized (this) {
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.interrupt();
listenThread.join(100);
} catch (InterruptedException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
}
listenThread = null;
socket = null;
return super.suspend();
}
}
@Override
public void resume() {
synchronized (this) {
if (!isRunning()) {
super.resume();
RECV_IRQ_ENABLED = false;
TRANS_IRQ_ENABLED = false;
IRQ_TRIGGERED = false;
//socket.setReuseAddress(true);
listenThread = new Thread(this::socketMonitor);
listenThread.setDaemon(false);
listenThread.setName("SSC port listener, slot" + getSlot());
listenThread.start();
}
}
}
@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
}
@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,334 @@
/*
* 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.EmulatorUILogic;
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.Optional;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.scene.control.Label;
/**
* 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 {
Optional<Label> clockIcon;
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(Computer computer) {
super(computer);
try {
loadRom("jace/data/thunderclock_plus.rom");
} catch (IOException ex) {
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
}
clockIcon = Utility.loadIconLabel("clock.png");
}
// 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(computer);
}
getTime();
clockIcon.ifPresent(icon->{
icon.setText("Slot " + getSlot());
long now = System.currentTimeMillis();
if ((now - lastShownIcon) > MIN_WAIT) {
EmulatorUILogic.addIndicator(this, icon, 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.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<>();
} 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;
static 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.
* @param computer
*/
public static void performProdosPatch(Computer computer) {
PagedMemory ram = computer.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());
Utility.loadIconLabel("clock_fix.png").ifPresent(clockFixIcon->{
clockFixIcon.setText("Fixed");
EmulatorUILogic.addIndicator(CardThunderclock.class, 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,313 @@
/*
* 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.EmulatorUILogic;
import jace.core.Computer;
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.Optional;
import java.util.Set;
import java.util.concurrent.locks.LockSupport;
import javafx.scene.control.Label;
/**
* 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 {
Computer computer;
public DiskIIDrive(Computer computer) {
this.computer = computer;
}
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<>();
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];
if (isOn()) {
nibbleOffset++;
if (nibbleOffset >= FloppyDisk.TRACK_NIBBLE_LENGTH) {
nibbleOffset = 0;
}
}
} else {
result = (byte) 0x0ff;
}
}
} 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, computer);
}
}
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) {
dirtyTracks.stream().forEach((track) -> {
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(() -> {
long diff;
// 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, computer);
dirtyTracks = new HashSet<>();
// Emulator state has changed significantly, reset state manager
StateManager.getInstance(computer).invalidate();
}
private Optional<Label> icon;
@Override
public Optional<Label> getIcon() {
return icon;
}
@Override
public void setIcon(Optional<Label> i) {
icon = i;
}
// Optionals make some things easier, but they slow down things considerably when called a lot
// This reduces the number of Optional checks when rapidly accessing the disk drive.
long lastAdded = 0;
public void addIndicator() {
long now = System.currentTimeMillis();
if (lastAdded == 0 || now - lastAdded >= 500) {
EmulatorUILogic.addIndicator(this, icon.get());
lastAdded = now;
}
}
public void removeIndicator() {
if (lastAdded > 0) {
EmulatorUILogic.removeIndicator(this, icon.get());
lastAdded = 0;
}
}
private MediaEntry currentMediaEntry;
private MediaFile currentMediaFile;
@Override
public void eject() {
if (disk == null) {
return;
}
waitForPendingWrites();
disk = null;
dirtyTracks = new HashSet<>();
// Emulator state has changed significantly, reset state manager
StateManager.getInstance(computer).invalidate();
}
@Override
public void insertMedia(MediaEntry e, MediaFile f) throws IOException {
if (!isAccepted(e, f)) {
return;
}
eject();
insertDisk(f.path);
currentMediaEntry = e;
currentMediaFile = f;
}
@Override
public MediaEntry getMediaEntry() {
return currentMediaEntry;
}
@Override
public MediaFile getMediaFile() {
return currentMediaFile;
}
@Override
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,422 @@
/*
* 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.core.Computer;
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
* @param computer
* @throws IOException
*/
public FloppyDisk(File diskFile, Computer computer) throws IOException {
FileInputStream input = new FileInputStream(diskFile);
String name = diskFile.getName().toUpperCase();
readDisk(input, name.endsWith(".PO"), computer);
writeProtected = !diskFile.canWrite();
diskPath = diskFile;
}
// brendanr: refactored to use input stream
public void readDisk(InputStream diskFile, boolean prodosOrder, Computer computer) 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, computer);
StateManager.markDirtyValue(currentSectorOrder, computer);
}
/*
* 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++) {
int gap2 = (int) ((Math.random() * 5.0) + 4);
// 15 junk bytes
writeJunkBytes(output, 15);
// Address block
writeAddressBlock(output, track, sector);
// 4 junk bytes
writeJunkBytes(output, gap2);
// Data block
nibblizeBlock(output, track, currentSectorOrder[sector], nibbles);
// 34 junk bytes
writeJunkBytes(output, 38 - gap2);
}
}
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) {
// Locate start of 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);
} 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,262 @@
/*
* 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.config.InvokableAction;
import jace.core.Computer;
import jace.core.Device;
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.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 = "Center Mouse", shortName = "center", description = "Moves mouse back to the center of the screen, can get annoying.")
public boolean centerMouse = false;
@ConfigurableField(name = "Use keyboard", shortName = "useKeys", description = "Arrow keys will control joystick instead of the mouse.")
public boolean useKeyboard = true;
@ConfigurableField(name = "Hog keypresses", shortName = "hog", description = "Key presses will not be sent to emulator.")
public boolean hogKeyboard = false;
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, Computer computer) {
super(computer);
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 ? -128 : 0) + (rightPressed ? 255 : 128);
joyY = (upPressed ? -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();
super.detach();
}
@Override
public void reconfigure() {
removeListeners();
x = 0;
y = 0;
registerListeners();
}
@InvokableAction(name = "Left", category = "joystick", defaultKeyMapping = "left", notifyOnRelease = true)
public boolean joystickLeft(boolean pressed) {
if (!useKeyboard) {
return false;
}
leftPressed = pressed;
if (pressed) {
rightPressed = false;
}
return hogKeyboard;
}
;
@InvokableAction(name = "Right", category = "joystick", defaultKeyMapping = "right", notifyOnRelease = true)
public boolean joystickRight(boolean pressed) {
if (!useKeyboard) {
return false;
}
rightPressed = pressed;
if (pressed) {
leftPressed = false;
}
return hogKeyboard;
}
;
@InvokableAction(name = "Up", category = "joystick", defaultKeyMapping = "up", notifyOnRelease = true)
public boolean joystickUp(boolean pressed) {
if (!useKeyboard) {
return false;
}
upPressed = pressed;
if (pressed) {
downPressed = false;
}
return hogKeyboard;
}
;
@InvokableAction(name = "Down", category = "joystick", defaultKeyMapping = "down", notifyOnRelease = true)
public boolean joystickDown(boolean pressed) {
if (!useKeyboard) {
return false;
}
downPressed = pressed;
if (pressed) {
upPressed = false;
}
return hogKeyboard;
}
public void initJoystickRead(RAMEvent e) {
readJoystick();
xSwitch.setState(true);
x = 10 + joyX * 11;
ySwitch.setState(true);
y = 10 + joyY * 11;
e.setNewValue(computer.getVideo().getFloatingBus());
resume();
}
RAMListener listener;
private void registerListeners() {
listener = computer.getMemory().observe(RAMEvent.TYPE.ANY, 0x0c070, 0x0c07f, this::initJoystickRead);
}
private void removeListeners() {
computer.getMemory().removeListener(listener);
}
}

View File

@ -0,0 +1,169 @@
package jace.hardware;
import jace.EmulatorUILogic;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField;
import jace.core.Computer;
import jace.core.Device;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.Utility;
import java.util.Calendar;
import java.util.Optional;
import javafx.scene.control.Label;
/**
* Provide No-slot-clock compatibility
*
* @author blurry
*/
public class NoSlotClock extends Device {
boolean clockActive;
public long detectSequence = 0x5ca33ac55ca33ac5L;
public long testSequence = 0;
public long testMask = 0;
public long dataRegister = 0;
public long dataRegisterBit = 0;
public int patternCount = 0;
public boolean writeEnabled = false;
@ConfigurableField(category = "Clock", name = "Patch Prodos", description = "If enabled, prodos clock routines will be patched directly", defaultValue = "false")
public boolean patchProdosClock = false;
Optional<Label> clockIcon;
private final RAMListener listener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0C100);
setScopeEnd(0x0CFFF);
}
@Override
public boolean isRelevant(RAMEvent e) {
// Ref: Sather UAIIe 5-28
if (SoftSwitches.CXROM.isOn()) {
return true;
}
if ((e.getAddress() & 0x0ff00) == 0x0c300 && SoftSwitches.SLOTC3ROM.isOff()) {
return true;
}
return e.getAddress() >= 0x0c800 && SoftSwitches.INTC8ROM.isOn();
}
@Override
protected void doEvent(RAMEvent e) {
boolean readMode = (e.getAddress() & 0x04) != 0;
if (clockActive) {
if (readMode) {
int val = e.getOldValue() & 0b011111110;
int bit = getNextDataBit();
val |= bit;
e.setNewValue(val);
} else if (writeEnabled) {
fakeWrite(e);
} else {
return;
}
dataRegisterBit++;
if (dataRegisterBit >= 64) {
deactivateClock();
}
} else if (readMode) {
writeEnabled = true;
testSequence = detectSequence;
patternCount = 0;
} else if (writeEnabled) {
int bit = e.getAddress() & 0x01;
if (bit == (testSequence & 0x01)) {
testSequence >>= 1;
patternCount++;
if (patternCount == 64) {
activateClock();
}
} else {
writeEnabled = false;
}
}
}
};
public NoSlotClock(Computer computer) {
super(computer);
this.clockIcon = Utility.loadIconLabel("clock.png");
this.clockIcon.ifPresent(icon -> icon.setText("No Slot Clock"));
}
@Override
protected String getDeviceName() {
return "No Slot Clock";
}
@Override
public String getShortName() {
return "clock";
}
@Override
public void tick() {
}
@Override
public void reconfigure() {
}
@Override
public void attach() {
computer.getMemory().addListener(listener);
}
@Override
public void detach() {
computer.getMemory().removeListener(listener);
}
public void activateClock() {
Calendar now = Calendar.getInstance();
dataRegisterBit = 0;
dataRegister = 0L;
storeBCD(now.get(Calendar.MILLISECOND) / 10, 0);
storeBCD(now.get(Calendar.SECOND), 1);
storeBCD(now.get(Calendar.MINUTE), 2);
storeBCD(now.get(Calendar.HOUR), 3);
storeBCD(now.get(Calendar.DAY_OF_WEEK), 4);
storeBCD(now.get(Calendar.DAY_OF_MONTH), 5);
storeBCD(now.get(Calendar.MONTH) + 1, 6);
storeBCD(now.get(Calendar.YEAR) % 100, 7);
clockActive = true;
clockIcon.ifPresent(icon
-> EmulatorUILogic.addIndicator(this, icon, 1000));
if (patchProdosClock) {
CardThunderclock.performProdosPatch(computer);
}
}
public void storeBCD(int val, int offset) {
storeNibble(val % 10, offset * 8);
storeNibble(val / 10, offset * 8 + 4);
}
public void storeNibble(int val, int offset) {
for (int i = 0; i < 4; i++) {
if ((val & 1) != 0) {
dataRegister |= (1L << (offset + i));
}
val >>= 1;
}
}
public void deactivateClock() {
clockActive = false;
}
public int getNextDataBit() {
return (int) ((dataRegister >> dataRegisterBit) & 0x01);
}
public void fakeWrite(RAMEvent e) {
}
}

View File

@ -0,0 +1,596 @@
/*
* 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.DynamicSelection;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import java.util.LinkedHashMap;
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.Receiver;
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 {
private Receiver midiOut;
public PassportMidiInterface(Computer computer) {
super(computer);
}
@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, SINGLE_SHOT, FREQ_COMPARISON, PULSE_COMPARISON
};
@ConfigurableField(name = "Midi Output Device", description = "Midi output device")
public static DynamicSelection<String> preferredMidiDevice = new DynamicSelection<String>(null) {
@Override
public boolean allowNull() {
return false;
}
@Override
public LinkedHashMap<? extends String, String> getSelections() {
LinkedHashMap<String, String> out = new LinkedHashMap<>();
MidiDevice.Info[] devices = MidiSystem.getMidiDeviceInfo();
for (MidiDevice.Info dev : devices) {
try {
MidiDevice device = MidiSystem.getMidiDevice(dev);
if (device.getMaxReceivers() > 0 || dev instanceof Synthesizer)
System.out.println("MIDI Device found: " + dev);
out.put(dev.getName(), dev.getName());
} catch (MidiUnavailableException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}
}
return out;
}
};
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 TIMER3_PRESCALED = 1;
public static final int TIMER3_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_16BIT = 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 final PTMTimer[] ptmTimer = {
new PTMTimer(),
new PTMTimer(),
new PTMTimer()
};
private boolean ptmStatusReadSinceIRQ = false;
// ---------------------- ACIA CONFIGURATION
private final boolean aciaInterruptOnSend = false;
private final boolean aciaInterruptOnReceive = false;
// ---------------------- ACIA STATUS BITS
// True when MIDI IN receives a byte
private final boolean receivedACIAByte = false;
// True when data is not transmitting (always true because we aren't really doing wire transmission);
private final boolean transmitACIAEmpty = true;
// True if another byte is received before the previous byte was processed
private final boolean receiverACIAOverrun = false;
// True if ACIA generated interrupt request
private final boolean irqRequestedACIA = false;
@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
}
@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 & TIMER3_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.getCpu().generateInterrupt();
ptmStatusReadSinceIRQ = false;
}
if (t.mode == TIMER_MODE.CONTINUOUS || t.mode == TIMER_MODE.FREQ_COMPARISON) {
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_16BIT) != 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.PULSE_COMPARISON;
break;
case PTM_FREQ_COMP:
timer.mode = TIMER_MODE.FREQ_COMPARISON;
break;
case PTM_SINGLE_SHOT:
timer.mode = TIMER_MODE.SINGLE_SHOT;
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 (midiOut != null) {
// Send message
try {
// System.out.println("Sending MIDI message "+currentMessageStatus+","+currentMessageData1+","+currentMessageData2);
currentMessage.setMessage(currentMessageStatus, currentMessageData1, currentMessageData2);
midiOut.send(currentMessage, -1L);
} catch (InvalidMidiDataException 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() && midiOut != null) {
return;
}
try {
MidiDevice selectedDevice = MidiSystem.getSynthesizer();
MidiDevice.Info[] devices = MidiSystem.getMidiDeviceInfo();
if (devices.length == 0) {
System.out.println("No MIDI devices found");
} else {
for (MidiDevice.Info dev : devices) {
if (MidiSystem.getMidiDevice(dev).getMaxReceivers() == 0) {
continue;
}
System.out.println("MIDI Device found: " + dev);
if ((preferredMidiDevice.getValue() == null && dev.getName().contains("Java Sound") && dev instanceof Synthesizer) ||
preferredMidiDevice.getValue().equalsIgnoreCase(dev.getName())
) {
selectedDevice = MidiSystem.getMidiDevice(dev);
break;
}
}
}
if (selectedDevice != null) {
System.out.println("Selected MIDI device: " + selectedDevice.getDeviceInfo().getName());
selectedDevice.open();
midiOut = selectedDevice.getReceiver();
super.resume();
}
} catch (MidiUnavailableException ex) {
System.out.println("Could not open MIDI synthesizer");
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void suspendACIA() {
// TODO: Stop ACIA thread...
if (midiOut != null) {
currentMessage = new ShortMessage();
// Send a note-off on every channel
for (int channel = 0; channel < 16; channel++) {
try {
// All Notes Off
currentMessage.setMessage(0x0B0 | channel, 123, 0);
midiOut.send(currentMessage, 0);
// All Oscillators Off
currentMessage.setMessage(0x0B0 | channel, 120, 0);
midiOut.send(currentMessage, 0);
} catch (InvalidMidiDataException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}
}
midiOut.close();
midiOut = null;
}
}
}

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.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 {
Computer computer;
public ProdosDriver(Computer computer) {
this.computer = computer;
}
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.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.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.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,129 @@
/*
* 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 {
Computer computer;
public SmartportDriver(Computer computer) {
this.computer = computer;
}
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.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.getCpu();
RAM ram = computer.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;
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,283 @@
/*
* 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.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.hardware.ProdosDriver;
import jace.hardware.SmartportDriver;
import jace.library.MediaCache;
import jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
import jace.library.MediaEntry;
import java.io.File;
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 {
@ConfigurableField(category = "Disk", defaultValue = "", shortName = "d1", name = "Drive 1 disk image", description = "Path of disk 1")
public String disk1;
@ConfigurableField(category = "Disk", defaultValue = "", shortName = "d2", name = "Drive 2 disk image", description = "Path of disk 2")
public String disk2;
MassStorageDrive drive1;
MassStorageDrive drive2;
public CardMassStorage(Computer computer) {
super(computer);
drive1 = new MassStorageDrive();
drive2 = new MassStorageDrive();
drive1.setIcon(Utility.loadIconLabel("drive-harddisk.png"));
drive2.setIcon(Utility.loadIconLabel("drive-harddisk.png"));
drive1.onInsert(this::reconfigure);
currentDrive = drive1;
}
@Override
public void setSlot(int slot) {
super.setSlot(slot);
drive1.getIcon().ifPresent(icon -> icon.setText("S" + getSlot() + "D1"));
drive2.getIcon().ifPresent(icon -> icon.setText("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(computer) {
@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, computer.getMemory());
}
@Override
public void mliWrite(int block, int bufferAddress) throws IOException {
getCurrentDisk().mliWrite(block, bufferAddress, computer.getMemory());
}
@Override
public Card getOwner() {
return theCard;
}
};
@Override
public void reconfigure() {
unregisterListeners();
if (disk1 != null && !disk1.isEmpty()) {
try {
MediaEntry entry = MediaCache.getMediaFromFile(new File(disk1));
disk1 = null;
drive1.insertMedia(entry, entry.files.get(0));
} catch (IOException ex) {
Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (disk2 != null && !disk2.isEmpty()) {
try {
MediaEntry entry = MediaCache.getMediaFromFile(new File(disk2));
disk2 = null;
drive2.insertMedia(entry, entry.files.get(0));
} catch (IOException ex) {
Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (computer.getCpu() != null) {
int pc = computer.getCpu().getProgramCounter();
if (drive1.getCurrentDisk() != null && getSlot() == 7 && (pc >= 0x0c65e && pc <= 0x0c66F)) {
// 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;
EmulatorUILogic.simulateCtrlAppleReset();
}
}
registerListeners();
}
@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.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(), computer);
} 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.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(computer) {
@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, computer.getMemory());
}
@Override
public void write(int blockNum, int buffer) throws IOException {
getCurrentDisk().mliWrite(blockNum, buffer, computer.getMemory());
}
@Override
public ERROR_CODE returnStatus(int dataBuffer, int[] params) {
return ERROR_CODE.NO_ERROR;
}
};
@Override
public MediaConsumer[] getConsumers() {
return new MediaConsumer[]{drive1, drive2};
}
}

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.massStorage;
import static jace.hardware.massStorage.IDisk.BLOCK_SIZE;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
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 final byte STANDARD_PERMISSIONS = (byte) 0x0c3;
public static final int PRODOS_VERSION = 0x023;
public static final int FILE_ENTRY_SIZE = 0x027;
public static final int ENTRIES_PER_BLOCK = (ProdosVirtualDisk.BLOCK_SIZE - 4) / FILE_ENTRY_SIZE;
private boolean isRoot;
private List<DiskNode> directoryEntries;
public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, int baseBlock, boolean root) throws IOException {
super(ownerFilesystem, baseBlock);
init(ownerFilesystem, physicalDir, root);
}
public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, boolean root) throws IOException {
super(ownerFilesystem);
init(ownerFilesystem, physicalDir, root);
}
private void init(ProdosVirtualDisk ownerFilesystem, File physicalFile, boolean root) throws IOException {
isRoot = root;
directoryEntries = new ArrayList<>();
setPhysicalFile(physicalFile);
setType(EntryType.SUBDIRECTORY);
setName(physicalFile.getName());
allocate();
}
@Override
public void doDeallocate() {
}
@Override
public void doAllocate() throws IOException {
for (int i = 1; i < getBlockCount(); i++) {
if (isRoot) {
new SubNode(i, this, getOwnerFilesystem().getNextFreeBlock(3));
} else {
new SubNode(i, this);
}
}
for (File f : physicalFile.listFiles()) {
addFile(f);
}
Collections.sort(children, (DiskNode o1, DiskNode o2) -> 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 (!allocated) {
allocate();
} else {
try {
if (!super.checkFile()) {
return false;
}
HashSet<String> realFiles = new HashSet<>();
File[] realFileList = physicalFile.listFiles(this);
for (File f : realFileList) {
realFiles.add(f.getName());
}
for (Iterator<DiskNode> i = directoryEntries.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!
realFiles.stream().forEach((fileName) -> {
addFile(new File(physicalFile, fileName));
});
}
} catch (IOException ex) {
return false;
}
}
return success;
}
@Override
public void readBlock(int block, byte[] buffer) throws IOException {
checkFile();
int start = 0;
int end = 0;
int offset = 4;
generatePointers(buffer, block);
// System.out.println("Directory "+getName()+" sequence "+block+"; physical block "+getNodeSequence(block).getBaseBlock());
if (block == 0) {
generateHeader(buffer);
offset += FILE_ENTRY_SIZE;
end = ENTRIES_PER_BLOCK - 1;
} else {
start = (block * ENTRIES_PER_BLOCK) - 1;
end = start + ENTRIES_PER_BLOCK;
}
for (int i = start; i < end && i < directoryEntries.size(); i++, offset += FILE_ENTRY_SIZE) {
// TODO: Add any parts that are not file entries.
// System.out.println("Entry "+i+": "+children.get(i).getName()+"; offset "+offset);
generateFileEntry(buffer, offset, i);
}
}
@Override
public boolean accept(File file) {
if (file.getName().endsWith("~")) {
return false;
}
char c = file.getName().charAt(0);
if (c == '.' || c == '~') {
return false;
}
return !file.isHidden();
}
private void generatePointers(byte[] buffer, int sequence) {
DiskNode prev = getNodeSequence(sequence - 1);
DiskNode next = getNodeSequence(sequence + 1);
// System.out.println("Sequence "+sequence+" prev="+(prev != null ? prev.getBaseBlock() : 0)+"; next="+(next != null ? next.getBaseBlock() : 0));
// Previous block (or 0)
generateWord(buffer, 0, prev != null ? prev.getBaseBlock() : 0);
// Next block (or 0)
generateWord(buffer, 0x02, next != null ? next.getBaseBlock() : 0);
}
/**
* 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) {
// Directory header + name length
// Volumme header = 0x0f0; Subdirectory header = 0x0e0
buffer[4] = (byte) ((isRoot ? 0x0F0 : 0x0E0) | getName().length());
generateName(buffer, 5, this);
if (!isRoot) {
buffer[0x014] = 0x075;
buffer[0x015] = PRODOS_VERSION;
buffer[0x017] = STANDARD_PERMISSIONS;
buffer[0x018] = FILE_ENTRY_SIZE;
buffer[0x019] = ENTRIES_PER_BLOCK;
}
generateTimestamp(buffer, 0x01c, getPhysicalFile().lastModified());
// Prodos 1.0 = 0
buffer[0x020] = PRODOS_VERSION;
// Minimum version = 0 (no min)
buffer[0x021] = 0x000;
// Directory may be read/written to, may not be destroyed or renamed
buffer[0x022] = STANDARD_PERMISSIONS;
// Entry size
buffer[0x023] = (byte) FILE_ENTRY_SIZE;
// Entries per block
buffer[0x024] = (byte) ENTRIES_PER_BLOCK;
// Directory items count
generateWord(buffer, 0x025, directoryEntries.size()+1);
if (isRoot) {
// Volume bitmap pointer
generateWord(buffer, 0x027, ownerFilesystem.freespaceBitmap.getBaseBlock());
// Total number of blocks
generateWord(buffer, 0x029, ownerFilesystem.MAX_BLOCK);
} else {
// According to the Beneath Apple Prodos supplement
int indexInParent = getParent().getChildren().indexOf(this) + 2;
int parentBlock = getParent().getNodeSequence(indexInParent / ENTRIES_PER_BLOCK).getBaseBlock();
// Parent pointer
generateWord(buffer, 0x027, parentBlock);
buffer[0x029] = (byte) (indexInParent % ENTRIES_PER_BLOCK);
buffer[0x02a] = (byte) FILE_ENTRY_SIZE;
}
}
/**
* 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 {
DiskNode child = directoryEntries.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
generateWord(buffer, offset + 0x013, child.additionalNodes.size() + 1);
// EOF (file size or directory structure size
int length = child.getLength();
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.0
buffer[offset + 0x01c] = PRODOS_VERSION;
// Minimum version = 0
buffer[offset + 0x01d] = 0;
// Access = Read-only
buffer[offset + 0x01e] = STANDARD_PERMISSIONS;
// 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 + 0] = (byte) (((((c.get(Calendar.MONTH) + 1) & 7) << 5) | c.get(Calendar.DAY_OF_MONTH)) & 0x0ff);
buffer[offset + 1] = (byte) (((c.get(Calendar.YEAR) - 2000) << 1) | ((c.get(Calendar.MONTH) + 1) >> 3));
buffer[offset + 2] = (byte) c.get(Calendar.MINUTE);
buffer[offset + 3] = (byte) c.get(Calendar.HOUR_OF_DAY);
}
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 < 15; i++) {
buffer[offset + i] = (byte) node.getName().charAt(i);
}
}
private Optional<DiskNode> findChildByFilename(String name) {
return directoryEntries.stream().filter((child) -> child.getPhysicalFile().getName().equals(name)).findFirst();
}
private void addFile(File file) {
if (!hasChildNamed(file.getName())) {
try {
if (file.isDirectory()) {
addFileEntry(new DirectoryNode(getOwnerFilesystem(), file, false));
} else {
addFileEntry(new FileNode(getOwnerFilesystem(), file));
}
} catch (IOException ex) {
Logger.getLogger(DirectoryNode.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
private void addFileEntry(DiskNode entry) {
directoryEntries.add(entry);
entry.setParent(this);
}
@Override
public int getLength() {
return getBlockCount() * BLOCK_SIZE;
}
private int getBlockCount() {
return isRoot ? 4 : 1 + (physicalFile.listFiles().length / ENTRIES_PER_BLOCK);
}
}

View File

@ -0,0 +1,281 @@
/*
* 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;
import java.util.Optional;
/**
* 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;
private int baseBlock = -1;
List<DiskNode> additionalNodes;
ProdosVirtualDisk ownerFilesystem;
File physicalFile;
DiskNode parent;
List<DiskNode> children;
private EntryType type;
private String name;
public DiskNode(ProdosVirtualDisk fs) throws IOException {
init(fs);
fs.allocateEntry(this);
}
public DiskNode(ProdosVirtualDisk fs, int blockNumber) throws IOException {
init(fs);
fs.allocateEntryNear(this, blockNumber);
}
private void init(ProdosVirtualDisk fs) throws IOException {
additionalNodes = new ArrayList<>();
children = new ArrayList<>();
setOwnerFilesystem(fs);
}
public boolean checkFile() throws IOException {
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;
}
}
public void deallocate() {
if (allocated) {
ownerFilesystem.deallocateEntry(this);
doDeallocate();
allocationTime = -1L;
allocated = false;
additionalNodes.clear();
// NOTE: This is recursive!
getChildren().stream().forEach((node) -> {
node.deallocate();
});
}
}
public void refresh() throws IOException {
deallocate();
doRefresh();
allocate();
}
public DiskNode getNodeSequence(int num) {
if (num == 0) {
return this;
} else if (num > 0 && num <= additionalNodes.size()) {
return additionalNodes.get(num-1);
} else {
return null;
}
}
/**
* @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
*/
private void setOwnerFilesystem(ProdosVirtualDisk ownerFilesystem) throws IOException {
this.ownerFilesystem = ownerFilesystem;
}
/**
* @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());
lastCheckTime = physicalFile.lastModified();
}
/**
* @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) {
child.setParent(this);
children.add(child);
}
public void removeChild(DiskNode child) {
children.remove(child);
}
public boolean hasChildNamed(String name) {
return findChildByFilename(name).isPresent();
}
private Optional<DiskNode> findChildByFilename(String name) {
return getChildren().stream().filter((child) -> child.getPhysicalFile().getName().equals(name)).findFirst();
}
/**
* @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) {
this.name = (name.length() > 15 ? name.substring(0, 15) : 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 abstract int getLength();
public void readBlock(byte[] buffer) throws IOException {
checkFile();
readBlock(0, buffer);
}
}

View File

@ -0,0 +1,222 @@
/*
* 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 {
@Override
public int getLength() {
return (int) getPhysicalFile().length();
}
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 static FileType findByCode(int code) {
for (FileType t : FileType.values()) {
if (t.code == code) {
return t;
}
}
return UNKNOWN;
}
}
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;
} else {
setType(EntryType.TREE);
return EntryType.TREE;
}
}
@Override
public void setName(String name) {
FileType t = FileType.UNKNOWN;
int offset = 0;
String prodosName = name;
if (name.matches("^.*?#[0-9A-Fa-f]{6}$")) {
int type = Integer.parseInt(name.substring(name.length() - 6, name.length() - 4), 16);
offset = Integer.parseInt(name.substring(name.length() - 4), 16);
t = FileType.findByCode(type);
prodosName = name.substring(0, name.length()-7).replaceAll("[^A-Za-z0-9#]", ".").toUpperCase();
} else {
String[] parts = name.replaceAll("[^A-Za-z0-9#]", ".").split("\\.");
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!");
}
prodosName = "";
for (int i = 0; i < parts.length - 1; i++) {
prodosName += (i > 0 ? "." + parts[i] : parts[i]);
}
if (extParts[extParts.length - 1].equals("SYSTEM")) {
prodosName += ".SYSTEM";
}
}
}
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(prodosName);
}
public FileNode(ProdosVirtualDisk ownerFilesystem, File file) throws IOException {
super(ownerFilesystem);
setPhysicalFile(file);
setName(file.getName());
allocate();
}
@Override
public void doDeallocate() {
}
@Override
public void doAllocate() throws IOException {
int dataBlocks = (int) ((getPhysicalFile().length() + ProdosVirtualDisk.BLOCK_SIZE - 1) / ProdosVirtualDisk.BLOCK_SIZE);
int treeBlocks = (((dataBlocks * 2) + (ProdosVirtualDisk.BLOCK_SIZE - 2)) / ProdosVirtualDisk.BLOCK_SIZE);
if (treeBlocks > 1) {
treeBlocks++;
}
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 {
allocate();
int dataBlocks = (int) ((getPhysicalFile().length() + ProdosVirtualDisk.BLOCK_SIZE - 1) / ProdosVirtualDisk.BLOCK_SIZE);
int treeBlocks = (((dataBlocks * 2) + (ProdosVirtualDisk.BLOCK_SIZE - 2)) / ProdosVirtualDisk.BLOCK_SIZE);
if (treeBlocks > 1) {
treeBlocks++;
}
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, 1, dataBlocks + 1);
}
break;
case TREE:
if (block == 0) {
generateIndex(buffer, 1, treeBlocks);
} else if (block <= treeBlocks) {
int start = treeBlocks + ((block - 1) * 256);
int end = treeBlocks + dataBlocks;
generateIndex(buffer, start, end);
} else {
readFile(buffer, (block - treeBlocks - 1));
}
break;
}
}
private void readFile(byte[] buffer, int start) throws IOException {
try (FileInputStream f = new FileInputStream(physicalFile)) {
f.skip(start * ProdosVirtualDisk.BLOCK_SIZE);
f.read(buffer, 0, ProdosVirtualDisk.BLOCK_SIZE);
}
}
private void generateIndex(byte[] buffer, int indexStart, int indexLimit) {
for (int i = indexStart, count = 0; count < 256 && i < indexLimit && i <= additionalNodes.size(); i++, count++) {
int base = getNodeSequence(i).getBaseBlock();
buffer[count] = (byte) (base & 0x0ff);
buffer[count + 256] = (byte) (base >> 8);
}
}
}

View File

@ -0,0 +1,71 @@
/*
* 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 {
super(fs, start);
allocate();
}
@Override
public void doDeallocate() {
//
}
@Override
public void doAllocate() throws IOException {
for (int i = 1; i < size; i++) {
SubNode subNode = new SubNode(i, this, getBaseBlock());
}
}
@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;
for (int i = startBlock; i < endBlock; i++) {
if (!getOwnerFilesystem().isBlockAllocated(i)) {
int pos = (i - startBlock) / 8;
int bit = 1 << (i % 8);
buffer[pos] |= bit;
}
}
}
@Override
public int getLength() {
return (1 + getChildren().size()) * IDisk.BLOCK_SIZE;
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.core.Computer;
import jace.core.RAM;
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 = 0x07fff;
public void mliFormat() throws IOException;
public void mliRead(int block, int bufferAddress, RAM memory) throws IOException;
public void mliWrite(int block, int bufferAddress, RAM memory) throws IOException;
public void boot0(int slot, Computer computer) throws IOException;
// Return size in 512k blocks
public int getSize();
public void eject();
public boolean isWriteProtected();
}

View File

@ -0,0 +1,176 @@
/*
* 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);
}
}
@Override
public void mliFormat() throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void mliRead(int block, int bufferAddress, RAM memory) throws IOException {
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);
}
}
}
@Override
public void mliWrite(int block, int bufferAddress, RAM memory) throws IOException {
if (block < physicalBlocks) {
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);
}
}
@Override
public void boot0(int slot, Computer computer) throws IOException {
computer.pause();
mliRead(0, 0x0800, computer.getMemory());
byte slot16 = (byte) (slot << 4);
((MOS65C02) computer.getCpu()).X = slot16;
RAM memory = computer.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);
computer.getCpu().setProgramCounter(0x0800);
computer.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 {
if (fis != null) {
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,135 @@
/*
* 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.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.scene.control.Label;
/**
*
* @author brobert
*/
public class MassStorageDrive implements MediaConsumer {
IDisk disk = null;
Optional<Label> icon = null;
@Override
public Optional<Label> getIcon() {
return icon;
}
/**
*
* @param i
*/
@Override
public void setIcon(Optional<Label> i) {
icon = i;
}
MediaEntry currentEntry;
MediaFile currentFile;
/**
*
* @param e
* @param f
* @throws IOException
*/
@Override
public void insertMedia(MediaEntry e, MediaFile f) throws IOException {
eject();
currentEntry = e;
currentFile = f;
disk= readDisk(currentFile.path);
if (postInsertAction != null) {
postInsertAction.run();
}
}
Runnable postInsertAction = null;
public void onInsert(Runnable r) {
postInsertAction = r;
}
/**
*
* @return
*/
@Override
public MediaEntry getMediaEntry() {
return currentEntry;
}
/**
*
* @return
*/
@Override
public MediaFile getMediaFile() {
return currentFile;
}
/**
*
* @param e
* @param f
* @return
*/
@Override
public boolean isAccepted(MediaEntry e, MediaFile f) {
return e.type.isProdosOrdered;
}
/**
*
*/
@Override
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;
}
}

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