mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 20:31:51 +09:00
361 lines
13 KiB
Kotlin
361 lines
13 KiB
Kotlin
package net.torvald.terrarum.modulebasegame.redeemable
|
|
|
|
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.toInt
|
|
import net.torvald.terrarum.utils.PasswordBase32
|
|
import net.torvald.unicode.CURRENCY
|
|
import java.security.MessageDigest
|
|
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.
|
|
*/
|
|
object RedeemCodeMachine {
|
|
|
|
private val itemTypeTable = hashMapOf(
|
|
"" to 0,
|
|
"wall" to 1,
|
|
"item" to 2,
|
|
"wire" to 3
|
|
)
|
|
|
|
private val itemTypeTableReverse = itemTypeTable.entries.associate { it.value to it.key }
|
|
|
|
private val moduleTable = hashMapOf(
|
|
"basegame" to 0,
|
|
"dwarventech" to 1,
|
|
)
|
|
|
|
private val moduleTableReverse = moduleTable.entries.associate { it.value to it.key }
|
|
|
|
private val amountIndexToAmount = intArrayOf(1,2,3,4,5,6,10,12,15,20,24,32,50,100,250,500)
|
|
|
|
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.",
|
|
).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 ""
|
|
val (itemModule, itemNumber0) = itemID.substringAfter("@").split(":")
|
|
val itemNumber = itemNumber0.toInt()
|
|
|
|
if (itemType !in itemTypeTable.keys)
|
|
throw IllegalArgumentException("Unsupported type for ItemID: $itemID")
|
|
if (itemModule !in moduleTable.keys)
|
|
throw IllegalArgumentException("Unsupported module for ItemID: $itemID")
|
|
if (itemNumber !in 0..65535)
|
|
throw IllegalArgumentException("Unsupported item number for ItemID: $itemID")
|
|
|
|
val unpaddedStr = asciiToBaudot(args)
|
|
|
|
val isShortCode = (unpaddedStr.length <= 60)
|
|
|
|
var bytes = ByteArray(if (isShortCode) 15 else 30)
|
|
|
|
// sync pattern and flags
|
|
bytes[0] = (isReusable.toInt() or 0xA4).toByte()
|
|
bytes[1] = 0xA5.toByte()
|
|
// compressed item name
|
|
// 0b nnnn mm cc
|
|
bytes[2] = (itemTypeTable[itemType]!! or moduleTable[itemModule]!!.shl(2) or amountIndex.and(15).shl(4)).toByte()
|
|
bytes[3] = itemNumber.toByte() // 0b item number low
|
|
bytes[4] = itemNumber.ushr(8).toByte()// 0b item number high
|
|
|
|
// convert ascii to baudot
|
|
val paddedTextBits = (unpaddedStr).let {
|
|
// then fill the remainder with random bits
|
|
val remaining = (if (isShortCode) 60 else 180) - it.length
|
|
it + StringBuilder().also { sb ->
|
|
sb.append("00000") // add null terminator
|
|
repeat(remaining - 5) { sb.append((Math.random() < 0.5).toInt()) } // add random bits
|
|
}.toString()
|
|
}
|
|
|
|
// fill upper nybble of bytes[5] first
|
|
// lower nybble will have msgType
|
|
bytes[5] = ((msgType and 15) or (paddedTextBits.substring(0, 4).toInt(2).shl(4))).toByte()
|
|
|
|
var c = 4
|
|
for (i in 6 until bytes.size - 2) {
|
|
bytes[i] = paddedTextBits.substring(c, c + 8).toInt(2).toByte()
|
|
c += 8
|
|
}
|
|
|
|
val crc16 = Crc16().let {
|
|
for (i in 0 until bytes.size - 2) {
|
|
it.add_bits(bytes[i].toInt(), 8)
|
|
}
|
|
it.checksum().toInt()
|
|
}
|
|
|
|
println("Encoding CRC: $crc16")
|
|
|
|
bytes[bytes.size - 2] = crc16.ushr(8).toByte()
|
|
bytes[bytes.size - 1] = crc16.toByte()
|
|
|
|
|
|
val basePwd = initialPassword.random().copyOf()
|
|
val receiverPwd = receiver?.toByteArray() ?: ByteArray(16) // 128 bits of something
|
|
|
|
// xor basePWD with receiverPwd
|
|
for (i in 0 until 32) {
|
|
basePwd[i] = basePwd[i] xor receiverPwd[i % 16]
|
|
}
|
|
|
|
return PasswordBase32.encode(shuffleBits(bytes, crc16), basePwd)
|
|
}
|
|
|
|
private fun UUID.toByteArray(): ByteArray {
|
|
return this.mostSignificantBits.toBig64() + this.leastSignificantBits.toBig64()
|
|
}
|
|
|
|
fun decode(codeStr: String, decoderUUID: UUID? = null): RedeemVoucher? {
|
|
val receiverPwd = decoderUUID?.toByteArray() ?: ByteArray(16) // 128 bits of something
|
|
|
|
// for decrypting targeted code
|
|
val passwords1 = initialPassword.map { basePwd ->
|
|
ByteArray(32) { i ->
|
|
basePwd[i] xor receiverPwd[i % 16]
|
|
}
|
|
}
|
|
// for decrypting generic code
|
|
val passwords2 = initialPassword.map { basePwd ->
|
|
ByteArray(32) { i ->
|
|
basePwd[i]
|
|
}
|
|
}
|
|
val passwords = passwords1 + passwords2
|
|
|
|
// try to decode the input string by just trying all 8 possible keys
|
|
val decodeds = passwords.map {
|
|
PasswordBase32.decode(codeStr, if (codeStr.length > 24) 30 else 15, it)
|
|
}
|
|
|
|
// check which one of the 8 keys passes CRC test
|
|
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()
|
|
}
|
|
|
|
println("Trying CRC $crc0 to $crc")
|
|
|
|
((crc == crc0) to decoded).also {
|
|
if (it.first) key = crc0
|
|
}
|
|
}
|
|
|
|
// if all CRC fails...
|
|
if (key == null) {
|
|
return null
|
|
}
|
|
|
|
println("Decoding CRC: $key")
|
|
|
|
val decoded = crcResults.first { it.first }.second
|
|
|
|
val reusable = (decoded[0] and 1) != 0.toByte()
|
|
|
|
val itemCategory = itemTypeTableReverse[decoded[2].toUint() and 3]!!
|
|
val itemModule = moduleTableReverse[decoded[2].toUint().ushr(2) and 3]!!
|
|
val itemAmount = amountIndexToAmount[decoded[2].toUint().ushr(4)]
|
|
val itemNumber = decoded[3].toUint() or decoded[4].toUint().shl(8)
|
|
|
|
val itemID = ("$itemModule:$itemNumber").let { if (itemCategory.isNotBlank()) "$itemCategory@$it" else it }
|
|
|
|
val messageTemplateIndex = decoded[5].toUint() and 15
|
|
|
|
// baudot to ascii
|
|
val baudotBits = StringBuilder().also {
|
|
it.append(decoded[5].toUint().ushr(4).toString(2).padStart(4,'0'))
|
|
for (i in 6 until decoded.size - 2) {
|
|
it.append(decoded[i].toUint().toString(2).padStart(8,'0'))
|
|
}
|
|
}.toString()
|
|
val decodedStr = baudotToAscii(baudotBits).substringBefore('\u0000') // remove trailing random bits
|
|
|
|
return RedeemVoucher(itemID, itemAmount, reusable, messageTemplateIndex, decodedStr.split('\n'))
|
|
}
|
|
|
|
data class RedeemVoucher(val itemID: ItemID, val amount: Int, val reusable: Boolean, val msgTemplateIndex: Int, val args: List<String>)
|
|
|
|
|
|
val tableLtrs = "\u0000E\nA SIU\rDRJNFCKTZLWHYPQOBG\u000FMXV\u000E"
|
|
val tableFigs = "\u00003\n- '87\r\u00054\u0007,!:(5+)2${CURRENCY}6019?&\u000F./;\u000E"
|
|
|
|
val setLtrs = (tableLtrs).toHashSet()
|
|
val setFigs = tableFigs.toHashSet()
|
|
|
|
val codeLtrs = tableLtrs.mapIndexed { index: Int, c: Char -> c to index.toString(2).padStart(5,'0') }.toMap()
|
|
val codeFigs = tableFigs.mapIndexed { index: Int, c: Char -> c to index.toString(2).padStart(5,'0') }.toMap()
|
|
|
|
val basket = arrayOf(codeLtrs, codeFigs) // think of it as a "type basket" of a typewriter
|
|
val basket2 = arrayOf(tableLtrs, tableFigs) // think of it as a "type basket" of a typewriter
|
|
|
|
private fun isInLtrs(char: Char) = char in setLtrs
|
|
private fun isInFigs(char: Char) = char in setFigs
|
|
private fun nextShift(char: Char) = if (isInLtrs(char)) 0 else if (isInFigs(char)) 1 else null
|
|
|
|
val shiftCode = arrayOf("11111", "11011")
|
|
|
|
private fun asciiToBaudot(instr: String): String {
|
|
val sb = StringBuilder()
|
|
|
|
var currentShift = 0 // initially ltrs
|
|
var cursor = 0
|
|
|
|
while (cursor < instr.length) {
|
|
val char = instr[cursor].uppercaseChar()
|
|
val nextShift = nextShift(char)
|
|
|
|
// decide if a shift must prepend
|
|
if (nextShift != null && currentShift != nextShift) {
|
|
sb.append(shiftCode[nextShift])
|
|
currentShift = nextShift
|
|
}
|
|
|
|
// append baudot code into the string builder
|
|
if (nextShift != null) {
|
|
sb.append(basket[currentShift][char])
|
|
}
|
|
|
|
// advance to the next character
|
|
cursor += 1
|
|
}
|
|
|
|
return sb.toString()
|
|
}
|
|
|
|
private fun baudotToAscii(binaryStr: String): String {
|
|
val sb = StringBuilder()
|
|
|
|
var currentShift = 0 // initially ltrs
|
|
var cursor = 0
|
|
|
|
while (cursor < binaryStr.length) {
|
|
val number = binaryStr.substring(cursor, cursor + 5).toInt(2)
|
|
|
|
if (number == 0b11011)
|
|
currentShift = 1
|
|
else if (number == 0b11111)
|
|
currentShift = 0
|
|
else
|
|
sb.append(basket2[currentShift][number])
|
|
|
|
cursor += 5
|
|
}
|
|
|
|
return sb.toString()
|
|
}
|
|
|
|
} |