ocelot-desktop/src/main/scala/ocelot/desktop/OcelotDesktop.scala

270 lines
7.4 KiB
Scala

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()
})
}
}
}