From f29bb81865701ede9bb8393d1da827435d56274d Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 20 Aug 2020 08:38:32 -0400 Subject: [PATCH] Fix config map time limit and commands, add client password --- Makefile | 2 +- README.md | 42 +++++++++++++++++++++- config.yaml | 8 ++++- example.yaml | 9 +++-- internal/quake/client/router.go | 13 ++++++- internal/quake/server/config.go | 63 +++++++++++++++++++++------------ internal/quake/server/server.go | 2 +- public/index.html | 17 ++++++--- public/zz_generated.static.go | 4 +-- 9 files changed, 123 insertions(+), 37 deletions(-) diff --git a/Makefile b/Makefile index cfe85d2..0a09e29 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ q3: gen gen: ## Generate and embed templates @go run tools/genstatic.go public public -VERSION ?= v1.0.3 +VERSION ?= v1.0.4 IMAGE ?= docker.io/criticalstack/quake:$(VERSION) .PHONY: build diff --git a/README.md b/README.md index 6e190b1..bb6b600 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,49 @@ Any commands not captured by the config yaml can be specified in the `commands` commands: - seta g_inactivity 600 - seta sv_timeout 120 - ``` +### Add bots + +Bots can be added individually to map rotations using the `commands` section of the config: + +```yaml +commands: + - addbot crash 1 + - addbot sarge 2 +``` + +The `addbot` server command requires the name of the bot and skill level (crash and sarge are a couple of the built-in bots). + +Another way to add bots is by setting a minimum number of players to allow the server to add bots up to a certain value (removed when human players join): + +```yaml +bot: + minPlayers: 8 +game: + singlePlayerSkill: 2 +``` + +`singlePlayerSkill` can be used to set the skill level of the automatically added bots (2 is the default skill level). + +### Setting a password + +A password should be set for the server to allow remote administration and is found in the server configuration settings: + +```yaml +server: + password: "changeme" +``` + +This will allow clients to use `\rcon changeme ` to remotely administrate the server. To create a password that must be provided by clients to connect: + +```yaml +game: + password: "letmein" +``` + +This will add an additional dialog to the in-browser client to accept the password. It will only appear if the server indicates it needs a password. + ### Add custom maps The content server hosts a small upload app to allow uploading `pk3` or `zip` files containing maps. The content server in the [example.yaml](example.yaml) shares a volume with the game server, effectively "side-loading" the map content, however, in the future the game server will introspect into the maps and make sure that it can fulfill the users map configuration before starting. diff --git a/config.yaml b/config.yaml index b5d78b6..9db2d34 100644 --- a/config.yaml +++ b/config.yaml @@ -1,19 +1,25 @@ fragLimit: 25 timeLimit: 15m +bot: + minPlayers: 3 game: motd: "Welcome to Critical Stack" type: FreeForAll forceRespawn: false inactivity: 10m + #password: "letmein" quadFactor: 3 weaponRespawn: 3 server: hostname: "quakekube" - maxClients: 12 + maxClients: 16 password: "changeme" +commands: + - addbot sarge 2 maps: - name: q3dm7 type: FreeForAll + timeLimit: 10m - name: q3dm17 type: FreeForAll - name: q3wctf1 diff --git a/example.yaml b/example.yaml index 9bf85dd..4cd73f7 100644 --- a/example.yaml +++ b/example.yaml @@ -22,7 +22,7 @@ spec: - --config=/config/config.yaml - --content-server=http://localhost:9090 - --agree-eula - image: docker.io/criticalstack/quake:v1.0.3 + image: docker.io/criticalstack/quake:v1.0.4 name: server ports: - containerPort: 8080 @@ -40,7 +40,7 @@ spec: - q3 - content - --seed-content-url=http://content.quakejs.com - image: docker.io/criticalstack/quake:v1.0.3 + image: docker.io/criticalstack/quake:v1.0.4 name: content-server ports: - containerPort: 9090 @@ -84,6 +84,8 @@ data: config.yaml: | fragLimit: 25 timeLimit: 15m + bot: + minPlayers: 3 game: motd: "Welcome to Critical Stack" type: FreeForAll @@ -96,10 +98,11 @@ data: maxClients: 12 password: "changeme" commands: - - seta g_inactivity 600 + - addbot sarge 2 maps: - name: q3dm7 type: FreeForAll + timeLimit: 10m - name: q3dm17 type: FreeForAll - name: q3wctf1 diff --git a/internal/quake/client/router.go b/internal/quake/client/router.go index 329e00d..fa05c9e 100644 --- a/internal/quake/client/router.go +++ b/internal/quake/client/router.go @@ -48,8 +48,19 @@ func NewRouter(cfg *Config) (*echo.Echo, error) { // default route e.GET("/", func(c echo.Context) error { - return c.Render(http.StatusOK, "index", map[string]string{ + m, err := quakenet.GetInfo(cfg.ServerAddr) + if err != nil { + return err + } + needsPass := false + if v, ok := m["g_needpass"]; ok { + if v == "1" { + needsPass = true + } + } + return c.Render(http.StatusOK, "index", map[string]interface{}{ "ServerAddr": cfg.ServerAddr, + "NeedsPass": needsPass, }) }) diff --git a/internal/quake/server/config.go b/internal/quake/server/config.go index ea69f8f..b9f0fd8 100644 --- a/internal/quake/server/config.go +++ b/internal/quake/server/config.go @@ -61,6 +61,7 @@ type Config struct { FragLimit int `name:"fraglimit"` TimeLimit metav1.Duration `name:"timelimit"` + BotConfig `json:"bot"` GameConfig `json:"game"` FileServerConfig `json:"fs"` ServerConfig `json:"server"` @@ -69,14 +70,21 @@ type Config struct { Maps } +type BotConfig struct { + MinPlayers int `name:"bot_minplayers"` + NoChat bool `name:"bot_nochat"` +} + type GameConfig struct { - ForceRespawn bool `name:"g_forcerespawn"` - GameType GameType `json:"type" name:"g_gametype"` - Inactivity metav1.Duration `name:"g_inactivity"` - Log string `name:"g_log"` - MOTD string `name:"g_motd"` - QuadFactor int `name:"g_quadfactor"` - WeaponRespawn int `name:"g_weaponrespawn"` + ForceRespawn bool `name:"g_forcerespawn"` + GameType GameType `json:"type" name:"g_gametype"` + Inactivity metav1.Duration `name:"g_inactivity"` + Log string `name:"g_log"` + MOTD string `name:"g_motd"` + Password string `name:"g_password"` + QuadFactor int `name:"g_quadfactor"` + SinglePlayerSkill int `name:"g_spSkill"` + WeaponRespawn int `name:"g_weaponrespawn"` } type FileServerConfig struct { @@ -128,8 +136,6 @@ func writeStruct(v reflect.Value) ([]byte, error) { data, _ := val.Marshal() b.Write(data) case []string: - data := strings.Join(val, "\n") - b.WriteString(fmt.Sprintf("%s\n", data)) default: panic(fmt.Errorf("received unknown type %T", val)) } @@ -149,6 +155,15 @@ func writeStruct(v reflect.Value) ([]byte, error) { } } } + for i := 0; i < v.Type().NumField(); i++ { + if v.Type().Field(i).Name == "Commands" { + cmds := v.Field(i).Interface().([]string) + for _, cmd := range cmds { + b.WriteString(cmd) + b.WriteString("\n") + } + } + } return b.Bytes(), nil } @@ -185,14 +200,18 @@ func Default() *Config { FragLimit: 25, TimeLimit: metav1.Duration{Duration: 15 * time.Minute}, Commands: []string{}, + BotConfig: BotConfig{ + NoChat: true, + }, GameConfig: GameConfig{ - Log: "", - MOTD: "Welcome to Critical Stack", - QuadFactor: 3, - GameType: FreeForAll, - WeaponRespawn: 3, - Inactivity: metav1.Duration{Duration: 10 * time.Minute}, - ForceRespawn: false, + Log: "", + MOTD: "Welcome to Critical Stack", + QuadFactor: 3, + GameType: FreeForAll, + WeaponRespawn: 3, + Inactivity: metav1.Duration{Duration: 10 * time.Minute}, + SinglePlayerSkill: 2, + ForceRespawn: false, }, ServerConfig: ServerConfig{ MaxClients: 12, @@ -210,9 +229,9 @@ type Map struct { Name string `json:"name"` Type GameType `json:"type"` - CaptureLimit int `json:"captureLimit"` - FragLimit int `json:"fragLimit"` - TimeLimit time.Duration `json:"timeLimit"` + CaptureLimit int `json:"captureLimit"` + FragLimit int `json:"fragLimit"` + TimeLimit metav1.Duration `json:"timeLimit"` } type Maps []Map @@ -229,8 +248,8 @@ func (maps Maps) Marshal() ([]byte, error) { if m.FragLimit != 0 { cmds = append(cmds, fmt.Sprintf("fraglimit %d", m.FragLimit)) } - if m.TimeLimit != 0 { - cmds = append(cmds, fmt.Sprintf("timelimit %d", int(m.TimeLimit.Minutes()))) + if m.TimeLimit.Duration != 0 { + cmds = append(cmds, fmt.Sprintf("timelimit %s", toString("TimeLimit", reflect.ValueOf(m.TimeLimit)))) } cmds = append(cmds, fmt.Sprintf("map %s", m.Name)) nextmap := "d0" @@ -240,6 +259,6 @@ func (maps Maps) Marshal() ([]byte, error) { cmds = append(cmds, fmt.Sprintf("set nextmap vstr %s", nextmap)) b.WriteString(fmt.Sprintf("set d%d \"seta %s\"\n", i, strings.Join(cmds, " ; "))) } - b.WriteString("vstr d0") + b.WriteString("vstr d0\n") return b.Bytes(), nil } diff --git a/internal/quake/server/server.go b/internal/quake/server/server.go index 9807b5b..8c42b06 100644 --- a/internal/quake/server/server.go +++ b/internal/quake/server/server.go @@ -153,7 +153,7 @@ func (s *Server) reload() error { if err != nil { return err } - var cfg *Config + cfg := Default() if err := yaml.Unmarshal(data, &cfg); err != nil { return err } diff --git a/public/index.html b/public/index.html index 405b2e6..01b4e9e 100644 --- a/public/index.html +++ b/public/index.html @@ -87,7 +87,7 @@ transform: translate(-50%, -60%); z-index: 1; } - .form input[type=text] { + .form input[type=text], [type=password] { margin: 8px 0; display: inline-block; border: 1px solid #ccc; @@ -118,6 +118,9 @@
+ {{ with .NeedsPass }} + + {{ end }}