using gdx's preferred way to generate world json

This commit is contained in:
minjaesong
2021-08-25 11:14:37 +09:00
parent 6b74f3a9c4
commit 8499746ad0
5 changed files with 229 additions and 47 deletions

View File

@@ -56,7 +56,7 @@ open class GameWorld : Disposable {
internal set internal set
/** Used to calculate play time */ /** Used to calculate play time */
open val loadTime: Long = System.currentTimeMillis() / 1000L @Transient open val loadTime: Long = System.currentTimeMillis() / 1000L
//layers //layers
val layerWall: BlockLayer val layerWall: BlockLayer
@@ -82,8 +82,8 @@ open class GameWorld : Disposable {
private val wirings: HashMap<BlockAddress, WiringNode> private val wirings: HashMap<BlockAddress, WiringNode>
private val wiringGraph = HashMap<BlockAddress, HashMap<ItemID, WiringSimCell>>() private val wiringGraph = HashMap<BlockAddress, HashMap<ItemID, WiringSimCell>>()
private val WIRE_POS_MAP = intArrayOf(1,2,4,8) @Transient private val WIRE_POS_MAP = intArrayOf(1,2,4,8)
private val WIRE_ANTIPOS_MAP = intArrayOf(4,8,1,2) @Transient private val WIRE_ANTIPOS_MAP = intArrayOf(4,8,1,2)
/** /**
* Used by the renderer. When wirings are updated, `wirings` and this properties must be synchronised. * Used by the renderer. When wirings are updated, `wirings` and this properties must be synchronised.
@@ -102,7 +102,7 @@ open class GameWorld : Disposable {
open var generatorSeed: Long = 0 open var generatorSeed: Long = 0
internal set internal set
var disposed = false @Transient var disposed = false
private set private set
val worldTime: WorldTime = WorldTime( // Year EPOCH (125), Month 1, Day 1 is implied val worldTime: WorldTime = WorldTime( // Year EPOCH (125), Month 1, Day 1 is implied
@@ -113,7 +113,7 @@ open class GameWorld : Disposable {
val tileNumberToNameMap: HashMap<Int, ItemID> val tileNumberToNameMap: HashMap<Int, ItemID>
// does not go to the savefile // does not go to the savefile
val tileNameToNumberMap: HashMap<ItemID, Int> @Transient val tileNameToNumberMap: HashMap<ItemID, Int>
/** /**
* Create new world * Create new world

View File

@@ -14,8 +14,8 @@ class GameWorldExtension : GameWorld {
//internal constructor(worldIndex: Int, layerData: ReadLayerDataZip.LayerData, creationTIME_T: Long, lastPlayTIME_T: Long, totalPlayTime: Int) : super(worldIndex, layerData, creationTIME_T, lastPlayTIME_T, totalPlayTime) //internal constructor(worldIndex: Int, layerData: ReadLayerDataZip.LayerData, creationTIME_T: Long, lastPlayTIME_T: Long, totalPlayTime: Int) : super(worldIndex, layerData, creationTIME_T, lastPlayTIME_T, totalPlayTime)
val economy = GameEconomy() var economy = GameEconomy()
internal set
// delegated properties // // delegated properties //
/*val layerWall: MapLayer; get() = baseworld.layerWall /*val layerWall: MapLayer; get() = baseworld.layerWall

View File

@@ -0,0 +1,28 @@
package net.torvald.terrarum.serialise
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.modulebasegame.gameworld.GameEconomy
import net.torvald.terrarum.modulebasegame.gameworld.GameWorldExtension
/**
* Created by minjaesong on 2021-08-25.
*/
open class ReadWorld(val ingame: TerrarumIngame) {
open fun invoke(worldIndex: Int, metadata: JsonValue, worlddata: JsonValue) {
val json = Json()
val world = GameWorldExtension(
worldIndex,
worlddata.getInt("width"),
worlddata.getInt("height"),
metadata.getLong("creation_t"),
metadata.getLong("lastplay_t"),
metadata.getInt("playtime_t")
)
//world.economy = json.fromJson(GameEconomy::class.java, worlddata.get("basegame.economy").)
}
}

View File

@@ -1,16 +1,29 @@
package net.torvald.terrarum.serialise package net.torvald.terrarum.serialise
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonReader
import com.badlogic.gdx.utils.JsonValue
import com.badlogic.gdx.utils.JsonWriter
import net.torvald.terrarum.gameworld.BlockLayer
import net.torvald.terrarum.gameworld.WorldTime
import net.torvald.terrarum.modulebasegame.TerrarumIngame import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.ByteArray64 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.math.BigInteger
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
/** /**
* Created by minjaesong on 2021-08-23. * Created by minjaesong on 2021-08-23.
*/ */
class WriteWorld(val ingame: TerrarumIngame) { open class WriteWorld(val ingame: TerrarumIngame) {
open fun invoke(): String { open fun invoke(): String {
val world = ingame.world val world = ingame.world
return "{${world.getJsonFields().joinToString(",\n")}}" //return "{${world.getJsonFields().joinToString(",\n")}}"
return jsoner.toJson(world)
} }
fun encodeToByteArray64(): ByteArray64 { fun encodeToByteArray64(): ByteArray64 {
@@ -27,4 +40,148 @@ class WriteWorld(val ingame: TerrarumIngame) {
ba.add('}'.code.toByte()) ba.add('}'.code.toByte())
return ba return ba
} }
companion object {
/** dispose of the `offendingObject` after rejection! */
class BlockLayerHashMismatchError(val offendingObject: BlockLayer) : Error()
private fun Byte.tostr() = this.toInt().and(255).toString(16).padStart(2,'0')
private val digester = DigestUtils.getSha256Digest()
val jsoner = Json(JsonWriter.OutputType.json)
// install custom (de)serialiser
init {
// BigInteger
jsoner.setSerializer(BigInteger::class.java, object : Json.Serializer<BigInteger> {
override fun write(json: Json, obj: BigInteger?, knownType: Class<*>?) {
json.writeValue(obj?.toString())
}
override fun read(json: Json, jsonData: JsonValue, type: Class<*>?): BigInteger {
return BigInteger(jsonData.asString())
}
})
// BlockLayer
jsoner.setSerializer(BlockLayer::class.java, object : Json.Serializer<BlockLayer> {
override fun write(json: Json, obj: BlockLayer, knownType: Class<*>?) {
digester.reset()
obj.bytesIterator().forEachRemaining { digester.update(it) }
val hash = StringBuilder().let { sb -> digester.digest().forEach { sb.append(it.tostr()) }; sb.toString() }
val layer = LayerInfo(hash, blockLayerToStr(obj), obj.width, obj.height)
json.writeValue(layer)
}
override fun read(json: Json, jsonData: JsonValue, type: Class<*>): BlockLayer {
// full auto
//return strToBlockLayer(json.fromJson(type, jsonData.toJson(JsonWriter.OutputType.minimal)) as LayerInfo)
// full manual
return strToBlockLayer(LayerInfo(
jsonData.getString("h"),
jsonData.getString("b"),
jsonData.getInt("x"),
jsonData.getInt("y")
))
}
})
// WorldTime
jsoner.setSerializer(WorldTime::class.java, object : Json.Serializer<WorldTime> {
override fun write(json: Json, obj: WorldTime, knownType: Class<*>?) {
json.writeValue(obj.TIME_T)
}
override fun read(json: Json, jsonData: JsonValue, type: Class<*>?): WorldTime {
return WorldTime(jsonData.asLong())
}
})
}
private data class LayerInfo(val h: String, val b: String, val x: Int, val y: Int)
/**
* @param b a BlockLayer
* @return Bytes in [b] which are GZip'd then Ascii85-encoded
*/
private fun blockLayerToStr(b: BlockLayer): String {
val sb = StringBuilder()
val bo = ByteArray64GrowableOutputStream()
val zo = GZIPOutputStream(bo)
// zip
b.bytesIterator().forEachRemaining {
zo.write(it.toInt())
}
zo.flush(); zo.close()
// enascii
val ba = bo.toByteArray64()
var bai = 0
val buf = IntArray(4) { Ascii85.PAD_BYTE }
ba.forEach {
if (bai > 0 && bai % 4 == 0) {
sb.append(Ascii85.encode(buf[0], buf[1], buf[2], buf[3]))
buf.fill(Ascii85.PAD_BYTE)
}
buf[bai % 4] = it.toInt() and 255
bai += 1
}; sb.append(Ascii85.encode(buf[0], buf[1], buf[2], buf[3]))
return sb.toString()
}
private fun strToBlockLayer(layerInfo: LayerInfo): BlockLayer {
val layer = BlockLayer(layerInfo.x, layerInfo.y)
val unasciidBytes = ByteArray64()
val unzipdBytes = ByteArray64()
// unascii
var bai = 0
val buf = CharArray(5) { Ascii85.PAD_CHAR }
layerInfo.b.forEach {
if (bai > 0 && bai % 5 == 0) {
Ascii85.decode(buf[0], buf[1], buf[2], buf[3], buf[4]).forEach { unasciidBytes.add(it) }
buf.fill(Ascii85.PAD_CHAR)
}
buf[bai % 5] = it
bai += 1
}; Ascii85.decode(buf[0], buf[1], buf[2], buf[3], buf[4]).forEach { unasciidBytes.add(it) }
// unzip
val zi = GZIPInputStream(ByteArray64InputStream(unasciidBytes))
while (true) {
val byte = zi.read()
if (byte == -1) break
unzipdBytes.add(byte.toByte())
}
zi.close()
// write to blocklayer and the digester
digester.reset()
var writeCursor = 0L
unzipdBytes.forEach {
if (writeCursor < layer.ptr.size) {
layer.ptr[writeCursor] = it
digester.update(it)
writeCursor += 1
}
}
// check hash
val hash = StringBuilder().let { sb -> digester.digest().forEach { sb.append(it.tostr()) }; sb.toString() }
if (hash != layerInfo.h) {
throw BlockLayerHashMismatchError(layer)
}
return layer
}
}
} }

View File

@@ -27,59 +27,56 @@ Following code is an example savegame JSON files.
#### world1.json #### world1.json
File is named as `"world"+world_index+".json"` File is named as `"world"+world_index+".json"`.
The fields are auto-generated by GDX's JSON serialiser.
``` ```
{ {
worldname: "New World", worldName: "New World",
comp: <null, "gzip">, worldIndex: 1,
width: 8192, width: 8192,
height: 2048, height: 2048,
spawnx: 4096, spawnX: 4096,
spawny: 248, spawnY: 248,
genver: 4, /* generator version in integer */ creationTime: 1629857065,
time_t: <in-game TIME_T of this world>, lastPlayTime: 1629857065,
terr: { totalPlayTime: 0,
layerTerrain: {
h: "a441b15fe9a3cf56661190a0b93b9dec7d04127288cc87250967cf3b52894d11", h: "a441b15fe9a3cf56661190a0b93b9dec7d04127288cc87250967cf3b52894d11",
b: <Ascii85-encoded gzipped terrain layerdata> b: <Ascii85-encoded gzipped terrain layerdata>,
x: 8192,
y: 2048
}, },
wall: { layerWall: {
h: <SHA-256 hash of 'b'>, h: <SHA-256 hash of 'b'>,
b: <Ascii85-encoded gzipped wall layerdata> b: <Ascii85-encoded gzipped wall layerdata>,
x: 8192,
y: 2048
}, },
tdmg: { wallDamages:{},
h: <SHA-256 hash of 'b'>, terrainDamages: {},
b: <Ascii85-encoded gzipped terrain damage in JSON> fluidTypes: {}
fluidFills: {},
wirings: {},
wiringGraph: {},
gravitation: {y:9.8}
globalLight: {
r:0.8826928,
g:0.8901961,
b:0.9055425,
a:0.93691504
}, },
wdmg: { averageTemperature: 288,
h: <SHA-256 hash of 'b'>, generatorSeed: 0,
b: <Ascii85-encoded gzipped wall damage in JSON> worldTime: 27874,
}, tileNumberToNameMap: {}
flut: {
h: <SHA-256 hash of 'b'>,
b: <Ascii85-encoded gzipped fluidTypes in JSON>
},
fluf: {
h: <SHA-256 hash of 'b'>,
b: <Ascii85-encoded gzipped fluidFills in JSON>
},
wire: {
h: <SHA-256 hash of 'b'>,
b: <Ascii85-encoded gzipped wirings in JSON>
},
wirg: {
h: <SHA-256 hash of 'b'>,
b: <Ascii85-encoded gzipped wiringGraph in JSON>
},
tmap: {
h: <SHA-256 hash of 'b'>,
b: <Ascii85-encoded gzipped tilenumber-to-tilename map in JSON>
}
} }
``` ```
#### actors.json #### actors.json
The fields are auto-generated by GDX's JSON serialiser.
``` ```
{ {
<actor id>: { actor serialised in JSON }, <actor id>: { actor serialised in JSON },