37 Commits

Author SHA1 Message Date
Brendan Robert
6b1d75be15 Remove lawless music selection, add additional default cards 2024-07-07 16:23:41 -05:00
Brendan Robert
d0a77855cd Clean up the first-run funkiness a little bit 2024-07-02 01:33:10 -05:00
Brendan Robert
137988ac43 Delete nbactions.xml 2024-07-01 14:29:21 -05:00
Brendan Robert
037cc35676 Delete nb-configuration.xml 2024-07-01 14:29:12 -05:00
Brendan Robert
eaf1212249 Update LICENSE to Apache 2.0
It makes more sense to use a license that is clearer about using this within other commercial products.
2024-07-01 14:17:13 -05:00
Brendan Robert
40b92dea51 Fix broken images 2024-07-01 14:10:15 -05:00
Brendan Robert
81cb05848e Updated readme 2024-07-01 13:49:18 -05:00
Brendan Robert
61e98bfc7d formatting correction 2024-07-01 12:07:12 -05:00
Brendan Robert
b9f3368696 Update readme and remove defunct build scripts 2024-07-01 12:06:05 -05:00
Brendan Robert
e3b2484ec2 Fix wolfenstein cheats for new version 2024-07-01 11:29:24 -05:00
Brendan Robert
2155fe02e6 Complete modernization and overhaul (too many things to list here) 2024-06-30 14:46:31 -05:00
Brendan Robert
9674e59f1e Merge pull request #39 from badvision/dependabot/maven/junit-junit-4.13.1
Bump junit from 4.10 to 4.13.1
2021-09-28 18:16:52 -05:00
dependabot[bot]
774f706f68 Bump junit from 4.10 to 4.13.1
Bumps [junit](https://github.com/junit-team/junit4) from 4.10 to 4.13.1.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.10.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.10...r4.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 01:37:17 +00:00
Brendan Robert
cc0cead894 Added better standalone testing for command line arguments. Got video mode to be a first-class startup parameter 2019-06-19 00:47:04 -05:00
Brendan Robert
553d439ff8 Merge pull request #36 from Michaelangel007/master
Fix spelling, and detect if maven isn't installed.
2018-11-29 12:52:49 -06:00
Brendan Robert
15be6e3436 Added accelerator support, also refined how child devices are tracked to better support a new abstraction for sound devices 2018-06-02 13:50:13 -05:00
Brendan Robert
8bd9ec1781 Wolfenstein cheats: Added "Day in the office" hack mode for cheap laughs 2018-05-26 01:46:31 -05:00
Brendan Robert
f7ca7c198c Wolfenstein cheat module updated to keep the big boss alive so you can drop him yourself, if you so choose. 2018-05-26 01:00:48 -05:00
Brendan Robert
1f2eff2e42 Added Beyond Wolfenstein cheats 2018-05-26 00:47:57 -05:00
Brendan Robert
d4073b9096 UI and Metacheat improvements, also new Wolfenstein trainer module! 2018-05-24 01:53:24 -05:00
Brendan Robert
9cee0cece9 Upstream fixes from Lawless Legends for keyboard focus and speed handling 2018-05-23 00:50:56 -05:00
Brendan Robert
142ee2df2a BEHOLD! The Resurrection of MetaCheat! 2018-05-23 00:38:22 -05:00
Brendan Robert
9118b83a43 Remove dead code 2018-05-23 00:38:02 -05:00
Michaelangel007
8525330d53 Test and Display if maven isn't installed 2018-05-22 22:24:48 -06:00
Michaelangel007
ddc41ec84e Fix spelling 2018-05-22 22:24:07 -06:00
Brendan Robert
eb776d44af Committing upstream changes from Lawless Legends to address mockingboard init issues. 2018-05-22 22:30:33 -05:00
Brendan Robert
ad9da99cb8 Update README.md 2018-01-15 15:20:50 -06:00
Brendan Robert
79c1ee825c Update README.md 2018-01-15 15:19:19 -06:00
Brendan Robert
0d07d65b82 Update README.md 2018-01-15 15:17:42 -06:00
Brendan Robert
769d7f4302 Update README.md 2018-01-15 15:16:03 -06:00
Brendan Robert
d8ab357d84 Update README.md 2018-01-15 15:14:36 -06:00
Brendan Robert
ce9027cb6b Update README.md 2018-01-15 15:05:32 -06:00
Brendan Robert
ede44af6d1 Update README.md 2018-01-15 15:04:24 -06:00
Brendan Robert
4425e8884d Shaking the can...
(putting kids through college is a bigger challenge than I anticipated...)
2018-01-15 15:03:48 -06:00
Brendan Robert
0c0b2c107c Backported changes from Lawless Legends app experience; namely better boot behavior and also UI Controls Overlay. :) 2018-01-12 23:27:56 -06:00
Brendan Robert
dba57e6e89 Created application logo 2017-12-26 16:46:12 -06:00
Brendan Robert
09c1d78832 Fixed font missing issue when loading a packaged version of the application on OSX 2017-12-26 14:35:13 -06:00
197 changed files with 73562 additions and 86960 deletions

1
.java-version Normal file
View File

@@ -0,0 +1 @@
graalvm64-17.0.3

476
LICENSE
View File

@@ -1,339 +1,201 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
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.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Preamble
1. Definitions.
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.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
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.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
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.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
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.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
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.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
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.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
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.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
The precise terms and conditions for copying, distribution and
modification follow.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
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".
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
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.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
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.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
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.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
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 give any other recipients of the Work or
Derivative Works a copy of this License; and
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 modified files to carry prominent notices
stating that You changed the files; and
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) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
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.)
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
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.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
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.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
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.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
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:
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
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,
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
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,
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
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.)
END OF TERMS AND CONDITIONS
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.
APPENDIX: How to apply the Apache License to your work.
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.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
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.
Copyright [yyyy] [name of copyright owner]
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.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.
http://www.apache.org/licenses/LICENSE-2.0
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.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,36 +1,66 @@
Java Apple Computer Emulator
====
Download:
Jace is a mature cycle-accurate emulation of an Apple //e computer. The full library of software for that series is 100% compatible with this emulator, as well as many popular hardware add-ons such as:
- Joysticks (emulated by mouse or using real gamepads)
- Mouse
- Extended 80 Column card or Ramworks (Apple memory expansion compatible)
- Ramfactor
- Floppy drives (up to 14 supported)
- Hard drives (up to 12 supported) and 800kb disk images
- Mockingboard/Applied engineering Phasor (up to 6 supported)
- Passport MIDI Interface
- Super Serial Card (over TCP/IP emulation)
- Transwarp / Zip Chip
- Thunderclock / NoSlot Clock
- Apple RGB graphics modes
Other features of Jace include:
- Small IDE for programming Applesoft basic and Assembly (via built-in ACME cross assembler)
- Cheat features for some popular games like Prince of Persia, Montezuma's Revenge, Wolfenstein and more
- Metacheat feature allows searching memory for discovering new game cheats/mods
## Download:
* [See releases page for most recent](https://github.com/badvision/jace/releases)
To Run:
## To Run:
* See [run.sh](run.sh)
* The easiest way to run Jace is by downloading a native build from gitub (see releases link above)
or `java -jar Jace.jar`
Running the standard java version of Jace requires you have installed Java 17 or later.
* If you are building from source you can use any of the following:
- mvn javafx:run
- mvn gluonfx:run (Note: gluonfx plugin currently only supports up to Maven 3.8.8)
To Build:
## To Build natively:
* See [build.sh](build.sh)
In order to build Jace as a native application you need to install the following:
- Gluon's fork of GraalVM: https://github.com/gluonhq/graal/releases
- Compiler (XCode for Mac, GCC for Linux, Visual Studio for Windows)
- Maven 3.8.8 (not a newer version, unfortunately)
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!
The Gluon instructions have more details about where to find and download these components. Once you have them installed you can test the Java version via `mvn gluonfx:run` and if that is working correctly, then you can use `mvn gluonfx:build` to create a native binary in the target/gluonfx folder for your local platform. Gluon is only able to build for the OS you are running it on, so building for multiple platforms requires separate windows, mac and linux machines.
![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)
The Gluon documentation provides a compatibility matrix for each OS platform and the prerequisites needed to create native applications on each. See here for more details: https://docs.gluonhq.com/#_platforms
More information here: https://sites.google.com/site/brendanrobert/projects/jace
All other native dependencies are automatically downloaded as needed by Maven for the various LWJGL libraries.
### First time build note:
Because Jace provides an annotation processor for compilation, there is a chicken-and-egg problem when building the first time. Currently, this means the first time you compile, run `mvn install` twice. You don't have to do this step again as long as Maven is able to find a previously build version of Jace to provide this annotation processor. I tried to set up the profiles in the pom.xml so that it disables the annotation processor the first time you compile to avoid any issues. If running in a CICD environment, keep in mind you will likely always need to run the "mvn install" step twice, but only if your goal is to build the entire application including the annotations (should not be needed for just running unit tests.)
## Support JACE:
JACE will always be free, but it does take considerable time to refine and add new features. If you would like to show your support and encourage the author to keep maintaining this emulator, why not throw him some change to buy him a drink? (The emulator was named for the Jack and Cokes consumed during its inception.)
Donate here to support Jace developement:
<a href="https://www.paypal.me/BrendanRobert"><img src="images/donate.png" width="64"></a>
* <a href="bitcoin:1TmP94jrEtJNqz7wrCpViA6musGsiTXEq?amount=0.000721&label=Jace%20Donations">BTC address: 1TmP94jrEtJNqz7wrCpViA6musGsiTXEq</a>
* <a href="https://www.paypal.me/BrendanRobert">Paypal</a>
<img src="images/airheart.png" height="192"> <img src="images/colors.png" height="192"> <img src="images/desktop2.png" height="192">
More information here: https://sites.google.com/site/brendanrobert/projects/java-apple-computer-emulator

101
build.sh
View File

@@ -1,101 +0,0 @@
#!/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

BIN
images/airheart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
images/colors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

BIN
images/desktop2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
images/donate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,19 +0,0 @@
<?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

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

344
pom.xml
View File

@@ -1,18 +1,21 @@
<?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">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.badvision</groupId>
<artifactId>jace</artifactId>
<version>2.0-SNAPSHOT</version>
<version>3.0</version>
<packaging>jar</packaging>
<name>jace</name>
<name>JaceApplication</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mainClass>jace.JaceApplication</mainClass>
<netbeans.hint.license>apache20</netbeans.hint.license>
<lwjgl.version>3.3.3</lwjgl.version>
</properties>
<organization>
@@ -21,78 +24,169 @@
</organization>
<build>
<finalName>Jace</finalName>
<finalName>JaceApplication</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId>
<version>1.0.22</version>
<configuration>
<mainClass>jace.JaceApplication</mainClass>
<resourcesList>
<resource>.*</resource>
</resourcesList>
<releaseConfiguration>
<vendor>org.badvision</vendor>
<skipSigning>true</skipSigning>
</releaseConfiguration>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>jace/jace.JaceApplication</mainClass>
<executions>
<execution>
<!-- Default configuration for running -->
<!-- Usage: mvn clean javafx:run -->
<id>default-cli</id>
</execution>
<execution>
<!-- Configuration for manual attach debugging -->
<!-- Usage: mvn clean javafx:run@debug -->
<id>debug</id>
<configuration>
<options>
<option>
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:8000</option>
</options>
</configuration>
</execution>
<execution>
<!-- Configuration for automatic IDE debugging -->
<id>ide-debug</id>
<configuration>
<options>
<option>
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:8000</option>
</options>
</configuration>
</execution>
<execution>
<!-- Configuration for automatic IDE profiling -->
<id>ide-profile</id>
<configuration>
<options>
<option>${profiler.jvmargs.arg1}</option>
<option>${profiler.jvmargs.arg2}</option>
<option>${profiler.jvmargs.arg3}</option>
<option>${profiler.jvmargs.arg4}</option>
<option>${profiler.jvmargs.arg5}</option>
</options>
</configuration>
</execution>
</executions>
</configuration>
</plugin>
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.Final</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>package</phase>
<?m2e execute onConfiguration,onIncremental?>
<id>add-module-infos</id>
<phase>generate-resources</phase>
<goals>
<goal>unpack-dependencies</goal>
<goal>add-module-info</goal>
</goals>
<configuration>
<excludeScope>system</excludeScope>
<excludeGroupIds>junit,org.mockito,org.hamcrest</excludeGroupIds>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<modules>
<module>
<artifact>
<groupId>org.xerial.thirdparty</groupId>
<artifactId>nestedvm</artifactId>
<version>1.0</version>
</artifact>
<moduleInfoSource>
module nestedvm {
exports org.ibex.nestedvm;
exports org.ibex.nestedvm.util;
}
</moduleInfoSource>
</module>
</modules>
<overwriteExistingFiles>true</overwriteExistingFiles>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.4.0</version>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<configuration>
<excludes>
<exclude>jace/assembly/AcmeCrossAssembler.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>package</phase>
<id>default-prepare-agent</id>
<goals>
<goal>exec</goal>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>default-check</id>
<goals>
<goal>check</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>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>COMPLEXITY</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum>
</limit>
</limits>
</rule>
</rules>
</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>
</executions>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-bom</artifactId>
<version>${lwjgl.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<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>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -100,5 +194,159 @@
<artifactId>nestedvm</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>21.0.2</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>21.0.2</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>21.0.2</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>21.0.2</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>21.0.2</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-stb</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-glfw</artifactId>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-openal</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-stb</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-glfw</artifactId>
<classifier>${lwjgl.natives}</classifier>
</dependency>
</dependencies>
<profiles>
<profile>
<id>default</id>
<activation>
<file>
<exists>target/classes</exists>
</file>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.badvision</groupId>
<artifactId>jace</artifactId>
<version>3.0</version>
</path>
</annotationProcessorPaths>
<annotationProcessors>jace.config.InvokableActionAnnotationProcessor</annotationProcessors>
</configuration>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>firstRun</id>
<activation>
<file>
<missing>target/classes</missing>
</file>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>lwjgl-natives-linux-amd64</id>
<activation>
<os>
<family>unix</family>
<arch>amd64</arch>
</os>
</activation>
<properties>
<lwjgl.natives>natives-linux</lwjgl.natives>
</properties>
</profile>
<profile>
<id>lwjgl-natives-macos-x86_64</id>
<activation>
<os>
<family>mac</family>
<arch>x86_64</arch>
</os>
</activation>
<properties>
<lwjgl.natives>natives-macos</lwjgl.natives>
</properties>
</profile>
<profile>
<id>lwjgl-natives-windows-amd64</id>
<activation>
<os>
<family>windows</family>
<arch>amd64</arch>
</os>
</activation>
<properties>
<lwjgl.natives>natives-windows</lwjgl.natives>
</properties>
</profile>
</profiles>
</project>

4
run.sh
View File

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

View File

@@ -1,28 +1,27 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace;
import jace.hardware.FloppyDisk;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import jace.hardware.FloppyDisk;
/**
* Generic disk conversion utility, using the FloppyDisk nibblize/denibblize to
* convert between DSK and NIB formats (wherever possible anyway)
@@ -71,7 +70,7 @@ public class ConvertDiskImage {
// First read in the disk image, this decodes the disk as necessary
FloppyDisk theDisk;
try {
theDisk = new FloppyDisk(in, null);
theDisk = new FloppyDisk(in);
} catch (IOException ex) {
System.out.println("Couldn't read disk image");
return;

View File

@@ -1,28 +1,30 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace;
import jace.apple2e.Apple2e;
import jace.config.Configuration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import jace.config.Configuration;
import jace.core.RAM;
import jace.apple2e.Apple2e;
/**
* Created on January 15, 2007, 10:10 PM
@@ -31,7 +33,7 @@ import java.util.Map;
public class Emulator {
public static Emulator instance;
public static EmulatorUILogic logic = new EmulatorUILogic();
private static EmulatorUILogic logic;
public static Thread mainThread;
// public static void main(String... args) {
@@ -39,48 +41,140 @@ public class Emulator {
// instance = new Emulator(args);
// }
public static Apple2e computer;
private final Apple2e computer;
public static EmulatorUILogic getUILogic() {
if (logic == null) {
logic = new EmulatorUILogic();
}
return logic;
}
public static Emulator getInstance(List<String> args) {
Emulator i = getInstance();
i.processCmdlineArgs(args);
return i;
}
public static void abort() {
if (instance != null) {
if (instance.computer != null) {
instance.computer.getMotherboard().suspend();
instance.computer.getMotherboard().detach();
}
}
instance = null;
}
public static Emulator getInstance() {
if (instance == null) {
instance = new Emulator();
}
return instance;
}
private static Apple2e getComputer() {
return getInstance().computer;
}
public static void whileSuspended(Consumer<Apple2e> action) {
withComputer(c->c.getMotherboard().whileSuspended(()->action.accept(c)));
}
public static <T> T whileSuspended(Function<Apple2e, T> action, T defaultValue) {
return withComputer(c->c.getMotherboard().whileSuspended(()->action.apply(c), defaultValue), defaultValue);
}
public static void withComputer(Consumer<Apple2e> c) {
Apple2e computer = getComputer();
if (computer != null) {
c.accept(computer);
} else {
System.err.println("No computer available!");
Thread.dumpStack();
}
}
public static <T> T withComputer(Function<Apple2e, T> f, T defaultValue) {
Apple2e computer = getComputer();
if (computer != null) {
return f.apply(computer);
} else {
System.err.println("No computer available!");
Thread.dumpStack();
return defaultValue;
}
}
public static void withMemory(Consumer<RAM> m) {
Emulator.withMemory(mem-> {
m.accept(mem);
return null;
}, null);
}
public static <T> T withMemory(Function<RAM, T> m, T defaultValue) {
return withComputer(c->{
RAM memory = c.getMemory();
if (memory != null) {
return m.apply(memory);
} else {
System.err.println("No memory available!");
Thread.dumpStack();
return defaultValue;
}
}, defaultValue);
}
public static void withVideo(Consumer<jace.core.Video> v) {
withComputer(c->{
jace.core.Video video = c.getVideo();
if (video != null) {
v.accept(video);
} else {
System.err.println("No video available!");
Thread.dumpStack();
}
});
}
/**
* Creates a new instance of Emulator
* @param args
*/
public Emulator(List<String> args) {
private Emulator() {
instance = this;
computer = new Apple2e();
Configuration.buildTree();
Configuration.loadSettings();
Configuration.applySettings(Configuration.BASE);
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();
// }
private void processCmdlineArgs(List<String> args) {
if (args == null || args.isEmpty()) {
return;
}
Map<String, String> settings = new LinkedHashMap<>();
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);
}
}

View File

@@ -1,40 +1,25 @@
/**
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace;
import com.sun.javafx.tk.quantum.OverlayWarning;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
import jace.apple2e.SoftSwitches;
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 static jace.core.Utility.gripe;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
@@ -47,12 +32,23 @@ import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
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.Debugger;
import jace.core.RAM;
import jace.core.RAMListener;
import jace.ide.IdeController;
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;
@@ -76,11 +72,17 @@ public class EmulatorUILogic implements Reconfigurable {
@Override
public void updateStatus() {
enableDebug(true);
MOS65C02 cpu = (MOS65C02) Emulator.computer.getCpu();
MOS65C02 cpu = (MOS65C02) Emulator.withComputer(c->c.getCpu(), null);
updateCPURegisters(cpu);
}
};
}
@ConfigurableField(
category = "General",
name = "Show Drives"
)
public boolean showDrives = true;
public static void updateCPURegisters(MOS65C02 cpu) {
// DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel();
@@ -102,7 +104,7 @@ public class EmulatorUILogic implements Reconfigurable {
}
public static void enableTrace(boolean b) {
Emulator.computer.getCpu().setTraceEnabled(b);
Emulator.withComputer(c->c.getCpu().setTraceEnabled(b));
}
public static void stepForward() {
@@ -110,7 +112,7 @@ public class EmulatorUILogic implements Reconfigurable {
}
static void registerDebugger() {
Emulator.computer.getCpu().setDebug(debugger);
Emulator.withComputer(c->c.getCpu().setDebug(debugger));
}
public static Integer getValidAddress(String s) {
@@ -129,7 +131,7 @@ public class EmulatorUILogic implements Reconfigurable {
// public static void updateWatchList(final DebuggerPanel panel) {
// java.awt.EventQueue.invokeLater(() -> {
// watches.stream().forEach((oldWatch) -> {
// Emulator.computer.getMemory().removeListener(oldWatch);
// Emulator.getComputer().getMemory().removeListener(oldWatch);
// });
// if (panel == null) {
// return;
@@ -156,10 +158,10 @@ public class EmulatorUILogic implements Reconfigurable {
// watchValue.setText(Integer.toHexString(e.getNewValue() & 0x0FF));
// }
// };
// Emulator.computer.getMemory().addListener(newListener);
// Emulator.getComputer().getMemory().addListener(newListener);
// watches.add(newListener);
// // Print out the current value right away
// byte b = Emulator.computer.getMemory().readRaw(address);
// byte b = Emulator.getComputer().getMemory().readRaw(address);
// watchValue.setText(Integer.toString(b & 0x0ff, 16));
// } else {
// watchValue.setText("00");
@@ -199,14 +201,13 @@ public class EmulatorUILogic implements Reconfigurable {
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(JaceApplication.getApplication().primaryStage);
if (binary == null) {
Emulator.computer.resume();
return;
}
runFileNamed(binary);
Emulator.whileSuspended(c-> {
FileChooser select = new FileChooser();
File binary = select.showOpenDialog(JaceApplication.getApplication().primaryStage);
if (binary != null) {
runFileNamed(binary);
}
});
}
public static void runFileNamed(File binary) {
@@ -221,32 +222,29 @@ public class EmulatorUILogic implements Reconfigurable {
}
} 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);
public static void brun(File binary, int address) throws IOException {
byte[] data;
try (FileInputStream in = new FileInputStream(binary)) {
data = new byte[in.available()];
in.read(data);
}
CPU cpu = Emulator.computer.getCpu();
Emulator.computer.getCpu().setProgramCounter(address);
Emulator.computer.resume();
Emulator.whileSuspended(c-> {
RAM ram = c.getMemory();
for (int i = 0; i < data.length; i++) {
ram.write(address + i, data[i], false, true);
}
c.getCpu().setProgramCounter(address);
});
}
@InvokableAction(
name = "Toggle Debug",
category = "debug",
description = "Show/hide the debug panel",
alternatives = "Show Debug;Hide Debug",
alternatives = "Show Debug;Hide Debug;Inspect",
defaultKeyMapping = "ctrl+shift+d")
public static void toggleDebugPanel() {
// AbstractEmulatorFrame frame = Emulator.getFrame();
@@ -262,13 +260,14 @@ public class EmulatorUILogic implements Reconfigurable {
name = "Toggle fullscreen",
category = "general",
description = "Activate/deactivate fullscreen mode",
alternatives = "fullscreen,maximize",
alternatives = "fullscreen;maximize",
defaultKeyMapping = "ctrl+shift+f")
public static void toggleFullscreen() {
Platform.runLater(() -> {
Stage stage = JaceApplication.getApplication().primaryStage;
stage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH);
stage.setFullScreen(!stage.isFullScreen());
JaceApplication.getApplication().controller.setAspectRatioEnabled(stage.isFullScreen());
});
}
@@ -276,13 +275,13 @@ public class EmulatorUILogic implements Reconfigurable {
name = "Save Raw Screenshot",
category = "general",
description = "Save raw (RAM) format of visible screen",
alternatives = "screendump, raw screenshot",
alternatives = "screendump;raw screenshot",
defaultKeyMapping = "ctrl+shift+z")
public static void saveScreenshotRaw() throws FileNotFoundException, IOException {
public static void saveScreenshotRaw() throws 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 start = Emulator.withComputer(c->c.getVideo().getCurrentWriter().actualWriter().getYOffset(0), 0);
int len;
if (start < 0x02000) {
// Lo-res or double-lores
@@ -299,16 +298,21 @@ public class EmulatorUILogic implements Reconfigurable {
}
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));
Emulator.whileSuspended(c -> {
RAM128k ram = (RAM128k) c.getMemory();
try {
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));
}
} catch (IOException e) {
Logger.getLogger(EmulatorUILogic.class.getName()).log(Level.SEVERE, "Error writing screenshot", e);
}
}
for (int i = 0; i < len; i++) {
out.write(ram.getMainMemory().readByte(start + i));
}
});
}
System.out.println("Wrote screenshot to " + outFile.getAbsolutePath());
}
@@ -317,12 +321,11 @@ public class EmulatorUILogic implements Reconfigurable {
name = "Save Screenshot",
category = "general",
description = "Save image of visible screen",
alternatives = "Save image,save framebuffer,screenshot",
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();
// Image i = Emulator.getComputer().getVideo().getFrameBuffer();
// BufferedImage bufImageARGB = SwingFXUtils.fromFXImage(i, null);
File targetFile = select.showSaveDialog(JaceApplication.getApplication().primaryStage);
if (targetFile == null) {
@@ -330,7 +333,7 @@ public class EmulatorUILogic implements Reconfigurable {
}
String filename = targetFile.getName();
System.out.println("Writing screenshot to " + filename);
String extension = filename.substring(filename.lastIndexOf(".") + 1);
// String extension = filename.substring(filename.lastIndexOf(".") + 1);
// BufferedImage bufImageRGB = new BufferedImage(bufImageARGB.getWidth(), bufImageARGB.getHeight(), BufferedImage.OPAQUE);
//
// Graphics2D graphics = bufImageRGB.createGraphics();
@@ -346,14 +349,14 @@ public class EmulatorUILogic implements Reconfigurable {
name = "Configuration",
category = "general",
description = "Edit emulator configuraion",
alternatives = "Reconfigure,Preferences,Settings",
alternatives = "Reconfigure;Preferences;Settings;Config",
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();
AnchorPane node = fxmlLoader.load();
ConfigurationUIController controller = fxmlLoader.getController();
controller.initialize();
Scene s = new Scene(node);
@@ -368,14 +371,14 @@ public class EmulatorUILogic implements Reconfigurable {
name = "Open IDE",
category = "development",
description = "Open new IDE window for Basic/Assembly/Plasma coding",
alternatives = "dev,development,acme,assembler,editor",
alternatives = "IDE;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();
AnchorPane node = fxmlLoader.load();
IdeController controller = fxmlLoader.getController();
controller.initialize();
Scene s = new Scene(node);
@@ -392,44 +395,67 @@ public class EmulatorUILogic implements Reconfigurable {
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;",
alternatives = "Aspect;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(() -> {
JaceApplication.getApplication().primaryStage.setFullScreen(false);
if (JaceApplication.getApplication() == null
|| JaceApplication.getApplication().primaryStage == null) {
return;
}
Stage stage = JaceApplication.getApplication().primaryStage;
size++;
if (size > 3) {
size = 0;
}
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;
if (stage.isFullScreen()) {
JaceApplication.getApplication().controller.toggleAspectRatio();
} else {
int width, height;
switch (size) {
case 0 -> {
// 1x
width = 560;
height = 384;
}
case 1 -> {
// 1.5x
width = 840;
height = 576;
}
case 2 -> {
// 2x
width = 560 * 2;
height = 384 * 2;
}
case 3 -> {
// 3x (retina) 2880x1800
width = 560 * 3;
height = 384 * 3;
}
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);
}
Stage stage = JaceApplication.getApplication().primaryStage;
double vgap = stage.getScene().getY();
double hgap = stage.getScene().getX();
stage.setWidth(hgap*2 + width);
stage.setHeight(vgap + height);
});
}
@InvokableAction(
name = "About",
category = "general",
description = "Display about window",
alternatives = "info;credits",
defaultKeyMapping = {"ctrl+shift+."})
public static void showAboutWindow() {
//TODO: Implement
}
public static boolean confirm(String message) {
// return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(Emulator.getFrame(), message);
@@ -499,16 +525,17 @@ public class EmulatorUILogic implements Reconfigurable {
}
public static void simulateCtrlAppleReset() {
Computer computer = JaceApplication.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);
Emulator.withComputer(c -> {
c.getKeyboard().openApple(true);
c.warmStart();
Platform.runLater(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(EmulatorUILogic.class.getName()).log(Level.SEVERE, null, ex);
}
c.getKeyboard().openApple(false);
});
});
}

View File

@@ -1,28 +1,28 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package jace;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.apple2e.MOS65C02;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.Utility;
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 JaceApplication extends Application {
@@ -30,9 +30,10 @@ public class JaceApplication extends Application {
static JaceApplication singleton;
public Stage primaryStage;
JaceUIController controller;
public JaceUIController controller;
static boolean romStarted = false;
static AtomicBoolean romStarted = new AtomicBoolean(false);
int watchdogDelay = 500;
@Override
public void start(Stage stage) throws Exception {
@@ -41,34 +42,47 @@ public class JaceApplication extends Application {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/JaceUI.fxml"));
fxmlLoader.setResources(null);
try {
AnchorPane node = (AnchorPane) fxmlLoader.load();
AnchorPane node = fxmlLoader.load();
controller = fxmlLoader.getController();
controller.initialize();
Scene s = new Scene(node);
s.setFill(Color.BLACK);
primaryStage.setScene(s);
primaryStage.setTitle("Jace");
EmulatorUILogic.scaleIntegerRatio();
Utility.loadIcon("woz_figure.gif").ifPresent(primaryStage.getIcons()::add);
primaryStage.titleProperty().set("Jace");
Utility.loadIcon("app_icon.png").ifPresent(icon -> {
primaryStage.getIcons().add(icon);
});
} catch (IOException exception) {
throw new RuntimeException(exception);
}
primaryStage.show();
Emulator emulator = new Emulator(getParameters().getRaw());
javafx.application.Platform.runLater(() -> {
while (Emulator.computer.getVideo() == null || Emulator.computer.getVideo().getFrameBuffer() == null) {
Thread.yield();
new Thread(() -> {
Emulator.getInstance(getParameters().getRaw());
reconnectUIHooks();
EmulatorUILogic.scaleIntegerRatio();
AtomicBoolean waitingForVideo = new AtomicBoolean(true);
while (waitingForVideo.get()) {
Emulator.withVideo(v -> {
if (v.getFrameBuffer() != null) {
waitingForVideo.set(false);
}
});
Thread.onSpinWait();
}
controller.connectComputer(Emulator.computer, primaryStage);
bootWatchdog();
});
}).start();
primaryStage.setOnCloseRequest(event -> {
Emulator.computer.deactivate();
Emulator.withComputer(Computer::deactivate);
Platform.exit();
System.exit(0);
});
}
public void reconnectUIHooks() {
controller.connectComputer(primaryStage);
}
public static JaceApplication getApplication() {
return singleton;
}
@@ -97,6 +111,16 @@ public class JaceApplication extends Application {
return cheatController;
}
public void closeMetacheat() {
if (cheatStage != null) {
cheatStage.close();
}
if (cheatController != null) {
cheatController.detach();
cheatController = null;
}
}
/**
* @param args the command line arguments
*/
@@ -109,22 +133,41 @@ public class JaceApplication extends Application {
* for cold boot
*/
private void bootWatchdog() {
romStarted = false;
RAMListener startListener = Emulator.computer.getMemory().
observe(RAMEvent.TYPE.EXECUTE, 0x0FA62, (e) -> {
romStarted = true;
Emulator.withComputer(c -> {
// We know the game started properly when it runs the decompressor the first time
int watchAddress = 0x0ff3a;
new Thread(()->{
// Logger.getLogger(getClass().getName()).log(Level.WARNING, "Booting with watchdog");
final RAMListener startListener = c.getMemory().observeOnce("Boot watchdog", RAMEvent.TYPE.EXECUTE, watchAddress, (e) -> {
// Logger.getLogger(getClass().getName()).log(Level.WARNING, "Boot was detected, watchdog terminated.");
romStarted.set(true);
});
Emulator.computer.coldStart();
try {
Thread.sleep(250);
if (!romStarted) {
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Boot not detected, performing a cold start");
Emulator.computer.coldStart();
}
} catch (InterruptedException ex) {
Logger.getLogger(JaceApplication.class.getName()).log(Level.SEVERE, null, ex);
}
Emulator.computer.getMemory().removeListener(startListener);
romStarted.set(false);
c.coldStart();
try {
// Logger.getLogger(getClass().getName()).log(Level.WARNING, "Watchdog: waiting " + watchdogDelay + "ms for boot to start.");
Thread.sleep(watchdogDelay);
watchdogDelay = 500;
if (!romStarted.get() || !c.isRunning() || c.getCpu().getProgramCounter() == MOS65C02.FASTBOOT || c.getCpu().getProgramCounter() == 0) {
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Boot not detected, performing a cold start");
Logger.getLogger(getClass().getName()).log(Level.WARNING, "Old PC: {0}", Integer.toHexString(c.getCpu().getProgramCounter()));
resetEmulator();
bootWatchdog();
} else {
startListener.unregister();
}
} catch (InterruptedException ex) {
Logger.getLogger(JaceApplication.class.getName()).log(Level.SEVERE, null, ex);
}
}).start();
});
}
public void resetEmulator() {
// Reset the emulator memory and restart
Emulator.withComputer(c -> {
c.getMemory().resetState();
c.warmStart();
});
}
}

View File

@@ -1,23 +1,8 @@
/*
* 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.cheat.MetaCheat;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Keyboard;
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;
@@ -28,12 +13,34 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.core.Card;
import jace.core.Motherboard;
import jace.core.Utility;
import jace.core.Video;
import jace.library.MediaCache;
import jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
import jace.library.MediaEntry;
import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
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.Node;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
@@ -43,11 +50,14 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import javafx.util.StringConverter;
/**
*
@@ -55,9 +65,6 @@ import javafx.stage.Stage;
*/
public class JaceUIController {
@FXML
private URL location;
@FXML
private AnchorPane rootPane;
@@ -70,7 +77,25 @@ public class JaceUIController {
@FXML
private ImageView appleScreen;
Computer computer;
@FXML
private BorderPane controlOverlay;
@FXML
private Slider speedSlider;
@FXML
private AnchorPane menuButtonPane;
@FXML
private Button menuButton;
@FXML
private Slider speakerToggle;
private final BooleanProperty aspectRatioCorrectionEnabled = new SimpleBooleanProperty(false);
public static final double MIN_SPEED = 0.5;
public static final double MAX_SPEED = 5.0;
@FXML
void initialize() {
@@ -78,21 +103,213 @@ public class JaceUIController {
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'.";
appleScreen.fitWidthProperty().bind(rootPane.widthProperty());
speedSlider.setValue(1.0);
controlOverlay.setVisible(false);
menuButtonPane.setVisible(false);
controlOverlay.setFocusTraversable(false);
menuButtonPane.setFocusTraversable(true);
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)));
rootPane.setOnMouseMoved(this::showMenuButton);
rootPane.setOnMouseExited(this::hideControlOverlay);
rootPane.setOnMouseClicked((evt)->{
rootPane.requestFocus();
});
menuButton.setOnMouseClicked(this::showControlOverlay);
controlOverlay.setOnMouseClicked(this::hideControlOverlay);
delayTimer.getKeyFrames().add(new KeyFrame(Duration.millis(3000), evt -> {
hideControlOverlay(null);
rootPane.requestFocus();
}));
rootPane.requestFocus();
speakerToggle.setValue(1.0);
speakerToggle.setOnMouseClicked(evt -> {
speakerEnabled = !speakerEnabled;
int desiredValue = speakerEnabled ? 1 : 0;
speakerToggle.setValue(desiredValue);
Emulator.withComputer(computer -> {
Motherboard.enableSpeaker = speakerEnabled;
computer.motherboard.reconfigure();
if (!speakerEnabled) {
computer.motherboard.speaker.detach();
} else {
computer.motherboard.speaker.attach();
}
});
});
}
boolean speakerEnabled = true;
private void showMenuButton(MouseEvent evt) {
if (!evt.isPrimaryButtonDown() && !evt.isSecondaryButtonDown() && !controlOverlay.isVisible()) {
resetMenuButtonTimer();
if (!menuButtonPane.isVisible()) {
menuButtonPane.setVisible(true);
FadeTransition ft = new FadeTransition(Duration.millis(500), menuButtonPane);
ft.setFromValue(0.0);
ft.setToValue(1.0);
ft.play();
}
}
rootPane.requestFocus();
}
public void connectComputer(Computer computer, Stage primaryStage) {
this.computer = computer;
appleScreen.setImage(computer.getVideo().getFrameBuffer());
EventHandler<KeyEvent> keyboardHandler = computer.getKeyboard().getListener();
primaryStage.setOnShowing(evt -> computer.getKeyboard().resetState());
rootPane.setFocusTraversable(true);
rootPane.setOnKeyPressed(keyboardHandler);
rootPane.setOnKeyReleased(keyboardHandler);
rootPane.requestFocus();
Timeline delayTimer = new Timeline();
private void resetMenuButtonTimer() {
delayTimer.playFromStart();
}
private void showControlOverlay(MouseEvent evt) {
if (!evt.isPrimaryButtonDown() && !evt.isSecondaryButtonDown()) {
delayTimer.stop();
menuButtonPane.setVisible(false);
controlOverlay.setVisible(true);
FadeTransition ft = new FadeTransition(Duration.millis(500), controlOverlay);
ft.setFromValue(0.0);
ft.setToValue(1.0);
ft.play();
rootPane.requestFocus();
}
}
private void hideControlOverlay(MouseEvent evt) {
if (menuButtonPane.isVisible()) {
FadeTransition ft1 = new FadeTransition(Duration.millis(500), menuButtonPane);
ft1.setFromValue(1.0);
ft1.setToValue(0.0);
ft1.setOnFinished(evt1 -> menuButtonPane.setVisible(false));
ft1.play();
}
if (controlOverlay.isVisible()) {
FadeTransition ft2 = new FadeTransition(Duration.millis(500), controlOverlay);
ft2.setFromValue(1.0);
ft2.setToValue(0.0);
ft2.setOnFinished(evt1 -> controlOverlay.setVisible(false));
ft2.play();
}
}
protected double convertSpeedToRatio(Double setting) {
if (setting < 1.0) {
return 0.5;
} else if (setting == 1.0) {
return 1.0;
} else if (setting >= 5) {
return Double.MAX_VALUE;
} else {
return setting;
}
}
Stage primaryStage;
public void reconnectKeyboard() {
Emulator.withComputer(computer -> {
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);
}
});
}
private void connectControls(Stage ps) {
primaryStage = ps;
connectButtons(controlOverlay);
speedSlider.setMinorTickCount(3);
speedSlider.setMajorTickUnit(1);
speedSlider.setMax(MAX_SPEED);
speedSlider.setMin(MIN_SPEED);
speedSlider.setLabelFormatter(new StringConverter<Double>() {
@Override
public String toString(Double val) {
if (val <= MIN_SPEED) {
return "Half";
} else if (val >= MAX_SPEED) {
return "";
}
double v = convertSpeedToRatio(val);
if (v != Math.floor(v)) {
return v + "x";
} else {
return (int) v + "x";
}
}
@Override
public Double fromString(String string) {
return 1.0;
}
});
Platform.runLater(() -> {
double currentSpeed = (double) Emulator.withComputer(c->c.getMotherboard().getSpeedRatio(), 100) / 100.0;
speedSlider.valueProperty().set(currentSpeed);
speedSlider.valueProperty().addListener((val, oldValue, newValue) -> setSpeed(newValue.doubleValue()));
});
reconnectKeyboard();
}
private void connectButtons(Node n) {
if (n instanceof Button button) {
Function<Boolean, Boolean> action = Utility.getNamedInvokableAction(button.getText());
button.setOnMouseClicked(evt -> action.apply(false));
} else if (n instanceof Parent parent) {
parent.getChildrenUnmodifiable().forEach(child -> connectButtons(child));
}
}
public void setSpeed(double speed) {
double newSpeed = Math.max(speed, MIN_SPEED);
if (speedSlider.getValue() != speed) {
Platform.runLater(()->speedSlider.setValue(newSpeed));
}
if (newSpeed >= MAX_SPEED) {
Emulator.withComputer(c -> {
c.getMotherboard().setMaxSpeed(true);
});
} else {
Emulator.withComputer(c -> {
c.getMotherboard().setMaxSpeed(false);
c.getMotherboard().setSpeedInPercentage((int) (newSpeed * 100));
});
}
}
public void toggleAspectRatio() {
setAspectRatioEnabled(aspectRatioCorrectionEnabled.not().get());
}
public void setAspectRatioEnabled(boolean enabled) {
aspectRatioCorrectionEnabled.set(enabled);
}
public void connectComputer(Stage primaryStage) {
Platform.runLater(() -> {
connectControls(primaryStage);
Emulator.withVideo(this::connectVideo);
appleScreen.setVisible(true);
rootPane.requestFocus();
});
}
public void connectVideo(Video video) {
if (video != null) {
appleScreen.setImage(video.getFrameBuffer());
} else {
appleScreen.setImage(null);
}
}
private void processDragEnteredEvent(DragEvent evt) {
@@ -156,15 +373,15 @@ public class JaceUIController {
});
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);
}
Emulator.whileSuspended(c-> {
try {
consumer.insertMedia(media, media.files.get(0));
} catch (IOException ex) {
Logger.getLogger(JaceUIController.class.getName()).log(Level.SEVERE, null, ex);
}
});
event.setDropCompleted(true);
event.consume();
endDragEvent();
});
});
@@ -175,16 +392,18 @@ public class JaceUIController {
private void endDragEvent() {
stackPane.getChildren().remove(drivePanel);
drivePanel.getChildren().stream().forEach((n) -> {
n.setOnDragDropped(null);
});
drivePanel.getChildren().forEach((n) -> n.setOnDragDropped(null));
}
private List<MediaConsumer> getMediaConsumers() {
List<MediaConsumer> consumers = new ArrayList<>();
for (Optional<Card> card : computer.memory.getAllCards()) {
card.filter(c -> c instanceof MediaConsumerParent).ifPresent(parent -> {
consumers.addAll(Arrays.asList(((MediaConsumerParent) parent).getConsumers()));
if (Emulator.getUILogic().showDrives) {
Emulator.withMemory(m -> {
for (Optional<Card> card : m.getAllCards()) {
card.filter(c -> c instanceof MediaConsumerParent).ifPresent(parent ->
consumers.addAll(Arrays.asList(((MediaConsumerParent) parent).getConsumers()))
);
}
});
}
return consumers;
@@ -192,29 +411,30 @@ public class JaceUIController {
Map<Label, Long> iconTTL = new ConcurrentHashMap<>();
void addIndicator(Label icon) {
public void addIndicator(Label icon) {
addIndicator(icon, 250);
}
void addIndicator(Label icon, long TTL) {
public void addIndicator(Label icon, long TTL) {
if (!iconTTL.containsKey(icon)) {
Application.invokeLater(() -> {
Platform.runLater(() -> {
if (!notificationBox.getChildren().contains(icon)) {
notificationBox.getChildren().add(icon);
notificationBox.getChildren().add(0, icon);;
}
});
}
trackTTL(icon, TTL);
}
void removeIndicator(Label icon) {
Application.invokeLater(() -> {
public void removeIndicator(Label icon) {
Platform.runLater(() -> {
notificationBox.getChildren().remove(icon);
iconTTL.remove(icon);
});
}
ScheduledExecutorService notificationExecutor = Executors.newSingleThreadScheduledExecutor();
@SuppressWarnings("all")
ScheduledFuture ttlCleanupTask = null;
private void trackTTL(Label icon, long TTL) {
@@ -229,9 +449,7 @@ public class JaceUIController {
Long now = System.currentTimeMillis();
iconTTL.keySet().stream()
.filter((icon) -> (iconTTL.get(icon) <= now))
.forEach((icon) -> {
removeIndicator(icon);
});
.forEach(this::removeIndicator);
if (iconTTL.isEmpty()) {
ttlCleanupTask.cancel(true);
ttlCleanupTask = null;
@@ -240,29 +458,30 @@ public class JaceUIController {
public void addMouseListener(EventHandler<MouseEvent> handler) {
appleScreen.addEventHandler(MouseEvent.ANY, handler);
rootPane.addEventHandler(MouseEvent.ANY, handler);
}
public void removeMouseListener(EventHandler<MouseEvent> handler) {
appleScreen.removeEventHandler(MouseEvent.ANY, handler);
rootPane.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(() -> {
notification.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 80, 0.7), new CornerRadii(5.0), new Insets(-5.0))));
Platform.runLater(() -> {
stackPane.getChildren().remove(oldNotification);
stackPane.getChildren().add(notification);
});
notificationExecutor.schedule(()->{
Application.invokeLater(() -> {
stackPane.getChildren().remove(notification);
});
}, 4, TimeUnit.SECONDS);
notificationExecutor.schedule(
() -> Platform.runLater(() -> stackPane.getChildren().remove(notification)),
4, TimeUnit.SECONDS);
}
}

View File

@@ -1,47 +1,27 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e;
import jace.Emulator;
import jace.cheat.Cheats;
import jace.config.ClassSelection;
import jace.config.ConfigurableField;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Motherboard;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.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 java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -49,6 +29,26 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.apple2e.softswitch.VideoSoftSwitch;
import jace.cheat.Cheats;
import jace.config.ConfigurableField;
import jace.config.DeviceSelection;
import jace.core.Card;
import jace.core.Computer;
import jace.core.Device;
import jace.core.Motherboard;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.Utility;
import jace.hardware.Cards;
import jace.hardware.FPSMonitorDevice;
import jace.hardware.Joystick;
import jace.hardware.NoSlotClock;
import jace.hardware.VideoImpls;
import jace.hardware.ZipWarpAccelerator;
import jace.state.Stateful;
/**
* 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
@@ -60,58 +60,61 @@ import java.util.logging.Logger;
*/
@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);
public DeviceSelection<Cards> card1 = new DeviceSelection<>(Cards.class, null);
@ConfigurableField(name = "Slot 2", shortName = "s2card")
public ClassSelection card2 = new ClassSelection(Card.class, null);
public DeviceSelection<Cards> card2 = new DeviceSelection<>(Cards.class, Cards.AppleMouse, true);
@ConfigurableField(name = "Slot 3", shortName = "s3card")
public ClassSelection card3 = new ClassSelection(Card.class, null);
public DeviceSelection<Cards> card3 = new DeviceSelection<>(Cards.class, null);
@ConfigurableField(name = "Slot 4", shortName = "s4card")
public ClassSelection card4 = new ClassSelection(Card.class, null);
public DeviceSelection<Cards> card4 = new DeviceSelection<>(Cards.class, Cards.Mockingboard, true);
@ConfigurableField(name = "Slot 5", shortName = "s5card")
public ClassSelection card5 = new ClassSelection(Card.class, null);
public DeviceSelection<Cards> card5 = new DeviceSelection<>(Cards.class, Cards.RamFactor, true);
@ConfigurableField(name = "Slot 6", shortName = "s6card")
public ClassSelection card6 = new ClassSelection(Card.class, CardDiskII.class);
public DeviceSelection<Cards> card6 = new DeviceSelection<>(Cards.class, Cards.DiskIIDrive, true);
@ConfigurableField(name = "Slot 7", shortName = "s7card")
public ClassSelection card7 = new ClassSelection(Card.class, CardMassStorage.class);
public DeviceSelection<Cards> card7 = new DeviceSelection<>(Cards.class, Cards.MassStorage, true);
@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);
public DeviceSelection<VideoImpls> videoRenderer = new DeviceSelection<>(VideoImpls.class, VideoImpls.NTSC, false);
@ConfigurableField(name = "Aux Ram", shortName = "ram", description = "Aux ram card")
public ClassSelection ramCard = new ClassSelection(RAM128k.class, CardExt80Col.class);
public DeviceSelection<RAM128k.RamCards> ramCard = new DeviceSelection<>(RAM128k.RamCards.class, RAM128k.RamCards.CardRamworks, false);
@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;
@ConfigurableField(name = "Accelerator Enabled", shortName = "zip", description = "If checked, add support for Zip/Transwarp", enablesDevice = true)
public boolean acceleratorEnabled = true;
public Joystick joystick1;
public Joystick joystick2;
@ConfigurableField(name = "Activate Cheats", shortName = "cheat", defaultValue = "")
public ClassSelection cheatEngine = new ClassSelection(Cheats.class, null);
@ConfigurableField(name = "Activate Cheats", shortName = "cheat")
public DeviceSelection<Cheats.Cheat> cheatEngine = new DeviceSelection<>(Cheats.Cheat.class, null);
public Cheats activeCheatEngine = null;
public NoSlotClock clock;
public ZipWarpAccelerator accelerator;
FPSMonitorDevice fpsCounters;
@ConfigurableField(name = "Show speed monitors", shortName = "showFps")
public boolean showSpeedMonitors = false;
/**
* Creates a new instance of Apple2e
*/
public Apple2e() {
super();
fpsCounters = new FPSMonitorDevice();
try {
reconfigure();
setCpu(new MOS65C02(this));
reinitMotherboard();
setCpu(new MOS65C02());
setMotherboard(new Motherboard(null));
} catch (Throwable t) {
System.err.println("Unable to initalize virtual machine");
System.err.println("Unable to initialize virtual machine");
t.printStackTrace(System.err);
}
}
@@ -121,44 +124,37 @@ public class Apple2e extends Computer {
return "Computer (Apple //e)";
}
private void reinitMotherboard() {
if (motherboard != null && motherboard.isRunning()) {
motherboard.suspend();
}
setMotherboard(new Motherboard(this, motherboard));
reconfigure();
motherboard.reconfigure();
}
@Override
public void coldStart() {
pause();
reinitMotherboard();
RAM128k r = (RAM128k) getMemory();
System.err.println("Cold starting computer: RESETTING SOFT SWITCHES");
r.resetState();
for (SoftSwitches s : SoftSwitches.values()) {
s.getSwitch().reset();
if ((s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset();
}
}
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset);
// This isn't really authentic behavior but sometimes games like memory to have a consistent state when booting.
r.zeroAllRam();
// Sather 4-15:
// An open Apple (left Apple) reset causes meaningless values to be stored in two locations
// of every memory page from Page $01 through Page $BF before the power-up byte is checked.
int offset = IRQ_VECTOR & 0x0ff;
byte garbage = (byte) (Math.random() * 256.0);
for (int page=1; page < 0xc0; page++) {
r.write(page << 8 + offset, garbage, false, true);
r.write(page << 8 + 1 + offset, garbage, false, true);
}
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()) {
s.getSwitch().reset();
if (! (s.getSwitch() instanceof VideoSoftSwitch)) {
s.getSwitch().reset();
}
}
getMemory().configureActiveMemory();
getVideo().configureVideoMode();
@@ -166,7 +162,7 @@ public class Apple2e extends Computer {
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(Card::reset);
}
getCpu().resume();
motherboard.disableTempMaxSpeed();
resume();
}
@@ -174,208 +170,194 @@ public class Apple2e extends Computer {
return activeCheatEngine;
}
private void insertCard(Class<? extends Card> type, int slot) throws NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
private void insertCard(DeviceSelection<Cards> type, int slot) {
if (getMemory().getCard(slot).isPresent()) {
if (getMemory().getCard(slot).get().getClass().equals(type)) {
if (type.getValue() != null && type.getValue().isInstance(getMemory().getCard(slot).get())) {
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);
if (type != null && type.getValue() != null) {
Card card = type.getValue().create();
getMemory().addCard(card, slot);
}
}
private RAM128k.RamCards getDesiredMemoryConfiguration() {
if (ramCard.getValue() == null) {
return RAM128k.RamCards.CardExt80Col;
} else {
return ramCard.getValue();
}
}
private boolean isMemoryConfigurationCorrect() {
if (getMemory() == null) {
return false;
}
return getDesiredMemoryConfiguration().isInstance((RAM128k) getMemory());
}
private boolean isVideoConfigurationCorrect() {
VideoImpls videoSelection = videoRenderer.getValue();
return videoSelection != null && videoSelection.isInstance(getVideo());
}
@Override
protected RAM createMemory() {
return getDesiredMemoryConfiguration().create();
}
@Override
public void loadRom(boolean reload) throws IOException {
if (!romLoaded.isDone() && reload) {
if (useDebugRom) {
loadRom("/jace/data/apple2e_debug.rom");
} else {
loadRom("/jace/data/apple2e.rom");
}
}
}
@Override
public final void reconfigure() {
boolean restart = pause();
super.reconfigure();
if (Utility.isHeadlessMode()) {
joy1enabled = false;
joy2enabled = false;
}
if (getMotherboard() == null) {
System.err.println("No motherboard, cannot reconfigure");
Thread.dumpStack();
return;
}
getMotherboard().whileSuspended(()-> {
// System.err.println("Reconfiguring computer...");
if (!isMemoryConfigurationCorrect()) {
System.out.println("Creating new ram using " + getDesiredMemoryConfiguration().getName());
setMemory(createMemory());
}
// Make sure all softswitches are configured after confirming memory exists
for (SoftSwitches s : SoftSwitches.values()) {
s.getSwitch().register();
}
try {
loadRom(true);
} catch (IOException e) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, "Failed to load system rom ROMs", e);
}
}
getMemory().configureActiveMemory();
super.reconfigure();
Set<Device> newDeviceSet = new HashSet<>();
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 ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
} catch (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);
if (acceleratorEnabled) {
if (accelerator == null) {
accelerator = new ZipWarpAccelerator();
}
} catch (Throwable ex) {
newDeviceSet.add(accelerator);
}
}
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);
newDeviceSet.add(joystick1);
} else {
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);
newDeviceSet.add(joystick2);
} else {
joystick2 = null;
}
if (clockEnabled) {
if (clock == null) {
clock = new NoSlotClock(this);
motherboard.miscDevices.add(clock);
clock.attach();
clock = new NoSlotClock();
}
} else if (clock != null) {
motherboard.miscDevices.remove(clock);
clock.detach();
newDeviceSet.add(clock);
} else {
clock = null;
}
}
try {
if (useConsoleProbe) {
probe.init(this);
} else {
probe.shutdown();
if (!isVideoConfigurationCorrect()) {
setVideo(videoRenderer.getValue().create());
}
if (useDebugRom) {
loadRom("jace/data/apple2e_debug.rom");
} else {
loadRom("jace/data/apple2e.rom");
}
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();
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);
}
// Add all new cards
insertCard(card1, 1);
insertCard(card2, 2);
insertCard(card3, 3);
insertCard(card4, 4);
insertCard(card5, 5);
insertCard(card6, 6);
insertCard(card7, 7);
if (enableHints) {
enableHints();
} else {
disableHints();
}
getMemory().configureActiveMemory();
if (cheatEngine.getValue() == null) {
if (activeCheatEngine != null) {
activeCheatEngine.detach();
motherboard.miscDevices.remove(activeCheatEngine);
activeCheatEngine.suspend();
activeCheatEngine = null;
}
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 (activeCheatEngine != null && !cheatEngine.getValue().isInstance(activeCheatEngine)) {
activeCheatEngine.detach();
activeCheatEngine.suspend();
activeCheatEngine = null;
}
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);
if (activeCheatEngine == null && cheatEngine.getValue() != null) {
activeCheatEngine = cheatEngine.getValue().create();
}
if (activeCheatEngine != null) {
newDeviceSet.add(activeCheatEngine);
}
}
} catch (IOException ex) {
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
}
if (restart) {
resume();
}
newDeviceSet.add(getCpu());
newDeviceSet.add(getVideo());
for (Optional<Card> c : getMemory().getAllCards()) {
c.ifPresent(newDeviceSet::add);
}
if (showSpeedMonitors) {
newDeviceSet.add(fpsCounters);
}
getMotherboard().setAllDevices(newDeviceSet);
getMotherboard().attach();
getMotherboard().reconfigure();
});
}
@Override
protected void doPause() {
if (motherboard == null) {
if (getMotherboard() == null) {
return;
}
motherboard.pause();
getMotherboard().setPaused(true);
}
@Override
protected void doResume() {
if (motherboard == null) {
if (getMotherboard() == null) {
return;
}
motherboard.resume();
getMotherboard().resumeAll();
}
// public boolean isRunning() {
// if (motherboard == null) {
// return false;
// }
// return motherboard.isRunning() && !motherboard.isPaused;
// }
private List<RAMListener> hints = new ArrayList<>();
private final List<RAMListener> hints = new ArrayList<>();
ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1);
Runnable drawHints = () -> {
@@ -392,7 +374,7 @@ public class Apple2e extends Computer {
" Java Apple Computer Emulator",
"",
" Presented by BLuRry",
" http://goo.gl/SnzqG",
" https://goo.gl/SnzqG",
"",
"To insert a disk, please drag it over",
"this window and drop on the desired",
@@ -414,7 +396,7 @@ public class Apple2e extends Computer {
int animAddr, animCycleNumber;
byte animOldValue;
final String animation = "+xX*+-";
ScheduledFuture animationSchedule;
ScheduledFuture<?> animationSchedule;
Runnable doAnimation = () -> {
if (animAddr == 0 || animCycleNumber >= animation.length()) {
if (animAddr > 0) {
@@ -438,9 +420,9 @@ public class Apple2e extends Computer {
private void enableHints() {
if (hints.isEmpty()) {
hints.add(getMemory().observe(RAMEvent.TYPE.EXECUTE, 0x0FB63, (e)->{
animationTimer.schedule(drawHints, 1, TimeUnit.SECONDS);
animationSchedule =
hints.add(getMemory().observe("Helpful hints", 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
@@ -457,7 +439,7 @@ public class Apple2e extends Computer {
if (c == 0x0d) break;
in += c;
}
System.err.println("Intercepted command: "+in);
}
});
@@ -466,13 +448,12 @@ public class Apple2e extends Computer {
}
private void disableHints() {
hints.stream().forEach((hint) -> {
getMemory().removeListener(hint);
});
hints.forEach((hint) -> getMemory().removeListener(hint));
hints.clear();
}
@Override
public String getShortName() {
return "computer";
}
}
}

View File

@@ -1,32 +1,32 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e;
import jace.config.ConfigurableField;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.RAMEvent.TYPE;
import jace.state.Stateful;
import java.util.HashMap;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.Emulator;
import jace.config.ConfigurableField;
import jace.core.CPU;
import jace.core.RAMEvent.TYPE;
import jace.state.Stateful;
/**
* This is a full implementation of a MOS-65c02 processor, including the BBR,
* BBS, RMB and SMB opcodes. It is possible that this will be later refactored
@@ -40,8 +40,9 @@ public class MOS65C02 extends CPU {
private static final Logger LOG = Logger.getLogger(MOS65C02.class.getName());
public boolean readAddressTriggersEvent = true;
static int RESET_VECTOR = 0x00FFFC;
static int INT_VECTOR = 0x00FFFE;
public static int RESET_VECTOR = 0x00FFFC;
public static int INT_VECTOR = 0x00FFFE;
public static int FASTBOOT = 0x00FAA9;
@Stateful
public int A = 0x0FF;
@Stateful
@@ -71,19 +72,11 @@ public class MOS65C02 extends CPU {
@ConfigurableField(name = "Ext. opcode warnings", description = "If on, uses of 65c02 extended opcodes (or undocumented 6502 opcodes -- which will fail) will be logged to stdout for debugging purposes")
public boolean warnAboutExtendedOpcodes = false;
private RAM getMemory() {
return computer.getMemory();
}
public MOS65C02(Computer computer) {
super(computer);
public MOS65C02() {
initOpcodes();
clearState();
}
@Override
public void reconfigure() {
}
@Override
public void clearState() {
A = 0x0ff;
@@ -100,7 +93,7 @@ public class MOS65C02 extends CPU {
STACK = 0xff;
setWaitCycles(0);
}
public enum OPCODE {
ADC_IMM(0x0069, COMMAND.ADC, MODE.IMMEDIATE, 2),
ADC_ZP(0x0065, COMMAND.ADC, MODE.ZEROPAGE, 3),
@@ -144,15 +137,15 @@ public class MOS65C02 extends CPU {
BBS6(0x0ef, COMMAND.BBS6, MODE.ZP_REL, 5, true),
BBS7(0x0ff, COMMAND.BBS7, MODE.ZP_REL, 5, true),
BEQ_REL0(0x00F0, COMMAND.BEQ, MODE.RELATIVE, 2),
BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 3, true),
BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 2, true),
BIT_ZP(0x0024, COMMAND.BIT, MODE.ZEROPAGE, 3),
BIT_ZP_X(0x0034, COMMAND.BIT, MODE.ZEROPAGE_X, 3, true),
BIT_ZP_X(0x0034, COMMAND.BIT, MODE.ZEROPAGE_X, 4, true),
BIT_AB(0x002C, COMMAND.BIT, MODE.ABSOLUTE, 4),
BIT_AB_X(0x003C, COMMAND.BIT, MODE.ABSOLUTE_X, 4, true),
BMI_REL(0x0030, COMMAND.BMI, MODE.RELATIVE, 2),
BNE_REL(0x00D0, COMMAND.BNE, MODE.RELATIVE, 2),
BPL_REL(0x0010, COMMAND.BPL, MODE.RELATIVE, 2),
BRA_REL(0x0080, COMMAND.BRA, MODE.RELATIVE, 2, true),
BRA_REL(0x0080, COMMAND.BRA, MODE.RELATIVE, 3, true),
// BRK(0x0000, COMMAND.BRK, MODE.IMPLIED, 7),
// Do this so that BRK is treated as a two-byte instruction
BRK(0x0000, COMMAND.BRK, MODE.IMMEDIATE, 7),
@@ -317,23 +310,23 @@ public class MOS65C02 extends CPU {
TXS(0x009A, COMMAND.TXS, MODE.IMPLIED, 2),
TYA(0x0098, COMMAND.TYA, MODE.IMPLIED, 2),
WAI(0x00CB, COMMAND.WAI, MODE.IMPLIED, 3, true);
private int code;
private boolean isExtendedOpcode;
private final int code;
private final boolean isExtendedOpcode;
public int getCode() {
return code;
}
private int waitCycles;
private final int waitCycles;
public int getWaitCycles() {
return waitCycles;
}
private COMMAND command;
private final COMMAND command;
public COMMAND getCommand() {
return command;
}
private MODE addressingMode;
private final MODE addressingMode;
public MODE getMode() {
return addressingMode;
@@ -351,15 +344,15 @@ public class MOS65C02 extends CPU {
command.getProcessor().processCommand(address, value, addressingMode, cpu);
}
private OPCODE(int val, COMMAND c, MODE m, int wait) {
OPCODE(int val, COMMAND c, MODE m, int wait) {
this(val, c, m, wait, m.fetchValue, false);
}
private OPCODE(int val, COMMAND c, MODE m, int wait, boolean extended) {
OPCODE(int val, COMMAND c, MODE m, int wait, boolean extended) {
this(val, c, m, wait, m.fetchValue, extended);
}
private OPCODE(int val, COMMAND c, MODE m, int wait, boolean fetch, boolean extended) {
OPCODE(int val, COMMAND c, MODE m, int wait, boolean fetch, boolean extended) {
code = val;
waitCycles = wait - 1;
command = c;
@@ -369,9 +362,9 @@ public class MOS65C02 extends CPU {
}
}
public static interface AddressCalculator {
public interface AddressCalculator {
abstract int calculateAddress(MOS65C02 cpu);
int calculateAddress(MOS65C02 cpu);
default int getValue(boolean generateEvent, MOS65C02 cpu) {
int address = calculateAddress(cpu);
@@ -414,26 +407,23 @@ public class MOS65C02 extends CPU {
int address = 0x00FF & cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
address = cpu.getMemory().readWord(address, TYPE.READ_DATA, true, false);
int address2 = address + cpu.Y;
if ((address & 0x00ff00) != (address2 & 0x00ff00)) {
cpu.addWaitCycles(1);
}
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address2;
}),
ABSOLUTE(3, "$~2~1", (cpu) -> cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false)),
ABSOLUTE_X(3, "$~2~1,X", (cpu) -> {
int address2 = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
int address = 0x0FFFF & (address2 + cpu.X);
if ((address & 0x00FF00) != (address2 & 0x00FF00)) {
cpu.addWaitCycles(1);
}
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address;
}),
ABSOLUTE_Y(3, "$~2~1,Y", (cpu) -> {
int address2 = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
int address = 0x0FFFF & (address2 + cpu.Y);
if ((address & 0x00FF00) != (address2 & 0x00FF00)) {
cpu.addWaitCycles(1);
}
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address;
}),
ZP_REL(3, "$~1,$R", new AddressCalculator() {
@@ -444,6 +434,7 @@ public class MOS65C02 extends CPU {
int address = pc + 3 + cpu.getMemory().read(pc + 2, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
// The wait cycles are not added unless the branch actually happens!
cpu.setPageBoundaryPenalty((address & 0x00ff00) != ((pc+3) & 0x00ff00));
cpu.setPageBoundaryApplied(true);
return address;
}
@@ -454,7 +445,7 @@ public class MOS65C02 extends CPU {
return cpu.getMemory().read(address, TYPE.READ_DATA, true, false);
}
});
private int size;
private final int size;
public int getSize() {
return this.size;
@@ -464,27 +455,20 @@ public class MOS65C02 extends CPU {
// public String getFormat() {
// return this.format;
// }
private AddressCalculator calculator;
private final AddressCalculator calculator;
public int calcAddress(MOS65C02 cpu) {
return calculator.calculateAddress(cpu);
}
private boolean indirect;
public boolean isIndirect() {
return indirect;
}
String f1;
String f2;
boolean twoByte = false;
boolean relative = false;
boolean implied = true;
boolean fetchValue = true;
boolean fetchValue;
private MODE(int size, String fmt, AddressCalculator calc) {
MODE(int size, String fmt, AddressCalculator calc) {
this(size, fmt, calc, true);
}
private MODE(int size, String fmt, AddressCalculator calc, boolean fetch) {
MODE(int size, String fmt, AddressCalculator calc, boolean fetch) {
this.fetchValue = fetch;
this.size = size;
if (fmt.contains("~")) {
@@ -503,11 +487,6 @@ public class MOS65C02 extends CPU {
// this.format = fmt;
this.calculator = calc;
this.indirect = toString().startsWith("INDIRECT");
}
public MOS65C02.AddressCalculator getCalculator() {
return calculator;
}
public String formatMode(int pc, MOS65C02 cpu) {
@@ -528,9 +507,8 @@ public class MOS65C02 extends CPU {
}
}
public static interface CommandProcessor {
public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu);
public interface CommandProcessor {
void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu);
}
private static class BBRCommand implements CommandProcessor {
@@ -562,7 +540,8 @@ public class MOS65C02 extends CPU {
public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu) {
if ((value & (1 << bit)) != 0) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}
}
@@ -604,36 +583,24 @@ public class MOS65C02 extends CPU {
if (cpu.D) {
// Decimal Mode
w = (cpu.A & 0x0f) + (value & 0x0f) + cpu.C;
if (w >= 10) {
w = 0x010 | ((w + 6) & 0x0f);
if (w >= 0x0A) {
w = 0x10 | ((w + 6) & 0x0f);
}
w += (cpu.A & 0x0f0) + (value & 0x00f0);
if (w >= 0x0A0) {
w += (cpu.A & 0xf0) + (value & 0xf0);
if (w >= 0xA0) {
cpu.C = 1;
if (cpu.V && w >= 0x0180) {
cpu.V = false;
}
w += 0x060;
cpu.V &= w < 0x180;
w += 0x60;
} else {
cpu.C = 0;
if (cpu.V && w < 0x080) {
cpu.V = false;
}
cpu.V &= w >= 0x80;
}
cpu.addWaitCycles(1);
} else {
// Binary Mode
w = cpu.A + value + cpu.C;
if (w >= 0x0100) {
cpu.C = 1;
if (cpu.V && w >= 0x0180) {
cpu.V = false;
}
} else {
cpu.C = 0;
if (cpu.V && w < 0x080) {
cpu.V = false;
}
}
cpu.V = ((cpu.A ^ w) & (value ^ w) & 0x080) != 0;
cpu.C = (w >= 0x0100) ? 1 : 0;
}
cpu.A = w & 0x0ff;
cpu.setNZ(cpu.A);
@@ -675,19 +642,22 @@ public class MOS65C02 extends CPU {
BCC((address, value, addressMode, cpu) -> {
if (cpu.C == 0) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
BCS((address, value, addressMode, cpu) -> {
if (cpu.C != 0) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
BEQ((address, value, addressMode, cpu) -> {
if (cpu.Z) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
BIT((address, value, addressMode, cpu) -> {
@@ -702,24 +672,27 @@ public class MOS65C02 extends CPU {
BMI((address, value, addressMode, cpu) -> {
if (cpu.N) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
BNE((address, value, addressMode, cpu) -> {
if (!cpu.Z) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
BPL((address, value, addressMode, cpu) -> {
if (!cpu.N) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
BRA((address, value, addressMode, cpu) -> {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 1 : 0);
cpu.setPageBoundaryApplied(true);
}),
BRK((address, value, addressMode, cpu) -> {
cpu.BRK();
@@ -727,13 +700,15 @@ public class MOS65C02 extends CPU {
BVC((address, value, addressMode, cpu) -> {
if (!cpu.V) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
BVS((address, value, addressMode, cpu) -> {
if (cpu.V) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}),
CLC((address, value, addressMode, cpu) -> {
@@ -769,6 +744,7 @@ public class MOS65C02 extends CPU {
cpu.getMemory().write(address, (byte) value, true, false);
cpu.getMemory().write(address, (byte) value, true, false);
cpu.setNZ(value);
cpu.setPageBoundaryApplied(false); // AB,X already takes 7 cycles, no boundary penalties
}),
DEA((address, value, addressMode, cpu) -> {
cpu.A = 0x0FF & (cpu.A - 1);
@@ -792,6 +768,7 @@ public class MOS65C02 extends CPU {
cpu.getMemory().write(address, (byte) value, true, false);
cpu.getMemory().write(address, (byte) value, true, false);
cpu.setNZ(value);
cpu.setPageBoundaryApplied(false); // AB,X already takes 7 cycles, no boundary penalties
}),
INA((address, value, addressMode, cpu) -> {
cpu.A = 0x0FF & (cpu.A + 1);
@@ -919,47 +896,32 @@ public class MOS65C02 extends CPU {
cpu.setProgramCounter(cpu.popWord() + 1);
}),
SBC((address, value, addressMode, cpu) -> {
cpu.V = ((cpu.A ^ value) & 0x080) != 0;
int w;
if (cpu.D) {
int temp = 0x0f + (cpu.A & 0x0f) - (value & 0x0f) + cpu.C;
if (temp < 0x10) {
w = 0;
temp -= 6;
} else {
w = 0x10;
temp -= 0x10;
}
w += 0x00f0 + (cpu.A & 0x00f0) - (value & 0x00f0);
if (w < 0x100) {
cpu.C = 0;
if (cpu.V && w < 0x080) {
cpu.V = false;
}
w -= 0x60;
} else {
cpu.C = 1;
if (cpu.V && w >= 0x180) {
cpu.V = false;
}
}
w += temp;
if (!cpu.D) {
ADC.getProcessor().processCommand(address, 0x0ff & (~value), addressMode, cpu);
} else {
w = 0x0ff + cpu.A - value + cpu.C;
if (w < 0x100) {
cpu.V = ((cpu.A ^ value) & 0x80) != 0;
int w = 0x0F + (cpu.A & 0x0F) - (value & 0x0F) + cpu.C;
int val = 0;
if (w < 0x10) {
w -= 0x06;
} else {
val = 0x10;
w -= 0x10;
}
val += 0xF0 + (cpu.A & 0xF0) - (value & 0xF0);
if (val < 0x100) {
cpu.C = 0;
if (cpu.V && (w < 0x080)) {
cpu.V = false;
}
cpu.V &= val >= 0x80;
val -= 0x60;
} else {
cpu.C = 1;
if (cpu.V && (w >= 0x180)) {
cpu.V = false;
}
cpu.V &= val < 0x180;
}
val += w;
cpu.A = val & 0xFF;
cpu.setNZ(cpu.A);
cpu.addWaitCycles(1);
}
cpu.A = w & 0x0ff;
cpu.setNZ(cpu.A);
}),
SEC((address, value, addressMode, cpu) -> {
cpu.C = 1;
@@ -980,6 +942,7 @@ public class MOS65C02 extends CPU {
SMB7(new SMBCommand(7)),
STA(true, (address, value, addressMode, cpu) -> {
cpu.getMemory().write(address, (byte) cpu.A, true, false);
cpu.setPageBoundaryApplied(false); // AB,X has no noted penalty for this opcode
}),
STP((address, value, addressMode, cpu) -> {
cpu.suspend();
@@ -992,6 +955,7 @@ public class MOS65C02 extends CPU {
}),
STZ(true, (address, value, addressMode, cpu) -> {
cpu.getMemory().write(address, (byte) 0, true, false);
cpu.setPageBoundaryApplied(false); // AB,X has no noted penalty for this opcode
}),
TAX((address, value, addressMode, cpu) -> {
cpu.X = cpu.A;
@@ -1027,30 +991,29 @@ public class MOS65C02 extends CPU {
WAI((address, value, addressMode, cpu) -> {
cpu.waitForInterrupt();
});
private CommandProcessor processor;
private final CommandProcessor processor;
public CommandProcessor getProcessor() {
return processor;
}
private boolean storeOnly;
private final boolean storeOnly;
public boolean isStoreOnly() {
return storeOnly;
}
private COMMAND(CommandProcessor processor) {
COMMAND(CommandProcessor processor) {
this(false, processor);
}
private COMMAND(boolean storeOnly, CommandProcessor processor) {
COMMAND(boolean storeOnly, CommandProcessor processor) {
this.storeOnly = storeOnly;
this.processor = processor;
}
}
static private OPCODE[] opcodes;
private final OPCODE[] opcodes = new OPCODE[256];
static {
opcodes = new OPCODE[256];
private void initOpcodes() {
for (OPCODE o : OPCODE.values()) {
opcodes[o.getCode()] = o;
}
@@ -1065,7 +1028,7 @@ public class MOS65C02 extends CPU {
String traceEntry = null;
if (isSingleTraceEnabled() || isTraceEnabled() || isLogEnabled() || warnAboutExtendedOpcodes) {
traceEntry = getState().toUpperCase() + " " + Integer.toString(pc, 16) + " : " + disassemble();
traceEntry = String.format("%s %X : %s; %s", getState(), pc, disassemble(), getMemory().getState());
captureSingleTrace(traceEntry);
if (isTraceEnabled()) {
LOG.log(Level.INFO, traceEntry);
@@ -1084,38 +1047,38 @@ public class MOS65C02 extends CPU {
log(">>EXTENDED OPCODE DETECTED " + Integer.toHexString(opcode.code) + "<<");
log(traceEntry);
}
}
}
if (opcode == null) {
// handle bad opcode as a NOP
int wait = 0;
int bytes = 2;
int bytes;
int n = op & 0x0f;
switch (n) {
case 2:
case 2 -> {
bytes = 2;
wait = 2;
break;
case 3:
case 7:
case 0x0b:
case 0x0f:
}
case 3, 7, 0x0b, 0x0f -> {
wait = 1;
bytes = 1;
break;
case 4:
}
case 4 -> {
bytes = 2;
if ((op & 0x0f0) == 0x040) {
wait = 3;
} else {
wait = 4;
} break;
case 0x0c:
}
}
case 0x0c -> {
bytes = 3;
if ((op & 0x0f0) == 0x050) {
wait = 8;
} else {
wait = 4;
} break;
default:
}
}
default -> bytes = 2;
}
incrementProgramCounter(bytes);
addWaitCycles(wait);
@@ -1131,6 +1094,11 @@ public class MOS65C02 extends CPU {
incrementProgramCounter(opcode.getMode().getSize());
opcode.execute(this);
addWaitCycles(opcode.getWaitCycles());
if (isPageBoundaryPenalty()) {
addWaitCycles(1);
}
setPageBoundaryPenalty(false);
setPageBoundaryApplied(false);
}
}
@@ -1155,8 +1123,7 @@ public class MOS65C02 extends CPU {
public byte pop() {
STACK = (STACK + 1) & 0x0FF;
byte val = getMemory().read(0x0100 + STACK, TYPE.READ_DATA, true, false);
return val;
return getMemory().read(0x0100 + STACK, TYPE.READ_DATA, true, false);
}
private byte getStatus() {
@@ -1192,8 +1159,8 @@ public class MOS65C02 extends CPU {
@Override
public void JSR(int address) {
pushPC();
setProgramCounter(address);
pushPC();
setProgramCounter(address);
}
public void BRK() {
@@ -1231,10 +1198,6 @@ public class MOS65C02 extends CPU {
}
}
public int getSTACK() {
return STACK;
}
// Cold/Warm boot procedure
@Override
public void reset() {
@@ -1249,7 +1212,8 @@ public class MOS65C02 extends CPU {
// N = true;
// V = true;
// Z = true;
int newPC = getMemory().readWord(RESET_VECTOR, TYPE.READ_DATA, true, false);
int resetVector = getMemory().readWord(RESET_VECTOR, TYPE.READ_DATA, true, false);
int newPC = resetVector;
LOG.log(Level.WARNING, "Reset called, setting PC to ({0}) = {1}", new Object[]{Integer.toString(RESET_VECTOR, 16), Integer.toString(newPC, 16)});
setProgramCounter(newPC);
}
@@ -1282,27 +1246,23 @@ public class MOS65C02 extends CPU {
}
public String getState() {
StringBuilder out = new StringBuilder();
out.append(byte2(A)).append(" ");
out.append(byte2(X)).append(" ");
out.append(byte2(Y)).append(" ");
// out += "PC:"+wordString(getProgramCounter())+" ";
out.append("01").append(byte2(STACK)).append(" ");
out.append(getFlags());
return out.toString();
return byte2(A) +
" " + byte2(X) +
" " + byte2(Y) +
" 01" + byte2(STACK) +
getFlags();
}
public String getFlags() {
StringBuilder out = new StringBuilder();
out.append(N ? "N" : ".");
out.append(V ? "V" : ".");
out.append("R");
out.append(B ? "B" : ".");
out.append(D ? "D" : ".");
out.append(I ? "I" : ".");
out.append(Z ? "Z" : ".");
out.append((C != 0) ? "C" : ".");
return out.toString();
StringBuilder sb = new StringBuilder(7);
sb.append(N ? "N" : ".");
sb.append(V ? "V" : ".");
sb.append(B ? "B" : ".");
sb.append(D ? "D" : ".");
sb.append(I ? "I" : ".");
sb.append(Z ? "Z" : ".");
sb.append(C != 0 ? "C" : ".");
return sb.toString();
}
public String disassemble() {
@@ -1324,21 +1284,27 @@ public class MOS65C02 extends CPU {
((o.getMode().getSize() > 2) ?
byte2(b2) : " " ) + " ";
*/
StringBuilder out = new StringBuilder(o.getCommand().toString());
out.append(" ").append(format);
return out.toString();
return String.format("%s %s", o.getCommand().toString(), format);
}
private boolean pageBoundaryPenalty = false;
private boolean applyPageBoundaryPenalty = false;
private void setPageBoundaryPenalty(boolean b) {
pageBoundaryPenalty = b;
}
public void setPageBoundaryApplied(boolean e) {
applyPageBoundaryPenalty = e;
}
boolean isPageBoundaryPenalty() {
return applyPageBoundaryPenalty && pageBoundaryPenalty;
}
@Override
public void pushPC() {
pushWord(getProgramCounter() - 1);
}
// FC is typically a NOP instruction, but let's pretend it is our own opcode that we can use for special commands
HashMap<Integer, Consumer<Byte>> extendedCommandHandlers = new HashMap<>();
/**
* Special commands -- these are usually treated as NOP but can be reused for emulator controls
* !byte $fc, $65, $00 ; Turn off tracing
@@ -1347,40 +1313,59 @@ public class MOS65C02 extends CPU {
* !byte $fc, $5b, NN ; print number NN to stdout with newline
* !byte $fc, $5c, NN ; print character NN to stdout
* @param param1
* @param param2
* @param param2
*/
public void performExtendedCommand(byte param1, byte param2) {
// LOG.log(Level.INFO, "Extended command {0},{1}", new Object[]{Integer.toHexString(param1), Integer.toHexString(param2)});
switch (param1 & 0x0ff) {
case 0x50:
// System out
// System out #
System.out.print(param2 & 0x0ff);
break;
case 0x5b:
// System out (with line break)
// System out # (with line break)
System.out.println(param2 & 0x0ff);
break;
case 0x5c:
// System out (with line break)
System.out.println((char) (param2 & 0x0ff));
// System out char
System.out.print((char) (param2 & 0x0ff));
break;
case 0x65:
// CPU functions
switch (param2 & 0x0ff) {
case 0x00:
// Turn off tracing
case 0x00 -> // Turn off tracing
trace = false;
break;
case 0x01:
// Turn on tracing
case 0x01 -> // Turn on tracing
trace = true;
break;
}
break;
case 0x64:
// Memory functions
getMemory().performExtendedCommand(param2 & 0x0ff);
break;
default:
Consumer<Byte> handler = extendedCommandHandlers.get((int) param1);
if (handler != null) {
handler.accept(param2);
}
}
}
public void registerExtendedCommandHandler(int param1, Consumer<Byte> handler) {
extendedCommandHandlers.put(param1, handler);
}
public void unregisterExtendedCommandHandler(int param1) {
extendedCommandHandlers.remove(param1);
}
public void unregisterExtendedCommandHandler(Consumer<Byte> handler) {
extendedCommandHandlers.values().remove(handler);
}
@Override
public void reconfigure() {
// Nothing to do here
}
}

View File

@@ -1,36 +1,40 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jace.Emulator;
import jace.config.DeviceEnum;
import jace.core.CPU;
import jace.core.Card;
import jace.core.PagedMemory;
import jace.core.RAM;
import jace.hardware.CardExt80Col;
import jace.hardware.CardRamworks;
import jace.state.Stateful;
/**
* Implementation of a 128k memory space and the MMU found in an Apple //e. The
@@ -40,9 +44,42 @@ import java.util.logging.Logger;
*/
@Stateful
abstract public class RAM128k extends RAM {
Logger LOG = Logger.getLogger(RAM128k.class.getName());
// Memory card implementations
public static enum RamCards implements DeviceEnum<RAM128k> {
CardExt80Col("80-Column Card (128k)", CardExt80Col.class, CardExt80Col::new),
CardRamworks("Ramworks (4mb)", CardRamworks.class, CardRamworks::new);
Supplier<? extends RAM128k> factory;
String name;
Class<? extends RAM128k> clazz;
RamCards(String name, Class<? extends RAM128k> clazz, Supplier<? extends RAM128k> factory) {
this.factory = factory;
this.name = name;
this.clazz = clazz;
}
@Override
public RAM128k create() {
return factory.get();
}
@Override
public String getName() {
return name;
}
@Override
public boolean isInstance(RAM128k card) {
return card != null && clazz.equals(card.getClass());
}
}
static final Logger LOG = Logger.getLogger(RAM128k.class.getName());
Map<String, PagedMemory> banks;
Map<String, PagedMemory> memoryConfigurations = new HashMap<>();
String state = "???";
private Map<String, PagedMemory> getBanks() {
if (banks == null) {
@@ -76,36 +113,38 @@ abstract public class RAM128k extends RAM {
return banks;
}
@Override
public String getState() {
return state;
}
@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;
}
if (param == 0xda) {// 64 da : Dump all memory mappings
System.out.println("Active banks");
for (int i = 0; i < 256; i++) {
byte[] read = this.activeRead.get(i);
byte[] write = this.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");
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:
}
return false;
}).findFirst().orElse("unknown");
LOG.log(Level.INFO, "Bank {0}\t{1}\t{2}", new Object[]{Integer.toHexString(i), readBank, writeBank});
}
}
}
@@ -119,22 +158,17 @@ abstract public class RAM128k extends RAM {
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 RAM128k() {
super();
mainMemory = new PagedMemory(0xc000, PagedMemory.Type.RAM);
rom = new PagedMemory(0x3000, PagedMemory.Type.FIRMWARE_MAIN);
cPageRom = new PagedMemory(0x1000, PagedMemory.Type.SLOW_ROM);
languageCard = new PagedMemory(0x3000, PagedMemory.Type.LANGUAGE_CARD);
languageCard2 = new PagedMemory(0x1000, PagedMemory.Type.LANGUAGE_CARD);
activeRead = new PagedMemory(0x10000, PagedMemory.Type.RAM);
activeWrite = new PagedMemory(0x10000, PagedMemory.Type.RAM);
blank = new PagedMemory(0x100, PagedMemory.Type.RAM);
zeroAllRam();
}
public final void initMemoryPattern(PagedMemory mem) {
@@ -147,133 +181,275 @@ abstract public class RAM128k extends RAM {
}
}
private final Semaphore configurationSemaphone = new Semaphore(1, true);
public final void zeroAllRam() {
for (int i = 0; i < 0x0100; i++) {
blank.get(0)[i] = (byte) 0x0FF;
}
initMemoryPattern(mainMemory);
if (getAuxMemory() != null) {
initMemoryPattern(getAuxMemory());
}
}
public String getReadConfiguration() {
String rstate = "";
if (SoftSwitches.RAMRD.getState()) {
rstate += "Ra";
} else {
rstate += "R0";
}
String LCR = "L0R";
if (SoftSwitches.LCRAM.isOn()) {
if (SoftSwitches.AUXZP.isOff()) {
LCR = "L1R";
if (SoftSwitches.LCBANK1.isOff()) {
LCR = "L2R";
}
} else {
LCR = "L1aR";
if (SoftSwitches.LCBANK1.isOff()) {
LCR = "L2aR";
}
}
}
rstate += LCR;
if (SoftSwitches.CXROM.getState()) {
rstate += "CXROM";
} else {
rstate += "!CX";
if (SoftSwitches.SLOTC3ROM.isOff()) {
rstate += "C3";
}
if (SoftSwitches.INTC8ROM.isOn()) {
rstate += "C8";
} else {
rstate += "C8"+getActiveSlot();
}
}
return rstate;
}
public String getWriteConfiguration() {
String wstate = "";
if (SoftSwitches.RAMWRT.getState()) {
wstate += "Wa";
} else {
wstate += "W0";
}
String LCW = "L0W";
if (SoftSwitches.LCWRITE.isOn()) {
if (SoftSwitches.AUXZP.isOff()) {
LCW = "L1W";
if (SoftSwitches.LCBANK1.isOff()) {
LCW = "L2W";
}
} else {
LCW = "L1aW";
if (SoftSwitches.LCBANK1.isOff()) {
LCW = "L2aW";
}
}
}
wstate += LCW;
return wstate;
}
public String getAuxZPConfiguration() {
String astate = "";
if (SoftSwitches._80STORE.isOn()) {
astate += "80S";
if (SoftSwitches.PAGE2.isOn()) {
astate += "2";
}
if (SoftSwitches.HIRES.isOn()) {
astate += "H";
}
}
// Handle zero-page bankswitching
if (SoftSwitches.AUXZP.getState()) {
astate += "Za";
} else {
astate += "Z0";
}
return astate;
}
public PagedMemory buildReadConfiguration() {
PagedMemory read = new PagedMemory(0x10000, PagedMemory.Type.RAM);
// First off, set up read/write for main memory (might get changed later on)
read.fillBanks(SoftSwitches.RAMRD.getState() ? getAuxMemory() : mainMemory);
// Handle language card softswitches
read.fillBanks(rom);
if (SoftSwitches.LCRAM.isOn()) {
if (SoftSwitches.AUXZP.isOff()) {
read.fillBanks(languageCard);
if (SoftSwitches.LCBANK1.isOff()) {
read.fillBanks(languageCard2);
}
} else {
read.fillBanks(getAuxLanguageCard());
if (SoftSwitches.LCBANK1.isOff()) {
read.fillBanks(getAuxLanguageCard2());
}
}
}
// Handle 80STORE logic for bankswitching video ram
if (SoftSwitches._80STORE.isOn()) {
read.setBanks(0x04, 0x04, 0x04,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
if (SoftSwitches.HIRES.isOn()) {
read.setBanks(0x020, 0x020, 0x020,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
}
}
// Handle zero-page bankswitching
if (SoftSwitches.AUXZP.getState()) {
// Aux pages 0 and 1
read.setBanks(0, 2, 0, getAuxMemory());
} else {
// Main pages 0 and 1
read.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
read.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);
read.setBanks(0, 1, 0x0c0 + slot, page);
}
if (getActiveSlot() == 0) {
for (int i = 0x0C8; i < 0x0D0; i++) {
read.set(i, blank.get(0));
}
} else {
getCard(getActiveSlot()).ifPresent(c -> read.setBanks(0, 8, 0x0c8, c.getC8Rom()));
}
if (SoftSwitches.SLOTC3ROM.isOff()) {
// Enable C3 to point to internal ROM
read.setBanks(2, 1, 0x0C3, cPageRom);
}
if (SoftSwitches.INTC8ROM.isOn()) {
// Enable C8-CF to point to internal ROM
read.setBanks(7, 8, 0x0C8, cPageRom);
}
}
// All ROM reads not intecepted will return 0xFF!
read.set(0x0c0, blank.get(0));
return read;
}
public PagedMemory buildWriteConfiguration() {
PagedMemory write = new PagedMemory(0x10000, PagedMemory.Type.RAM);
// First off, set up read/write for main memory (might get changed later on)
write.fillBanks(SoftSwitches.RAMWRT.getState() ? getAuxMemory() : mainMemory);
// Handle language card softswitches
for (int i = 0x0c0; i < 0x0d0; i++) {
write.set(i, null);
}
if (SoftSwitches.LCWRITE.isOn()) {
if (SoftSwitches.AUXZP.isOff()) {
write.fillBanks(languageCard);
if (SoftSwitches.LCBANK1.isOff()) {
write.fillBanks(languageCard2);
}
} else {
write.fillBanks(getAuxLanguageCard());
if (SoftSwitches.LCBANK1.isOff()) {
write.fillBanks(getAuxLanguageCard2());
}
}
} else {
// Make 0xd000 - 0xffff non-writable!
for (int i = 0x0d0; i < 0x0100; i++) {
write.set(i, null);
}
}
// Handle 80STORE logic for bankswitching video ram
if (SoftSwitches._80STORE.isOn()) {
write.setBanks(0x04, 0x04, 0x04,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
if (SoftSwitches.HIRES.isOn()) {
write.setBanks(0x020, 0x020, 0x020,
SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
}
}
// Handle zero-page bankswitching
if (SoftSwitches.AUXZP.getState()) {
// Aux pages 0 and 1
write.setBanks(0, 2, 0, getAuxMemory());
} else {
// Main pages 0 and 1
write.setBanks(0, 2, 0, mainMemory);
}
return write;
}
/**
*
*/
@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);
String auxZpConfiguration = getAuxZPConfiguration();
String readConfiguration = getReadConfiguration() + auxZpConfiguration;
String writeConfiguration = getWriteConfiguration() + auxZpConfiguration;
String newState = readConfiguration + ";" + writeConfiguration;
if (newState.equals(state)) {
return;
}
state = newState;
// 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());
}
}
}
log("MMU Switches");
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);
}
}
if (memoryConfigurations.containsKey(readConfiguration)) {
activeRead = memoryConfigurations.get(readConfiguration);
} else {
activeRead = buildReadConfiguration();
memoryConfigurations.put(readConfiguration, activeRead);
}
// 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);
if (memoryConfigurations.containsKey(writeConfiguration)) {
activeWrite = memoryConfigurations.get(writeConfiguration);
} else {
activeWrite = buildWriteConfiguration();
memoryConfigurations.put(writeConfiguration, activeWrite);
}
}
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() + ");";
Emulator.withComputer(computer -> {
CPU cpu = computer.getCpu();
if (cpu != null && cpu.isLogEnabled()) {
StringBuilder stack = new StringBuilder();
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
stack.append(String.format("%s.%s(%s);",e.getClassName(), e.getMethodName(), e.getLineNumber()));
}
cpu.log(stack.toString());
String switches = Stream.of(
SoftSwitches.RAMRD, SoftSwitches.RAMWRT, SoftSwitches.AUXZP,
SoftSwitches._80STORE, SoftSwitches.HIRES, SoftSwitches.PAGE2,
SoftSwitches.LCBANK1, SoftSwitches.LCRAM, SoftSwitches.LCWRITE
).map(Object::toString).collect(Collectors.joining(";"));
cpu.log(String.join(";", message, switches));
}
cpu.log(stack);
cpu.log(message + ";" + SoftSwitches.RAMRD + ";" + SoftSwitches.RAMWRT + ";" + SoftSwitches.AUXZP + ";" + SoftSwitches._80STORE + ";" + SoftSwitches.HIRES + ";" + SoftSwitches.PAGE2 + ";" + SoftSwitches.LCBANK1 + ";" + SoftSwitches.LCRAM + ";" + SoftSwitches.LCWRITE);
}
});
}
/**
@@ -285,14 +461,21 @@ abstract public class RAM128k extends RAM {
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++) {
byte[][] restore = new byte[18][];
for (int i = 0; i < 17; i++) {
restore[i] = activeWrite.get(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);
InputStream inputRom = getClass().getResourceAsStream(path);
if (inputRom == null) {
LOG.log(Level.SEVERE, "Rom not found: {0}", path);
return;
}
// Clear cached configurations as we might have outdated references now
memoryConfigurations.clear();
int read = 0;
int addr = 0;
byte[] in = new byte[1024];
@@ -303,6 +486,9 @@ abstract public class RAM128k extends RAM {
}
// System.out.println("Finished reading rom with " + inputRom.available() + " bytes left unread!");
//dump();
for (int i = 0; i < 17; i++) {
activeWrite.set(i, restore[i]);
}
configureActiveMemory();
}
@@ -349,16 +535,27 @@ abstract public class RAM128k extends RAM {
return rom;
}
void copyFrom(RAM128k currentMemory) {
@Override
public void copyFrom(RAM otherMemory) {
RAM128k currentMemory = (RAM128k) otherMemory;
// 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;
// Clear cached configurations as we might have outdated references now
memoryConfigurations.clear();
super.copyFrom(otherMemory);
}
@Override
public void resetState() {
memoryConfigurations.clear();
}
}

View File

@@ -1,23 +1,22 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e;
import jace.Emulator;
import jace.apple2e.softswitch.IntC8SoftSwitch;
import jace.apple2e.softswitch.KeyboardSoftSwitch;
import jace.apple2e.softswitch.Memory2SoftSwitch;
@@ -25,6 +24,7 @@ import jace.apple2e.softswitch.MemorySoftSwitch;
import jace.apple2e.softswitch.VideoSoftSwitch;
import jace.core.RAMEvent;
import jace.core.SoftSwitch;
import jace.core.Video;
/**
* Softswitches reside in the addresses C000-C07f and control everything from
@@ -62,7 +62,7 @@ public enum SoftSwitches {
@Override
public void stateChanged() {
super.stateChanged();
computer.getVideo().forceRefresh();
Video.forceRefresh();
}
}),
TEXT(new VideoSoftSwitch("Text", 0x0c050, 0x0c051, 0x0c01a, RAMEvent.TYPE.ANY, true)),
@@ -70,21 +70,11 @@ public enum SoftSwitches {
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();
Emulator.withMemory(m->m.configureActiveMemory());
} else {
computer.getVideo().configureVideoMode();
Emulator.withVideo(v->v.configureVideoMode());
}
}
}),
@@ -101,7 +91,7 @@ public enum SoftSwitches {
@Override
protected byte readSwitch() {
setState(true);
return computer.getVideo().getFloatingBus();
return Emulator.withComputer(c->c.getVideo().getFloatingBus(), (byte) 0);
}
@Override
@@ -128,7 +118,7 @@ public enum SoftSwitches {
KEYBOARD_STROBE_READ(new SoftSwitch("KeyStrobe_Read", 0x0c010, -1, -1, RAMEvent.TYPE.READ, false) {
@Override
protected byte readSwitch() {
return computer.getVideo().getFloatingBus();
return Emulator.withComputer(c->c.getVideo().getFloatingBus(), (byte) 0);
}
@Override
@@ -141,10 +131,7 @@ public enum SoftSwitches {
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();
return Emulator.withComputer(c->c.getVideo().getFloatingBus(), (byte) 0);
}
@Override
@@ -167,7 +154,7 @@ public enum SoftSwitches {
/**
* Creates a new instance of SoftSwitches
*/
private SoftSwitches(SoftSwitch softswitch) {
SoftSwitches(SoftSwitch softswitch) {
this.softswitch = softswitch;
}

View File

@@ -1,43 +1,43 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e;
import jace.JaceApplication;
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.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.Emulator;
import jace.JaceApplication;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import jace.core.Device;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.SoundMixer;
import jace.core.SoundMixer.SoundBuffer;
import jace.core.SoundMixer.SoundError;
import jace.core.TimedDevice;
import jace.core.Utility;
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
@@ -48,7 +48,14 @@ public class Speaker extends Device {
static boolean fileOutputActive = false;
static OutputStream out;
@ConfigurableField(category = "sound", name = "1mhz timing", description = "Force speaker output to 1mhz?")
public static boolean force1mhz = true;
@ConfigurableField(category = "sound", name = "Show sound", description = "Use black color value to show sound output")
public static boolean showSound = false;
@InvokableAction(category = "sound", name = "Record sound", description="Toggles recording (saving) sound output to a file", defaultKeyMapping = "ctrl+shift+w")
public static void toggleFileOutput() {
if (fileOutputActive) {
try {
@@ -64,12 +71,6 @@ public class Speaker extends Device {
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;
@@ -78,6 +79,7 @@ public class Speaker extends Device {
}
}
}
/**
* Counter tracks the number of cycles between sampling
*/
@@ -91,57 +93,27 @@ public class Speaker extends Device {
* (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;
public static int VOLUME = 400;
private int currentVolume = 0;
private int fadeOffAmount = 1;
/**
* 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 static double TICKS_PER_SAMPLE = ((double) TimedDevice.NTSC_1MHZ) / SoundMixer.RATE;
private RAMListener listener = null;
private SoundBuffer buffer = null;
/**
* Creates a new instance of Speaker
*
* @param computer
* Number of idle cycles until speaker playback is deactivated
*/
public Speaker(Computer computer) {
super(computer);
}
@ConfigurableField(name = "Idle cycles before sleep", shortName = "idle")
// public static int MAX_IDLE_CYCLES = (int) (SoundMixer.BUFFER_SIZE * TICKS_PER_SAMPLE * 2);
public static int MAX_IDLE_CYCLES = (int) TimedDevice.NTSC_1MHZ / 4;
/**
* Suspend playback of sound
@@ -151,11 +123,17 @@ public class Speaker extends Device {
@Override
public boolean suspend() {
boolean result = super.suspend();
playbackTimer.cancel();
speakerBit = false;
sdl = null;
computer.getMotherboard().cancelSpeedRequest(this);
computer.mixer.returnLine(this);
if (buffer != null) {
try {
buffer.shutdown();
} catch (InterruptedException | ExecutionException | SoundError e) {
// Ignore
} finally {
buffer = null;
}
}
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));
return result;
}
@@ -165,48 +143,41 @@ public class Speaker extends Device {
*/
@Override
public void resume() {
if (sdl != null && isRunning()) {
if (Utility.isHeadlessMode()) {
return;
}
try {
if (sdl == null || !sdl.isOpen()) {
sdl = computer.mixer.getLine(this);
if (buffer == null || !buffer.isAlive()) {
try {
buffer = SoundMixer.createBuffer(false);
} catch (InterruptedException | ExecutionException | SoundError e) {
e.printStackTrace();
detach();
return;
}
sdl.start();
setRun(true);
}
if (buffer != null) {
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);
} else {
Logger.getLogger(getClass().getName()).severe("Unable to get audio buffer for speaker!");
detach();
return;
}
}
public void playCurrentBuffer() {
byte[] buffer;
int len;
synchronized (bufferLock) {
len = bufferPos;
buffer = primaryBuffer;
primaryBuffer = secondaryBuffer;
bufferPos = 0;
if (force1mhz) {
TICKS_PER_SAMPLE = ((double) TimedDevice.NTSC_1MHZ) / SoundMixer.RATE;
} else {
TICKS_PER_SAMPLE = Emulator.withComputer(c-> ((double) c.getMotherboard().getSpeedInHz()) / SoundMixer.RATE, 0.0);
}
secondaryBuffer = buffer;
sdl.write(buffer, 0, len);
super.resume();
}
/**
* Reset idle counter whenever sound playback occurs
*/
public void resetIdle() {
currentVolume = VOLUME;
idleCycles = 0;
if (!isRunning()) {
resume();
@@ -220,59 +191,88 @@ public class Speaker extends Device {
*/
@Override
public void tick() {
if (!isRunning() || sdl == null) {
return;
}
if (idleCycles++ >= MAX_IDLE_CYCLES) {
suspend();
}
if (speakerBit) {
level++;
if (showSound) {
VideoNTSC.CHANGE_BLACK_COLOR(40, 20, 20);
}
} else if (showSound) {
VideoNTSC.CHANGE_BLACK_COLOR(20,20,40);
}
if (idleCycles++ >= MAX_IDLE_CYCLES && (currentVolume <= 0 || !speakerBit)) {
suspend();
if (showSound) {
VideoNTSC.CHANGE_BLACK_COLOR(0,0,0);
}
}
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;
if (idleCycles >= MAX_IDLE_CYCLES) {
currentVolume -= fadeOffAmount;
}
playSample(level * currentVolume);
// Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
// Set level back to 0
level = 0;
// Set counter to 0
counter -= TICKS_PER_SAMPLE_FLOOR;
counter -= TICKS_PER_SAMPLE;
}
}
private void toggleSpeaker(RAMEvent e) {
if (e.getType() == RAMEvent.TYPE.WRITE) {
level += 2;
} else {
speakerBit = !speakerBit;
}
// if (e.getType() == RAMEvent.TYPE.WRITE) {
// level += 2;
// }
speakerBit = !speakerBit;
resetIdle();
}
private void playSample(int sample) {
try {
if (buffer == null || !buffer.isAlive()) {
// Logger.getLogger(getClass().getName()).severe("Audio buffer not initalized properly!");
buffer = SoundMixer.createBuffer(false);
if (buffer == null) {
System.err.println("Unable to create emergency audio buffer, detaching speaker");
detach();
return;
}
}
buffer.playSample((short) sample);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} catch (SoundError e) {
System.err.println("Sound error, detaching speaker: " + e.getMessage());
e.printStackTrace();
detach();
buffer = null;
}
if (fileOutputActive) {
byte[] bytes = new byte[2];
bytes[0] = (byte) (sample & 0x0ff);
bytes[1] = (byte) ((sample >> 8) & 0x0ff);
try {
out.write(bytes, 0, 2);
} catch (IOException ex) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Error recording sound", ex);
toggleFileOutput();
}
}
}
/**
* 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);
listener = Emulator.withMemory(m->m.observe("Speaker", RAMEvent.TYPE.ANY, 0x0c030, 0x0c03f, this::toggleSpeaker), null);
}
private void removeListener() {
computer.getMemory().removeListener(listener);
Emulator.withMemory(m->m.removeListener(listener));
}
/**
@@ -292,24 +292,16 @@ public class Speaker extends Device {
@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

@@ -1,33 +1,28 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e;
import jace.core.Computer;
import java.util.logging.Logger;
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;
@@ -65,7 +60,7 @@ public class VideoDHGR extends Video {
private VideoWriter dhiresPage1;
private VideoWriter dhiresPage2;
// Mixed mode
private final VideoWriter mixed;
private VideoWriter mixed;
private VideoWriter currentGraphicsWriter = null;
private VideoWriter currentTextWriter = null;
@@ -74,8 +69,142 @@ public class VideoDHGR extends Video {
*
* @param computer
*/
public VideoDHGR(Computer computer) {
super(computer);
public VideoDHGR() {
super();
initCharMap();
initHgrDhgrTables();
initVideoWriters();
registerDirtyFlagChecks();
currentTextWriter = textPage1;
currentGraphicsWriter = loresPage1;
}
// 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...
final int[][] HGR_TO_DHGR = new int[512][256];
// Take two consecutive bytes and double them, disregarding hi-bit
// Useful for text mode
final int[][] HGR_TO_DHGR_BW = new int[256][256];
final int[] TIMES_14 = new int[40];
final int[] FLIP_BITS = new int[256];
private void initHgrDhgrTables() {
// complete reverse of 8 bits
for (int i = 0; i < 256; i++) {
FLIP_BITS[i] = (((i * 0x0802 & 0x22110) | (i * 0x8020 & 0x88440)) * 0x10101 >> 16) & 0x0ff;
}
for (int i = 0; i < 40; i++) {
TIMES_14[i] = i * 14;
}
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);
}
}
}
boolean flashInverse = false;
int flashTimer = 0;
int FLASH_SPEED = 16; // UTAIIe:8-13,P7 - FLASH toggles every 16 scans
final int[] CHAR_MAP1 = new int[256];
final int[] CHAR_MAP2 = new int[256];
final int[] CHAR_MAP3 = new int[256];
int[] currentCharMap = CHAR_MAP1;
private void initCharMap() {
// 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
// MAP2: Normal map, flash inverse = true
// MAP3: Alt map, mousetext mode
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;
}
}
}
private void initVideoWriters() {
hiresPage1 = new VideoWriter() {
@Override
public int getYOffset(int y) {
@@ -287,23 +416,23 @@ public class VideoDHGR extends Video {
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) {
if ((xOffset & 0x01) == 1 || xOffset < 0) {
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 b1 = ((RAM128k) getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset );
int b2 = ((RAM128k) getMemory()).getMainMemory() .readByte(rowAddress + xOffset );
int b3 = ((RAM128k) getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1);
int b4 = ((RAM128k) 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) {
if (useColOffset >= 77 || useColOffset < 0) {
useColOffset = 76;
}
useColor[useColOffset ] = (b1 & 0x80) != 0;
@@ -320,67 +449,21 @@ public class VideoDHGR extends Video {
protected void displayHires(WritableImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
if ((xOffset & 0x01) == 1 || xOffset < 0 || xOffset > 39) {
return;
}
int b1 = 0x0ff & ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset);
int b2 = 0x0ff & ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1);
int b1 = 0x0ff & ((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset);
int b2 = 0x0ff & ((RAM128k) 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 (xOffset < 0) return;
int c1 = ((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
} else {
@@ -403,12 +486,13 @@ public class VideoDHGR extends Video {
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 (xOffset < 0) return;
int c1 = ((RAM128k) getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset) & 0x0FF;
int c2 = ((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c1 &= 15;
c2 &= 15;
@@ -435,89 +519,9 @@ public class VideoDHGR extends Video {
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;
}
}
}
writer.setColor(xx, y, color);
}
@Override
public void vblankStart() {
// ALTCHR set only affects character mapping and disables FLASH.
@@ -526,7 +530,9 @@ public class VideoDHGR extends Video {
} else {
flashTimer--;
if (flashTimer <= 0) {
markFlashDirtyBits();
if (SoftSwitches.MIXED.isOn() || SoftSwitches.TEXT.isOn()) {
markFlashDirtyBits();
}
flashTimer = FLASH_SPEED;
flashInverse = !flashInverse;
if (flashInverse) {
@@ -549,12 +555,12 @@ public class VideoDHGR extends Video {
protected void displayText(WritableImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
if ((xOffset & 0x01) == 1 || xOffset < 0) {
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));
byte byte2 = ((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1);
int c1 = getFontChar(((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset));
int c2 = getFontChar(byte2);
int b1 = Font.getByte(c1, yOffset);
int b2 = Font.getByte(c2, yOffset);
@@ -566,14 +572,14 @@ public class VideoDHGR extends Video {
protected void displayText80(WritableImage screen, int xOffset, int y, int rowAddress) {
// Skip odd columns since this does two at once
if ((xOffset & 0x01) == 1) {
if ((xOffset & 0x01) == 1 || xOffset < 0) {
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 c1 = getFontChar(((RAM128k) getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset));
int c2 = getFontChar(((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset));
int c3 = getFontChar(((RAM128k) getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1));
int c4 = getFontChar(((RAM128k) 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);
@@ -634,7 +640,7 @@ public class VideoDHGR extends Video {
Logger.getLogger(getClass().getName()).warning("Went out of bounds in video display");
}
}
static final Color BLACK = Color.BLACK;
static Color BLACK = Color.BLACK;
static Color WHITE = Color.WHITE;
static final int[][] XY_OFFSET;
@@ -683,7 +689,6 @@ public class VideoDHGR extends 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);
}
@@ -712,8 +717,8 @@ public class VideoDHGR extends Video {
}
private void registerDirtyFlagChecks() {
computer.getMemory().observe(RAMEvent.TYPE.WRITE, 0x0400, 0x0bff, this::registerTextDirtyFlag);
computer.getMemory().observe(RAMEvent.TYPE.WRITE, 0x02000, 0x05fff, this::registerHiresDirtyFlag);
getMemory().observe("Check for text changes", RAMEvent.TYPE.WRITE, 0x0400, 0x0bff, this::registerTextDirtyFlag);
getMemory().observe("Check for graphics changes", RAMEvent.TYPE.WRITE, 0x02000, 0x05fff, this::registerHiresDirtyFlag);
}
@Override

View File

@@ -1,35 +1,33 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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 jace.Emulator;
import jace.EmulatorUILogic;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.Video;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
@@ -51,90 +49,99 @@ import javafx.scene.paint.Color;
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;
public boolean useTextPalette = false;
final int[][] SOLID_PALETTE = new int[4][128];
final int[][] TEXT_PALETTE = new int[4][128];
int[][] activePalette = SOLID_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 int[] divBy28 = new int[560];
final int[] scanline = new int[20];
final public int[] divBy28 = new int[560];
static {
for (int i = 0; i < 560; i++) {
divBy28[i] = i / 28;
}
}
boolean[] colorActive = new boolean[80];
protected boolean[] colorActive = new boolean[80];
int rowStart = 0;
public VideoNTSC(Computer computer) {
super(computer);
public VideoNTSC() {
super();
initDivideTables();
initNtscPalette();
registerStateListeners();
}
public static enum VideoMode {
Color("Color"),
public enum VideoMode {
TextFriendly("Text-friendly color"),
Mode7("Mode7 Mixed RGB"),
Color("Color"),
Mode7TextFriendly("Mode7 with Text-friendly palette"),
Mode7("Mode7 Mixed RGB"),
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",
alternatives = "Gfx 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();
currentMode = (currentMode + 1) % VideoMode.values().length;
Emulator.withVideo(v->((VideoNTSC) v)._setVideoMode(VideoMode.values()[currentMode], true));
}
public static void setVideoMode(VideoMode newMode, boolean showNotification) {
Emulator.withVideo(v->((VideoNTSC) v)._setVideoMode(newMode, showNotification));
}
private void _setVideoMode(VideoMode newMode, boolean showNotification) {
Emulator.withVideo(v-> {
VideoNTSC thiss = (VideoNTSC) v;
thiss.monochomeMode = false;
WHITE = Color.WHITE;
switch (newMode) {
case Amber -> {
thiss.monochomeMode = true;
WHITE = Color.web("ff8000");
}
case Greenscreen -> {
thiss.monochomeMode = true;
WHITE = Color.web("0ccc68");
}
case Monochrome -> thiss.monochomeMode = true;
case Color -> {
thiss.useTextPalette = false;
thiss.enableVideo7 = false;
}
case Mode7 -> {
thiss.useTextPalette = false;
thiss.enableVideo7 = true;
}
case Mode7TextFriendly -> {
thiss.useTextPalette = true;
thiss.enableVideo7 = true;
}
case TextFriendly -> {
thiss.useTextPalette = true;
thiss.enableVideo7 = false;
}
}
thiss.activePalette = thiss.useTextPalette ? TEXT_PALETTE : SOLID_PALETTE;
if (showNotification) {
EmulatorUILogic.notify("Video mode: " + newMode.name);
}
forceRefresh();
});
}
@Override
protected void showBW(WritableImage screen, int x, int y, int dhgrWord) {
if (x < 0) return;
int pos = divBy28[x];
if (rowStart < 0) {
rowStart = pos;
@@ -145,6 +152,7 @@ public class VideoNTSC extends VideoDHGR {
@Override
protected void showDhgr(WritableImage screen, int x, int y, int dhgrWord) {
if (x < 0) return;
int pos = divBy28[x];
if (rowStart < 0) {
rowStart = pos;
@@ -155,7 +163,8 @@ public class VideoNTSC extends VideoDHGR {
@Override
protected void displayLores(WritableImage screen, int xOffset, int y, int rowAddress) {
int data = ((RAM128k) computer.getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if (xOffset < 0) return;
int data = ((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
int pos = xOffset >> 1;
if (rowStart < 0) {
rowStart = pos;
@@ -185,33 +194,34 @@ public class VideoNTSC extends VideoDHGR {
@Override
protected void displayDoubleLores(WritableImage screen, int xOffset, int y, int rowAddress) {
if (xOffset < 0) return;
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;
int c1 = ((RAM128k) 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;
int c2 = ((RAM128k) getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF;
if ((y & 7) < 4) {
c2 &= 15;
} else {
c2 >>= 4;
}
int pat;
if ((xOffset & 0x01) == 0) {
int pat = c1 | (c1 & 7) << 4;
pat = c1 | (c1 & 7) << 4;
pat |= c2 << 7 | (c2 & 7) << 11;
scanline[pos] = pat;
} else {
int pat = scanline[pos];
pat = scanline[pos];
pat |= (c1 & 12) << 12 | c1 << 16 | (c1 & 1) << 20;
pat |= (c2 & 12) << 19 | c2 << 23 | (c2 & 1) << 27;
scanline[pos] = pat;
}
scanline[pos] = pat;
}
@Override
@@ -223,7 +233,7 @@ public class VideoNTSC extends VideoDHGR {
// 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 int[][] pyOffset;
static {
pyOffset = new int[192][21];
@@ -235,6 +245,7 @@ public class VideoNTSC extends VideoDHGR {
}
boolean monochomeMode = false;
private void renderScanline(WritableImage screen, int y) {
int p = 0;
if (rowStart != 0) {
@@ -248,6 +259,7 @@ public class VideoNTSC extends VideoDHGR {
// Reset scanline position
int byteCounter = 0;
for (int s = rowStart; s < 20; s++) {
if (s < 0) continue;
int add = 0;
int bits;
if (hiresMode) {
@@ -301,8 +313,6 @@ public class VideoNTSC extends VideoDHGR {
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
@@ -322,7 +332,26 @@ public class VideoNTSC extends VideoDHGR {
{1.0, 0.0, 0.0}, //1111 f
};
static {
public static void CHANGE_BLACK_COLOR(int r, int g, int b) {
Emulator.withVideo(v->{
VideoNTSC vntsc = (VideoNTSC) v;
BLACK = Color.rgb(r, g, b);
int c = colorToInt(BLACK);
for (int i1 = 0; i1 < 4; i1++) {
vntsc.SOLID_PALETTE[i1][0] = c;
vntsc.TEXT_PALETTE[i1][0] = c;
}
});
Video.forceRefresh();
}
private void initDivideTables() {
for (int i = 0; i < 560; i++) {
divBy28[i] = i / 28;
}
}
private void initNtscPalette() {
int maxLevel = 10;
for (int offset = 0; offset < 4; offset++) {
for (int pattern = 0; pattern < 128; pattern++) {
@@ -346,20 +375,25 @@ public class VideoNTSC extends VideoDHGR {
}
static public int yiqToRgb(double y, double i, double q) {
return colorToInt(yiqToRgbColor(y, i, q));
}
static public Color yiqToRgbColor(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;
return Color.rgb(r, g, b);
}
static public int colorToInt(Color c) {
return (int) (255 << 24) | (int) (c.getRed() * 255) << 16 | (int) (c.getGreen() * 255) << 8 | (int) (c.getBlue() * 255);
}
public static double normalize(double x, double minX, double maxX) {
if (x < minX) {
return minX;
}
if (x > maxX) {
return maxX;
}
return x;
return Math.min(x, maxX);
}
@Override
@@ -369,12 +403,12 @@ public class VideoNTSC extends VideoDHGR {
}
// 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
// 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 {
public enum rgbMode {
COLOR(true), MIX(true), BW(false), COL_160(false);
boolean colorMode = false;
@@ -388,10 +422,6 @@ public class VideoNTSC extends VideoDHGR {
}
}
public static enum ModeStateChanges {
SET_AN3, CLEAR_AN3, SET_80, CLEAR_80;
}
boolean f1 = true;
boolean f2 = true;
boolean an3 = false;
@@ -408,15 +438,14 @@ public class VideoNTSC extends VideoDHGR {
Set<RAMListener> rgbStateListeners = new HashSet<>();
private void registerStateListeners() {
if (!rgbStateListeners.isEmpty() || computer.getVideo() != this) {
if (!rgbStateListeners.isEmpty() || Emulator.withComputer(Computer::getVideo, null) != this) {
return;
}
RAM memory = computer.getMemory();
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.ANY, 0x0c05e, (e) -> {
rgbStateListeners.add(getMemory().observe("NTSC: AN3 state change", RAMEvent.TYPE.ANY, 0x0c05e, (e) -> {
an3 = false;
rgbStateChange();
}));
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.ANY, 0x0c05f, (e) -> {
rgbStateListeners.add(getMemory().observe("NTSC: 80COL state change", RAMEvent.TYPE.ANY, 0x0c05f, (e) -> {
if (!an3) {
f2 = f1;
f1 = SoftSwitches._80COL.getState();
@@ -424,9 +453,9 @@ public class VideoNTSC extends VideoDHGR {
an3 = true;
rgbStateChange();
}));
rgbStateListeners.add(memory.observe(RAMEvent.TYPE.EXECUTE, 0x0fa62, (e) -> {
rgbStateListeners.add(getMemory().observe("NTSC: Reset hook for reverting RGB mode", RAMEvent.TYPE.EXECUTE, 0x0fa62, (e) -> {
// When reset hook is called, reset the graphics mode
// This is useful in case a program is running that
// This is useful in case a program is running that
// is totally clueless how to set the RGB state correctly.
f1 = true;
f2 = true;
@@ -440,7 +469,7 @@ public class VideoNTSC extends VideoDHGR {
public void detach() {
rgbStateListeners.stream().forEach((l) -> {
computer.getMemory().removeListener(l);
getMemory().removeListener(l);
});
rgbStateListeners.clear();
super.detach();

View File

@@ -1,23 +1,22 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e.softswitch;
import jace.Emulator;
import jace.apple2e.SoftSwitches;
import jace.core.RAMEvent;
import jace.core.RAMListener;
@@ -39,7 +38,7 @@ public class IntC8SoftSwitch extends SoftSwitch {
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) {
new RAMListener("Softswitch " + getName() + " on", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0C300);
@@ -56,7 +55,7 @@ public class IntC8SoftSwitch extends SoftSwitch {
// INTCXRom shoud deactivate whenever CFFF is accessed
addListener(
new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
new RAMListener("Softswitch " + getName() + " off", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0CFFF);
@@ -76,8 +75,6 @@ public class IntC8SoftSwitch extends SoftSwitch {
@Override
public void stateChanged() {
if (computer.getMemory() != null) {
computer.getMemory().configureActiveMemory();
}
Emulator.withMemory(m->m.configureActiveMemory());
}
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e.softswitch;
import jace.core.Keyboard;

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e.softswitch;
import jace.core.RAMEvent.TYPE;

View File

@@ -1,23 +1,22 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e.softswitch;
import jace.Emulator;
import jace.core.RAMEvent;
import jace.core.SoftSwitch;
@@ -40,15 +39,13 @@ public class MemorySoftSwitch extends SoftSwitch {
@Override
public void stateChanged() {
// System.out.println(getName()+ " was switched to "+getState());
if (computer.getMemory() != null) {
computer.getMemory().configureActiveMemory();
}
Emulator.withMemory(m->m.configureActiveMemory());
}
// Todo: Implement floating bus, maybe?
@Override
protected byte readSwitch() {
byte value = computer.getVideo().getFloatingBus();
byte value = Emulator.withComputer(c->c.getVideo().getFloatingBus(), (byte) 0);
if (getState()) {
return (byte) (value | 0x080);
} else {

View File

@@ -1,23 +1,22 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.apple2e.softswitch;
import jace.Emulator;
import jace.core.RAMEvent;
import jace.core.SoftSwitch;
@@ -40,9 +39,7 @@ public class VideoSoftSwitch extends SoftSwitch {
@Override
public void stateChanged() {
// System.out.println("Set "+getName()+" -> "+getState());
if (computer.getVideo() != null) {
computer.getVideo().configureVideoMode();
}
Emulator.withVideo(video -> video.configureVideoMode());
}
@Override

View File

@@ -1,14 +1,15 @@
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;
import jace.Emulator;
import jace.ide.CompileResult;
import jace.ide.LanguageHandler;
import jace.ide.Program;
/**
*
* @author blurry
@@ -17,7 +18,7 @@ public class ApplesoftHandler implements LanguageHandler<ApplesoftProgram> {
@Override
public String getNewDocumentContent() {
return ApplesoftProgram.fromMemory(Emulator.computer.getMemory()).toString();
return Emulator.withComputer(c->ApplesoftProgram.fromMemory(c.getMemory()).toString(), "");
}
@Override
@@ -41,7 +42,7 @@ public class ApplesoftHandler implements LanguageHandler<ApplesoftProgram> {
@Override
public Map<Integer, String> getErrors() {
return Collections.EMPTY_MAP;
return Collections.emptyMap();
}
@Override
@@ -51,12 +52,12 @@ public class ApplesoftHandler implements LanguageHandler<ApplesoftProgram> {
@Override
public List<String> getOtherMessages() {
return Collections.EMPTY_LIST;
return Collections.emptyList();
}
@Override
public List<String> getRawOutput() {
return Collections.EMPTY_LIST;
return Collections.emptyList();
}
};
}

View File

@@ -1,37 +1,30 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.applesoft;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
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
@@ -54,23 +47,7 @@ public class ApplesoftProgram {
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 final int START_ADDRESS = 0x0801;
public static Byte[] toObjects(byte[] bytesPrim) {
Byte[] bytes = new Byte[bytesPrim.length];
@@ -93,7 +70,7 @@ public class ApplesoftProgram {
}
public static ApplesoftProgram fromBinary(List<Byte> binary) {
return fromBinary(binary, 0x0801);
return fromBinary(binary, START_ADDRESS);
}
public static ApplesoftProgram fromBinary(List<Byte> binary, int startAddress) {
@@ -149,70 +126,70 @@ public class ApplesoftProgram {
}
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);
Emulator.whileSuspended(c-> {
int programStart = c.getMemory().readWordRaw(START_OF_PROG_POINTER);
int programEnd = programStart + getProgramSize();
if (isProgramRunning()) {
whenReady(()->{
relocateVariables(programEnd);
injectProgram();
});
} else {
injectProgram();
});
} else {
injectProgram();
clearVariables(programEnd);
}
Emulator.computer.resume();
clearVariables(programEnd);
}
});
}
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);
Emulator.withMemory(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);
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;
return Emulator.withComputer(c->(c.getMemory().readRaw(RUNNING_FLAG) & 0x0FF) != NOT_RUNNING, false);
}
/**
* 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);
}
Emulator.withMemory(memory->{
memory.addListener(new RAMListener("Applesoft: Trap GOTO command", 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);
}
@Override
protected void doEvent(RAMEvent e) {
r.run();
memory.removeListener(this);
}
});
});
}
@@ -222,11 +199,12 @@ public class ApplesoftProgram {
* @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);
Emulator.withMemory(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);
});
}
/**
@@ -234,24 +212,29 @@ public class ApplesoftProgram {
* @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);
Emulator.withMemory(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);
}
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;
}
public int getLength() {
return lines.size();
}
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.applesoft;
import java.util.ArrayList;
@@ -30,7 +28,7 @@ import java.util.stream.Collectors;
*/
public class Command {
public static enum TOKEN {
public enum TOKEN {
END((byte) 0x080, "END"),
FOR((byte) 0x081, "FOR"),
@@ -160,7 +158,7 @@ public class Command {
}
return null;
}
private String str;
private final String str;
public byte code;
TOKEN(byte b, String str) {

View File

@@ -1,28 +1,28 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.applesoft;
import jace.applesoft.Command.TOKEN;
import static java.lang.Character.isDigit;
import java.util.ArrayList;
import java.util.List;
import jace.applesoft.Command.TOKEN;
/**
* Representation of a line of applesoft basic, having a line number and a list
* of program commands.

View File

@@ -0,0 +1,85 @@
# Notes on compiling ACME Cross Assembler
Acme is a very handy macro assembler for the 6502 family of processors. It is also very easy to build. Because of this, we can port ACME to Java without even having to alter the source thanks to NestedVM.
A word of caution: NestedVM is very old and very unmaintained, so it goes without saying that there be dragons. Re-transpiling new versions of ACME is still possible (as of Feb 2024) but it is not easy.
## Getting set up
First use a fork of NestedVM (the original is buggy and not maintained at all, wherease newer forks are at least a little better)
- https://github.com/bgould/nestedvm/tree/master
Next, check the urls in the upstream/Makefile to ensure they are valid. Currently, I found that I needed to make the following changes:
```
diff --git a/upstream/Makefile b/upstream/Makefile
index 83eaa0a..d1f1cbb 100644
--- a/upstream/Makefile
+++ b/upstream/Makefile
@@ -211,7 +211,7 @@ configure_binutils = --target=mips-unknown-elf --disable-werror
## newlib ##############################################################################
version_newlib = 1.20.0
-url_newlib = ftp://sources.redhat.com/pub/newlib/newlib-$(version_newlib).tar.gz
+url_newlib = ftp://sourceware.org/pub/newlib/newlib-$(version_newlib).tar.gz
patches_newlib = newlib-mips.patch newlib-tzset.patch newlib-malloc.patch newlib-nomemcpy.patch newlib-unix.patch newlib-unistd.patch newlib-nestedvm-define.patch newlib-sdata.patch newlib-new.patch
configure_newlib = --enable-multilib --target=mips-unknown-elf
@@ -236,13 +236,14 @@ tasks/build_openbsdglob: tasks/download_openbsdglob tasks/build_newlib
## regex ##############################################################################
-url_regex = http://www.arglist.com/regex/files/regex3.8a.tar.gz
+#url_regex = http://www.arglist.com/regex/files/regex3.8a.tar.gz
+url_regex = https://github.com/garyhouston/regex/archive/refs/tags/alpha3.8p1.tar.gz
tasks/build_regex: tasks/download_regex tasks/build_newlib
@mkdir -p $(usr)/mips-unknown-elf/{include,lib}
mkdir -p build/regex build/regex/fake
cd build && \
- tar xvzf ../download/regex3.8a.tar.gz && cd regex && \
+ tar xvzf ../download/alpha3.8p1.tar.gz && cd regex-alpha3.8p1 && \
make CC=mips-unknown-elf-gcc CFLAGS="-I. $(MIPS_CFLAGS)" regcomp.o regexec.o regerror.o regfree.o && \
mips-unknown-elf-ar cr libregex.a regcomp.o regexec.o regerror.o regfree.o && \
mips-unknown-elf-ranlib libregex.a && \
```
From here it's a matter of running Make from the main folder and waiting a long time (approx 2 hours, it has to compile GCC)
Next: Use these commands to build the rest of the things you might need:
```
make env.sh
```
This will create a convenient shell script that demonstrates all the GCC and linker flags you'll need for Acme, so it's good for future reference.
```
make test
```
Quick sanity check. If you look closely it reveals how to use the nestedvm compiler... sort of.
## Building ACME
With the MIPS GCC binary available, now grab the source for ACME you want to use and extract to another folder. Modify the Makefile like so:
```
CC=mips-unknown-elf-gcc
CXX=mips-unknown-elf-g++
AS=mips-unknown-elf-as
AR=mips-unknown-elf-ar
LD=mips-unknown-elf-ld
RANLIB=mips-unknown-elf-ranlib
CFLAGS= -O2 -mmemcpy -ffunction-sections -fdata-sections -falign-functions=512 -fno-rename-registers -fno-schedule-insns -fno-delayed-branch -Wstrict-prototypes -march=mips1 -specs=/Users/brobert/Documents/code/nestedvm/upstream/install/mips-unknown-elf/lib/crt0-override.spec -static -mmemcpy --static -Wl,--gc-sect>
```
Note that I used -O2 not -O3. It probably doesn't make much of a functional difference but -O3 produced something that was 20% larger.
Also remove the `strip acme` line because nestedvm needs the symbol table.
## Converting to Java
After you build acme, next you need to transpile it to java. I used the following command to do that:
```
java -cp ../nestedvm/build:../nestedvm/upstream/build/classgen/build org.ibex.nestedvm.Compiler -outformat java -outfile AcmeCrossAssembler.java -o unixRuntime jace.assembly.AcmeCrossAssembler acme
```
This produces a file called AcmeCrossAssembler.java which can replace the current one. You should run tests via `mvn test` in Jace to make sure that ACME is working properly. Since the CPU unit tests use it heavily, that's a pretty good test for Acme as well. :)

View File

@@ -7,6 +7,8 @@ import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
@@ -20,10 +22,10 @@ import java.util.stream.Collectors;
*
* @author blurry
*/
public class AcmeCompiler implements CompileResult<File> {
public class AcmeCompiler implements CompileResult<ByteBuffer> {
boolean successful = false;
File compiledAsset = null;
ByteBuffer compiledAsset = null;
Map<Integer, String> errors = new LinkedHashMap<>();
Map<Integer, String> warnings = new LinkedHashMap<>();
List<String> otherWarnings = new ArrayList<>();
@@ -35,7 +37,7 @@ public class AcmeCompiler implements CompileResult<File> {
}
@Override
public File getCompiledAsset() {
public ByteBuffer getCompiledAsset() {
return compiledAsset;
}
@@ -100,21 +102,26 @@ public class AcmeCompiler implements CompileResult<File> {
private void invokeAcme(File sourceFile, File workingDirectory) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IOException {
String oldPath = System.getProperty("user.dir");
File tempFile = null;
redirectSystemOutput();
try {
compiledAsset = File.createTempFile(sourceFile.getName(), "bin", sourceFile.getParentFile());
tempFile = File.createTempFile(sourceFile.getName(), "bin", sourceFile.getParentFile());
tempFile.deleteOnExit();
System.setProperty("user.dir", workingDirectory.getAbsolutePath());
AcmeCrossAssembler acme = new AcmeCrossAssembler();
String[] params = {"--outfile", normalizeWindowsPath(compiledAsset.getAbsolutePath()), "-f", "cbm", "--maxerrors","16",normalizeWindowsPath(sourceFile.getAbsolutePath())};
String[] params = {"--outfile", normalizeWindowsPath(tempFile.getAbsolutePath()), "-f", "cbm", "--maxerrors","16",normalizeWindowsPath(sourceFile.getAbsolutePath())};
int status = acme.run("Acme", params);
successful = status == 0;
if (!successful) {
compiledAsset.delete();
compiledAsset = null;
if (successful) {
compiledAsset = ByteBuffer.wrap(Files.readAllBytes(tempFile.toPath()));
}
tempFile.delete();
} finally {
restoreSystemOutput();
System.setProperty("user.dir", oldPath);
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
}
rawOutput.add("Error output:");
extractOutput(baosErr.toString());

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +1,89 @@
package jace.assembly;
import java.nio.ByteBuffer;
import jace.Emulator;
import jace.core.Computer;
import jace.core.RAM;
import jace.ide.CompileResult;
import jace.ide.HeadlessProgram;
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> {
public class AssemblyHandler implements LanguageHandler<ByteBuffer> {
@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) {
public CompileResult<ByteBuffer> compile(Program proxy) {
AcmeCompiler compiler = new AcmeCompiler();
compiler.compile(proxy);
return compiler;
}
@Override
public void execute(CompileResult<File> lastResult) {
public void compileToRam(String code) {
HeadlessProgram prg = new HeadlessProgram(Program.DocumentType.assembly);
prg.setValue(code);
CompileResult<ByteBuffer> lastResult = compile(prg);
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);
}
Emulator.withComputer(c -> {
RAM memory = c.getMemory();
ByteBuffer input = lastResult.getCompiledAsset();
input.rewind();
int startLSB = input.get();
int startMSB = input.get();
int start = startLSB + startMSB << 8;
System.out.printf("Storing assembled code to $%s%n", Integer.toHexString(start));
c.getCpu().whileSuspended(() -> {
int pos = start;
while (input.hasRemaining()) {
memory.write(pos++, input.get(), false, true);
}
});
});
}
}
@Override
public void execute(CompileResult<ByteBuffer> lastResult) throws Exception {
if (lastResult.isSuccessful()) {
Computer c = Emulator.withComputer(c1 -> c1, null);
RAM memory = c.getMemory();
ByteBuffer input = lastResult.getCompiledAsset();
input.rewind();
int startLSB = input.get() & 0x0ff;
int startMSB = input.get() & 0x0ff;
int start = startLSB + startMSB << 8;
// System.out.printf("Executing code at $%s%n", Integer.toHexString(start));
c.getCpu().whileSuspended(() -> {
// System.out.printf("Storing assembled code to $%s%n", Integer.toHexString(start));
int pos = start;
while (input.hasRemaining()) {
memory.write(pos++, input.get(), false, true);
}
// System.out.printf("Issuing JSR to $%s%n", Integer.toHexString(start));
c.getCpu().JSR(start);
});
// });
} else {
System.err.println("Compilation failed");
lastResult.getErrors().forEach((line, message) -> System.err.printf("Line %d: %s%n", line, message));
lastResult.getOtherMessages().forEach(System.err::println);
throw new Exception("Compilation failed");
}
clean(lastResult);
}
@Override
public void clean(CompileResult<File> lastResult) {
if (lastResult.getCompiledAsset() != null) {
lastResult.getCompiledAsset().delete();
}
public void clean(CompileResult<ByteBuffer> lastResult) {
// Nothing to do here
}
}

View File

@@ -1,31 +1,31 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.cheat;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import jace.apple2e.MOS65C02;
import jace.config.DeviceEnum;
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
@@ -34,14 +34,46 @@ import java.util.Set;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class Cheats extends Device {
public static enum Cheat implements DeviceEnum<Cheats> {
Metacheat("Metacheat", MetaCheat.class, MetaCheat::new),
MontezumasRevenge("Montezuma's Revenge", MontezumasRevengeCheats.class, MontezumasRevengeCheats::new),
PrinceOfPersia("Prince of Persia", PrinceOfPersiaCheats.class, PrinceOfPersiaCheats::new),
ProgramIdentity("Identify program", ProgramIdentity.class, ProgramIdentity::new),
Wolfenstein("Wolfenstein", WolfensteinCheats.class, WolfensteinCheats::new);
Supplier<Cheats> factory;
String name;
Class<? extends Cheats> clazz;
Cheat(String name, Class<? extends Cheats> clazz, Supplier<Cheats> factory) {
this.name = name;
this.clazz = clazz;
this.factory = factory;
}
@Override
public String getName() {
return name;
}
@Override
public Cheats create() {
return factory.get();
}
@Override
public boolean isInstance(Cheats cheat) {
if (cheat == null) {
return false;
}
return clazz == cheat.getClass();
}
}
boolean cheatsActive = true;
Set<RAMListener> listeners = new HashSet<>();
public Cheats(Computer computer) {
super(computer);
}
@InvokableAction(name = "Toggle Cheats", alternatives = "cheat", defaultKeyMapping = "ctrl+shift+m")
@InvokableAction(name = "Toggle Cheats", alternatives = "cheat;Plug-in", defaultKeyMapping = "ctrl+shift+m")
public void toggleCheats() {
cheatsActive = !cheatsActive;
if (cheatsActive) {
@@ -51,36 +83,36 @@ public abstract class Cheats extends Device {
}
}
public RAMListener bypassCode(int address, int addressEnd) {
public RAMListener bypassCode(String name, int address, int addressEnd) {
int noOperation = MOS65C02.COMMAND.NOP.ordinal();
return addCheat(RAMEvent.TYPE.READ, (e) -> e.setNewValue(noOperation), address, addressEnd);
return addCheat(name, 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(String name, int value, int... address) {
return addCheat(name, 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 forceValue(String name, int value, Boolean auxFlag, int... address) {
return addCheat(name, RAMEvent.TYPE.ANY, auxFlag, (e) -> e.setNewValue(value), address);
}
public RAMListener addCheat(RAMEvent.TYPE type, RAMEvent.RAMEventHandler handler, int... address) {
public RAMListener addCheat(String name, RAMEvent.TYPE type, RAMEvent.RAMEventHandler handler, int... address) {
RAMListener listener;
if (address.length == 1) {
listener = computer.getMemory().observe(type, address[0], handler);
listener = getMemory().observe(getName() + ": " + name, type, address[0], handler);
} else {
listener = computer.getMemory().observe(type, address[0], address[1], handler);
listener = getMemory().observe(getName() + ": " + name, type, address[0], address[1], handler);
}
listeners.add(listener);
return listener;
}
public RAMListener addCheat(RAMEvent.TYPE type, boolean auxFlag, RAMEvent.RAMEventHandler handler, int... address) {
public RAMListener addCheat(String name, 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);
listener = getMemory().observe(getName() + ": " + name, type, address[0], auxFlag, handler);
} else {
listener = computer.getMemory().observe(type, address[0], address[1], auxFlag, handler);
listener = getMemory().observe(getName() + ": " + name, type, address[0], address[1], auxFlag, handler);
}
listeners.add(listener);
return listener;
@@ -97,17 +129,17 @@ public abstract class Cheats extends Device {
super.detach();
}
abstract void registerListeners();
public abstract void registerListeners();
protected void unregisterListeners() {
listeners.stream().forEach((l) -> {
computer.getMemory().removeListener(l);
getMemory().removeListener(l);
});
listeners.clear();
}
public void removeListener(RAMListener l) {
computer.getMemory().removeListener(l);
getMemory().removeListener(l);
listeners.remove(l);
}

View File

@@ -9,7 +9,6 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.util.Callback;
import javax.script.ScriptException;
/**
*
@@ -21,22 +20,23 @@ public class DynamicCheat extends RAMListener {
StringProperty expression;
BooleanProperty active;
StringProperty name;
String cheatName;
Callback<RAMEvent, Integer> expressionCallback;
public DynamicCheat(int address, String expr) {
super(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY);
public DynamicCheat(String cheatName, int address, int holdValue) {
super(cheatName, RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY);
id = (int) (Math.random() * 10000000);
addr = new SimpleIntegerProperty(address);
expression = new SimpleStringProperty(expr);
expression = new SimpleStringProperty(String.valueOf(holdValue));
isHold = true;
active = new SimpleBooleanProperty(false);
name = new SimpleStringProperty("Untitled");
expression.addListener((param, oldValue, newValue) -> {
expressionCallback = parseExpression(newValue);
});
expressionCallback = parseExpression(expr);
expressionCallback = (RAMEvent e) -> holdValue;
doConfig();
}
boolean isHold = false;
@Override
protected void doConfig() {
if (addr != null) {
@@ -73,29 +73,6 @@ public class DynamicCheat extends RAMListener {
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");
}
@@ -106,18 +83,19 @@ public class DynamicCheat extends RAMListener {
public static final String DELIMITER = ";";
public String serialize() {
return escape(name.get()) + DELIMITER
return escape(cheatName) + DELIMITER + 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]);
String cheatName = unescape(parts[0]);
String name = unescape(parts[1]);
Integer addr = Integer.parseInt(parts[2].substring(1), 16);
String expr = unescape(parts[3]);
DynamicCheat out = new DynamicCheat(addr, expr);
DynamicCheat out = new DynamicCheat(cheatName, addr, Integer.parseInt(expr));
out.name.set(name);
return out;
}

View File

@@ -6,6 +6,7 @@
package jace.cheat;
import java.util.ArrayList;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
@@ -77,6 +78,15 @@ public class MemoryCell implements Comparable<MemoryCell> {
return address - o.address;
}
@Override
public boolean equals(Object o) {
if (o instanceof MemoryCell) {
MemoryCell om = (MemoryCell) o;
return address == om.address || (x == om.x && y == om.y);
}
return false;
}
public boolean hasCounts() {
return hasCount.get();
}

View File

@@ -1,445 +1,423 @@
package jace.cheat;
import jace.Emulator;
import jace.JaceApplication;
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
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 = JaceApplication.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);
}
}
}
}
package jace.cheat;
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 jace.Emulator;
import jace.JaceApplication;
import jace.core.CPU;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.State;
import jace.ui.MetacheatUI;
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;
public class MetaCheat extends Cheats {
public enum SearchType {
VALUE, TEXT, CHANGE
}
public 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 = 0x0BFFF;
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() {
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);
getMemory().addListener(cheat);
cheat.addressProperty().addListener((prop, oldVal, newVal) -> {
getMemory().removeListener(cheat);
cheat.doConfig();
getMemory().addListener(cheat);
});
}
public void removeCheat(DynamicCheat cheat) {
cheat.active.set(false);
getMemory().removeListener(cheat);
cheatList.remove(cheat);
}
@Override
protected void unregisterListeners() {
super.unregisterListeners();
cheatList.forEach(getMemory()::removeListener);
}
@Override
protected String getDeviceName() {
return "MetaCheat";
}
@Override
public void detach() {
super.detach();
JaceApplication.getApplication().closeMetacheat();
}
@Override
public void attach() {
ui = JaceApplication.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() {
Emulator.withMemory(memory -> {
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() {
Emulator.withMemory(memory -> {
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;
}
}
}
case TEXT -> {
}
}
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() {
Emulator.withMemory(memory -> {
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("Metacheat memory viewer", RAMEvent.TYPE.ANY, startAddress, endAddress, this::processMemoryEvent);
listeners.add(memoryViewListener);
}
});
}
int fadeCounter = 0;
int FADE_TIMER_VALUE = Emulator.withComputer(c-> (int) (c.getMotherboard().getSpeedInHz() / 60), 100);
@Override
public void tick() {
Emulator.withComputer(c-> c.getCpu().performSingleTrace());
if (fadeCounter-- <= 0) {
fadeCounter = FADE_TIMER_VALUE;
memoryCells.values().stream()
.filter(MemoryCell::hasCounts)
.forEach((cell) -> {
cell.execCount.set(Math.max(0, cell.execCount.get() - fadeRate));
cell.readCount.set(Math.max(0, cell.readCount.get() - fadeRate));
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) {
Emulator.withComputer(c -> {
CPU cpu = c.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) {
try (FileWriter writer = new FileWriter(saveFile)) {
for (DynamicCheat cheat : cheatList) {
writer.write(cheat.serialize());
writer.write("\n");
}
} catch (IOException ex) {
Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void loadCheats(File saveFile) {
try (BufferedReader in = new BufferedReader(new FileReader(saveFile))) {
String line;
while ((line = in.readLine()) != null) {
DynamicCheat cheat = DynamicCheat.deserialize(line);
addCheat(cheat);
}
} catch (IOException ex) {
Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex);
}
}
}

View File

@@ -3,8 +3,6 @@ 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;
@@ -54,10 +52,6 @@ public class MontezumasRevengeCheats extends Cheats {
public static int lastX = 0;
public MontezumasRevengeCheats(Computer computer) {
super(computer);
}
double mouseX;
double mouseY;
EventHandler<javafx.scene.input.MouseEvent> listener = (event) -> {
@@ -70,55 +64,56 @@ public class MontezumasRevengeCheats extends Cheats {
};
@Override
void registerListeners() {
RAM memory = Emulator.computer.memory;
public void registerListeners() {
if (repulsiveHack) {
addCheat(RAMEvent.TYPE.WRITE, this::repulsiveBehavior, 0x1508, 0x1518);
addCheat("Repulsive", RAMEvent.TYPE.WRITE, this::repulsiveBehavior, 0x1508, 0x1518);
}
if (featherFall) {
addCheat(RAMEvent.TYPE.WRITE, this::featherFallBehavior, PLAYER_Y);
addCheat("Feather fall", RAMEvent.TYPE.WRITE, this::featherFallBehavior, PLAYER_Y);
// Bypass the part that realizes you should die when you hit the floor
bypassCode(0x6bb3, 0x6bb4);
bypassCode("Feather fall code hack", 0x6bb3, 0x6bb4);
}
if (moonJump) {
addCheat(RAMEvent.TYPE.WRITE, this::moonJumpBehavior, Y_VELOCITY);
addCheat("Moon jump", RAMEvent.TYPE.WRITE, this::moonJumpBehavior, Y_VELOCITY);
}
if (infiniteLives) {
forceValue(11, LIVES);
forceValue("Infinite lives", 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);
Emulator.withMemory(memory -> {
//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("Hack floor timer", 32, FLOOR_TIMER);
forceValue("Hack hazard timer", 32, HAZARD_TIMER);
forceValue("Hack hazard flag", 1, HAZARD_FLAG);
});
}
if (scoreHack) {
// Score: 900913
forceValue(0x90, SCORE);
forceValue(0x09, SCORE + 1);
forceValue(0x13, SCORE + 2);
forceValue("Hack score 1", 0x90, SCORE);
forceValue("Hack score 2", 0x09, SCORE + 1);
forceValue("Hack score 3", 0x13, SCORE + 2);
}
if (snakeCharmer) {
// Skip the code that determines you're touching an enemy
bypassCode(0x07963, 0x07964);
bypassCode("Snake charmer", 0x07963, 0x07964);
}
if (mouseHack) {
EmulatorUILogic.addMouseListener(listener);
@@ -132,11 +127,11 @@ public class MontezumasRevengeCheats extends Cheats {
}
private void repulsiveBehavior(RAMEvent e) {
int playerX = computer.getMemory().readRaw(PLAYER_X);
int playerY = computer.getMemory().readRaw(PLAYER_Y);
int playerX = getMemory().readRaw(PLAYER_X);
int playerY = 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);
int monsterX = getMemory().readRaw(PLAYER_X + num);
int monsterY = getMemory().readRaw(PLAYER_Y + num);
if (monsterX != 0 && monsterY != 0) {
if (Math.abs(monsterY - playerY) < 19) {
if (Math.abs(monsterX - playerX) < 7) {
@@ -149,7 +144,7 @@ public class MontezumasRevengeCheats extends Cheats {
monsterX = 80;
}
}
computer.getMemory().write(PLAYER_X + num, (byte) monsterX, false, false);
getMemory().write(PLAYER_X + num, (byte) monsterX, false, false);
}
}
}
@@ -159,9 +154,9 @@ public class MontezumasRevengeCheats extends Cheats {
private void featherFallBehavior(RAMEvent yCoordChangeEvent) {
if (yCoordChangeEvent.getNewValue() != yCoordChangeEvent.getOldValue()) {
int yVel = computer.getMemory().readRaw(Y_VELOCITY);
int yVel = getMemory().readRaw(Y_VELOCITY);
if (yVel > MAX_VEL) {
computer.getMemory().write(Y_VELOCITY, (byte) MAX_VEL, false, false);
getMemory().write(Y_VELOCITY, (byte) MAX_VEL, false, false);
}
}
}
@@ -177,7 +172,7 @@ public class MontezumasRevengeCheats extends Cheats {
}
private boolean inStartingSequence() {
int roomLevel = computer.getMemory().readRaw(ROOM_LEVEL);
int roomLevel = getMemory().readRaw(ROOM_LEVEL);
return roomLevel == -1;
}
@@ -198,7 +193,7 @@ public class MontezumasRevengeCheats extends Cheats {
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);
getMemory().write(PLAYER_X, newX, false, false);
getMemory().write(PLAYER_Y, newY, false, false);
}
}

View File

@@ -1,28 +1,24 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
@@ -145,17 +141,13 @@ public class PrinceOfPersiaCheats extends Cheats {
// 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()) {
if (event.isPrimaryButtonDown() || event.isSecondaryButtonDown()) {
mouseClicked(event.getButton());
}
};
@@ -173,19 +165,19 @@ public class PrinceOfPersiaCheats extends Cheats {
@Override
public void registerListeners() {
if (velocityHack) {
addCheat(RAMEvent.TYPE.READ_DATA, true, this::velocityHackBehavior, CharYVel);
addCheat("Hack velocity", RAMEvent.TYPE.READ_DATA, true, this::velocityHackBehavior, CharYVel);
}
if (invincibilityHack) {
forceValue(3, true, KidStrength);
forceValue("Hack invincibility", 3, true, KidStrength);
}
if (sleepHack) {
forceValue(0, true, EnemyAlert);
forceValue("Go to sleep!", 0, true, EnemyAlert);
}
if (swordHack) {
forceValue(1, true, hasSword);
forceValue("Can haz sword", 1, true, hasSword);
}
if (timeHack) {
forceValue(0x69, true, MinLeft);
forceValue("Hack time", 0x69, true, MinLeft);
}
if (mouseHack) {
EmulatorUILogic.addMouseListener(listener);
@@ -234,7 +226,7 @@ public class PrinceOfPersiaCheats extends Cheats {
// 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();
RAM128k mem = (RAM128k) getMemory();
PagedMemory auxMem = mem.getAuxMemory();
if (button == MouseButton.PRIMARY) {
@@ -262,7 +254,7 @@ public class PrinceOfPersiaCheats extends Cheats {
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);
// System.out.println("Warping to " + warpX + "," + warpY);
auxMem.writeByte(KidX, warpX);
auxMem.writeByte(KidY, warpY);
auxMem.writeByte(KidBlockX, (byte) col);
@@ -280,7 +272,7 @@ public class PrinceOfPersiaCheats extends Cheats {
* @param direction
*/
public void performAction(int row, int col, int direction) {
RAM128k mem = (RAM128k) computer.getMemory();
RAM128k mem = (RAM128k) getMemory();
PagedMemory auxMem = mem.getAuxMemory();
byte currentScrn = auxMem.readByte(KidScrn);
if (col < 0) {
@@ -291,7 +283,7 @@ public class PrinceOfPersiaCheats extends Cheats {
}
currentScrn = (byte) scrnLeft;
byte prev = auxMem.readByte(PREV + row);
byte sprev = auxMem.readByte(SPREV + 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) {

View File

@@ -0,0 +1,119 @@
package jace.cheat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.CRC32;
import jace.apple2e.RAM128k;
import jace.apple2e.SoftSwitches;
import jace.core.PagedMemory;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
public class ProgramIdentity extends Cheats {
private Map<String, String> programIdentities;
@Override
public void registerListeners() {
addCheat("Track execution", TYPE.ANY, this::trackActivity, 0, 0x0ffff);
}
@Override
protected String getDeviceName() {
return "Program Identity";
}
int INTERVAL = 1000000;
int THRESHOLD_VALUE = 10000;
int CLIP_VALUE = THRESHOLD_VALUE * 2;
int DECAY = THRESHOLD_VALUE / 2;
int[] programRegions = new int[512];
private void trackActivity(RAMEvent e) {
int bank = e.getAddress() >> 8;
if (bank >= 0xc0 && bank < 0xd0) {
// Skip I/O region
return;
}
// Detect language card ram execution
if (bank >= 0xd0 && SoftSwitches.LCRAM.isOff()) {
// Skip rom execution
return;
}
if (!e.isMainMemory()) {
bank += 256;
}
if (e.getType() == RAMEvent.TYPE.EXECUTE) {
programRegions[bank] = Math.min(CLIP_VALUE, programRegions[bank] + 1);
} else if (e.getType() == RAMEvent.TYPE.WRITE) {
programRegions[bank] = 0;
}
}
private String generateChecksum() {
CRC32 crc = new CRC32();
RAM128k ram = (RAM128k) getMemory();
int bankCount = 0;
for (int i=0; i < 512; i++) {
if (programRegions[i] > THRESHOLD_VALUE) {
PagedMemory mem = ram.getMainMemory();
if (i >= 0x0d0 && i < 0x0100) {
mem = ram.getLanguageCard();
} else if (i >= 0x0100 && i < 0x01d0) {
mem = ram.getAuxMemory();
} else if (i >= 0x01d0) {
mem = ram.getAuxLanguageCard();
}
bankCount++;
crc.update(mem.getMemoryPage((i & 0x0ff) << 8));
}
}
return Long.toHexString(crc.getValue())+"-"+bankCount;
}
@Override
public void resume() {
super.resume();
Arrays.fill(programRegions, 0);
readProgramIdentities();
}
int counter = 0;
String lastChecksum = "";
@Override
public void tick() {
if (counter++ >= INTERVAL) {
String checksum = generateChecksum();
if (!checksum.equals(lastChecksum)) {
String identity = programIdentities.getOrDefault(checksum, "UNKNOWN");
System.out.println(checksum + "," + identity);
lastChecksum = checksum;
}
counter = 0;
for (int i=0; i < 512; i++) {
programRegions[i] = Math.max(0, programRegions[i] - DECAY);
}
}
}
private void readProgramIdentities() {
// Read from resources file
InputStream in = Cheats.class.getResourceAsStream("/jace/cheats/program-identities.txt");
try {
programIdentities = new HashMap<>();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
programIdentities.put(parts[0], parts[1]);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,352 @@
/*
* Copyright 2018 org.badvision.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jace.cheat;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.config.ConfigurableField;
import jace.core.RAMEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
/**
* Cheats for the Wolfenstein series games
*/
public class WolfensteinCheats extends Cheats {
// Specific to Wolfenstein
static final int KEYS = 0x04359;
static final int GRENADES = 0x04348;
// Object types
static final int CHEST = 48;
static final int SS = 32;
// Specific to Beyond Wolfenstein
static final int MARKS = 0x0434b;
static final int PASSES = 0x04360;
static final int CLOSET_CONTENTS_CMP = 0x05FB9; // Only locks by type, so mess up the check
// Object types
static final int CLOSET = 32;
static final int ALARM = 48;
static final int SEATED_GUARD = 80;
static final int BW_DOOR = 96;
// Same in both Wolfenstein and Beyond Wolfenstein
static final int PLAYER_LOCATION = 0x04343;
static final int BULLETS = 0x04347;
// Object types
static final int CORPSE = 64;
static final int GUARD = 16;
static final int DOOR = 80;
static final int NOTHING = 0;
private EventHandler<MouseEvent> mouseListener = this::processMouseEvent;
@ConfigurableField(category = "Hack", name = "Beyond Wolfenstein", defaultValue = "false", description = "Make sure cheats work with Beyond Wolfenstein")
public static boolean _isBeyondWolfenstein = false;
@ConfigurableField(category = "Hack", name = "Mouse (1+2)", defaultValue = "false", description = "Left click kills/opens, Right click teleports")
public static boolean mouseMode = true;
@ConfigurableField(category = "Hack", name = "Ammo (1+2)", defaultValue = "false", description = "All the bullets and grenades you'll need")
public static boolean ammo = true;
@ConfigurableField(category = "Hack", name = "Rich (2)", defaultValue = "false", description = "All the money")
public static boolean rich = true;
@ConfigurableField(category = "Hack", name = "Uniform (1)", defaultValue = "false", description = "PUT SOME CLOTHES ON!")
public static boolean uniform = true;
@ConfigurableField(category = "Hack", name = "Vest (1)", defaultValue = "false", description = "Bulletproof vest")
public static boolean vest = true;
@ConfigurableField(category = "Hack", name = "Skeleton Key (1+2)", defaultValue = "false", description = "Open all things")
public static boolean skeletonKey = true;
@ConfigurableField(category = "Hack", name = "Fast Open (1)", defaultValue = "false", description = "Open all things quickly")
public static boolean fastOpen = true;
@ConfigurableField(category = "Hack", name = "All dead (1+2)", defaultValue = "false", description = "Everything is dead")
public static boolean allDead = true;
@ConfigurableField(category = "Hack", name = "Sleepy Time (1+2)", defaultValue = "false", description = "Nobody move, nobody get hurt")
public static boolean sleepyTime = false;
@ConfigurableField(category = "Hack", name = "Legendary (1)", defaultValue = "false", description = "All of them are SS guards!")
public static boolean legendary = false;
@ConfigurableField(category = "Hack", name = "Day at the office (2)", defaultValue = "false", description = "All of them are at desks")
public static boolean dayAtTheOffice = false;
@Override
public void registerListeners() {
if (_isBeyondWolfenstein) {
// Only work in Beyond Wolfenstein
if (rich) {
forceValue("Wolfenstein Money cheat", MARKS, 255);
}
if (dayAtTheOffice) {
for (int i = 0x04080; i < 0x04100; i += 0x010) {
addCheat("Wolfenstein day at the office cheat " + i, RAMEvent.TYPE.READ, this::allDesks, i);
}
}
} else {
// Only work in the first Wolfenstein game
if (uniform) {
forceValue("Wolfenstein Uniform cheat", 255, 0x04349);
}
if (vest) {
forceValue("Wolfenstein Vest cheat", 255, 0x0434A);
}
if (fastOpen) {
addCheat("Wolfenstein FastOpen cheat (1)", RAMEvent.TYPE.WRITE, this::fastOpenHandler, 0x04351);
addCheat("Wolfenstein FastOpen cheat (2)", RAMEvent.TYPE.WRITE, this::fastOpenHandler, 0x0587B);
}
}
if (ammo) {
forceValue("Wolfenstein ammo cheat", 10, BULLETS);
if (!_isBeyondWolfenstein) {
forceValue("Wolfenstein grenades cheat", 9, GRENADES);
}
}
if (skeletonKey) {
if (_isBeyondWolfenstein) {
forceValue("Wolfenstein passes cheat", 255, PASSES);
forceValue("Wolfenstein unlock closets cheat", 64, CLOSET_CONTENTS_CMP); // Fake it out so it thinks all doors are unlocked
} else {
forceValue("Wolfenstein keys cheat", 255, KEYS);
}
}
if (allDead) {
for (int i = 0x04080; i < 0x04100; i += 0x010) {
addCheat("Wolfenstein all dead cheat " + i, RAMEvent.TYPE.READ, this::allDead, i);
}
}
if (sleepyTime) {
for (int i = 0x04080; i < 0x04100; i += 0x010) {
forceValue("Wolfenstein sleep cheat (1)", 0, i + 2);
forceValue("Wolfenstein sleep cheat (2)", 0, i + 3);
// This makes them shout ACHTUNG over and over again... so don't do that.
// forceValue(144, i+12);
forceValue("Wolfenstein sleep cheat (3)", 0, i + 12);
}
}
if (legendary) {
for (int i = 0x04080; i < 0x04100; i += 0x010) {
addCheat("Wolfenstein legendary cheat", RAMEvent.TYPE.READ, this::legendaryMode, i);
}
}
if (mouseMode) {
EmulatorUILogic.addMouseListener(mouseListener);
} else {
EmulatorUILogic.removeMouseListener(mouseListener);
}
}
private void fastOpenHandler(RAMEvent evt) {
int newVal = evt.getNewValue() & 0x0ff;
if (newVal > 1) {
evt.setNewValue(1);
}
}
private boolean isFinalRoom() {
for (int i = 0x04080; i < 0x04100; i += 0x010) {
int objectType = getMemory().readRaw(i) & 0x0ff;
if (objectType == BW_DOOR) {
return true;
}
}
return false;
}
private void allDesks(RAMEvent evt) {
int location = getMemory().readRaw(evt.getAddress() + 1);
if (!isFinalRoom() || location < 32) {
int type = evt.getNewValue();
if (type == GUARD) {
evt.setNewValue(SEATED_GUARD);
// Reset the status flag to 0 to prevent the boss desk from rendering, but don't revive dead guards!
if (getMemory().readRaw(evt.getAddress() + 4) != 4) {
getMemory().write(evt.getAddress() + 4, (byte) 0, false, false);
}
}
}
}
private void allDead(RAMEvent evt) {
int type = evt.getNewValue();
if (_isBeyondWolfenstein) {
int location = getMemory().readRaw(evt.getAddress() + 1);
if (!isFinalRoom() || location < 32) {
if (type == GUARD) {
evt.setNewValue(CORPSE);
} else if (type == SEATED_GUARD) {
getMemory().write(evt.getAddress() + 4, (byte) 4, false, false);
}
}
} else {
if (type == GUARD || type == SS) {
evt.setNewValue(CORPSE);
}
}
}
private int debugTicks = 0;
private void legendaryMode(RAMEvent evt) {
int type = evt.getNewValue();
if (type == 16) {
evt.setNewValue(32);
}
}
private void processMouseEvent(MouseEvent evt) {
if (evt.isPrimaryButtonDown() || evt.isSecondaryButtonDown()) {
Node source = (Node) evt.getSource();
double mouseX = evt.getSceneX() / source.getBoundsInLocal().getWidth();
double mouseY = evt.getSceneY() / source.getBoundsInLocal().getHeight();
int x = Math.max(0, Math.min(7, (int) ((mouseX - 0.148) * 11)));
int y = Math.max(0, Math.min(7, (int) ((mouseY - 0.101) * 11)));
int location = x + (y << 3);
if (evt.getButton() == MouseButton.PRIMARY) {
killEnemyAt(location);
} else {
teleportTo(location);
}
}
}
private void killEnemyAt(int location) {
System.out.println("Looking for bad guy at " + location);
for (int i = 0x04080; i < 0x04100; i += 0x010) {
int enemyLocation = getMemory().readRaw(i + 1) & 0x0ff;
System.out.print("Location " + enemyLocation);
String type = "";
boolean isAlive = false;
boolean isSeatedGuard = false;
if (_isBeyondWolfenstein) {
switch (getMemory().readRaw(i) & 0x0ff) {
case GUARD:
type = "guard";
isAlive = true;
break;
case SEATED_GUARD:
type = "seated guard";
isAlive = true;
isSeatedGuard = true;
break;
case CLOSET:
type = "closet";
break;
case CORPSE:
type = "corpse";
break;
case NOTHING:
type = "nothing";
break;
default:
type = "unknown type " + (getMemory().readRaw(i) & 0x0ff);
}
} else {
switch (getMemory().readRaw(i) & 0x0ff) {
case GUARD:
type = "guard";
isAlive = true;
break;
case SS:
type = "SS";
isAlive = true;
break;
case CHEST:
type = "chest";
break;
case CORPSE:
type = "corpse";
break;
case DOOR:
type = "door";
break;
case NOTHING:
type = "nothing";
break;
default:
type = "unknown type " + (getMemory().readRaw(i) & 0x0ff);
}
}
System.out.println(" is a " + type);
for (int j = 0x00; j < 0x0f; j++) {
int val = getMemory().readRaw(i + j) & 0x0ff;
System.out.print(Integer.toHexString(val) + " ");
}
System.out.println();
if (isAlive && location == enemyLocation) {
if (isSeatedGuard) {
getMemory().write(i + 4, (byte) 4, false, false);
} else {
getMemory().write(i, (byte) CORPSE, false, true);
}
System.out.println("*BLAM*");
}
}
}
private void teleportTo(int location) {
getMemory().write(0x04343, (byte) location, false, true);
}
@Override
public void unregisterListeners() {
super.unregisterListeners();
EmulatorUILogic.removeMouseListener(mouseListener);
}
public static int BlueType = 0x0b700;
@Override
protected String getDeviceName() {
return "Wolfenstein Cheats";
}
@Override
public void tick() {
if (debugTicks > 0) {
debugTicks--;
if (debugTicks == 0) {
Emulator.withComputer(c->c.getCpu().setTraceEnabled(false));
}
}
}
/**
* 4147-4247: Room map?
*
* 4080-40ff : Enemies and chests 4090-409f : Enemy 2 40a0-40af : Enemy 1 0: State/Type (0-15 = Nothing?, 16 =
* soldier, 32 = SS, 48 = Chest, 64 = dead) 1: Location 2: Direction (0 = still) 3: Aim (0 = no gun) C: Caution?
* (144 = stickup)
*
* 4341 : Player walking direction (0 = still, 1=D, 2=U, 4=L, 8=R) 4342 : Player gun direction 4343 : Real Player
* location (4 high bits = vertical, 4 low bits = horizontal) .. use this for teleport 4344 : Player Drawing X
* location 4345 : Player Drawing Y location 4347 : Bullets 4348 : Grenades 4349 : Uniform (0 = none, 1+ = yes) 434A
* : Vest (0 = none, 1+ = yes) 434C : Wall collision animation timer 434D/E : Game timer (lo/high) -- no immediate
* effect 4351 : Search / Use timer 4352 : 0 normally, 144/176 opening chest, 160 when searching body, 176 opening
* door 4359 : Keys (8-bit flags, 255=skeleton key) 587B : Search timer
*/
}

View File

@@ -1,138 +0,0 @@
/*
* 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

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.config;
import java.lang.annotation.ElementType;
@@ -39,10 +37,10 @@ import java.lang.annotation.Target;
@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;
String name();
String shortName() default "";
String defaultValue() default "";
String description() default "";
String category() default "General";
boolean enablesDevice() default false;
}

View File

@@ -1,40 +1,33 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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.InvalidClassException;
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;
@@ -47,11 +40,18 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.core.Keyboard;
import jace.core.Utility;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
/**
@@ -60,14 +60,10 @@ import javafx.scene.image.ImageView;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Configuration implements Reconfigurable {
public EmulatorUILogic ui;
private static Method findAnyMethodByName(Class<? extends Reconfigurable> aClass, String m) {
for (Method method : aClass.getMethods()) {
if (method.getName().equals(m)) {
return method;
}
}
return null;
public Configuration() {
ui = Emulator.getUILogic();
}
static ConfigurableField getConfigurableFieldInfo(Reconfigurable subject, String settingName) {
@@ -85,15 +81,6 @@ public class Configuration implements Reconfigurable {
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);
}
@@ -120,6 +107,7 @@ public class Configuration implements Reconfigurable {
* configuration 2) Provide a simple persistence mechanism to load/store
* configuration
*/
@SuppressWarnings("all")
public static class ConfigNode extends TreeItem implements Serializable {
public transient ConfigNode root;
@@ -144,14 +132,21 @@ public class Configuration implements Reconfigurable {
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
children = super.getChildren();
// Create children list if it doesn't exist
if (children == null) {
children = FXCollections.observableArrayList();
children.setAll(getChildren());
}
children.setAll(super.getChildren());
id = (String) in.readObject();
name = (String) in.readObject();
settings = (Map) in.readObject();
hotkeys = (Map) in.readObject();
settings = (Map<String, Serializable>) in.readObject();
hotkeys = (Map<String, String[]>) in.readObject();
Object[] nodeArray = (Object[]) in.readObject();
for (Object child : nodeArray) {
children.add((ConfigNode) child);
synchronized (children) {
for (Object child : nodeArray) {
children.add((ConfigNode) child);
}
}
}
@@ -215,19 +210,23 @@ public class Configuration implements Reconfigurable {
}
@Override
public ObservableList<ConfigNode> getChildren() {
final public ObservableList<ConfigNode> getChildren() {
return super.getChildren();
}
private boolean removeChild(String childName) {
ConfigNode child = findChild(childName);
return children.remove(child);
synchronized (children) {
return children.remove(child);
}
}
private ConfigNode findChild(String id) {
for (ConfigNode node : children) {
if (id.equalsIgnoreCase(node.id)) {
return node;
synchronized (children) {
for (ConfigNode node : children) {
if (id.equalsIgnoreCase(node.id)) {
return node;
}
}
}
return null;
@@ -244,7 +243,9 @@ public class Configuration implements Reconfigurable {
index++;
}
}
children.add(index, newChild);
synchronized (children) {
children.add(index, newChild);
}
}
private void setChanged(boolean b) {
@@ -255,36 +256,49 @@ public class Configuration implements Reconfigurable {
getChangedIcon().ifPresent(this::setGraphic);
}
}
public Stream<ConfigNode> getTreeAsStream() {
synchronized (children) {
return Stream.concat(
Stream.of(this),
children.stream().flatMap(ConfigNode::getTreeAsStream));
}
}
}
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());
Set<ConfigNode> visited = new LinkedHashSet<>();
buildTree(BASE, visited);
Emulator.withComputer(c->{
ConfigNode computer = new ConfigNode(BASE, c);
BASE.putChild(c.getName(), computer);
buildTree(computer, visited);
});
}
@SuppressWarnings("all")
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());
}
InvokableActionRegistry registry = InvokableActionRegistry.getInstance();
registry.getStaticMethodNames(node.subject.getClass()).stream().forEach((name) ->
node.hotkeys.put(name, registry.getStaticMethodInfo(name).defaultKeyMapping())
);
registry.getInstanceMethodNames(node.subject.getClass()).stream().forEach((name) ->
node.hotkeys.put(name, registry.getInstanceMethodInfo(name).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)) {
if (o == null || !f.getType().isPrimitive() && f.getType() != String.class && visited.contains(o)) {
continue;
}
visited.add(o);
@@ -294,24 +308,22 @@ public class Configuration implements Reconfigurable {
// 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())) {
if (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;
if (o instanceof Reconfigurable r) {
ConfigNode child = node.findChild(r.getName());
if (child == null || !child.subject.equals(o)) {
child = new ConfigNode(node, r);
node.putChild(f.getName(), child);
} else {
Logger.getLogger(Configuration.class.getName()).severe("Unable to find child named %s for node %s".formatted(r.getName(), node.name));
}
buildTree(child, visited);
} else if (o.getClass().isArray()) {
@@ -324,8 +336,7 @@ public class Configuration implements Reconfigurable {
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;
if (genericTypes instanceof GenericArrayType aType) {
ParameterizedType pType = (ParameterizedType) aType.getGenericComponentType();
if (pType.getActualTypeArguments().length != 1) {
continue;
@@ -339,11 +350,13 @@ public class Configuration implements Reconfigurable {
continue;
}
for (Optional<Reconfigurable> child : (Optional<Reconfigurable>[]) o) {
if (child.isPresent()) {
children.add(child.get());
} else {
children.add(null);
synchronized (children) {
for (Optional<Reconfigurable> child : (Optional<Reconfigurable>[]) o) {
if (child.isPresent()) {
children.add(child.get());
} else {
children.add(null);
}
}
}
}
@@ -379,7 +392,6 @@ public class Configuration implements Reconfigurable {
defaultKeyMapping = "meta+ctrl+s"
)
public static void saveSettings() {
FileOutputStream fos = null;
{
ObjectOutputStream oos = null;
try {
@@ -410,29 +422,29 @@ public class Configuration implements Reconfigurable {
defaultKeyMapping = "meta+ctrl+r"
)
public static void loadSettings() {
{
boolean successful = false;
ObjectInputStream ois = null;
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 (InvalidClassException | NullPointerException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.WARNING, "Unable to load settings, Jace version is newer and incompatible with old settings.");
} catch (ClassNotFoundException | IOException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
} finally {
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);
if (ois != null) {
ois.close();
}
if (!successful) {
applySettings(BASE);
}
} catch (IOException ex) {
Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@@ -445,6 +457,34 @@ public class Configuration implements Reconfigurable {
return new File(System.getProperty("user.dir"), ".jace.conf");
}
public static void registerKeyHandlers() {
registerKeyHandlers(BASE, true);
}
public static void registerKeyHandlers(ConfigNode node, boolean recursive) {
Keyboard.unregisterAllHandlers(node.subject);
InvokableActionRegistry registry = InvokableActionRegistry.getInstance();
node.hotkeys.keySet().stream().forEach((name) -> {
InvokableAction action = registry.getStaticMethodInfo(name);
if (action != null) {
for (String code : node.hotkeys.get(name)) {
Keyboard.registerInvokableAction(action, node.subject, registry.getStaticFunction(name), code);
}
}
action = registry.getInstanceMethodInfo(name);
if (action != null) {
for (String code : node.hotkeys.get(name)) {
Keyboard.registerInvokableAction(action, node.subject, registry.getInstanceFunction(name), code);
}
}
});
if (recursive) {
node.getChildren().stream().forEach((child) -> {
registerKeyHandlers(child, true);
});
}
}
/**
* Apply settings from node tree to the object model This also calls
* "reconfigure" on objects in sequence
@@ -454,33 +494,29 @@ public class Configuration implements Reconfigurable {
* 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;
}
AtomicBoolean hasChanged = new AtomicBoolean(false);
// Now that the object structure reflects the current configuration,
// process reconfiguration from the children, etc.
for (ConfigNode child : node.getChildren()) {
hasChanged |= applySettings(child);
}
Emulator.whileSuspended(c-> {
if (node.changed) {
doApply(node);
hasChanged.set(true);
}
if (node.equals(BASE) && hasChanged) {
// Now that the object structure reflects the current configuration,
// process reconfiguration from the children, etc.
for (ConfigNode child : node.getChildren()) {
if (applySettings(child)) hasChanged.set(true);
}
});
if (node.equals(BASE) && hasChanged.get()) {
buildTree();
}
if (resume) {
Emulator.computer.resume();
}
return hasChanged;
return hasChanged.get();
}
@SuppressWarnings("all")
private static void applyConfigTree(ConfigNode newRoot, ConfigNode oldRoot) {
if (oldRoot == null || newRoot == null) {
return;
@@ -494,24 +530,18 @@ public class Configuration implements Reconfigurable {
newRoot.getChildren().stream().forEach((child) -> {
String childName = child.toString();
ConfigNode oldChild = oldRoot.findChild(childName);
if (oldChild == null) {oldChild = oldRoot.findChild(child.id);}
if (oldChild == null) {
oldChild = oldRoot.findChild(child.id);
}
// System.out.println("Applying settings for " + childName);
applyConfigTree(child, oldChild);
});
}
@SuppressWarnings("all")
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);
}
}
});
registerKeyHandlers(node, false);
for (String f : node.settings.keySet()) {
try {
@@ -576,7 +606,7 @@ public class Configuration implements Reconfigurable {
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(), ", "));
System.err.println("Unable to find device named " + deviceName + ", try one of these: " + String.join(", ", shortNames.keySet()));
continue;
}
@@ -598,7 +628,7 @@ public class Configuration implements Reconfigurable {
}
}
if (!found) {
System.err.println("Unable to find property " + fieldName + " for device " + deviceName + ". Try one of these: " + Utility.join(shortFieldNames, ", "));
System.err.println("Unable to find property " + fieldName + " for device " + deviceName + ". Try one of these: " + String.join(", ", shortFieldNames));
}
}
}
@@ -606,27 +636,29 @@ public class Configuration implements Reconfigurable {
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);
});
synchronized (n.getChildren()) {
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);
});
}
// 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, i + 1);
// });
// }
}

View File

@@ -1,19 +1,19 @@
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.Optional;
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 jace.config.Configuration.ConfigNode;
import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
@@ -88,11 +88,12 @@ public class ConfigurationUIController {
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();
cancelConfig(null);
deviceTree.getSelectionModel().selectedItemProperty().addListener(this::selectionChanged);
deviceTree.maxWidthProperty().bind(treeScroll.widthProperty());
}
@SuppressWarnings("all")
private void resetDeviceTree() {
Set<String> expanded = new HashSet<>();
String current = getCurrentNodePath();
@@ -104,8 +105,8 @@ public class ConfigurationUIController {
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();
root.getChildren().stream().filter(TreeItem::isExpanded).forEach((item) -> {
String name = prefix+ item;
expanded.add(name);
getExpandedNodes(name+DELIMITER, item, expanded);
});
@@ -113,7 +114,7 @@ public class ConfigurationUIController {
private void setExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
if (root == null) return;
root.getChildren().stream().forEach((item) -> {
root.getChildren().forEach((item) -> {
String name = prefix+item.toString();
if (expanded.contains(name)) {
item.setExpanded(true);
@@ -133,6 +134,7 @@ public class ConfigurationUIController {
return out;
}
@SuppressWarnings("all")
private void setCurrentNodePath(String value) {
if (value == null) return;
String[] parts = value.split(Pattern.quote(DELIMITER));
@@ -163,12 +165,8 @@ public class ConfigurationUIController {
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));
});
node.hotkeys.forEach((name, values) -> buildKeyShortcutRow(node, name, values).ifPresent(settingsVbox.getChildren()::add));
node.settings.forEach((name, value) -> settingsVbox.getChildren().add(buildSettingRow(node, name, value)));
}
private Node buildSettingRow(ConfigNode node, String settingName, Serializable value) {
@@ -188,33 +186,36 @@ public class ConfigurationUIController {
return row;
}
private Node buildKeyShortcutRow(ConfigNode node, String actionName, String[] values) {
InvokableAction actionInfo = Configuration.getInvokableActionInfo(node.subject, actionName);
private Optional<Node> buildKeyShortcutRow(ConfigNode node, String actionName, String[] values) {
InvokableActionRegistry registry = InvokableActionRegistry.getInstance();
InvokableAction actionInfo = registry.getInstanceMethodInfo(actionName);
if (actionInfo == null) {
return null;
actionInfo = registry.getStaticMethodInfo(actionName);
}
if (actionInfo == null) {
return Optional.empty();
}
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 "));
String value = String.join(" or ", values);
Text widget = new Text(value);
widget.setWrappingWidth(180.0);
widget.getStyleClass().add("setting-keyboard-value");
widget.setOnMouseClicked((event) -> {
editKeyboardShortcut(node, actionName, widget);
});
widget.setOnMouseClicked((event) -> editKeyboardShortcut(node, actionName, widget));
label.setLabelFor(widget);
row.getChildren().add(label);
row.getChildren().add(widget);
return row;
return Optional.of(row);
}
private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
throw new UnsupportedOperationException("Not supported yet.");
}
@SuppressWarnings("all")
private Node buildEditField(ConfigNode node, String settingName, Serializable value) {
Field field;
try {
@@ -237,8 +238,6 @@ public class ConfigurationUIController {
}
} 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);
}
@@ -247,21 +246,18 @@ public class ConfigurationUIController {
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());
});
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());
});
widget.selectedProperty().addListener((e) -> node.setFieldValue(settingName, widget.isSelected()));
return widget;
}
@SuppressWarnings("all")
private Node buildDynamicSelectComponent(ConfigNode node, String settingName, Serializable value) {
try {
DynamicSelection sel = (DynamicSelection) node.subject.getClass().getField(settingName).get(node.subject);
@@ -284,9 +280,9 @@ public class ConfigurationUIController {
} else {
widget.setValue(selected);
}
widget.valueProperty().addListener((Observable e) -> {
node.setFieldValue(settingName, widget.getConverter().toString(widget.getValue()));
});
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);

View File

@@ -0,0 +1,7 @@
package jace.config;
public interface DeviceEnum<T extends Reconfigurable> {
public String getName();
public T create();
public boolean isInstance(T t);
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
* @param <C> Enum class which implements DeviceEnum
*/
// C is an enum class which implements DeviceEnum
@SuppressWarnings("all")
public class DeviceSelection<C extends Enum & DeviceEnum> extends DynamicSelection<C> {
Class<C> enumClass;
boolean nullAllowed = false;
public DeviceSelection(Class<C> enumClass, C defaultValue) {
super(defaultValue);
if (defaultValue == null) {
nullAllowed = true;
}
this.enumClass = enumClass;
}
public DeviceSelection(Class<C> enumClass, C defaultValue, boolean nullAllowed) {
this(enumClass, defaultValue);
this.nullAllowed = nullAllowed;
}
@Override
public LinkedHashMap<C, String> getSelections() {
LinkedHashMap<C, String> selections = new LinkedHashMap<>();
if (allowNull()) {
selections.put(null, "***Empty***");
}
// Sort enum constants by getName
List<C> sorted = new ArrayList<>();
sorted.addAll(Arrays.asList(enumClass.getEnumConstants()));
Collections.sort(sorted, (C o1, C o2) -> o1.getName().compareTo(o2.getName()));
for (C c : enumClass.getEnumConstants()) {
selections.put(c, c.getName());
}
return selections;
}
@Override
public boolean allowNull() {
return nullAllowed;
}
@Override
public void setValue(C value) {
Object v = value;
if (v != null && v instanceof String) {
super.setValueByMatch((String) v);
return;
}
super.setValue(value);
}
}

View File

@@ -1,27 +1,26 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.config;
import jace.core.Utility;
import java.util.Iterator;
import java.util.Map;
import jace.core.Utility;
/**
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.config;
import java.io.Serializable;
@@ -27,11 +25,11 @@ import java.util.LinkedHashMap;
*/
public interface ISelection<T> extends Serializable {
public LinkedHashMap<? extends T, String> getSelections();
LinkedHashMap<? extends T, String> getSelections();
public T getValue();
T getValue();
public void setValue(T value);
void setValue(T value);
public void setValueByMatch(String value);
void setValueByMatch(String value);
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.config;
import java.lang.annotation.ElementType;
@@ -26,19 +24,19 @@ 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/>
* <br>
* Name should be short, meaningful, and succinct. e.g. "Insert disk"
* <br/>
* <br>
* Category can be used to group actions by overall topic, for example an automated table of contents
* <br/>
* <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/>
* <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/>
* <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
@@ -51,37 +49,37 @@ public @interface InvokableAction {
/*
* Should be short and meaningful name for action being invoked, e.g. "Insert disk"
*/
public String name();
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";
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 "";
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 "";
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;
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;
boolean notifyOnRelease() default false;
/*
* Standard keyboard mapping
*/
public String[] defaultKeyMapping();
String[] defaultKeyMapping();
}

View File

@@ -0,0 +1,168 @@
package jace.config;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
// Compile-time annotation processor which creates a registry of all static methods annotated with @InvokableAction.
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedAnnotationTypes("jace.config.InvokableAction")
public class InvokableActionAnnotationProcessor extends AbstractProcessor {
Messager messager;
Map<InvokableAction, ExecutableElement> staticMethods = new HashMap<>();
Map<InvokableAction, ExecutableElement> instanceMethods = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
messager.printMessage(javax.tools.Diagnostic.Kind.NOTE, "InvokableActionAnnotationProcessor init()");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
messager.printMessage(javax.tools.Diagnostic.Kind.NOTE, "InvokableActionAnnotationProcessor process()");
// Get list of methods annotated with @InvokableAction.
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(InvokableAction.class);
for (Element element : elements) {
if (element.getModifiers().contains(javax.lang.model.element.Modifier.STATIC)) {
// If the annotation method is static, add it to the static method registry.
trackStaticMethod(element);
} else {
// For non-static methods, track in a separate registry.
trackInstanceMethod(element);
}
try {
// Write class that contains static and instance methods.
writeRegistryClass();
} catch (IOException ex) {
messager.printMessage(javax.tools.Diagnostic.Kind.ERROR, "Error writing InvokableActionRegistry.java: " + ex.getMessage());
}
}
return true;
}
private void trackStaticMethod(Element element) {
// Store the method in the static method registry.
staticMethods.put(element.getAnnotation(InvokableAction.class), (ExecutableElement) element);
}
private void trackInstanceMethod(Element element) {
// Store the method in the instance method registry.
instanceMethods.put(element.getAnnotation(InvokableAction.class), (ExecutableElement) element);
}
private String serializeArrayOfStrings(String... strings) {
return Arrays.stream(strings).map(s -> "\"" + s + "\"").collect(Collectors.joining(","));
}
private void serializeInvokableAction(InvokableAction annotation, String variableName, PrintWriter writer) {
writer.append("""
%s = createInvokableAction("%s", "%s", "%s", "%s", %s, %s, new String[] {%s});
""".formatted(
variableName,
annotation.name(),
annotation.category(),
annotation.description(),
annotation.alternatives(),
annotation.consumeKeyEvent(),
annotation.notifyOnRelease(),
serializeArrayOfStrings(annotation.defaultKeyMapping())
));
}
// Write the registry class.
private void writeRegistryClass() throws IOException {
Files.createDirectories(new File("target/generated-sources/jace/config").toPath());
try (PrintWriter writer = new PrintWriter(new FileWriter("target/generated-sources/jace/config/InvokableActionRegistryImpl.java"))) {
writer.write("""
package jace.config;
import java.util.logging.Level;
public class InvokableActionRegistryImpl extends InvokableActionRegistry {
@Override
public void init() {
InvokableAction annotation;
""");
for (Map.Entry<InvokableAction, ExecutableElement> entry : staticMethods.entrySet()) {
InvokableAction annotation = entry.getKey();
ExecutableElement method = entry.getValue();
String packageName = method.getEnclosingElement().getEnclosingElement().toString();
String className = method.getEnclosingElement().getSimpleName().toString();
String fqnClassName = packageName + "." + className;
serializeInvokableAction(annotation, "annotation", writer);
boolean takesBoolenParameter = method.getParameters().size() == 1 && method.getParameters().get(0).asType().toString().equalsIgnoreCase("boolean");
boolean returnsBoolean = method.getReturnType().toString().equalsIgnoreCase("boolean");
writer.write("""
putStaticAction(annotation.name(), %s.class, annotation, (b) -> {
try {
%s %s.%s(%s);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking %s", ex);
%s
}
});
""".formatted(
fqnClassName,
returnsBoolean ? "return " : "",
fqnClassName,
method.getSimpleName(),
takesBoolenParameter ? "b" : "",
fqnClassName + "." + method.getSimpleName(),
returnsBoolean ? "return false;" : ""
));
}
// Now for the instance methods, do the same except use a biconsumer which takes the instance as well as the boolean parameter.
for (Map.Entry<InvokableAction, ExecutableElement> entry : instanceMethods.entrySet()) {
InvokableAction annotation = entry.getKey();
ExecutableElement method = entry.getValue();
String packageName = method.getEnclosingElement().getEnclosingElement().toString();
String className = method.getEnclosingElement().getSimpleName().toString();
String fqnClassName = packageName + "." + className;
serializeInvokableAction(annotation, "annotation", writer);
boolean takesBoolenParameter = method.getParameters().size() == 1 && method.getParameters().get(0).asType().toString().equalsIgnoreCase("boolean");
boolean returnsBoolean = method.getReturnType().toString().equalsIgnoreCase("boolean");
writer.write("""
putInstanceAction(annotation.name(), %s.class, annotation, (o, b) -> {
try {
%s ((%s) o).%s(%s);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking %s", ex);
%s
}
});
""".formatted(
fqnClassName,
returnsBoolean ? "return " : "",
fqnClassName,
method.getSimpleName(),
takesBoolenParameter ? "b" : "",
fqnClassName + "." + method.getSimpleName(),
returnsBoolean ? "return false;" : ""
));
}
writer.write("}\n}");
}
}
}

View File

@@ -0,0 +1,149 @@
package jace.config;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Logger;
@SuppressWarnings("all")
public abstract class InvokableActionRegistry {
protected static final Logger logger = Logger.getLogger(InvokableActionRegistry.class.getName());
private final Map<Class, Set<String>> staticMethodNames = new HashMap<>();
private final Map<String, InvokableAction> staticMethodInfo = new HashMap<>();
private final Map<String, Function<Boolean, Boolean>> staticMethodCallers = new HashMap<>();
private final Map<Class, Set<String>> instanceMethodNames = new HashMap<>();
private final Map<String, InvokableAction> instanceMethodInfo = new HashMap<>();
private final Map<String, BiFunction<Object, Boolean, Boolean>> instanceMethodCallers = new HashMap<>();
protected static InvokableActionRegistry instance;
public static InvokableActionRegistry getInstance() {
if (instance == null) {
instance = new InvokableActionRegistryImpl();
instance.init();
}
return instance;
}
abstract public void init();
final public void putStaticAction(String name, Class c, InvokableAction action, Consumer<Boolean> caller) {
putStaticAction(name, c, action, (b) -> {
caller.accept(b);
return true;
});
}
final public void putStaticAction(String name, Class c, InvokableAction action, Function<Boolean, Boolean> caller) {
staticMethodInfo.put(name, action);
staticMethodCallers.put(name, caller);
staticMethodNames.computeIfAbsent(c, k -> new TreeSet<>()).add(name);
}
public void putInstanceAction(String name, Class c, InvokableAction action, BiConsumer<Object, Boolean> caller) {
putInstanceAction(name, c, action, (o, b) -> {
caller.accept(o, b);
return true;
});
}
public void putInstanceAction(String name, Class c, InvokableAction action, BiFunction<Object, Boolean, Boolean> caller) {
instanceMethodInfo.put(name, action);
instanceMethodCallers.put(name, caller);
instanceMethodNames.computeIfAbsent(c, k -> new TreeSet<>()).add(name);
}
public Set<String> getStaticMethodNames(Class c) {
// Build a set of all the method names for this class and all its superclasses.
Set<String> result = new TreeSet<>();
Class current = c;
while (current != null) {
result.addAll(staticMethodNames.getOrDefault(current, Collections.EMPTY_SET));
current = current.getSuperclass();
}
return result;
}
public Set<String> getInstanceMethodNames(Class c) {
// Build a set of all the method names for this class and all its superclasses.
Set<String> result = new TreeSet<>();
Class current = c;
while (current != null) {
result.addAll(instanceMethodNames.getOrDefault(current, Collections.EMPTY_SET));
current = current.getSuperclass();
}
return result;
}
public InvokableAction getStaticMethodInfo(String name) {
return staticMethodInfo.get(name);
}
public InvokableAction getInstanceMethodInfo(String name) {
return instanceMethodInfo.get(name);
}
public Function<Boolean, Boolean> getStaticFunction(String name) {
return staticMethodCallers.get(name);
}
public BiFunction<Object, Boolean, Boolean> getInstanceFunction(String name) {
return instanceMethodCallers.get(name);
}
public Set<InvokableAction> getAllStaticActions() {
return new HashSet<>(staticMethodInfo.values());
}
protected InvokableAction createInvokableAction(String name, String category, String description, String alternatives, boolean consumeKeyEvent, boolean notifyOnRelease, String[] defaultKeyMapping) {
return new InvokableAction() {
@Override
public String name() {
return name;
}
@Override
public String category() {
return category;
}
@Override
public String description() {
return description;
}
@Override
public String alternatives() {
return alternatives;
}
@Override
public boolean consumeKeyEvent() {
return consumeKeyEvent;
}
@Override
public boolean notifyOnRelease() {
return notifyOnRelease;
}
@Override
public String[] defaultKeyMapping() {
return defaultKeyMapping;
}
@Override
public Class<? extends java.lang.annotation.Annotation> annotationType() {
return InvokableAction.class;
}
};
}
}

View File

@@ -0,0 +1,226 @@
package jace.config;
import java.io.IOException;
import java.util.logging.Level;
// NOTE: This is generated code. Do not edit.
public class InvokableActionRegistryImpl extends InvokableActionRegistry {
@Override
public void init() {
InvokableAction annotation;
annotation = createInvokableAction("Resize window", "general", "Resize the screen to 1x/1.5x/2x/3x video size", "Aspect;Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;", true, false, new String[]{"ctrl+shift+a"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.scaleIntegerRatio();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.scaleIntegerRatio", ex);
}
});
annotation = createInvokableAction("Rewind", "General", "Go back 1 second", "Timewarp", true, false, new String[]{"ctrl+shift+Open Bracket"});
putStaticAction(annotation.name(), jace.state.StateManager.class, annotation, (b) -> {
try {
jace.state.StateManager.beKindRewind();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.state.StateManager.beKindRewind", ex);
}
});
annotation = createInvokableAction("Configuration", "general", "Edit emulator configuraion", "Reconfigure;Preferences;Settings;Config", true, false, new String[]{"f4", "ctrl+shift+c"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.showConfig();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.showConfig", ex);
}
});
annotation = createInvokableAction("Load settings", "general", "Load all configuration settings previously saved", "load preferences;revert settings;revert preferences", true, false, new String[]{"meta+ctrl+r"});
putStaticAction(annotation.name(), jace.config.Configuration.class, annotation, (b) -> {
try {
jace.config.Configuration.loadSettings();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.config.Configuration.loadSettings", ex);
}
});
annotation = createInvokableAction("About", "general", "Display about window", "info;credits", true, false, new String[]{"ctrl+shift+."});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.showAboutWindow();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.showAboutWindow", ex);
}
});
annotation = createInvokableAction("Record sound", "sound", "Toggles recording (saving) sound output to a file", "", true, false, new String[]{"ctrl+shift+w"});
putStaticAction(annotation.name(), jace.apple2e.Speaker.class, annotation, (b) -> {
try {
jace.apple2e.Speaker.toggleFileOutput();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.apple2e.Speaker.toggleFileOutput", ex);
}
});
annotation = createInvokableAction("BRUN file", "file", "Loads a binary file in memory and executes it. File should end with #06xxxx, where xxxx is the start address in hex", "Execute program;Load binary;Load program;Load rom;Play single-load game", true, false, new String[]{"ctrl+shift+b"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.runFile();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.runFile", ex);
}
});
annotation = createInvokableAction("Save Raw Screenshot", "general", "Save raw (RAM) format of visible screen", "screendump;raw screenshot", true, false, new String[]{"ctrl+shift+z"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.saveScreenshotRaw();
} catch (IOException ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.saveScreenshotRaw", ex);
}
});
annotation = createInvokableAction("Save settings", "general", "Save all configuration settings as defaults", "save preferences;save defaults", true, false, new String[]{"meta+ctrl+s"});
putStaticAction(annotation.name(), jace.config.Configuration.class, annotation, (b) -> {
try {
jace.config.Configuration.saveSettings();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.config.Configuration.saveSettings", ex);
}
});
annotation = createInvokableAction("Toggle fullscreen", "general", "Activate/deactivate fullscreen mode", "fullscreen;maximize", true, false, new String[]{"ctrl+shift+f"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.toggleFullscreen();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.toggleFullscreen", ex);
}
});
annotation = createInvokableAction("Toggle Debug", "debug", "Show/hide the debug panel", "Show Debug;Hide Debug;Inspect", true, false, new String[]{"ctrl+shift+d"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.toggleDebugPanel();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.toggleDebugPanel", ex);
}
});
annotation = createInvokableAction("Refresh screen", "display", "Marks screen contents as changed, forcing full screen redraw", "redraw", true, false, new String[]{"ctrl+shift+r"});
putStaticAction(annotation.name(), jace.core.Video.class, annotation, (b) -> {
try {
jace.core.Video.forceRefresh();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Video.forceRefresh", ex);
}
});
annotation = createInvokableAction("Toggle video mode", "video", "", "Gfx mode;color;b&w;monochrome", true, false, new String[]{"ctrl+shift+g"});
putStaticAction(annotation.name(), jace.apple2e.VideoNTSC.class, annotation, (b) -> {
try {
jace.apple2e.VideoNTSC.changeVideoMode();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.apple2e.VideoNTSC.changeVideoMode", ex);
}
});
annotation = createInvokableAction("Save Screenshot", "general", "Save image of visible screen", "Save image;save framebuffer;screenshot", true, false, new String[]{"ctrl+shift+s"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.saveScreenshot();
} catch (IOException ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.saveScreenshot", ex);
}
});
annotation = createInvokableAction("Paste clipboard", "Keyboard", "", "paste", true, false, new String[]{"Ctrl+Shift+V", "Shift+Insert"});
putStaticAction(annotation.name(), jace.core.Keyboard.class, annotation, (b) -> {
try {
jace.core.Keyboard.pasteFromClipboard();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Keyboard.pasteFromClipboard", ex);
}
});
annotation = createInvokableAction("Open IDE", "development", "Open new IDE window for Basic/Assembly/Plasma coding", "IDE;dev;development;acme;assembler;editor", true, false, new String[]{"ctrl+shift+i"});
putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> {
try {
jace.EmulatorUILogic.showIDE();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.showIDE", ex);
}
});
annotation = createInvokableAction("Up", "joystick", "", "", true, true, new String[]{"up"});
putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> {
try {
return ((jace.hardware.Joystick) o).joystickUp(b);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickUp", ex);
return false;
}
});
annotation = createInvokableAction("Open Apple Key", "Keyboard", "", "OA", false, true, new String[]{"Alt"});
putInstanceAction(annotation.name(), jace.core.Keyboard.class, annotation, (o, b) -> {
try {
((jace.core.Keyboard) o).openApple(b);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Keyboard.openApple", ex);
}
});
annotation = createInvokableAction("Left", "joystick", "", "", true, true, new String[]{"left"});
putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> {
try {
return ((jace.hardware.Joystick) o).joystickLeft(b);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickLeft", ex);
return false;
}
});
annotation = createInvokableAction("Right", "joystick", "", "", true, true, new String[]{"right"});
putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> {
try {
return ((jace.hardware.Joystick) o).joystickRight(b);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickRight", ex);
return false;
}
});
annotation = createInvokableAction("Closed Apple Key", "Keyboard", "", "CA", false, true, new String[]{"Shortcut", "Meta", "Command"});
putInstanceAction(annotation.name(), jace.core.Keyboard.class, annotation, (o, b) -> {
try {
((jace.core.Keyboard) o).solidApple(b);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Keyboard.solidApple", ex);
}
});
annotation = createInvokableAction("Down", "joystick", "", "", true, true, new String[]{"down"});
putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> {
try {
return ((jace.hardware.Joystick) o).joystickDown(b);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickDown", ex);
return false;
}
});
annotation = createInvokableAction("Pause", "General", "Stops the computer, allowing reconfiguration of core elements", "freeze;halt", true, false, new String[]{"meta+pause", "alt+pause"});
putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> {
try {
return ((jace.core.Computer) o).pause();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Computer.pause", ex);
return false;
}
});
annotation = createInvokableAction("Reset", "general", "Process user-initatiated reboot (ctrl+apple+reset)", "reboot;reset;three-finger-salute;restart", true, false, new String[]{"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"});
putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> {
try {
((jace.core.Computer) o).invokeReset();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Computer.invokeWarmStart", ex);
}
});
annotation = createInvokableAction("Toggle Cheats", "General", "", "cheat;Plug-in", true, false, new String[]{"ctrl+shift+m"});
putInstanceAction(annotation.name(), jace.cheat.Cheats.class, annotation, (o, b) -> {
try {
((jace.cheat.Cheats) o).toggleCheats();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.cheat.Cheats.toggleCheats", ex);
}
});
annotation = createInvokableAction("Resume", "General", "Resumes the computer if it was previously paused", "unpause;unfreeze;resume;play", true, false, new String[]{"meta+shift+pause", "alt+shift+pause"});
putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> {
try {
((jace.core.Computer) o).resume();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Error invoking jace.core.Computer.resume", ex);
}
});
}
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.config;
import java.lang.annotation.ElementType;
@@ -30,6 +28,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Name {
public String value();
public String description() default "";
String value();
String description() default "";
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.config;
/**
@@ -23,7 +21,7 @@ package jace.config;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public interface Reconfigurable {
public String getName();
public String getShortName();
public void reconfigure();
String getName();
String getShortName();
void reconfigure();
}

View File

@@ -1,28 +1,27 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.config.ConfigurableField;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.config.ConfigurableField;
/**
* 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
@@ -35,10 +34,6 @@ import java.util.logging.Logger;
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";
@@ -73,14 +68,14 @@ public abstract class CPU extends Device {
}
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();
whileSuspended(()->{
ArrayList<String> newLog = new ArrayList<>();
ArrayList<String> oldLog = traceLog;
traceLog = newLog;
LOG.log(Level.INFO, "Most recent {0} instructions:", traceLength);
oldLog.forEach(LOG::info);
oldLog.clear();
});
}
public void setDebug(Debugger d) {
@@ -117,9 +112,9 @@ public abstract class CPU extends Device {
try {
if (debugger != null) {
if (!debugger.isActive() && debugger.hasBreakpoints()) {
debugger.getBreakpoints().stream().filter((i) -> (i == getProgramCounter())).forEach((_item) -> {
if (debugger.getBreakpoints().contains(getProgramCounter())){
debugger.setActive(true);
});
}
}
if (debugger.isActive()) {
debugger.updateStatus();
@@ -127,11 +122,7 @@ public abstract class CPU extends Device {
// 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);
}
Thread.onSpinWait();
return;
}
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.apple2e.SoftSwitches;
@@ -33,10 +31,10 @@ import jace.apple2e.SoftSwitches;
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class Card extends Device {
public abstract class Card extends TimedDevice {
private final PagedMemory cxRom;
private final PagedMemory c8Rom;
private PagedMemory cxRom;
private PagedMemory c8Rom;
private int slot;
private RAMListener ioListener;
private RAMListener firmwareListener;
@@ -47,10 +45,10 @@ public abstract class Card extends Device {
*
* @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);
public Card(boolean isThrottled) {
super(isThrottled);
cxRom = new PagedMemory(0x0100, PagedMemory.Type.CARD_FIRMWARE);
c8Rom = new PagedMemory(0x0800, PagedMemory.Type.CARD_FIRMWARE);
}
@Override
@@ -101,50 +99,43 @@ public abstract class Card extends Device {
@Override
public void reconfigure() {
boolean restart = suspend();
unregisterListeners();
if (restart) {
resume();
}
registerListeners();
// Emulator.whileSuspended(c-> {
unregisterListeners();
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) -> {
ioListener = getMemory().observe("Slot " + getSlot() + " " + getDeviceName() + " IO access", 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);
firmwareListener = getMemory().observe("Slot " + getSlot() + " " + getDeviceName() + " CX Firmware access", RAMEvent.TYPE.ANY, baseRom, baseRom + 255, (e) -> {
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) -> {
c8firmwareListener = getMemory().observe("Slot " + getSlot() + " " + getDeviceName() + " C8 Firmware access", RAMEvent.TYPE.ANY, 0xc800, 0xcfff, (e) -> {
if (SoftSwitches.CXROM.isOff() && SoftSwitches.INTC8ROM.isOff()
&& computer.getMemory().getActiveSlot() == slot) {
&& 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);
getMemory().removeListener(ioListener);
getMemory().removeListener(firmwareListener);
getMemory().removeListener(c8firmwareListener);
}
}

View File

@@ -1,29 +1,32 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import jace.JaceApplication;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField;
import jace.config.Configuration;
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;
@@ -44,31 +47,35 @@ public abstract class Computer implements Reconfigurable {
public Keyboard keyboard;
public StateManager stateManager;
public Motherboard motherboard;
public boolean romLoaded;
public final CompletableFuture<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;
public final SoundMixer mixer = new SoundMixer();
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;
romLoaded = new CompletableFuture<>();
}
public RAM getMemory() {
abstract protected RAM createMemory();
final public RAM getMemory() {
if (memory == null) {
memory = createMemory();
memory.configureActiveMemory();
}
return memory;
}
public Motherboard getMotherboard() {
final public Motherboard getMotherboard() {
return motherboard;
}
ChangeListener<Boolean> runningPropertyListener = (prop, oldVal, newVal) -> runningProperty.set(newVal);
public void setMotherboard(Motherboard m) {
final public void setMotherboard(Motherboard m) {
if (motherboard != null && motherboard.isRunning()) {
motherboard.suspend();
}
@@ -78,11 +85,11 @@ public abstract class Computer implements Reconfigurable {
public BooleanProperty getRunningProperty() {
return runningProperty;
}
public boolean isRunning() {
final public boolean isRunning() {
return getRunningProperty().get();
}
public void notifyVBLStateChanged(boolean state) {
for (Optional<Card> c : getMemory().cards) {
c.ifPresent(card -> card.notifyVBLStateChanged(state));
@@ -92,81 +99,108 @@ public abstract class Computer implements Reconfigurable {
}
}
public void setMemory(RAM memory) {
final public void setMemory(RAM memory) {
if (this.memory != memory) {
if (this.memory != null) {
this.memory.detach();
RAM oldMemory = this.memory;
if (oldMemory != null) {
oldMemory.detach();
}
this.memory = memory;
if (memory != null) {
if (oldMemory != null) {
memory.copyFrom(oldMemory);
oldMemory.detach();
}
memory.attach();
}
memory.attach();
}
this.memory = memory;
}
public void waitForNextCycle() {
//@TODO IMPLEMENT TIMER SLEEP CODE!
}
public Video getVideo() {
final public Video getVideo() {
return video;
}
public void setVideo(Video video) {
final public void setVideo(Video video) {
if (this.video != null && this.video != video) {
getMotherboard().removeChildDevice(this.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() {
cpu.suspend();
motherboard.suspend();
video.suspend();
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();
if (video != null) {
getMotherboard().addChildDevice(video);
video.configureVideoMode();
video.reconfigure();
}
if (JaceApplication.getApplication() != null) {
JaceApplication.getApplication().reconnectUIHooks();
JaceApplication.getApplication().controller.connectVideo(video);
}
}
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();
final public CPU getCpu() {
return cpu;
}
final public void setCpu(CPU cpu) {
this.cpu = cpu;
}
abstract public void loadRom(boolean reload) throws IOException;
public void loadRom(String path) throws IOException {
memory.loadRom(path);
romLoaded.complete(true);
}
final public void deactivate() {
if (cpu != null) {
cpu.suspend();
}
if (motherboard != null) {
motherboard.suspend();
}
if (video != null) {
video.suspend();
}
if (mixer != null) {
mixer.detach();
}
}
/**
* If the user wants a full reset, use the coldStart method.
* This ensures a more consistent state of the machine.
* Some games make bad assumptions about the initial state of the machine
* and that fails to work if the machine is not reset to a known state first.
*/
@InvokableAction(
name = "Reset",
description = "Process user-initatiated reboot (ctrl+apple+reset)",
category = "general",
alternatives = "reboot;reset;three-finger-salute;restart",
defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"})
public void invokeReset() {
if (SoftSwitches.PDL0.isOn()) {
coldStart();
} else {
warmStart();
}
}
/**
* In a cold start, memory is reset (either two bytes per page as per Sather 4-15) or full-wipe
* Also video softswitches are reset
* Otherwise it does the same as warm start
**/
public abstract void coldStart();
/**
* In a warm start, memory is not reset, but the CPU and cards are reset
* All but video softswitches are reset, putting the MMU in a known state
*/
public abstract void warmStart();
public Keyboard getKeyboard() {
@@ -178,21 +212,24 @@ public abstract class Computer implements Reconfigurable {
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() {
final 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() {
@InvokableAction(name = "Resume", description = "Resumes the computer if it was previously paused", alternatives = "unpause;unfreeze;resume;play", defaultKeyMapping = {"meta+shift+pause", "alt+shift+pause"})
final public void resume() {
doResume();
getRunningProperty().set(true);
}
@Override
public void reconfigure() {
if (keyboard == null) {
keyboard = new Keyboard();
}
mixer.reconfigure();
if (enableStateManager) {
stateManager = StateManager.getInstance(this);
@@ -200,5 +237,6 @@ public abstract class Computer implements Reconfigurable {
stateManager = null;
StateManager.getInstance(this).invalidate();
}
Configuration.registerKeyHandlers();
}
}

View File

@@ -1,25 +1,24 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* A debugger has the ability to track a list of breakpoints and step a CPU one
@@ -48,19 +47,9 @@ public abstract class Debugger {
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;
}
}
return !breakpoints.isEmpty() && breakpoints.stream().anyMatch(Objects::nonNull);
}
boolean takeStep() {

View File

@@ -1,27 +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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.state.Stateful;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Supplier;
import jace.Emulator;
import jace.config.Reconfigurable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import jace.state.Stateful;
/**
* Device is a very simple abstraction of any emulation component. A device
@@ -31,32 +33,78 @@ import javafx.beans.property.SimpleBooleanProperty;
*
* 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
* 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
* @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;
}
private final Set<Device> children = new CopyOnWriteArraySet<>();
private Device[] childrenArray = new Device[0];
private Runnable tickHandler = this::__doTickNotRunning;
// Number of cycles to do nothing (for cpu/video cycle accuracy)
@Stateful
private int waitCycles = 0;
@Stateful
private final BooleanProperty run = new SimpleBooleanProperty(true);
private boolean run = false;
@Stateful
public boolean isPaused = false;
// Pausing a device overrides its run state, and is not reset by resuming directly
// Therefore a caller pausing a device must unpause it directly!
private boolean paused = false;
@Stateful
public boolean isAttached = false;
public BooleanProperty getRunningProperty() {
return run;
private RAM _ram = null;
protected RAM getMemory() {
if (_ram == null) {
_ram = Emulator.withMemory(m->m, null);
_ram.onDetach(()->_ram = null);
}
return _ram;
}
Device parentDevice = null;
public Device getParent() {
return parentDevice;
}
public void addChildDevice(Device d) {
if (d == null || children.contains(d) || d.equals(this)) {
return;
}
d.parentDevice = this;
children.add(d);
d.attach();
childrenArray = children.toArray(Device[]::new);
updateTickHandler();
}
public void removeChildDevice(Device d) {
if (d == null) {
return;
}
children.remove(d);
d.suspend();
d.detach();
childrenArray = children.toArray(Device[]::new);
updateTickHandler();
}
public void addAllDevices(Iterable<Device> devices) {
devices.forEach(this::addChildDevice);
}
public Iterable<Device> getChildren() {
return children;
}
public void setAllDevices(Collection<Device> newDevices) {
children.stream().filter(d-> !newDevices.contains(d)).forEach(this::removeChildDevice);
newDevices.stream().filter(d-> !children.contains(d)).forEach(this::addChildDevice);
}
public void addWaitCycles(int wait) {
waitCycles += wait;
}
@@ -65,40 +113,76 @@ public abstract class Device implements Reconfigurable {
waitCycles = wait;
}
private void updateTickHandler() {
if (!isRunning() || isPaused()) {
tickHandler = this::__doTickNotRunning;
} else if (childrenArray.length == 0) {
tickHandler = this::__doTickNoDevices;
} else {
tickHandler = this::__doTickIsRunning;
}
}
private void __doTickNotRunning() {
// Do nothing
}
private void __doTickIsRunning() {
for (Device d : childrenArray) {
if (d.isRunning() && !d.isPaused()) {
d.doTick();
}
}
if (waitCycles <= 0) {
tick();
return;
}
waitCycles--;
}
private void __doTickNoDevices() {
if (waitCycles <= 0) {
tick();
return;
}
waitCycles--;
}
/**
* This is called every tick, but it is critical that tick() should be overridden
* not this method! This is only overridable so timed device can implement timing
* semantics around this without interfering with the tick() method implementations.
*/
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();
tickHandler.run();
}
public boolean isRunning() {
return run.get();
return run;
}
public final boolean isPaused() {
return paused;
}
public synchronized void setRun(boolean run) {
// System.out.println(Thread.currentThread().getName() + (run ? " resuming " : " suspending ")+ getDeviceName());
isPaused = false;
this.run.set(run);
public final synchronized void setRun(boolean run) {
// if (this.run != run) {
// System.out.println(getDeviceName() + " " + (run ? "RUN" : "STOP"));
// Thread.dumpStack();
// }
this.run = run;
updateTickHandler();
}
public synchronized void setPaused(boolean paused) {
// if (this.paused != paused) {
// System.out.println(getDeviceName() + " " + (paused ? "PAUSED" : "UNPAUSED"));
// Thread.dumpStack();
// }
this.paused = paused;
updateTickHandler();
}
protected abstract String getDeviceName();
@Override
@@ -107,8 +191,49 @@ public abstract class Device implements Reconfigurable {
}
public abstract void tick();
public void whileSuspended(Runnable r) {
whileSuspended(()->{
r.run();
return null;
}, null);
}
public void whilePaused(Runnable r) {
whilePaused(()->{
r.run();
return null;
}, null);
}
public <T> T whileSuspended(Supplier<T> r, T defaultValue) {
T result;
if (isRunning()) {
suspend();
result = r.get();
resume();
} else {
result = r.get();
}
return result != null ? result : defaultValue;
}
public <T> T whilePaused(Supplier<T> r, T defaultValue) {
T result;
if (!isPaused() && isRunning()) {
setPaused(true);
result = r.get();
setPaused(false);
} else {
result = r.get();
}
return result != null ? result : defaultValue;
}
public boolean suspend() {
// Suspending the parent device means the children are not going to run
// children.forEach(Device::suspend);
if (isRunning()) {
setRun(false);
return true;
@@ -116,14 +241,34 @@ public abstract class Device implements Reconfigurable {
return false;
}
public void resume() {
setRun(true);
waitCycles = 0;
public void resumeAll() {
resume();
children.forEach(Device::resumeAll);
}
public abstract void attach();
public void resume() {
// Resuming children pre-emptively might lead to unexpected behavior
// Don't do that unless we really mean to (such as cold-starting the computer)
// children.forEach(Device::resume);
if (!isRunning()) {
setRun(true);
waitCycles = 0;
}
}
public void attach() {
isAttached = true;
children.forEach(Device::attach);
}
public void detach() {
children.forEach(Device::suspend);
children.forEach(Device::detach);
Keyboard.unregisterAllHandlers(this);
if (this.isRunning()) {
this.suspend();
}
isAttached = false;
_ram = null;
}
}

View File

@@ -1,24 +1,23 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import java.io.InputStream;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.paint.Color;
@@ -35,34 +34,31 @@ public class Font {
static public boolean initialized = false;
static public int getByte(int c, int yOffset) {
if (!initialized) {
if (font == null || !initialized) {
initalize();
}
return font[c][yOffset];
}
private static void initalize() {
initialized = true;
font = new int[256][8];
Thread fontLoader = new Thread(() -> {
InputStream in = ClassLoader.getSystemResourceAsStream("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;
InputStream in = Font.class.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();
}
initialized = true;
}
/**

View File

@@ -0,0 +1,150 @@
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
/**
* This is the core of a device that runs with its own independent clock in its
* own thread. Device timing is controlled by pausing the thread at regular
* intervals as necessary.
*
* This is primarily only used for the system clock, but it is possible to
* use for other devices that need to operate independently -- but it is best
* to do so only with caution as extra threads can lead to weird glitches if they
* need to have guarantees of synchronization, etc.
*
* @author brobert
*/
public abstract class IndependentTimedDevice extends TimedDevice {
public IndependentTimedDevice() {
super(false);
}
// The actual worker that the device runs as
public Thread worker;
public boolean hasStopped = true;
@Override
/* We really don't want to suspect the worker thread if we're running in it.
* The goal for suspending the thread is to prevent any concurrent activity
* affecting the emulator state. However, if we're already in the worker
* thread, then we're already blocking the execution of the emulator, so
* we don't need to suspend it.
*/
public void whileSuspended(Runnable r) {
if (isDeviceThread()) {
r.run();
} else {
super.whileSuspended(r);
}
}
@Override
public <T> T whileSuspended(java.util.function.Supplier<T> r, T defaultValue) {
if (isDeviceThread()) {
return r.get();
} else {
return super.whileSuspended(r, defaultValue);
}
}
public boolean isDeviceThread() {
return Thread.currentThread() == worker;
}
/**
* This is used in unit tests where we want the device
* to act like it is resumed, but not actual free-running.
* This allows tests to step manually to check behaviors, etc.
*/
public void resumeInThread() {
super.resume();
setPaused(false);
}
@Override
public boolean suspend() {
boolean result = super.suspend();
Thread w = worker;
worker = null;
if (w != null && w.isAlive()) {
try {
w.interrupt();
w.join(100);
} catch (InterruptedException ex) {
}
}
return result;
}
@Override
protected void pauseStart() {
// KLUDGE: Sleeping to wait for worker thread to hit paused state. We might be inside the worker (?)
if (!isDeviceThread()) {
Thread.onSpinWait();
}
}
public static int SLEEP_PRECISION_LIMIT = 100;
public void sleepUntil(Long time) {
if (time != null) {
while (System.nanoTime() < time) {
int waitTime = (int) ((time - System.nanoTime()) / 1000000);
if (waitTime >= SLEEP_PRECISION_LIMIT) {
try {
Thread.sleep(waitTime);
} catch (InterruptedException ex) {
return;
}
} else {
Thread.onSpinWait();
}
}
}
}
@Override
public synchronized void resume() {
super.resume();
if (worker != null && worker.isAlive()) {
return;
}
Thread newWorker = new Thread(() -> {
// System.out.println("Worker thread for " + getDeviceName() + " starting");
while (isRunning()) {
if (isPaused()) {
hasStopped = true;
while (isPaused() && isRunning()) {
Thread.onSpinWait();
}
hasStopped = false;
} else {
doTick();
sleepUntil(calculateResyncDelay());
}
}
hasStopped = true;
// System.out.println("Worker thread for " + getDeviceName() + " stopped");
});
this.worker = newWorker;
newWorker.setDaemon(false);
newWorker.setPriority(Thread.MAX_PRIORITY);
newWorker.setName("Timed device " + getDeviceName() + " worker");
newWorker.start();
}
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import javafx.scene.input.KeyCode;

View File

@@ -1,45 +1,40 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
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.function.BiFunction;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.Emulator;
import jace.apple2e.SoftSwitches;
import jace.config.InvokableAction;
import jace.config.Reconfigurable;
import javafx.event.EventHandler;
import javafx.scene.input.Clipboard;
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
@@ -57,17 +52,12 @@ public class Keyboard implements Reconfigurable {
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);
@@ -94,51 +84,58 @@ public class Keyboard implements Reconfigurable {
*/
public Keyboard() {
}
private static Map<KeyCode, Set<KeyHandler>> keyHandlersByKey = new HashMap<>();
private static Map<Object, Set<KeyHandler>> keyHandlersByOwner = new HashMap<>();
private static final Map<KeyCode, Set<KeyHandler>> keyHandlersByKey = new HashMap<>();
private static final 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());
/**
*
* @param action
* @param owner
* @param method
* @param code
*/
public static void registerInvokableAction(InvokableAction action, Object owner, Function<Boolean, Boolean> method, String code) {
registerKeyHandler(new KeyHandler(code) {
@Override
public boolean handleKeyUp(KeyEvent e) {
Emulator.withComputer(c -> c.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();
return method.apply(false) && action.consumeKeyEvent();
}
@Override
public boolean handleKeyDown(KeyEvent e) {
// System.out.println("Key down: "+method.toString());
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);
Emulator.withComputer(c -> c.getKeyboard().shiftPressed = e.isShiftDown());
if (action == null) {
return false;
}
if (returnValue != null) {
return (Boolean) returnValue;
return method.apply(true) && action.consumeKeyEvent();
}
}, owner);
}
public static void registerInvokableAction(InvokableAction action, Object owner, BiFunction<Object, Boolean, Boolean> method, String code) {
registerKeyHandler(new KeyHandler(code) {
@Override
public boolean handleKeyUp(KeyEvent e) {
Emulator.withComputer(c -> c.getKeyboard().shiftPressed = e.isShiftDown());
if (action == null || !action.notifyOnRelease()) {
return false;
}
return action != null ? action.consumeKeyEvent() : null;
return method.apply(owner, false) && action.consumeKeyEvent();
}
@Override
public boolean handleKeyDown(KeyEvent e) {
// System.out.println("Key down: "+method.toString());
Emulator.withComputer(c -> c.getKeyboard().shiftPressed = e.isShiftDown());
if (action == null) {
return false;
}
return method.apply(owner, true) && action.consumeKeyEvent();
}
}, owner);
}
@@ -159,9 +156,8 @@ public class Keyboard implements Reconfigurable {
if (!keyHandlersByOwner.containsKey(owner)) {
return;
}
keyHandlersByOwner.get(owner).stream().filter((handler) -> !(!keyHandlersByKey.containsKey(handler.key))).forEach((handler) -> {
keyHandlersByKey.get(handler.key).remove(handler);
});
keyHandlersByOwner.get(owner).stream().filter((handler) -> keyHandlersByKey.containsKey(handler.key)).forEach(
(handler) -> keyHandlersByKey.get(handler.key).remove(handler));
keyHandlersByOwner.remove(owner);
}
@@ -248,6 +244,7 @@ public class Keyboard implements Reconfigurable {
default:
}
Emulator.withComputer(computer -> computer.getKeyboard().shiftPressed = e.isShiftDown());
if (e.isShiftDown()) {
c = fixShiftedChar(c);
}
@@ -305,18 +302,18 @@ public class Keyboard implements Reconfigurable {
e.consume();
}
public static boolean isOpenApplePressed = false;
@InvokableAction(name = "Open Apple Key", alternatives = "OA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = "Alt", consumeKeyEvent = false)
public void openApple(boolean pressed) {
computer.pause();
isOpenApplePressed = pressed;
SoftSwitches.PB0.getSwitch().setState(pressed);
computer.resume();
}
public static boolean isClosedApplePressed = false;
@InvokableAction(name = "Closed Apple Key", alternatives = "CA", category = "Keyboard", notifyOnRelease = true, defaultKeyMapping = {"Shortcut","Meta","Command"}, consumeKeyEvent = false)
public void solidApple(boolean pressed) {
computer.pause();
isClosedApplePressed = pressed;
SoftSwitches.PB1.getSwitch().setState(pressed);
computer.resume();
}
public static void pasteFromString(String text) {
@@ -326,18 +323,10 @@ public class Keyboard implements Reconfigurable {
@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);
Clipboard clipboard = Clipboard.getSystemClipboard();
if (clipboard.hasString()) {
pasteFromString(clipboard.getString());
}
}
static StringReader pasteBuffer = null;

View File

@@ -0,0 +1,181 @@
package jace.core;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import org.lwjgl.stb.STBVorbis;
import org.lwjgl.stb.STBVorbisInfo;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import javafx.util.Duration;
public class Media {
int totalSamples = 0;
float totalDuration = 0;
long sampleRate = 0;
boolean isStereo = true;
ShortBuffer sampleBuffer;
File tempFile;
public Media(String resourcePath) throws IOException {
System.out.println("Loading media: " + resourcePath);
byte[] oggFile;
try (InputStream oggStream = getClass().getResourceAsStream(resourcePath)) {
oggFile = oggStream.readAllBytes();
}
ByteBuffer oggBuffer = null;
STBVorbisInfo info = null;
ShortBuffer tempSampleBuffer = null;
try (MemoryStack stack = MemoryStack.stackPush()) {
oggBuffer = MemoryUtil.memAlloc(oggFile.length);
oggBuffer.put(oggFile);
oggBuffer.flip();
IntBuffer error = stack.callocInt(1);
Long decoder = STBVorbis.stb_vorbis_open_memory(oggBuffer, error, null);
if (decoder == null || decoder <= 0) {
throw new RuntimeException("Failed to open Ogg Vorbis file. Error: " + getError(error.get(0)) + " -- file is located at " + resourcePath);
}
info = STBVorbisInfo.malloc(stack);
STBVorbis.stb_vorbis_get_info(decoder, info);
totalSamples = STBVorbis.stb_vorbis_stream_length_in_samples(decoder);
totalDuration = STBVorbis.stb_vorbis_stream_length_in_seconds(decoder);
sampleRate = info.sample_rate();
isStereo = info.channels() == 2;
if (isStereo) {
totalSamples *= 2;
}
tempSampleBuffer = MemoryUtil.memAllocShort(2048);
sampleBuffer = ShortBuffer.allocate(totalSamples);
int sampleCount = 1;
int currentOffset = 0;
while (sampleCount > 0) {
sampleCount = STBVorbis.stb_vorbis_get_samples_short_interleaved(decoder, isStereo?2:1, tempSampleBuffer);
if (sampleCount == 0) {
break;
}
// copy sample buffer into byte buffer so we can deallocate, then transfer the buffer contents
sampleBuffer.put(currentOffset, tempSampleBuffer, 0, sampleCount * (isStereo ? 2 : 1));
tempSampleBuffer.rewind();
currentOffset += sampleCount * (isStereo ? 2 : 1);
}
STBVorbis.stb_vorbis_close(decoder);
sampleBuffer.rewind();
} catch (RuntimeException ex) {
throw ex;
} finally {
if (oggBuffer != null)
MemoryUtil.memFree(oggBuffer);
if (tempSampleBuffer != null)
MemoryUtil.memFree(tempSampleBuffer);
}
}
public String getError(int vorbisErrorCode) {
switch (vorbisErrorCode) {
case STBVorbis.VORBIS__no_error:
return "VORBIS_no_error";
case STBVorbis.VORBIS_need_more_data:
return "VORBIS_need_more_data";
case STBVorbis.VORBIS_invalid_api_mixing:
return "VORBIS_invalid_api_mixing";
case STBVorbis.VORBIS_outofmem:
return "VORBIS_outofmem";
case STBVorbis.VORBIS_feature_not_supported:
return "VORBIS_feature_not_supported";
case STBVorbis.VORBIS_too_many_channels:
return "VORBIS_too_many_channels";
case STBVorbis.VORBIS_file_open_failure:
return "VORBIS_file_open_failure";
case STBVorbis.VORBIS_seek_without_length:
return "VORBIS_seek_without_length";
case STBVorbis.VORBIS_unexpected_eof:
return "VORBIS_unexpected_eof";
case STBVorbis.VORBIS_seek_invalid:
return "VORBIS_seek_invalid";
case STBVorbis.VORBIS_invalid_setup:
return "VORBIS_invalid_setup";
case STBVorbis.VORBIS_invalid_stream:
return "VORBIS_invalid_stream";
case STBVorbis.VORBIS_missing_capture_pattern:
return "VORBIS_missing_capture_pattern";
case STBVorbis.VORBIS_invalid_stream_structure_version:
return "VORBIS_invalid_stream_structure_version";
case STBVorbis.VORBIS_continued_packet_flag_invalid:
return "VORBIS_continued_packet_flag_invalid";
case STBVorbis.VORBIS_incorrect_stream_serial_number:
return "VORBIS_incorrect_stream_serial_number";
case STBVorbis.VORBIS_invalid_first_page:
return "VORBIS_invalid_first_page";
case STBVorbis.VORBIS_bad_packet_type:
return "VORBIS_bad_packet_type";
case STBVorbis.VORBIS_cant_find_last_page:
return "VORBIS_cant_find_last_page";
case STBVorbis.VORBIS_seek_failed:
return "VORBIS_seek_failed";
case STBVorbis.VORBIS_ogg_skeleton_not_supported:
return "VORBIS_ogg_skeleton_not_supported";
default:
return "Unknown error code: " + vorbisErrorCode;
}
}
public void close() {
if (sampleBuffer != null)
sampleBuffer.clear();
if (tempFile != null && tempFile.exists())
tempFile.delete();
}
public void seekToTime(Duration millis) {
int sampleNumber = (int) (millis.toMillis() * sampleRate / 1000);
sampleNumber = Math.max(0, Math.min(sampleNumber, totalSamples));
sampleBuffer.position(sampleNumber * (isStereo ? 2 : 1));
}
public boolean isEnded() {
return sampleBuffer.remaining() == 0;
}
public void restart() {
sampleBuffer.rewind();
}
public short getNextLeftSample() {
// read next sample for left and right channels
if (isEnded()) {
return 0;
}
return sampleBuffer.get();
}
public short getNextRightSample() {
if (isEnded()) {
return 0;
}
return isStereo ? sampleBuffer.get() : sampleBuffer.get(sampleBuffer.position());
}
public java.time.Duration getCurrentTime() {
int sampleNumber = sampleBuffer.position() / (isStereo ? 2 : 1);
return java.time.Duration.ofMillis((long) (sampleNumber * 1000 / sampleRate));
}
public float getTotalDuration() {
return totalDuration;
}
public int getTotalSamples() {
return totalSamples;
}
public long getSampleRate() {
return sampleRate;
}
}

View File

@@ -0,0 +1,134 @@
package jace.core;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import jace.core.SoundMixer.SoundBuffer;
import jace.core.SoundMixer.SoundError;
public class MediaPlayer {
double vol = 1.0;
int repeats = 0;
int maxRepetitions = 1;
Status status = Status.NOT_STARTED;
Media soundData;
SoundBuffer playbackBuffer;
Duration lastKnownDuration = Duration.ZERO;
Executor executor = Executors.newSingleThreadExecutor();
public static enum Status {
NOT_STARTED, PLAYING, PAUSED, STOPPED
}
public static final int INDEFINITE = -1;
public MediaPlayer(Media song) {
this.soundData = song;
}
public Status getStatus() {
return status;
}
public Duration getCurrentTime() {
if (soundData == null) {
return lastKnownDuration;
} else {
return soundData.getCurrentTime();
}
}
public double getVolume() {
return vol;
}
// NOTE: Once a song is stopped, it cannot be restarted.
public void stop() {
status = Status.STOPPED;
try {
if (playbackBuffer != null) {
playbackBuffer.flush();
playbackBuffer.shutdown();
playbackBuffer = null;
}
} catch (InterruptedException | ExecutionException | SoundError e) {
// Ignore exception on shutdown
} finally {
if (soundData != null) {
lastKnownDuration = soundData.getCurrentTime();
soundData.close();
}
soundData = null;
}
}
public void setCycleCount(int i) {
maxRepetitions = i;
}
public void setVolume(double d) {
vol = Math.max(0.0, Math.min(1.0, d));
}
public void setStartTime(javafx.util.Duration millis) {
soundData.seekToTime(millis);
}
public void pause() {
status = Status.PAUSED;
}
public void play() {
if (status == Status.STOPPED) {
return;
} else if (status == Status.NOT_STARTED) {
repeats = 0;
if (playbackBuffer == null || !playbackBuffer.isAlive()) {
try {
playbackBuffer = SoundMixer.createBuffer(true);
} catch (InterruptedException | ExecutionException | SoundError e) {
stop();
return;
}
if (playbackBuffer == null) {
stop();
return;
}
}
}
executor.execute(() -> {
SoundBuffer theBuffer = playbackBuffer;
status = Status.PLAYING;
// System.out.println("Song playback thread started");
Media theSoundData = soundData;
while (status == Status.PLAYING && (maxRepetitions == INDEFINITE || repeats < maxRepetitions) && theSoundData != null && theBuffer != null) {
if (theSoundData.isEnded()) {
if (maxRepetitions == INDEFINITE) {
theSoundData.restart();
} else {
repeats++;
if (repeats < maxRepetitions) {
theSoundData.restart();
} else {
System.out.println("Song ended");
this.stop();
break;
}
}
}
try {
theBuffer.playSample((short) (theSoundData.getNextLeftSample() * vol));
theBuffer.playSample((short) (theSoundData.getNextRightSample() * vol));
} catch (InterruptedException | ExecutionException | SoundError e) {
e.printStackTrace();
this.stop();
}
theSoundData = soundData;
theBuffer = playbackBuffer;
}
});
}
}

View File

@@ -1,32 +1,27 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import java.util.HashSet;
import jace.Emulator;
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
@@ -38,21 +33,20 @@ import java.util.logging.Logger;
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class Motherboard extends TimedDevice {
public class Motherboard extends IndependentTimedDevice {
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);
Emulator.withComputer(c->c.notifyVBLStateChanged(true));
}
void vblankStart() {
SoftSwitches.VBL.getSwitch().setState(false);
computer.notifyVBLStateChanged(false);
Emulator.withComputer(c->c.notifyVBLStateChanged(false));
}
/**
@@ -60,11 +54,14 @@ public class Motherboard extends TimedDevice {
* @param computer
* @param oldMotherboard
*/
public Motherboard(Computer computer, Motherboard oldMotherboard) {
super(computer);
public Motherboard(Motherboard oldMotherboard) {
super();
if (oldMotherboard != null) {
miscDevices.addAll(oldMotherboard.miscDevices);
addAllDevices(oldMotherboard.getChildren());
speaker = oldMotherboard.speaker;
accelorationRequestors.addAll(oldMotherboard.accelorationRequestors);
setSpeedInHz(oldMotherboard.getSpeedInHz());
setMaxSpeed(oldMotherboard.isMaxSpeed());
}
}
@@ -77,77 +74,45 @@ public class Motherboard extends TimedDevice {
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;
private CPU _cpu = null;
public CPU getCpu() {
if (_cpu == null) {
_cpu = Emulator.withComputer(Computer::getCpu, null);
}
return _cpu;
}
@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();
_cpu = null;
accelorationRequestors.clear();
disableTempMaxSpeed();
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 = new Speaker();
speaker.attach();
}
speaker.reconfigure();
addChildDevice(speaker);
} catch (Throwable t) {
System.out.println("Unable to initalize sound -- deactivating speaker out");
speaker.detach();
miscDevices.remove(speaker);
t.printStackTrace();
}
} 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<>();
HashSet<Object> accelorationRequestors = new HashSet<>();
public void requestSpeed(Object requester) {
accelorationRequestors.add(requester);
@@ -155,55 +120,8 @@ public class Motherboard extends TimedDevice {
}
public void cancelSpeedRequest(Object requester) {
accelorationRequestors.remove(requester);
if (accelorationRequestors.isEmpty()) {
if (accelorationRequestors.remove(requester) && 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

@@ -1,26 +1,25 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import java.util.Arrays;
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
@@ -41,9 +40,9 @@ public class PagedMemory {
FIRMWARE_80COL(0x0c300),
SLOW_ROM(0x0c100),
RAM(0x0000);
protected int baseAddress;
int baseAddress;
private Type(int newBase) {
Type(int newBase) {
baseAddress = newBase;
}
@@ -59,10 +58,10 @@ public class PagedMemory {
/**
* Creates a new instance of PagedMemory
* @param size The size of the memory region, in multiples of 256
* @param memType The type of the memory region
*/
Computer computer;
public PagedMemory(int size, Type memType, Computer computer) {
this.computer = computer;
public PagedMemory(int size, Type memType) {
type = memType;
internalMemory = new byte[size >> 8][256];
for (int i = 0; i < size; i += 256) {
@@ -77,12 +76,10 @@ public class PagedMemory {
loadData(romData);
}
public void loadData(byte[] romData) {
public final 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];
}
System.arraycopy(romData, i, b, 0, 256);
internalMemory[i >> 8] = b;
}
}
@@ -111,9 +108,7 @@ public class PagedMemory {
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];
}
@@ -129,7 +124,7 @@ public class PagedMemory {
public void writeByte(int address, byte value) {
byte[] page = getMemoryPage(address);
StateManager.markDirtyValue(page, computer);
StateManager.markDirtyValue(page);
getMemoryPage(address)[address & 0x0ff] = value;
}
@@ -137,10 +132,10 @@ public class PagedMemory {
byte[][] sourceMemory = source.getMemory();
int sourceBase = source.type.getBaseAddress() >> 8;
int thisBase = type.getBaseAddress() >> 8;
int start = sourceBase > thisBase ? sourceBase : thisBase;
int start = Math.max(sourceBase, thisBase);
int sourceEnd = sourceBase + source.getMemory().length;
int thisEnd = thisBase + getMemory().length;
int end = sourceEnd < thisEnd ? sourceEnd : thisEnd;
int end = Math.min(sourceEnd, thisEnd);
for (int i = start; i < end; i++) {
set(i - thisBase, sourceMemory[i - sourceBase]);
}

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import javafx.scene.paint.Color;

View File

@@ -1,29 +1,33 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import jace.Emulator;
import jace.apple2e.SoftSwitches;
import jace.config.Reconfigurable;
/**
* RAM is a 64K address space of paged memory. It also manages sets of memory
@@ -37,23 +41,22 @@ 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;
private final Set<RAMListener> listeners = new ConcurrentSkipListSet<>();
@SuppressWarnings("unchecked")
private final Set<RAMListener>[] listenerMap = (Set<RAMListener>[]) new Set[256];
@SuppressWarnings("unchecked")
private final Set<RAMListener>[] ioListenerMap = (Set<RAMListener>[]) new Set[256];
@SuppressWarnings("unchecked")
public Optional<Card>[] cards = (Optional<Card>[]) new Optional[8];
// 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];
public RAM() {
for (int i = 0; i < 8; i++) {
cards[i] = Optional.empty();
}
@@ -89,11 +92,10 @@ public abstract class RAM implements Reconfigurable {
cards[slot] = Optional.of(c);
c.setSlot(slot);
c.attach();
configureActiveMemory();
}
public void removeCard(Card c) {
c.suspend();
c.detach();
removeCard(c.getSlot());
}
@@ -104,6 +106,13 @@ public abstract class RAM implements Reconfigurable {
}
abstract public void configureActiveMemory();
public void copyFrom(RAM other) {
cards = other.cards;
activeSlot = other.activeSlot;
listeners.addAll(other.listeners);
refreshListenerMap();
configureActiveMemory();
}
public void write(int address, byte b, boolean generateEvent, boolean requireSynchronization) {
byte[] page = activeWrite.getMemoryPage(address);
@@ -124,9 +133,9 @@ public abstract class RAM implements Reconfigurable {
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);
write(address + 1, (byte) ((w >> 8) & 0x0ff), generateEvent, requireSynchronization);
}
public byte readRaw(int address) {
// if (address >= 65536) return 0;
return activeRead.getMemoryPage(address)[address & 0x0FF];
@@ -136,7 +145,8 @@ public abstract class RAM implements Reconfigurable {
// if (address >= 65536) return 0;
byte value = activeRead.getMemoryPage(address)[address & 0x0FF];
// if (triggerEvent || ((address & 0x0FF00) == 0x0C000)) {
if (triggerEvent || (address & 0x0FFF0) == 0x0c030) {
// if (triggerEvent || (address & 0x0FFF0) == 0x0c030) {
if (triggerEvent) {
value = callListener(eventType, address, value, value, requireSyncronization);
}
return value;
@@ -151,31 +161,26 @@ public abstract class RAM implements Reconfigurable {
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;
return msb + lsb;
}
private void mapListener(RAMListener l, int address) {
private synchronized void mapListener(RAMListener l, int address) {
if ((address & 0x0FF00) == 0x0C000) {
int index = address & 0x0FF;
List<RAMListener> ioListeners = ioListenerMap[index];
Set<RAMListener> ioListeners = ioListenerMap[index];
if (ioListeners == null) {
ioListeners = new ArrayList<>();
ioListeners = new ConcurrentSkipListSet<>();
ioListenerMap[index] = ioListeners;
}
if (!ioListeners.contains(l)) {
ioListeners.add(l);
}
ioListeners.add(l);
} else {
int index = address >> 8;
List<RAMListener> otherListeners = listenerMap[index];
int index = (address >> 8) & 0x0FF;
Set<RAMListener> otherListeners = listenerMap[index];
if (otherListeners == null) {
otherListeners = new ArrayList<>();
otherListeners = new ConcurrentSkipListSet<>();
listenerMap[index] = otherListeners;
}
if (!otherListeners.contains(l)) {
otherListeners.add(l);
}
otherListeners.add(l);
}
}
@@ -183,11 +188,11 @@ public abstract class RAM implements Reconfigurable {
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();
int start = l.getScopeStart();
int end = l.getScopeEnd();
if (l.getScope() == RAMEvent.SCOPE.ANY) {
start = 0;
end = 0x0FFFF;
}
for (int i = start; i <= end; i++) {
mapListener(l, i);
@@ -196,145 +201,189 @@ public abstract class RAM implements Reconfigurable {
}
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;
// Wipe out existing maps
for (int i = 0; i < 256; i++) {
listenerMap[i] = null;
ioListenerMap[i] = null;
}
return true;
listeners.forEach(this::addListenerRange);
}
public RAMListener observeOnce(String observerationName, RAMEvent.TYPE type, int address, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(observerationName, type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(address);
}
@Override
protected void doEvent(RAMEvent e) {
handler.handleEvent(e);
unregister();
}
});
}
public RAMListener observe(String observerationName, RAMEvent.TYPE type, int address, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(observerationName, 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(String observerationName, RAMEvent.TYPE type, int address, Boolean auxFlag, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(observerationName, 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(String observerationName, RAMEvent.TYPE type, int addressStart, int addressEnd, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(observerationName, 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(String observerationName, RAMEvent.TYPE type, int addressStart, int addressEnd, Boolean auxFlag, RAMEvent.RAMEventHandler handler) {
return addListener(new RAMListener(observerationName, 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) {
return SoftSwitches.AUXZP.getState() == auxFlag;
} else if (e.getAddress() >= 0x0C000 && e.getAddress() <= 0x0CFFF) {
// I/O page doesn't care about the aux flag
return true;
} else return auxFlag == null || SoftSwitches.RAMRD.getState() == auxFlag;
}
public RAMListener addListener(final RAMListener l) {
boolean restart = computer.pause();
if (listeners.contains(l)) {
if (l == null) {
return l;
}
listeners.add(l);
addListenerRange(l);
if (restart) {
computer.resume();
if (listeners.contains(l)) {
removeListener(l);
}
listeners.add(l);
Emulator.whileSuspended((c)->addListenerRange(l));
return l;
}
public RAMListener addExecutionTrap(String observerationName, int address, Consumer<RAMEvent> handler) {
RAMListener listener = new RAMListener(observerationName, RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(address);
}
@Override
protected void doEvent(RAMEvent e) {
handler.accept(e);
}
};
addListener(listener);
return listener;
}
public void removeListener(final RAMListener l) {
boolean restart = computer.pause();
listeners.remove(l);
refreshListenerMap();
if (restart) {
computer.resume();
if (l == null) {
return;
}
if (!listeners.contains(l)) {
return;
}
listeners.remove(l);
Emulator.whileSuspended(c->refreshListenerMap());
}
public byte callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue, boolean requireSyncronization) {
List<RAMListener> activeListeners;
if (requireSyncronization) {
computer.getCpu().suspend();
}
private byte _callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue) {
Set<RAMListener> activeListeners;
if ((address & 0x0FF00) == 0x0C000) {
activeListeners = ioListenerMap[address & 0x0FF];
if (activeListeners == null && t.isRead()) {
if (requireSyncronization) {
computer.getCpu().resume();
}
return computer.getVideo().getFloatingBus();
return Emulator.withComputer(c->c.getVideo().getFloatingBus(), (byte) 0);
}
} else {
activeListeners = listenerMap[(address >> 8) & 0x0ff];
}
if (activeListeners != null) {
if (activeListeners != null && !activeListeners.isEmpty()) {
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();
activeListeners.forEach((l) -> l.handleEvent(e));
if (!e.isIntercepted() && (address & 0x0FF00) == 0x0C000) {
return Emulator.withComputer(c->c.getVideo().getFloatingBus(), (byte) 0);
}
return (byte) e.getNewValue();
}
if (requireSyncronization) {
computer.getCpu().resume();
}
return (byte) newValue;
}
public byte callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue, boolean requireSyncronization) {
if (requireSyncronization && Emulator.withComputer(c->c.getMotherboard().isDeviceThread(), false)) {
AtomicInteger returnValue = new AtomicInteger();
Emulator.withComputer(computer -> {
computer.getCpu().whileSuspended(()-> returnValue.set(_callListener(t, address, oldValue, newValue)));
_callListener(t, address, oldValue, newValue);
});
return (byte) returnValue.get();
} else {
return _callListener(t, address, oldValue, newValue);
}
}
abstract protected void loadRom(String path) throws IOException;
abstract public void attach();
abstract public void detach();
public void detach() {
detachListeners.forEach(Runnable::run);
}
abstract public void performExtendedCommand(int i);
abstract public String getState();
abstract public void resetState();
List<Runnable> detachListeners = new ArrayList<>();
public void onDetach(Runnable r) {
detachListeners.add(r);
}
}

View File

@@ -1,23 +1,23 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.apple2e.SoftSwitches;
/**
* 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
@@ -31,19 +31,19 @@ package jace.core;
*/
public class RAMEvent {
static public interface RAMEventHandler {
public void handleEvent(RAMEvent e);
public interface RAMEventHandler {
void handleEvent(RAMEvent e);
}
public enum TYPE {
READ(true),
READ_DATA(true),
EXECUTE(true),
READ_OPERAND(true),
EXECUTE(true),
WRITE(false),
ANY(false);
boolean read = false;
boolean read;
TYPE(boolean r) {
this.read = r;
@@ -52,14 +52,14 @@ public class RAMEvent {
public boolean isRead() {
return read;
}
};
}
public enum SCOPE {
ADDRESS,
RANGE,
ANY
};
}
public enum VALUE {
@@ -68,11 +68,13 @@ public class RAMEvent {
EQUALS,
NOT_EQUALS,
CHANGE_BY
};
}
private TYPE type;
private SCOPE scope;
private VALUE value;
private int address, oldValue, newValue;
private boolean valueIntercepted = false;
/**
* Creates a new instance of RAMEvent
@@ -97,6 +99,9 @@ public class RAMEvent {
}
public final void setType(TYPE type) {
if (type == TYPE.ANY) {
throw new RuntimeException("Event type=Any is reserved for listeners, not for triggering events!");
}
this.type = type;
}
@@ -138,5 +143,27 @@ public class RAMEvent {
public final void setNewValue(int newValue) {
this.newValue = newValue;
valueIntercepted = true;
}
public final boolean isIntercepted() {
return valueIntercepted;
}
public boolean isMainMemory() {
if (type.isRead() && SoftSwitches.RAMRD.isOn()) {
return false;
} else if (!type.isRead() && SoftSwitches.RAMWRT.isOn()) {
return false;
} else if (address < 0x0200) {
// Check if zero page is pointed to auxiliary memory
return SoftSwitches.AUXZP.isOff();
}
if ((address >= 0x400 && address < 0x0800) || (address >= 0x2000 && address < 0x4000)) {
if (SoftSwitches._80STORE.isOn() && SoftSwitches.PAGE2.isOn()) {
return false;
}
}
return true;
}
}

View File

@@ -1,23 +1,22 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.Emulator;
import jace.core.RAMEvent.TYPE;
/**
@@ -29,7 +28,7 @@ import jace.core.RAMEvent.TYPE;
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class RAMListener implements RAMEvent.RAMEventHandler {
public abstract class RAMListener implements RAMEvent.RAMEventHandler, Comparable<RAMListener> {
private RAMEvent.TYPE type;
private RAMEvent.SCOPE scope;
@@ -39,20 +38,27 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler {
private int valueStart;
private int valueEnd;
private int valueAmount;
private String name;
/**
* Creates a new instance of RAMListener
* @param name
* @param t
* @param s
* @param v
*/
public RAMListener(RAMEvent.TYPE t, RAMEvent.SCOPE s, RAMEvent.VALUE v) {
public RAMListener(String name, RAMEvent.TYPE t, RAMEvent.SCOPE s, RAMEvent.VALUE v) {
setName(name);
setType(t);
setScope(s);
setValue(v);
doConfig();
}
public void unregister() {
Emulator.withMemory(m -> m.removeListener(this));
}
public RAMEvent.TYPE getType() {
return type;
}
@@ -77,6 +83,14 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScopeStart() {
return scopeStart;
}
@@ -118,18 +132,6 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler {
}
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) {
@@ -139,6 +141,11 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler {
}
}
// Skip event if it's not the right type
if (!(type == TYPE.ANY || type == e.getType() || (type == TYPE.READ && e.getType().isRead()))) {
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) {
@@ -147,9 +154,7 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler {
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;
}
} else return value != RAMEvent.VALUE.RANGE || (e.getNewValue() >= valueStart && e.getNewValue() <= valueEnd);
}
// Ok, so we've filtered out the uninteresting stuff
@@ -158,7 +163,7 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler {
}
@Override
public void handleEvent(RAMEvent e) {
public final void handleEvent(RAMEvent e) {
if (isRelevant(e)) {
doEvent(e);
}
@@ -167,4 +172,39 @@ public abstract class RAMListener implements RAMEvent.RAMEventHandler {
abstract protected void doConfig();
abstract protected void doEvent(RAMEvent e);
@Override
public int compareTo(RAMListener o) {
if (o.name.equals(name)) {
if (o.scopeStart == scopeStart) {
if (o.scopeEnd == scopeEnd) {
if (o.type == type) {
// Ignore hash codes -- combination of name, address range and type should identify similar listeners.
return (int) 0;
} else {
return Integer.compare(o.type.ordinal(), type.ordinal());
}
} else {
return Integer.compare(o.scopeEnd, scopeEnd);
}
} else {
return Integer.compare(o.scopeStart, scopeStart);
}
} else {
return o.name.compareTo(name);
}
}
/**
*
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (o == null || ! (o instanceof RAMListener) ) {
return false;
}
return this.compareTo((RAMListener) o) == 0;
}
}

View File

@@ -1,28 +1,28 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.state.Stateful;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import jace.Emulator;
import jace.state.Stateful;
/**
* 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
@@ -40,14 +40,13 @@ public abstract class SoftSwitch {
@Stateful
public Boolean state;
private Boolean initalState;
private List<RAMListener> listeners;
private final Boolean initalState;
private final 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 final String name;
private boolean toggleType = false;
protected Computer computer;
/**
* Creates a new instance of SoftSwitch
@@ -105,7 +104,7 @@ public abstract class SoftSwitch {
exclusionActivate.add(i);
}
}
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
RAMListener l = new RAMListener("Softswitch toggle " + name, changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
@@ -135,7 +134,7 @@ public abstract class SoftSwitch {
exclusionActivate.add(i);
}
}
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
RAMListener l = new RAMListener("Softswitch on " + name, changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
@@ -145,10 +144,10 @@ public abstract class SoftSwitch {
@Override
protected void doEvent(RAMEvent e) {
if (e.getType().isRead()) {
e.setNewValue(computer.getVideo().getFloatingBus());
e.setNewValue(Emulator.withComputer(c->c.getVideo().getFloatingBus(), (byte) 0));
}
if (!exclusionActivate.contains(e.getAddress())) {
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
setState(true);
}
}
@@ -168,7 +167,7 @@ public abstract class SoftSwitch {
exclusionDeactivate.add(i);
}
}
RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
RAMListener l = new RAMListener("Softswitch off " + name, changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
@@ -179,7 +178,7 @@ public abstract class SoftSwitch {
protected void doEvent(RAMEvent e) {
if (!exclusionDeactivate.contains(e.getAddress())) {
setState(false);
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" disables switch "+getName());
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" disables switch "+getName());
}
}
};
@@ -200,7 +199,7 @@ public abstract class SoftSwitch {
}
}
// RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
RAMListener l = new RAMListener(RAMEvent.TYPE.READ, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
RAMListener l = new RAMListener("Softswitch read state " + name, RAMEvent.TYPE.READ, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(beginAddr);
@@ -239,39 +238,23 @@ public abstract class SoftSwitch {
}
}
public void register(Computer computer) {
this.computer = computer;
RAM m = computer.getMemory();
listeners.stream().forEach((l) -> {
m.addListener(l);
public void register() {
Emulator.withMemory(m -> {
listeners.forEach(m::addListener);
});
}
public void unregister() {
RAM m = computer.getMemory();
listeners.stream().forEach((l) -> {
m.removeListener(l);
Emulator.withMemory(m -> {
listeners.forEach(m::removeListener);
});
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();
}

View File

@@ -1,43 +1,43 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.config.ConfigurableField;
import jace.config.DynamicSelection;
import jace.config.Reconfigurable;
import java.nio.BufferOverflowException;
import java.nio.ShortBuffer;
import java.util.ArrayList;
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.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
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;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.ALC;
import org.lwjgl.openal.ALC10;
import org.lwjgl.openal.ALCCapabilities;
import org.lwjgl.openal.ALCapabilities;
import jace.Emulator;
import jace.config.ConfigurableField;
/**
* Manages sound resources used by various audio devices (such as speaker and
@@ -49,54 +49,310 @@ import javax.sound.sampled.SourceDataLine;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public class SoundMixer extends Device {
public static boolean DEBUG_SOUND = false;
private final Set<SourceDataLine> availableLines = Collections.synchronizedSet(new HashSet<>());
private final Map<Object, SourceDataLine> activeLines = Collections.synchronizedMap(new HashMap<>());
/**
* Bits per sample
* Making this configurable requires too much effort and not a lot of benefit
*/
@ConfigurableField(name = "Bits per sample", shortName = "bits")
// @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;
public static int RATE = 44100;
@ConfigurableField(name = "Mute", shortName = "mute")
public static boolean MUTE = false;
@ConfigurableField(name = "Buffer size", shortName = "buffer")
//public static int BUFFER_SIZE = 1024; Ok on MacOS but choppy on Windows!
public static int BUFFER_SIZE = 2048;
public static boolean PLAYBACK_ENABLED = false;
// Innocent until proven guilty by a failed initialization
public static boolean PLAYBACK_DRIVER_DETECTED = true;
public static boolean PLAYBACK_INITIALIZED = 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;
private static String defaultDeviceName;
private static long audioDevice = -1;
private static long audioContext = -1;
private static ALCCapabilities audioCapabilities;
private static ALCapabilities audioLibCapabilities;
// In case the OpenAL implementation wants to be run in a single thread, use a single thread executor
protected static ExecutorService soundThreadExecutor = Executors.newSingleThreadExecutor();
public SoundMixer() {
if (!Utility.isHeadlessMode()) {
defaultDeviceName = ALC10.alcGetString(0, ALC10.ALC_DEFAULT_DEVICE_SPECIFIER);
}
}
@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());
public static class SoundError extends Exception {
private static final long serialVersionUID = 1L;
public SoundError(String message) {
super(message);
}
}
public static <T> T performSoundFunction(Callable<T> operation, String action) throws SoundError {
return performSoundFunction(operation, action, false);
}
public static <T> T performSoundFunction(Callable<T> operation, String action, boolean ignoreError) throws SoundError {
Future<T> result = soundThreadExecutor.submit(operation);
try {
Future<Integer> error = soundThreadExecutor.submit(AL10::alGetError);
int err;
err = error.get();
if (!ignoreError && DEBUG_SOUND) {
if (err != AL10.AL_NO_ERROR) {
System.err.println(">>>SOUND ERROR " + AL10.alGetString(err) + " when performing action: " + action);
// throw new SoundError(AL10.alGetString(err));
}
}
return out;
return result.get();
} catch (ExecutionException e) {
System.out.println("Error when executing sound action: " + e.getMessage());
e.printStackTrace();
} catch (InterruptedException e) {
// Do nothing: sound is probably being reset
}
};
private Mixer theMixer;
return null;
}
public SoundMixer(Computer computer) {
super(computer);
public static void performSoundOperation(Runnable operation, String action) throws SoundError {
performSoundOperation(operation, action, false);
}
public static void performSoundOperation(Runnable operation, String action, boolean ignoreError) throws SoundError {
performSoundFunction(()->{
operation.run();
return null;
}, action, ignoreError);
}
public static void performSoundOperationAsync(Runnable operation, String action) {
soundThreadExecutor.submit(operation, action);
}
protected static void initSound() {
if (Utility.isHeadlessMode()) {
return;
}
try {
performSoundOperation(()->{
if (!PLAYBACK_INITIALIZED) {
audioDevice = ALC10.alcOpenDevice(defaultDeviceName);
audioContext = ALC10.alcCreateContext(audioDevice, new int[]{0});
ALC10.alcMakeContextCurrent(audioContext);
audioCapabilities = ALC.createCapabilities(audioDevice);
audioLibCapabilities = AL.createCapabilities(audioCapabilities);
if (!audioLibCapabilities.OpenAL10) {
PLAYBACK_DRIVER_DETECTED = false;
Logger.getLogger(SoundMixer.class.getName()).warning("OpenAL 1.0 not supported");
Emulator.withComputer(c->c.mixer.detach());
}
PLAYBACK_INITIALIZED = true;
} else {
ALC10.alcMakeContextCurrent(audioContext);
}
}, "Initalize audio device");
} catch (SoundError e) {
PLAYBACK_DRIVER_DETECTED = false;
Logger.getLogger(SoundMixer.class.getName()).warning("Error when initializing sound: " + e.getMessage());
Emulator.withComputer(c->c.mixer.detach());
}
}
// Lots of inspiration from https://www.youtube.com/watch?v=dLrqBTeipwg
@Override
public void attach() {
if (Utility.isHeadlessMode()) {
return;
}
if (!PLAYBACK_DRIVER_DETECTED) {
Logger.getLogger(SoundMixer.class.getName()).warning("Sound driver not detected");
return;
}
super.attach();
initSound();
PLAYBACK_ENABLED = true;
}
private static List<SoundBuffer> buffers = Collections.synchronizedList(new ArrayList<>());
public static SoundBuffer createBuffer(boolean stereo) throws InterruptedException, ExecutionException, SoundError {
if (!PLAYBACK_ENABLED) {
System.err.println("Sound playback not enabled, buffer not created.");
return null;
}
SoundBuffer buffer = new SoundBuffer(stereo);
buffers.add(buffer);
return buffer;
}
public static class SoundBuffer {
public static int MAX_BUFFER_ID;
private ShortBuffer currentBuffer;
private ShortBuffer alternateBuffer;
private int audioFormat;
private int currentBufferId;
private int alternateBufferId;
private int sourceId;
private boolean isAlive;
private int buffersGenerated = 0;
public SoundBuffer(boolean stereo) throws InterruptedException, ExecutionException, SoundError {
initSound();
currentBuffer = BufferUtils.createShortBuffer(BUFFER_SIZE * (stereo ? 2 : 1));
alternateBuffer = BufferUtils.createShortBuffer(BUFFER_SIZE * (stereo ? 2 : 1));
try {
currentBufferId = performSoundFunction(AL10::alGenBuffers, "Initalize sound buffer: primary");
alternateBufferId = performSoundFunction(AL10::alGenBuffers, "Initalize sound buffer: alternate");
boolean hasSource = false;
while (!hasSource) {
sourceId = performSoundFunction(AL10::alGenSources, "Initalize sound buffer: create source");
hasSource = performSoundFunction(()->AL10.alIsSource(sourceId), "Initalize sound buffer: Check if source is valid");
}
performSoundOperation(()->AL10.alSourcei(sourceId, AL10.AL_LOOPING, AL10.AL_FALSE), "Set looping to false");
} catch (SoundError e) {
Logger.getLogger(SoundMixer.class.getName()).warning("Error when creating sound buffer: " + e.getMessage());
Thread.dumpStack();
shutdown();
throw e;
}
audioFormat = stereo ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_MONO16;
isAlive = true;
}
public boolean isAlive() {
return isAlive;
}
/* If stereo, call this once for left and then again for right sample */
public void playSample(short sample) throws InterruptedException, ExecutionException, SoundError {
if (!isAlive) {
return;
}
if (!currentBuffer.hasRemaining()) {
this.flush();
}
try {
currentBuffer.put(sample);
} catch (BufferOverflowException e) {
if (DEBUG_SOUND) {
System.err.println("Buffer overflow, trying to compensate");
}
currentBuffer.clear();
currentBuffer.put(sample);
}
}
public void shutdown() throws InterruptedException, ExecutionException, SoundError {
if (!isAlive) {
return;
}
isAlive = false;
try {
performSoundOperation(()->{if (AL10.alIsSource(sourceId)) AL10.alSourceStop(sourceId);}, "Shutdown: stop source");
} finally {
try {
performSoundOperation(()->{if (AL10.alIsSource(sourceId)) AL10.alDeleteSources(sourceId);}, "Shutdown: delete source");
} finally {
sourceId = -1;
try {
performSoundOperation(()->{if (AL10.alIsBuffer(alternateBufferId)) AL10.alDeleteBuffers(alternateBufferId);}, "Shutdown: delete buffer 1");
} finally {
alternateBufferId = -1;
try {
performSoundOperation(()->{if (AL10.alIsBuffer(currentBufferId)) AL10.alDeleteBuffers(currentBufferId);}, "Shutdown: delete buffer 2");
} finally {
currentBufferId = -1;
buffers.remove(this);
}
}
}
}
}
public void flush() throws SoundError {
buffersGenerated++;
if (buffersGenerated > 2) {
int[] unqueueBuffers = new int[]{currentBufferId};
performSoundOperation(()->{
int buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
while (buffersProcessed < 1) {
Thread.onSpinWait();
buffersProcessed = AL10.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED);
}
}, "Flush: wait for buffers to finish playing");
if (!isAlive) {
return;
}
// TODO: Figure out why we get Invalid Value on a new buffer
performSoundOperation(()->AL10.alSourceUnqueueBuffers(sourceId, unqueueBuffers), "Flush: unqueue buffers");
}
if (!isAlive) {
return;
}
// TODO: Figure out why we get Invalid Operation error on a new buffer after unqueue reports Invalid Value
currentBuffer.flip();
performSoundOperation(()->AL10.alBufferData(currentBufferId, audioFormat, currentBuffer, RATE), "Flush: buffer data");
currentBuffer.clear();
if (!isAlive) {
return;
}
// TODO: Figure out why we get Invalid Operation error on a new buffer after unqueue reports Invalid Value
performSoundOperation(()->AL10.alSourceQueueBuffers(sourceId, currentBufferId), "Flush: queue buffer");
performSoundOperationAsync(()->{
if (AL10.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
AL10.alSourcePlay(sourceId);
}
}, "Flush: Start playing buffer");
// Swap AL buffers
int tempId = currentBufferId;
currentBufferId = alternateBufferId;
alternateBufferId = tempId;
// Swap Java buffers
ShortBuffer tempBuffer = currentBuffer;
currentBuffer = alternateBuffer;
alternateBuffer = tempBuffer;
}
}
public int getActiveBuffers() {
return buffers.size();
}
@Override
public void detach() {
if (!PLAYBACK_ENABLED) {
return;
}
MUTE = true;
PLAYBACK_ENABLED = false;
while (!buffers.isEmpty()) {
SoundBuffer buffer = buffers.remove(0);
try {
buffer.shutdown();
} catch (InterruptedException | ExecutionException | SoundError e) {
Logger.getLogger(SoundMixer.class.getName()).warning("Error when detaching sound mixer: " + e.getMessage());
}
}
buffers.clear();
PLAYBACK_INITIALIZED = false;
try {
performSoundOperation(()->ALC10.alcDestroyContext(audioContext), "Detach: destroy context", true);
performSoundOperation(()->ALC10.alcCloseDevice(audioDevice), "Detach: close device", true);
} catch (SoundError e) {
// Shouldn't throw but have to catch anyway
}
super.detach();
}
@Override
public String getDeviceName() {
return "Sound Output";
}
@@ -108,173 +364,15 @@ public class SoundMixer extends Device {
@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);
}
PLAYBACK_ENABLED = PLAYBACK_DRIVER_DETECTED && !MUTE;
if (PLAYBACK_ENABLED) {
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);
detach();
}
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

@@ -1,131 +1,158 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
* Copyright (C) 2024 Brendan Robert brendan.robert@gmail.com.
* *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.
* 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.
*
* This basic implementation does not run freely and instead will skip cycles if it is running too fast
* This allows a parent timer to run at a faster rate without causing this device to do the same.
* Useful for devices which generate sound or video at a specific rate.
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
public abstract class TimedDevice extends Device {
// From the holy word of Sather 3:5 (Table 3.1) :-)
// This average speed averages in the "long" cycles
public static final long NTSC_1MHZ = 1020484L;
public static final long PAL_1MHZ = 1015625L;
public static final long SYNC_FREQ_HZ = 60;
public static final double NANOS_PER_SECOND = 1000000000.0;
public static final long NANOS_PER_MILLISECOND = 1000000L;
public static final long SYNC_SLOP = NANOS_PER_MILLISECOND * 10L; // 10ms slop for synchronization
public static int TEMP_SPEED_MAX_DURATION = 1000000;
@ConfigurableField(name = "Speed", description = "(Percentage)")
public int speedRatio = 100;
@ConfigurableField(name = "Max speed")
public boolean forceMaxspeed = false;
public boolean maxspeed = false;
private long cyclesPerSecond = defaultCyclesPerSecond();
private int cycleTimer = 0;
private int tempSpeedDuration = 0;
private long nanosPerInterval; // How long to wait between pauses
private long cyclesPerInterval; // How many cycles to wait until a pause interval
private long nextSync = System.nanoTime(); // When is the next sync interval supposed to finish?
protected Runnable unthrottledTick = () -> super.doTick();
Long waitUntil = null;
protected Runnable throttledTick = () -> {
if (waitUntil == null || System.nanoTime() >= waitUntil) {
super.doTick();
waitUntil = calculateResyncDelay();
}
};
protected final Runnable tickHandler;
/**
* Creates a new instance of TimedDevice
* @param computer
* Creates a new instance of TimedDevice, setting default speed
* Protected as overriding the tick handler should only done
* for the independent timed device
*/
public TimedDevice(Computer computer) {
super(computer);
setSpeed(cyclesPerSecond);
protected TimedDevice(boolean throttleUsingTicks) {
super();
setSpeedInHz(defaultCyclesPerSecond());
tickHandler = throttleUsingTicks ? throttledTick : unthrottledTick;
resetSyncTimer();
}
@ConfigurableField(name = "Speed", description = "(in hertz)")
public long cyclesPerSecond = defaultCyclesPerSecond();
@ConfigurableField(name = "Max speed")
public boolean maxspeed = false;
public TimedDevice() {
this(true);
}
@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;
public final void doTick() {
tickHandler.run();
}
public final void resetSyncTimer() {
nextSync = System.nanoTime() + nanosPerInterval;
cycleTimer = 0;
}
@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;
return super.suspend();
}
Thread timerThread;
public boolean pause() {
if (!isRunning()) {
return false;
@Override
public void setPaused(boolean paused) {
if (!isPaused() && paused) {
pauseStart();
}
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) {
super.setPaused(paused);
if (!paused) {
resetSyncTimer();
}
return true;
}
protected void pauseStart() {
// Override if you need to add a pause behavior
}
@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;
setPaused(false);
resetSyncTimer();
}
long skip = 0;
long wait = 0;
public final void resetSyncTimer() {
nextSync = System.nanoTime() + nanosPerInterval;
cycleTimer = 0;
public final int getSpeedRatio() {
return speedRatio;
}
public final void setMaxSpeed(boolean enabled) {
maxspeed = enabled;
if (!enabled) {
resetSyncTimer();
}
}
public final boolean isMaxSpeedEnabled() {
return maxspeed;
}
public final boolean isMaxSpeed() {
return forceMaxspeed || maxspeed;
}
public final long getSpeedInHz() {
return cyclesPerSecond;
}
public final void setSpeedInHz(long newSpeed) {
// System.out.println("Raw set speed for " + getName() + " to " + cyclesPerSecond + "hz");
// Thread.dumpStack();
cyclesPerSecond = newSpeed;
speedRatio = (int) Math.round(cyclesPerSecond * 100.0 / defaultCyclesPerSecond());
cyclesPerInterval = cyclesPerSecond / SYNC_FREQ_HZ;
nanosPerInterval = (long) (cyclesPerInterval * NANOS_PER_SECOND / cyclesPerSecond);
// System.out.println("Will pause " + nanosPerInterval + " nanos every " + cyclesPerInterval + " cycles");
resetSyncTimer();
}
public final void setSpeedInPercentage(int ratio) {
cyclesPerSecond = defaultCyclesPerSecond() * ratio / 100;
if (cyclesPerSecond == 0) {
cyclesPerSecond = defaultCyclesPerSecond();
}
setSpeedInHz(cyclesPerSecond);
}
public void enableTempMaxSpeed() {
@@ -137,44 +164,41 @@ public abstract class TimedDevice extends Device {
resetSyncTimer();
}
protected void resync() {
if (++cycleTimer >= cyclesPerInterval) {
if (maxspeed || tempSpeedDuration > 0) {
if (tempSpeedDuration > 0) {
tempSpeedDuration -= cyclesPerInterval;
}
resetSyncTimer();
return;
}
long now = System.nanoTime();
if (now < nextSync) {
cycleTimer = 0;
long currentSyncDiff = nextSync - now;
// Don't bother resynchronizing unless we're off by 10ms
if (currentSyncDiff > 10000000L) {
try {
// System.out.println("Sleeping for " + currentSyncDiff / 1000000 + " milliseconds");
Thread.sleep(currentSyncDiff / 1000000L, (int) (currentSyncDiff % 1000000L));
} catch (InterruptedException ex) {
System.err.println(getDeviceName() + " was trying to sleep for " + (currentSyncDiff / 1000000) + " millis but was woken up");
// Logger.getLogger(TimedDevice.class.getName()).log(Level.SEVERE, null, ex);
}
} else {
// System.out.println("Sleeping for " + currentSyncDiff + " nanoseconds");
// LockSupport.parkNanos(currentSyncDiff);
}
}
nextSync += nanosPerInterval;
}
}
@Override
public void reconfigure() {
if (cyclesPerSecond == 0) {
cyclesPerSecond = defaultCyclesPerSecond();
}
setSpeed(cyclesPerSecond);
resetSyncTimer();
}
public abstract long defaultCyclesPerSecond();
}
public long defaultCyclesPerSecond() {
return NTSC_1MHZ;
}
private boolean useParentTiming() {
if (getParent() != null && getParent() instanceof TimedDevice) {
TimedDevice pd = (TimedDevice) getParent();
if (pd.useParentTiming() || (!pd.isMaxSpeed() && pd.getSpeedInHz() <= getSpeedInHz())) {
return true;
}
}
return false;
}
protected Long calculateResyncDelay() {
if (++cycleTimer < cyclesPerInterval) {
return null;
}
cycleTimer = 0;
long retVal = nextSync;
nextSync = Math.max(nextSync, System.nanoTime()) + nanosPerInterval;
if (isMaxSpeed() || useParentTiming()) {
if (tempSpeedDuration > 0) {
tempSpeedDuration -= cyclesPerInterval;
if (tempSpeedDuration <= 0) {
disableTempMaxSpeed();
}
}
return null;
}
return retVal;
}
}

View File

@@ -1,29 +1,23 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
@@ -32,19 +26,22 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.function.Function;
import jace.config.InvokableAction;
import jace.config.InvokableActionRegistry;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar.ButtonData;
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.
@@ -52,10 +49,6 @@ import javafx.scene.paint.Color;
* @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
/**
@@ -65,11 +58,11 @@ public class Utility {
*
* @param s
* @param t
* @return Distance (higher is better)
* @return Distance (lower means a closer match, zero is identical)
*/
public static int levenshteinDistance(String s, String t) {
if (s == null || t == null || s.length() == 0 || t.length() == 0) {
return -1;
return Integer.MAX_VALUE;
}
s = s.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", "");
@@ -95,7 +88,18 @@ public class Utility {
}
}
}
return Math.max(m, n) - dist[m][n];
return dist[m][n];
}
/**
* Normalize distance based on longest string
*
* @param s
* @param t
* @return Similarity ranking, higher is better
*/
public static int adjustedLevenshteinDistance(String s, String t) {
return Math.max(s.length(), t.length()) - levenshteinDistance(s, t);
}
/**
@@ -107,7 +111,7 @@ public class Utility {
* @param c1
* @param c2
* @param width Search window size
* @return Overall similarity score (higher is beter)
* @return Overall similarity score (higher is better)
*/
public static double rankMatch(String c1, String c2, int width) {
double score = 0;
@@ -130,11 +134,8 @@ public class Utility {
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;
}
@@ -142,12 +143,16 @@ public class Utility {
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);
InputStream stream = Utility.class.getResourceAsStream("/jace/data/" + filename);
if (stream == null) {
System.err.println("Could not load icon: " + filename);
return Optional.empty();
}
return Optional.of(new Image(stream));
}
@@ -155,12 +160,14 @@ public class Utility {
if (isHeadless) {
return Optional.empty();
}
Image img = loadIcon(filename).get();
Optional<Image> img = loadIcon(filename);
if (img.isEmpty()) {
return Optional.empty();
}
Label label = new Label() {
@Override
public boolean equals(Object obj) {
if (obj instanceof Label) {
Label l2 = (Label) obj;
if (obj instanceof Label l2) {
return super.equals(l2) || l2.getText().equals(getText());
} else {
return super.equals(obj);
@@ -172,7 +179,7 @@ public class Utility {
return getText().hashCode();
}
};
label.setGraphic(new ImageView(img));
label.setGraphic(new ImageView(img.get()));
label.setAlignment(Pos.CENTER);
label.setContentDisplay(ContentDisplay.TOP);
label.setTextFill(Color.WHITE);
@@ -181,27 +188,39 @@ public class Utility {
return Optional.of(label);
}
// 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 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 decision(String title, String message, String aLabel, String bLabel, Runnable aAction, Runnable bAction) {
Platform.runLater(() -> {
ButtonType buttonA = new ButtonType(aLabel, ButtonData.LEFT);
ButtonType buttonB = new ButtonType(bLabel, ButtonData.RIGHT);
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, message, buttonA, buttonB);
confirm.setTitle(title);
Optional<ButtonType> response = confirm.showAndWait();
response.ifPresent(b -> {
if (b.getButtonData() == ButtonData.LEFT && aAction != null) {
Platform.runLater(aAction);
} else if (b.getButtonData() == ButtonData.RIGHT && bAction != null) {
Platform.runLater(bAction);
}
});
});
}
public static class RankingComparator implements Comparator<String> {
String match;
@@ -215,8 +234,8 @@ public class Utility {
@Override
public int compare(String o1, String o2) {
double s1 = levenshteinDistance(match, o1);
double s2 = levenshteinDistance(match, o2);
double s1 = adjustedLevenshteinDistance(match, o1);
double s2 = adjustedLevenshteinDistance(match, o2);
if (s2 == s1) {
s1 = rankMatch(o1, match, 3) + rankMatch(o1, match, 2);
s2 = rankMatch(o2, match, 3) + rankMatch(o2, match, 2);
@@ -256,120 +275,37 @@ public class Utility {
// 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));
double score = adjustedLevenshteinDistance(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) {
gripe(message, false, null);
}
public static void gripe(final String message, boolean wait, Runnable andThen) {
Platform.runLater(() -> {
Alert errorAlert = new Alert(Alert.AlertType.ERROR);
errorAlert.setContentText(message);
errorAlert.setTitle("Error");
errorAlert.show();
if (wait) {
errorAlert.showAndWait();
if (andThen != null) {
andThen.run();
}
} else {
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;
}
@SuppressWarnings("all")
static Map<Class, Map<String, Object>> enumCache = new HashMap<>();
@SuppressWarnings("all")
public static Object findClosestEnumConstant(String value, Class type) {
Map<String, Object> enumConstants = enumCache.get(type);
if (enumConstants == null) {
@@ -388,38 +324,43 @@ public class Utility {
return enumConstants.get(key);
}
@SuppressWarnings("all")
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);
return Integer.valueOf(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);
return Short.valueOf(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);
return Long.valueOf(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);
return Byte.valueOf(value, radix);
} catch (NumberFormatException ex) {
return null;
}
} else if (type.equals(Boolean.TYPE) || type == Boolean.class) {
return Boolean.valueOf(value);
} else if (type.equals(Float.TYPE) || type == Float.class) {
return Float.parseFloat(value);
} else if (type.equals(Double.TYPE) || type == Double.class) {
return Double.parseDouble(value);
} else if (type == File.class) {
return new File(String.valueOf(value));
} else if (type.isEnum()) {
@@ -429,25 +370,39 @@ public class Utility {
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 Function<Boolean, Boolean> getNamedInvokableAction(String action) {
InvokableActionRegistry registry = InvokableActionRegistry.getInstance();
List<InvokableAction> actionsList = new ArrayList<>(registry.getAllStaticActions());
actionsList.sort((a, b) -> Integer.compare(getActionNameMatch(action, a), getActionNameMatch(action, b)));
// for (InvokableAction a : actionsList) {
// String actionName = a.alternatives() == null ? a.name() : (a.name() + ";" + a.alternatives());
// System.out.println("Score for " + action + " evaluating " + a.name() + ": " + getActionNameMatch(action, a));
// }
return registry.getStaticFunction(actionsList.get(0).name());
}
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;
private static int getActionNameMatch(String str, InvokableAction action) {
int nameMatch = levenshteinDistance(str, action.name());
if (action.alternatives() != null) {
for (String alt : action.alternatives().split(";")) {
nameMatch = Math.min(nameMatch, levenshteinDistance(str, alt));
}
}
return setChild(object, paths[paths.length - 1], value, hex);
return nameMatch;
}
public static enum OS {Windows, Linux, Mac, Unknown}
public static OS getOS() {
String osName = System.getProperty("os.name").toLowerCase();
if (osName.contains("windows")) {
return OS.Windows;
} else if (osName.contains("linux")) {
return OS.Linux;
} else if (osName.contains("mac")) {
return OS.Mac;
} else {
System.out.println("Unknown %s".formatted(osName));
return OS.Unknown;
}
}
}

View File

@@ -1,27 +1,25 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import jace.Emulator;
import jace.state.Stateful;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import jace.state.Stateful;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
@@ -34,7 +32,7 @@ import javafx.scene.image.WritableImage;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
public abstract class Video extends Device {
public abstract class Video extends TimedDevice {
@Stateful
WritableImage video;
@@ -59,20 +57,15 @@ public abstract class Video extends Device {
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;
static final public int[] textOffset = new int[192];
static final public int[] hiresOffset = new int[192];
static final public int[] textRowLookup = new int[0x0400];
static final public int[] hiresRowLookup = new int[0x02000];
private boolean screenDirty = true;
private boolean lineDirty = true;
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];
static void initLookupTables() {
for (int i = 0; i < 192; i++) {
textOffset[i] = calculateTextOffset(i >> 3);
hiresOffset[i] = calculateHiresOffset(i);
@@ -85,16 +78,15 @@ public abstract class Video extends Device {
}
}
private int forceRedrawRowCount = 0;
Thread updateThread;
/**
* Creates a new instance of Video
*
* @param computer
*/
public Video(Computer computer) {
super(computer);
suspend();
public Video() {
super();
initLookupTables();
video = new WritableImage(560, 192);
visible = new WritableImage(560, 192);
vPeriod = 0;
@@ -132,13 +124,13 @@ public abstract class Video extends Device {
public static int MIN_SCREEN_REFRESH = 15;
Runnable redrawScreen = () -> {
if (computer.getRunningProperty().get()) {
if (visible != null && video != null) {
screenDirty = false;
visible.getPixelWriter().setPixels(0, 0, 560, 192, video.getPixelReader(), 0, 0);
}
};
public void redraw() {
screenDirty = false;
javafx.application.Platform.runLater(redrawScreen);
}
@@ -158,18 +150,20 @@ public abstract class Video extends Device {
@Override
public void tick() {
setScannerLocation(currentWriter.getYOffset(y));
setFloatingBus(computer.getMemory().readRaw(scannerAddress + x));
addWaitCycles(waitsPerCycle);
if (y < APPLE_SCREEN_LINES) setScannerLocation(currentWriter.getYOffset(y));
setFloatingBus(getMemory().readRaw(scannerAddress + x));
if (hPeriod > 0) {
hPeriod--;
if (hPeriod == 0) {
x = -1;
}
} else {
if (!isVblank && x < APPLE_CYCLES_PER_LINE) {
draw();
int xVal = x;
if (!isVblank && xVal < APPLE_CYCLES_PER_LINE && xVal >= 0) {
draw(xVal);
}
if (x >= APPLE_CYCLES_PER_LINE - 1) {
if (xVal >= APPLE_CYCLES_PER_LINE - 1) {
int yy = y + hblankOffsetY;
if (yy < 0) {
yy += APPLE_SCREEN_LINES;
@@ -189,17 +183,18 @@ public abstract class Video extends Device {
}
hPeriod = HBLANK;
y++;
getCurrentWriter().setCurrentRow(y);
if (y >= APPLE_SCREEN_LINES) {
if (!isVblank) {
y = APPLE_SCREEN_LINES - (TOTAL_LINES - APPLE_SCREEN_LINES);
isVblank = true;
vblankStart();
computer.getMotherboard().vblankStart();
Emulator.withComputer(c->c.getMotherboard().vblankStart());
} else {
y = 0;
isVblank = false;
vblankEnd();
computer.getMotherboard().vblankEnd();
Emulator.withComputer(c->c.getMotherboard().vblankEnd());
}
}
}
@@ -229,12 +224,11 @@ public abstract class Video extends Device {
@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() {
private void draw(int xVal) {
if (lineDirty || forceRedrawRowCount > 0 || currentWriter.isRowDirty(y)) {
lineDirty = true;
currentWriter.displayByte(video, x, y, textOffset[y], hiresOffset[y]);
currentWriter.displayByte(video, xVal, y, textOffset[y], hiresOffset[y]);
}
setWaitCycles(waitsPerCycle);
doPostDraw();
}
@@ -276,13 +270,11 @@ public abstract class Video extends Device {
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();
}
public static void forceRefresh() {
Emulator.withVideo(v->v._forceRefresh());
}
private void _forceRefresh() {
protected void _forceRefresh() {
lineDirty = true;
screenDirty = true;
forceRedrawRowCount = APPLE_SCREEN_LINES + 1;

View File

@@ -1,21 +1,19 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.core;
import javafx.scene.image.WritableImage;
@@ -32,6 +30,11 @@ import javafx.scene.image.WritableImage;
*/
public abstract class VideoWriter {
int currentRow = -1;
public void setCurrentRow(int y) {
currentRow = y;
}
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
@@ -44,12 +47,19 @@ public abstract class VideoWriter {
// Very useful for knowing if we should bother drawing changes
private final boolean[] dirtyFlags = new boolean[192];
boolean updatedDuringRaster = false;
public void markDirty(int y) {
actualWriter().dirtyFlags[y] = true;
if (y == currentRow) {
updatedDuringRaster = true;
}
}
public void clearDirty(int y) {
actualWriter().dirtyFlags[y] = false;
if (!updatedDuringRaster) {
actualWriter().dirtyFlags[y] = false;
}
updatedDuringRaster = false;
}
public boolean isRowDirty(int y) {

View File

@@ -1,23 +1,22 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.hardware;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
@@ -28,16 +27,16 @@ 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 jace.state.Stateful;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
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
@@ -74,7 +73,7 @@ public class CardAppleMouse extends Card {
@Stateful
public int statusByte;
@Stateful
public Point2D lastMouseLocation;
public Point2D lastMouseLocation = new Point2D(0, 0);
@Stateful
public Rectangle2D clampWindow = new Rectangle2D(0, 0, 0x03ff, 0x03ff);
// By default, update 60 times a second -- roughly every VBL period (in theory)
@@ -88,8 +87,8 @@ public class CardAppleMouse extends Card {
public boolean movedSinceLastTick = false;
public boolean movedSinceLastRead = false;
public CardAppleMouse(Computer computer) {
super(computer);
public CardAppleMouse() {
super(false);
}
@Override
@@ -108,8 +107,16 @@ public class CardAppleMouse extends Card {
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());
double x = 0.0;
double y = 0.0;
if (event.getSource() != null && event.getSource() instanceof Node) {
// This is a bit of a hack to get the mouse position in the local coordinate system of the source (the emulator screen
Node source = (Node) event.getSource();
Bounds bounds = source.getBoundsInLocal();
x=event.getSceneX() / bounds.getWidth();
y=event.getSceneY() / bounds.getHeight();
}
updateLocation(x, y);
event.consume();
}
if (event.getEventType() == MouseEvent.MOUSE_PRESSED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
@@ -121,10 +128,8 @@ public class CardAppleMouse extends Card {
}
}
private void updateLocation(double x, double y, Bounds bounds) {
double scaledX = x / bounds.getWidth();
double scaledY = y / bounds.getHeight();
lastMouseLocation = new Point2D(scaledX, scaledY);
private void updateLocation(double x, double y) {
lastMouseLocation = new Point2D(x, y);
movedSinceLastTick = true;
movedSinceLastRead = true;
}
@@ -175,33 +180,15 @@ public class CardAppleMouse extends Card {
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;
case 0 -> setMouse();
case 1 -> serveMouse();
case 2 -> readMouse();
case 3 -> clearMouse();
case 4 -> posMouse();
case 5 -> clampMouse();
case 6 -> homeMouse();
case 7 -> initMouse();
case 8 -> getMouseClamp();
}
// Always pass back RTS
e.setNewValue(0x060);
@@ -229,6 +216,7 @@ public class CardAppleMouse extends Card {
case 0x08:
// Pascal signature byte
e.setNewValue(0x001);
break;
case 0x011:
e.setNewValue(0x000);
break;
@@ -268,7 +256,7 @@ public class CardAppleMouse extends Card {
}
private MOS65C02 getCPU() {
return (MOS65C02) computer.getCpu();
return (MOS65C02) Emulator.withComputer(Computer::getCpu, null);
}
/*
@@ -359,7 +347,7 @@ public class CardAppleMouse extends Card {
* //gs homes mouse to low address, but //c and //e do not
*/
private void clampMouse() {
RAM128k memory = (RAM128k) computer.memory;
RAM128k memory = (RAM128k) getMemory();
byte clampMinLo = memory.getMainMemory().readByte(0x0478);
byte clampMaxLo = memory.getMainMemory().readByte(0x04F8);
byte clampMinHi = memory.getMainMemory().readByte(0x0578);
@@ -395,7 +383,9 @@ public class CardAppleMouse extends Card {
* Screen holes are updated
*/
private void initMouse() {
mouseActive.setText("Active");
if (mouseActive != null) {
mouseActive.setText("Active");
}
EmulatorUILogic.addIndicator(this, mouseActive, 2000);
setClampWindowX(0, 0x3ff);
setClampWindowY(0, 0x3ff);
@@ -436,40 +426,24 @@ public class CardAppleMouse extends Card {
* 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
* Return value should be stored in $578
* 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 reg = 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;
case 0 -> val = (byte) ((int) clampWindow.getMinX() >> 8);
case 1 -> val = (byte) ((int) clampWindow.getMinY() >> 8);
case 2 -> val = (byte) ((int) clampWindow.getMinX() & 255);
case 3 -> val = (byte) ((int) clampWindow.getMinY() & 255);
case 4 -> val = (byte) ((int) clampWindow.getMaxX() >> 8);
case 5 -> val = (byte) ((int) clampWindow.getMaxY() >> 8);
case 6 -> val = (byte) ((int) clampWindow.getMaxX() & 255);
case 7 -> val = (byte) ((int) clampWindow.getMaxY() & 255);
}
computer.getMemory().write(0x0578, val, false, false);
getMemory().write(0x0578, val, false, false);
}
/*
@@ -550,7 +524,7 @@ public class CardAppleMouse extends Card {
y += clampWindow.getMinY();
y = Math.min(Math.max(y, clampWindow.getMinY()), clampWindow.getMaxY());
PagedMemory m = ((RAM128k) computer.getMemory()).getMainMemory();
PagedMemory m = ((RAM128k) getMemory()).getMainMemory();
int s = getSlot();
/*
* $0478 + slot Low byte of absolute X position

View File

@@ -1,40 +1,38 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
/**
* Apple Disk ][ interface implementation. This card represents the interface
* side of the Disk ][ controller interface as well as the on-board "boot0" ROM.
@@ -45,11 +43,11 @@ import java.util.logging.Logger;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Disk ][ Controller")
public class CardDiskII extends Card implements Reconfigurable, MediaConsumerParent {
public class CardDiskII extends Card implements MediaConsumerParent {
DiskIIDrive currentDrive;
DiskIIDrive drive1 = new DiskIIDrive(computer);
DiskIIDrive drive2 = new DiskIIDrive(computer);
DiskIIDrive drive1 = new DiskIIDrive();
DiskIIDrive drive2 = new DiskIIDrive();
@ConfigurableField(category = "Disk", defaultValue = "254", name = "Default volume", description = "Value to use for disk volume number")
static public int DEFAULT_VOLUME_NUMBER = 0x0FE;
@ConfigurableField(category = "Disk", defaultValue = "true", name = "Speed boost", description = "If enabled, emulator will run at max speed during disk access")
@@ -59,10 +57,10 @@ public class CardDiskII extends Card implements Reconfigurable, MediaConsumerPar
@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);
public CardDiskII() {
super(false);
try {
loadRom("jace/data/DiskII.rom");
loadRom("/jace/data/DiskII.rom");
} catch (IOException ex) {
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
}
@@ -86,18 +84,24 @@ public class CardDiskII extends Card implements Reconfigurable, MediaConsumerPar
// 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:
// Fall-through
case 0x1:
// Fall-through
case 0x2:
// Fall-through
case 0x3:
// Fall-through
case 0x4:
// Fall-through
case 0x5:
// Fall-through
case 0x6:
// Fall-through
case 0x7:
currentDrive.step(register);
break;
@@ -127,7 +131,8 @@ public class CardDiskII extends Card implements Reconfigurable, MediaConsumerPar
case 0xC:
// read/write latch
currentDrive.write();
e.setNewValue(currentDrive.readLatch());
int latch = currentDrive.readLatch();
e.setNewValue(latch);
break;
case 0xF:
// write mode
@@ -168,7 +173,10 @@ public class CardDiskII extends Card implements Reconfigurable, MediaConsumerPar
}
public void loadRom(String path) throws IOException {
InputStream romFile = CardDiskII.class.getClassLoader().getResourceAsStream(path);
InputStream romFile = CardDiskII.class.getResourceAsStream(path);
if (romFile == null) {
throw new IOException("Cannot find Disk ][ ROM at " + path);
}
final int cxRomLength = 0x100;
byte[] romData = new byte[cxRomLength];
try {
@@ -209,10 +217,10 @@ public class CardDiskII extends Card implements Reconfigurable, MediaConsumerPar
private void tweakTiming() {
if ((drive1.isOn() && drive1.disk != null) || (drive2.isOn() && drive2.disk != null)) {
if (USE_MAX_SPEED) {
computer.getMotherboard().requestSpeed(this);
Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
}
} else {
computer.getMotherboard().cancelSpeedRequest(this);
Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest (this));
}
}

View File

@@ -1,25 +1,22 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.hardware;
import jace.apple2e.RAM128k;
import jace.core.Computer;
import jace.core.PagedMemory;
import jace.state.Stateful;
@@ -45,12 +42,12 @@ public class CardExt80Col extends RAM128k {
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);
public CardExt80Col() {
super();
auxMemory = new PagedMemory(0xc000, PagedMemory.Type.RAM);
auxLanguageCard = new PagedMemory(0x3000, PagedMemory.Type.LANGUAGE_CARD);
auxLanguageCard2 = new PagedMemory(0x1000, PagedMemory.Type.LANGUAGE_CARD);
initMemoryPattern(auxMemory);
}
@@ -93,9 +90,4 @@ public class CardExt80Col extends RAM128k {
public void attach() {
// Nothing to do...
}
@Override
public void detach() {
// Nothing to do...
}
}

View File

@@ -1,31 +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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
import jace.config.Name;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
/**
* Partial Hayes Micromodem II implementation, acting more as a bridge to
* provide something similar to the Super Serial support for applications which
@@ -43,8 +41,8 @@ public class CardHayesMicromodem extends CardSSC {
public int RING_INDICATOR_REG = 5;
private boolean ringIndicator = false;
public CardHayesMicromodem(Computer computer) {
super(computer);
public CardHayesMicromodem() {
super();
ACIA_Data = 7;
ACIA_Status = 6;
ACIA_Control = 5;

View File

@@ -1,44 +1,38 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.hardware;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.Emulator;
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.core.SoundMixer.SoundBuffer;
import jace.core.SoundMixer.SoundError;
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
@@ -48,9 +42,12 @@ import javax.sound.sampled.SourceDataLine;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Mockingboard")
public class CardMockingboard extends Card implements Runnable {
public class CardMockingboard extends Card {
// If true, emulation will cover 4 AY chips. Otherwise, only 2 AY chips
@ConfigurableField(name = "Debug", category = "Sound", description = "Enable debug output")
public static boolean DEBUG = false;
@ConfigurableField(name = "Volume", shortName = "vol",
category = "Sound",
description = "Mockingboard volume, 100=max, 0=silent")
@@ -65,36 +62,29 @@ public class CardMockingboard extends Card implements Runnable {
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;
SoundBuffer buffer;
double ticksBetweenPlayback = 24.0;
int MAX_IDLE_TICKS = 1000000;
boolean activatedAfterReset = false;
@Override
public String getDeviceName() {
return "Mockingboard";
}
public CardMockingboard(Computer computer) {
super(computer);
public CardMockingboard() {
super(true);
activatedAfterReset = false;
controllers = new R6522[2];
for (int i = 0; i < 2; i++) {
//don't ask...
// has to be final to be used inside of anonymous class below
final int j = i;
controllers[i] = new R6522(computer) {
int controller = j;
controllers[i] = new R6522() {
@Override
public void sendOutputA(int value) {
chips[j].setBus(value);
@@ -130,20 +120,30 @@ public class CardMockingboard extends Card implements Runnable {
@Override
public String getShortName() {
return "timer" + j;
}
}
};
addChildDevice(controllers[i]);
}
}
@Override
public void reset() {
activatedAfterReset = false;
if (chips != null) {
for (PSG p : chips) {
p.reset();
}
}
suspend();
}
RAMListener mainListener = null;
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
resume();
if (chips == null) {
reconfigure();
}
int chip = 0;
for (PSG psg : chips) {
if (psg.getBaseReg() == (register & 0x0f0)) {
@@ -152,87 +152,101 @@ public class CardMockingboard extends Card implements Runnable {
chip++;
}
if (chip >= 2) {
System.err.println("Could not determine which PSG to communicate to");
e.setNewValue(computer.getVideo().getFloatingBus());
if (DEBUG) {
System.err.println("Could not determine which PSG to communicate to for access to regsiter + " + Integer.toHexString(register));
}
Emulator.withVideo(v->e.setNewValue(v.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);
if (DEBUG) System.out.println("Chip " + chip + " Read "+Integer.toHexString(register & 0x0f)+" == "+val);
} else {
controller.writeRegister(register & 0x0f, e.getNewValue());
// System.out.println("Write "+Integer.toHexString(register)+" == "+e.getNewValue());
if (DEBUG) System.out.println("Chip " + chip + " Write "+Integer.toHexString(register & 0x0f)+" == "+e.getNewValue());
}
}
// Any firmware access will reset the idle counter and wake up the card, this allows the timers to start running again
// Games such as "Skyfox" use the timer to detect if the card is present.
idleTicks = 0;
if (!isRunning() || isPaused()) {
activatedAfterReset = true;
// ResumeAll is important so that the 6522's can start their timers
resumeAll();
}
}
@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());
if (DEBUG) {
System.out.println("MB I/O Access "+type.name()+" "+register+":"+value);
}
Emulator.withVideo(v->e.setNewValue(v.getFloatingBus()));
}
long ticksSinceLastPlayback = 0;
double ticksSinceLastPlayback = 0;
long idleTicks = 0;
@Override
public void tick() {
for (R6522 c : controllers) {
if (c == null || !c.isRunning()) {
continue;
try {
ticksSinceLastPlayback++;
if (ticksSinceLastPlayback >= ticksBetweenPlayback) {
ticksSinceLastPlayback -= ticksBetweenPlayback;
if (playSound()) {
idleTicks = 0;
} else {
idleTicks += ticksBetweenPlayback;
}
}
c.tick();
} catch (InterruptedException | ExecutionException | SoundError | NullPointerException ex) {
Logger.getLogger(CardMockingboard.class.getName()).log(Level.SEVERE, "Mockingboard playback encountered fatal exception", ex);
suspend();
// Do nothing, probably suspending CPU
}
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();
}
if (idleTicks >= MAX_IDLE_TICKS) {
suspend();
}
}
@Override
public void reconfigure() {
boolean restart = suspend();
initPSG();
for (PSG chip : chips) {
chip.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SAMPLE_RATE);
chip.reset();
if (DEBUG) {
System.out.println("Reconfiguring Mockingboard");
}
ticksBetweenPlayback = (double) CLOCK_SPEED / (double) SoundMixer.RATE;
initPSG();
super.reconfigure();
if (restart) {
resume();
if (DEBUG) {
System.out.println("Reconfiguring Mockingboard completed");
}
}
///////////////////////////////////////////////////////////
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);
AtomicInteger left = new AtomicInteger(0);
AtomicInteger right = new AtomicInteger(0);
public boolean playSound() throws InterruptedException, ExecutionException, SoundError {
if (phasorMode && chips.length != 4) {
System.err.println("Wrong number of chips for phasor mode, correcting this");
initPSG();
}
chips[0].update(left, true, left, false, left, false);
chips[1].update(right, true, right, false, right, false);
if (phasorMode) {
chips[2].update(left, false, left, false, left, false);
chips[3].update(right, false, right, false, right, false);
}
SoundBuffer b = buffer;
if (b == null) {
return false;
}
b.playSample((short) left.get());
b.playSample((short) right.get());
return (left.get() != 0 || right.get() != 0);
}
public void buildMixerTable() {
@@ -246,7 +260,7 @@ public class CardMockingboard extends Card implements Runnable {
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;
// 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 */
@@ -257,172 +271,82 @@ public class CardMockingboard extends Card implements Runnable {
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();
}
if (DEBUG) {
System.out.println("Resuming Mockingboard");
}
if (!activatedAfterReset) {
if (DEBUG) {
System.out.println("Resuming Mockingboard: not activated after reset, not resuming");
}
for (R6522 controller : controllers) {
controller.attach();
controller.resume();
// Do not re-activate until firmware access was made
return;
}
initPSG();
if (buffer == null || !buffer.isAlive()) {
if (DEBUG) {
System.out.println("Resuming Mockingboard: creating sound buffer");
}
try {
buffer = SoundMixer.createBuffer(true);
} catch (InterruptedException | ExecutionException | SoundError e) {
System.out.println("Error whhen trying to create sound buffer for Mockingboard: " + e.getMessage());
e.printStackTrace();
suspend();
}
}
idleTicks = 0;
super.resume();
if (playbackThread == null || !playbackThread.isAlive()) {
playbackThread = new Thread(this, "Mockingboard sound playback");
playbackThread.start();
if (DEBUG) {
System.out.println("Resuming Mockingboard: resume completed");
}
}
@Override
public boolean suspend() {
super.suspend();
for (R6522 controller : controllers) {
controller.suspend();
controller.detach();
if (DEBUG) {
System.out.println("Suspending Mockingboard");
Thread.dumpStack();
}
if (playbackThread == null || !playbackThread.isAlive()) {
return false;
}
if (playbackThread != null) {
playbackThread.interrupt();
if (buffer != null) {
try {
// Wait for thread to die
playbackThread.join();
} catch (InterruptedException ex) {
buffer.shutdown();
} catch (InterruptedException | ExecutionException | SoundError e) {
System.out.println("Error when trying to shutdown sound buffer for Mockingboard: " + e.getMessage());
e.printStackTrace();
} finally {
buffer = null;
}
}
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);
for (R6522 c : controllers) {
c.suspend();
}
return super.suspend();
}
private void initPSG() {
if (phasorMode) {
if (phasorMode && (chips == null || chips.length < 4)) {
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[0] = new PSG(0x10, CLOCK_SPEED * 2, SoundMixer.RATE, "AY1", 8);
chips[1] = new PSG(0x80, CLOCK_SPEED * 2, SoundMixer.RATE, "AY2", 8);
chips[2] = new PSG(0x10, CLOCK_SPEED * 2, SoundMixer.RATE, "AY3", 16);
chips[3] = new PSG(0x80, CLOCK_SPEED * 2, SoundMixer.RATE, "AY4", 16);
} else if (chips == null || chips.length != 2) {
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);
chips[0] = new PSG(0, CLOCK_SPEED, SoundMixer.RATE, "AY1", 255);
chips[1] = new PSG(0x80, CLOCK_SPEED, SoundMixer.RATE, "AY2", 255);
}
for (PSG psg : chips) {
psg.setRate(phasorMode ? CLOCK_SPEED * 2 : CLOCK_SPEED, SoundMixer.RATE);
}
buildMixerTable();
}
@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

@@ -1,37 +1,36 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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 jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.state.Stateful;
import javafx.scene.control.Label;
/**
@@ -64,11 +63,11 @@ public class CardRamFactor extends Card {
return "RamFactor";
}
Optional<Label> indicator;
public CardRamFactor(Computer computer) {
super(computer);
public CardRamFactor() {
super(false);
indicator = Utility.loadIconLabel("ram.png");
try {
loadRom("jace/data/RAMFactor14.rom");
loadRom("/jace/data/RAMFactor14.rom");
} catch (IOException ex) {
Logger.getLogger(CardRamFactor.class.getName()).log(Level.SEVERE, null, ex);
}
@@ -163,7 +162,7 @@ public class CardRamFactor extends Card {
@Override
protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
if (speedBoost) {
computer.getMotherboard().requestSpeed(this);
Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
}
}
@@ -175,7 +174,7 @@ public class CardRamFactor extends Card {
@Override
protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
if (speedBoost) {
computer.getMotherboard().requestSpeed(this);
Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));
}
}
@@ -215,7 +214,7 @@ public class CardRamFactor extends Card {
final int cxRomLength = 0x02000;
byte[] romData = new byte[cxRomLength];
public void loadRom(String path) throws IOException {
InputStream romFile = CardRamFactor.class.getClassLoader().getResourceAsStream(path);
InputStream romFile = CardRamFactor.class.getResourceAsStream(path);
try {
if (romFile.read(romData) != cxRomLength) {
throw new IOException("Bad RamFactor rom size");

View File

@@ -1,40 +1,39 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
import jace.Emulator;
import jace.apple2e.RAM128k;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.PagedMemory;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.state.Stateful;
/**
* Emulates the Ramworks Basic and Ramworks III cards
*
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Stateful
@Name("Ramworks III Memory Expansion")
@@ -47,32 +46,32 @@ public class CardRamworks extends RAM128k {
public Map<BankType, PagedMemory> nullBank = generateBank();
@ConfigurableField(
category = "memory",
defaultValue = "3072",
defaultValue = "4096",
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 memorySize = 4096;
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));
memoryBank.put(BankType.MAIN_MEMORY, new PagedMemory(0xc000, PagedMemory.Type.RAM));
memoryBank.put(BankType.LANGUAGE_CARD_1, new PagedMemory(0x3000, PagedMemory.Type.LANGUAGE_CARD));
memoryBank.put(BankType.LANGUAGE_CARD_2, new PagedMemory(0x1000, PagedMemory.Type.LANGUAGE_CARD));
return memoryBank;
}
public static enum BankType {
public enum BankType {
MAIN_MEMORY, LANGUAGE_CARD_1, LANGUAGE_CARD_2
};
}
public CardRamworks(Computer computer) {
super(computer);
public CardRamworks() {
super();
memory = new ArrayList<>(maxBank);
reconfigure();
}
private PagedMemory getAuxBank(BankType type, int bank) {
if (bank >= maxBank) {
return nullBank.get(type);
return nullBank == null ? null : nullBank.get(type);
}
Map<BankType, PagedMemory> memoryBank = memory.get(bank);
if (memoryBank == null) {
@@ -103,6 +102,11 @@ public class CardRamworks extends RAM128k {
return getAuxBank(BankType.LANGUAGE_CARD_2, currentBank);
}
@Override
public String getAuxZPConfiguration() {
return super.getAuxZPConfiguration() + currentBank;
}
@Override
public String getName() {
return "Ramworks III";
@@ -115,33 +119,32 @@ public class CardRamworks extends RAM128k {
@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();
}
Emulator.whileSuspended(computer -> {
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();
});
}
private RAMListener bankSelectListener;
@Override
public void attach() {
bankSelectListener = computer.getMemory().observe(RAMEvent.TYPE.WRITE, BANK_SELECT, (e) -> {
bankSelectListener = observe("Ramworks bank select", RAMEvent.TYPE.WRITE, BANK_SELECT, (e) -> {
currentBank = e.getNewValue();
configureActiveMemory();
configureActiveMemory();
});
}
@Override
public void detach() {
removeListener(bankSelectListener);
super.detach();
}
}

View File

@@ -1,32 +1,21 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
@@ -34,8 +23,18 @@ import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import javafx.scene.control.Label;
/**
@@ -45,7 +44,7 @@ import javafx.scene.control.Label;
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
*/
@Name("Super Serial Card")
public class CardSSC extends Card implements Reconfigurable {
public class CardSSC extends Card {
@ConfigurableField(name = "TCP/IP Port", shortName = "port")
public short IP_PORT = 1977;
@@ -55,16 +54,16 @@ public class CardSSC extends Card implements Reconfigurable {
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(category = "Advanced", name = "Liveness check interval", description = "How often the connection is polled for signs of life when idle (in milliseconds)")
public int livenessCheck = 5000000;
@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
public static int SW1 = 0x01; // Read = Jumper block SW1
//Bit 0 = !SW1-6
//Bit 1 = !SW1-5
//Bit 4 = !SW1-4
@@ -74,7 +73,7 @@ public class CardSSC extends Card implements Reconfigurable {
// 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
public static 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
@@ -86,10 +85,10 @@ public class CardSSC extends Card implements Reconfigurable {
// 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 static int ACIA_Data = 0x08; // Read=Receive / Write=transmit
public static int ACIA_Status = 0x09; // Read=Status / Write=Reset
public static int ACIA_Command = 0x0A;
public static int ACIA_Control = 0x0B;
public boolean PORT_CONNECTED = false;
public boolean RECV_IRQ_ENABLED = false;
public boolean TRANS_IRQ_ENABLED = false;
@@ -97,8 +96,8 @@ public class CardSSC extends Card implements Reconfigurable {
// Bitmask for stop bits (FF = 8, 7F = 7, etc)
private int DATA_BITS = 0x07F;
public CardSSC(Computer computer) {
super(computer);
public CardSSC() {
super(false);
}
@Override
@@ -111,7 +110,7 @@ public class CardSSC extends Card implements Reconfigurable {
@Override
public void setSlot(int slot) {
try {
loadRom("jace/data/SSC.rom");
loadRom("/jace/data/SSC.rom");
} catch (IOException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
@@ -122,7 +121,7 @@ public class CardSSC extends Card implements Reconfigurable {
});
}
boolean newInputAvailable = false;
AtomicBoolean newInputAvailable = new AtomicBoolean();
public void socketMonitor() {
try {
socket = new ServerSocket(IP_PORT);
@@ -141,13 +140,12 @@ public class CardSSC extends Card implements Reconfigurable {
clientConnected();
clientSocket.setTcpNoDelay(true);
while (isConnected()) {
try {
Thread.sleep(10);
if (socketInput.ready()) {
newInputAvailable = true;
Thread.onSpinWait();
if (!newInputAvailable.get() && inputAvailable()) {
lastTransmission = System.currentTimeMillis();
synchronized (newInputAvailable) {
newInputAvailable.set(true);
}
} catch (InterruptedException ex) {
// Do nothing
}
}
clientDisconnected();
@@ -178,34 +176,30 @@ public class CardSSC extends Card implements Reconfigurable {
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);
InputStream romFile = CardSSC.class.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;
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);
}
@Override
public void reset() {
suspend();
Thread resetThread = new Thread(() -> {
try {
Thread.sleep(50);
Thread.sleep(100);
} catch (InterruptedException ex) {
Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex);
}
suspend();
resume();
});
resetThread.start();
@@ -216,6 +210,7 @@ public class CardSSC extends Card implements Reconfigurable {
try {
int newValue = -1;
switch (type) {
case ANY:
case EXECUTE:
case READ_OPERAND:
case READ_DATA:
@@ -226,7 +221,7 @@ public class CardSSC extends Card implements Reconfigurable {
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;
newValue |= (PORT_CONNECTED && newInputAvailable.get() ? 0x00 : 0x01);
}
if (register == ACIA_Data) {
EmulatorUILogic.addIndicator(this, activityIndicator);
@@ -238,7 +233,7 @@ public class CardSSC extends Card implements Reconfigurable {
// 1 = Framing error (1)
// 2 = Overrun error (1)
// 3 = ACIA Receive Register full (1)
if (newInputAvailable || inputAvailable()) {
if (newInputAvailable.get()) {
newValue |= 0x08;
}
// 4 = ACIA Transmit Register empty (1)
@@ -299,19 +294,15 @@ public class CardSSC extends Card implements Reconfigurable {
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)
@@ -353,15 +344,14 @@ public class CardSSC extends Card implements Reconfigurable {
@Override
public void tick() {
if (RECV_IRQ_ENABLED && newInputAvailable) {
newInputAvailable = false;
if (RECV_IRQ_ENABLED && newInputAvailable.get()) {
// newInputAvailable = false;
triggerIRQ();
}
}
public boolean inputAvailable() throws IOException {
if (isConnected() && clientSocket != null && socketInput != null) {
// return socketInput.available() > 0;
return socketInput.ready();
} else {
return false;
@@ -369,16 +359,19 @@ public class CardSSC extends Card implements Reconfigurable {
}
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;
if (newInputAvailable.get()) {
synchronized (newInputAvailable) {
int in = socketInput.read() & DATA_BITS;
if (RECV_STRIP_LF && in == 10 && lastInputByte == 13) {
in = socketInput.read() & DATA_BITS;
}
lastInputByte = in;
newInputAvailable.set(false);
}
}
return lastInputByte;
}
long lastSuccessfulWrite = -1L;
long lastTransmission = -1L;
private void sendOutputByte(int i) throws IOException {
if (clientSocket != null && clientSocket.isConnected()) {
@@ -388,35 +381,36 @@ public class CardSSC extends Card implements Reconfigurable {
clientSocket.getOutputStream().write(10);
}
clientSocket.getOutputStream().flush();
lastSuccessfulWrite = System.currentTimeMillis();
lastTransmission = System.currentTimeMillis();
} catch (IOException e) {
lastSuccessfulWrite = -1L;
lastTransmission = -1L;
hangUp();
}
} else {
lastSuccessfulWrite = -1L;
lastTransmission = -1L;
}
}
private void setCTS(boolean b) throws InterruptedException {
PORT_CONNECTED = b;
if (b == false) {
reset();
}
}
// CTS isn't used here -- it's assumed that we're always clear-to-send
// private void setCTS(boolean b) throws InterruptedException {
// PORT_CONNECTED = b;
// if (b == false) {
// reset();
// }
// }
private boolean getCTS() throws InterruptedException {
return PORT_CONNECTED;
}
// private boolean getCTS() throws InterruptedException {
// return PORT_CONNECTED;
// }
private void triggerIRQ() {
IRQ_TRIGGERED = true;
computer.getCpu().generateInterrupt();
Emulator.withComputer(c->c.getCpu().generateInterrupt());
}
public void hangUp() {
lastInputByte = 0;
lastSuccessfulWrite = -1L;
lastTransmission = -1L;
if (clientSocket != null && clientSocket.isConnected()) {
try {
clientSocket.shutdownInput();
@@ -437,55 +431,48 @@ public class CardSSC extends Card implements Reconfigurable {
*/
@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);
}
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();
}
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()) {
RECV_IRQ_ENABLED = false;
TRANS_IRQ_ENABLED = false;
IRQ_TRIGGERED = false;
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();
}
//socket.setReuseAddress(true);
listenThread = new Thread(this::socketMonitor);
listenThread.setDaemon(false);
listenThread.setName("SSC port listener, slot" + getSlot());
listenThread.start();
}
super.resume();
}
@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)) {
if (lastTransmission == -1 || System.currentTimeMillis() > (lastTransmission + livenessCheck)) {
try {
sendOutputByte(0);
return true;

View File

@@ -1,34 +1,21 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
@@ -36,6 +23,19 @@ import java.util.Optional;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.apple2e.MOS65C02;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.PagedMemory;
import jace.core.RAM;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.TimedDevice;
import jace.core.Utility;
import javafx.scene.control.Label;
/**
@@ -59,10 +59,10 @@ public class CardThunderclock extends Card {
@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);
public CardThunderclock() {
super(true);
try {
loadRom("jace/data/thunderclock_plus.rom");
loadRom("/jace/data/thunderclock_plus.rom");
} catch (IOException ex) {
Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex);
}
@@ -149,7 +149,7 @@ public class CardThunderclock extends Card {
shiftMode = isShift;
if (isRead) {
if (attemptYearPatch) {
performProdosPatch(computer);
_performProdosPatch();
}
getTime();
clockIcon.ifPresent(icon->{
@@ -169,13 +169,13 @@ public class CardThunderclock extends Card {
if (timerEnabled) {
switch (value & 0x038) {
case 0x020:
timerRate = (int) (Motherboard.SPEED / 64);
timerRate = (int) (TimedDevice.NTSC_1MHZ / 64);
break;
case 0x028:
timerRate = (int) (Motherboard.SPEED / 256);
timerRate = (int) (TimedDevice.NTSC_1MHZ / 256);
break;
case 0x030:
timerRate = (int) (Motherboard.SPEED / 2048);
timerRate = (int) (TimedDevice.NTSC_1MHZ / 2048);
break;
default:
timerEnabled = false;
@@ -214,7 +214,7 @@ public class CardThunderclock extends Card {
ticks = 0;
irqAsserted = true;
if (irqEnabled) {
computer.getCpu().generateInterrupt();
Emulator.withComputer(c->c.getCpu().generateInterrupt());
}
}
}
@@ -261,7 +261,7 @@ public class CardThunderclock extends Card {
}
public void loadRom(String path) throws IOException {
InputStream romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path);
InputStream romFile = CardThunderclock.class.getResourceAsStream(path);
final int cxRomLength = 0x0100;
final int c8RomLength = 0x0700;
byte[] romxData = new byte[cxRomLength];
@@ -272,7 +272,7 @@ public class CardThunderclock extends Card {
}
getCxRom().loadData(romxData);
romFile.close();
romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path);
romFile = CardThunderclock.class.getResourceAsStream(path);
if (romFile.read(rom8Data) != c8RomLength) {
throw new IOException("Bad Thunderclock rom size");
}
@@ -296,8 +296,12 @@ public class CardThunderclock extends Card {
* always tell time correctly.
* @param computer
*/
public static void performProdosPatch(Computer computer) {
PagedMemory ram = computer.getMemory().activeRead;
public void _performProdosPatch() {
performProdosPatch(getMemory());
}
public static void performProdosPatch(RAM memory) {
PagedMemory ram = memory.activeRead;
if (patchLoc > 0) {
// We've already patched, just validate
if (ram.readByte(patchLoc) == (byte) MOS65C02.OPCODE.LDA_IMM.getCode()) {

View File

@@ -0,0 +1,44 @@
package jace.hardware;
import java.util.function.Supplier;
import jace.config.DeviceEnum;
import jace.core.Card;
import jace.hardware.massStorage.CardMassStorage;
public enum Cards implements DeviceEnum<Card> {
AppleMouse("Apple Mouse", CardAppleMouse.class, CardAppleMouse::new),
DiskIIDrive("Disk II Floppy Controller", CardDiskII.class, CardDiskII::new),
HayesMicroModem("Hayes MicroModem", CardHayesMicromodem.class, CardHayesMicromodem::new),
MassStorage("Mass Storage", CardMassStorage.class, CardMassStorage::new),
Mockingboard("Mockingboard", CardMockingboard.class, CardMockingboard::new),
PassportMidi("Passport MIDI", PassportMidiInterface.class, PassportMidiInterface::new),
RamFactor("RamFactor", CardRamFactor.class, CardRamFactor::new),
SuperSerialCard("Super Serial Card", CardSSC.class, CardSSC::new),
Thunderclock("Thunderclock", CardThunderclock.class, CardThunderclock::new);
Supplier<Card> factory;
String name;
Class<? extends Card> clazz;
Cards(String name, Class<? extends Card> clazz, Supplier<Card> factory) {
this.name = name;
this.factory = factory;
this.clazz = clazz;
}
@Override
public String getName() {
return name;
}
@Override
public Card create() {
return factory.get();
}
@Override
public boolean isInstance(Card card) {
return card != null && clazz.isInstance(card);
}
}

View File

@@ -1,166 +0,0 @@
/*
* 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

@@ -1,99 +0,0 @@
/*
* 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

@@ -1,36 +1,36 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import jace.Emulator;
import jace.EmulatorUILogic;
import jace.library.MediaConsumer;
import jace.library.MediaEntry;
import jace.library.MediaEntry.MediaFile;
import jace.state.StateManager;
import jace.state.Stateful;
import javafx.scene.control.Label;
/**
@@ -44,17 +44,17 @@ import javafx.scene.control.Label;
*/
@Stateful
public class DiskIIDrive implements MediaConsumer {
Computer computer;
public DiskIIDrive(Computer computer) {
this.computer = computer;
public DiskIIDrive() {
}
public boolean DEBUG = false;
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;
AtomicBoolean diskUpdatePending = new AtomicBoolean();
// Last time of write operation
long lastWriteTime;
// Managed thread to update disk image in background
@@ -86,7 +86,7 @@ public class DiskIIDrive implements MediaConsumer {
driveOn = false;
magnets = 0;
dirtyTracks = new HashSet<>();
diskUpdatePending = false;
diskUpdatePending.set(false);
}
void step(int register) {
@@ -113,13 +113,18 @@ public class DiskIIDrive implements MediaConsumer {
}
nibbleOffset = 0;
//System.out.printf("new half track %d\n", currentHalfTrack);
if (DEBUG) {
System.out.printf("step %d, new half track %d\n", register, halfTrack);
}
}
}
}
}
void setOn(boolean b) {
if (DEBUG) {
System.out.println("Drive setOn: "+b);
}
driveOn = b;
}
@@ -155,17 +160,20 @@ public class DiskIIDrive implements MediaConsumer {
void write() {
if (writeMode) {
while (diskUpdatePending) {
while (diskUpdatePending.get()) {
// If another thread requested writes to block (e.g. because of disk activity), wait for it to finish!
LockSupport.parkNanos(1000);
Thread.onSpinWait();
}
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);
// Holding the lock should block any other threads from writing to disk
synchronized (diskUpdatePending) {
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);
}
}
}
@@ -192,19 +200,20 @@ public class DiskIIDrive implements MediaConsumer {
}
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);
});
synchronized (diskUpdatePending) {
diskUpdatePending.set(true);
// Update all tracks as necessary
if (disk != null) {
dirtyTracks.stream().forEach((track) -> {
disk.updateTrack(track);
});
}
// Empty out dirty list
dirtyTracks.clear();
}
// Empty out dirty list
dirtyTracks.clear();
// Signal disk update is completed
diskUpdatePending = false;
diskUpdatePending.set(false);
}
private void triggerDiskUpdate() {
@@ -226,10 +235,13 @@ public class DiskIIDrive implements MediaConsumer {
}
void insertDisk(File diskPath) throws IOException {
disk = new FloppyDisk(diskPath, computer);
if (DEBUG) {
System.out.println("inserting disk " + diskPath.getAbsolutePath() + " into drive");
}
disk = new FloppyDisk(diskPath);
dirtyTracks = new HashSet<>();
// Emulator state has changed significantly, reset state manager
StateManager.getInstance(computer).invalidate();
Emulator.withComputer(c->StateManager.getInstance(c).invalidate());
}
private Optional<Label> icon;
@@ -273,7 +285,7 @@ public class DiskIIDrive implements MediaConsumer {
disk = null;
dirtyTracks = new HashSet<>();
// Emulator state has changed significantly, reset state manager
StateManager.getInstance(computer).invalidate();
Emulator.withComputer(c->StateManager.getInstance(c).invalidate());
}
@Override
@@ -300,14 +312,16 @@ public class DiskIIDrive implements MediaConsumer {
@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);
if (DEBUG) {
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()) {
while (diskUpdatePending.get()) {
// If the current disk has unsaved changes, wait!!!
LockSupport.parkNanos(1000);
Thread.onSpinWait();
}
}
}

View File

@@ -0,0 +1,118 @@
package jace.hardware;
import jace.JaceApplication;
import jace.apple2e.SoftSwitches;
import jace.apple2e.softswitch.VideoSoftSwitch;
import jace.core.Device;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
/**
* Simple device that displays speed and fps stats
*/
public class FPSMonitorDevice extends Device {
public static final long UPDATE_CHECK_FREQUENCY = 1000;
Label cpuSpeedIcon;
Label fpsIcon;
long checkCounter = 0;
long tickCounter = 0;
int frameCounter = 0;
long lastUpdate = 0;
long UPDATE_INTERVAL = 1000/2;
boolean lastVBLState = false;
public FPSMonitorDevice() {
}
@Override
protected String getDeviceName() {
return "FPS Monitor";
}
int cpuPerClock = 1;
VideoSoftSwitch vss;
@Override
public void tick() {
tickCounter += cpuPerClock;
boolean vblState = vss.getState();
if (!vblState && lastVBLState) {
frameCounter++;
}
lastVBLState = vblState;
if (--checkCounter <= UPDATE_CHECK_FREQUENCY) {
updateIcon();
checkCounter = UPDATE_CHECK_FREQUENCY;
}
}
Label initLabel(Label l) {
l.setTextFill(Color.WHITE);
l.setEffect(new DropShadow(2.0, Color.BLACK));
l.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.8), new CornerRadii(5.0), new Insets(-5.0))));
l.setMinWidth(64.0);
l.setMaxWidth(Region.USE_PREF_SIZE);
return l;
}
void updateIcon() {
long now = System.currentTimeMillis();
long ellapsed = now - lastUpdate;
if (ellapsed < UPDATE_INTERVAL) {
return;
}
if (cpuSpeedIcon == null) {
cpuSpeedIcon = initLabel(new Label());
fpsIcon = initLabel(new Label());
}
JaceApplication.getApplication().controller.addIndicator(cpuSpeedIcon,1000);
JaceApplication.getApplication().controller.addIndicator(fpsIcon,1000);
double secondsEllapsed = ((double) ellapsed) / 1000.0;
double speed = ((double) tickCounter) / secondsEllapsed / 1000000.0;
double fps = ((double) frameCounter)/secondsEllapsed;
String mhzStr = String.format("%1.1fmhz", speed);
String fpsStr = String.format("%1.1ffps", fps);
// System.out.println(mhzStr+";"+fpsStr);
Platform.runLater(()->{
cpuSpeedIcon.setText(mhzStr);
fpsIcon.setText(fpsStr);
});
// Reset counters
lastUpdate = now;
tickCounter = 0;
frameCounter = 0;
}
@Override
public String getShortName() {
return "fps";
}
@Override
public void attach() {
tickCounter = 0;
frameCounter = 0;
vss = (VideoSoftSwitch) SoftSwitches.VBL.getSwitch();
}
@Override
public void reconfigure() {
}
@Override
public void detach() {
if (cpuSpeedIcon != null) {
JaceApplication.getApplication().controller.removeIndicator(cpuSpeedIcon);
JaceApplication.getApplication().controller.removeIndicator(fpsIcon);
}
}
}

View File

@@ -1,26 +1,21 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
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;
@@ -32,6 +27,9 @@ import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import jace.state.StateManager;
import jace.state.Stateful;
/**
* Representation of a 140kb floppy disk image. This also performs conversions
* as needed. Internally, the emulator will always use a "nibblized" disk
@@ -99,7 +97,7 @@ public class FloppyDisk {
NIBBLE_62_REVERSE[NIBBLE_62[i] & 0x0ff] = 0x0ff & i;
}
}
private static boolean DEBUG = false;
private static final boolean DEBUG = false;
public FloppyDisk() throws IOException {
// This constructor is only used for disk conversion...
@@ -108,19 +106,18 @@ public class FloppyDisk {
/**
*
* @param diskFile
* @param computer
* @throws IOException
*/
public FloppyDisk(File diskFile, Computer computer) throws IOException {
public FloppyDisk(File diskFile) throws IOException {
FileInputStream input = new FileInputStream(diskFile);
String name = diskFile.getName().toUpperCase();
readDisk(input, name.endsWith(".PO"), computer);
readDisk(input, name.endsWith(".PO"));
writeProtected = !diskFile.canWrite();
diskPath = diskFile;
}
// brendanr: refactored to use input stream
public void readDisk(InputStream diskFile, boolean prodosOrder, Computer computer) throws IOException {
public void readDisk(InputStream diskFile, boolean prodosOrder) throws IOException {
isNibblizedImage = true;
volumeNumber = CardDiskII.DEFAULT_VOLUME_NUMBER;
headerLength = 0;
@@ -156,8 +153,8 @@ public class FloppyDisk {
} catch (IOException ex) {
throw ex;
}
StateManager.markDirtyValue(nibbles, computer);
StateManager.markDirtyValue(currentSectorOrder, computer);
StateManager.markDirtyValue(nibbles);
StateManager.markDirtyValue(currentSectorOrder);
}
/*
@@ -180,6 +177,17 @@ public class FloppyDisk {
writeJunkBytes(output, 38 - gap2);
}
}
// Write output to stdout for debugging purposes
if (DEBUG) {
System.out.println("Nibblized disk:");
for (int i = 0; i < output.size(); i++) {
System.out.print(Integer.toString(output.toByteArray()[i] & 0x0ff, 16) + " ");
if (i % 16 == 255) {
System.out.println();
}
}
System.out.println();
}
return output.toByteArray();
}
@@ -220,8 +228,7 @@ public class FloppyDisk {
private int decodeOddEven(byte b1, byte b2) {
// return (((b1 ^ 0x0AA) << 1) & 0x0ff) | ((b2 ^ 0x0AA) & 0x0ff);
int result = ((((b1 << 1) | 1) & b2) & 0x0ff);
return result;
return ((((b1 << 1) | 1) & b2) & 0x0ff);
}
private void nibblizeBlock(ByteArrayOutputStream output, int track, int sector, byte[] nibbles) {
@@ -299,17 +306,23 @@ public class FloppyDisk {
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);
// }
if (DEBUG) {
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];
if (DEBUG) {
System.out.print(Integer.toString(nibbles[pos] & 0x0ff, 16)+" ");
if (i % 16 == 15) {
System.out.println();
System.out.printf("%04d:",i+1);
}
}
}
if (DEBUG) {
System.out.println();
}
// System.out.println();
int pos = 0;
for (int i = 0; i < SECTOR_COUNT; i++) {
@@ -319,7 +332,9 @@ public class FloppyDisk {
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);
if (DEBUG) {
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
@@ -327,7 +342,9 @@ public class FloppyDisk {
// 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));
if (DEBUG) {
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

View File

@@ -1,41 +1,49 @@
/*
* 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
*/
/**
* Copyright 2024 Brendan Robert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package jace.hardware;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.lwjgl.glfw.GLFW;
import jace.Emulator;
import jace.JaceApplication;
import jace.apple2e.SoftSwitches;
import jace.apple2e.softswitch.MemorySoftSwitch;
import jace.config.ConfigurableField;
import jace.config.DynamicSelection;
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.core.Utility;
import jace.core.Utility.OS;
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;
import javafx.application.Platform;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
/**
* Simple implementation of joystick support that supports mouse or keyboard.
@@ -46,13 +54,204 @@ import java.util.logging.Logger;
*/
@Stateful
public class Joystick extends Device {
static {
Platform.runLater(()->{
GLFW.glfwInit();
// Load joystick mappings from resources
// First read the file into a ByteBuffer
try (InputStream inputStream = Joystick.class.getResourceAsStream("/jace/data/gamecontrollerdb.txt")) {
// Throw it into a string
String mappings = new String(inputStream.readAllBytes());
parseGameControllerDB(mappings);
} catch (Exception e) {
System.err.println("Failed to load joystick mappings; error: " + e.getMessage());
e.printStackTrace();
}
});
}
static public class ControllerMapping {
public String name;
public String guid;
public String platform;
public int button0 = -1;
public int button0rapid = -1;
public int button1 = -1;
public int button1rapid = -1;
public int pause = -1;
public boolean xinvert = false;
public int xaxis = -1;
public boolean yinvert = false;
public int yaxis = -1;
public int up = -1;
public int down = -1;
public int left = -1;
public int right = -1;
public boolean hasGamepad() {
return up >=0 && down >= 0 && left >= 0 && right >= 0;
}
}
static Map<OS, Map<String, ControllerMapping>> controllerMappings = new HashMap<>();
static void parseGameControllerDB(String mappings) {
for (OS os : OS.values()) {
controllerMappings.put(os, new HashMap<>());
}
// File format:
// Any line starting with a # or empty is a comment
// Format is GUID, name, mappings
// Mappings are a comma-separated list of buttons, axes, and hats with a : delimiter
// Buttons are B<index>, axes are A<index>, hats are H<index>
// Read into a map of GUID to mappings
String[] lines = mappings.split("\n");
for (String line : lines) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split(",");
if (parts.length < 3) {
continue;
}
String guid = parts[0].trim();
String name = parts[1].trim();
ControllerMapping controller = new ControllerMapping();
controller.guid = guid;
controller.name = name;
OS os = OS.Unknown;
// Split the mapping into parts
for (int i = 2; i < parts.length; i++) {
String[] mappingParts = parts[i].split(":");
if (mappingParts.length < 2) {
continue;
}
String target = mappingParts[0].trim();
String source = mappingParts[1].trim();
boolean inverted = source.endsWith("~");
if (inverted) {
source = source.substring(0, source.length() - 1);
}
boolean isAxis = source.charAt(0) == 'a';
boolean isButton = source.charAt(0) == 'b';
boolean isHat = source.charAt(0) == 'h';
boolean isNAN = !isAxis && !isButton && !isHat;
int index = isNAN ? -1 : Integer.parseInt(source.substring(isHat ? 3 : 1));
if (isAxis) {
switch (target) {
case "leftx" -> {
controller.xaxis = index;
controller.xinvert = inverted;
}
case "lefty" -> {
controller.yaxis = index;
controller.yinvert = inverted;
}
}
} else if (isButton) {
switch (target) {
case "a" -> controller.button0 = index;
case "b" -> controller.button1 = index;
case "x" -> controller.button0rapid = index;
case "y" -> controller.button1rapid = index;
case "dpup" -> controller.up = index;
case "dpdown" -> controller.down = index;
case "dpleft" -> controller.left = index;
case "dpright" -> controller.right = index;
case "start" -> controller.pause = index;
}
} else {
if (target.equals("platform")) {
controller.platform = source;
if (source.toLowerCase().contains("windows")) {
os = OS.Windows;
} else if (source.toLowerCase().contains("mac")) {
os = OS.Mac;
} else if (source.toLowerCase().contains("linux")) {
os = OS.Linux;
}
}
}
}
controllerMappings.get(os).put(guid, controller);
}
}
@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;
public boolean useKeyboard = false;
@ConfigurableField(name = "Hog keypresses", shortName = "hog", description = "Key presses will not be sent to emulator.")
public boolean hogKeyboard = false;
@ConfigurableField(name = "Controller", shortName = "glfwController", description = "Physical game controller")
public DynamicSelection<String> glfwController = new DynamicSelection<>(null) {
@Override
public boolean allowNull() {
return true;
}
@Override
public LinkedHashMap<String, String> getSelections() {
// Get list of joysticks from GLFW
LinkedHashMap<String, String> selections = new LinkedHashMap<>();
selections.put("", "***Empty***");
for (int i = GLFW.GLFW_JOYSTICK_1; i <= GLFW.GLFW_JOYSTICK_LAST; i++) {
if (GLFW.glfwJoystickPresent(i)) {
selections.put(GLFW.glfwGetJoystickName(i), GLFW.glfwGetJoystickName(i));
}
}
return selections;
}
};
@ConfigurableField(name = "X Axis", shortName = "xaxis", description = "Physical game controller X Axis")
public int xaxis = 0;
@ConfigurableField(name = "Y Axis", shortName = "yaxis", description = "Physical game controller Y Axis")
public int yaxis = 1;
@ConfigurableField(name = "Button 0", shortName = "buttonA", description = "Physical game controller A button")
public int button0 = 1;
@ConfigurableField(name = "Button 0 rapid", shortName = "buttonX", description = "Physical game controller X button")
public int button0rapid = 3;
@ConfigurableField(name = "Button 1", shortName = "buttonB", description = "Physical game controller B button")
public int button1 = 2;
@ConfigurableField(name = "Button 1 rapid", shortName = "buttonX", description = "Physical game controller X button")
public int button1rapid = 4;
@ConfigurableField(name = "Manual mapping", shortName = "manual", description = "Use custom controller mapping instead of DB settings")
public boolean useManualMapping = false;
@ConfigurableField(name = "Use D-PAD", shortName = "dpad", description = "Physical game controller enable D-PAD")
public boolean useDPad = true;
@ConfigurableField(name = "Dead Zone", shortName = "deadZone", description = "Dead zone for joystick (0-1)")
public static float deadZone = 0.095f;
@ConfigurableField(name = "Sensitivity", shortName = "sensitivity", description = "Joystick value mutiplier")
public static float sensitivity = 1.1f;
@ConfigurableField(name = "Rapid fire interval (ms)", shortName = "rapidfire", description = "Interval for rapid fire (ms)")
public int rapidFireInterval = 16;
Integer controllerNumber = null;
ControllerMapping controllerMapping = null;
public Integer getControllerNum() {
if (controllerNumber != null) {
return controllerNumber >= 0 ? controllerNumber : null;
}
String controllerName = glfwController.getValue();
if (controllerName == null || controllerName.isEmpty()) {
return null;
}
for (int i = GLFW.GLFW_JOYSTICK_1; i <= GLFW.GLFW_JOYSTICK_LAST; i++) {
if (controllerName.equals(GLFW.glfwGetJoystickName(i)) && GLFW.glfwJoystickPresent(i)) {
controllerNumber = i;
return i;
}
}
return null;
}
private boolean selectedPhysicalController() {
return getControllerNum() != null;
}
public int port;
@Stateful
public int x = 0;
@@ -62,14 +261,25 @@ public class Joystick extends Device {
private int joyY = 0;
MemorySoftSwitch xSwitch;
MemorySoftSwitch ySwitch;
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Point lastMouseLocation;
Robot robot;
Point centerPoint;
long lastPollTime = System.currentTimeMillis();
FloatBuffer axes;
ByteBuffer buttons;
public Joystick(int port, Computer computer) {
super(computer);
centerPoint = new Point(screenSize.width / 2, screenSize.height / 2);
super();
if (JaceApplication.getApplication() == null) {
return;
}
Stage stage = JaceApplication.getApplication().primaryStage;
// Register a mouse handler on the primary stage that tracks the
// mouse x/y position as a percentage of window width and height
stage.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
if (!useKeyboard && !selectedPhysicalController()) {
joyX = (int) (event.getX() / stage.getWidth() * 255);
joyY = (int) (event.getY() / stage.getHeight() * 255);
}
});
this.port = port;
if (port == 0) {
xSwitch = (MemorySoftSwitch) SoftSwitches.PDL0.getSwitch();
@@ -78,12 +288,6 @@ public class Joystick extends Device {
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;
@@ -91,50 +295,155 @@ public class Joystick extends Device {
public boolean downPressed = false;
private void readJoystick() {
ticksSinceLastRead = 0;
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;
joyX = (leftPressed ? -128 : 0) + (rightPressed ? 256 : 128);
joyY = (upPressed ? -128 : 0) + (downPressed ? 256 : 128);
} else if (readGLFWJoystick()) {
float x = -0.5f;
float y = 0.5f;
if (controllerMapping != null && !useManualMapping) {
x = axes.get(controllerMapping.xaxis) * (controllerMapping.xinvert ? -1.0f : 1.0f);
y = axes.get(controllerMapping.yaxis) * (controllerMapping.yinvert ? -1.0f : 1.0f);
} else {
joyX = 128;
if (xaxis >= 0 && xaxis < axes.capacity()) {
x = axes.get(xaxis);
}
if (yaxis >= 0 && yaxis < axes.capacity()) {
y = axes.get(yaxis);
}
}
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();
if (useDPad && controllerMapping != null) {
if (getButton(controllerMapping.left)) {
x = -1;
} else if (getButton(controllerMapping.right)) {
x = 1;
}
lastMouseLocation = l;
if (getButton(controllerMapping.up)) {
y = -1;
} else if (getButton(controllerMapping.down)) {
y = 1;
}
}
if (Math.abs(x) < deadZone) {
x = 0;
}
if (Math.abs(y) < deadZone) {
y = 0;
}
// We have to let the joystick go a little further in the positive direction
// because boulderdash is a little too sensitive!
x = Math.max(-1.0f, Math.min(1.0f, x * sensitivity));
y = Math.max(-1.0f, Math.min(1.0f, y * sensitivity));
joyX = (int) (x * 128.0 + 128.0);
joyY = (int) (y * 128.0 + 128.0);
readButtons();
}
}
public static long POLLING_TIME = 15;
public static int CALIBRATION_ITERATIONS = 15;
private void calibrateTiming() {
if (selectedPhysicalController()) {
Integer controllerNum = getControllerNum();
if (controllerNum != null) {
long start = System.currentTimeMillis();
Emulator.whileSuspended((c) -> {
for (int i = 0; i < CALIBRATION_ITERATIONS; i++) {
buttons = GLFW.glfwGetJoystickButtons(controllerNumber);
axes = GLFW.glfwGetJoystickAxes(controllerNumber);
}
});
String guid = GLFW.glfwGetJoystickGUID(controllerNumber);
long end = System.currentTimeMillis();
POLLING_TIME = (end - start) / CALIBRATION_ITERATIONS + 1;
POLLING_TIME = Math.min(POLLING_TIME*2, 45);
lastPollTime = end;
System.out.println("Calibrated polling time to " + POLLING_TIME + "ms for joystick " + guid);
}
}
}
private boolean readGLFWJoystick() {
if (System.currentTimeMillis() - lastPollTime >= POLLING_TIME) {
lastPollTime = System.currentTimeMillis();
if (selectedPhysicalController()) {
Integer controllerNum = getControllerNum();
if (controllerNum != null) {
Platform.runLater(()->{
buttons = GLFW.glfwGetJoystickButtons(controllerNumber);
axes = GLFW.glfwGetJoystickAxes(controllerNumber);
});
}
}
}
return axes != null && buttons != null;
}
long button0heldSince = 0;
long button1heldSince = 0;
boolean justPaused = false;
private boolean getButton(Integer... choices) {
for (Integer choice : choices) {
if (choice != null && choice >= 0 && choice < buttons.capacity()) {
return buttons.get(choice) != 0;
}
}
return false;
}
private void readButtons() {
if (readGLFWJoystick()) {
boolean hasMapping = !useManualMapping && controllerMapping != null;
boolean b0 = getButton(hasMapping ? controllerMapping.button0 : null, button0);
boolean b0rapid = getButton(hasMapping ? controllerMapping.button0rapid : null, button0rapid);
boolean b1 = getButton(hasMapping ? controllerMapping.button1 : null, button1);
boolean b1rapid = getButton(hasMapping ? controllerMapping.button1rapid : null, button1rapid);
boolean pause = getButton(hasMapping ? controllerMapping.pause : null);
if (b0rapid) {
if (button0heldSince == 0) {
button0heldSince = System.currentTimeMillis();
} else {
long timeHeld = System.currentTimeMillis() - button0heldSince;
int intervalNumber = (int) (timeHeld / rapidFireInterval);
b0 = (intervalNumber % 2 == 0);
}
} else {
button0heldSince = 0;
}
if (b1rapid) {
if (button1heldSince == 0) {
button1heldSince = System.currentTimeMillis();
} else {
long timeHeld = System.currentTimeMillis() - button1heldSince;
int intervalNumber = (int) (timeHeld / rapidFireInterval);
b1 = (intervalNumber % 2 == 0);
}
} else {
button1heldSince = 0;
}
if (pause) {
if (!justPaused) {
// Paste the esc character
Keyboard.pasteFromString("\u001b");
}
justPaused = true;
} else {
justPaused = false;
}
SoftSwitches.PB0.getSwitch().setState(b0 || Keyboard.isOpenApplePressed);
SoftSwitches.PB1.getSwitch().setState(b1 || Keyboard.isClosedApplePressed);
}
}
@@ -148,6 +457,8 @@ public class Joystick extends Device {
return "joy" + port;
}
int ticksSinceLastRead = Integer.MAX_VALUE;
@Override
public void tick() {
boolean finished = true;
@@ -165,7 +476,12 @@ public class Joystick extends Device {
finished = false;
}
}
if (finished) {
if (selectedPhysicalController()) {
ticksSinceLastRead++;
if (ticksSinceLastRead % 1000 == 0) {
readButtons();
}
} else if (finished) {
setRun(false);
}
}
@@ -186,12 +502,51 @@ public class Joystick extends Device {
removeListeners();
x = 0;
y = 0;
controllerNumber = null;
Integer controllerNum = getControllerNum();
if (controllerNum != null) {
OS currentOS = Utility.getOS();
OS[] searchOrder = {};
switch (currentOS) {
case Linux:
searchOrder = new OS[]{OS.Linux, OS.Windows, OS.Mac, OS.Unknown};
break;
case Mac:
searchOrder = new OS[]{OS.Mac, OS.Windows, OS.Linux, OS.Unknown};
break;
case Unknown:
searchOrder = new OS[]{OS.Unknown, OS.Linux, OS.Windows, OS.Mac};
break;
case Windows:
searchOrder = new OS[]{OS.Windows, OS.Unknown, OS.Linux, OS.Mac};
break;
default:
break;
}
String guid = GLFW.glfwGetJoystickGUID(controllerNum);
controllerMapping = null;
for (OS searchOS : searchOrder) {
if (controllerMappings.get(searchOS).containsKey(guid)) {
System.out.println("Found mapping for %s, OS=%s".formatted(guid, searchOS));
controllerMapping = controllerMappings.get(searchOS).get(guid);
break;
}
}
if (controllerMapping != null) {
System.out.println("Using controller " + controllerMapping.name);
} else {
System.out.println("No controller mapping found for " + GLFW.glfwGetJoystickGUID(controllerNum));
}
} else {
controllerMapping = null;
}
Platform.runLater(this::calibrateTiming);
registerListeners();
}
@InvokableAction(name = "Left", category = "joystick", defaultKeyMapping = "left", notifyOnRelease = true)
public boolean joystickLeft(boolean pressed) {
if (!useKeyboard) {
if (!isAttached || !useKeyboard) {
return false;
}
leftPressed = pressed;
@@ -201,10 +556,9 @@ public class Joystick extends Device {
return hogKeyboard;
}
;
@InvokableAction(name = "Right", category = "joystick", defaultKeyMapping = "right", notifyOnRelease = true)
public boolean joystickRight(boolean pressed) {
if (!useKeyboard) {
if (!isAttached || !useKeyboard) {
return false;
}
rightPressed = pressed;
@@ -214,10 +568,9 @@ public class Joystick extends Device {
return hogKeyboard;
}
;
@InvokableAction(name = "Up", category = "joystick", defaultKeyMapping = "up", notifyOnRelease = true)
public boolean joystickUp(boolean pressed) {
if (!useKeyboard) {
if (!isAttached || !useKeyboard) {
return false;
}
upPressed = pressed;
@@ -227,10 +580,9 @@ public class Joystick extends Device {
return hogKeyboard;
}
;
@InvokableAction(name = "Down", category = "joystick", defaultKeyMapping = "down", notifyOnRelease = true)
public boolean joystickDown(boolean pressed) {
if (!useKeyboard) {
if (!isAttached || !useKeyboard) {
return false;
}
downPressed = pressed;
@@ -243,20 +595,28 @@ public class Joystick extends Device {
public void initJoystickRead(RAMEvent e) {
readJoystick();
xSwitch.setState(true);
// Some games just suck and don't want to read the joystick properly
// Use larger-than-necessary values to try to get around this
if (joyX >= 254) {
joyX = 280;
}
if (joyY >= 255) {
joyY = 280;
}
x = 10 + joyX * 11;
ySwitch.setState(true);
y = 10 + joyY * 11;
e.setNewValue(computer.getVideo().getFloatingBus());
Emulator.withVideo(v->e.setNewValue(v.getFloatingBus()));
resume();
}
RAMListener listener;
private void registerListeners() {
listener = computer.getMemory().observe(RAMEvent.TYPE.ANY, 0x0c070, 0x0c07f, this::initJoystickRead);
listener = getMemory().observe("Joystick I/O", RAMEvent.TYPE.ANY, 0x0c070, 0x0c07f, this::initJoystickRead);
}
private void removeListeners() {
computer.getMemory().removeListener(listener);
getMemory().removeListener(listener);
}
}

View File

@@ -1,15 +1,15 @@
package jace.hardware;
import java.util.Calendar;
import java.util.Optional;
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;
/**
@@ -31,7 +31,7 @@ public class NoSlotClock extends Device {
public boolean patchProdosClock = false;
Optional<Label> clockIcon;
private final RAMListener listener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
private final RAMListener listener = new RAMListener("No slot clock read", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
@Override
protected void doConfig() {
setScopeStart(0x0C100);
@@ -87,8 +87,8 @@ public class NoSlotClock extends Device {
}
};
public NoSlotClock(Computer computer) {
super(computer);
public NoSlotClock() {
super();
this.clockIcon = Utility.loadIconLabel("clock.png");
this.clockIcon.ifPresent(icon -> icon.setText("No Slot Clock"));
}
@@ -113,12 +113,12 @@ public class NoSlotClock extends Device {
@Override
public void attach() {
computer.getMemory().addListener(listener);
getMemory().addListener(listener);
}
@Override
public void detach() {
computer.getMemory().removeListener(listener);
getMemory().removeListener(listener);
}
public void activateClock() {
@@ -137,7 +137,7 @@ public class NoSlotClock extends Device {
clockIcon.ifPresent(icon
-> EmulatorUILogic.addIndicator(this, icon, 1000));
if (patchProdosClock) {
CardThunderclock.performProdosPatch(computer);
CardThunderclock.performProdosPatch(getMemory());
}
}

View File

@@ -1,33 +1,24 @@
/*
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
/**
* Copyright 2024 Brendan Robert
*
* 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.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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
*/
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* */
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;
@@ -36,21 +27,29 @@ import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
import jace.Emulator;
import jace.config.ConfigurableField;
import jace.config.DynamicSelection;
import jace.config.Name;
import jace.core.Card;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
/**
* 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
* @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);
public PassportMidiInterface() {
super(true);
}
@Override
@@ -59,11 +58,11 @@ public class PassportMidiInterface extends Card {
}
// MIDI timing: 31250 BPS, 8-N-1 (roughly 3472k per second)
public static enum TIMER_MODE {
public 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
@@ -78,8 +77,9 @@ public class PassportMidiInterface extends Card {
for (MidiDevice.Info dev : devices) {
try {
MidiDevice device = MidiSystem.getMidiDevice(dev);
if (device.getMaxReceivers() > 0 || dev instanceof Synthesizer)
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);
@@ -188,8 +188,8 @@ public class PassportMidiInterface extends Card {
};
private boolean ptmStatusReadSinceIRQ = false;
// ---------------------- ACIA CONFIGURATION
private final boolean aciaInterruptOnSend = false;
private final boolean aciaInterruptOnReceive = false;
// 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;
@@ -202,13 +202,11 @@ public class PassportMidiInterface extends Card {
@Override
public void reset() {
// TODO: Deactivate card
suspend();
}
@Override
public boolean suspend() {
// TODO: Deactivate card
suspendACIA();
return super.suspend();
}
@@ -221,77 +219,70 @@ public class PassportMidiInterface extends Card {
@Override
protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) {
switch (type) {
case READ_DATA:
case READ_DATA -> {
int returnValue = 0;
switch (register) {
case ACIA_STATUS:
case ACIA_STATUS ->
returnValue = getACIAStatus();
break;
case ACIA_RECV:
case ACIA_RECV ->
returnValue = getACIARecieve();
break;
//TODO: Implement PTM registers
case TIMER_CONTROL_1:
// Technically it's not supposed to return anything...
case TIMER_CONTROL_1 -> // Technically it's not supposed to return anything...
returnValue = getPTMStatus();
break;
case TIMER_CONTROL_2:
case TIMER_CONTROL_2 ->
returnValue = getPTMStatus();
break;
case TIMER1_LSB:
case TIMER1_LSB -> {
returnValue = (int) (ptmTimer[0].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false;
}
break;
case TIMER1_MSB:
}
case TIMER1_MSB -> {
returnValue = (int) (ptmTimer[0].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[0].irqRequested = false;
}
break;
case TIMER2_LSB:
}
case TIMER2_LSB -> {
returnValue = (int) (ptmTimer[1].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false;
}
break;
case TIMER2_MSB:
}
case TIMER2_MSB -> {
returnValue = (int) (ptmTimer[1].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[1].irqRequested = false;
}
break;
case TIMER3_LSB:
}
case TIMER3_LSB -> {
returnValue = (int) (ptmTimer[2].value & 0x0ff);
if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false;
}
break;
case TIMER3_MSB:
}
case TIMER3_MSB -> {
returnValue = (int) (ptmTimer[2].value >> 8) & 0x0ff;
if (ptmStatusReadSinceIRQ) {
ptmTimer[2].irqRequested = false;
}
break;
default:
}
default ->
System.out.println("Passport midi read unrecognized, port " + register);
}
//TODO: Implement PTM registers
e.setNewValue(returnValue);
// System.out.println("Passport I/O read register " + register + " == " + returnValue);
break;
case WRITE:
}
case WRITE -> {
int v = e.getNewValue() & 0x0ff;
// System.out.println("Passport I/O write register " + register + " == " + v);
switch (register) {
case ACIA_CONTROL:
case ACIA_CONTROL ->
processACIAControl(v);
break;
case ACIA_SEND:
case ACIA_SEND ->
processACIASend(v);
break;
case TIMER_CONTROL_1:
case TIMER_CONTROL_1 -> {
if (ptmTimer3Selected) {
// System.out.println("Configuring timer 3");
ptmTimer[2].prescaledTimer = ((v & TIMER3_PRESCALED) != 0);
@@ -305,36 +296,32 @@ public class PassportMidiInterface extends Card {
}
processPTMConfiguration(ptmTimer[0], v);
}
break;
case TIMER_CONTROL_2:
// System.out.println("Configuring timer 2");
}
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:
}
case TIMER1_LSB ->
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff00) | v;
break;
case TIMER1_MSB:
case TIMER1_MSB ->
ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff) | (v << 8);
break;
case TIMER2_LSB:
case TIMER2_LSB ->
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff00) | v;
break;
case TIMER2_MSB:
case TIMER2_MSB ->
ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff) | (v << 8);
break;
case TIMER3_LSB:
case TIMER3_LSB ->
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | v;
break;
case TIMER3_MSB:
case TIMER3_MSB ->
ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | (v << 8);
break;
default:
default ->
System.out.println("Passport midi write unrecognized, port " + register);
}
break;
}
default -> {
}
}
// Nothing
}
@Override
@@ -350,7 +337,7 @@ public class PassportMidiInterface extends Card {
if (t.irqEnabled) {
// System.out.println("Timer generating interrupt!");
t.irqRequested = true;
computer.getCpu().generateInterrupt();
Emulator.withComputer(c -> c.getCpu().generateInterrupt());
ptmStatusReadSinceIRQ = false;
}
if (t.mode == TIMER_MODE.CONTINUOUS || t.mode == TIMER_MODE.FREQ_COMPARISON) {
@@ -423,6 +410,7 @@ public class PassportMidiInterface extends Card {
}
return status;
}
//------------------------------------------------------ ACIA
/*
ACIA status register
@@ -552,9 +540,8 @@ public class PassportMidiInterface extends Card {
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())
) {
if ((preferredMidiDevice.getValue() == null && dev.getName().contains("Java Sound") && dev instanceof Synthesizer)
|| preferredMidiDevice.getValue().equalsIgnoreCase(dev.getName())) {
selectedDevice = MidiSystem.getMidiDevice(dev);
break;
}
@@ -573,7 +560,6 @@ public class PassportMidiInterface extends Card {
}
private void suspendACIA() {
// TODO: Stop ACIA thread...
if (midiOut != null) {
currentMessage = new ShortMessage();
// Send a note-off on every channel
@@ -581,10 +567,10 @@ public class PassportMidiInterface extends Card {
try {
// All Notes Off
currentMessage.setMessage(0x0B0 | channel, 123, 0);
midiOut.send(currentMessage, 0);
midiOut.send(currentMessage, 0);
// All Oscillators Off
currentMessage.setMessage(0x0B0 | channel, 120, 0);
midiOut.send(currentMessage, 0);
midiOut.send(currentMessage, 0);
} catch (InvalidMidiDataException ex) {
Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex);
}

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