diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/chat.go | 61 | ||||
| -rw-r--r-- | server/commands.go | 1 | ||||
| -rw-r--r-- | server/coords.go | 34 | ||||
| -rw-r--r-- | server/data.go | 87 | ||||
| -rw-r--r-- | server/level.go | 266 | ||||
| -rw-r--r-- | server/player.go | 242 | ||||
| -rw-r--r-- | server/server.go | 396 |
7 files changed, 1087 insertions, 0 deletions
diff --git a/server/chat.go b/server/chat.go new file mode 100644 index 0000000..a32da17 --- /dev/null +++ b/server/chat.go @@ -0,0 +1,61 @@ +package server + +import ( + "strings" + "git.citrons.xyz/metronode/classic" +) + +func processChatMessage(message string) []classic.Packet { + var ( + packets []classic.Packet + in = strings.NewReader(message) + line strings.Builder + word strings.Builder + color byte = 'f' + ) + for in.Len() > 0 { + for { + b, err := in.ReadByte() + if b > 127 { + b = '?' + } + if err == nil { + if b == '&' || b == '%' { + if word.Len() + 1 >= 64 { + line.WriteString(word.String()) + word.Reset() + } + next, err := in.ReadByte() + in.UnreadByte() + if err != nil { + word.WriteString("& ") + } else if (next >= '0' && next <= '9') || + (next >= 'a' && next <= 'f') { + b = '&' + color = next + } + } + word.WriteByte(b) + } + if line.Len() + word.Len() > 64 { + break + } + if err != nil { + line.WriteString(word.String()) + word.Reset() + break + } + if word.Len() >= 64 || b == ' ' { + line.WriteString(word.String()) + word.Reset() + } + } + packet := &classic.Message { + Message: classic.PadString(line.String()), + } + packets = append(packets, packet) + line.Reset() + line.WriteString("&f> &" + string([]byte{color})) + } + return packets +} diff --git a/server/commands.go b/server/commands.go new file mode 100644 index 0000000..abb4e43 --- /dev/null +++ b/server/commands.go @@ -0,0 +1 @@ +package server diff --git a/server/coords.go b/server/coords.go new file mode 100644 index 0000000..ebe71c9 --- /dev/null +++ b/server/coords.go @@ -0,0 +1,34 @@ +package server + +const playerHeight = 51 +const blockSize = 32 + +type blockCoord int64 +type entityCoord int64 + +type entityFacing struct { + Yaw, Pitch uint8 +} + +type blockPos struct { + X, Y, Z blockCoord +} +type entityPos struct { + X, Y, Z entityCoord +} + +func entityToBlock(pos entityPos) blockPos { + return blockPos { + blockCoord(pos.X >> 5), + blockCoord(pos.Y >> 5), + blockCoord(pos.Z >> 5), + } +} + +func blockToEntity(pos blockPos) entityPos { + return entityPos { + entityCoord(pos.X << 5), + entityCoord(pos.Y << 5), + entityCoord(pos.Z << 5), + } +} diff --git a/server/data.go b/server/data.go new file mode 100644 index 0000000..b7ed280 --- /dev/null +++ b/server/data.go @@ -0,0 +1,87 @@ +package server + +import ( + "os" + "encoding/binary" + "git.citrons.xyz/metronode/phony" +) + +var dataManager struct { + phony.Inbox + errHand interface { + OnSaveError(from phony.Actor, err error) + OnLoadError(from phony.Actor, err error) + } +} + +type atomicWriter struct { + from phony.Actor + name string + tmpFile *os.File +} + +func createAtomic(from phony.Actor, name string) (atomicWriter, error) { + os.Mkdir("world", 0777) + f, err := os.CreateTemp("world", "*.tmp") + if err != nil { + dataManager.errHand.OnSaveError(from, err) + return atomicWriter {}, err + } + return atomicWriter {from, name, f}, nil +} + +func (f atomicWriter) Write(b []byte) (n int, err error) { + return f.tmpFile.Write(b) +} + +func (f atomicWriter) Close() error { + if f.tmpFile == nil { + return nil + } + dataManager.Act(f.from, func() { + err := f.tmpFile.Close() + if err != nil { + dataManager.errHand.OnSaveError(&dataManager, err) + return + } + err = os.Rename(f.tmpFile.Name(), f.name) + if err != nil { + dataManager.errHand.OnSaveError(&dataManager, err) + } + }) + return nil +} + +func saveDataFile[T any](from phony.Actor, name string, data T) { + f, err := createAtomic(from, "world/" + name + ".bin") + if err != nil { + dataManager.errHand.OnSaveError(from, err) + return + } + defer f.Close() + err = binary.Write(f, binary.BigEndian, data) + if err != nil { + dataManager.errHand.OnSaveError(from, err) + } +} + +func loadDataFile[T any]( + from phony.Actor, name string, reply func(data T, ok bool)) { + dataManager.Act(from, func() { + f, err := os.Open("world/" + name + ".bin") + if err != nil { + if !os.IsNotExist(err) { + dataManager.errHand.OnLoadError(&dataManager, err) + } + return + } + defer f.Close() + var data T + err = binary.Read(f, binary.BigEndian, &data) + if err != nil { + dataManager.errHand.OnLoadError(&dataManager, err) + } + from.Act(nil, func() {reply(data, err == nil)}) + }) +} + diff --git a/server/level.go b/server/level.go new file mode 100644 index 0000000..577d0dc --- /dev/null +++ b/server/level.go @@ -0,0 +1,266 @@ +package server + +import ( + "io" + "os" + "fmt" + "bytes" + "compress/gzip" + "encoding/binary" + "git.citrons.xyz/metronode/phony" +) + +type levelId int32 +type blockType byte +type levelPlayerId int8 +type levelInfo struct { + Id levelId + Size blockPos + IsSpawn bool +} + +type level struct { + phony.Inbox + levelInfo + loadingState int + server *Server + blocks []byte + ids map[levelPlayerId]*player + players map[*player]levelPlayerId +} + +const ( + levelUnloaded = iota + levelLoading + levelLoaded +) + +func newLevel(s *Server, info levelInfo) *level { + return &level { + levelInfo: info, + server: s, + loadingState: levelLoaded, + blocks: make([]byte, info.Size.X * info.Size.Y * info.Size.Z), + ids: make(map[levelPlayerId]*player), + players: make(map[*player]levelPlayerId), + } +} + +func loadLevel(s *Server, id levelId, isSpawn bool) *level { + l := &level { + levelInfo: levelInfo {Id: id, IsSpawn: isSpawn}, + server: s, + ids: make(map[levelPlayerId]*player), + players: make(map[*player]levelPlayerId), + } + l.load() + return l +} + +func (l *level) load() { + if l.loadingState == levelUnloaded { + l.loadingState = levelLoading + id := l.Id + dataManager.Act(l, func() { + f, err := os.Open(fmt.Sprintf("world/level/%d.bin", id)) + if l.OnLevelError(&dataManager, err) != nil { + return + } + defer f.Close() + var ( + info levelInfo + blocks []byte + ) + err = binary.Read(f, binary.BigEndian, &info) + if l.OnLevelError(&dataManager, err) != nil { + return + } + z, err := gzip.NewReader(f) + if l.OnLevelError(&dataManager, err) != nil { + return + } + z.Read(make([]byte, 4)) + blocks, err = io.ReadAll(z) + if l.OnLevelError(&dataManager, err) != nil { + return + } + l.Act(&dataManager, func() { + l.loadingState = levelLoaded + l.levelInfo = info + l.Id = id + l.blocks = blocks + for player := range l.players { + player.OnLevelData(l, l.levelInfo, l.compressLevelData()) + } + }) + }) + } +} + +func (l *level) OnLevelError(from phony.Actor, err error) error { + if err == nil { + return err + } + l.Act(from, func() { + l.loadingState = levelUnloaded + dataManager.errHand.OnLoadError(l, err) + errString := err.Error() + if os.IsNotExist(err) { + errString = "level not found" + } + for player := range l.players { + player.OnLevelError(l, errString, l.levelInfo) + } + }) + return err +} + +func (l *level) save(done func()) { + if l.loadingState != levelLoaded { + return + } + os.Mkdir("world/level", 0777) + f, err := createAtomic(l, fmt.Sprintf("world/level/%d.bin", l.Id)) + if err != nil { + dataManager.errHand.OnSaveError(l, err) + return + } + defer f.Close() + + err = binary.Write(f, binary.BigEndian, l.levelInfo) + if err != nil { + dataManager.errHand.OnSaveError(l, err) + return + } + data := l.compressLevelData() + _, err = io.Copy(f, data) + if err != nil { + dataManager.errHand.OnSaveError(l, err) + return + } + if done != nil { + dataManager.Act(nil, done) + } +} + +func (l *level) blockIndex(pos blockPos) int { + return int(pos.X + pos.Z*l.Size.X + pos.Y*l.Size.X*l.Size.Z) +} + +func (l *level) setBlock(pos blockPos, block blockType) { + l.blocks[l.blockIndex(pos)] = byte(block) +} + +func (l *level) getBlock(pos blockPos) blockType { + return blockType(l.blocks[l.blockIndex(pos)]) +} + +func (l *level) compressLevelData() io.Reader { + rd, wr := io.Pipe() + data := bytes.NewReader(bytes.Clone(l.blocks)) + go func() { + defer wr.Close() + z := gzip.NewWriter(wr) + defer z.Close() + binary.Write(z, binary.BigEndian, uint32(len(l.blocks))) + io.Copy(z, data) + }() + return rd +} + +func (l *level) generateFlat() { + var p blockPos + for p.Z = 0; p.Z < l.levelInfo.Size.Z; p.Z++ { + for p.Y = 0; p.Y < l.levelInfo.Size.Y / 2; p.Y++ { + for p.X = 0; p.X < l.levelInfo.Size.X; p.X++ { + var block blockType + if p.Y == 0 { + block = 7 + } else if p.Y == l.levelInfo.Size.Y/2 - 1 { + block = 2 + } else if p.Y > l.levelInfo.Size.Y/2 - 15 { + block = 3 + } else { + block = 1 + } + l.setBlock(p, block) + } + } + } +} + +func (l *level) Save(from phony.Actor, done func()) { + l.Act(from, func() { + l.save(func() { + if done != nil { + from.Act(nil, done) + } + }) + }) +} + +func (l *level) SetBlock(from phony.Actor, pos blockPos, block blockType) { + if l.loadingState != levelLoaded { + return + } + l.Act(from, func() { + l.setBlock(pos, block) + for player := range l.players { + player.OnSetBlock(l, pos, block) + } + }) +} + +func (l *level) OnAddPlayer(from *player, name string, pos entityPos) { + l.Act(from, func() { + l.load() + if l.loadingState == levelLoaded { + from.OnLevelData(l, l.levelInfo, l.compressLevelData()) + } + var newId levelPlayerId + for newId = 0; newId <= 127; newId++ { + if l.ids[newId] == nil { + break + } + } + if newId == -1 { + from.OnLevelError( + l, "there are too many players in this area", l.levelInfo, + ) + } + for player, id := range l.players { + if player != from { + player.OnPlayer(l, newId, name, pos) + player.GetInfo(l, + func(name string, state playerState) { + from.OnPlayer(l, id, name, state.Pos) + }, + ) + } + } + l.ids[newId] = from + l.players[from] = newId + }) +} + +func (l *level) OnRemovePlayer(from *player) { + l.Act(from, func() { + delId := l.players[from] + delete(l.ids, delId) + delete(l.players, from) + for player := range l.players { + player.OnRemovePlayer(l, delId) + } + }) +} + +func (l *level) OnMovePlayer( + from *player, pos entityPos, facing entityFacing) { + l.Act(from, func() { + for player := range l.players { + if player != from { + player.OnMovePlayer(l, l.players[from], pos, facing) + } + } + }) +} diff --git a/server/player.go b/server/player.go new file mode 100644 index 0000000..bc6ee38 --- /dev/null +++ b/server/player.go @@ -0,0 +1,242 @@ +package server + +import ( + "io" + "os" + "fmt" + "regexp" + "git.citrons.xyz/metronode/phony" + "git.citrons.xyz/metronode/classic" +) + +type player struct { + phony.Inbox + state playerState + client *client + server *Server + name string + level *level +} + +type playerState struct { + LevelId levelId + Pos entityPos + Facing entityFacing +} + +var playerNameRegex = regexp.MustCompile("^[.-_a-zA-Z0-9]*$") + +func newPlayer(s *Server, cl *client, name string) *player { + pl := &player {client: cl, server: s, name: name} + loadDataFile(pl, "player/" + name, func(state playerState, ok bool) { + if ok { + pl.state = state + pl.ChangeLevel(pl, state.LevelId, state.Pos) + } else { + s.SendToSpawn(pl, pl) + } + }) + return pl +} + +func (p *player) save(done func()) { + os.Mkdir("world/player", 0777) + saveDataFile(p, "player/" + p.name, p.state) + if done != nil { + dataManager.Act(nil, done) + } +} + +func (p *player) kick(reason string) { + p.save(nil) + p.client.Disconnect(p, reason) + if p.level != nil { + p.level.OnRemovePlayer(p) + p.level = nil + } +} + +func (p *player) handlePacket(packet classic.Packet) { + if p.level == nil { + return + } + switch pck := packet.(type) { + case *classic.SetPosFacing: + p.state.Pos = entityPos { + entityCoord(pck.X), entityCoord(pck.Y), entityCoord(pck.Z), + } + p.state.Facing = entityFacing {pck.Yaw, pck.Pitch} + p.level.OnMovePlayer(p, p.state.Pos, p.state.Facing) + case *classic.ClientSetBlock: + block := blockType(pck.Block) + if pck.Mode == classic.BlockDestroyed { + block = 0 + } + pos := blockPos { + blockCoord(pck.X), + blockCoord(pck.Y), + blockCoord(pck.Z), + } + p.level.SetBlock(p, pos, block) + case *classic.Message: + p.server.OnPlayerMessage(p, p.name, classic.UnpadString(pck.Message)) + } +} + +func (p *player) Save(from phony.Actor, done func()) { + p.Act(from, func() { + p.save(func() { + if done != nil { + from.Act(nil, done) + } + }) + }) +} + +func (p *player) Kick(from phony.Actor, reason string) { + p.Act(from, func() { + p.kick(reason) + }) +} + +func (p *player) OnPacket(from phony.Actor, packet classic.Packet) { + p.Act(from, func() { + p.handlePacket(packet) + }) +} + +func (p *player) joinLevel(id levelId, lvl *level, pos entityPos) { + lvl.OnAddPlayer(p, p.name, pos) + p.level = lvl + p.state.LevelId = id + p.state.Pos = pos +} + +func (p *player) ChangeLevel(from phony.Actor, lvl levelId, pos entityPos) { + p.Act(from, func() { + if p.level != nil { + p.level.OnRemovePlayer(p) + } + p.server.GetLevel(p, lvl, func(l *level) { + p.joinLevel(lvl, l, pos) + }) + }) +} + +func (p *player) MovePlayer( + from phony.Actor, pos entityPos, facing entityFacing) { + p.Act(from, func() { + p.level.OnMovePlayer(p, pos, facing) + }) +} + +func (p *player) GetInfo(from phony.Actor, + reply func(name string, state playerState)) { + p.Act(from, func() { + name := p.name + state := p.state + from.Act(nil, func() {reply(name, state)}) + }) +} + +func (p *player) SendMessage(from phony.Actor, message string) { + p.Act(from, func() { + p.client.SendPackets(p, processChatMessage(message)) + }) +} + +func (p *player) OnPlayerMessage(from *Server, name string, message string) { + p.SendMessage(from, fmt.Sprintf("&7<&b%s&7>&f %s", name, message)) +} + +func (p *player) OnLevelData(from *level, info levelInfo, data io.Reader) { + p.Act(from, func() { + if from != p.level { + return + } + var packets []classic.Packet + for { + var packet classic.LevelDataChunk + n, err := io.ReadFull(data, packet.Data[:]) + if err == io.EOF || err == io.ErrUnexpectedEOF { + if n == 0 { + break + } + } else if err != nil { + panic(err) + } + packet.Length = int16(n) + packets = append(packets, &packet) + } + for i := 0; i < len(packets); i++ { + chunk := packets[i].(*classic.LevelDataChunk) + chunk.PercentComplete = byte(i * 100 / len(packets)) + } + p.client.SendPackets(p, packets) + p.client.SendPacket(p, &classic.LevelFinalize { + Width: int16(info.Size.X), + Height: int16(info.Size.Y), + Length: int16(info.Size.Z), + }) + p.client.SendPacket(p, &classic.SpawnPlayer { + PlayerId: -1, + Username: classic.PadString(p.name), + X: classic.FShort(p.state.Pos.X), + Y: classic.FShort(p.state.Pos.Y), + Z: classic.FShort(p.state.Pos.Z), + }) + }) +} + +func (p *player) OnLevelError(from *level, message string, info levelInfo) { + p.SendMessage(from, "&cCannot join level: " + message) + fmt.Println(info) + if !info.IsSpawn { + p.Act(from, func() { + p.server.SendToSpawn(p, p) + }) + } else { + p.Kick(from, "Error: " + message) + } +} + +func (p *player) OnPlayer( + from *level, id levelPlayerId, name string, pos entityPos) { + p.Act(from, func() { + p.client.SendPacket(p, &classic.SpawnPlayer { + PlayerId: int8(id), + Username: classic.PadString(name), + X: classic.FShort(pos.X), + Y: classic.FShort(pos.Y), + Z: classic.FShort(pos.Z), + }) + }) +} + +func (p *player) OnRemovePlayer(from *level, id levelPlayerId) { + p.Act(from, func() { + p.client.SendPacket(p, &classic.DespawnPlayer {int8(id)}) + }) +} + +func (p *player) OnMovePlayer( + from *level, id levelPlayerId, pos entityPos, facing entityFacing) { + p.Act(from, func() { + p.client.SendPacket(p, &classic.SetPosFacing { + PlayerId: int8(id), + X: classic.FShort(pos.X), + Y: classic.FShort(pos.Y), + Z: classic.FShort(pos.Z), + Yaw: facing.Yaw, Pitch: facing.Pitch, + }) + }) +} + +func (p *player) OnSetBlock(from *level, pos blockPos, block blockType) { + p.client.SendPacket(p, &classic.SetBlock { + X: int16(pos.X), + Y: int16(pos.Y), + Z: int16(pos.Z), + Block: byte(block), + }) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..13e06f6 --- /dev/null +++ b/server/server.go @@ -0,0 +1,396 @@ +package server + +import ( + "os" + "net" + "log" + "fmt" + "time" + "binary" + "os/signal" + "git.citrons.xyz/metronode/classic" + "git.citrons.xyz/metronode/phony" +) + +var SoftwareName = "Metronode" + +type ServerInfo struct { + Name string + Motd string +} + +type Server struct { + phony.Inbox + worldState + info ServerInfo + clients map[*client]bool + players map[string]*player + levels map[levelId]*level + listener net.Listener + stopping bool + stopped chan struct{} +} + +type worldState struct { + SpawnLevel levelId + SpawnPos entityPos +} + +func NewServer(info ServerInfo) *Server { + s := &Server { + info: info, + clients: make(map[*client]bool), + players: make(map[string]*player), + levels: make(map[levelId]*level), + stopped: make(chan struct{}), + } + err := os.Mkdir("world", 0777) + if err == nil { + spawnLevel := s.newLevel(levelInfo { + Id: 0, + Size: blockPos {X: 256, Y: 256, Z: 256}, + IsSpawn: true, + }) + spawnLevel.generateFlat() + s.SpawnPos = entityPos { + 128*blockSize, + 128*blockSize + playerHeight, + 128*blockSize, + } + } else { + f, err := os.Open("world/world.bin") + if err != nil { + log.Fatal(err) + } + err = binary.Read(f, binary.BigEndian, &s.worldState) + if err != nil { + log.Fatal("read world data: %s", err) + } + return s +} + +func (s *Server) Serve(ln net.Listener) { + dataManager.errHand = s + s.Act(nil, func() {s.listener = ln}) + go func() { + defer s.Stop(nil) + for { + conn, err := ln.Accept() + if err != nil { + log.Println(err) + return + } + s.Act(nil, func() { + s.clients[newClient(s, s.info, conn)] = true + }) + } + }() + var ( + ping = time.Tick(10 * time.Second) + tick = time.Tick(time.Second / 20) + save = time.Tick(time.Minute) + sigterm = make(chan os.Signal) + ) + signal.Notify(sigterm, os.Interrupt) + defer signal.Stop(sigterm) + for { + select { + case <-ping: s.SendPings() + case <-tick: s.Tick() + case <-save: s.Save(nil) + case <-sigterm: s.Stop(nil) + case <-s.stopped: return + } + } +} + +func (s *Server) stop() { + if s.stopping { + return + } + log.Println("stopping the server. please wait...") + s.listener.Close() + s.stopping = true + var ( + savedPlayers int + savedLevels int + ) + checkSaved := func() { + if savedLevels >= len(s.levels) && savedPlayers >= len(s.players) { + close(s.stopped) + for client := range s.clients { + client.Disconnect(s, "Shutting down...") + } + } + } + for _, player := range s.players { + player.Save(s, func() { + savedPlayers++ + checkSaved() + }) + } + for _, level := range s.levels { + level.Save(s, func() { + savedLevels++ + checkSaved() + }) + } +} + +func (s *Server) Stop(from phony.Actor) { + s.Act(from, s.stop) +} + +func (s *Server) Save(from phony.Actor) { + s.Act(from, func() { + for _, player := range s.players { + player.Save(s, nil) + } + for _, level := range s.levels { + level.Save(s, nil) + } + }) +} + +func (s *Server) SendPings() { + s.Act(nil, func() { + for cl := range s.clients { + cl.SendPing(s) + } + }) +} + +func (s *Server) OnDisconnect(cl *client, username string, pl *player) { + s.Act(cl, func() { + delete(s.clients, cl) + if s.stopping { + return + } + if s.players[username] == pl { + delete(s.players, username) + } + if username == "" { + return + } + s.Broadcast(nil, fmt.Sprintf("&e%s has left", username)) + }) +} + +func (s *Server) OnPlayerMessage(from *player, name string, message string) { + s.Act(from, func() { + for _, player := range s.players { + player.OnPlayerMessage(s, name, message) + } + }) +} + +func (s *Server) OnLoadError(from phony.Actor, err error) { + log.Printf("error loading world: %s", err) +} + +func (s *Server) OnSaveError(from phony.Actor, err error) { + log.Printf("error saving world: %s", err) + s.Broadcast(from, fmt.Sprintf("&4Error saving world: %s", err)) +} + +func (s *Server) Tick() { +} + +func (s *Server) newLevel(info levelInfo) *level { + l := newLevel(s, info) + s.levels[info.Id] = l + return l +} + +func (s *Server) newPlayer(cl *client, name string) *player { + pl := newPlayer(s, cl, name) + s.players[name] = pl + return pl +} + +func (s *Server) NewPlayer( + from phony.Actor, cl *client, name string, reply func(*player)) { + s.Act(from, func() { + s.Broadcast(nil, fmt.Sprintf("&e%s has joined", name)) + if s.players[name] != nil { + s.players[name].Act(s, func() { + s.players[name].kick("Replaced by new connection") + s.Act(s.players[name], func() { + s.newPlayer(cl, name) + s.GetPlayer(from, name, reply) + }) + }) + } else { + s.newPlayer(cl, name) + s.GetPlayer(from, name, reply) + } + }) +} + +func (s *Server) getLevel(lvl levelId) *level { + if s.levels[lvl] == nil { + s.levels[lvl] = loadLevel(s, lvl, lvl == s.spawnLevel) + } + return s.levels[lvl] +} + +func (s *Server) GetLevel( + from phony.Actor, lvl levelId, reply func(*level)) { + s.Act(from, func() { + if s.stopping { + return + } + l := s.getLevel(lvl) + from.Act(nil, func() {reply(l)}) + }) +} + +func (s *Server) SendToSpawn(from phony.Actor, pl *player) { + s.Act(from, func() { + pl.ChangeLevel(s, s.spawnLevel, s.spawnPos) + }) +} + +func (s *Server) GetPlayer( + from phony.Actor, name string, reply func(*player)) { + s.Act(from, func() { + from.Act(nil, func() {reply(s.players[name])}) + }) +} + +func (s *Server) Broadcast(from phony.Actor, message string) { + for _, player := range s.players { + player.SendMessage(s, message) + } +} + +type client struct { + phony.Inbox + server *Server + conn net.Conn + username string + player *player +} + +func newClient(server *Server, srvInfo ServerInfo, conn net.Conn) *client { + cl := &client {server: server} + cl.Act(nil, func() { + cl.performHandshake(conn, srvInfo) + if cl.conn != nil { + go cl.readPackets(cl.conn) + } + }) + return cl +} + +func (cl *client) performHandshake(conn net.Conn, srvInfo ServerInfo) { + cl.conn = conn + conn.SetDeadline(time.Now().Add(10 * time.Second)) + + packet, err := classic.SReadPacket(conn) + if cl.handleError(err) != nil { + return + } + switch pid := packet.(type) { + case *classic.PlayerId: + if pid.Version != 7 { + cl.disconnect( + "Please join on protocol version 7 (Minecraft Classic 0.30 / "+ + "ClassiCube)", + ) + } + cl.username = classic.UnpadString(pid.Username) + default: + cl.disconnect("Expected handshake") + return + } + err = classic.WritePacket(conn, &classic.ServerId { + Version: 7, + ServerName: classic.PadString(srvInfo.Name), + Motd: classic.PadString(srvInfo.Motd), + }) + if cl.handleError(err) != nil { + return + } + cl.server.NewPlayer(cl, cl, cl.username, func(pl *player) { + cl.player = pl + }) + + conn.SetDeadline(time.Time{}) +} + +func (cl *client) readPackets(conn net.Conn) { + for { + packet, err := classic.SReadPacket(conn) + cl.Act(nil, func() { + if cl.handleError(err) != nil { + return + } + if cl.player != nil { + cl.player.OnPacket(cl, packet) + } + }) + if err != nil { + return + } + } +} + +func (cl *client) handleError(err error) error { + if err == nil { + return err + } + cl.disconnect(err.Error()) + return err +} + +func (cl *client) disconnect(reason string) { + if cl.player != nil { + cl.player.Kick(cl, reason) + cl.player = nil + } + if cl.conn != nil { + cl.server.OnDisconnect(cl, cl.username, cl.player) + log.Printf("disconnecting client (%s): %s", cl.username, reason) + cl.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + classic.WritePacket(cl.conn, &classic.DisconnectPlayer { + Reason: classic.PadString(reason), + }) + cl.conn.Close() + cl.conn = nil + } +} + +func (cl *client) SendPacket(from phony.Actor, packet classic.Packet) { + cl.Act(from, func() { + if cl.conn != nil { + cl.handleError(classic.WritePacket(cl.conn, packet)) + } + }) +} + +func (cl *client) SendPackets(from phony.Actor, packets []classic.Packet) { + cl.Act(from, func() { + for _, packet := range packets { + if cl.conn == nil { + return + } + cl.handleError(classic.WritePacket(cl.conn, packet)) + } + }) +} + +func (cl *client) SendPing(from phony.Actor) { + cl.Act(from, func() { + if cl.conn != nil { + cl.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + cl.handleError(classic.WritePacket(cl.conn, &classic.Ping{})) + } + }) +} + +func (cl *client) Disconnect(from phony.Actor, reason string) { + cl.Act(from, func() { + cl.disconnect(reason) + }) +} |
