redeem code gen wip

This commit is contained in:
minjaesong
2025-02-15 16:18:30 +09:00
parent 3d5672bc4d
commit c2d0803ee3
2 changed files with 198 additions and 58 deletions

View File

@@ -1,58 +0,0 @@
package net.torvald.terrarum.modulebasegame.redeemable
import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.serialise.toBig64
import net.torvald.terrarum.toInt
import net.torvald.terrarum.utils.PasswordBase32
import java.util.*
/**
* Created by minjaesong on 2025-02-13.
*/
object GenerateRedeemCode {
private val itemTypeTable = hashMapOf(
"" to 0,
"wall" to 1,
"item" to 2,
"wire" to 3
)
private val moduleTable = hashMapOf(
"basegame" to 0,
"dwarventech" to 1,
)
operator fun invoke(itemID: ItemID, amountIndex: Int, isUnique: Boolean, receiver: UUID? = null, msgType: Int = 0, arg1: String = "", arg2: String = ""): String {
// filter item ID
val itemType = itemID.substringBefore("@")
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 bytes = ByteArray(240)
// sync pattern and flags
bytes[0] = (isUnique.toInt() or (receiver != null).toInt(1) or 0xA0).toByte()
bytes[1] = 0xA5.toByte()
// compressed item name
bytes[2] = (itemTypeTable[itemType]!! or moduleTable[itemModule]!!.shl(2) or amountIndex.and(15).shl(4)).toByte() // 0b nnnn mm cc
bytes[3] = itemNumber.toByte() // 0b item number low
bytes[4] = itemNumber.ushr(8).toByte()// 0b item number high
// TODO ascii to baudot
return PasswordBase32.encode(bytes, receiver?.toByteArray() ?: ByteArray(0))
}
private fun UUID.toByteArray(): ByteArray {
return this.mostSignificantBits.toBig64() + this.leastSignificantBits.toBig64()
}
}

View File

@@ -0,0 +1,198 @@
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
/**
* 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
"N'Gasta! Kvaka! Kvakis! ahkstas ",
"so novajxletero (oix jhemile) so",
"Ranetauw. Ricevas gxin pagintaj ",
"membrauw kaj aliaj individuauw, ",
"kiujn iamaniere tusxas so raneta",
" aktivado. En gxi aperas informa",
"uw unuavice pri so lokauw so cxi",
"umonataj kunvenauw, sed nature a"
).map { MessageDigest.getInstance("SHA-256").digest(it.toByteArray()) }
fun encode(itemID: ItemID, amountIndex: Int, isUnique: Boolean, receiver: UUID? = null, msgType: Int = 0, arg1: String = "", arg2: String = ""): String {
// filter item ID
val itemType = itemID.substringBefore("@")
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 isShortCode = true
val bytes = ByteArray(isShortCode.toInt().plus(1) * 15)
// sync pattern and flags
bytes[0] = (isUnique.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
// TODO ascii to baudot
val crc16 = Crc16().let {
for (i in 0 until bytes.size - 2) {
it.add_bits(bytes[i].toInt(), 8)
}
it.checksum().toInt()
}
bytes[bytes.size - 2] = crc16.ushr(8).toByte()
bytes[bytes.size - 1] = crc16.toByte()
val basePwd = initialPassword[bytes[bytes.size - 1].toUint().shr(2).and(7)]
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(bytes, 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
// 0x [byte 29] [byte 30]
val crc = PasswordBase32.decode(codeStr.substring(codeStr.length - 8), 5).let {
it[3].toUint().shl(8) or it[4].toUint()
}
val basePwdIndex = crc.shr(2).and(7)
val password = initialPassword[basePwdIndex].let { basePwd ->
ByteArray(32) { i ->
basePwd[i] xor receiverPwd[i % 16]
}
}
// try to decode the input string
val decoded = PasswordBase32.decode(codeStr, if (codeStr.length > 24) 30 else 15, password)
// check CRC
val crc2 = decoded[decoded.size - 2].toUint().shl(8) or decoded[decoded.size - 1].toUint()
// if CRC fails...
if (crc != crc2)
return null
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
// TODO baudot to ascii
return RedeemVoucher(itemID, itemAmount, reusable, "", listOf())
}
data class RedeemVoucher(val itemID: ItemID, val amount: Int, val reusable: Boolean, val msgTemplate: String, 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
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 {
TODO()
}
}