diff --git a/cmd/simpleServer/main.go b/cmd/simpleServer/main.go new file mode 100644 index 0000000..e3daffb --- /dev/null +++ b/cmd/simpleServer/main.go @@ -0,0 +1,153 @@ +// Example minecraft 1.15.2 server +package main + +import ( + "github.com/Tnze/go-mc/bot" + "github.com/Tnze/go-mc/data" + "github.com/Tnze/go-mc/net" + pk "github.com/Tnze/go-mc/net/packet" + "github.com/google/uuid" + "log" +) + +const ProtocolVersion = 578 +const Threshold = 256 +const MaxPlayer = 200 + +func main() { + l, err := net.ListenMC(":25565") + if err != nil { + log.Fatalf("Listen error: %v", err) + } + + for { + conn, err := l.Accept() + if err != nil { + log.Fatalf("Accept error: %v", err) + } + go acceptConn(conn) + } +} + +func acceptConn(conn net.Conn) { + defer conn.Close() + // handshake + protocol, intention, err := handshake(conn) + if err != nil { + log.Printf("Handshake error: %v", err) + return + } + + switch intention { + default: //unknown error + log.Printf("Unknown handshake intention: %v", intention) + case 1: //for status + acceptListPing(conn) + case 2: //for login + handlePlaying(conn, protocol) + } +} + +func handlePlaying(conn net.Conn, protocol int32) { + // login, get player info + info, err := acceptLogin(conn) + if err != nil { + log.Print("Login failed") + return + } + + // Write LoginSuccess packet + err = loginSuccess(conn, info.Name, info.UUID) + if err != nil { + log.Print("Login failed on success") + return + } + + joinGame(conn) + conn.WritePacket(pk.Marshal(data.PlayerPositionAndLookClientbound, + // https://wiki.vg/Protocol#Player_Position_And_Look_.28clientbound.29 + pk.Double(0), pk.Double(0), pk.Double(0), // XYZ + pk.Float(0), pk.Float(0), // Yaw Pitch + pk.Byte(0), // flag + pk.VarInt(0), // TP ID + )) + // Just for block this goroutine. Keep the connection + for { + if _, err := conn.ReadPacket(); err != nil { + log.Printf("ReadPacket error: %v", err) + break + } + // KeepAlive packet is not handled, so client might + // exit because of "time out". + } +} + +type PlayerInfo struct { + Name string + UUID uuid.UUID + OPLevel int +} + +// acceptLogin check player's account +func acceptLogin(conn net.Conn) (info PlayerInfo, err error) { + //login start + var p pk.Packet + p, err = conn.ReadPacket() + if err != nil { + return + } + + err = p.Scan((*pk.String)(&info.Name)) //decode username as pk.String + if err != nil { + return + } + + //auth + const OnlineMode = false + if OnlineMode { + log.Panic("Not Implement") + } else { + // offline-mode UUID + info.UUID = bot.OfflineUUID(info.Name) + } + + return +} + +// handshake receive and parse Handshake packet +func handshake(conn net.Conn) (protocol, intention int32, err error) { + var ( + Protocol, Intention pk.VarInt + ServerAddress pk.String // ignored + ServerPort pk.UnsignedShort // ignored + ) + // receive handshake packet + p, err := conn.ReadPacket() + if err != nil { + return 0, 0, err + } + err = p.Scan(&Protocol, &ServerAddress, &ServerPort, &Intention) + return int32(Protocol), int32(Intention), err +} + +// loginSuccess send LoginSuccess packet to client +func loginSuccess(conn net.Conn, name string, uuid uuid.UUID) error { + return conn.WritePacket(pk.Marshal(0x02, + pk.String(uuid.String()), //uuid as string with hyphens + pk.String(name), + )) +} + +func joinGame(conn net.Conn) error { + return conn.WritePacket(pk.Marshal(data.JoinGame, + pk.Int(0), // EntityID + pk.UnsignedByte(1), // Gamemode + pk.Int(0), // Dimension + pk.Long(0), // HashedSeed + pk.UnsignedByte(MaxPlayer), // MaxPlayer + pk.String("default"), // LevelType + pk.VarInt(15), // View Distance + pk.Boolean(false), // Reduced Debug Info + pk.Boolean(true), // Enable respawn screen + )) +} diff --git a/cmd/simpleServer/status.go b/cmd/simpleServer/status.go new file mode 100644 index 0000000..9cb2831 --- /dev/null +++ b/cmd/simpleServer/status.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/json" + "github.com/Tnze/go-mc/chat" + "github.com/Tnze/go-mc/net" + pk "github.com/Tnze/go-mc/net/packet" + "github.com/google/uuid" + "log" +) + +func acceptListPing(conn net.Conn) { + for i := 0; i < 2; i++ { // ping or list. Only accept twice + p, err := conn.ReadPacket() + if err != nil { + return + } + + switch p.ID { + case 0x00: //List + err = conn.WritePacket(pk.Marshal(0x00, pk.String(listResp()))) + case 0x01: //Ping + err = conn.WritePacket(p) + } + if err != nil { + return + } + } +} + +type player struct { + Name string `json:"name"` + ID uuid.UUID `json:"id"` +} + +// listResp return server status as JSON string +func listResp() string { + var list struct { + Version struct { + Name string `json:"name"` + Protocol int `json:"protocol"` + } `json:"version"` + Players struct { + Max int `json:"max"` + Online int `json:"online"` + Sample []player `json:"sample"` + } `json:"players"` + Description chat.Message `json:"description"` + FavIcon string `json:"favicon,omitempty"` + } + + list.Version.Name = "Chat Server" + list.Version.Protocol = ProtocolVersion + list.Players.Max = MaxPlayer + list.Players.Online = 123 + list.Players.Sample = []player{} // must init. can't be nil + list.Description = chat.Message{Text: "Powered by go-mc", Color: "blue"} + + data, err := json.Marshal(list) + if err != nil { + log.Panic("Marshal JSON for status checking fail") + } + return string(data) +}