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
}*/
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.
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
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()

View File

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

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.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<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 {
// 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()

View File

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