Files
Frodding/network/session.go
2026-03-03 15:05:55 +03:00

362 lines
8.2 KiB
Go

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