This commit is contained in:
Bruno Rybársky 2024-09-18 19:58:25 +02:00
commit efd6d64d01
9 changed files with 2782 additions and 0 deletions

0
.gitignore vendored Normal file

8
go.mod Normal file

@ -0,0 +1,8 @@
module mcsrv
go 1.23.1
require (
github.com/Tnze/go-mc v1.20.2
github.com/google/uuid v1.6.0
)

4
go.sum Normal file

@ -0,0 +1,4 @@
github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q=
github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

387
main.go Normal file

@ -0,0 +1,387 @@
package main
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"errors"
"fmt"
"github.com/google/uuid"
"log"
"net"
"os"
"reflect"
"regexp"
"strings"
"time"
)
const (
HOST = "127.0.0.2"
PORT = "25565"
TYPE = "tcp"
ProtocolVersion = 767
VERSION = "1.21.1"
CompressionThreshold = -1
)
var maxPlayers int32
var icon string
var serverID = make([]byte, 20)
var serverPrivateKey *rsa.PrivateKey
var serverPublicKey *rsa.PublicKey
var usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_]{3,16}$")
var world World
func readIcon(fileName string) (icon string) {
iconData, err := os.ReadFile(fileName)
if err != nil {
return
}
icon = "data:image/png;base64," + base64.StdEncoding.EncodeToString(iconData)
return
}
func main() {
maxPlayers = 200
icon = readIcon("icon.png")
world = World{
Difficulty: 0,
DifficultyLocked: false,
Hardcore: false,
Dimensions: []Dimension{
{
ID: 0,
Name: "minecraft:overworld",
}, {
ID: 1,
Name: "minecraft:the_nether",
},
{
ID: 2,
Name: "minecraft:the_end",
},
},
ViewDistance: 4,
SimulationDistance: 4,
ReducedDebugInfo: false,
EnableRespawnScreen: true,
DoLimitedCrafting: false,
Seed: uuid.New(),
FeatureFlags: []string{"minecraft:vanilla", "minecraft:bundle", "minecraft:trade_rebalance"},
DataPacks: []DatapackInfo{{
Namespace: "minecraft",
ID: "core",
Version: "1.21",
}},
ReportDetails: map[string]string{"I hate myself": "YES, REALLY"},
ServerLinks: []ServerLink{{
LabelString: "",
LabelInt: 0,
URL: "https://brn.systems",
}},
}
var err error
//serverID, err = serverIDCreate()
serverID = []byte("")
if err != nil {
log.Fatal(err)
}
serverPrivateKey, serverPublicKey, err = GenerateRSAKeyPair()
if err != nil {
return
}
listen, err := net.Listen(TYPE, HOST+":"+PORT)
if err != nil {
log.Fatal(err)
}
// close listener
defer func(listen net.Listener) {
err := listen.Close()
if err != nil {
log.Println(err)
}
}(listen)
for {
conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
}
go handleRequest(conn)
}
}
func handleRequest(conn net.Conn) {
var errOut error
var player = Player{
state: 0,
conn: conn,
name: "",
version: 0,
requestedAddress: "",
requestedPort: 0,
reader: bufio.NewReader(conn),
compressionThreshold: -1,
}
for {
packetID, packetData, err := player.readPacket()
if err != nil {
errOut = err
break
}
switch player.state {
case 0: // Handshake state
switch packetID {
case 0x00:
version, address, port, nextState, err := getHandshakePacket(packetData)
if err != nil {
errOut = err
break
}
player.version = version
player.requestedAddress = address
player.requestedPort = port
player.state = nextState
if player.state == 3 {
player.state = 2
player.transferred = true
}
world.Players = append(world.Players, player)
default:
errOut = fmt.Errorf("unexpected packet %x in handshake state", packetID)
log.Println(errOut)
}
case 1: // Status state
switch packetID {
case 0x00:
err := player.sendStatusResponse("Hello there")
if err != nil {
errOut = err
break
}
case 0x01:
payload, err := getPingPacket(packetData)
if err != nil {
errOut = err
break
}
err = player.sendPongPacket(payload)
if err != nil {
errOut = err
break
}
default:
errOut = fmt.Errorf("unexpected packet %x in status state", packetID)
log.Println(errOut)
}
case 2: // Login state
switch packetID {
case 0x00:
username, playerUUID, err := getLoginStart(packetData)
if err != nil {
errOut = err
break
}
player.name = username
player.uuid = playerUUID
if !usernameRegex.MatchString(player.name) {
errOut = errors.New("bad username")
break
}
err = player.sendEncryptionRequest()
if err != nil {
errOut = err
break
}
case 0x01:
sharedSecretFromClient, verifyTokenFromClient, err := getEncryptionResponse(packetData)
if err != nil {
errOut = err
break
}
decryptedVerifyTokenFromClient, err := rsa.DecryptPKCS1v15(rand.Reader, serverPrivateKey, verifyTokenFromClient)
if err != nil {
errOut = err
break
}
if !reflect.DeepEqual(player.verifyToken, decryptedVerifyTokenFromClient) {
errOut = errors.New("wrong session verify token")
break
}
decryptedSecret, err := rsa.DecryptPKCS1v15(rand.Reader, serverPrivateKey, sharedSecretFromClient)
if err != nil {
errOut = err
break
}
success, err := player.hasJoinedSession(decryptedSecret)
if err != nil {
errOut = err
break
}
if !success {
errOut = errors.New("joined session not found")
break
}
if player.profile.ID != strings.Replace(player.uuid.String(), "-", "", -1) || player.profile.Name != player.name {
errOut = errors.New("wrong session name")
break
}
player.encryptStream, player.decryptStream, err = NewCFB8Cipher(decryptedSecret)
if err != nil {
errOut = err
break
}
err = player.sendCompression()
if err != nil {
errOut = err
break
}
player.compressionThreshold = CompressionThreshold
log.Println("sent compression")
err = player.sendLoginSuccess()
if err != nil {
errOut = err
break
}
log.Println("sent login success")
err = player.sendDataPacks()
if err != nil {
errOut = err
break
}
err = player.sendRegistryData()
if err != nil {
errOut = err
break
}
err = player.sendFeatureFlags()
if err != nil {
errOut = err
break
}
err = player.sendUpdateTags()
if err != nil {
errOut = err
break
}
err = player.sendReportDetails()
if err != nil {
errOut = err
break
}
err = player.sendServerLinks()
if err != nil {
errOut = err
break
}
case 0x03:
player.state = 3
default:
errOut = fmt.Errorf("unexpected packet %x in login state", packetID)
log.Println(errOut)
}
case 3: //configuration
switch packetID {
case 0x00:
locale, viewDistance, chatMode, chatColors, skinParts, isRightHanded, textFiltering, serverListing, err := getClientInformation(packetData)
if err != nil {
errOut = err
break
}
player.locale = locale
player.viewDistance = viewDistance
player.chatMode = chatMode
player.chatColors = chatColors
player.skinParts = skinParts
player.isRightHanded = isRightHanded
player.textFiltering = textFiltering
player.serverListing = serverListing
case 0x01:
//TODO Implement cookie
case 0x02:
_, _, _ = getPluginPacket(packetData)
case 0x03:
player.state = 4
go player.sendKeepAlives()
err := player.sendPlayStart()
if err != nil {
errOut = err
break
}
player.sendDifficulty()
//player.send
case 0x04:
player.keepAliveReceivedTimestamp = time.Now().Unix()
case 0x05:
err := player.getPongPacket(packetData)
if err != nil {
errOut = err
break
}
case 0x06:
err := player.getResourcePackPacket(packetData)
if err != nil {
errOut = err
break
}
case 0x07:
err := player.getDataPacksPacket(packetData)
if err != nil {
errOut = err
break
}
default:
errOut = fmt.Errorf("unexpected packet %x in configration state", packetID)
log.Println(errOut)
}
case 4:
switch packetID {
default:
errOut = fmt.Errorf("unexpected packet %x in play state", packetID)
log.Println(errOut)
}
default:
errOut = fmt.Errorf("unknown player state %d", player.state)
break
}
if errOut != nil {
break
}
}
if errOut != nil {
log.Println(errOut.Error())
_ = player.sendDisconnect(fmt.Sprintf("Your server thread crashed: %s", errOut.Error()))
}
// Close connection
err := conn.Close()
if err != nil {
log.Println(err)
}
for index, client := range world.Players {
if player.conn == client.conn {
world.Players = append(world.Players[:index], world.Players[index+1:]...)
break
}
}
}

812
packetLib.go Normal file

@ -0,0 +1,812 @@
package main
import (
"bufio"
"bytes"
"compress/zlib"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"io"
"log"
"math"
"math/big"
"net/http"
"regexp"
"strings"
)
func generateRandomBytes(size int) ([]byte, error) {
randomBytes := make([]byte, size)
_, err := rand.Read(randomBytes)
if err != nil {
return nil, err
}
return randomBytes, nil
}
func serverIDCreate() ([]byte, error) {
result := make([]byte, 20)
charRange := 0x7E - 0x21 + 1
for i := 0; i < 20; i++ {
// Generate a random number in the range [charStart, charEnd]
num, err := rand.Int(rand.Reader, big.NewInt(int64(charRange)))
if err != nil {
return []byte{}, err
}
result[i] = byte(0x21 + num.Int64())
}
return result, nil
}
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 RegistryEntry struct {
EntryID string `json:"entryID,omitempty"`
HasNBT bool `json:"hasNBT,omitempty"`
//TODO implement the nbt data
NBTData *bytes.Buffer `json:"nbtData,omitempty"`
}
type RegistryData struct {
RegistryID string `json:"registryID,omitempty"`
Entries []RegistryEntry `json:"entries,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 DatapackInfo struct {
Namespace string `json:"namespace,omitempty"`
ID string `json:"id,omitempty"`
Version string `json:"version,omitempty"`
}
type ServerLink struct {
LabelString string `json:"labelString,omitempty"`
LabelInt int32 `json:"labelInt,omitempty"`
URL string `json:"url,omitempty"`
}
// GenerateRSAKeyPair generates an RSA key pair and returns the public and private keys.
func GenerateRSAKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return nil, nil, err
}
return privateKey, &privateKey.PublicKey, nil
}
// ExportRSAPublicKey exports the public key in ASN.1 DER format.
func ExportRSAPublicKey(pubKey *rsa.PublicKey) ([]byte, error) {
// Marshal the public key to ASN.1 DER format
pubASN1, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return nil, err
}
// Return the raw DER-encoded bytes
return pubASN1, nil
}
type cfb8 struct {
b cipher.Block
blockSize int
in []byte
out []byte
decrypt bool
}
func (x *cfb8) XORKeyStream(dst, src []byte) {
for i := range src {
x.b.Encrypt(x.out, x.in)
copy(x.in[:x.blockSize-1], x.in[1:])
if x.decrypt {
x.in[x.blockSize-1] = src[i]
}
dst[i] = src[i] ^ x.out[0]
if !x.decrypt {
x.in[x.blockSize-1] = dst[i]
}
}
}
// NewCFB8Encryptor returns a Stream which encrypts with cipher feedback mode
// (segment size = 8), using the given Block. The iv must be the same length as
// the Block's block size.
func newCFB8Encryptor(block cipher.Block, iv []byte) cipher.Stream {
return newCFB8(block, iv, false)
}
// NewCFB8Decryptor returns a Stream which decrypts with cipher feedback mode
// (segment size = 8), using the given Block. The iv must be the same length as
// the Block's block size.
func newCFB8Decryptor(block cipher.Block, iv []byte) cipher.Stream {
return newCFB8(block, iv, true)
}
func newCFB8(block cipher.Block, iv []byte, decrypt bool) cipher.Stream {
blockSize := block.BlockSize()
if len(iv) != blockSize {
// stack trace will indicate whether it was de or encryption
panic("cipher.newCFB: IV length must equal block size")
}
x := &cfb8{
b: block,
blockSize: blockSize,
out: make([]byte, blockSize),
in: make([]byte, blockSize),
decrypt: decrypt,
}
copy(x.in, iv)
return x
}
// NewCFB8Cipher Initialize AES/CFB8 encryption using the shared secret
func NewCFB8Cipher(secret []byte) (cipher.Stream, cipher.Stream, error) {
block, err := aes.NewCipher(secret)
if err != nil {
return nil, nil, err
}
// Create CFB8 encryption/decryption ciphers using the custom implementation
iv := secret // Initial vector (IV) is the same as the secret in this case
encryptStream := newCFB8Encryptor(block, iv)
decryptStream := newCFB8Decryptor(block, iv)
return encryptStream, decryptStream, nil
}
func AuthDigest(parts [][]byte) string {
h := sha1.New()
// Hash each part incrementally
for _, part := range parts {
io.WriteString(h, string(part))
}
// Finalize the hash
hash := h.Sum(nil)
// Check for negative hashes
negative := (hash[0] & 0x80) == 0x80
if negative {
hash = twosComplement(hash)
}
// Trim away zeroes
res := strings.TrimLeft(hex.EncodeToString(hash), "0")
if negative {
res = "-" + res
}
return res
}
// little endian
func twosComplement(p []byte) []byte {
carry := true
for i := len(p) - 1; i >= 0; i-- {
p[i] = ^p[i]
if carry {
carry = p[i] == 0xff
p[i]++
}
}
return p
}
type MojangProfile struct {
ID string `json:"id"`
Name string `json:"name"`
Properties []MojangProperty `json:"properties"`
}
type MojangProperty struct {
Name string `json:"name"`
Value string `json:"value"`
Signature string `json:"signature"`
}
func (player *Player) hasJoinedSession(sharedSecret []byte) (bool, error) {
serverKey, err := ExportRSAPublicKey(serverPublicKey)
serverIDHash := AuthDigest([][]byte{serverID, sharedSecret, serverKey})
remoteAddr := strings.Split(player.conn.RemoteAddr().String(), ":")[0]
url := fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s", player.name, serverIDHash, remoteAddr)
resp, err := http.Get(url)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("failed to verify session: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
var profile MojangProfile
err = json.Unmarshal(body, &profile)
if err != nil {
return false, err
}
player.profile = profile
return true, nil
}
// EncryptPacket encrypts a packet using the AES/CFB8 cipher stream.
func EncryptPacket(stream cipher.Stream, data *bytes.Buffer) *bytes.Buffer {
if stream == nil {
return data
}
encryptedData := make([]byte, data.Len())
stream.XORKeyStream(encryptedData, data.Bytes())
return bytes.NewBuffer(encryptedData)
}
// DecryptingReader returns a *bufio.Reader that provides decrypted data from the original reader.
func DecryptingReader(stream cipher.Stream, reader *bufio.Reader) *bufio.Reader {
pr, pw := io.Pipe() // create a pipe to stream decrypted data
go func() {
defer pw.Close() // ensure the pipe writer is closed when done
buf := make([]byte, 4096) // buffer size, adjust as needed
for {
n, err := reader.Read(buf)
if err != nil {
if err == io.EOF {
break
}
pw.CloseWithError(err) // propagate any non-EOF error to the pipe reader
return
}
data := buf[:n]
decryptedBlock := make([]byte, len(data))
if stream != nil {
stream.XORKeyStream(decryptedBlock, data) // decrypt the data
} else {
decryptedBlock = data // if no stream, just pass the data through
}
_, err = pw.Write(decryptedBlock) // write decrypted data to the pipe writer
if err != nil {
pw.CloseWithError(err) // propagate write errors
return
}
}
}()
return bufio.NewReader(pr) // wrap the pipe reader in a bufio.Reader
}
// 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, "")
}
// Decompress function using zlib
func decompress(data *bytes.Buffer) (buf *bytes.Buffer, err error) {
reader, err := zlib.NewReader(data)
if err != nil {
return
}
defer func(reader io.ReadCloser) {
_ = reader.Close()
}(reader)
var decompressedData bytes.Buffer
_, err = io.Copy(&decompressedData, reader)
if err != nil {
return
}
return &decompressedData, nil
}
// Compress function using zlib
func compress(data *bytes.Buffer) (*bytes.Buffer, error) {
var compressedData bytes.Buffer
writer := zlib.NewWriter(&compressedData)
_, err := writer.Write(data.Bytes())
if err != nil {
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
return &compressedData, nil
}
// Modified version of `sendPacket` with hexdump logging, including unencrypted data
func (player *Player) sendPacket(packetID int32, packetData *bytes.Buffer) (err error) {
var origDataBuffer bytes.Buffer
addVarint(&origDataBuffer, packetID) // Add the Packet ID as a VarInt
_, err = origDataBuffer.ReadFrom(packetData)
if err != nil {
return err
}
var outBuffer bytes.Buffer
var outTempBuffer bytes.Buffer
length := int32(origDataBuffer.Len()) // Get the length of the uncompressed packet
compressed := false // Track if the packet is compressed
// Compression based on a threshold
if player.compressionThreshold >= 0 && length >= player.compressionThreshold {
compressed = true // Mark that compression was applied
compressedData, err := compress(&origDataBuffer)
if err != nil {
return err
}
origDataBuffer = *compressedData
// Add the uncompressed length to outTempBuffer
addVarint(&outTempBuffer, length)
}
encrypted := player.encryptStream != nil
// Log the unencrypted packet data
log.Printf("Unencrypted Packet ID: %d", packetID)
log.Printf("Unencrypted Packet Length: %d (compressed: %v, encrypted: %v)", origDataBuffer.Len(), compressed, encrypted)
log.Printf("Hexdump of Unencrypted Packet:\n%s", hex.Dump(origDataBuffer.Bytes()))
// Encrypt the (compressed or uncompressed) data buffer
dataBuffer := origDataBuffer
// Append the (compressed or uncompressed) data buffer to outTempBuffer
_, err = outTempBuffer.ReadFrom(&dataBuffer)
if err != nil {
return err
}
// Add the total packet length to outBuffer
addVarint(&outBuffer, int32(outTempBuffer.Len()))
_, err = outBuffer.ReadFrom(&outTempBuffer)
if err != nil {
return err
}
if encrypted {
outBuffer = *EncryptPacket(player.encryptStream, &outBuffer)
}
_, err = player.conn.Write(outBuffer.Bytes())
if err != nil {
return err
}
return nil
}
// Modified version of `readPacket` with hexdump logging
func (player *Player) readPacket() (packetID int32, packetData *bytes.Buffer, err error) {
packetData = &bytes.Buffer{}
finalReader := player.reader
if player.decryptStream != nil {
finalReader = DecryptingReader(player.decryptStream, player.reader)
}
packetLength, _, err := readVarint(finalReader)
if err != nil {
return
}
packetDataBytes := make([]byte, packetLength)
nt, err := io.ReadFull(finalReader, packetDataBytes)
if err != nil {
return
}
_, err = packetData.Write(packetDataBytes)
if err != nil {
return 0, &bytes.Buffer{}, err
}
n := int32(nt)
if n != packetLength {
err = errors.New("packet read length mismatch")
return
}
var dataLength int
compressed := false // Track if the packet is compressed
if player.compressionThreshold > 0 {
dataLengthTemp, errTemp := receiveVarint(packetData)
err = errTemp
dataLength = int(dataLengthTemp)
}
// If dataLength > 0, it means the packet is compressed
if dataLength > 0 {
compressed = true
packetData, err = decompress(packetData)
if err != nil {
return
}
}
// Extract packet ID
packetID, err = receiveVarint(packetData)
// Log the packet information
log.Printf("Received Packet ID: %d", packetID)
log.Printf("Packet Length: %d (compressed: %v)", packetLength, compressed)
log.Printf("Hexdump of Decoded Packet:\n%s", hex.Dump(packetData.Bytes()))
return
}
func addVarint(buffer *bytes.Buffer, value int32) {
uValue := uint32(value) // Explicitly cast to an unsigned type
for {
b := byte(uValue & 0x7F)
uValue >>= 7
if (uValue != 0 && uValue != 0xFFFFFFFF) || (b&0x40 != 0) {
b |= 0x80
}
addByte(buffer, b)
if uValue == 0 || uValue == 0xFFFFFFFF {
break
}
}
}
func addVarlong(buffer *bytes.Buffer, value int64) {
uValue := uint64(value) // Explicitly cast to an unsigned type
for {
b := byte(uValue & 0x7F)
uValue >>= 7
if (uValue != 0 && uValue != 0xFFFFFFFFFFFFFFFF) || (b&0x40 != 0) {
b |= 0x80
}
addByte(buffer, b)
if uValue == 0 || uValue == 0xFFFFFFFFFFFFFFFF {
break
}
}
}
func readVarint(r *bufio.Reader) (value int32, length uint32, err error) {
var (
b byte
shift uint64
uValue uint32
)
for {
b, err = r.ReadByte()
if err != nil {
return 0, 0, err
}
length++
uValue |= (uint32(b) & 0x7F) << shift
shift += 7
if b&0x80 == 0 {
break
}
}
if shift < 8*uint64(length) && b&0x40 != 0 {
uValue |= ^uint32(0) << shift
}
value = int32(uValue)
return
}
func receiveVarint(buf *bytes.Buffer) (value int32, err error) {
var (
b byte
shift uint64
uValue uint32
length uint32
)
for i := 0; i < buf.Len(); i++ {
b, err = buf.ReadByte()
if err != nil {
return
}
length++
uValue |= (uint32(b) & 0x7F) << shift
shift += 7
if b&0x80 == 0 {
break
}
}
if shift < 8*uint64(length) && b&0x40 != 0 {
uValue |= ^uint32(0) << shift
}
value = int32(uValue)
return
}
func receiveVarlong(buf *bytes.Buffer) (currentValue int64) {
var (
b byte
shift uint64
uValue uint64
length uint32
)
for i := 0; i < buf.Len(); i++ {
bx, err := buf.ReadByte()
b = bx
if err != nil {
return
}
length++
uValue |= (uint64(b) & 0x7F) << shift
shift += 7
if b&0x80 == 0 {
break
}
}
if shift < 8*uint64(length) && b&0x40 != 0 {
uValue |= ^uint64(0) << shift
}
currentValue = int64(uValue)
return currentValue
}
func addUUID(buffer *bytes.Buffer, inUUID uuid.UUID) int32 {
binaryUUID, err := inUUID.MarshalBinary()
if err != nil {
log.Println("Error marshalling UUID:", err)
return 0
}
buffer.Write(binaryUUID)
return int32(len(binaryUUID))
}
// Decodes a UUID from the buffer.
func receiveUUID(buffer *bytes.Buffer) (value uuid.UUID, err error) {
b := make([]byte, 16)
_, err = buffer.Read(b)
if err != nil {
return uuid.UUID{}, err
}
value, err = uuid.FromBytes(b)
if err != nil {
return uuid.UUID{}, err
}
return value, nil
}
func addByte(buffer *bytes.Buffer, byte byte) {
buffer.WriteByte(byte)
}
func addBool(buffer *bytes.Buffer, boolean bool) {
var boolByte byte = 0x00
if boolean {
boolByte = 0x01
}
buffer.WriteByte(boolByte)
}
func receiveBool(buffer *bytes.Buffer) (boolean bool, err error) {
b, err := buffer.ReadByte()
if err != nil {
return false, err
}
boolean = b == 1
return
}
func addUint16(buffer *bytes.Buffer, value uint16) {
b := make([]byte, 2)
binary.BigEndian.PutUint16(b, value)
buffer.Write(b)
}
// Decodes an uint16 from the buffer.
func receiveUint16(buffer *bytes.Buffer) (value uint16) {
// Create a byte slice to hold the 2 bytes for the uint16.
b := make([]byte, 2)
// Read exactly 2 bytes from the buffer.
_, err := buffer.Read(b)
if err != nil {
return 0
}
// Convert the byte slice to uint16 using BigEndian.
value = binary.BigEndian.Uint16(b)
return value
}
func addInt64(buffer *bytes.Buffer, value int64) {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(value))
buffer.Write(b)
}
func receiveInt64(buffer *bytes.Buffer) (value int64) {
b := make([]byte, 8)
_, err := buffer.Read(b)
if err != nil {
return 0
}
value = int64(binary.BigEndian.Uint64(b))
return
}
func addFloat32(buffer *bytes.Buffer, value float32) {
bits := math.Float32bits(value)
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, bits)
buffer.Write(b)
}
// Decodes a float32 from the buffer.
func receiveFloat32(buffer *bytes.Buffer) (value float32) {
b := make([]byte, 4)
_, err := buffer.Read(b)
if err != nil {
return 0
}
value = math.Float32frombits(binary.BigEndian.Uint32(b))
return
}
func addFloat64(buffer *bytes.Buffer, value float64) {
bits := math.Float64bits(value)
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, bits)
buffer.Write(b)
}
// Decodes a float64 from the buffer.
func receiveFloat64(buffer *bytes.Buffer) (value float64) {
b := make([]byte, 8)
_, err := buffer.Read(b)
if err != nil {
return 0
}
value = math.Float64frombits(binary.BigEndian.Uint64(b))
return
}
func addInt32(buffer *bytes.Buffer, value int32) {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(value))
buffer.Write(b)
}
// Decodes an int32 from the buffer.
func receiveInt32(buffer *bytes.Buffer) (value int32) {
b := make([]byte, 4)
_, err := buffer.Read(b)
if err != nil {
return 0
}
value = int32(binary.BigEndian.Uint32(b))
return
}
func addInt16(buffer *bytes.Buffer, value int16) {
b := make([]byte, 2)
binary.BigEndian.PutUint16(b, uint16(value))
buffer.Write(b)
}
func addUint32(buffer *bytes.Buffer, value uint32) {
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, value)
buffer.Write(b)
}
// Decodes an uint32 from the buffer.
func receiveUint32(buffer *bytes.Buffer) (value uint32, err error) {
b := make([]byte, 4)
if _, err = buffer.Read(b); err != nil {
return 0, err
}
value = binary.BigEndian.Uint32(b)
return
}
func addString(buffer *bytes.Buffer, str string) {
// Encode string length using VarInt.
addVarint(buffer, int32(len(str)))
// Append the string bytes.
buffer.Write([]byte(str))
}
// Decodes a string from the buffer.
func receiveString(buf *bytes.Buffer) (strOut string, err error) {
stringLen, err := receiveVarint(buf)
if err != nil {
return "", err
}
stringBytes := make([]byte, stringLen)
_, err = buf.Read(stringBytes)
if err != nil {
return "", err
}
strOut = string(stringBytes)
return
}
// addTextComponent serializes a TextComponent to JSON and adds it to the buffer
func addTextComponent(buffer *bytes.Buffer, component TextComponent) {
// Serialize the TextComponent to a JSON string
jsonBytes, err := json.Marshal(component)
if err != nil {
return
}
// Convert JSON bytes to a string
jsonString := string(jsonBytes)
// Use addString to addExplosion the serialized TextComponent JSON to the buffer
addString(buffer, jsonString)
return
}
func receiveTextComponent(buf *bytes.Buffer) (component TextComponent, err error) {
jsonString, err := receiveString(buf)
if err != nil {
return
}
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
}

361
playPackets.go Normal file

@ -0,0 +1,361 @@
package main
import (
"bufio"
"bytes"
"github.com/Tnze/go-mc/nbt"
)
func (player *Player) sendBundleDelimiter() error {
return player.sendPacket(0x00, &bytes.Buffer{})
}
func (player *Player) sendSpawnEntity(entity *Entity) error {
var buf bytes.Buffer
entity.add(&buf)
return player.sendPacket(0x01, &buf)
}
func (player *Player) sendSpawnXP(xpOrb XPOrb) error {
var buf bytes.Buffer
xpOrb.add(&buf)
return player.sendPacket(0x02, &buf)
}
func (player *Player) sendEntityAnimation(entity *Entity) error {
var buf bytes.Buffer
addVarint(&buf, entity.ID)
addByte(&buf, entity.Animation)
return player.sendPacket(0x03, &buf)
}
func (player *Player) sendAwardStatistics() error {
var buf bytes.Buffer
addVarint(&buf, int32(len(player.statistics)))
for _, stat := range player.statistics {
addVarint(&buf, stat.CategoryID)
addVarint(&buf, stat.StatisticID)
addVarint(&buf, stat.Value)
}
return player.sendPacket(0x04, &buf)
}
func (player *Player) sendBlockChangeACK(seqID int32) error {
var buf bytes.Buffer
addVarint(&buf, seqID)
return player.sendPacket(0x05, &buf)
}
func (player *Player) sendBlockDestroyStage(entity *Entity, block *Block) error {
var buf bytes.Buffer
addVarint(&buf, entity.ID)
block.Position.add(&buf)
addByte(&buf, block.DestroyStage)
return player.sendPacket(0x06, &buf)
}
func (player *Player) sendBlockEntityData(entity *Entity, block *Block) error {
var buf bytes.Buffer
block.Position.add(&buf)
addVarint(&buf, entity.ID)
var nbtBuf bytes.Buffer
nbtWriter := bufio.NewWriter(&nbtBuf)
nbtEnc := nbt.NewEncoder(nbtWriter)
for key, val := range block.BlockEntityData {
err := nbtEnc.Encode(val, key)
if err != nil {
return err
}
}
nbtWriter.Flush()
buf.ReadFrom(&nbtBuf)
return player.sendPacket(0x07, &buf)
}
func (player *Player) sendBlockAction(block *Block, actionID uint8, actionParam uint8) error {
var buf bytes.Buffer
block.Position.add(&buf)
addByte(&buf, actionID)
addByte(&buf, actionParam)
addVarint(&buf, block.BlockTypeID)
return player.sendPacket(0x08, &buf)
}
func (player *Player) sendBlockUpdate(block *Block) error {
var buf bytes.Buffer
block.Position.add(&buf)
addVarint(&buf, block.BlockStateID)
return player.sendPacket(0x09, &buf)
}
func (player *Player) sendDifficulty() error {
var buf bytes.Buffer
addByte(&buf, world.Difficulty)
addBool(&buf, world.DifficultyLocked)
return player.sendPacket(0x0B, &buf)
}
func (player *Player) sendChunks() error {
//var buf bytes.Buffer
var bufFinished bytes.Buffer
err := player.sendPacket(0x0D, &bytes.Buffer{}) //chunk batch start
if err != nil {
return err
}
//count:= int32(len(chunks))
count := int32(0)
addVarint(&bufFinished, count)
err = player.sendPacket(0x0C, &bufFinished) //chunk batch end
if err != nil {
return err
}
return nil
}
//0x0E is chunk biomes, might implement later
//0x10 is Command Suggestions Response, might implement later
//0x11 is Commands, might implement later
func (player *Player) sendCloseContainer(windowID uint8) error {
var buf bytes.Buffer
addByte(&buf, windowID)
return player.sendPacket(0x12, &buf)
}
func (player *Player) sendContainerContent(windowID uint8, slots []*Slot, cursor *Slot) error {
var buf bytes.Buffer
addByte(&buf, windowID)
addVarint(&buf, player.lastContainerStateID)
player.lastContainerStateID++
addVarint(&buf, int32(len(slots)))
for _, slot := range slots {
slot.add(&buf)
}
cursor.add(&buf)
return player.sendPacket(0x13, &buf)
}
func (player *Player) sendContainerProperty(windowID uint8, property int16, value int16) error {
var buf bytes.Buffer
addByte(&buf, windowID)
addInt16(&buf, property)
addInt16(&buf, value)
return player.sendPacket(0x14, &buf)
}
func (player *Player) sendContainerSlot(windowID uint8, slotIndex int16, slot *Slot) error {
var buf bytes.Buffer
addByte(&buf, windowID)
addVarint(&buf, player.lastContainerStateID)
player.lastContainerStateID++
addInt16(&buf, slotIndex)
slot.add(&buf)
return player.sendPacket(0x15, &buf)
}
//0x16 is cookie, might get implemented cross-state
func (player *Player) sendCooldown(itemID int32, cooldownTicks int32) error {
var buf bytes.Buffer
addVarint(&buf, itemID)
addVarint(&buf, cooldownTicks)
return player.sendPacket(0x17, &buf)
}
func (player *Player) sendChatSuggestions(action int32, entries []string) error {
var buf bytes.Buffer
addVarint(&buf, action)
addVarint(&buf, int32(len(entries)))
for _, entry := range entries {
addString(&buf, entry)
}
return player.sendPacket(0x18, &buf)
}
//0x19 is plugin message, should be implemented globally
func (player *Player) sendDamageEvent(damagedEntity *Entity, damageType int32, sourceEntity *Entity, sourceDirectEntity *Entity) error {
var buf bytes.Buffer
addVarint(&buf, damagedEntity.ID)
addVarint(&buf, damageType)
addVarint(&buf, sourceEntity.ID)
if sourceDirectEntity != nil {
addVarint(&buf, sourceDirectEntity.ID)
} else {
addVarint(&buf, sourceEntity.ID)
}
addBool(&buf, true)
sourceEntity.Position.add(&buf)
return player.sendPacket(0x1A, &buf)
}
func (player *Player) sendDebugSample(sampleType int32, sample []int64) error {
var buf bytes.Buffer
addVarint(&buf, int32(len(sample)))
for _, val := range sample {
addInt64(&buf, val)
}
addVarint(&buf, sampleType)
return player.sendPacket(0x1B, &buf)
}
func (player *Player) deleteMessage(messageID int32, signature []byte) error {
var buf bytes.Buffer
addVarint(&buf, messageID)
if messageID == 0 {
for _, sigPiece := range signature {
addByte(&buf, sigPiece)
}
}
return player.sendPacket(0x1C, &buf)
}
//0x1D is disconnect, should be implemented globally
func (player *Player) sendDisguisedChatMessage(message TextComponent, typeID int32, senderName TextComponent, targetName *TextComponent) error {
var buf bytes.Buffer
addTextComponent(&buf, message)
addVarint(&buf, typeID)
addTextComponent(&buf, senderName)
hasTargetName := targetName != nil
addBool(&buf, hasTargetName)
if hasTargetName {
addTextComponent(&buf, *targetName)
}
return player.sendPacket(0x1E, &buf)
}
func (player *Player) sendEntityEvent(entity *Entity) error {
var buf bytes.Buffer
addInt32(&buf, entity.ID)
addByte(&buf, byte(entity.status))
return player.sendPacket(0x1F, &buf)
}
func (player *Player) sendExplosion(explosion *Explosion) error {
var buf bytes.Buffer
explosion.Position.add(&buf)
addFloat32(&buf, explosion.Strength)
addVarint(&buf, int32(len(explosion.AffectedBlockRecords)))
for _, affectedBlockRecord := range explosion.AffectedBlockRecords {
affectedBlockRecord.add(&buf)
}
explosion.PlayerMotion.add(&buf)
addVarint(&buf, explosion.BlockInteraction)
explosion.SmallExplosionParticle.addExplosion(&buf)
explosion.LargeExplosionParticle.addExplosion(&buf)
explosion.ExplosionSound.add(&buf)
return player.sendPacket(0x20, &buf)
}
func (player *Player) sendUnloadChunk(chunk *Chunk) error {
var buf bytes.Buffer
addInt32(&buf, chunk.Z)
addInt32(&buf, chunk.X)
return player.sendPacket(0x21, &buf)
}
func (player *Player) sendGameEvent(event uint8, value float32) error {
var buf bytes.Buffer
addByte(&buf, event)
addFloat32(&buf, value)
return player.sendPacket(0x22, &buf)
}
func (player *Player) sendHorseScreen(windowID uint8, slotCount int32, entityID int32) error {
var buf bytes.Buffer
addByte(&buf, windowID)
addVarint(&buf, slotCount)
addInt32(&buf, entityID)
return player.sendPacket(0x23, &buf)
}
// Example usage in sendHurtAnimation
func (player *Player) sendHurtAnimation(attacker *Entity) error {
var buf bytes.Buffer
addVarint(&buf, attacker.ID)
// Calculate yaw based on attacker and player positions
attackYaw := calculateYaw(attacker.Position, player.Position)
addFloat32(&buf, attackYaw)
return player.sendPacket(0x24, &buf)
}
func (player *Player) sendWorldBorder(border *WorldBorder) error {
var buf bytes.Buffer
addFloat64(&buf, border.X)
addFloat64(&buf, border.Z)
addFloat64(&buf, border.OldDiameter)
addFloat64(&buf, border.NewDiameter)
addVarlong(&buf, border.Speed)
addVarint(&buf, border.PortalTeleportBoundary)
addVarint(&buf, border.WarningBlocks)
addVarint(&buf, border.WarningTime)
return player.sendPacket(0x25, &buf)
}
//0x26 is keepalive
//0x27 is chunk and light //TODO LATER
func (player *Player) sendWorldEvent(event int32, position BlockPosition, data int32, disableRelativeVolume bool) error {
var buf bytes.Buffer
addInt32(&buf, event)
position.add(&buf)
addInt32(&buf, data)
addBool(&buf, disableRelativeVolume)
return player.sendPacket(0x28, &buf)
}
func (player *Player) sendParticle(particle *Particle, count int32) error {
var buf bytes.Buffer
particle.addPacket(&buf, count)
return player.sendPacket(0x29, &buf)
}
//0x2A is Update light levels //TODO LATER
func (player *Player) sendPlayStart() error {
var buf bytes.Buffer
addInt32(&buf, player.Entity.ID)
addBool(&buf, world.Hardcore)
addVarint(&buf, int32(len(world.Dimensions)))
for _, dimension := range world.Dimensions {
addString(&buf, dimension.Name)
}
addVarint(&buf, maxPlayers)
addVarint(&buf, world.ViewDistance)
addVarint(&buf, world.SimulationDistance)
addBool(&buf, world.ReducedDebugInfo)
addBool(&buf, world.EnableRespawnScreen)
addBool(&buf, world.DoLimitedCrafting)
addVarint(&buf, player.Dimension.ID)
addString(&buf, player.Dimension.Name)
noise, err := world.Seed.MarshalBinary()
if err != nil {
return err
}
for i := 0; i < 8; i++ {
addByte(&buf, noise[i])
}
addByte(&buf, player.GameMode)
addByte(&buf, byte(player.PreviousGameMode))
addBool(&buf, world.Debug)
addBool(&buf, world.Flat)
hasDeathLocation := player.DeathDimension != nil
addBool(&buf, hasDeathLocation)
if hasDeathLocation {
addString(&buf, player.DeathDimension.Name)
player.DeathPosition.add(&buf)
}
addVarint(&buf, player.Entity.PortalCooldown)
addBool(&buf, world.EnforceSecureChat)
return player.sendPacket(0x2B, &buf)
}
//func (player *Player) sendMapItemData(mapId int32) error {
// mapEntry := world.m
//}

494
prePlayPackets.go Normal file

@ -0,0 +1,494 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"github.com/google/uuid"
"math/rand"
"time"
)
func getHandshakePacket(data *bytes.Buffer) (version int32, address string, port uint16, nextState int32, err error) {
version, err = receiveVarint(data)
address, err = receiveString(data)
port = receiveUint16(data)
nextState, err = receiveVarint(data)
return
}
func encodeTextComponent(buffer *bytes.Buffer, component TextComponent) (err error) {
// First, serialize the component to JSON
jsonData, err := json.Marshal(component)
if err != nil {
return err
}
addString(buffer, string(jsonData))
return
}
func getPingPacket(data *bytes.Buffer) (payload int64, err error) {
payload = receiveInt64(data)
return
}
func getLoginStart(data *bytes.Buffer) (userName string, playerUUID uuid.UUID, err error) {
userName, err = receiveString(data)
playerUUID, err = receiveUUID(data)
return
}
// Decodes encryption response data from the buffer.
func getEncryptionResponse(data *bytes.Buffer) (sharedSecret []byte, verifyToken []byte, err error) {
// Read sharedSecret length using VarInt
sharedSecretLen, err := receiveVarint(data)
if err != nil {
return nil, nil, err
}
// Read sharedSecret bytes
sharedSecret = make([]byte, sharedSecretLen)
_, err = data.Read(sharedSecret)
if err != nil {
return nil, nil, err
}
// Read verifyToken length using VarInt
verifyTokenLen, err := receiveVarint(data)
if err != nil {
return nil, nil, err
}
// Read verifyToken bytes
verifyToken = make([]byte, verifyTokenLen)
_, err = data.Read(verifyToken)
if err != nil {
return nil, nil, err
}
return sharedSecret, verifyToken, nil
}
func getClientInformation(data *bytes.Buffer) (locale string, viewDistance int8, chatMode int32, chatColors bool, skinParts uint8, isRightHanded bool, textFiltering bool, serverListing bool, err error) {
var offset int32
// Decode locale string
locale, err = receiveString(data)
if err != nil {
return
}
// Decode viewDistance
viewDistance = int8(data.Bytes()[offset])
offset++
// Decode chatMode
chatMode, err = receiveVarint(data)
if err != nil {
return
}
// Decode chatColors
chatColors, err = receiveBool(data)
if err != nil {
return
}
// Decode skinParts
skinParts = data.Bytes()[offset]
offset++
// Decode isRightHanded
mainHand, err := receiveVarint(data)
if err != nil {
return
}
isRightHanded = mainHand == 1
// Decode textFiltering
textFiltering, err = receiveBool(data)
if err != nil {
return
}
// Decode serverListing
serverListing, err = receiveBool(data)
if err != nil {
return
}
return
}
func getPluginPacket(inData *bytes.Buffer) (key string, data *bytes.Buffer, err error) {
key, err = receiveString(inData)
data = inData
return
}
func (player *Player) getResourcePackPacket(data *bytes.Buffer) (err error) {
packUUID, err := receiveUUID(data)
if err != nil {
return
}
result, err := receiveVarint(data)
player.resourcePackResponse[packUUID] = result
return
}
func (player *Player) getDataPacksPacket(data *bytes.Buffer) (err error) {
packCount, err := receiveVarint(data)
if err != nil {
return err
}
for i := 0; i < int(packCount); i++ {
namespace, err := receiveString(data)
if err != nil {
return err
}
id, err := receiveString(data)
if err != nil {
return err
}
version, err := receiveString(data)
if err != nil {
return err
}
player.knownPacks = append(player.knownPacks, DatapackInfo{
Namespace: namespace,
ID: id,
Version: version,
})
}
return
}
func (player *Player) sendPongPacket(payload int64) (err error) {
var buf bytes.Buffer
addInt64(&buf, payload)
err = player.sendPacket(0x01, &buf)
if err != nil {
return
}
return
}
func (player *Player) sendDisconnect(reason string) (err error) {
var buf bytes.Buffer
addString(&buf, reason)
switch player.state {
case 2: //login
err = player.sendPacket(0x00, &buf)
case 3: //configuration
err = player.sendPacket(0x02, &buf)
case 4: //play
err = player.sendPacket(0x1D, &buf)
}
return
}
func (player *Player) sendEncryptionRequest() error {
var buf bytes.Buffer
// Send an empty server ID
addVarint(&buf, int32(len(serverID))) // VarInt length of server ID (0)
if len(serverID) > 0 {
buf.Write(serverID)
}
// Continue with the rest of the packet (public key, token, etc.)
serverPubKeyBytes, err := ExportRSAPublicKey(serverPublicKey)
if err != nil {
return err
}
verifyToken, err := generateRandomBytes(4)
if err != nil {
return err
}
player.verifyToken = verifyToken
addVarint(&buf, int32(len(serverPubKeyBytes)))
buf.Write(serverPubKeyBytes)
addVarint(&buf, int32(len(player.verifyToken)))
buf.Write(player.verifyToken)
// Indicate that encryption is enabled
addBool(&buf, true)
// Send the packet with ID 0x01 (encryption request)
err = player.sendPacket(0x01, &buf)
if err != nil {
return err
}
return nil
}
func (player *Player) sendLoginSuccess() error {
var buf bytes.Buffer
addUUID(&buf, player.uuid)
addString(&buf, player.name)
addVarint(&buf, int32(len(player.profile.Properties)))
for _, property := range player.profile.Properties {
addString(&buf, property.Name)
addString(&buf, property.Value)
isSigned := len(property.Signature) > 0
addBool(&buf, isSigned)
if isSigned {
addString(&buf, property.Signature)
}
}
addBool(&buf, true)
return player.sendPacket(0x02, &buf)
}
func (player *Player) sendKeepAlive() error {
player.keepAlivePayload = rand.Int63()
player.keepAliveSentTimestamp = time.Now().Unix()
var buf bytes.Buffer
addInt64(&buf, player.keepAlivePayload)
switch player.state {
case 3:
return player.sendPacket(0x04, &buf)
case 4:
return player.sendPacket(0x26, &buf)
}
return errors.New("invalid keep alive state")
}
func (player *Player) sendKeepAlives() {
for {
err := player.sendKeepAlive()
if err != nil {
break
}
time.Sleep(18 * time.Second)
}
}
func (player *Player) sendPingPacket() error {
player.pingID = rand.Uint32()
var buf bytes.Buffer
addUint32(&buf, player.pingID)
player.pingSentTimestamp = time.Now().Unix()
switch player.state {
case 3:
return player.sendPacket(0x05, &buf)
case 4:
return player.sendPacket(0x36, &buf)
}
return errors.New("invalid ping state")
}
func (player *Player) getPongPacket(data *bytes.Buffer) (err error) {
pingID, err := receiveUint32(data)
if pingID == player.pingID {
player.pingReceivedTimestamp = time.Now().Unix()
}
return
}
func (player *Player) sendResetChat() error {
return player.sendPacket(0x06, &bytes.Buffer{})
}
func (player *Player) sendRegistryData() (err error) {
for _, registryData := range world.RegistryDataThings {
var buf bytes.Buffer
addString(&buf, registryData.RegistryID)
addVarint(&buf, int32(len(registryData.Entries)))
for _, entry := range registryData.Entries {
addString(&buf, entry.EntryID)
addBool(&buf, entry.HasNBT)
if entry.HasNBT {
buf.ReadFrom(entry.NBTData)
}
}
err = player.sendPacket(0x07, &buf)
if err != nil {
return
}
}
return nil
}
func (player *Player) removeResourcePack(packUUID uuid.UUID) (err error) {
var buf bytes.Buffer
hasUUID := packUUID != uuid.Nil
addBool(&buf, hasUUID)
if hasUUID {
addUUID(&buf, packUUID)
}
return player.sendPacket(0x08, &buf)
}
func (player *Player) addResourcePack(packUUID uuid.UUID, url string, hash string, forced bool, message string) (err error) {
var buf bytes.Buffer
addUUID(&buf, packUUID)
addString(&buf, url)
addString(&buf, hash)
addBool(&buf, forced)
hasMessage := len(message) > 0
addBool(&buf, hasMessage)
if hasMessage {
addString(&buf, message)
}
return player.sendPacket(0x09, &buf)
}
func (player *Player) sendTransfer(host string, port uint16) (err error) {
var buf bytes.Buffer
addString(&buf, host)
addVarint(&buf, int32(port))
switch player.state {
case 3:
return player.sendPacket(0x0B, &buf)
case 4:
return player.sendPacket(0x73, &buf)
}
return nil
}
func (player *Player) sendFeatureFlags() error {
var buf bytes.Buffer
addVarint(&buf, int32(len(world.FeatureFlags)))
for _, featureFlag := range world.FeatureFlags {
addString(&buf, featureFlag)
}
return player.sendPacket(0x0C, &buf)
}
func (player *Player) sendUpdateTags() error {
var buf bytes.Buffer
addVarint(&buf, int32(len(world.UpdateTags)))
for _, updateTag := range world.UpdateTags {
addString(&buf, updateTag.TagRegistryIdentifier)
addVarint(&buf, int32(len(updateTag.Tags)))
for _, tag := range updateTag.Tags {
addString(&buf, tag.TagName)
addVarint(&buf, int32(len(tag.Entries)))
for _, entry := range tag.Entries {
addVarint(&buf, entry)
}
}
}
return player.sendPacket(0x0D, &buf)
}
func (player *Player) sendDataPacks() error {
var buf bytes.Buffer
addVarint(&buf, int32(len(world.DataPacks)))
for _, dataPack := range world.DataPacks {
addString(&buf, dataPack.Namespace)
addString(&buf, dataPack.ID)
addString(&buf, dataPack.Version)
}
return player.sendPacket(0x0E, &buf)
}
func (player *Player) sendReportDetails() (err error) {
var buf bytes.Buffer
addVarint(&buf, int32(len(world.ReportDetails)))
for key, value := range world.ReportDetails {
addString(&buf, key)
addString(&buf, value)
}
return player.sendPacket(0x0F, &buf)
}
func (player *Player) sendServerLinks() error {
var buf bytes.Buffer
addVarint(&buf, int32(len(world.ServerLinks)))
for _, serverLink := range world.ServerLinks {
if len(serverLink.LabelString) > 0 {
addBool(&buf, false)
addString(&buf, serverLink.LabelString)
} else {
addBool(&buf, true)
addVarint(&buf, serverLink.LabelInt)
}
addString(&buf, serverLink.URL)
}
return player.sendPacket(0x10, &buf)
}
func (player *Player) sendPluginMessage(channel string, data *bytes.Buffer) (err error) {
var buf bytes.Buffer
addString(&buf, channel)
_, err = buf.ReadFrom(data)
if err != nil {
return err
}
switch player.state {
case 3:
return player.sendPacket(0x01, &buf)
case 4:
return player.sendPacket(0x19, &buf)
}
return errors.New("unknown player state")
}
func (player *Player) sendConfigurationFinish() error {
return player.sendPacket(0x03, &bytes.Buffer{})
}
func (player *Player) sendCompression() error {
var buf bytes.Buffer
addVarint(&buf, CompressionThreshold)
return player.sendPacket(0x03, &buf)
}
func (player *Player) sendStatusResponse(description string) (err error) {
if player.state == 1 {
var playerSamples []PlayerSample
for _, player := range world.Players {
if !player.serverListing || player.uuid == uuid.Nil {
continue
}
playerSamples = append(playerSamples, PlayerSample{
Name: player.name,
ID: player.uuid.String(),
})
}
status := ServerStatus{
Version: Version{
Name: VERSION,
Protocol: ProtocolVersion,
},
Players: Players{
Max: maxPlayers,
Online: len(world.Players) - 1,
Sample: playerSamples,
},
Description: Description{description},
Favicon: icon,
EnforcesSecureChat: false,
}
jsonData, err := json.Marshal(status)
if err != nil {
return err
}
var outBuffer bytes.Buffer
addString(&outBuffer, string(jsonData))
err = player.sendPacket(0x00, &outBuffer)
if err != nil {
return err
}
}
return nil
}

716
structs.go Normal file

@ -0,0 +1,716 @@
package main
import (
"bufio"
"bytes"
"crypto/cipher"
"encoding/binary"
"github.com/google/uuid"
"math"
"net"
)
type Version struct {
Name string `json:"name"`
Protocol int `json:"protocol"`
}
type PlayerSample struct {
Name string `json:"name"`
ID string `json:"id"`
}
type Players struct {
Max int32 `json:"max"`
Online int `json:"online"`
Sample []PlayerSample `json:"sample"`
}
type Description struct {
Text string `json:"text"`
}
type ServerStatus struct {
Version Version `json:"version"`
Players Players `json:"players,omitempty"`
Description Description `json:"description,omitempty"`
Favicon string `json:"favicon,omitempty"`
EnforcesSecureChat bool `json:"enforcesSecureChat"`
}
type Player struct {
conn net.Conn
state int32
version int32
requestedAddress string
requestedPort uint16
compressionThreshold int32
verifyToken []byte
decryptStream cipher.Stream
encryptStream cipher.Stream
reader *bufio.Reader
profile MojangProfile
transferred bool
resourcePackResponse map[uuid.UUID]int32
knownPacks []DatapackInfo
Entity *Entity
Dimension *Dimension
DeathDimension *Dimension
DeathPosition BlockPosition
Position EntityPosition
GameMode uint8
PreviousGameMode int8
locale string
viewDistance int8
chatMode int32
chatColors bool
skinParts uint8
isRightHanded bool
textFiltering bool
serverListing bool
keepAlivePayload int64
keepAliveSentTimestamp int64
keepAliveReceivedTimestamp int64
pingID uint32
pingSentTimestamp int64
pingReceivedTimestamp int64
statistics []StatisticPiece
lastContainerStateID int32
name string
uuid uuid.UUID
}
type Dimension struct {
ID int32
Name string
}
type World struct {
Difficulty uint8
DifficultyLocked bool
Hardcore bool
Dimensions []Dimension
ViewDistance int32
SimulationDistance int32
ReducedDebugInfo bool
EnableRespawnScreen bool
DoLimitedCrafting bool
Seed uuid.UUID
Debug bool
Flat bool
EnforceSecureChat bool
Entities []Entity
Players []Player
RegistryDataThings []RegistryData
FeatureFlags []string
UpdateTags []UpdateTag
DataPacks []DatapackInfo
ReportDetails map[string]string
ServerLinks []ServerLink
Maps map[int32]Map
}
type StatisticPiece struct {
CategoryID int32
StatisticID int32
Value int32
}
type EntityPosition struct {
X float64
Y float64
Z float64
}
type FloatOffset struct {
X float32
Y float32
Z float32
}
type BlockPosition struct {
X int32
Y int16
Z int32
}
type AffectedBlockRecord struct {
OffsetX int8
OffsetY int8
OffsetZ int8
}
func (affectedBlockRecord *AffectedBlockRecord) add(buf *bytes.Buffer) {
addByte(buf, byte(affectedBlockRecord.OffsetX))
addByte(buf, byte(affectedBlockRecord.OffsetY))
addByte(buf, byte(affectedBlockRecord.OffsetZ))
}
type ExplosionSound struct {
Name string
FixedRange float32
}
func (explosionSound *ExplosionSound) add(buf *bytes.Buffer) {
addString(buf, explosionSound.Name)
hasFixedRange := explosionSound.FixedRange > 0
addBool(buf, hasFixedRange)
if hasFixedRange {
addFloat32(buf, explosionSound.FixedRange)
}
}
type Particle struct {
ParticleID int32
BlockState int32 // Used by block-related particles
FromColor [3]float32 // Used for color particles (RGB)
ToColor [3]float32 // Used for dust_color_transition particles (RGB)
Scale float32 // Used for dust/dust_color_transition particles
PositionSourceType int32 // Used for vibration particles
BlockPosition BlockPosition // Used for vibration particles (minecraft:block)
EntityID int32 // Used for vibration particles (minecraft:entity)
EntityEyeHeight float32 // Used for vibration particles (minecraft:entity)
Ticks int32 // Used for vibration particles
Delay int32 // Used for shriek particles
Count int32
LongDistance bool
Position EntityPosition
Offset FloatOffset
MaxSpeed float32
}
type WorldBorder struct {
X float64
Z float64
OldDiameter float64
NewDiameter float64
Speed int64
PortalTeleportBoundary int32
WarningBlocks int32
WarningTime int32
}
// Compute yaw between two positions
func calculateYaw(attackerPos, playerPos EntityPosition) float32 {
deltaX := attackerPos.X - playerPos.X
deltaZ := attackerPos.Z - playerPos.Z
// atan2 returns the angle in radians, we convert it to degrees
yaw := math.Atan2(deltaZ, deltaX) * (180 / math.Pi)
// In Minecraft, yaw 0 points towards positive Z, so we adjust the yaw to this system
yaw = -yaw + 90
// Normalize yaw to be between 0 and 360 degrees
if yaw < 0 {
yaw += 360
}
return float32(yaw)
}
func encodeARGB(color [3]float32) int32 {
// Encodes RGB color values into an ARGB int (alpha assumed to be 255)
r := int32(color[0] * 255)
g := int32(color[1] * 255)
b := int32(color[2] * 255)
a := int32(255)
return (a << 24) | (r << 16) | (g << 8) | b
}
func clampScale(scale float32) float32 {
// Clamps the scale between 0.01 and 4.0
if scale < 0.01 {
return 0.01
}
if scale > 4.0 {
return 4.0
}
return scale
}
func (p *Particle) addPacket(buf *bytes.Buffer, count int32) {
addBool(buf, p.LongDistance)
p.Position.add(buf)
p.Offset.add(buf)
addFloat32(buf, p.MaxSpeed)
addInt32(buf, count)
addInt32(buf, p.ParticleID)
// Handle based on particle ID
switch p.ParticleID {
case 1, 2, 28, 105: // minecraft:block, minecraft:block_marker, falling_dust, dust_pillar
addVarint(buf, p.BlockState)
case 13: // minecraft:dust
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, clampScale(p.Scale))
case 14: // minecraft:dust_color_transition
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, p.ToColor[0])
addFloat32(buf, p.ToColor[1])
addFloat32(buf, p.ToColor[2])
addFloat32(buf, clampScale(p.Scale))
case 20: // minecraft:entity_effect
addInt32(buf, encodeARGB(p.FromColor))
case 45: // minecraft:vibration
addVarint(buf, p.PositionSourceType)
if p.PositionSourceType == 0 {
p.BlockPosition.add(buf)
} else if p.PositionSourceType == 1 {
addVarint(buf, p.EntityID)
addFloat32(buf, p.EntityEyeHeight)
}
addVarint(buf, p.Ticks)
case 99: // minecraft:shriek
addVarint(buf, p.Delay)
// Particles with no additional data
case 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 100, 101, 102, 103, 104, 106, 107, 108:
// No additional data required
}
}
func (p *Particle) addExplosion(buf *bytes.Buffer) {
addInt32(buf, p.ParticleID)
// Handle based on particle ID
switch p.ParticleID {
case 1, 2, 28, 105: // minecraft:block, minecraft:block_marker, falling_dust, dust_pillar
addVarint(buf, p.BlockState)
case 13: // minecraft:dust
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, clampScale(p.Scale))
case 14: // minecraft:dust_color_transition
addFloat32(buf, p.FromColor[0])
addFloat32(buf, p.FromColor[1])
addFloat32(buf, p.FromColor[2])
addFloat32(buf, p.ToColor[0])
addFloat32(buf, p.ToColor[1])
addFloat32(buf, p.ToColor[2])
addFloat32(buf, clampScale(p.Scale))
case 20: // minecraft:entity_effect
addInt32(buf, encodeARGB(p.FromColor))
case 45: // minecraft:vibration
addVarint(buf, p.PositionSourceType)
if p.PositionSourceType == 0 {
p.BlockPosition.add(buf)
} else if p.PositionSourceType == 1 {
addVarint(buf, p.EntityID)
addFloat32(buf, p.EntityEyeHeight)
}
addVarint(buf, p.Ticks)
case 99: // minecraft:shriek
addVarint(buf, p.Delay)
// Particles with no additional data
case 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 100, 101, 102, 103, 104, 106, 107, 108:
// No additional data required
}
}
type Explosion struct {
Position EntityPosition
Strength float32
AffectedBlockRecords []AffectedBlockRecord
PlayerMotion EntityPosition
BlockInteraction int32
SmallExplosionParticle Particle
LargeExplosionParticle Particle
ExplosionSound ExplosionSound
}
type Slot struct {
Count int32
ItemID int32
ComponentsToAdd map[int32]any
ComponentsToRemove []int32
}
func (slot *Slot) add(buf *bytes.Buffer) {
addVarint(buf, slot.Count)
if slot.Count > 0 {
addVarint(buf, slot.ItemID)
addVarint(buf, int32(len(slot.ComponentsToAdd)))
addVarint(buf, int32(len(slot.ComponentsToRemove)))
for componentID, component := range slot.ComponentsToAdd {
addVarint(buf, componentID)
component = component //TODO IMPLEMENT ALL THESE STUPID COMPONENTS
}
for _, componentID := range slot.ComponentsToRemove {
addVarint(buf, componentID)
}
}
}
// PalettedContainer represents a palette-based storage of entries.
type PalettedContainer struct {
BitsPerEntry uint8
Palette []int32
DataArray []uint64 // Packed data for blocks or biomes
}
// AddEntry adds an entry (block or biome) to the palette and updates the DataArray.
func (p *PalettedContainer) AddEntry(entry int32, index int) {
// Check if the entry already exists in the palette.
for i, val := range p.Palette {
if val == entry {
// Entry already exists, no need to addExplosion to palette.
p.setEntryAtIndex(i, index)
return
}
}
// Add the new entry to the palette.
p.Palette = append(p.Palette, entry)
// Use the index of the new entry in the palette and store it in the DataArray.
p.setEntryAtIndex(len(p.Palette)-1, index)
}
// setEntryAtIndex sets the palette index for the block/biome at the specified index in the DataArray.
func (p *PalettedContainer) setEntryAtIndex(paletteIndex int, index int) {
// Each entry is packed into the DataArray with the specified BitsPerEntry.
entriesPerLong := 64 / p.BitsPerEntry
longIndex := index / int(entriesPerLong)
bitIndex := (index % int(entriesPerLong)) * int(p.BitsPerEntry)
// Clear the bits at the specified location before setting the new value.
mask := uint64(math.MaxUint64>>(64-p.BitsPerEntry)) << bitIndex
p.DataArray[longIndex] &= ^mask
// Set the new value.
p.DataArray[longIndex] |= (uint64(paletteIndex) & (math.MaxUint64 >> (64 - p.BitsPerEntry))) << bitIndex
}
// add serializes the PalettedContainer to a buffer for network transmission.
func (p *PalettedContainer) add(buf *bytes.Buffer) error {
// Write BitsPerEntry
buf.WriteByte(p.BitsPerEntry)
// Write Palette
if len(p.Palette) > 0 {
// Write the length of the palette as a VarInt.
addVarint(buf, int32(len(p.Palette)))
// Write each palette entry as a VarInt.
for _, entry := range p.Palette {
addVarint(buf, entry)
}
} else {
// Write a palette length of 0 for a global palette.
addVarint(buf, 0)
}
// Write DataArray
// First, write the length of the DataArray (in terms of the number of longs).
addVarint(buf, int32(len(p.DataArray)))
// Write each uint64 in the DataArray as a little-endian long.
for _, data := range p.DataArray {
err := binary.Write(buf, binary.BigEndian, data)
if err != nil {
return err
}
}
return nil
}
// ChunkSection represents a 16x16x16 section of a chunk.
type ChunkSection struct {
BlockCount uint16 // Non-air block count
BlockStates PalettedContainer // Block palette and states
Biomes PalettedContainer // Biome palette and data
}
// add serializes the ChunkSection into the buffer.
func (cs *ChunkSection) add(buf *bytes.Buffer) error {
err := binary.Write(buf, binary.BigEndian, cs.BlockCount)
if err != nil {
return err
}
// Write BlockStates and Biomes.
err = cs.BlockStates.add(buf)
if err != nil {
return err
}
err = cs.Biomes.add(buf)
if err != nil {
return err
}
return nil
}
// Chunk represents a chunk column, containing multiple chunk sections.
type Chunk struct {
X int32 // Chunk X coordinate
Z int32 // Chunk Z coordinate
Sections []ChunkSection // Chunk sections (16x16x16)
}
// add serializes the Chunk into the buffer.
func (c *Chunk) add(buf *bytes.Buffer) error {
err := binary.Write(buf, binary.BigEndian, c.X)
if err != nil {
return err
}
err = binary.Write(buf, binary.BigEndian, c.Z)
if err != nil {
return err
}
// Write the number of sections as a VarInt.
addVarint(buf, int32(len(c.Sections)))
// Write each chunk section.
for _, section := range c.Sections {
err := section.add(buf)
if err != nil {
return err
}
}
return nil
}
type Block struct {
Position BlockPosition
BlockEntityType int32
BlockTypeID int32
BlockStateID int32
BlockEntityData map[string]any
DestroyStage uint8
}
type BossBar struct {
UUID uuid.UUID
Title TextComponent
Health float32
Color int32
Division int32
Flags uint8
}
func (bossBar BossBar) add(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 0)
addTextComponent(&buf, bossBar.Title)
addFloat32(&buf, bossBar.Health)
addVarint(&buf, bossBar.Color)
addVarint(&buf, bossBar.Division)
addByte(&buf, bossBar.Flags)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) remove(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 1)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateHealth(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 2)
addFloat32(&buf, bossBar.Health)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateTitle(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 3)
addTextComponent(&buf, bossBar.Title)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateStyle(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 4)
addVarint(&buf, bossBar.Color)
addVarint(&buf, bossBar.Division)
return player.sendPacket(0x0A, &buf)
}
func (bossBar BossBar) updateFlags(player *Player) error {
var buf bytes.Buffer
addUUID(&buf, bossBar.UUID)
addVarint(&buf, 5)
addByte(&buf, bossBar.Flags)
return player.sendPacket(0x0A, &buf)
}
func (blockPosition *BlockPosition) add(buf *bytes.Buffer) {
var xBuf, yBuf, zBuf []byte
binary.BigEndian.PutUint32(xBuf, uint32(blockPosition.X))
binary.BigEndian.PutUint16(yBuf, uint16(blockPosition.Y))
binary.BigEndian.PutUint32(zBuf, uint32(blockPosition.Z))
buf.Write(xBuf[0:26])
buf.Write(zBuf[0:26])
buf.Write(yBuf[0:12])
}
func (entityPosition *EntityPosition) add(buf *bytes.Buffer) {
addFloat64(buf, entityPosition.X)
addFloat64(buf, entityPosition.Y)
addFloat64(buf, entityPosition.Z)
}
func (floatOffset *FloatOffset) add(buf *bytes.Buffer) {
addFloat32(buf, floatOffset.X)
addFloat32(buf, floatOffset.Y)
addFloat32(buf, floatOffset.Z)
}
type EntityRotation struct {
Pitch uint8
Yaw uint8
HeadYaw uint8
}
func (entityRotation *EntityRotation) add(buf *bytes.Buffer) {
addByte(buf, entityRotation.Pitch)
addByte(buf, entityRotation.Yaw)
addByte(buf, entityRotation.HeadYaw)
}
type Velocity struct {
X float32
Y float32
Z float32
}
func (velocity *Velocity) add(buf *bytes.Buffer) {
addFloat32(buf, velocity.X)
addFloat32(buf, velocity.Y)
addFloat32(buf, velocity.Z)
}
type Entity struct {
ID int32
UUID uuid.UUID
Type int32
Position EntityPosition
Rotation EntityRotation
Data int32
Velocity Velocity
Animation uint8
status int8
PortalCooldown int32
}
func (entity *Entity) add(buf *bytes.Buffer) {
addVarint(buf, entity.ID)
addUUID(buf, entity.UUID)
addVarint(buf, entity.Type)
entity.Position.add(buf)
entity.Rotation.add(buf)
addVarint(buf, entity.Data)
entity.Velocity.add(buf)
}
type XPOrb struct {
ID int32
Position EntityPosition
Count int16
}
func (XPOrb *XPOrb) add(buf *bytes.Buffer) {
addVarint(buf, XPOrb.ID)
XPOrb.Position.add(buf)
addInt16(buf, XPOrb.Count)
}
type MapIcon struct {
Type int32
X int8
Z int8
Direction int8
DisplayName *TextComponent
}
func (MapIcon *MapIcon) add(buf *bytes.Buffer) {
addVarint(buf, MapIcon.Type)
addByte(buf, byte(MapIcon.X))
addByte(buf, byte(MapIcon.Z))
addByte(buf, byte(MapIcon.Direction))
hasTextComponent := MapIcon.DisplayName != nil
addBool(buf, hasTextComponent)
if hasTextComponent {
addTextComponent(buf, *MapIcon.DisplayName)
}
}
type Map struct {
ID int32
Scale int8
Locked bool
Icons []MapIcon
MapUpdateDataW uint8
MapUpdateDataH uint8
MapUpdateDataX uint8
MapUpdateDataZ uint8
MapUpdateData []uint8
}
func (Map *Map) add(buf *bytes.Buffer) {
addVarint(buf, Map.ID)
addByte(buf, byte(Map.Scale))
addBool(buf, Map.Locked)
iconCount := int32(len(Map.Icons))
addBool(buf, iconCount > 0)
if iconCount > 0 {
addVarint(buf, iconCount)
for _, mapIcon := range Map.Icons {
mapIcon.add(buf)
}
}
addByte(buf, Map.MapUpdateDataH)
if Map.MapUpdateDataH > 0 {
addByte(buf, Map.MapUpdateDataW)
addByte(buf, Map.MapUpdateDataX)
addByte(buf, Map.MapUpdateDataZ)
addVarint(buf, int32(len(Map.MapUpdateData)))
for _, mapUpdatePiece := range Map.MapUpdateData {
addByte(buf, mapUpdatePiece)
}
}
}