gameserver

package
v0.2.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2024 License: MIT, BSD-2-Clause Imports: 24 Imported by: 0

README

Waiter

A game server for Cube 2: Sauerbraten forked from sauerbraten/waiter.

Features

What works:

  • ffa, insta, insta team, effic, effic team, tactics, tactics team
  • ctf, insta ctf, effic ctf
  • chat, team chat
  • changing weapon, shooting, killing, suiciding, spawning
  • global auth (/auth and /authkick)
  • local auth (/sauth, /dauth, /sauthkick, /dauthkick, auth-on-connect)
  • sharing master
  • setting mastermode
  • forcing gamemode and/or map
  • pausing & resuming (with countdown)
  • locking teams (keepteams server command)
  • queueing maps (queuemap server command)
  • changing your name
  • extinfo (server mod ID: -9)

Server commands:

These can be used either as #cmd bla foo or /servcmd cmd bla foo:

  • keepteams 0|1 (a.k.a. persist): set to 1 to disable randomizing teams on map load
  • queuemap [map...]: check the map queue or enqueue one or more maps
  • competitive 0|1: in competitive mode, the server waits for all players to load the map before starting the game, and automatically pauses the game when a player leaves or goes to spectating mode

Pretty much everything else is not yet implemented:

  • any modes requiring bases (capture) or tokens (collect)
  • demo recording
  • /checkmaps (will compare against server-side hash, not majority)
  • overtime (& maybe golden goal)

Some things are specifically not planned and will likely never be implemented:

  • bots
  • map voting
  • coop edit mode (including /sendmap and /getmap)
  • claiming privileges using /setmaster 1 (relinquishing them with /setmaster 0 and sharing master using /setmaster 1 <cn> already works)

Building

Make sure you have Go installed as well as the ENet development headers (on Fedora: sudo dnf install enet-devel, on macOS: brew install enet). Clone the repository, cd waiter, then make all.

You can then start the server with ./waiter. The server requires config.json, bans.json and users.json to be placed in the working directory.

To Do

  • capture and regen capture (capture base events)
  • intermission stats (depending on mode)
  • #stats command
  • store frags, deaths, etc. in case a player re-connects

Project Structure

All functionality is organized into packages. /cmd/waiter/ contains the actual command to start a server, i.e. configuration file parsing, initialization of all components, and preliminary handling of incoming packets. Detailed packet handling can be found in /pkg/gameserver/ along with other server logic like managing the current game. /pkg/game/ has game mode logic like teams, timing, flags, and so on. Protocol definitions (like network message codes) can be found in pkg/protocol.

Other interesting packages:

In cmd/genauth, there is a command to generate auth keys for users. While you can use auth keys generated with Sauerbraten's /genauthkey command, genauth provides better output (auth.cfg line for the player, JSON object for this server's users.json file).

Why?

I started this mainly as a challenge to myself and because I have ideas to improve the integration of Sauerbraten servers with other services and interfaces. For example, making the server state and game events available via WebSockets in real-time, instead of the UDP-based extinfo protocol, and integrating a third-party auth system (spanning multiple servers).

Writing a server that makes it easy to modify gameplay is not one of the goals of this project, neither is plugin support, although it might happen at some point. If you want that, now, use pisto's great spaghettimod.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var SetTimeLeft = &ServerCommand{
	name:        "settime",
	argsFormat:  "[Xm][Ys]",
	aliases:     []string{"time", "settimeleft", "settimeremaining", "timeleft", "timeremaining"},
	description: "sets the time remaining to play to X minutes and Y seconds",
	minRole:     role.Admin,
	f: func(s *Server, c *Client, args []string) {
		if len(args) < 1 {
			return
		}

		d, err := time.ParseDuration(args[0])
		if err != nil {
			c.Message(cubecode.Error("could not parse duration: " + err.Error()))
			return
		}

		if d == 0 {
			d = 1 * time.Second
			s.Message(fmt.Sprintf("%s forced intermission", s.Clients.UniqueName(c)))
		} else {
			s.Message(fmt.Sprintf("%s set the time remaining to %s", s.Clients.UniqueName(c), d))
		}

		s.Clock.SetTimeLeft(d)
	},
}
View Source
var ToggleCompetitiveMode = &ServerCommand{
	name:        "competitive",
	argsFormat:  "0|1",
	aliases:     []string{"comp"},
	description: "in competitive mode, the server waits for all clients to load the map and auto-pauses when a player leaves the game",
	minRole:     role.Master,
	f: func(s *Server, c *Client, args []string) {
		changed := false
		if len(args) >= 1 {
			val, err := strconv.Atoi(args[0])
			if err != nil || (val != 0 && val != 1) {
				return
			}
			changed = s.CompetitiveMode != (val == 1)
			switch val {
			case 1:

				s.CompetitiveMode = true

				s.SetMasterMode(c, mastermode.Locked)
			default:
				s.CompetitiveMode = false
			}
		}
		if changed {
			if s.CompetitiveMode {
				s.Clients.Message("competitive mode will be enabled with next game")
			} else {
				s.Clients.Message("competitive mode will be disabled with next game")
			}
		} else {
			if s.CompetitiveMode {
				c.Message("competitive mode is on")
			} else {
				c.Message("competitive mode is off")
			}
		}
	},
}
View Source
var ToggleKeepTeams = &ServerCommand{
	name:        "keepteams",
	argsFormat:  "0|1",
	aliases:     []string{"persist", "persistteams"},
	description: "keeps teams the same across map change",
	minRole:     role.Master,
	f: func(s *Server, c *Client, args []string) {
		changed := false
		if len(args) >= 1 {
			val, err := strconv.Atoi(args[0])
			if err != nil || (val != 0 && val != 1) {
				return
			}
			changed = s.KeepTeams != (val == 1)
			s.KeepTeams = val == 1
		}
		if changed {
			if s.KeepTeams {
				s.Clients.Message("teams will be kept")
			} else {
				s.Clients.Message("teams will be shuffled")
			}
		} else {
			if s.KeepTeams {
				c.Message("teams will be kept")
			} else {
				c.Message("teams will be shuffled")
			}
		}
	},
}
View Source
var ToggleReportStats = &ServerCommand{
	name:        "reportstats",
	argsFormat:  "0|1",
	aliases:     []string{"repstats"},
	description: "when enabled, end-game stats of players will be reported at intermission",
	minRole:     role.Admin,
	f: func(s *Server, c *Client, args []string) {
		changed := false
		if len(args) >= 1 {
			val, err := strconv.Atoi(args[0])
			if err != nil || (val != 0 && val != 1) {
				return
			}
			changed = s.ReportStats != (val == 1)
			s.ReportStats = val == 1
		}
		if changed {
			if s.ReportStats {
				s.Clients.Message("stats will be reported at intermission")
			} else {
				s.Clients.Message("stats will not be reported")
			}
		} else {
			if s.ReportStats {
				c.Message("stats reporting is on")
			} else {
				c.Message("stats reporting is off")
			}
		}
	},
}

Functions

This section is empty.

Types

type Authentication

type Authentication struct {
	// contains filtered or unexported fields
}

type Client

type Client struct {
	game.Player

	Role                role.ID
	Joined              bool                // true if the player is actually in the game
	AuthRequiredBecause disconnectreason.ID // e.g. server is in private mode
	SessionID           uint32
	Ping                int32
	Positions           *relay.Publisher
	Packets             *relay.Publisher
	Authentications     map[string]*Authentication
	// contains filtered or unexported fields
}

Describes a client.

func NewClient

func NewClient(cn uint32, sessionId uint32, outgoing Outgoing) *Client

func (*Client) GrantMaster

func (c *Client) GrantMaster()

func (*Client) Message

func (c *Client) Message(text string)

func (*Client) RefreshWelcome

func (c *Client) RefreshWelcome()

func (*Client) Send

func (c *Client) Send(messages ...protocol.Message)

func (*Client) String

func (c *Client) String() string

type ClientManager

type ClientManager struct {
	// contains filtered or unexported fields
}

func (*ClientManager) Add

func (cm *ClientManager) Add(sessionId uint32, outgoing Outgoing) *Client

func (*ClientManager) Broadcast

func (cm *ClientManager) Broadcast(messages ...P.Message)

Sends a packet to all clients currently in use.

func (*ClientManager) Disconnect

func (cm *ClientManager) Disconnect(c *Client, reason disconnectreason.ID)

Tells other clients that the client disconnected, giving a disconnect reason in case it's not a normal leave.

func (*ClientManager) FindClientByName

func (cm *ClientManager) FindClientByName(name string) *Client

func (*ClientManager) ForEach

func (cm *ClientManager) ForEach(do func(c *Client))

func (*ClientManager) GetClientByCN

func (cm *ClientManager) GetClientByCN(cn uint32) *Client

func (*ClientManager) GetClientByID

func (cm *ClientManager) GetClientByID(sessionId uint32) *Client

func (*ClientManager) GetNumClients

func (cm *ClientManager) GetNumClients() (n int)

Returns the number of connected clients.

func (*ClientManager) InformOthersOfJoin

func (cm *ClientManager) InformOthersOfJoin(c *Client)

Informs all other clients that a client joined the game.

func (*ClientManager) Message

func (cm *ClientManager) Message(message string)

func (*ClientManager) PrivilegedUsers

func (cm *ClientManager) PrivilegedUsers() (privileged []*Client)

func (*ClientManager) Relay

func (cm *ClientManager) Relay(from *Client, messages ...P.Message)

func (*ClientManager) SendToTeam

func (cm *ClientManager) SendToTeam(c *Client, messages ...P.Message)

Send a packet to a client's team, but not the client himself, over the specified channel.

func (*ClientManager) UniqueName

func (cm *ClientManager) UniqueName(c *Client) string

type Config

type Config struct {
	MaxClients       int
	MatchLength      int
	DefaultGameSpeed int
	DefaultMode      string
	DefaultMap       string
	Maps             []string
}

type Incoming

type Incoming <-chan ServerPacket

type MapEdit

type MapEdit struct {
	Client  uint32
	Message P.Message
}

type Outgoing

type Outgoing chan<- ServerPacket

type Server

type Server struct {
	utils.Session

	*Config
	*State

	Description string

	Clients *ClientManager

	Commands *commands.CommandGroup[*Client]

	Broadcasts *utils.Topic[[]P.Message]
	Edits      *utils.Topic[MapEdit]

	// non-standard stuff
	KeepTeams       bool
	CompetitiveMode bool
	ReportStats     bool
	// contains filtered or unexported fields
}

func New

func New(ctx context.Context, conf *Config) *Server

func (*Server) AuthKick

func (s *Server) AuthKick(client *Client, rol role.ID, domain, name string, victim *Client, reason string)

func (*Server) Broadcast

func (s *Server) Broadcast(messages ...P.Message)

func (*Server) BroadcastTime

func (s *Server) BroadcastTime(seconds int)

func (*Server) ChangeMap

func (s *Server) ChangeMap(mode int32, map_ string)

func (*Server) ConfirmSpawn

func (s *Server) ConfirmSpawn(client *Client, lifeSequence, _weapon int32)

func (*Server) Connect

func (s *Server) Connect(sessionId uint32) (*Client, <-chan bool)

func (*Server) Disconnect

func (s *Server) Disconnect(client *Client, reason disconnectreason.ID)

func (*Server) Empty

func (s *Server) Empty()

func (*Server) EmptyMap

func (s *Server) EmptyMap()

func (*Server) ForEachPlayer

func (s *Server) ForEachPlayer(f func(p *game.Player))

func (*Server) ForceRespawn

func (s *Server) ForceRespawn(target *Client)

Forcibly respawn a player. Passing nil respawns all non-spectating players.

func (*Server) GameDuration

func (s *Server) GameDuration() time.Duration

func (*Server) HandleExplode

func (s *Server) HandleExplode(client *Client, millis int32, wpn weapon.Weapon, id int32, hits []hit)

func (*Server) HandlePacket

func (s *Server) HandlePacket(client *Client, channelID uint8, message P.Message)

parses a packet and decides what to do based on the network message code at the front of the packet

func (*Server) HandleShoot

func (s *Server) HandleShoot(client *Client, wpn weapon.Weapon, id int32, from, to *geom.Vector, hits []hit)

func (*Server) Incoming

func (s *Server) Incoming() chan<- ServerPacket

func (*Server) Intermission

func (s *Server) Intermission()

func (*Server) Join

func (s *Server) Join(c *Client)

Puts a client into the current game, using the data the client provided with his nmc.TryJoin packet.

func (*Server) Kick

func (s *Server) Kick(client *Client, victim *Client, reason string)

func (*Server) Leave

func (s *Server) Leave(sessionId uint32)

func (*Server) MapChange

func (s *Server) MapChange()

func (*Server) Message

func (s *Server) Message(message string)

func (*Server) NumberOfPlayers

func (s *Server) NumberOfPlayers() (n int)

Returns the number of connected clients playing (i.e. joined and not spectating)

func (*Server) Outgoing

func (s *Server) Outgoing() <-chan ServerPacket

func (*Server) Pause

func (s *Server) Pause()

func (*Server) Poll

func (s *Server) Poll(ctx context.Context)

func (*Server) PrivilegedUsersPacket

func (s *Server) PrivilegedUsersPacket() (P.Message, bool)

func (*Server) ReceiveMaps

func (s *Server) ReceiveMaps() <-chan string

func (*Server) RefreshServerInfo

func (s *Server) RefreshServerInfo()

Send the server info to clients again, which updates the description on the scoreboard.

func (*Server) RefreshTime

func (s *Server) RefreshTime()

func (*Server) ResetPlayers

func (s *Server) ResetPlayers(resetFrags bool)

Kill all players, reset their scores (if resetFrags is true), and respawn them.

func (*Server) Resume

func (s *Server) Resume()

func (*Server) SendWelcome

func (s *Server) SendWelcome(c *Client)

Sends 'welcome' information to a newly joined client like map, mode, time left, other players, etc.

func (*Server) SetDescription

func (s *Server) SetDescription(description string)

func (*Server) SetMap

func (s *Server) SetMap(map_ string)

func (*Server) SetMasterMode

func (s *Server) SetMasterMode(c *Client, mm mastermode.ID)

func (*Server) SetMode

func (s *Server) SetMode(mode int32)

func (*Server) SetPublicServer

func (s *Server) SetPublicServer(mm mastermode.ID)

func (*Server) Spawn

func (s *Server) Spawn(client *Client)

func (*Server) StartGame

func (s *Server) StartGame(mode game.Mode, mapname string)

func (*Server) StartMode

func (s *Server) StartMode(id gamemode.ID) game.Mode

func (*Server) TryJoin

func (s *Server) TryJoin(c *Client, name string, playerModel int32, authDomain, authName string)

func (*Server) UniqueName

func (s *Server) UniqueName(p *game.Player) string

func (*Server) Unsupervised

func (s *Server) Unsupervised()

type ServerCommand

type ServerCommand struct {
	// contains filtered or unexported fields
}

func (*ServerCommand) Detailed

func (cmd *ServerCommand) Detailed() string

func (*ServerCommand) String

func (cmd *ServerCommand) String() string

type ServerCommands

type ServerCommands struct {
	// contains filtered or unexported fields
}

func NewCommands

func NewCommands(s *Server, cmds ...*ServerCommand) *ServerCommands

func (*ServerCommands) Handle

func (sc *ServerCommands) Handle(c *Client, msg string)

func (*ServerCommands) PrintCommands

func (sc *ServerCommands) PrintCommands(c *Client)

func (*ServerCommands) Register

func (sc *ServerCommands) Register(cmd *ServerCommand)

func (*ServerCommands) Unregister

func (sc *ServerCommands) Unregister(cmd *ServerCommand)

type ServerPacket

type ServerPacket struct {
	// Either the sender (if incoming) or the recipient (if outgoing)
	Session  uint32
	Channel  uint8
	Messages []P.Message
}

type State

type State struct {
	Clock      game.Clock
	MasterMode mastermode.ID
	GameMode   game.Mode
	Map        string
	UpSince    time.Time
	NumClients func() int // number of clients connected
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL