token ring stuff wip

This commit is contained in:
minjaesong
2025-02-27 20:58:14 +09:00
parent 65f771e9de
commit e889b397d0
8 changed files with 303 additions and 20 deletions

View File

@@ -178,7 +178,7 @@ open class GameWorld(
it[Fluid.NULL] = 0 it[Fluid.NULL] = 0
}*/ }*/
val extraFields = HashMap<String, Any?>() val extraFields = HashMap<String, TerrarumSavegameExtrafieldSerialisable?>()
// 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. // 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 internal var comp = -1 // only gets used when the game saves and loads

View File

@@ -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

View File

@@ -1,6 +1,7 @@
package net.torvald.terrarum.modulebasegame.gameworld package net.torvald.terrarum.modulebasegame.gameworld
import net.torvald.terrarum.gameactors.ActorID 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, * 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. * Created by minjaesong on 2017-04-23.
*/ */
class GameEconomy { class GameEconomy : TerrarumSavegameExtrafieldSerialisable {
val transactionHistory = TransanctionHistory() val transactionHistory = TransanctionHistory()

View File

@@ -1,5 +1,6 @@
package net.torvald.terrarum.modulebasegame.gameworld package net.torvald.terrarum.modulebasegame.gameworld
import net.torvald.terrarum.gameworld.TerrarumSavegameExtrafieldSerialisable
import net.torvald.terrarum.modulebasegame.gameactors.FixtureInventory import net.torvald.terrarum.modulebasegame.gameactors.FixtureInventory
import kotlin.math.ceil import kotlin.math.ceil
@@ -9,7 +10,7 @@ import kotlin.math.ceil
* *
* Created by minjaesong on 2024-12-29. * Created by minjaesong on 2024-12-29.
*/ */
class GamePostalService { class GamePostalService : TerrarumSavegameExtrafieldSerialisable {
companion object { companion object {

View File

@@ -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)
}
}
}

View File

@@ -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<Int, IngameNetPacket>()
operator fun set(id: Int, packet: IngameNetPacket) {
ledger[id] = packet
}
operator fun get(id: Int) = ledger[id]!!
}

View File

@@ -4,7 +4,6 @@ import javazoom.jl.decoder.Crc16
import net.torvald.terrarum.gameitems.ItemID import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.serialise.toBig64 import net.torvald.terrarum.serialise.toBig64
import net.torvald.terrarum.serialise.toUint import net.torvald.terrarum.serialise.toUint
import net.torvald.terrarum.toHex
import net.torvald.terrarum.toInt import net.torvald.terrarum.toInt
import net.torvald.terrarum.utils.PasswordBase32 import net.torvald.terrarum.utils.PasswordBase32
import net.torvald.unicode.CURRENCY import net.torvald.unicode.CURRENCY
@@ -13,6 +12,15 @@ import java.util.*
import kotlin.experimental.and import kotlin.experimental.and
import kotlin.experimental.xor 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. * Created by minjaesong on 2025-02-13.
*/ */
@@ -38,15 +46,98 @@ object RedeemCodeMachine {
val initialPassword = listOf( // will be list of 256 bits of something val initialPassword = listOf( // will be list of 256 bits of something
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Nam nisl leo, semper a ligula a, sollicitudin congue turpis.", // "Nam nisl leo, semper a ligula a, sollicitudin congue turpis.",
"Aenean id malesuada nibh, vitae accumsan risus.", // "Aenean id malesuada nibh, vitae accumsan risus.",
"Morbi tempus velit et consequat vehicula.", // "Morbi tempus velit et consequat vehicula.",
"Integer varius turpis nec euismod mattis.", // "Integer varius turpis nec euismod mattis.",
"Vivamus dictum non ipsum vitae mollis.", // "Vivamus dictum non ipsum vitae mollis.",
"Quisque tincidunt, diam non dictum sodales, nisl neque aliquet risus, pulvinar posuere lacus est a arcu.", // "Quisque tincidunt, diam non dictum sodales, nisl neque aliquet risus, pulvinar posuere lacus est a arcu.",
"Fusce eu venenatis sapien, non aliquam massa.", // "Fusce eu venenatis sapien, non aliquam massa.",
).map { MessageDigest.getInstance("SHA-256").digest(it.toByteArray()) } ).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<Int>()
val unshuffledBits = mutableListOf<Int>()
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<Int>()
val unshuffledBits = mutableListOf<Int>()
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 { fun encode(itemID: ItemID, amountIndex: Int, isReusable: Boolean, receiver: UUID? = null, msgType: Int = 0, args: String = ""): String {
// filter item ID // filter item ID
val itemType = if (itemID.contains('@')) itemID.substringBefore("@") else "" val itemType = if (itemID.contains('@')) itemID.substringBefore("@") else ""
@@ -64,7 +155,7 @@ object RedeemCodeMachine {
val isShortCode = (unpaddedStr.length <= 60) 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 // sync pattern and flags
bytes[0] = (isReusable.toInt() or 0xA4).toByte() bytes[0] = (isReusable.toInt() or 0xA4).toByte()
@@ -102,6 +193,8 @@ object RedeemCodeMachine {
it.checksum().toInt() it.checksum().toInt()
} }
println("Encoding CRC: $crc16")
bytes[bytes.size - 2] = crc16.ushr(8).toByte() bytes[bytes.size - 2] = crc16.ushr(8).toByte()
bytes[bytes.size - 1] = crc16.toByte() bytes[bytes.size - 1] = crc16.toByte()
@@ -114,7 +207,7 @@ object RedeemCodeMachine {
basePwd[i] = basePwd[i] xor receiverPwd[i % 16] 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 { private fun UUID.toByteArray(): ByteArray {
@@ -144,26 +237,32 @@ object RedeemCodeMachine {
} }
// check which one of the 8 keys passes CRC test // 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 { val crc = Crc16().let {
for (i in 0 until decoded.size - 2) { for (i in 0 until decoded.size - 2) {
it.add_bits(decoded[i].toInt(), 8) 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 all CRC fails...
if (crcResults.indexOf(true) < 0) { if (key == null) {
return null return null
} }
println("Decoding CRC: $key")
val decoded = crcResults.first { it.first }.second
val decoded = decodeds[crcResults.indexOf(true)]
val reusable = (decoded[0] and 1) != 0.toByte() val reusable = (decoded[0] and 1) != 0.toByte()

View File

@@ -115,3 +115,33 @@ fun ByteArray.toBigInt64(offset: Int = 0): Long {
} }
fun Byte.toUlong() = java.lang.Byte.toUnsignedLong(this) fun Byte.toUlong() = java.lang.Byte.toUnsignedLong(this)
fun Byte.toUint() = java.lang.Byte.toUnsignedInt(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()
}
}