package server import ( "os" "net" "log" "fmt" "time" "iter" "strconv" "strings" "os/signal" "git.citrons.xyz/metronode/classic" "git.citrons.xyz/metronode/phony" ) var SoftwareName = "Metronode" var supportedExtensions = []string { "ExtEntityPositions", "EnvMapAspect.2", "HackControl", "LongerMessages", "FullCP437", "CustomBlocks", "ExtPlayerList.2", "BlockDefinitions", "BlockDefinitionsExt.2", "InventoryOrder", } var requiredExtensions = []string { "ExtEntityPositions", "EnvMapAspect.2", "CustomBlocks", "ExtPlayerList.2", "BlockDefinitions", "BlockDefinitionsExt.2", } type ServerInfo struct { Name string Motd string TexturePack string } type Server struct { phony.Inbox worldState info ServerInfo Chat gameChat clients map[*client]bool players map[string]*player listIds map[listId]string playerList map[string]listEntry levels map[levelId]*level listener net.Listener stopping bool stopped chan struct{} } type worldState struct { LastId levelId SpawnLevel levelId SpawnPos entityPos Banned map[string]string } type listId int16 type listEntry struct { id listId playerName string listName string player *player } func NewServer(info ServerInfo) *Server { s := &Server { info: info, clients: make(map[*client]bool), players: make(map[string]*player), listIds: make(map[listId]string), playerList: make(map[string]listEntry), Chat: gameChat {members: make(map[chatListener]bool)}, levels: make(map[levelId]*level), stopped: make(chan struct{}), } err := os.Mkdir("world", 0777) dataManager.errHand = s if err == nil { s.LastId = -1 var spawnLevel *level s.SpawnLevel, spawnLevel = s.newLevel(levelInfo { Size: blockPos {X: 64, Y: 64, Z: 64}, IsSpawn: true, }) spawnLevel.generateFlat() s.SpawnPos = entityPos { 32*blockSize, 32*blockSize + playerHeight, 32*blockSize, } } else { loaded := make(chan worldState, 1) loadDataFile(s, "world", func(state worldState, ok bool) { loaded <- state }) s.worldState = <-loaded } if s.worldState.Banned == nil { s.worldState.Banned = make(map[string]string) } return s } func (s *Server) Serve(ln net.Listener) { 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(5 * time.Minute) 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) { 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() }) } 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) GetInfo(from phony.Actor, reply func(ServerInfo)) { info := s.info from.Act(nil, func() {reply(info)}) } func (s *Server) OnDisconnect(cl *client) { s.Act(cl, func() { delete(s.clients, cl) }) } func (s *Server) OnLeave(pl *player, username string) { s.Act(pl, func() { s.Chat.Send(s, nil, fmt.Sprintf("&e%s has left", username)) if s.players[username] == pl { delete(s.players, username) s.removeListEntry(username) s.Chat.RemoveMember(s, pl) } }) } 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.Chat.Send(s, nil, fmt.Sprintf("&4Error saving world: %s", err)) } func (s *Server) Tick() { } func (s *Server) newLevel(info levelInfo) (levelId, *level) { s.LastId++ for { _, err := os.Stat(fmt.Sprintf("world/levels/%d.bin", s.LastId)) if err != nil { break } s.LastId++ } log.Printf("creating new level with id %d", s.LastId) info.Id = s.LastId l := createNewLevel(s, info) s.levels[info.Id] = l return s.LastId, l } func (s *Server) newPlayer( cl *client, name string, ext map[string]bool) *player { pl := newPlayer(s, cl, name, ext) s.players[name] = pl for _, entry := range s.playerList { pl.OnAddListEntry(s, entry) } s.Chat.AddMember(s, pl) return pl } func (s *Server) changePlayerAuth( playerName string, auth authLevel, done func(ok bool)) { log.Printf("changing auth level of %s to %d", playerName, auth) 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) ban(playerName string, reason string) { log.Printf("banning %s ('%s')", playerName, reason) s.kick(playerName, reason) s.worldState.Banned[playerName] = reason } func (s *Server) unban(playerName string) { log.Printf("unbanning %s", playerName) delete(s.worldState.Banned, playerName) } func (s *Server) kick(playerName string, reason string) bool { pl := s.players[playerName] if pl == nil { return false } if reason == "" { reason = "Bye!" } pl.Kick(s, reason) return true } func (s *Server) NewPlayer( from phony.Actor, cl *client, name string, ext map[string]bool, reply func(*player)) { s.Act(from, func() { banReason, ok := s.worldState.Banned[name] if ok { cl.Disconnect(s, banReason) return } s.Chat.Send(s, nil, fmt.Sprintf("&e%s has joined", name)) log.Printf("%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, ext) s.GetPlayer(from, name, reply) }) }) } else { s.newPlayer(cl, name, ext) s.GetPlayer(from, name, reply) } s.addListEntry(name) }) } func (s *Server) addListEntry(name string) { var id listId for s.listIds[id] != "" { id++ } s.listIds[id] = name s.playerList[name] = listEntry { id: id, playerName: name, listName: name, } for _, player := range s.players { player.OnAddListEntry(s, s.playerList[name]) } } func (s *Server) removeListEntry(name string) { id := s.playerList[name].id delete(s.playerList, name) delete(s.listIds, id) for _, player := range s.players { player.OnRemoveListEntry(s, id) } } func (s *Server) getLevel(lvl levelId) *level { if s.levels[lvl] == nil { s.levels[lvl] = initLevel(s, levelInfo {Id: lvl, IsSpawn: 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])}) }) } type client struct { phony.Inbox server *Server conn net.Conn username string player *player extensions map[string]bool } 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)) var ext = make(map[string]bool) packet, err := classic.SReadPacket(conn, ext) if cl.handleError(err) != nil { return } switch pid := packet.(type) { case *classic.PlayerId: if pid.Version != 7 || pid.Ext != classic.UseCpe { cl.disconnect("Please join in a CPE client (i.e. ClassiCube)") return } cl.username = classic.UnpadString(pid.Username) default: cl.disconnect("Expected handshake") return } if !playerNameRegex.Match([]byte(cl.username)) { cl.disconnect("Invalid player name") return } if !cl.cpeHandshake(conn, ext) { 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, ext, func(pl *player) { cl.player = pl }) if srvInfo.TexturePack != "" { cl.SendPacket(nil, &classic.SetMapEnvUrl { TexturePackUrl: classic.PadDString(srvInfo.TexturePack), }) } conn.SetDeadline(time.Time{}) } func (cl *client) cpeHandshake(conn net.Conn, ext map[string]bool) bool { var (packet classic.Packet; err error) err = classic.WritePacket(conn, &classic.ExtInfo { AppName: classic.PadString(SoftwareName), ExtensionCount: int16(len(supportedExtensions)), }) if cl.handleError(err) != nil { return false } for _, extString := range supportedExtensions { var (name string; version = 1) split := strings.Split(extString, ".") name = split[0] if len(split) > 1 { version, err = strconv.Atoi(split[1]) if err != nil { panic(err) } } err = classic.WritePacket(conn, &classic.ExtEntry { ExtName: classic.PadString(name), Version: int32(version), }) if cl.handleError(err) != nil { return false } } packet, err = classic.SReadPacket(conn, ext) if cl.handleError(err) != nil { return false } var count int switch info := packet.(type) { case *classic.ExtInfo: log.Printf( "%s is connecting via '%s' client with %d extensions", cl.username, info.AppName, info.ExtensionCount, ) count = int(info.ExtensionCount) default: cl.disconnect("Expected ExtInfo") return false } for i := 0; i < count; i++ { var extString string packet, err = classic.SReadPacket(conn, ext) if cl.handleError(err) != nil { return false } switch entry := packet.(type) { case *classic.ExtEntry: extString = classic.UnpadString(entry.ExtName) if entry.Version != 1 { extString += "." + strconv.Itoa(int(entry.Version)) } default: cl.disconnect("Expected ExtEntry") return false } ext[extString] = true } var extList []string for extString := range ext { extList = append(extList, extString) } log.Printf( "%s has extensions: %s", cl.username, strings.Join(extList, ", "), ) for _, req := range requiredExtensions { if !ext[req] { cl.disconnect("Missing required extension: " + req) } } err = classic.WritePacket(conn, &classic.CustomBlocksSupportLevel { SupportLevel: 1, }) // it doesn't matter what the client sends in response to this if cl.handleError(err) != nil { return false } cl.SendPackets(nil, getBlockDefPackets()) cl.SendPackets(nil, getInventoryPackets(inventoryList)) cl.extensions = ext return true } func (cl *client) readPackets(conn net.Conn) { for { packet, err := classic.SReadPacket(conn, cl.extensions) 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) 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) { deadline := time.Now().Add(5 * time.Minute) // nil sender: we don't want to block the server on I/O cl.Act(nil, func() { if cl.conn != nil { cl.conn.SetWriteDeadline(deadline) cl.handleError(classic.WritePacket(cl.conn, packet)) } }) } func (cl *client) SendPackets( from phony.Actor, packets iter.Seq[classic.Packet]) { deadline := time.Now().Add(5 * time.Minute) cl.Act(nil, func() { if cl.conn == nil { return } cl.conn.SetWriteDeadline(deadline) 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(nil, 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(nil, func() { cl.disconnect(reason) }) }