├── .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 | * 封装JavafxUI相关操作工具类 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 | 58 | 59 | 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 | 34 | 41 | 48 | 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 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 67 | 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 | 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 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 provider; 40 | 41 | NMapper(Class 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 | *

    9 | * INES Mapper 003(CNROM) 10 | *

    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 | --------------------------------------------------------------------------------