mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
270 lines
7.4 KiB
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()
|
|
})
|
|
}
|
|
}
|
|
}
|