Add packet parsing, CLI commands, and serialization support

Introduce new packet parsing functionality with a PacketDecoder and serializer-based system. Add CLI commands for listing packets, displaying replay info, and extracting schematics. Enhance project dependencies and build configuration to support serialization and CLI tools.
This commit is contained in:
2025-03-06 21:45:45 +01:00
parent 9ddc02ad79
commit 7803e05431
12 changed files with 665 additions and 14 deletions

6
.idea/gradle.xml generated
View File

@ -1,10 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>

3
.idea/misc.xml generated
View File

@ -12,6 +12,9 @@
</option>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>

View File

@ -1,5 +1,7 @@
plugins {
kotlin("jvm") version "2.1.10"
kotlin("plugin.serialization") version "2.1.0"
application
}
group = "de.chaoscaot"
@ -11,6 +13,13 @@ repositories {
dependencies {
testImplementation(kotlin("test"))
implementation("com.github.ajalt.clikt:clikt:5.0.3")
implementation("com.github.ajalt.clikt:clikt-markdown:5.0.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
implementation("com.github.ajalt.mordant:mordant:3.0.2")
implementation("com.github.ajalt.mordant:mordant-coroutines:3.0.2")
implementation("com.github.ajalt.mordant:mordant-markdown:3.0.2")
implementation("dev.dewy:nbt:1.5.1")
}
tasks.test {

View File

@ -1,2 +1,33 @@
package de.chaoscaot.replay
import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.optional
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import de.chaoscaot.replay.commands.*
import java.io.File
data class RunConfig(var replay: File? = null)
object ReplayUtils : CliktCommand() {
override val printHelpOnEmptyArgs = true
private val config by findOrSetObject { RunConfig() }
private val replay by argument(help = "Replay ID or Path").optional()
private val path by option("-p", "--path", help = "Replay is a Path").flag()
override fun run() {
if (replay != null) {
if (path) {
config.replay = File(replay!!)
} else {
config.replay = File("/mnt/storage/replays/$replay.replay")
}
} else {
config.replay = null
}
}
}
fun main(args: Array<String>) = ReplayUtils.subcommands(Info, ListPackets, ExtractSchematics).main(readln().split(" "))

View File

@ -1,4 +1,43 @@
package de.chaoscaot.replay.commands
object ExtractSchematics {
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import de.chaoscaot.replay.RunConfig
import de.chaoscaot.replay.parser.BlueEmbeddedSchemPacket
import de.chaoscaot.replay.parser.RedEmbeddedSchemPacket
import de.chaoscaot.replay.parser.ReplayParser
import com.github.ajalt.mordant.rendering.TextColors.*
import com.github.ajalt.mordant.rendering.TextStyles
import dev.dewy.nbt.Nbt
import dev.dewy.nbt.io.CompressionType
import java.io.File
object ExtractSchematics: CliktCommand() {
private val config by requireObject<RunConfig>()
override fun run() {
if (config.replay == null) {
error("No Replay specified")
}
val nbt by lazy { Nbt() }
val packets = ReplayParser.read(config.replay!!).toList()
val blueSchem = packets.filterIsInstance<BlueEmbeddedSchemPacket>().firstOrNull()
if (blueSchem == null) {
echo((brightRed + TextStyles.bold)("No Blue Embedded Schem found"))
} else {
nbt.toFile(blueSchem.schematic.data, File("blue.dump"), CompressionType.GZIP)
}
val redSchem = packets.filterIsInstance<RedEmbeddedSchemPacket>().firstOrNull()
if (redSchem == null) {
echo((brightRed + TextStyles.bold)("No Red Embedded Schem found"))
} else {
nbt.toFile(redSchem.schematic.data, File("red.dump"), CompressionType.GZIP)
}
}
}

View File

@ -1,4 +1,53 @@
package de.chaoscaot.replay.commands
object Info {
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.multiple
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.mordant.table.table
import de.chaoscaot.replay.RunConfig
import de.chaoscaot.replay.parser.ReplayParser
import de.chaoscaot.replay.parser.UserJoinPacket
object Info : CliktCommand() {
private val config by requireObject<RunConfig>()
private val players by option("-p", "--players").flag()
private val toCount by option("-c", "--count").multiple(required = false)
override fun run() {
if (config.replay == null) {
error("No Replay specified")
}
val packets = ReplayParser.read(config.replay!!).toList()
if (players) {
echo("Players:")
packets.filterIsInstance<UserJoinPacket>().map { it.userId }.forEach { echo("\t$it") }
echo()
}
if (toCount.isNotEmpty()) {
echo(table {
header {
row("PacketType", "Count")
}
body {
toCount.forEach { name ->
val klass = packets.firstOrNull { it.javaClass.simpleName.lowercase() == "${name}packet".lowercase() }?.javaClass
if (klass == null) {
row(name, 0)
} else {
row(klass.simpleName, packets.count { it.javaClass == klass })
}
}
}
})
}
}
}

View File

@ -1,4 +1,11 @@
package de.chaoscaot.replay.commands
class ListPackets {
import com.github.ajalt.clikt.core.CliktCommand
import de.chaoscaot.replay.parser.PacketType
object ListPackets : CliktCommand() {
override fun run() {
echo("Packets:")
PacketType.entries.forEach { echo("\t${it.name.lowercase().replace("_packet", "").replace("_", "").padEnd(25, ' ')}: 0x${it.id.toString(16).padStart(2, '0')}") }
}
}

View File

@ -1,4 +1,32 @@
package de.chaoscaot.replay.parser
class EmbeddedSchematic {
import dev.dewy.nbt.Nbt
import dev.dewy.nbt.tags.collection.CompoundTag
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ByteArraySerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.io.DataInputStream
import java.io.InputStream
@Serializable(with = EmbeddedSchematicSerializer::class)
data class EmbeddedSchematic(val data: CompoundTag)
object EmbeddedSchematicSerializer: KSerializer<EmbeddedSchematic> {
private val delegateSerializer = ByteArraySerializer()
private val nbt = Nbt()
override val descriptor: SerialDescriptor = SerialDescriptor("de.chaoscaot.EmbeddedSchematic", delegateSerializer.descriptor)
override fun deserialize(decoder: Decoder): EmbeddedSchematic = EmbeddedSchematic(nbt.fromStream(DataInputStream(DecoderStream(decoder))))
override fun serialize(encoder: Encoder, value: EmbeddedSchematic) {
TODO("Not yet implemented")
}
}
class DecoderStream(private val decoder: Decoder): InputStream() {
override fun read(): Int = decoder.decodeByte().toUByte().toInt()
}

View File

@ -1,3 +1,57 @@
package de.chaoscaot.replay.parser
data class Message()
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/*
* Message-Format
* String message + byte prefixed Object-Params + byte 0x00
* 0x00: End of message
* 0x01: boolean following
* 0x02: byte following
* 0x03: short following
* 0x04: int following
* 0x05: float following
* 0x06: double following
* 0x07: String following
* 0x08: Message following
*/
@Serializable(with = MessageSerializer::class)
data class Message(val message: String, val params: List<Any>)
object MessageSerializer: KSerializer<Message> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("de.chaoscaot.Message", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Message {
val msg = decoder.decodeString()
val params = mutableListOf<Any>()
var type: Byte
while (decoder.decodeByte().also { type = it } != 0.toByte()) {
when (type.toInt()) {
1 -> params.add(decoder.decodeBoolean())
2 -> params.add(decoder.decodeByte())
3 -> params.add(decoder.decodeShort())
4 -> params.add(decoder.decodeInt())
5 -> params.add(decoder.decodeFloat())
6 -> params.add(decoder.decodeDouble())
7 -> params.add(decoder.decodeString())
8 -> params.add(decoder.decodeSerializableValue(this))
}
}
return Message(msg, params)
}
override fun serialize(encoder: Encoder, value: Message) {
TODO("Not yet implemented")
}
}

View File

@ -1,18 +1,110 @@
package de.chaoscaot.replay.parser.packets
package de.chaoscaot.replay.parser
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
import java.io.DataInputStream
abstract class Packet(val type: PacketType)
@OptIn(ExperimentalSerializationApi::class)
fun readPacket(type: Int, stream: DataInputStream): Any = PacketType.entries.find { it.id == type }.let {
if (it == null) {
println("next bytes: ${stream.readNBytes(40).map { it.toUByte().toString(16) }.joinToString("")}")
throw IllegalArgumentException("Unknown packet type: $type")
}
interface PacketReader<T> {
fun readPacket(stream: DataInputStream): T
it
}.let { PacketDecoder(stream).decodeSerializableValue(it.serializer) }
companion object {
fun readPacket(type: Byte, stream: DataInputStream): Packet =
PacketType.entries.find { it.id == type }?.reader?.readPacket(stream) ?: throw IllegalArgumentException("Unknown Packet")
fun readPackets(stream: DataInputStream): Sequence<Any> = sequence {
while (stream.available() > 0) {
yield(readPacket(stream.readByte().toUByte().toInt(), stream))
}
}
enum class PacketType(val id: Byte, val reader: PacketReader<out Packet>) {
PLAYER_JOIN_PACKET(0x00, UserJoinPacketReader),
@ExperimentalSerializationApi
class PacketDecoder(val input: DataInputStream, var elementsCount: Int = 0): AbstractDecoder() {
private var elementIndex = 0
override fun decodeBoolean(): Boolean = input.readByte().toInt() != 0
override fun decodeByte(): Byte = input.readByte()
override fun decodeShort(): Short = input.readShort()
override fun decodeInt(): Int = input.readInt()
override fun decodeLong(): Long = input.readLong()
override fun decodeFloat(): Float = input.readFloat()
override fun decodeDouble(): Double = input.readDouble()
override fun decodeChar(): Char = input.readChar()
override fun decodeString(): String = input.readUTF()
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.readInt()
override val serializersModule: SerializersModule = EmptySerializersModule()
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE
return elementIndex++
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
PacketDecoder(input, descriptor.elementsCount)
override fun decodeSequentially(): Boolean = true
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
decodeInt().also { elementsCount = it }
override fun decodeNotNullMark(): Boolean = decodeBoolean()
}
enum class PacketType(val id: Int, val serializer: KSerializer<out Any>) {
USER_JOIN_PACKET(0x00, UserJoinPacket.serializer()),
ENTITY_MOVE_PACKET(0x01, EntityMovePacket.serializer()),
ENTITY_DESPAWNS_PACKET(0x02, EntityDespawnsPacket.serializer()),
PLAYER_SNEAK_PACKET(0x03, PlayerSneakPacket.serializer()),
ENTITY_ANIMATION_PACKET(0x04, EntityAnimationPacket.serializer()),
TNT_SPAWN_PACKET(0x05, TNTSpawnPacket.serializer()),
ENTITY_SPEED_PACKET(0x06, EntitySpeedPacket.serializer()),
PLAYER_ITEM_PACKET(0x07, PlayerItemPacket.serializer()),
ARROW_SPAWN_PACKET(0x08, ArrowSpawnPacket.serializer()),
FIREBALL_SPAWN_PACKET(0x09, FireballSpawnPacket.serializer()),
BOW_SPAN_PACKET(0x0a, BowSpanPacket.serializer()),
PLAYER_DAMAGE_PACKET(0x0b, PlayerDamagePacket.serializer()),
SET_ON_FIRE_PACKET(0x0c, SetOnFirePacket.serializer()),
LEGACY_ARENA_INFO(0x20, LegacyArenaInfo.serializer()),
ARENA_INFO(0x21, ArenaInfo.serializer()),
LEGACY_BLOCK_PACKET(0x30, LegacyBlockPacket.serializer()),
PARTICLE_PACKET(0x31, ParticlePacket.serializer()),
SOUND_PACKET(0x32, SoundPacket.serializer()),
LEGACY_SHORT_BLOCK_PACKET(0x33, LegacyShortBlockPacket.serializer()),
SOUND_AT_PLAYER_PACKET(0x34, SoundAtPlayerPacket.serializer()),
SHORT_BLOCK_PACKET(0x035, ShortBlockPacket.serializer()),
BLOCK_PACKET(0x36, BlockPacket.serializer()),
LEGACY_CHAT_PACKET(0xa0, LegacyChatPacket.serializer()),
LEGACY_ACTION_BAR_PACKET(0xa1, LegacyActionBarPacket.serializer()),
LEGACY_SYSTEM_PACKET(0xa2, LegacySystemPacket.serializer()),
COUNTDOWN_PACKET(0xa3, CountdownPacket.serializer()),
CHAT_PACKET(0xa4, ChatPacket.serializer()),
SYSTEM_PACKET(0xa5, SystemPacket.serializer()),
BLUE_SCHEM_PACKET(0xb0, BlueSchemPacket.serializer()),
RED_SCHEM_PACKET(0xb1, RedSchemPacket.serializer()),
TEAM_ID_PACKET(0xb2, TeamIDPacket.serializer()),
BLUE_EMBEDDED_SCHEM_PACKET(0xb3, BlueEmbeddedSchemPacket.serializer()),
RED_EMBEDDED_SCHEM_PACKET(0xb4, RedEmbeddedSchemPacket.serializer()),
LEGACY_SCOREBOARD_TITLE_PACKET(0xc0, LegacyScoreboardTitlePacket.serializer()),
LEGACY_SCOREBOARD_DATA_PACKET(0xc1, LegacyScoreboardDataPacket.serializer()),
LEGACY_BOSS_BAR_PACKET(0xc2, LegacyBossBarPacket.serializer()),
LEGACY_SUBTITLE_PACKET(0xc3, LegacySubtitlePacket.serializer()),
LEGACY_PRINT_WIN_PACKET(0xc4, LegacyPrintWinPacket.serializer()),
SUBTITLE_PACKET(0xc5, SubtitlePacket.serializer()),
WIN_PACKET(0xc6, WinPacket.serializer()),
BOSS_BAR_PACKET(0xc7, BossBarPacket.serializer()),
COMMENT_PACKET(0xfe, CommentPacket.serializer()),
TICK_PACKET(0xff, TickPacket.serializer()),
}

View File

@ -1,2 +1,327 @@
package de.chaoscaot.replay.parser
import kotlinx.serialization.Serializable
/*
* PlayerJoinPacket (0x00) + int EntityId + int SWUserId
* EntityMovePacket (0x01) + int EntityId + double x, y, z + float pitch, yaw + byte headyaw
* EntityDespawnsPacket (0x02) + int EntityId
* PlayerSneakPacket (0x03) + int EntityId + boolean sneaks
* EntityAnimationPacket (0x04) + int EntityId + byte animation
* TNTSpawnPacket (0x05) + int EntityId
* EntitySpeedPacket (0x06) + int EntityId + double dx, dy, dz
* PlayerItemPacket (0x07) + int EntityId + String item + boolean enchanted + String slot
* ArrowSpawnPacket (0x08) + int EntityId
* FireballSpawnPacket (0x09) + int EntityId
* BowSpanPacket (0x0a) + int EntityId + boolean start + hand
* PlayerDamagePacket (0x0b) + int EntityId
* SetOnFire (0x0c) + int EntityId + boolean perma
*
* DEPRECATED ArenaInfo (0x20) + bool blueNegZ + byte arenaY + int arenaMinX + int arenaMinZ
* ArenaInfo (0x21) + bool blueNegZ + int arenaMinX + int arenaMinY + int arenaMinZ
*
* DEPRECATED BlockPacket (0x30) + pos int, byte, int + int BlockState
* ParticlePacket (0x31) + double x, y, z + string particleType
* SoundPacket (0x32) + int x, y, z + string soundType + string soundCategory + float volume, pitch
* DEPRECATED ShortBlockPacket (0x33) + pos relative to ArenaMinX,ArenaMinZ byte, byte, byte + short BlockState
* SoundAtPlayerPacket (0x34) + string (soundType, soundCategory) + float volume, pitch
* ShortBlockPacket (0x35) + pos relative to ArenaMinX,BluePasteY,ArenaMinZ byte, byte, byte + short BlockState
* BlockPacket (0x36) + pos int, short, int + int BlockState
*
*
* DEPRECATED ChatPacket (0xa0) + String message
* DEPRECATED ActionBarPacket (0xa1) + String message
* DEPRECATED SystemPacket (0xa2) + String message
* CountdownPacket (0xa3) + String message, int displaytime, Message appendix
* ChatPacket (0xa4) + Message
* SystemPacket (0xa5) + Message
*
* BlueSchemPacket (0xb0) + int blueSchemId
* RedSchemPacket (0xb1) + int redSchemId
* TeamIDPacket (0xb2) + int blueTeamId, redTeamId
* BlueEmbeddedSchemPacket (0xb3) + int blueSchemId + gzipt NBT blob
* RedEmbeddedSchemPacket (0xb4) + int redSchemId + gzipt NBT blob
*
* DEPRECATED ScoreboardTitlePacket (0xc0) + String scoreboardTitle
* DEPRECATED ScoreboardDataPacket (0xc1) + String key + int value
* DEPRECATED BossBarPacket (0xc2) + double leftBlueProgress, leftRedProgress + String leftBlueText, leftRedText
* DEPRECATED SubtitlePacket (0xc3) + String subtitle
* DEPRECATED PrintWinPacket (0xc4) + String title, subtitle
* SubtitlePacket (0xc5) + Message
* WinPacket (0xc6) + byte team + Message subtitle
* BossBarPacket (0xc7) + double leftBlueProgress, leftRedProgress + Message leftBlueText, leftRedText
*
* CommentPacket (0xfe) + String comment
* TickPacket (0xff)
*
* Message-Format
* String message + byte prefixed Object-Params + byte 0x00
* 0x00: End of message
* 0x01: boolean following
* 0x02: byte following
* 0x03: short following
* 0x04: int following
* 0x05: float following
* 0x06: double following
* 0x07: String following
* 0x08: Message following
* */
@Serializable
data class UserJoinPacket(val entityId: Int, val userId: Int)
@Serializable
data class EntityMovePacket(val entityId: Int, val x: Double, val y: Double, val z: Double, val pitch: Float, val yaw: Float, val headYaw: Byte)
@Serializable
data class EntityDespawnsPacket(val entityId: Int)
@Serializable
data class EntitySpeedPacket(val entityId: Int, val dx: Double, val dy: Double, val dz: Double)
@Serializable
data class PlayerSneakPacket(
val entityId: Int,
val sneaks: Boolean
)
@Serializable
data class EntityAnimationPacket(
val entityId: Int,
val animation: Byte
)
@Serializable
data class TNTSpawnPacket(
val entityId: Int
)
@Serializable
data class PlayerItemPacket(
val entityId: Int,
val item: String,
val enchanted: Boolean,
val slot: String
)
@Serializable
data class ArrowSpawnPacket(
val entityId: Int
)
@Serializable
data class FireballSpawnPacket(
val entityId: Int
)
@Serializable
data class BowSpanPacket(
val entityId: Int,
val start: Boolean,
val hand: String
)
@Serializable
data class PlayerDamagePacket(
val entityId: Int
)
@Serializable
data class SetOnFirePacket(
val entityId: Int,
val perma: Boolean
)
@Serializable
data class ArenaInfo(
val blueNegZ: Boolean,
val arenaMinX: Int,
val arenaMinY: Int,
val arenaMinZ: Int
)
@Serializable
data class ParticlePacket(
val x: Double,
val y: Double,
val z: Double,
val particleType: String
)
@Serializable
data class SoundPacket(
val x: Int,
val y: Int,
val z: Int,
val soundType: String,
val soundCategory: String,
val volume: Float,
val pitch: Float
)
@Serializable
data class SoundAtPlayerPacket(
val soundType: String,
val volume: Float,
val pitch: Float
)
@Serializable
data class ShortBlockPacket(
val x: Byte,
val y: Byte,
val z: Byte,
val blockState: Short
)
@Serializable
data class BlockPacket(
val x: Int,
val y: Short,
val z: Int,
val blockState: Int
)
// Verbleibende aktive Pakete
@Serializable
data class BlueEmbeddedSchemPacket(
val blueSchemId: Int,
val schematic: EmbeddedSchematic
)
@Serializable
data class RedEmbeddedSchemPacket(
val redSchemId: Int,
val schematic: EmbeddedSchematic
)
@Serializable
data class BlueSchemPacket(
val blueSchemId: Int
)
@Serializable
data class RedSchemPacket(
val redSchemId: Int
)
@Serializable
data class TeamIDPacket(
val blueTeamId: Int,
val redTeamId: Int
)
@Serializable
data class CountdownPacket(
val message: String,
val displayTime: Int,
val appendix: Message
)
@Serializable
data class ChatPacket(
val message: Message
)
@Serializable
data class SystemPacket(
val message: Message
)
@Serializable
data class SubtitlePacket(
val message: Message
)
@Serializable
data class WinPacket(
val team: Byte,
val subtitle: Message
)
@Serializable
data class BossBarPacket(
val leftBlueProgress: Double,
val leftRedProgress: Double,
val leftBlueText: Message,
val leftRedText: Message
)
@Serializable
data class CommentPacket(
val comment: String
)
@Serializable
class TickPacket
// Legacy (Deprecated) Pakete
@Serializable
data class LegacyArenaInfo(
val blueNegZ: Boolean,
val arenaY: Byte,
val arenaMinX: Int,
val arenaMinZ: Int
)
@Serializable
data class LegacyBlockPacket(
val x: Int,
val y: Byte,
val z: Int,
val blockState: Int
)
@Serializable
data class LegacyShortBlockPacket(
val x: Byte,
val y: Byte,
val z: Byte,
val blockState: Short
)
@Serializable
data class LegacyChatPacket(
val message: String
)
@Serializable
data class LegacyActionBarPacket(
val message: String
)
@Serializable
data class LegacySystemPacket(
val message: String
)
@Serializable
data class LegacyScoreboardTitlePacket(
val scoreboardTitle: String
)
@Serializable
data class LegacyScoreboardDataPacket(
val key: String,
val value: Int
)
@Serializable
data class LegacyBossBarPacket(
val leftBlueProgress: Double,
val leftRedProgress: Double,
val leftBlueText: String,
val leftRedText: String
)
@Serializable
data class LegacySubtitlePacket(
val subtitle: String
)
@Serializable
data class LegacyPrintWinPacket(
val title: String,
val subtitle: String
)

View File

@ -1,4 +1,12 @@
package de.chaoscaot.replay.parser
import java.io.DataInputStream
import java.io.File
import java.io.InputStream
import java.util.zip.GZIPInputStream
object ReplayParser {
fun read(file: File): Sequence<Any> = read(file.inputStream())
fun read(inputStream: InputStream, gzipped: Boolean = true): Sequence<Any> = readPackets(DataInputStream(if (gzipped) GZIPInputStream(inputStream) else inputStream))
}