Initial commit

This commit is contained in:
SantianDev
2026-03-03 15:05:55 +03:00
commit a9c89f027a
37 changed files with 3256 additions and 0 deletions

4
build.bat Normal file
View File

@@ -0,0 +1,4 @@
@echo off
go mod tidy
go build -o frodding.exe ./cmd/frodding
echo Build complete: frodding.exe

28
cmd/frodding/main.go Normal file
View File

@@ -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)
}
}

8
go.mod Normal file
View File

@@ -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
)

6
go.sum Normal file
View File

@@ -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=

56
logger/logger.go Normal file
View File

@@ -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)
}

81
net/info/info.go Normal file
View File

@@ -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
)

View File

@@ -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()
}

View File

@@ -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() {}

View File

@@ -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()
}
}

View File

@@ -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() {}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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() {}

View File

@@ -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() {}

View File

@@ -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() {}

View File

@@ -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() {}

View File

@@ -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() {}

View File

@@ -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()
}
}

View File

@@ -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"},
}
}

195
net/packets/packet.go Normal file
View File

@@ -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
}

133
network/server.go Normal file
View File

@@ -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
}

361
network/session.go Normal file
View File

@@ -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))
}

66
plugin/manager.go Normal file
View File

@@ -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
}

40
plugin/plugin.go Normal file
View File

@@ -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
}

47
raknet/const.go Normal file
View File

@@ -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
}

122
raknet/datagram.go Normal file
View File

@@ -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
}

109
raknet/frame.go Normal file
View File

@@ -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
}

166
raknet/packets.go Normal file
View File

@@ -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
}

352
raknet/server.go Normal file
View File

@@ -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
}

244
raknet/session.go Normal file
View File

@@ -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)
}

242
raknet/stream.go Normal file
View File

@@ -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)
}
}

65
resources/config.go Normal file
View File

@@ -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)
}

3
run.bat Normal file
View File

@@ -0,0 +1,3 @@
@echo off
go mod tidy
go run ./cmd/frodding

107
server.go Normal file
View File

@@ -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")
}

13
server.yml Normal file
View File

@@ -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