Files
mc-router/mcproto/write.go
T
2025-12-20 13:31:34 -06:00

149 lines
3.3 KiB
Go

package mcproto
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"io"
"unicode/utf16"
)
// WriteVarInt writes a VarInt (Minecraft format) to w
func WriteVarInt(w io.Writer, value int32) error {
var buf [5]byte
i := 0
v := uint32(value)
for {
temp := byte(v & 0x7F)
v >>= 7
if v != 0 {
temp |= 0x80
}
buf[i] = temp
i++
if v == 0 {
break
}
}
_, err := w.Write(buf[:i])
return err
}
// WriteString writes a Minecraft length-prefixed string
func WriteString(w io.Writer, s string) error {
if err := WriteVarInt(w, int32(len(s))); err != nil {
return err
}
_, err := io.WriteString(w, s)
return err
}
// buildPacket builds a framed packet: [length VarInt][packetId VarInt][payload]
func buildPacket(packetID int32, payload []byte) []byte {
var b bytes.Buffer
_ = WriteVarInt(&b, packetID)
b.Write(payload)
var framed bytes.Buffer
_ = WriteVarInt(&framed, int32(b.Len()))
framed.Write(b.Bytes())
return framed.Bytes()
}
// WriteStatusJSONPacket writes a Status Response (packet 0x00) with the provided JSON string
func WriteStatusJSONPacket(w io.Writer, jsonString string) error {
// payload is the JSON as a Minecraft string
var payload bytes.Buffer
if err := WriteString(&payload, jsonString); err != nil {
return err
}
pkt := buildPacket(PacketIdStatusResponse, payload.Bytes())
_, err := w.Write(pkt)
return err
}
// WriteStatusFromStruct writes a Status Response from a struct
func WriteStatusFromStruct(w io.Writer, status StatusResponse) error {
b, err := json.Marshal(status)
if err != nil {
return err
}
return WriteStatusJSONPacket(w, string(b))
}
// WritePongPacket writes Pong (packet 0x01) with the same payload
func WritePongPacket(w io.Writer, timestamp int64) error {
var pl bytes.Buffer
// payload is a signed long (64-bit)
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], uint64(timestamp))
pl.Write(buf[:])
pkt := buildPacket(PackedIdPongResponse, pl.Bytes())
_, err := w.Write(pkt)
return err
}
// WriteLegacySLPResponse writes the 1.6-compatible legacy response packet (0xFF)
// Format: FF, [length short], UTF16BE string beginning with "\u00A7\u0031\u0000" then null-delimited fields
// fields: protocol, version, motd, online, max
func WriteLegacySLPResponse(w io.Writer, protocol int, version string, motd string, online int, max int) error {
// Build the string with null separators
s := "\u00A7\u0031\u0000" +
intToString(protocol) + "\u0000" +
version + "\u0000" +
motd + "\u0000" +
intToString(online) + "\u0000" +
intToString(max)
// Encode UTF-16BE
runes := []rune(s)
encoded := utf16.Encode(runes)
var be bytes.Buffer
for _, v := range encoded {
var tmp [2]byte
binary.BigEndian.PutUint16(tmp[:], v)
be.Write(tmp[:])
}
bw := bufio.NewWriter(w)
// 0xFF
if _, err := bw.Write([]byte{0xFF}); err != nil {
return err
}
// length short in code units
var lenBuf [2]byte
binary.BigEndian.PutUint16(lenBuf[:], uint16(len(encoded)))
if _, err := bw.Write(lenBuf[:]); err != nil {
return err
}
if _, err := bw.Write(be.Bytes()); err != nil {
return err
}
return bw.Flush()
}
// helpers
func intToString(i int) string {
if i == 0 {
return "0"
}
neg := false
if i < 0 {
neg = true
i = -i
}
var buf [20]byte
pos := len(buf)
for i > 0 {
pos--
buf[pos] = byte('0' + (i % 10))
i /= 10
}
if neg {
pos--
buf[pos] = '-'
}
return string(buf[pos:])
}