Fix config map time limit and commands, add client password

This commit is contained in:
chris
2020-08-20 08:38:32 -04:00
committed by Chris Marshall
parent f92180af5e
commit f29bb81865
9 changed files with 123 additions and 37 deletions

View File

@ -9,7 +9,7 @@ q3: gen
gen: ## Generate and embed templates gen: ## Generate and embed templates
@go run tools/genstatic.go public public @go run tools/genstatic.go public public
VERSION ?= v1.0.3 VERSION ?= v1.0.4
IMAGE ?= docker.io/criticalstack/quake:$(VERSION) IMAGE ?= docker.io/criticalstack/quake:$(VERSION)
.PHONY: build .PHONY: build

View File

@ -107,9 +107,49 @@ Any commands not captured by the config yaml can be specified in the `commands`
commands: commands:
- seta g_inactivity 600 - seta g_inactivity 600
- seta sv_timeout 120 - 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 <cmd>` 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 ### 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. 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.

View File

@ -1,19 +1,25 @@
fragLimit: 25 fragLimit: 25
timeLimit: 15m timeLimit: 15m
bot:
minPlayers: 3
game: game:
motd: "Welcome to Critical Stack" motd: "Welcome to Critical Stack"
type: FreeForAll type: FreeForAll
forceRespawn: false forceRespawn: false
inactivity: 10m inactivity: 10m
#password: "letmein"
quadFactor: 3 quadFactor: 3
weaponRespawn: 3 weaponRespawn: 3
server: server:
hostname: "quakekube" hostname: "quakekube"
maxClients: 12 maxClients: 16
password: "changeme" password: "changeme"
commands:
- addbot sarge 2
maps: maps:
- name: q3dm7 - name: q3dm7
type: FreeForAll type: FreeForAll
timeLimit: 10m
- name: q3dm17 - name: q3dm17
type: FreeForAll type: FreeForAll
- name: q3wctf1 - name: q3wctf1

View File

@ -22,7 +22,7 @@ spec:
- --config=/config/config.yaml - --config=/config/config.yaml
- --content-server=http://localhost:9090 - --content-server=http://localhost:9090
- --agree-eula - --agree-eula
image: docker.io/criticalstack/quake:v1.0.3 image: docker.io/criticalstack/quake:v1.0.4
name: server name: server
ports: ports:
- containerPort: 8080 - containerPort: 8080
@ -40,7 +40,7 @@ spec:
- q3 - q3
- content - content
- --seed-content-url=http://content.quakejs.com - --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 name: content-server
ports: ports:
- containerPort: 9090 - containerPort: 9090
@ -84,6 +84,8 @@ data:
config.yaml: | config.yaml: |
fragLimit: 25 fragLimit: 25
timeLimit: 15m timeLimit: 15m
bot:
minPlayers: 3
game: game:
motd: "Welcome to Critical Stack" motd: "Welcome to Critical Stack"
type: FreeForAll type: FreeForAll
@ -96,10 +98,11 @@ data:
maxClients: 12 maxClients: 12
password: "changeme" password: "changeme"
commands: commands:
- seta g_inactivity 600 - addbot sarge 2
maps: maps:
- name: q3dm7 - name: q3dm7
type: FreeForAll type: FreeForAll
timeLimit: 10m
- name: q3dm17 - name: q3dm17
type: FreeForAll type: FreeForAll
- name: q3wctf1 - name: q3wctf1

View File

@ -48,8 +48,19 @@ func NewRouter(cfg *Config) (*echo.Echo, error) {
// default route // default route
e.GET("/", func(c echo.Context) error { 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, "ServerAddr": cfg.ServerAddr,
"NeedsPass": needsPass,
}) })
}) })

View File

@ -61,6 +61,7 @@ type Config struct {
FragLimit int `name:"fraglimit"` FragLimit int `name:"fraglimit"`
TimeLimit metav1.Duration `name:"timelimit"` TimeLimit metav1.Duration `name:"timelimit"`
BotConfig `json:"bot"`
GameConfig `json:"game"` GameConfig `json:"game"`
FileServerConfig `json:"fs"` FileServerConfig `json:"fs"`
ServerConfig `json:"server"` ServerConfig `json:"server"`
@ -69,14 +70,21 @@ type Config struct {
Maps Maps
} }
type BotConfig struct {
MinPlayers int `name:"bot_minplayers"`
NoChat bool `name:"bot_nochat"`
}
type GameConfig struct { type GameConfig struct {
ForceRespawn bool `name:"g_forcerespawn"` ForceRespawn bool `name:"g_forcerespawn"`
GameType GameType `json:"type" name:"g_gametype"` GameType GameType `json:"type" name:"g_gametype"`
Inactivity metav1.Duration `name:"g_inactivity"` Inactivity metav1.Duration `name:"g_inactivity"`
Log string `name:"g_log"` Log string `name:"g_log"`
MOTD string `name:"g_motd"` MOTD string `name:"g_motd"`
QuadFactor int `name:"g_quadfactor"` Password string `name:"g_password"`
WeaponRespawn int `name:"g_weaponrespawn"` QuadFactor int `name:"g_quadfactor"`
SinglePlayerSkill int `name:"g_spSkill"`
WeaponRespawn int `name:"g_weaponrespawn"`
} }
type FileServerConfig struct { type FileServerConfig struct {
@ -128,8 +136,6 @@ func writeStruct(v reflect.Value) ([]byte, error) {
data, _ := val.Marshal() data, _ := val.Marshal()
b.Write(data) b.Write(data)
case []string: case []string:
data := strings.Join(val, "\n")
b.WriteString(fmt.Sprintf("%s\n", data))
default: default:
panic(fmt.Errorf("received unknown type %T", val)) 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 return b.Bytes(), nil
} }
@ -185,14 +200,18 @@ func Default() *Config {
FragLimit: 25, FragLimit: 25,
TimeLimit: metav1.Duration{Duration: 15 * time.Minute}, TimeLimit: metav1.Duration{Duration: 15 * time.Minute},
Commands: []string{}, Commands: []string{},
BotConfig: BotConfig{
NoChat: true,
},
GameConfig: GameConfig{ GameConfig: GameConfig{
Log: "", Log: "",
MOTD: "Welcome to Critical Stack", MOTD: "Welcome to Critical Stack",
QuadFactor: 3, QuadFactor: 3,
GameType: FreeForAll, GameType: FreeForAll,
WeaponRespawn: 3, WeaponRespawn: 3,
Inactivity: metav1.Duration{Duration: 10 * time.Minute}, Inactivity: metav1.Duration{Duration: 10 * time.Minute},
ForceRespawn: false, SinglePlayerSkill: 2,
ForceRespawn: false,
}, },
ServerConfig: ServerConfig{ ServerConfig: ServerConfig{
MaxClients: 12, MaxClients: 12,
@ -210,9 +229,9 @@ type Map struct {
Name string `json:"name"` Name string `json:"name"`
Type GameType `json:"type"` Type GameType `json:"type"`
CaptureLimit int `json:"captureLimit"` CaptureLimit int `json:"captureLimit"`
FragLimit int `json:"fragLimit"` FragLimit int `json:"fragLimit"`
TimeLimit time.Duration `json:"timeLimit"` TimeLimit metav1.Duration `json:"timeLimit"`
} }
type Maps []Map type Maps []Map
@ -229,8 +248,8 @@ func (maps Maps) Marshal() ([]byte, error) {
if m.FragLimit != 0 { if m.FragLimit != 0 {
cmds = append(cmds, fmt.Sprintf("fraglimit %d", m.FragLimit)) cmds = append(cmds, fmt.Sprintf("fraglimit %d", m.FragLimit))
} }
if m.TimeLimit != 0 { if m.TimeLimit.Duration != 0 {
cmds = append(cmds, fmt.Sprintf("timelimit %d", int(m.TimeLimit.Minutes()))) cmds = append(cmds, fmt.Sprintf("timelimit %s", toString("TimeLimit", reflect.ValueOf(m.TimeLimit))))
} }
cmds = append(cmds, fmt.Sprintf("map %s", m.Name)) cmds = append(cmds, fmt.Sprintf("map %s", m.Name))
nextmap := "d0" nextmap := "d0"
@ -240,6 +259,6 @@ func (maps Maps) Marshal() ([]byte, error) {
cmds = append(cmds, fmt.Sprintf("set nextmap vstr %s", nextmap)) 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(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 return b.Bytes(), nil
} }

View File

@ -153,7 +153,7 @@ func (s *Server) reload() error {
if err != nil { if err != nil {
return err return err
} }
var cfg *Config cfg := Default()
if err := yaml.Unmarshal(data, &cfg); err != nil { if err := yaml.Unmarshal(data, &cfg); err != nil {
return err return err
} }

View File

@ -87,7 +87,7 @@
transform: translate(-50%, -60%); transform: translate(-50%, -60%);
z-index: 1; z-index: 1;
} }
.form input[type=text] { .form input[type=text], [type=password] {
margin: 8px 0; margin: 8px 0;
display: inline-block; display: inline-block;
border: 1px solid #ccc; border: 1px solid #ccc;
@ -118,6 +118,9 @@
<div class="centered"> <div class="centered">
<form class="form"> <form class="form">
<input type="text" id="playerName"> <input type="text" id="playerName">
{{ with .NeedsPass }}
<input type="password" placeholder="password" id="password">
{{ end }}
<button onclick="join()" type="submit" class="button">Join</button> <button onclick="join()" type="submit" class="button">Join</button>
</form> </form>
<script type="text/javascript"> <script type="text/javascript">
@ -128,15 +131,19 @@
document.getElementById("playerName").placeholder = placeholder document.getElementById("playerName").placeholder = placeholder
function join(){ function join(){
var inputPlayerName = document.getElementById("playerName"); var inputPlayerName = document.getElementById("playerName");
if (inputPlayerName.value != "") { if (inputPlayerName.value != "") {
localStorage.setItem("playerName", inputPlayerName.value); localStorage.setItem("playerName", inputPlayerName.value);
} }
host = document.location.host host = document.location.host
if (!host.includes(":")) { host = host + ":80" } if (!host.includes(":")) { host = host + ":80" }
var args = ['+set', 'fs_cdn', host, '+connect', host]; var args = ['+set', 'fs_cdn', host, '+connect', host];
args.push.apply(args, ['+set', 'cl_allowDownload', '1']) args.push.apply(args, ['+set', 'cl_allowDownload', '1'])
args.push.apply(args, ['+name', localStorage.playerName]) args.push.apply(args, ['+name', localStorage.playerName])
args.push.apply(args, getQueryCommands()); args.push.apply(args, getQueryCommands());
var inputPassword = document.getElementById("password");
if (inputPassword && inputPassword.value != "") {
args.push.apply(args, ['+set', 'password', inputPassword.value]);
}
var element = document.getElementById("main"); var element = document.getElementById("main");
element.parentNode.removeChild(element); element.parentNode.removeChild(element);
ioq3.callMain(args); ioq3.callMain(args);

File diff suppressed because one or more lines are too long