mirror of
https://github.com/Octops/quake-kube.git
synced 2026-04-05 09:10:34 +00:00
Fix config map time limit and commands, add client password
This commit is contained in:
2
Makefile
2
Makefile
@ -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
|
||||||
|
|||||||
42
README.md
42
README.md
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
Reference in New Issue
Block a user