package server import ( "os" "net" "log" "fmt" "time" "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 { loaded := make(chan worldState, 1) loadDataFile(s, "world", func(state worldState, ok bool) { loaded <- state }) s.worldState = <-loaded } 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: fmt.Println("") 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) { for client := range s.clients { client.Disconnect(s, "Shutting down...") } saveDataFile(s, "world", s.worldState) dataManager.Act(s, func() { close(s.stopped) }) } } 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() { saveDataFile(s, "world", s.worldState) 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) ExecuteCommand( from CommandSender, auth authLevel, command string) { s.Act(nil, func() { executeCommand(s, auth, from, command) }) } 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) changePlayerAuth( playerName string, auth authLevel, done func(ok bool)) { pl := s.players[playerName] if pl != nil { pl.SetAuthLevel(s, auth) pl.Act(s, func() { done(true) }) return } loadPlayerData(s, playerName, func(state playerState, ok bool) { if !ok { done(false) return } state.Auth = auth savePlayerData(s, playerName, state, func() { done(true) }) }) } 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) }) }