package com.beust.chip8 import com.beust.jcommander.JCommander import com.beust.jcommander.Parameter import javafx.application.Application import javafx.application.Platform import javafx.beans.property.SimpleBooleanProperty import javafx.beans.property.SimpleStringProperty import javafx.event.EventHandler import javafx.fxml.FXMLLoader import javafx.fxml.Initializable import javafx.scene.Scene import javafx.scene.control.Button import javafx.scene.control.Label import javafx.scene.control.ScrollPane import javafx.scene.control.TextField import javafx.scene.input.KeyCode import javafx.scene.input.KeyEvent import javafx.scene.layout.AnchorPane import javafx.scene.layout.GridPane import javafx.scene.layout.VBox import javafx.stage.FileChooser import javafx.stage.Stage import java.io.File import java.net.URL import java.nio.file.Paths import java.util.* import kotlin.system.exitProcess class Controller : Initializable { var currentRomPath: SimpleStringProperty? = null var computer: Computer? = null var pauseButton: Button? = null override fun initialize(location: URL?, resources: ResourceBundle?) { println("Initializing controller") } } class MyFxApp : Application() { private val computer = Computer() private var pauseButton: Button? = null private var disassembly: GridPane? = null private val isPaused = SimpleBooleanProperty().apply { addListener { _, _, newVal -> computer.apply { // Computer is paused // Update the pause button label pauseButton?.text = if (newVal) "Resume" else "Pause" // Display the disassembly updateDisassembly(disassembly!!, computer.disassemble()) } }} /** * Display the disassembly. */ private fun updateDisassembly(disassembly: GridPane, listing: List) { disassembly.children.clear() listing.forEachIndexed() { row, al -> var column = 0 Label(al.counter.h).let { label -> GridPane.setConstraints(label, column++, row) disassembly.children.add(label) } Label(al.byte0.h + " " + al.byte1.h).let { label -> GridPane.setConstraints(label, column++, row) disassembly.children.add(label) } Label(al.name).let { label -> GridPane.setConstraints(label, column++, row) disassembly.children.add(label) } } } private val currentRomName = SimpleStringProperty() private val currentRomPath = SimpleStringProperty().apply { addListener { _, _, newVal -> computer.apply { // Restart computer stop() loadRom(File(newVal)) // Update the rom name val start = newVal.lastIndexOf(File.separatorChar) val end = newVal.lastIndexOf(".ch8") val romName = if (end == -1) newVal.substring(start + 1) else newVal.substring(start + 1, end) currentRomName.set(romName) }} } override fun start(primaryStage: Stage) { val keyListener = object: KeyListener { override fun onKey(key: Int?) { computer.keyboard.key = key } } primaryStage.title = "CHIP-8" primaryStage.onCloseRequest = EventHandler { Platform.exit() exitProcess(0) } val url = this::class.java.classLoader.getResource("main.fxml") val loader = FXMLLoader(url) val res = url.openStream() val root = loader.load(res) pauseButton = root.lookup("#pause") as Button val scrollPane = root.lookup("#disassemblyScrollPane") as ScrollPane disassembly = scrollPane.content as GridPane val controller = loader.getController() controller.let { it.currentRomPath = currentRomPath it.computer = computer it.pauseButton = pauseButton } computer.listener = object: ComputerListener { override fun onKey(key: Int?) { computer.keyboard.key = key } override fun onPause() { isPaused.set(true) } override fun onStart() { isPaused.set(false) } } val scene = Scene(root) val bp = root.lookup("#emulator") as VBox bp.children.add(computer.display.pane) primaryStage.scene = scene primaryStage.show() // Load rom button (root.lookup("#loadRom") as Button).setOnAction { computer.pause() val romFile = FileChooser().run { initialDirectory = File("roms") showOpenDialog(Stage()) } println("Picked file $romFile") if (romFile != null) { currentRomPath.set(romFile.absolutePath) } } // Pause button pauseButton?.setOnAction { if (computer.paused) { computer.start() } else { computer.pause() } } // CPU clock button with((root.lookup("#cpuClock") as TextField)) { fun updateClock() { computer.cpuClockHz = text.toLong() } text = computer.cpuClockHz.toString() onAction = EventHandler { e -> updateClock() } focusedProperty().addListener { _, _, onFocus -> if (!onFocus) { updateClock() } } } // Current ROM val currentRomLabel = root.lookup("#currentRom") as Label currentRomLabel.textProperty().bind(currentRomName) scene.setOnKeyPressed { event: KeyEvent -> when(event.code) { KeyCode.Q -> { primaryStage.close() computer.stop() exitProcess(0) } KeyCode.P -> { computer.pause() } else -> { if (computer.paused) computer.start() else { try { val key = Integer.parseInt(event.code.char, 16) keyListener.onKey(key) } catch (ex: NumberFormatException) { println("Can't parse key " + event.code.char + ", ignoring") } } } } } scene.setOnKeyReleased { event: KeyEvent -> keyListener.onKey(null) } val spaceInvaders = Paths.get("roms", "Space Invaders [David Winter].ch8") val breakout = Paths.get("roms", "Breakout [Carmelo Cortez, 1979].ch8") val file3 = Paths.get("roms", "Tetris [Fran Dachille, 1991].ch8") val file4 = Paths.get("roms", "Clock Program [Bill Fisher, 1981].ch8") val file5 = Paths.get("roms", "IBM Logo.ch8") val keypad = Paths.get("roms", "Keypad Test [Hap, 2006].ch8") currentRomPath.set(spaceInvaders.toFile().absolutePath) // val rom = file2.toFile() // if (arg.rom != null) File(arg.rom) else file3.toFile() // chip8.run(rom) } } fun main(args: Array) { if (args.isNotEmpty()) { class Arg { @Parameter(names = arrayOf("--rom")) var rom: String? = null @Parameter(names = arrayOf("--start")) var start: String? = "200" } val arg = Arg() JCommander.newBuilder().addObject(arg).args(args).build() val computer = Computer(sound = false) val start = Integer.parseInt(arg.start!!, 16) val listing = computer.disassemble(start) println("=== " + arg.rom + " PC=0x" + Integer.toHexString(start)) listing.forEach { println(it) } } else { Application.launch(MyFxApp::class.java) } }