From 4658fa2aed3d8bf6c732080c90b71a8a8a169ecc Mon Sep 17 00:00:00 2001 From: Minjae Song Date: Mon, 8 Oct 2018 01:16:29 +0900 Subject: [PATCH] worldinfo writer --- src/net/torvald/random/HQRNG.kt | 4 +- src/net/torvald/terrarum/ModMgr.kt | 36 +++++ .../terrarum/blockproperties/BlockCodex.kt | 3 + .../torvald/terrarum/gameworld/GameWorld.kt | 19 ++- .../terrarum/serialise/WriteWorldInfo.kt | 126 ++++++++++++++++++ work_files/DataFormats/Savegame metadata.txt | 3 +- 6 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 src/net/torvald/terrarum/serialise/WriteWorldInfo.kt diff --git a/src/net/torvald/random/HQRNG.kt b/src/net/torvald/random/HQRNG.kt index 9304e5c34..a43398af3 100644 --- a/src/net/torvald/random/HQRNG.kt +++ b/src/net/torvald/random/HQRNG.kt @@ -10,8 +10,8 @@ import java.util.Random */ class HQRNG @JvmOverloads constructor(seed: Long = System.nanoTime()) : Random() { - private var s0: Long - private var s1: Long + var s0: Long; private set + var s1: Long; private set constructor(s0: Long, s1: Long) : this() { this.s0 = s0 diff --git a/src/net/torvald/terrarum/ModMgr.kt b/src/net/torvald/terrarum/ModMgr.kt index e23d5b492..ab9fa276c 100644 --- a/src/net/torvald/terrarum/ModMgr.kt +++ b/src/net/torvald/terrarum/ModMgr.kt @@ -61,6 +61,7 @@ object ModMgr { } const val modDir = "./assets/mods" + /** Module name (directory name), ModuleMetadata */ val moduleInfo = HashMap() init { @@ -156,6 +157,7 @@ object ModMgr { checkExistence(module) return "$modDir/$module/${path.sanitisePath()}" } + /** Returning files are read-only */ fun getGdxFile(module: String, path: String): FileHandle { return Gdx.files.internal(getPath(module, path)) } @@ -174,6 +176,40 @@ object ModMgr { } } + /** Get a common file from all the installed mods. Files are guaranteed to exist. If a mod does not + * contain the file, the mod will be skipped. */ + fun getFilesFromEveryMod(path: String): List { + val path = path.sanitisePath() + val moduleNames = moduleInfo.keys.toList() + + val filesList = ArrayList() + moduleNames.forEach { + val file = File(getPath(it, path)) + + if (file.exists()) filesList.add(file) + } + + return filesList.toList() + } + + /** Get a common file from all the installed mods. Files are guaranteed to exist. If a mod does not + * contain the file, the mod will be skipped. + * + * Returning files are read-only. */ + fun getGdxFilesFromEveryMod(path: String): List { + val path = path.sanitisePath() + val moduleNames = moduleInfo.keys.toList() + + val filesList = ArrayList() + moduleNames.forEach { + val file = Gdx.files.internal(getPath(it, path)) + + if (file.exists()) filesList.add(file) + } + + return filesList.toList() + } + object GameBlockLoader { diff --git a/src/net/torvald/terrarum/blockproperties/BlockCodex.kt b/src/net/torvald/terrarum/blockproperties/BlockCodex.kt index ff5a9e9cd..7670dba08 100644 --- a/src/net/torvald/terrarum/blockproperties/BlockCodex.kt +++ b/src/net/torvald/terrarum/blockproperties/BlockCodex.kt @@ -24,6 +24,9 @@ object BlockCodex { blockProps = Array(TILE_UNIQUE_MAX * 2, { BlockProp() }) } + /** + * Later entry (possible from other modules) will replace older ones + */ operator fun invoke(module: String, path: String) { try { val records = CSVFetcher.readFromModule(module, path) diff --git a/src/net/torvald/terrarum/gameworld/GameWorld.kt b/src/net/torvald/terrarum/gameworld/GameWorld.kt index 17faa8100..30a8ada44 100644 --- a/src/net/torvald/terrarum/gameworld/GameWorld.kt +++ b/src/net/torvald/terrarum/gameworld/GameWorld.kt @@ -12,10 +12,17 @@ typealias BlockDamage = Float open class GameWorld { + var worldName: String = "New World" var worldIndex: Int val width: Int val height: Int + val creationTime: Long + var lastPlayTime: Long + internal set // there's a case of save-and-continue-playing + + /** Used to calculate play time */ + val loadTime: Long = System.currentTimeMillis() / 1000L //layers val layerWall: MapLayer @@ -49,7 +56,7 @@ open class GameWorld { internal set - constructor(worldIndex: Int, width: Int, height: Int) { + constructor(worldIndex: Int, width: Int, height: Int, creationTIME_T: Long, lastPlayTIME_T: Long = creationTIME_T) { this.worldIndex = worldIndex this.width = width this.height = height @@ -71,9 +78,13 @@ open class GameWorld { // air pressure layer: 4 * 8 is one cell //layerAirPressure = MapLayerHalfFloat(width / 4, height / 8, 13f) // 1013 mBar + + + creationTime = creationTIME_T + lastPlayTime = lastPlayTIME_T } - internal constructor(worldIndex: Int, layerData: ReadLayerDataLzma.LayerData) { + internal constructor(worldIndex: Int, layerData: ReadLayerDataLzma.LayerData, creationTIME_T: Long, lastPlayTIME_T: Long = creationTIME_T) { this.worldIndex = worldIndex layerTerrain = layerData.layerTerrain @@ -90,6 +101,10 @@ open class GameWorld { width = layerTerrain.width height = layerTerrain.height + + + creationTime = creationTIME_T + lastPlayTime = lastPlayTIME_T } diff --git a/src/net/torvald/terrarum/serialise/WriteWorldInfo.kt b/src/net/torvald/terrarum/serialise/WriteWorldInfo.kt new file mode 100644 index 000000000..a46eb1c3b --- /dev/null +++ b/src/net/torvald/terrarum/serialise/WriteWorldInfo.kt @@ -0,0 +1,126 @@ +package net.torvald.terrarum.serialise + +import com.badlogic.gdx.Gdx +import net.torvald.terrarum.ModMgr +import net.torvald.terrarum.Terrarum +import net.torvald.terrarum.modulebasegame.gameactors.PlayerBuilder +import net.torvald.terrarum.modulebasegame.gameworld.GameWorldExtension +import net.torvald.terrarum.modulebasegame.weather.WeatherMixer +import net.torvald.terrarum.modulebasegame.worldgenerator.RoguelikeRandomiser +import org.apache.commons.codec.digest.DigestUtils +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +object WriteWorldInfo { + + // FIXME UNTESTED + + val META_MAGIC = "TESV".toByteArray(Charsets.UTF_8) + val NULL = 0.toByte() + + /** + * 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(): List? { + val world = (Terrarum.ingame!!.world) + + val path = "${Terrarum.defaultSaveDir}/tmp_worldinfo" + + val infileList = arrayOf( + ModMgr.getGdxFilesFromEveryMod("blocks/blocks.csv"), + ModMgr.getGdxFilesFromEveryMod("items/items.csv"), + ModMgr.getGdxFilesFromEveryMod("materials/materials.csv") + ) + + val metaFile = File(path + "0") + + val outFiles = ArrayList() + outFiles.add(metaFile) + val worldInfoHash = ArrayList() // hash of worldinfo1-3 + // try to write worldinfo1-3 + + for (filenum in 1..3) { + val outFile = File(path + filenum.toString()) + if (outFile.exists()) outFile.delete() + outFile.createNewFile() + + val outputStream = BufferedOutputStream(FileOutputStream(outFile), 256) + val infile = infileList[filenum - 1] + + infile.forEach { + val readBytes = it.readBytes() + outputStream.write(readBytes) + } + + outputStream.flush() + outputStream.close() + + + outFiles.add(outFile) + + + worldInfoHash.add(DigestUtils.sha256(FileInputStream(outFile))) + } + + + // compose save meta + val metaOut = BufferedOutputStream(FileOutputStream(metaFile), 256) + + + metaOut.write(META_MAGIC) + + // world name + val worldNameBytes = world.worldName.toByteArray(Charsets.UTF_8) + metaOut.write(worldNameBytes) + if (worldNameBytes.last() != NULL) metaOut.write(NULL.toInt()) + + // terrain seed + metaOut.write(world.generatorSeed.toLittle()) + + // randomiser seed + metaOut.write(RoguelikeRandomiser.RNG.s0.toLittle()) + metaOut.write(RoguelikeRandomiser.RNG.s1.toLittle()) + + // weather seed + metaOut.write(WeatherMixer.RNG.s0.toLittle()) + metaOut.write(WeatherMixer.RNG.s1.toLittle()) + + // SHA256SUM of worldinfo1-3 + worldInfoHash.forEach { + metaOut.write(it) + } + + // reference ID of the player + metaOut.write(Terrarum.PLAYER_REF_ID.toLittle()) + + // time_t + metaOut.write((world as GameWorldExtension).time.TIME_T.toLittle()) + + // creation time (real world time) + metaOut.write(world.creationTime.toLittle48()) + + // time at save + val timeNow = System.currentTimeMillis() / 1000L + metaOut.write(timeNow.toLittle48()) + + // get playtime and save it + val timeToAdd = timeNow - world.loadTime + metaOut.write(world.lastPlayTime.plus(timeToAdd).toInt().toLittle()) + world.lastPlayTime = timeNow + + + + + metaOut.flush() + metaOut.close() + + + + return outFiles.toList() + } + +} \ No newline at end of file diff --git a/work_files/DataFormats/Savegame metadata.txt b/work_files/DataFormats/Savegame metadata.txt index ca40b5b4c..011b92404 100644 --- a/work_files/DataFormats/Savegame metadata.txt +++ b/work_files/DataFormats/Savegame metadata.txt @@ -9,7 +9,7 @@ Ord Hex Description 02 4D S 03 44 V -04 Name of the world in UTF-8 (arbitrary length) +04 Name of the world in UTF-8 (arbitrary length, must not contain NULL) ... 00 String terminator ... Terrain seed (8 bytes) @@ -21,7 +21,6 @@ Ord Hex Description ... SHA-256 hash of worldinfo1 (32 bytes) ... SHA-256 hash of worldinfo2 (32 bytes) ... SHA-256 hash of worldinfo3 (32 bytes) -... SHA-256 hash of worldinfo4 (32 bytes) ... ReferenceID of the player (4 bytes, a fixed value of 91A7E2) ... Current world's time_t (the ingame time, 8 bytes)