Initial commit
This commit is contained in:
4
build.bat
Normal file
4
build.bat
Normal 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
28
cmd/frodding/main.go
Normal 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
8
go.mod
Normal 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
6
go.sum
Normal 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
56
logger/logger.go
Normal 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
81
net/info/info.go
Normal 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
|
||||
)
|
||||
39
net/packets/bedrock/adventure_settings.go
Normal file
39
net/packets/bedrock/adventure_settings.go
Normal 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()
|
||||
}
|
||||
41
net/packets/bedrock/chunk_radius.go
Normal file
41
net/packets/bedrock/chunk_radius.go
Normal 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() {}
|
||||
35
net/packets/bedrock/disconnect.go
Normal file
35
net/packets/bedrock/disconnect.go
Normal 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()
|
||||
}
|
||||
}
|
||||
97
net/packets/bedrock/full_chunk_data.go
Normal file
97
net/packets/bedrock/full_chunk_data.go
Normal 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() {}
|
||||
165
net/packets/bedrock/login.go
Normal file
165
net/packets/bedrock/login.go
Normal 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)
|
||||
}
|
||||
}
|
||||
47
net/packets/bedrock/move_player.go
Normal file
47
net/packets/bedrock/move_player.go
Normal 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()
|
||||
}
|
||||
38
net/packets/bedrock/play_status.go
Normal file
38
net/packets/bedrock/play_status.go
Normal 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()
|
||||
}
|
||||
37
net/packets/bedrock/resource_pack_client_response.go
Normal file
37
net/packets/bedrock/resource_pack_client_response.go
Normal 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()
|
||||
}
|
||||
}
|
||||
27
net/packets/bedrock/resource_pack_info.go
Normal file
27
net/packets/bedrock/resource_pack_info.go
Normal 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() {}
|
||||
27
net/packets/bedrock/resource_pack_stack.go
Normal file
27
net/packets/bedrock/resource_pack_stack.go
Normal 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() {}
|
||||
24
net/packets/bedrock/set_game_mode.go
Normal file
24
net/packets/bedrock/set_game_mode.go
Normal 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() {}
|
||||
25
net/packets/bedrock/set_time.go
Normal file
25
net/packets/bedrock/set_time.go
Normal 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() {}
|
||||
92
net/packets/bedrock/start_game.go
Normal file
92
net/packets/bedrock/start_game.go
Normal 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() {}
|
||||
61
net/packets/bedrock/text.go
Normal file
61
net/packets/bedrock/text.go
Normal 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()
|
||||
}
|
||||
}
|
||||
53
net/packets/bedrock/update_attributes.go
Normal file
53
net/packets/bedrock/update_attributes.go
Normal 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
195
net/packets/packet.go
Normal 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
133
network/server.go
Normal 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
361
network/session.go
Normal 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
66
plugin/manager.go
Normal 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
40
plugin/plugin.go
Normal 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
47
raknet/const.go
Normal 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
122
raknet/datagram.go
Normal 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
109
raknet/frame.go
Normal 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
166
raknet/packets.go
Normal 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
352
raknet/server.go
Normal 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
244
raknet/session.go
Normal 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
242
raknet/stream.go
Normal 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
65
resources/config.go
Normal 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)
|
||||
}
|
||||
107
server.go
Normal file
107
server.go
Normal 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
13
server.yml
Normal 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
|
||||
Reference in New Issue
Block a user