From 8726a03e66887380974d7404af5dd5bb3e903748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Ryb=C3=A1rsky?= Date: Thu, 22 Aug 2024 19:33:17 +0200 Subject: [PATCH] init --- .gitignore | 3 + .idea/.gitignore | 8 + .idea/mcpingquick.iml | 9 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + favicon.png | Bin 0 -> 2312 bytes go.mod | 8 + go.sum | 4 + main.go | 27 ++ packetcreator.go | 459 +++++++++++++++++++++++++ packetsender.go | 782 ++++++++++++++++++++++++++++++++++++++++++ player.go | 8 + response.go | 251 ++++++++++++++ 13 files changed, 1573 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/mcpingquick.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 favicon.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 packetcreator.go create mode 100644 packetsender.go create mode 100644 player.go create mode 100644 response.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47b2a32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +out +out/ +out/* \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/mcpingquick.iml b/.idea/mcpingquick.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/mcpingquick.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..77eeca6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..93888617b5f07b930be4e2641e4d5b68128d7b2e GIT binary patch literal 2312 zcmV+j3HSDiP)^AOt^B;gd^o9+|ZKJ07(h~g9D^Wq*f|wMAf84 zZKZ}A+uQ*P;l6ESdyQ=l12#6c!59oS=1?1KukB^`>GRIFpKW$+*QWbNKC~nKH1i!Z zJMS~^T;GmJID|tughM!lLpZb)2FNsg`*`TKeo|peP7cmkdp34@7ZbFnByHN4oT_c!-YY>xwm|{IuN);;mbcJ3xS() zKv^tsoA++a1P(G^qFl!YxoTpYkP5I%ZZ!yCQ}{ZN$@mPp_ybn=nV(*lA&o)>a&5XLKmv_FGt@^B zZZr@w0n#ae($x6Wr@T1KSUsS<>?65@tcqWrN@K?X#Up`BD!^dI6yS(huD*$$cdVH* zY%cV&%2_SN$R7c$W6RrwOn{V5un^0g6?J*7SbI^!F3$yyGR~jO^e<`j6pApO@hBQU zjII8BUi2b6%8R0@R_kfYKMIG)4H~~!ta0PY zdvyDE+4{eNDZ6k2yUsYPMeRH)RA4zx*)Ns=t0d1f&{nzKK*$6*F4aixQiI~3TNTm@ zwim7J*-0vb5C3Ai>_)l7WuO@zZWG7XgtfhGS3pB_yfj#c50vs5S+g?b`up~ zXNL;T9vnEI)o$hvtpH};rMk(_=nuNv%`O^$`PXV(Eeh0lPGLn9pqS~R`Gnn~@Hgtd z_k;4bg?x6QBj2qy0%%=}CR<(*G68h@(3E2R!i)s(RTW5A9~VQYeujK>C8?c79hPF`&m~Xjk|-dly2H-n$QwKhui>ig1apUl_}Qb-oX~dsPON(>nK%+^-rZ2YTR& ztmhh_5@pjH1yC`La`hQ90dCVh%Q<3iHUivZY#C>>*^Ih(414>O(o^mju~J^HLQ+pR za{B@W6THZu;zce+xHyBnPvd9%%wV|Hly~PLw5-5;P01K_5BXC)Oy4#2*};14;{8L< z-RBzM8us-s4xIoCBms+MJr+u$A&%Gc8GpokN*tLctFiQ{1NgklL43mNMG}u#*^Huh zxFQ_m!MO?b$erRr?mPAL1P=-*f*QVADuUQb%IG2g{AoHqHNTgL-^xbAJXyzb*Rme# zOq~`Hjg^o1-Xc@4cvI()383_q@pwfhqn}Jfl#E1gGJyA9m$76V@4by^8HGsRe~s6D znbv$uWDm}dr2wNT07bYkg-ox-z8_XTv}|okBKbm&;t#UTl+lZ2_mVM97~LX0`CRMK zU55EWBEbI>wDOrHwthQf0h(_!3pOJuuM+k(nRk3-gF~-a1Hx-u)%?B zr2xw$1Amot=L_(qoJHmv9%R1iMrxnxAY+>&fjhwEl7qiXmh%NzAs2BX<{M=9uIAuY z8f0v9tfv5Lq=4e(IA4IZQiK!HRh+NOgA8qnE$jmE+}x~?bIunafvuj@y(-AamRQF% zK+ZHbHgPeNASKQhV7>qQT%e)Nv4&#hjH<)AiFMfC@fJ2x495!)-^zU7lub0&OqC<+ zkUOFtJHAtH#sbF+uu*hWFKu<ACBSRk-v^fKm=G<)%@>-(FIlau)1hsA zY7fiB0}Zz+ouz+x-oUF)7C=4V(13C-Z_DWVjV+%Xc^~_}-@NPF{7*EO|6GMV)fC z0D5fY;>*@yX`7-ZX+22Y9QN~Dv%zU}gL4H~-PMIpWFrm_xrvmamDts*5c9b%SRZ{J zzkm8`bIwT5{Cp^DF^9}$I?kN^NigSv7Ba4!3+D%7RWbS6~`lQbQ0000 0 { + n, dataLengthTemp := receiveVarint(packetData) + packetData = packetData[n:] + dataLength = int(dataLengthTemp) + } + + // If dataLength > 0, it means the packet is compressed + if dataLength > 0 { + packetData, err = decompress(packetData) + if err != nil { + return + } + } + + n, packetID = receiveVarint(packetData) + packetLength -= n + packetData = packetData[n:] + + return +} + +func createPacket(packetID int32, packetData []byte, threshold int32, startedCompression bool) ([]byte, error) { + var dataBuffer []byte + addVarint(&dataBuffer, packetID) + + dataBuffer = append(dataBuffer, packetData...) + + var outBuffer []byte + length := int32(len(dataBuffer)) + + if startedCompression { + if threshold > 0 && length >= threshold { + // Compress the packet + compressedData, err := compress(dataBuffer) + if err != nil { + return nil, err + } + dataBuffer = compressedData + + // Add the uncompressed length + addVarint(&outBuffer, int32(len(packetData))) + } else { + // Set Data Length to 0 if not compressed + addVarint(&outBuffer, 0) + } + } + + addVarint(&outBuffer, int32(len(dataBuffer))) + + // Append compressed or uncompressed data + outBuffer = append(outBuffer, dataBuffer...) + + // Add the Packet Length + + return outBuffer, nil +} + +func addString(buffer *[]byte, str string) { + length := len(str) + addVarint(buffer, int32(length)) + + *buffer = append(*buffer, str...) +} + +// Reads a VarInt from a bufio.Reader and returns the decoded value, the length read, and an error if any. +// Only negative values are transformed similar to Java's implementation. +func readVarint(r *bufio.Reader) (value int, length int, err error) { + var position uint + value = 0 + for { + currentByte, err := r.ReadByte() + if err != nil { + return 0, length, err + } + value |= int(currentByte&0x7F) << position + length++ + if (currentByte & 0x80) == 0 { + break + } + position += 7 + if position >= 32 { + return 0, length, fmt.Errorf("VarInt is too big") + } + } + return value, length, nil +} + +// Decodes a VarInt from the byte slice. +// This function handles the decoding in a manner similar to Java's VarInt, +// correctly managing positive and negative values. +func receiveVarint(b []byte) (currentOffset int, value int32) { + var shift uint + for { + byteValue := b[currentOffset] + currentOffset++ + value |= int32(byteValue&0x7F) << shift + if (byteValue & 0x80) == 0 { + // Apply sign extension for negative values + break + } + shift += 7 + if shift >= 32 { + panic("VarInt is too big") + } + } + return +} + +func addVarint(buffer *[]byte, value int32) { + for { + // Extract the lower 7 bits of the value + temp := byte(value & 0x7F) + value >>= 7 + + // If the value is not zero or if there are more bytes to encode + if value != 0 && value != -1 { + temp |= 0x80 // Set the continuation bit + } else { + temp &= 0x7F // Clear the continuation bit if we're done + } + + // Append the byte to the buffer + addByte(buffer, temp) + + // Break the loop if no more continuation bit is set + if (temp & 0x80) == 0 { + break + } + } +} + +func addVarlong(buffer *[]byte, value int64) { + for { + temp := byte(value & 0x7F) // Get the last 7 bits + value >>= 7 + if value != 0 && value != -1 { + temp |= 0x80 // Set the continuation bit + } + addByte(buffer, temp) + if temp&0x80 == 0 { + break + } + } +} + +// Decodes a VarLong from the byte slice. +func receiveVarlong(b []byte) (currentOffset int, value int64) { + var shift uint + for { + byteValue := b[currentOffset] + currentOffset++ + value |= int64(byteValue&0x7F) << shift + if (byteValue & 0x80) == 0 { + // Sign extension for negative values + if shift < 32 { + value |= -1 << shift + } + break + } + shift += 7 + if shift >= 32 { + panic("VarLong is too big") + } + } + return +} + +func addByte(buffer *[]byte, byte byte) { + *buffer = append(*buffer, byte) +} + +func receiveBool(buffer []byte) (currentOffset int, boolean bool) { + boolean = buffer[0] == 1 + currentOffset = 1 + return +} + +func addBool(buffer *[]byte, bool bool) { + var boolByte byte + boolByte = 0x00 + if bool { + boolByte = 0x01 + } + *buffer = append(*buffer, boolByte) +} + +func constructOfflinePlayerUUID(username string) (dataArray []byte) { + data := md5.Sum([]byte("OfflinePlayer:" + username)) + dataArray = data[:] + + // Set the version to 3 -> Name based md5 hash + dataArray[6] = (dataArray[6] & 0x0f) | 0x30 + // IETF variant + dataArray[8] = (dataArray[8] & 0x3f) | 0x80 + + return dataArray +} + +func createOfflineLoginPacket(username string, threshold int32) (outBuffer []byte, err error) { + addString(&outBuffer, username) + outBuffer = append(outBuffer, constructOfflinePlayerUUID(username)...) + outBuffer, err = createPacket(0, outBuffer, threshold, false) + return +} + +func createClientBrandPacket(brand string, threshold int32, startedCompression bool) (outBuffer []byte, err error) { + addString(&outBuffer, "minecraft:brand") + addString(&outBuffer, brand) + outBuffer, err = createPacket(2, outBuffer, threshold, startedCompression) + return +} + +func createConfirmLoginPacket(threshold int32, startedCompression bool) (packet []byte, err error) { + packet, err = createPacket(3, []byte{}, threshold, startedCompression) + return +} + +func createStatusRequestPacket(threshold int32) (packet []byte, err error) { + packet, err = createPacket(0, []byte{}, threshold, false) + return +} + +func addUint16(buffer *[]byte, value uint16) { + *buffer = binary.BigEndian.AppendUint16(*buffer, value) +} + +func addInt64(buffer *[]byte, value int64) { + *buffer = binary.BigEndian.AppendUint64(*buffer, uint64(value)) +} + +func receiveFloat32(b []byte) (currentOffset int, value float32) { + value = math.Float32frombits(binary.BigEndian.Uint32(b)) + currentOffset = binary.Size(value) + return +} + +func receiveFloat64(b []byte) (currentOffset int, value float64) { + value = math.Float64frombits(binary.BigEndian.Uint64(b)) + currentOffset = binary.Size(value) + return +} + +func receiveInt32(b []byte) (currentOffset int, value int32) { + value = int32(binary.BigEndian.Uint32(b)) + currentOffset = binary.Size(value) + return +} + +func receiveInt64(b []byte) (currentOffset int, value int64) { + value = int64(binary.BigEndian.Uint64(b)) + currentOffset = binary.Size(value) + return +} + +func receiveByte(b []byte) (currentOffset int, value byte) { + value = b[0] + currentOffset = binary.Size(value) + return +} + +func receivePosition(b []byte) (currentOffset int, position Position) { + encoded := binary.BigEndian.Uint64(b) + x, y, z := DecodePosition(encoded) + position = Position{X: x, Y: y, Z: z} + currentOffset = binary.Size(encoded) + return +} + +func DecodePosition(encoded uint64) (x int32, y int16, z int32) { + // Extract x, y, z from encoded value + x = int32((encoded >> 38) & 0x3FFFFFF) + y = int16(encoded & 0xFFF) + z = int32((encoded >> 12) & 0x3FFFFFF) + + // Adjust for sign + if x >= 1<<25 { + x -= 1 << 26 + } + if y >= 1<<11 { + y -= 1 << 12 + } + if z >= 1<<25 { + z -= 1 << 26 + } + + return +} + +func createHandshakePacket(version int32, address string, port uint16, nextState int32, threshold int32) (outBuffer []byte, err error) { + addVarint(&outBuffer, version) + addString(&outBuffer, address) + addUint16(&outBuffer, port) + addVarint(&outBuffer, nextState) + outBuffer, err = createPacket(0, outBuffer, threshold, false) + return +} + +func createTeleportConfirmPacket(teleportID int32, threshold int32, startedCompression bool) (outBuffer []byte, err error) { + addVarint(&outBuffer, teleportID) + outBuffer, err = createPacket(0, outBuffer, threshold, startedCompression) + return +} + +func receiveString(buf []byte) (currentOffset int, strOut string) { + n, stringLen := receiveVarint(buf) + currentOffset = n + strOut = string(buf[currentOffset : currentOffset+int(stringLen)]) + currentOffset += int(stringLen) + return +} + +func receiveTextComponent(buf []byte) (currentOffset int, component TextComponent, err error) { + currentOffset, jsonString := receiveString(buf) + if strings.ContainsAny(jsonString, "") { + } + jsonDecoderText := json.NewDecoder(strings.NewReader(jsonString)) + err = jsonDecoderText.Decode(&component) + if err != nil { + err = nil + component.Text = jsonString + component.CleanText = cleanMinecraftFormatting(jsonString) + } + return +} + +func createEnabledDatapacksPacket(datapacks []DatapackInfo, threshold int32, startedCompression bool) (outBuffer []byte, err error) { + addVarint(&outBuffer, int32(len(datapacks))) + for _, datapack := range datapacks { + addString(&outBuffer, datapack.Namespace) + addString(&outBuffer, datapack.ID) + addString(&outBuffer, datapack.Version) + } + outBuffer, err = createPacket(0x07, outBuffer, threshold, startedCompression) + return +} + +func createChatMessagePacket(message string, threshold int32, startedCompression bool) (outBuffer []byte, err error) { + addString(&outBuffer, message) + addInt64(&outBuffer, time.Now().Unix()) + addInt64(&outBuffer, time.Now().Unix()) + addBool(&outBuffer, false) + addVarint(&outBuffer, 0) + addByte(&outBuffer, 0) + addByte(&outBuffer, 0) + addByte(&outBuffer, 0) + outBuffer, err = createPacket(0x06, outBuffer, threshold, startedCompression) + return +} + +func decodeEnabledDatapacks(buf []byte) (currentOffset int, outPacks []DatapackInfo) { + currentOffset, datapackCount := receiveVarint(buf) + for i := 0; i < int(datapackCount); i++ { + nextOffset, namespace := receiveString(buf[currentOffset:]) + currentOffset += nextOffset + nextOffset, ID := receiveString(buf[currentOffset:]) + currentOffset += nextOffset + nextOffset, version := receiveString(buf[currentOffset:]) + currentOffset += nextOffset + enabledDatapack := DatapackInfo{ + Namespace: namespace, + ID: ID, + Version: version, + } + outPacks = append(outPacks, enabledDatapack) + } + return +} + +func createClientInformationPacket(locale string, renderDistance byte, chatMode int32, colors bool, skinParts byte, mainLeftHand bool, textFiltering bool, serverListing bool, threshold int32, startedCompression bool) (outBuffer []byte, err error) { + var mainHand int32 + if mainLeftHand { + mainHand = 0 + } else { + mainHand = 1 + } + addString(&outBuffer, locale) + addByte(&outBuffer, renderDistance) + addVarint(&outBuffer, chatMode) + addBool(&outBuffer, colors) + addByte(&outBuffer, skinParts) + addVarint(&outBuffer, mainHand) + addBool(&outBuffer, textFiltering) + addBool(&outBuffer, serverListing) + outBuffer, err = createPacket(0, outBuffer, threshold, startedCompression) + return +} diff --git a/packetsender.go b/packetsender.go new file mode 100644 index 0000000..0007e8a --- /dev/null +++ b/packetsender.go @@ -0,0 +1,782 @@ +package main + +import ( + "bufio" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net" + "reflect" + "strconv" + "strings" +) + +func PingIP(ip net.IP, port uint16, host string) (response Response, errOut error) { + + if host == "" { + host = ip.String() + } + + addr := &net.TCPAddr{IP: ip, Port: int(port)} + + conn, err := net.DialTCP("tcp", nil, addr) + if err != nil { + errOut = err + return + } + + didRestartConnection := false + + defer func(conn *net.TCPConn) { + _ = conn.Close() + }(conn) + + var state byte + state = 0 + // 0 - handshaking + // 1 - status + // 2 - login + // 3 - configuration + // 4 - play(since this is a scanner, we don't need to actually join the game) + + response.Encryption = false + response.CompressionThreshold = -2 + response.PluginDataSent = map[string]string{} + + handshakePacketPing, err := createHandshakePacket(-1, host, port, 1, response.CompressionThreshold) + if err != nil { + errOut = err + return + } + _, err = conn.Write(handshakePacketPing) + if err != nil { + errOut = err + return + } + + statusRequestPacket, err := createStatusRequestPacket(response.CompressionThreshold) + if err != nil { + errOut = err + return + } + _, err = conn.Write(statusRequestPacket) + if err != nil { + errOut = err + return + } + + var username string + var PlayerUUID []byte + compressionStarted := false + reader := bufio.NewReader(conn) + for { + packetID, packetData, packetLength, err := readPacket(reader, response.CompressionThreshold) + if err != nil { + errOut = err + return + } + switch packetID { + case 0x00: + if state == 0 { + _, stringJson := receiveString(packetData) + jsonDecoder := json.NewDecoder(strings.NewReader(stringJson)) + err = jsonDecoder.Decode(&response) + if !didRestartConnection { + handshakePacketJoin, err := createHandshakePacket(response.Version.Protocol, host, port, 2, response.CompressionThreshold) + if err != nil { + errOut = err + return + } + err = conn.Close() + if err != nil { + errOut = err + return + } + + conn, err = net.DialTCP("tcp", nil, addr) + if err != nil { + errOut = err + return + } + reader = bufio.NewReader(conn) + _, err = conn.Write(handshakePacketJoin) + if err != nil { + errOut = err + return + } + didRestartConnection = true + } + if len(response.Players.Sample) > 0 { + username = response.Players.Sample[0].Name + } else { + username = "YeahAkis_" + } + response.Username = username + PlayerUUID = constructOfflinePlayerUUID(username) + response.RawMessage = stringJson + if err != nil { + errOut = err + println(stringJson) + return + } + state = 1 + loginPacket, err := createOfflineLoginPacket(username, response.CompressionThreshold) + if err != nil { + errOut = err + return + } + _, err = conn.Write(loginPacket) + if err != nil { + errOut = err + return + } + state = 2 + } else if state == 2 { + currentOffset, component, err := receiveTextComponent(packetData) + if err != nil { + errOut = err + return + } + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.Message = component + return + } + break + + case 0x01: + if state == 3 { + + currentOffset, channelName := receiveString(packetData) + nextOffset, channelValue := receiveString(packetData[currentOffset:]) + currentOffset += nextOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.PluginDataSent[channelName] = channelValue + } else if state == 2 { + response.Encryption = true + return + } else if state == 0 { + //ping response + } + break + + case 0x02: + if state == 2 { + playerRecvUUID := packetData[:16] + currentOffset := 16 + if !reflect.DeepEqual(PlayerUUID, playerRecvUUID) { + errOut = errors.New("player UUID mismatch") + return + } + addToOffset, receivedUsername := receiveString(packetData[16:]) + currentOffset += addToOffset + if receivedUsername != username { + errOut = errors.New("username mismatch") + return + } + //TODO property array handling + confirmLoginPacket, err := createConfirmLoginPacket(response.CompressionThreshold, compressionStarted) + if err != nil { + errOut = err + return + } + _, err = conn.Write(confirmLoginPacket) + if err != nil { + errOut = err + return + } + clientBrandPacket, err := createClientBrandPacket("vanilla", response.CompressionThreshold, compressionStarted) + if err != nil { + errOut = err + return + } + _, err = conn.Write(clientBrandPacket) + if err != nil { + errOut = err + return + } + state = 3 + } else if state == 3 { + currentOffset, component, err := receiveTextComponent(packetData) + if err != nil { + errOut = err + return + } + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.Message = component + return + } + break + + case 0x03: + if state == 2 { + currentOffset, compression := receiveVarint(packetData) + response.CompressionThreshold = compression + compressionStarted = true + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + } else if state == 3 { + confirmPlaySwitchPacket, err := createConfirmLoginPacket(response.CompressionThreshold, compressionStarted) + if err != nil { + errOut = err + return + } + _, err = conn.Write(confirmPlaySwitchPacket) + state = 4 + } + break + + case 0x07: + if state == 3 { + currentOffset, registryID := receiveString(packetData) + newOffset, entryCount := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + var entries []RegistryEntry + for i := 0; i < int(entryCount); i++ { + newOffset, entryID := receiveString(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, hasNBT := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + var nbtData []byte + if hasNBT { + nbtData = packetData[currentOffset:] + } + entries = append(entries, RegistryEntry{EntryID: entryID, HasNBT: hasNBT, NBTData: nbtData}) + } + + registryData := RegistryData{RegistryID: registryID, Entries: entries} + response.RegistryDatas = append(response.RegistryDatas, registryData) + } + break + + case 0x0b: + if state == 4 { + currentOffset, difficulty := receiveByte(packetData) + newOffset, locked := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.ServerDifficulty = DifficultyObject{Difficulty: difficulty, Locked: locked} + } + + case 0x0c: + if state == 3 { + currentOffset, flagCount := receiveVarint(packetData) + for i := 0; i < int(flagCount); i++ { + nextOffset, feature := receiveString(packetData[currentOffset:]) + response.FeatureFlags = append(response.FeatureFlags, feature) + currentOffset += nextOffset + } + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + } + clientInformationPacket, err := createClientInformationPacket("en_us", 32, 0, true, 0x7f, false, false, false, response.CompressionThreshold, compressionStarted) + if err != nil { + errOut = err + return + } + _, err = conn.Write(clientInformationPacket) + if err != nil { + errOut = err + return + } + } + break + case 0x0d: + if state == 3 { + currentOffset, entryCount := receiveVarint(packetData) + for i := 0; i < int(entryCount); i++ { + nextOffset, registryIdentifier := receiveString(packetData[currentOffset:]) + currentOffset += nextOffset + nextOffset, lengthSubarray := receiveVarint(packetData[currentOffset:]) + currentOffset += nextOffset + var tagArray []TagArray + for j := 0; j < int(lengthSubarray); j++ { + nextOffset, tagIdentifier := receiveString(packetData[currentOffset:]) + currentOffset += nextOffset + nextOffset, subSubArrayLength := receiveVarint(packetData[currentOffset:]) + currentOffset += nextOffset + var varInts []int32 + for e := 0; e < int(subSubArrayLength); e++ { + nextOffset, varIntInArray := receiveVarint(packetData[currentOffset:]) + currentOffset += nextOffset + varInts = append(varInts, varIntInArray) + } + tagPiece := TagArray{TagName: tagIdentifier, Entries: varInts} + tagArray = append(tagArray, tagPiece) + } + updateTag := UpdateTag{TagRegistryIdentifier: registryIdentifier, Tags: tagArray} + response.Tags = append(response.Tags, updateTag) + + } + } + break + case 0x0e: + if state == 3 { + currentOffset, enabledDatapacks := decodeEnabledDatapacks(packetData) + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + } + response.EnabledDatapacks = enabledDatapacks + datapackPacket, err := createEnabledDatapacksPacket(enabledDatapacks, response.CompressionThreshold, compressionStarted) + if err != nil { + errOut = err + return + } + _, err = conn.Write(datapackPacket) + if err != nil { + errOut = err + return + } + } + break + + case 0x1D: + if state == 4 { + currentOffset, component, err := receiveTextComponent(packetData) + if err != nil { + errOut = err + return + } + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.Message = component + return + } + break + + case 0x2b: + if state == 4 { + currentOffset, entityID := receiveInt32(packetData) + newOffset, isHardCore := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, dimensionCount := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + var dimensions []string + for i := 0; i < int(dimensionCount); i++ { + newOffset, dimension := receiveString(packetData[currentOffset:]) + currentOffset += newOffset + dimensions = append(dimensions, dimension) + } + newOffset, maxPlayers := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, viewDistance := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, simulationDistance := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, reducedDebugInfo := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, enableRespawnScreen := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, doLimitedCrafting := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, dimensionType := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, dimensionName := receiveString(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, hashedSeed := receiveInt64(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, gameMode := receiveByte(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, previousGameMode := receiveByte(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, isDebug := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, isFlat := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, hasDeathLocation := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + var deathDimensionName string + var deathLocation Position + + if hasDeathLocation { + newOffset, deathDimensionName = receiveString(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, deathLocation = receivePosition(packetData[currentOffset:]) + currentOffset += newOffset + } else { + deathDimensionName = "" + deathLocation = Position{} + } + newOffset, portalCooldown := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, enforcesSecureChat := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.PlayerLoginInfo = LoginInfo{ + EntityID: entityID, + Hardcore: isHardCore, + Dimensions: dimensions, + MaxPlayers: maxPlayers, + ViewDistance: viewDistance, + SimulationDistance: simulationDistance, + ReducedDebugInfo: reducedDebugInfo, + EnableRespawnScreen: enableRespawnScreen, + DoLimitedCrafting: doLimitedCrafting, + DimensionType: dimensionType, + DimensionName: dimensionName, + HashedSeed: hashedSeed, + GameMode: gameMode, + PreviousGameMode: previousGameMode, + IsDebug: isDebug, + IsFlat: isFlat, + HasDeathLocation: hasDeathLocation, + DeathDimensionName: deathDimensionName, + DeathLocation: deathLocation, + PortalCooldown: portalCooldown, + EnforcesSecureChat: enforcesSecureChat, + } + } + break + + case 0x38: + if state == 4 { + currentOffset, flags := receiveByte(packetData) + newOffset, flyingSpeed := receiveFloat32(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, fieldOfViewModifer := receiveFloat32(packetData[currentOffset:]) + currentOffset += newOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + } + response.PlayerAbilities = PlayerAbilitiesObject{ + Invulnerable: flags&0x01 != 0x00, + Flying: flags&0x02 != 0x00, + AllowFlying: flags&0x04 != 0x00, + CreativeMode: flags&0x08 != 0x00, + FlyingSpeed: flyingSpeed, + FieldOfViewModifier: fieldOfViewModifer, + } + } + break + + case 0x53: + if state == 4 { + currentOffset, heldSlot := receiveByte(packetData) + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.PlayerSlot = heldSlot + } + break + case 0x77: + if state == 4 { + //todo RECIPES + } + break + case 0x11: + //commands + if state == 4 { + + } + + break + + case 0x41: + //recipe book + if state == 4 { + + } + break + + case 0x40: + //position update + if state == 4 { + currentOffset, x := receiveFloat64(packetData) + newOffset, y := receiveFloat64(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, z := receiveFloat64(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, yaw := receiveFloat32(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, pitch := receiveFloat32(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, flags := receiveByte(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, teleportId := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + teleportConfirmPacket, err := createTeleportConfirmPacket(teleportId, response.CompressionThreshold, compressionStarted) + if err != nil { + errOut = err + return + } + _, err = conn.Write(teleportConfirmPacket) + if err != nil { + errOut = err + return + } + response.PlayerLocation = PlayerPosition{ + X: x, + Y: y, + Z: z, + Yaw: yaw, + Pitch: pitch, + IsXRelative: flags&0x01 != 0x00, + IsYRelative: flags&0x02 != 0x00, + IsZRelative: flags&0x04 != 0x00, + IsYawRelative: flags&0x08 != 0x00, + IsPitchRelative: flags&0x10 != 0x00, + } + } + break + + case 0x4b: + if state == 4 { + currentOffset, motd, errx := receiveTextComponent(packetData[2:]) + currentOffset += 2 + err = errx + if err != nil { + errOut = err + return + } + response.Description = motd + newOffset, hasIcon := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + var iconSize int32 + var iconData []byte + if hasIcon { + newOffset, iconSize = receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + iconData = packetData[currentOffset : currentOffset+int(iconSize)] + response.Favicon.PngData = iconData + } else { + iconSize = 0 + iconData = []byte{} + } + } + break + + case 0x3e: + if state == 4 { + currentOffset, actions := receiveByte(packetData) + newOffset, numberOfPlayers := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + + for i := 0; i < int(numberOfPlayers); i++ { + player := PlayerUpdate{} + player.UUID = packetData[currentOffset : currentOffset+16] + currentOffset += 16 + + if actions&0x01 != 0x00 { + newOffset, player.Name = receiveString(packetData[currentOffset:]) + currentOffset += newOffset + + newOffset, numberOfProperties := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + + for x := 0; x < int(numberOfProperties); x++ { + property := PlayerProperty{} + + newOffset, property.Name = receiveString(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, property.Value = receiveString(packetData[currentOffset:]) + currentOffset += newOffset + + newOffset, propertySigned := receiveByte(packetData[currentOffset:]) + currentOffset += newOffset + + if propertySigned != 0 { + newOffset, property.Signature = receiveString(packetData[currentOffset:]) + currentOffset += newOffset + } + + player.Properties = append(player.Properties, property) + } + } + + if actions&0x02 != 0x00 { + newOffset, hasSignatureData := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + + if hasSignatureData { + player.SignatureData = &PlayerSignatureData{} + + player.SignatureData.ChatSessionID = packetData[currentOffset : currentOffset+16] + currentOffset += 16 + + newOffset, player.SignatureData.PublicKeyExpiryTime = receiveInt64(packetData[currentOffset:]) + currentOffset += newOffset + + newOffset, encodedPublicKeySize := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + player.SignatureData.EncodedPublicKey = packetData[currentOffset : currentOffset+int(encodedPublicKeySize)] + currentOffset += int(encodedPublicKeySize) + + newOffset, publicKeySignatureSize := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + player.SignatureData.PublicKeySignature = packetData[currentOffset : currentOffset+int(publicKeySignatureSize)] + currentOffset += int(publicKeySignatureSize) + } + } + + if actions&0x04 != 0x00 { + newOffset, player.GameMode = receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + } + + if actions&0x08 != 0x00 { + newOffset, player.Listed = receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + } + + if actions&0x10 != 0x00 { + newOffset, player.Ping = receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + } + + if actions&0x20 != 0x00 { + newOffset, hasDisplayName := receiveBool(packetData[currentOffset:]) + currentOffset += newOffset + + if hasDisplayName { + newOffset, displayName, err := receiveTextComponent(packetData[currentOffset:]) + if err != nil { + errOut = err + return + } + currentOffset += newOffset + player.DisplayName = &displayName + } + } + + response.PlayersInfo = append(response.PlayersInfo, player) + } + } + break + case 0x25: + if state == 4 { + currentOffset, x := receiveFloat64(packetData) + newOffset, z := receiveFloat64(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, oldDiameter := receiveFloat64(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, newDiameter := receiveFloat64(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, speed := receiveVarlong(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, portalTeleportBoundary := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, warningBlocks := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + newOffset, warningTime := receiveVarint(packetData[currentOffset:]) + currentOffset += newOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + + response.WorldBorder = WorldBorderInfo{ + X: x, + Z: z, + OldDiameter: oldDiameter, + NewDiameter: newDiameter, + Speed: speed, + PortalTeleportBoundry: portalTeleportBoundary, + WarningBlocks: warningBlocks, + WarningTime: warningTime, + } + + } + break + case 0x64: + if state == 4 { + currentOffset, worldAge := receiveInt64(packetData) + newOffset, timeOfDay := receiveInt64(packetData[currentOffset:]) + currentOffset += newOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.WorldAge = worldAge + response.TimeOfDay = timeOfDay + } + break + + case 0x56: + if state == 4 { + currentOffset, location := receivePosition(packetData) + newOffset, angle := receiveFloat32(packetData[currentOffset:]) + currentOffset += newOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + response.DefaultPositionSpawn = DefaultSpawnPosition{ + Location: location, + Angle: angle, + } + } + break + + case 0x22: + if state == 4 { + currentOffset, event := receiveByte(packetData) + nextOffset, value := receiveFloat32(packetData[currentOffset:]) + currentOffset += nextOffset + if currentOffset != packetLength { + errOut = errors.New("packet length mismatch") + return + } + if event == 13 && value == 0 { + chatMessagePacket, err := createChatMessagePacket("Thanks for letting me scan this server", response.CompressionThreshold, compressionStarted) + if err != nil { + errOut = err + return + } + _, err = conn.Write(chatMessagePacket) + if err != nil { + errOut = err + return + } + return //we dont want chunks + } + } + + case 0x1f: + if state == 4 { + //todo entityEvent + } + break + + default: + packetIDBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(packetIDBytes, uint64(packetID)) + fmt.Printf("Unknown packet type %d (%s)\n", packetID, hex.EncodeToString(packetIDBytes)) + } + } +} + +func PingHostname(host string, port uint16) (Response, error) { + + tcpServer, err := net.ResolveTCPAddr("tcp", host+":"+strconv.Itoa(int(port))) + + if err != nil { + return Response{}, err + } + return PingIP(tcpServer.IP, port, host) + +} diff --git a/player.go b/player.go new file mode 100644 index 0000000..75d2f49 --- /dev/null +++ b/player.go @@ -0,0 +1,8 @@ +package main + +import "github.com/google/uuid" + +type Player struct { + Name string `json:"name"` + ID uuid.UUID `json:"id"` +} diff --git a/response.go b/response.go new file mode 100644 index 0000000..264e4af --- /dev/null +++ b/response.go @@ -0,0 +1,251 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "regexp" + "strings" +) + +type PlayerPosition struct { + X float64 `json:"x,omitempty"` + Y float64 `json:"y,omitempty"` + Z float64 `json:"z,omitempty"` + Yaw float32 `json:"yaw,omitempty"` + Pitch float32 `json:"pitch,omitempty"` + IsXRelative bool `json:"isXRelative,omitempty"` + IsYRelative bool `json:"isYRelative,omitempty"` + IsZRelative bool `json:"isZRelative,omitempty"` + IsYawRelative bool `json:"isYawRelative,omitempty"` + IsPitchRelative bool `json:"isPitchRelative,omitempty"` +} + +type Position struct { + X int32 `json:"x,omitempty"` + Y int16 `json:"y,omitempty"` + Z int32 `json:"z,omitempty"` +} + +type DefaultSpawnPosition struct { + Location Position `json:"location,omitempty"` + Angle float32 `json:"angle,omitempty"` +} + +type PlayerAbilitiesObject struct { + Invulnerable bool `json:"invulnerable,omitempty"` + Flying bool `json:"flying,omitempty"` + AllowFlying bool `json:"allowFlying,omitempty"` + CreativeMode bool `json:"creativeMode,omitempty"` + FlyingSpeed float32 `json:"flyingSpeed,omitempty"` + FieldOfViewModifier float32 `json:"fieldOfViewModifier,omitempty"` +} + +type PlayerProperty struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + Signature string `json:"signature,omitempty"` +} + +type PlayerSignatureData struct { + ChatSessionID []byte `json:"chatSessionID,omitempty"` + PublicKeyExpiryTime int64 `json:"publicKeyExpiryTime,omitempty"` + EncodedPublicKey []byte `json:"encodedPublicKey,omitempty"` + PublicKeySignature []byte `json:"publicKeySignature,omitempty"` +} + +type PlayerUpdate struct { + UUID []byte `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + Properties []PlayerProperty `json:"properties,omitempty"` + SignatureData *PlayerSignatureData `json:"signatureData,omitempty"` + GameMode int32 `json:"gameMode,omitempty"` + Listed bool `json:"listed,omitempty"` + Ping int32 `json:"ping,omitempty"` + DisplayName *TextComponent `json:"displayName,omitempty"` +} + +type LoginInfo struct { + EntityID int32 `json:"entityID,omitempty"` + Hardcore bool `json:"hardcore,omitempty"` + Dimensions []string `json:"dimensions,omitempty"` + MaxPlayers int32 `json:"maxPlayers,omitempty"` + ViewDistance int32 `json:"viewDistance,omitempty"` + SimulationDistance int32 `json:"simulationDistance,omitempty"` + ReducedDebugInfo bool `json:"reducedDebugInfo,omitempty"` + EnableRespawnScreen bool `json:"enableRespawnScreen,omitempty"` + DoLimitedCrafting bool `json:"doLimitedCrafting,omitempty"` + DimensionType int32 `json:"dimensionType,omitempty"` + DimensionName string `json:"dimensionName,omitempty"` + HashedSeed int64 `json:"hashedSeed,omitempty"` + GameMode byte `json:"gameMode,omitempty"` + PreviousGameMode byte `json:"previousGameMode,omitempty"` + IsDebug bool `json:"isDebug,omitempty"` + IsFlat bool `json:"isFlat,omitempty"` + HasDeathLocation bool `json:"hasDeathLocation,omitempty"` + DeathDimensionName string `json:"deathDimensionName,omitempty"` + DeathLocation Position `json:"deathLocation,omitempty"` + PortalCooldown int32 `json:"portalCooldown,omitempty"` + EnforcesSecureChat bool `json:"enforcesSecureChat,omitempty"` +} + +type DifficultyObject struct { + Difficulty byte `json:"difficulty,omitempty"` + Locked bool `json:"locked,omitempty"` +} + +type TagArray struct { + TagName string `json:"tagName,omitempty"` + Entries []int32 `json:"entries,omitempty"` +} + +type UpdateTag struct { + TagRegistryIdentifier string `json:"tagRegistryIdentifier,omitempty"` + Tags []TagArray `json:"tags,omitempty"` +} + +type RegistryEntry struct { + EntryID string `json:"entryID,omitempty"` + HasNBT bool `json:"hasNBT,omitempty"` + //TODO implement the nbt data + NBTData []byte `json:"nbtData,omitempty"` +} + +type RegistryData struct { + RegistryID string `json:"registryID,omitempty"` + Entries []RegistryEntry `json:"entries,omitempty"` +} + +type Favicon struct { + PngData []byte `json:"pngData,omitempty"` +} + +type DatapackInfo struct { + Namespace string `json:"namespace,omitempty"` + ID string `json:"id,omitempty"` + Version string `json:"version,omitempty"` +} + +type TextPiece struct { + Text string `json:"text,omitempty"` + Color string `json:"color,omitempty"` + CleanText string `json:"cleantext,omitempty"` +} + +type TextComponent struct { + Text string `json:"text,omitempty"` + Extra []TextPiece `json:"extra,omitempty"` + CleanText string `json:"cleantext,omitempty"` +} + +type WorldBorderInfo struct { + X float64 `json:"x,omitempty"` + Z float64 `json:"z,omitempty"` + OldDiameter float64 `json:"oldDiameter,omitempty"` + NewDiameter float64 `json:"newDiameter,omitempty"` + Speed int64 `json:"speed,omitempty"` + PortalTeleportBoundry int32 `json:"portalTeleportBoundry,omitempty"` + WarningBlocks int32 `json:"warningBlocks,omitempty"` + WarningTime int32 `json:"warningTime,omitempty"` +} + +// Custom unmarshaler for the TextComponent type +func (d *TextComponent) UnmarshalJSON(data []byte) error { + // Try to unmarshal as a simple string + var text string + if err := json.Unmarshal(data, &text); err == nil { + d.Text = text + d.CleanText = cleanMinecraftFormatting(text) + return nil + } + + // If that fails, try to unmarshal as an object with a "text" field and optional "extra" field + var temp struct { + Text string `json:"text,omitempty"` + Extra []TextPiece `json:"extra,omitempty"` + } + if err := json.Unmarshal(data, &temp); err == nil { + d.Text = temp.Text + d.Extra = temp.Extra + + // Clean the text and combine it with cleaned children texts + var sb strings.Builder + sb.Write([]byte(temp.Text)) + for _, extra := range temp.Extra { + sb.Write([]byte(extra.Text)) + } + d.CleanText = cleanMinecraftFormatting(sb.String()) + + return nil + } + + // Return an error if neither unmarshaling succeeds + return fmt.Errorf("could not unmarshal TextComponent: data is neither a string nor an object with 'text' and 'extra' fields") +} + +// Function to clean Minecraft formatting from a text +func cleanMinecraftFormatting(text string) string { + // Regex to match Minecraft formatting codes like § and & + regex := regexp.MustCompile(`(?i)§[0-9A-FK-OR]`) + return regex.ReplaceAllString(text, "") +} + +// Custom unmarshaler for the TextComponent type +func (f *Favicon) UnmarshalJSON(data []byte) error { + // Try to unmarshal as a simple string + var text string + if err := json.Unmarshal(data, &text); err == nil { + if strings.HasPrefix(text, "data:image/png;base64,") { + trimmed := strings.TrimPrefix(text, "data:image/png;base64,") + pngDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(trimmed)) + pngData, err := io.ReadAll(pngDecoder) + if err != nil { + return err + } + f.PngData = pngData + return nil + } else { + return fmt.Errorf("wrong prefix for favicon") + } + } + return errors.New("could not unmarshal Favicon") +} + +type Response struct { + RawMessage string `json:"rawMessage,omitempty"` + Version struct { + Name string `json:"name"` + Protocol int32 `json:"protocol"` + } `json:"version"` + Players struct { + Max int32 `json:"max"` + Online int32 `json:"online"` + Sample []Player `json:"sample"` + } `json:"players"` + Description TextComponent `json:"description"` // Handles both string and object with "text" and "extra" fields + Favicon Favicon `json:"favicon"` + EnforcesSecureChat bool `json:"enforcesSecureChat"` + PreventsChatReports bool `json:"preventsChatReports"` + CompressionThreshold int32 `json:"compressionThreshold,omitempty"` + IsOfflineMode bool `json:"IsOfflineMode,omitempty"` + PluginDataSent map[string]string `json:"PluginDataSent,omitempty"` + FeatureFlags []string `json:"featureFlags,omitempty"` + EnabledDatapacks []DatapackInfo `json:"EnabledDatapacks,omitempty"` + Encryption bool `json:"Encryption,omitempty"` + Message TextComponent `json:"Message,omitempty"` + RegistryDatas []RegistryData `json:"RegistryDatas,omitempty"` + Tags []UpdateTag `json:"Tags,omitempty"` + Username string `json:"Username,omitempty"` + PlayerLoginInfo LoginInfo `json:"PlayerLoginInfo,omitempty"` + ServerDifficulty DifficultyObject `json:"ServerDifficulty,omitempty"` + PlayerAbilities PlayerAbilitiesObject `json:"PlayerAbilities,omitempty"` + PlayerSlot byte `json:"PlayerSlot,omitempty"` + PlayerLocation PlayerPosition `json:"PlayerLocation,omitempty"` + DefaultPositionSpawn DefaultSpawnPosition `json:"DefaultPositionSpawn,omitempty"` + TimeOfDay int64 `json:"TimeOfDay,omitempty"` + WorldAge int64 `json:"worldAge,omitempty"` + WorldBorder WorldBorderInfo `json:"worldBorder,omitempty"` + PlayersInfo []PlayerUpdate `json:"playersinfo,omitempty"` +}