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