From bb868d2d0d4d5a16d798d01d4a2ca8573ddc8a60 Mon Sep 17 00:00:00 2001 From: raven Date: Sat, 21 Mar 2026 20:17:53 -0500 Subject: support CPE --- classic/cpe.go | 17 ++++++++ classic/packets.go | 19 +++++---- server/player.go | 11 +++++- server/server.go | 112 +++++++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 classic/cpe.go diff --git a/classic/cpe.go b/classic/cpe.go new file mode 100644 index 0000000..222ec74 --- /dev/null +++ b/classic/cpe.go @@ -0,0 +1,17 @@ +package classic + +type ExtInfo struct { + AppName String + ExtensionCount int16 +} +func (e *ExtInfo) PacketId() byte { + return 0x10 +} + +type ExtEntry struct { + ExtName String + Version int32 +} +func (e *ExtEntry) PacketId() byte { + return 0x11 +} diff --git a/classic/packets.go b/classic/packets.go index c1f650e..48ea80d 100644 --- a/classic/packets.go +++ b/classic/packets.go @@ -21,6 +21,10 @@ const ( NonOpUser = 0x00 OpUser = 0x64 ) +const ( + NoCpe = 0x00 + UseCpe = 0x42 +) type PlayerId struct { Version byte @@ -161,7 +165,7 @@ func (p *UpdateUserType) PacketId() byte { return 0x0f } -func createPacketType(packetId byte, client bool) Packet { +func createPacketType(packetId byte, client bool, ext map[string]bool) Packet { switch packetId { case 0x00: if client { @@ -188,7 +192,8 @@ func createPacketType(packetId byte, client bool) Packet { } } -func readPacket(rd io.Reader, client bool) (Packet, error) { +func readPacket( + rd io.Reader, client bool, ext map[string]bool) (Packet, error) { var b [1]byte _, err := io.ReadFull(rd, b[:]) if err != nil { @@ -196,7 +201,7 @@ func readPacket(rd io.Reader, client bool) (Packet, error) { } packetId := b[0] - packet := createPacketType(packetId, client) + packet := createPacketType(packetId, client, ext) if packet == nil { return nil, fmt.Errorf("Unknown packet type: 0x%02x", packetId) } @@ -207,12 +212,12 @@ func readPacket(rd io.Reader, client bool) (Packet, error) { return packet, nil } -func SReadPacket(rd io.Reader) (Packet, error) { - return readPacket(rd, false) +func SReadPacket(rd io.Reader, ext map[string]bool) (Packet, error) { + return readPacket(rd, false, ext) } -func CReadPacket(rd io.Reader) (Packet, error) { - return readPacket(rd, true) +func CReadPacket(rd io.Reader, ext map[string]bool) (Packet, error) { + return readPacket(rd, true, ext) } func WritePacket(wr io.Writer, packet Packet) error { diff --git a/server/player.go b/server/player.go index 9904bbe..66dcc88 100644 --- a/server/player.go +++ b/server/player.go @@ -15,6 +15,7 @@ type player struct { client *client server *Server name string + extensions map[string]bool level *level levelLoaded bool } @@ -51,8 +52,14 @@ func savePlayerData( } } -func newPlayer(s *Server, cl *client, name string) *player { - pl := &player {client: cl, server: s, name: name} +func newPlayer( + s *Server, cl *client, name string, ext map[string]bool) *player { + pl := &player { + client: cl, + server: s, + name: name, + extensions: ext, + } loadPlayerData(pl, name, func(state playerState, ok bool) { if ok { pl.state = state diff --git a/server/server.go b/server/server.go index 522cd17..369d5b1 100644 --- a/server/server.go +++ b/server/server.go @@ -6,12 +6,20 @@ import ( "log" "fmt" "time" + "strconv" + "strings" "os/signal" "git.citrons.xyz/metronode/classic" "git.citrons.xyz/metronode/phony" ) var SoftwareName = "Metronode" +var supportedExtensions = []string { + "ExtEntityPositions", +} +var requiredExtensions = []string { + "ExtEntityPositions", +} type ServerInfo struct { Name string @@ -227,8 +235,9 @@ func (s *Server) newLevel(info levelInfo) (levelId, *level) { return s.LastId, l } -func (s *Server) newPlayer(cl *client, name string) *player { - pl := newPlayer(s, cl, name) +func (s *Server) newPlayer( + cl *client, name string, ext map[string]bool) *player { + pl := newPlayer(s, cl, name, ext) s.players[name] = pl return pl } @@ -280,7 +289,8 @@ func (s *Server) kick(playerName string, reason string) bool { } func (s *Server) NewPlayer( - from phony.Actor, cl *client, name string, reply func(*player)) { + from phony.Actor, cl *client, name string, ext map[string]bool, + reply func(*player)) { s.Act(from, func() { banReason, ok := s.worldState.Banned[name] if ok { @@ -293,12 +303,12 @@ func (s *Server) NewPlayer( s.players[name].Act(s, func() { s.players[name].kick("Replaced by new connection") s.Act(s.players[name], func() { - s.newPlayer(cl, name) + s.newPlayer(cl, name, ext) s.GetPlayer(from, name, reply) }) }) } else { - s.newPlayer(cl, name) + s.newPlayer(cl, name, ext) s.GetPlayer(from, name, reply) } }) @@ -347,6 +357,7 @@ type client struct { conn net.Conn username string player *player + extensions map[string]bool } func newClient(server *Server, srvInfo ServerInfo, conn net.Conn) *client { @@ -364,7 +375,9 @@ func (cl *client) performHandshake(conn net.Conn, srvInfo ServerInfo) { cl.conn = conn conn.SetDeadline(time.Now().Add(10 * time.Second)) - packet, err := classic.SReadPacket(conn) + var ext = make(map[string]bool) + + packet, err := classic.SReadPacket(conn, ext) if cl.handleError(err) != nil { return } @@ -377,6 +390,11 @@ func (cl *client) performHandshake(conn net.Conn, srvInfo ServerInfo) { ) } cl.username = classic.UnpadString(pid.Username) + if pid.Ext == classic.UseCpe { + if !cl.cpeHandshake(conn, ext) { + return + } + } default: cl.disconnect("Expected handshake") return @@ -385,6 +403,7 @@ func (cl *client) performHandshake(conn net.Conn, srvInfo ServerInfo) { cl.disconnect("Invalid player name") return } + err = classic.WritePacket(conn, &classic.ServerId { Version: 7, ServerName: classic.PadString(srvInfo.Name), @@ -393,16 +412,93 @@ func (cl *client) performHandshake(conn net.Conn, srvInfo ServerInfo) { if cl.handleError(err) != nil { return } - cl.server.NewPlayer(cl, cl, cl.username, func(pl *player) { + cl.server.NewPlayer(cl, cl, cl.username, ext, func(pl *player) { cl.player = pl }) conn.SetDeadline(time.Time{}) } +func (cl *client) cpeHandshake(conn net.Conn, ext map[string]bool) bool { + var (packet classic.Packet; err error) + err = classic.WritePacket(conn, &classic.ExtInfo { + AppName: classic.PadString(SoftwareName), + ExtensionCount: int16(len(supportedExtensions)), + }) + if cl.handleError(err) != nil { + return false + } + for _, extString := range supportedExtensions { + var (name string; version = 1) + split := strings.Split(extString, ".") + name = split[0] + if len(split) > 1 { + version, err = strconv.Atoi(split[1]) + if err != nil { + panic(err) + } + } + err = classic.WritePacket(conn, &classic.ExtEntry { + ExtName: classic.PadString(name), + Version: int32(version), + }) + if cl.handleError(err) != nil { + return false + } + } + packet, err = classic.SReadPacket(conn, ext) + if cl.handleError(err) != nil { + return false + } + var count int + switch info := packet.(type) { + case *classic.ExtInfo: + log.Printf( + "%s is connecting via '%s' client with %d extensions", + cl.username, info.AppName, info.ExtensionCount, + ) + count = int(info.ExtensionCount) + default: + cl.disconnect("Expected ExtInfo") + return false + } + for i := 0; i < count; i++ { + var extString string + packet, err = classic.SReadPacket(conn, ext) + if cl.handleError(err) != nil { + return false + } + switch entry := packet.(type) { + case *classic.ExtEntry: + extString = classic.UnpadString(entry.ExtName) + if entry.Version != 1 { + extString += "." + strconv.Itoa(int(entry.Version)) + } + default: + cl.disconnect("Expected ExtEntry") + return false + } + ext[extString] = true + } + var extList []string + for extString := range ext { + extList = append(extList, extString) + } + log.Printf( + "%s has extensions: %s", cl.username, strings.Join(extList, ", "), + ) + for _, req := range requiredExtensions { + if !ext[req] { + cl.disconnect("Missing required extension: " + req) + } + } + cl.extensions = ext + return true +} + func (cl *client) readPackets(conn net.Conn) { for { - packet, err := classic.SReadPacket(conn) + packet, err := classic.SReadPacket(conn, cl.extensions) cl.Act(nil, func() { if cl.handleError(err) != nil { return -- cgit v1.2.3