From c3d63652a4b80add587ee17f5c9f3773417203ad Mon Sep 17 00:00:00 2001 From: raven Date: Fri, 20 Mar 2026 14:29:52 -0500 Subject: initial commit --- server/server.go | 396 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 server/server.go (limited to 'server/server.go') 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) + }) +} -- cgit v1.2.3