mirror of
https://github.com/badvision/jace.git
synced 2026-03-11 08:42:12 +00:00
Compare commits
63 Commits
2.0-Stable
...
65c02_func
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae5cf18bd | ||
|
|
519c561537 | ||
|
|
1c26ecad3d | ||
|
|
751c66c53f | ||
|
|
f34ba40ff0 | ||
|
|
45dfabfe14 | ||
|
|
d6252e5c8b | ||
|
|
9e04a05726 | ||
|
|
7f598bcdc1 | ||
|
|
dafd4453eb | ||
|
|
3a8e55d9dd | ||
|
|
1fb9f925fc | ||
|
|
2dfe146e54 | ||
|
|
bb90292ab5 | ||
|
|
39dd9d81b5 | ||
|
|
88cd03a9e6 | ||
|
|
455151abf7 | ||
|
|
9769f3282a | ||
|
|
4c6a8f976c | ||
|
|
6d1a5e7edd | ||
|
|
7e638dbf05 | ||
|
|
41310b43e7 | ||
|
|
2d2753ed99 | ||
|
|
0f475ba186 | ||
|
|
f413c1baad | ||
|
|
55d444477c | ||
|
|
6b1d75be15 | ||
|
|
d0a77855cd | ||
|
|
137988ac43 | ||
|
|
037cc35676 | ||
|
|
eaf1212249 | ||
|
|
40b92dea51 | ||
|
|
81cb05848e | ||
|
|
61e98bfc7d | ||
|
|
b9f3368696 | ||
|
|
e3b2484ec2 | ||
|
|
2155fe02e6 | ||
|
|
9674e59f1e | ||
|
|
774f706f68 | ||
|
|
cc0cead894 | ||
|
|
553d439ff8 | ||
|
|
15be6e3436 | ||
|
|
8bd9ec1781 | ||
|
|
f7ca7c198c | ||
|
|
1f2eff2e42 | ||
|
|
d4073b9096 | ||
|
|
9cee0cece9 | ||
|
|
142ee2df2a | ||
|
|
9118b83a43 | ||
|
|
8525330d53 | ||
|
|
ddc41ec84e | ||
|
|
eb776d44af | ||
|
|
ad9da99cb8 | ||
|
|
79c1ee825c | ||
|
|
0d07d65b82 | ||
|
|
769d7f4302 | ||
|
|
d8ab357d84 | ||
|
|
ce9027cb6b | ||
|
|
ede44af6d1 | ||
|
|
4425e8884d | ||
|
|
0c0b2c107c | ||
|
|
dba57e6e89 | ||
|
|
09c1d78832 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ hs_err_pid*
|
||||
*.DS_Store
|
||||
!/lib/nestedvm.jar
|
||||
_acme_tmp*
|
||||
.vscode/settings.json
|
||||
|
||||
1
.java-version
Normal file
1
.java-version
Normal file
@@ -0,0 +1 @@
|
||||
graalvm64-17.0.3
|
||||
476
LICENSE
476
LICENSE
@@ -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.
|
||||
|
||||
76
README.md
76
README.md
@@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||
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, and remain Apache-licensed, 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.) Also, should you want to use Jace under the terms of the Apache-license for commercial works, you are under no obligation to contribute any source code modifications or royalties to me, but I would appreciate you credit and mention my project and let me know about it.
|
||||
|
||||
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
101
build.sh
@@ -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
BIN
images/airheart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
images/colors.png
Normal file
BIN
images/colors.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 581 KiB |
BIN
images/desktop2.png
Normal file
BIN
images/desktop2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
BIN
images/donate.png
Normal file
BIN
images/donate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
354
pom.xml
354
pom.xml
@@ -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.1</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>jace</name>
|
||||
<name>Jace</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.4</lwjgl.version>
|
||||
</properties>
|
||||
|
||||
<organization>
|
||||
@@ -24,75 +27,166 @@
|
||||
<finalName>Jace</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.23</version>
|
||||
<configuration>
|
||||
<mainClass>jace.JaceApplication</mainClass>
|
||||
<resourcesList>ceAppl
|
||||
<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.2.2.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.35</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,171 @@
|
||||
<artifactId>nestedvm</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-web</artifactId>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-graphics</artifactId>
|
||||
<version>21.0.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-swing</artifactId>
|
||||
<version>21.0.4</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.1</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.13.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-macos-arm64</id>
|
||||
<activation>
|
||||
<os>
|
||||
<family>mac</family>
|
||||
<arch>aarch64</arch>
|
||||
</os>
|
||||
</activation>
|
||||
<properties>
|
||||
<lwjgl.natives>natives-macos-arm64</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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,144 @@ 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();
|
||||
if (instance.computer.getVideo() != null) {
|
||||
instance.computer.getVideo().suspend();
|
||||
instance.computer.getVideo().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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = () -> {
|
||||
@@ -383,28 +365,28 @@ public class Apple2e extends Computer {
|
||||
return;
|
||||
}
|
||||
int row = 2;
|
||||
for (String s : new String[]{
|
||||
" Welcome to",
|
||||
" _ __ ___ ____ ",
|
||||
" | | / /\\ / / ` | |_ ",
|
||||
" \\_|_| /_/--\\ \\_\\_, |_|__ ",
|
||||
"",
|
||||
" Java Apple Computer Emulator",
|
||||
"",
|
||||
" Presented by BLuRry",
|
||||
" http://goo.gl/SnzqG",
|
||||
"",
|
||||
"To insert a disk, please drag it over",
|
||||
"this window and drop on the desired",
|
||||
"drive icon.",
|
||||
"",
|
||||
"Press CTRL+SHIFT+C for configuration.",
|
||||
"Press CTRL+SHIFT+I for IDE window.",
|
||||
"",
|
||||
"O-A: Alt/Option",
|
||||
"C-A: Shortcut/Command",
|
||||
"Reset: Delete/Backspace"
|
||||
}) {
|
||||
for (String s : """
|
||||
Welcome to
|
||||
_ __ ___ ____
|
||||
| | / /\\ / / ` | |_
|
||||
\\_|_| /_/--\\ \\_\\_, |_|__
|
||||
|
||||
Java Apple Computer Emulator
|
||||
|
||||
Presented by Brendan Robert
|
||||
https://github.com/badvision/jace
|
||||
|
||||
To insert a disk, please drag it over
|
||||
this window and drop on the desired
|
||||
drive icon.
|
||||
|
||||
Press CTRL+SHIFT+C for configuration.
|
||||
Press CTRL+SHIFT+I for IDE window.
|
||||
|
||||
O-A: Alt/Option
|
||||
C-A: Shortcut/Command
|
||||
Reset: Delete/Backspace"""
|
||||
.split("\n")) {
|
||||
int addr = 0x0401 + VideoDHGR.calculateTextOffset(row++);
|
||||
for (char c : s.toCharArray()) {
|
||||
getMemory().write(addr++, (byte) (c | 0x080), false, true);
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +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.apple2e;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
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.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* This is a full implementation of a MOS-65c02 processor, including the BBR,
|
||||
@@ -40,8 +39,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 +71,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 +92,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 +136,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 +309,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 +343,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 +361,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 +406,27 @@ 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);
|
||||
}
|
||||
// False read
|
||||
cpu.getMemory().read(address, TYPE.READ_DATA, true, false);
|
||||
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);
|
||||
}
|
||||
// False read
|
||||
cpu.getMemory().read(address, TYPE.READ_DATA, true, false);
|
||||
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
|
||||
cpu.setPageBoundaryApplied(true);
|
||||
return address;
|
||||
}),
|
||||
ZP_REL(3, "$~1,$R", new AddressCalculator() {
|
||||
@@ -444,6 +437,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 +448,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 +458,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 +490,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 +510,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 +543,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 +586,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 +645,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 +675,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 +703,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 +747,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 +771,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 +899,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 +945,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 +958,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 +994,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 +1031,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 +1050,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 +1097,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 +1126,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 +1162,8 @@ public class MOS65C02 extends CPU {
|
||||
|
||||
@Override
|
||||
public void JSR(int address) {
|
||||
pushPC();
|
||||
setProgramCounter(address);
|
||||
pushPC();
|
||||
setProgramCounter(address);
|
||||
}
|
||||
|
||||
public void BRK() {
|
||||
@@ -1231,10 +1201,6 @@ public class MOS65C02 extends CPU {
|
||||
}
|
||||
}
|
||||
|
||||
public int getSTACK() {
|
||||
return STACK;
|
||||
}
|
||||
|
||||
// Cold/Warm boot procedure
|
||||
@Override
|
||||
public void reset() {
|
||||
@@ -1249,7 +1215,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 +1249,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 +1287,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 +1316,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.core.RAMEvent;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
|
||||
/**
|
||||
@@ -25,32 +24,30 @@ import jace.core.RAMEvent.TYPE;
|
||||
* @author Brendan Robert (BLuRry) brendan.robert@gmail.com
|
||||
*/
|
||||
public class Memory2SoftSwitch extends MemorySoftSwitch {
|
||||
public Memory2SoftSwitch(String name, int offAddress, int onAddress, int queryAddress, TYPE changeType, Boolean initalState) {
|
||||
super(name, offAddress, onAddress, queryAddress, changeType, initalState);
|
||||
}
|
||||
|
||||
public Memory2SoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, TYPE changeType, Boolean initalState) {
|
||||
super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState);
|
||||
}
|
||||
|
||||
// The switch must be set true two times in a row before it will actually be set.
|
||||
int count = 0;
|
||||
int readCount = 0;
|
||||
@Override
|
||||
public void setState(boolean newState) {
|
||||
public void setState(boolean newState, RAMEvent e) {
|
||||
if (!newState) {
|
||||
count = 0;
|
||||
super.setState(newState);
|
||||
super.setState(false);
|
||||
readCount = 0;
|
||||
} else {
|
||||
count++;
|
||||
if (count >= 2) {
|
||||
super.setState(newState);
|
||||
count = 0;
|
||||
if (e.getType().isRead()) {
|
||||
readCount++;
|
||||
} else {
|
||||
readCount = 0;
|
||||
}
|
||||
if (readCount >= 2) {
|
||||
super.setState(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName()+(getState()?":1":":0")+"~~"+count;
|
||||
return getName()+(getState()?":1":":0")+"~~"+readCount;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,36 +199,42 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move variables around to accommodate bigger program
|
||||
* @param programEnd Program ending address
|
||||
*/
|
||||
private void relocateVariables(int programEnd) {
|
||||
RAM memory = Emulator.computer.memory;
|
||||
int currentEnd = memory.readWordRaw(END_OF_PROG_POINTER);
|
||||
memory.writeWord(END_OF_PROG_POINTER, programEnd, false, true);
|
||||
if (programEnd > currentEnd) {
|
||||
int diff = programEnd - currentEnd;
|
||||
int himem = memory.readWordRaw(HIMEM);
|
||||
for (int i=himem - diff; i >= programEnd; i--) {
|
||||
memory.write(i+diff, memory.readRaw(i), false, true);
|
||||
public void relocateVariables(int programEnd) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
85
src/main/java/jace/assembly/ACME_README.md
Normal file
85
src/main/java/jace/assembly/ACME_README.md
Normal 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. :)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
119
src/main/java/jace/cheat/ProgramIdentity.java
Normal file
119
src/main/java/jace/cheat/ProgramIdentity.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
352
src/main/java/jace/cheat/WolfensteinCheats.java
Normal file
352
src/main/java/jace/cheat/WolfensteinCheats.java
Normal 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
|
||||
*/
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -31,6 +31,7 @@ import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class ConfigurationUIController {
|
||||
@@ -88,24 +89,35 @@ 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();
|
||||
getExpandedNodes("", deviceTree.getRoot(), expanded);
|
||||
deviceTree.setRoot(Configuration.BASE);
|
||||
for (ConfigNode node : Configuration.BASE.getChildren()) {
|
||||
String prefix = node.name;
|
||||
expanded.add(prefix);
|
||||
for (ConfigNode child : node.getChildren()) {
|
||||
expanded.add(prefix + DELIMITER + child.toString());
|
||||
for (ConfigNode grandchild : node.getChildren()) {
|
||||
expanded.add(prefix + DELIMITER + child.toString() + DELIMITER + grandchild.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
setExpandedNodes("", deviceTree.getRoot(), expanded);
|
||||
setCurrentNodePath(current);
|
||||
}
|
||||
|
||||
private void getExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
|
||||
if (root == null) return;
|
||||
root.getChildren().stream().filter((item) -> (item.isExpanded())).forEach((item) -> {
|
||||
String name = prefix+item.toString();
|
||||
root.getChildren().stream().filter(TreeItem::isExpanded).forEach((item) -> {
|
||||
String name = prefix+ item;
|
||||
expanded.add(name);
|
||||
getExpandedNodes(name+DELIMITER, item, expanded);
|
||||
});
|
||||
@@ -113,7 +125,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 +145,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 +176,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 +197,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.");
|
||||
}
|
||||
// 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 {
|
||||
@@ -236,32 +248,54 @@ public class ConfigurationUIController {
|
||||
return buildTextField(node, settingName, value, null);
|
||||
}
|
||||
} else if (type.equals(File.class)) {
|
||||
// TODO: Add file support!
|
||||
} else if (Class.class.isEnum()) {
|
||||
// TODO: Add enumeration support!
|
||||
return buildFileField(node, settingName, value);
|
||||
} else if (ISelection.class.isAssignableFrom(type)) {
|
||||
return buildDynamicSelectComponent(node, settingName, value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE: This was written but not tested/used currently. Test before using!
|
||||
private Node buildFileField(ConfigNode node, String settingName, Serializable value) {
|
||||
// Create a label that shows the name of the file and lets you select a file when the label is clicked.
|
||||
HBox hbox = new HBox();
|
||||
Label label = new Label(value == null ? "" : ((File) value).getName());
|
||||
label.setMinWidth(150.0);
|
||||
label.getStyleClass().add("setting-file-label");
|
||||
label.setOnMouseClicked((e) -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
File file = fileChooser.showOpenDialog(label.getScene().getWindow());
|
||||
if (file != null) {
|
||||
node.setFieldValue(settingName, file);
|
||||
label.setText(file.getName());
|
||||
}
|
||||
});
|
||||
hbox.getChildren().add(label);
|
||||
// Add a button that lets you clear the file selection.
|
||||
Label clearButton = new Label("Clear");
|
||||
clearButton.getStyleClass().add("setting-file-clear");
|
||||
clearButton.setOnMouseClicked((e) -> {
|
||||
node.setFieldValue(settingName, null);
|
||||
label.setText("");
|
||||
});
|
||||
return hbox;
|
||||
|
||||
}
|
||||
|
||||
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 +318,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);
|
||||
|
||||
7
src/main/java/jace/config/DeviceEnum.java
Normal file
7
src/main/java/jace/config/DeviceEnum.java
Normal 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);
|
||||
}
|
||||
80
src/main/java/jace/config/DeviceSelection.java
Normal file
80
src/main/java/jace/config/DeviceSelection.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
149
src/main/java/jace/config/InvokableActionRegistry.java
Normal file
149
src/main/java/jace/config/InvokableActionRegistry.java
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
226
src/main/java/jace/config/InvokableActionRegistryImpl.java
Normal file
226
src/main/java/jace/config/InvokableActionRegistryImpl.java
Normal 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"});
|
||||
putStaticAction(annotation.name(), jace.core.Computer.class, annotation, (b) -> {
|
||||
try {
|
||||
jace.core.Computer.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Emulator;
|
||||
import jace.JaceApplication;
|
||||
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,105 @@ 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 static void invokeReset() {
|
||||
System.out.println("Resetting computer");
|
||||
Emulator.withComputer(Computer::coldStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +209,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 +234,6 @@ public abstract class Computer implements Reconfigurable {
|
||||
stateManager = null;
|
||||
StateManager.getInstance(this).invalidate();
|
||||
}
|
||||
Configuration.registerKeyHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
150
src/main/java/jace/core/IndependentTimedDevice.java
Normal file
150
src/main/java/jace/core/IndependentTimedDevice.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
181
src/main/java/jace/core/Media.java
Normal file
181
src/main/java/jace/core/Media.java
Normal 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;
|
||||
}
|
||||
}
|
||||
134
src/main/java/jace/core/MediaPlayer.java
Normal file
134
src/main/java/jace/core/MediaPlayer.java
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -115,8 +114,7 @@ public abstract class SoftSwitch {
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
if (!exclusionActivate.contains(e.getAddress())) {
|
||||
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
|
||||
setState(!getState());
|
||||
setState(!getState(), e);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -135,7 +133,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,11 +143,11 @@ 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());
|
||||
setState(true);
|
||||
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName());
|
||||
setState(true, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -168,7 +166,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);
|
||||
@@ -178,8 +176,8 @@ public abstract class SoftSwitch {
|
||||
@Override
|
||||
protected void doEvent(RAMEvent e) {
|
||||
if (!exclusionDeactivate.contains(e.getAddress())) {
|
||||
setState(false);
|
||||
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" disables switch "+getName());
|
||||
setState(false, e);
|
||||
// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" disables switch "+getName());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -200,7 +198,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 +237,29 @@ 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;
|
||||
}
|
||||
|
||||
// Most softswitches act the same regardless of the ram event triggering them
|
||||
// But some softswitches are a little tricky (such as language card write) and need to assert extra conditions
|
||||
public void setState(boolean newState, RAMEvent e) {
|
||||
setState(newState);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +1,157 @@
|
||||
/*
|
||||
* 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 int NTSC_1MHZ = 1020484;
|
||||
public static final int PAL_1MHZ = 1015625;
|
||||
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;
|
||||
waitUntil = null;
|
||||
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;
|
||||
resetSyncTimer();
|
||||
}
|
||||
|
||||
public final boolean isMaxSpeedEnabled() {
|
||||
return maxspeed;
|
||||
}
|
||||
|
||||
public final boolean isMaxSpeed() {
|
||||
return forceMaxspeed || maxspeed || tempSpeedDuration > 0;
|
||||
}
|
||||
|
||||
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 +163,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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
44
src/main/java/jace/hardware/Cards.java
Normal file
44
src/main/java/jace/hardware/Cards.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/main/java/jace/hardware/FPSMonitorDevice.java
Normal file
118
src/main/java/jace/hardware/FPSMonitorDevice.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,31 +97,38 @@ 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...
|
||||
}
|
||||
|
||||
public static enum SectorOrder {
|
||||
DOS(DOS_33_SECTOR_ORDER), PRODOS(PRODOS_SECTOR_ORDER), UNKNOWN(DOS_33_SECTOR_ORDER);
|
||||
public final int[] sectors;
|
||||
SectorOrder(int[] sectors) {
|
||||
this.sectors = sectors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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, diskFile.getName().toUpperCase().endsWith(".PO") ? SectorOrder.PRODOS : SectorOrder.DOS);
|
||||
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, SectorOrder assumedOrder) throws IOException {
|
||||
isNibblizedImage = true;
|
||||
volumeNumber = CardDiskII.DEFAULT_VOLUME_NUMBER;
|
||||
headerLength = 0;
|
||||
SectorOrder sectorOrder = SectorOrder.UNKNOWN;
|
||||
try {
|
||||
int bytesRead = diskFile.read(nibbles);
|
||||
if (bytesRead == DISK_2MG_NIB_LENGTH) {
|
||||
@@ -137,13 +142,38 @@ public class FloppyDisk {
|
||||
if (bytesRead == DISK_2MG_NON_NIB_LENGTH) {
|
||||
bytesRead -= 0x040;
|
||||
// Try to pick up correct sector ordering and volume from 2MG header.
|
||||
prodosOrder = (nibbles[12] == 01);
|
||||
if (nibbles[12] == 01) {
|
||||
sectorOrder = SectorOrder.PRODOS;
|
||||
} else {
|
||||
sectorOrder = SectorOrder.DOS;
|
||||
}
|
||||
volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254;
|
||||
nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length);
|
||||
|
||||
headerLength = 0x040;
|
||||
}
|
||||
currentSectorOrder = prodosOrder ? PRODOS_SECTOR_ORDER : DOS_33_SECTOR_ORDER;
|
||||
|
||||
if (sectorOrder == SectorOrder.UNKNOWN) {
|
||||
if (isProdosVolumeBlock(nibbles, 0x0400)) {
|
||||
if (DEBUG) {
|
||||
System.out.println("Prodos volume block found at 0x0400");
|
||||
}
|
||||
sectorOrder = SectorOrder.PRODOS;
|
||||
} else if (isProdosVolumeBlock(nibbles, 0x0B00)) {
|
||||
if (DEBUG) {
|
||||
System.out.println("Prodos volume block found at 0x0B00");
|
||||
}
|
||||
sectorOrder = SectorOrder.DOS;
|
||||
}
|
||||
}
|
||||
if (sectorOrder == SectorOrder.UNKNOWN) {
|
||||
if (DEBUG) {
|
||||
System.out.println("Assuming sector order based on file extension");
|
||||
}
|
||||
sectorOrder = assumedOrder;
|
||||
}
|
||||
System.out.println(null == sectorOrder ? "Sector order is null" : "Sector order is " + sectorOrder.name());
|
||||
currentSectorOrder = sectorOrder.sectors;
|
||||
if (bytesRead == DISK_PLAIN_LENGTH) {
|
||||
isNibblizedImage = false;
|
||||
nibbles = nibblize(nibbles);
|
||||
@@ -156,10 +186,27 @@ public class FloppyDisk {
|
||||
} catch (IOException ex) {
|
||||
throw ex;
|
||||
}
|
||||
StateManager.markDirtyValue(nibbles, computer);
|
||||
StateManager.markDirtyValue(currentSectorOrder, computer);
|
||||
StateManager.markDirtyValue(nibbles);
|
||||
StateManager.markDirtyValue(currentSectorOrder);
|
||||
}
|
||||
|
||||
private boolean isProdosVolumeBlock(byte[] nibbles, int offset) {
|
||||
// First two bytes are zero (no previous block)
|
||||
if (nibbles[offset] != 0 || nibbles[offset+1] != 0) {
|
||||
return false;
|
||||
}
|
||||
// Next two bytes are either both zero or at least in the range of 3...280
|
||||
int nextBlock = (nibbles[offset+2] & 0x0ff) | (nibbles[offset+3] << 8);
|
||||
if (nextBlock == 1 || nextBlock == 2 || nextBlock > 280) {
|
||||
return false;
|
||||
}
|
||||
// Now check total blocks at offset 0x29
|
||||
int totalBlocks = (nibbles[offset+0x29] & 0x0ff) | (nibbles[offset+0x2a] << 8);
|
||||
if (totalBlocks != 280) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
* Convert a block-format disk to a 6-by-2 nibblized encoding scheme (raw NIB disk format)
|
||||
*/
|
||||
@@ -180,6 +227,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 +278,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 +356,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 +382,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 +392,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
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301 USA
|
||||
*/
|
||||
/**
|
||||
* 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.Scene;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
|
||||
/**
|
||||
* Simple implementation of joystick support that supports mouse or keyboard.
|
||||
@@ -46,13 +55,205 @@ 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)) {
|
||||
// System.out.println("Detected " + GLFW.glfwGetJoystickName(i) + ": " + GLFW.glfwGetJoystickGUID(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 +263,57 @@ 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;
|
||||
}
|
||||
Scene scene = JaceApplication.getApplication().primaryStage.getScene();
|
||||
// Register a mouse handler on the primary stage that tracks the
|
||||
// mouse x/y position as a percentage of window width and height
|
||||
scene.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
|
||||
if (!useKeyboard && !selectedPhysicalController()) {
|
||||
joyX = (int) (event.getX() / scene.getWidth() * 255);
|
||||
joyY = (int) (event.getY() / scene.getHeight() * 255);
|
||||
}
|
||||
});
|
||||
scene.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (!useKeyboard && !selectedPhysicalController()) {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (port == 0) {
|
||||
SoftSwitches.PB0.getSwitch().setState(true);
|
||||
Keyboard.isOpenApplePressed = true;
|
||||
} else {
|
||||
SoftSwitches.PB2.getSwitch().setState(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
Keyboard.isClosedApplePressed = true;
|
||||
SoftSwitches.PB1.getSwitch().setState(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
scene.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (port == 0) {
|
||||
SoftSwitches.PB0.getSwitch().setState(false);
|
||||
Keyboard.isOpenApplePressed = false;
|
||||
} else {
|
||||
SoftSwitches.PB2.getSwitch().setState(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getButton() == MouseButton.SECONDARY) {
|
||||
Keyboard.isClosedApplePressed = false;
|
||||
SoftSwitches.PB1.getSwitch().setState(false);
|
||||
}
|
||||
});
|
||||
this.port = port;
|
||||
if (port == 0) {
|
||||
xSwitch = (MemorySoftSwitch) SoftSwitches.PDL0.getSwitch();
|
||||
@@ -78,12 +322,25 @@ 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);
|
||||
}
|
||||
new Thread(()->{
|
||||
try {
|
||||
// We have to wait for the the library to be loaded and the UI to be active
|
||||
// Otherwise this will fail
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (port == 0 && glfwController.getSelections().keySet().size() == 2) {
|
||||
// Get the entry that is not null
|
||||
glfwController.setValue(glfwController.getSelections().keySet().stream().filter(s->s != null && !s.isBlank()).findFirst().get());
|
||||
System.out.println("Using device for joystick: " + glfwController.getValue());
|
||||
useKeyboard = false;
|
||||
useDPad = true;
|
||||
reconfigure();
|
||||
} else {
|
||||
System.out.println("Using device for joystick: " + (useKeyboard ? "keyboard" : "mouse"));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
public boolean leftPressed = false;
|
||||
public boolean rightPressed = false;
|
||||
@@ -91,50 +348,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 +510,8 @@ public class Joystick extends Device {
|
||||
return "joy" + port;
|
||||
}
|
||||
|
||||
int ticksSinceLastRead = Integer.MAX_VALUE;
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
boolean finished = true;
|
||||
@@ -165,7 +529,12 @@ public class Joystick extends Device {
|
||||
finished = false;
|
||||
}
|
||||
}
|
||||
if (finished) {
|
||||
if (selectedPhysicalController()) {
|
||||
ticksSinceLastRead++;
|
||||
if (ticksSinceLastRead % 1000 == 0) {
|
||||
readButtons();
|
||||
}
|
||||
} else if (finished) {
|
||||
setRun(false);
|
||||
}
|
||||
}
|
||||
@@ -186,6 +555,45 @@ 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();
|
||||
}
|
||||
|
||||
@@ -201,7 +609,6 @@ public class Joystick extends Device {
|
||||
return hogKeyboard;
|
||||
}
|
||||
|
||||
;
|
||||
@InvokableAction(name = "Right", category = "joystick", defaultKeyMapping = "right", notifyOnRelease = true)
|
||||
public boolean joystickRight(boolean pressed) {
|
||||
if (!useKeyboard) {
|
||||
@@ -214,7 +621,6 @@ public class Joystick extends Device {
|
||||
return hogKeyboard;
|
||||
}
|
||||
|
||||
;
|
||||
@InvokableAction(name = "Up", category = "joystick", defaultKeyMapping = "up", notifyOnRelease = true)
|
||||
public boolean joystickUp(boolean pressed) {
|
||||
if (!useKeyboard) {
|
||||
@@ -227,7 +633,6 @@ public class Joystick extends Device {
|
||||
return hogKeyboard;
|
||||
}
|
||||
|
||||
;
|
||||
@InvokableAction(name = "Down", category = "joystick", defaultKeyMapping = "down", notifyOnRelease = true)
|
||||
public boolean joystickDown(boolean pressed) {
|
||||
if (!useKeyboard) {
|
||||
@@ -243,20 +648,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user