├── .github
└── workflows
│ └── maven.yml
├── .gitignore
├── LICENSE
├── README.md
├── README_zh.md
├── SNAPSHOTS
├── DuckTables.png
├── Main.png
├── MemoryView.png
├── Super Mario.png
├── assemblera.png
└── img.png
├── app
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ ├── cn
│ │ │ └── navclub
│ │ │ │ └── nes4j
│ │ │ │ └── app
│ │ │ │ ├── INes.java
│ │ │ │ ├── Launcher.java
│ │ │ │ ├── assets
│ │ │ │ └── FXResource.java
│ │ │ │ ├── audio
│ │ │ │ ├── JavaXAudio.java
│ │ │ │ └── NativePlayer.java
│ │ │ │ ├── config
│ │ │ │ ├── EventBusAddress.java
│ │ │ │ └── NESConfig.java
│ │ │ │ ├── control
│ │ │ │ ├── BreakLine.java
│ │ │ │ ├── CPUControlPane.java
│ │ │ │ ├── GTCMenuProvider.java
│ │ │ │ ├── GameTray.java
│ │ │ │ ├── IconPopup.java
│ │ │ │ ├── LoadingPane.java
│ │ │ │ ├── PPUControlPane.java
│ │ │ │ ├── SearchTextField.java
│ │ │ │ ├── Tile.java
│ │ │ │ └── skin
│ │ │ │ │ └── IconPopupSkin.java
│ │ │ │ ├── dialog
│ │ │ │ ├── DHandle.java
│ │ │ │ ├── DNesHeader.java
│ │ │ │ ├── DPalette.java
│ │ │ │ └── ExceptionDialog.java
│ │ │ │ ├── event
│ │ │ │ ├── DragEventHandler.java
│ │ │ │ ├── GameEventWrap.java
│ │ │ │ └── NodeDragEvent.java
│ │ │ │ ├── model
│ │ │ │ ├── GTreeItem.java
│ │ │ │ └── KeyMapper.java
│ │ │ │ ├── service
│ │ │ │ ├── LoadingService.java
│ │ │ │ └── TaskService.java
│ │ │ │ ├── util
│ │ │ │ ├── IOUtil.java
│ │ │ │ ├── JsonUtil.java
│ │ │ │ ├── OSUtil.java
│ │ │ │ ├── StrUtil.java
│ │ │ │ └── UIUtil.java
│ │ │ │ └── view
│ │ │ │ ├── Debugger.java
│ │ │ │ ├── GameHall.java
│ │ │ │ ├── GameWorld.java
│ │ │ │ └── PPUViewer.java
│ │ └── module-info.java
│ └── resources
│ │ └── cn
│ │ └── navclub
│ │ └── nes4j
│ │ └── app
│ │ └── assets
│ │ ├── css
│ │ ├── Common.css
│ │ ├── DException.css
│ │ ├── DHandle.css
│ │ ├── DNesHeaderStyle.css
│ │ ├── DebuggerStyle.css
│ │ ├── GameHallStyle.css
│ │ ├── GameWorldStyle.css
│ │ ├── Nes4j.css
│ │ ├── SystemPalette.css
│ │ └── TextPopup.css
│ │ ├── fxml
│ │ ├── DNesHeader.fxml
│ │ ├── Debugger.fxml
│ │ ├── GameHall.fxml
│ │ └── GameWorld.fxml
│ │ ├── img
│ │ ├── bin.png
│ │ ├── delete.png
│ │ ├── empty.png
│ │ ├── game.png
│ │ ├── handler
│ │ │ ├── down.png
│ │ │ ├── left.png
│ │ │ ├── right.png
│ │ │ └── up.png
│ │ ├── icon.png
│ │ ├── launcher.png
│ │ ├── loading.png
│ │ ├── max.png
│ │ ├── mwin.png
│ │ ├── nes4j.png
│ │ ├── poster.png
│ │ ├── rrun.png
│ │ ├── run.png
│ │ ├── speed.png
│ │ ├── stepinto.png
│ │ ├── stepout.png
│ │ └── xwin.png
│ │ └── language
│ │ ├── nes4j.properties
│ │ └── nes4j_zh_CN.properties
│ └── native
│ ├── CMakeLists.txt
│ ├── include
│ ├── asoundlib.h
│ ├── jni
│ │ ├── cn_navclub_nes4j_app_audio_NativePlayer.h
│ │ └── jni_util.h
│ ├── memory.h
│ ├── str_util.h
│ ├── sys_sound.h
│ └── type.h
│ ├── jni
│ ├── apu_player_jni.c
│ └── jni_util.c
│ ├── memory.c
│ ├── str_util.c
│ ├── sys_sound.c
│ └── test.c
├── assembly
├── common
│ ├── ascii.chr
│ └── nes.inc
├── make
├── nes.conf
├── nes4j.s
└── test_enable_render_vbl.s
├── bin
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ ├── cn
│ │ │ └── navclub
│ │ │ │ └── nes4j
│ │ │ │ └── bin
│ │ │ │ ├── NesConsole.java
│ │ │ │ ├── NesConsoleHook.java
│ │ │ │ ├── apu
│ │ │ │ ├── APU.java
│ │ │ │ ├── Channel.java
│ │ │ │ ├── Envelope.java
│ │ │ │ ├── FrameCounter.java
│ │ │ │ ├── LengthCounter.java
│ │ │ │ ├── LinearCounter.java
│ │ │ │ ├── Player.java
│ │ │ │ ├── Sequencer.java
│ │ │ │ ├── SweepUnit.java
│ │ │ │ ├── Timer.java
│ │ │ │ └── impl
│ │ │ │ │ ├── DMChannel.java
│ │ │ │ │ ├── NoiseChannel.java
│ │ │ │ │ ├── PulseChannel.java
│ │ │ │ │ ├── TriangleChannel.java
│ │ │ │ │ ├── sequencer
│ │ │ │ │ ├── NoiseSequencer.java
│ │ │ │ │ ├── SeqSequencer.java
│ │ │ │ │ └── TriangleSequencer.java
│ │ │ │ │ └── timer
│ │ │ │ │ ├── Divider.java
│ │ │ │ │ └── TriangleTimer.java
│ │ │ │ ├── config
│ │ │ │ ├── AddressMode.java
│ │ │ │ ├── AudioSampleRate.java
│ │ │ │ ├── CPUInterrupt.java
│ │ │ │ ├── ChannelType.java
│ │ │ │ ├── ICPUStatus.java
│ │ │ │ ├── Instruction.java
│ │ │ │ ├── MSequencer.java
│ │ │ │ ├── NESFormat.java
│ │ │ │ ├── NMapper.java
│ │ │ │ ├── NameMirror.java
│ │ │ │ ├── NameTMirror.java
│ │ │ │ ├── PControl.java
│ │ │ │ ├── PMask.java
│ │ │ │ ├── PStatus.java
│ │ │ │ ├── Register.java
│ │ │ │ ├── TV.java
│ │ │ │ └── WS6502.java
│ │ │ │ ├── core
│ │ │ │ ├── Bus.java
│ │ │ │ ├── CPU.java
│ │ │ │ ├── Component.java
│ │ │ │ ├── Mapper.java
│ │ │ │ ├── MemoryBus.java
│ │ │ │ ├── MemoryBusAdapter.java
│ │ │ │ ├── impl
│ │ │ │ │ ├── CNMapper.java
│ │ │ │ │ ├── KonamiVRC24.java
│ │ │ │ │ ├── MMC1Mapper.java
│ │ │ │ │ ├── MMC3Mapper.java
│ │ │ │ │ ├── NRMapper.java
│ │ │ │ │ └── UXMapper.java
│ │ │ │ └── register
│ │ │ │ │ └── CPUStatus.java
│ │ │ │ ├── debug
│ │ │ │ ├── Debugger.java
│ │ │ │ ├── OpenCode.java
│ │ │ │ ├── OpenCodeFormat.java
│ │ │ │ └── Operand.java
│ │ │ │ ├── eventbus
│ │ │ │ ├── EventBus.java
│ │ │ │ ├── Message.java
│ │ │ │ ├── MessageConsumer.java
│ │ │ │ └── impl
│ │ │ │ │ ├── MessageConsumerImpl.java
│ │ │ │ │ └── MessageImpl.java
│ │ │ │ ├── function
│ │ │ │ ├── CycleDriver.java
│ │ │ │ └── GameLoopCallback.java
│ │ │ │ ├── io
│ │ │ │ ├── Cartridge.java
│ │ │ │ └── JoyPad.java
│ │ │ │ ├── logging
│ │ │ │ ├── Level.java
│ │ │ │ ├── LoggerDelegate.java
│ │ │ │ ├── LoggerFactory.java
│ │ │ │ ├── formatter
│ │ │ │ │ └── NFormatter.java
│ │ │ │ ├── handler
│ │ │ │ │ └── JUConsoleHandler.java
│ │ │ │ └── impl
│ │ │ │ │ └── JULoggerDelegate.java
│ │ │ │ ├── ppu
│ │ │ │ ├── Frame.java
│ │ │ │ ├── PPU.java
│ │ │ │ ├── Render.java
│ │ │ │ └── register
│ │ │ │ │ ├── PPUControl.java
│ │ │ │ │ ├── PPUMask.java
│ │ │ │ │ └── PPUStatus.java
│ │ │ │ └── util
│ │ │ │ ├── BinUtil.java
│ │ │ │ ├── IOUtil.java
│ │ │ │ └── internal
│ │ │ │ ├── ScriptUtil.java
│ │ │ │ └── package-info.java
│ │ └── module-info.java
│ └── resources
│ │ └── cn
│ │ └── navclub
│ │ └── nes4j
│ │ └── bin
│ │ └── core
│ │ └── 6502.txt
│ └── test
│ └── resources
│ └── logback.xml
├── document
├── NESDoc.pdf
├── apu_ref.txt
├── dmc.txt
├── im_qq.jpg
├── nes4j.png
└── nessound.txt
├── folder.svg
└── pom.xml
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Java CI with Maven
10 |
11 | on:
12 | push:
13 | branches: [ "master" ]
14 | pull_request:
15 | branches: [ "master" ]
16 |
17 | jobs:
18 | build:
19 |
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: Set up JDK 21
25 | uses: actions/setup-java@v3
26 | with:
27 | java-version: '21'
28 | distribution: 'temurin'
29 | cache: maven
30 | - name: Build with Maven
31 | run: mvn -B package --file pom.xml
32 |
33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
34 | - name: Update dependency graph
35 | uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | target
4 | nes
5 | *.o
6 | *.nes
7 | cmake-build-debug
--------------------------------------------------------------------------------
/SNAPSHOTS/DuckTables.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/SNAPSHOTS/DuckTables.png
--------------------------------------------------------------------------------
/SNAPSHOTS/Main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/SNAPSHOTS/Main.png
--------------------------------------------------------------------------------
/SNAPSHOTS/MemoryView.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/SNAPSHOTS/MemoryView.png
--------------------------------------------------------------------------------
/SNAPSHOTS/Super Mario.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/SNAPSHOTS/Super Mario.png
--------------------------------------------------------------------------------
/SNAPSHOTS/assemblera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/SNAPSHOTS/assemblera.png
--------------------------------------------------------------------------------
/SNAPSHOTS/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/SNAPSHOTS/img.png
--------------------------------------------------------------------------------
/app/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | nes4j
7 | cn.navclub
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | 21.0.1
13 |
14 |
15 | 1.0.0
16 | app
17 |
18 |
19 |
20 | org.openjfx
21 | javafx-controls
22 | ${javafx.version}
23 |
24 |
25 | org.openjfx
26 | javafx-base
27 | ${javafx.version}
28 |
29 |
30 | org.openjfx
31 | javafx-graphics
32 | ${javafx.version}
33 |
34 |
35 | org.openjfx
36 | javafx-fxml
37 | ${javafx.version}
38 |
39 |
40 | cn.navclub
41 | nes4j-bin
42 | 1.0.6
43 |
44 |
45 |
46 | com.fasterxml.jackson.core
47 | jackson-core
48 |
49 |
50 | com.fasterxml.jackson.core
51 | jackson-databind
52 |
53 |
54 |
55 |
56 |
57 | org.apache.maven.plugins
58 | maven-compiler-plugin
59 |
60 |
61 | -h
62 | ${project.basedir}/src/native/include/jni
63 |
64 |
65 |
66 |
67 | org.openjfx
68 | javafx-maven-plugin
69 | 0.0.8
70 |
71 | nes4j
72 | true
73 | nes4j
74 |
75 | -XX:+UseZGC -Dnes4j.log.level=WARN
76 |
77 | cn.navclub.nes4j.app/cn.navclub.nes4j.app.Launcher
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/INes.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app;
2 |
3 | import cn.navclub.nes4j.app.config.EventBusAddress;
4 | import cn.navclub.nes4j.app.config.NESConfig;
5 | import cn.navclub.nes4j.app.view.GameHall;
6 | import cn.navclub.nes4j.app.view.GameWorld;
7 | import cn.navclub.nes4j.bin.eventbus.EventBus;
8 | import javafx.application.Application;
9 |
10 | import javafx.stage.Stage;
11 |
12 | import java.util.ResourceBundle;
13 |
14 | public class INes extends Application {
15 | public final static EventBus eventBus;
16 | public static final ResourceBundle RESOURCE_BUNDLE;
17 |
18 | static {
19 | eventBus = new EventBus();
20 | System.setProperty("java.util.PropertyResourceBundle.encoding", "UTF-8");
21 | RESOURCE_BUNDLE = ResourceBundle.getBundle("cn.navclub.nes4j.app.assets.language.nes4j");
22 | }
23 |
24 |
25 | @Override
26 | public void start(Stage stage) {
27 | this.localEventBus();
28 | var config = NESConfig.getInstance();
29 | //If set extra nes show game hall otherwise load extra nes game rom
30 | if (!config.isExtraNes()) {
31 | new GameHall(stage);
32 | } else {
33 | GameWorld.run(config.getExtraNes(), 3);
34 | }
35 | }
36 |
37 | @Override
38 | public void stop() throws Exception {
39 | //Close file context and release file lock
40 | Launcher.getLockFile().close();
41 | }
42 |
43 | /**
44 | * Register app internal event-bus
45 | */
46 | public void localEventBus() {
47 | //Open uri use system default browser
48 | eventBus.listener(EventBusAddress.OPEN_URI, message -> {
49 | getHostServices().showDocument(message.body());
50 | return null;
51 | });
52 | }
53 |
54 | public static String localeValue(String key) {
55 | return localeValue(key, false);
56 | }
57 |
58 | public static String localeValue(String key, boolean titleCase) {
59 | var value = RESOURCE_BUNDLE.getString(key);
60 | if (titleCase) {
61 | var arr = value.getBytes();
62 | var tb = arr[0];
63 | if (tb >= 97 && tb <= 122) {
64 | arr[0] = (byte) (tb - 32);
65 | }
66 | value = new String(arr);
67 | }
68 | return value;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/Launcher.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app;
2 |
3 | import cn.navclub.nes4j.app.config.NESConfig;
4 | import cn.navclub.nes4j.app.util.OSUtil;
5 | import cn.navclub.nes4j.bin.logging.LoggerFactory;
6 | import cn.navclub.nes4j.bin.logging.LoggerDelegate;
7 | import javafx.application.Application;
8 | import lombok.Getter;
9 |
10 | import java.io.File;
11 | import java.io.IOException;
12 | import java.io.RandomAccessFile;
13 |
14 | public class Launcher {
15 | private static final LoggerDelegate log = LoggerFactory.logger(Launcher.class);
16 |
17 | @Getter
18 | private static RandomAccessFile lockFile;
19 |
20 | public static void main(String[] args) throws Exception {
21 | if (!checkAccess()) {
22 | log.warning("Detected already exist application instance.");
23 | return;
24 | }
25 | //Register global catch thread exception
26 | Thread.setDefaultUncaughtExceptionHandler(
27 | (t, e) -> log.fatal("Catch target thread {} un-catch exception.", e, t.getName()));
28 | //Default load the last extra game rom
29 | var length = args.length;
30 | if (length > 0) {
31 | NESConfig.getInstance().setExtraNes(new File(args[length - 1]));
32 | }
33 | //Launch application
34 | Application.launch(INes.class, args);
35 | }
36 |
37 | /**
38 | * Usage to system file lock ensure application single instance
39 | *
40 | * @return If current is single instance return {@code true} otherwise {@code false}
41 | * @throws IOException {@inheritDoc}
42 | */
43 | private static boolean checkAccess() throws IOException {
44 | lockFile = new RandomAccessFile(OSUtil.workstation("process") + "mutex", "rw");
45 | var fc = lockFile.getChannel();
46 | var lock = fc.tryLock();
47 | if (lock == null) {
48 | return false;
49 | }
50 | lockFile.writeBytes(Long.toString(OSUtil.pid()));
51 | return true;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/assets/FXResource.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.assets;
2 |
3 | import cn.navclub.nes4j.app.INes;
4 | import cn.navclub.nes4j.bin.logging.LoggerDelegate;
5 | import cn.navclub.nes4j.bin.logging.LoggerFactory;
6 | import javafx.fxml.FXMLLoader;
7 | import javafx.scene.Parent;
8 | import javafx.scene.image.Image;
9 |
10 | import java.io.IOException;
11 |
12 | public class FXResource {
13 |
14 | private static final LoggerDelegate log = LoggerFactory.logger(FXResource.class);
15 |
16 | public static String loadStyleSheet(String name) {
17 | var url = FXResource.class.getResource("css/" + name);
18 | if (url == null) {
19 | log.warning("Target stylesheet {} not found.", name);
20 | return "";
21 | }
22 | return url.toExternalForm();
23 | }
24 |
25 | public static Image loadImage(String name) {
26 | var url = FXResource.class.getResource("img/" + name);
27 | if (url == null) {
28 | log.warning("Target image {} not found.", name);
29 | throw new RuntimeException("Target image:" + name + " not exist.");
30 | }
31 | try {
32 | return new Image(url.openStream());
33 | } catch (IOException e) {
34 | throw new RuntimeException(e);
35 | }
36 | }
37 |
38 | /**
39 | * 加载FXML视图文件
40 | *
41 | * @param controller FXML控制器
42 | * @param FXML视图类型
43 | * @return FXML实例
44 | */
45 | public static T loadFXML(Object controller) {
46 | var name = controller.getClass().getSimpleName();
47 | var file = String.format("fxml/%s.fxml", name);
48 | var loader = new FXMLLoader();
49 | loader.setController(controller);
50 | loader.setResources(INes.RESOURCE_BUNDLE);
51 | loader.setLocation(FXResource.class.getResource(file));
52 | try {
53 | return loader.load();
54 | } catch (IOException e) {
55 | throw new RuntimeException(e);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/audio/JavaXAudio.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.audio;
2 |
3 | import cn.navclub.nes4j.bin.apu.Player;
4 | import cn.navclub.nes4j.bin.logging.LoggerDelegate;
5 | import cn.navclub.nes4j.bin.logging.LoggerFactory;
6 |
7 | import javax.sound.sampled.*;
8 | import java.util.concurrent.locks.LockSupport;
9 |
10 |
11 | @SuppressWarnings("all")
12 | public class JavaXAudio implements Player {
13 | private final byte[] sample;
14 | private final byte[] buffer;
15 | private final Line.Info info;
16 | private final AudioFormat format;
17 | private final SourceDataLine line;
18 | private int ldx;
19 | //Current fill index
20 | private int index;
21 | private final Thread thread;
22 | private volatile boolean stop;
23 | private final static int SAMPLE_SIZE = 55;
24 |
25 | private static final LoggerDelegate log = LoggerFactory.logger(JavaXAudio.class);
26 |
27 |
28 | public JavaXAudio(Integer sampleRate) throws LineUnavailableException {
29 | this.sample = new byte[SAMPLE_SIZE];
30 | this.buffer = new byte[SAMPLE_SIZE];
31 | this.thread = new Thread(this::exec);
32 | this.format = new AudioFormat(sampleRate, 8, 1, false, false);
33 | this.info = new DataLine.Info(SourceDataLine.class, format);
34 | this.line = (SourceDataLine) AudioSystem.getLine(info);
35 |
36 | line.open(format);
37 | line.start();
38 |
39 | this.thread.start();
40 | }
41 |
42 | @Override
43 | public void output(byte sample) {
44 | this.buffer[this.index] = sample;
45 | this.index++;
46 | if (this.index == SAMPLE_SIZE) {
47 | this.index = 0;
48 | System.arraycopy(this.buffer, 0, this.sample, 0, SAMPLE_SIZE);
49 | LockSupport.unpark(this.thread);
50 | }
51 | this.index %= SAMPLE_SIZE;
52 | }
53 |
54 |
55 | private void exec() {
56 | while (!this.stop) {
57 | LockSupport.park();
58 | this.line.write(this.sample, 0, SAMPLE_SIZE);
59 | }
60 | }
61 |
62 | @Override
63 | public void stop() {
64 | this.stop = true;
65 | LockSupport.unpark(this.thread);
66 | this.line.close();
67 | }
68 |
69 | @Override
70 | public void reset() {
71 | this.index = 0;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/audio/NativePlayer.java:
--------------------------------------------------------------------------------
1 | //package cn.navclub.nes4j.app.audio;
2 | //
3 | //import cn.navclub.nes4j.bin.apu.Player;
4 | //
5 | //public class NativePlayer implements Player {
6 | // private final float[] samples;
7 | // private int index;
8 | //
9 | // public NativePlayer() {
10 | // this.samples = new float[735*2];
11 | // this.config("default", 1, 44100, 50000);
12 | // }
13 | //
14 | // @Override
15 | // public void output(float sample) {
16 | // this.samples[this.index++] = sample;
17 | // if (this.index >= this.samples.length) {
18 | // this.index = 0;
19 | // this.play(samples);
20 | // }
21 | // }
22 | //
23 | // /**
24 | // * 调用Native模块关闭音频相关资源
25 | // */
26 | // public synchronized native void stop();
27 | //
28 | // /**
29 | // * 调用Native模块播放音频样本
30 | // *
31 | // * @param samples 音频样本
32 | // * @return 返回播放样本数量
33 | // */
34 | // private native long play(float[] samples);
35 | //
36 | // /**
37 | // * 配置音频输出
38 | // *
39 | // * @param latency 延迟时长
40 | // * @param device 输出设备
41 | // * @param rate 音频采样率
42 | // * @param channel 音频输出通道
43 | // */
44 | // private synchronized native void config(String device, int channel, int rate, int latency);
45 | //}
46 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/config/EventBusAddress.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.config;
2 |
3 | public class EventBusAddress {
4 | public static final String OPEN_URI = "nes4j:app:uri";
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/config/NESConfig.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.config;
2 |
3 | import cn.navclub.nes4j.app.model.KeyMapper;
4 | import cn.navclub.nes4j.app.util.IOUtil;
5 | import cn.navclub.nes4j.app.util.JsonUtil;
6 | import cn.navclub.nes4j.app.util.OSUtil;
7 | import cn.navclub.nes4j.bin.io.JoyPad;
8 | import cn.navclub.nes4j.bin.logging.LoggerDelegate;
9 | import cn.navclub.nes4j.bin.logging.LoggerFactory;
10 | import com.fasterxml.jackson.annotation.JsonIgnore;
11 | import javafx.scene.input.KeyCode;
12 | import lombok.Data;
13 |
14 | import java.io.File;
15 | import java.nio.file.Files;
16 | import java.nio.file.Path;
17 | import java.util.Optional;
18 |
19 | @Data
20 | public class NESConfig {
21 | private static LoggerDelegate log = LoggerFactory.logger(NESConfig.class);
22 |
23 | private static final String DEFAULT_CONFIG_PATH = OSUtil.workstation() + "config.json";
24 |
25 | private static final KeyMapper[] DEFAULT_KEY_MAPPER = {
26 | new KeyMapper(JoyPad.JoypadButton.BTN_A, KeyCode.A),
27 | new KeyMapper(JoyPad.JoypadButton.BTN_B, KeyCode.S),
28 | new KeyMapper(JoyPad.JoypadButton.BTN_UP, KeyCode.UP),
29 | new KeyMapper(JoyPad.JoypadButton.BTN_DN, KeyCode.DOWN),
30 | new KeyMapper(JoyPad.JoypadButton.BTN_SE, KeyCode.SPACE),
31 | new KeyMapper(JoyPad.JoypadButton.BTN_ST, KeyCode.ENTER),
32 | new KeyMapper(JoyPad.JoypadButton.BTN_LF, KeyCode.LEFT),
33 | new KeyMapper(JoyPad.JoypadButton.BTN_RT, KeyCode.RIGHT)
34 |
35 | };
36 |
37 | private static NESConfig instance;
38 |
39 | /**
40 | * Extra Nes file
41 | */
42 | @JsonIgnore
43 | private File extraNes;
44 | /**
45 | * Handle mapper
46 | */
47 | private KeyMapper[] mapper;
48 |
49 | public KeyMapper[] getMapper() {
50 | return Optional.ofNullable(this.mapper).orElse(DEFAULT_KEY_MAPPER);
51 | }
52 |
53 | public void save() {
54 | var text = JsonUtil.toJsonStr(this);
55 | IOUtil.writeStr(Path.of(DEFAULT_CONFIG_PATH), text);
56 | }
57 |
58 | public boolean isExtraNes() {
59 | return this.extraNes != null && this.extraNes.exists();
60 | }
61 |
62 |
63 | public static synchronized NESConfig getInstance() {
64 | if (instance == null) {
65 | instance = new NESConfig();
66 | var path = Path.of(DEFAULT_CONFIG_PATH);
67 | if (Files.exists(path)) {
68 | try {
69 | var jsonStr = Files.readString(path);
70 | instance = JsonUtil.parse(jsonStr, NESConfig.class);
71 | } catch (Exception e) {
72 | log.warning("Fail to load program config file.", e);
73 | }
74 | }
75 | }
76 | return instance;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/control/GTCMenuProvider.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.control;
2 |
3 | import cn.navclub.nes4j.app.INes;
4 | import javafx.geometry.Side;
5 | import javafx.scene.control.ContextMenu;
6 | import javafx.scene.control.MenuItem;
7 |
8 | public class GTCMenuProvider {
9 | private GameTray tray;
10 | private final ContextMenu context;
11 |
12 | public GTCMenuProvider() {
13 | this.context = new ContextMenu();
14 |
15 | var runner = new MenuItem(INes.localeValue("nes4j.run"));
16 | var delete = new MenuItem(INes.localeValue("nes4j.delete"));
17 |
18 | runner.setOnAction(event -> this.tray.run());
19 | delete.setOnAction(event -> this.tray.delete());
20 |
21 | this.context.getItems().addAll(runner, delete);
22 | }
23 |
24 | public void showOnTray(GameTray tray, int dx, int dy) {
25 | assert tray != null;
26 | this.tray = tray;
27 | this.context.show(tray, Side.BOTTOM, dx, dy);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/control/GameTray.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.control;
2 |
3 | import cn.navclub.nes4j.app.INes;
4 | import cn.navclub.nes4j.app.assets.FXResource;
5 | import cn.navclub.nes4j.app.model.GTreeItem;
6 | import cn.navclub.nes4j.app.util.StrUtil;
7 | import cn.navclub.nes4j.app.util.UIUtil;
8 | import cn.navclub.nes4j.app.view.GameHall;
9 | import javafx.geometry.Side;
10 | import javafx.scene.control.ContextMenu;
11 | import javafx.scene.control.Label;
12 | import javafx.scene.control.MenuItem;
13 | import javafx.scene.image.Image;
14 | import javafx.scene.image.ImageView;
15 | import javafx.scene.input.MouseButton;
16 | import javafx.scene.layout.FlowPane;
17 | import javafx.scene.layout.Pane;
18 | import javafx.scene.layout.VBox;
19 |
20 | import java.io.File;
21 | import java.io.IOException;
22 | import java.nio.file.Files;
23 |
24 | public class GameTray extends VBox {
25 | private static final Image DEFAULT_IMAGE;
26 | private final static GTCMenuProvider GTC_MENU_PROVIDER;
27 |
28 | static {
29 | GTC_MENU_PROVIDER = new GTCMenuProvider();
30 | DEFAULT_IMAGE = FXResource.loadImage("game.png");
31 | }
32 |
33 | private final File file;
34 | private final Label label;
35 |
36 |
37 | public GameTray(File file) {
38 | this.file = file;
39 | var icon = new ImageView(DEFAULT_IMAGE);
40 | this.label = new Label(StrUtil.getFileName(file));
41 |
42 | this.getStyleClass().add("game-tray");
43 | this.getChildren().addAll(icon, this.label);
44 |
45 | this.setOnMouseClicked(event -> {
46 | var btn = event.getButton();
47 | if (btn == MouseButton.PRIMARY) {
48 | this.run();
49 | } else {
50 | GTC_MENU_PROVIDER.showOnTray(this, 0, 0);
51 | }
52 | });
53 | }
54 |
55 | public void run() {
56 | INes.eventBus.publish(GameHall.INES_OPEN_GAME, this.file);
57 | }
58 |
59 | public void delete() {
60 | var title = String.format(INes.localeValue("nes4j.game.delete"), this.label.getText());
61 | if (UIUtil.confirm(title)) {
62 | try {
63 | Files.delete(this.file.toPath());
64 | var pane = (Pane) (this.getParent());
65 | pane.getChildren().remove(this);
66 | } catch (IOException e) {
67 | UIUtil.showError(e, "", null);
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/control/IconPopup.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.control;
2 |
3 | import cn.navclub.nes4j.app.control.skin.IconPopupSkin;
4 | import javafx.animation.FadeTransition;
5 | import javafx.beans.property.ObjectProperty;
6 | import javafx.beans.property.SimpleObjectProperty;
7 | import javafx.scene.control.PopupControl;
8 | import javafx.scene.control.Skin;
9 | import javafx.scene.image.Image;
10 | import javafx.stage.Screen;
11 | import javafx.util.Duration;
12 |
13 | /**
14 | * A icon popup implement.
15 | *
16 | * @author GZYangKui
17 | */
18 | public class IconPopup extends PopupControl {
19 | private final FadeTransition transition;
20 | private final ObjectProperty image;
21 |
22 | public IconPopup() {
23 | this.setAutoFix(true);
24 | this.transition = new FadeTransition();
25 | this.transition.setToValue(0);
26 | this.transition.setFromValue(1.0);
27 | this.transition.setOnFinished(event -> this.hide());
28 | this.transition.setDuration(Duration.millis(1000));
29 | this.image = new SimpleObjectProperty<>(this, "image", null);
30 |
31 | this.setOnShown(event -> {
32 | this.transition.stop();
33 | this.transition.setNode(this.getSkin().getNode());
34 | this.transition.setDelay(Duration.millis(500));
35 | this.transition.play();
36 | this.calculateXY();
37 | });
38 | }
39 |
40 | public IconPopup(Image image) {
41 | this();
42 | this.setImage(image);
43 | }
44 |
45 | @Override
46 | protected Skin> createDefaultSkin() {
47 | return new IconPopupSkin(this);
48 | }
49 |
50 | public Image getImage() {
51 | return image.get();
52 | }
53 |
54 | public ObjectProperty imageProperty() {
55 | return image;
56 | }
57 |
58 | public void setImage(Image image) {
59 | this.image.set(image);
60 | }
61 |
62 | private void calculateXY() {
63 | var screen = Screen.getPrimary();
64 | var rect = screen.getVisualBounds();
65 |
66 | var x = (rect.getWidth() - this.getWidth()) / 2;
67 | var y = (rect.getHeight() - this.getHeight()) - 10;
68 |
69 | this.setX(x);
70 | this.setY(y);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/control/PPUControlPane.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.control;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.util.BinUtil;
5 | import javafx.scene.control.Label;
6 | import javafx.scene.control.Tab;
7 | import javafx.scene.control.TextField;
8 | import javafx.scene.layout.GridPane;
9 | import javafx.scene.layout.Priority;
10 |
11 | public class PPUControlPane extends Tab {
12 | private final TextField x = new TextField();
13 | private final TextField y = new TextField();
14 | private final TextField ctrl = new TextField();
15 | private final TextField mask = new TextField();
16 | private final TextField cycle = new TextField();
17 | private final TextField status = new TextField();
18 | private final TextField oaddr = new TextField();
19 | private final TextField paddr = new TextField();
20 | private final TextField scanline = new TextField();
21 |
22 |
23 | public PPUControlPane() {
24 | var l0 = new Label("PPUCTRL");
25 | var l1 = new Label("PPUMASK");
26 | var l2 = new Label("PPUSTAT");
27 | var l3 = new Label("OAMADDR");
28 | var l4 = new Label("PPUADDR");
29 | var l5 = new Label("Scanline");
30 | var l6 = new Label("X Scroll");
31 | var l7 = new Label("Y Scroll");
32 | var l8 = new Label("Pixel");
33 |
34 | var gridPane = new GridPane();
35 |
36 |
37 | gridPane.add(l0, 0, 0);
38 | gridPane.add(ctrl, 1, 0);
39 | gridPane.add(l1, 2, 0);
40 | gridPane.add(mask, 3, 0);
41 |
42 | gridPane.add(l2, 0, 1);
43 | gridPane.add(status, 1, 1);
44 | gridPane.add(l3, 2, 1);
45 | gridPane.add(oaddr, 3, 1);
46 |
47 | gridPane.add(l4, 0, 2);
48 | gridPane.add(paddr, 1, 2);
49 | gridPane.add(l5, 2, 2);
50 | gridPane.add(scanline, 3, 2);
51 |
52 | gridPane.add(l6, 0, 3);
53 | gridPane.add(x, 1, 3);
54 | gridPane.add(l7, 2, 3);
55 | gridPane.add(y, 3, 3);
56 | gridPane.add(l8, 0, 4);
57 | gridPane.add(cycle, 1, 4);
58 |
59 |
60 | GridPane.setHgrow(ctrl, Priority.ALWAYS);
61 | GridPane.setHgrow(mask, Priority.ALWAYS);
62 | GridPane.setHgrow(status, Priority.ALWAYS);
63 | GridPane.setHgrow(oaddr, Priority.ALWAYS);
64 |
65 | this.setText("PPU");
66 | this.setClosable(false);
67 | this.setContent(gridPane);
68 | }
69 |
70 | public void update(NesConsole console) {
71 | var ppu = console.getPpu();
72 |
73 | this.x.setText(Integer.toString(ppu.x()));
74 | this.y.setText(Integer.toString(ppu.y()));
75 | this.cycle.setText(Long.toString(ppu.getCycle() - 1));
76 | this.scanline.setText(Long.toString(ppu.getScanline()));
77 | this.paddr.setText(String.format("$%s", Integer.toHexString(ppu.getV())));
78 | this.oaddr.setText(String.format("$%s", BinUtil.toHexStr((byte) ppu.getOamAddr())));
79 | this.mask.setText(String.format("$%s", BinUtil.toHexStr(ppu.getMask().getBits())));
80 | this.ctrl.setText(String.format("$%s", BinUtil.toHexStr(ppu.getCtr().getBits())));
81 | this.status.setText(String.format("$%s", BinUtil.toHexStr(ppu.getStatus())));
82 |
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/control/SearchTextField.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.control;
2 |
3 | import cn.navclub.nes4j.app.INes;
4 | import javafx.scene.control.Label;
5 | import javafx.scene.control.TextField;
6 | import javafx.scene.layout.HBox;
7 | import javafx.scene.layout.Priority;
8 |
9 | public class SearchTextField extends HBox {
10 | private final Label prefix;
11 | private final TextField textField;
12 |
13 | public SearchTextField() {
14 | this.prefix = new Label();
15 | this.textField = new TextField();
16 | this.prefix.getStyleClass().add("prefix");
17 | this.textField.setFocusTraversable(false);
18 | this.textField.setPromptText(INes.localeValue("nes4j.search", true));
19 |
20 | HBox.setHgrow(this.textField, Priority.ALWAYS);
21 |
22 | this.getStyleClass().add("search-text-field");
23 |
24 | this.getChildren().addAll(this.prefix, this.textField);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/control/Tile.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.control;
2 |
3 | import javafx.scene.control.Label;
4 | import javafx.scene.image.Image;
5 | import javafx.scene.image.ImageView;
6 | import javafx.scene.layout.HBox;
7 | import javafx.scene.layout.VBox;
8 |
9 | public class Tile extends VBox {
10 | public Tile(final Image image, int index) {
11 | var titleBox = new HBox();
12 | this.getChildren().add(titleBox);
13 | this.getChildren().add(new ImageView(image));
14 | titleBox.getChildren().add(new Label(String.valueOf(index + 1)));
15 |
16 | titleBox.setStyle("-fx-background-color: #fefefe");
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/control/skin/IconPopupSkin.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.control.skin;
2 |
3 | import cn.navclub.nes4j.app.assets.FXResource;
4 | import cn.navclub.nes4j.app.control.IconPopup;
5 | import javafx.scene.Node;
6 | import javafx.scene.control.Skin;
7 | import javafx.scene.image.ImageView;
8 | import javafx.scene.layout.VBox;
9 |
10 |
11 | public class IconPopupSkin implements Skin {
12 | private VBox node;
13 | @SuppressWarnings("all")
14 | private final ImageView icon;
15 | private final IconPopup popup;
16 |
17 | public IconPopupSkin(IconPopup popup) {
18 | this.popup = popup;
19 | this.node = new VBox();
20 | this.icon = new ImageView();
21 |
22 | this.icon.imageProperty().bind(popup.imageProperty());
23 |
24 | this.node.getChildren().add(this.icon);
25 | this.node.getStyleClass().add("text-popup");
26 | this.node.getStylesheets().add(FXResource.loadStyleSheet("TextPopup.css"));
27 | }
28 |
29 | @Override
30 | public IconPopup getSkinnable() {
31 | return this.popup;
32 | }
33 |
34 | @Override
35 | public Node getNode() {
36 | return node;
37 | }
38 |
39 | @Override
40 | public void dispose() {
41 | this.node = null;
42 | this.icon.imageProperty().unbind();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/dialog/DNesHeader.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.dialog;
2 |
3 | import cn.navclub.nes4j.app.INes;
4 | import cn.navclub.nes4j.app.assets.FXResource;
5 | import cn.navclub.nes4j.app.util.StrUtil;
6 | import cn.navclub.nes4j.bin.config.NMapper;
7 | import cn.navclub.nes4j.bin.config.NameMirror;
8 | import cn.navclub.nes4j.bin.config.TV;
9 | import cn.navclub.nes4j.bin.io.Cartridge;
10 | import javafx.event.Event;
11 | import javafx.fxml.FXML;
12 | import javafx.scene.control.*;
13 | import javafx.scene.input.MouseEvent;
14 | import javafx.scene.layout.GridPane;
15 | import javafx.stage.Window;
16 |
17 | import java.io.File;
18 | import java.util.Arrays;
19 |
20 | public class DNesHeader extends Dialog {
21 | private static final int DEFAULT_SCALE = 3;
22 | @FXML
23 | private GridPane attrGrid;
24 | @FXML
25 | private ToggleGroup vGroup;
26 | @FXML
27 | private ChoiceBox cb0;
28 | @FXML
29 | private ChoiceBox cb1;
30 | @FXML
31 | private ChoiceBox cb2;
32 | @FXML
33 | private ChoiceBox cb3;
34 | @FXML
35 | private TextField scaleTextField;
36 | @FXML
37 | private ChoiceBox cbMapper;
38 |
39 | public DNesHeader(File file, Window owner) {
40 | Cartridge cartridge = new Cartridge(file);
41 |
42 |
43 | var pane = this.getDialogPane();
44 |
45 | pane.setContent(FXResource.loadFXML(this));
46 |
47 | //Read only attribute
48 | this.attrGrid.addEventFilter(MouseEvent.ANY, Event::consume);
49 |
50 |
51 | Arrays.stream(TV.values()).forEach(it -> this.cb3.getItems().add(it));
52 | Arrays.stream(NameMirror.values()).forEach(it -> this.cb2.getItems().add(it));
53 | Arrays.stream(NMapper.values()).forEach(it -> this.cbMapper.getItems().add(it));
54 |
55 | this.scaleTextField.setText(Integer.toString(DEFAULT_SCALE));
56 | this.cb1.getSelectionModel().select(StrUtil.toKB(cartridge.getChSize()));
57 | this.cb0.getSelectionModel().select(StrUtil.toKB(cartridge.getRgbSize()));
58 | this.cb2.getSelectionModel().select(cartridge.getMirrors().ordinal());
59 | this.cb3.getSelectionModel().select(cartridge.getTv().ordinal());
60 | this.cbMapper.getSelectionModel().select(cartridge.getMapper());
61 | this.vGroup.selectToggle(this.vGroup.getToggles().get(cartridge.getFormat().ordinal()));
62 |
63 |
64 | pane.getButtonTypes().addAll(ButtonType.APPLY);
65 | var btn = ((Button) (pane.lookupButton(ButtonType.APPLY)));
66 | if (cartridge.getMapper().isImpl()) {
67 | btn.setText(INes.localeValue("nes4j.run"));
68 | } else {
69 | btn.setText(INes.localeValue("nes4j.unsupport"));
70 | }
71 | this.initOwner(owner);
72 | this.setTitle(StrUtil.getFileName(file));
73 | this.setResultConverter(buttonType -> !(buttonType == null) && cartridge.getMapper().isImpl());
74 | }
75 |
76 | public int viewportScale() {
77 | var scale = DEFAULT_SCALE;
78 | var text = scaleTextField.getText();
79 | if (StrUtil.isNotBlank(text)) {
80 | try {
81 | scale = Integer.parseInt(text);
82 | } catch (Exception ignore) {
83 |
84 | }
85 | }
86 | return scale;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/dialog/DPalette.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.dialog;
2 |
3 | import cn.navclub.nes4j.app.assets.FXResource;
4 |
5 | import cn.navclub.nes4j.app.INes;
6 | import javafx.scene.control.*;
7 | import javafx.scene.layout.GridPane;
8 | import javafx.scene.layout.VBox;
9 | import javafx.scene.paint.Color;
10 | import javafx.scene.shape.Rectangle;
11 |
12 | public class DPalette extends Dialog {
13 | private static final String STYLE_SHEET = FXResource.loadStyleSheet("SystemPalette.css");
14 |
15 | private final int[][] copy;
16 | private final GridPane gridPane;
17 | private final ColorPicker colorPicker;
18 |
19 | private int index;
20 | private boolean manual;
21 |
22 |
23 | public DPalette(int[][] palette) {
24 | this.gridPane = new GridPane();
25 | this.colorPicker = new ColorPicker();
26 | this.copy = new int[palette.length][];
27 | var content = new VBox(this.colorPicker, this.gridPane);
28 |
29 | this.arrDeepCopy(palette, copy);
30 |
31 | this.colorPicker.valueProperty().addListener((observable, oldValue, newValue) -> {
32 | if (this.gridPane.getChildren().isEmpty() || !manual) {
33 | this.manual = true;
34 | return;
35 | }
36 |
37 | var rgb = palette[this.index];
38 |
39 | rgb[0] = (int) Math.round(newValue.getRed() * 0xff);
40 | rgb[1] = (int) Math.round(newValue.getGreen() * 0xff);
41 | rgb[2] = (int) Math.round(newValue.getBlue() * 0xff);
42 |
43 | var cell = (Rectangle) this.gridPane.getChildren().get(this.index);
44 | cell.setFill(newValue);
45 | });
46 |
47 | this.initPalette();
48 |
49 | this.getDialogPane().setContent(content);
50 | this.getDialogPane().getStylesheets().add(STYLE_SHEET);
51 | this.getDialogPane().getButtonTypes().addAll(ButtonType.APPLY, ButtonType.CANCEL);
52 |
53 | this.setTitle(INes.localeValue("nes4j.palette", true));
54 | }
55 |
56 | private void initPalette() {
57 | //Loop generate color cell grid
58 | var size = this.copy.length;
59 | for (int i = 0; i < size; i++) {
60 | var col = i % 20;
61 | var row = i / 20;
62 | var rgb = this.copy[i];
63 | var cell = new Rectangle(30, 30);
64 | cell.setFill(Color.rgb(rgb[0], rgb[1], rgb[2]));
65 | if (i == 0) {
66 | this.colorPicker.setValue((Color) cell.getFill());
67 | }
68 | int finalI = i;
69 | cell.setOnMouseClicked(event -> {
70 | this.index = finalI;
71 | this.manual = false;
72 | this.colorPicker.setValue((Color) cell.getFill());
73 | });
74 | this.gridPane.add(cell, col, row);
75 | }
76 | }
77 |
78 | private void arrDeepCopy(int[][] src, int[][] dst) {
79 | for (int i = 0; i < src.length; i++) {
80 | var s = src[i];
81 | var d = dst[i];
82 | if (d == null) {
83 | dst[i] = d = new int[s.length];
84 | }
85 | System.arraycopy(s, 0, d, 0, s.length);
86 | }
87 | }
88 |
89 | public void restore(int[][] palette) {
90 | this.arrDeepCopy(this.copy, palette);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/dialog/ExceptionDialog.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.dialog;
2 |
3 | import cn.navclub.nes4j.app.INes;
4 | import cn.navclub.nes4j.app.assets.FXResource;
5 | import javafx.scene.control.ButtonType;
6 | import javafx.scene.control.Dialog;
7 | import javafx.scene.control.TextArea;
8 |
9 | import java.io.PrintWriter;
10 | import java.io.StringWriter;
11 |
12 | /**
13 | * Custom exception dialog
14 | *
15 | * @author GZYangKui
16 | */
17 | @SuppressWarnings("all")
18 | public class ExceptionDialog extends Dialog {
19 | private final TextArea textArea;
20 | private final Throwable throwable;
21 |
22 |
23 | public ExceptionDialog(final Throwable throwable, String headerText) {
24 | this.setResizable(true);
25 |
26 | this.throwable = throwable;
27 | this.textArea = new TextArea();
28 | this.textArea.setEditable(false);
29 | this.textArea.setText(throwable2Str(throwable));
30 |
31 | this.setHeaderText(headerText);
32 | this.setTitle(INes.localeValue("nes4j.error"));
33 |
34 | this.getDialogPane().setContent(this.textArea);
35 | this.getDialogPane().setContentText(throwable.getMessage());
36 | this.getDialogPane().getButtonTypes().add(ButtonType.OK);
37 | this.getDialogPane().getStylesheets().add(FXResource.loadStyleSheet("DException.css"));
38 | }
39 |
40 |
41 | private String throwable2Str(Throwable throwable) {
42 | try (var sw = new StringWriter();
43 | var pw = new PrintWriter(sw)) {
44 | throwable.printStackTrace(pw);
45 | return sw.toString();
46 | } catch (Exception e) {
47 | return throwable2Str(new RuntimeException("Log exception parser error!"));
48 | }
49 | }
50 |
51 | public static ExceptionDialog create(Throwable throwable, String headerText) {
52 | return new ExceptionDialog(throwable, headerText);
53 | }
54 |
55 | public static void showAndWait(Throwable throwable, String headerText) {
56 | create(throwable, headerText).showAndWait();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/event/DragEventHandler.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.event;
2 |
3 | import javafx.beans.property.ObjectProperty;
4 | import javafx.beans.property.SimpleObjectProperty;
5 | import javafx.scene.Node;
6 | import javafx.scene.image.Image;
7 | import javafx.scene.input.Dragboard;
8 | import javafx.scene.input.TransferMode;
9 |
10 | import java.io.File;
11 |
12 | /**
13 | * Wrap {@link Node} drag event.
14 | *
15 | * @author GZYangKui
16 | */
17 | public class DragEventHandler {
18 | public interface DragEventService {
19 | /**
20 | * When DragOver event trigger will call this function check drag data whether satisfy condition.
21 | *
22 | * @param board {@inheritDoc}
23 | * @return If return {@code true} drag continue otherwise cancel
24 | */
25 | boolean before(Dragboard board);
26 |
27 | /**
28 | * When DragDrop event trigger will call this function and transform {@link Dragboard} object.
29 | *
30 | * @param board {@inheritDoc}
31 | * @apiNote If {@link DragEventService#before(Dragboard)} return {@code false} will can't call this function.
32 | */
33 | void after(Dragboard board);
34 | }
35 |
36 | /**
37 | * Simplify drag file operation.
38 | */
39 | public abstract static class FileDragEventService implements DragEventService {
40 | private final String suffix;
41 | private final boolean multiple;
42 |
43 | public FileDragEventService(String suffix, boolean multiple) {
44 | this.suffix = suffix;
45 | this.multiple = multiple;
46 | }
47 |
48 | @Override
49 | public boolean before(Dragboard board) {
50 | var list = board.getFiles();
51 | var ok = multiple || list.size() == 1;
52 | return ok && list
53 | .stream()
54 | .filter(File::isFile)
55 | .filter(it -> it.getName().endsWith(suffix))
56 | .count() == list.size();
57 | }
58 | }
59 |
60 | private boolean accept;
61 |
62 | private final Node node;
63 | private final ObjectProperty image;
64 |
65 | private DragEventHandler(Node node, DragEventService service, TransferMode... modes) {
66 | this.node = node;
67 | this.image = new SimpleObjectProperty<>(this, "image", null);
68 |
69 | this.node.setOnDragEntered(event -> {
70 | var board = event.getDragboard();
71 | this.accept = service.before(event.getDragboard());
72 | if (accept && this.getImage() != null) {
73 | board.setDragView(this.getImage());
74 | }
75 | });
76 |
77 | this.node.setOnDragOver(event -> {
78 | if (!accept) {
79 | event.consume();
80 | return;
81 | }
82 | event.acceptTransferModes(modes);
83 | });
84 |
85 | this.node.setOnDragDropped(event -> {
86 | if (!this.accept) {
87 | return;
88 | }
89 | service.after(event.getDragboard());
90 | event.setDropCompleted(true);
91 | });
92 | }
93 |
94 | public Image getImage() {
95 | return image.get();
96 | }
97 |
98 | public ObjectProperty imageProperty() {
99 | return image;
100 | }
101 |
102 | public void setImage(Image image) {
103 | this.image.set(image);
104 | }
105 |
106 | public Node getNode() {
107 | return node;
108 | }
109 |
110 | public static DragEventHandler register(Node node, DragEventService service, TransferMode... modes) {
111 | return new DragEventHandler(node, service, modes);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/event/GameEventWrap.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.event;
2 |
3 | import cn.navclub.nes4j.bin.io.JoyPad;
4 | import javafx.event.EventType;
5 | import javafx.scene.input.KeyEvent;
6 |
7 | public record GameEventWrap(EventType event, JoyPad.JoypadButton btn) {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/event/NodeDragEvent.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.event;
2 |
3 | import javafx.event.EventHandler;
4 | import javafx.scene.Node;
5 | import javafx.scene.input.MouseEvent;
6 | import javafx.stage.Stage;
7 |
8 | public class NodeDragEvent implements EventHandler {
9 | private final Node node;
10 |
11 | private double x;
12 | private double y;
13 |
14 | public NodeDragEvent(Node node) {
15 | this.node = node;
16 | this.node.addEventFilter(MouseEvent.ANY, this);
17 | }
18 |
19 | @Override
20 | public void handle(MouseEvent event) {
21 | var scene = node.getScene();
22 | var window = (Stage) scene.getWindow();
23 | var type = event.getEventType();
24 |
25 | if (type == MouseEvent.MOUSE_PRESSED) {
26 | this.x = event.getSceneX();
27 | this.y = event.getSceneY();
28 | } else if (type == MouseEvent.MOUSE_DRAGGED) {
29 | var xo = event.getScreenX() - this.x;
30 | var yo = event.getScreenY() - this.y;
31 |
32 | window.setX(Math.max(xo, 0));
33 | window.setY(Math.max(yo, 0));
34 | }
35 | }
36 |
37 |
38 | public static void setBind(Node node,String ignore) {
39 | new NodeDragEvent(node);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/model/GTreeItem.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.model;
2 |
3 | import javafx.scene.control.Label;
4 | import javafx.scene.control.TreeItem;
5 | import lombok.Getter;
6 |
7 | import java.io.File;
8 |
9 | public class GTreeItem extends TreeItem {
10 | @Getter
11 | private final File file;
12 | @SuppressWarnings("all")
13 | private final Label label;
14 |
15 | public GTreeItem(File file) {
16 | this.file = file;
17 | this.label = new Label();
18 | this.setGraphic(this.label);
19 | this.setValue(file.getName());
20 | this.label.getStyleClass().add("assort-folder");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/model/KeyMapper.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.model;
2 |
3 | import cn.navclub.nes4j.bin.io.JoyPad;
4 | import javafx.scene.input.KeyCode;
5 | import lombok.Data;
6 |
7 | @Data
8 | public class KeyMapper {
9 | private KeyCode keyCode;
10 | private JoyPad.JoypadButton button;
11 |
12 | public KeyMapper(JoyPad.JoypadButton button, KeyCode keyCode) {
13 | this.button = button;
14 | this.keyCode = keyCode;
15 | }
16 |
17 | public KeyMapper() {
18 | }
19 |
20 | public KeyMapper copy() {
21 | return new KeyMapper(this.button, this.keyCode);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/service/LoadingService.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.service;
2 |
3 | public interface LoadingService {
4 | /**
5 | * Prepare async execute
6 | */
7 | default void preExecute() {
8 |
9 | }
10 |
11 | /**
12 | * Execute async load data
13 | *
14 | * @param params Execute params
15 | */
16 | T execute(Object... params);
17 |
18 | /**
19 | * Data load success call this function
20 | *
21 | * @param data Target data
22 | */
23 | void onSuccess(T data);
24 |
25 | /**
26 | * When load data occur error call this function
27 | *
28 | * @param throwable Error detail
29 | */
30 | default void onError(Throwable throwable) {
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/service/TaskService.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.service;
2 |
3 | import javafx.concurrent.Service;
4 | import javafx.concurrent.Task;
5 |
6 | public class TaskService extends Service {
7 | private final Task task;
8 |
9 | public TaskService(Task task) {
10 | this.task = task;
11 | }
12 |
13 | @Override
14 | protected Task createTask() {
15 | return task;
16 | }
17 |
18 | public static TaskService execute(Task task) {
19 | var service = new TaskService<>(task);
20 | service.start();
21 | return service;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/util/IOUtil.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.util;
2 |
3 | import java.nio.file.Files;
4 | import java.nio.file.Path;
5 |
6 | public class IOUtil {
7 | public static void writeStr(Path path, String text) {
8 | try {
9 | mkdirs(path, true);
10 | if (!Files.exists(path)) {
11 | Files.createFile(path);
12 | }
13 | Files.writeString(path, text);
14 | } catch (Exception e) {
15 | throw new RuntimeException(e);
16 | }
17 | }
18 |
19 | public static void mkdirs(Path path, boolean file) {
20 | if (file) {
21 | path = path.getParent();
22 | }
23 | path.toFile().mkdirs();
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/util/JsonUtil.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.util;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 |
6 | public class JsonUtil {
7 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
8 |
9 | public static String toJsonStr(Object obj) {
10 | try {
11 | return OBJECT_MAPPER.writeValueAsString(obj);
12 | } catch (JsonProcessingException e) {
13 | throw new RuntimeException(e);
14 | }
15 | }
16 |
17 | public static T parse(String text, Class clazz) {
18 | try {
19 | return OBJECT_MAPPER.readValue(text, clazz);
20 | } catch (JsonProcessingException e) {
21 | throw new RuntimeException(e);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/util/StrUtil.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.util;
2 |
3 | import java.io.File;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 |
7 | public class StrUtil {
8 | /**
9 | * 获取文件名称且移除后缀
10 | */
11 | public static String getFileName(File file) {
12 | var name = file.getName();
13 | var index = name.lastIndexOf(".");
14 | if (index > 0) {
15 | name = name.substring(0, index);
16 | }
17 | return name;
18 | }
19 |
20 | public static String toKB(int bytes) {
21 | return String.format("%dKB", bytes / 1024);
22 | }
23 |
24 |
25 | public static boolean isBlank(String str) {
26 | return str == null || str.trim().isEmpty();
27 | }
28 |
29 | public static boolean isNotBlank(String str) {
30 | return !isBlank(str);
31 | }
32 |
33 | public static Map args2Map(String[] args) {
34 | var map = new HashMap();
35 |
36 | for (int i = 0; i < args.length; i += 2) {
37 | var key = args[i];
38 | var value = i >= args.length - 1 ? "" : args[i + 1];
39 | map.put(key, value);
40 | }
41 | return map;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/util/UIUtil.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.util;
2 |
3 | import cn.navclub.nes4j.app.INes;
4 | import cn.navclub.nes4j.app.dialog.ExceptionDialog;
5 | import javafx.application.Platform;
6 | import javafx.scene.control.Alert;
7 | import javafx.scene.control.ButtonType;
8 | import javafx.scene.control.TextInputDialog;
9 |
10 | import java.util.Optional;
11 | import java.util.function.Consumer;
12 |
13 | /**
14 | * 封装Javafx UI相关操作工具类
15 | */
16 | public class UIUtil {
17 | public static void showError(Throwable t, String headerText, Consumer consumer) {
18 | Platform.runLater(() -> {
19 | ExceptionDialog.showAndWait(t, headerText);
20 |
21 | if (consumer != null) {
22 | consumer.accept(null);
23 | }
24 | });
25 | }
26 |
27 | public static Optional prompt(String headerTitle) {
28 | var dialog = new TextInputDialog();
29 | dialog.setHeaderText(INes.localeValue(headerTitle));
30 | return dialog.showAndWait();
31 | }
32 |
33 | /**
34 | * Show a confirm dialog.
35 | *
36 | * @param text Confirm text
37 | * @return If OK
was pressed return {@code true} otherwise {@code false}
38 | * @apiNote Please ensure call in Javafx UI thread
39 | */
40 | public static boolean confirm(String text) {
41 | var alert = new Alert(Alert.AlertType.CONFIRMATION);
42 | alert.setHeaderText(text);
43 | var optional = alert.showAndWait();
44 | return optional.isPresent() && optional.get() == ButtonType.OK;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/navclub/nes4j/app/view/PPUViewer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.app.view;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.util.BinUtil;
5 | import javafx.scene.Scene;
6 | import javafx.scene.canvas.Canvas;
7 | import javafx.scene.image.*;
8 | import javafx.scene.layout.BorderPane;
9 | import javafx.stage.Stage;
10 |
11 | import java.nio.IntBuffer;
12 |
13 | @SuppressWarnings("all")
14 | public class PPUViewer extends Stage {
15 | private final Canvas canvas;
16 | private final BorderPane borderPane;
17 |
18 | //BLACK-RED-GREEN-BLUE
19 | private static int[] DEF_FILL_COLOR = {0xff000000, 0xffff0000, 0xff00ff00, 0xff0000ff};
20 |
21 | public PPUViewer(byte[] buffer, int[] colors) {
22 | this.canvas = new Canvas();
23 | this.borderPane = new BorderPane(this.canvas);
24 | this.setScene(new Scene(this.borderPane));
25 |
26 | var intBuf = IntBuffer.allocate(1);
27 | var image = new WritableImage(256, 128);
28 | var writer = image.getPixelWriter();
29 | var pixelFormat = PixelFormat.getIntArgbInstance();
30 | var stride = 32;
31 | for (int i = 0; i < 512; i++) {
32 | var offset = i * 16;
33 | var index = i % stride;
34 | var rowNum = i / stride;
35 | for (int j = 0; j < 8; j++) {
36 | var l = buffer[offset + j];
37 | var r = buffer[offset + 8 + j];
38 | for (int k = 7; k >= 0; k--) {
39 | var y = rowNum * 8 + j;
40 | var x = index * 8 + (7 - k);
41 | var idx = ((l >> k) & 1) | ((r >> k & 1) << 1);
42 | writer.setPixels(x, y, 1, 1, pixelFormat, intBuf.put(0, colors[idx]), 256);
43 | }
44 | }
45 | }
46 | this.canvas.setWidth(256);
47 | this.canvas.setHeight(128);
48 | this.canvas.getGraphicsContext2D().drawImage(image, 0, 0);
49 |
50 | // By console print click charactar index
51 | this.canvas.setOnMouseClicked(event -> {
52 | var x = event.getSceneX();
53 | var y = event.getSceneY();
54 | var h = (int) (y / 8);
55 | var k = (int) (x / 8);
56 | var idx = h * stride + k;
57 | System.out.printf("idx=0x%s\n", BinUtil.toHexStr((int) idx));
58 | });
59 |
60 | this.setResizable(false);
61 | this.setTitle("PPU Viewer");
62 |
63 | this.show();
64 | }
65 |
66 | public PPUViewer(NesConsole console) {
67 | this(console.getCartridge().getChrom(), DEF_FILL_COLOR);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | import cn.navclub.nes4j.bin.apu.Player;
2 |
3 | module cn.navclub.nes4j.app {
4 | requires static lombok;
5 |
6 | requires java.desktop;
7 | requires java.management;
8 |
9 | requires javafx.fxml;
10 | requires javafx.base;
11 | requires javafx.graphics;
12 | requires javafx.controls;
13 | requires cn.navclub.nes4j.bin;
14 |
15 | requires com.fasterxml.jackson.core;
16 | requires com.fasterxml.jackson.databind;
17 |
18 | exports cn.navclub.nes4j.app.audio to cn.navclub.nes4j.bin;
19 |
20 | opens cn.navclub.nes4j.app;
21 | opens cn.navclub.nes4j.app.view;
22 |
23 | opens cn.navclub.nes4j.app.event to javafx.fxml;
24 | opens cn.navclub.nes4j.app.control to javafx.fxml;
25 | opens cn.navclub.nes4j.app.dialog to javafx.fxml;
26 |
27 | opens cn.navclub.nes4j.app.config to com.fasterxml.jackson.databind;
28 | opens cn.navclub.nes4j.app.model to javafx.base, com.fasterxml.jackson.databind;
29 | opens cn.navclub.nes4j.app.assets;
30 |
31 | uses Player;
32 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/Common.css:
--------------------------------------------------------------------------------
1 | .root {
2 | -nes4j-background-color: #fff;
3 | /**Game hall navbar background color**/
4 | -nes4j-game-hall-navbar: #ffffff;
5 | /**Game hall navbar bottom border color**/
6 | -nes4j-game-hall-navbar-border: #cdc7c7;
7 | /**list view background color**/
8 | -nes4j-game-hall-list-view: #ededed;
9 | -nes4j-game-hall-xbox: -nes4j-game-hall-list-view;
10 | /**Game hall left assort text fill**/
11 | -nes4j-game-hall-assort-fill: #bfcbd9;
12 | /**Game hall center content background color**/
13 | -nes4j-game-hall-content: #ffffff;
14 | /**Game hall app name text fill**/
15 | -nes4j-game-hall-app-name: #ffffff;
16 | /**Game hall text color**/
17 | -nes4j-game-hall-text-fill: #000000;
18 | /**Game hall list-cell select background color**/
19 | -nes4j-game-hall-list-cell-active: linear-gradient(to right, #b4cbf3, #c6d6ef, #cfddef, #e4e7ef);
20 | /**Game hall list-cell select text fill*/
21 | -nes4j-game-hall-list-cell-text: #5185fd;
22 | /**Default list view select row background color**/
23 | -nes4j-list-cell-select-color: rgba(10, 29, 47, 1);
24 | /**Game tray hover background color**/
25 | -nes4j-game-tray-hover-color: rgba(13, 10, 49, .4);
26 | /**Game tray hover text color**/
27 | -nes4j-game-tray-hover-text-color: #fff;
28 | /**Game hall assort TreeView root background color**/
29 | -nes4j-game-hall-assort-tree-view-root: #ffcc99;
30 |
31 | }
32 |
33 | /*设置全局字体和大小*/
34 | * {
35 | -fx-font-family: 'Gen Jyuu Gothic';
36 | }
37 |
38 | ScrollPane, .list-view {
39 | -fx-background-insets: 0;
40 | -fx-padding: 0;
41 | }
42 |
43 | .status-indicator {
44 | -fx-alignment: CENTER;
45 | -fx-spacing: .3em;
46 | -fx-font-size: 1.5em;
47 | }
48 |
49 | .search-text-field {
50 | -fx-alignment: CENTER;
51 | -fx-spacing: .2em;
52 | -fx-padding: .1em .2em;
53 | -fx-background-color: #f4f4f4;
54 | -fx-background-radius: .2em;
55 | }
56 |
57 | .search-text-field .prefix {
58 | -fx-background-color: #939393;
59 | -fx-pref-width: .8em;
60 | -fx-pref-height: .5em;
61 | -fx-shape: "M474.453333 884.053333c-225.28 0-409.6-184.32-409.6-409.6s184.32-409.6 409.6-409.6 409.6 184.32 409.6 409.6-184.32 409.6-409.6 409.6z m0-68.266666c187.733333 0 341.333333-153.6 341.333334-341.333334s-153.6-341.333333-341.333334-341.333333-341.333333 153.6-341.333333 341.333333 153.6 341.333333 341.333333 341.333334z m252.586667 54.613333c-13.653333-13.653333-10.24-37.546667 3.413333-47.786667s37.546667-10.24 47.786667 3.413334l64.853333 78.506666c13.653333 13.653333 10.24 37.546667-3.413333 47.786667s-37.546667 10.24-47.786667-3.413333l-64.853333-78.506667z";
62 | }
63 |
64 | .search-text-field > .text-field {
65 | -fx-background-color: transparent;
66 | -fx-min-width: 15em;
67 | -fx-font-size: 1.2em;
68 | }
69 |
70 | .loading-pane .mask-pane {
71 | -fx-spacing: 1em;
72 | -fx-alignment: CENTER;
73 | -fx-background-color: rgba(255, 255, 255, .9);
74 | }
75 |
76 | .loading-pane .mask-pane .label {
77 | -fx-text-fill: #409eff;
78 | }
79 |
80 | .loading-pane .mask-pane .mask-pane-icon {
81 | -fx-pref-width: 5em;
82 | -fx-pref-height: 5em;
83 | -fx-background-color: #409eff;
84 | -fx-shape: 'M485.717333 196.949333a26.282667 26.282667 0 0 1 52.565334 0v157.525334a26.282667 26.282667 0 0 1-52.565334 0V196.9152z m161.109334 29.047467a26.282667 26.282667 0 0 1 45.4656 26.282667l-78.779734 136.430933a26.282667 26.282667 0 0 1-45.4656-26.282667l78.7456-136.430933z m124.928 105.710933a26.282667 26.282667 0 1 1 26.248533 45.499734l-136.430933 78.7456a26.282667 26.282667 0 0 1-26.282667-45.4656l136.430933-78.779734z m55.296 154.043734a26.282667 26.282667 0 1 1 0 52.497066h-157.525334a26.282667 26.282667 0 0 1 0-52.497066h157.559467z m-29.047467 161.041066a26.282667 26.282667 0 0 1-26.282667 45.499734l-136.430933-78.779734a26.282667 26.282667 0 0 1 26.282667-45.4656l136.430933 78.7456z m-105.710933 124.928a26.282667 26.282667 0 1 1-45.499734 26.282667l-78.7456-136.430933a26.282667 26.282667 0 0 1 45.4656-26.282667l78.779734 136.430933z m-154.043734 55.364267a26.282667 26.282667 0 1 1-52.497066 0v-157.559467a26.282667 26.282667 0 0 1 52.497066 0v157.559467z m-161.041066-29.0816a26.282667 26.282667 0 0 1-45.499734-26.282667l78.779734-136.430933a26.282667 26.282667 0 0 1 45.4656 26.282667l-78.7456 136.430933z m-124.928-105.710933a26.282667 26.282667 0 0 1-26.282667-45.499734l136.430933-78.7456a26.282667 26.282667 0 0 1 26.282667 45.4656L252.245333 692.292267z m-55.364267-154.043734a26.282667 26.282667 0 0 1 0-52.497066h157.559467a26.282667 26.282667 0 0 1 0 52.497066H196.9152z m29.0816-161.041066a26.282667 26.282667 0 0 1 26.282667-45.499734l136.430933 78.779734a26.282667 26.282667 0 0 1-26.282667 45.4656l-136.430933-78.7456z m105.710933-124.928a26.282667 26.282667 0 0 1 45.499734-26.282667l78.7456 136.430933a26.282667 26.282667 0 0 1-45.4656 26.282667L331.707733 252.245333z';
85 | }
86 |
87 | .split-pane:horizontal > .split-pane-divider {
88 | -fx-padding: 0 .1em 0 0;
89 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/DException.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | .text-area {
4 | -fx-padding: 0;
5 | -fx-background-insets: 0;
6 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/DHandle.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | .box, .content {
4 | -fx-spacing: 2em;
5 | -fx-alignment: CENTER;
6 | }
7 |
8 |
9 | .right-box {
10 | -fx-hgap: .5em;
11 | }
12 |
13 | .top-btn {
14 | -fx-graphic: url('../img/handler/up.png');
15 | }
16 |
17 | .left-btn {
18 | -fx-graphic: url('../img/handler/left.png');
19 | }
20 |
21 | .right-btn {
22 | -fx-graphic: url('../img/handler/right.png');
23 | }
24 |
25 | .bottom-btn {
26 | -fx-graphic: url('../img/handler/down.png');
27 | }
28 |
29 | .center-box HBox {
30 | -fx-spacing: .5em;
31 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/DNesHeaderStyle.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | .header-box {
4 | -fx-spacing: 1em;
5 | }
6 |
7 | GridPane {
8 | -fx-hgap: 1em;
9 | -fx-vgap: 1em;
10 | }
11 |
12 | .choice-box, .text-field {
13 | -fx-pref-width: 15em;
14 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/DebuggerStyle.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | GridPane {
4 | -fx-vgap: 1em;
5 | -fx-hgap: 1em;
6 | -fx-alignment: TOP-CENTER;
7 | -fx-padding: .5em;
8 | }
9 |
10 | .status-grid {
11 | -fx-hgap: .5em;
12 | -fx-vgap: .5em;
13 | -fx-alignment: CENTER-LEFT;
14 | }
15 |
16 | .top-box {
17 | -fx-padding: .2em;
18 | -fx-spacing: .5em;
19 | -fx-border-width: 0 0 .05em 0;
20 | -fx-border-color: #b8b8b8;
21 | }
22 |
23 | .top-box .button {
24 | -fx-padding: 0;
25 | -fx-background-color: transparent;
26 | }
27 |
28 | .list-view .list-cell {
29 | -fx-padding: 0;
30 | }
31 |
32 | .break-line {
33 | -fx-spacing: .5em;
34 | -fx-alignment: CENTER-LEFT;
35 | }
36 |
37 | .break-line .break-label {
38 | -fx-pref-width: 1.5em;
39 | -fx-pref-height: 1.5em;
40 | -fx-text-fill: #000000;
41 | -fx-font-weight: bolder;
42 | -fx-background-color: #b8b8b8;
43 | }
44 |
45 | .break-line .break-label-drag {
46 | -fx-background-color: red !important;
47 | }
48 |
49 | .debug-line {
50 | -fx-background-color: red !important;
51 | }
52 |
53 | .debug-line .label {
54 | -fx-text-fill: #fff !important;
55 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/GameWorldStyle.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | .menu-bar {
4 | -fx-font-size: 1.2em;
5 | }
6 |
7 | .gsi {
8 | -fx-padding: .2em;
9 | }
10 |
11 | .gsi, .gsi > HBox {
12 | -fx-alignment: CENTER;
13 | -fx-spacing: .3em;
14 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/Nes4j.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | .left-box, .right-box {
4 | -fx-spacing: .5em;
5 | -fx-alignment: CENTER_LEFT;
6 | }
7 |
8 | .right-box .button {
9 | -fx-background-color: transparent;
10 | -fx-padding: 0;
11 | }
12 |
13 | .right-box {
14 | visibility: hidden;
15 | }
16 |
17 | .list-view .list-cell:selected .right-box {
18 | visibility: visible !important;
19 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/SystemPalette.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | VBox {
4 | -fx-spacing: 1em;
5 | -fx-alignment: CENTER-LEFT;
6 | -fx-padding: .5em;
7 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/css/TextPopup.css:
--------------------------------------------------------------------------------
1 | @import "Common.css";
2 |
3 | .text-popup {
4 | -fx-padding: 2em 3em;
5 | -fx-alignment: CENTER;
6 | -fx-border-radius: .5em;
7 | -fx-background-radius: .5em;
8 | -fx-background-color: rgba(0, 0, 0, .8);
9 | }
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/fxml/DNesHeader.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/fxml/Debugger.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/fxml/GameHall.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/fxml/GameWorld.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/bin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/bin.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/delete.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/empty.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/game.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/game.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/down.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/left.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/right.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/handler/up.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/icon.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/launcher.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/loading.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/max.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/max.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/mwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/mwin.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/nes4j.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/nes4j.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/poster.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/rrun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/rrun.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/run.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/speed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/speed.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/stepinto.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/stepinto.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/stepout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/stepout.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/img/xwin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/app/src/main/resources/cn/navclub/nes4j/app/assets/img/xwin.png
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/language/nes4j.properties:
--------------------------------------------------------------------------------
1 | nes4j.run=Run
2 | nes4j.new=New
3 | nes4j.file=File
4 | nes4j.view=View
5 | nes4j.tool=Tool
6 | nes4j.help=Help
7 | nes4j.error=Error
8 | nes4j.search=search
9 | nes4j.debug=Debug
10 | nes4j.reset=Reset
11 | nes4j.handle=Handle
12 | nes4j.donate=Donate
13 | nes4j.delete=Delete
14 | nes4j.memory=Memory
15 | nes4j.pplay=Play/Pause
16 | nes4j.setting=Setting
17 | nes4j.palette=Palette
18 | nes4j.options=Options
19 | nes4j.loading=Loading...
20 | nes4j.name.table=Name Table
21 | nes4j.repository=Repository
22 | nes4j.snap.ram=Ram snapshot
23 | nes4j.snap.vram=Vram snapshot
24 | nes4j.pattern.table=Pattern Table
25 | nes4j.game.error=Game running happen error
26 | nes4j.assort.name=Please input assort name
27 | nes4j.assembler.debugger=Assembler Debugger
28 | nes4j.game.delete=Are you sure delete game [%s]?
29 | nes4j.assort.delete=Are you sure you want to delete the current category?
--------------------------------------------------------------------------------
/app/src/main/resources/cn/navclub/nes4j/app/assets/language/nes4j_zh_CN.properties:
--------------------------------------------------------------------------------
1 | nes4j.file=文件
2 | nes4j.new=新建
3 | nes4j.view=视图
4 | nes4j.reset=重置
5 | nes4j.tool=工具
6 | nes4j.run=运行
7 | nes4j.help=帮助
8 | nes4j.memory=内存
9 | nes4j.delete=删除
10 | nes4j.error=错误
11 | nes4j.debug=调试
12 | nes4j.search=搜索
13 | nes4j.handle=手柄
14 | nes4j.donate=捐赠
15 | nes4j.setting=设置
16 | nes4j.palette=画布
17 | nes4j.options=选项
18 | nes4j.pplay=播放/暂停
19 | nes4j.loading=加载中...
20 | nes4j.repository=仓库
21 | nes4j.name.table=命名表
22 | nes4j.unsupport=暂不支持
23 | nes4j.snap.ram=RAM 快照
24 | nes4j.pattern.table=模式表
25 | nes4j.snap.vram=Vram 快照
26 | nes4j.assort.name=请输入分类名称
27 | nes4j.assembler.debugger=汇编调试器
28 | nes4j.game.error=游戏运行过程中发生错误
29 | nes4j.game.delete=你确定要删除游戏 [%s]?
30 | nes4j.assort.delete=你确定要删除当前分类?
--------------------------------------------------------------------------------
/app/src/native/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.22)
2 | project(native C)
3 |
4 | #开启测试
5 | enable_testing()
6 |
7 | find_package(JNI REQUIRED)
8 | find_package(ALSA REQUIRED)
9 |
10 | set(CMAKE_C_STANDARD 99)
11 |
12 | aux_source_directory(. SRC_DIR)
13 | aux_source_directory(jni JNI_DIR)
14 |
15 | list(APPEND SRC_DIR ${JNI_DIR})
16 |
17 | add_library(nes4j SHARED ${SRC_DIR})
18 | add_executable(nes4j_test ${SRC_DIR})
19 |
20 | include_directories(nes4j ${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2})
21 | include_directories(nes4j_test ${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2})
22 |
23 | target_link_libraries(nes4j ${ALSA_LIBRARIES} m)
24 | target_link_libraries(nes4j_test ${ALSA_LIBRARIES} m)
25 |
26 |
27 | add_test(NAME test COMMAND nes4j_test 2 3)
28 |
--------------------------------------------------------------------------------
/app/src/native/include/asoundlib.h:
--------------------------------------------------------------------------------
1 | /**
2 | * \file include/asoundlib.h
3 | * \brief Application interface library for the ALSA driver
4 | * \author Jaroslav Kysela
5 | * \author Abramo Bagnara
6 | * \author Takashi Iwai
7 | * \date 1998-2001
8 | *
9 | * Application interface library for the ALSA driver
10 | */
11 | /*
12 | * This library is free software; you can redistribute it and/or modify
13 | * it under the terms of the GNU Lesser General Public License as
14 | * published by the Free Software Foundation; either version 2.1 of
15 | * the License, or (at your option) any later version.
16 | *
17 | * This program is distributed in the hope that it will be useful,
18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | * GNU Lesser General Public License for more details.
21 | *
22 | * You should have received a copy of the GNU Lesser General Public
23 | * License along with this library; if not, write to the Free Software
24 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
25 | *
26 | */
27 |
28 | #ifndef __ASOUNDLIB_H
29 | #define __ASOUNDLIB_H
30 |
31 | #include
32 | #include
33 | #include
34 | #include
35 | #include
36 | #include
37 | #include
38 | #include
39 | #include
40 | #include
41 | #include
42 |
43 | #ifndef __GNUC__
44 | #define __inline__ inline
45 | #endif
46 |
47 | #include
48 | #include
49 | #include
50 | #include
51 | #include
52 | #include
53 | #include
54 | #include
55 | #include
56 | #include
57 | #include
58 | #include
59 | #include
60 | #include
61 | #include
62 | #include
63 | #include
64 |
65 | #endif /* __ASOUNDLIB_H */
66 |
--------------------------------------------------------------------------------
/app/src/native/include/jni/cn_navclub_nes4j_app_audio_NativePlayer.h:
--------------------------------------------------------------------------------
1 | /* DO NOT EDIT THIS FILE - it is machine generated */
2 | #include
3 | /* Header for class cn_navclub_nes4j_app_audio_NativePlayer */
4 |
5 | #ifndef _Included_cn_navclub_nes4j_app_audio_NativePlayer
6 | #define _Included_cn_navclub_nes4j_app_audio_NativePlayer
7 | #ifdef __cplusplus
8 | extern "C" {
9 | #endif
10 | /*
11 | * Class: cn_navclub_nes4j_app_audio_NativePlayer
12 | * Method: stop
13 | * Signature: ()V
14 | */
15 | JNIEXPORT void JNICALL Java_cn_navclub_nes4j_app_audio_NativePlayer_stop
16 | (JNIEnv *, jobject);
17 |
18 | /*
19 | * Class: cn_navclub_nes4j_app_audio_NativePlayer
20 | * Method: play
21 | * Signature: ([F)J
22 | */
23 | JNIEXPORT jlong JNICALL Java_cn_navclub_nes4j_app_audio_NativePlayer_play
24 | (JNIEnv *, jobject, jfloatArray);
25 |
26 | /*
27 | * Class: cn_navclub_nes4j_app_audio_NativePlayer
28 | * Method: config
29 | * Signature: (Ljava/lang/String;III)V
30 | */
31 | JNIEXPORT void JNICALL Java_cn_navclub_nes4j_app_audio_NativePlayer_config
32 | (JNIEnv *, jobject, jstring, jint, jint, jint);
33 |
34 | #ifdef __cplusplus
35 | }
36 | #endif
37 | #endif
38 |
--------------------------------------------------------------------------------
/app/src/native/include/jni/jni_util.h:
--------------------------------------------------------------------------------
1 | #ifndef NATIVE_JNI_UTIL_H
2 | #define NATIVE_JNI_UTIL_H
3 |
4 | #include
5 | #include "../type.h"
6 |
7 | /**
8 | *
9 | * 获取java对象hash code
10 | *
11 | */
12 | extern jint Nes4j_jni_hash_code(JNIEnv *, jobject);
13 |
14 |
15 | extern void Nes4j_jni_runtime_exception(JNIEnv *, String);
16 |
17 | extern void Nes4j_jni_throw_exception(JNIEnv *,String,String);
18 |
19 | #endif //NATIVE_JNI_UTIL_H
20 |
--------------------------------------------------------------------------------
/app/src/native/include/memory.h:
--------------------------------------------------------------------------------
1 | //
2 | // Created by yangkui on 2022/11/22.
3 | //
4 |
5 | #ifndef NATIVE_MEMORY_H
6 | #define NATIVE_MEMORY_H
7 |
8 | #include "type.h"
9 |
10 |
11 | extern SoundHardware *Nes4j_new_sound_hardware(int id, Object context);
12 |
13 |
14 | extern SoundHardware *Nes4j_sound_hardware_clone(SoundHardware *,Object);
15 |
16 | extern LinkedList *Nes4j_new_linked_list(Object pre, Object next, Object content);
17 |
18 | extern void Nes4j_linked_list_lose(LinkedList **);
19 |
20 | extern void Nes4j_sound_hardware_close(SoundHardware **);
21 |
22 | #endif //NATIVE_MEMORY_H
23 |
--------------------------------------------------------------------------------
/app/src/native/include/str_util.h:
--------------------------------------------------------------------------------
1 | #ifndef NATIVE_STR_UTIL_H
2 | #define NATIVE_STR_UTIL_H
3 |
4 | #include "type.h"
5 |
6 | /**
7 | *
8 | * 克隆目标字符串
9 | *
10 | */
11 | extern String Nes4j_str_clone(String);
12 |
13 | #endif //NATIVE_STR_UTIL_H
14 |
--------------------------------------------------------------------------------
/app/src/native/include/sys_sound.h:
--------------------------------------------------------------------------------
1 | #ifndef NATIVE_SYS_SOUND_H
2 | #define NATIVE_SYS_SOUND_H
3 |
4 | #include "type.h"
5 |
6 | /**
7 | *
8 | * 查询音频实例
9 | *
10 | */
11 | extern SoundHardware *Nes4j_find_hardware(int id);
12 |
13 | /**
14 | * 初始化音频硬件参数
15 | *
16 | * @return 如果初始化成功则返回{@code True},否则返回{@code False}
17 | */
18 | extern bool Nes4j_init_hardware(SoundHardware *, SoundHardware **);
19 |
20 |
21 | /**
22 | *
23 | * 播放音频
24 | *
25 | */
26 | extern usize Nes4j_apu_play(SoundHardware *hardware, const float *sample, usize length);
27 |
28 | /**
29 | *
30 | * 停止正在播放音频资源
31 | *
32 | */
33 | extern void Nes4j_apu_stop(SoundHardware *hardware);
34 |
35 | #endif //NATIVE_SYS_SOUND_H
36 |
--------------------------------------------------------------------------------
/app/src/native/include/type.h:
--------------------------------------------------------------------------------
1 |
2 | #ifndef NATIVE_TYPE_H
3 | #define NATIVE_TYPE_H
4 |
5 | #define True (1)
6 | #define False (0)
7 |
8 | typedef int bool;
9 | typedef void *Object;
10 | typedef char *String;
11 | typedef unsigned char byte;
12 | typedef unsigned long usize;
13 | typedef struct LinkedList0 LinkedList;
14 | typedef struct SoundHardware0 SoundHardware;
15 |
16 | struct SoundHardware0 {
17 | int id;
18 | int rate;
19 | int latency;
20 | int channel;
21 | String device;
22 | Object context;
23 | };
24 |
25 | struct LinkedList0 {
26 | Object content;
27 | struct LinkedList *pre;
28 | struct LinkedList *next;
29 | };
30 |
31 |
32 | #endif //NATIVE_TYPE_H
33 |
--------------------------------------------------------------------------------
/app/src/native/jni/apu_player_jni.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include "../include/jni/cn_navclub_nes4j_app_audio_NativePlayer.h"
3 |
4 | #include "../include/sys_sound.h"
5 | #include "../include/jni/jni_util.h"
6 |
7 | static SoundHardware *Nes4j_hardware_instance(JNIEnv *, jobject);
8 |
9 | JNIEXPORT jlong JNICALL
10 | Java_cn_navclub_nes4j_app_audio_NativePlayer_play(JNIEnv *env, jobject this, jfloatArray array) {
11 | SoundHardware *hardware = Nes4j_hardware_instance(env, this);
12 | if (hardware == NULL) {
13 | fprintf(stderr, "Call before Please init SoundHardware.\n");
14 | return 0;
15 | }
16 | jint length = (*env)->GetArrayLength(env, array);
17 | jboolean copy = JNI_FALSE;
18 | jfloat *temp = (*env)->GetFloatArrayElements(env, array, ©);
19 | if (!temp) {
20 | fprintf(stderr, "Get double array elements is NULL it was gc recovery?\n");
21 | return 0;
22 | }
23 | jlong size = Nes4j_apu_play(hardware, temp, length);
24 | (*env)->ReleaseFloatArrayElements(env, array, temp, JNI_ABORT);
25 | return size;
26 | }
27 |
28 | JNIEXPORT void JNICALL Java_cn_navclub_nes4j_app_audio_NativePlayer_stop(JNIEnv *env, jobject this) {
29 | SoundHardware *hardware = Nes4j_hardware_instance(env, this);
30 | if (hardware != NULL)
31 | Nes4j_apu_stop(hardware);
32 | }
33 |
34 | JNIEXPORT void JNICALL
35 | Java_cn_navclub_nes4j_app_audio_NativePlayer_config(JNIEnv *env, jobject this, jstring device, jint channel,
36 | jint rate, jint latency) {
37 | SoundHardware *hardware = Nes4j_hardware_instance(env, this);
38 | if (hardware) {
39 | Nes4j_jni_runtime_exception(env, "Please not repeat config audio hardware.");
40 | return;
41 | }
42 | jboolean copy = False;
43 | const char *str = (*env)->GetStringUTFChars(env, device, ©);
44 | SoundHardware config = {
45 | Nes4j_jni_hash_code(env, this),
46 | rate,
47 | latency,
48 | channel,
49 | str,
50 | NULL
51 | };
52 | bool success = Nes4j_init_hardware(&config, NULL);
53 | (*env)->ReleaseStringUTFChars(env, device, str);
54 | if (!success) {
55 | Nes4j_jni_runtime_exception(env, "Audio hardware init fail.");
56 | }
57 | }
58 |
59 | static SoundHardware *Nes4j_hardware_instance(JNIEnv *env, jobject this) {
60 | jint id = Nes4j_jni_hash_code(env, this);
61 | SoundHardware *hardware = (SoundHardware *) Nes4j_find_hardware(id);
62 | return hardware;
63 | }
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/src/native/jni/jni_util.c:
--------------------------------------------------------------------------------
1 | #include "../include/jni/jni_util.h"
2 |
3 | extern jint Nes4j_jni_hash_code(JNIEnv *env, jobject this) {
4 | jclass class = (*env)->GetObjectClass(env, this);
5 | jmethodID id = (*env)->GetMethodID(env, class, "hashCode", "()I");
6 | jint hash_code = (*env)->CallIntMethod(env, this, id);
7 | return hash_code;
8 | }
9 |
10 | extern void Nes4j_jni_runtime_exception(JNIEnv *env, String msg) {
11 | Nes4j_jni_throw_exception(env, "java/lang/RuntimeException", msg);
12 | }
13 |
14 | extern void Nes4j_jni_throw_exception(JNIEnv *env, String class, String msg) {
15 | jclass clazz = (*env)->FindClass(env, (const char *) class);
16 | jint code = (*env)->ThrowNew(env, clazz, msg);
17 | if (code != 0) {
18 | fprintf(stderr, "JNI Throw exception fail errcode:%d\n", code);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/native/memory.c:
--------------------------------------------------------------------------------
1 | #include "include/memory.h"
2 | #include "include/str_util.h"
3 | #include
4 |
5 | #ifdef __linux__
6 |
7 | #include
8 |
9 | #endif
10 |
11 | extern SoundHardware *Nes4j_new_sound_hardware(int id, Object context) {
12 | SoundHardware *hardware = malloc(sizeof(SoundHardware));
13 | hardware->context = context;
14 | hardware->id = id;
15 | return hardware;
16 | }
17 |
18 | extern SoundHardware *Nes4j_sound_hardware_clone(SoundHardware *hardware, Object context) {
19 | SoundHardware *temp = Nes4j_new_sound_hardware(hardware->id, context);
20 |
21 | temp->rate = hardware->rate;
22 | temp->latency = hardware->latency;
23 | temp->channel = hardware->channel;
24 | temp->device = Nes4j_str_clone(hardware->device);
25 |
26 | return temp;
27 |
28 | }
29 |
30 |
31 | extern LinkedList *Nes4j_new_linked_list(Object pre, Object next, Object content) {
32 | LinkedList *linked_list = malloc(sizeof(LinkedList));
33 | linked_list->pre = pre;
34 | linked_list->next = next;
35 | linked_list->content = content;
36 | return linked_list;
37 | }
38 |
39 | extern void Nes4j_linked_list_lose(LinkedList **node) {
40 | if (node == NULL || *node == NULL) {
41 | return;
42 | }
43 | (*node)->pre = NULL;
44 | (*node)->next = NULL;
45 | (*node)->content = NULL;
46 | free((*node));
47 | (*node) = NULL;
48 | }
49 |
50 | extern void Nes4j_sound_hardware_close(SoundHardware **ptr) {
51 | SoundHardware *hardware = *ptr;
52 | #ifdef __linux__
53 | snd_pcm_t *ctl = hardware->context;
54 | snd_pcm_drop(ctl);
55 | snd_pcm_close(ctl);
56 | #endif
57 | if (hardware->device != NULL) {
58 | free(hardware->device);
59 | hardware->device = NULL;
60 | }
61 | *ptr = NULL;
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/app/src/native/str_util.c:
--------------------------------------------------------------------------------
1 | //
2 | // Created by yangkui on 2022/11/26.
3 | //
4 |
5 | #include
6 | #include
7 | #include "include/str_util.h"
8 |
9 | extern String Nes4j_str_clone(String src) {
10 | usize len = strlen(src);
11 | String dst = malloc(len + 1);
12 | memset(dst, '\0', len);
13 | usize pos = 0;
14 | while (pos < len) {
15 | *(dst + pos) = (char) *(src + pos);
16 | pos++;
17 | }
18 | return dst;
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/native/sys_sound.c:
--------------------------------------------------------------------------------
1 | #include "include/sys_sound.h"
2 |
3 |
4 | #ifdef __linux__
5 |
6 | #include "alsa/asoundlib.h"
7 | #include "include/memory.h"
8 |
9 | static usize Nes4j_apu_play_linux(SoundHardware *hardware, const float *sample, usize length);
10 |
11 | #endif
12 |
13 | LinkedList *linked_list = NULL;
14 |
15 | extern bool Nes4j_init_hardware(SoundHardware *config, SoundHardware **dst) {
16 | Object context = NULL;
17 | #ifdef __linux__
18 | int err;
19 | snd_pcm_t *handle;
20 | if ((err = snd_pcm_open(&handle, config->device, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) < 0) {
21 | printf("Playback open error: %s\n", snd_strerror(err));
22 | return False;
23 | }
24 | if ((err = snd_pcm_set_params(handle,
25 | SND_PCM_FORMAT_FLOAT,
26 | SND_PCM_ACCESS_RW_INTERLEAVED,
27 | config->channel,
28 | config->rate,
29 | 1,
30 | config->latency)) < 0) {
31 | printf("Playback open error: %s\n", snd_strerror(err));
32 | }
33 | //Open linux sound card fail
34 | if (err < 0) {
35 | return False;
36 | }
37 | context = handle;
38 | #endif
39 | SoundHardware *hardware = Nes4j_sound_hardware_clone(config, context);
40 | LinkedList *node = Nes4j_new_linked_list(NULL, NULL, hardware);
41 | LinkedList *temp = linked_list;
42 | while (temp && temp->next) {
43 | temp = (LinkedList *) linked_list->next;
44 | }
45 | if (temp == NULL) {
46 | linked_list = node;
47 | } else {
48 | temp->next = (struct LinkedList *) node;
49 | }
50 | if (dst != NULL)
51 | *dst = hardware;
52 | return True;
53 | }
54 |
55 | extern usize Nes4j_apu_play(SoundHardware *hardware, const float *sample, usize length) {
56 | usize size = 0;
57 | #ifdef __linux__
58 | size = Nes4j_apu_play_linux(hardware, sample, length);
59 | #endif
60 | return size;
61 | }
62 |
63 | extern void Nes4j_apu_stop(SoundHardware *hardware) {
64 | if (hardware == NULL) {
65 | return;
66 | }
67 | LinkedList *temp = linked_list;
68 | while (temp) {
69 | Object obj = temp->content;
70 | if (obj == hardware) {
71 | LinkedList *pre = (LinkedList *) temp->pre;
72 | LinkedList *next = (LinkedList *) temp->next;
73 | if (next != NULL) {
74 | if (pre != NULL)
75 | pre->next = (struct LinkedList *) next;
76 | else
77 | linked_list = next;
78 | } else {
79 | if (pre == NULL) {
80 | linked_list = NULL;
81 | }
82 | }
83 | break;
84 | }
85 | temp = (LinkedList *) temp->next;
86 | }
87 | if (temp != NULL) {
88 | Nes4j_linked_list_lose(&temp);
89 | }
90 | Nes4j_sound_hardware_close(&hardware);
91 | }
92 |
93 |
94 | static usize Nes4j_apu_play_linux(SoundHardware *hardware, const float *sample, usize length) {
95 | snd_pcm_sframes_t frames;
96 | snd_pcm_t *t = hardware->context;
97 | snd_pcm_sframes_t left = snd_pcm_avail(t);
98 | if (left <= 0) {
99 | return 0;
100 | }
101 | frames = snd_pcm_writei(t, sample, length);
102 | if (frames < 0)
103 | frames = snd_pcm_recover(t, frames, 0);
104 | if (frames < 0) {
105 | fprintf(stderr, "snd_pcm_writei failed:%s\n", snd_strerror(frames));
106 | return 0;
107 | }
108 | return frames;
109 | }
110 |
111 |
112 | extern SoundHardware *Nes4j_find_hardware(int id) {
113 | LinkedList *temp = linked_list;
114 | LinkedList *last = linked_list;
115 | while (temp) {
116 | SoundHardware *content = ((SoundHardware *) temp->content);
117 | int id0 = content->id;
118 | if (id == id0) {
119 | break;
120 | }
121 | last = temp;
122 | temp = (LinkedList *) temp->next;
123 | }
124 | return last ? last->content : NULL;
125 | }
126 |
127 |
--------------------------------------------------------------------------------
/app/src/native/test.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "include/sys_sound.h"
5 |
6 | int main(int argc, char **argv) {
7 | SoundHardware config = {
8 | 1, 44100, 500000, 1, "default"
9 | };
10 |
11 | SoundHardware *hardware = NULL;
12 | bool success = Nes4j_init_hardware(&config, &hardware);
13 | if (!success) {
14 | fprintf(stderr, "Sound hardware init fail.");
15 | return 0;
16 | }
17 | usize i = 0;
18 | float buffer[1];
19 | usize length = sizeof(buffer) / sizeof(int);
20 | while (i < 16 * 1024) {
21 | for (int i = 0; i < length; ++i) {
22 | long r = rand();
23 | buffer[i] = r / (float) 0x7fffffff;
24 | }
25 | Nes4j_apu_play(hardware, buffer, length);
26 | i++;
27 | }
28 | Nes4j_apu_stop(hardware);
29 |
30 | return 0;
31 | }
--------------------------------------------------------------------------------
/assembly/common/ascii.chr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/assembly/common/ascii.chr
--------------------------------------------------------------------------------
/assembly/make:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | function CompilerAndLink {
3 | IDX=$(echo "$1" | awk -F"." '{print length($1)}')
4 | FILE_NAME=$(echo "$1" | cut -c 1-$IDX)
5 | OBJ_FILE="$FILE_NAME.o"
6 | ca65 -o "$OBJ_FILE" "$1"
7 | NES_FILE="$FILE_NAME.nes"
8 | if [ -e "$NES_FILE" ]; then
9 | rm -rf "$NES_FILE"
10 | fi
11 | ld65 -C nes.conf --obj "$OBJ_FILE" -o "$NES_FILE"
12 | if [ -e "$OBJ_FILE" ]; then
13 | rm -rf "$OBJ_FILE"
14 | fi
15 | printf "\nBuild success.\n"
16 | if [ -e "${NES4J_HOME}" ]; then
17 | printf "Starting game.......\n"
18 | "${NES4J_HOME}"/bin/nes4j "$NES_FILE"
19 | fi
20 | }
21 |
22 | function PrintHelper {
23 | printf "\nmake [options]
24 | options:
25 | -b Release nes program
26 | --help Print command guide\n"
27 | exit
28 | }
29 |
30 | OPTIONS="$1"
31 | if [ "$OPTIONS" = "-b" ]; then
32 | SOURCE_FILE="$2"
33 | if [ "$SOURCE_FILE" = "" ]; then
34 | printf "\n6502 assembly source file not found.\n" >&2
35 | exit
36 | fi
37 | CompilerAndLink "$SOURCE_FILE"
38 |
39 | elif [ "$1" = "--help" ]; then
40 | PrintHelper
41 | else
42 | printf "\nNot support Option:'%s'" "$OPTIONS"
43 | PrintHelper
44 | fi
45 |
46 |
47 |
--------------------------------------------------------------------------------
/assembly/nes.conf:
--------------------------------------------------------------------------------
1 | SYMBOLS {
2 | __STACKSIZE__: type = weak, value = $0300; # 3 pages stack
3 | }
4 | MEMORY {
5 | ZP: file = "", start = $0002, size = $001A, type = rw, define = yes;
6 |
7 | # INES Cartridge Header
8 | HEADER: file = %O, start = $0000, size = $0010, fill = yes;
9 |
10 | # 2 16K ROM Banks
11 | # - startup
12 | # - code
13 | # - rodata
14 | # - data (load)
15 | ROM0: file = %O, start = $8000, size = $7FFA, fill = yes, define = yes;
16 |
17 | # Hardware Vectors at End of 2nd 8K ROM
18 | ROMV: file = %O, start = $FFFA, size = $0006, fill = yes;
19 |
20 | # 1 8k CHR Bank
21 | ROM2: file = %O, start = $0000, size = $2000, fill = yes;
22 |
23 | # standard 2k SRAM (-zeropage)
24 | # $0100-$0200 cpu stack
25 | # $0200-$0500 3 pages for ppu memory write buffer
26 | # $0500-$0800 3 pages for cc65 parameter stack
27 | SRAM: file = "", start = $0500, size = __STACKSIZE__, define = yes;
28 |
29 | # additional 8K SRAM Bank
30 | # - data (run)
31 | # - bss
32 | # - heap
33 | RAM: file = "", start = $6000, size = $2000, define = yes;
34 | }
35 | SEGMENTS {
36 | ZEROPAGE: load = ZP, type = zp;
37 | HEADER: load = HEADER, type = ro;
38 | STARTUP: load = ROM0, type = ro, define = yes;
39 | LOWCODE: load = ROM0, type = ro, optional = yes;
40 | ONCE: load = ROM0, type = ro, optional = yes;
41 | CODE: load = ROM0, type = ro, define = yes;
42 | RODATA: load = ROM0, type = ro, define = yes;
43 | DATA: load = ROM0, run = RAM, type = rw, define = yes;
44 | VECTORS: load = ROMV, type = rw;
45 | CHARS: load = ROM2, type = rw;
46 | BSS: load = RAM, type = bss, define = yes;
47 | }
48 | FEATURES {
49 | CONDES: type = constructor,
50 | label = __CONSTRUCTOR_TABLE__,
51 | count = __CONSTRUCTOR_COUNT__,
52 | segment = ONCE;
53 | CONDES: type = destructor,
54 | label = __DESTRUCTOR_TABLE__,
55 | count = __DESTRUCTOR_COUNT__,
56 | segment = RODATA;
57 | CONDES: type = interruptor,
58 | label = __INTERRUPTOR_TABLE__,
59 | count = __INTERRUPTOR_COUNT__,
60 | segment = RODATA,
61 | import = __CALLIRQ__;
62 | }
--------------------------------------------------------------------------------
/assembly/nes4j.s:
--------------------------------------------------------------------------------
1 | ;
2 | ; Apache License, Version 2.0
3 | ;
4 | ; Copyright (c) 2023 杨奎 (Kui Yang)
5 | ;
6 | ;
7 | ; 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
8 | ;
9 | ; http://www.apache.org/licenses/LICENSE-2.0
10 | ;
11 | ; 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.
12 | ;
13 | .include "common/nes.inc"
14 |
15 | .segment "CHARS"
16 | .incbin "common/ascii.chr"
17 | LOG = $FF
18 | NULL = 0
19 | .segment "STARTUP"
20 |
21 | start:
22 | .byte LOG,"ra=\{c.a},rx=\{c.x},ry=\{c.y}",NULL
23 | sei
24 | clc
25 | lda #$80
26 | sta PPU_CTRL ;Enable val flag
27 | jmp waitvbl
28 |
29 | irq:
30 | jmp waitvbl
31 |
32 | nmi:
33 | lda #$00
34 | sta PPU_MASK
35 | lda PPU_STATUS ; Reset w to 0
36 | lda #$20
37 | sta PPU_ADDR
38 | lda #$01
39 | sta PPU_ADDR ; Set VRAM address to $2001
40 | ldx #$00
41 | VENDOR_RENDER: ; Write vendor name(nes4j)
42 | lda VENDOR_TEXT,x
43 | sta PPU_DATA
44 | inx
45 | cpx #$05
46 | bne VENDOR_RENDER
47 | lda #$20 ; Set VRAM address to $2041
48 | sta PPU_ADDR
49 | lda #$41
50 | sta PPU_ADDR
51 | ldx #$00
52 | RENDER_AUTHOR: ; Write author info
53 | lda PAUTHOR,x
54 | sta PPU_DATA
55 | inx
56 | cpx #$1b
57 | bne RENDER_AUTHOR
58 | lda #$00
59 | sta PPU_SCROLL
60 | lda PPU_STATUS ; Reset w to 0
61 | lda #$3f ; Set VRAM address to $3f01
62 | sta PPU_ADDR
63 | lda #$01
64 | sta PPU_ADDR
65 | palette:
66 | sta PPU_DATA
67 | adc #$01
68 | cmp #$0f
69 | bne palette
70 | lda #$00 ; Set x scroll was 0
71 | sta PPU_SCROLL
72 | lda #$00
73 | ; LDA #%11101000 ; Scroll y to 31 (31+3)=
74 | sta PPU_SCROLL
75 | lda #$80
76 | sta PPU_CTRL ;Switch to first name table
77 | lda #%00001010 ;Enable background show
78 | sta PPU_MASK
79 | jmp forever
80 |
81 |
82 | forever:
83 | jmp forever
84 |
85 | waitvbl:
86 | jmp waitvbl
87 |
88 | VENDOR_TEXT:
89 | ; nes4j
90 | .byte $4e,$45,$53,$14,$4a
91 | PAUTHOR:
92 | ;Author
93 | .byte $21,$55,$54,$48,$4f,$52,$1a
94 | AUTHOR: ;GZYangKui@github
95 | .byte $27,$3a,$39,$41,$4e,$47,$2b,$55,$49,$20,$47,$49,$54,$48,$55,$42
96 |
97 |
--------------------------------------------------------------------------------
/assembly/test_enable_render_vbl.s:
--------------------------------------------------------------------------------
1 | ;
2 | ; Apache License, Version 2.0
3 | ;
4 | ; Copyright (c) 2023 杨奎 (Kui Yang)
5 | ;
6 | ;
7 | ; 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
8 | ;
9 | ; http://www.apache.org/licenses/LICENSE-2.0
10 | ;
11 | ; 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.
12 | ;
13 | .include "common/nes.inc"
14 |
15 |
16 | .segment "CHARS"
17 | .incbin "common/ascii.chr"
18 |
19 | .segment "STARTUP"
20 |
21 |
22 | start:
23 | SEI
24 | CLC
25 | LDA #$80 ; enabl vbl flag
26 | STA PPU_CTRL
27 | LDA #%00001010 ; enable background render
28 | STA PPU_MASK
29 |
30 | waitvbl:
31 | jmp waitvbl
32 |
33 | forever:
34 | jmp forever
35 |
36 | nmi:
37 | ; LDA #$00 ;
38 | ; LDA #%00001010 ; enable background render
39 | ; STA PPU_MASK
40 | LDA #$20 ; set vram address to $2021
41 | STA PPU_ADDR
42 | LDA #$41
43 | STA PPU_ADDR
44 | LDX #$00 ; 14*3=42 ppu cycle
45 | wait257:
46 | INX ; 2
47 | CPX #$0a ; 2
48 | BNE wait257 ; 2
49 | LDX #$1e ; Write 30 times
50 | RENDER_CHAR:
51 | LDA #$21 ; write 'a' char
52 | STA PPU_DATA
53 | DEX
54 | CPX #$00
55 | BNE RENDER_CHAR ; set vram addr to $3f01
56 | LDA #$3f
57 | STA PPU_ADDR
58 | LDA #$01
59 | STA PPU_ADDR
60 | LDA #$01
61 | RENDER_PAL: ; Render palatte
62 | STA PPU_DATA
63 | ADC #$01
64 | CMP #$20
65 | BNE RENDER_PAL
66 | LDA #%10000000
67 | STA PPU_CTRL
68 | ; LDA #%00001010
69 | ; STA PPU_MASK
70 | LDA #$00
71 | STA PPU_SCROLL
72 | LDA #$00
73 | STA PPU_SCROLL
74 | JMP forever
75 |
76 | irq:
77 | jmp forever
78 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/NesConsoleHook.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin;
2 |
3 | import cn.navclub.nes4j.bin.io.JoyPad;
4 | import cn.navclub.nes4j.bin.logging.LoggerDelegate;
5 | import cn.navclub.nes4j.bin.logging.LoggerFactory;
6 | import cn.navclub.nes4j.bin.ppu.Frame;
7 |
8 | public interface NesConsoleHook {
9 | LoggerDelegate LOG = LoggerFactory.logger(NesConsoleHook.class);
10 |
11 | /**
12 | * Game loop
13 | */
14 | void callback(Integer fps, boolean enableRender, Frame frame, JoyPad joyPad, JoyPad joyPad1);
15 |
16 | /**
17 | * If nes rom use emulator custom logger instruction call this function
18 | *
19 | * @param tStr String template
20 | * @param value Eval value
21 | */
22 | default void logger(String tStr, String value) {
23 | LOG.info("Emulator say:{}", value);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/Channel.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu;
2 |
3 | import cn.navclub.nes4j.bin.core.Component;
4 | import lombok.Getter;
5 |
6 | import static cn.navclub.nes4j.bin.util.BinUtil.uint8;
7 |
8 | /**
9 | * Abstract audio channel
10 | *
11 | * @param Timer driver sequencer type
12 | * @see cn.navclub.nes4j.bin.apu.impl.PulseChannel
13 | * @see cn.navclub.nes4j.bin.apu.impl.TriangleChannel
14 | * @see cn.navclub.nes4j.bin.apu.impl.DMChannel
15 | * @see cn.navclub.nes4j.bin.apu.impl.NoiseChannel
16 | */
17 | public abstract class Channel implements Component {
18 | protected final APU apu;
19 | @Getter
20 | protected final LengthCounter lengthCounter;
21 |
22 | @Getter
23 | protected Timer timer;
24 | @Getter
25 | protected T sequencer;
26 | @Getter
27 | protected boolean enable;
28 |
29 |
30 | public Channel(final APU apu, T sequencer) {
31 | this.apu = apu;
32 | this.enable = false;
33 | this.sequencer = sequencer;
34 | this.lengthCounter = new LengthCounter();
35 | this.timer = sequencer == null ? null : new Timer<>(this.sequencer);
36 | }
37 |
38 | public Channel(APU apu) {
39 | this(apu, null);
40 | }
41 |
42 | /**
43 | * Due to apu all register only write except when open bus status register
44 | *
45 | * @param address {@inheritDoc}
46 | * @return {@inheritDoc}
47 | * @throws RuntimeException
48 | */
49 | @SuppressWarnings("all")
50 | @Override
51 | public byte read(int address) {
52 | throw new RuntimeException("Write-only register.");
53 | }
54 |
55 | @Override
56 | public void tick() {
57 | this.timer.tick();
58 | }
59 |
60 | public void setEnable(boolean enable) {
61 | this.enable = enable;
62 | //
63 | // Counting can be halted and the counter can be disabled by clearing the appropriate bit in the status
64 | // register,which immediately sets the counter to 0 and keeps it there.
65 | //
66 | if (!this.enable) {
67 | this.lengthCounter.setCounter(0);
68 | }
69 | }
70 |
71 | /**
72 | * When the enabled bit is cleared (via $4015), the length counter is forced to 0 and cannot be changed until
73 | * enabled is set again (the length counter's previous value is lost).
74 | */
75 | public void lengthTick() {
76 | if (!this.enable) {
77 | return;
78 | }
79 | this.lengthCounter.tick();
80 | }
81 |
82 | /**
83 | * Waveform channel output
84 | *
85 | * @return Channel output value
86 | */
87 | public abstract int output();
88 |
89 | /**
90 | * Update timer period
91 | *
92 | * @param address Register address
93 | * @param b Register value
94 | */
95 | protected void updateTimeValue(int address, byte b) {
96 | if (this.timer == null) {
97 | return;
98 | }
99 | var first = address == 0x4002 || address == 0x4006 || address == 0x400a;
100 | var second = address == 0x4003 || address == 0x4007 || address == 0x400b;
101 | var update = first || second;
102 | if (update) {
103 | var value = this.timer.getPeriod();
104 | if (first) {
105 | value = (value & 0xff00) | uint8(b);
106 | } else {
107 | value = (value & 0x00ff) | ((uint8(b) & 0x07) << 8);
108 | }
109 | this.timer.setPeriod(value);
110 | }
111 |
112 | }
113 |
114 | public int readState() {
115 | return 0;
116 | }
117 |
118 | @Override
119 | public void reset() {
120 | this.enable = false;
121 | this.lengthCounter.setCounter(0);
122 | this.lengthCounter.setHalt(true);
123 | if (this.timer != null) {
124 | this.timer.period = 0;
125 | this.timer.counter = 0;
126 | }
127 | if (this.sequencer != null) {
128 | this.sequencer.reset();
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/LengthCounter.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu;
2 |
3 | import cn.navclub.nes4j.bin.function.CycleDriver;
4 | import lombok.Data;
5 |
6 | import static cn.navclub.nes4j.bin.util.BinUtil.uint8;
7 |
8 | @Data
9 | public class LengthCounter implements CycleDriver {
10 | private static final int[] LOOKUP_TABLE = {
11 | 0x0a, 0xfe,
12 | 0x14, 0x02,
13 | 0x28, 0x04,
14 | 0x50, 0x06,
15 | 0xa0, 0x08,
16 | 0x3c, 0x0a,
17 | 0x0e, 0x0c,
18 | 0x1a, 0x0e,
19 | 0x0c, 0x10,
20 | 0x18, 0x12,
21 | 0x30, 0x14,
22 | 0x60, 0x16,
23 | 0xc0, 0x18,
24 | 0x48, 0x1a,
25 | 0x10, 0x1c,
26 | 0x20, 0x1e
27 | };
28 |
29 | private int counter;
30 | private boolean halt;
31 |
32 | /**
33 | * When clocked by the frame sequencer, if the halt flag is clear and the counter
34 | * is non-zero, it is decremented.
35 | */
36 | @Override
37 | public void tick() {
38 | if (this.halt || this.counter == 0) {
39 | return;
40 | }
41 | this.counter--;
42 | }
43 |
44 | /**
45 | *
46 | * Unless disabled, a write the channel's fourth register immediately reloads the
47 | * counter with the value from a lookup table, based on the index formed by the
48 | * upper 5 bits:
49 | *
50 | * iiii i--- length index
51 | *
52 | * bits bit 3
53 | * 7-4 0 1
54 | * -------
55 | * 0 $0A $FE
56 | * 1 $14 $02
57 | * 2 $28 $04
58 | * 3 $50 $06
59 | * 4 $A0 $08
60 | * 5 $3C $0A
61 | * 6 $0E $0C
62 | * 7 $1A $0E
63 | * 8 $0C $10
64 | * 9 $18 $12
65 | * A $30 $14
66 | * B $60 $16
67 | * C $C0 $18
68 | * D $48 $1A
69 | * E $10 $1C
70 | * F $20 $1E
71 | *
72 | *
73 | * @param b Register valuer
74 | */
75 | public void lookupTable(byte b) {
76 | this.counter = LOOKUP_TABLE[uint8(b) >> 3];
77 | }
78 |
79 |
80 | public int stateVal() {
81 | return this.counter > 0 ? 1 : 0;
82 | }
83 |
84 | /**
85 | *
86 | * In the actual APU, the length counter silences the channel when clocked while already zero
87 | * (provided the length counter halt flag isn't set). The values in the above table are the actual
88 | * values the length counter gets loaded with plus one, to allow us to use a model where the channel
89 | * is silenced when the length counter becomes zero.
90 | *
91 | * The triangle's linear counter works differently, and does silence the channel when it reaches zero.
92 | */
93 | public boolean silence() {
94 | return this.counter == 0 && !this.halt;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/LinearCounter.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu;
2 |
3 | import cn.navclub.nes4j.bin.function.CycleDriver;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 |
7 | public class LinearCounter implements CycleDriver {
8 | @Getter
9 | private int counter;
10 | //Reload flag
11 | @Setter
12 | private boolean halt;
13 | @Getter
14 | private boolean control;
15 | private int reloadValue;
16 |
17 | /**
18 | *
19 | * Register $4008 contains a control flag and reload value:
20 | *
21 | * crrr rrrr control flag, reload value
22 | *
23 | * Note that the bit position for the control flag is also mapped to a flag in the
24 | * Length Counter.
25 | *
26 | */
27 | public void update(byte b) {
28 | this.reloadValue = (b & 0x7f);
29 | this.control = (b & 0x80) == 0x80;
30 | }
31 |
32 | /**
33 | * When clocked by the frame sequencer, the following actions occur in order:
34 | *
35 | * If halt flag is set, set counter to reload value, otherwise if counter
36 | * is non-zero, decrement it.
37 | *
38 | * If control flag is clear, clear halt flag.
39 | */
40 | @Override
41 | public void tick() {
42 | if (this.halt) {
43 | this.counter = this.reloadValue;
44 | } else {
45 | if (this.counter != 0) {
46 | this.counter--;
47 | }
48 | }
49 | this.halt = (this.halt && this.control);
50 | }
51 |
52 | public void reset() {
53 | this.counter = 0;
54 | this.halt = false;
55 | this.control = false;
56 | this.reloadValue = 0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/Player.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu;
2 |
3 | /**
4 | * @author GZYangKui
5 | */
6 | public interface Player {
7 | /**
8 | * When apu component product a sample will call this method
9 | *
10 | * @param sample Audio sample
11 | */
12 | void output(byte sample);
13 |
14 | /**
15 | * When game was close will call this method release resource
16 | */
17 | default void stop() {
18 |
19 | }
20 |
21 |
22 | default void reset() {
23 |
24 | }
25 |
26 | static Player newInstance(Class extends Player> clazz, Object... args) {
27 | try {
28 | return clazz.getConstructor(Integer.class).newInstance(args);
29 | } catch (Exception e) {
30 | throw new RuntimeException(e);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/Sequencer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu;
2 |
3 | import cn.navclub.nes4j.bin.function.CycleDriver;
4 |
5 | /**
6 | * A sequencer continuously loops over a sequence of values or events. When clocked, the next item in the sequence
7 | * is generated. In this APU documentation, clocking a sequencer usually means either advancing to the next step in
8 | * a waveform, or the event sequence of the Frame Counter device.
9 | *
10 | * @author GZYangKui
11 | * @see cn.navclub.nes4j.bin.apu.impl.sequencer.NoiseSequencer
12 | * @see cn.navclub.nes4j.bin.apu.impl.sequencer.TriangleSequencer
13 | * @see cn.navclub.nes4j.bin.apu.impl.sequencer.SeqSequencer
14 | */
15 | public interface Sequencer extends CycleDriver {
16 | /**
17 | * Current sequencer value
18 | *
19 | * @return Return current sequencer value
20 | */
21 | int value();
22 |
23 | /**
24 | * Reset Sequencer
25 | */
26 | void reset();
27 | }
28 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/SweepUnit.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu;
2 |
3 | import cn.navclub.nes4j.bin.apu.impl.PulseChannel;
4 | import cn.navclub.nes4j.bin.apu.impl.timer.Divider;
5 | import cn.navclub.nes4j.bin.function.CycleDriver;
6 | import lombok.Getter;
7 |
8 | /**
9 | * An NES APU sweep unit can be made to periodically adjust a pulse channel's period up or down.
10 | * Each sweep unit contains the following:
11 | * divider
12 | * reload flag
13 | *
14 | * @author GZYangKui
15 | */
16 | public class SweepUnit implements CycleDriver {
17 | //Divider
18 | private final Divider divider;
19 | private final PulseChannel channel;
20 | //Calculate result
21 | private int result;
22 | //Shift
23 | private int shift;
24 | private boolean reloadFlag;
25 | //Enable flag
26 | private boolean enable;
27 | @Getter
28 | private boolean silence;
29 | //Negative flag
30 | private boolean negative;
31 |
32 |
33 | public SweepUnit(PulseChannel channel) {
34 | this.channel = channel;
35 | //
36 | // if the sweep unit is enabled and the
37 | // shift count is greater than 0, when the divider outputs a clock, the channel's
38 | // period in the third and fourth registers are updated with the result of the
39 | // shifter.
40 | //
41 | this.divider = new Divider((n) -> {
42 | //
43 | // When the channel's period is less than 8 or the result of the shifter is
44 | // greater than $7FF, the channel's DAC receives 0 and the sweep unit doesn't
45 | // change the channel's period.Otherwise
46 | //
47 | if (!this.silence && this.enable && this.shift > 0) {
48 | this.channel.timer.setPeriod(result);
49 | }
50 | });
51 | }
52 |
53 | /**
54 | *
55 | * A channel's second register configures the sweep unit:
56 | *
57 | * eppp nsss enable, period, negate, shift
58 | *
59 | * The divider's period is set to p + 1.
60 | *
61 | */
62 | public void update(byte value) {
63 | this.reloadFlag = true;
64 | this.shift = value & 0x07;
65 | this.enable = ((value & 0x80) == 0x80);
66 | this.negative = (value & 0x08) == 0x08;
67 | this.divider.setPeriod(((value & 0x70) >> 4) + 1);
68 | }
69 |
70 | /**
71 | * The shifter continuously calculates a result based on the channel's period. The
72 | * channel's period (from the third and fourth registers) is first shifted right
73 | * by s bits. If negate is set, the shifted value's bits are inverted, and on the
74 | * second square channel, the inverted value is incremented by 1. The resulting
75 | * value is added with the channel's current period, yielding the final result.
76 | *
77 | * @return Calculate result
78 | */
79 | private int calculate(int val) {
80 | var tmp = (val >> this.shift);
81 |
82 | if (this.negative) {
83 | tmp = Math.negateExact(tmp);
84 | if (this.channel.isSecond()) {
85 | tmp -= 1;
86 | }
87 | }
88 |
89 | return tmp + val;
90 | }
91 |
92 | /**
93 | * When the sweep unit is clocked, the divider is *first* clocked and then if
94 | * there was a write to the sweep register since the last sweep clock, the divider
95 | * is reset.
96 | */
97 | @Override
98 | public void tick() {
99 | this.divider.tick();
100 | if (this.reloadFlag) {
101 | this.divider.reset();
102 | this.reloadFlag = false;
103 | }
104 | var tmp = this.channel.timer.period;
105 | this.result = this.calculate(tmp);
106 | this.silence = tmp < 8 || result > 0x7ff;
107 | }
108 |
109 | public void reset() {
110 | this.shift = 0;
111 | this.result = 0;
112 | this.enable = false;
113 | this.silence = false;
114 | this.negative = false;
115 | this.reloadFlag = false;
116 | this.divider.period = 0;
117 | this.divider.counter = 0;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/Timer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu;
2 |
3 | import cn.navclub.nes4j.bin.apu.impl.timer.Divider;
4 | import cn.navclub.nes4j.bin.function.CycleDriver;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 |
8 | /**
9 | * A timer is used in each of the five channels to control the sound frequency. It contains a divider which
10 | * is clocked by the CPU clock. The triangle channel's timer is clocked on every CPU cycle, but the pulse, noise,
11 | * and DMC timers are clocked only on every second CPU cycle and thus produce only even periods.
12 | *
13 | * @author GZYangKui
14 | * @see cn.navclub.nes4j.bin.apu.impl.TriangleChannel
15 | * @see Divider
16 | */
17 | public class Timer implements CycleDriver {
18 | @Getter
19 | protected int counter;
20 | @Setter
21 | @Getter
22 | protected int period;
23 | protected final T sequencer;
24 |
25 | public Timer(T sequencer) {
26 | this.sequencer = sequencer;
27 | }
28 |
29 | @Override
30 | public void tick() {
31 | if (this.counter == 0) {
32 | this.counter = this.period;
33 | //Generate sequence
34 | if (this.sequencer != null) {
35 | this.sequencer.tick();
36 | }
37 | } else {
38 | if (this.counter > 0) {
39 | this.counter--;
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/NoiseChannel.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl;
2 |
3 | import cn.navclub.nes4j.bin.apu.Channel;
4 | import cn.navclub.nes4j.bin.apu.Envelope;
5 | import cn.navclub.nes4j.bin.apu.impl.sequencer.NoiseSequencer;
6 | import cn.navclub.nes4j.bin.apu.APU;
7 | import lombok.Getter;
8 |
9 | /**
10 | * Noise channel
11 | *
12 | * +---------+ +---------+ +---------+
13 | * | Timer |--->| Random | | Length |
14 | * +---------+ +---------+ +---------+
15 | * | |
16 | * v v
17 | * +---------+ |\ |\ +---------+
18 | * |Envelope |------->| >----------->| >------->| DAC |
19 | * +---------+ |/ |/ +---------+
20 | *
21 | */
22 | @Getter
23 | public class NoiseChannel extends Channel {
24 | private static final int[] LOOK_TABLE = {
25 | 0x004,
26 | 0x008,
27 | 0x010,
28 | 0x020,
29 | 0x040,
30 | 0x060,
31 | 0x080,
32 | 0x0a0,
33 | 0x0ca,
34 | 0x0fe,
35 | 0x17c,
36 | 0x1fc,
37 | 0x2fa,
38 | 0x3f8,
39 | 0x3f2,
40 | 0xfe4
41 | };
42 | private final Envelope envelope;
43 |
44 | public NoiseChannel(APU apu) {
45 | super(apu, new NoiseSequencer());
46 | this.envelope = new Envelope();
47 | }
48 |
49 | @Override
50 | public void write(int address, byte b) {
51 | if (address == 0x400c) {
52 | this.envelope.update(b);
53 |
54 | if (this.envelope.shareFBit()) {
55 | this.lengthCounter.setHalt((b & 0x20) != 0);
56 | }
57 | }
58 | //
59 | // Register $400E sets the random generator mode and timer period based on a 4-bit
60 | // index into a period table:
61 | //
62 | // m--- iiii mode, period index
63 | //
64 | // i timer period
65 | // ----------------
66 | // 0 $004
67 | // 1 $008
68 | // 2 $010
69 | // 3 $020
70 | // 4 $040
71 | // 5 $060
72 | // 6 $080
73 | // 7 $0A0
74 | // 8 $0CA
75 | // 9 $0FE
76 | // A $17C
77 | // B $1FC
78 | // C $2FA
79 | // D $3F8
80 | // E $7F2
81 | // F $FE4
82 | //
83 | if (address == 0x400e) {
84 | var index = b & 0x0f;
85 | var mode = (b & 0x80) >> 7;
86 | this.timer.setPeriod(LOOK_TABLE[index]);
87 | this.sequencer.setMode(mode);
88 | }
89 |
90 | if (address == 0x400f) {
91 | this.envelope.resetLoop();
92 | if (this.enable) {
93 | this.lengthCounter.setCounter(b >>> 3);
94 | }
95 | }
96 | }
97 |
98 | /**
99 | * The mixer receives the current envelope volume except when
100 | *
101 | * Bit 0 of the shift register is set, or
102 | * The length counter is zero
103 | * Within the mixer, the DMC level has a noticeable effect on the noise's level.
104 | */
105 | @Override
106 | public int output() {
107 | if (!this.enable
108 | || this.sequencer.value() == 1
109 | || this.lengthCounter.silence()) {
110 | return 0;
111 | }
112 | return this.envelope.getVolume();
113 | }
114 |
115 | @Override
116 | public int readState() {
117 | return this.lengthCounter.stateVal() << 3;
118 | }
119 |
120 | @Override
121 | public void reset() {
122 | super.reset();
123 | this.envelope.reset();
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/PulseChannel.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl;
2 |
3 | import cn.navclub.nes4j.bin.apu.Channel;
4 | import cn.navclub.nes4j.bin.apu.Envelope;
5 | import cn.navclub.nes4j.bin.apu.SweepUnit;
6 | import cn.navclub.nes4j.bin.apu.impl.sequencer.SeqSequencer;
7 | import cn.navclub.nes4j.bin.apu.APU;
8 | import lombok.Getter;
9 |
10 | /**
11 | *
12 | * Each of the two NES APU pulse (square) wave channels generate a pulse wave with variable duty.
13 | *
14 | *
15 | * Each pulse channel contains the following:
16 | *
17 | * envelope generator
18 | * sweep unit
19 | * timer
20 | * 8-step sequencer
21 | * length counter
22 | *
23 | * @author GZYangKui
24 | */
25 | @Getter
26 | public class PulseChannel extends Channel {
27 | private final boolean second;
28 | private final Envelope envelope;
29 | private final SweepUnit sweepUnit;
30 |
31 | public PulseChannel(APU apu, boolean second) {
32 | super(apu, new SeqSequencer());
33 |
34 | this.second = second;
35 | this.envelope = new Envelope();
36 | this.sweepUnit = new SweepUnit(this);
37 | }
38 |
39 | @Override
40 | public void write(int address, byte b) {
41 | //$4000/4 ddle nnnn duty, loop env/disable length, env disable, vol/env
42 | if (address == 0x4000 || address == 0x4004) {
43 | this.envelope.update(b);
44 |
45 | if (this.envelope.shareFBit()) {
46 | this.lengthCounter.setHalt((b & 0x20) != 0);
47 | }
48 | //Update duty
49 | this.sequencer.setDuty((b & 0xc0) >> 6);
50 | }
51 |
52 | //Update sweep properties
53 | if (address == 0x4001 || address == 0x4005) {
54 | this.sweepUnit.update(b);
55 | }
56 |
57 | if (address == 0x4003 || address == 0x4007) {
58 | this.envelope.resetLoop();
59 | if (this.enable) {
60 | this.lengthCounter.lookupTable(b);
61 | }
62 | }
63 |
64 | //Update timer period
65 | this.updateTimeValue(address, b);
66 | }
67 |
68 | /**
69 | *
70 | * +---------+ +---------+
71 | * | Sweep |--->|Timer / 2|
72 | * +---------+ +---------+
73 | * | |
74 | * | v
75 | * | +---------+ +---------+
76 | * | |Sequencer| | Length |
77 | * | +---------+ +---------+
78 | * | | |
79 | * v v v
80 | * +---------+ |\ |\ |\ +---------+
81 | * |Envelope |------->| >----------->| >----------->| >-------->| DAC |
82 | * +---------+ |/ |/ |/ +---------+
83 | *
84 | *
85 | *
86 | * Pulse channel output to mixer
87 | *
88 | *
89 | * The mixer receives the pulse channel's current envelope volume (lower 4 bits from $4000 or $4004) except when
90 | *
The sequencer output is zero, or
91 | * overflow from the sweep unit's adder is silencing the channel, or
92 | * the length counter is zero, or
93 | * the timer has a value less than eight (t<8, noted above).
94 | * If any of the above are true, then the pulse channel sends zero (silence) to the mixer.
95 | *
96 | */
97 | @Override
98 | public int output() {
99 | if (!this.enable
100 | || this.sequencer.value() == 0
101 | || this.lengthCounter.silence()
102 | || this.sweepUnit.isSilence()) {
103 | return 0;
104 | }
105 | return this.envelope.getVolume();
106 | }
107 |
108 | @Override
109 | public void lengthTick() {
110 | super.lengthTick();
111 | this.sweepUnit.tick();
112 | }
113 |
114 | @Override
115 | public int readState() {
116 | return this.lengthCounter.stateVal() << (this.second ? 1 : 0);
117 | }
118 |
119 | @Override
120 | public void reset() {
121 | super.reset();
122 | this.envelope.reset();
123 | this.sweepUnit.reset();
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/TriangleChannel.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl;
2 |
3 | import cn.navclub.nes4j.bin.apu.Channel;
4 | import cn.navclub.nes4j.bin.apu.LinearCounter;
5 | import cn.navclub.nes4j.bin.apu.impl.sequencer.TriangleSequencer;
6 | import cn.navclub.nes4j.bin.apu.APU;
7 | import cn.navclub.nes4j.bin.apu.impl.timer.TriangleTimer;
8 | import lombok.Getter;
9 |
10 | /**
11 | *
12 | * The NES APU triangle channel generates a pseudo-triangle wave. It has no volume control; the waveform is either
13 | * cycling or suspended. It includes a linear counter, an extra duration timer of higher accuracy than the length
14 | * counter.
15 | *
16 | *
17 | * The triangle channel contains the following: timer, length counter, linear counter, linear counter reload flag,
18 | * control flag, sequencer.
19 | *
20 | *
21 | * Linear Counter Length Counter
22 | * | |
23 | * v v
24 | * Timer ---> Gate ----------> Gate ---> Sequencer ---> (to mixer)
25 | *
26 | */
27 | @Getter
28 | public class TriangleChannel extends Channel {
29 | private final LinearCounter linearCounter;
30 |
31 | public TriangleChannel(APU apu) {
32 | super(apu);
33 | this.linearCounter = new LinearCounter();
34 | this.sequencer = new TriangleSequencer();
35 | this.timer = new TriangleTimer(this.sequencer, this);
36 | }
37 |
38 | @Override
39 | public void write(int address, byte b) {
40 |
41 | if (address == 0x4008) {
42 | this.linearCounter.update(b);
43 | if (!this.linearCounter.isControl()) {
44 | this.lengthCounter.setHalt((b & 0x80) == 0x80);
45 | }
46 | }
47 |
48 | if (address == 0x400b) {
49 | //When register $400B is written to, the halt flag is set.
50 | this.linearCounter.setHalt(true);
51 | if (this.enable) {
52 | this.lengthCounter.lookupTable(b);
53 | }
54 | }
55 |
56 | this.updateTimeValue(address, b);
57 | }
58 |
59 | /**
60 | *
61 | * +---------+ +---------+
62 | * |LinearCtr| | Length |
63 | * +---------+ +---------+
64 | * | |
65 | * v v
66 | * +---------+ |\ |\ +---------+ +---------+
67 | * | Timer |------->| >----------->| >------->|Sequencer|--->| DAC |
68 | * +---------+ |/ |/ +---------+ +---------+
69 | *
70 | */
71 | @Override
72 | public int output() {
73 | // Silencing the triangle channel merely halts it. It will continue to output its last value rather than 0.
74 | return sequencer.value();
75 | }
76 |
77 | @Override
78 | public int readState() {
79 | return this.lengthCounter.stateVal() << 2;
80 | }
81 |
82 | @Override
83 | public void reset() {
84 | super.reset();
85 | this.linearCounter.reset();
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/sequencer/NoiseSequencer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl.sequencer;
2 |
3 | import cn.navclub.nes4j.bin.apu.Sequencer;
4 | import lombok.Setter;
5 |
6 | /**
7 | *
8 | *
9 | * bit: 14 13 12 11 10 9 8 7 6 4 3 2 1 0
10 | * ^ | | |
11 | * | v v |
12 | * | \"1"""""""0"/ |
13 | * | $400E.7 ---->\ Mux / |
14 | * | \_______/ |
15 | * | | |
16 | * | /"""""//<-------' |
17 | * `------( XOR (( |
18 | * \_____\\<-----------------'
19 | *
20 | * The shift register is clocked by the timer and the vacated bit 14 is filled
21 | * with the exclusive-OR of *pre-shifted* bits 0 and 1 (mode = 0) or bits 0 and 6
22 | * (mode = 1), resulting in 32767-bit and 93-bit sequences, respectively.
23 | *
24 | *
25 | * @author GZYangKui
26 | */
27 | public class NoiseSequencer implements Sequencer {
28 | @Setter
29 | private int mode;
30 |
31 | private int sequence;
32 |
33 | public NoiseSequencer() {
34 | this.reset();
35 | }
36 |
37 | @Override
38 | public int value() {
39 | return this.sequence & 0x01;
40 | }
41 |
42 | /**
43 | *
44 | * The shift register is 15 bits wide, with bits numbered
45 | * 14 - 13 - 12 - 11 - 10 - 9 - 8 - 7 - 6 - 5 - 4 - 3 - 2 - 1 - 0
46 | *
47 | * When the timer clocks the shift register, the following actions occur in order:
48 | *
49 | * Feedback is calculated as the exclusive-OR of bit 0 and one other bit: bit 6 if Mode flag is set, otherwise bit 1.
50 | * The shift register is shifted right by one bit.
51 | * Bit 14, the leftmost bit, is set to the feedback calculated earlier.
52 | * This results in a pseudo-random bit sequence, 32767 steps long when Mode flag is clear,
53 | * and randomly 93 or 31 steps long otherwise. (The particular 31- or 93-step sequence depends on where in the 32767-step sequence the shift register was when Mode flag was set).
54 | *
55 | */
56 | @Override
57 | public void tick() {
58 | var index = this.mode == 0 ? 1 : 6;
59 | var a = this.sequence & 0x01;
60 | var b = (this.sequence >> index) & 0x01;
61 | this.sequence >>= 1;
62 | this.sequence |= ((a ^ b) << 14);
63 | }
64 |
65 | @Override
66 | public void reset() {
67 | this.mode = 0;
68 | //On power-up, the shift register is loaded with the value 1.
69 | this.sequence = 1;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/sequencer/SeqSequencer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl.sequencer;
2 |
3 | import cn.navclub.nes4j.bin.apu.Sequencer;
4 | import lombok.Setter;
5 |
6 | /**
7 | * Sequencer behavior
8 | *
9 | * The sequencer is clocked by an 11-bit timer. Given the timer value t = HHHLLLLLLLL formed by timer high and timer
10 | * low, this timer is updated every APU cycle (i.e., every second CPU cycle), and counts t, t-1, ..., 0, t, t-1, ...,
11 | * clocking the waveform generator when it goes from 0 to t. Since the period of the timer is t+1 APU cycles and the
12 | * sequencer has 8 steps, the period of the waveform is 8*(t+1) APU cycles, or equivalently 16*(t+1) CPU cycles.
13 | *
14 | *
15 | * Hence
16 | *
fpulse = fCPU/(16*(t+1)) (where fCPU is 1.789773 MHz for NTSC, 1.662607 MHz for PAL, and 1.773448 MHz for Dendy)
17 | * t = fCPU/(16*fpulse) - 1
18 | *
19 | *
20 | * Note: A period of t < 8, either set explicitly or via a sweep period update, silences the corresponding pulse
21 | * channel. The highest frequency a pulse channel can output is hence about 12.4 kHz for NTSC. (TODO: PAL behavior?)
22 | *
23 | *
24 | * @author GZYangKui
25 | */
26 | public class SeqSequencer implements Sequencer {
27 | /**
28 | *
29 | * The reason for the odd output from the sequencer is that the counter is initialized to zero
30 | * but counts downward rather than upward. Thus it reads the sequence lookup table in the order
31 | * 0, 7, 6, 5, 4, 3, 2, 1.
32 | *
33 | */
34 | private final byte[][] sequences = new byte[][]{
35 | {0, 0, 0, 0, 0, 0, 0, 1},
36 | {0, 0, 0, 0, 0, 0, 1, 1},
37 | {0, 0, 0, 0, 1, 1, 1, 1},
38 | {1, 1, 1, 1, 1, 1, 0, 0}
39 | };
40 | @Setter
41 | private int duty;
42 | private int index;
43 |
44 | @Override
45 | public void tick() {
46 | this.index = (this.index + 1) % 8;
47 | }
48 |
49 | @Override
50 | public int value() {
51 | return this.sequences[this.duty][this.index];
52 | }
53 |
54 | @Override
55 | public void reset() {
56 | this.duty = 0;
57 | this.index = 0;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/sequencer/TriangleSequencer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl.sequencer;
2 |
3 | import cn.navclub.nes4j.bin.apu.Sequencer;
4 |
5 | public class TriangleSequencer implements Sequencer {
6 | private final int[] sequencer = new int[]{
7 | 0x0f,
8 | 0x0e,
9 | 0x0d,
10 | 0x0c,
11 | 0x0b,
12 | 0x0a,
13 | 0x09,
14 | 0x08,
15 | 0x07,
16 | 0x06,
17 | 0x05,
18 | 0x04,
19 | 0x03,
20 | 0x02,
21 | 0x01,
22 | 0x00,
23 | 0x00,
24 | 0x01,
25 | 0x02,
26 | 0x03,
27 | 0x04,
28 | 0x05,
29 | 0x06,
30 | 0x07,
31 | 0x08,
32 | 0x09,
33 | 0x0a,
34 | 0x0b,
35 | 0x0c,
36 | 0x0d,
37 | 0x0e,
38 | 0x0f
39 | };
40 |
41 | private int index;
42 |
43 | @Override
44 | public int value() {
45 | return this.sequencer[this.index];
46 | }
47 |
48 | @Override
49 | public void tick() {
50 | this.index = (this.index + 1) % sequencer.length;
51 | }
52 |
53 | @Override
54 | public void reset() {
55 | this.index = 0;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/timer/Divider.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl.timer;
2 |
3 | import cn.navclub.nes4j.bin.apu.Sequencer;
4 | import cn.navclub.nes4j.bin.apu.Timer;
5 | import lombok.Setter;
6 |
7 | import java.util.function.Consumer;
8 | import java.util.function.Function;
9 |
10 | /**
11 | *
12 | * A divider outputs a clock periodically. It contains a period reload value, P, and a counter,
13 | * that starts at P. When the divider is clocked, if the counter is currently 0, it is reloaded with
14 | * P and generates an output clock, otherwise the counter is decremented. In other words, the divider's
15 | * period is P + 1.
16 | *
17 | *
18 | * A divider can also be forced to reload its counter immediately (counter = P), but this does not output
19 | * a clock. Similarly, changing a divider's period reload value does not affect the counter. Some counters
20 | * offer no way to force a reload, but setting P to 0 at least synchronizes it to a known state once the
21 | * current count expires.
22 | *
23 | *
24 | * A divider may be implemented as a down counter (5, 4, 3, ...) or as a linear feedback shift register
25 | * (LFSR). The dividers in the pulse and triangle channels are linear down-counters. The dividers for
26 | * noise, DMC, and the APU Frame Counter are implemented as LFSRs to save gates compared to the equivalent
27 | * down counter.
28 | *
29 | *
30 | * @author GZYangKui
31 | */
32 | public class Divider extends Timer {
33 | private final Consumer consumer;
34 |
35 | public Divider(Consumer consumer) {
36 | super(null);
37 | this.consumer = consumer;
38 | }
39 |
40 | @Override
41 | public void tick() {
42 | this.counter--;
43 | //Reset divider and clock
44 | if (this.counter == 0) {
45 | this.reset();
46 | if (this.consumer != null) {
47 | consumer.accept(null);
48 | }
49 | }
50 | }
51 |
52 | public void reset() {
53 | this.counter = this.period;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/apu/impl/timer/TriangleTimer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.apu.impl.timer;
2 |
3 | import cn.navclub.nes4j.bin.apu.Timer;
4 | import cn.navclub.nes4j.bin.apu.impl.TriangleChannel;
5 | import cn.navclub.nes4j.bin.apu.impl.sequencer.TriangleSequencer;
6 |
7 | /**
8 | * Triangle timer
9 | *
10 | * @author GZYangKui
11 | */
12 | public class TriangleTimer extends Timer {
13 | private final TriangleChannel channel;
14 |
15 | @SuppressWarnings("all")
16 | public TriangleTimer(TriangleSequencer sequencer, TriangleChannel channel) {
17 | super(sequencer);
18 | this.channel = channel;
19 | }
20 |
21 | @Override
22 | public void tick() {
23 | if (this.counter == 0) {
24 | this.counter = this.period;
25 | } else {
26 | if (this.counter > 0)
27 | this.counter--;
28 | //
29 | // When the timer generates a clock and the Length Counter and Linear Counter both
30 | // have a non-zero count, the sequencer is clocked.
31 | //
32 | if (this.counter == 0) {
33 | var linearCounter = this.channel.getLinearCounter();
34 | var lengthCounter = this.channel.getLengthCounter();
35 | if (channel.isEnable() && lengthCounter.getCounter() != 0 && linearCounter.getCounter() != 0) {
36 | this.sequencer.tick();
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/AddressMode.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | /**
4 | * CPU Address mode
5 | *
6 | * @author GZYangKui
7 | */
8 | public enum AddressMode {
9 | Accumulator,
10 | Immediate,
11 | ZeroPage,
12 | ZeroPage_X,
13 | ZeroPage_Y,
14 | Absolute,
15 | Absolute_X,
16 | Absolute_Y,
17 | Indirect,
18 | Indirect_X,
19 | Indirect_Y,
20 | Implied,
21 | Relative
22 | }
23 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/AudioSampleRate.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | /**
4 | * Enum variable audio sample rate
5 | *
6 | * @author GZYangKui
7 | */
8 | public enum AudioSampleRate {
9 | HZ11025(11025),
10 | HZ22050(22050),
11 | HZ44100(44100),
12 | HZ48000(48000),
13 | HZ96000(96000);
14 |
15 | public final int value;
16 | public final int sample;
17 |
18 | AudioSampleRate(int sample) {
19 | this.sample = sample;
20 | // 1.79 MHz / sample = value
21 | this.value = Math.floorDiv(1790000, sample);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/CPUInterrupt.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | import lombok.Getter;
4 |
5 | /**
6 | * NMI、IRQ属于外部输入硬件中断
7 | */
8 | @Getter
9 | public enum CPUInterrupt {
10 | //PPU
11 | NMI(7, 0xfffa),
12 | //APU
13 | IRQ(7, 0xfffe),
14 | //CPU
15 | BRK(2, 0xfffe);
16 |
17 | private final int cycle;
18 | private final int vector;
19 |
20 | CPUInterrupt(int cycle, int vector) {
21 | this.cycle = cycle;
22 | this.vector = vector;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/ChannelType.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public enum ChannelType {
7 | PULSE(0x4000, .5),
8 | PULSE1(0x4004, .5),
9 | TRIANGLE(0x4008, 1),
10 | NOISE(0x400c, .5),
11 | DMC(0x4010, 1);
12 |
13 | private final int offset;
14 | private final double multiple;
15 |
16 | ChannelType(int offset, double multiple) {
17 | this.offset = offset;
18 | this.multiple = multiple;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/ICPUStatus.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | public enum ICPUStatus {
4 | //Carry flag
5 | CARRY,
6 | //Zero flag
7 | ZERO,
8 | //Interrupt disable
9 | INTERRUPT_DISABLE,
10 | //Decimal mode
11 | DECIMAL_MODE,
12 | //Break command
13 | BREAK_COMMAND,
14 | //B_FLAG
15 | B_FLAG,
16 | //Overflow flag
17 | OVERFLOW,
18 | //Negative flag
19 | NEGATIVE
20 | }
21 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/Instruction.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 |
4 | /**
5 | * 6502 instructions
6 | *
7 | * @author GZYangKui
8 | */
9 | public enum Instruction {
10 | ADC,
11 | LDA,
12 | STA,
13 | STX,
14 | STY,
15 | AND,
16 | ORA,
17 | EOR,
18 | PHA,
19 | PHP,
20 | PLA,
21 | PLP,
22 | ASL,
23 | LSR,
24 | CMP,
25 | CPX,
26 | CPY,
27 | LDX,
28 | LDY,
29 | INC,
30 | JSR,
31 | BIT,
32 | DEC,
33 | JMP,
34 | ROR,
35 | ROL,
36 | SBC,
37 | DEX,
38 | DEY,
39 | RTS,
40 | CLC,
41 | CLD,
42 | CLI,
43 | CLV,
44 | INX,
45 | INY,
46 | NOP,
47 | BCC,
48 | BCS,
49 | BEQ,
50 | BNE,
51 | BPL,
52 | BMI,
53 | BVC,
54 | BVS,
55 | RTI,
56 | SEC,
57 | SED,
58 | SEI,
59 | TAX,
60 | TAY,
61 | TSX,
62 | TXA,
63 | TXS,
64 | TYA,
65 | BRK,
66 | ALR,
67 | SHX,
68 | XAA,
69 | ARR,
70 | RRA,
71 | LAS,
72 | LXA,
73 | ANC,
74 | LAX,
75 | DCP,
76 | RLA,
77 | ISC,
78 | SLO,
79 | SRE,
80 | SAX,
81 | NOP_S,
82 | LOG
83 | }
84 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/MSequencer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public enum MSequencer {
7 | FOUR_STEP_SEQ(new int[]{7457, 7456, 7458, 7458}),
8 | FIVE_STEP_SEQ(new int[]{7457, 7456, 7458, 14910});
9 |
10 | //将APU时钟换算为CPU时钟
11 | private final int[] steps;
12 |
13 | MSequencer(int[] steps) {
14 | this.steps = steps;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/NESFormat.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | /**
4 | * NES文件类型
5 | *
6 | * @author GZYangKui
7 | */
8 | public enum NESFormat {
9 | INES,
10 | NES_20
11 | }
12 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/NMapper.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.core.Mapper;
5 | import cn.navclub.nes4j.bin.core.impl.*;
6 | import cn.navclub.nes4j.bin.io.Cartridge;
7 |
8 |
9 | /**
10 | * INES Mappers
11 | */
12 | public enum NMapper {
13 | NROM(NRMapper.class),
14 | MMC1(MMC1Mapper.class),
15 | UX_ROM(UXMapper.class),
16 | CN_ROM(CNMapper.class),
17 | MMC3(MMC3Mapper.class),
18 | NOT_IMPL_5,
19 | NOT_IMPL_6,
20 | NOT_IMPL_7,
21 | NOT_IMPL_8,
22 | NOT_IMPL_9,
23 | NOT_IMPL_10,
24 | NOT_IMPL_11,
25 | NOT_IMPL_12,
26 | NOT_IMPL_13,
27 | NOT_IMPL_14,
28 | NOT_IMPL_15,
29 | NOT_IMPL_16,
30 | NOT_IMPL_17,
31 | NOT_IMPL_18,
32 | NOT_IMPL_19,
33 | NOT_IMPL_20,
34 | NOT_IMPL_21,
35 | NOT_IMPL_22,
36 | KONAMI_VRC24(KonamiVRC24.class),
37 | UNKNOWN;
38 |
39 | private final Class extends Mapper> provider;
40 |
41 | NMapper(Class extends Mapper> provider) {
42 | this.provider = provider;
43 | }
44 |
45 | NMapper() {
46 | this(null);
47 | }
48 |
49 | @SuppressWarnings("all")
50 | public T newProvider(Cartridge cartridge, NesConsole console) {
51 | try {
52 | return (T) this
53 | .provider
54 | .getDeclaredConstructor(Cartridge.class, NesConsole.class)
55 | .newInstance(cartridge, console);
56 | } catch (Exception e) {
57 | throw new RuntimeException(e);
58 | }
59 | }
60 |
61 | public boolean isImpl() {
62 | return this.provider != null;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/NameMirror.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | public enum NameMirror {
4 | HORIZONTAL,
5 | VERTICAL,
6 | ONE_SCREEN_LOWER,
7 | ONE_SCREEN_UPPER,
8 | FOUR_SCREEN
9 | }
10 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/NameTMirror.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 |
4 | public enum NameTMirror {
5 | L1(0x2000),
6 | L2(0x2400),
7 | L3(0x2800),
8 | L4(0x2c00);
9 |
10 | public final int address;
11 |
12 | NameTMirror(int address) {
13 | this.address = address;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/PControl.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | public enum PControl {
4 | NAME_TABLE1,
5 | NAME_TABLE2,
6 | VRAM_INCREMENT,
7 | SPRITE_PATTERN_ADDR,
8 | BKG_PATTERN_TABLE,
9 | SPRITE_SIZE,
10 | MASTER_SLAVE,
11 | V_BANK_NMI
12 | }
13 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/PMask.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | public enum PMask {
4 | // Greyscale (0: normal color, 1: produce a greyscale display)
5 | GREYSCALE,
6 | //
7 | // Bits 1 and 2 enable rendering of the background and sprites in the leftmost 8 pixel columns.
8 | // Setting these bits to 0 will mask these columns, which is often useful in horizontal scrolling
9 | // situations where you want partial sprites or tiles to scroll in from the left.
10 | //
11 | LEFTMOST_8PXL_BACKGROUND,
12 | LEFTMOST_8PXL_SPRITE,
13 | //Bits 3 and 4 enable the rendering of background and sprites, respectively.
14 | SHOW_BACKGROUND,
15 | SHOW_SPRITES,
16 | //
17 | // Bit 0 controls a greyscale mode, which causes the palette to use only the colors from the grey
18 | // column: $00, $10, $20, $30. This is implemented as a bitwise AND with $30 on any value read from
19 | // PPU $3F00-$3FFF, both on the display and through PPUDATA. Writes to the palette through PPUDATA are
20 | // not affected. Also note that black colours like $0F will be replaced by a non-black grey $00.
21 | // Bits 5, 6 and 7 control a color "emphasis" or "tint" effect. See Colour emphasis for details.
22 | // Note that the emphasis bits are applied independently of bit 0, so they will still tint the color of the grey image.
23 | //
24 | EMPHASISE_RED,
25 | EMPHASISE_GREEN,
26 | EMPHASISE_BLUE
27 | }
28 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/PStatus.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | public enum PStatus {
4 | NONE,
5 | NONE1,
6 | NONE2,
7 | NONE3,
8 | NONE4,
9 | SPRITE_OVERFLOW,
10 | SPRITE_ZERO_HIT,
11 | V_BLANK_OCCUR
12 | }
13 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/Register.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | import cn.navclub.nes4j.bin.util.BinUtil;
4 | import lombok.Data;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 |
8 | @Data
9 | public class Register> {
10 | protected byte bits;
11 |
12 | public Register(byte bits) {
13 | this.bits = bits;
14 | }
15 |
16 | public Register() {
17 | }
18 |
19 | public final void set(T instance) {
20 | this.bits |= (byte) (1 << instance.ordinal());
21 | }
22 |
23 | @SafeVarargs
24 | public final void set(T... is) {
25 | for (T i : is) {
26 | this.set(i);
27 | }
28 | }
29 |
30 | /**
31 | * 清除某个标识位
32 | */
33 | public final void clear(T instance) {
34 | this.bits &= (byte) (0xff - (int) (Math.pow(2, instance.ordinal())));
35 | }
36 |
37 | /**
38 | * 批量清除标识
39 | */
40 | @SafeVarargs
41 | public final void clear(T... is) {
42 | for (T c : is) {
43 | clear(c);
44 | }
45 | }
46 |
47 | /**
48 | * 更新某个标识
49 | */
50 | public final void update(T instance, boolean set) {
51 | if (set) {
52 | this.set(instance);
53 | } else {
54 | this.clear(instance);
55 | }
56 | }
57 |
58 | /**
59 | * 获取某个标识的值
60 | */
61 | public final int get(T flag) {
62 | return this.contain(flag) ? 1 : 0;
63 | }
64 |
65 | /**
66 | * 判断某个标识是否设置
67 | */
68 | public final boolean contain(T instance) {
69 | return ((1 << instance.ordinal()) & bits) > 0;
70 | }
71 |
72 | @Override
73 | public String toString() {
74 | return BinUtil.toBinStr(this.bits);
75 | }
76 |
77 | public final Register copy() {
78 | return new Register<>(this.bits);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/TV.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 | public enum TV {
4 | NTSC,
5 | PAL
6 | }
7 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/config/WS6502.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.config;
2 |
3 |
4 | public record WS6502(byte openCode, int size, int cycle, AddressMode addrMode, Instruction instruction) {
5 | }
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/core/Bus.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.core;
2 |
3 | public interface Bus extends Component {
4 | /**
5 | * Write unsigned data to target memory address
6 | *
7 | * @param address Target memory address
8 | * @param value Unsigned byte data
9 | */
10 | void WriteU8(int address, int value);
11 |
12 | /**
13 | * Read unsigned data from target memory address
14 | *
15 | * @param address Target memory address
16 | * @return Unsigned byte data
17 | */
18 | int ReadU8(int address);
19 |
20 | /**
21 | * Little endian read continue two memory address value
22 | *
23 | * @param address Memory address offset
24 | * @return Two address memory value
25 | */
26 | int readInt(int address);
27 |
28 | @Override
29 | default void write(int address, byte b) {
30 |
31 | }
32 |
33 | @Override
34 | default byte read(int address) {
35 | return 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/core/Component.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.core;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.function.CycleDriver;
5 |
6 | /**
7 | * Abstract nes system core component common function.
8 | *
9 | * @author GZYangKui
10 | */
11 | public interface Component extends CycleDriver {
12 | /**
13 | * Write a byte to address
14 | */
15 | void write(int address, byte b);
16 |
17 | /**
18 | * Read a byte from address
19 | */
20 | default byte read(int address) {
21 | throw new UnsupportedOperationException("Current component not support read operator.");
22 | }
23 |
24 | /**
25 | * When {@link NesConsole} instance stop call
26 | */
27 | default void stop() {
28 |
29 | }
30 |
31 | /**
32 | * When {@link NesConsole} was reset call
33 | */
34 | default void reset() {
35 |
36 | }
37 |
38 | /**
39 | * Snapshot current component status info
40 | *
41 | * @return Current status info data
42 | */
43 | default byte[] snapshot() {
44 | return null;
45 | }
46 |
47 | /**
48 | * Recovery current component to target status
49 | *
50 | * @param snapshot Status data
51 | */
52 | default void load(byte[] snapshot) {
53 |
54 | }
55 |
56 | @Override
57 | default void tick() {
58 |
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/core/Mapper.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.core;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.config.NMapper;
5 | import cn.navclub.nes4j.bin.io.Cartridge;
6 |
7 | /**
8 | * @author GZYangKui
9 | */
10 | public abstract class Mapper {
11 | protected static final int CHR_BANK_SIZE = 8 * 1024;
12 | protected static final int PRG_BANK_SIZE = 16 * 1024;
13 |
14 | protected final NesConsole console;
15 | protected final Cartridge cartridge;
16 |
17 | public Mapper(Cartridge cartridge, NesConsole console) {
18 | this.console = console;
19 | this.cartridge = cartridge;
20 | }
21 |
22 | /**
23 | * Read from rpg-rom
24 | *
25 | * @param address Target address
26 | * @return rpg-rom data
27 | */
28 | public byte PRGRead(int address) {
29 | return this.cartridge.getRgbrom()[address];
30 | }
31 |
32 | /**
33 | * Write data to rpg-rom address
34 | *
35 | * @param b Write target address value
36 | * @param address Target address
37 | */
38 | public void PRGWrite(int address, byte b) {
39 |
40 | }
41 |
42 | /**
43 | * Read from ch-rom
44 | *
45 | * @param address Target memory address
46 | * @return Target memory address value
47 | */
48 | public byte CHRead(int address) {
49 | return this.getChrom()[address];
50 | }
51 |
52 | /**
53 | * Write data to ch-rom address
54 | *
55 | * @param address Target address
56 | * @param b Write target address value
57 | */
58 | public final void CHWrite(int address, byte b) {
59 | this.getChrom()[address] = b;
60 | }
61 |
62 | /**
63 | * Get ROM mapper type
64 | *
65 | * @return {@link NMapper}
66 | */
67 | public NMapper type() {
68 | return this.cartridge.getMapper();
69 | }
70 |
71 | protected int getLastBank() {
72 | return calMaxBankIdx() * PRG_BANK_SIZE;
73 | }
74 |
75 | public final int calMaxBankIdx() {
76 | return this.calMaxBankIdx(PRG_BANK_SIZE);
77 | }
78 |
79 | protected final int calMaxBankIdx(int unit) {
80 | return this.prgSize() / unit - 1;
81 |
82 | }
83 |
84 | /**
85 | * NES instance reset call this function
86 | */
87 | public void reset() {
88 |
89 | }
90 |
91 | public final int prgSize() {
92 | return this.cartridge.getRgbSize();
93 | }
94 |
95 | public final int chrSize() {
96 | return this.cartridge.getChSize();
97 | }
98 |
99 | public final byte[] getChrom() {
100 | return this.cartridge.getChrom();
101 | }
102 |
103 | public void PPUVideoAddrState(int addr) {
104 |
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/core/impl/CNMapper.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.core.impl;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.core.Mapper;
5 | import cn.navclub.nes4j.bin.io.Cartridge;
6 |
7 | /**
8 | *
11 | * Overview
12 | *
13 | * PRG ROM size: 16 KiB or 32 KiB
14 | * PRG ROM bank size: Not bankswitched
15 | * PRG RAM: None
16 | * CHR capacity: Up to 2048 KiB ROM
17 | * CHR bank size: 8 KiB
18 | * Nametable mirroring: Fixed vertical or horizontal mirroring
19 | * Subject to bus conflicts: Yes (CNROM), but not all compatible boards have bus conflicts.
20 | *
21 | * Register
22 | *
23 | * 7 bit 0
24 | * ---- ----
25 | * cccc ccCC
26 | * |||| ||||
27 | * ++++-++++- Select 8 KB CHR ROM bank for PPU $0000-$1FFF
28 | *
29 | * CNROM only implements the lowest 2 bits, capping it at 32 KiB CHR. Other boards may implement 4 or more bits for larger CHR.
30 | *
31 | *
32 | * @author GZYangKui
33 | */
34 | public class CNMapper extends Mapper {
35 | private final int shifter;
36 |
37 | private int chrBank;
38 |
39 | public CNMapper(Cartridge cartridge, NesConsole console) {
40 | super(cartridge, console);
41 | this.shifter = this.chrSize() / CHR_BANK_SIZE - 1;
42 | }
43 |
44 | @Override
45 | public void PRGWrite(int address, byte b) {
46 | this.chrBank = (b & shifter);
47 | }
48 |
49 | @Override
50 | public byte CHRead(int address) {
51 | return this.getChrom()[this.chrBank * CHR_BANK_SIZE + address];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/core/impl/NRMapper.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.core.impl;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.core.Mapper;
5 | import cn.navclub.nes4j.bin.io.Cartridge;
6 |
7 | public class NRMapper extends Mapper {
8 |
9 | public NRMapper(Cartridge cartridge, NesConsole console) {
10 | super(cartridge, console);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/core/impl/UXMapper.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.core.impl;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.core.Mapper;
5 | import cn.navclub.nes4j.bin.io.Cartridge;
6 |
7 | public class UXMapper extends Mapper {
8 | private int offset;
9 | private final int[] PRGBank;
10 |
11 | public UXMapper(Cartridge cartridge, NesConsole console) {
12 | super(cartridge, console);
13 | this.PRGBank = new int[2];
14 | this.PRGBank[1] = this.calMaxBankIdx();
15 | }
16 |
17 | /**
18 | *
19 | * CPU $8000-$BFFF: 16 KB switchable PRG ROM bank
20 | * CPU $C000-$FFFF: 16 KB PRG ROM bank, fixed to the last bank
21 | *
22 | * 7 bit 0
23 | * ---- ----
24 | * xxxx pPPP
25 | * ||||
26 | * ++++- Select 16 KB PRG ROM bank for CPU $8000-$BFFF
27 | * (UNROM uses bits 2-0; UOROM uses bits 3-0)
28 | *
29 | */
30 | @Override
31 | public void PRGWrite(int address, byte b) {
32 | this.PRGBank[0] = b & this.PRGBank[1];
33 | }
34 |
35 | @Override
36 | public byte PRGRead(int address) {
37 | var idx = address / 0x4000;
38 | var offset = address % 0x4000;
39 | return super.PRGRead((this.PRGBank[idx] * PRG_BANK_SIZE) + offset);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/core/register/CPUStatus.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.core.register;
2 |
3 | import cn.navclub.nes4j.bin.config.ICPUStatus;
4 | import cn.navclub.nes4j.bin.config.Register;
5 |
6 | /**
7 | *
8 | *
9 | * As instructions are executed a set of processor flags are set or clear to record the results of the operation.
10 | * This flags and some additional control flags are held in a special status register. Each flag has a single bit
11 | * within the register.
12 | *
13 | * Instructions exist to test the values of the various bits, to set or clear some of them and to push or pull the
14 | * entire set to or from the stack.
15 | *
16 | * Carry Flag
17 | * The carry flag is set if the last operation caused an overflow from bit 7 of the result or an underflow from bit
18 | * 0. This condition is set during arithmetic, comparison and during logical shifts. It can be explicitly set using
19 | * the 'Set Carry Flag' (SEC) instruction and cleared with 'Clear Carry Flag' (CLC).
20 | *
21 | * Zero Flag
22 | * The zero flag is set if the result of the last operation as was zero.
23 | *
24 | * Interrupt Disable
25 | * The interrupt disable flag is set if the program has executed a 'Set Interrupt Disable' (SEI) instruction. While
26 | * this flag is set the processor will not respond to interrupts from devices until it is cleared by a 'Clear Interrupt
27 | * Disable' (CLI) instruction.
28 | *
29 | * Decimal Mode
30 | * While the decimal mode flag is set the processor will obey the rules of Binary Coded Decimal (BCD) arithmetic
31 | * during addition and subtraction. The flag can be explicitly set using 'Set Decimal Flag' (SED) and cleared with
32 | * 'Clear Decimal Flag' (CLD).
33 | *
34 | * Break Command
35 | * The break command bit is set when a BRK instruction has been executed and an interrupt has been generated to
36 | * process it.
37 | *
38 | * Overflow Flag
39 | * The overflow flag is set during arithmetic operations if the result has yielded an invalid 2's complement result
40 | * (e.g. adding to positive numbers and ending up with a negative result: 64 + 64 => -128). It is determined by
41 | * looking at the carry between bits 6 and 7 and between bit 7 and the carry flag.
42 | *
43 | * Negative Flag
44 | * The negative flag is set if the result of the last operation had bit 7 set to a one.
45 | *
46 | *
47 | * @author GZYangKui
48 | */
49 | public class CPUStatus extends Register {
50 | }
51 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/debug/Debugger.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.debug;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 | import cn.navclub.nes4j.bin.core.CPU;
5 |
6 | /**
7 | * NES instance debugger function
8 | *
9 | * @author GZYangKui
10 | */
11 | public interface Debugger {
12 | /**
13 | * When {@link CPU#next()} was executed before call
14 | *
15 | * @return If return {@code true} block current thread,otherwise do nothing.
16 | */
17 | boolean hack(NesConsole console);
18 |
19 | /**
20 | * When {@link NesConsole} instance rpg-rom data happen change call
21 | *
22 | * @param buffer Change after rpg data
23 | */
24 | void buffer(byte[] buffer);
25 |
26 | /**
27 | * When {@link NesConsole} wsa created call
28 | *
29 | * @param console {@link NesConsole} instance
30 | */
31 | void inject(NesConsole console);
32 | }
33 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/debug/OpenCode.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.debug;
2 |
3 | import cn.navclub.nes4j.bin.config.Instruction;
4 |
5 | /**
6 | * @param index Memory address
7 | * @param instruction 6502 instruction
8 | * @param operand Memory address value
9 | * @author GZYangKui
10 | */
11 | public record OpenCode(int index, Instruction instruction, Operand operand) {
12 | }
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/debug/OpenCodeFormat.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.debug;
2 |
3 | import cn.navclub.nes4j.bin.config.AddressMode;
4 | import cn.navclub.nes4j.bin.config.Instruction;
5 | import cn.navclub.nes4j.bin.core.CPU;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | import static cn.navclub.nes4j.bin.util.BinUtil.int8;
11 |
12 | /**
13 | * 6502 open code format utils
14 | *
15 | * @author GZYangKui
16 | */
17 | public class OpenCodeFormat {
18 |
19 | public static List formatOpenCode(byte[] buffer) {
20 | var list = new ArrayList();
21 | for (int i = 0; i < buffer.length; ) {
22 | var b = buffer[i];
23 |
24 | i += 1;
25 |
26 | var instance = CPU.IS6502Get(b);
27 | if (instance != null) {
28 | var mode = instance.addrMode();
29 |
30 | var operator = switch (mode) {
31 | case Immediate -> new Operand(AddressMode.Immediate, buffer[i], int8(0));
32 | case Accumulator -> new Operand(AddressMode.Accumulator, (byte) 0, int8(0));
33 | case Absolute,
34 | Absolute_X,
35 | Absolute_Y,
36 | Indirect -> new Operand(mode, buffer[i], buffer[i + 1]);
37 |
38 | case ZeroPage,
39 | ZeroPage_X,
40 | ZeroPage_Y,
41 | Indirect_Y,
42 | Indirect_X,
43 | Relative -> new Operand(mode, buffer[i], int8(0));
44 | default -> Operand.DEFAULT_OPERAND;
45 | };
46 | var index = 0x8000 + i - 1;
47 | list.add(new OpenCode(index, instance.instruction(), operator));
48 | i += (instance.size() - 1);
49 | } else {
50 | list.add(new OpenCode(0x8000 + i - 1, null, null));
51 |
52 | }
53 | }
54 | return list;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/debug/Operand.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.debug;
2 |
3 | import cn.navclub.nes4j.bin.config.AddressMode;
4 |
5 | /**
6 | * Wrap 6502 operand
7 | *
8 | * @param mode Address mode
9 | * @param lsb The lower byte
10 | * @param msb The upper byte
11 | * @author GZYangKui
12 | */
13 | public record Operand(AddressMode mode, byte lsb, byte msb) {
14 | public static final Operand DEFAULT_OPERAND = new Operand(AddressMode.Implied, (byte) 0, (byte) 0);
15 | }
16 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/eventbus/EventBus.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.eventbus;
2 |
3 |
4 | import cn.navclub.nes4j.bin.eventbus.impl.MessageConsumerImpl;
5 |
6 | import java.util.Map;
7 | import java.util.concurrent.ConcurrentHashMap;
8 | import java.util.function.Function;
9 |
10 | /**
11 | * @author GZYangKui
12 | */
13 | public class EventBus {
14 | private final Map> map;
15 |
16 | public EventBus() {
17 | this.map = new ConcurrentHashMap<>();
18 | }
19 |
20 | /**
21 | * listener address to event-bus
22 | *
23 | * @param address Wait consumer address
24 | * @param handler When message arrive callback handler
25 | * @param Accept message type
26 | * @return {@link MessageConsumer}
27 | */
28 | public MessageConsumer listener(String address, Function, Object> handler) {
29 | if (this.map.containsKey(address)) {
30 | throw new RuntimeException("Repeat consumer target address:" + address);
31 | }
32 | var consumer = new MessageConsumerImpl<>(handler);
33 | this.map.put(address, consumer);
34 | return consumer;
35 | }
36 |
37 | /**
38 | * Send message to event-bus
39 | *
40 | * @param address Target address
41 | * @param body The message of body
42 | * @param The message of type
43 | * @return Return reply message or null
44 | */
45 | @SuppressWarnings("all")
46 | public R publish(String address, T body) {
47 | var consumer = (MessageConsumerImpl) (this.map.get(address));
48 | if (consumer == null) {
49 | throw new RuntimeException(address + " address already register?");
50 | }
51 | return (R) (consumer.accept(body));
52 | }
53 |
54 | /**
55 | * Remove target address listener
56 | *
57 | * @param address Wait remove listener address
58 | */
59 | public void removeListener(String address) {
60 | if (!this.map.containsKey(address)) {
61 | return;
62 | }
63 | this.map.remove(address);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/eventbus/Message.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.eventbus;
2 |
3 | /**
4 | * @param Message java type
5 | * @author GZYangKui
6 | */
7 | public interface Message {
8 | /**
9 | * The body of the message.Can be null
10 | *
11 | * @return the body or null
12 | */
13 | T body();
14 | }
15 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/eventbus/MessageConsumer.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.eventbus;
2 |
3 | /**
4 | * @param Receive message body type
5 | * @author GZYangKui
6 | */
7 | public interface MessageConsumer {
8 | }
9 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/eventbus/impl/MessageConsumerImpl.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.eventbus.impl;
2 |
3 | import cn.navclub.nes4j.bin.eventbus.Message;
4 | import cn.navclub.nes4j.bin.eventbus.MessageConsumer;
5 |
6 | import java.util.function.Function;
7 |
8 | public class MessageConsumerImpl implements MessageConsumer {
9 | private final Function, Object> handler;
10 |
11 | public MessageConsumerImpl(Function, Object> handler) {
12 | this.handler = handler;
13 | }
14 |
15 | public Object accept(T body) {
16 | return this.handler.apply(new MessageImpl<>(body));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/eventbus/impl/MessageImpl.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.eventbus.impl;
2 |
3 | import cn.navclub.nes4j.bin.eventbus.Message;
4 |
5 |
6 | public class MessageImpl implements Message {
7 | private final T body;
8 |
9 | public MessageImpl(T body) {
10 | this.body = body;
11 | }
12 |
13 | @Override
14 | public T body() {
15 | return this.body;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/function/CycleDriver.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.function;
2 |
3 | /**
4 | * Cycle driver interface
5 | *
6 | * @author GZYangKui
7 | */
8 | public interface CycleDriver {
9 | /**
10 | * Cycle tick function
11 | */
12 | void tick();
13 | }
14 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/function/GameLoopCallback.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.function;
2 |
3 | import cn.navclub.nes4j.bin.io.JoyPad;
4 | import cn.navclub.nes4j.bin.ppu.Frame;
5 |
6 | /**
7 | * Game loop callback
8 | *
9 | * @author GZYangKui
10 | */
11 | @FunctionalInterface
12 | public interface GameLoopCallback {
13 | void accept(Integer a, Boolean b, Frame c, JoyPad d, JoyPad e);
14 | }
15 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/logging/Level.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.logging;
2 |
3 | /**
4 | * Enum common log level
5 | *
6 | * @author GZYangKui
7 | */
8 | public enum Level {
9 | ALL,
10 | TRACE,
11 | DEBUG,
12 | INFO,
13 | WARN,
14 | ERROR,
15 | FATAL,
16 | OFF
17 | }
18 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/logging/LoggerDelegate.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.logging;
2 |
3 | /**
4 | * Nes4j log delegate
5 | *
6 | * @author GZYangKui
7 | */
8 | public interface LoggerDelegate {
9 | /**
10 | * Output a trace level message
11 | *
12 | * @param message message content
13 | * @param params message params
14 | */
15 | void trace(String message, Object... params);
16 |
17 | /**
18 | * Output a debug level message
19 | *
20 | * @param msg message content
21 | * @param params message params
22 | */
23 | void debug(String msg, Object... params);
24 |
25 | /**
26 | * Output a info level message
27 | *
28 | * @param msg message content
29 | * @param params message params
30 | */
31 | void info(String msg, Object... params);
32 |
33 | /**
34 | * Output a warning level message
35 | *
36 | * @param msg message content
37 | * @param params message params
38 | */
39 | void warning(String msg, Object... params);
40 |
41 | /**
42 | * Output a fatal level message
43 | *
44 | * @param msg message content
45 | * @param throwable exception detail
46 | */
47 | void fatal(String msg, Throwable throwable);
48 |
49 | /**
50 | * Output a fatal level message with params
51 | *
52 | * @param msg Message content
53 | * @param throwable exception detail
54 | * @param params Params list
55 | */
56 | void fatal(String msg, Throwable throwable, Object... params);
57 |
58 | /**
59 | * Whether debug is enable
60 | *
61 | * @return If debug is enable return {@code true} otherwise {@code false}
62 | */
63 | boolean isDebugEnabled();
64 |
65 | /**
66 | * Whether trace is enable
67 | *
68 | * @return If trace is enable return {@code true} otherwise {@code false}
69 | */
70 | boolean isTraceEnabled();
71 | }
72 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/logging/LoggerFactory.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.logging;
2 |
3 | import cn.navclub.nes4j.bin.logging.impl.JULoggerDelegate;
4 |
5 | public class LoggerFactory {
6 | private static final Level level;
7 |
8 | static {
9 | var str = System.getProperty("nes4j.log.level");
10 | if (str == null || str.trim().isEmpty()) {
11 | level = Level.WARN;
12 | } else {
13 | level = Level.valueOf(str.toUpperCase());
14 | }
15 | }
16 |
17 | public static LoggerDelegate logger(Class> clazz) {
18 | return new JULoggerDelegate(clazz, level);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/logging/formatter/NFormatter.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.logging.formatter;
2 |
3 | import java.io.PrintWriter;
4 | import java.io.StringWriter;
5 | import java.text.SimpleDateFormat;
6 | import java.util.Date;
7 | import java.util.logging.Formatter;
8 | import java.util.logging.Level;
9 | import java.util.logging.LogRecord;
10 | import java.util.regex.Pattern;
11 |
12 | public class NFormatter extends Formatter {
13 | private final Pattern pattern;
14 | private final SimpleDateFormat format;
15 |
16 | public NFormatter() {
17 | this.pattern = Pattern.compile("\\{}");
18 | this.format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
19 | }
20 |
21 | @Override
22 | public String format(LogRecord record) {
23 | var str = String.format("[%s [%s] [%-7s] [%s]",
24 | this.format.format(new Date()),
25 | Thread.currentThread().getName(),
26 | record.getLevel().getName(),
27 | record.getLoggerName()
28 | );
29 | var message = record.getMessage();
30 | var params = record.getParameters();
31 | var length = params == null ? 0 : params.length;
32 |
33 | var index = 0;
34 | var offset = 0;
35 | var sb = new StringBuilder(message);
36 | var matter = pattern.matcher(message);
37 | while (matter.find() && index < length) {
38 | var start = matter.start();
39 | var end = matter.end();
40 | var param = record.getParameters()[index];
41 | if (param == null) {
42 | param = "nil";
43 | }
44 | var value = param.toString();
45 | sb.replace(start + offset, end + offset, value);
46 | offset += (value.length() - 2);
47 | index++;
48 | }
49 | str = String.format("%s %s", str, sb);
50 |
51 | if (record.getLevel() == Level.SEVERE) {
52 | var e = record.getThrown();
53 | if (e != null) {
54 | str += "\n";
55 | str += throwable2Str(e);
56 | }
57 | }
58 | str += "\n";
59 | return str;
60 | }
61 |
62 | private String throwable2Str(Throwable throwable) {
63 | try (
64 | var sw = new StringWriter();
65 | var pw = new PrintWriter(sw)) {
66 | throwable.printStackTrace(pw);
67 | return sw.toString();
68 | } catch (Exception e) {
69 | return throwable2Str(new RuntimeException("Log exception parser error!"));
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/logging/handler/JUConsoleHandler.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.logging.handler;
2 |
3 | import cn.navclub.nes4j.bin.logging.formatter.NFormatter;
4 |
5 | import java.util.logging.Level;
6 | import java.util.logging.LogRecord;
7 | import java.util.logging.StreamHandler;
8 |
9 | public class JUConsoleHandler extends StreamHandler {
10 |
11 | public JUConsoleHandler(Level level) {
12 | super(System.out, new NFormatter());
13 | this.setLevel(level);
14 | }
15 |
16 | @Override
17 | public synchronized void publish(LogRecord record) {
18 | super.publish(record);
19 | this.flush();
20 | }
21 |
22 | @Override
23 | public synchronized void close() throws SecurityException {
24 | flush();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/logging/impl/JULoggerDelegate.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.logging.impl;
2 |
3 | import cn.navclub.nes4j.bin.logging.LoggerDelegate;
4 | import cn.navclub.nes4j.bin.logging.handler.JUConsoleHandler;
5 |
6 | import java.util.logging.*;
7 |
8 | /**
9 | * Java logger delegate
10 | *
11 | * @author GZYangKui
12 | */
13 | public class JULoggerDelegate implements LoggerDelegate {
14 |
15 | private final Logger logger;
16 |
17 | @SuppressWarnings("all")
18 | private final Handler console;
19 |
20 | public JULoggerDelegate(Class> clazz, cn.navclub.nes4j.bin.logging.Level level) {
21 | var level0 = switch (level) {
22 | case ALL -> Level.ALL;
23 | case TRACE -> Level.FINEST;
24 | case DEBUG -> Level.FINE;
25 | case INFO -> Level.INFO;
26 | case WARN -> Level.WARNING;
27 | case FATAL, ERROR -> Level.SEVERE;
28 | case OFF -> Level.OFF;
29 | };
30 | this.console = new JUConsoleHandler(level0);
31 | this.logger = Logger.getLogger(clazz.getName());
32 | this.logger.setLevel(level0);
33 | this.logger.addHandler(console);
34 | this.logger.setUseParentHandlers(false);
35 | }
36 |
37 | @Override
38 | public void debug(String msg, Object... params) {
39 | this.logger.log(Level.FINE, msg, params);
40 | }
41 |
42 | @Override
43 | public void info(String msg, Object... params) {
44 | this.logger.log(Level.INFO, msg, params);
45 | }
46 |
47 | @Override
48 | public void warning(String msg, Object... params) {
49 | this.logger.log(Level.WARNING, msg, params);
50 | }
51 |
52 | @Override
53 | public void fatal(String msg, Throwable throwable) {
54 | this.logger.log(Level.SEVERE, msg, throwable);
55 | }
56 |
57 | @Override
58 | public void fatal(String msg, Throwable throwable, Object... params) {
59 | var lr = new LogRecord(Level.SEVERE, msg);
60 | lr.setThrown(throwable);
61 | lr.setParameters(params);
62 | lr.setLoggerName(this.logger.getName());
63 | this.logger.log(lr);
64 | }
65 |
66 | @Override
67 | public void trace(String message, Object... params) {
68 | this.logger.log(Level.FINEST, message, params);
69 | }
70 |
71 | @Override
72 | public boolean isDebugEnabled() {
73 | return this.logger.getLevel() == Level.FINE;
74 | }
75 |
76 | @Override
77 | public boolean isTraceEnabled() {
78 | return this.logger.getLevel() == Level.FINEST;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/ppu/Frame.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.ppu;
2 |
3 | import java.util.Arrays;
4 |
5 | /**
6 | * @author GZYangKui
7 | */
8 | public class Frame {
9 | public static final int width = 256;
10 | public static final int height = 240;
11 |
12 | private final int[] pixels;
13 |
14 | public Frame() {
15 | this.pixels = new int[width * height];
16 | }
17 |
18 | public final int getPixel(int pos) {
19 | return this.pixels[pos];
20 | }
21 |
22 | public void update(int x, int y, int pixel) {
23 | this.pixels[y * width + x] = pixel;
24 | }
25 |
26 | public void clear() {
27 | Arrays.fill(this.pixels, 0, this.pixels.length, (byte) 0);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/ppu/register/PPUControl.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.ppu.register;
2 |
3 | import cn.navclub.nes4j.bin.config.NameTMirror;
4 | import cn.navclub.nes4j.bin.config.PControl;
5 | import cn.navclub.nes4j.bin.config.Register;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 | /**
9 | * PPU控制寄存器
10 | *
11 | *
12 | * 7 bit 0
13 | * ---- ----
14 | * VPHB SINN
15 | * |||| ||||
16 | * |||| ||++- Base nametable address
17 | * |||| || (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
18 | * |||| |+--- VRAM address increment per CPU read/write of PPUDATA
19 | * |||| | (0: add 1, going across; 1: add 32, going down)
20 | * |||| +---- Sprite pattern table address for 8x8 sprites
21 | * |||| (0: $0000; 1: $1000; ignored in 8x16 mode)
22 | * |||+------ Background pattern table address (0: $0000; 1: $1000)
23 | * ||+------- Sprite size (0: 8x8 pixels; 1: 8x16 pixels – see PPU OAM#Byte 1)
24 | * |+-------- PPU master/slave select
25 | * | (0: read backdrop from EXT pins; 1: output color on EXT pins)
26 | * +--------- Generate an NMI at the start of the
27 | * vertical blanking interval (0: off; 1: on)
28 | *
29 | *
30 | * @author GZYangKui
31 | */
32 |
33 | public class PPUControl extends Register {
34 |
35 | public PPUControl() {
36 | this.bits = 0;
37 | }
38 |
39 | /**
40 | * Get current name table address
41 | */
42 | public NameTMirror nameTableAddr() {
43 | return NameTMirror.values()[this.bits & 0x03];
44 | }
45 |
46 | public int inc() {
47 | return this.contain(PControl.VRAM_INCREMENT) ? 32 : 1;
48 | }
49 |
50 | public int spritePattern8() {
51 | return this.contain(PControl.SPRITE_PATTERN_ADDR) ? 0x1000 : 0x000;
52 | }
53 |
54 | /**
55 | * 8x16 sprites use different pattern tables based on their index number. If the index number is
56 | * even the sprite data is in the first pattern table at $0000, otherwise it is in the second pattern
57 | * table at $1000.
58 | *
59 | * @param index Sprite index
60 | */
61 | public int spritePattern16(int index) {
62 | return (index & 0x01) * 0x1000;
63 | }
64 |
65 | public int backgroundNameTable() {
66 | return this.contain(PControl.BKG_PATTERN_TABLE) ? 0x1000 : 0x000;
67 | }
68 |
69 | public int spriteSize() {
70 | return this.contain(PControl.SPRITE_SIZE) ? 0x10 : 0x08;
71 | }
72 |
73 | public boolean generateVBlankNMI() {
74 | return this.contain(PControl.V_BANK_NMI);
75 | }
76 |
77 | public void update(byte bits) {
78 | this.bits = bits;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/ppu/register/PPUMask.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.ppu.register;
2 |
3 |
4 | import cn.navclub.nes4j.bin.config.Register;
5 | import cn.navclub.nes4j.bin.config.PMask;
6 |
7 | /**
8 | *
9 | * Mask ($2001) > write
10 | * Common name: PPUMASK
11 | * Description: PPU mask register
12 | * Access: write
13 | * This register controls the rendering of sprites and backgrounds, as well as colour effects.
14 | *
15 | *
16 | * 7 bit 0
17 | * ---- ----
18 | * BGRs bMmG
19 | * |||| ||||
20 | * |||| |||+- Greyscale (0: normal color, 1: produce a greyscale display)
21 | * |||| ||+-- 1: Show background in leftmost 8 pixels of screen, 0: Hide
22 | * |||| |+--- 1: Show sprites in leftmost 8 pixels of screen, 0: Hide
23 | * |||| +---- 1: Show background
24 | * |||+------ 1: Show sprites
25 | * ||+------- Emphasize red (green on PAL/Dendy)
26 | * |+-------- Emphasize green (red on PAL/Dendy)
27 | * +--------- Emphasize blue
28 | *
29 | * @author GZYangKui
30 | */
31 | public class PPUMask extends Register {
32 | public boolean showSprite() {
33 | return this.contain(PMask.SHOW_SPRITES);
34 | }
35 |
36 | public boolean showBackground() {
37 | return this.contain(PMask.SHOW_BACKGROUND);
38 | }
39 |
40 | @SuppressWarnings("all")
41 | public boolean showLeftMostBackground(int x) {
42 | return x > 8 || this.contain(PMask.LEFTMOST_8PXL_BACKGROUND);
43 | }
44 |
45 | public boolean showLeftMostSprite(int x) {
46 | return x > 8 || this.contain(PMask.LEFTMOST_8PXL_SPRITE);
47 | }
48 |
49 | public boolean enableRender() {
50 | return this.showSprite() || this.showBackground();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/ppu/register/PPUStatus.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.ppu.register;
2 |
3 | import cn.navclub.nes4j.bin.config.PStatus;
4 | import cn.navclub.nes4j.bin.config.Register;
5 |
6 | /**
7 | *
8 | * Status ($2002) < read
9 | * Common name: PPUSTATUS
10 | * Description: PPU status register
11 | * Access: read
12 | * This register reflects the state of various functions inside the PPU.
13 | * It is often used for determining timing.To determine when the PPU has reached a given pixel of the screen,
14 | * put an opaque (non-transparent) pixel of sprite 0 there.
15 | *
16 | * ---- ----
17 | * VSO. ....
18 | * |||| ||||
19 | * |||+-++++- PPU open bus. Returns stale PPU bus contents.
20 | * ||+------- Sprite overflow. The intent was for this flag to be set
21 | * || whenever more than eight sprites appear on a scanline, but a
22 | * || hardware bug causes the actual behavior to be more complicated
23 | * || and generate false positives as well as false negatives; see
24 | * || PPU sprite evaluation. This flag is set during sprite
25 | * || evaluation and cleared at dot 1 (the second dot) of the
26 | * || pre-render line.
27 | * |+-------- Sprite 0 Hit. Set when a nonzero pixel of sprite 0 overlaps
28 | * | a nonzero background pixel; cleared at dot 1 of the pre-render
29 | * | line. Used for raster timing.
30 | * +--------- Vertical blank has started (0: not in vblank; 1: in vblank).
31 | * Set at dot 1 of line 241 (the line *after* the post-render
32 | * line); cleared after reading $2002 and at dot 1 of the
33 | * pre-render line.
34 | *
35 | *
36 | * @author GZYangKui
37 | */
38 | public class PPUStatus extends Register {
39 | }
40 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/util/BinUtil.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.util;
2 |
3 | import cn.navclub.nes4j.bin.logging.LoggerDelegate;
4 | import cn.navclub.nes4j.bin.logging.LoggerFactory;
5 |
6 | import java.io.*;
7 |
8 | /**
9 | * @author GZYangKui
10 | */
11 | public class BinUtil {
12 | private static final LoggerDelegate log = LoggerFactory.logger(BinUtil.class);
13 |
14 | public static byte int8(int value) {
15 | return (byte) value;
16 | }
17 |
18 |
19 | public static int u8add(int a, int b) {
20 | return uint8(a + b);
21 | }
22 |
23 | public static int u8sbc(int a, int b) {
24 | return (a - b) & 0xff;
25 | }
26 |
27 | /**
28 | * Byte transform binary string
29 | */
30 | public static String toBinStr(byte value) {
31 | var sb = new StringBuilder();
32 | for (int i = 0; i < 8; i++) {
33 | sb.append((value & (1 << i)) != 0 ? 1 : 0);
34 | }
35 | return sb.reverse().toString();
36 | }
37 |
38 | /**
39 | * Byte array transform to int
40 | */
41 | public static int toInt(byte[] arr) {
42 | if (arr.length < 4) {
43 | throw new RuntimeException("Byte array required four length.");
44 | }
45 | return arr[0] | arr[1] << 8 | arr[2] << 16 | arr[3] << 24;
46 | }
47 |
48 | public static String toHexStr(byte b) {
49 | var hex = Integer.toHexString(b & 0xff);
50 | if (hex.length() == 1) {
51 | hex = String.format("0%s", hex);
52 | }
53 | return hex;
54 | }
55 |
56 | public static String toHexStr(int b) {
57 | var sb = new StringBuilder();
58 | for (int i = 0; i < 4; i++) {
59 | sb.append(toHexStr((byte) (b >> (24 - i * 8))));
60 | }
61 | return sb.toString();
62 | }
63 |
64 | public static int uint8(byte b) {
65 | return (b & 0xff);
66 | }
67 |
68 | public static int uint8(int b) {
69 | return b & 0xff;
70 | }
71 |
72 |
73 | public static int uint16(int i) {
74 | return i & 0xffff;
75 | }
76 |
77 | public static void snapshot(File file, int row, byte[] src, int offset) {
78 | try (var os = new FileOutputStream(file)) {
79 | snapshot(os, row, src, offset, src.length);
80 | } catch (IOException e) {
81 | throw new RuntimeException(e);
82 | }
83 | }
84 |
85 | /**
86 | * Snapshot target memory view
87 | */
88 | public static void snapshot(OutputStream outputStream, int row, byte[] src, int offset, int length) throws IOException {
89 | if (offset + length > src.length) {
90 | throw new ArrayIndexOutOfBoundsException("Out of array length.");
91 | }
92 | var endSymbol = offset + length;
93 | var buffer = new OutputStreamWriter(outputStream);
94 | var h = length / 16 + ((length) % 16 != 0 ? 1 : 0);
95 | var sb = new StringBuilder("|");
96 | for (int i = 0; i < h; i++) {
97 | var oft = (i * row + offset);
98 | var tmp = endSymbol - oft;
99 | var k = Math.min(tmp, row);
100 | sb.delete(1, sb.length());
101 | buffer.append(BinUtil.toHexStr(offset + i * row)).append(" ");
102 | for (int j = 0; j < 16; j++) {
103 | final byte value;
104 | if (j < k) {
105 | value = src[oft + j];
106 | buffer.append(BinUtil.toHexStr(value)).append(" ");
107 | } else {
108 | value = 0;
109 | buffer.append(". ");
110 | }
111 | sb.append(toVisualChar(value));
112 | }
113 | sb.append("|");
114 | buffer.append(sb);
115 | buffer.append("\r\n");
116 | }
117 | buffer.flush();
118 | }
119 |
120 | public static char toVisualChar(byte b) {
121 | if (!(b > 31 && b < 127)) {
122 | return '.';
123 | }
124 | return (char) b;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/util/IOUtil.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.util;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.nio.file.Files;
6 |
7 | /**
8 | * @author GZYangKui
9 | */
10 | public class IOUtil {
11 | public static byte[] readFileAllByte(File file) {
12 | try {
13 | return Files.readAllBytes(file.toPath());
14 | } catch (IOException e) {
15 | throw new RuntimeException(e);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/util/internal/ScriptUtil.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.util.internal;
2 |
3 | import cn.navclub.nes4j.bin.NesConsole;
4 |
5 | import java.util.Objects;
6 | import java.util.regex.Pattern;
7 |
8 | /**
9 | * Assembly custom script utils
10 | * GZYangKui
11 | */
12 | public class ScriptUtil {
13 | private final static Pattern STR_TEMPLATE = Pattern.compile("\\\\\\{\\w*\\.\\w*}");
14 |
15 | /**
16 | * Eval assembly code string template value
17 | *
18 | * @param text String template value
19 | * @param console Nes instance
20 | * @return String eval value
21 | */
22 | public static String evalTStr(String text, NesConsole console) {
23 | var offset = 0;
24 | var cpu = console.getCpu();
25 | var sb = new StringBuilder(text);
26 | var matcher = STR_TEMPLATE.matcher(text);
27 | while (matcher.find()) {
28 | var i = matcher.start();
29 | var j = matcher.end();
30 | var expr = text.substring(i + 2, j - 1);
31 | var context = switch (expr) {
32 | case ScriptStrSlotConstant.CPU_A -> cpu.getRa();
33 | case ScriptStrSlotConstant.CPU_X -> cpu.getRx();
34 | case ScriptStrSlotConstant.CPU_Y -> cpu.getRy();
35 | case ScriptStrSlotConstant.CPU_S -> cpu.getSp();
36 | default -> null;
37 | };
38 | if (Objects.isNull(context)) {
39 | continue;
40 | }
41 | var str = context.toString();
42 | sb.replace(i + offset, j + offset, str);
43 | offset += (str.length() - (j - i));
44 | }
45 | return sb.toString();
46 | }
47 | }
--------------------------------------------------------------------------------
/bin/src/main/java/cn/navclub/nes4j/bin/util/internal/package-info.java:
--------------------------------------------------------------------------------
1 | package cn.navclub.nes4j.bin.util.internal;
2 |
3 | /**
4 | * Script slot constant define
5 | *
6 | * @author GZYangKui
7 | */
8 | class ScriptStrSlotConstant {
9 | public static final String CPU_A = "c.a";
10 | public static final String CPU_X = "c.x";
11 | public static final String CPU_Y = "c.y";
12 | public static final String CPU_S = "c.s";
13 | }
--------------------------------------------------------------------------------
/bin/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | import cn.navclub.nes4j.bin.apu.Player;
2 |
3 | module cn.navclub.nes4j.bin {
4 | requires static lombok;
5 | requires java.logging;
6 |
7 | exports cn.navclub.nes4j.bin;
8 | exports cn.navclub.nes4j.bin.logging;
9 | exports cn.navclub.nes4j.bin.core;
10 | exports cn.navclub.nes4j.bin.util;
11 | exports cn.navclub.nes4j.bin.ppu;
12 | exports cn.navclub.nes4j.bin.config;
13 | exports cn.navclub.nes4j.bin.debug;
14 | exports cn.navclub.nes4j.bin.function;
15 | exports cn.navclub.nes4j.bin.io;
16 | exports cn.navclub.nes4j.bin.ppu.register;
17 | exports cn.navclub.nes4j.bin.apu;
18 | exports cn.navclub.nes4j.bin.eventbus;
19 |
20 | uses Player;
21 | }
--------------------------------------------------------------------------------
/bin/src/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ${LOG_PATTERN}
13 |
14 |
15 |
16 |
17 | ${LOG_HOME}
18 |
19 |
20 | ${LOG_HOME}.%d{yyyy-MM-dd}.%i
21 | 30
22 | 50MB
23 |
24 |
25 | ${LOG_PATTERN}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/document/NESDoc.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/document/NESDoc.pdf
--------------------------------------------------------------------------------
/document/im_qq.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/document/im_qq.jpg
--------------------------------------------------------------------------------
/document/nes4j.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GZYangKui/nes4j/20c68431b771f3e262a9d090f85238c3fa2236d7/document/nes4j.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | cn.navclub
4 | nes4j
5 | pom
6 | 1.0-SNAPSHOT
7 | 4.0.0
8 |
9 |
10 | bin
11 | app
12 |
13 |
14 |
15 | 2.0.3
16 | 5.9.0
17 | 1.18.30
18 | 1.4.4
19 | 2.15.2
20 | 17
21 | 17
22 |
23 |
24 |
25 |
26 |
27 | org.junit.jupiter
28 | junit-jupiter-api
29 | ${junit.version}
30 | test
31 |
32 |
33 | org.junit.jupiter
34 | junit-jupiter
35 | ${junit.version}
36 | test
37 |
38 |
39 | org.projectlombok
40 | lombok
41 | ${lombok.version}
42 | provided
43 |
44 |
45 |
46 |
47 |
48 | com.fasterxml.jackson.core
49 | jackson-databind
50 | ${jackson.version}
51 |
52 |
53 | com.fasterxml.jackson.core
54 | jackson-core
55 | ${jackson.version}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | org.apache.maven.plugins
64 | maven-shade-plugin
65 | 3.3.0
66 |
67 |
68 | org.apache.maven.plugins
69 | maven-compiler-plugin
70 | 3.10.1
71 |
72 | UTF-8
73 |
74 |
75 | org.projectlombok
76 | lombok
77 | ${lombok.version}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------