- Add minecraft-data for version 1.8 (protocol 47) - Generate proto_generated/v1_8 from 1.8 protocol.json - Fix generator compatibility: missing CompareTo natives (i16, i32, u16, u32, i64, string, nbt, optionalNbt), parameterized type placeholder handling () - Add raw-to-tmcpr subcommand: converts raw tcpflow captures to TMCPR with zlib decompression and state-aware compression threshold tracking - Extend print-replay: 1.8 state transitions (success->Play, login_start->Play, handshake/status initial states, set_protocol NextState detection) - Comment out broken raw-capture subcommand (use tcpflow instead) - Fix nbt.Anon Encode/Decode on value receivers
218 lines
5.7 KiB
Go
218 lines
5.7 KiB
Go
package print_replay
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
_ "embed"
|
|
"encoding/binary"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"go/format"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/admin-else/strom/mc/data"
|
|
"github.com/admin-else/strom/mc/proto"
|
|
"github.com/admin-else/strom/mc/proto_base"
|
|
)
|
|
|
|
var (
|
|
cmd = flag.NewFlagSet("print-replay", flag.ContinueOnError)
|
|
FileFlag = cmd.String("file", "", "The tmcpr file to print")
|
|
ProtocolVersion = cmd.Int("protocol", -1, "The protocol version")
|
|
CreateTestsPath = cmd.String("create-tests", "", "")
|
|
StateFlag = cmd.String("state", "login", "Initial state: handshake, login, config, status, play")
|
|
DirectionFlag = cmd.String("direction", "clientbound", "Packet direction: clientbound (ToClient) or serverbound (ToServer)")
|
|
)
|
|
|
|
// TODO: maybe use the proto replay api
|
|
|
|
//go:embed failed_packet.go.tmpl
|
|
var TestSrcF string
|
|
|
|
func SaveUnCodeAbleAsTest(packet *proto.UnCodablePacket) (err error) {
|
|
if packet.Info.Type == nil {
|
|
slog.Warn("skipping test for unknown packet id", "packet_id", packet.Info.PacketId, "state", packet.Info.State, "error", packet.Err)
|
|
return
|
|
}
|
|
|
|
hUntrimmed := sha256.Sum256(packet.Data)
|
|
h := hUntrimmed[:8]
|
|
if err = os.MkdirAll(*CreateTestsPath, 0755); err != nil {
|
|
return
|
|
}
|
|
|
|
ret, err := data.LookUpVersionByProtocolVersion(packet.Info.ProtocolVersion)
|
|
if err != nil {
|
|
return
|
|
}
|
|
goPackage := "v" + strings.ReplaceAll(ret.MinecraftVersion, ".", "_")
|
|
typeName := reflect.TypeOf(packet.Info.Type).Elem().Name()
|
|
|
|
src := fmt.Sprintf(TestSrcF, goPackage, packet.Err, h, typeName, packet.Data)
|
|
formatted, formatErr := format.Source([]byte(src))
|
|
if formatErr != nil {
|
|
formatted = []byte(src)
|
|
}
|
|
|
|
filePath := fmt.Sprintf(*CreateTestsPath+"/%v_%10x_test.go", len(packet.Data), h)
|
|
if err = os.WriteFile(filePath, formatted, 0644); err != nil {
|
|
return
|
|
}
|
|
slog.Info("Packet failed to parse", "error", packet.Err, "saved", filePath, "data", fmt.Sprintf("%q", packet.Data))
|
|
return
|
|
}
|
|
|
|
var state = proto_base.Login
|
|
var direction = proto_base.ToClient
|
|
|
|
func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, i proto_base.PacketInfo, err error) {
|
|
var timestamp, length uint32
|
|
err = binary.Read(r, binary.BigEndian, ×tamp)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = binary.Read(r, binary.BigEndian, &length)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Read the packet data into a byte slice
|
|
packetData := make([]byte, length)
|
|
_, err = io.ReadFull(r, packetData)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Use bytes.Reader instead of bytes.Buffer
|
|
b := bytes.NewReader(packetData)
|
|
|
|
packetId, err := proto_base.DecodeVarInt(b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Calculate position after reading the packet ID
|
|
// indexafterid represents how many bytes we've read so far
|
|
indexafterid := int(b.Size()) - b.Len()
|
|
|
|
var ok bool
|
|
i, ok = proto.LookUpTypeByPacketInfo(direction, state, packetId, int32(*ProtocolVersion))
|
|
if !ok {
|
|
packet = &proto.UnCodablePacket{Err: proto.BadPacketIdErr, Data: packetData, Info: i}
|
|
return
|
|
}
|
|
packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble)
|
|
err = packet.Decode(b)
|
|
if err != nil {
|
|
packet = &proto.UnCodablePacket{Err: err, Data: packetData[indexafterid:], Info: i}
|
|
return
|
|
}
|
|
if b.Len() != 0 {
|
|
packet = &proto.UnCodablePacket{Err: proto.PacketNotFullyDecodedErr, Data: packetData[indexafterid:], Info: i}
|
|
return
|
|
}
|
|
trackState(packet, i)
|
|
return
|
|
}
|
|
|
|
func trackState(packet proto_base.EncodeDecodeAble, i proto_base.PacketInfo) {
|
|
switch i.Name {
|
|
case "set_protocol":
|
|
v := reflect.ValueOf(packet).Elem()
|
|
nsField := v.FieldByName("NextState")
|
|
if nsField.IsValid() && nsField.Kind() == reflect.Int32 {
|
|
switch nsField.Int() {
|
|
case 1:
|
|
state = proto_base.Status
|
|
case 2:
|
|
state = proto_base.Login
|
|
}
|
|
}
|
|
case "login_start":
|
|
if int32(*ProtocolVersion) <= 47 {
|
|
state = proto_base.Play
|
|
}
|
|
case "success":
|
|
if int32(*ProtocolVersion) >= 764 {
|
|
state = proto_base.Configuration
|
|
} else {
|
|
state = proto_base.Play
|
|
}
|
|
case "finish_configuration":
|
|
state = proto_base.Play
|
|
case "login_acknowledged":
|
|
state = proto_base.Configuration
|
|
}
|
|
}
|
|
|
|
func Run(args []string) (err error) {
|
|
err = cmd.Parse(args)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if *ProtocolVersion == -1 {
|
|
return fmt.Errorf("no protocol version specified")
|
|
}
|
|
if *FileFlag == "" {
|
|
return fmt.Errorf("no file specified")
|
|
}
|
|
switch *StateFlag {
|
|
case "handshake":
|
|
state = proto_base.Handshaking
|
|
case "login":
|
|
state = proto_base.Login
|
|
case "config":
|
|
state = proto_base.Configuration
|
|
case "play":
|
|
state = proto_base.Play
|
|
case "status":
|
|
state = proto_base.Status
|
|
default:
|
|
return fmt.Errorf("unknown state: %s", *StateFlag)
|
|
}
|
|
switch *DirectionFlag {
|
|
case "clientbound", "to-client":
|
|
direction = proto_base.ToClient
|
|
case "serverbound", "to-server":
|
|
direction = proto_base.ToServer
|
|
default:
|
|
return fmt.Errorf("unknown direction: %s", *DirectionFlag)
|
|
}
|
|
f, err := os.Open(*FileFlag)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
var totalPackets, failedPackets int
|
|
for {
|
|
var packet proto_base.EncodeDecodeAble
|
|
var info proto_base.PacketInfo
|
|
packet, info, err = UnpackPacket(f)
|
|
unCodablePacket, ok := packet.(*proto.UnCodablePacket)
|
|
if ok {
|
|
slog.Error("failed to decode packet", "error", err, "packet_name", info.Name)
|
|
if *CreateTestsPath != "" {
|
|
err = SaveUnCodeAbleAsTest(unCodablePacket)
|
|
if err != nil {
|
|
slog.Error("failed to save uncodable packet as test", "error", err)
|
|
}
|
|
}
|
|
failedPackets++
|
|
}
|
|
|
|
fmt.Printf("Packet: %#v\n", packet)
|
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
|
break
|
|
}
|
|
totalPackets++
|
|
}
|
|
fmt.Printf("Total packets: %d, failed packets: %d, %%failed: %.2f%%\n", totalPackets, failedPackets, float64(failedPackets)/float64(totalPackets)*100)
|
|
return
|
|
}
|