mirror of
https://github.com/ethauvin/chip-8.git
synced 2025-04-25 16:57:12 -07:00
240 lines
7.9 KiB
Kotlin
240 lines
7.9 KiB
Kotlin
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<Computer.AssemblyLine>) {
|
|
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<AnchorPane>(res)
|
|
|
|
pauseButton = root.lookup("#pause") as Button
|
|
val scrollPane = root.lookup("#disassemblyScrollPane") as ScrollPane
|
|
disassembly = scrollPane.content as GridPane
|
|
val controller = loader.getController<Controller>()
|
|
|
|
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<String>) {
|
|
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)
|
|
}
|
|
}
|
|
|