diff --git a/cmd/strom/mca_to_tmcpr/main.go b/cmd/strom/mca_to_tmcpr/main.go index 94d04c4..39c296b 100644 --- a/cmd/strom/mca_to_tmcpr/main.go +++ b/cmd/strom/mca_to_tmcpr/main.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "os" + "path/filepath" "github.com/admin-else/strom/mc/level" "github.com/admin-else/strom/mc/level/anvil" @@ -51,7 +52,7 @@ func GetChunkFromMca(x, z int32, version string) (b []byte, err error) { z += anvil.ChunksWidthRegionFile } - f, err := os.Open(fmt.Sprintf("./.devres/anvil_testdata/securechattestworld/region/r.%v.%v.mca", xmca, zmca)) + f, err := os.Open(filepath.Join(*worldPath, "region", fmt.Sprintf("r.%v.%v.mca", xmca, zmca))) if err != nil { return } diff --git a/cmd/strom/raw_to_tmcpr/main.go b/cmd/strom/raw_to_tmcpr/main.go index bf0aaa9..6e4a63f 100644 --- a/cmd/strom/raw_to_tmcpr/main.go +++ b/cmd/strom/raw_to_tmcpr/main.go @@ -20,8 +20,9 @@ var ClientboundIn = Cmd.String("clientbound", "", "raw tcpflow file for clientbo var ServerboundIn = Cmd.String("serverbound", "", "raw tcpflow file for serverbound (client->server) direction") var ClientboundOut = Cmd.String("clientbound-out", "clientbound.tmcpr", "output TMCPR file for clientbound") var ServerboundOut = Cmd.String("serverbound-out", "serverbound.tmcpr", "output TMCPR file for serverbound") -var ClientboundState = Cmd.String("clientbound-state", "login", "initial state for clientbound: handshake, login, status, play") -var ServerboundState = Cmd.String("serverbound-state", "handshake", "initial state for serverbound: handshake, login, status, play") +var ClientboundState = Cmd.String("clientbound-state", "login", "initial state for clientbound: handshake, login, config, status, play") +var ServerboundState = Cmd.String("serverbound-state", "handshake", "initial state for serverbound: handshake, login, config, status, play") +var UseConfig = Cmd.Bool("config", false, "use Configuration state transitions (for protocol >= 764, Minecraft 1.20.2+)") type captureReader struct { r io.Reader @@ -32,6 +33,7 @@ type captureReader struct { haveStartTime bool direction proto_base.Direction pendingThreshold int32 + useConfig bool } func (cr *captureReader) setStartTime() { @@ -123,13 +125,21 @@ func (cr *captureReader) trackState(packetBytes []byte) { cr.threshold = threshold } } else if packetId == 0x02 { - cr.state = proto_base.Play + if cr.useConfig { + cr.state = proto_base.Configuration + } else { + cr.state = proto_base.Play + } } } else if cr.direction == proto_base.ToServer && packetId == 0 { if cr.pendingThreshold >= 0 { cr.threshold = cr.pendingThreshold } } + case proto_base.Configuration: + if cr.direction == proto_base.ToClient && packetId == 0x02 { + cr.state = proto_base.Play + } case proto_base.Status: case proto_base.Play: } @@ -172,12 +182,14 @@ func Run(args []string) (err error) { return proto_base.Handshaking, nil case "login": return proto_base.Login, nil + case "config": + return proto_base.Configuration, nil case "play": return proto_base.Play, nil case "status": return proto_base.Status, nil default: - return 0, fmt.Errorf("unknown state: %s (valid: handshake, login, status, play)", name) + return 0, fmt.Errorf("unknown state: %s (valid: handshake, login, config, status, play)", name) } } @@ -188,7 +200,7 @@ func Run(args []string) (err error) { if err != nil { return err } - compressionThreshold, err = processFile(*ClientboundIn, *ClientboundOut, state, proto_base.ToClient, -1) + compressionThreshold, err = processFile(*ClientboundIn, *ClientboundOut, state, proto_base.ToClient, -1, *UseConfig) if err != nil { return fmt.Errorf("clientbound: %w", err) } @@ -199,7 +211,7 @@ func Run(args []string) (err error) { if err != nil { return err } - _, err = processFile(*ServerboundIn, *ServerboundOut, state, proto_base.ToServer, compressionThreshold) + _, err = processFile(*ServerboundIn, *ServerboundOut, state, proto_base.ToServer, compressionThreshold, *UseConfig) if err != nil { return fmt.Errorf("serverbound: %w", err) } @@ -208,7 +220,7 @@ func Run(args []string) (err error) { return } -func processFile(inPath, outPath string, initial proto_base.State, dir proto_base.Direction, initialThreshold int32) (finalThreshold int32, err error) { +func processFile(inPath, outPath string, initial proto_base.State, dir proto_base.Direction, initialThreshold int32, useConfig bool) (finalThreshold int32, err error) { in, err := os.Open(inPath) if err != nil { return initialThreshold, fmt.Errorf("open input: %w", err) @@ -228,6 +240,7 @@ func processFile(inPath, outPath string, initial proto_base.State, dir proto_bas threshold: -1, direction: dir, pendingThreshold: initialThreshold, + useConfig: useConfig, } if dir == proto_base.ToClient { cr.threshold = initialThreshold diff --git a/mc/proto/multiversion_test.go b/mc/proto/multiversion_test.go new file mode 100644 index 0000000..877d0b9 --- /dev/null +++ b/mc/proto/multiversion_test.go @@ -0,0 +1,160 @@ +package proto_test + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/admin-else/strom/mc/data" + "github.com/admin-else/strom/mc/proto" + "github.com/admin-else/strom/mc/proto_base" + "github.com/admin-else/strom/mc/proto_generated" +) + +func TestPacketRoundTripAllVersions(t *testing.T) { + results := map[string][4]int{} + for _, version := range proto_generated.SupportedVersions { + protoVer := data.MustLookupProtocolVersion(version) + total, encFailed, decFailed, ok := 0, 0, 0, 0 + for _, pin := range proto_generated.Packets { + if pin.ProtocolVersion != int32(protoVer) { + continue + } + total++ + b, err := proto.SimplePacketToBytes(pin.Type) + if err != nil { + if err == proto_base.ToDoError || strings.Contains(err.Error(), "unsupported") || strings.Contains(err.Error(), "index not found") || strings.Contains(err.Error(), "bad type") { + encFailed++ + continue + } + t.Errorf("%s/%s: encode error: %v", version, pin.Name, err) + continue + } + decoded, err := proto.SimpleBytesToPacket(b, int32(protoVer), pin.Direction, pin.State) + if err != nil { + t.Errorf("%s/%s: decode error: %v", version, pin.Name, err) + continue + } + if unc, ok := decoded.(*proto.UnCodablePacket); ok { + if unc.Err == proto_base.ToDoError { + decFailed++ + continue + } + t.Errorf("%s/%s: uncodable: %v", version, pin.Name, unc.Err) + continue + } + b2, err := proto.SimplePacketToBytes(decoded) + if err != nil { + t.Errorf("%s/%s: re-encode error: %v", version, pin.Name, err) + continue + } + if len(b) != len(b2) { + t.Errorf("%s/%s: byte length mismatch %d != %d", version, pin.Name, len(b), len(b2)) + } + ok++ + } + results[version] = [4]int{total, encFailed, decFailed, ok} + } + fmt.Println() + for _, version := range proto_generated.SupportedVersions { + r := results[version] + fmt.Printf("%-8s %3d pkt enc-skip:%-3d dec-skip:%-3d ok:%-3d\n", version, r[0], r[1], r[2], r[3]) + } +} + +func TestSmartConvertCheck(t *testing.T) { + panicCount := 0 + notConvertible := 0 + convertible := 0 + totalPairs := 0 + for _, fromV := range proto_generated.SupportedVersions { + for _, toV := range proto_generated.SupportedVersions { + if fromV == toV { + continue + } + fromVer := data.MustLookupProtocolVersion(fromV) + toVer := data.MustLookupProtocolVersion(toV) + for _, pinFrom := range proto_generated.Packets { + if pinFrom.ProtocolVersion != int32(fromVer) { + continue + } + pinTo, found := proto.LookupPacketInfoByNameProtocolVersionStateAndDirection( + pinFrom.Name, int32(toVer), pinFrom.State, pinFrom.Direction, + ) + if !found { + continue + } + totalPairs++ + func() { + defer func() { + if r := recover(); r != nil { + panicCount++ + } + }() + fromT := typeInfo(pinFrom.Type) + toT := typeInfo(pinTo.Type) + if proto.SmartConvertibleTo(fromT, toT) { + convertible++ + } else { + notConvertible++ + } + }() + } + } + } + fmt.Printf("smart convert: %d pairs, %d convertible, %d not, %d panics\n", totalPairs, convertible, notConvertible, panicCount) +} + +func typeInfo(t proto_base.EncodeDecodeAble) reflect.Type { + rt := reflect.TypeOf(t) + for rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + return rt +} + +func TestCrossVersionDecode(t *testing.T) { + errors := 0 + panics := 0 + total := 0 + for _, toV := range proto_generated.SupportedVersions { + toVer := data.MustLookupProtocolVersion(toV) + for _, pin := range proto_generated.Packets { + if pin.ProtocolVersion == int32(toVer) { + continue + } + _, found := proto.LookupPacketInfoByNameProtocolVersionStateAndDirection( + pin.Name, int32(toVer), pin.State, pin.Direction, + ) + if !found { + continue + } + b, err := proto.SimplePacketToBytes(pin.Type) + if err != nil { + continue + } + total++ + func() { + defer func() { + if r := recover(); r != nil { + panics++ + } + }() + decoded, err := proto.SimpleBytesToPacket(b, int32(toVer), pin.Direction, pin.State) + if err != nil { + errors++ + return + } + if _, ok := decoded.(*proto.UnCodablePacket); ok { + return + } + _, err = proto.SimplePacketToBytes(decoded) + if err != nil { + errors++ + } + }() + } + } + fmt.Printf("cross-version decode: %d attempts, %d errors, %d panics\n", total, errors, panics) +}