From 6a218c26326db769a5a6b4d12bed4f2de39d487b Mon Sep 17 00:00:00 2001 From: minjaesong Date: Wed, 3 Oct 2018 23:15:24 +0900 Subject: [PATCH] GameWorld: adding "worldIndex"; more save/load stuffs --- src/net/torvald/terrarum/IngameInstance.kt | 6 + .../torvald/terrarum/gameworld/GameWorld.kt | 2 +- .../torvald/terrarum/modulebasegame/Ingame.kt | 3 +- .../gameworld/GameWorldExtension.kt | 2 +- .../virtualcomputer/tvd/DiskSkimmer.kt | 120 ++- .../virtualcomputer/tvd/Spec.md | 113 +++ .../virtualcomputer/tvd/VDUtil.kt | 10 +- .../virtualcomputer/tvd/finder/Popups.kt | 107 +++ .../tvd/finder/VirtualDiskCracker.kt | 843 ++++++++++++++++++ .../terrarum/serialise/ReadLayerDataZip.kt | 30 +- .../terrarum/serialise/SavegameLoader.kt | 10 + .../terrarum/serialise/SavegameWriter.kt | 60 ++ .../terrarum/serialise/WriteLayerDataZip.kt | 42 +- work_files/DataFormats/Savegame container.txt | 11 +- 14 files changed, 1301 insertions(+), 58 deletions(-) create mode 100644 src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/Spec.md create mode 100644 src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/Popups.kt create mode 100644 src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/VirtualDiskCracker.kt create mode 100644 src/net/torvald/terrarum/serialise/SavegameLoader.kt create mode 100644 src/net/torvald/terrarum/serialise/SavegameWriter.kt diff --git a/src/net/torvald/terrarum/IngameInstance.kt b/src/net/torvald/terrarum/IngameInstance.kt index b55243fbf..f5c137c80 100644 --- a/src/net/torvald/terrarum/IngameInstance.kt +++ b/src/net/torvald/terrarum/IngameInstance.kt @@ -11,6 +11,10 @@ import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock import javax.swing.JOptionPane +/** + * Although the game (as product) can have infinitely many stages/planets/etc., those stages must be manually managed by YOU; + * this instance only stores the stage that is currently being used. + */ open class IngameInstance(val batch: SpriteBatch) : Screen { var screenZoom = 1.0f @@ -20,6 +24,8 @@ open class IngameInstance(val batch: SpriteBatch) : Screen { open lateinit var consoleHandler: ConsoleWindow open lateinit var world: GameWorld + /** how many different planets/stages/etc. are thenre. Whole stages must be manually managed by YOU. */ + var gameworldCount = 0 /** The actor the game is currently allowing you to control. * * Most of the time it'd be the "player", but think about the case where you have possessed diff --git a/src/net/torvald/terrarum/gameworld/GameWorld.kt b/src/net/torvald/terrarum/gameworld/GameWorld.kt index b67035cce..47198e80e 100644 --- a/src/net/torvald/terrarum/gameworld/GameWorld.kt +++ b/src/net/torvald/terrarum/gameworld/GameWorld.kt @@ -9,7 +9,7 @@ import org.dyn4j.geometry.Vector2 typealias BlockAddress = Long typealias BlockDamage = Float -open class GameWorld(val width: Int, val height: Int) { +open class GameWorld(var worldIndex: Int, val width: Int, val height: Int) { //layers diff --git a/src/net/torvald/terrarum/modulebasegame/Ingame.kt b/src/net/torvald/terrarum/modulebasegame/Ingame.kt index 507d693cc..2e9ee7a49 100644 --- a/src/net/torvald/terrarum/modulebasegame/Ingame.kt +++ b/src/net/torvald/terrarum/modulebasegame/Ingame.kt @@ -231,7 +231,8 @@ open class Ingame(batch: SpriteBatch) : IngameInstance(batch) { // init map as chosen size - gameworld = GameWorldExtension(worldParams.width, worldParams.height) + gameworld = GameWorldExtension(1, worldParams.width, worldParams.height) + gameworldCount++ world = gameworld as GameWorld // generate terrain for the map diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/GameWorldExtension.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/GameWorldExtension.kt index 123c20477..02add8a3c 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameworld/GameWorldExtension.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/GameWorldExtension.kt @@ -7,7 +7,7 @@ import kotlin.properties.Delegates /** * Created by minjaesong on 2018-07-03. */ -class GameWorldExtension(width: Int, height: Int): GameWorld(width, height) { +class GameWorldExtension(worldIndex: Int, width: Int, height: Int): GameWorld(worldIndex, width, height) { val time: WorldTime val economy = GameEconomy() diff --git a/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/DiskSkimmer.kt b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/DiskSkimmer.kt index 39148bd23..a2a3f4747 100644 --- a/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/DiskSkimmer.kt +++ b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/DiskSkimmer.kt @@ -1,18 +1,24 @@ package net.torvald.terrarum.modulecomputers.virtualcomputer.tvd +import net.torvald.terrarum.serialise.toLittleInt import java.io.File import java.io.FileInputStream +import java.io.InputStream /** * Creates entry-to-offset tables to allow streaming from the disk, without storing whole VD file to the memory. * * Created by minjaesong on 2017-11-17. */ -class DiskSkimmer(diskFile: File) { +class DiskSkimmer(private val diskFile: File) { - class EntryOffsetPair(val entryID: Int, val offset: Long) - val entryToOffsetTable = ArrayList() + /** + * EntryID to Offset. + * + * Offset is where the header begins, so first 4 bytes are exactly the same as the EntryID. + */ + val entryToOffsetTable = HashMap() init { @@ -64,7 +70,7 @@ class DiskSkimmer(diskFile: File) { // fill up table - entryToOffsetTable.add(EntryOffsetPair(entryID, currentPosition)) + entryToOffsetTable[entryID] = currentPosition skipRead(4) // skip entryID of parent @@ -88,15 +94,109 @@ class DiskSkimmer(diskFile: File) { } + /** + * Using entryToOffsetTable, composes DiskEntry on the fly upon request. + * @return DiskEntry if the entry exists on the disk, `null` otherwise. + */ + fun requestFile(entryID: EntryID): DiskEntry? { + // FIXME untested + entryToOffsetTable[entryID].let { offset -> + if (offset == null) + return null + else { + val fis = FileInputStream(diskFile) + fis.skip(offset + 4) // get to the EntryHeader's parent directory area + val parent = fis.read(4).toLittleInt() + val fileFlag = fis.read(1)[0] + val filename = fis.read(256) + val creationTime = fis.read(6).toInt48() + val modifyTime = fis.read(6).toInt48() + val skip_crc = fis.read(4) + + // get entry size // TODO future me, does this kind of comment helpful or redundant? + val entrySize = when (fileFlag) { + DiskEntry.NORMAL_FILE -> { + fis.read(6).toInt48() + } + DiskEntry.DIRECTORY -> { + fis.read(2).toUint16().toLong() + } + DiskEntry.SYMLINK -> 4L + else -> throw UnsupportedOperationException("Unsupported entry type: $fileFlag") // FIXME no support for compressed file + } + + + val entryContent = when (fileFlag) { + DiskEntry.NORMAL_FILE -> { + val byteArray = ByteArray64(entrySize) + // read one byte at a time + for (c in 0L until entrySize) { + byteArray[c] = fis.read().toByte() + } + + EntryFile(byteArray) + } + DiskEntry.DIRECTORY -> { + val dirContents = ArrayList() + // read 4 bytes at a time + val bytesBuffer4 = ByteArray(4) + for (c in 0L until entrySize) { + fis.read(bytesBuffer4) + dirContents.add(bytesBuffer4.toIntBig()) + } + + EntryDirectory(dirContents) + } + DiskEntry.SYMLINK -> { + val target = fis.read(4).toIntBig() + + EntrySymlink(target) + } + else -> throw UnsupportedOperationException("Unsupported entry type: $fileFlag") // FIXME no support for compressed file + } + + return DiskEntry(entryID, parent, filename, creationTime, modifyTime, entryContent) + } + } + } + + companion object { + fun InputStream.read(size: Int): ByteArray { + val ba = ByteArray(size) + this.read(ba) + return ba + } + } + + private fun ByteArray.toUint16(): Int { + return this[0].toUint().shl(8) or + this[1].toUint() + } + private fun ByteArray.toIntBig(): Int { - return this[0].toUint().shl(24) or this[1].toUint().shl(16) or - this[2].toUint().shl(8) or this[2].toUint() + return this[0].toUint().shl(24) or + this[1].toUint().shl(16) or + this[2].toUint().shl(8) or + this[3].toUint() } private fun ByteArray.toInt48(): Long { - return this[0].toUlong().shl(56) or this[1].toUlong().shl(48) or - this[2].toUlong().shl(40) or this[3].toUlong().shl(32) or - this[4].toUlong().shl(24) or this[5].toUlong().shl(16) or - this[6].toUlong().shl(8) or this[7].toUlong() + return this[0].toUlong().shl(40) or + this[1].toUlong().shl(32) or + this[2].toUlong().shl(24) or + this[3].toUlong().shl(16) or + this[4].toUlong().shl(8) or + this[5].toUlong() + } + + private fun ByteArray.toInt64(): Long { + return this[0].toUlong().shl(56) or + this[1].toUlong().shl(48) or + this[2].toUlong().shl(40) or + this[3].toUlong().shl(32) or + this[4].toUlong().shl(24) or + this[5].toUlong().shl(16) or + this[6].toUlong().shl(8) or + this[7].toUlong() } } \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/Spec.md b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/Spec.md new file mode 100644 index 000000000..7f1bc5ec9 --- /dev/null +++ b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/Spec.md @@ -0,0 +1,113 @@ +# Terran Virtual Disk Image Format Specification + +current specversion number: 0x03 + +## Changes + +### 0x03 +- Option to compress file entry + +### 0x02 +- 48-Bit filesize and timestamp (Max 256 TiB / 8.9 million years) +- 8 Reserved footer + +### 0x01 +**Note: this version were never released in public** +- Doubly Linked List instead of Singly + + +## Specs + +* File structure + + + Header + + IndexNumber + + + IndexNumber + + + IndexNumber + + + ... + + Footer + + +* Order of the indices does not matter. Actual sorting is a job of the application. +* Endianness: Big + + +## Header + Uint8[4] Magic: TEVd + Int48 Disk size in bytes (max 256 TiB) + Uint8[32] Disk name + Int32 CRC-32 + 1. create list of arrays that contains CRC + 2. put all the CRCs of entries + 3. sort the list (here's the catch -- you will treat CRCs as SIGNED integer) + 4. for elems on list: update crc with the elem (crc = calculateCRC(crc, elem)) + Int8 Version + + (Header size: 47 bytes) + + + +## IndexNumber and Contents + + + +### Entry Header + Int32 EntryID (random Integer). This act as "jump" position for directory listing. + NOTE: Index 0 must be a root "Directory"; 0xFEFEFEFE is invalid (used as footer marker) + Int32 EntryID of parent directory + Int8 Flag for file or directory or symlink (cannot be negative) + 0x01: Normal file, 0x02: Directory list, 0x03: Symlink + 0x11: Compressed normal file + Uint8[256] File name in UTF-8 + Int48 Creation date in real-life UNIX timestamp + Int48 Last modification date in real-life UNIX timestamp + Int32 CRC-32 of Actual Entry + + (Header size: 281 bytes) + +### Entry of File (Uncompressed) + Int48 File size in bytes (max 256 TiB) + Actual Contents + + (Header size: 6 bytes) + +### Entry of File (Compressed) + Int48 Size of compressed payload (max 256 TiB) + Int48 Size of uncompressed file (max 256 TiB) + Actual Contents, DEFLATEd payload + + (Header size: 12 bytes) + +### Entry of Directory + Uint16 Number of entries (normal files, other directories, symlinks) + Entry listing, contains IndexNumber + + (Header size: 2 bytes) + +### Entry of Symlink + Int32 Target IndexNumber + + (Content size: 4 bytes) + + + + +## Footer + Uint8[4] 0xFE 0xFE 0xFE 0xFE (footer marker) + Int8 Disk properties flag 1 + 0b 7 6 5 4 3 2 1 0 + + 0th bit: Readonly + + Int8[7] Reserved, should be filled with zero + + Uint8[2] 0xFF 0x19 (EOF mark) diff --git a/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/VDUtil.kt b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/VDUtil.kt index 22fd674bb..2c7f3b4dd 100644 --- a/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/VDUtil.kt +++ b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/VDUtil.kt @@ -792,18 +792,22 @@ object VDUtil { /** * Creates new zero-filled file with given name and size */ - fun createNewBlankFile(disk: VirtualDisk, directoryID: EntryID, fileSize: Long, filename: String, charset: Charset) { + fun createNewBlankFile(disk: VirtualDisk, directoryID: EntryID, fileSize: Long, filename: String, charset: Charset): EntryID { disk.checkReadOnly() disk.checkCapacity(fileSize + DiskEntry.HEADER_SIZE + 4) - addFile(disk, directoryID, DiskEntry( + val newEntry = DiskEntry( disk.generateUniqueID(), directoryID, filename.toEntryName(DiskEntry.NAME_LENGTH, charset = charset), currentUnixtime, currentUnixtime, EntryFile(fileSize) - )) + ) + + addFile(disk, directoryID, newEntry) + + return newEntry.entryID } diff --git a/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/Popups.kt b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/Popups.kt new file mode 100644 index 000000000..f787de684 --- /dev/null +++ b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/Popups.kt @@ -0,0 +1,107 @@ +package net.torvald.terrarum.virtualcomputer.tvd.finder + +import java.awt.BorderLayout +import java.awt.GridLayout +import javax.swing.* + +/** + * Created by SKYHi14 on 2017-04-01. + */ +object Popups { + val okCancel = arrayOf("OK", "Cancel") + +} + +class OptionDiskNameAndCap { + val name = JTextField(11) + val capacity = JSpinner(SpinnerNumberModel( + 368640L.toJavaLong(), + 0L.toJavaLong(), + (1L shl 38).toJavaLong(), + 1L.toJavaLong() + )) // default 360 KiB, MAX 256 GiB + val mainPanel = JPanel() + val settingPanel = JPanel() + + init { + mainPanel.layout = BorderLayout() + settingPanel.layout = GridLayout(2, 2, 2, 0) + + //name.text = "Unnamed" + + settingPanel.add(JLabel("Name (max 32 bytes)")) + settingPanel.add(name) + settingPanel.add(JLabel("Capacity (bytes)")) + settingPanel.add(capacity) + + mainPanel.add(settingPanel, BorderLayout.CENTER) + mainPanel.add(JLabel("Set capacity to 0 to make the disk read-only"), BorderLayout.SOUTH) + } + + /** + * returns either JOptionPane.OK_OPTION or JOptionPane.CANCEL_OPTION + */ + fun showDialog(title: String): Int { + return JOptionPane.showConfirmDialog(null, mainPanel, + title, JOptionPane.OK_CANCEL_OPTION) + } +} + +fun kotlin.Long.toJavaLong() = java.lang.Long(this) + +class OptionFileNameAndCap { + val name = JTextField(11) + val capacity = JSpinner(SpinnerNumberModel( + 4096L.toJavaLong(), + 0L.toJavaLong(), + ((1L shl 48) - 1L).toJavaLong(), + 1L.toJavaLong() + )) // default 360 KiB, MAX 256 TiB + val mainPanel = JPanel() + val settingPanel = JPanel() + + init { + mainPanel.layout = BorderLayout() + settingPanel.layout = GridLayout(2, 2, 2, 0) + + //name.text = "Unnamed" + + settingPanel.add(JLabel("Name (max 32 bytes)")) + settingPanel.add(name) + settingPanel.add(JLabel("Capacity (bytes)")) + settingPanel.add(capacity) + + mainPanel.add(settingPanel, BorderLayout.CENTER) + } + + /** + * returns either JOptionPane.OK_OPTION or JOptionPane.CANCEL_OPTION + */ + fun showDialog(title: String): Int { + return JOptionPane.showConfirmDialog(null, mainPanel, + title, JOptionPane.OK_CANCEL_OPTION) + } +} + +class OptionSize { + val capacity = JSpinner(SpinnerNumberModel( + 368640L.toJavaLong(), + 0L.toJavaLong(), + (1L shl 38).toJavaLong(), + 1L.toJavaLong() + )) // default 360 KiB, MAX 256 GiB + val settingPanel = JPanel() + + init { + settingPanel.add(JLabel("Size (bytes)")) + settingPanel.add(capacity) + } + + /** + * returns either JOptionPane.OK_OPTION or JOptionPane.CANCEL_OPTION + */ + fun showDialog(title: String): Int { + return JOptionPane.showConfirmDialog(null, settingPanel, + title, JOptionPane.OK_CANCEL_OPTION) + } +} diff --git a/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/VirtualDiskCracker.kt b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/VirtualDiskCracker.kt new file mode 100644 index 000000000..1103c3d45 --- /dev/null +++ b/src/net/torvald/terrarum/modulecomputers/virtualcomputer/tvd/finder/VirtualDiskCracker.kt @@ -0,0 +1,843 @@ +package net.torvald.terrarum.virtualcomputer.tvd.finder + +import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.* +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.KeyEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.nio.charset.Charset +import java.time.Instant +import java.time.format.DateTimeFormatter +import java.util.* +import java.util.logging.Level +import javax.swing.* +import javax.swing.table.AbstractTableModel +import javax.swing.ListSelectionModel +import javax.swing.text.DefaultCaret + + +/** + * Created by SKYHi14 on 2017-04-01. + */ +class VirtualDiskCracker(val sysCharset: Charset = Charsets.UTF_8) : JFrame() { + + + private val annoyHackers = true // Jar build settings. Intended for Terrarum proj. + + + private val PREVIEW_MAX_BYTES = 4L * 1024 // 4 kBytes + + private val appName = "TerranVirtualDiskCracker" + private val copyright = "Copyright 2017 Torvald (minjaesong). Distributed under MIT license." + + private val magicOpen = "I solemnly swear that I am up to no good." + private val magicSave = "Mischief managed." + private val annoyWhenLaunchMsg = "Type in following to get started:\n$magicOpen" + private val annoyWhenSaveMsg = "Type in following to save:\n$magicSave" + + private val panelMain = JPanel() + private val menuBar = JMenuBar() + private val tableFiles: JTable + private val fileDesc = JTextArea() + private val diskInfo = JTextArea() + private val statBar = JLabel("Open a disk or create new to get started") + + private var vdisk: VirtualDisk? = null + private var clipboard: DiskEntry? = null + + private val labelPath = JLabel("(root)") + private var currentDirectoryEntries: Array? = null + private val directoryHierarchy = Stack(); init { directoryHierarchy.push(0) } + + private fun gotoSubDirectory(id: EntryID) { + directoryHierarchy.push(id) + labelPath.text = vdisk!!.entries[id]!!.getFilenameString(sysCharset) + selectedFile = null + fileDesc.text = "" + updateDiskInfo() + } + val currentDirectory: EntryID + get() = directoryHierarchy.peek() + val upperDirectory: EntryID + get() = if (directoryHierarchy.lastIndex == 0) 0 + else directoryHierarchy[directoryHierarchy.lastIndex - 1] + private fun gotoRoot() { + directoryHierarchy.removeAllElements() + directoryHierarchy.push(0) + selectedFile = null + fileDesc.text = "" + updateDiskInfo() + } + private fun gotoParent() { + if (directoryHierarchy.size > 1) + directoryHierarchy.pop() + selectedFile = null + fileDesc.text = "" + updateDiskInfo() + } + + + + private var selectedFile: EntryID? = null + + val tableColumns = arrayOf("Name", "Date Modified", "Size") + val tableParentRecord = arrayOf(arrayOf("..", "", "")) + + init { + + if (annoyHackers) { + val mantra = JOptionPane.showInputDialog(annoyWhenLaunchMsg) + if (mantra != magicOpen) { + System.exit(1) + } + } + + + + panelMain.layout = BorderLayout() + this.defaultCloseOperation = JFrame.EXIT_ON_CLOSE + + + tableFiles = JTable(tableParentRecord, tableColumns) + tableFiles.addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + val table = e.source as JTable + val row = table.rowAtPoint(e.point) + + + selectedFile = if (row > 0) + currentDirectoryEntries!![row - 1].entryID + else + null // clicked ".." + + + fileDesc.text = if (selectedFile != null) { + getFileInfoText(vdisk!!.entries[selectedFile!!]!!) + } + else + "" + + fileDesc.caretPosition = 0 + + // double click + if (e.clickCount == 2) { + if (row == 0) { + gotoParent() + } + else { + val record = currentDirectoryEntries!![row - 1] + if (record.contents is EntryDirectory) { + gotoSubDirectory(record.entryID) + } + } + } + } + }) + tableFiles.selectionModel = object : DefaultListSelectionModel() { + init { selectionMode = ListSelectionModel.SINGLE_SELECTION } + override fun clearSelection() { } // required! + override fun removeSelectionInterval(index0: Int, index1: Int) { } // required! + override fun fireValueChanged(isAdjusting: Boolean) { } // required! + } + tableFiles.model = object : AbstractTableModel() { + override fun getRowCount(): Int { + return if (vdisk != null) + 1 + (currentDirectoryEntries?.size ?: 0) + else 1 + } + + override fun getColumnCount() = tableColumns.size + + override fun getColumnName(column: Int) = tableColumns[column] + + override fun getValueAt(rowIndex: Int, columnIndex: Int): Any { + if (rowIndex == 0) { + return tableParentRecord[0][columnIndex] + } + else { + if (vdisk != null) { + val entry = currentDirectoryEntries!![rowIndex - 1] + return when(columnIndex) { + 0 -> entry.getFilenameString(sysCharset) + 1 -> Instant.ofEpochSecond(entry.modificationDate). + atZone(TimeZone.getDefault().toZoneId()). + format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + 2 -> entry.getEffectiveSize() + else -> "" + } + } + else { + return "" + } + } + } + } + + + + val menuFile = JMenu("File") + menuFile.mnemonic = KeyEvent.VK_F + menuFile.add("New Disk…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + try { + val makeNewDisk: Boolean + if (vdisk != null) { + makeNewDisk = confirmedDiscard() + } + else { + makeNewDisk = true + } + if (makeNewDisk) { + // inquire new size + val dialogBox = OptionDiskNameAndCap() + val confirmNew = JOptionPane.OK_OPTION == dialogBox.showDialog("Set Property of New Disk") + + if (confirmNew) { + vdisk = VDUtil.createNewDisk( + (dialogBox.capacity.value as Long).toLong(), + dialogBox.name.text, + sysCharset + ) + gotoRoot() + updateDiskInfo() + setStat("Disk created") + } + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + }) + menuFile.add("Open Disk…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + val makeNewDisk: Boolean + if (vdisk != null) { + makeNewDisk = confirmedDiscard() + } + else { + makeNewDisk = true + } + if (makeNewDisk) { + val fileChooser = JFileChooser() + fileChooser.showOpenDialog(null) + if (fileChooser.selectedFile != null) { + try { + vdisk = VDUtil.readDiskArchive(fileChooser.selectedFile, Level.WARNING, { popupWarning(it) }, sysCharset) + if (vdisk != null) { + gotoRoot() + updateDiskInfo() + setStat("Disk loaded") + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + } + }) + menuFile.addSeparator() + menuFile.add("Save Disk as…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + + + if (annoyHackers) { + val mantra = JOptionPane.showInputDialog(annoyWhenSaveMsg) + if (mantra != magicSave) { + popupError("Nope!") + return + } + } + + + + val fileChooser = JFileChooser() + fileChooser.showSaveDialog(null) + if (fileChooser.selectedFile != null) { + try { + VDUtil.dumpToRealMachine(vdisk!!, fileChooser.selectedFile) + setStat("Disk saved") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + } + }) + menuBar.add(menuFile) + + val menuEdit = JMenu("Edit") + menuEdit.mnemonic = KeyEvent.VK_E + menuEdit.add("New File…").addMouseListener(object: MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + val dialogBox = OptionFileNameAndCap() + val confirmNew = JOptionPane.OK_OPTION == dialogBox.showDialog("Set Property of New File") + + if (confirmNew) { + if (VDUtil.nameExists(vdisk!!, dialogBox.name.text, currentDirectory, sysCharset)) { + popupError("The name already exists") + } + else { + VDUtil.createNewBlankFile( + vdisk!!, + currentDirectory, + (dialogBox.capacity.value as Long).toLong(), + dialogBox.name.text, + sysCharset + ) + updateDiskInfo() + setStat("File created") + } + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuEdit.add("New Directory…").addMouseListener(object: MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + val newname = JOptionPane.showInputDialog("Enter a new directory name:") + if (newname != null) { + try { + if (VDUtil.nameExists(vdisk!!, newname, currentDirectory, sysCharset)) { + popupError("The name already exists") + } + else { + VDUtil.addDir(vdisk!!, currentDirectory, newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset)) + updateDiskInfo() + setStat("Directory created") + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + } + }) + menuEdit.addSeparator() + menuEdit.add("Cut").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + // copy + clipboard = vdisk!!.entries[selectedFile] + + // delete + if (vdisk != null && selectedFile != null) { + try { + VDUtil.deleteFile(vdisk!!, selectedFile!!) + updateDiskInfo() + setStat("File deleted") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuEdit.add("Copy").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + clipboard = vdisk!!.entries[selectedFile] + setStat("File copied") + } + }) + menuEdit.add("Paste").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + fun paste1(newname: ByteArray) { + try { + VDUtil.addFile(vdisk!!, currentDirectory, DiskEntry(// clone + vdisk!!.generateUniqueID(), + currentDirectory, + newname, + clipboard!!.creationDate, + clipboard!!.modificationDate, + clipboard!!.contents + )) + + updateDiskInfo() + setStat("File pasted") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + if (clipboard != null && vdisk != null) { + // check name collision. If it is, ask for new one + if (VDUtil.nameExists(vdisk!!, clipboard!!.getFilenameString(sysCharset), currentDirectory, sysCharset)) { + val newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:") + if (newname != null) { + paste1(newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset)) + } + } + else { + paste1(clipboard!!.filename) + } + } + } + }) + menuEdit.add("Paste as Symbolic Link").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + fun pasteSymbolic(newname: ByteArray) { + try { + // check if the original file is there in the first place + if (vdisk!!.entries[clipboard!!.entryID] != null) { + val entrySymlink = EntrySymlink(clipboard!!.entryID) + VDUtil.addFile(vdisk!!, currentDirectory, DiskEntry( + vdisk!!.generateUniqueID(), + currentDirectory, + newname, + VDUtil.currentUnixtime, + VDUtil.currentUnixtime, + entrySymlink + )) + + updateDiskInfo() + setStat("Symbolic link created") + } + else { + popupError("The orignal file is gone") + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + if (clipboard != null && vdisk != null) { + // check name collision. If it is, ask for new one + if (VDUtil.nameExists(vdisk!!, clipboard!!.getFilenameString(sysCharset), currentDirectory, sysCharset)) { + val newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:") + if (newname != null) { + pasteSymbolic(newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset)) + } + } + else { + pasteSymbolic(clipboard!!.filename) + } + } + } + }) + menuEdit.add("Delete").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null && selectedFile != null) { + try { + VDUtil.deleteFile(vdisk!!, selectedFile!!) + updateDiskInfo() + setStat("File deleted") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuEdit.add("Rename…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (selectedFile != null) { + try { + val newname = JOptionPane.showInputDialog("Enter a new name:") + if (newname != null) { + if (VDUtil.nameExists(vdisk!!, newname, currentDirectory, sysCharset)) { + popupError("The name already exists") + } + else { + VDUtil.renameFile(vdisk!!, selectedFile!!, newname, sysCharset) + updateDiskInfo() + setStat("File renamed") + } + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuEdit.add("Look Clipboard").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + popupMessage(if (clipboard != null) + "${clipboard ?: "(bug found)"}" + else "(nothing)", "Clipboard" + ) + } + + }) + menuEdit.addSeparator() + menuEdit.add("Import…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + val fileChooser = JFileChooser() + fileChooser.fileSelectionMode = JFileChooser.FILES_AND_DIRECTORIES + fileChooser.isMultiSelectionEnabled = true + fileChooser.showOpenDialog(null) + if (fileChooser.selectedFiles.isNotEmpty()) { + try { + fileChooser.selectedFiles.forEach { + if (!it.isDirectory) { + val entry = VDUtil.importFile(it, vdisk!!.generateUniqueID(), sysCharset) + + val newname: String? + if (VDUtil.nameExists(vdisk!!, entry.filename, currentDirectory)) { + newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:") + } + else { + newname = entry.getFilenameString(sysCharset) + } + + if (newname != null) { + entry.filename = newname.toEntryName(DiskEntry.NAME_LENGTH, sysCharset) + VDUtil.addFile(vdisk!!, currentDirectory, entry) + } + } + else { + val newname: String? + if (VDUtil.nameExists(vdisk!!, it.name.toEntryName(DiskEntry.NAME_LENGTH, sysCharset), currentDirectory)) { + newname = JOptionPane.showInputDialog("The name already exists. Enter a new name:") + } + else { + newname = it.name + } + + if (newname != null) { + VDUtil.importDirRecurse(vdisk!!, it, currentDirectory, sysCharset, newname) + } + } + } + updateDiskInfo() + setStat("File added") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + + fileChooser.isMultiSelectionEnabled = false + } + } + }) + menuEdit.add("Export…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + val file = vdisk!!.entries[selectedFile ?: currentDirectory]!! + + val fileChooser = JFileChooser() + fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY + fileChooser.isMultiSelectionEnabled = false + fileChooser.showSaveDialog(null) + if (fileChooser.selectedFile != null) { + try { + val file = VDUtil.resolveIfSymlink(vdisk!!, file.entryID) + if (file.contents is EntryFile) { + VDUtil.exportFile(file.contents, fileChooser.selectedFile) + setStat("File exported") + } + else { + VDUtil.exportDirRecurse(vdisk!!, file.entryID, fileChooser.selectedFile, sysCharset) + setStat("Files exported") + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + } + }) + menuEdit.addSeparator() + menuEdit.add("Rename Disk…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + val newname = JOptionPane.showInputDialog("Enter a new disk name:") + if (newname != null) { + vdisk!!.diskName = newname.toEntryName(VirtualDisk.NAME_LENGTH, sysCharset) + updateDiskInfo() + setStat("Disk renamed") + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuEdit.add("Resize Disk…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + val dialog = OptionSize() + val confirmed = dialog.showDialog("Input") == JOptionPane.OK_OPTION + if (confirmed) { + vdisk!!.capacity = (dialog.capacity.value as Long).toLong() + updateDiskInfo() + setStat("Disk resized") + } + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuEdit.addSeparator() + menuEdit.add("Set/Unset Write Protection").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + vdisk!!.isReadOnly = vdisk!!.isReadOnly.not() + updateDiskInfo() + setStat("Disk write protection ${if (vdisk!!.isReadOnly) "" else "dis"}engaged") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuBar.add(menuEdit) + + val menuManage = JMenu("Manage") + menuManage.add("Report Orphans…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + val reports = VDUtil.gcSearchOrphan(vdisk!!) + val orphansCount = reports.size + val orphansSize = reports.map { vdisk!!.entries[it]!!.contents.getSizeEntry() }.sum() + val message = "Orphans count: $orphansCount\n" + + "Size: ${orphansSize.bytes()}" + popupMessage(message, "Orphans Report") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuManage.add("Report Phantoms…").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + val reports = VDUtil.gcSearchPhantomBaby(vdisk!!) + val phantomsSize = reports.size + val message = "Phantoms count: $phantomsSize" + popupMessage(message, "Phantoms Report") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuManage.addSeparator() + menuManage.add("Remove Orphans").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + val oldSize = vdisk!!.usedBytes + VDUtil.gcDumpOrphans(vdisk!!) + val newSize = vdisk!!.usedBytes + popupMessage("Saved ${(oldSize - newSize).bytes()}", "GC Report") + updateDiskInfo() + setStat("Orphan nodes removed") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuManage.add("Full Garbage Collect").addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + if (vdisk != null) { + try { + val oldSize = vdisk!!.usedBytes + VDUtil.gcDumpAll(vdisk!!) + val newSize = vdisk!!.usedBytes + popupMessage("Saved ${(oldSize - newSize).bytes()}", "GC Report") + updateDiskInfo() + setStat("Orphan nodes and null directory pointers removed") + } + catch (e: Exception) { + e.printStackTrace() + popupError(e.toString()) + } + } + } + }) + menuBar.add(menuManage) + + val menuAbout = JMenu("About") + menuAbout.addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent?) { + popupMessage(copyright, "Copyright") + } + }) + menuBar.add(menuAbout) + + + + diskInfo.highlighter = null + diskInfo.text = "(Disk not loaded)" + diskInfo.preferredSize = Dimension(-1, 60) + + fileDesc.highlighter = null + fileDesc.text = "" + fileDesc.caret.isVisible = false + (fileDesc.caret as DefaultCaret).updatePolicy = DefaultCaret.NEVER_UPDATE + + val fileDescScroll = JScrollPane(fileDesc) + val tableFilesScroll = JScrollPane(tableFiles) + tableFilesScroll.size = Dimension(200, -1) + + val panelFinder = JPanel(BorderLayout()) + panelFinder.add(labelPath, BorderLayout.NORTH) + panelFinder.add(tableFilesScroll, BorderLayout.CENTER) + + val panelFileDesc = JPanel(BorderLayout()) + panelFileDesc.add(JLabel("Entry Information"), BorderLayout.NORTH) + panelFileDesc.add(fileDescScroll, BorderLayout.CENTER) + + val filesSplit = JSplitPane(JSplitPane.HORIZONTAL_SPLIT, panelFinder, panelFileDesc) + filesSplit.resizeWeight = 0.714285 + + + val panelDiskOp = JPanel(BorderLayout(2, 2)) + panelDiskOp.add(filesSplit, BorderLayout.CENTER) + panelDiskOp.add(diskInfo, BorderLayout.SOUTH) + + + panelMain.add(menuBar, BorderLayout.NORTH) + panelMain.add(panelDiskOp, BorderLayout.CENTER) + panelMain.add(statBar, BorderLayout.SOUTH) + + + this.title = appName + this.add(panelMain) + this.setSize(700, 700) + this.isVisible = true + } + + private fun confirmedDiscard() = 0 == JOptionPane.showOptionDialog( + null, // parent + "Any changes to current disk will be discarded. Continue?", + "Confirm Discard", // window title + JOptionPane.DEFAULT_OPTION, // option type + JOptionPane.WARNING_MESSAGE, // message type + null, // icon + Popups.okCancel, // options (provided by JOptionPane.OK_CANCEL_OPTION in this case) + Popups.okCancel[1] // default selection + ) + private fun popupMessage(message: String, title: String = "") { + JOptionPane.showOptionDialog( + null, + message, + title, + JOptionPane.DEFAULT_OPTION, + JOptionPane.INFORMATION_MESSAGE, + null, null, null + ) + } + private fun popupError(message: String, title: String = "Uh oh…") { + JOptionPane.showOptionDialog( + null, + message, + title, + JOptionPane.DEFAULT_OPTION, + JOptionPane.ERROR_MESSAGE, + null, null, null + ) + } + private fun popupWarning(message: String, title: String = "Careful…") { + JOptionPane.showOptionDialog( + null, + message, + title, + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, null, null + ) + } + private fun updateCurrentDirectory() { + currentDirectoryEntries = VDUtil.getDirectoryEntries(vdisk!!, currentDirectory) + } + private fun updateDiskInfo() { + val sb = StringBuilder() + directoryHierarchy.forEach { + sb.append(vdisk!!.entries[it]!!.getFilenameString(sysCharset)) + sb.append('/') + } + sb.dropLast(1) + labelPath.text = sb.toString() + + diskInfo.text = if (vdisk == null) "(Disk not loaded)" else getDiskInfoText(vdisk!!) + tableFiles.revalidate() + tableFiles.repaint() + + + updateCurrentDirectory() + } + private fun getDiskInfoText(disk: VirtualDisk): String { + return """Name: ${String(disk.diskName, sysCharset)} +Capacity: ${disk.capacity} bytes (${disk.usedBytes} bytes used, ${disk.capacity - disk.usedBytes} bytes free) +Write protected: ${disk.isReadOnly.toEnglish()}""" + } + + + private fun Boolean.toEnglish() = if (this) "Yes" else "No" + + + private fun getFileInfoText(file: DiskEntry): String { + return """Name: ${file.getFilenameString(sysCharset)} +Size: ${file.getEffectiveSize()} +Type: ${DiskEntry.getTypeString(file.contents)} +CRC: ${file.hashCode().toHex()} +EntryID: ${file.entryID} +ParentID: ${file.parentEntryID}""" + if (file.contents is EntryFile) """ + +Contents: +${String(file.contents.bytes.sliceArray64(0L..minOf(PREVIEW_MAX_BYTES, file.contents.bytes.size) - 1).toByteArray(), sysCharset)}""" else "" + } + private fun Long.bytes() = if (this == 1L) "1 byte" else "$this bytes" + private fun Int.entries() = if (this == 1) "1 entry" else "$this entries" + private fun DiskEntry.getEffectiveSize() = if (this.contents is EntryFile) + this.contents.getSizePure().bytes() + else if (this.contents is EntryDirectory) + this.contents.entryCount.entries() + else if (this.contents is EntrySymlink) + "(symlink)" + else + "n/a" + private fun setStat(message: String) { + statBar.text = message + } +} + +fun main(args: Array) { + VirtualDiskCracker(Charset.forName("CP437")) +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/serialise/ReadLayerDataZip.kt b/src/net/torvald/terrarum/serialise/ReadLayerDataZip.kt index b4ba41c0c..abb0789b5 100644 --- a/src/net/torvald/terrarum/serialise/ReadLayerDataZip.kt +++ b/src/net/torvald/terrarum/serialise/ReadLayerDataZip.kt @@ -4,13 +4,12 @@ import net.torvald.terrarum.gameworld.BlockAddress import net.torvald.terrarum.gameworld.BlockDamage import net.torvald.terrarum.gameworld.MapLayer import net.torvald.terrarum.gameworld.PairedMapLayer -import net.torvald.terrarum.modulebasegame.gameworld.GameWorldExtension import net.torvald.terrarum.realestate.LandUtil +import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.DiskSkimmer.Companion.read import java.io.* import java.nio.charset.Charset import java.util.* import java.util.zip.Inflater -import java.util.zip.InflaterOutputStream import kotlin.IllegalArgumentException import kotlin.collections.HashMap @@ -27,13 +26,6 @@ internal object ReadLayerDataZip { val magicBytes = ByteArray(4) - val versionNumber = ByteArray(1) - val layerCount = ByteArray(1) - val payloadCountByte = ByteArray(1) - val compression = ByteArray(1) - val worldWidth = ByteArray(4) - val worldHeight = ByteArray(4) - val spawnAddress = ByteArray(6) ////////////////// @@ -47,18 +39,16 @@ internal object ReadLayerDataZip { throw IllegalArgumentException("File not a Layer Data") } - - inputStream.read(versionNumber) - inputStream.read(layerCount) - inputStream.read(payloadCountByte) - inputStream.read(compression) - inputStream.read(worldWidth) - inputStream.read(worldHeight) - inputStream.read(spawnAddress) + val versionNumber = inputStream.read(1)[0].toUint() + val layerCount = inputStream.read(1)[0].toUint() + val payloadCount = inputStream.read(1)[0].toUint() + val compression = inputStream.read(1)[0].toUint() + val width = inputStream.read(4).toLittleInt() + val height = inputStream.read(4).toLittleInt() + val spawnAddress = inputStream.read(6).toLittleInt48() // read payloads - val payloadCount = payloadCountByte[0].toUint() val pldBuffer4 = ByteArray(4) val pldBuffer6 = ByteArray(6) val pldBuffer8 = ByteArray(8) @@ -114,8 +104,6 @@ internal object ReadLayerDataZip { // END OF FILE READ // ////////////////////// - val width = worldWidth.toLittleInt() - val height = worldHeight.toLittleInt() val worldSize = width.toLong() * height val payloadBytes = HashMap() @@ -140,7 +128,7 @@ internal object ReadLayerDataZip { } } - val spawnPoint = LandUtil.resolveBlockAddr(width, spawnAddress.toLittleInt48()) + val spawnPoint = LandUtil.resolveBlockAddr(width, spawnAddress) val terrainDamages = HashMap() val wallDamages = HashMap() diff --git a/src/net/torvald/terrarum/serialise/SavegameLoader.kt b/src/net/torvald/terrarum/serialise/SavegameLoader.kt new file mode 100644 index 000000000..857766afd --- /dev/null +++ b/src/net/torvald/terrarum/serialise/SavegameLoader.kt @@ -0,0 +1,10 @@ +package net.torvald.terrarum.serialise + +/** + * Created by minjaesong on 2018-10-03. + */ +object SavegameLoader { + + // TODO read TEVd, load necessary shits + +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/serialise/SavegameWriter.kt b/src/net/torvald/terrarum/serialise/SavegameWriter.kt new file mode 100644 index 000000000..5db37a616 --- /dev/null +++ b/src/net/torvald/terrarum/serialise/SavegameWriter.kt @@ -0,0 +1,60 @@ +package net.torvald.terrarum.serialise + +import net.torvald.terrarum.Terrarum +import net.torvald.terrarum.modulebasegame.gameworld.GameWorldExtension +import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.DiskEntry +import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.DiskSkimmer +import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.VDUtil +import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.VirtualDisk +import java.io.File +import java.nio.charset.Charset + +/** + * Created by minjaesong on 2018-10-03. + */ +object SavegameWriter { + + // TODO create temporary files (worldinfo), create JSON files on RAM, pack those into TEVd as per Savegame container.txt + + private val charset = Charset.forName("UTF-8") + + operator fun invoke(): Boolean { + val diskImage = generateDiskImage(null) + + return false + } + + + private fun generateDiskImage(oldDiskFile: File?): VirtualDisk { + val disk = VDUtil.createNewDisk(0x7FFFFFFFFFFFFFFFL, "TerrarumSave", charset) + val oldDiskSkimmer = oldDiskFile?.let { DiskSkimmer(oldDiskFile) } + + val ROOT = disk.root.entryID + val ingame = Terrarum.ingame!! + val gameworld = ingame.world + + // serialise current world (stage) + val world = WriteLayerDataZip() // filename can be anything that is "tmp_world[n]" where [n] is any number + val worldFile = VDUtil.importFile(world!!, gameworld.worldIndex, charset) + + // add current world (stage) to the disk + VDUtil.addFile(disk, ROOT, worldFile) + + // put other worlds (stages) to the disk (without loading whole oldDiskFile onto the disk) + oldDiskSkimmer?.let { + // skim-and-write other worlds + for (c in 1..ingame.gameworldCount) { + if (c != gameworld.worldIndex) { + val oldWorldFile = oldDiskSkimmer.requestFile(c) + VDUtil.addFile(disk, ROOT, oldWorldFile!!) + } + } + } + + // TODO world[n] is done, needs whole other things + + + + return disk + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/serialise/WriteLayerDataZip.kt b/src/net/torvald/terrarum/serialise/WriteLayerDataZip.kt index 0c71e4dc6..15e1d1bed 100644 --- a/src/net/torvald/terrarum/serialise/WriteLayerDataZip.kt +++ b/src/net/torvald/terrarum/serialise/WriteLayerDataZip.kt @@ -14,7 +14,12 @@ import java.util.zip.DeflaterOutputStream import java.util.zip.GZIPOutputStream /** - * TODO this one does not use TerranVirtualDisk + * This object only writes a file named 'worldinfo1'. + * + * The intended operation is as follows: + * 1. This and others write + * + * TODO temporarily dump on the disk THEN pack? Or put all the files (in ByteArray64) in the RAM THEN pack? * * Created by minjaesong on 2016-03-18. */ @@ -23,7 +28,7 @@ internal object WriteLayerDataZip { // FIXME UNTESTED !! - val LAYERS_FILENAME = "worldinfo1" + val LAYERS_FILENAME = "world" val MAGIC = byteArrayOf(0x54, 0x45, 0x4D, 0x7A) val VERSION_NUMBER = 3.toByte() @@ -34,29 +39,36 @@ internal object WriteLayerDataZip { val PAYLOAD_FOOTER = byteArrayOf(0x45, 0x6E, 0x64, 0x50, 0x59, 0x4C, 0x64, -1) val FILE_FOOTER = byteArrayOf(0x45, 0x6E, 0x64, 0x54, 0x45, 0x4D, -1, -2) - val NULL: Byte = 0 + //val NULL: Byte = 0 - internal operator fun invoke(saveDirectoryName: String): Boolean { - val path = "${Terrarum.defaultSaveDir}/$saveDirectoryName/${LAYERS_FILENAME}" - val tempPath = "${path}_bak" + /** + * TODO currently it'll dump the temporary file (tmp_worldinfo1) onto the disk and will return the temp file. + * + * @return File on success; `null` on failure + */ + internal operator fun invoke(): File? { val world = (Terrarum.ingame!!.world) - val parentDir = File("${Terrarum.defaultSaveDir}/$saveDirectoryName") + val path = "${Terrarum.defaultSaveDir}/tmp_$LAYERS_FILENAME${world.worldIndex}" + + // TODO let's try dump-on-the-disk-then-pack method... + + /*val parentDir = File("${Terrarum.defaultSaveDir}/$saveDirectoryName") if (!parentDir.exists()) { parentDir.mkdir() } else if (!parentDir.isDirectory) { EchoError("Savegame directory is not actually a directory, aborting...") return false - } + }*/ - val tempFile = File(tempPath) val outFile = File(path) - tempFile.createNewFile() + if (outFile.exists()) outFile.delete() + outFile.createNewFile() - val outputStream = BufferedOutputStream(FileOutputStream(tempFile), 8192) + val outputStream = BufferedOutputStream(FileOutputStream(outFile), 8192) val deflater = DeflaterOutputStream(outputStream, true) fun wb(byteArray: ByteArray) { outputStream.write(byteArray) } @@ -152,12 +164,8 @@ internal object WriteLayerDataZip { outputStream.flush() outputStream.close() - outFile.delete() - tempFile.copyTo(outFile, overwrite = true) - tempFile.delete() - println("Saved map data '$LAYERS_FILENAME' to $saveDirectoryName.") - return true + return outFile } catch (e: IOException) { e.printStackTrace() @@ -166,7 +174,7 @@ internal object WriteLayerDataZip { outputStream.close() } - return false + return null } diff --git a/work_files/DataFormats/Savegame container.txt b/work_files/DataFormats/Savegame container.txt index e30ded270..a352aa036 100644 --- a/work_files/DataFormats/Savegame container.txt +++ b/work_files/DataFormats/Savegame container.txt @@ -5,10 +5,13 @@ Files contained the TerranVirtualDisk is as follows: (root) worldinfo0 -- Savegame Metadata (TESV) - worldinfo1 -- Layer Data (TEMD) - worldinfo2 -- Copy of blocks.csv -- will use this from the next load - worldinfo3 -- Copy of items.csv -- will use this from the next load - worldinfo4 -- Copy of materials.csv -- will use this from the next load + Has fixed Entry ID of 32767 + worldinfo1 -- Copy of blocks.csv -- will use this from the next load + worldinfo2 -- Copy of items.csv -- will use this from the next load + worldinfo3 -- Copy of materials.csv -- will use this from the next load + world[n] -- Layer Data (TEMD); [n] is a serial number of the world (starts at 1) + Has fixed Entry ID of [n] + (any random number in Hex 32768..ACTORID_MIN - 1) -- Serialised Dynamic Item? (any random number in Hex ACTORID_MIN..FFFFFFFF) -- Serialised Entity Information (including Player) (PLAYER_REF_ID in Hex -- 91A7E2) -- Player Character Information (Serialised--JSON'd--Entity Information) (51621D) -- The Debug Player (Serialised Entity Information)