diff --git a/assets/mods/basegame/weathers/generic_skybox.tga b/assets/mods/basegame/weathers/generic_skybox.tga index 9b35a1f34..680799220 100644 --- a/assets/mods/basegame/weathers/generic_skybox.tga +++ b/assets/mods/basegame/weathers/generic_skybox.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24c9e5e2eb919f33e326c2f22319d0c769614ab38b01bc9455a4647b667211fa -size 3212 +oid sha256:a89e79942fb0a4b2243215d156b2d44019fa8e731745cd19cfbea30091a43be6 +size 3500 diff --git a/lib/Terrarum_Joise.jar b/lib/Terrarum_Joise.jar index be920faae..f0ed0d0d8 100644 Binary files a/lib/Terrarum_Joise.jar and b/lib/Terrarum_Joise.jar differ diff --git a/src/net/torvald/random/HQRNG.java b/src/net/torvald/random/HQRNG.java new file mode 100644 index 000000000..5d8065fc1 --- /dev/null +++ b/src/net/torvald/random/HQRNG.java @@ -0,0 +1,192 @@ +package net.torvald.random; + +import java.util.Random; + +/** + * Xoroshift128 + * + * see https://github.com/SquidPony/SquidLib/blob/master/squidlib-util/src/main/java/squidpony/squidmath/XoRoRNG.java + */ +public class HQRNG extends Random { + + private static final long DOUBLE_MASK = (1L << 53) - 1; + private static final double NORM_53 = 1. / (1L << 53); + private static final long FLOAT_MASK = (1L << 24) - 1; + private static final double NORM_24 = 1. / (1L << 24); + + private static final long serialVersionUID = 1018744536171610262L; + + private long state0, state1; + + public long getState0() { + return state0; + } + + public long getState1() { + return state1; + } + + /** + * Creates a new generator seeded using four calls to Math.random(). + */ + public HQRNG() { + this((long) ((Math.random() - 0.5) * 0x10000000000000L) + ^ (long) (((Math.random() - 0.5) * 2.0) * 0x8000000000000000L), + (long) ((Math.random() - 0.5) * 0x10000000000000L) + ^ (long) (((Math.random() - 0.5) * 2.0) * 0x8000000000000000L)); + } + /** + * Constructs this XoRoRNG by dispersing the bits of seed using {@link #setSeed(long)} across the two parts of state + * this has. + * @param seed a long that won't be used exactly, but will affect both components of state + */ + public HQRNG(final long seed) { + setSeed(seed); + } + /** + * Constructs this XoRoRNG by calling {@link #setSeed(long, long)} on the arguments as given; see that method for + * the specific details (stateA and stateB are kept as-is unless they are both 0). + * @param stateA the number to use as the first part of the state; this will be 1 instead if both seeds are 0 + * @param stateB the number to use as the second part of the state + */ + public HQRNG(final long stateA, final long stateB) { + setSeed(stateA, stateB); + } + + @Override + public final int next(int bits) { + final long s0 = state0; + long s1 = state1; + final int result = (int)(s0 + s1) >>> (32 - bits); + s1 ^= s0; + state0 = (s0 << 55 | s0 >>> 9) ^ s1 ^ (s1 << 14); // a, b + state1 = (s1 << 36 | s1 >>> 28); // c + return result; + } + + @Override + public final long nextLong() { + final long s0 = state0; + long s1 = state1; + final long result = s0 + s1; + + s1 ^= s0; + state0 = (s0 << 55 | s0 >>> 9) ^ s1 ^ (s1 << 14); // a, b + state1 = (s1 << 36 | s1 >>> 28); // c + /* + state0 = Long.rotateLeft(s0, 55) ^ s1 ^ (s1 << 14); // a, b + state1 = Long.rotateLeft(s1, 36); // c + */ + return result; + } + + /** + * Can return any int, positive or negative, of any size permissible in a 32-bit signed integer. + * @return any int, all 32 bits are random + */ + public int nextInt() { + return (int)nextLong(); + } + + /** + * Exclusive on the outer bound; the inner bound is 0. The bound may be negative, which will produce a non-positive + * result. + * @param bound the outer exclusive bound; may be positive or negative + * @return a random int between 0 (inclusive) and bound (exclusive) + */ + public int nextInt(final int bound) { + return (int) ((bound * (nextLong() >>> 33)) >> 31); + } + /** + * Inclusive lower, exclusive upper. + * @param inner the inner bound, inclusive, can be positive or negative + * @param outer the outer bound, exclusive, should be positive, should usually be greater than inner + * @return a random int that may be equal to inner and will otherwise be between inner and outer + */ + public int nextInt(final int inner, final int outer) { + return inner + nextInt(outer - inner); + } + + /** + * Exclusive on the outer bound; the inner bound is 0. The bound may be negative, which will produce a non-positive + * result. + * @param bound the outer exclusive bound; may be positive or negative + * @return a random long between 0 (inclusive) and bound (exclusive) + */ + public long nextLong(long bound) { + long rand = nextLong(); + final long randLow = rand & 0xFFFFFFFFL; + final long boundLow = bound & 0xFFFFFFFFL; + rand >>>= 32; + bound >>= 32; + final long z = (randLow * boundLow >> 32); + long t = rand * boundLow + z; + final long tLow = t & 0xFFFFFFFFL; + t >>>= 32; + return rand * bound + t + (tLow + randLow * bound >> 32) - (z >> 63) - (bound >> 63); + } + /** + * Inclusive inner, exclusive outer; both inner and outer can be positive or negative. + * @param inner the inner bound, inclusive, can be positive or negative + * @param outer the outer bound, exclusive, can be positive or negative and may be greater than or less than inner + * @return a random long that may be equal to inner and will otherwise be between inner and outer + */ + public long nextLong(final long inner, final long outer) { + return inner + nextLong(outer - inner); + } + + public double nextDouble() { + return (nextLong() & DOUBLE_MASK) * NORM_53; + } + + public float nextFloat() { + return (float) ((nextLong() & FLOAT_MASK) * NORM_24); + } + + public boolean nextBoolean() { + return nextLong() < 0L; + } + + public void nextBytes(final byte[] bytes) { + int i = bytes.length, n = 0; + while (i != 0) { + n = Math.min(i, 8); + for (long bits = nextLong(); n-- != 0; bits >>>= 8) { + bytes[--i] = (byte) bits; + } + } + } + + /** + * Sets the seed of this generator using one long, running that through LightRNG's algorithm twice to get the state. + * @param seed the number to use as the seed + */ + public void setSeed(final long seed) { + + long state = seed + 0x9E3779B97F4A7C15L, + z = state; + z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; + state0 = z ^ (z >>> 31); + state += 0x9E3779B97F4A7C15L; + z = state; + z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L; + z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL; + state1 = z ^ (z >>> 31); + } + + /** + * Sets the seed of this generator using two longs, using them without changes unless both are 0 (then it makes the + * state variable corresponding to stateA 1 instead). + * @param stateA the number to use as the first part of the state; this will be 1 instead if both seeds are 0 + * @param stateB the number to use as the second part of the state + */ + public void setSeed(final long stateA, final long stateB) { + + state0 = stateA; + state1 = stateB; + if((stateA | stateB) == 0L) + state0 = 1L; + } + +} diff --git a/src/net/torvald/random/HQRNG.kt b/src/net/torvald/random/HQRNG.kt index a43398af3..d0f17572c 100644 --- a/src/net/torvald/random/HQRNG.kt +++ b/src/net/torvald/random/HQRNG.kt @@ -1,4 +1,4 @@ -package net.torvald.random +/*package net.torvald.random import net.torvald.terrarum.serialise.toLittle import net.torvald.terrarum.serialise.toLittleLong @@ -6,41 +6,178 @@ import org.apache.commons.codec.digest.DigestUtils import java.util.Random /** - * Xorshift128+ + * Xoroshift128 + * + * @see https://github.com/SquidPony/SquidLib/blob/master/squidlib-util/src/main/java/squidpony/squidmath/XoRoRNG.java */ -class HQRNG @JvmOverloads constructor(seed: Long = System.nanoTime()) : Random() { +class HQRNG() : Random() { - var s0: Long; private set - var s1: Long; private set + private val DOUBLE_MASK = (1L shl 53) - 1 + private val NORM_53 = 1.0 / (1L shl 53) + private val FLOAT_MASK = (1L shl 24) - 1 + private val NORM_24 = 1.0 / (1L shl 24) - constructor(s0: Long, s1: Long) : this() { - this.s0 = s0 - this.s1 = s1 + var state0: Long = 0L; private set + var state1: Long = 0L; private set + + /** + * Creates a new generator seeded using four calls to Math.random(). + */ + init { + reseed(((Math.random() - 0.5) * 0x10000000000000L).toLong() xor ((Math.random() - 0.5) * 2.0 * -0x8000000000000000L).toLong(), + ((Math.random() - 0.5) * 0x10000000000000L).toLong() xor ((Math.random() - 0.5) * 2.0 * -0x8000000000000000L).toLong()) } - init { - if (seed == 0L) - throw IllegalArgumentException("Invalid seed: cannot be zero") + /** + * Constructs this XoRoRNG by dispersing the bits of seed using [.setSeed] across the two parts of state + * this has. + * @param seed a long that won't be used exactly, but will affect both components of state + */ + constructor(seed: Long): this() { + setSeed(seed) + } - val hash = DigestUtils.sha256(seed.toString()) + /** + * Constructs this XoRoRNG by calling [.setSeed] on the arguments as given; see that method for + * the specific details (stateA and stateB are kept as-is unless they are both 0). + * @param stateA the number to use as the first part of the state; this will be 1 instead if both seeds are 0 + * @param stateB the number to use as the second part of the state + */ + constructor(stateA: Long, stateB: Long): this() { + reseed(stateA, stateB) + } - s0 = hash.copyOfRange(0, 8).toLittleLong() - s1 = hash.copyOfRange(8, 16).toLittleLong() + public override fun next(bits: Int): Int { + val s0 = state0 + var s1 = state1 + val result = (s0 + s1).toInt().ushr(32 - bits) + s1 = s1 xor s0 + state0 = s0 shl 55 or s0.ushr(9) xor s1 xor (s1 shl 14) // a, b + state1 = s1 shl 36 or s1.ushr(28) // c + return result } override fun nextLong(): Long { - var x = s0 - val y = s1 - s0 = y - x = x xor (x shl 23) - s1 = x xor y xor (x ushr 17) xor (y ushr 26) - return s1 + y + val s0 = state0 + var s1 = state1 + val result = s0 + s1 + + s1 = s1 xor s0 + state0 = s0 shl 55 or s0.ushr(9) xor s1 xor (s1 shl 14) // a, b + state1 = s1 shl 36 or s1.ushr(28) // c + /* + state0 = Long.rotateLeft(s0, 55) ^ s1 ^ (s1 << 14); // a, b + state1 = Long.rotateLeft(s1, 36); // c + */ + return result } - fun serialize() = s0.toLittle() + s1.toLittle() - - fun reseed(s0: Long, s1: Long) { - this.s0 = s0 - this.s1 = s1 + /** + * Exclusive on the outer bound; the inner bound is 0. The bound may be negative, which will produce a non-positive + * result. + * @param bound the outer exclusive bound; may be positive or negative + * @return a random int between 0 (inclusive) and bound (exclusive) + */ + override fun nextInt(bound: Int): Int { + return (bound * nextLong().ushr(33) shr 31).toInt() } -} \ No newline at end of file + + /** + * Inclusive lower, exclusive upper. + * @param inner the inner bound, inclusive, can be positive or negative + * @param outer the outer bound, exclusive, should be positive, should usually be greater than inner + * @return a random int that may be equal to inner and will otherwise be between inner and outer + */ + fun nextInt(inner: Int, outer: Int): Int { + return inner + nextInt(outer - inner) + } + + /** + * Exclusive on the outer bound; the inner bound is 0. The bound may be negative, which will produce a non-positive + * result. + * @param bound the outer exclusive bound; may be positive or negative + * @return a random long between 0 (inclusive) and bound (exclusive) + */ + fun nextLong(bound: Long): Long { + var bound = bound + var rand = nextLong() + val randLow = rand and 0xFFFFFFFFL + val boundLow = bound and 0xFFFFFFFFL + rand = rand ushr 32 + bound = bound shr 32 + val z = randLow * boundLow shr 32 + var t = rand * boundLow + z + val tLow = t and 0xFFFFFFFFL + t = t ushr 32 + return rand * bound + t + (tLow + randLow * bound shr 32) - (z shr 63) - (bound shr 63) + } + + /** + * Inclusive inner, exclusive outer; both inner and outer can be positive or negative. + * @param inner the inner bound, inclusive, can be positive or negative + * @param outer the outer bound, exclusive, can be positive or negative and may be greater than or less than inner + * @return a random long that may be equal to inner and will otherwise be between inner and outer + */ + fun nextLong(inner: Long, outer: Long): Long { + return inner + nextLong(outer - inner) + } + + override fun nextDouble(): Double { + return (nextLong() and DOUBLE_MASK) * NORM_53 + } + + override fun nextFloat(): Float { + return ((nextLong() and FLOAT_MASK) * NORM_24).toFloat() + } + + override fun nextBoolean(): Boolean { + return nextLong() < 0L + } + + override fun nextBytes(bytes: ByteArray) { + var i = bytes.size + var n = 0 + while (i != 0) { + n = Math.min(i, 8) + var bits = nextLong() + while (n-- != 0) { + bytes[--i] = bits.toByte() + bits = bits ushr 8 + } + } + } + + + fun serialize() = state0.toLittle() + state1.toLittle() + + /** + * Sets the seed of this generator using one long, running that through LightRNG's algorithm twice to get the state. + * @param seed the number to use as the seed + */ + override fun setSeed(seed: Long) { + var state = seed + -0x61c8864680b583ebL + var z = state + z = (z xor z.ushr(30)) * -0x40a7b892e31b1a47L + z = (z xor z.ushr(27)) * -0x6b2fb644ecceee15L + state0 = z xor z.ushr(31) + state += -0x61c8864680b583ebL + z = state + z = (z xor z.ushr(30)) * -0x40a7b892e31b1a47L + z = (z xor z.ushr(27)) * -0x6b2fb644ecceee15L + state1 = z xor z.ushr(31) + } + + /** + * Sets the seed of this generator using two longs, using them without changes unless both are 0 (then it makes the + * state variable corresponding to stateA 1 instead). + * @param stateA the number to use as the first part of the state; this will be 1 instead if both seeds are 0 + * @param stateB the number to use as the second part of the state + */ + fun reseed(stateA: Long, stateB: Long) { + + state0 = stateA + state1 = stateB + if (stateA or stateB == 0L) + state0 = 1L + } +}*/ \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/RNGConsumer.kt b/src/net/torvald/terrarum/modulebasegame/RNGConsumer.kt index 1c6f17dff..262470c9c 100644 --- a/src/net/torvald/terrarum/modulebasegame/RNGConsumer.kt +++ b/src/net/torvald/terrarum/modulebasegame/RNGConsumer.kt @@ -8,7 +8,7 @@ internal interface RNGConsumer { val RNG: HQRNG fun loadFromSave(s0: Long, s1: Long) { - RNG.reseed(s0, s1) + RNG.setSeed(s0, s1) } } diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/ActorHumanoid.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/ActorHumanoid.kt index 6a6dffd35..d12b8cb96 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/ActorHumanoid.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/ActorHumanoid.kt @@ -491,7 +491,7 @@ open class ActorHumanoid( private var oldJUMPPOWERBUFF = -1.0 // init private var oldScale = -1.0 private var oldDragCoefficient = -1.0 - val jumpAirTime: Double = -1.0 + var jumpAirTime: Double = -1.0 get() { // compare all the affecting variables if (oldMAX_JUMP_LENGTH == MAX_JUMP_LENGTH && diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/WorldTime.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/WorldTime.kt index 2b27ebac7..1cfa2b1bb 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameworld/WorldTime.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/WorldTime.kt @@ -13,9 +13,10 @@ typealias time_t = Long * https://en.wikipedia.org/wiki/World_Calendar * http://dwarffortresswiki.org/index.php/DF2014:Calendar * - * And there is no AM/PM concept, 22-hour clock is forced; no leap years. - * (AM 12 is still 00h in this system, again, to reduce confusion) + * And there is no AM/PM concept, 24-hour clock is forced; no leap years. + * An ingame day should last 22 real-life minutes. * + * // TODO 4-month year? like Stardew Valley * * Calendar * @@ -110,13 +111,13 @@ class WorldTime(initTime: Long = 0L) { "Mala", "Gale", "Lime", "Sand", "Timb", "Moon") companion object { - /** Each day is 22-hour long */ - val DAY_LENGTH = 79200 //must be the multiple of 3600 + /** Each day is displayed as 24 hours, but in real-life clock it's 22 mins long */ + val DAY_LENGTH = 86400 //must be the multiple of 3600 val HOUR_SEC: Int = 3600 val MINUTE_SEC: Int = 60 val HOUR_MIN: Int = 60 - val GAME_MIN_TO_REAL_SEC: Float = 60f + val GAME_MIN_TO_REAL_SEC: Float = 720f/11f val HOURS_PER_DAY = DAY_LENGTH / HOUR_SEC val YEAR_DAYS: Int = 365 @@ -164,8 +165,8 @@ class WorldTime(initTime: Long = 0L) { val dayName: String get() = DAY_NAMES[dayOfWeek] - inline fun Long.toPositiveInt() = this.and(0x7FFFFFFF).toInt() - inline fun Long.abs() = Math.abs(this) + fun Long.toPositiveInt() = this.and(0x7FFFFFFF).toInt() + fun Long.abs() = Math.abs(this) /** Format: "%A, %d %B %Y %X" */ fun getFormattedTime() = "${getDayNameShort()}, " + diff --git a/src/net/torvald/terrarum/serialise/ReadWorldInfo.kt b/src/net/torvald/terrarum/serialise/ReadWorldInfo.kt index 55a705edb..87945d43e 100644 --- a/src/net/torvald/terrarum/serialise/ReadWorldInfo.kt +++ b/src/net/torvald/terrarum/serialise/ReadWorldInfo.kt @@ -22,6 +22,11 @@ object ReadWorldInfo { 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) @@ -38,14 +43,14 @@ object ReadWorldInfo { fis.read(8).toLittleLong(), // rng s1 fis.read(8).toLittleLong(), // weather s0 fis.read(8).toLittleLong(), // weather s1 - fis.read(32), - fis.read(32), - fis.read(32), 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(4).toLittleInt(), // total time wasted + fis.read(32), // sha256sum worldinfo1 + fis.read(32), // sha256sum worldinfo2 + fis.read(32) // sha256sum worldinfo3 ) } @@ -57,13 +62,13 @@ object ReadWorldInfo { val rngS1: Long, val weatherS0: Long, val weatherS1: Long, - val worldinfo1Hash: ByteArray, - val worldInfo2Hash: ByteArray, - val worldInfo3Hash: ByteArray, val playerID: Int, val timeNow: Long, val creationTime: Long, val lastPlayTime: Long, - val totalPlayTime: Int + val totalPlayTime: Int, + val worldinfo1Hash: ByteArray, + val worldInfo2Hash: ByteArray, + val worldInfo3Hash: ByteArray ) } \ No newline at end of file diff --git a/src/net/torvald/terrarum/serialise/WriteWorldInfo.kt b/src/net/torvald/terrarum/serialise/WriteWorldInfo.kt index f6a2fb115..18ce61b76 100644 --- a/src/net/torvald/terrarum/serialise/WriteWorldInfo.kt +++ b/src/net/torvald/terrarum/serialise/WriteWorldInfo.kt @@ -18,6 +18,9 @@ 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. * @@ -38,10 +41,11 @@ object WriteWorldInfo { val outFiles = ArrayList() outFiles.add(metaFile) + val worldInfoHash = ArrayList() // hash of worldinfo1-3 // try to write worldinfo1-3 - for (filenum in 1..3) { + for (filenum in 1..HASHED_FILES_COUNT) { val outFile = File(path + filenum.toString()) if (outFile.exists()) outFile.delete() outFile.createNewFile() @@ -65,11 +69,13 @@ object WriteWorldInfo { } - // compose save meta + // compose save meta (actual writing part) val metaOut = BufferedOutputStream(FileOutputStream(metaFile), 256) metaOut.write(META_MAGIC) + metaOut.write(VERSION) + metaOut.write(HASHED_FILES_COUNT) // world name val worldNameBytes = world.worldName.toByteArray(Charsets.UTF_8) @@ -80,28 +86,23 @@ object WriteWorldInfo { metaOut.write(world.generatorSeed.toLittle()) // randomiser seed - metaOut.write(RoguelikeRandomiser.RNG.s0.toLittle()) - metaOut.write(RoguelikeRandomiser.RNG.s1.toLittle()) + metaOut.write(RoguelikeRandomiser.RNG.state0.toLittle()) + metaOut.write(RoguelikeRandomiser.RNG.state1.toLittle()) // weather seed - metaOut.write(WeatherMixer.RNG.s0.toLittle()) - metaOut.write(WeatherMixer.RNG.s1.toLittle()) - - // SHA256SUM of worldinfo1-3 - worldInfoHash.forEach { - metaOut.write(it) - } + metaOut.write(WeatherMixer.RNG.state0.toLittle()) + metaOut.write(WeatherMixer.RNG.state1.toLittle()) // reference ID of the player metaOut.write(Terrarum.PLAYER_REF_ID.toLittle()) - // time_t + // ingame time_t metaOut.write((world as GameWorldExtension).time.TIME_T.toLittle()) // creation time (real world time) metaOut.write(world.creationTime.toLittle48()) - // time at save + // time at save (real world time) val timeNow = System.currentTimeMillis() / 1000L metaOut.write(timeNow.toLittle48()) @@ -111,8 +112,14 @@ object WriteWorldInfo { world.lastPlayTime = timeNow world.totalPlayTime += timeToAdd + // SHA256SUM of worldinfo1-3 + worldInfoHash.forEach { + metaOut.write(it) + } + // more data goes here // + metaOut.flush() metaOut.close() diff --git a/work_files/DataFormats/Savegame metadata.txt b/work_files/DataFormats/Savegame metadata.txt index 011b92404..d7d15d488 100644 --- a/work_files/DataFormats/Savegame metadata.txt +++ b/work_files/DataFormats/Savegame metadata.txt @@ -9,7 +9,11 @@ Ord Hex Description 02 4D S 03 44 V -04 Name of the world in UTF-8 (arbitrary length, must not contain NULL) +04 01 Descriptor version number + +05 03 Number of hashes + +06 Name of the world in UTF-8 (arbitrary length, must not contain NULL) ... 00 String terminator ... Terrain seed (8 bytes) @@ -18,10 +22,6 @@ Ord Hex Description ... Weather s0 (8 bytes) ... Weather s1 (8 bytes) -... SHA-256 hash of worldinfo1 (32 bytes) -... SHA-256 hash of worldinfo2 (32 bytes) -... SHA-256 hash of worldinfo3 (32 bytes) - ... ReferenceID of the player (4 bytes, a fixed value of 91A7E2) ... Current world's time_t (the ingame time, 8 bytes) @@ -29,7 +29,9 @@ Ord Hex Description ... Last play time in time_t (6 bytes) ... Total playtime in time_t (4 bytes) // will record 136.1 years of playtime - +... SHA-256 hash of worldinfo1 (32 bytes) +... SHA-256 hash of worldinfo2 (32 bytes) +... SHA-256 hash of worldinfo3 (32 bytes)