commit a9c89f027a551af0b67029478935fea320704829 Author: SantianDev Date: Tue Mar 3 15:05:55 2026 +0300 Initial commit diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..b56d434 --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ +@echo off +go mod tidy +go build -o frodding.exe ./cmd/frodding +echo Build complete: frodding.exe diff --git a/cmd/frodding/main.go b/cmd/frodding/main.go new file mode 100644 index 0000000..dafc665 --- /dev/null +++ b/cmd/frodding/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + frodding "github.com/cubexteam/Frodding" +) + +func main() { + wd, err := os.Getwd() + if err != nil { + wd = "." + } + configPath := filepath.Join(wd, "server.yml") + + srv, err := frodding.NewServer(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + if err := srv.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Fatal: %v\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2c521c3 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/cubexteam/Frodding + +go 1.21 + +require ( + github.com/google/uuid v1.6.0 + gopkg.in/yaml.v2 v2.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b0e7e64 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..3b0b4a2 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,56 @@ +package logger + +import ( + "fmt" + "os" + "time" +) + +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorYellow = "\033[33m" + colorCyan = "\033[36m" + colorGray = "\033[90m" + colorWhite = "\033[97m" +) + +type Logger struct { + debug bool +} + +func New(debug bool) *Logger { + return &Logger{debug: debug} +} + +func (l *Logger) timestamp() string { + return colorGray + time.Now().Format("15:04:05") + colorReset +} + +func (l *Logger) Infof(format string, args ...any) { + fmt.Fprintf(os.Stdout, "%s %sINFO%s %s\n", l.timestamp(), colorCyan, colorReset, fmt.Sprintf(format, args...)) +} + +func (l *Logger) Warnf(format string, args ...any) { + fmt.Fprintf(os.Stdout, "%s %sWARN%s %s\n", l.timestamp(), colorYellow, colorReset, fmt.Sprintf(format, args...)) +} + +func (l *Logger) Errorf(format string, args ...any) { + fmt.Fprintf(os.Stderr, "%s %sERROR%s %s\n", l.timestamp(), colorRed, colorReset, fmt.Sprintf(format, args...)) +} + +func (l *Logger) Debugf(format string, args ...any) { + if !l.debug { + return + } + fmt.Fprintf(os.Stdout, "%s %sDEBUG%s %s\n", l.timestamp(), colorGray, colorReset, fmt.Sprintf(format, args...)) +} + +func (l *Logger) Fatalf(format string, args ...any) { + fmt.Fprintf(os.Stderr, "%s %sFATAL%s %s\n", l.timestamp(), colorRed, colorReset, fmt.Sprintf(format, args...)) + os.Exit(1) +} + +func (l *Logger) Chat(name, message string) { + fmt.Fprintf(os.Stdout, "%s %sCHAT%s <%s> %s\n", l.timestamp(), colorWhite, colorReset, name, message) +} diff --git a/net/info/info.go b/net/info/info.go new file mode 100644 index 0000000..a19f873 --- /dev/null +++ b/net/info/info.go @@ -0,0 +1,81 @@ +package info + +const ( + LatestProtocol = 113 + MinProtocol = 105 + LatestGameVersion = "v1.1.7" + LatestGameVersionNetwork = "1.1.7" +) + +var SupportedProtocols = map[int32]string{ + 105: "1.1.0", + 107: "1.1.3", + 110: "1.1.5", + 111: "1.1.6", + 113: "1.1.7", +} + +func IsSupported(protocol int32) bool { + _, ok := SupportedProtocols[protocol] + return ok +} + +const ( + IDLogin = 0x01 + IDPlayStatus = 0x02 + IDServerHandshake = 0x03 + IDClientHandshake = 0x04 + IDDisconnect = 0x05 + IDResourcePackInfo = 0x06 + IDResourcePackStack = 0x07 + IDResourcePackClientResponse = 0x08 + IDText = 0x09 + IDSetTime = 0x0a + IDStartGame = 0x0b + IDAddPlayer = 0x0c + IDAddEntity = 0x0d + IDRemoveEntity = 0x0e + IDAddItemEntity = 0x0f + IDTakeItemEntity = 0x11 + IDMoveEntity = 0x12 + IDMovePlayer = 0x13 + IDPlayerAction = 0x24 + IDSetEntityData = 0x27 + IDSetEntityMotion = 0x28 + IDUpdateAttributes = 0x1d + IDInventoryTransaction = 0x1e + IDMobEquipment = 0x1f + IDMobArmorEquipment = 0x20 + IDInteract = 0x21 + IDBlockPickRequest = 0x22 + IDEntityPickRequest = 0x23 + IDAnimate = 0x2c + IDRespawn = 0x2d + IDContainerOpen = 0x2e + IDContainerClose = 0x2f + IDPlayerHotbar = 0x30 + IDInventoryContent = 0x31 + IDInventorySlot = 0x32 + IDContainerSetData = 0x33 + IDCraftingData = 0x34 + IDCraftingEvent = 0x35 + IDAdventureSettings = 0x37 + IDBlockEntityData = 0x38 + IDFullChunkData = 0x3a + IDSetGameMode = 0x3b + IDSetCommandsEnabled = 0x3b + IDSetDifficulty = 0x3c + IDChangeDimension = 0x3d + IDSetPlayerGameType = 0x3e + IDPlayerList = 0x3f + IDRequestChunkRadius = 0x45 + IDChunkRadiusUpdated = 0x46 + IDAvailableCommands = 0x4c + IDCommandRequest = 0x4d + IDResourcePackDataInfo = 0x52 + IDResourcePackChunkData = 0x53 + IDResourcePackChunkRequest = 0x54 + IDTransfer = 0x55 + IDSetTitle = 0x58 + IDPlayerSkin = 0x5d +) diff --git a/net/packets/bedrock/adventure_settings.go b/net/packets/bedrock/adventure_settings.go new file mode 100644 index 0000000..cdeb2cf --- /dev/null +++ b/net/packets/bedrock/adventure_settings.go @@ -0,0 +1,39 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type AdventureSettingsPacket struct { + *packets.Packet + Flags uint32 + CommandPermission uint32 + Flags2 uint32 + PlayerPermission uint32 + CustomFlags uint32 + EntityID int64 +} + +func NewAdventureSettingsPacket() *AdventureSettingsPacket { + return &AdventureSettingsPacket{Packet: packets.NewPacket(info.IDAdventureSettings)} +} + +func (pk *AdventureSettingsPacket) Encode() { + pk.PutUVarInt(pk.Flags) + pk.PutUVarInt(pk.CommandPermission) + pk.PutUVarInt(pk.Flags2) + pk.PutUVarInt(pk.PlayerPermission) + pk.PutUVarInt(pk.CustomFlags) + pk.PutLLong(pk.EntityID) +} + +func (pk *AdventureSettingsPacket) Decode() { + pk.Flags = pk.GetUVarInt() + pk.CommandPermission = pk.GetUVarInt() + pk.Flags2 = pk.GetUVarInt() + pk.PlayerPermission = pk.GetUVarInt() + pk.CustomFlags = pk.GetUVarInt() + pk.EntityID = pk.GetLLong() +} diff --git a/net/packets/bedrock/chunk_radius.go b/net/packets/bedrock/chunk_radius.go new file mode 100644 index 0000000..d01cd0b --- /dev/null +++ b/net/packets/bedrock/chunk_radius.go @@ -0,0 +1,41 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type RequestChunkRadiusPacket struct { + *packets.Packet + Radius int32 +} + +func NewRequestChunkRadiusPacket() *RequestChunkRadiusPacket { + return &RequestChunkRadiusPacket{Packet: packets.NewPacket(info.IDRequestChunkRadius)} +} + +func (pk *RequestChunkRadiusPacket) Encode() {} + +func (pk *RequestChunkRadiusPacket) Decode() { + pk.Radius = pk.GetVarInt() +} + + +type ChunkRadiusUpdatedPacket struct { + *packets.Packet + Radius int32 +} + +func NewChunkRadiusUpdatedPacket(radius int32) *ChunkRadiusUpdatedPacket { + return &ChunkRadiusUpdatedPacket{ + Packet: packets.NewPacket(info.IDChunkRadiusUpdated), + Radius: radius, + } +} + +func (pk *ChunkRadiusUpdatedPacket) Encode() { + pk.PutVarInt(pk.Radius) +} + +func (pk *ChunkRadiusUpdatedPacket) Decode() {} diff --git a/net/packets/bedrock/disconnect.go b/net/packets/bedrock/disconnect.go new file mode 100644 index 0000000..3f153a1 --- /dev/null +++ b/net/packets/bedrock/disconnect.go @@ -0,0 +1,35 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type DisconnectPacket struct { + *packets.Packet + HideScreen bool + Message string +} + +func NewDisconnectPacket(message string, hideScreen bool) *DisconnectPacket { + return &DisconnectPacket{ + Packet: packets.NewPacket(info.IDDisconnect), + HideScreen: hideScreen, + Message: message, + } +} + +func (pk *DisconnectPacket) Encode() { + pk.PutBool(pk.HideScreen) + if !pk.HideScreen { + pk.PutString(pk.Message) + } +} + +func (pk *DisconnectPacket) Decode() { + pk.HideScreen = pk.GetBool() + if !pk.HideScreen { + pk.Message = pk.GetString() + } +} diff --git a/net/packets/bedrock/full_chunk_data.go b/net/packets/bedrock/full_chunk_data.go new file mode 100644 index 0000000..90c5473 --- /dev/null +++ b/net/packets/bedrock/full_chunk_data.go @@ -0,0 +1,97 @@ +package bedrock + +import ( + "bytes" + + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type FullChunkDataPacket struct { + *packets.Packet + ChunkX int32 + ChunkZ int32 + Data []byte +} + +func NewFullChunkDataPacket(cx, cz int32, data []byte) *FullChunkDataPacket { + return &FullChunkDataPacket{ + Packet: packets.NewPacket(info.IDFullChunkData), + ChunkX: cx, + ChunkZ: cz, + Data: data, + } +} + +func BuildFlatChunk() []byte { + buf := &bytes.Buffer{} + + const numSubChunks = 8 + buf.WriteByte(numSubChunks) + + for sc := 0; sc < numSubChunks; sc++ { + buf.WriteByte(0) + + blocks := make([]byte, 4096) + baseY := sc * 16 + + for x := 0; x < 16; x++ { + for z := 0; z < 16; z++ { + for y := 0; y < 16; y++ { + absY := baseY + y + idx := x*256 + z*16 + y + if absY == 0 { + blocks[idx] = 7 + } else if absY < 62 { + blocks[idx] = 1 + } else if absY == 62 { + blocks[idx] = 3 + } else if absY == 63 { + blocks[idx] = 2 + } + } + } + } + buf.Write(blocks) + + buf.Write(make([]byte, 2048)) + + skylight := make([]byte, 2048) + for i := range skylight { + if baseY >= 64 { + skylight[i] = 0xFF + } else { + skylight[i] = 0x00 + } + } + buf.Write(skylight) + + buf.Write(make([]byte, 2048)) + } + + heightmap := make([]byte, 256) + for i := range heightmap { + heightmap[i] = 64 + } + buf.Write(heightmap) + + biomes := make([]byte, 256) + for i := range biomes { + biomes[i] = 1 + } + buf.Write(biomes) + + buf.WriteByte(0) + buf.WriteByte(0) + + return buf.Bytes() +} + +func (pk *FullChunkDataPacket) Encode() { + pk.PutVarInt(pk.ChunkX) + pk.PutVarInt(pk.ChunkZ) + pk.PutUVarInt(uint32(len(pk.Data))) + pk.PutBytes(pk.Data) +} + +func (pk *FullChunkDataPacket) Decode() {} diff --git a/net/packets/bedrock/login.go b/net/packets/bedrock/login.go new file mode 100644 index 0000000..dd8d13c --- /dev/null +++ b/net/packets/bedrock/login.go @@ -0,0 +1,165 @@ +package bedrock + +import ( + "encoding/base64" + "encoding/binary" + "encoding/json" + "strings" + + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" + "github.com/google/uuid" +) + +type LoginPacket struct { + *packets.Packet + Protocol int32 + Username string + ClientUUID uuid.UUID + ClientXUID string + ClientID int64 + ServerAddr string + Language string + SkinID string + SkinData []byte + CapeData []byte + GeometryName string + GeometryData string +} + +func NewLoginPacket() *LoginPacket { + return &LoginPacket{ + Packet: packets.NewPacket(info.IDLogin), + ClientUUID: uuid.New(), + } +} + +func (pk *LoginPacket) Encode() {} + +func (pk *LoginPacket) Decode() { + pk.Protocol = pk.GetInt() + pk.GetByte() + totalLen := int(pk.GetUVarInt()) + if totalLen <= 0 || totalLen > pk.Remaining() { + pk.Username = "Player" + return + } + blob := pk.GetBytes(totalLen) + if len(blob) < 4 { + pk.Username = "Player" + return + } + chainLen := int(binary.LittleEndian.Uint32(blob[:4])) + if chainLen <= 0 || 4+chainLen > len(blob) { + pk.Username = "Player" + return + } + var chainData struct { + Chain []string `json:"chain"` + } + if err := json.Unmarshal(blob[4:4+chainLen], &chainData); err == nil { + for _, raw := range chainData.Chain { + pk.parseChain(raw) + } + } + offset := 4 + chainLen + if offset+4 > len(blob) { + if pk.Username == "" { + pk.Username = "Player" + } + return + } + clientLen := int(binary.LittleEndian.Uint32(blob[offset : offset+4])) + offset += 4 + if clientLen > 0 && offset+clientLen <= len(blob) { + pk.parseClientData(string(blob[offset : offset+clientLen])) + } + if pk.Username == "" { + pk.Username = "Player" + } +} + +func decodeJWTPayload(s string) ([]byte, error) { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + switch len(s) % 4 { + case 2: + s += "==" + case 3: + s += "=" + } + b, err = base64.URLEncoding.DecodeString(s) + } + return b, err +} + +func (pk *LoginPacket) parseChain(raw string) { + parts := strings.Split(raw, ".") + if len(parts) < 2 { + return + } + payload, err := decodeJWTPayload(parts[1]) + if err != nil { + return + } + var data struct { + ExtraData map[string]interface{} `json:"extraData"` + } + if err := json.Unmarshal(payload, &data); err != nil || data.ExtraData == nil { + return + } + if v, ok := data.ExtraData["displayName"]; ok { + if name, _ := v.(string); name != "" { + pk.Username = name + } + } + if v, ok := data.ExtraData["identity"]; ok { + if idStr, _ := v.(string); idStr != "" { + if u, err := uuid.Parse(idStr); err == nil { + pk.ClientUUID = u + } + } + } + if v, ok := data.ExtraData["XUID"]; ok { + pk.ClientXUID, _ = v.(string) + } +} + +func (pk *LoginPacket) parseClientData(raw string) { + parts := strings.Split(raw, ".") + if len(parts) < 2 { + return + } + payload, err := decodeJWTPayload(parts[1]) + if err != nil { + return + } + var data struct { + ClientRandomID int64 `json:"ClientRandomId"` + ServerAddress string `json:"ServerAddress"` + LanguageCode string `json:"LanguageCode"` + SkinID string `json:"SkinId"` + SkinData string `json:"SkinData"` + CapeData string `json:"CapeData"` + SkinGeomName string `json:"SkinGeometryName"` + SkinGeomData string `json:"SkinGeometry"` + } + if err := json.Unmarshal(payload, &data); err != nil { + return + } + pk.ClientID = data.ClientRandomID + pk.ServerAddr = data.ServerAddress + pk.Language = data.LanguageCode + if pk.Language == "" { + pk.Language = "en_US" + } + pk.SkinID = data.SkinID + pk.GeometryName = data.SkinGeomName + pk.SkinData, _ = base64.RawStdEncoding.DecodeString(data.SkinData) + pk.CapeData, _ = base64.RawStdEncoding.DecodeString(data.CapeData) + geomBytes, _ := base64.RawStdEncoding.DecodeString(data.SkinGeomData) + pk.GeometryData = string(geomBytes) + for len(pk.SkinData) < 8192 { + pk.SkinData = append(pk.SkinData, 0x00) + } +} diff --git a/net/packets/bedrock/move_player.go b/net/packets/bedrock/move_player.go new file mode 100644 index 0000000..fb8c0ea --- /dev/null +++ b/net/packets/bedrock/move_player.go @@ -0,0 +1,47 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type MovePlayerPacket struct { + *packets.Packet + RuntimeID uint64 + X, Y, Z float32 + Pitch, Yaw, HeadYaw float32 + Mode byte + OnGround bool + RidingID uint64 +} + +func NewMovePlayerPacket() *MovePlayerPacket { + return &MovePlayerPacket{Packet: packets.NewPacket(info.IDMovePlayer)} +} + +func (pk *MovePlayerPacket) Encode() { + pk.PutUVarLong(pk.RuntimeID) + pk.PutLFloat(pk.X) + pk.PutLFloat(pk.Y) + pk.PutLFloat(pk.Z) + pk.PutLFloat(pk.Pitch) + pk.PutLFloat(pk.Yaw) + pk.PutLFloat(pk.HeadYaw) + pk.PutByte(pk.Mode) + pk.PutBool(pk.OnGround) + pk.PutUVarLong(pk.RidingID) +} + +func (pk *MovePlayerPacket) Decode() { + pk.RuntimeID = pk.GetUVarLong() + pk.X = pk.GetLFloat() + pk.Y = pk.GetLFloat() + pk.Z = pk.GetLFloat() + pk.Pitch = pk.GetLFloat() + pk.Yaw = pk.GetLFloat() + pk.HeadYaw = pk.GetLFloat() + pk.Mode = pk.GetByte() + pk.OnGround = pk.GetBool() + pk.RidingID = pk.GetUVarLong() +} diff --git a/net/packets/bedrock/play_status.go b/net/packets/bedrock/play_status.go new file mode 100644 index 0000000..2b25841 --- /dev/null +++ b/net/packets/bedrock/play_status.go @@ -0,0 +1,38 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +const ( + PlayStatusLoginSuccess int32 = 0 + PlayStatusLoginFailedClient int32 = 1 + PlayStatusLoginFailedServer int32 = 2 + PlayStatusPlayerSpawn int32 = 3 + PlayStatusLoginFailedInvalidTenant int32 = 4 + PlayStatusLoginFailedVanillaEdu int32 = 5 + PlayStatusLoginFailedEduVanilla int32 = 6 +) + +type PlayStatusPacket struct { + *packets.Packet + Status int32 +} + +func NewPlayStatusPacket(status int32) *PlayStatusPacket { + pk := &PlayStatusPacket{ + Packet: packets.NewPacket(info.IDPlayStatus), + Status: status, + } + return pk +} + +func (pk *PlayStatusPacket) Encode() { + pk.PutInt(pk.Status) +} + +func (pk *PlayStatusPacket) Decode() { + pk.Status = pk.GetInt() +} diff --git a/net/packets/bedrock/resource_pack_client_response.go b/net/packets/bedrock/resource_pack_client_response.go new file mode 100644 index 0000000..b73940f --- /dev/null +++ b/net/packets/bedrock/resource_pack_client_response.go @@ -0,0 +1,37 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +const ( + ResourcePackResponseRefused = 1 + ResourcePackResponseSendPacks = 2 + ResourcePackResponseAllPacksSent = 3 + ResourcePackResponseCompleted = 4 +) + +type ResourcePackClientResponsePacket struct { + *packets.Packet + Status byte + PackIDs []string +} + +func NewResourcePackClientResponsePacket() *ResourcePackClientResponsePacket { + return &ResourcePackClientResponsePacket{ + Packet: packets.NewPacket(info.IDResourcePackClientResponse), + } +} + +func (pk *ResourcePackClientResponsePacket) Encode() {} + +func (pk *ResourcePackClientResponsePacket) Decode() { + pk.Status = pk.GetByte() + count := pk.GetLShort() + pk.PackIDs = make([]string, count) + for i := uint16(0); i < count; i++ { + pk.PackIDs[i] = pk.GetString() + } +} diff --git a/net/packets/bedrock/resource_pack_info.go b/net/packets/bedrock/resource_pack_info.go new file mode 100644 index 0000000..30c8ab4 --- /dev/null +++ b/net/packets/bedrock/resource_pack_info.go @@ -0,0 +1,27 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type ResourcePackInfoPacket struct { + *packets.Packet + MustAccept bool +} + +func NewResourcePackInfoPacket(mustAccept bool) *ResourcePackInfoPacket { + return &ResourcePackInfoPacket{ + Packet: packets.NewPacket(info.IDResourcePackInfo), + MustAccept: mustAccept, + } +} + +func (pk *ResourcePackInfoPacket) Encode() { + pk.PutBool(pk.MustAccept) + pk.PutLShort(0) + pk.PutLShort(0) +} + +func (pk *ResourcePackInfoPacket) Decode() {} diff --git a/net/packets/bedrock/resource_pack_stack.go b/net/packets/bedrock/resource_pack_stack.go new file mode 100644 index 0000000..3f128c2 --- /dev/null +++ b/net/packets/bedrock/resource_pack_stack.go @@ -0,0 +1,27 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type ResourcePackStackPacket struct { + *packets.Packet + MustAccept bool +} + +func NewResourcePackStackPacket(mustAccept bool) *ResourcePackStackPacket { + return &ResourcePackStackPacket{ + Packet: packets.NewPacket(info.IDResourcePackStack), + MustAccept: mustAccept, + } +} + +func (pk *ResourcePackStackPacket) Encode() { + pk.PutBool(pk.MustAccept) + pk.PutLShort(0) + pk.PutLShort(0) +} + +func (pk *ResourcePackStackPacket) Decode() {} diff --git a/net/packets/bedrock/set_game_mode.go b/net/packets/bedrock/set_game_mode.go new file mode 100644 index 0000000..4e5d984 --- /dev/null +++ b/net/packets/bedrock/set_game_mode.go @@ -0,0 +1,24 @@ +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type SetGameModePacket struct { + *packets.Packet + GameMode int32 +} + +func NewSetGameModePacket(gm int32) *SetGameModePacket { + return &SetGameModePacket{ + Packet: packets.NewPacket(info.IDSetGameMode), + GameMode: gm, + } +} + +func (pk *SetGameModePacket) Encode() { + pk.PutVarInt(pk.GameMode) +} + +func (pk *SetGameModePacket) Decode() {} diff --git a/net/packets/bedrock/set_time.go b/net/packets/bedrock/set_time.go new file mode 100644 index 0000000..8a6c9f6 --- /dev/null +++ b/net/packets/bedrock/set_time.go @@ -0,0 +1,25 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type SetTimePacket struct { + *packets.Packet + Time int32 +} + +func NewSetTimePacket(time int32) *SetTimePacket { + return &SetTimePacket{ + Packet: packets.NewPacket(info.IDSetTime), + Time: time, + } +} + +func (pk *SetTimePacket) Encode() { + pk.PutVarInt(pk.Time) +} + +func (pk *SetTimePacket) Decode() {} diff --git a/net/packets/bedrock/start_game.go b/net/packets/bedrock/start_game.go new file mode 100644 index 0000000..a7640cb --- /dev/null +++ b/net/packets/bedrock/start_game.go @@ -0,0 +1,92 @@ +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type StartGamePacket struct { + *packets.Packet + EntityUniqueID int64 + EntityRuntimeID uint64 + PlayerGameMode int32 + SpawnX float32 + SpawnY float32 + SpawnZ float32 + Yaw float32 + Pitch float32 + Seed int32 + Dimension int32 + Generator int32 + GameMode int32 + Difficulty int32 + SpawnBX int32 + SpawnBY int32 + SpawnBZ int32 + AchievementsDisabled bool + Time int32 + EduMode bool + RainLevel float32 + LightningLevel float32 + IsMultiplayer bool + BroadcastToLAN bool + CommandsEnabled bool + ForceTexturePacks bool + LevelName string + IsTrial bool + CurrentTick int64 + EnchantmentSeed int32 +} + +func NewStartGamePacket() *StartGamePacket { + return &StartGamePacket{ + Packet: packets.NewPacket(info.IDStartGame), + } +} + +func (pk *StartGamePacket) Encode() { + pk.PutVarLong(pk.EntityUniqueID) + pk.PutUVarLong(pk.EntityRuntimeID) + pk.PutVarInt(pk.PlayerGameMode) + pk.PutLFloat(pk.SpawnX) + pk.PutLFloat(pk.SpawnY) + pk.PutLFloat(pk.SpawnZ) + pk.PutLFloat(pk.Yaw) + pk.PutLFloat(pk.Pitch) + pk.PutVarInt(pk.Seed) + pk.PutVarInt(pk.Dimension) + pk.PutVarInt(pk.Generator) + pk.PutVarInt(pk.GameMode) + pk.PutVarInt(pk.Difficulty) + pk.PutVarInt(pk.SpawnBX) + pk.PutUVarInt(uint32(pk.SpawnBY)) + pk.PutVarInt(pk.SpawnBZ) + pk.PutBool(pk.AchievementsDisabled) + pk.PutVarInt(pk.Time) + pk.PutBool(pk.EduMode) + pk.PutLFloat(pk.RainLevel) + pk.PutLFloat(pk.LightningLevel) + pk.PutBool(pk.IsMultiplayer) + pk.PutBool(pk.BroadcastToLAN) + pk.PutBool(false) + pk.PutBool(pk.CommandsEnabled) + pk.PutBool(pk.ForceTexturePacks) + pk.PutUVarInt(0) + pk.PutBool(false) + pk.PutBool(false) + pk.PutBool(false) + pk.PutVarInt(1) + pk.PutLInt(4) + pk.PutBool(false) + pk.PutBool(false) + pk.PutBool(false) + pk.PutBool(false) + pk.PutBool(false) + pk.PutString(pk.LevelName) + pk.PutString("") + pk.PutBool(pk.IsTrial) + pk.PutLLong(pk.CurrentTick) + pk.PutVarInt(pk.EnchantmentSeed) +} + +func (pk *StartGamePacket) Decode() {} diff --git a/net/packets/bedrock/text.go b/net/packets/bedrock/text.go new file mode 100644 index 0000000..9cb38ee --- /dev/null +++ b/net/packets/bedrock/text.go @@ -0,0 +1,61 @@ + +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +const ( + TextTypeRaw = 0 + TextTypeChat = 1 + TextTypeTranslation = 2 + TextTypePopup = 3 + TextTypeTip = 4 + TextTypeSystem = 5 + TextTypeWhisper = 6 + TextTypeAnnouncement = 7 +) + +type TextPacket struct { + *packets.Packet + TextType byte + Source string + Message string + XUID string +} + +func NewTextPacket(textType byte, source, message string) *TextPacket { + return &TextPacket{ + Packet: packets.NewPacket(info.IDText), + TextType: textType, + Source: source, + Message: message, + } +} + +func (pk *TextPacket) Encode() { + pk.PutByte(pk.TextType) + switch pk.TextType { + case TextTypeChat, TextTypeWhisper, TextTypeAnnouncement: + pk.PutString(pk.Source) + pk.PutString(pk.Message) + case TextTypeRaw, TextTypeTip, TextTypeSystem, TextTypePopup: + pk.PutString(pk.Message) + case TextTypeTranslation: + pk.PutString(pk.Message) + pk.PutVarInt(0) + } + pk.PutString(pk.XUID) +} + +func (pk *TextPacket) Decode() { + pk.TextType = pk.GetByte() + switch pk.TextType { + case TextTypeChat, TextTypeWhisper, TextTypeAnnouncement: + pk.Source = pk.GetString() + pk.Message = pk.GetString() + default: + pk.Message = pk.GetString() + } +} diff --git a/net/packets/bedrock/update_attributes.go b/net/packets/bedrock/update_attributes.go new file mode 100644 index 0000000..712454d --- /dev/null +++ b/net/packets/bedrock/update_attributes.go @@ -0,0 +1,53 @@ +package bedrock + +import ( + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" +) + +type Attribute struct { + Min float32 + Max float32 + Current float32 + Default float32 + Name string +} + +type UpdateAttributesPacket struct { + *packets.Packet + RuntimeID uint64 + Attributes []Attribute +} + +func NewUpdateAttributesPacket(runtimeID uint64) *UpdateAttributesPacket { + return &UpdateAttributesPacket{ + Packet: packets.NewPacket(info.IDUpdateAttributes), + RuntimeID: runtimeID, + } +} + +func (pk *UpdateAttributesPacket) Encode() { + pk.PutUVarLong(pk.RuntimeID) + pk.PutUVarInt(uint32(len(pk.Attributes))) + for _, attr := range pk.Attributes { + pk.PutLFloat(attr.Min) + pk.PutLFloat(attr.Max) + pk.PutLFloat(attr.Current) + pk.PutLFloat(attr.Default) + pk.PutString(attr.Name) + } +} + +func (pk *UpdateAttributesPacket) Decode() {} + +func DefaultAttributes() []Attribute { + return []Attribute{ + {Min: 0, Max: 20, Current: 20, Default: 20, Name: "minecraft:health"}, + {Min: 0, Max: 20, Current: 20, Default: 20, Name: "minecraft:player.hunger"}, + {Min: 0, Max: 5, Current: 5, Default: 5, Name: "minecraft:player.saturation"}, + {Min: 0, Max: 5, Current: 0, Default: 0, Name: "minecraft:player.exhaustion"}, + {Min: 0, Max: 24791, Current: 0, Default: 0, Name: "minecraft:player.level"}, + {Min: 0, Max: 1, Current: 0, Default: 0, Name: "minecraft:player.experience"}, + {Min: 0, Max: 10, Current: 0.1, Default: 0.1, Name: "minecraft:movement"}, + } +} diff --git a/net/packets/packet.go b/net/packets/packet.go new file mode 100644 index 0000000..48bfdf3 --- /dev/null +++ b/net/packets/packet.go @@ -0,0 +1,195 @@ +package packets + +import ( + "bytes" + "encoding/binary" + "math" +) + +type IPacket interface { + GetID() byte + Encode() + Decode() + GetBuffer() []byte + SetBuffer([]byte) +} + +type Packet struct { + id byte + buf *bytes.Buffer + raw []byte + pos int +} + +func NewPacket(id byte) *Packet { + return &Packet{id: id, buf: &bytes.Buffer{}} +} + +func (p *Packet) GetID() byte { return p.id } +func (p *Packet) GetBuffer() []byte { return p.buf.Bytes() } +func (p *Packet) SetBuffer(data []byte) { p.raw = data; p.pos = 0 } + +func (p *Packet) PutByte(v byte) { p.buf.WriteByte(v) } +func (p *Packet) PutBytes(v []byte) { p.buf.Write(v) } + +func (p *Packet) PutBool(v bool) { + if v { + p.buf.WriteByte(0x01) + } else { + p.buf.WriteByte(0x00) + } +} + +func (p *Packet) PutShort(v int16) { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, uint16(v)) + p.buf.Write(b) +} + +func (p *Packet) PutLShort(v uint16) { + b := make([]byte, 2) + binary.LittleEndian.PutUint16(b, v) + p.buf.Write(b) +} + +func (p *Packet) PutInt(v int32) { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, uint32(v)) + p.buf.Write(b) +} + +func (p *Packet) PutLInt(v int32) { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, uint32(v)) + p.buf.Write(b) +} + +func (p *Packet) PutLLong(v int64) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(v)) + p.buf.Write(b) +} + +func (p *Packet) PutLFloat(v float32) { + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, math.Float32bits(v)) + p.buf.Write(b) +} + +func (p *Packet) PutVarInt(v int32) { + p.PutUVarInt((uint32(v) << 1) ^ uint32(v>>31)) +} + +func (p *Packet) PutUVarInt(v uint32) { + for v >= 0x80 { + p.buf.WriteByte(byte(v&0x7f) | 0x80) + v >>= 7 + } + p.buf.WriteByte(byte(v)) +} + +func (p *Packet) PutVarLong(v int64) { + p.PutUVarLong((uint64(v) << 1) ^ uint64(v>>63)) +} + +func (p *Packet) PutUVarLong(v uint64) { + for v >= 0x80 { + p.buf.WriteByte(byte(v&0x7f) | 0x80) + v >>= 7 + } + p.buf.WriteByte(byte(v)) +} + +func (p *Packet) PutString(v string) { + p.PutUVarInt(uint32(len(v))) + p.buf.WriteString(v) +} + +func (p *Packet) checkRead(n int) bool { return p.pos+n <= len(p.raw) } + +func (p *Packet) GetByte() byte { + if !p.checkRead(1) { return 0 } + b := p.raw[p.pos]; p.pos++; return b +} + +func (p *Packet) GetBytes(n int) []byte { + if !p.checkRead(n) { return nil } + b := p.raw[p.pos : p.pos+n]; p.pos += n; return b +} + +func (p *Packet) GetBool() bool { return p.GetByte() != 0x00 } + +func (p *Packet) GetShort() int16 { + b := p.GetBytes(2) + if b == nil { return 0 } + return int16(binary.BigEndian.Uint16(b)) +} + +func (p *Packet) GetLShort() uint16 { + b := p.GetBytes(2) + if b == nil { return 0 } + return binary.LittleEndian.Uint16(b) +} + +func (p *Packet) GetInt() int32 { + b := p.GetBytes(4) + if b == nil { return 0 } + return int32(binary.BigEndian.Uint32(b)) +} + +func (p *Packet) GetLInt() int32 { + b := p.GetBytes(4) + if b == nil { return 0 } + return int32(binary.LittleEndian.Uint32(b)) +} + +func (p *Packet) GetLLong() int64 { + b := p.GetBytes(8) + if b == nil { return 0 } + return int64(binary.LittleEndian.Uint64(b)) +} + +func (p *Packet) GetLFloat() float32 { + b := p.GetBytes(4) + if b == nil { return 0 } + return math.Float32frombits(binary.LittleEndian.Uint32(b)) +} + +func (p *Packet) GetVarInt() int32 { + uv := p.GetUVarInt() + return int32((uv >> 1) ^ -(uv & 1)) +} + +func (p *Packet) GetUVarInt() uint32 { + var v uint32 + for i := uint(0); i < 35; i += 7 { + b := p.GetByte() + v |= uint32(b&0x7f) << i + if b&0x80 == 0 { break } + } + return v +} + +func (p *Packet) GetVarLong() int64 { + uv := p.GetUVarLong() + return int64((uv >> 1) ^ -(uv & 1)) +} + +func (p *Packet) GetUVarLong() uint64 { + var v uint64 + for i := uint(0); i < 70; i += 7 { + b := p.GetByte() + v |= uint64(b&0x7f) << i + if b&0x80 == 0 { break } + } + return v +} + +func (p *Packet) GetString() string { + return string(p.GetBytes(int(p.GetUVarInt()))) +} + +func (p *Packet) Remaining() int { + if p.pos >= len(p.raw) { return 0 } + return len(p.raw) - p.pos +} diff --git a/network/server.go b/network/server.go new file mode 100644 index 0000000..bf29cd8 --- /dev/null +++ b/network/server.go @@ -0,0 +1,133 @@ +package network + +import ( + "fmt" + "sync" + + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/raknet" + "github.com/cubexteam/Frodding/resources" +) + +type Logger interface { + Infof(format string, args ...any) + Warnf(format string, args ...any) + Errorf(format string, args ...any) + Debugf(format string, args ...any) + Chat(name, message string) +} + +type Server struct { + cfg *resources.Config + log Logger + rak *raknet.Server + mu sync.RWMutex + sessions map[int64]*Session + + onShutdown func() +} + +func NewServer(cfg *resources.Config, log Logger) *Server { + return &Server{ + cfg: cfg, + log: log, + sessions: make(map[int64]*Session), + } +} + +func (s *Server) SetShutdownHook(fn func()) { + s.onShutdown = fn +} + +func (s *Server) Start() error { + guid := int64(0x526f6464696e6721) + + motdFunc := func() string { + protocol := info.LatestProtocol + versionTag := info.LatestGameVersionNetwork + if s.cfg.MotdProtocol > 0 { + protocol = s.cfg.MotdProtocol + if vt, ok := info.SupportedProtocols[int32(protocol)]; ok { + versionTag = vt + } + } + return fmt.Sprintf("MCPE;%s;%d;%s;%d;%d;%d;%s;Survival", + s.cfg.ServerName, protocol, versionTag, + s.OnlineCount(), s.cfg.MaxPlayers, + guid, s.cfg.ServerName, + ) + } + + s.rak = raknet.NewServer(guid, motdFunc, s) + s.log.Infof("Starting on %s:%d (MCBE %s, protocol %d)", + s.cfg.ServerIP, s.cfg.ServerPort, + info.LatestGameVersionNetwork, info.LatestProtocol) + err := s.rak.Start(s.cfg.ServerIP, s.cfg.ServerPort) + if err != nil { + return err + } + s.log.Infof("RakNet server started!") + return nil +} + +func (s *Server) Shutdown() { + s.rak.Shutdown() + if s.onShutdown != nil { + s.onShutdown() + } +} + +func (s *Server) OnConnect(rs *raknet.Session) { + s.log.Debugf("[NET] New connection guid=%d from %s", rs.GUID, rs.Addr) + sess := NewSession(s, rs) + s.mu.Lock() + s.sessions[rs.GUID] = sess + s.mu.Unlock() +} + +func (s *Server) OnPacket(rs *raknet.Session, payload []byte) { + s.mu.RLock() + sess, ok := s.sessions[rs.GUID] + s.mu.RUnlock() + if !ok { + return + } + sess.HandlePayload(payload) +} + +func (s *Server) OnDisconnect(rs *raknet.Session) { + s.mu.Lock() + sess, ok := s.sessions[rs.GUID] + delete(s.sessions, rs.GUID) + s.mu.Unlock() + if ok && sess.Spawned { + s.BroadcastMessage(fmt.Sprintf("§e%s left the game", sess.Username)) + s.log.Infof("%s left the game", sess.Username) + } +} + +func (s *Server) BroadcastMessage(msg string) { + s.mu.RLock() + defer s.mu.RUnlock() + for _, sess := range s.sessions { + if sess.Spawned { + sess.SendMessage(msg) + } + } +} + +func (s *Server) OnlineCount() int { + s.mu.RLock() + defer s.mu.RUnlock() + return len(s.sessions) +} + +func (s *Server) GetSessions() []*Session { + s.mu.RLock() + defer s.mu.RUnlock() + list := make([]*Session, 0, len(s.sessions)) + for _, sess := range s.sessions { + list = append(list, sess) + } + return list +} diff --git a/network/session.go b/network/session.go new file mode 100644 index 0000000..60c5276 --- /dev/null +++ b/network/session.go @@ -0,0 +1,361 @@ +package network + +import ( + "bytes" + "compress/zlib" + "fmt" + "io" + "strings" + "sync" + + "github.com/cubexteam/Frodding/net/info" + "github.com/cubexteam/Frodding/net/packets" + "github.com/cubexteam/Frodding/net/packets/bedrock" + "github.com/cubexteam/Frodding/raknet" + "github.com/google/uuid" +) + +type Session struct { + mu sync.Mutex + server *Server + rak *raknet.Session + + UUID uuid.UUID + XUID string + Username string + ClientID int64 + Language string + SkinID string + SkinData []byte + CapeData []byte + GeomName string + GeomData string + + Spawned bool + X, Y, Z float32 + ViewDistance int32 + sentChunks map[[2]int32]bool +} + +func NewSession(srv *Server, rak *raknet.Session) *Session { + return &Session{ + server: srv, + rak: rak, + UUID: uuid.New(), + ViewDistance: 8, + sentChunks: make(map[[2]int32]bool), + Y: 64, + } +} + +func (s *Session) SendPacket(pk packets.IPacket) { + pk.Encode() + s.sendBatch(pk.GetID(), pk.GetBuffer()) +} + +func (s *Session) sendBatch(id byte, data []byte) { + var payload bytes.Buffer + payload.WriteByte(id) + payload.Write(data) + + var raw bytes.Buffer + writeVarUint32(&raw, uint32(payload.Len())) + raw.Write(payload.Bytes()) + + var compressed bytes.Buffer + w, _ := zlib.NewWriterLevel(&compressed, zlib.BestSpeed) + w.Write(raw.Bytes()) + w.Close() + + batch := append([]byte{0xfe}, compressed.Bytes()...) + s.rak.Send(batch, raknet.ReliableOrdered, 0) +} + +func writeVarUint32(buf *bytes.Buffer, v uint32) { + for v >= 0x80 { + buf.WriteByte(byte(v&0x7f) | 0x80) + v >>= 7 + } + buf.WriteByte(byte(v)) +} + +func (s *Session) HandlePayload(data []byte) { + defer func() { recover() }() + if len(data) == 0 || data[0] != 0xfe { + return + } + r, err := zlib.NewReader(bytes.NewReader(data[1:])) + if err != nil { + return + } + defer r.Close() + decompressed, err := io.ReadAll(r) + if err != nil { + return + } + reader := bytes.NewReader(decompressed) + for reader.Len() > 0 { + pkLen, err := readVarUint32(reader) + if err != nil || pkLen == 0 || int(pkLen) > reader.Len() { + break + } + pkData := make([]byte, pkLen) + reader.Read(pkData) + s.handlePacketData(pkData) + } +} + +func readVarUint32(r *bytes.Reader) (uint32, error) { + var v uint32 + for i := uint(0); i < 35; i += 7 { + b, err := r.ReadByte() + if err != nil { + return 0, err + } + v |= uint32(b&0x7f) << i + if b&0x80 == 0 { + break + } + } + return v, nil +} + +func (s *Session) handlePacketData(data []byte) { + if len(data) == 0 { + return + } + id := data[0] + body := data[1:] + + switch id { + case info.IDLogin: + pk := bedrock.NewLoginPacket() + pk.SetBuffer(body) + pk.Decode() + s.handleLogin(pk) + case info.IDResourcePackClientResponse: + pk := bedrock.NewResourcePackClientResponsePacket() + pk.SetBuffer(body) + pk.Decode() + s.handleResourcePackResponse(pk) + case info.IDMovePlayer: + pk := bedrock.NewMovePlayerPacket() + pk.SetBuffer(body) + pk.Decode() + s.handleMovePlayer(pk) + case info.IDRequestChunkRadius: + pk := bedrock.NewRequestChunkRadiusPacket() + pk.SetBuffer(body) + pk.Decode() + s.handleChunkRadius(pk) + case info.IDText: + pk := bedrock.NewTextPacket(0, "", "") + pk.SetBuffer(body) + pk.Decode() + s.handleText(pk) + } +} + +func (s *Session) handleLogin(pk *bedrock.LoginPacket) { + s.server.log.Infof("Player connecting: %s (protocol %d)", pk.Username, pk.Protocol) + + if pk.Protocol < info.MinProtocol { + s.SendPacket(bedrock.NewPlayStatusPacket(bedrock.PlayStatusLoginFailedClient)) + return + } + if pk.Protocol > info.LatestProtocol { + s.SendPacket(bedrock.NewPlayStatusPacket(bedrock.PlayStatusLoginFailedServer)) + return + } + + s.UUID = pk.ClientUUID + s.XUID = pk.ClientXUID + s.Username = pk.Username + s.ClientID = pk.ClientID + s.Language = pk.Language + s.SkinID = pk.SkinID + s.SkinData = pk.SkinData + s.CapeData = pk.CapeData + s.GeomName = pk.GeometryName + s.GeomData = pk.GeometryData + + s.SendPacket(bedrock.NewPlayStatusPacket(bedrock.PlayStatusLoginSuccess)) + s.SendPacket(bedrock.NewResourcePackInfoPacket(false)) +} + +func (s *Session) handleResourcePackResponse(pk *bedrock.ResourcePackClientResponsePacket) { + switch pk.Status { + case bedrock.ResourcePackResponseSendPacks: + s.SendPacket(bedrock.NewResourcePackInfoPacket(false)) + case bedrock.ResourcePackResponseAllPacksSent: + s.SendPacket(bedrock.NewResourcePackStackPacket(false)) + case bedrock.ResourcePackResponseCompleted: + s.startGame() + case bedrock.ResourcePackResponseRefused: + s.SendPacket(bedrock.NewResourcePackStackPacket(false)) + } +} + +func (s *Session) startGame() { + sg := bedrock.NewStartGamePacket() + sg.EntityUniqueID = 1 + sg.EntityRuntimeID = 1 + sg.PlayerGameMode = 0 + sg.SpawnX = s.X + sg.SpawnY = s.Y + 1 + sg.SpawnZ = s.Z + sg.Seed = 12345 + sg.Dimension = 0 + sg.Generator = 1 + sg.GameMode = 0 + sg.Difficulty = 1 + sg.SpawnBX = 0 + sg.SpawnBY = 64 + sg.SpawnBZ = 0 + sg.AchievementsDisabled = true + sg.Time = 6000 + sg.IsMultiplayer = true + sg.BroadcastToLAN = true + sg.CommandsEnabled = true + sg.ForceTexturePacks = false + sg.LevelName = s.server.cfg.ServerName + sg.IsTrial = false + sg.CurrentTick = 0 + sg.EnchantmentSeed = 0 + s.SendPacket(sg) + + s.SendPacket(bedrock.NewSetTimePacket(6000)) + + adv := bedrock.NewAdventureSettingsPacket() + adv.Flags = 0x20 | 0x80 + adv.CommandPermission = 0 + adv.Flags2 = 0x00 + adv.PlayerPermission = 1 + adv.EntityID = 1 + s.SendPacket(adv) + + attr := bedrock.NewUpdateAttributesPacket(1) + attr.Attributes = bedrock.DefaultAttributes() + s.SendPacket(attr) +} + +func (s *Session) handleChunkRadius(pk *bedrock.RequestChunkRadiusPacket) { + radius := pk.Radius + if radius > 4 { + radius = 4 + } + if radius < 2 { + radius = 2 + } + s.ViewDistance = radius + s.SendPacket(bedrock.NewChunkRadiusUpdatedPacket(radius)) + s.sendChunks() +} + +func (s *Session) sendChunks() { + cx := int32(s.X) >> 4 + cz := int32(s.Z) >> 4 + r := s.ViewDistance + chunkData := bedrock.BuildFlatChunk() + + for x := cx - r; x <= cx+r; x++ { + for z := cz - r; z <= cz+r; z++ { + key := [2]int32{x, z} + if s.sentChunks[key] { + continue + } + s.sentChunks[key] = true + s.SendPacket(bedrock.NewFullChunkDataPacket(x, z, chunkData)) + } + } + + if !s.Spawned { + s.Spawned = true + s.SendPacket(bedrock.NewPlayStatusPacket(bedrock.PlayStatusPlayerSpawn)) + s.server.log.Infof("%s joined the game", s.Username) + s.server.BroadcastMessage(fmt.Sprintf("§e%s joined the game", s.Username)) + } +} + +func (s *Session) handleMovePlayer(pk *bedrock.MovePlayerPacket) { + s.X = pk.X + s.Y = pk.Y + s.Z = pk.Z +} + +func (s *Session) handleText(pk *bedrock.TextPacket) { + msg := pk.Message + if strings.HasPrefix(msg, "/") { + s.handleCommand(strings.TrimPrefix(msg, "/")) + return + } + s.server.log.Chat(s.Username, msg) + s.server.BroadcastMessage(fmt.Sprintf("<%s> %s", s.Username, msg)) +} + +func (s *Session) handleCommand(cmd string) { + args := strings.Fields(cmd) + if len(args) == 0 { + return + } + switch strings.ToLower(args[0]) { + case "list": + sessions := s.server.GetSessions() + names := make([]string, 0, len(sessions)) + for _, sess := range sessions { + if sess.Spawned { + names = append(names, sess.Username) + } + } + s.SendMessage(fmt.Sprintf("§eOnline (%d/%d): %s", len(names), s.server.cfg.MaxPlayers, strings.Join(names, ", "))) + case "gm": + if len(args) < 2 { + s.SendMessage("§cUsage: /gm <0|1|2>") + return + } + var gm int32 + switch args[1] { + case "0", "s", "survival": + gm = 0 + case "1", "c", "creative": + gm = 1 + case "2", "a", "adventure": + gm = 2 + default: + s.SendMessage("§cUsage: /gm <0|1|2>") + return + } + s.setGameMode(gm) + case "stop": + s.server.BroadcastMessage("§cServer is shutting down!") + s.server.Shutdown() + default: + s.SendMessage(fmt.Sprintf("§cUnknown command: %s", args[0])) + } +} + +func (s *Session) setGameMode(gm int32) { + s.SendPacket(bedrock.NewSetGameModePacket(gm)) + adv := bedrock.NewAdventureSettingsPacket() + if gm == 1 { + adv.Flags = 0x20 | 0x40 + adv.Flags2 = 0x40 + } else { + adv.Flags = 0x20 + adv.Flags2 = 0x00 + } + adv.CommandPermission = 0 + adv.PlayerPermission = 1 + adv.EntityID = 1 + s.SendPacket(adv) + names := map[int32]string{0: "Survival", 1: "Creative", 2: "Adventure"} + s.SendMessage(fmt.Sprintf("§aGame mode set to %s", names[gm])) +} + +func (s *Session) Disconnect(reason string) { + s.SendPacket(bedrock.NewDisconnectPacket(reason, false)) +} + +func (s *Session) SendMessage(msg string) { + s.SendPacket(bedrock.NewTextPacket(bedrock.TextTypeSystem, "", msg)) +} diff --git a/plugin/manager.go b/plugin/manager.go new file mode 100644 index 0000000..a5ee67d --- /dev/null +++ b/plugin/manager.go @@ -0,0 +1,66 @@ +package plugin + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" +) + +var ( + loaded atomic.Bool + plugins []*Plugin +) + +func Add(impl Impl) { + if loaded.Load() { + panic("cannot add plugin after server has started") + } + plugins = append(plugins, &Plugin{impl: impl}) +} + +func Initialize(log Logger, folder string) (func(context.Context) *sync.WaitGroup, error) { + if !loaded.CompareAndSwap(false, true) { + panic("plugins already initialized") + } + + log.Infof("Loading %d plugin(s)...", len(plugins)) + + names := map[string]struct{}{} + for _, p := range plugins { + key := strings.ToLower(p.impl.Name()) + if _, ok := names[key]; ok { + return nil, fmt.Errorf("duplicate plugin name: %s", p.impl.Name()) + } + names[key] = struct{}{} + } + + wd, err := os.Getwd() + if err != nil { + return nil, err + } + + for _, p := range plugins { + p.logger = log + p.directory = filepath.Join(wd, folder, p.impl.Name()) + if err := p.impl.Setup(p); err != nil { + return nil, fmt.Errorf("plugin '%s' setup failed: %w", p.impl.Name(), err) + } + log.Infof("Plugin '%s' loaded.", p.impl.Name()) + } + + return func(ctx context.Context) *sync.WaitGroup { + wg := &sync.WaitGroup{} + for _, p := range plugins { + wg.Add(1) + go func(p *Plugin) { + defer wg.Done() + p.impl.Run(ctx, p) + }(p) + } + return wg + }, nil +} diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000..a42607e --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,40 @@ +package plugin + +import ( + "context" + "fmt" + "os" +) + +type Impl interface { + Name() string + Setup(p *Plugin) error + Run(ctx context.Context, p *Plugin) +} + +type Plugin struct { + impl Impl + logger Logger + directory string + directoryCreated bool +} + +type Logger interface { + Infof(format string, args ...any) + Warnf(format string, args ...any) + Errorf(format string, args ...any) + Debugf(format string, args ...any) +} + +func (p *Plugin) Impl() Impl { return p.impl } +func (p *Plugin) Logger() Logger { return p.logger } + +func (p *Plugin) DataFolder() string { + if !p.directoryCreated { + if err := os.MkdirAll(p.directory, os.ModePerm); err != nil { + panic(fmt.Sprintf("failed to create data folder for plugin %s: %v", p.impl.Name(), err)) + } + p.directoryCreated = true + } + return p.directory +} diff --git a/raknet/const.go b/raknet/const.go new file mode 100644 index 0000000..2faa0b4 --- /dev/null +++ b/raknet/const.go @@ -0,0 +1,47 @@ +package raknet + +import "time" + +func RaknetMagic() []byte { + return []byte{0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78} +} + +const ( + MaxMTU = 1400 + MinMTU = 400 + MaxChannels = 32 +) + +var ( + SessionTimeout = 10 * time.Second + PingSendInterval = 2500 * time.Millisecond + DetectionSendInterval = 5 * time.Second + SendInterval = 50 * time.Millisecond + RecoverySendInterval = 50 * time.Millisecond +) + +type Reliability byte + +const ( + Unreliable Reliability = 0 + UnreliableSequenced Reliability = 1 + Reliable Reliability = 2 + ReliableOrdered Reliability = 3 + ReliableSequenced Reliability = 4 + UnreliableWithACKReceipt Reliability = 5 + ReliableWithACKReceipt Reliability = 6 + ReliableOrderedWithACKReceipt Reliability = 7 +) + +func (r Reliability) IsReliable() bool { + return r == Reliable || r == ReliableOrdered || r == ReliableSequenced || + r == ReliableWithACKReceipt || r == ReliableOrderedWithACKReceipt +} + +func (r Reliability) IsOrdered() bool { + return r == ReliableOrdered || r == ReliableOrderedWithACKReceipt +} + +func (r Reliability) IsSequenced() bool { + return r == UnreliableSequenced || r == ReliableSequenced +} diff --git a/raknet/datagram.go b/raknet/datagram.go new file mode 100644 index 0000000..df7a004 --- /dev/null +++ b/raknet/datagram.go @@ -0,0 +1,122 @@ +package raknet + +type Datagram struct { + SeqNum uint32 + Frames []*Frame +} + +func (d *Datagram) Encode() []byte { + s := NewStream() + s.WriteByte(0x84) + s.WriteUint24LE(d.SeqNum) + for _, f := range d.Frames { + f.Encode(s) + } + return s.Bytes() +} + +func DecodeDatagram(data []byte) (*Datagram, error) { + s := NewStreamBytes(data) + _, err := s.ReadByte() + if err != nil { + return nil, err + } + seq, err := s.ReadUint24LE() + if err != nil { + return nil, err + } + d := &Datagram{SeqNum: seq} + for s.Len() >= 3 { + f, err := DecodeFrame(s) + if err != nil { + break + } + d.Frames = append(d.Frames, f) + } + return d, nil +} + +func (d *Datagram) Size() int { + size := 4 + for _, f := range d.Frames { + size += f.Size() + } + return size +} + +func EncodeACK(seqNums []uint32) []byte { + return encodeAckNack(0xc0, seqNums) +} + +func EncodeNACK(seqNums []uint32) []byte { + return encodeAckNack(0xa0, seqNums) +} + +func encodeAckNack(id byte, seqNums []uint32) []byte { + if len(seqNums) == 0 { + s := NewStream() + s.WriteByte(id) + s.WriteUint16BE(0) + return s.Bytes() + } + + type run struct{ start, end uint32 } + var runs []run + cur := run{seqNums[0], seqNums[0]} + for _, n := range seqNums[1:] { + if n == cur.end+1 { + cur.end = n + } else { + runs = append(runs, cur) + cur = run{n, n} + } + } + runs = append(runs, cur) + + s := NewStream() + s.WriteByte(id) + s.WriteUint16BE(uint16(len(runs))) + for _, r := range runs { + if r.start == r.end { + s.WriteBool(true) + s.WriteUint24LE(r.start) + } else { + s.WriteBool(false) + s.WriteUint24LE(r.start) + s.WriteUint24LE(r.end) + } + } + return s.Bytes() +} + +func DecodeACKNACK(data []byte) ([]uint32, error) { + s := NewStreamBytes(data) + s.Skip(1) + count, err := s.ReadUint16BE() + if err != nil { + return nil, err + } + var nums []uint32 + for i := 0; i < int(count); i++ { + single, err := s.ReadBool() + if err != nil { + return nil, err + } + start, err := s.ReadUint24LE() + if err != nil { + return nil, err + } + if single { + nums = append(nums, start) + } else { + end, err := s.ReadUint24LE() + if err != nil { + return nil, err + } + for n := start; n <= end; n++ { + nums = append(nums, n) + } + } + } + return nums, nil +} diff --git a/raknet/frame.go b/raknet/frame.go new file mode 100644 index 0000000..50c2a62 --- /dev/null +++ b/raknet/frame.go @@ -0,0 +1,109 @@ +package raknet + +import "fmt" + +type Frame struct { + Reliability Reliability + Split bool + MessageIndex uint32 + OrderIndex uint32 + OrderChannel byte + SplitCount uint32 + SplitID uint16 + SplitIndex uint32 + Payload []byte +} + +func (f *Frame) Encode(s *Stream) { + flags := byte(f.Reliability) << 5 + if f.Split { + flags |= 0x10 + } + s.WriteByte(flags) + s.WriteUint16BE(uint16(len(f.Payload) * 8)) + + if f.Reliability.IsReliable() { + s.WriteUint24LE(f.MessageIndex) + } + if f.Reliability.IsOrdered() || f.Reliability.IsSequenced() { + s.WriteUint24LE(f.OrderIndex) + s.WriteByte(f.OrderChannel) + } + if f.Split { + s.WriteUint32BE(f.SplitCount) + s.WriteUint16BE(f.SplitID) + s.WriteUint32BE(f.SplitIndex) + } + s.WriteBytes(f.Payload) +} + +func DecodeFrame(s *Stream) (*Frame, error) { + flags, err := s.ReadByte() + if err != nil { + return nil, err + } + f := &Frame{} + f.Reliability = Reliability((flags >> 5) & 0x07) + f.Split = (flags & 0x10) != 0 + + bitLen, err := s.ReadUint16BE() + if err != nil { + return nil, err + } + length := int(bitLen / 8) + + if f.Reliability.IsReliable() { + f.MessageIndex, err = s.ReadUint24LE() + if err != nil { + return nil, err + } + } + if f.Reliability.IsOrdered() || f.Reliability.IsSequenced() { + f.OrderIndex, err = s.ReadUint24LE() + if err != nil { + return nil, err + } + f.OrderChannel, err = s.ReadByte() + if err != nil { + return nil, err + } + } + if f.Split { + sc, err := s.ReadUint32BE() + if err != nil { + return nil, err + } + f.SplitCount = sc + si, err := s.ReadUint16BE() + if err != nil { + return nil, err + } + f.SplitID = si + idx, err := s.ReadUint32BE() + if err != nil { + return nil, err + } + f.SplitIndex = idx + } + + f.Payload, err = s.ReadBytes(length) + if err != nil { + return nil, fmt.Errorf("frame payload: need %d, %w", length, err) + } + return f, nil +} + +func (f *Frame) Size() int { + size := 3 + if f.Reliability.IsReliable() { + size += 3 + } + if f.Reliability.IsOrdered() || f.Reliability.IsSequenced() { + size += 4 + } + if f.Split { + size += 10 + } + size += len(f.Payload) + return size +} diff --git a/raknet/packets.go b/raknet/packets.go new file mode 100644 index 0000000..661cc9f --- /dev/null +++ b/raknet/packets.go @@ -0,0 +1,166 @@ +package raknet + +import ( + "encoding/binary" + "net" +) + +func BuildUnconnectedPong(timestamp int64, serverGUID int64, motd string) []byte { + s := NewStream() + s.WriteByte(0x1c) + s.WriteInt64BE(timestamp) + s.WriteInt64BE(serverGUID) + s.WriteMagic() + s.WriteString(motd) + return s.Bytes() +} + +func BuildOpenConnectionReply1(serverGUID int64, mtu uint16) []byte { + s := NewStream() + s.WriteByte(0x06) + s.WriteMagic() + s.WriteInt64BE(serverGUID) + s.WriteBool(false) + s.WriteUint16BE(mtu) + return s.Bytes() +} + +func BuildOpenConnectionReply2(serverGUID int64, clientAddr net.UDPAddr, mtu uint16) []byte { + s := NewStream() + s.WriteByte(0x08) + s.WriteMagic() + s.WriteInt64BE(serverGUID) + s.WriteAddress(clientAddr) + s.WriteUint16BE(mtu) + s.WriteBool(false) + return s.Bytes() +} + +func BuildConnectionRequestAccepted(clientAddr net.UDPAddr, clientTimestamp int64, serverTimestamp int64) []byte { + s := NewStream() + s.WriteByte(0x10) + s.WriteAddress(clientAddr) + s.WriteUint16BE(0) + for i := 0; i < 10; i++ { + s.WriteAddress(net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0}) + } + s.WriteInt64BE(clientTimestamp) + s.WriteInt64BE(serverTimestamp) + return s.Bytes() +} + +func BuildConnectedPong(pingTime int64, pongTime int64) []byte { + s := NewStream() + s.WriteByte(0x03) + s.WriteInt64BE(pingTime) + s.WriteInt64BE(pongTime) + return s.Bytes() +} + +func ParseUnconnectedPing(data []byte) (timestamp int64, guid int64, ok bool) { + if len(data) < 1+8+16 { + return + } + timestamp = int64(binary.BigEndian.Uint64(data[1:9])) + magic := RaknetMagic() + for i, b := range magic { + if data[9+i] != b { + return + } + } + if len(data) >= 1+8+16+8 { + guid = int64(binary.BigEndian.Uint64(data[25:33])) + } + ok = true + return +} + +func ParseOCR1(data []byte) (proto byte, mtu uint16, ok bool) { + if len(data) < 1+16+1 { + return + } + magic := RaknetMagic() + for i, b := range magic { + if data[1+i] != b { + return + } + } + proto = data[17] + mtu = uint16(len(data) + 28) + ok = true + return +} + +func ParseOCR2(data []byte) (clientGUID int64, clientAddr net.UDPAddr, mtu uint16, ok bool) { + s := NewStreamBytes(data) + s.Skip(1) + magic := RaknetMagic() + mb, err := s.ReadBytes(16) + if err != nil { + return + } + for i, b := range magic { + if mb[i] != b { + return + } + } + addr, err := s.ReadAddress() + if err != nil { + return + } + clientAddr = addr + mtuV, err := s.ReadUint16BE() + if err != nil { + return + } + mtu = mtuV + guidV, err := s.ReadInt64BE() + if err != nil { + return + } + clientGUID = guidV + ok = true + return +} + +func ParseConnectionRequest(payload []byte) (clientGUID int64, timestamp int64, ok bool) { + if len(payload) < 17 { + return + } + if payload[0] != 0x09 { + return + } + s := NewStreamBytes(payload[1:]) + g, err := s.ReadInt64BE() + if err != nil { + return + } + t, err := s.ReadInt64BE() + if err != nil { + return + } + clientGUID = g + timestamp = t + ok = true + return +} + +func IsNewIncomingConnection(payload []byte) bool { + return len(payload) > 0 && payload[0] == 0x13 +} + +func IsDisconnect(payload []byte) bool { + return len(payload) > 0 && payload[0] == 0x15 +} + +func ParseConnectedPing(payload []byte) (timestamp int64, ok bool) { + if len(payload) < 9 || payload[0] != 0x00 { + return + } + s := NewStreamBytes(payload[1:]) + t, err := s.ReadInt64BE() + if err != nil { + return + } + return t, true +} diff --git a/raknet/server.go b/raknet/server.go new file mode 100644 index 0000000..bf4d7d9 --- /dev/null +++ b/raknet/server.go @@ -0,0 +1,352 @@ +package raknet + +import ( + "net" + "sync" + "time" +) + +type Handler interface { + OnConnect(s *Session) + OnPacket(s *Session, payload []byte) + OnDisconnect(s *Session) +} + +type Server struct { + mu sync.RWMutex + conn *net.UDPConn + sessions map[string]*Session + guid int64 + motdFunc func() string + handler Handler + running bool +} + +func NewServer(guid int64, motdFunc func() string, handler Handler) *Server { + return &Server{ + guid: guid, + motdFunc: motdFunc, + handler: handler, + sessions: make(map[string]*Session), + } +} + +func (s *Server) Start(ip string, port int) error { + addr := &net.UDPAddr{IP: net.ParseIP(ip), Port: port} + conn, err := net.ListenUDP("udp4", addr) + if err != nil { + return err + } + s.conn = conn + s.running = true + go s.readLoop() + go s.tickLoop() + return nil +} + +func (s *Server) Shutdown() { + s.running = false + if s.conn != nil { + s.conn.Close() + } +} + +func (s *Server) readLoop() { + buf := make([]byte, 2048) + for s.running { + n, addr, err := s.conn.ReadFromUDP(buf) + if err != nil { + if !s.running { + return + } + continue + } + data := make([]byte, n) + copy(data, buf[:n]) + go func(a *net.UDPAddr, d []byte) { + defer func() { recover() }() + s.handlePacket(a, d) + }(addr, data) + } +} + +func (s *Server) tickLoop() { + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + for s.running { + <-ticker.C + s.tick() + } +} + +func (s *Server) tick() { + s.mu.RLock() + sessions := make([]*Session, 0, len(s.sessions)) + for _, sess := range s.sessions { + sessions = append(sessions, sess) + } + s.mu.RUnlock() + + for _, sess := range sessions { + sess.FlushACKs() + if sess.TimedOut() { + s.removeSession(sess) + if s.handler != nil { + s.handler.OnDisconnect(sess) + } + } + } +} + +func (s *Server) handlePacket(addr *net.UDPAddr, data []byte) { + if len(data) == 0 { + return + } + id := data[0] + + switch { + case id == 0x01 || id == 0x02: + ts, _, ok := ParseUnconnectedPing(data) + if !ok { + return + } + pong := BuildUnconnectedPong(ts, s.guid, s.motdFunc()) + s.conn.WriteToUDP(pong, addr) + + case id == 0x05: + proto, mtu, ok := ParseOCR1(data) + if !ok || proto != 8 { + return + } + if mtu > MaxMTU { + mtu = MaxMTU + } + s.conn.WriteToUDP(BuildOpenConnectionReply1(s.guid, mtu), addr) + + case id == 0x07: + clientGUID, _, mtu, ok := ParseOCR2(data) + if !ok { + return + } + if mtu > MaxMTU { + mtu = MaxMTU + } + key := addr.String() + s.mu.Lock() + if _, exists := s.sessions[key]; exists { + s.mu.Unlock() + return + } + sess := NewSession(s.conn, addr, clientGUID, mtu) + s.sessions[key] = sess + s.mu.Unlock() + s.conn.WriteToUDP(BuildOpenConnectionReply2(s.guid, *addr, mtu), addr) + + case id >= 0x80 && id <= 0x8f: + sess := s.getSession(addr) + if sess == nil || sess.IsDisconnected() { + return + } + s.handleDatagram(sess, data) + + case id == 0xc0: + sess := s.getSession(addr) + if sess != nil { + sess.HandleACK(data) + } + + case id == 0xa0: + sess := s.getSession(addr) + if sess != nil { + sess.HandleNACK(data) + } + } +} + +func (s *Server) handleDatagram(sess *Session, data []byte) { + dg, err := DecodeDatagram(data) + if err != nil { + return + } + + sess.mu.Lock() + sess.lastRecv = time.Now() + + if sess.recvSeqs[dg.SeqNum] { + sess.mu.Unlock() + return + } + sess.recvSeqs[dg.SeqNum] = true + sess.pendingACK = append(sess.pendingACK, dg.SeqNum) + + if dg.SeqNum > sess.recvSeq+1 { + for n := sess.recvSeq + 1; n < dg.SeqNum; n++ { + if !sess.recvSeqs[n] { + sess.pendingNACK = append(sess.pendingNACK, n) + } + } + } + if dg.SeqNum >= sess.recvSeq { + sess.recvSeq = dg.SeqNum + 1 + } + sess.mu.Unlock() + + for _, f := range dg.Frames { + s.handleFrame(sess, f) + } +} + +func (s *Server) handleFrame(sess *Session, f *Frame) { + if f.Split { + sess.mu.Lock() + buf, ok := sess.splitMap[f.SplitID] + if !ok { + buf = &splitBuf{count: f.SplitCount, parts: make(map[uint32][]byte)} + sess.splitMap[f.SplitID] = buf + } + buf.parts[f.SplitIndex] = f.Payload + assembled := false + var full []byte + if uint32(len(buf.parts)) >= buf.count { + for i := uint32(0); i < buf.count; i++ { + full = append(full, buf.parts[i]...) + } + delete(sess.splitMap, f.SplitID) + assembled = true + } + sess.mu.Unlock() + if assembled { + combined := &Frame{ + Reliability: f.Reliability, + MessageIndex: f.MessageIndex, + OrderIndex: f.OrderIndex, + OrderChannel: f.OrderChannel, + Payload: full, + } + s.handleFrame(sess, combined) + } + return + } + + if f.Reliability.IsOrdered() { + ch := int(f.OrderChannel) + sess.mu.Lock() + expected := sess.orderNext[ch] + if f.OrderIndex == expected { + sess.orderNext[ch]++ + payload := f.Payload + var extras [][]byte + for { + next := sess.orderQueues[ch][sess.orderNext[ch]] + if next == nil { + break + } + delete(sess.orderQueues[ch], sess.orderNext[ch]) + sess.orderNext[ch]++ + extras = append(extras, next) + } + sess.mu.Unlock() + s.dispatch(sess, payload) + for _, e := range extras { + s.dispatch(sess, e) + } + } else if f.OrderIndex > expected { + sess.orderQueues[ch][f.OrderIndex] = f.Payload + sess.mu.Unlock() + } else { + sess.mu.Unlock() + } + return + } + + s.dispatch(sess, f.Payload) +} + +func (s *Server) dispatch(sess *Session, payload []byte) { + if len(payload) == 0 { + return + } + id := payload[0] + + if id == 0x09 { + _, timestamp, ok := ParseConnectionRequest(payload) + if !ok { + return + } + accepted := BuildConnectionRequestAccepted(*sess.Addr, timestamp, sess.Timestamp()) + sess.Send(accepted, ReliableOrdered, 0) + return + } + + if id == 0x13 { + sess.SetConnected() + if s.handler != nil { + s.handler.OnConnect(sess) + } + return + } + + if id == 0x15 { + s.removeSession(sess) + if s.handler != nil { + s.handler.OnDisconnect(sess) + } + return + } + + if id == 0x00 { + t, ok := ParseConnectedPing(payload) + if ok { + pong := BuildConnectedPong(t, sess.Timestamp()) + sess.Send(pong, Unreliable, 0) + } + return + } + + if !sess.IsConnected() { + return + } + + if s.handler != nil { + s.handler.OnPacket(sess, payload) + } +} + +func (s *Server) getSession(addr *net.UDPAddr) *Session { + s.mu.RLock() + defer s.mu.RUnlock() + return s.sessions[addr.String()] +} + +func (s *Server) removeSession(sess *Session) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.sessions, sess.Addr.String()) +} + +func (s *Server) GetSessionByGUID(guid int64) *Session { + s.mu.RLock() + defer s.mu.RUnlock() + for _, sess := range s.sessions { + if sess.GUID == guid { + return sess + } + } + return nil +} + +func (s *Server) Count() int { + s.mu.RLock() + defer s.mu.RUnlock() + return len(s.sessions) +} + +func (s *Server) Sessions() []*Session { + s.mu.RLock() + defer s.mu.RUnlock() + list := make([]*Session, 0, len(s.sessions)) + for _, sess := range s.sessions { + list = append(list, sess) + } + return list +} diff --git a/raknet/session.go b/raknet/session.go new file mode 100644 index 0000000..a6ad60c --- /dev/null +++ b/raknet/session.go @@ -0,0 +1,244 @@ +package raknet + +import ( + "net" + "sync" + "time" +) + +type SessionState int + +const ( + StateHandshaking SessionState = iota + StateConnected + StateDisconnected +) + +type Session struct { + mu sync.Mutex + Addr *net.UDPAddr + GUID int64 + MTU uint16 + conn *net.UDPConn + + state SessionState + + sendSeq uint32 + recvSeq uint32 + msgIndex uint32 + + orderIndex [MaxChannels]uint32 + orderNext [MaxChannels]uint32 + orderQueues [MaxChannels]map[uint32][]byte + + recvSeqs map[uint32]bool + pendingACK []uint32 + pendingNACK []uint32 + + splitMap map[uint16]*splitBuf + + recoveryMap map[uint32]*Datagram + + lastRecv time.Time + connTime time.Time +} + +type splitBuf struct { + count uint32 + parts map[uint32][]byte +} + +func NewSession(conn *net.UDPConn, addr *net.UDPAddr, guid int64, mtu uint16) *Session { + s := &Session{ + conn: conn, + Addr: addr, + GUID: guid, + MTU: mtu, + state: StateHandshaking, + recvSeqs: make(map[uint32]bool), + splitMap: make(map[uint16]*splitBuf), + recoveryMap: make(map[uint32]*Datagram), + lastRecv: time.Now(), + } + for i := 0; i < MaxChannels; i++ { + s.orderQueues[i] = make(map[uint32][]byte) + } + return s +} + +func (s *Session) IsConnected() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.state == StateConnected +} + +func (s *Session) IsDisconnected() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.state == StateDisconnected +} + +func (s *Session) SetConnected() { + s.mu.Lock() + defer s.mu.Unlock() + s.state = StateConnected + s.connTime = time.Now() +} + +func (s *Session) Timestamp() int64 { + if s.connTime.IsZero() { + return 0 + } + return time.Since(s.connTime).Milliseconds() +} + +func (s *Session) HandleACK(data []byte) { + nums, err := DecodeACKNACK(data) + if err != nil { + return + } + s.mu.Lock() + defer s.mu.Unlock() + for _, n := range nums { + delete(s.recoveryMap, n) + } +} + +func (s *Session) HandleNACK(data []byte) { + nums, err := DecodeACKNACK(data) + if err != nil { + return + } + s.mu.Lock() + defer s.mu.Unlock() + for _, n := range nums { + dg, ok := s.recoveryMap[n] + if ok { + s.conn.WriteToUDP(dg.Encode(), s.Addr) + } + } +} + +func (s *Session) FlushACKs() { + s.mu.Lock() + acks := s.pendingACK + nacks := s.pendingNACK + s.pendingACK = nil + s.pendingNACK = nil + s.mu.Unlock() + + if len(acks) > 0 { + s.conn.WriteToUDP(EncodeACK(acks), s.Addr) + } + if len(nacks) > 0 { + s.conn.WriteToUDP(EncodeNACK(nacks), s.Addr) + } +} + +func (s *Session) Send(payload []byte, reliability Reliability, orderChannel byte) { + s.mu.Lock() + defer s.mu.Unlock() + + maxPayload := int(s.MTU) - 4 - 3 + if reliability.IsReliable() { + maxPayload -= 3 + } + if reliability.IsOrdered() || reliability.IsSequenced() { + maxPayload -= 4 + } + if maxPayload < 100 { + maxPayload = 100 + } + + if len(payload) <= maxPayload { + f := &Frame{Reliability: reliability, Payload: payload} + if reliability.IsReliable() { + f.MessageIndex = s.msgIndex + s.msgIndex++ + } + if reliability.IsOrdered() { + f.OrderIndex = s.orderIndex[orderChannel] + f.OrderChannel = orderChannel + s.orderIndex[orderChannel]++ + } + s.sendDatagram([]*Frame{f}) + return + } + + splitID := s.splitID() + count := (len(payload) + maxPayload - 1) / maxPayload + orderIdx := s.orderIndex[orderChannel] + if reliability.IsOrdered() { + s.orderIndex[orderChannel]++ + } + for i := 0; i < count; i++ { + start := i * maxPayload + end := start + maxPayload + if end > len(payload) { + end = len(payload) + } + f := &Frame{ + Reliability: reliability, + Split: true, + SplitCount: uint32(count), + SplitID: splitID, + SplitIndex: uint32(i), + Payload: make([]byte, end-start), + } + copy(f.Payload, payload[start:end]) + if reliability.IsReliable() { + f.MessageIndex = s.msgIndex + s.msgIndex++ + } + if reliability.IsOrdered() { + f.OrderIndex = orderIdx + f.OrderChannel = orderChannel + } + s.sendDatagram([]*Frame{f}) + } +} + +func (s *Session) splitID() uint16 { + id := uint16(s.msgIndex & 0xffff) + return id +} + +func (s *Session) sendDatagram(frames []*Frame) { + seq := s.sendSeq + s.sendSeq++ + dg := &Datagram{SeqNum: seq, Frames: frames} + pkt := dg.Encode() + s.conn.WriteToUDP(pkt, s.Addr) + + hasReliable := false + for _, f := range frames { + if f.Reliability.IsReliable() { + hasReliable = true + break + } + } + if hasReliable { + s.recoveryMap[seq] = dg + } +} + +func (s *Session) SendRaw(data []byte) { + s.conn.WriteToUDP(data, s.Addr) +} + +func (s *Session) TimedOut() bool { + s.mu.Lock() + defer s.mu.Unlock() + return time.Since(s.lastRecv) > SessionTimeout +} + +func (s *Session) Disconnect() { + s.mu.Lock() + s.state = StateDisconnected + seq := s.sendSeq + s.sendSeq++ + s.mu.Unlock() + f := &Frame{Reliability: Unreliable, Payload: []byte{0x15}} + dg := &Datagram{SeqNum: seq, Frames: []*Frame{f}} + s.conn.WriteToUDP(dg.Encode(), s.Addr) +} diff --git a/raknet/stream.go b/raknet/stream.go new file mode 100644 index 0000000..da97d76 --- /dev/null +++ b/raknet/stream.go @@ -0,0 +1,242 @@ +package raknet + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + "strings" + "strconv" +) + +type Stream struct { + buf []byte + pos int +} + +func NewStream() *Stream { + return &Stream{} +} + +func NewStreamBytes(b []byte) *Stream { + c := make([]byte, len(b)) + copy(c, b) + return &Stream{buf: c} +} + +func (s *Stream) Bytes() []byte { + return s.buf +} + +func (s *Stream) Len() int { + return len(s.buf) - s.pos +} + +func (s *Stream) Reset() { + s.buf = s.buf[:0] + s.pos = 0 +} + +func (s *Stream) Skip(n int) { + s.pos += n +} + +func (s *Stream) ReadByte() (byte, error) { + if s.pos >= len(s.buf) { + return 0, errors.New("stream underflow") + } + b := s.buf[s.pos] + s.pos++ + return b, nil +} + +func (s *Stream) ReadBytes(n int) ([]byte, error) { + if s.pos+n > len(s.buf) { + return nil, fmt.Errorf("stream underflow: need %d have %d", n, len(s.buf)-s.pos) + } + b := s.buf[s.pos : s.pos+n] + s.pos += n + return b, nil +} + +func (s *Stream) ReadUint16BE() (uint16, error) { + b, err := s.ReadBytes(2) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b), nil +} + +func (s *Stream) ReadUint32BE() (uint32, error) { + b, err := s.ReadBytes(4) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint32(b), nil +} + +func (s *Stream) ReadInt32BE() (int32, error) { + v, err := s.ReadUint32BE() + return int32(v), err +} + +func (s *Stream) ReadInt64BE() (int64, error) { + b, err := s.ReadBytes(8) + if err != nil { + return 0, err + } + return int64(binary.BigEndian.Uint64(b)), nil +} + +func (s *Stream) ReadUint24LE() (uint32, error) { + b, err := s.ReadBytes(3) + if err != nil { + return 0, err + } + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16, nil +} + +func (s *Stream) ReadBool() (bool, error) { + b, err := s.ReadByte() + return b != 0, err +} + +func (s *Stream) ReadString() (string, error) { + n, err := s.ReadUint16BE() + if err != nil { + return "", err + } + b, err := s.ReadBytes(int(n)) + if err != nil { + return "", err + } + return string(b), nil +} + +func (s *Stream) ReadMagic() (bool, error) { + b, err := s.ReadBytes(16) + if err != nil { + return false, err + } + expected := RaknetMagic() + for i, v := range expected { + if b[i] != v { + return false, nil + } + } + return true, nil +} + +func (s *Stream) ReadAddress() (net.UDPAddr, error) { + ver, err := s.ReadByte() + if err != nil { + return net.UDPAddr{}, err + } + if ver == 4 { + ip := make([]byte, 4) + for i := 0; i < 4; i++ { + b, err := s.ReadByte() + if err != nil { + return net.UDPAddr{}, err + } + ip[i] = ^b + } + port, err := s.ReadUint16BE() + if err != nil { + return net.UDPAddr{}, err + } + return net.UDPAddr{IP: net.IP(ip), Port: int(port)}, nil + } + b, err := s.ReadBytes(18) + if err != nil { + return net.UDPAddr{}, err + } + port := binary.BigEndian.Uint16(b[0:2]) + ip := net.IP(b[2:18]) + return net.UDPAddr{IP: ip, Port: int(port)}, nil +} + +func (s *Stream) WriteByte(v byte) { + s.buf = append(s.buf, v) +} + +func (s *Stream) WriteBytes(v []byte) { + s.buf = append(s.buf, v...) +} + +func (s *Stream) WriteUint16BE(v uint16) { + s.buf = append(s.buf, byte(v>>8), byte(v)) +} + +func (s *Stream) WriteUint32BE(v uint32) { + s.buf = append(s.buf, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) +} + +func (s *Stream) WriteInt32BE(v int32) { + s.WriteUint32BE(uint32(v)) +} + +func (s *Stream) WriteInt64BE(v int64) { + s.buf = append(s.buf, + byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), + byte(v>>24), byte(v>>16), byte(v>>8), byte(v), + ) +} + +func (s *Stream) WriteUint24LE(v uint32) { + s.buf = append(s.buf, byte(v), byte(v>>8), byte(v>>16)) +} + +func (s *Stream) WriteBool(v bool) { + if v { + s.WriteByte(1) + } else { + s.WriteByte(0) + } +} + +func (s *Stream) WriteString(v string) { + b := []byte(v) + s.WriteUint16BE(uint16(len(b))) + s.WriteBytes(b) +} + +func (s *Stream) WriteMagic() { + s.WriteBytes(RaknetMagic()) +} + +func (s *Stream) WriteAddress(addr net.UDPAddr) { + ip := addr.IP.To4() + if ip != nil { + s.WriteByte(4) + for _, b := range ip { + s.WriteByte(^b) + } + s.WriteUint16BE(uint16(addr.Port)) + } else { + s.WriteByte(6) + s.WriteUint16BE(uint16(addr.Port)) + s.WriteUint32BE(0) + s.WriteBytes(addr.IP.To16()) + s.WriteUint32BE(0) + } +} + +func (s *Stream) WriteAddressRaw(ipStr string, port uint16) { + if strings.Contains(ipStr, ".") { + parts := strings.Split(ipStr, ".") + s.WriteByte(4) + for _, p := range parts { + v, _ := strconv.Atoi(p) + s.WriteByte(^byte(v)) + } + s.WriteUint16BE(port) + } else { + ip := net.ParseIP(ipStr) + s.WriteByte(6) + s.WriteUint16BE(port) + s.WriteUint32BE(0) + s.WriteBytes(ip.To16()) + s.WriteUint32BE(0) + } +} diff --git a/resources/config.go b/resources/config.go new file mode 100644 index 0000000..d838999 --- /dev/null +++ b/resources/config.go @@ -0,0 +1,65 @@ +package resources + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v2" +) + +type ConsoleConfig struct { + Debug bool `yaml:"debug"` +} + +type PluginsConfig struct { + Folder string `yaml:"folder"` +} + +type Config struct { + ServerName string `yaml:"server-name"` + ServerIP string `yaml:"server-ip"` + ServerPort int `yaml:"server-port"` + MaxPlayers int `yaml:"max-players"` + MOTD string `yaml:"motd"` + MotdProtocol int `yaml:"motd-protocol"` + Console ConsoleConfig `yaml:"console"` + Plugins PluginsConfig `yaml:"plugins"` +} + +func DefaultConfig() *Config { + return &Config{ + ServerName: "Frodding Server", + ServerIP: "0.0.0.0", + ServerPort: 19132, + MaxPlayers: 20, + MOTD: "A Frodding Server", + MotdProtocol: 0, + Console: ConsoleConfig{Debug: false}, + Plugins: PluginsConfig{Folder: "plugins"}, + } +} + +func LoadConfig(path string) (*Config, error) { + cfg := DefaultConfig() + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + _ = SaveConfig(path, cfg) + fmt.Println("[Frodding] Created default server.yml") + return cfg, nil + } + return cfg, err + } + if err := yaml.Unmarshal(data, cfg); err != nil { + return cfg, err + } + return cfg, nil +} + +func SaveConfig(path string, cfg *Config) error { + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..d96f382 --- /dev/null +++ b/run.bat @@ -0,0 +1,3 @@ +@echo off +go mod tidy +go run ./cmd/frodding diff --git a/server.go b/server.go new file mode 100644 index 0000000..54ea509 --- /dev/null +++ b/server.go @@ -0,0 +1,107 @@ +package frodding + +import ( + "context" + "fmt" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/cubexteam/Frodding/logger" + "github.com/cubexteam/Frodding/network" + "github.com/cubexteam/Frodding/plugin" + "github.com/cubexteam/Frodding/resources" +) + +const ( + Name = "Frodding" + Version = "1.0.0" + Author = "SantianDev, bota" +) + +type Server struct { + Config *resources.Config + Log *logger.Logger + Network *network.Server + Running bool +} + +func NewServer(configPath string) (*Server, error) { + cfg, err := resources.LoadConfig(configPath) + if err != nil { + return nil, fmt.Errorf("failed to load config: %w", err) + } + log := logger.New(cfg.Console.Debug) + netSrv := network.NewServer(cfg, log) + return &Server{ + Config: cfg, + Log: log, + Network: netSrv, + }, nil +} + +func (s *Server) Start() error { + s.printBanner() + + runPlugins, err := plugin.Initialize(s.Log, s.Config.Plugins.Folder) + if err != nil { + return fmt.Errorf("plugin init failed: %w", err) + } + + s.Running = true + + s.Network.SetShutdownHook(func() { + s.Running = false + }) + + if err := s.Network.Start(); err != nil { + return err + } + + ctx, stopPlugins := context.WithCancel(context.Background()) + var wg *sync.WaitGroup + wg = runPlugins(ctx) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + + for s.Running { + select { + case <-sigChan: + fmt.Println() + s.Log.Infof("Shutting down...") + s.Stop() + case <-ticker.C: + } + } + + stopPlugins() + wg.Wait() + return nil +} + +func (s *Server) Stop() { + s.Running = false + s.Network.Shutdown() + s.Log.Infof("Server stopped.") +} + +func (s *Server) printBanner() { + fmt.Printf("\n") + fmt.Printf(" ███████╗██████╗ ██████╗ ██████╗ ██████╗ ██╗███╗ ██╗ ██████╗ \n") + fmt.Printf(" ██╔════╝██╔══██╗██╔═══██╗██╔══██╗██╔══██╗██║████╗ ██║██╔════╝ \n") + fmt.Printf(" █████╗ ██████╔╝██║ ██║██║ ██║██║ ██║██║██╔██╗██║██║ ███╗\n") + fmt.Printf(" ██╔══╝ ██╔══██╗██║ ██║██║ ██║██║ ██║██║██║╚████║██║ ██║\n") + fmt.Printf(" ██║ ██║ ██║╚██████╔╝██████╔╝██████╔╝██║██║ ╚███║╚██████╔╝\n") + fmt.Printf(" ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚══╝ ╚═════╝ \n") + fmt.Printf("\n") + fmt.Printf(" %s v%s by %s\n", Name, Version, Author) + fmt.Printf(" MCBE 1.1.x (Protocol 113)\n") + fmt.Printf(" https://github.com/cubexteam/Frodding\n") + fmt.Printf("\n") +} diff --git a/server.yml b/server.yml new file mode 100644 index 0000000..843630b --- /dev/null +++ b/server.yml @@ -0,0 +1,13 @@ +server-name: Frodding Server +server-ip: 0.0.0.0 +server-port: 19132 +max-players: 20 +motd: A Frodding Server +# Протокол в списке серверов. Меняй под версию клиентов: +# 105=1.1.0 | 107=1.1.3 | 110=1.1.5 | 111=1.1.6 | 113=1.1.7 +# 0 = автоматически последний (113) +motd-protocol: 110 +console: + debug: true +plugins: + folder: plugins