package ocelot.desktop import li.flor.nativejfilechooser.NativeJFileChooser import ocelot.desktop.audio.{Audio, SoundSource} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.widget.{ExitConfirmationDialog, RootWidget, SettingsDialog} import ocelot.desktop.util._ import org.apache.commons.io.FileUtils import org.apache.logging.log4j.LogManager import totoro.ocelot.brain.Ocelot import totoro.ocelot.brain.event.FileSystemActivityType.Floppy import totoro.ocelot.brain.event._ import totoro.ocelot.brain.nbt.{CompressedStreamTools, NBTTagCompound} import totoro.ocelot.brain.workspace.Workspace import java.io._ import java.nio.file.{Files, Path} import java.util.concurrent.locks.{Lock, ReentrantLock} import javax.swing.JFileChooser import scala.io.Source import scala.jdk.CollectionConverters._ import scala.util.Random object OcelotDesktop extends Logging { var root: RootWidget = _ val tpsCounter = new FPSCalculator val ticker = new Ticker private val tickLock: Lock = new ReentrantLock() private val random: Random = new Random() def withTickLockAcquired(f: () => Unit): Unit = withLockAcquired(tickLock, f) def mainInner(): Unit = { logger.info("Starting up Ocelot Desktop") Ocelot.initialize(LogManager.getLogger(Ocelot)) val settingsFile = new File("ocelot.conf") Settings.load(settingsFile) ColorScheme.load(Source.fromURL(getClass.getResource("/ocelot/desktop/colorscheme.txt"))) createWorkspace() root = new RootWidget() UiHandler.init(root) // loading resources _after_ the UiHandler was initialized, because the native libraries are not available before ResourceManager.initResources() setupEventHandlers() new Thread(() => { while (true) { Profiler.startTimeMeasurement("tick") withTickLockAcquired(() => { workspace.update() tpsCounter.tick() }) Profiler.endTimeMeasurement("tick") ticker.waitNext() } }).start() UiHandler.start() logger.info("Cleaning up") ResourceManager.freeResources() UiHandler.terminate() Ocelot.shutdown() Settings.save(settingsFile) logger.info("Thanks for using Ocelot Desktop") System.exit(0) } def main(args: Array[String]): Unit = { try mainInner() catch { case e: Exception => val sw = new StringWriter val pw = new PrintWriter(sw) e.printStackTrace(pw) logger.error(s"${sw.toString}") System.exit(1) } } private def saveWorld(nbt: NBTTagCompound): Unit = withTickLockAcquired(() => { val backendNBT = new NBTTagCompound val frontendNBT = new NBTTagCompound workspace.save(backendNBT) root.workspaceView.save(frontendNBT) nbt.setTag("back", backendNBT) nbt.setTag("front", frontendNBT) }) private def loadWorld(nbt: NBTTagCompound): Unit = withTickLockAcquired(() => { val backendNBT = nbt.getCompoundTag("back") val frontendNBT = nbt.getCompoundTag("front") workspace.load(backendNBT) root.workspaceView.load(frontendNBT) }) private var savePath: Option[Path] = None private val tmpPath = Files.createTempDirectory("ocelot-save") def newWorkspace(): Unit = { root.workspaceView.newWorkspace() } def save(): Unit = { if (savePath.isEmpty) { saveAs() return } val oldPath = workspace.path val newPath = savePath.get if (oldPath != newPath) { val oldFiles = Files.list(oldPath).iterator.asScala.toArray val newFiles = Files.list(newPath).iterator.asScala.toArray val toRemove = newFiles.intersect(oldFiles) for (path <- toRemove) { if (Files.isDirectory(path)) { FileUtils.deleteDirectory(path.toFile) } else { Files.delete(path) } } for (path <- oldFiles) { FileUtils.copyDirectory(oldPath.resolve(path.getFileName).toFile, newPath.resolve(path.getFileName).toFile) } workspace.path = newPath } val path = newPath + "/workspace.nbt" val writer = new DataOutputStream(new FileOutputStream(path)) val nbt = new NBTTagCompound saveWorld(nbt) CompressedStreamTools.writeCompressed(nbt, writer) writer.flush() } def saveAs(): Unit = { chooseDirectory(dir => { if (dir.isDefined) { savePath = dir.map(_.toPath) save() } }, JFileChooser.SAVE_DIALOG) } def open(): Unit = { chooseDirectory(dir => { if (dir.isDefined) { val path = dir.get + "/workspace.nbt" val reader = new DataInputStream(new FileInputStream(path)) val nbt = CompressedStreamTools.readCompressed(reader) savePath = Some(dir.get.toPath) workspace.path = dir.get.toPath loadWorld(nbt) } }, JFileChooser.OPEN_DIALOG) } def chooseDirectory(fun: Option[File] => Unit, dialogType: Int): Unit = { new Thread(() => { val lastFile = savePath.map(_.toFile).orNull val chooser: JFileChooser = try { new NativeJFileChooser(lastFile) } catch { case _: Throwable => new JFileChooser(lastFile) } chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY) chooser.setDialogType(dialogType) val option = chooser.showDialog(null, null) fun(if (option == JFileChooser.APPROVE_OPTION) Some(chooser.getSelectedFile) else None) }).start() } def settings(): Unit = { UiHandler.root.modalDialogPool.pushDialog(new SettingsDialog()) } def cleanup(): Unit = { FileUtils.deleteDirectory(tmpPath.toFile) } def exit(): Unit = { if (UiHandler.root.modalDialogPool.children.exists(_.isInstanceOf[ExitConfirmationDialog])) return if (savePath.isDefined) { save() UiHandler.exit() cleanup() return } UiHandler.root.modalDialogPool.pushDialog(new ExitConfirmationDialog { override def onSaveSelected(): Unit = { chooseDirectory(dir => { if (dir.isDefined) { savePath = dir.map(_.toPath) save() UiHandler.exit() cleanup() } }, JFileChooser.SAVE_DIALOG) } override def onExitSelected(): Unit = { UiHandler.exit() cleanup() } }) } var workspace: Workspace = _ private def createWorkspace(): Unit = { workspace = new Workspace(tmpPath) } private def setupEventHandlers(): Unit = { EventBus.listenTo(classOf[BeepEvent], { case event: BeepEvent => if (!UiHandler.audioDisabled) Audio.beep(event.frequency, event.duration) else logger.info(s"Beep ${event.frequency} Hz for ${event.duration} ms") }) EventBus.listenTo(classOf[BeepPatternEvent], { case event: BeepPatternEvent => if (!UiHandler.audioDisabled) Audio.beep(event.pattern) else logger.info(s"Beep pattern `${event.pattern}``") }) EventBus.listenTo(classOf[MachineCrashEvent], { case event: MachineCrashEvent => logger.info(s"[EVENT] Machine crash! (address = ${event.address}, ${event.message})") }) if (!UiHandler.audioDisabled) { val soundFloppyAccess = Audio.FloppyAccess.map(buffer => new SoundSource(buffer)) val soundHDDAccess = Audio.HDDAccess.map(buffer => new SoundSource(buffer)) EventBus.listenTo(classOf[FileSystemActivityEvent], { case event: FileSystemActivityEvent => val sound = if (event.activityType == Floppy) soundFloppyAccess else soundHDDAccess sound(random.nextInt(sound.length)).play() }) } } }