diff --git a/src/net/torvald/terrarum/gameworld/GameWorld.kt b/src/net/torvald/terrarum/gameworld/GameWorld.kt index c97ec9e35..ef53b3e76 100644 --- a/src/net/torvald/terrarum/gameworld/GameWorld.kt +++ b/src/net/torvald/terrarum/gameworld/GameWorld.kt @@ -178,7 +178,7 @@ open class GameWorld( it[Fluid.NULL] = 0 }*/ - val extraFields = HashMap() + val extraFields = HashMap() // NOTE: genver was here but removed: genver will be written by manually editing the serialising JSON. Reason: the 'genver' string must be found on a fixed offset on the file. internal var comp = -1 // only gets used when the game saves and loads diff --git a/src/net/torvald/terrarum/gameworld/TerrarumSavegameExtrafieldSerialisable.kt b/src/net/torvald/terrarum/gameworld/TerrarumSavegameExtrafieldSerialisable.kt new file mode 100644 index 000000000..4ef6f8064 --- /dev/null +++ b/src/net/torvald/terrarum/gameworld/TerrarumSavegameExtrafieldSerialisable.kt @@ -0,0 +1,8 @@ +package net.torvald.terrarum.gameworld + +/** + * Dummy interface, except every implementing class must have a no-arg constructor. + * + * Created by minjaesong on 2025-02-27. + */ +interface TerrarumSavegameExtrafieldSerialisable \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/GameEconomy.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/GameEconomy.kt index 0abc4549a..a1b479512 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameworld/GameEconomy.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/GameEconomy.kt @@ -1,6 +1,7 @@ package net.torvald.terrarum.modulebasegame.gameworld import net.torvald.terrarum.gameactors.ActorID +import net.torvald.terrarum.gameworld.TerrarumSavegameExtrafieldSerialisable /** * The whole world is economically isolated system. Economy will be important to make player keep playing, @@ -11,7 +12,7 @@ import net.torvald.terrarum.gameactors.ActorID * * Created by minjaesong on 2017-04-23. */ -class GameEconomy { +class GameEconomy : TerrarumSavegameExtrafieldSerialisable { val transactionHistory = TransanctionHistory() diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/GamePostalService.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/GamePostalService.kt index 367713861..e8b1974d9 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameworld/GamePostalService.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/GamePostalService.kt @@ -1,5 +1,6 @@ package net.torvald.terrarum.modulebasegame.gameworld +import net.torvald.terrarum.gameworld.TerrarumSavegameExtrafieldSerialisable import net.torvald.terrarum.modulebasegame.gameactors.FixtureInventory import kotlin.math.ceil @@ -9,7 +10,7 @@ import kotlin.math.ceil * * Created by minjaesong on 2024-12-29. */ -class GamePostalService { +class GamePostalService : TerrarumSavegameExtrafieldSerialisable { companion object { diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/IngameNetPacket.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/IngameNetPacket.kt new file mode 100644 index 000000000..dcc878830 --- /dev/null +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/IngameNetPacket.kt @@ -0,0 +1,122 @@ +package net.torvald.terrarum.modulebasegame.gameworld + +import net.torvald.terrarum.serialise.toBigInt32 +import net.torvald.terrarum.serialise.toUint +import net.torvald.terrarum.serialise.writeBigInt32 +import java.util.zip.CRC32 + +/** + * # Packet data structure + * + * Endianness: big + * + * ## The Header + * + * - (Byte1) Frame Type + * - 00 : invalid + * - FF : token (an "empty" packet for a Token Ring) + * - AA : data + * - EE : abort + * - 99 : ballot (an initialiser packet for electing the Active Monitor for a Token Ring) + * - (Byte1) Frame number. Always 0 for a Token Ring + * - (Byte4) Sender MAC address + * + * ## The Body + * + * The following specification differs by the Frame Type + * + * ### Token and Abort + * + * The Token and Abort frame has no further bytes + * + * ### Ballot + * + * - (Byte4) Currently elected Monitor candidate. A NIC examines this number, and if its MAC is larger than + * this value, the NIC writes its own MAC to this area and passes the packet to the next NIC; otherwise it + * just passes the packet as-is + * + * ### Data + * + * - (Byte4) Receiver MAC address + * - (Byte4) Length of data in bytes + * - (Bytes) The actual data + * - (Byte4) CRC-32 of the actual data + * + * Created by minjaesong on 2025-02-27. + */ +data class IngameNetPacket(val byteArray: ByteArray) { + + fun getFrameType(): String { + return when (byteArray.first().toUint()) { + 0xff -> "token" + 0xaa -> "data" + 0xee -> "abort" + 0x99 -> "ballot" + 0x00 -> "invalid" + else -> "unknown" + } + } + + private fun checkIsToken() { if (getFrameType() != "token") throw Error() } + private fun checkIsData() { if (getFrameType() != "data") throw Error() } + private fun checkIsAbort() { if (getFrameType() != "abort") throw Error() } + private fun checkIsBallot() { if (getFrameType() != "ballot") throw Error() } + + fun getBallot(): Int { + checkIsBallot() + return byteArray.toBigInt32(6) + } + + fun setBallot(mac: Int) { + checkIsBallot() + byteArray.writeBigInt32(mac, 6) + } + + fun shouldIintercept(mac: Int) = when (getFrameType()) { + "ballot" -> (getBallot() < mac) + "data" -> (getDataRecipient() == mac) + else -> false + } + + /** + * returns null if CRC check fails + */ + fun getDataContents(): ByteArray? { + checkIsData() + val len = byteArray.toBigInt32(10) + val ret = ByteArray(len) + System.arraycopy(byteArray, 14, ret, 0, len) + val crc0 = byteArray.toBigInt32(14 + len) + val crc = CRC32().also { it.update(ret) }.value.toInt() + return if (crc != crc0) null else ret + } + + fun getDataRecipient(): Int { + checkIsData() + return byteArray.toBigInt32(6) + } + + companion object { + private fun ByteArray.makeHeader(frameType: Int, mac: Int): ByteArray { + this[0] = frameType.toByte() + this.writeBigInt32(mac, 2) + return this + } + + fun makeToken(mac: Int) = ByteArray(5).makeHeader(0xff, mac) + + fun makeAbort(mac: Int) = ByteArray(5).makeHeader(0xee, mac) + + fun makeBallot(mac: Int) = ByteArray(9).makeHeader(0x99, mac) + + fun makeData(sender: Int, recipient: Int, data: ByteArray) = ByteArray(18 + data.size).also { + it.makeHeader(0xaa, sender) + it.writeBigInt32(recipient, 6) + it.writeBigInt32(data.size, 10) + System.arraycopy(data, 0, it, 14, data.size) + val crc = CRC32().also { it.update(data) }.value.toInt() + it.writeBigInt32(crc, 14 + data.size) + } + } + +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/gameworld/PacketRunner.kt b/src/net/torvald/terrarum/modulebasegame/gameworld/PacketRunner.kt new file mode 100644 index 000000000..2ce583e4f --- /dev/null +++ b/src/net/torvald/terrarum/modulebasegame/gameworld/PacketRunner.kt @@ -0,0 +1,22 @@ +package net.torvald.terrarum.modulebasegame.gameworld + +import net.torvald.terrarum.gameworld.TerrarumSavegameExtrafieldSerialisable +import java.util.TreeMap + +/** + * Manages packet-number-to-actual-packet mapping, and safely puts them into the savegame + * + * Created by minjaesong on 2025-02-27. + */ +class PacketRunner : TerrarumSavegameExtrafieldSerialisable { + + private val ledger = TreeMap() + + operator fun set(id: Int, packet: IngameNetPacket) { + ledger[id] = packet + } + + operator fun get(id: Int) = ledger[id]!! + +} + diff --git a/src/net/torvald/terrarum/modulebasegame/redeemable/RedeemCodeMachine.kt b/src/net/torvald/terrarum/modulebasegame/redeemable/RedeemCodeMachine.kt index 68efb5c03..5145e19d5 100644 --- a/src/net/torvald/terrarum/modulebasegame/redeemable/RedeemCodeMachine.kt +++ b/src/net/torvald/terrarum/modulebasegame/redeemable/RedeemCodeMachine.kt @@ -4,7 +4,6 @@ import javazoom.jl.decoder.Crc16 import net.torvald.terrarum.gameitems.ItemID import net.torvald.terrarum.serialise.toBig64 import net.torvald.terrarum.serialise.toUint -import net.torvald.terrarum.toHex import net.torvald.terrarum.toInt import net.torvald.terrarum.utils.PasswordBase32 import net.torvald.unicode.CURRENCY @@ -13,6 +12,15 @@ import java.util.* import kotlin.experimental.and import kotlin.experimental.xor +class SimplePRNG(seed: Int) { + private var state: Int = seed + + fun nextInt(): Int { + state = (state * 1664525 + 1013904223) and 0x7FFFFFFF // LCG Algorithm + return state + } +} + /** * Created by minjaesong on 2025-02-13. */ @@ -38,15 +46,98 @@ object RedeemCodeMachine { val initialPassword = listOf( // will be list of 256 bits of something "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - "Nam nisl leo, semper a ligula a, sollicitudin congue turpis.", - "Aenean id malesuada nibh, vitae accumsan risus.", - "Morbi tempus velit et consequat vehicula.", - "Integer varius turpis nec euismod mattis.", - "Vivamus dictum non ipsum vitae mollis.", - "Quisque tincidunt, diam non dictum sodales, nisl neque aliquet risus, pulvinar posuere lacus est a arcu.", - "Fusce eu venenatis sapien, non aliquam massa.", +// "Nam nisl leo, semper a ligula a, sollicitudin congue turpis.", +// "Aenean id malesuada nibh, vitae accumsan risus.", +// "Morbi tempus velit et consequat vehicula.", +// "Integer varius turpis nec euismod mattis.", +// "Vivamus dictum non ipsum vitae mollis.", +// "Quisque tincidunt, diam non dictum sodales, nisl neque aliquet risus, pulvinar posuere lacus est a arcu.", +// "Fusce eu venenatis sapien, non aliquam massa.", ).map { MessageDigest.getInstance("SHA-256").digest(it.toByteArray()) } + private fun shuffleBits(data: ByteArray, seed: Int): ByteArray { + return data + + val rng = SimplePRNG(seed) + val bitList = mutableListOf() + val unshuffledBits = mutableListOf() + for (i in data.indices) { + for (bit in 0..7) { + if (i < data.size - 2) { + bitList.add((data[i].toInt() shr bit) and 1) + } else { + unshuffledBits.add((data[i].toInt() shr bit) and 1) + } + } + } + val indices = bitList.indices.toMutableList() + val shuffledBits = MutableList(bitList.size) { 0 } + val shuffleMap = indices.toMutableList() + val originalToShuffled = indices.toMutableList() + + for (i in indices.indices.reversed()) { + val j = rng.nextInt() % (i + 1) + shuffledBits[i] = bitList[shuffleMap[j]] + originalToShuffled[shuffleMap[j]] = i + shuffleMap.removeAt(j) + } + + val shuffledBytes = ByteArray(data.size) + for (i in shuffledBits.indices) { + shuffledBytes[i / 8] = (shuffledBytes[i / 8].toInt() or (shuffledBits[i] shl (i % 8))).toByte() + } + + // Restore the last two bytes without shuffling + for (i in 0 until 16) { + shuffledBytes[(data.size - 2) + (i / 8)] = (shuffledBytes[(data.size - 2) + (i / 8)].toInt() or (unshuffledBits[i] shl (i % 8))).toByte() + } + + return shuffledBytes + } + + private fun unshuffleBits(data: ByteArray, seed: Int): ByteArray { + return data + + val rng = SimplePRNG(seed) + val bitList = mutableListOf() + val unshuffledBits = mutableListOf() + for (i in data.indices) { + for (bit in 0..7) { + if (i < data.size - 2) { + bitList.add((data[i].toInt() shr bit) and 1) + } else { + unshuffledBits.add((data[i].toInt() shr bit) and 1) + } + } + } + val indices = bitList.indices.toMutableList() + val shuffleMap = indices.toMutableList() + val shuffledToOriginal = MutableList(bitList.size) { 0 } + + for (i in indices.indices.reversed()) { + val j = rng.nextInt() % (i + 1) + shuffledToOriginal[i] = shuffleMap[j] + shuffleMap.removeAt(j) + } + + val originalBits = MutableList(bitList.size) { 0 } + for (i in bitList.indices) { + originalBits[shuffledToOriginal[i]] = bitList[i] + } + + val originalBytes = ByteArray(data.size) + for (i in originalBits.indices) { + originalBytes[i / 8] = (originalBytes[i / 8].toInt() or (originalBits[i] shl (i % 8))).toByte() + } + + // Restore the last two bytes without unshuffling + for (i in 0 until 16) { + originalBytes[(data.size - 2) + (i / 8)] = (originalBytes[(data.size - 2) + (i / 8)].toInt() or (unshuffledBits[i] shl (i % 8))).toByte() + } + + return originalBytes + } + fun encode(itemID: ItemID, amountIndex: Int, isReusable: Boolean, receiver: UUID? = null, msgType: Int = 0, args: String = ""): String { // filter item ID val itemType = if (itemID.contains('@')) itemID.substringBefore("@") else "" @@ -64,7 +155,7 @@ object RedeemCodeMachine { val isShortCode = (unpaddedStr.length <= 60) - val bytes = ByteArray(if (isShortCode) 15 else 30) + var bytes = ByteArray(if (isShortCode) 15 else 30) // sync pattern and flags bytes[0] = (isReusable.toInt() or 0xA4).toByte() @@ -102,6 +193,8 @@ object RedeemCodeMachine { it.checksum().toInt() } + println("Encoding CRC: $crc16") + bytes[bytes.size - 2] = crc16.ushr(8).toByte() bytes[bytes.size - 1] = crc16.toByte() @@ -114,7 +207,7 @@ object RedeemCodeMachine { basePwd[i] = basePwd[i] xor receiverPwd[i % 16] } - return PasswordBase32.encode(bytes, basePwd) + return PasswordBase32.encode(shuffleBits(bytes, crc16), basePwd) } private fun UUID.toByteArray(): ByteArray { @@ -144,26 +237,32 @@ object RedeemCodeMachine { } // check which one of the 8 keys passes CRC test - val crcResults = decodeds.map { decoded -> + var key: Int? = null + val crcResults = decodeds.map { decoded0 -> + val crc0 = decoded0[decoded0.size - 2].toInt().shl(8) or decoded0[decoded0.size - 1].toInt() + val decoded = unshuffleBits(decoded0, crc0) val crc = Crc16().let { for (i in 0 until decoded.size - 2) { it.add_bits(decoded[i].toInt(), 8) } - it.checksum().toInt().and(0xFFFF) + it.checksum().toInt() } - val crc2 = decoded[decoded.size - 2].toUint().shl(8) or decoded[decoded.size - 1].toUint() - (crc == crc2) + println("Trying CRC $crc0 to $crc") + + ((crc == crc0) to decoded).also { + if (it.first) key = crc0 + } } // if all CRC fails... - if (crcResults.indexOf(true) < 0) { + if (key == null) { return null } + println("Decoding CRC: $key") - - val decoded = decodeds[crcResults.indexOf(true)] + val decoded = crcResults.first { it.first }.second val reusable = (decoded[0] and 1) != 0.toByte() diff --git a/src/net/torvald/terrarum/serialise/ByteUtils.kt b/src/net/torvald/terrarum/serialise/ByteUtils.kt index 87959c714..6e508eb6e 100644 --- a/src/net/torvald/terrarum/serialise/ByteUtils.kt +++ b/src/net/torvald/terrarum/serialise/ByteUtils.kt @@ -115,3 +115,33 @@ fun ByteArray.toBigInt64(offset: Int = 0): Long { } fun Byte.toUlong() = java.lang.Byte.toUnsignedLong(this) fun Byte.toUint() = java.lang.Byte.toUnsignedInt(this) + +fun ByteArray.writeBigInt16(value: Int, offset: Int = 0) { + for (i in 0..1) { + this[i + offset] = value.shr((1 - i) * 8).toByte() + } +} + +fun ByteArray.writeBigInt24(value: Int, offset: Int = 0) { + for (i in 0..2) { + this[i + offset] = value.shr((2 - i) * 8).toByte() + } +} + +fun ByteArray.writeBigInt32(value: Int, offset: Int = 0) { + for (i in 0..3) { + this[i + offset] = value.shr((3 - i) * 8).toByte() + } +} + +fun ByteArray.writeBigInt48(value: Long, offset: Int = 0) { + for (i in 0..5) { + this[i + offset] = value.shr((5 - i) * 8).toByte() + } +} + +fun ByteArray.writeBigInt64(value: Long, offset: Int = 0) { + for (i in 0..7) { + this[i + offset] = value.shr((7 - i) * 8).toByte() + } +} \ No newline at end of file