mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 20:31:51 +09:00
token ring stuff wip
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]!!
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user