/* * Leave sends a LEAVE message to the client associated to given connection */ func (reg *ClientRegistry) Leave(reason string, c *network.Conn) { clientData := c.GetUserData().(ClientData) // send LEAVE to client leave := messages.New(messages.LeaveId, messages.Leave{ Id: uint32(clientData.Id), Reason: reason, }) if err := c.AsyncSendPacket(leave, 5*time.Millisecond); err != nil { // either the client received the LEAVE or not, we will close the // connection afterwards, so there's nothing more to do in order to // gracefully handle this error log.WithError(err).WithField("clientID", clientData.Id).Error("LEAVE message couldn't be sent") } else { log.WithField("clientID", clientData.Id).Info("LEAVE message has been sent") } // TODO: Remove this: the client should remain alive even when the connection // is closed. Example: when the lobby will be implemented // closes the connection, registry cleanup will be performed in OnClose go func() { time.Sleep(100 * time.Millisecond) if !c.IsClosed() { c.Close() } }() }
/* * OnClose gets called by the server at connection closing, once by * connection. This gives us the chance to unregister a new client and perform * client cleanup */ func (srv *Server) OnClose(c *network.Conn) { log.WithField("addr", c.GetRawConn().RemoteAddr()).Debug("Connection closed") // unregister the client before anything clientData := c.GetUserData().(ClientData) srv.clients.unregister(clientData.Id) if clientData.Joined { // client is still JOINED so that's a disconnection initiated externally // send a LEAVE to the rest of the world msg := messages.New(messages.LeaveId, messages.Leave{ Id: uint32(clientData.Id), Reason: "client disconnection", }) srv.Broadcast(msg) } else { // the client was not marked as JOINED, so nobody knows about him // and we have nothing more to do } if srv.playerLeftCb != nil { // raise 'player left' external callback srv.playerLeftCb(clientData.Id) } }
/* * OnIncomingMsg gets called by the server each time a message has been read * from the connection */ func (srv *Server) OnIncomingPacket(c *network.Conn, packet network.Packet) bool { clientData := c.GetUserData().(ClientData) raw := packet.(*messages.Message) log.WithFields( log.Fields{ "clientData": clientData, "addr": c.GetRawConn().RemoteAddr(), "type": raw.Type.String(), }).Debug("Incoming message") // decode the raw message msg := srv.factory.Decode(raw) // get handler handler, ok := srv.msgHandlers[raw.Type] if ok { if err := handler(c, msg); err != nil { log.WithError(err).Error("Error handling message") return false } } else { switch raw.Type { case messages.PingId: // immediately reply pong ping := msg.(messages.Ping) pong := messages.New(messages.PongId, messages.Pong{ Id: ping.Id, Tstamp: time.Now().UnixNano() / int64(time.Millisecond), }) if err := c.AsyncSendPacket(pong, time.Second); err != nil { log.WithError(err).Error("Error handling message") return false } case messages.JoinId: join := msg.(messages.Join) // JOIN is handled by the handshaker if srv.handshaker.Join(join, c) { // new client has been accepted if srv.playerJoinedCb != nil { // raise 'player joined' external callback srv.playerJoinedCb(clientData.Id, join.Type) } } } } return true }
func (reg *ClientRegistry) Join(join messages.Join, c *network.Conn) bool { clientData := c.GetUserData().(ClientData) log.WithFields(log.Fields{"name": join.Name, "clientData": clientData}).Info("Received JOIN from client") // client already JOINED? if clientData.Joined { reg.Leave("Joined already received", c) return false } // name length condition if len(join.Name) < 3 { reg.Leave("Name is too short", c) return false } // name already taken? nameTaken := false playerNames := make(map[uint32]string) // compute the list of joined players, populating the STAY message and // checking if name is taken as well reg.ForEach(func(cd ClientData) bool { nameTaken = cd.Name == join.Name playerNames[cd.Id] = cd.Name // stop iteration if name is taken return !nameTaken }) if nameTaken { reg.Leave("Name is already taken", c) return false } // create and send STAY to the new client stay := messages.Stay{Id: clientData.Id, Players: playerNames} err := c.AsyncSendPacket(messages.New(messages.StayId, stay), time.Second) if err != nil { // handle error in case we couldn't send the STAY message log.WithError(err).Error("Couldn't send STAY message to the new client") reg.Leave("Couldn't finish handshaking", c) return false } // fill a JOINED message joined := &messages.Joined{ Id: clientData.Id, Name: join.Name, Type: join.Type, } log.WithField("joined", joined).Info("Tell to the world this client has joined") reg.Broadcast(messages.New(messages.JoinedId, joined)) // at this point we consider the client as accepted clientData.Joined = true clientData.Name = join.Name c.SetUserData(clientData) return true }
/* * loop is the main game loop, it fetches messages from a channel, processes * them immediately. After performing some initialization, it waits forever, * waiting for a wake-up call coming from any one of those events: * - external loop close request -> exits immediately * - arrival of a message -> process it * - logic tick -> perform logic update * - gamestate tick -> pack and broadcast the current game state * - telnet request -> perform a game state related telnet request */ func (g *Game) loop() error { // will tick when it's time to send the gamestate to the clients sendTickChan := time.NewTicker( time.Millisecond * time.Duration(g.cfg.SendTickPeriod)).C // will tick when it's time to update the game tickChan := time.NewTicker( time.Millisecond * time.Duration(g.cfg.LogicTickPeriod)).C // will tick when a minute in game time elapses timeChan := time.NewTicker( time.Minute * 1 / time.Duration(g.cfg.TimeFactor)).C // event listeners g.eventManager.Subscribe(events.PlayerJoinId, g.state.onPlayerJoin) g.eventManager.Subscribe(events.PlayerLeaveId, g.state.onPlayerLeave) g.eventManager.Subscribe(events.PlayerMoveId, g.state.onPlayerMove) g.eventManager.Subscribe(events.PlayerBuildId, g.state.onPlayerBuild) g.eventManager.Subscribe(events.PlayerRepairId, g.state.onPlayerRepair) g.eventManager.Subscribe(events.PlayerAttackId, g.state.onPlayerAttack) g.eventManager.Subscribe(events.PlayerOperateId, g.state.onPlayerOperate) g.eventManager.Subscribe(events.PlayerDeathId, g.state.onPlayerDeath) g.eventManager.Subscribe(events.ZombieDeathId, g.state.onZombieDeath) g.eventManager.Subscribe(events.ZombieDeathId, g.ai.OnZombieDeath) g.eventManager.Subscribe(events.BuildingDestroyId, g.state.onBuildingDestroy) var lastTime, curTime time.Time lastTime = time.Now() log.Info("Starting game loop") g.wg.Add(1) go func() { defer func() { g.wg.Done() log.Info("Stopping game loop") }() for { select { case <-g.quitChan: return case <-sendTickChan: // pack the gamestate into a message if gsMsg := g.state.pack(); gsMsg != nil { // wrap the gameStateMsg into a generic Message if msg := messages.New(messages.GameStateId, *gsMsg); msg != nil { g.server.Broadcast(msg) } } case <-tickChan: // poll and process accumulated events g.eventManager.Process() // compute delta time curTime = time.Now() dt := curTime.Sub(lastTime) // update AI g.ai.Update(curTime) // update entities for _, ent := range g.state.entities { ent.Update(dt) } lastTime = curTime case <-timeChan: // increment game time by 1 minute g.state.gameTime++ // clamp the game time to 24h if g.state.gameTime >= 1440 { g.state.gameTime -= 1440 } case tnr := <-g.telnetReq: // received a telnet request g.telnetDone <- g.telnetHandler(tnr) default: // let the rest of the world spin time.Sleep(1 * time.Millisecond) } } }() return nil }