From c516cc5283ada976df5b113e883009ee7c0451a6 Mon Sep 17 00:00:00 2001 From: adman Date: Mon, 9 Mar 2026 21:30:31 +0100 Subject: [PATCH] version compat system start --- client/client.go | 17 ++++-- client/handshake_packet.go | 26 +++++++++ client/login.go | 26 +++------ client/status.go | 58 ++++++++++++++------ level/packet.go | 31 ++++------- proto/connection.go | 105 ++++++++++++++++++++++++++++--------- proto/packet_lookup.go | 29 +++++----- tools/status/main.go | 20 +++++++ 8 files changed, 213 insertions(+), 99 deletions(-) create mode 100644 client/handshake_packet.go create mode 100644 tools/status/main.go diff --git a/client/client.go b/client/client.go index 0fc4a5c..56f0b90 100644 --- a/client/client.go +++ b/client/client.go @@ -7,13 +7,24 @@ import ( "github.com/admin-else/strom/proto_base" ) -// Connect connects to the given address note that it does not resolve the SRV records its just a raw net.Dial +// Connect connects to the given address note that it does not resolve the SRV records it's just a raw net.Dial func Connect(addr string) (ret *proto.Conn, err error) { - ret = &proto.Conn{} - err = ret.SetVersion("1.21.8") + s, err := Status(addr) if err != nil { return } + ret, err = ConnectVersionLess(addr) + if err != nil { + return + } + err = ret.SetProtocolVersion(s.Version.Protocol) + return +} + +// ConnectVersionLess connects to the given address without setting the version you should set it +// when using this function. +func ConnectVersionLess(addr string) (ret *proto.Conn, err error) { + ret = &proto.Conn{} ret.SetState(proto_base.Handshaking) ret.SetCompressionThreshold(-1) ret.Actor = proto_base.Client diff --git a/client/handshake_packet.go b/client/handshake_packet.go new file mode 100644 index 0000000..ee266a4 --- /dev/null +++ b/client/handshake_packet.go @@ -0,0 +1,26 @@ +package client + +import ( + "net" + "strconv" + + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/proto_base" + "github.com/admin-else/strom/proto_generated/v1_21_8" +) + +func MakeHandshakePacket(s *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) { + p = &v1_21_8.HandshakingToServerPacketSetProtocol{NextState: int32(nextState), ProtocolVersion: s.ProtocolVersion} + var portStr string + p.ServerHost, portStr, err = net.SplitHostPort(s.RemoteAddr().String()) + if err != nil { + return + } + var portUint uint64 + portUint, err = strconv.ParseUint(portStr, 10, 16) + if err != nil { + return + } + p.ServerPort = uint16(portUint) + return +} diff --git a/client/login.go b/client/login.go index 642dc20..8881fb3 100644 --- a/client/login.go +++ b/client/login.go @@ -7,8 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "net" - "strconv" "github.com/admin-else/strom/api" "github.com/admin-else/strom/crypto" @@ -51,26 +49,16 @@ func (s *LoginClient) OnClose(_ event.Close) (err error) { } func (s *LoginClient) OnStart(_ event.Start) (err error) { + p, err := MakeHandshakePacket(s.Conn, proto_base.Login) + if err != nil { + return + } if s.ServerHost == "" && s.ServerPort == 0 { - var portStr string - s.ServerHost, portStr, err = net.SplitHostPort(s.RemoteAddr().String()) - if err != nil { - return - } - var portUint uint64 - portUint, err = strconv.ParseUint(portStr, 10, 16) - if err != nil { - return - } - s.ServerPort = uint16(portUint) + p.ServerHost = s.ServerHost + p.ServerPort = s.ServerPort } - err = s.Send(&v1_21_8.HandshakingToServerPacketSetProtocol{ - ProtocolVersion: s.ProtocolVersion, - ServerHost: s.ServerHost, - ServerPort: s.ServerPort, - NextState: int32(proto_base.Login), - }) + err = s.Send(p) if err != nil { return } diff --git a/client/status.go b/client/status.go index 272e721..9b43d23 100644 --- a/client/status.go +++ b/client/status.go @@ -1,21 +1,22 @@ package client import ( + "encoding/json" "fmt" - "strconv" - "strings" + "time" "github.com/admin-else/strom/event" "github.com/admin-else/strom/proto" "github.com/admin-else/strom/proto_base" + "github.com/admin-else/strom/server" - "github.com/admin-else/strom/data" "github.com/admin-else/strom/proto_generated/v1_21_8" ) type StatusClient struct { *proto.Conn - Status string + Status string + PingSendTime, PingReceiveTime time.Time } func (s *StatusClient) Default(event any) (err error) { @@ -24,21 +25,11 @@ func (s *StatusClient) Default(event any) (err error) { } func (s *StatusClient) OnStart(_ event.Start) (err error) { - parts := strings.Split(s.RemoteAddr().String(), ":") - versionData, err := data.LookUpVersionByName(s.Version) + p, err := MakeHandshakePacket(s.Conn, proto_base.Status) if err != nil { return } - port, err := strconv.Atoi(parts[1]) - if err != nil { - return - } - err = s.Send(&v1_21_8.HandshakingToServerPacketSetProtocol{ - ProtocolVersion: versionData.Version, - ServerHost: parts[0], - ServerPort: uint16(port), - NextState: int32(proto_base.Status), - }) + err = s.Send(p) if err != nil { return } @@ -47,8 +38,41 @@ func (s *StatusClient) OnStart(_ event.Start) (err error) { return } -func (s *StatusClient) OnStatus(p v1_21_8.StatusToClientPacketServerInfo) (err error) { +func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) (err error) { s.Status = p.Response + s.PingSendTime = time.Now() + err = s.Send(&v1_21_8.StatusToServerPacketPing{Time: s.PingSendTime.UnixMilli()}) + return +} + +func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) { + s.PingReceiveTime = time.Now() err = event.HandlerDoneErr{} return } + +func StatusRaw(addr string) (s *StatusClient, err error) { + c, err := ConnectVersionLess(addr) + if err != nil { + return + } + err = c.SetVersion("1.21.8") + if err != nil { + return + } + s = &StatusClient{ + Conn: c, + } + err = s.Start(s) + defer c.Close() + return +} + +func Status(addr string) (status server.StatusResponse, err error) { + s, err := StatusRaw(addr) + if err != nil { + return + } + err = json.Unmarshal([]byte(s.Status), &status) + return +} diff --git a/level/packet.go b/level/packet.go index b3b0e4b..8159977 100644 --- a/level/packet.go +++ b/level/packet.go @@ -76,71 +76,62 @@ func LongsToData(data []uint64, n, bpe int32) (ret []int32) { return } -func UnpackBlockData(r io.Reader) (blocks [BlocksPerChunkSection]int32, err error) { +func UnpackBlockData(r io.Reader) (blocks []int32, err error) { var bitsPerEntry uint8 err = binary.Read(r, binary.BigEndian, &bitsPerEntry) if err != nil { return } - var blocksSlice []int32 fmt.Println("blocks bpe", bitsPerEntry) switch bitsPerEntry { case 0: - blocksSlice, err = UnpackSingleValuePalette(r, BlocksPerChunkSection) + blocks, err = UnpackSingleValuePalette(r, BlocksPerChunkSection) case 1, 2, 3, 4: - blocksSlice, err = UnpackArrayPalette(r, bitsPerEntry, BlocksPerChunkSection) + blocks, err = UnpackArrayPalette(r, bitsPerEntry, BlocksPerChunkSection) case 5, 6, 7, 8: err = errors.New("unimplemented") default: - blocksSlice, err = UnpackLongData(r, bitsPerEntry, BlocksPerChunkSection) + blocks, err = UnpackLongData(r, bitsPerEntry, BlocksPerChunkSection) } if err != nil { return } - blocks = [BlocksPerChunkSection]int32(blocksSlice) return } -func UnpackBiomeData(r io.Reader) (biomes [BiomesPerChunkSection]int32, err error) { +func UnpackBiomeData(r io.Reader) (biomes []int32, err error) { // net.minecraft.world.chunk.PaletteProvider#forBiomes var bitsPerEntry uint8 err = binary.Read(r, binary.BigEndian, &bitsPerEntry) if err != nil { return } - var biomesSlice []int32 fmt.Println("biome bpe", bitsPerEntry) switch bitsPerEntry { case 0: - biomesSlice, err = UnpackSingleValuePalette(r, BiomesPerChunkSection) + biomes, err = UnpackSingleValuePalette(r, BiomesPerChunkSection) case 1, 2, 3: - biomesSlice, err = UnpackArrayPalette(r, bitsPerEntry, BiomesPerChunkSection) + biomes, err = UnpackArrayPalette(r, bitsPerEntry, BiomesPerChunkSection) default: - biomesSlice, err = UnpackLongData(r, bitsPerEntry, BiomesPerChunkSection) + biomes, err = UnpackLongData(r, bitsPerEntry, BiomesPerChunkSection) } if err != nil { return } - biomes = [BiomesPerChunkSection]int32(biomesSlice) return } -type ChunkSection struct { - BlockData [BlocksPerChunkSection]int32 - BiomeData [BiomesPerChunkSection]int32 -} - -func UnpackSection(r io.Reader) (s ChunkSection, err error) { +func UnpackSection(r io.Reader) (blocks, biomes []int32, err error) { var blockCount int16 err = binary.Read(r, binary.BigEndian, &blockCount) if err != nil { return } - s.BlockData, err = UnpackBlockData(r) + blocks, err = UnpackBlockData(r) if err != nil { return } - s.BiomeData, err = UnpackBiomeData(r) + biomes, err = UnpackBiomeData(r) if err != nil { return } diff --git a/proto/connection.go b/proto/connection.go index 9167383..1c26be3 100644 --- a/proto/connection.go +++ b/proto/connection.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log/slog" + "reflect" "sync" "github.com/admin-else/strom/data" @@ -13,10 +14,16 @@ import ( "github.com/admin-else/strom/proto_base" ) -var BadPacketTypeError = errors.New("bad packet type") -var BadPacketIdError = errors.New("bad packet id") -var PacketNotFullyDecodedError = errors.New("packet not fully decoded") -var ErrContextDone = errors.New("context done") +var ( + BadPacketTypeErr = errors.New("bad packet type") + BadPacketIdErr = errors.New("bad packet id") + PacketNotFullyDecodedErr = errors.New("packet not fully decoded") + ContextDoneErr = errors.New("context done") + WrongStateErr = errors.New("wrong state") + WrongDirectionErr = errors.New("wrong direction") + CantTranslateErr = errors.New("cant translate packet") + PacketDoesntExistInFutureVersionErr = errors.New("packet doesnt exist in future version") +) // UnCodablePacket represents a packet that could not be decoded. type UnCodablePacket struct { @@ -77,42 +84,92 @@ func (c *Conn) State() proto_base.State { } func (c *Conn) SetVersion(version string) (err error) { - c.Version = version - versionData, err := data.LookUpVersionByName(c.Version) + versionData, err := data.LookUpVersionByName(version) if err != nil { return } c.ProtocolVersion = versionData.Version + c.Version = version return } +func (c *Conn) SetProtocolVersion(version int32) (err error) { + versionData, err := data.LookUpVersionByProtocolVersion(version) + if err != nil { + return + } + c.ProtocolVersion = versionData.Version + c.Version = versionData.MinecraftVersion + return +} + +// ActivateReceiveRoutine activates the receive-routine. +// It ensures that the receive-routine is only started once and creates a buffered channel for packet handling. func (c *Conn) ActivateReceiveRoutine() { + if c.receiveRoutine { + return + } c.receiveRoutine = true c.packetCh = make(chan proto_base.EncodeDecodeAble, 100) // i dunno how many to buffer 100 seems reasonable go c.ReceiveJob() +} +func (c *Conn) translatePacketVersion(packet proto_base.EncodeDecodeAble, packetInfo proto_base.PacketInfo) (convertedPacket proto_base.EncodeDecodeAble, err error) { + packetInfoReal, found := LookupPacketInfoByNameProtocolVersionAndState(packetInfo.Name, c.ProtocolVersion, c.State()) + if !found { + err = PacketDoesntExistInFutureVersionErr + return + } + dstType := reflect.TypeOf(packetInfoReal.Type) + if !reflect.TypeOf(packetInfo.Type).ConvertibleTo(dstType) { + err = CantTranslateErr + return + } + // can't fail cause pacetinfo.type is proto_base.EncodeDecodeAble + convertedPacket = reflect.ValueOf(packet).Convert(dstType).Interface().(proto_base.EncodeDecodeAble) + + return +} + +func (c *Conn) sendRegisteredPacket(packet proto_base.EncodeDecodeAble) (err error) { + i, ok := LookupPacketInfoByTypeAndState(packet, c.State()) + if !ok { + err = BadPacketTypeErr + return + } + if i.Direction != c.Actor.SendDirection() { + err = WrongDirectionErr + return + } + if i.State != c.State() { + err = WrongStateErr + return + } + if i.ProtocolVersion != c.ProtocolVersion { + packet, err = c.translatePacketVersion(packet, i) + if err != nil { + return + } + } + var packetBuff = bytes.NewBuffer(nil) + err = proto_base.EncodeVarInt(packetBuff, i.PacketId) + if err != nil { + return + } + err = packet.Encode(packetBuff) + if err != nil { + return + } + slog.Debug("send", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet)) + return c.SendRaw(packetBuff.Bytes()) } func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) { - slog.Debug("send", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet)) switch packet := packet.(type) { case *UnCodablePacket: err = c.SendRaw(packet.Data) default: - i, ok := LookupPacketInfoByTypeLookupPacketInfoByTypeAndMore(packet, c.State()) - if !ok { - err = BadPacketTypeError - } - var packetBuff = bytes.NewBuffer(nil) - err = proto_base.EncodeVarInt(packetBuff, i.PacketId) - if err != nil { - return - } - err = packet.Encode(packetBuff) - if err != nil { - return - } - err = c.SendRaw(packetBuff.Bytes()) + return c.sendRegisteredPacket(packet) } return } @@ -129,7 +186,7 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) { } packet, ok := LookUpTypeByPacketInfoAndCopyType(c.Actor.ReceiveDirection(), c.State(), id, c.ProtocolVersion) if !ok { - packet = &UnCodablePacket{Err: BadPacketIdError, Data: packetBytes, Direction: c.Actor.ReceiveDirection()} + packet = &UnCodablePacket{Err: BadPacketIdErr, Data: packetBytes, Direction: c.Actor.ReceiveDirection()} return } err = packet.Decode(b) @@ -140,7 +197,7 @@ func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) { } if b.Len() != 0 { err = nil - packet = &UnCodablePacket{Err: PacketNotFullyDecodedError, Data: packetBytes, Direction: c.Actor.ReceiveDirection()} + packet = &UnCodablePacket{Err: PacketNotFullyDecodedErr, Data: packetBytes, Direction: c.Actor.ReceiveDirection()} return } return @@ -154,7 +211,7 @@ func (c *Conn) OnTick(_ event.Tick) (err error) { slog.Debug("receive", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet)) err = c.Loop.Fire(packet) case <-c.Ctx.Done(): - err = ErrContextDone + err = ContextDoneErr return default: } diff --git a/proto/packet_lookup.go b/proto/packet_lookup.go index eb55c38..6a03d50 100644 --- a/proto/packet_lookup.go +++ b/proto/packet_lookup.go @@ -7,7 +7,7 @@ import ( "github.com/admin-else/strom/proto_generated" ) -// I know this is in-performant and i dont care until it becomes a problem for somebody it will stay like this and it is pretty easy to make caches or simular to speed this up so yeah +// I know this is in-performant and i dont care until it becomes a problem for somebody it will stay like this and it is pretty easy to make caches or similar to speed this up so yeah func LookUpTypeByPacketInfo(direction proto_base.Direction, state proto_base.State, pid, version int32) (p proto_base.PacketInfo, ok bool) { for _, pin := range proto_generated.Packets { @@ -50,22 +50,19 @@ func LookupPacketInfoByType(packet proto_base.EncodeDecodeAble) (p proto_base.Pa return } -func LookupPacketInfoByTypeLookupPacketInfoByTypeAndMore(packet proto_base.EncodeDecodeAble, state proto_base.State) (p proto_base.PacketInfo, ok bool) { - switch packet := packet.(type) { - case *UnCodablePacket: - p = proto_base.PacketInfo{ - Type: &UnCodablePacket{}, - Name: "undecodeable", - Direction: packet.Direction, - State: -1, - PacketId: -1, - ProtocolVersion: -1, +func LookupPacketInfoByTypeAndState(packet proto_base.EncodeDecodeAble, state proto_base.State) (p proto_base.PacketInfo, ok bool) { + for _, p := range proto_generated.Packets { + if state == p.State && reflect.TypeOf(p.Type) == reflect.TypeOf(packet) { + return p, true } - default: - for _, p := range proto_generated.Packets { - if state == p.State && reflect.TypeOf(p.Type) == reflect.TypeOf(packet) { - return p, true - } + } + return +} + +func LookupPacketInfoByNameProtocolVersionAndState(name string, version int32, state proto_base.State) (p proto_base.PacketInfo, ok bool) { + for _, p := range proto_generated.Packets { + if state == p.State && p.Name == name && p.ProtocolVersion == version { + return p, true } } return diff --git a/tools/status/main.go b/tools/status/main.go new file mode 100644 index 0000000..61d86d9 --- /dev/null +++ b/tools/status/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/admin-else/strom/client" +) + +var addrFlag = flag.String("addr", "localhost:25565", "address to connect to") + +func main() { + flag.Parse() + status, err := client.StatusRaw(*addrFlag) + if err != nil { + panic(err) + } + fmt.Println(status.Status) + fmt.Printf("%v ping", status.PingReceiveTime.Sub(status.PingSendTime)) +}