completely abolishing GSON; new save format impl wip

This commit is contained in:
minjaesong
2021-08-23 16:55:51 +09:00
parent df1ebdf93d
commit e15d5c9b05
58 changed files with 421 additions and 2101 deletions

View File

@@ -1,114 +0,0 @@
package net.torvald.terrarum.serialise
import net.torvald.terrarum.AppLoader
import net.torvald.terrarum.serialise.WriteLayerDataZip.FILE_FOOTER
import net.torvald.terrarum.serialise.WriteLayerDataZip.PAYLOAD_FOOTER
import net.torvald.terrarum.toHex
import java.nio.charset.Charset
import java.util.*
/**
* Created by minjaesong on 2019-02-20.
*/
object PayloadUtil {
/**
* InputStream must be located manually at the payload begin
*
* For the actual use case, take a look at the source of the [ReadLayerDataZip].
*/
fun readAll(inputStream: MarkableFileInputStream, footer: ByteArray = FILE_FOOTER): HashMap<String, TEMzPayload> {
val pldBuffer4 = ByteArray(4)
val pldBuffer6 = ByteArray(6)
val pldBuffer8 = ByteArray(8)
val payloads = HashMap<String, TEMzPayload>()
var pldCnt = 1
while (true) {
// read header and get payload's name
inputStream.read(pldBuffer8)
// check if end of payload reached
if (pldBuffer8.contentEquals(footer)) {
break
}
val payloadName = pldBuffer8.copyOfRange(4, 8).toString(Charset.forName("US-ASCII"))
AppLoader.printdbg(this, "Payload $pldCnt name: $payloadName") // maybe maybe related with buffer things?
// get uncompressed size
inputStream.read(pldBuffer6)
val uncompressedSize = pldBuffer6.toLittleInt48()
// get deflated size
inputStream.mark(2147483647) // FIXME deflated stream cannot be larger than 2 GB
// creep forward until we hit the PAYLOAD_FOOTER
var compressedSize: Int = 0 // FIXME deflated stream cannot be larger than 2 GB
// loop init
inputStream.read(pldBuffer8)
// loop main
while (!pldBuffer8.contentEquals(PAYLOAD_FOOTER)) {
val aByte = inputStream.read(); compressedSize += 1
if (aByte == -1) throw InternalError("Unexpected end-of-file at payload $pldCnt")
pldBuffer8.shiftLeftBy(1, aByte.toByte())
}
// at this point, we should have correct size of deflated bytestream
AppLoader.printdbg(this, "Payload $pldCnt compressed size: $compressedSize")
val compressedBytes = ByteArray(compressedSize) // FIXME deflated stream cannot be larger than 2 GB
inputStream.reset() // go back to marked spot
inputStream.read(compressedBytes)
// PRO Debug tip: every deflated bytes must begin with 0x789C or 0x78DA
// Thus, \0pLd + [10] must be either of these.
// put constructed payload into a container
payloads.put(payloadName, TEMzPayload(uncompressedSize, compressedBytes))
// skip over to be aligned with the next payload
inputStream.skip(8)
pldCnt += 1
}
return payloads
}
private fun ByteArray.shiftLeftBy(size: Int, fill: Byte = 0.toByte()) {
if (size == 0) {
return
}
else if (size < 0) {
throw IllegalArgumentException("This won't shift to right (size = $size)")
}
else if (size >= this.size) {
Arrays.fill(this, 0.toByte())
}
else {
for (c in size..this.lastIndex) {
this[c - size] = this[c]
}
for (c in (this.size - size)..this.lastIndex) {
this[c] = fill
}
}
}
private fun ByteArray.toByteString(): String {
val sb = StringBuilder()
this.forEach {
sb.append(it.toUint().toHex().takeLast(2))
sb.append(' ')
}
sb.deleteCharAt(sb.lastIndex)
return sb.toString()
}
data class TEMzPayload(val uncompressedSize: Long, val bytes: ByteArray) // FIXME deflated stream cannot be larger than 2 GB
}

View File

@@ -1,188 +0,0 @@
package net.torvald.terrarum.serialise
/**
* Only being used by the title screen and the demoworld. This object may get deleted at any update
*
* Created by minjaesong on 2016-08-24.
*/
// internal for everything: prevent malicious module from messing up the savedata
@Deprecated("TEMD is deprecated format; use TEMz which does compression")
internal object ReadLayerData {
init {
throw Error("TEMD is old and removed format; use TEMz which does compression")
}
/*internal operator fun invoke(inputStream: InputStream, inWorld: GameWorldExtension? = null): GameWorldExtension {
val magicBytes = ByteArray(4)
val layerSizeBytes = ByteArray(1)
val layerCountBytes = ByteArray(1)
val worldWidthBytes = ByteArray(4)
val worldHeightBytes = ByteArray(4)
val spawnCoordXBytes = ByteArray(4)
val spawnCoordYBytes = ByteArray(4)
// read header first
inputStream.read(magicBytes)
if (!Arrays.equals(magicBytes, WriteLayerData.MAGIC)) {
throw IllegalArgumentException("File not a Layer Data")
}
inputStream.read(layerSizeBytes)
inputStream.read(layerCountBytes)
inputStream.skip(2) // reserved bytes
inputStream.read(worldWidthBytes)
inputStream.read(worldHeightBytes)
inputStream.read(spawnCoordXBytes)
inputStream.read(spawnCoordYBytes)
val worldWidth = worldWidthBytes.toLittleInt()
val worldHeight = worldHeightBytes.toLittleInt()
val bytesPerTile = layerSizeBytes[0].toUint()
val layerCount = layerCountBytes[0].toUint()
val layerSize = worldWidth * worldHeight * bytesPerTile
val terrainLayerMSB = ByteArray(layerSize)
val wallLayerMSB = ByteArray(layerSize)
val terrainLayerLSB = ByteArray(layerSize / 2)
val wallLayerLSB = ByteArray(layerSize / 2)
var wireLayer: ByteArray? = null
inputStream.read(terrainLayerMSB)
inputStream.read(wallLayerMSB)
inputStream.read(terrainLayerLSB)
inputStream.read(wallLayerLSB)
if (layerCount == 4) {
wireLayer = ByteArray(layerSize)
inputStream.read(wireLayer)
}
// create world out of tiles data
val retWorld = inWorld ?: GameWorldExtension(1, worldWidth, worldHeight, 0, 0, 0) // FIXME null TIME_T for the (partial) test to pass
retWorld.layerTerrain.data = terrainLayerMSB
retWorld.layerWall.data = wallLayerMSB
retWorld.layerTerrainLowBits.data = terrainLayerLSB
retWorld.layerWallLowBits.data = wallLayerLSB
retWorld.spawnX = spawnCoordXBytes.toLittleInt()
retWorld.spawnY = spawnCoordYBytes.toLittleInt()
return retWorld
}
internal fun InputStream.readRelative(b: ByteArray, off: Int, len: Int): Int {
if (b == null) {
throw NullPointerException()
} else if (off < 0 || len < 0 || len > b.size) {
throw IndexOutOfBoundsException()
} else if (len == 0) {
return 0
}
var c = read()
if (c == -1) {
return -1
}
b[0] = c.toByte()
var i = 1
try {
while (i < len) {
c = read()
if (c == -1) {
break
}
b[i] = c.toByte()
i++
}
} catch (ee: IOException) {
}
return i
}*/
}
fun Int.toLittle() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte(),
this.ushr(16).and(0xFF).toByte(),
this.ushr(24).and(0xFF).toByte()
)
/** Converts int as 2-byte array, discarding the sign.*/
fun Int.toULittleShort() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte()
)
/** Converts int as 2-byte array, preserving the sign. In other words, it converts int to short. */
fun Int.toLittleShort() = byteArrayOf(
this.and(0xFF).toByte(),
this.shr(8).and(0xFF).toByte()
)
fun Long.toLittle() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte(),
this.ushr(16).and(0xFF).toByte(),
this.ushr(24).and(0xFF).toByte(),
this.ushr(32).and(0xFF).toByte(),
this.ushr(40).and(0xFF).toByte(),
this.ushr(48).and(0xFF).toByte(),
this.ushr(56).and(0xFF).toByte()
)
/** Converts long as 6-byte array, discarding the sign. */
fun Long.toULittle48() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte(),
this.ushr(16).and(0xFF).toByte(),
this.ushr(24).and(0xFF).toByte(),
this.ushr(32).and(0xFF).toByte(),
this.ushr(40).and(0xFF).toByte()
)
fun Double.toLittle() = java.lang.Double.doubleToRawLongBits(this).toLittle()
fun Boolean.toLittle() = byteArrayOf(if (this) 0xFF.toByte() else 0.toByte())
fun ByteArray.toLittleInt() =
if (this.size != 4) throw Error("Array not in size of 4")
else this[0].toUint() or
this[1].toUint().shl(8) or
this[2].toUint().shl(16) or
this[3].toUint().shl(24)
fun ByteArray.toULittleShort() =
if (this.size != 4) throw Error("Array not in size of 2")
else this[0].toUint() or
this[1].toUint().shl(8)
fun ByteArray.toLittleShort() =
if (this.size != 4) throw Error("Array not in size of 2")
else this[0].toUint() or
this[1].toInt().shl(8)
fun ByteArray.toLittleLong() =
if (this.size != 8) throw Error("Array not in size of 8")
else this[0].toUlong() or
this[1].toUlong().shl(8) or
this[2].toUlong().shl(16) or
this[3].toUlong().shl(24) or
this[4].toUlong().shl(32) or
this[5].toUlong().shl(40) or
this[6].toUlong().shl(48) or
this[7].toUlong().shl(56)
fun ByteArray.toLittleInt48() =
if (this.size != 6) throw Error("Array not in size of 6")
else this[0].toUlong() or
this[1].toUlong().shl(8) or
this[2].toUlong().shl(16) or
this[3].toUlong().shl(24) or
this[4].toUlong().shl(32) or
this[5].toUlong().shl(40)
fun ByteArray.toLittleFloat() = java.lang.Float.intBitsToFloat(this.toLittleInt())
fun Byte.toUlong() = java.lang.Byte.toUnsignedLong(this)
fun Byte.toUint() = java.lang.Byte.toUnsignedInt(this)
const val WORLD_GENERATOR_VERSION = 1

View File

@@ -1,224 +0,0 @@
package net.torvald.terrarum.serialise
import com.badlogic.gdx.utils.compression.Lzma
import net.torvald.terrarum.AppLoader.printdbg
import net.torvald.terrarum.gameitem.ItemID
import net.torvald.terrarum.gameworld.BlockAddress
import net.torvald.terrarum.gameworld.BlockLayer
import net.torvald.terrarum.gameworld.FluidType
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.DiskSkimmer.Companion.read
import net.torvald.terrarum.realestate.LandUtil
import java.io.*
import java.util.*
import kotlin.collections.HashMap
/**
* Created by minjaesong on 2016-08-24.
*/
// internal for everything: prevent malicious module from messing up the savedata
internal object ReadLayerDataLzma {
// FIXME TERRAIN DAMAGE UNTESTED
internal operator fun invoke(file: File): ReadLayerDataZip.LayerData {
val inputStream = MarkableFileInputStream(FileInputStream(file))
val magicBytes = ByteArray(4)
//////////////////
// FILE READING //
//////////////////
// read header first
inputStream.read(magicBytes)
if (!Arrays.equals(magicBytes, WriteLayerDataZip.MAGIC)) {
throw IllegalArgumentException("File not a Layer Data")
}
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 generatorVer = inputStream.read(2).toULittleShort()
val width = inputStream.read(4).toLittleInt()
val height = inputStream.read(4).toLittleInt()
val spawnAddress = inputStream.read(6).toLittleInt48()
if (compression != 2) throw IllegalArgumentException("Input file is not compressed as LZMA; it's using algorithm $compression")
printdbg(this, "Version number: $versionNumber")
printdbg(this, "Layers count: $layerCount")
printdbg(this, "Payloads count: $payloadCount")
printdbg(this, "Compression: $compression")
printdbg(this, "World generator version: $generatorVer")
printdbg(this, "Dimension: ${width}x$height")
// read payloads
val payloads = PayloadUtil.readAll(inputStream)
/*val pldBuffer4 = ByteArray(4)
val pldBuffer6 = ByteArray(6)
val pldBuffer8 = ByteArray(8)
val payloads = HashMap<String, TEMzPayload>()
// TODO please test the read; write has been fixed up
for (pldCnt in 0 until payloadCount) {
inputStream.read(pldBuffer4)
// check payload header
if (!pldBuffer4.contentEquals(WriteLayerDataZip.PAYLOAD_HEADER))
throw InternalError("Payload $pldCnt not found -- expected ${WriteLayerDataZip.PAYLOAD_HEADER.toByteString()}, got ${pldBuffer4.toByteString()}")
// get payload's name
inputStream.read(pldBuffer4)
val payloadName = pldBuffer4.toString(Charset.forName("US-ASCII"))
printdbg(this, "Payload $pldCnt name: $payloadName") // maybe maybe related with buffer things?
// get uncompressed size
inputStream.read(pldBuffer6)
val uncompressedSize = pldBuffer6.toLittleInt48()
// get deflated size
inputStream.mark(2147483647) // FIXME deflated stream cannot be larger than 2 GB
// creep forward until we hit the PAYLOAD_FOOTER
var deflatedSize: Int = 0 // FIXME deflated stream cannot be larger than 2 GB
// loop init
inputStream.read(pldBuffer8)
// loop main
while (!pldBuffer8.contentEquals(WriteLayerDataZip.PAYLOAD_FOOTER)) {
val aByte = inputStream.read(); deflatedSize += 1
if (aByte == -1) throw InternalError("Unexpected end-of-file at payload $pldCnt")
pldBuffer8.shiftLeftBy(1, aByte.toByte())
}
// at this point, we should have correct size of deflated bytestream
printdbg(this, "Payload $pldCnt compressed size: $deflatedSize")
val deflatedBytes = ByteArray(deflatedSize) // FIXME deflated stream cannot be larger than 2 GB
inputStream.reset() // go back to marked spot
inputStream.read(deflatedBytes)
// PRO Debug tip: every deflated bytes must begin with 0x789C or 0x78DA
// Thus, \0pLd + [10] must be either of these.
// put constructed payload into a container
payloads.put(payloadName, TEMzPayload(uncompressedSize, deflatedBytes))
// skip over to be aligned with the next payload
inputStream.skip(8)
}
// test for EOF
inputStream.read(pldBuffer8)
if (!pldBuffer8.contentEquals(WriteLayerDataZip.FILE_FOOTER))
throw InternalError("Expected end-of-file, got not-so-end-of-file")*/
//////////////////////
// END OF FILE READ //
//////////////////////
val worldSize = width.toLong() * height
val payloadBytes = HashMap<String, ByteArray>()
payloads.forEach { t, u ->
val inflatedOS = ByteArrayOutputStream(u.uncompressedSize.toInt()) // FIXME deflated stream cannot be larger than 2 GB
try {
Lzma.decompress(ByteArrayInputStream(u.bytes), inflatedOS)
}
catch (e: RuntimeException) {
// keep it empty (zero-sized file was compressed)
}
val inflatedFile = inflatedOS.toByteArray()
payloadBytes[t] = inflatedFile
}
val spawnPoint = LandUtil.resolveBlockAddr(width, spawnAddress)
val terrainDamages = HashMap<BlockAddress, Float>()
val wallDamages = HashMap<BlockAddress, Float>()
val fluidTypes = HashMap<BlockAddress, FluidType>()
val fluidFills = HashMap<BlockAddress, Float>()
val tileNumToName = HashMap<Int, ItemID>()
// parse terrain damages
for (c in payloadBytes["TdMG"]!!.indices step 10) {
val bytes = payloadBytes["TdMG"]!!
val tileAddr = bytes.sliceArray(c..c+5)
val value = bytes.sliceArray(c+6..c+9)
terrainDamages[tileAddr.toLittleInt48()] = value.toLittleFloat()
}
// parse wall damages
for (c in payloadBytes["WdMG"]!!.indices step 10) {
val bytes = payloadBytes["WdMG"]!!
val tileAddr = bytes.sliceArray(c..c+5)
val value = bytes.sliceArray(c+6..c+9)
wallDamages[tileAddr.toLittleInt48()] = value.toLittleFloat()
}
// TODO parse fluid(Types|Fills)
// TODO parse tileNumToName
return ReadLayerDataZip.LayerData(
BlockLayer(width, height, payloadBytes["WALL"]!!),
BlockLayer(width, height, payloadBytes["TERR"]!!),
spawnPoint.first, spawnPoint.second,
wallDamages, terrainDamages, fluidTypes, fluidFills, tileNumToName
)
}
internal fun InputStream.readRelative(b: ByteArray, off: Int, len: Int): Int {
if (b == null) {
throw NullPointerException()
} else if (off < 0 || len < 0 || len > b.size) {
throw IndexOutOfBoundsException()
} else if (len == 0) {
return 0
}
var c = read()
if (c == -1) {
return -1
}
b[0] = c.toByte()
var i = 1
try {
while (i < len) {
c = read()
if (c == -1) {
break
}
b[i] = c.toByte()
i++
}
} catch (ee: IOException) {
}
return i
}
}

View File

@@ -1,243 +0,0 @@
package net.torvald.terrarum.serialise
import net.torvald.terrarum.AppLoader.printdbg
import net.torvald.terrarum.gameitem.ItemID
import net.torvald.terrarum.gameworld.BlockAddress
import net.torvald.terrarum.gameworld.BlockLayer
import net.torvald.terrarum.gameworld.FluidType
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.DiskSkimmer.Companion.read
import net.torvald.terrarum.realestate.LandUtil
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.util.*
import java.util.zip.Inflater
import kotlin.collections.HashMap
/**
* Created by minjaesong on 2016-08-24.
*/
// internal for everything: prevent malicious module from messing up the savedata
internal object ReadLayerDataZip {
// FIXME TERRAIN DAMAGE UNTESTED
internal operator fun invoke(file: File): LayerData {
val inputStream = MarkableFileInputStream(FileInputStream(file))
val magicBytes = ByteArray(4)
//////////////////
// FILE READING //
//////////////////
// read header first
inputStream.read(magicBytes)
if (!Arrays.equals(magicBytes, WriteLayerDataZip.MAGIC)) {
throw IllegalArgumentException("File not a Layer Data")
}
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 generatorVer = inputStream.read(2).toULittleShort()
val width = inputStream.read(4).toLittleInt()
val height = inputStream.read(4).toLittleInt()
val spawnAddress = inputStream.read(6).toLittleInt48()
if (compression != 1) throw IllegalArgumentException("Input file is not compressed as DEFLATE; it's using algorithm $compression")
printdbg(this, "Version number: $versionNumber")
printdbg(this, "Layers count: $layerCount")
printdbg(this, "Payloads count: $payloadCount")
printdbg(this, "Compression: $compression")
printdbg(this, "World generator version: $generatorVer")
printdbg(this, "Dimension: ${width}x$height")
// read payloads
val payloads = PayloadUtil.readAll(inputStream)
/*val pldBuffer4 = ByteArray(4)
val pldBuffer6 = ByteArray(6)
val pldBuffer8 = ByteArray(8)
val payloads = HashMap<String, TEMzPayload>()
// TODO please test the read; write has been fixed up
for (pldCnt in 0 until payloadCount) {
inputStream.read(pldBuffer4)
// check payload header
if (!pldBuffer4.contentEquals(WriteLayerDataZip.PAYLOAD_HEADER))
throw InternalError("Payload $pldCnt not found -- expected ${WriteLayerDataZip.PAYLOAD_HEADER.toByteString()}, got ${pldBuffer4.toByteString()}")
// get payload's name
inputStream.read(pldBuffer4)
val payloadName = pldBuffer4.toString(Charset.forName("US-ASCII"))
printdbg(this, "Payload $pldCnt name: $payloadName") // maybe maybe related with buffer things?
// get uncompressed size
inputStream.read(pldBuffer6)
val uncompressedSize = pldBuffer6.toLittleInt48()
// get deflated size
inputStream.mark(2147483647) // FIXME deflated stream cannot be larger than 2 GB
// creep forward until we hit the PAYLOAD_FOOTER
var deflatedSize: Int = 0 // FIXME deflated stream cannot be larger than 2 GB
// loop init
inputStream.read(pldBuffer8)
// loop main
while (!pldBuffer8.contentEquals(WriteLayerDataZip.PAYLOAD_FOOTER)) {
val aByte = inputStream.read(); deflatedSize += 1
if (aByte == -1) throw InternalError("Unexpected end-of-file at payload $pldCnt")
pldBuffer8.shiftLeftBy(1, aByte.toByte())
}
// at this point, we should have correct size of deflated bytestream
printdbg(this, "Payload $pldCnt compressed size: $deflatedSize")
val deflatedBytes = ByteArray(deflatedSize) // FIXME deflated stream cannot be larger than 2 GB
inputStream.reset() // go back to marked spot
inputStream.read(deflatedBytes)
// PRO Debug tip: every deflated bytes must begin with 0x789C or 0x78DA
// Thus, \0pLd + [10] must be either of these.
// put constructed payload into a container
payloads.put(payloadName, TEMzPayload(uncompressedSize, deflatedBytes))
// skip over to be aligned with the next payload
inputStream.skip(8)
}
// test for EOF
inputStream.read(pldBuffer8)
if (!pldBuffer8.contentEquals(WriteLayerDataZip.FILE_FOOTER))
throw InternalError("Expected end-of-file, got not-so-end-of-file")*/
//////////////////////
// END OF FILE READ //
//////////////////////
val worldSize = width.toLong() * height
val payloadBytes = HashMap<String, ByteArray>()
payloads.forEach { t, u ->
val inflatedFile = ByteArray(u.uncompressedSize.toInt()) // FIXME deflated stream cannot be larger than 2 GB
val inflater = Inflater()
inflater.setInput(u.bytes, 0, u.bytes.size)
val uncompLen = inflater.inflate(inflatedFile)
// just in case
if (uncompLen.toLong() != u.uncompressedSize)
throw InternalError("Payload $t DEFLATE size mismatch -- expected ${u.uncompressedSize}, got $uncompLen")
payloadBytes[t] = inflatedFile
}
val spawnPoint = LandUtil.resolveBlockAddr(width, spawnAddress)
val terrainDamages = HashMap<BlockAddress, Float>()
val wallDamages = HashMap<BlockAddress, Float>()
val fluidTypes = HashMap<BlockAddress, FluidType>()
val fluidFills = HashMap<BlockAddress, Float>()
val tileNumberToNameMap = HashMap<Int, ItemID>()
// parse terrain damages
for (c in payloadBytes["TdMG"]!!.indices step 10) {
val bytes = payloadBytes["TdMG"]!!
val tileAddr = bytes.sliceArray(c..c+5)
val value = bytes.sliceArray(c+6..c+9)
terrainDamages[tileAddr.toLittleInt48()] = value.toLittleFloat()
}
// parse wall damages
for (c in payloadBytes["WdMG"]!!.indices step 10) {
val bytes = payloadBytes["WdMG"]!!
val tileAddr = bytes.sliceArray(c..c+5)
val value = bytes.sliceArray(c+6..c+9)
wallDamages[tileAddr.toLittleInt48()] = value.toLittleFloat()
}
// TODO parse fluid(Types|Fills)
return LayerData(
BlockLayer(width, height, payloadBytes["WALL"]!!),
BlockLayer(width, height, payloadBytes["TERR"]!!),
spawnPoint.first, spawnPoint.second,
wallDamages, terrainDamages, fluidTypes, fluidFills, tileNumberToNameMap
)
}
/**
* Immediately deployable, a part of the gameworld
*/
internal data class LayerData(
val layerWall: BlockLayer,
val layerTerrain: BlockLayer,
//val layerThermal: MapLayerHalfFloat, // in Kelvins
//val layerAirPressure: MapLayerHalfFloat, // (milibar - 1000)
val spawnX: Int,
val spawnY: Int,
//val wirings: HashMap<BlockAddress, SortedArrayList<GameWorld.WiringNode>>,
val wallDamages: HashMap<BlockAddress, Float>,
val terrainDamages: HashMap<BlockAddress, Float>,
val fluidTypes: HashMap<BlockAddress, FluidType>,
val fluidFills: HashMap<BlockAddress, Float>,
val tileNumberToNameMap: HashMap<Int, ItemID>
)
internal fun InputStream.readRelative(b: ByteArray, off: Int, len: Int): Int {
if (b == null) {
throw NullPointerException()
} else if (off < 0 || len < 0 || len > b.size) {
throw IndexOutOfBoundsException()
} else if (len == 0) {
return 0
}
var c = read()
if (c == -1) {
return -1
}
b[0] = c.toByte()
var i = 1
try {
while (i < len) {
c = read()
if (c == -1) {
break
}
b[i] = c.toByte()
i++
}
} catch (ee: IOException) {
}
return i
}
}

View File

@@ -1,84 +0,0 @@
package net.torvald.terrarum.serialise
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.DiskSkimmer.Companion.read
import java.io.File
import java.io.FileInputStream
import java.util.*
object ReadWorldInfo {
// FIXME UNTESTED
internal operator fun invoke(file: File): SaveMetaData {
val magic = ByteArray(4)
val worldNameUTF8 = ArrayList<Byte>()
val fis = FileInputStream(file)
fis.read(magic)
if (!Arrays.equals(magic, WriteWorldInfo.META_MAGIC)) {
throw IllegalArgumentException("File not a Save Meta")
}
val descVersion = fis.read(1) // 0-127
val numberOfHashes = fis.read() // 0-127
var byteRead = fis.read()
while (byteRead != 0) {
if (byteRead == -1)
throw InternalError("Unexpected EOF")
worldNameUTF8.add(byteRead.toByte())
byteRead = fis.read()
}
return SaveMetaData(
String(worldNameUTF8.toByteArray(), Charsets.UTF_8),
fis.read(8).toLittleLong(), // terrain seed
fis.read(8).toLittleLong(), // rng s0
fis.read(8).toLittleLong(), // rng s1
fis.read(8).toLittleLong(), // weather s0
fis.read(8).toLittleLong(), // weather s1
fis.read(4).toLittleInt(), // player id
fis.read(8).toLittleLong(), // world TIME_T
fis.read(6).toLittleLong(), // creation time
fis.read(6).toLittleLong(), // last play time
fis.read(4).toLittleInt(), // total time wasted
fis.read(32), // sha256sum worldinfo1
fis.read(32), // sha256sum worldinfo2
fis.read(32) // sha256sum worldinfo3
)
}
data class SaveMetaData(
val worldName: String,
val terrainSeed: Long,
val rngS0: Long,
val rngS1: Long,
val weatherS0: Long,
val weatherS1: Long,
val playerID: Int,
val timeNow: Long,
val creationTime: Long,
val lastPlayTime: Long,
val totalPlayTime: Int,
val worldinfo1Hash: ByteArray,
val worldInfo2Hash: ByteArray,
val worldInfo3Hash: ByteArray,
// gzipped TGA in meta
val thumbnail: Texture = Texture(2, 2, Pixmap.Format.RGBA8888),
// skim through the virtualdisk entries
val worldCount: Int = 1,
// read from the entry file
val playerName: String = "Savegame",
val playerWallet: Int = 0
)
}

View File

@@ -3,7 +3,7 @@ package net.torvald.terrarum.serialise
/**
* Created by minjaesong on 2018-10-03.
*/
object SavegameLoader {
object SavegameReader {
// TODO read TEVd, load necessary shits

View File

@@ -9,7 +9,6 @@ import net.torvald.terrarum.gameactors.Actor
import net.torvald.terrarum.gameitem.GameItem
import net.torvald.terrarum.itemproperties.ItemCodex
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.*
import net.torvald.terrarum.utils.JsonWriter.getJsonBuilder
import net.torvald.util.SortedArrayList
import java.io.File
import java.nio.charset.Charset
@@ -77,76 +76,17 @@ object SavegameWriter {
// serialise current world (stage)
val worldBytes = WriteLayerDataZip(gameworld) // filename can be anything that is "world[n]" where [n] is any number
if (worldBytes == null) {
throw Error("Serialising world failed")
}
if (!worldBytes.sliceArray(0..3).contentEquals(WriteLayerDataZip.MAGIC)) {
worldBytes.forEach {
print(it.toUInt().and(255u).toString(16).toUpperCase().padStart(2, '0'))
print(' ')
}; println()
throw Error()
}
// add current world (stage) to the disk
VDUtil.registerFile(disk, DiskEntry(
gameworld.worldIndex, ROOT,
"world${gameworld.worldIndex}".toByteArray(charset),
creationDate, creationDate,
EntryFile(worldBytes)
))
// TODO world[n] is done, needs whole other things
// worldinfo0..3
val worldinfoBytes = WriteWorldInfo(ingame)
worldinfoBytes?.forEachIndexed { index, bytes ->
VDUtil.registerFile(disk, DiskEntry(
32766 - index, ROOT, "worldinfo$index".toByteArray(charset),
creationDate, creationDate,
EntryFile(bytes)
))
} ?: throw Error("Serialising worldinfo failed")
// loadorder.txt
VDUtil.registerFile(disk, DiskEntry(
32767, ROOT, "load_order.txt".toByteArray(charset),
creationDate, creationDate,
EntryFile(ByteArray64.fromByteArray(Gdx.files.internal("./assets/mods/LoadOrder.csv").readBytes()))
))
// actors
ingame.actorContainerActive.forEach {
VDUtil.registerFile(disk, DiskEntry(
rngPool.next(), ROOT,
it.referenceID.toString(16).toUpperCase().toByteArray(charset),
creationDate, creationDate,
EntryFile(serialiseActor(it))
))
}
ingame.actorContainerInactive.forEach {
VDUtil.registerFile(disk, DiskEntry(
rngPool.next(), ROOT,
it.referenceID.toString(16).toUpperCase().toByteArray(charset),
creationDate, creationDate,
EntryFile(serialiseActor(it))
))
}
// items
ItemCodex.dynamicItemDescription.forEach { dynamicID, item ->
/*ItemCodex.dynamicItemDescription.forEach { dynamicID, item ->
VDUtil.registerFile(disk, DiskEntry(
rngPool.next(), ROOT,
dynamicID.toByteArray(charset),
creationDate, creationDate,
EntryFile(serialiseItem(item))
))
}
}*/
System.gc()
@@ -156,14 +96,79 @@ object SavegameWriter {
fun modifyExistingSave(savefile: File): VirtualDisk {
TODO()
}
}
private fun serialiseActor(a: Actor): ByteArray64 {
val gson = getJsonBuilder().toJson(a).toByteArray(charset)
return ByteArray64.fromByteArray(gson)
}
fun Int.toLittle() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte(),
this.ushr(16).and(0xFF).toByte(),
this.ushr(24).and(0xFF).toByte()
)
/** Converts int as 2-byte array, discarding the sign.*/
fun Int.toULittleShort() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte()
)
/** Converts int as 2-byte array, preserving the sign. In other words, it converts int to short. */
fun Int.toLittleShort() = byteArrayOf(
this.and(0xFF).toByte(),
this.shr(8).and(0xFF).toByte()
)
fun Long.toLittle() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte(),
this.ushr(16).and(0xFF).toByte(),
this.ushr(24).and(0xFF).toByte(),
this.ushr(32).and(0xFF).toByte(),
this.ushr(40).and(0xFF).toByte(),
this.ushr(48).and(0xFF).toByte(),
this.ushr(56).and(0xFF).toByte()
)
/** Converts long as 6-byte array, discarding the sign. */
fun Long.toULittle48() = byteArrayOf(
this.and(0xFF).toByte(),
this.ushr(8).and(0xFF).toByte(),
this.ushr(16).and(0xFF).toByte(),
this.ushr(24).and(0xFF).toByte(),
this.ushr(32).and(0xFF).toByte(),
this.ushr(40).and(0xFF).toByte()
)
fun Double.toLittle() = java.lang.Double.doubleToRawLongBits(this).toLittle()
fun Boolean.toLittle() = byteArrayOf(if (this) 0xFF.toByte() else 0.toByte())
private fun serialiseItem(i: GameItem): ByteArray64 {
val gson = getJsonBuilder().toJson(i).toByteArray(charset)
return ByteArray64.fromByteArray(gson)
}
}
fun ByteArray.toLittleInt() =
if (this.size != 4) throw Error("Array not in size of 4")
else this[0].toUint() or
this[1].toUint().shl(8) or
this[2].toUint().shl(16) or
this[3].toUint().shl(24)
fun ByteArray.toULittleShort() =
if (this.size != 4) throw Error("Array not in size of 2")
else this[0].toUint() or
this[1].toUint().shl(8)
fun ByteArray.toLittleShort() =
if (this.size != 4) throw Error("Array not in size of 2")
else this[0].toUint() or
this[1].toInt().shl(8)
fun ByteArray.toLittleLong() =
if (this.size != 8) throw Error("Array not in size of 8")
else this[0].toUlong() or
this[1].toUlong().shl(8) or
this[2].toUlong().shl(16) or
this[3].toUlong().shl(24) or
this[4].toUlong().shl(32) or
this[5].toUlong().shl(40) or
this[6].toUlong().shl(48) or
this[7].toUlong().shl(56)
fun ByteArray.toLittleInt48() =
if (this.size != 6) throw Error("Array not in size of 6")
else this[0].toUlong() or
this[1].toUlong().shl(8) or
this[2].toUlong().shl(16) or
this[3].toUlong().shl(24) or
this[4].toUlong().shl(32) or
this[5].toUlong().shl(40)
fun ByteArray.toLittleFloat() = java.lang.Float.intBitsToFloat(this.toLittleInt())
fun Byte.toUlong() = java.lang.Byte.toUnsignedLong(this)
fun Byte.toUint() = java.lang.Byte.toUnsignedInt(this)

View File

@@ -1,61 +0,0 @@
package net.torvald.terrarum.serialise
import net.torvald.terrarum.AppLoader
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
/**
* Created by minjaesong on 2016-03-18.
*/
// internal for everything: prevent malicious module from messing up the savedata
internal object WriteCSV {
val META_FILENAME_TILE = "worldinfo2"
val META_FILENAME_ITEM = "worldinfo3"
val META_FILENAME_MAT = "worldinfo4"
internal fun write(saveDirectoryName: String): Boolean {
//val tileCSV = CSVFetcher.readCSVasString(BlockCodex.CSV_PATH)
//val itemCSV = CSVFetcher.readCSVasString(ItemCodex.CSV_PATH)
//val matCSV = CSVFetcher.readCSVasString(MaterialCodex.CSV_PATH)
val pathTile = Paths.get("${AppLoader.defaultSaveDir}" +
"/$saveDirectoryName/${META_FILENAME_TILE}")
val pathItem = Paths.get("${AppLoader.defaultSaveDir}" +
"/$saveDirectoryName/${META_FILENAME_ITEM}")
val pathMat = Paths.get("${AppLoader.defaultSaveDir}" +
"/$saveDirectoryName/${META_FILENAME_MAT}")
val tempPathTile = Files.createTempFile(pathTile.toString(), "_temp")
val tempPathItem = Files.createTempFile(pathItem.toString(), "_temp")
val tempPathMat = Files.createTempFile(pathMat.toString(), "_temp")
// TODO gzip
// write CSV to path
//Files.write(tempPathTile, tileCSV.toByteArray(Charsets.UTF_8))
//Files.write(tempPathItem, itemCSV.toByteArray(Charsets.UTF_8))
//Files.write(tempPathMat, matCSV.toByteArray(Charsets.UTF_8))
// replace savemeta with tempfile
try {
Files.copy(tempPathTile, pathTile, StandardCopyOption.REPLACE_EXISTING)
Files.deleteIfExists(tempPathTile)
Files.copy(tempPathItem, pathItem, StandardCopyOption.REPLACE_EXISTING)
Files.deleteIfExists(tempPathItem)
Files.copy(tempPathMat, pathMat, StandardCopyOption.REPLACE_EXISTING)
Files.deleteIfExists(tempPathMat)
println("Saved map data '${WriteLayerData.LAYERS_FILENAME}' to $saveDirectoryName.")
return true
}
catch (e: IOException) {
e.printStackTrace()
}
return false
}
}

View File

@@ -1,85 +0,0 @@
package net.torvald.terrarum.serialise
/**
* TODO this one does not use TerranVirtualDisk
*
* Created by minjaesong on 2016-03-18.
*/
// internal for everything: prevent malicious module from messing up the savedata
@Deprecated("TEMD is old and removed format; use TEMz which does compression")
internal object WriteLayerData {
init {
throw Error("TEMD is old and removed format; use TEMz which does compression")
}
val LAYERS_FILENAME = "worldinfo1"
/*val MAGIC = "TEMD".toByteArray(charset = Charset.forName("US-ASCII"))
val BYTE_NULL: Byte = 0
internal operator fun invoke(saveDirectoryName: String): Boolean {
val path = "${AppLoader.defaultSaveDir}/$saveDirectoryName/${LAYERS_FILENAME}"
val tempPath = "${path}_bak"
val map = (Terrarum.ingame!!.world)
val parentDir = File("${AppLoader.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()
val outputStream = GZIPOutputStream(FileOutputStream(tempFile))
// write binary
outputStream.write(MAGIC)
outputStream.write(byteArrayOf(GameWorld.SIZEOF))
outputStream.write(byteArrayOf(GameWorld.LAYERS))
outputStream.write(byteArrayOf(BYTE_NULL))
outputStream.write(byteArrayOf(BYTE_NULL))
outputStream.write(map.width.toLittle())
outputStream.write(map.height.toLittle())
outputStream.write(map.spawnX.toLittle())
outputStream.write(map.spawnY.toLittle())
// write one row (byteArray) at a time
outputStream.write(map.layerTerrain.data)
outputStream.write(map.layerWall.data)
outputStream.write(map.layerTerrainLowBits.data)
outputStream.write(map.layerWallLowBits.data)
// replace savemeta with tempfile
try {
outputStream.flush()
outputStream.close()
outFile.delete()
tempFile.copyTo(outFile, overwrite = true)
tempFile.delete()
println("Saved map data '$LAYERS_FILENAME' to $saveDirectoryName.")
return true
}
catch (e: IOException) {
e.printStackTrace()
}
finally {
outputStream.close()
}
return false
}*/
}
const val WORLD_WRITER_FORMAT_VERSION = 3

View File

@@ -1,208 +0,0 @@
package net.torvald.terrarum.serialise
import com.badlogic.gdx.utils.compression.Lzma
import net.torvald.terrarum.AppLoader
import net.torvald.terrarum.Terrarum
import net.torvald.terrarum.realestate.LandUtil
import net.torvald.terrarum.serialise.WriteLayerDataZip.FILE_FOOTER
import net.torvald.terrarum.serialise.WriteLayerDataZip.PAYLOAD_FOOTER
import net.torvald.terrarum.serialise.WriteLayerDataZip.PAYLOAD_HEADER
import java.io.*
import java.util.zip.DeflaterOutputStream
/**
* 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.
*/
// internal for everything: prevent malicious module from messing up the savedata
internal object WriteLayerDataLzma {
// FIXME TERRAIN DAMAGE UNTESTED
// 2400x800 world size: about .. kB
// 8192x2048 world size: about 470 kB but writes much slower than DEFLATE
val LAYERS_FILENAME = "world"
val MAGIC = byteArrayOf(0x54, 0x45, 0x4D, 0x7A)
val VERSION_NUMBER = WORLD_WRITER_FORMAT_VERSION.toByte()
val NUMBER_OF_LAYERS = 3.toByte()
val NUMBER_OF_PAYLOADS = 5.toByte()
val COMPRESSION_ALGORITHM = 2.toByte()
val GENERATOR_VERSION = WORLD_GENERATOR_VERSION.toULittleShort()
//val NULL: Byte = 0
/**
* 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 path = "${AppLoader.defaultSaveDir}/tmp_$LAYERS_FILENAME${world.worldIndex}"
// TODO let's try dump-on-the-disk-then-pack method...
/*val parentDir = File("${AppLoader.defaultSaveDir}/$saveDirectoryName")
if (!parentDir.exists()) {
parentDir.mkdir()
}
else if (!parentDir.isDirectory) {
EchoError("Savegame directory is not actually a directory, aborting...")
return false
}*/
val outFile = File(path)
if (outFile.exists()) outFile.delete()
outFile.createNewFile()
val outputStream = BufferedOutputStream(FileOutputStream(outFile), 8192)
var deflater: DeflaterOutputStream // couldn't really use one outputstream for all the files.
fun wb(byteArray: ByteArray) { outputStream.write(byteArray) }
fun wb(byte: Byte) { outputStream.write(byte.toInt()) }
//fun wb(byte: Int) { outputStream.write(byte) }
fun wi32(int: Int) { wb(int.toLittle()) }
fun wi48(long: Long) { wb(long.toULittle48()) }
fun wi64(long: Long) { wb(long.toLittle()) }
fun wf32(float: Float) { wi32(float.toRawBits()) }
////////////////////
// WRITE BINARIES //
////////////////////
// all the necessary headers
wb(MAGIC); wb(VERSION_NUMBER); wb(NUMBER_OF_LAYERS); wb(NUMBER_OF_PAYLOADS); wb(COMPRESSION_ALGORITHM); wb(GENERATOR_VERSION)
// world width, height, and spawn point
wi32(world.width); wi32(world.height)
wi48(LandUtil.getBlockAddr(world, world.spawnX, world.spawnY))
// write payloads //
outputStream.flush()
// TERR payload
// PRO Debug tip: every deflated bytes must begin with 0x789C or 0x78DA
// Thus, \0pLd + [10] must be either of these.
wb(PAYLOAD_HEADER); wb("TERR".toByteArray())
wi48(world.width * world.height * 3L / 2)
world.layerTerrain.bytesIterator().forEach {
val tempByteArray = ByteArray(1)
tempByteArray[0] = it
val tempByteArrayStream = ByteArrayInputStream(tempByteArray)
Lzma.compress(tempByteArrayStream, outputStream)
}
wb(PAYLOAD_FOOTER)
// WALL payload
wb(PAYLOAD_HEADER); wb("WALL".toByteArray())
wi48(world.width * world.height * 3L / 2)
world.layerWall.bytesIterator().forEach {
val tempByteArray = ByteArray(1)
tempByteArray[0] = it
val tempByteArrayStream = ByteArrayInputStream(tempByteArray)
Lzma.compress(tempByteArrayStream, outputStream)
}
wb(PAYLOAD_FOOTER)
// WIRE payload
/*wb(PAYLOAD_HEADER); wb("WIRE".toByteArray())
wi48(world.width * world.height.toLong())
Lzma.compress(ByteArrayInputStream(world.wireArray), outputStream)
wb(PAYLOAD_FOOTER)*/
// TdMG payload
wb(PAYLOAD_HEADER); wb("TdMG".toByteArray())
wi48(world.terrainDamages.size * 10L)
world.terrainDamages.forEach { t, u ->
Lzma.compress(ByteArrayInputStream(t.toULittle48()), outputStream)
Lzma.compress(ByteArrayInputStream(u.toRawBits().toLittle()), outputStream)
}
wb(PAYLOAD_FOOTER)
// WdMG payload
wb(PAYLOAD_HEADER); wb("WdMG".toByteArray())
wi48(world.wallDamages.size * 10L)
world.wallDamages.forEach { t, u ->
Lzma.compress(ByteArrayInputStream(t.toULittle48()), outputStream)
Lzma.compress(ByteArrayInputStream(u.toRawBits().toLittle()), outputStream)
}
wb(PAYLOAD_FOOTER)
// FlTP payload
wb(PAYLOAD_HEADER); wb("FlTP".toByteArray())
wi48(world.fluidTypes.size * 8L)
world.fluidTypes.forEach { t, u ->
Lzma.compress(ByteArrayInputStream(t.toULittle48()), outputStream)
Lzma.compress(ByteArrayInputStream(u.value.toLittleShort()), outputStream)
}
wb(PAYLOAD_FOOTER)
// FlFL payload
wb(PAYLOAD_HEADER); wb("FlFL".toByteArray())
wi48(world.fluidFills.size * 10L)
world.fluidFills.forEach { t, u ->
Lzma.compress(ByteArrayInputStream(t.toULittle48()), outputStream)
Lzma.compress(ByteArrayInputStream(u.toRawBits().toLittle()), outputStream)
}
wb(PAYLOAD_FOOTER)
// write footer
wb(FILE_FOOTER)
//////////////////
// END OF WRITE //
//////////////////
// replace savemeta with tempfile
try {
outputStream.flush()
outputStream.close()
return outFile
}
catch (e: IOException) {
e.printStackTrace()
}
finally {
outputStream.close()
}
return null
}
}

View File

@@ -1,208 +0,0 @@
package net.torvald.terrarum.serialise
import net.torvald.terrarum.AppLoader.printdbg
import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.ByteArray64
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.ByteArray64GrowableOutputStream
import net.torvald.terrarum.realestate.LandUtil
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
/**
* 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.
*/
// internal for everything: prevent malicious module from messing up the savedata
internal object WriteLayerDataZip {
// FIXME TERRAIN DAMAGE UNTESTED
// 2400x800 world size, default comp level: about 90 kB
// 8192x2048 world size, default comp level: about 670 kB
// 2400x800 world size, best comp level: about 75 kB
// 8192x2048 world size, best comp level: about 555 kB
val LAYERS_FILENAME = "world"
val MAGIC = byteArrayOf(0x54, 0x45, 0x4D, 0x7A)
val VERSION_NUMBER = WORLD_WRITER_FORMAT_VERSION.toByte()
val NUMBER_OF_LAYERS = 3.toByte()
val NUMBER_OF_PAYLOADS = 5.toByte()
val COMPRESSION_ALGORITHM = 1.toByte()
val GENERATOR_VERSION = WORLD_GENERATOR_VERSION.toULittleShort()
val PAYLOAD_HEADER = byteArrayOf(0, 0x70, 0x4C, 0x64) // \0pLd
val PAYLOAD_FOOTER = byteArrayOf(0x45, 0x6E, 0x64, 0x50, 0x59, 0x4C, 0x64, -1) // EndPYLd\xFF
val FILE_FOOTER = byteArrayOf(0x45, 0x6E, 0x64, 0x54, 0x45, 0x4D, -1, -2) // EndTEM with BOM
//val NULL: Byte = 0
/**
* @param world The world to serialise
* @param path The directory where the temporary file goes, in relative to the AppLoader.defaultSaveDir. Should NOT start with slashed
*
* @return File on success; `null` on failure
*/
internal operator fun invoke(world: GameWorld): ByteArray64? {
//val sanitisedPath = path.replace('\\', '/').removePrefix("/").replace("../", "")
//val path = "${AppLoader.defaultSaveDir}/$sanitisedPath/tmp_$LAYERS_FILENAME${world.worldIndex}"
//val path = "${AppLoader.defaultSaveDir}/tmp_$LAYERS_FILENAME${world.worldIndex}"
// TODO let's try dump-on-the-disk-then-pack method...
/*val parentDir = File("${AppLoader.defaultSaveDir}/$saveDirectoryName")
if (!parentDir.exists()) {
parentDir.mkdir()
}
else if (!parentDir.isDirectory) {
EchoError("Savegame directory is not actually a directory, aborting...")
return false
}*/
//val outFile = File(path)
//if (outFile.exists()) outFile.delete()
//outFile.createNewFile()
//val outputStream = BufferedOutputStream(FileOutputStream(outFile), 8192)
val outputStream = ByteArray64GrowableOutputStream()
var deflater: DeflaterOutputStream // couldn't really use one outputstream for all the files.
fun wb(byteArray: ByteArray) { outputStream.write(byteArray) }
fun wb(byte: Byte) { outputStream.write(byte.toInt()) }
//fun wb(byte: Int) { outputStream.write(byte) }
fun wi32(int: Int) { wb(int.toLittle()) }
fun wi48(long: Long) { wb(long.toULittle48()) }
fun wi64(long: Long) { wb(long.toLittle()) }
fun wf32(float: Float) { wi32(float.toRawBits()) }
////////////////////
// WRITE BINARIES //
////////////////////
// all the necessary headers
wb(MAGIC); wb(VERSION_NUMBER); wb(NUMBER_OF_LAYERS); wb(NUMBER_OF_PAYLOADS); wb(COMPRESSION_ALGORITHM); wb(GENERATOR_VERSION)
// world width, height, and spawn point
wi32(world.width); wi32(world.height)
wi48(LandUtil.getBlockAddr(world, world.spawnX, world.spawnY))
// write payloads //
// TERR payload
// PRO Debug tip: every deflated bytes must begin with 0x789C or 0x78DA
// Thus, \0pLd + [10] must be either of these.
// TODO serialised payloads have bit too much zeros, should I be worried?
val savePayloads = world.javaClass.superclass.declaredFields//.filter { it.isAnnotationPresent(TEMzPayload::class.java) }
printdbg(this, "")
savePayloads.forEach { printdbg(this, "${it.name}: ${it.type} @${it.declaredAnnotations.size}") }
wb(PAYLOAD_HEADER); wb("TERR".toByteArray())
wi48(world.width * world.height * 3L / 2)
deflater = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), false)
world.layerTerrain.bytesIterator().forEach { deflater.write(it.toInt()) }
deflater.flush(); deflater.finish()
wb(PAYLOAD_FOOTER)
// WALL payload
wb(PAYLOAD_HEADER); wb("WALL".toByteArray())
wi48(world.width * world.height * 3L / 2)
deflater = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), false)
world.layerWall.bytesIterator().forEach { deflater.write(it.toInt()) }
deflater.flush(); deflater.finish()
wb(PAYLOAD_FOOTER)
// WIRE payload
/*wb(PAYLOAD_HEADER); wb("WIRE".toByteArray())
wi48(world.width * world.height.toLong())
deflater = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), false)
deflater.write(world.wireArray)
deflater.flush(); deflater.finish()
wb(PAYLOAD_FOOTER)*/
// TdMG payload
wb(PAYLOAD_HEADER); wb("TdMG".toByteArray())
wi48(world.terrainDamages.size * 10L)
deflater = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), false)
world.terrainDamages.forEach { t, u ->
deflater.write(t.toULittle48())
deflater.write(u.toRawBits().toLittle())
}
deflater.flush(); deflater.finish()
wb(PAYLOAD_FOOTER)
// WdMG payload
wb(PAYLOAD_HEADER); wb("WdMG".toByteArray())
wi48(world.wallDamages.size * 10L)
deflater = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), false)
world.wallDamages.forEach { t, u ->
deflater.write(t.toULittle48())
deflater.write(u.toRawBits().toLittle())
}
deflater.flush(); deflater.finish()
wb(PAYLOAD_FOOTER)
// FlTP payload
wb(PAYLOAD_HEADER); wb("FlTP".toByteArray())
wi48(world.fluidTypes.size * 8L)
deflater = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), false)
world.fluidTypes.forEach { t, u ->
deflater.write(t.toULittle48())
deflater.write(u.value.toLittleShort())
}
deflater.flush(); deflater.finish()
wb(PAYLOAD_FOOTER)
// FlFL payload
wb(PAYLOAD_HEADER); wb("FlFL".toByteArray())
wi48(world.fluidFills.size * 10L)
deflater = DeflaterOutputStream(outputStream, Deflater(Deflater.BEST_COMPRESSION, true), false)
world.fluidFills.forEach { t, u ->
deflater.write(t.toULittle48())
deflater.write(u.toRawBits().toLittle())
}
deflater.flush(); deflater.finish()
wb(PAYLOAD_FOOTER)
// write footer
wb(FILE_FOOTER)
//////////////////
// END OF WRITE //
//////////////////
outputStream.flush()
outputStream.close()
return outputStream.toByteArray64()
}
}

View File

@@ -1,70 +1,73 @@
package net.torvald.terrarum.serialise
import net.torvald.terrarum.modulebasegame.worldgenerator.Worldgen
import java.nio.charset.Charset
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonWriter
import net.torvald.terrarum.ModMgr
import net.torvald.terrarum.blockproperties.BlockCodex
import net.torvald.terrarum.blockproperties.WireCodex
import net.torvald.terrarum.itemproperties.ItemCodex
import net.torvald.terrarum.itemproperties.MaterialCodex
import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.modulebasegame.worldgenerator.RoguelikeRandomiser
import net.torvald.terrarum.weather.WeatherMixer
/**
* Created by minjaesong on 2016-03-15.
*/
// internal for everything: prevent malicious module from messing up the savedata
internal object WriteMeta {
open class WriteMeta(val ingame: TerrarumIngame) {
val META_FILENAME = "worldinfo0"
open fun invoke(): String {
val world = ingame.world
val props = hashMapOf<String, Any>(
"genver" to 4,
"savename" to world.worldName,
"terrseed" to world.generatorSeed,
"randseed0" to RoguelikeRandomiser.RNG.state0,
"randseed1" to RoguelikeRandomiser.RNG.state1,
"weatseed0" to WeatherMixer.RNG.state0,
"weatseed1" to WeatherMixer.RNG.state1,
"playerid" to ingame.theRealGamer.referenceID,
"creation_t" to world.creationTime,
"lastplay_t" to world.lastPlayTime,
"playtime_t" to world.totalPlayTime,
val MAGIC = "TESV".toByteArray(charset = Charset.forName("US-ASCII"))
// CSVs
"blocks" to StringBuilder().let {
ModMgr.getFilesFromEveryMod("blocks/blocks.csv").forEach { (modname, file) ->
it.append("\n\n## module: $modname ##\n\n")
it.append(file.readText())
}
it.toString()
},
"items" to StringBuilder().let {
ModMgr.getFilesFromEveryMod("items/itemid.csv").forEach { (modname, file) ->
it.append("\n\n## module: $modname ##\n\n")
it.append(file.readText())
}
it.toString()
},
val BYTE_NULL: Byte = 0
"wires" to StringBuilder().let {
ModMgr.getFilesFromEveryMod("wires/wires.csv").forEach { (modname, file) ->
it.append("\n\n## module: $modname ##\n\n")
it.append(file.readText())
}
it.toString()
},
val terraseed: Long = Worldgen.params.seed
// TODO fluids
"materials" to StringBuilder().let {
ModMgr.getFilesFromEveryMod("materials/materials.csv").forEach { (modname, file) ->
it.append("\n\n## module: $modname ##\n\n")
it.append(file.readText())
}
it.toString()
},
/**
* Write save meta to specified directory. Returns false if something went wrong.
* @param saveDirectoryName
* @param savegameName -- Nullable. If the value is not specified, saveDirectoryName will be used instead.
*/
internal fun write(saveDirectoryName: String, savegameName: String?): Boolean {
/*val hashArray: ArrayList<ByteArray> = ArrayList()
val savenameAsByteArray: ByteArray =
(savegameName ?: saveDirectoryName).toByteArray(Charsets.UTF_8)
// define Strings to be hashed
val props = arrayOf(
TilePropCSV()
//, (item, mat, ...)
"loadorder" to ModMgr.loadOrder,
"worlds" to ingame.gameworldIndices
)
// get and store hash from the list
props.map { hashArray.add(DigestUtils.sha256(it)) }
// open file and delete it
val metaPath = Paths.get("$AppLoader.defaultSaveDir" +
"/$saveDirectoryName/$META_FILENAME")
val metaTempPath = Files.createTempFile(metaPath.toString(), "_temp")
// TODO gzip
// write bytes in tempfile
Files.write(metaTempPath, MAGIC)
Files.write(metaTempPath, savenameAsByteArray)
Files.write(metaTempPath, byteArrayOf(BYTE_NULL))
Files.write(metaTempPath, toByteArray(terraseed))
Files.write(metaTempPath, toByteArray(rogueseed))
for (hash in hashArray)
Files.write(metaTempPath, hash)
// replace savemeta with tempfile
try {
Files.copy(metaTempPath, metaPath, StandardCopyOption.REPLACE_EXISTING)
Files.deleteIfExists(metaTempPath)
println("Saved metadata to $saveDirectoryName.")
return true
}
catch (e: IOException) {
e.printStackTrace()
}*/
return false
return Json(JsonWriter.OutputType.json).toJson(props)
}
}

View File

@@ -1,185 +0,0 @@
package net.torvald.terrarum.serialise
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.GL30
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.glutils.FrameBuffer
import com.badlogic.gdx.utils.ScreenUtils
import net.torvald.gdx.graphics.PixmapIO2
import net.torvald.terrarum.*
import net.torvald.terrarum.modulebasegame.gameworld.GameWorldExtension
import net.torvald.terrarum.weather.WeatherMixer
import net.torvald.terrarum.modulebasegame.worldgenerator.RoguelikeRandomiser
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.ByteArray64
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.ByteArray64GrowableOutputStream
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.ByteArray64InputStream
import org.apache.commons.codec.digest.DigestUtils
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
object WriteWorldInfo {
val META_MAGIC = "TESV".toByteArray(Charsets.UTF_8)
val NULL = 0.toByte()
val VERSION = 1
val HASHED_FILES_COUNT = 3
/**
* TODO currently it'll dump the temporary file (tmp_worldinfo1) onto the disk and will return the temp file.
*
* @return List of ByteArray64, worldinfo0..worldinfo3; `null` on failure
*/
internal operator fun invoke(ingame: IngameInstance): List<ByteArray64>? {
//val path = "${AppLoader.defaultSaveDir}/tmp_worldinfo"
val infileList = arrayOf(
ModMgr.getGdxFilesFromEveryMod("blocks/blocks.csv"),
ModMgr.getGdxFilesFromEveryMod("items/items.csv"),
ModMgr.getGdxFilesFromEveryMod("materials/materials.csv")
)
val outFiles = ArrayList<ByteArray64>() // for worldinfo1-3 only
val worldInfoHash = ArrayList<ByteArray>() // hash of worldinfo1-3
// try to write worldinfo1-3
for (filenum in 1..HASHED_FILES_COUNT) {
val outputStream = ByteArray64GrowableOutputStream()
val infile = infileList[filenum - 1]
infile.forEach {
outputStream.write("## from file: ${it.second.nameWithoutExtension()} ##############################\n".toByteArray())
val readBytes = it.second.readBytes()
outputStream.write(readBytes)
outputStream.write("\n".toByteArray())
}
outputStream.flush()
outputStream.close()
outFiles.add(outputStream.toByteArray64())
worldInfoHash.add(DigestUtils.sha256(ByteArray64InputStream(outputStream.toByteArray64())))
}
// compose save meta (actual writing part)
val metaOut = ByteArray64GrowableOutputStream()
metaOut.write(META_MAGIC)
metaOut.write(VERSION)
metaOut.write(HASHED_FILES_COUNT)
// world name
val world = ingame.world
val worldNameBytes = world.worldName.toByteArray(Charsets.UTF_8)
//metaOut.write(worldNameBytes)
worldNameBytes.forEach {
if (it != 0.toByte()) metaOut.write(it.toInt())
}
metaOut.write(NULL.toInt())
// terrain seed
metaOut.write(world.generatorSeed.toLittle())
// randomiser seed
metaOut.write(RoguelikeRandomiser.RNG.state0.toLittle())
metaOut.write(RoguelikeRandomiser.RNG.state1.toLittle())
// weather seed
metaOut.write(WeatherMixer.RNG.state0.toLittle())
metaOut.write(WeatherMixer.RNG.state1.toLittle())
// reference ID of the player
metaOut.write(Terrarum.PLAYER_REF_ID.toLittle())
// ingame time_t
metaOut.write((world as GameWorldExtension).worldTime.TIME_T.toLittle())
// creation time (real world time)
metaOut.write(world.creationTime.toULittle48())
// time at save (real world time)
val timeNow = System.currentTimeMillis() / 1000L
metaOut.write(timeNow.toULittle48())
// get playtime and save it
val timeToAdd = (timeNow - world.loadTime).toInt()
metaOut.write((world.totalPlayTime + timeToAdd).toLittle())
world.lastPlayTime = timeNow
world.totalPlayTime += timeToAdd
// SHA256SUM of worldinfo1-3
worldInfoHash.forEach {
metaOut.write(it)
}
// thumbnail
val texreg = ingame.actorGamer.sprite?.textureRegion
if (texreg != null) {
val batch = SpriteBatch()
val camera = OrthographicCamera(texreg.tileW.toFloat(), texreg.tileH.toFloat())
val fbo = FrameBuffer(Pixmap.Format.RGBA8888, texreg.tileW, texreg.tileH, false)
fbo.inAction(camera, batch) {
batch.inUse {
batch.draw(texreg.get(0, 0), 0f, 0f)
}
}
// bind and unbind the fbo so that I can get the damned Pixmap using ScreenUtils
// NullPointerException if not appconfig.useGL30
Gdx.gl30.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, fbo.framebufferHandle)
Gdx.gl30.glReadBuffer(GL30.GL_COLOR_ATTACHMENT0)
val outpixmap = ScreenUtils.getFrameBufferPixmap(0, 0, fbo.width, fbo.height)
Gdx.gl30.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, 0)
Gdx.gl30.glReadBuffer(GL30.GL_BACK)
val tgaSize = PixmapIO2.HEADER_FOOTER_SIZE + outpixmap.width * outpixmap.height * 4
val byteArrayOS = ByteArrayOutputStream(tgaSize)
PixmapIO2._writeTGA(byteArrayOS, outpixmap, true, true)
byteArrayOS.flush()
byteArrayOS.close()
//PixmapIO2.writeTGA(Gdx.files.absolute(AppLoader.defaultDir+"/tmp_writeworldinfo+outpixmap.tga"), outpixmap, true)
outpixmap.dispose()
batch.dispose()
fbo.dispose()
// write uncompressed size
metaOut.write(tgaSize.toULittleShort())
// write compressed tga
val deflater = DeflaterOutputStream(metaOut, Deflater(Deflater.BEST_COMPRESSION, true), false)
deflater.write(byteArrayOS.toByteArray())
deflater.flush(); deflater.finish()
// write footer
metaOut.write(-1); metaOut.write(-2)
}
// more data goes here //
metaOut.flush()
metaOut.close()
return listOf(metaOut.toByteArray64()) + outFiles.toList()
}
}