Add ViaAxiom
Deploy / Build (push) Skipped
Pull Request Build / Build (pull_request) Failing after 17s

This commit is contained in:
Jakob Schulz
2026-11-01 15:30:21 +01:00
parent e198772885
commit f373e3d6d9
76 changed files with 4496 additions and 896 deletions
+37
View File
@@ -0,0 +1,37 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
}
base {
archivesName.set("viaaxiom-core")
}
// Velocity can run on Java 21, so the shared core cannot target the Fabric mod's Java 25.
val targetJavaVersion = 21
repositories {
mavenCentral()
}
dependencies {
api("com.github.luben:zstd-jni:1.5.5-4")
testImplementation(kotlin("test"))
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(targetJavaVersion.toString()))
freeCompilerArgs.add("-Xjdk-release=$targetJavaVersion")
}
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(targetJavaVersion)
}
tasks.test {
useJUnitPlatform()
}
@@ -0,0 +1,149 @@
package me.wargearsystems.viaaxiom.binary
import java.lang.invoke.MethodHandles
import java.lang.invoke.VarHandle
import java.nio.ByteOrder
import java.nio.charset.StandardCharsets
class AxiomByteReader(
private val bytes: ByteArray,
) {
var readerIndex: Int = 0
private set
val readableBytes: Int
get() = bytes.size - readerIndex
fun isReadable(): Boolean = readableBytes > 0
fun readByte(): Byte {
requireReadable(1)
return bytes[readerIndex++]
}
fun readUnsignedByte(): Int = readByte().toInt() and 0xFF
fun readBoolean(): Boolean = readUnsignedByte() != 0
fun readShort(): Short {
requireReadable(Short.SIZE_BYTES)
val value = ((bytes[readerIndex].toInt() and 0xFF) shl 8) or
(bytes[readerIndex + 1].toInt() and 0xFF)
readerIndex += Short.SIZE_BYTES
return value.toShort()
}
fun readInt(): Int {
requireReadable(Int.SIZE_BYTES)
val value = ((bytes[readerIndex].toInt() and 0xFF) shl 24) or
((bytes[readerIndex + 1].toInt() and 0xFF) shl 16) or
((bytes[readerIndex + 2].toInt() and 0xFF) shl 8) or
(bytes[readerIndex + 3].toInt() and 0xFF)
readerIndex += Int.SIZE_BYTES
return value
}
fun readLong(): Long {
requireReadable(Long.SIZE_BYTES)
var value = 0L
repeat(Long.SIZE_BYTES) {
value = (value shl 8) or (bytes[readerIndex++].toLong() and 0xFFL)
}
return value
}
fun readLongArray(): LongArray {
return readLongArray(readVarInt())
}
fun readLongArray(length: Int): LongArray {
if (length < 0) {
throw AxiomByteCodecException("Long array length cannot be negative: $length")
}
if (length > readableBytes / Long.SIZE_BYTES) {
throw AxiomByteCodecException("Long array exceeds remaining packet bytes")
}
val values = LongArray(length)
for (index in values.indices) {
values[index] = BIG_ENDIAN_LONGS.get(bytes, readerIndex) as Long
readerIndex += Long.SIZE_BYTES
}
return values
}
fun readVarInt(): Int {
var value = 0
var position = 0
while (position < 32) {
val currentByte = readUnsignedByte()
value = value or ((currentByte and 0x7F) shl position)
if ((currentByte and 0x80) == 0) {
return value
}
position += 7
}
throw AxiomByteCodecException("VarInt is too large")
}
fun readByteArray(maxLength: Int = Int.MAX_VALUE): ByteArray {
val length = readVarInt()
if (length > maxLength) {
throw AxiomByteCodecException("Byte array is too long: $length bytes")
}
return readBytes(length)
}
fun readUtf(maxLength: Int = 32767): String {
val byteLength = readVarInt()
if (byteLength < 0) {
throw AxiomByteCodecException("String byte length cannot be negative: $byteLength")
}
if (byteLength > maxLength * 4) {
throw AxiomByteCodecException("Encoded string is too long: $byteLength bytes")
}
val value = String(readBytes(byteLength), StandardCharsets.UTF_8)
if (value.length > maxLength) {
throw AxiomByteCodecException("Decoded string is too long: ${value.length} characters")
}
return value
}
fun readBytes(length: Int): ByteArray {
if (length < 0) {
throw AxiomByteCodecException("Byte length cannot be negative: $length")
}
requireReadable(length)
val value = bytes.copyOfRange(readerIndex, readerIndex + length)
readerIndex += length
return value
}
fun skipBytes(length: Int) {
if (length < 0) {
throw AxiomByteCodecException("Skip length cannot be negative: $length")
}
requireReadable(length)
readerIndex += length
}
fun copyRemaining(): ByteArray = readBytes(readableBytes)
private fun requireReadable(length: Int) {
if (readableBytes < length) {
throw AxiomByteCodecException("Need $length readable bytes, only $readableBytes available")
}
}
}
internal val BIG_ENDIAN_LONGS: VarHandle =
MethodHandles.byteArrayViewVarHandle(LongArray::class.java, ByteOrder.BIG_ENDIAN)
class AxiomByteCodecException(message: String) : IllegalArgumentException(message)
@@ -0,0 +1,98 @@
package me.wargearsystems.viaaxiom.binary
import java.nio.charset.StandardCharsets
class AxiomByteWriter(initialCapacity: Int = 256) {
private var buffer = ByteArray(initialCapacity.coerceAtLeast(16))
var size: Int = 0
private set
fun writeByte(value: Int): AxiomByteWriter {
ensureCapacity(1)
buffer[size++] = value.toByte()
return this
}
fun writeBoolean(value: Boolean): AxiomByteWriter = writeByte(if (value) 1 else 0)
fun writeShort(value: Int): AxiomByteWriter {
writeByte(value ushr 8)
writeByte(value)
return this
}
fun writeInt(value: Int): AxiomByteWriter {
writeByte(value ushr 24)
writeByte(value ushr 16)
writeByte(value ushr 8)
writeByte(value)
return this
}
fun writeLong(value: Long): AxiomByteWriter {
for (shift in 56 downTo 0 step 8) {
writeByte((value ushr shift).toInt())
}
return this
}
fun writeLongArray(values: LongArray, includeLength: Boolean = true): AxiomByteWriter {
if (includeLength) {
writeVarInt(values.size)
}
ensureCapacity(values.size * Long.SIZE_BYTES)
for (value in values) {
BIG_ENDIAN_LONGS.set(buffer, size, value)
size += Long.SIZE_BYTES
}
return this
}
fun writeVarInt(value: Int): AxiomByteWriter {
var remaining = value
while ((remaining and -0x80) != 0) {
writeByte((remaining and 0x7F) or 0x80)
remaining = remaining ushr 7
}
writeByte(remaining)
return this
}
fun writeUtf(value: String, maxLength: Int = 32767): AxiomByteWriter {
if (value.length > maxLength) {
throw AxiomByteCodecException("String is too long: ${value.length} characters")
}
val encoded = value.toByteArray(StandardCharsets.UTF_8)
if (encoded.size > maxLength * 4) {
throw AxiomByteCodecException("Encoded string is too long: ${encoded.size} bytes")
}
writeVarInt(encoded.size)
writeBytes(encoded)
return this
}
fun writeByteArray(bytes: ByteArray): AxiomByteWriter {
writeVarInt(bytes.size)
writeBytes(bytes)
return this
}
fun writeBytes(bytes: ByteArray): AxiomByteWriter {
ensureCapacity(bytes.size)
bytes.copyInto(buffer, size)
size += bytes.size
return this
}
fun toByteArray(): ByteArray = buffer.copyOf(size)
private fun ensureCapacity(additional: Int) {
val required = size + additional
if (required > buffer.size) {
buffer = buffer.copyOf(maxOf(required, buffer.size * 2))
}
}
}
@@ -0,0 +1,56 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
object AxiomCustomBlockCodec {
fun rewrite(bytes: ByteArray, mapper: (Int) -> Int): ByteArray {
val reader = AxiomByteReader(bytes)
val writer = AxiomByteWriter()
writer.writeUtf(reader.readUtf()) // identifier
writer.writeUtf(reader.readUtf()) // translation key
val properties = count(reader, writer)
repeat(properties) {
val type = reader.readUnsignedByte()
writer.writeByte(type)
when (type) {
0 -> writer.writeUtf(reader.readUtf())
1 -> writer.writeUtf(reader.readUtf()).writeInt(reader.readInt()).writeInt(reader.readInt())
2 -> {
writer.writeUtf(reader.readUtf())
repeat(count(reader, writer)) { writer.writeUtf(reader.readUtf()) }
}
in 3..15 -> Unit
else -> error("Unsupported Axiom custom block property $type")
}
}
repeat(count(reader, writer)) { state(reader, writer, mapper) }
// Item components differ per version and have no codec here yet.
require(!reader.readBoolean()) { "Cross-version Axiom custom block item icons are not supported yet" }
writer.writeBoolean(false)
repeat(3) { writer.writeBoolean(reader.readBoolean()) }
repeat(count(reader, writer)) { writer.writeUtf(reader.readUtf()) }
writer.writeBoolean(reader.readBoolean())
repeat(4) {
repeat(count(reader, writer)) { state(reader, writer, mapper); state(reader, writer, mapper) }
}
require(count(reader, writer) == 0 && reader.readableBytes == 0) { "Unknown Axiom custom block extension" }
return writer.toByteArray()
}
private fun count(reader: AxiomByteReader, writer: AxiomByteWriter): Int {
val count = reader.readVarInt()
require(count in 0..65_536) { "Invalid custom block collection count" }
writer.writeVarInt(count)
return count
}
private fun state(reader: AxiomByteReader, writer: AxiomByteWriter, mapper: (Int) -> Int) {
val id = reader.readVarInt()
if (id == -1) writer.writeVarInt(-1).writeUtf(reader.readUtf())
else {
require(id >= 0) { "Invalid custom block state ID" }
writer.writeVarInt(mapper(id))
}
}
}
@@ -0,0 +1,45 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
data class AxiomCustomPayloadFrame(
val packetId: Int,
val channel: String,
val payloadBytes: ByteArray,
)
object AxiomCustomPayloadFrameCodec {
fun read(frameBytes: ByteArray): AxiomCustomPayloadFrame {
val reader = AxiomByteReader(frameBytes)
return AxiomCustomPayloadFrame(
packetId = reader.readVarInt(),
channel = reader.readUtf(),
payloadBytes = reader.copyRemaining(),
)
}
fun readIfCustomPayload(frameBytes: ByteArray, customPayloadPacketId: Int): AxiomCustomPayloadFrame? {
val reader = AxiomByteReader(frameBytes)
val packetId = reader.readVarInt()
if (packetId != customPayloadPacketId) return null
return AxiomCustomPayloadFrame(packetId, reader.readUtf(), reader.copyRemaining())
}
fun write(frame: AxiomCustomPayloadFrame): ByteArray {
return AxiomByteWriter()
.writeVarInt(frame.packetId)
.writeUtf(frame.channel)
.writeBytes(frame.payloadBytes)
.toByteArray()
}
fun rewriteIfCustomPayload(
frameBytes: ByteArray,
customPayloadPacketId: Int,
rewriter: (AxiomCustomPayloadFrame) -> ByteArray,
): ByteArray {
val frame = readIfCustomPayload(frameBytes, customPayloadPacketId) ?: return frameBytes
return write(frame.copy(payloadBytes = rewriter(frame)))
}
}
@@ -0,0 +1,63 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
object AxiomEnableCodec {
fun read(bytes: ByteArray, apiVersion: Int = AxiomProtocol.LEGACY_API_VERSION): AxiomEnable {
val reader = AxiomByteReader(bytes)
return read(reader, apiVersion)
}
fun read(reader: AxiomByteReader, apiVersion: Int = AxiomProtocol.LEGACY_API_VERSION): AxiomEnable {
require(apiVersion == AxiomProtocol.LEGACY_API_VERSION || apiVersion == AxiomProtocol.API_VERSION) {
"Unsupported Axiom API version: $apiVersion"
}
val enabled = reader.readBoolean()
if (!enabled) return AxiomEnable(false, 0, 0, 0, 0)
if (apiVersion == AxiomProtocol.LEGACY_API_VERSION) {
return AxiomEnable(true, reader.readInt(), reader.readVarInt(), reader.readVarInt(), reader.readVarInt())
}
val configVersion = reader.readUnsignedByte()
require(configVersion == 0) { "Unsupported Axiom server config version: $configVersion" }
val blueprintVersion = reader.readVarInt()
val splitSize = reader.readInt()
val maximumSize = reader.readInt()
require(splitSize > 1 && maximumSize > 0) { "Invalid Axiom tunnel limits" }
// Upstream always sends these empty, and their format is unknown.
val customDataCount = reader.readVarInt()
require(customDataCount == 0) { "Axiom custom data overrides are not supported" }
val rotationCount = reader.readVarInt()
require(rotationCount == 0) { "Axiom rotation overrides are not supported" }
val channelCount = reader.readVarInt()
require(channelCount in 0..1024) { "Invalid Axiom channel count: $channelCount" }
val channels = List(channelCount) { reader.readUtf() }
require(reader.readableBytes == 0) { "Trailing bytes in API 10 enable" }
return AxiomEnable(
enabled = true, blueprintVersion = blueprintVersion,
customDataOverridesCount = customDataCount, rotationOverridesCount = rotationCount,
serverConfigVersion = configVersion, tunnelSplitSize = splitSize,
maximumTunnelPacketSize = maximumSize, supportedServerboundChannels = channels,
)
}
fun write(enable: AxiomEnable, apiVersion: Int = AxiomProtocol.LEGACY_API_VERSION): ByteArray {
require(apiVersion == AxiomProtocol.LEGACY_API_VERSION || apiVersion == AxiomProtocol.API_VERSION)
val writer = AxiomByteWriter().writeBoolean(enable.enabled)
if (!enable.enabled) return writer.toByteArray()
if (apiVersion == AxiomProtocol.LEGACY_API_VERSION) {
return writer.writeInt(enable.maxBufferSize).writeVarInt(enable.blueprintVersion)
.writeVarInt(enable.customDataOverridesCount).writeVarInt(enable.rotationOverridesCount).toByteArray()
}
require(enable.serverConfigVersion == 0)
require(enable.customDataOverridesCount == 0 && enable.rotationOverridesCount == 0)
val splitSize = requireNotNull(enable.tunnelSplitSize)
val maximumSize = requireNotNull(enable.maximumTunnelPacketSize)
require(splitSize > 1 && maximumSize > 0)
require(enable.supportedServerboundChannels.size <= 1024)
writer.writeByte(0).writeVarInt(enable.blueprintVersion).writeInt(splitSize).writeInt(maximumSize)
.writeVarInt(0).writeVarInt(0).writeVarInt(enable.supportedServerboundChannels.size)
enable.supportedServerboundChannels.forEach { writer.writeUtf(it) }
return writer.toByteArray()
}
}
@@ -0,0 +1,36 @@
package me.wargearsystems.viaaxiom.protocol
data class AxiomHello(
val apiVersion: Int,
val dataVersion: Int,
val protocolVersion: Int,
val handshakeId: Long? = null,
) {
val isSupportedApi: Boolean
get() = apiVersion == AxiomProtocol.LEGACY_API_VERSION || apiVersion == AxiomProtocol.API_VERSION
}
data class AxiomEnable(
val enabled: Boolean,
val maxBufferSize: Int = 0,
val blueprintVersion: Int,
val customDataOverridesCount: Int,
val rotationOverridesCount: Int,
val serverConfigVersion: Int? = null,
val tunnelSplitSize: Int? = null,
val maximumTunnelPacketSize: Int? = null,
val supportedServerboundChannels: List<String> = emptyList(),
)
data class AxiomTranslationSession(
val hello: AxiomHello,
val serverProtocolVersion: Int,
val serverDataVersion: Int? = null,
val enable: AxiomEnable? = null,
) {
val protocolMismatch: Boolean
get() = hello.protocolVersion != serverProtocolVersion
val dataVersionMismatch: Boolean
get() = serverDataVersion != null && hello.dataVersion != serverDataVersion
}
@@ -0,0 +1,62 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
class AxiomHandshakeTracker {
var hello: AxiomHello? = null
private set
var enable: AxiomEnable? = null
private set
var challengeId: Long? = null
private set
var goodbyeReason: String? = null
private set
fun observe(direction: AxiomPayloadDirection, channel: String, bytes: ByteArray) {
when (AxiomPayloadKey(direction, channel)) {
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, AxiomProtocol.channel("hello")) -> {
reset()
require(bytes.size == Long.SIZE_BYTES) { "Invalid Axiom handshake challenge" }
challengeId = AxiomByteReader(bytes).readLong()
}
AxiomPayloadKey(AxiomPayloadDirection.SERVERBOUND, AxiomProtocol.channel("hello")) -> {
hello = null
enable = null
val candidate = AxiomHelloCodec.read(bytes)
if (candidate.apiVersion == AxiomProtocol.API_VERSION) {
require(challengeId != null && candidate.handshakeId == challengeId) {
"Axiom hello does not match the server handshake challenge"
}
} else {
require(challengeId == null) { "API 9 hello cannot answer an API 10 challenge" }
}
hello = candidate
goodbyeReason = null
}
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, AxiomProtocol.channel("enable")) -> {
enable = null
val apiVersion = requireNotNull(hello) { "Axiom enable received before client hello" }.apiVersion
enable = AxiomEnableCodec.read(bytes, apiVersion)
}
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, AxiomProtocol.channel("redo_handshake")) -> reset()
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, AxiomProtocol.channel("goodbye")) -> {
reset()
goodbyeReason = AxiomByteReader(bytes).readUtf()
}
}
}
fun session(serverProtocolVersion: Int?, serverDataVersion: Int? = null): AxiomTranslationSession? {
val hello = hello ?: return null
val enable = enable ?: return null
if (!enable.enabled || serverProtocolVersion == null) return null
return AxiomTranslationSession(hello, serverProtocolVersion, serverDataVersion, enable)
}
fun reset() {
hello = null
enable = null
challengeId = null
goodbyeReason = null
}
}
@@ -0,0 +1,38 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
object AxiomHelloCodec {
fun read(bytes: ByteArray): AxiomHello {
val reader = AxiomByteReader(bytes)
val hello = read(reader)
require(reader.readableBytes == 0) { "Trailing bytes in Axiom hello" }
return hello
}
fun read(reader: AxiomByteReader): AxiomHello {
val apiVersion = reader.readVarInt()
require(apiVersion == AxiomProtocol.LEGACY_API_VERSION || apiVersion == AxiomProtocol.API_VERSION) {
"Unsupported Axiom API version: $apiVersion"
}
val dataVersion = reader.readVarInt()
val protocolVersion = reader.readVarInt()
val handshakeId = if (apiVersion == AxiomProtocol.API_VERSION) reader.readLong() else null
return AxiomHello(apiVersion, dataVersion, protocolVersion, handshakeId)
}
fun write(hello: AxiomHello): ByteArray {
require(hello.isSupportedApi) { "Unsupported Axiom API version: ${hello.apiVersion}" }
val writer = AxiomByteWriter()
.writeVarInt(hello.apiVersion)
.writeVarInt(hello.dataVersion)
.writeVarInt(hello.protocolVersion)
if (hello.apiVersion == AxiomProtocol.API_VERSION) {
writer.writeLong(requireNotNull(hello.handshakeId) { "API 10 hello requires a handshake ID" })
} else {
require(hello.handshakeId == null) { "API 9 hello cannot contain a handshake ID" }
}
return writer.toByteArray()
}
}
@@ -0,0 +1,120 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import me.wargearsystems.viaaxiom.translation.AxiomPayload
import me.wargearsystems.viaaxiom.translation.AxiomPayloadTranslator
import java.util.UUID
/** API 10 client <-> API 9 server. The server still decides permissions and activation. */
class AxiomLegacyBridge(
private val serverProtocolVersion: Int,
private val translator: AxiomPayloadTranslator,
private val challengeSource: () -> Long = { UUID.randomUUID().mostSignificantBits },
) {
private val channels = linkedSetOf<String>()
private var modernServer = false
private var challenge: Long? = null
private var hello: AxiomHello? = null
private var enable: AxiomEnable? = null
private val tunnel = AxiomTunnelCollector()
val awaitingLegacyChallenge: Boolean
get() = !modernServer && challenge == null && "axiom:hello" in channels && "axiom:tunnel" !in channels
val active: Boolean get() = !modernServer && challenge != null
fun register(bytes: ByteArray) {
require(bytes.size <= 131_072) { "Oversized channel registration" }
channels.addAll(bytes.toString(Charsets.UTF_8).split('\u0000').filter(AxiomProtocol::isAxiomChannel))
if ("axiom:tunnel" in channels) {
modernServer = true
clearNegotiation()
}
}
fun createChallenge(packetId: Int): AxiomCustomPayloadFrame? {
if (!awaitingLegacyChallenge) return null
val id = challengeSource()
challenge = id
return AxiomCustomPayloadFrame(packetId, "axiom:hello", AxiomByteWriter().writeLong(id).toByteArray())
}
fun clientbound(frame: AxiomCustomPayloadFrame): AxiomCustomPayloadFrame {
if (frame.channel == "axiom:hello" || frame.channel == "axiom:goodbye") {
modernServer = true
clearNegotiation()
return frame
}
if (!active) return frame
if (frame.channel == "axiom:redo_handshake") {
clearNegotiation()
val id = challengeSource()
challenge = id
return frame.copy(channel = "axiom:hello", payloadBytes = AxiomByteWriter().writeLong(id).toByteArray())
}
if (frame.channel == "axiom:enable") {
val reader = AxiomByteReader(frame.payloadBytes)
val legacy = AxiomEnableCodec.read(reader, 9)
require(reader.readableBytes == 0 && legacy.customDataOverridesCount == 0 && legacy.rotationOverridesCount == 0) {
"Unsupported legacy Axiom configuration"
}
require(!legacy.enabled || hello != null) { "Legacy enable before client hello" }
require(!legacy.enabled || legacy.maxBufferSize in 1..AxiomTunnelCodec.DEFAULT_MAXIMUM_PACKET_SIZE) {
"Unsupported legacy Axiom buffer limit"
}
enable = legacy
tunnel.clear()
val supported = channels.filter { it in AxiomProtocol.SERVERBOUND_CHANNELS && it != "axiom:tunnel" }
val modern = legacy.copy(
serverConfigVersion = 0,
tunnelSplitSize = AxiomTunnelCodec.DEFAULT_SPLIT_SIZE,
maximumTunnelPacketSize = legacy.maxBufferSize,
// The bridge terminates the tunnel, the API 9 server never sees it.
supportedServerboundChannels = supported + "axiom:tunnel",
)
return frame.copy(payloadBytes = AxiomEnableCodec.write(modern, 10))
}
return rewrite(frame, AxiomPayloadDirection.CLIENTBOUND)
}
/** Null consumes an intermediate tunnel fragment. */
fun serverbound(frame: AxiomCustomPayloadFrame): AxiomCustomPayloadFrame? {
if (!active) return frame
if (frame.channel == "axiom:hello") {
val candidate = AxiomHelloCodec.read(frame.payloadBytes)
require(candidate.apiVersion == 10 && candidate.handshakeId == challenge) { "Unexpected legacy bridge challenge reply" }
hello = candidate
enable = null
tunnel.clear()
// The bridge translates IDs, so the server must not.
return frame.copy(payloadBytes = AxiomHelloCodec.write(candidate.copy(apiVersion = 9,
protocolVersion = serverProtocolVersion, handshakeId = null)))
}
if (frame.channel != "axiom:tunnel") return rewrite(frame, AxiomPayloadDirection.SERVERBOUND)
val negotiated = requireNotNull(enable?.takeIf { it.enabled }) { "Tunnel sent before legacy server enabled Axiom" }
val fragments = tunnel.add(frame.payloadBytes, AxiomTunnelCodec.DEFAULT_SPLIT_SIZE, negotiated.maxBufferSize) ?: return null
val packet = AxiomTunnelCodec.decode(fragments, maximumPacketSize = negotiated.maxBufferSize)
require(packet.channel in channels && packet.channel in AxiomProtocol.SERVERBOUND_CHANNELS && packet.channel != "axiom:hello") {
"Legacy server did not register ${packet.channel}"
}
val output = rewrite(frame.copy(channel = packet.channel, payloadBytes = packet.payload), AxiomPayloadDirection.SERVERBOUND)
val largeChannels = setOf("axiom:set_buffer", "axiom:upload_blueprint", "axiom:annotation_update")
val maximum = if (output.channel in largeChannels) negotiated.maxBufferSize else 32_767
require(output.payloadBytes.size <= maximum) { "${output.channel} exceeds legacy transport limit ($maximum)" }
return output
}
private fun rewrite(frame: AxiomCustomPayloadFrame, direction: AxiomPayloadDirection): AxiomCustomPayloadFrame {
if (enable?.enabled != true) return frame
val session = AxiomTranslationSession(requireNotNull(hello), serverProtocolVersion, enable = enable)
return frame.copy(payloadBytes = translator.rewrite(AxiomPayload(direction, frame.channel, frame.payloadBytes), session))
}
private fun clearNegotiation() {
challenge = null
hello = null
enable = null
tunnel.clear()
}
}
@@ -0,0 +1,54 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.translation.AxiomPayload
import me.wargearsystems.viaaxiom.translation.AxiomPayloadTranslator
import me.wargearsystems.viaaxiom.translation.AxiomTunnelTranslationEngine
/** API 10 client <-> API 10 server on another Minecraft version. */
class AxiomModernBridge(
private val serverProtocolVersion: Int,
private val translator: AxiomPayloadTranslator,
) {
private val handshake = AxiomHandshakeTracker()
private val tunnelEngine = AxiomTunnelTranslationEngine(translator)
private val tunnel = AxiomTunnelCollector()
fun clientbound(frame: AxiomCustomPayloadFrame): AxiomCustomPayloadFrame {
if (frame.channel in HANDSHAKE_CHANNELS) {
tunnel.clear()
handshake.observe(AxiomPayloadDirection.CLIENTBOUND, frame.channel, frame.payloadBytes)
return frame
}
val session = session ?: return frame
return frame.copy(payloadBytes = translator.rewrite(
AxiomPayload(AxiomPayloadDirection.CLIENTBOUND, frame.channel, frame.payloadBytes), session))
}
/** Empty consumes an intermediate tunnel fragment. Translation can change the fragment count. */
fun serverbound(frame: AxiomCustomPayloadFrame): List<AxiomCustomPayloadFrame> {
if (frame.channel == "axiom:hello") {
if (handshake.challengeId == null) return listOf(frame)
tunnel.clear()
handshake.observe(AxiomPayloadDirection.SERVERBOUND, frame.channel, frame.payloadBytes)
// The bridge translates IDs, so the server must not.
val hello = requireNotNull(handshake.hello)
return listOf(frame.copy(payloadBytes = AxiomHelloCodec.write(hello.copy(protocolVersion = serverProtocolVersion))))
}
val session = session ?: return listOf(frame)
if (frame.channel != "axiom:tunnel") {
return listOf(frame.copy(payloadBytes = translator.rewrite(
AxiomPayload(AxiomPayloadDirection.SERVERBOUND, frame.channel, frame.payloadBytes), session)))
}
val enable = requireNotNull(session.enable)
val fragments = tunnel.add(frame.payloadBytes, requireNotNull(enable.tunnelSplitSize),
requireNotNull(enable.maximumTunnelPacketSize)) ?: return emptyList()
return tunnelEngine.rewrite(fragments, session).map { frame.copy(payloadBytes = it) }
}
private val session: AxiomTranslationSession?
get() = handshake.session(serverProtocolVersion)
private companion object {
val HANDSHAKE_CHANNELS = setOf("axiom:hello", "axiom:enable", "axiom:goodbye", "axiom:redo_handshake")
}
}
@@ -0,0 +1,75 @@
package me.wargearsystems.viaaxiom.protocol
object AxiomProtocol {
const val NAMESPACE: String = "axiom"
const val API_VERSION: Int = 10
const val LEGACY_API_VERSION: Int = 9
const val MIN_POSITION_LONG: Long = -9223371899415820288L
val CLIENTBOUND_CHANNELS: Set<String> = setOf(
channel("enable"),
channel("hello"),
channel("goodbye"),
channel("redo_handshake"),
channel("response_chunk_data"),
channel("response_entity_data"),
channel("register_world_properties"),
channel("set_world_property"),
channel("ack_world_properties"),
channel("restrictions"),
channel("marker_data"),
channel("marker_nbt_response"),
channel("annotation_update"),
channel("add_server_heightmap"),
channel("custom_blocks"),
channel("register_custom_block_v2"),
channel("ignore_display_entities"),
channel("register_custom_items"),
channel("blueprint_manifest"),
channel("response_blueprint"),
)
val SERVERBOUND_CHANNELS: Set<String> = setOf(
channel("hello"),
channel("tunnel"),
channel("tick_blocks"),
channel("set_gamemode"),
channel("set_fly_speed"),
channel("teleport"),
channel("set_world_time"),
channel("set_no_physical_trigger"),
channel("set_world_property"),
channel("request_chunk_data"),
channel("request_entity_data"),
channel("spawn_entity"),
channel("manipulate_entity"),
channel("delete_entity"),
channel("marker_nbt_request"),
channel("set_block"),
channel("set_buffer"),
channel("upload_blueprint"),
channel("request_blueprint"),
channel("annotation_update"),
)
val BLOCK_STATE_TRANSLATION_CHANNELS: Set<AxiomPayloadKey> = setOf(
AxiomPayloadKey(AxiomPayloadDirection.SERVERBOUND, channel("set_block")),
AxiomPayloadKey(AxiomPayloadDirection.SERVERBOUND, channel("set_buffer")),
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, channel("response_chunk_data")),
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, channel("register_custom_block_v2")),
)
fun channel(path: String): String = "$NAMESPACE:$path"
fun isAxiomChannel(channel: String): Boolean = channel.startsWith("$NAMESPACE:")
}
enum class AxiomPayloadDirection {
CLIENTBOUND,
SERVERBOUND,
}
data class AxiomPayloadKey(
val direction: AxiomPayloadDirection,
val channel: String,
)
@@ -0,0 +1,54 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.translation.AxiomPayloadTranslator
import java.util.UUID
/**
* One player's Axiom traffic to one backend.
*
* An exception means the frame could not be translated. Never forward such a frame,
* its IDs would select the wrong blocks.
*/
class AxiomProxySession(
private val clientProtocolVersion: Int,
private val serverProtocolVersion: Int,
translator: AxiomPayloadTranslator,
challengeSource: () -> Long = { UUID.randomUUID().mostSignificantBits },
) {
private val legacy = AxiomLegacyBridge(serverProtocolVersion, translator, challengeSource)
private val modern = AxiomModernBridge(serverProtocolVersion, translator)
/** Set for clients other than API 10, which talk to the backend directly. */
var passthrough = false
private set
val awaitingLegacyChallenge: Boolean
get() = !passthrough && legacy.awaitingLegacyChallenge
fun backendRegistered(bytes: ByteArray) = legacy.register(bytes)
fun startLegacyHandshake(): AxiomCustomPayloadFrame? = if (passthrough) null else legacy.createChallenge(0)
fun clientbound(frame: AxiomCustomPayloadFrame): AxiomCustomPayloadFrame {
if (passthrough) return frame
val output = legacy.clientbound(frame)
if (legacy.active || !versionsDiffer) return output
return modern.clientbound(output)
}
/** Empty consumes an intermediate tunnel fragment. */
fun serverbound(frame: AxiomCustomPayloadFrame): List<AxiomCustomPayloadFrame> {
if (passthrough) return listOf(frame)
if (frame.channel == "axiom:hello" && AxiomByteReader(frame.payloadBytes).readVarInt() != AxiomProtocol.API_VERSION) {
passthrough = true
return listOf(frame)
}
if (legacy.active) return listOfNotNull(legacy.serverbound(frame))
if (!versionsDiffer) return listOf(frame)
return modern.serverbound(frame)
}
private val versionsDiffer: Boolean
get() = clientProtocolVersion != serverProtocolVersion
}
@@ -0,0 +1,57 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
object AxiomResponseChunkDataPayloadCodec {
fun rewriteSections(
bytes: ByteArray,
sectionRewriter: (reader: AxiomByteReader, writer: AxiomByteWriter) -> Unit,
): ByteArray {
val reader = AxiomByteReader(bytes)
val writer = AxiomByteWriter(bytes.size)
writer.writeLong(reader.readLong()) // Request id.
copyBlockEntities(reader, writer)
copySections(reader, writer, sectionRewriter)
writer.writeBoolean(reader.readBoolean()) // Finished.
writer.writeBytes(reader.copyRemaining())
return writer.toByteArray()
}
private fun copyBlockEntities(reader: AxiomByteReader, writer: AxiomByteWriter) {
while (true) {
val blockEntityPos = reader.readLong()
writer.writeLong(blockEntityPos)
if (blockEntityPos == AxiomProtocol.MIN_POSITION_LONG) {
break
}
writer.writeVarInt(reader.readVarInt()) // Original NBT size.
writer.writeByte(reader.readUnsignedByte()) // Compression dictionary id.
writer.writeByteArray(reader.readByteArray())
}
}
private fun copySections(
reader: AxiomByteReader,
writer: AxiomByteWriter,
sectionRewriter: (reader: AxiomByteReader, writer: AxiomByteWriter) -> Unit,
) {
while (true) {
val sectionIndex = reader.readLong()
writer.writeLong(sectionIndex)
if (sectionIndex == AxiomProtocol.MIN_POSITION_LONG) {
break
}
val hasContainer = reader.readBoolean()
writer.writeBoolean(hasContainer)
if (hasContainer) {
sectionRewriter(reader, writer)
}
}
}
}
@@ -0,0 +1,23 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
object AxiomSetBlockPayloadCodec {
fun rewriteBlockStateIds(bytes: ByteArray, mapper: (Int) -> Int): ByteArray {
val reader = AxiomByteReader(bytes)
val writer = AxiomByteWriter()
val blockCount = reader.readVarInt()
writer.writeVarInt(blockCount)
repeat(blockCount) {
writer.writeLong(reader.readLong())
writer.writeVarInt(mapper(reader.readVarInt()))
}
writer.writeBytes(reader.copyRemaining())
return writer.toByteArray()
}
}
@@ -0,0 +1,54 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
object AxiomSetBufferPayloadCodec {
private const val BLOCK_BUFFER_TYPE: Int = 0
fun rewriteBlockSections(
bytes: ByteArray,
sectionRewriter: (reader: AxiomByteReader, writer: AxiomByteWriter) -> Unit,
): ByteArray {
val reader = AxiomByteReader(bytes)
val writer = AxiomByteWriter(bytes.size)
writer.writeUtf(reader.readUtf()) // Dimension resource key value.
writer.writeLong(reader.readLong()) // UUID most significant bits.
writer.writeLong(reader.readLong()) // UUID least significant bits.
val type = reader.readUnsignedByte()
writer.writeByte(type)
if (type != BLOCK_BUFFER_TYPE) {
writer.writeBytes(reader.copyRemaining())
return writer.toByteArray()
}
while (true) {
val sectionIndex = reader.readLong()
writer.writeLong(sectionIndex)
if (sectionIndex == AxiomProtocol.MIN_POSITION_LONG) {
break
}
sectionRewriter(reader, writer)
copyBlockEntities(reader, writer)
}
writer.writeBytes(reader.copyRemaining())
return writer.toByteArray()
}
private fun copyBlockEntities(reader: AxiomByteReader, writer: AxiomByteWriter) {
val blockEntityCount = reader.readVarInt()
writer.writeVarInt(blockEntityCount)
repeat(blockEntityCount) {
writer.writeShort(reader.readShort().toInt())
writer.writeVarInt(reader.readVarInt()) // Original NBT size.
writer.writeByte(reader.readUnsignedByte()) // Compression dictionary id.
writer.writeByteArray(reader.readByteArray())
}
}
}
@@ -0,0 +1,86 @@
package me.wargearsystems.viaaxiom.protocol
import com.github.luben.zstd.Zstd
import com.github.luben.zstd.ZstdException
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import java.io.ByteArrayOutputStream
data class AxiomTunnelPacket(val channel: String, val payload: ByteArray, val compressed: Boolean = false)
/** API 10 serverbound transport. Split size includes the one-byte fragment flags. */
object AxiomTunnelCodec {
const val FIRST = 1
const val LAST = 2
const val ZSTD_COMPRESSED = 1
const val DEFAULT_SPLIT_SIZE = 31_000
const val DEFAULT_MAXIMUM_PACKET_SIZE = 2_097_152
private const val MAX_HEADER_SIZE = 131_080
fun decode(
fragments: List<ByteArray>,
splitSize: Int = DEFAULT_SPLIT_SIZE,
maximumPacketSize: Int = DEFAULT_MAXIMUM_PACKET_SIZE,
): AxiomTunnelPacket {
require(splitSize > 1 && maximumPacketSize > 0)
require(fragments.isNotEmpty()) { "Empty Axiom tunnel packet" }
val combined = ByteArrayOutputStream()
val maximumEncodedSize = Zstd.compressBound(maximumPacketSize.toLong()) + MAX_HEADER_SIZE
fragments.forEachIndexed { index, fragment ->
require(fragment.size in 2..splitSize) { "Invalid Axiom tunnel fragment size" }
val expectedFlags = (if (index == 0) FIRST else 0) or (if (index == fragments.lastIndex) LAST else 0)
require((fragment[0].toInt() and 0xFF) == expectedFlags) { "Invalid Axiom tunnel fragment order or flags" }
require(combined.size().toLong() + fragment.size - 1 <= maximumEncodedSize) { "Axiom tunnel packet exceeds limit" }
combined.write(fragment, 1, fragment.size - 1)
}
val reader = AxiomByteReader(combined.toByteArray())
val channel = reader.readUtf()
require(AxiomProtocol.isAxiomChannel(channel) && channel != AxiomProtocol.channel("tunnel")) {
"Invalid tunneled Axiom channel: $channel"
}
val uncompressedSize = reader.readInt()
require(uncompressedSize in 0..maximumPacketSize) { "Axiom tunnel payload exceeds limit" }
val flags = reader.readUnsignedByte()
require(flags == 0 || flags == ZSTD_COMPRESSED) { "Unsupported Axiom tunnel packet flags: $flags" }
val body = reader.copyRemaining()
val compressed = flags == ZSTD_COMPRESSED
val payload = if (compressed) {
val destination = ByteArray(uncompressedSize)
val result = try {
Zstd.decompressByteArray(destination, 0, destination.size, body, 0, body.size)
} catch (exception: ZstdException) {
throw IllegalArgumentException("Invalid Zstd Axiom tunnel payload", exception)
}
require(!Zstd.isError(result)) { "Invalid Zstd Axiom tunnel payload: ${Zstd.getErrorName(result)}" }
require(result == uncompressedSize.toLong()) { "Axiom tunnel decompressed size mismatch" }
destination
} else {
require(body.size == uncompressedSize) { "Axiom tunnel payload size mismatch" }
body
}
return AxiomTunnelPacket(channel, payload, compressed)
}
fun encode(
packet: AxiomTunnelPacket,
splitSize: Int = DEFAULT_SPLIT_SIZE,
maximumPacketSize: Int = DEFAULT_MAXIMUM_PACKET_SIZE,
): List<ByteArray> {
require(splitSize > 1 && maximumPacketSize > 0)
require(packet.payload.size <= maximumPacketSize) { "Axiom tunnel payload exceeds limit" }
require(AxiomProtocol.isAxiomChannel(packet.channel) && packet.channel != AxiomProtocol.channel("tunnel"))
val body = if (packet.compressed) Zstd.compress(packet.payload) else packet.payload
val bytes = AxiomByteWriter().writeUtf(packet.channel).writeInt(packet.payload.size)
.writeByte(if (packet.compressed) ZSTD_COMPRESSED else 0).writeBytes(body).toByteArray()
val bodyPerFragment = splitSize - 1
val count = ((bytes.size.toLong() + bodyPerFragment - 1) / bodyPerFragment).toInt()
return List(count) { index ->
val from = index * bodyPerFragment
val length = minOf(bodyPerFragment, bytes.size - from)
ByteArray(length + 1).also { fragment ->
fragment[0] = ((if (index == 0) FIRST else 0) or (if (index == count - 1) LAST else 0)).toByte()
bytes.copyInto(fragment, 1, from, from + length)
}
}
}
}
@@ -0,0 +1,36 @@
package me.wargearsystems.viaaxiom.protocol
class AxiomTunnelCollector {
private val fragments = mutableListOf<ByteArray>()
private var collectedBytes = 0L
/** Null until [fragment] completes a packet. */
fun add(fragment: ByteArray, splitSize: Int, maximumPacketSize: Int): List<ByteArray>? {
try {
require(fragment.size in 2..splitSize) { "Invalid tunnel fragment length" }
val flags = fragment[0].toInt() and 0xFF
require(flags in 0..3) { "Invalid tunnel flags" }
if (flags and AxiomTunnelCodec.FIRST != 0) {
require(fragments.isEmpty()) { "New tunnel before previous tunnel completed" }
} else require(fragments.isNotEmpty()) { "Tunnel continuation without first fragment" }
collectedBytes += fragment.size - 1
require(collectedBytes <= maximumPacketSize.toLong() + COMPRESSION_MARGIN) { "Tunnel exceeds server limit" }
fragments += fragment
if (flags and AxiomTunnelCodec.LAST == 0) return null
} catch (failure: IllegalArgumentException) {
clear()
throw failure
}
return fragments.toList().also { clear() }
}
fun clear() {
fragments.clear()
collectedBytes = 0
}
private companion object {
/** Incompressible payloads plus the header can slightly exceed the uncompressed limit. */
const val COMPRESSION_MARGIN = 262_144L
}
}
@@ -0,0 +1,326 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.math.max
/** Unboxed, because a section can need 4096 lookups. */
fun interface BlockStateIdMapper {
fun map(id: Int): Int
}
object PalettedContainerCodec {
const val PROTOCOL_1_21_5: Int = 770
private const val BLOCK_COUNT: Int = 4096
private const val BLOCK_HIGHEST_BITS_PER_VALUE: Int = 8
private const val MIN_BLOCK_BITS_PER_VALUE: Int = 4
fun rewriteBlockStateIds(
reader: AxiomByteReader,
writer: AxiomByteWriter,
protocolVersion: Int,
globalPaletteBits: Int,
mapper: BlockStateIdMapper,
targetProtocolVersion: Int = protocolVersion,
targetGlobalPaletteBits: Int = globalPaletteBits,
) {
val sameLayout = globalPaletteBits == targetGlobalPaletteBits &&
(protocolVersion >= PROTOCOL_1_21_5) == (targetProtocolVersion >= PROTOCOL_1_21_5)
if (sameLayout) {
rewriteInPlace(reader, writer, protocolVersion, globalPaletteBits, mapper)
return
}
val container = readBlockContainer(reader, protocolVersion, globalPaletteBits)
container.replaceIds(mapper::map)
writeBlockContainer(writer, container, targetProtocolVersion, targetGlobalPaletteBits)
}
/** Palette indexes are copied as they are, and global IDs are mapped inside the packed longs. */
private fun rewriteInPlace(
reader: AxiomByteReader,
writer: AxiomByteWriter,
protocolVersion: Int,
globalPaletteBits: Int,
mapper: BlockStateIdMapper,
) {
require(protocolVersion >= 757) { "Palette formats before Minecraft 1.18 are not supported" }
require(globalPaletteBits in 9..31) { "Invalid global palette width: $globalPaletteBits" }
val encodedBits = reader.readUnsignedByte()
writer.writeByte(encodedBits)
if (encodedBits == 0) {
writer.writeVarInt(mappedId(mapper, reader.readVarInt(), globalPaletteBits))
writeValues(writer, protocolVersion, readValues(reader, protocolVersion, bitsPerValue = 0))
return
}
val bitsPerValue = normalizeBitsPerValue(encodedBits, globalPaletteBits)
if (bitsPerValue == globalPaletteBits) {
val values = readValues(reader, protocolVersion, bitsPerValue)
mapGlobalValues(values, bitsPerValue, mapper)
writeValues(writer, protocolVersion, values)
return
}
val paletteLength = reader.readVarInt()
require(paletteLength in 1..(1 shl bitsPerValue)) { "Invalid palette length: $paletteLength" }
writer.writeVarInt(paletteLength)
repeat(paletteLength) {
writer.writeVarInt(mappedId(mapper, reader.readVarInt(), globalPaletteBits))
}
val values = readValues(reader, protocolVersion, bitsPerValue)
requirePaletteIndexes(values, bitsPerValue, paletteLength)
writeValues(writer, protocolVersion, values)
}
private fun mappedId(mapper: BlockStateIdMapper, id: Int, globalPaletteBits: Int): Int {
val mapped = mapper.map(id)
require(mapped >= 0 && mapped.toLong() < (1L shl globalPaletteBits)) {
"Block state ID does not fit the target registry width"
}
return mapped
}
private fun mapGlobalValues(values: LongArray, bitsPerValue: Int, mapper: BlockStateIdMapper) {
val mask = (1L shl bitsPerValue) - 1L
val valuesPerLong = 64 / bitsPerValue
var remaining = BLOCK_COUNT
for (cellIndex in values.indices) {
val cell = values[cellIndex]
var mappedCell = 0L
var position = 0
while (position < valuesPerLong && remaining > 0) {
val shift = position * bitsPerValue
val id = ((cell ushr shift) and mask).toInt()
mappedCell = mappedCell or (mappedId(mapper, id, bitsPerValue).toLong() shl shift)
position++
remaining--
}
values[cellIndex] = mappedCell
}
}
private fun requirePaletteIndexes(values: LongArray, bitsPerValue: Int, paletteLength: Int) {
val mask = (1L shl bitsPerValue) - 1L
val valuesPerLong = 64 / bitsPerValue
var remaining = BLOCK_COUNT
for (cell in values) {
var current = cell
var position = 0
while (position < valuesPerLong && remaining > 0) {
require((current and mask) < paletteLength) { "Block value references an absent palette entry" }
current = current ushr bitsPerValue
position++
remaining--
}
}
}
fun readBlockContainer(
reader: AxiomByteReader,
protocolVersion: Int,
globalPaletteBits: Int,
): PalettedContainerData {
require(protocolVersion >= 757) { "Palette formats before Minecraft 1.18 are not supported" }
require(globalPaletteBits in 9..31) { "Invalid global palette width: $globalPaletteBits" }
var bitsPerValue = reader.readUnsignedByte()
if (bitsPerValue == 0) {
val palette = mutableListOf(reader.readVarInt())
readValues(reader, protocolVersion, bitsPerValue = 0)
return PalettedContainerData(palette, IntArray(BLOCK_COUNT))
}
bitsPerValue = normalizeBitsPerValue(bitsPerValue, globalPaletteBits)
val palette = mutableListOf<Int>()
val values: IntArray
if (bitsPerValue != globalPaletteBits) {
val paletteLength = reader.readVarInt()
require(paletteLength in 1..(1 shl bitsPerValue)) { "Invalid palette length: $paletteLength" }
repeat(paletteLength) {
palette += reader.readVarInt()
}
values = unpackValues(readValues(reader, protocolVersion, bitsPerValue), bitsPerValue)
require(values.all { it in palette.indices }) { "Block value references an absent palette entry" }
} else {
val rawIds = unpackValues(readValues(reader, protocolVersion, bitsPerValue), bitsPerValue)
val indexesById = mutableMapOf<Int, Int>()
// Global values are registry IDs, whereas PalettedContainerData stores palette indexes.
values = IntArray(BLOCK_COUNT) { index ->
indexesById.getOrPut(rawIds[index]) {
palette.add(rawIds[index])
palette.lastIndex
}
}
}
return PalettedContainerData(palette, values)
}
fun writeBlockContainer(
writer: AxiomByteWriter,
container: PalettedContainerData,
protocolVersion: Int,
globalPaletteBits: Int,
) {
require(protocolVersion >= 757) { "Palette formats before Minecraft 1.18 are not supported" }
require(globalPaletteBits in 9..31) { "Invalid global palette width: $globalPaletteBits" }
val paletteSize = container.palette.size
require(paletteSize in 1..BLOCK_COUNT) { "Invalid palette size: $paletteSize" }
require(container.palette.all { it >= 0 && it.toLong() < (1L shl globalPaletteBits) }) {
"Block state ID does not fit the target registry width"
}
if (paletteSize == 1) {
writer.writeByte(0)
writer.writeVarInt(container.palette[0])
writeValues(writer, protocolVersion, longArrayOf())
return
}
val bitsPerValue = bitsPerValue(paletteSize, globalPaletteBits)
writer.writeByte(bitsPerValue)
val values = if (bitsPerValue == globalPaletteBits) {
IntArray(BLOCK_COUNT) { container.idAt(it) }
} else {
writer.writeVarInt(paletteSize)
for (id in container.palette) {
writer.writeVarInt(id)
}
IntArray(BLOCK_COUNT) { container.paletteIndexAt(it) }
}
writeValues(
writer = writer,
protocolVersion = protocolVersion,
values = packValues(bitsPerValue, values),
)
}
private fun normalizeBitsPerValue(bitsPerValue: Int, globalPaletteBits: Int): Int {
if (bitsPerValue < 0 || bitsPerValue > BLOCK_HIGHEST_BITS_PER_VALUE) {
return globalPaletteBits
}
return max(MIN_BLOCK_BITS_PER_VALUE, bitsPerValue)
}
private fun bitsPerValue(paletteSize: Int, globalPaletteBits: Int): Int {
val bits = max(MIN_BLOCK_BITS_PER_VALUE, ceilLog2(paletteSize))
return if (bits > BLOCK_HIGHEST_BITS_PER_VALUE) globalPaletteBits else bits
}
private fun readValues(reader: AxiomByteReader, protocolVersion: Int, bitsPerValue: Int): LongArray {
if (protocolVersion >= PROTOCOL_1_21_5) {
if (bitsPerValue == 0) {
return longArrayOf()
}
return reader.readLongArray(expectedLongArrayLength(bitsPerValue))
}
val length = reader.readVarInt()
val expected = if (bitsPerValue == 0) 0 else expectedLongArrayLength(bitsPerValue)
require(length == expected) { "Invalid palette data length: $length, expected $expected" }
return reader.readLongArray(length)
}
private fun writeValues(
writer: AxiomByteWriter,
protocolVersion: Int,
values: LongArray,
) {
if (protocolVersion >= PROTOCOL_1_21_5) {
writer.writeLongArray(values, includeLength = false)
} else {
writer.writeLongArray(values, includeLength = true)
}
}
private fun expectedLongArrayLength(bitsPerValue: Int): Int {
val valuesPerLong = 64 / bitsPerValue
return (BLOCK_COUNT + valuesPerLong - 1) / valuesPerLong
}
private fun unpackValues(values: LongArray, bitsPerValue: Int): IntArray {
val unpacked = IntArray(BLOCK_COUNT)
if (values.isEmpty() || bitsPerValue == 0) {
return unpacked
}
val maxValue = (1L shl bitsPerValue) - 1L
val valuesPerLong = 64 / bitsPerValue
var index = 0
for (cell in values) {
var current = cell
repeat(valuesPerLong) {
if (index >= BLOCK_COUNT) {
return unpacked
}
unpacked[index++] = (current and maxValue).toInt()
current = current ushr bitsPerValue
}
}
return unpacked
}
private fun packValues(bitsPerValue: Int, values: IntArray): LongArray {
val maxValue = (1L shl bitsPerValue) - 1L
val valuesPerLong = 64 / bitsPerValue
val packed = LongArray((values.size + valuesPerLong - 1) / valuesPerLong)
var index = 0
for (cellIndex in packed.indices) {
var cell = 0L
var bitIndex = 0
val limit = minOf(index + valuesPerLong, values.size)
while (index < limit) {
cell = cell or ((values[index++].toLong() and maxValue) shl bitIndex)
bitIndex += bitsPerValue
}
packed[cellIndex] = cell
}
return packed
}
private fun ceilLog2(value: Int): Int {
return if (value > 0) 32 - Integer.numberOfLeadingZeros(value - 1) else 0
}
}
class PalettedContainerData(
val palette: MutableList<Int>,
private val paletteIndexes: IntArray,
) {
init {
require(paletteIndexes.size == 4096) { "A block section must contain 4096 values" }
require(palette.isNotEmpty() && paletteIndexes.all { it in palette.indices }) { "Invalid block palette indexes" }
}
fun idAt(index: Int): Int = palette[paletteIndexes[index]]
fun paletteIndexAt(index: Int): Int = paletteIndexes[index]
fun replaceIds(mapper: (Int) -> Int) {
if (palette.isEmpty()) {
return
}
val newPalette = ArrayList<Int>(palette.size)
val indexesById = HashMap<Int, Int>(palette.size * 2)
val remap = IntArray(palette.size)
for (oldIndex in palette.indices) {
val mappedId = mapper(palette[oldIndex])
remap[oldIndex] = indexesById.getOrPut(mappedId) {
newPalette += mappedId
newPalette.lastIndex
}
}
for (i in paletteIndexes.indices) {
paletteIndexes[i] = remap[paletteIndexes[i]]
}
palette.clear()
palette += newPalette
}
}
@@ -0,0 +1,31 @@
package me.wargearsystems.viaaxiom.translation
import me.wargearsystems.viaaxiom.protocol.AxiomCustomPayloadFrameCodec
import me.wargearsystems.viaaxiom.protocol.AxiomPayloadDirection
import me.wargearsystems.viaaxiom.protocol.AxiomTranslationSession
class AxiomFrameTranslationEngine(
private val payloadTranslator: AxiomPayloadTranslator,
) {
fun rewrite(
frameBytes: ByteArray,
customPayloadPacketId: Int,
direction: AxiomPayloadDirection,
session: AxiomTranslationSession,
): ByteArray {
return AxiomCustomPayloadFrameCodec.rewriteIfCustomPayload(
frameBytes = frameBytes,
customPayloadPacketId = customPayloadPacketId,
) { frame ->
payloadTranslator.rewrite(
payload = AxiomPayload(
direction = direction,
channel = frame.channel,
bytes = frame.payloadBytes,
),
session = session,
)
}
}
}
@@ -0,0 +1,90 @@
package me.wargearsystems.viaaxiom.translation
import me.wargearsystems.viaaxiom.protocol.AxiomPayloadDirection
import me.wargearsystems.viaaxiom.protocol.AxiomPayloadKey
import me.wargearsystems.viaaxiom.protocol.AxiomProtocol
import me.wargearsystems.viaaxiom.protocol.AxiomSetBlockPayloadCodec
import me.wargearsystems.viaaxiom.protocol.AxiomTranslationSession
import me.wargearsystems.viaaxiom.protocol.AxiomSetBufferPayloadCodec
import me.wargearsystems.viaaxiom.protocol.AxiomResponseChunkDataPayloadCodec
import me.wargearsystems.viaaxiom.protocol.PalettedContainerCodec
import me.wargearsystems.viaaxiom.protocol.AxiomCustomBlockCodec
data class BlockStatePaletteWidths(val client: Int, val server: Int) {
init {
require(client in 9..31 && server in 9..31)
}
}
data class AxiomPayload(
val direction: AxiomPayloadDirection,
val channel: String,
val bytes: ByteArray,
) {
val key: AxiomPayloadKey
get() = AxiomPayloadKey(direction, channel)
}
interface AxiomPayloadTranslator {
fun rewrite(payload: AxiomPayload, session: AxiomTranslationSession): ByteArray
}
class AxiomTranslationEngine(
private val blockStateIdTranslator: BlockStateIdTranslator,
private val paletteWidths: BlockStatePaletteWidths? = null,
) : AxiomPayloadTranslator {
override fun rewrite(payload: AxiomPayload, session: AxiomTranslationSession): ByteArray {
if (!session.protocolMismatch || !AxiomProtocol.isAxiomChannel(payload.channel)) {
return payload.bytes
}
if (payload.key !in AxiomProtocol.BLOCK_STATE_TRANSLATION_CHANNELS) {
return payload.bytes
}
return when (payload.key) {
AxiomPayloadKey(AxiomPayloadDirection.SERVERBOUND, AxiomProtocol.channel("set_block")) -> {
rewriteSetBlock(payload.bytes)
}
AxiomPayloadKey(AxiomPayloadDirection.SERVERBOUND, AxiomProtocol.channel("set_buffer")) -> {
rewriteSetBuffer(payload.bytes, session)
}
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, AxiomProtocol.channel("response_chunk_data")) -> {
rewriteResponseChunkData(payload.bytes, session)
}
AxiomPayloadKey(AxiomPayloadDirection.CLIENTBOUND, AxiomProtocol.channel("register_custom_block_v2")) -> {
rewriteRegisterCustomBlock(payload.bytes)
}
else -> payload.bytes
}
}
private fun rewriteSetBlock(bytes: ByteArray): ByteArray {
return AxiomSetBlockPayloadCodec.rewriteBlockStateIds(bytes, blockStateIdTranslator::clientToServer)
}
private fun rewriteSetBuffer(bytes: ByteArray, session: AxiomTranslationSession): ByteArray {
val widths = paletteWidths ?: return bytes
return AxiomSetBufferPayloadCodec.rewriteBlockSections(bytes) { reader, writer ->
PalettedContainerCodec.rewriteBlockStateIds(
reader, writer, session.hello.protocolVersion, widths.client,
blockStateIdTranslator::clientToServer, session.serverProtocolVersion, widths.server,
)
}
}
private fun rewriteResponseChunkData(bytes: ByteArray, session: AxiomTranslationSession): ByteArray {
val widths = paletteWidths ?: return bytes
return AxiomResponseChunkDataPayloadCodec.rewriteSections(bytes) { reader, writer ->
PalettedContainerCodec.rewriteBlockStateIds(
reader, writer, session.serverProtocolVersion, widths.server,
blockStateIdTranslator::serverToClient, session.hello.protocolVersion, widths.client,
)
}
}
private fun rewriteRegisterCustomBlock(bytes: ByteArray): ByteArray {
// Raw IDs need mapping. The codec keeps -1/string states as they are.
return AxiomCustomBlockCodec.rewrite(bytes, blockStateIdTranslator::serverToClient)
}
}
@@ -0,0 +1,20 @@
package me.wargearsystems.viaaxiom.translation
import me.wargearsystems.viaaxiom.protocol.AxiomPayloadDirection
import me.wargearsystems.viaaxiom.protocol.AxiomTranslationSession
import me.wargearsystems.viaaxiom.protocol.AxiomTunnelCodec
class AxiomTunnelTranslationEngine(private val payloadTranslator: AxiomPayloadTranslator) {
fun rewrite(fragments: List<ByteArray>, session: AxiomTranslationSession): List<ByteArray> {
val enable = requireNotNull(session.enable) { "Axiom tunnel requires negotiated server limits" }
val splitSize = requireNotNull(enable.tunnelSplitSize)
val maximumSize = requireNotNull(enable.maximumTunnelPacketSize)
val packet = AxiomTunnelCodec.decode(fragments, splitSize, maximumSize)
require(packet.channel in enable.supportedServerboundChannels) { "Server does not support ${packet.channel}" }
val translated = payloadTranslator.rewrite(
AxiomPayload(AxiomPayloadDirection.SERVERBOUND, packet.channel, packet.payload), session,
)
if (translated.contentEquals(packet.payload)) return fragments
return AxiomTunnelCodec.encode(packet.copy(payload = translated), splitSize, maximumSize)
}
}
@@ -0,0 +1,83 @@
package me.wargearsystems.viaaxiom.translation
interface BlockStateIdTranslator {
fun clientToServer(blockStateId: Int): Int
fun serverToClient(blockStateId: Int): Int
}
enum class BlockStateMappingDirection {
CLIENT_TO_SERVER,
SERVER_TO_CLIENT,
}
data class MissingBlockStatePolicy(
val clientToServerFallbackId: Int,
val serverToClientFallbackId: Int,
) {
companion object {
fun placeholder(fallbackId: Int): MissingBlockStatePolicy {
return MissingBlockStatePolicy(
clientToServerFallbackId = fallbackId,
serverToClientFallbackId = fallbackId,
)
}
}
}
object IdentityBlockStateIdTranslator : BlockStateIdTranslator {
override fun clientToServer(blockStateId: Int): Int = blockStateId
override fun serverToClient(blockStateId: Int): Int = blockStateId
}
class TableBlockStateIdTranslator(
private val clientToServer: IntArray,
private val serverToClient: IntArray,
private val missingBlockStatePolicy: MissingBlockStatePolicy,
private val onMissingBlockState: ((direction: BlockStateMappingDirection, missingId: Int, fallbackId: Int) -> Unit)? = null,
) : BlockStateIdTranslator {
constructor(
clientToServer: IntArray,
serverToClient: IntArray,
fallbackId: Int,
) : this(clientToServer, serverToClient, MissingBlockStatePolicy.placeholder(fallbackId))
override fun clientToServer(blockStateId: Int): Int {
return translate(
table = clientToServer,
blockStateId = blockStateId,
fallbackId = missingBlockStatePolicy.clientToServerFallbackId,
direction = BlockStateMappingDirection.CLIENT_TO_SERVER,
)
}
override fun serverToClient(blockStateId: Int): Int {
return translate(
table = serverToClient,
blockStateId = blockStateId,
fallbackId = missingBlockStatePolicy.serverToClientFallbackId,
direction = BlockStateMappingDirection.SERVER_TO_CLIENT,
)
}
private fun translate(
table: IntArray,
blockStateId: Int,
fallbackId: Int,
direction: BlockStateMappingDirection,
): Int {
if (blockStateId < 0 || blockStateId >= table.size) {
onMissingBlockState?.invoke(direction, blockStateId, fallbackId)
return fallbackId
}
val mapped = table[blockStateId]
if (mapped >= 0) {
return mapped
}
onMissingBlockState?.invoke(direction, blockStateId, fallbackId)
return fallbackId
}
}
@@ -0,0 +1,38 @@
package me.wargearsystems.viaaxiom.translation
/** ViaVersion mapping tables run server-to-client, in reverse protocol-path order. */
object BlockStateMappingChain {
fun compose(serverToClientSteps: List<IntArray>, clientRegistrySize: Int, clientStoneId: Int): Pair<IntArray, IntArray> {
require(clientRegistrySize > 0 && clientStoneId in 0 until clientRegistrySize)
val serverSize = serverToClientSteps.firstOrNull()?.size ?: clientRegistrySize
require(serverSize > 0)
val serverToClient = IntArray(serverSize) { id ->
var mapped = id
for (step in serverToClientSteps) mapped = if (mapped in step.indices) step[mapped] else -1
if (mapped in 0 until clientRegistrySize) mapped else -1
}
val clientToServer = IntArray(clientRegistrySize) { -1 }
// On collisions, keep the earliest canonical state, as upstream Axiom does.
for (serverId in serverToClient.indices.reversed()) {
val clientId = serverToClient[serverId]
if (clientId >= 0) clientToServer[clientId] = serverId
}
require(clientToServer[clientStoneId] >= 0) { "Mapping chain cannot map the stone placeholder" }
return clientToServer to serverToClient
}
/** Block states with no counterpart become stone. */
fun translationEngine(
serverToClientSteps: List<IntArray>,
clientRegistrySize: Int,
clientStoneId: Int,
onMissingBlockState: (direction: BlockStateMappingDirection, missingId: Int, fallbackId: Int) -> Unit,
): AxiomTranslationEngine {
val (clientToServer, serverToClient) = compose(serverToClientSteps, clientRegistrySize, clientStoneId)
val translator = TableBlockStateIdTranslator(clientToServer, serverToClient,
MissingBlockStatePolicy(clientToServer[clientStoneId], clientStoneId), onMissingBlockState)
return AxiomTranslationEngine(translator, BlockStatePaletteWidths(paletteBits(clientRegistrySize), paletteBits(serverToClient.size)))
}
private fun paletteBits(registrySize: Int) = maxOf(9, 32 - Integer.numberOfLeadingZeros(registrySize - 1))
}
@@ -0,0 +1,76 @@
package me.wargearsystems.viaaxiom.binary
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class AxiomByteCodecTest {
@Test
fun `varint round trips minecraft edge values`() {
val values = listOf(0, 1, 2, 127, 128, 255, 25565, Int.MAX_VALUE, -1, Int.MIN_VALUE)
for (value in values) {
val bytes = AxiomByteWriter().writeVarInt(value).toByteArray()
val reader = AxiomByteReader(bytes)
assertEquals(value, reader.readVarInt())
assertEquals(0, reader.readableBytes)
}
}
@Test
fun `primitive values round trip in network byte order`() {
val bytes = AxiomByteWriter()
.writeBoolean(true)
.writeByte(0xFE)
.writeShort(0x3456)
.writeInt(0x12345678)
.writeLong(-9223371899415820288L)
.writeByteArray(byteArrayOf(9, 8, 7))
.toByteArray()
val reader = AxiomByteReader(bytes)
assertEquals(true, reader.readBoolean())
assertEquals(0xFE, reader.readUnsignedByte())
assertEquals(0x3456, reader.readShort().toInt())
assertEquals(0x12345678, reader.readInt())
assertEquals(-9223371899415820288L, reader.readLong())
assertContentEquals(byteArrayOf(9, 8, 7), reader.readByteArray())
assertEquals(0, reader.readableBytes)
}
@Test
fun `utf strings round trip`() {
val bytes = AxiomByteWriter()
.writeUtf("axiom:hello")
.writeUtf("stone")
.toByteArray()
val reader = AxiomByteReader(bytes)
assertEquals("axiom:hello", reader.readUtf())
assertEquals("stone", reader.readUtf())
assertEquals(0, reader.readableBytes)
}
@Test
fun `copy remaining preserves unread bytes`() {
val bytes = byteArrayOf(1, 2, 3, 4, 5)
val reader = AxiomByteReader(bytes)
assertEquals(1, reader.readUnsignedByte())
assertContentEquals(byteArrayOf(2, 3, 4, 5), reader.copyRemaining())
assertEquals(0, reader.readableBytes)
}
@Test
fun `oversized varint fails`() {
val oversized = byteArrayOf(0x80.toByte(), 0x80.toByte(), 0x80.toByte(), 0x80.toByte(), 0x80.toByte(), 0x01)
assertFailsWith<AxiomByteCodecException> {
AxiomByteReader(oversized).readVarInt()
}
}
}
@@ -0,0 +1,47 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.*
class AxiomCustomBlockCodecTest {
@Test fun `raw states and rotation mappings are rewritten`() {
val output = AxiomCustomBlockCodec.rewrite(packet(false)) { it + 100 }
val reader = AxiomByteReader(output)
assertEquals("test:block", reader.readUtf())
assertEquals("test.translation", reader.readUtf())
assertEquals(0, reader.readVarInt())
assertEquals(1, reader.readVarInt())
assertEquals(105, reader.readVarInt())
repeat(4) { reader.readBoolean() }
assertEquals(0, reader.readVarInt())
reader.readBoolean()
assertEquals(1, reader.readVarInt())
assertEquals(105, reader.readVarInt())
assertEquals(106, reader.readVarInt())
repeat(4) { assertEquals(0, reader.readVarInt()) }
assertEquals(0, reader.readableBytes)
}
@Test fun `string based mismatch states preserve their layout`() {
val input = packet(true)
assertContentEquals(input, AxiomCustomBlockCodec.rewrite(input) { error("String states should not map") })
}
@Test fun `embedded item components fail explicitly instead of decoding wrong registry`() {
val input = AxiomByteWriter().writeUtf("test:block").writeUtf("test.translation")
.writeVarInt(0).writeVarInt(0).writeBoolean(true).toByteArray()
assertFailsWith<IllegalArgumentException> { AxiomCustomBlockCodec.rewrite(input) { it } }
}
private fun packet(strings: Boolean): ByteArray {
val writer = AxiomByteWriter().writeUtf("test:block").writeUtf("test.translation").writeVarInt(0).writeVarInt(1)
fun state(id: Int) { if (strings) writer.writeVarInt(-1).writeUtf("minecraft:stone") else writer.writeVarInt(id) }
state(5)
repeat(4) { writer.writeBoolean(false) }
writer.writeVarInt(0).writeBoolean(true).writeVarInt(1)
state(5); state(6)
repeat(4) { writer.writeVarInt(0) }
return writer.toByteArray()
}
}
@@ -0,0 +1,71 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNull
class AxiomCustomPayloadFrameCodecTest {
@Test
fun `reads custom payload frame when packet id matches`() {
val frameBytes = frameBytes(packetId = 12, channel = "axiom:hello", payload = byteArrayOf(1, 2, 3))
val frame = AxiomCustomPayloadFrameCodec.readIfCustomPayload(frameBytes, customPayloadPacketId = 12)
requireNotNull(frame)
assertEquals(12, frame.packetId)
assertEquals("axiom:hello", frame.channel)
assertContentEquals(byteArrayOf(1, 2, 3), frame.payloadBytes)
}
@Test
fun `reads custom payload frame without a known packet id`() {
val frameBytes = frameBytes(packetId = 12, channel = "axiom:hello", payload = byteArrayOf(1, 2, 3))
val frame = AxiomCustomPayloadFrameCodec.read(frameBytes)
assertEquals(12, frame.packetId)
assertEquals("axiom:hello", frame.channel)
assertContentEquals(byteArrayOf(1, 2, 3), frame.payloadBytes)
}
@Test
fun `returns null when packet id does not match`() {
val frameBytes = frameBytes(packetId = 11, channel = "axiom:hello", payload = byteArrayOf(1, 2, 3))
assertNull(AxiomCustomPayloadFrameCodec.readIfCustomPayload(frameBytes, customPayloadPacketId = 12))
}
@Test
fun `unrelated packet is not parsed as a custom payload`() {
val frameBytes = byteArrayOf(11, 127)
assertNull(AxiomCustomPayloadFrameCodec.readIfCustomPayload(frameBytes, customPayloadPacketId = 12))
assertContentEquals(frameBytes, AxiomCustomPayloadFrameCodec.rewriteIfCustomPayload(frameBytes, 12) {
error("Unrelated packets must not be rewritten")
})
}
@Test
fun `rewrites only the payload bytes`() {
val original = frameBytes(packetId = 12, channel = "axiom:set_block", payload = byteArrayOf(1, 2, 3))
val rewritten = AxiomCustomPayloadFrameCodec.rewriteIfCustomPayload(original, customPayloadPacketId = 12) {
byteArrayOf(9, 8, 7)
}
val frame = AxiomCustomPayloadFrameCodec.readIfCustomPayload(rewritten, customPayloadPacketId = 12)
requireNotNull(frame)
assertEquals(12, frame.packetId)
assertEquals("axiom:set_block", frame.channel)
assertContentEquals(byteArrayOf(9, 8, 7), frame.payloadBytes)
}
private fun frameBytes(packetId: Int, channel: String, payload: ByteArray): ByteArray {
return AxiomByteWriter()
.writeVarInt(packetId)
.writeUtf(channel)
.writeBytes(payload)
.toByteArray()
}
}
@@ -0,0 +1,20 @@
package me.wargearsystems.viaaxiom.protocol
import kotlin.test.Test
import kotlin.test.assertEquals
class AxiomEnableCodecTest {
@Test
fun `enable payload header round trips`() {
val enable = AxiomEnable(
enabled = true,
maxBufferSize = 1_048_576,
blueprintVersion = 2,
customDataOverridesCount = 0,
rotationOverridesCount = 0,
)
assertEquals(enable, AxiomEnableCodec.read(AxiomEnableCodec.write(enable)))
}
}
@@ -0,0 +1,38 @@
package me.wargearsystems.viaaxiom.protocol
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class AxiomHelloCodecTest {
@Test
fun `hello payload round trips`() {
val hello = AxiomHello(
apiVersion = 9,
dataVersion = 4438,
protocolVersion = 773,
)
assertEquals(hello, AxiomHelloCodec.read(AxiomHelloCodec.write(hello)))
}
@Test
fun `hello exposes api compatibility`() {
assertTrue(AxiomHello(9, 4438, 773).isSupportedApi)
assertFalse(AxiomHello(8, 4438, 773).isSupportedApi)
}
@Test
fun `translation session exposes protocol and data mismatches`() {
val session = AxiomTranslationSession(
hello = AxiomHello(apiVersion = 9, dataVersion = 4438, protocolVersion = 772),
serverProtocolVersion = 773,
serverDataVersion = 4438,
)
assertTrue(session.protocolMismatch)
assertFalse(session.dataVersionMismatch)
}
}
@@ -0,0 +1,129 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import me.wargearsystems.viaaxiom.translation.*
import kotlin.test.*
class AxiomLegacyBridgeTest {
private val challenge = 42L
private val channels = "axiom:hello\u0000axiom:set_block\u0000axiom:set_buffer\u0000axiom:request_chunk_data"
private val engine = AxiomTranslationEngine(TableBlockStateIdTranslator(
intArrayOf(0, 2, 1), intArrayOf(0, 2, 1), 0), BlockStatePaletteWidths(15, 15))
@Test fun `registration starts legacy handshake only once`() {
val bridge = bridge()
val frame = assertNotNull(bridge.createChallenge(24))
assertEquals(24, frame.packetId)
assertEquals("axiom:hello", frame.channel)
assertContentEquals(AxiomByteWriter().writeLong(challenge).toByteArray(), frame.payloadBytes)
assertNull(bridge.createChallenge(24))
}
@Test fun `native API 10 server is never downgraded`() {
val bridge = bridge()
bridge.register("axiom:tunnel".toByteArray())
assertNull(bridge.createChallenge(24))
val hello = modernHello()
assertSame(hello, bridge.serverbound(hello))
}
@Test fun `real challenge supersedes pending legacy detection`() {
val bridge = bridge()
bridge.createChallenge(24)
val actual = AxiomCustomPayloadFrame(24, "axiom:hello", AxiomByteWriter().writeLong(99).toByteArray())
assertSame(actual, bridge.clientbound(actual))
assertFalse(bridge.active)
val reply = modernHello()
assertSame(reply, bridge.serverbound(reply))
}
@Test fun `client hello retains data version but advertises API 9 and server registry protocol`() {
val bridge = bridge()
bridge.createChallenge(24)
val output = assertNotNull(bridge.serverbound(modernHello()))
assertEquals(AxiomHello(9, 5000, 771), AxiomHelloCodec.read(output.payloadBytes))
}
@Test fun `wrong challenge cannot activate bridge`() {
val bridge = bridge()
bridge.createChallenge(24)
assertFailsWith<IllegalArgumentException> { bridge.serverbound(modernHello(99)) }
assertFailsWith<IllegalArgumentException> { bridge.clientbound(legacyEnable()) }
}
@Test fun `server enable is converted to modern config with only registered capabilities`() {
val bridge = negotiated()
val output = bridge.clientbound(legacyEnable())
val modern = AxiomEnableCodec.read(output.payloadBytes, 10)
assertTrue(modern.enabled)
assertEquals(0, modern.serverConfigVersion)
assertEquals(1_048_576, modern.maximumTunnelPacketSize)
assertTrue("axiom:tunnel" in modern.supportedServerboundChannels)
assertTrue("axiom:set_buffer" in modern.supportedServerboundChannels)
assertFalse("axiom:tick_blocks" in modern.supportedServerboundChannels)
}
@Test fun `server refusal remains disabled and prevents tunneling`() {
val bridge = negotiated()
val output = bridge.clientbound(legacyEnable(false))
assertFalse(AxiomEnableCodec.read(output.payloadBytes, 10).enabled)
assertFailsWith<IllegalArgumentException> { bridge.serverbound(tunnel("axiom:set_block", byteArrayOf(0)).single()) }
}
@Test fun `compressed multi fragment tunnel becomes one legacy payload`() {
val body = ByteArray(90_000) { ((it * 31 + it / 17) and 255).toByte() }
val frames = tunnel("axiom:set_buffer", body, compressed = true, split = 64)
assertTrue(frames.size > 1)
// Test transport independently; an opaque payload translator preserves this fixture.
val transportBridge = AxiomLegacyBridge(771, object : AxiomPayloadTranslator {
override fun rewrite(payload: AxiomPayload, session: AxiomTranslationSession) = payload.bytes
}) { challenge }
transportBridge.register(channels.toByteArray())
transportBridge.createChallenge(24)
transportBridge.serverbound(modernHello())
transportBridge.clientbound(legacyEnable())
frames.dropLast(1).forEach { assertNull(transportBridge.serverbound(it)) }
val output = assertNotNull(transportBridge.serverbound(frames.last()))
assertEquals("axiom:set_buffer", output.channel)
assertContentEquals(body, output.payloadBytes)
}
@Test fun `block IDs are rewritten after extracting tunnel`() {
val body = AxiomByteWriter().writeVarInt(1).writeLong(123).writeVarInt(1).toByteArray()
val output = assertNotNull(enabled().serverbound(tunnel("axiom:set_block", body).single()))
assertContentEquals(AxiomByteWriter().writeVarInt(1).writeLong(123).writeVarInt(2).toByteArray(), output.payloadBytes)
}
@Test fun `unregistered channels and out of order fragments fail closed`() {
assertFailsWith<IllegalArgumentException> { enabled().serverbound(tunnel("axiom:tick_blocks", byteArrayOf(0)).single()) }
val continuation = AxiomCustomPayloadFrame(20, "axiom:tunnel", byteArrayOf(2, 1))
assertFailsWith<IllegalArgumentException> { enabled().serverbound(continuation) }
}
@Test fun `redo handshake clears buffered fragments and requires new hello`() {
val bridge = enabled()
val fragmented = tunnel("axiom:set_buffer", ByteArray(100), split = 32)
assertNull(bridge.serverbound(fragmented.first()))
val redo = bridge.clientbound(AxiomCustomPayloadFrame(24, "axiom:redo_handshake", byteArrayOf()))
assertEquals("axiom:hello", redo.channel)
assertFailsWith<IllegalArgumentException> { bridge.serverbound(fragmented.last()) }
bridge.serverbound(modernHello())
bridge.clientbound(legacyEnable())
val body = byteArrayOf(0)
assertNotNull(bridge.serverbound(tunnel("axiom:set_block", body).single()))
}
@Test fun `nonempty legacy overrides are rejected`() {
val bridge = negotiated()
val payload = AxiomByteWriter().writeBoolean(true).writeInt(1000).writeVarInt(2).writeVarInt(1).writeVarInt(0).toByteArray()
assertFailsWith<IllegalArgumentException> { bridge.clientbound(AxiomCustomPayloadFrame(24, "axiom:enable", payload)) }
}
private fun bridge() = AxiomLegacyBridge(771, engine) { challenge }.also { it.register(channels.toByteArray()) }
private fun negotiated() = bridge().also { it.createChallenge(24); it.serverbound(modernHello()) }
private fun enabled() = negotiated().also { it.clientbound(legacyEnable()) }
private fun modernHello(id: Long = challenge) = AxiomCustomPayloadFrame(20, "axiom:hello", AxiomHelloCodec.write(AxiomHello(10, 5000, 776, id)))
private fun legacyEnable(enabled: Boolean = true) = AxiomCustomPayloadFrame(24, "axiom:enable", AxiomEnableCodec.write(AxiomEnable(enabled, 1_048_576, 2, 0, 0), 9))
private fun tunnel(channel: String, bytes: ByteArray, compressed: Boolean = false, split: Int = 31_000) =
AxiomTunnelCodec.encode(AxiomTunnelPacket(channel, bytes, compressed), split).map { AxiomCustomPayloadFrame(20, "axiom:tunnel", it) }
}
@@ -0,0 +1,99 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.*
class AxiomModernHandshakeTest {
private val challenge = 0x0102030405060708L
private val helloBytes = AxiomByteWriter().writeVarInt(10).writeVarInt(5000)
.writeVarInt(775).writeLong(challenge).toByteArray()
private val enableBytes = AxiomByteWriter().writeBoolean(true).writeByte(0).writeVarInt(2)
.writeInt(31_000).writeInt(2_097_152).writeVarInt(0).writeVarInt(0)
.writeVarInt(2).writeUtf("axiom:tunnel").writeUtf("axiom:set_buffer").toByteArray()
@Test fun `API 10 hello preserves the server-issued ID`() {
val hello = AxiomHelloCodec.read(helloBytes)
assertEquals(challenge, hello.handshakeId)
assertTrue(hello.isSupportedApi)
assertContentEquals(helloBytes, AxiomHelloCodec.write(hello))
}
@Test fun `API 10 hello rejects missing challenge ID and unknown API`() {
assertFailsWith<IllegalArgumentException> { AxiomHelloCodec.read(helloBytes.copyOf(helloBytes.size - 8)) }
assertFailsWith<IllegalArgumentException> { AxiomHelloCodec.write(AxiomHello(10, 5000, 775)) }
assertFailsWith<IllegalArgumentException> { AxiomHelloCodec.read(byteArrayOf(11, 0, 0)) }
}
@Test fun `API 10 enable decodes upstream server config without using legacy buffer layout`() {
val enable = AxiomEnableCodec.read(enableBytes, 10)
assertEquals(0, enable.serverConfigVersion)
assertEquals(31_000, enable.tunnelSplitSize)
assertEquals(2_097_152, enable.maximumTunnelPacketSize)
assertEquals(listOf("axiom:tunnel", "axiom:set_buffer"), enable.supportedServerboundChannels)
assertContentEquals(enableBytes, AxiomEnableCodec.write(enable, 10))
}
@Test fun `unknown server config and nonempty overrides reject unsupported layout`() {
val changedVersion = enableBytes.copyOf().also { it[1] = 1 }
assertFailsWith<IllegalArgumentException> { AxiomEnableCodec.read(changedVersion, 10) }
val overrides = AxiomByteWriter().writeBoolean(true).writeByte(0).writeVarInt(2)
.writeInt(31_000).writeInt(2_097_152).writeVarInt(1).toByteArray()
assertFailsWith<IllegalArgumentException> { AxiomEnableCodec.read(overrides, 10) }
}
@Test fun `handshake is directional and does not invent a server version`() {
val tracker = negotiatedTracker()
assertEquals(challenge, tracker.challengeId)
assertEquals(775, tracker.hello?.protocolVersion)
assertNull(tracker.session(null))
assertTrue(assertNotNull(tracker.session(773)).protocolMismatch)
tracker.observe(AxiomPayloadDirection.SERVERBOUND, "axiom:goodbye", byteArrayOf())
assertNotNull(tracker.enable)
}
@Test fun `wrong challenge never creates a session`() {
val tracker = AxiomHandshakeTracker()
tracker.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:hello", AxiomByteWriter().writeLong(challenge + 1).toByteArray())
assertFailsWith<IllegalArgumentException> {
tracker.observe(AxiomPayloadDirection.SERVERBOUND, "axiom:hello", helloBytes)
}
assertNull(tracker.hello)
assertNull(tracker.session(773))
}
@Test fun `redo handshake and goodbye clear old negotiation`() {
val tracker = negotiatedTracker()
tracker.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:redo_handshake", byteArrayOf())
assertNull(tracker.hello)
assertNull(tracker.enable)
assertNull(tracker.challengeId)
val goodbyeTracker = negotiatedTracker()
goodbyeTracker.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:goodbye", AxiomByteWriter().writeUtf("Missing axiom.use permission").toByteArray())
assertNull(goodbyeTracker.session(773))
assertEquals("Missing axiom.use permission", goodbyeTracker.goodbyeReason)
}
@Test fun `disabled and malformed enable invalidate previous session`() {
val tracker = negotiatedTracker()
tracker.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:enable", byteArrayOf(0))
assertNull(tracker.session(773))
assertFalse(assertNotNull(tracker.enable).enabled)
assertFailsWith<IllegalArgumentException> {
tracker.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:enable", byteArrayOf(1, 3))
}
assertNull(tracker.enable)
}
@Test fun `legacy handshake still negotiates API 9`() {
val tracker = AxiomHandshakeTracker()
tracker.observe(AxiomPayloadDirection.SERVERBOUND, "axiom:hello", byteArrayOf(9, 0, 1))
tracker.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:enable", AxiomEnableCodec.write(AxiomEnable(true, 1000, 2, 0, 0)))
assertEquals(9, assertNotNull(tracker.session(2)).hello.apiVersion)
}
private fun negotiatedTracker() = AxiomHandshakeTracker().also {
it.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:hello", AxiomByteWriter().writeLong(challenge).toByteArray())
it.observe(AxiomPayloadDirection.SERVERBOUND, "axiom:hello", helloBytes)
it.observe(AxiomPayloadDirection.CLIENTBOUND, "axiom:enable", enableBytes)
}
}
@@ -0,0 +1,126 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import me.wargearsystems.viaaxiom.translation.*
import kotlin.test.*
class AxiomProxySessionTest {
private val challenge = 42L
private val legacyChannels = "axiom:hello\u0000axiom:set_block\u0000axiom:set_buffer"
private val modernChannels = "$legacyChannels\u0000axiom:tunnel"
private val engine = AxiomTranslationEngine(TableBlockStateIdTranslator(
intArrayOf(0, 2, 1), intArrayOf(0, 2, 1), 0), BlockStatePaletteWidths(15, 15))
@Test fun `API 9 backend is bridged`() {
val session = session()
session.backendRegistered(legacyChannels.toByteArray())
assertTrue(session.awaitingLegacyChallenge)
assertEquals("axiom:hello", assertNotNull(session.startLegacyHandshake()).channel)
val hello = session.serverbound(modernHello()).single()
assertEquals(AxiomHello(9, 5000, 771), AxiomHelloCodec.read(hello.payloadBytes))
val enable = session.clientbound(frame("axiom:enable", AxiomEnableCodec.write(AxiomEnable(true, 1_048_576, 2, 0, 0), 9)))
assertTrue(AxiomEnableCodec.read(enable.payloadBytes, 10).enabled)
}
@Test fun `API 10 backend on another version gets the backend protocol in the hello`() {
val session = enabled()
assertFalse(session.awaitingLegacyChallenge)
session.clientbound(frame("axiom:goodbye", AxiomByteWriter().writeUtf("again").toByteArray()))
session.clientbound(challengeFrame())
val hello = session.serverbound(modernHello()).single()
assertEquals(AxiomHello(10, 5000, 771, challenge), AxiomHelloCodec.read(hello.payloadBytes))
}
@Test fun `API 10 small payloads are translated`() {
val output = enabled().serverbound(frame("axiom:set_block", setBlock(1))).single()
assertContentEquals(setBlock(2), output.payloadBytes)
}
@Test fun `API 10 tunnel is translated and keeps server split size`() {
val fragments = AxiomTunnelCodec.encode(AxiomTunnelPacket("axiom:set_block", setBlock(1)), SPLIT)
val output = enabled().serverbound(frame("axiom:tunnel", fragments.single()))
val packet = AxiomTunnelCodec.decode(output.map { it.payloadBytes }, SPLIT)
assertEquals("axiom:set_block", packet.channel)
assertContentEquals(setBlock(2), packet.payload)
}
@Test fun `API 10 fragments are held until the last one`() {
val body = AxiomByteWriter().writeVarInt(50).apply { repeat(50) { writeLong(it.toLong()).writeVarInt(1) } }.toByteArray()
val fragments = AxiomTunnelCodec.encode(AxiomTunnelPacket("axiom:set_block", body), 32)
assertTrue(fragments.size > 1)
val session = enabled(split = 32)
fragments.dropLast(1).forEach { assertTrue(session.serverbound(frame("axiom:tunnel", it)).isEmpty()) }
val output = session.serverbound(frame("axiom:tunnel", fragments.last()))
val packet = AxiomTunnelCodec.decode(output.map { it.payloadBytes }, 32)
val expected = AxiomByteWriter().writeVarInt(50).apply { repeat(50) { writeLong(it.toLong()).writeVarInt(2) } }.toByteArray()
assertContentEquals(expected, packet.payload)
}
@Test fun `tunnel continuation without a first fragment fails closed`() {
assertFailsWith<IllegalArgumentException> { enabled().serverbound(frame("axiom:tunnel", byteArrayOf(2, 1))) }
}
@Test fun `API 10 matching versions pass through untouched`() {
val session = AxiomProxySession(776, 776, engine) { challenge }
session.backendRegistered(modernChannels.toByteArray())
session.clientbound(challengeFrame())
val hello = modernHello()
assertSame(hello, session.serverbound(hello).single())
val tunnel = frame("axiom:tunnel", byteArrayOf(3, 1))
assertSame(tunnel, session.serverbound(tunnel).single())
}
@Test fun `matching versions with an API 9 backend still bridge the API`() {
val session = AxiomProxySession(771, 771, engine) { challenge }
session.backendRegistered(legacyChannels.toByteArray())
assertNotNull(session.startLegacyHandshake())
val hello = session.serverbound(frame("axiom:hello", AxiomHelloCodec.write(AxiomHello(10, 5000, 771, challenge)))).single()
assertEquals(9, AxiomHelloCodec.read(hello.payloadBytes).apiVersion)
}
@Test fun `API 9 client is left to negotiate directly`() {
val session = session()
session.backendRegistered(legacyChannels.toByteArray())
val hello = frame("axiom:hello", AxiomHelloCodec.write(AxiomHello(9, 5000, 776)))
assertSame(hello, session.serverbound(hello).single())
assertTrue(session.passthrough)
assertFalse(session.awaitingLegacyChallenge)
val edit = frame("axiom:set_block", setBlock(1))
assertSame(edit, session.serverbound(edit).single())
}
@Test fun `unknown client API does not break the session`() {
val session = session()
val hello = frame("axiom:hello", AxiomByteWriter().writeVarInt(11).toByteArray())
assertSame(hello, session.serverbound(hello).single())
assertTrue(session.passthrough)
}
@Test fun `unknown enable format fails rather than running untranslated`() {
val session = session()
session.backendRegistered(modernChannels.toByteArray())
session.clientbound(challengeFrame())
session.serverbound(modernHello())
val enable = AxiomByteWriter().writeBoolean(true).writeByte(7).toByteArray()
assertFailsWith<IllegalArgumentException> { session.clientbound(frame("axiom:enable", enable)) }
}
private fun session() = AxiomProxySession(776, 771, engine) { challenge }
private fun enabled(split: Int = SPLIT) = session().also {
it.backendRegistered(modernChannels.toByteArray())
it.clientbound(challengeFrame())
it.serverbound(modernHello())
it.clientbound(frame("axiom:enable", AxiomEnableCodec.write(AxiomEnable(
enabled = true, blueprintVersion = 2, customDataOverridesCount = 0, rotationOverridesCount = 0,
serverConfigVersion = 0, tunnelSplitSize = split, maximumTunnelPacketSize = 2_097_152,
supportedServerboundChannels = listOf("axiom:set_block", "axiom:set_buffer", "axiom:tunnel")), 10)))
}
private fun frame(channel: String, bytes: ByteArray) = AxiomCustomPayloadFrame(0, channel, bytes)
private fun challengeFrame() = frame("axiom:hello", AxiomByteWriter().writeLong(challenge).toByteArray())
private fun modernHello() = frame("axiom:hello", AxiomHelloCodec.write(AxiomHello(10, 5000, 776, challenge)))
private fun setBlock(id: Int) = AxiomByteWriter().writeVarInt(1).writeLong(123).writeVarInt(id).toByteArray()
private companion object {
const val SPLIT = 31_000
}
}
@@ -0,0 +1,69 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class AxiomResponseChunkDataPayloadCodecTest {
@Test
fun `rewrites present section payloads and preserves response structure`() {
val original = responsePayload(
sectionPayload = byteArrayOf(1, 2, 3),
finished = false,
)
val rewritten = AxiomResponseChunkDataPayloadCodec.rewriteSections(original) { reader, writer ->
assertContentEquals(byteArrayOf(1, 2, 3), reader.readByteArray())
writer.writeByteArray(byteArrayOf(9, 8, 7))
}
val reader = AxiomByteReader(rewritten)
assertEquals(42L, reader.readLong())
assertBlockEntityCopied(reader)
assertEquals(AxiomProtocol.MIN_POSITION_LONG, reader.readLong())
assertEquals(0x2122232425262728L, reader.readLong())
assertEquals(true, reader.readBoolean())
assertContentEquals(byteArrayOf(9, 8, 7), reader.readByteArray())
assertEquals(0x3132333435363738L, reader.readLong())
assertEquals(false, reader.readBoolean())
assertEquals(AxiomProtocol.MIN_POSITION_LONG, reader.readLong())
assertEquals(false, reader.readBoolean())
assertEquals(99, reader.readVarInt())
assertEquals(0, reader.readableBytes)
}
private fun responsePayload(sectionPayload: ByteArray, finished: Boolean): ByteArray {
val writer = AxiomByteWriter()
.writeLong(42L)
writeBlockEntity(writer)
writer
.writeLong(AxiomProtocol.MIN_POSITION_LONG)
.writeLong(0x2122232425262728L)
.writeBoolean(true)
.writeByteArray(sectionPayload)
.writeLong(0x3132333435363738L)
.writeBoolean(false)
.writeLong(AxiomProtocol.MIN_POSITION_LONG)
.writeBoolean(finished)
.writeVarInt(99)
return writer.toByteArray()
}
private fun writeBlockEntity(writer: AxiomByteWriter) {
writer
.writeLong(0x0102030405060708L)
.writeVarInt(1234)
.writeByte(0)
.writeByteArray(byteArrayOf(5, 4, 3))
}
private fun assertBlockEntityCopied(reader: AxiomByteReader) {
assertEquals(0x0102030405060708L, reader.readLong())
assertEquals(1234, reader.readVarInt())
assertEquals(0, reader.readUnsignedByte())
assertContentEquals(byteArrayOf(5, 4, 3), reader.readByteArray())
}
}
@@ -0,0 +1,43 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class AxiomSetBlockPayloadCodecTest {
@Test
fun `rewrites only block state ids in leading block map`() {
val passthroughTail = AxiomByteWriter()
.writeBoolean(true)
.writeVarInt(1)
.writeLong(0x1112131415161718L)
.writeVarInt(128)
.writeBoolean(false)
.writeLong(0x2122232425262728L)
.writeVarInt(2)
.writeVarInt(0)
.toByteArray()
val payload = AxiomByteWriter()
.writeVarInt(2)
.writeLong(0x0102030405060708L)
.writeVarInt(10)
.writeLong(0x090A0B0C0D0E0F10L)
.writeVarInt(20)
.writeBytes(passthroughTail)
.toByteArray()
val rewritten = AxiomSetBlockPayloadCodec.rewriteBlockStateIds(payload) { id -> id + 1000 }
val reader = AxiomByteReader(rewritten)
assertEquals(2, reader.readVarInt())
assertEquals(0x0102030405060708L, reader.readLong())
assertEquals(1010, reader.readVarInt())
assertEquals(0x090A0B0C0D0E0F10L, reader.readLong())
assertEquals(1020, reader.readVarInt())
assertContentEquals(passthroughTail, reader.copyRemaining())
}
}
@@ -0,0 +1,90 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class AxiomSetBufferPayloadCodecTest {
@Test
fun `rewrites block section payloads and preserves block entities plus tail`() {
val original = setBufferPayload(
type = 0,
sectionPayload = byteArrayOf(1, 2, 3),
)
val rewritten = AxiomSetBufferPayloadCodec.rewriteBlockSections(original) { reader, writer ->
assertContentEquals(byteArrayOf(1, 2, 3), reader.readByteArray())
writer.writeByteArray(byteArrayOf(9, 8, 7, 6))
}
val reader = AxiomByteReader(rewritten)
assertEquals("minecraft:overworld", reader.readUtf())
assertEquals(0x0102030405060708L, reader.readLong())
assertEquals(0x1112131415161718L, reader.readLong())
assertEquals(0, reader.readUnsignedByte())
assertEquals(0x2122232425262728L, reader.readLong())
assertContentEquals(byteArrayOf(9, 8, 7, 6), reader.readByteArray())
assertBlockEntityCopied(reader)
assertEquals(AxiomProtocol.MIN_POSITION_LONG, reader.readLong())
assertEquals(64, reader.readVarInt())
assertEquals(0, reader.readableBytes)
}
@Test
fun `passes non block buffer payloads through after header`() {
val original = setBufferPayload(
type = 1,
sectionPayload = byteArrayOf(1, 2, 3),
)
val rewritten = AxiomSetBufferPayloadCodec.rewriteBlockSections(original) { _, _ ->
error("Biome buffers should not invoke block section rewriter")
}
assertContentEquals(original, rewritten)
}
private fun setBufferPayload(type: Int, sectionPayload: ByteArray): ByteArray {
val writer = AxiomByteWriter()
.writeUtf("minecraft:overworld")
.writeLong(0x0102030405060708L)
.writeLong(0x1112131415161718L)
.writeByte(type)
if (type == 0) {
writer
.writeLong(0x2122232425262728L)
.writeByteArray(sectionPayload)
writeBlockEntity(writer)
writer
.writeLong(AxiomProtocol.MIN_POSITION_LONG)
.writeVarInt(64)
} else {
writer
.writeByteArray(sectionPayload)
.writeVarInt(64)
}
return writer.toByteArray()
}
private fun writeBlockEntity(writer: AxiomByteWriter) {
writer
.writeVarInt(1)
.writeShort(0x00AF)
.writeVarInt(1234)
.writeByte(0)
.writeByteArray(byteArrayOf(5, 4, 3, 2, 1))
}
private fun assertBlockEntityCopied(reader: AxiomByteReader) {
assertEquals(1, reader.readVarInt())
assertEquals(0x00AF, reader.readShort().toInt())
assertEquals(1234, reader.readVarInt())
assertEquals(0, reader.readUnsignedByte())
assertContentEquals(byteArrayOf(5, 4, 3, 2, 1), reader.readByteArray())
}
}
@@ -0,0 +1,51 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.*
class AxiomTunnelCodecTest {
@Test fun `uncompressed single fragment matches upstream wire format`() {
val fragment = AxiomByteWriter().writeByte(3).writeUtf("axiom:set_block").writeInt(3)
.writeByte(0).writeBytes(byteArrayOf(5, 6, 7)).toByteArray()
val packet = AxiomTunnelCodec.decode(listOf(fragment))
assertEquals("axiom:set_block", packet.channel)
assertContentEquals(byteArrayOf(5, 6, 7), packet.payload)
assertFalse(packet.compressed)
assertContentEquals(fragment, AxiomTunnelCodec.encode(packet).single())
}
@Test fun `fragmented packets obey total fragment size and recover exact payload`() {
val packet = AxiomTunnelPacket("axiom:set_buffer", ByteArray(257) { it.toByte() })
val fragments = AxiomTunnelCodec.encode(packet, splitSize = 23)
assertTrue(fragments.size > 2)
assertTrue(fragments.all { it.size <= 23 })
assertEquals(1, fragments.first()[0].toInt())
assertEquals(2, fragments.last()[0].toInt())
assertContentEquals(packet.payload, AxiomTunnelCodec.decode(fragments, 23).payload)
}
@Test fun `actual Zstd compression survives split transport`() {
val packet = AxiomTunnelPacket("axiom:set_buffer", ByteArray(16384) { (it * 17).toByte() }, true)
val fragments = AxiomTunnelCodec.encode(packet, 71)
assertTrue(fragments.size > 1)
val decoded = AxiomTunnelCodec.decode(fragments, 71)
assertTrue(decoded.compressed)
assertContentEquals(packet.payload, decoded.payload)
}
@Test fun `rejects out of order fragments and oversized decoded payload`() {
val packet = AxiomTunnelPacket("axiom:set_buffer", ByteArray(100), true)
val fragments = AxiomTunnelCodec.encode(packet, 10)
assertFailsWith<IllegalArgumentException> { AxiomTunnelCodec.decode(fragments.reversed(), 10) }
assertFailsWith<IllegalArgumentException> { AxiomTunnelCodec.decode(fragments, 10, 99) }
assertFailsWith<IllegalArgumentException> { AxiomTunnelCodec.encode(packet, 10, 99) }
}
@Test fun `rejects mismatched declared length corrupt compression and unknown flags`() {
for ((length, flags) in listOf(100 to 0, 3 to 1, 3 to 2, -1 to 0)) {
val fragment = AxiomByteWriter().writeByte(3).writeUtf("axiom:set_block").writeInt(length)
.writeByte(flags).writeBytes(byteArrayOf(5, 6, 7)).toByteArray()
assertFailsWith<IllegalArgumentException> { AxiomTunnelCodec.decode(listOf(fragment)) }
}
}
}
@@ -0,0 +1,119 @@
package me.wargearsystems.viaaxiom.protocol
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import kotlin.test.*
class PalettedContainerCodecTest {
@Test fun `singleton consumes exactly one section across both formats`() {
for (protocol in listOf(769, 770)) {
val bytes = AxiomByteWriter().writeByte(0).writeVarInt(1234).also {
if (protocol < 770) it.writeVarInt(0)
}.writeInt(0x12345678).toByteArray()
val reader = AxiomByteReader(bytes)
val container = PalettedContainerCodec.readBlockContainer(reader, protocol, 15)
assertEquals(1234, container.idAt(4095))
assertEquals(0x12345678, reader.readInt())
assertEquals(0, reader.readableBytes)
}
}
@Test fun `local palette conversion preserves indexes and merges colliding states`() {
val input = AxiomByteWriter().writeByte(4).writeVarInt(2).writeVarInt(2).writeVarInt(3)
.writeVarInt(256).writeLongArray(LongArray(256) { 0x1010101010101010L }, false).toByteArray()
val output = AxiomByteWriter()
PalettedContainerCodec.rewriteBlockStateIds(AxiomByteReader(input), output, 769, 15, { 700 }, 770, 16)
assertContentEquals(AxiomByteWriter().writeByte(0).writeVarInt(700).toByteArray(), output.toByteArray())
}
@Test fun `global wire IDs become indexes and sparse registry IDs survive mapping`() {
// Four 15-bit entries per long, with one slot unused at the top.
val packed = 1000L or (2000L shl 15) or (1000L shl 30) or (2000L shl 45)
val input = AxiomByteWriter().writeByte(15).writeLongArray(LongArray(1024) { packed }, false)
.writeByte(42).toByteArray()
val reader = AxiomByteReader(input)
val container = PalettedContainerCodec.readBlockContainer(reader, 770, 15)
assertEquals(1000, container.idAt(0))
assertEquals(2000, container.idAt(4095))
assertEquals(42, reader.readUnsignedByte())
container.replaceIds { it + 1 }
assertEquals(1001, container.idAt(0))
assertEquals(2001, container.idAt(4095))
}
@Test fun `global palette writes every value across partial final long`() {
val data = PalettedContainerData((1000..1299).toMutableList(), IntArray(4096) { it % 300 })
for (protocol in listOf(769, 770)) {
val writer = AxiomByteWriter()
PalettedContainerCodec.writeBlockContainer(writer, data, protocol, 13)
val bytes = writer.toByteArray()
assertEquals(13, bytes[0].toInt())
val restored = PalettedContainerCodec.readBlockContainer(AxiomByteReader(bytes), protocol, 13)
repeat(4096) { assertEquals(1000 + it % 300, restored.idAt(it)) }
}
}
@Test fun `same layout rewrite decodes like the full conversion`() {
val random = kotlin.random.Random(7)
val mapper = { id: Int -> (id * 3 + 1) % 500 }
for (protocol in listOf(769, 770)) {
for (distinct in listOf(1, 2, 16, 17, 256, 300)) {
for (width in listOf(9, 13, 15)) {
val ids = List(distinct) { random.nextInt(500) }.distinct()
val source = PalettedContainerData(ids.toMutableList(), IntArray(4096) { random.nextInt(ids.size) })
val input = AxiomByteWriter().also { PalettedContainerCodec.writeBlockContainer(it, source, protocol, width) }
.writeByte(42).toByteArray()
val output = AxiomByteWriter()
val reader = AxiomByteReader(input)
PalettedContainerCodec.rewriteBlockStateIds(reader, output, protocol, width, mapper)
assertEquals(42, reader.readUnsignedByte())
val result = AxiomByteReader(output.toByteArray())
val actual = PalettedContainerCodec.readBlockContainer(result, protocol, width)
assertEquals(0, result.readableBytes)
repeat(4096) { assertEquals(mapper(source.idAt(it)), actual.idAt(it)) }
}
}
}
}
@Test fun `palette indexes are copied without changing the packed data`() {
val packed = LongArray(256) { 0x0123012301230123L }
val input = AxiomByteWriter().writeByte(4).writeVarInt(4).writeVarInt(5).writeVarInt(6).writeVarInt(7).writeVarInt(8)
.writeLongArray(packed, false).toByteArray()
val output = AxiomByteWriter()
PalettedContainerCodec.rewriteBlockStateIds(AxiomByteReader(input), output, 770, 15, { 700 })
val expected = AxiomByteWriter().writeByte(4).writeVarInt(4).writeVarInt(700).writeVarInt(700).writeVarInt(700).writeVarInt(700)
.writeLongArray(packed, false).toByteArray()
assertContentEquals(expected, output.toByteArray())
}
@Test fun `same layout rewrite rejects bad indexes and IDs that do not fit`() {
val badIndex = AxiomByteWriter().writeByte(4).writeVarInt(1).writeVarInt(1)
.writeLongArray(LongArray(256) { 2L }, false).toByteArray()
assertFailsWith<IllegalArgumentException> {
PalettedContainerCodec.rewriteBlockStateIds(AxiomByteReader(badIndex), AxiomByteWriter(), 770, 15, { it })
}
val singleton = AxiomByteWriter().writeByte(0).writeVarInt(1).toByteArray()
assertFailsWith<IllegalArgumentException> {
PalettedContainerCodec.rewriteBlockStateIds(AxiomByteReader(singleton), AxiomByteWriter(), 770, 15, { 32768 })
}
val global = AxiomByteWriter().writeByte(15).writeLongArray(LongArray(1024), false).toByteArray()
assertFailsWith<IllegalArgumentException> {
PalettedContainerCodec.rewriteBlockStateIds(AxiomByteReader(global), AxiomByteWriter(), 770, 15, { -1 })
}
}
@Test fun `rejects invalid palette indexes and mismatched array sizes`() {
val invalidIndex = AxiomByteWriter().writeByte(4).writeVarInt(1).writeVarInt(1)
.writeLongArray(LongArray(256) { 2L }, false).toByteArray()
assertFailsWith<IllegalArgumentException> { PalettedContainerCodec.readBlockContainer(AxiomByteReader(invalidIndex), 770, 15) }
val invalidLength = AxiomByteWriter().writeByte(0).writeVarInt(1).writeVarInt(1).writeLong(0).toByteArray()
assertFailsWith<IllegalArgumentException> { PalettedContainerCodec.readBlockContainer(AxiomByteReader(invalidLength), 769, 15) }
}
@Test fun `target width overflow is rejected instead of truncating block IDs`() {
val data = PalettedContainerData(mutableListOf(32768), IntArray(4096))
assertFailsWith<IllegalArgumentException> { PalettedContainerCodec.writeBlockContainer(AxiomByteWriter(), data, 770, 15) }
}
}
@@ -0,0 +1,73 @@
package me.wargearsystems.viaaxiom.translation
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import me.wargearsystems.viaaxiom.protocol.*
import kotlin.test.*
class AxiomBulkTranslationTest {
private val engine = AxiomTranslationEngine(object : BlockStateIdTranslator {
override fun clientToServer(blockStateId: Int) = blockStateId + 1000
override fun serverToClient(blockStateId: Int) = blockStateId - 1000
}, BlockStatePaletteWidths(15, 16))
private val session = AxiomTranslationSession(AxiomHello(9, 1, 769), 770)
@Test fun `set buffer rewrites multiple real palettes and preserves compressed block entities`() {
val writer = AxiomByteWriter().writeUtf("minecraft:overworld").writeLong(10).writeLong(20).writeByte(0)
for ((section, state) in listOf(100L to 2, 200L to 3)) {
writer.writeLong(section).writeByte(0).writeVarInt(state).writeVarInt(0)
writer.writeVarInt(1).writeShort(42).writeVarInt(7).writeByte(0).writeByteArray(byteArrayOf(9, 8, 7))
}
writer.writeLong(AxiomProtocol.MIN_POSITION_LONG).writeVarInt(64)
val result = engine.rewrite(AxiomPayload(AxiomPayloadDirection.SERVERBOUND, "axiom:set_buffer", writer.toByteArray()), session)
val reader = AxiomByteReader(result)
assertEquals("minecraft:overworld", reader.readUtf())
assertEquals(10, reader.readLong())
assertEquals(20, reader.readLong())
assertEquals(0, reader.readUnsignedByte())
for ((section, state) in listOf(100L to 1002, 200L to 1003)) {
assertEquals(section, reader.readLong())
assertEquals(state, PalettedContainerCodec.readBlockContainer(reader, 770, 16).idAt(4095))
assertEquals(1, reader.readVarInt())
assertEquals(42, reader.readShort().toInt())
assertEquals(7, reader.readVarInt())
assertEquals(0, reader.readUnsignedByte())
assertContentEquals(byteArrayOf(9, 8, 7), reader.readByteArray())
}
assertEquals(AxiomProtocol.MIN_POSITION_LONG, reader.readLong())
assertEquals(64, reader.readVarInt())
assertEquals(0, reader.readableBytes)
}
@Test fun `response maps server palette to legacy client format and retains missing sections`() {
val bytes = AxiomByteWriter().writeLong(77).writeLong(AxiomProtocol.MIN_POSITION_LONG)
.writeLong(100).writeBoolean(true).writeByte(0).writeVarInt(1002)
.writeLong(200).writeBoolean(false).writeLong(AxiomProtocol.MIN_POSITION_LONG)
.writeBoolean(true).toByteArray()
val result = engine.rewrite(AxiomPayload(AxiomPayloadDirection.CLIENTBOUND, "axiom:response_chunk_data", bytes), session)
val reader = AxiomByteReader(result)
assertEquals(77, reader.readLong())
assertEquals(AxiomProtocol.MIN_POSITION_LONG, reader.readLong())
assertEquals(100, reader.readLong())
assertTrue(reader.readBoolean())
assertEquals(2, PalettedContainerCodec.readBlockContainer(reader, 769, 15).idAt(0))
assertEquals(200, reader.readLong())
assertFalse(reader.readBoolean())
assertEquals(AxiomProtocol.MIN_POSITION_LONG, reader.readLong())
assertTrue(reader.readBoolean())
assertEquals(0, reader.readableBytes)
}
@Test fun `biome buffer passes through without touching its body`() {
val bytes = AxiomByteWriter().writeUtf("minecraft:overworld").writeLong(10).writeLong(20)
.writeByte(1).writeBytes(byteArrayOf(1, 2, 3)).toByteArray()
assertContentEquals(bytes, engine.rewrite(AxiomPayload(AxiomPayloadDirection.SERVERBOUND, "axiom:set_buffer", bytes), session))
}
@Test fun `bulk rewriting requires explicit registry widths`() {
val bytes = byteArrayOf(1, 2, 3)
assertContentEquals(bytes, AxiomTranslationEngine(IdentityBlockStateIdTranslator).rewrite(
AxiomPayload(AxiomPayloadDirection.SERVERBOUND, "axiom:set_buffer", bytes), session,
))
}
}
@@ -0,0 +1,85 @@
package me.wargearsystems.viaaxiom.translation
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import me.wargearsystems.viaaxiom.protocol.AxiomCustomPayloadFrameCodec
import me.wargearsystems.viaaxiom.protocol.AxiomHello
import me.wargearsystems.viaaxiom.protocol.AxiomPayloadDirection
import me.wargearsystems.viaaxiom.protocol.AxiomProtocol
import me.wargearsystems.viaaxiom.protocol.AxiomTranslationSession
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class AxiomFrameTranslationEngineTest {
private val frameEngine = AxiomFrameTranslationEngine(
AxiomTranslationEngine(
TableBlockStateIdTranslator(
clientToServer = intArrayOf(0, 101, 102),
serverToClient = intArrayOf(0, 201, 202),
fallbackId = 0,
),
),
)
@Test
fun `rewrites set block payload inside a matching custom payload frame`() {
val rewrittenFrame = frameEngine.rewrite(
frameBytes = customPayloadFrame(packetId = 12, channel = AxiomProtocol.channel("set_block"), payload = setBlockPayload(2)),
customPayloadPacketId = 12,
direction = AxiomPayloadDirection.SERVERBOUND,
session = session(clientProtocol = 772, serverProtocol = 773),
)
val frame = AxiomCustomPayloadFrameCodec.readIfCustomPayload(rewrittenFrame, customPayloadPacketId = 12)
requireNotNull(frame)
val payload = AxiomByteReader(frame.payloadBytes)
assertEquals(1, payload.readVarInt())
assertEquals(0x0102030405060708L, payload.readLong())
assertEquals(102, payload.readVarInt())
}
@Test
fun `passes through when the packet id is not custom payload`() {
val frame = customPayloadFrame(packetId = 11, channel = AxiomProtocol.channel("set_block"), payload = setBlockPayload(2))
val rewrittenFrame = frameEngine.rewrite(
frameBytes = frame,
customPayloadPacketId = 12,
direction = AxiomPayloadDirection.SERVERBOUND,
session = session(clientProtocol = 772, serverProtocol = 773),
)
assertContentEquals(frame, rewrittenFrame)
}
private fun customPayloadFrame(packetId: Int, channel: String, payload: ByteArray): ByteArray {
return AxiomByteWriter()
.writeVarInt(packetId)
.writeUtf(channel)
.writeBytes(payload)
.toByteArray()
}
private fun setBlockPayload(blockStateId: Int): ByteArray {
return AxiomByteWriter()
.writeVarInt(1)
.writeLong(0x0102030405060708L)
.writeVarInt(blockStateId)
.writeBoolean(false)
.toByteArray()
}
private fun session(clientProtocol: Int, serverProtocol: Int): AxiomTranslationSession {
return AxiomTranslationSession(
hello = AxiomHello(
apiVersion = AxiomProtocol.API_VERSION,
dataVersion = 4438,
protocolVersion = clientProtocol,
),
serverProtocolVersion = serverProtocol,
)
}
}
@@ -0,0 +1,80 @@
package me.wargearsystems.viaaxiom.translation
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import me.wargearsystems.viaaxiom.protocol.AxiomHello
import me.wargearsystems.viaaxiom.protocol.AxiomPayloadDirection
import me.wargearsystems.viaaxiom.protocol.AxiomProtocol
import me.wargearsystems.viaaxiom.protocol.AxiomTranslationSession
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class AxiomTranslationEngineTest {
private val translator = TableBlockStateIdTranslator(
clientToServer = intArrayOf(0, 101, 102, 103),
serverToClient = intArrayOf(0, 201, 202, 203),
fallbackId = 0,
)
private val engine = AxiomTranslationEngine(translator)
@Test
fun `passes through when protocol versions match`() {
val payload = setBlockPayload(blockStateId = 2)
val rewritten = engine.rewrite(
payload = AxiomPayload(AxiomPayloadDirection.SERVERBOUND, AxiomProtocol.channel("set_block"), payload),
session = session(clientProtocol = 773, serverProtocol = 773),
)
assertContentEquals(payload, rewritten)
}
@Test
fun `passes through non axiom channels`() {
val payload = byteArrayOf(1, 2, 3)
val rewritten = engine.rewrite(
payload = AxiomPayload(AxiomPayloadDirection.SERVERBOUND, "minecraft:brand", payload),
session = session(clientProtocol = 772, serverProtocol = 773),
)
assertContentEquals(payload, rewritten)
}
@Test
fun `rewrites serverbound set block ids on protocol mismatch`() {
val rewritten = engine.rewrite(
payload = AxiomPayload(
direction = AxiomPayloadDirection.SERVERBOUND,
channel = AxiomProtocol.channel("set_block"),
bytes = setBlockPayload(blockStateId = 2),
),
session = session(clientProtocol = 772, serverProtocol = 773),
)
val reader = AxiomByteReader(rewritten)
assertEquals(1, reader.readVarInt())
assertEquals(0x0102030405060708L, reader.readLong())
assertEquals(102, reader.readVarInt())
}
private fun setBlockPayload(blockStateId: Int): ByteArray {
return AxiomByteWriter()
.writeVarInt(1)
.writeLong(0x0102030405060708L)
.writeVarInt(blockStateId)
.writeBoolean(false)
.toByteArray()
}
private fun session(clientProtocol: Int, serverProtocol: Int): AxiomTranslationSession {
return AxiomTranslationSession(
hello = AxiomHello(
apiVersion = AxiomProtocol.API_VERSION,
dataVersion = 4438,
protocolVersion = clientProtocol,
),
serverProtocolVersion = serverProtocol,
)
}
}
@@ -0,0 +1,43 @@
package me.wargearsystems.viaaxiom.translation
import me.wargearsystems.viaaxiom.binary.AxiomByteReader
import me.wargearsystems.viaaxiom.binary.AxiomByteWriter
import me.wargearsystems.viaaxiom.protocol.*
import kotlin.test.*
class AxiomTunnelTranslationEngineTest {
private val enable = AxiomEnable(true, 0, 2, 0, 0, 0, 17, 1024, listOf("axiom:set_block"))
private val session = AxiomTranslationSession(AxiomHello(10, 5000, 775, 42), 773, enable = enable)
private val translator = AxiomTranslationEngine(object : BlockStateIdTranslator {
override fun clientToServer(blockStateId: Int) = 100_000 + blockStateId
override fun serverToClient(blockStateId: Int) = blockStateId
})
@Test fun `fragmented compressed upload translates inner IDs and preserves tail`() {
val payload = AxiomByteWriter().writeVarInt(1).writeLong(123).writeVarInt(2)
.writeBytes(byteArrayOf(0, 9, 8, 7)).toByteArray()
for (compressed in listOf(false, true)) {
val fragments = AxiomTunnelCodec.encode(AxiomTunnelPacket("axiom:set_block", payload, compressed), 17, 1024)
val result = AxiomTunnelTranslationEngine(translator).rewrite(fragments, session)
assertTrue(result.all { it.size <= 17 })
val packet = AxiomTunnelCodec.decode(result, 17, 1024)
assertEquals(compressed, packet.compressed)
val reader = AxiomByteReader(packet.payload)
assertEquals(1, reader.readVarInt())
assertEquals(123, reader.readLong())
assertEquals(100_002, reader.readVarInt())
assertContentEquals(byteArrayOf(0, 9, 8, 7), reader.copyRemaining())
}
}
@Test fun `unchanged payload retains original compression and fragment bytes`() {
val payload = AxiomByteWriter().writeVarInt(0).writeBoolean(false).toByteArray()
val fragments = AxiomTunnelCodec.encode(AxiomTunnelPacket("axiom:set_block", payload, true), 17, 1024)
assertSame(fragments, AxiomTunnelTranslationEngine(translator).rewrite(fragments, session))
}
@Test fun `refuses channels not advertised by server`() {
val fragments = AxiomTunnelCodec.encode(AxiomTunnelPacket("axiom:delete_entity", byteArrayOf()), 17, 1024)
assertFailsWith<IllegalArgumentException> { AxiomTunnelTranslationEngine(translator).rewrite(fragments, session) }
}
}
@@ -0,0 +1,65 @@
package me.wargearsystems.viaaxiom.translation
import kotlin.test.Test
import kotlin.test.assertEquals
class BlockStateIdTranslatorTest {
@Test
fun `uses directional placeholders for missing block state ids`() {
val translator = TableBlockStateIdTranslator(
clientToServer = intArrayOf(0, 10, -1),
serverToClient = intArrayOf(0, -1, 20),
missingBlockStatePolicy = MissingBlockStatePolicy(
clientToServerFallbackId = 5,
serverToClientFallbackId = 6,
),
)
assertEquals(10, translator.clientToServer(1))
assertEquals(5, translator.clientToServer(2))
assertEquals(5, translator.clientToServer(99))
assertEquals(20, translator.serverToClient(2))
assertEquals(6, translator.serverToClient(1))
assertEquals(6, translator.serverToClient(99))
}
@Test
fun `single placeholder constructor applies in both directions`() {
val translator = TableBlockStateIdTranslator(
clientToServer = intArrayOf(-1),
serverToClient = intArrayOf(-1),
fallbackId = 7,
)
assertEquals(7, translator.clientToServer(0))
assertEquals(7, translator.serverToClient(0))
}
@Test
fun `reports missing block state fallback usage`() {
val reports = mutableListOf<Triple<BlockStateMappingDirection, Int, Int>>()
val translator = TableBlockStateIdTranslator(
clientToServer = intArrayOf(-1),
serverToClient = intArrayOf(-1),
missingBlockStatePolicy = MissingBlockStatePolicy(
clientToServerFallbackId = 5,
serverToClientFallbackId = 6,
),
onMissingBlockState = { direction, missingId, fallbackId ->
reports += Triple(direction, missingId, fallbackId)
},
)
translator.clientToServer(0)
translator.serverToClient(99)
assertEquals(
listOf(
Triple(BlockStateMappingDirection.CLIENT_TO_SERVER, 0, 5),
Triple(BlockStateMappingDirection.SERVER_TO_CLIENT, 99, 6),
),
reports,
)
}
}
@@ -0,0 +1,22 @@
package me.wargearsystems.viaaxiom.translation
import kotlin.test.*
class BlockStateMappingChainTest {
@Test fun `multi version mappings compose in server to client order`() {
val (outbound, inbound) = BlockStateMappingChain.compose(listOf(intArrayOf(0, 2, 1), intArrayOf(0, 1, 3)), 5, 1)
assertContentEquals(intArrayOf(0, 3, 1), inbound)
assertContentEquals(intArrayOf(0, 2, -1, 1, -1), outbound)
}
@Test fun `many to one collision prefers earlier canonical server state`() {
val (outbound, _) = BlockStateMappingChain.compose(listOf(intArrayOf(0, 1, 1, 2)), 3, 1)
assertEquals(1, outbound[1])
}
@Test fun `missing mappings remain missing and stone must exist`() {
val (_, inbound) = BlockStateMappingChain.compose(listOf(intArrayOf(0, 1, -1, 99)), 3, 1)
assertContentEquals(intArrayOf(0, 1, -1, -1), inbound)
assertFailsWith<IllegalArgumentException> { BlockStateMappingChain.compose(listOf(intArrayOf(0, -1)), 3, 1) }
}
}