Compare commits
19
Commits
17f6477b71
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c27519497 | ||
|
|
5be0d08b84 | ||
|
|
f80c715b72 | ||
|
|
bb4fd3e11c | ||
|
|
1a28f6c52e | ||
|
|
cb79e172b9 | ||
|
|
1a5959c46e | ||
|
|
ac3febf109 | ||
|
|
1902c62366 | ||
|
|
a918704ec6 | ||
|
|
5c192127f4 | ||
|
|
b9d445c57d | ||
|
|
1fcf05cc27 | ||
|
|
6baf4796fa | ||
|
|
143a074c7d | ||
|
|
f1271341ef | ||
|
|
f11c5eaf2f | ||
|
|
709cdb51e8 | ||
|
|
d225e49a8a |
No files matched your search
@@ -39,10 +39,26 @@ Know this before writing anything:
|
|||||||
- No real credentials, tokens or IDs anywhere in this repo. A value a server can't start without (a
|
- No real credentials, tokens or IDs anywhere in this repo. A value a server can't start without (a
|
||||||
player ID, a license token) goes in `test.yaml` under `secrets`, mapped to a CI secret name.
|
player ID, a license token) goes in `test.yaml` under `secrets`, mapped to a CI secret name.
|
||||||
|
|
||||||
|
### The image: SteamCMD first
|
||||||
|
- If the game's dedicated server is on Steam and ships a Linux build, the template is
|
||||||
|
`defaultDockerImage: cm2network/steamcmd` with `installMethod: STEAMCMD` and the **server's** app id
|
||||||
|
(not the game's). Never a community image of that server (`wolveix/...`, `lloesche/...` and the like):
|
||||||
|
they keep game files and saves in their own folders (`/config`, `/home/<user>`), outside `/server`, so
|
||||||
|
everything is lost on the next start, and no setting can move them.
|
||||||
|
- Community images are only for servers that aren't on Steam for Linux: Windows-only servers that need
|
||||||
|
Proton or Wine, and games not on Steam.
|
||||||
|
- Many Linux servers save under `$HOME` (Unreal Engine games: `~/.config/Epic/<Game>/Saved`). In
|
||||||
|
`cm2network/steamcmd`, `$HOME` is `/home/steam`, outside the volume. Start the command with
|
||||||
|
`export HOME=/server;` so those saves land in the volume, and list them in `backupPaths` relative to
|
||||||
|
`/server` (e.g. `.config/Epic/<Game>/Saved/SaveGames`).
|
||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
- `defaultPorts` lists **only what players connect to** (game and query ports). RCON, REST and admin
|
- `defaultPorts` lists **only what players connect to** (game and query ports). RCON, REST and admin
|
||||||
ports are **never** published.
|
ports are **never** published.
|
||||||
- Use `"7777/udp"` or `"8211:8211/udp"`. Every port needs its protocol.
|
- Use `"7777/udp"` or `"8211:8211/udp"`. Every port needs its protocol.
|
||||||
|
- A port the game uses over both protocols is listed twice: `"7777/udp"` and `"7777/tcp"`.
|
||||||
|
- A game that tells players a second port (a reliable-messaging or query port it announces) needs it as
|
||||||
|
`{PUBLIC_PORT_<port>}` passed to the game's "external port" option, since nodes may publish it elsewhere.
|
||||||
- Nodes always publish these container ports (only the host side moves when servers share a node).
|
- Nodes always publish these container ports (only the host side moves when servers share a node).
|
||||||
So the game's own port is **not a setting**: write it into the startup command or config file. A
|
So the game's own port is **not a setting**: write it into the startup command or config file. A
|
||||||
game that must announce the port players use gets `{PUBLIC_PORT_<port>}`.
|
game that must announce the port players use gets `{PUBLIC_PORT_<port>}`.
|
||||||
@@ -77,6 +93,20 @@ Know this before writing anything:
|
|||||||
- Take keys and default values from what the server really writes: run
|
- Take keys and default values from what the server really writes: run
|
||||||
`gbrgames first-boot -game <id>` and read the settings in its report.
|
`gbrgames first-boot -game <id>` and read the settings in its report.
|
||||||
- Every key in `defaultConfig` has a `configSchema` property, and the other way round.
|
- Every key in `defaultConfig` has a `configSchema` property, and the other way round.
|
||||||
|
- **Every setting must reach the server.** Settings only arrive as environment variables (and in
|
||||||
|
`configFilePath`, if set). A server that doesn't read environment variables (most official servers,
|
||||||
|
every Unreal Engine server) ignores a setting unless the template delivers it: in `configFilePath`, as a
|
||||||
|
launch option the startup command builds from the variable, or written into the server's own config
|
||||||
|
file by the startup command before it starts. A setting that nothing reads looks editable in the panel
|
||||||
|
and does nothing: leave it out instead.
|
||||||
|
- `configFilePath` writes **every** setting into **one** file. When a server reads its settings from
|
||||||
|
several files (Unreal: `Game.ini`, `Engine.ini`, `GameUserSettings.ini` under
|
||||||
|
`<Game>/Saved/Config/LinuxServer/`), leave `configFilePath` out and write each file from the startup
|
||||||
|
command before the server starts:
|
||||||
|
`printf '[/Script/Engine.GameSession]\nMaxPlayers=%s\n' "${MAX_PLAYERS:-4}" > "$cfg/Game.ini";`.
|
||||||
|
The server may rewrite these files when it stops; writing them on every start keeps the panel in charge.
|
||||||
|
- Never carry over a community image's variable names (`MAXPLAYERS`, `AUTOSAVENUM`, `PUID`, `SKIPUPDATE`...)
|
||||||
|
into a SteamCMD template: only that image's own scripts read them. Use the game's real setting names.
|
||||||
- Every property has a `title`, a short `description` for a player, a `type`, a `default` equal to the
|
- Every property has a `title`, a short `description` for a player, a `type`, a `default` equal to the
|
||||||
`defaultConfig` value, and a `category`: one of `Security & Access`, `Gameplay & Rules`,
|
`defaultConfig` value, and a `category`: one of `Security & Access`, `Gameplay & Rules`,
|
||||||
`World & Environment`, `System & General`. Mark the few that matter when deploying with
|
`World & Environment`, `System & General`. Mark the few that matter when deploying with
|
||||||
|
|||||||
@@ -61,5 +61,13 @@ docker volume prune -af --filter "label!=gbr.cache"
|
|||||||
```
|
```
|
||||||
|
|
||||||
Not `docker system prune`: it also deletes the `gbr-boottest` network whenever no container is using it,
|
Not `docker system prune`: it also deletes the `gbr-boottest` network whenever no container is using it,
|
||||||
and boot tests then refuse to run until `systemctl restart gbr-boottest-network`. Volumes labelled
|
and boot tests then refuse to run until `systemctl restart gbr-boottest-network`.
|
||||||
`gbr.cache` are the AI builder's Steam download cache.
|
|
||||||
|
### Steam download cache
|
||||||
|
|
||||||
|
Boot tests of Steam games (`installMethod: STEAMCMD`) start from a cached install, one Docker volume per
|
||||||
|
app (`gbr-steamcache-<app id>`, label `gbr.cache=steam`), the same cache the AI builder uses. The
|
||||||
|
cache is updated at most once a day; the node-agent's own install then only checks the files and
|
||||||
|
fetches what changed. So Steam isn't asked for the whole game on every run, which keeps traffic from
|
||||||
|
this machine's address low. The prune above keeps these volumes. To drop one game's cache:
|
||||||
|
`docker volume rm gbr-steamcache-<app id>`; `STEAM_CACHE=off` turns the cache off.
|
||||||
@@ -4,10 +4,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Every server the builder starts, first boots included, is deployed by the node-agent's own code
|
// Every server the builder starts, first boots included, is deployed by the node-agent's own code
|
||||||
@@ -73,14 +77,25 @@ var steamCacheFresh = map[string]bool{}
|
|||||||
|
|
||||||
var numericAppID = regexp.MustCompile(`^\d+$`)
|
var numericAppID = regexp.MustCompile(`^\d+$`)
|
||||||
|
|
||||||
|
// When the cache was last updated, in the cache volume (not copied into servers)
|
||||||
|
const steamCacheStamp = ".gbr-cache-updated"
|
||||||
|
|
||||||
|
// steamCacheMaxAge: a cache updated more recently isn't updated again, which saves Steam a login and an
|
||||||
|
// update check per boot; the node-agent's own install still fetches anything newer
|
||||||
|
const steamCacheMaxAge = 24 * time.Hour
|
||||||
|
|
||||||
// seedSteamApp fills a server volume from a cached SteamCMD install of the app, so a boot doesn't
|
// seedSteamApp fills a server volume from a cached SteamCMD install of the app, so a boot doesn't
|
||||||
// download the whole game again: the node-agent's own install then only checks the files. The cache is
|
// download the whole game again: the node-agent's own install then only checks the files. The cache is
|
||||||
// updated once per run. Without it (STEAM_CACHE=off, or the update failing) the agent downloads as usual.
|
// updated at most once a day. Without it (STEAM_CACHE=off, or the update failing) the agent downloads as usual.
|
||||||
func seedSteamApp(appID, volume string) {
|
func seedSteamApp(appID, volume string) {
|
||||||
if os.Getenv("STEAM_CACHE") == "off" || !numericAppID.MatchString(appID) {
|
if os.Getenv("STEAM_CACHE") == "off" || !numericAppID.MatchString(appID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cache := "gbr-steamcache-" + appID
|
cache := "gbr-steamcache-" + appID
|
||||||
|
if !steamCacheFresh[appID] && steamCacheUpdatedWithin(cache, steamCacheMaxAge) {
|
||||||
|
fmt.Printf("[steam-cache] the cached install of Steam app %s was updated in the last %s\n", appID, steamCacheMaxAge)
|
||||||
|
steamCacheFresh[appID] = true
|
||||||
|
}
|
||||||
if !steamCacheFresh[appID] {
|
if !steamCacheFresh[appID] {
|
||||||
fmt.Printf("[steam-cache] updating the cached install of Steam app %s\n", appID)
|
fmt.Printf("[steam-cache] updating the cached install of Steam app %s\n", appID)
|
||||||
if out, err := runCmd("docker", "volume", "create", "--label", "gbr.cache=steam", cache); err != nil {
|
if out, err := runCmd("docker", "volume", "create", "--label", "gbr.cache=steam", cache); err != nil {
|
||||||
@@ -98,12 +113,56 @@ func seedSteamApp(appID, volume string) {
|
|||||||
fmt.Printf("[steam-cache] update failed (%v); the boot downloads the app itself\n", err)
|
fmt.Printf("[steam-cache] update failed (%v); the boot downloads the app itself\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
runCmd("docker", "run", "--rm", "-v", cache+":/server", "alpine", "sh", "-c",
|
||||||
|
fmt.Sprintf("date +%%s > /server/%s", steamCacheStamp))
|
||||||
steamCacheFresh[appID] = true
|
steamCacheFresh[appID] = true
|
||||||
}
|
}
|
||||||
runCmd("docker", "volume", "create", volume)
|
runCmd("docker", "volume", "create", volume)
|
||||||
if out, err := runCmd("docker", "run", "--rm", "-v", cache+":/from:ro", "-v", volume+":/to", "alpine", "cp", "-a", "/from/.", "/to/"); err != nil {
|
if out, err := runCmd("docker", "run", "--rm", "-v", cache+":/from:ro", "-v", volume+":/to", "alpine", "sh", "-c",
|
||||||
|
"cp -a /from/. /to/ && rm -f /to/"+steamCacheStamp); err != nil {
|
||||||
fmt.Printf("[steam-cache] copying into %s failed (%v: %s); the boot downloads the app itself\n", volume, err, out)
|
fmt.Printf("[steam-cache] copying into %s failed (%v: %s); the boot downloads the app itself\n", volume, err, out)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("[steam-cache] %s seeded from the cached install of app %s\n", volume, appID)
|
fmt.Printf("[steam-cache] %s seeded from the cached install of app %s\n", volume, appID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func steamCacheUpdatedWithin(cache string, age time.Duration) bool {
|
||||||
|
out, err := runCmd("docker", "run", "--rm", "-v", cache+":/c:ro", "alpine", "cat", "/c/"+steamCacheStamp)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return stampWithin(out, time.Now(), age)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stampWithin(stamp string, now time.Time, age time.Duration) bool {
|
||||||
|
secs, err := strconv.ParseInt(strings.TrimSpace(stamp), 10, 64)
|
||||||
|
return err == nil && now.Sub(time.Unix(secs, 0)) < age
|
||||||
|
}
|
||||||
|
|
||||||
|
// runBootTest boot-tests one game the way CI does: its real payload, deployed by the node-agent, with
|
||||||
|
// the Steam download cache. Exits 3 when a secret it needs isn't set, 1 when it fails.
|
||||||
|
func runBootTest(args []string) error {
|
||||||
|
fs := flag.NewFlagSet("boot-test", flag.ExitOnError)
|
||||||
|
gamesDir := fs.String("games", "games", "directory holding one folder per game")
|
||||||
|
fs.Parse(args)
|
||||||
|
if fs.NArg() != 1 {
|
||||||
|
return errors.New("boot-test needs a game id")
|
||||||
|
}
|
||||||
|
payload, err := bootTestPayload(*gamesDir, fs.Arg(0), false)
|
||||||
|
var missing *missingSecretError
|
||||||
|
if errors.As(err, &missing) {
|
||||||
|
fmt.Fprintln(os.Stderr, "skip: "+missing.Error())
|
||||||
|
os.Exit(exitMissingSecret)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, passed, err := runAgentBootTest(payload, agentImageFromEnv())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !passed {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -153,6 +153,13 @@ func bootTestPayload(gamesDir, id string, fillBlank bool) (map[string]any, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The panel fills {PUBLIC_IP}, {PUBLIC_HOST} and {PUBLIC_PORT_n} in the startup command too
|
||||||
|
startup := g["defaultStartupCommand"]
|
||||||
|
if cmd, ok := startup.(string); ok {
|
||||||
|
cmd = strings.NewReplacer("{PUBLIC_IP}", "127.0.0.1", "{PUBLIC_HOST}", "127.0.0.1").Replace(cmd)
|
||||||
|
startup = publicPort.ReplaceAllString(cmd, "$1")
|
||||||
|
}
|
||||||
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"command": "DEPLOY_SERVER",
|
"command": "DEPLOY_SERVER",
|
||||||
"serverId": "boottest-" + id,
|
"serverId": "boottest-" + id,
|
||||||
@@ -166,7 +173,7 @@ func bootTestPayload(gamesDir, id string, fillBlank bool) (map[string]any, error
|
|||||||
"configFilePath": g["configFilePath"],
|
"configFilePath": g["configFilePath"],
|
||||||
"configFileFormat": g["configFileFormat"],
|
"configFileFormat": g["configFileFormat"],
|
||||||
"configFormatOptions": g["configFormatOptions"],
|
"configFormatOptions": g["configFormatOptions"],
|
||||||
"startupCommand": g["defaultStartupCommand"],
|
"startupCommand": startup,
|
||||||
"memoryLimitMb": g["requiredMemoryMb"],
|
"memoryLimitMb": g["requiredMemoryMb"],
|
||||||
"containerUser": g["containerUser"],
|
"containerUser": g["containerUser"],
|
||||||
"dockerNetwork": bootTestNetwork,
|
"dockerNetwork": bootTestNetwork,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
@@ -121,3 +122,28 @@ func TestBootTestPayloadLeavesOutUnboundPorts(t *testing.T) {
|
|||||||
t.Errorf("expectPorts %v", ports)
|
t.Errorf("expectPorts %v", ports)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSteamCacheStampAge(t *testing.T) {
|
||||||
|
now := time.Unix(1_800_000_000, 0)
|
||||||
|
if !stampWithin("1799990000\n", now, 24*time.Hour) {
|
||||||
|
t.Error("updated 10000s ago should be fresh")
|
||||||
|
}
|
||||||
|
if stampWithin("1700000000", now, 24*time.Hour) || stampWithin("", now, 24*time.Hour) || stampWithin("x", now, 24*time.Hour) {
|
||||||
|
t.Error("old, missing or garbled stamps are not fresh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootTestPayloadFillsPublicPlaceholdersInStartupCommand(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
game := filepath.Join(dir, "example")
|
||||||
|
os.MkdirAll(game, 0o755)
|
||||||
|
os.WriteFile(filepath.Join(game, "game.yaml"), []byte("id: example\nname: Example\ndefaultDockerImage: x\n"+
|
||||||
|
"defaultStartupCommand: ./server -Port={PUBLIC_PORT_7777} -ip={PUBLIC_IP}\ndefaultPorts:\n - 7777/udp\n"), 0o644)
|
||||||
|
p, err := bootTestPayload(dir, "example", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p["startupCommand"] != "./server -Port=7777 -ip=127.0.0.1" {
|
||||||
|
t.Errorf("startupCommand %v", p["startupCommand"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ var gameFields = []string{
|
|||||||
"defaultConfig", "configSchema", "configFilePath", "configFileFormat", "configFormatOptions",
|
"defaultConfig", "configSchema", "configFilePath", "configFileFormat", "configFormatOptions",
|
||||||
"agnosticCommandConfig", "clusterConfig",
|
"agnosticCommandConfig", "clusterConfig",
|
||||||
"logFilePath", "readyPattern", "failPatterns", "backupPaths", "maxBackupSizeMb",
|
"logFilePath", "readyPattern", "failPatterns", "backupPaths", "maxBackupSizeMb",
|
||||||
"publicIpConfigKey", "maxPlayersConfigKey", "mapConfigKey",
|
"publicIpConfigKey", "portMode", "maxPlayersConfigKey", "mapConfigKey",
|
||||||
"aiCoverPrompt",
|
"aiCoverPrompt",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
// go run ./cmd/gbrgames import export.json # write games/<id>/ from GET /api/v1/admin/games
|
// go run ./cmd/gbrgames import export.json # write games/<id>/ from GET /api/v1/admin/games
|
||||||
// go run ./cmd/gbrgames export -out p.json # build the games_published payload CI sends to the panel
|
// go run ./cmd/gbrgames export -out p.json # build the games_published payload CI sends to the panel
|
||||||
// go run ./cmd/gbrgames boottest-payload ID # build the deploy message the node-agent's BootTest runs
|
// go run ./cmd/gbrgames boottest-payload ID # build the deploy message the node-agent's BootTest runs
|
||||||
|
// go run ./cmd/gbrgames boot-test ID # boot-test a game with the node-agent, as CI does (Steam cache)
|
||||||
// go run ./cmd/gbrgames inspect-image -game ID # read the image's facts and check the template against them
|
// go run ./cmd/gbrgames inspect-image -game ID # read the image's facts and check the template against them
|
||||||
// go run ./cmd/gbrgames first-boot -game ID # start the server once, untouched; record its log and files
|
// go run ./cmd/gbrgames first-boot -game ID # start the server once, untouched; record its log and files
|
||||||
// go run ./cmd/gbrgames draft -name N -image I # have an AI model write a new game's template from those facts
|
// go run ./cmd/gbrgames draft -name N -image I # have an AI model write a new game's template from those facts
|
||||||
@@ -33,6 +34,8 @@ func main() {
|
|||||||
err = runExport(os.Args[2:])
|
err = runExport(os.Args[2:])
|
||||||
case "boottest-payload":
|
case "boottest-payload":
|
||||||
err = runBootTestPayload(os.Args[2:])
|
err = runBootTestPayload(os.Args[2:])
|
||||||
|
case "boot-test":
|
||||||
|
err = runBootTest(os.Args[2:])
|
||||||
case "inspect-image":
|
case "inspect-image":
|
||||||
err = runInspectImage(os.Args[2:])
|
err = runInspectImage(os.Args[2:])
|
||||||
case "first-boot":
|
case "first-boot":
|
||||||
@@ -59,6 +62,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gbrgames validate [-games DIR] | import [-games DIR] [-all] EXPORT.json | export [-repo R] [-commit SHA] [-out FILE] | boottest-payload [-out FILE] ID | inspect-image [-game ID] [-json] [-no-pull] [IMAGE] | first-boot [-game ID | -image IMAGE] [-env K=V] [-max D] [-quiet D] [-out DIR] | draft -name NAME -image IMAGE [-steam-app ID] [-first-boot REPORT] [-provider ollama|gemini] [-model M] | research -name NAME [-notes N] [-out FILE] | build -name NAME -variant JSON|@FILE [-out FILE] | repair -id ID [-reason R] | eval [-ids all] [-out eval.md] | worker [-once]")
|
fmt.Fprintln(os.Stderr, "usage: gbrgames validate [-games DIR] | import [-games DIR] [-all] EXPORT.json | export [-repo R] [-commit SHA] [-out FILE] | boottest-payload [-out FILE] ID | boot-test ID | inspect-image [-game ID] [-json] [-no-pull] [IMAGE] | first-boot [-game ID | -image IMAGE] [-env K=V] [-max D] [-quiet D] [-out DIR] | draft -name NAME -image IMAGE [-steam-app ID] [-first-boot REPORT] [-provider ollama|gemini] [-model M] | research -name NAME [-notes N] [-out FILE] | build -name NAME -variant JSON|@FILE [-out FILE] | repair -id ID [-reason R] | eval [-ids all] [-out eval.md] | worker [-once]")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
@@ -174,9 +174,10 @@ const researchPrompt = "You research how to host a game's dedicated server for G
|
|||||||
"the ways this game's dedicated server can be run (variants): separate editions of the game with their own server, " +
|
"the ways this game's dedicated server can be run (variants): separate editions of the game with their own server, " +
|
||||||
"crossplay builds, or notably different server images. For each variant give:\n" +
|
"crossplay builds, or notably different server images. For each variant give:\n" +
|
||||||
"- images: how to run this server, best first. When the game has a dedicated server on Steam that runs on Linux, " +
|
"- images: how to run this server, best first. When the game has a dedicated server on Steam that runs on Linux, " +
|
||||||
"cm2network/steamcmd (the plain SteamCMD route, installing the server into /server) always comes first and that " +
|
"cm2network/steamcmd (the plain SteamCMD route, installing the server into /server) is its only image, and " +
|
||||||
"variant is the recommended one. Then Docker Hub images that appear in the pages and run as a non-root user " +
|
"there is no separate variant for a community image of the same server: those keep their saves outside /server. " +
|
||||||
"(needed for servers that only exist for Windows).\n" +
|
"Docker Hub images that appear in the pages and run as a non-root user are only for servers that aren't on Steam " +
|
||||||
|
"for Linux (Windows-only servers that need Proton or Wine, games not on Steam).\n" +
|
||||||
"- steamAppId: the Steam app id of the DEDICATED SERVER (not the game itself), if installed via SteamCMD; else \"\".\n" +
|
"- steamAppId: the Steam app id of the DEDICATED SERVER (not the game itself), if installed via SteamCMD; else \"\".\n" +
|
||||||
"- startupHint: the command that starts the server from its install folder /server, with the launch options " +
|
"- startupHint: the command that starts the server from its install folder /server, with the launch options " +
|
||||||
"for name, port, world and save location exactly as the pages document them; \"\" if unknown.\n" +
|
"for name, port, world and save location exactly as the pages document them; \"\" if unknown.\n" +
|
||||||
@@ -311,7 +312,7 @@ func verifyVariants(res *researchResult) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
apps := steamApps(appIDs)
|
apps := steamApps(appIDs)
|
||||||
anyRunnable := false
|
anyRunnable, linuxSteam := false, false
|
||||||
for i := range res.Variants {
|
for i := range res.Variants {
|
||||||
v := &res.Variants[i]
|
v := &res.Variants[i]
|
||||||
var images []string
|
var images []string
|
||||||
@@ -333,7 +334,8 @@ func verifyVariants(res *researchResult) {
|
|||||||
v.Verified = append(v.Verified, fmt.Sprintf("Steam app %s is %q", v.SteamAppID, app.name))
|
v.Verified = append(v.Verified, fmt.Sprintf("Steam app %s is %q", v.SteamAppID, app.name))
|
||||||
v.Images = withSteamCMD(v.Images, app.linux)
|
v.Images = withSteamCMD(v.Images, app.linux)
|
||||||
if app.linux {
|
if app.linux {
|
||||||
v.Verified = append(v.Verified, "it ships a Linux server, so the SteamCMD option (installing it into /server) is recommended")
|
linuxSteam = true
|
||||||
|
v.Verified = append(v.Verified, "it ships a Linux server, so it is installed with SteamCMD into /server")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
v.Problems = append(v.Problems, "Steam app "+v.SteamAppID+" not found for anonymous SteamCMD")
|
v.Problems = append(v.Problems, "Steam app "+v.SteamAppID+" not found for anonymous SteamCMD")
|
||||||
@@ -352,10 +354,26 @@ func verifyVariants(res *researchResult) {
|
|||||||
res.Reason = "unknown"
|
res.Reason = "unknown"
|
||||||
res.Message = "No variant has a Docker image or Steam app that could be confirmed. " + res.Message
|
res.Message = "No variant has a Docker image or Steam app that could be confirmed. " + res.Message
|
||||||
}
|
}
|
||||||
|
if linuxSteam {
|
||||||
|
res.Variants = steamCMDOnly(res.Variants)
|
||||||
|
}
|
||||||
res.Variants = oneOptionPerImage(res.Variants)
|
res.Variants = oneOptionPerImage(res.Variants)
|
||||||
recommend(res.Variants)
|
recommend(res.Variants)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// steamCMDOnly keeps only the SteamCMD options once the game has a Linux server on Steam. Ready-made images
|
||||||
|
// of such servers keep their files and saves in their own folders (/config, /home/...), outside /server,
|
||||||
|
// and read settings from their own variables, so they can't be made or repaired into a working template.
|
||||||
|
func steamCMDOnly(variants []variant) []variant {
|
||||||
|
var out []variant
|
||||||
|
for _, v := range variants {
|
||||||
|
if len(v.Images) > 0 && isSteamCMDImage(v.Images[0]) && v.SteamAppID != "" {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// recommend marks exactly one variant recommended, among the runnable ones. A variant that installs a Linux
|
// recommend marks exactly one variant recommended, among the runnable ones. A variant that installs a Linux
|
||||||
// server with SteamCMD comes before the model's choice: it keeps everything under /server, as nodes need.
|
// server with SteamCMD comes before the model's choice: it keeps everything under /server, as nodes need.
|
||||||
func recommend(variants []variant) {
|
func recommend(variants []variant) {
|
||||||
@@ -422,7 +440,7 @@ func steamApps(apps []string) map[string]steamApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// withSteamCMD adds the plain SteamCMD route to a variant's images. When the Steam app ships a Linux server
|
// withSteamCMD adds the plain SteamCMD route to a variant's images. When the Steam app ships a Linux server
|
||||||
// it goes first: it installs into /server, which nodes keep, while ready-made images often keep their
|
// it is the only route: it installs into /server, which nodes keep, while ready-made images keep their
|
||||||
// files elsewhere. For Windows-only servers it stays last, behind images that bring Proton or Wine.
|
// files elsewhere. For Windows-only servers it stays last, behind images that bring Proton or Wine.
|
||||||
func withSteamCMD(images []string, linux bool) []string {
|
func withSteamCMD(images []string, linux bool) []string {
|
||||||
var others []string
|
var others []string
|
||||||
@@ -432,7 +450,7 @@ func withSteamCMD(images []string, linux bool) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if linux {
|
if linux {
|
||||||
return append([]string{"cm2network/steamcmd"}, others...)
|
return []string{"cm2network/steamcmd"}
|
||||||
}
|
}
|
||||||
return append(others, "cm2network/steamcmd")
|
return append(others, "cm2network/steamcmd")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ func TestParseAppInfoFindsLinuxServers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWithSteamCMDGoesFirstOnlyForLinuxServers(t *testing.T) {
|
func TestWithSteamCMDIsTheOnlyRouteForLinuxServers(t *testing.T) {
|
||||||
images := []string{"someone/example-server", "cm2network/steamcmd", "other/example"}
|
images := []string{"someone/example-server", "cm2network/steamcmd", "other/example"}
|
||||||
got := withSteamCMD(images, true)
|
got := withSteamCMD(images, true)
|
||||||
if len(got) != 3 || got[0] != "cm2network/steamcmd" || got[1] != "someone/example-server" {
|
if len(got) != 1 || got[0] != "cm2network/steamcmd" {
|
||||||
t.Errorf("linux: %v", got)
|
t.Errorf("linux: %v", got)
|
||||||
}
|
}
|
||||||
got = withSteamCMD([]string{"someone/proton-example"}, false)
|
got = withSteamCMD([]string{"someone/proton-example"}, false)
|
||||||
@@ -89,10 +89,22 @@ func TestRecommendPrefersTheSteamCMDVariant(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSteamCMDOnlyDropsImageVariantsOfLinuxSteamGames(t *testing.T) {
|
||||||
|
// A Linux Steam server researched as two variants: SteamCMD, and a community image under its own title
|
||||||
|
got := steamCMDOnly([]variant{
|
||||||
|
{ID: "dedicated", SteamAppID: "1000000", Images: []string{"cm2network/steamcmd"}},
|
||||||
|
{ID: "vanilla-stable", Images: []string{"someone/example-server"}},
|
||||||
|
})
|
||||||
|
if len(got) != 1 || got[0].ID != "dedicated" {
|
||||||
|
t.Errorf("got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOneOptionPerImageThenSteamCMDRecommended(t *testing.T) {
|
func TestOneOptionPerImageThenSteamCMDRecommended(t *testing.T) {
|
||||||
|
// A variant runnable two ways becomes one option per way, SteamCMD recommended
|
||||||
v := oneOptionPerImage([]variant{{
|
v := oneOptionPerImage([]variant{{
|
||||||
ID: "dedicated", Title: "Example Dedicated Server", Recommended: true, SteamAppID: "1000000",
|
ID: "dedicated", Title: "Example Dedicated Server", Recommended: true, SteamAppID: "1000000",
|
||||||
Images: withSteamCMD([]string{"someone/example-server"}, true),
|
Images: []string{"cm2network/steamcmd", "someone/example-server"},
|
||||||
Verified: []string{"image someone/example-server exists", "Steam app 1000000 is \"Example Dedicated Server\""},
|
Verified: []string{"image someone/example-server exists", "Steam app 1000000 is \"Example Dedicated Server\""},
|
||||||
}})
|
}})
|
||||||
recommend(v)
|
recommend(v)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
id: runescape-dragonwilds-native-linux
|
id: runescape-dragonwilds-native-linux
|
||||||
name: 'RuneScape: Dragonwilds (Native Linux Server)'
|
name: 'RuneScape: Dragonwilds (Native Linux Server)'
|
||||||
defaultDockerImage: cm2network/steamcmd:root
|
defaultDockerImage: cm2network/steamcmd:root
|
||||||
defaultStartupCommand: ./RSDragonwildsServer.sh -log -Port=7777
|
defaultStartupCommand: ./RSDragonwildsServer.sh -log -Port={PUBLIC_PORT_7777} -BeaconPort={PUBLIC_PORT_8888}
|
||||||
requiredMemoryMb: 10240
|
requiredMemoryMb: 10240
|
||||||
requiredDiskMb: 20480
|
requiredDiskMb: 20480
|
||||||
defaultPorts:
|
defaultPorts:
|
||||||
@@ -106,12 +106,13 @@ agnosticCommandConfig:
|
|||||||
Save:
|
Save:
|
||||||
type: UNSUPPORTED
|
type: UNSUPPORTED
|
||||||
playerJoinRegex: 'LogNet: Join succeeded: ([^\r\n]+)'
|
playerJoinRegex: 'LogNet: Join succeeded: ([^\r\n]+)'
|
||||||
playerLeaveRegex: 'LogNet: UNetConnection::.*Player(?: name)?:\s*([^,\r\n]+)'
|
playerLeaveRegex: 'LogDomMatcherSession: Player Removed from session \[[^\]]*\]-\[([^\]]+)\]'
|
||||||
queryType: LOG_ONLY
|
queryType: LOG_ONLY
|
||||||
logFilePath: RSDragonwilds/Saved/Logs/RSDragonwilds.log
|
logFilePath: RSDragonwilds/Saved/Logs/RSDragonwilds.log
|
||||||
backupPaths:
|
backupPaths:
|
||||||
- RSDragonwilds/Saved
|
- RSDragonwilds/Saved
|
||||||
maxBackupSizeMb: 5000
|
maxBackupSizeMb: 5000
|
||||||
publicIpConfigKey: MULTIHOME
|
publicIpConfigKey: MULTIHOME
|
||||||
|
portMode: MATCH_PUBLIC
|
||||||
maxPlayersConfigKey: MaxPlayers
|
maxPlayersConfigKey: MaxPlayers
|
||||||
aiCoverPrompt: 'A cinematic wide shot of Ashenfall in the fantasy world of Gielinor from RuneScape: Dragonwilds. Ancient stone spires and forgotten fortresses rise amidst untamed wilderness. Colossal ancient dragons circle through stormy twilight skies illuminated by magical embers, while adventurers gather around a runic encampment wielding enchanted sorcery and survival gear.'
|
aiCoverPrompt: 'A cinematic wide shot of Ashenfall in the fantasy world of Gielinor from RuneScape: Dragonwilds. Ancient stone spires and forgotten fortresses rise amidst untamed wilderness. Colossal ancient dragons circle through stormy twilight skies illuminated by magical embers, while adventurers gather around a runic encampment wielding enchanted sorcery and survival gear.'
|
||||||
@@ -1,165 +1,115 @@
|
|||||||
# yaml-language-server: $schema=../../schema/game.schema.json
|
# yaml-language-server: $schema=../../schema/game.schema.json
|
||||||
|
# The official dedicated server (Steam app 1690800), installed by the node with anonymous SteamCMD.
|
||||||
|
# Satisfactory keeps its saves under $HOME/.config/Epic/FactoryGame, so HOME is pointed at the server
|
||||||
|
# volume; otherwise they would land in /home/steam and be lost on the next start.
|
||||||
|
# The server reads no environment variables, so the startup command writes the settings into its
|
||||||
|
# Game.ini and Engine.ini before every start, and passes the rest as launch options.
|
||||||
id: satisfactory-vanilla-stable
|
id: satisfactory-vanilla-stable
|
||||||
name: Satisfactory (Vanilla Stable)
|
name: Satisfactory
|
||||||
defaultDockerImage: wolveix/satisfactory-server:latest
|
defaultDockerImage: cm2network/steamcmd
|
||||||
|
defaultStartupCommand: >-
|
||||||
|
export HOME=/server;
|
||||||
|
cfg=FactoryGame/Saved/Config/LinuxServer;
|
||||||
|
mkdir -p "$HOME/.steam/sdk64" "$cfg";
|
||||||
|
cp -f /home/steam/steamcmd/linux64/steamclient.so "$HOME/.steam/sdk64/" 2>/dev/null;
|
||||||
|
case "${SERVER_STREAMING:-true}" in true|True|1) ss=1;; *) ss=0;; esac;
|
||||||
|
printf '[/Script/Engine.GameSession]\nMaxPlayers=%s\n' "${MAX_PLAYERS:-4}" > "$cfg/Game.ini";
|
||||||
|
printf '[/Script/FactoryGame.FGSaveSession]\nmNumRotatingAutosaves=%s\n\n[/Script/OnlineSubsystemUtils.IpNetDriver]\nNetServerMaxTickRate=%s\nConnectionTimeout=%s\nInitialConnectTimeout=%s\n\n[/Script/Engine.GarbageCollectionSettings]\ngc.MaxObjectsInEditor=%s\ngc.MaxObjectsInGame=%s\n\n[ConsoleVariables]\nwp.Runtime.EnableServerStreaming=%s\n'
|
||||||
|
"${AUTOSAVE_NUM:-5}" "${MAX_TICK_RATE:-30}" "${CONNECTION_TIMEOUT:-30}" "${CONNECTION_TIMEOUT:-30}"
|
||||||
|
"${MAX_OBJECTS:-2162688}" "${MAX_OBJECTS:-2162688}" "$ss" > "$cfg/Engine.ini";
|
||||||
|
args=(-Port=7777 -ReliablePort=8888 -ExternalReliablePort="${EXTERNAL_RELIABLE_PORT:-8888}" -log -unattended);
|
||||||
|
case "${DISABLE_SEASONAL_EVENTS:-false}" in true|True|1) args+=(-DisableSeasonalEvents);; esac;
|
||||||
|
[ -n "${MULTIHOME:-}" ] && args+=(-multihome="$MULTIHOME");
|
||||||
|
exec ./FactoryServer.sh "${args[@]}"
|
||||||
requiredMemoryMb: 8192
|
requiredMemoryMb: 8192
|
||||||
requiredDiskMb: 16384
|
requiredDiskMb: 20480
|
||||||
defaultPorts:
|
defaultPorts:
|
||||||
- 7777/tcp
|
|
||||||
- 7777/udp
|
- 7777/udp
|
||||||
|
- 7777/tcp
|
||||||
- 8888/tcp
|
- 8888/tcp
|
||||||
installMethod: NONE
|
installMethod: STEAMCMD
|
||||||
|
installConfig:
|
||||||
|
appId: "1690800"
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
AUTOSAVENUM: 5
|
MaxPlayers: 4
|
||||||
DEBUG: false
|
AutosaveNum: 5
|
||||||
DISABLESEASONALEVENTS: false
|
MaxTickRate: 30
|
||||||
LOG: false
|
ConnectionTimeout: 30
|
||||||
MAXOBJECTS: 2162688
|
MaxObjects: 2162688
|
||||||
MAXPLAYERS: 4
|
ServerStreaming: true
|
||||||
MAXTICKRATE: 30
|
DisableSeasonalEvents: false
|
||||||
MULTIHOME: '::'
|
Multihome: ""
|
||||||
PGID: 1000
|
ExternalReliablePort: '{PUBLIC_PORT_8888}'
|
||||||
PUID: 1000
|
|
||||||
SERVERGAMEPORT: 7777
|
|
||||||
SERVERMESSAGINGPORT: 8888
|
|
||||||
SERVERSTREAMING: true
|
|
||||||
SKIPUPDATE: false
|
|
||||||
STEAMBETA: false
|
|
||||||
STEAMBETAID: ""
|
|
||||||
STEAMBETAKEY: ""
|
|
||||||
TIMEOUT: 30
|
|
||||||
VMOVERRIDE: false
|
|
||||||
configSchema:
|
configSchema:
|
||||||
$schema: http://json-schema.org/draft-07/schema#
|
$schema: http://json-schema.org/draft-07/schema#
|
||||||
|
type: object
|
||||||
properties:
|
properties:
|
||||||
AUTOSAVENUM:
|
MaxPlayers:
|
||||||
|
title: Max Players
|
||||||
|
description: How many players can be on the server at once. Set by your plan.
|
||||||
|
type: integer
|
||||||
|
category: Gameplay & Rules
|
||||||
|
default: 4
|
||||||
|
AutosaveNum:
|
||||||
|
title: Autosave Rotation Count
|
||||||
|
description: How many rotating autosave files to keep in the save folder.
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
category: World & Environment
|
category: World & Environment
|
||||||
default: 5
|
default: 5
|
||||||
description: The number of rotating autosave files to keep in the save folder.
|
MaxTickRate:
|
||||||
title: Autosave Rotation Count
|
title: Max Tick Rate
|
||||||
|
description: The highest simulation tick rate the server runs at, in Hz.
|
||||||
type: integer
|
type: integer
|
||||||
DEBUG:
|
minimum: 10
|
||||||
|
maximum: 120
|
||||||
category: System & General
|
category: System & General
|
||||||
default: false
|
default: 30
|
||||||
description: Prints diagnostic environment and system details on startup, then stops.
|
ConnectionTimeout:
|
||||||
title: Debug Mode
|
title: Connection Timeout
|
||||||
type: boolean
|
description: Seconds before a player who stops responding is disconnected.
|
||||||
DISABLESEASONALEVENTS:
|
type: integer
|
||||||
category: Gameplay & Rules
|
minimum: 5
|
||||||
default: false
|
|
||||||
description: Disables seasonal in-game events such as FICSMAS holiday events.
|
|
||||||
title: Disable Seasonal Events
|
|
||||||
type: boolean
|
|
||||||
LOG:
|
|
||||||
category: System & General
|
category: System & General
|
||||||
default: false
|
default: 30
|
||||||
description: Disables automatic pruning of previous Satisfactory server log files on container startup.
|
MaxObjects:
|
||||||
title: Retain Old Logs
|
title: Max Objects Limit
|
||||||
type: boolean
|
description: Unreal Engine object limit. Raise it for very large late-game factories.
|
||||||
MAXOBJECTS:
|
type: integer
|
||||||
category: World & Environment
|
category: World & Environment
|
||||||
default: 2162688
|
default: 2162688
|
||||||
description: Unreal Engine garbage collector UObject limit to support large late-game factories.
|
ServerStreaming:
|
||||||
title: Max Objects Limit
|
title: Server Streaming
|
||||||
type: integer
|
description: Lets the server stream world data in and out instead of keeping it all loaded.
|
||||||
MAXPLAYERS:
|
type: boolean
|
||||||
category: System & General
|
|
||||||
default: 4
|
|
||||||
description: Sets the maximum number of concurrent players allowed on the server.
|
|
||||||
title: Max Players
|
|
||||||
type: integer
|
|
||||||
MAXTICKRATE:
|
|
||||||
category: System & General
|
|
||||||
default: 30
|
|
||||||
description: Sets the maximum simulation tick rate for the server in Hz.
|
|
||||||
title: Max Tick Rate
|
|
||||||
type: integer
|
|
||||||
MULTIHOME:
|
|
||||||
category: System & General
|
|
||||||
default: '::'
|
|
||||||
description: 'Network interface binding IP address (defaults to :: for dual-stack IPv4/IPv6).'
|
|
||||||
title: Multihome Address
|
|
||||||
type: string
|
|
||||||
PGID:
|
|
||||||
category: System & General
|
|
||||||
default: 1000
|
|
||||||
description: Sets the Linux group ID under which the dedicated server process runs.
|
|
||||||
title: Process Group ID
|
|
||||||
type: integer
|
|
||||||
PUID:
|
|
||||||
category: System & General
|
|
||||||
default: 1000
|
|
||||||
description: Sets the Linux user ID under which the dedicated server process runs.
|
|
||||||
title: Process User ID
|
|
||||||
type: integer
|
|
||||||
SERVERGAMEPORT:
|
|
||||||
category: System & General
|
|
||||||
default: 7777
|
|
||||||
description: Primary game port for UDP and TCP network communication.
|
|
||||||
title: Server Game Port
|
|
||||||
type: integer
|
|
||||||
SERVERMESSAGINGPORT:
|
|
||||||
category: System & General
|
|
||||||
default: 8888
|
|
||||||
description: Port used for reliable server messaging and beacon network traffic.
|
|
||||||
title: Server Messaging Port
|
|
||||||
type: integer
|
|
||||||
SERVERSTREAMING:
|
|
||||||
category: System & General
|
category: System & General
|
||||||
default: true
|
default: true
|
||||||
description: Toggles whether the game server utilizes Unreal Engine World Partition asset streaming.
|
DisableSeasonalEvents:
|
||||||
title: Server Streaming
|
title: Disable Seasonal Events
|
||||||
|
description: Turns off seasonal events such as FICSMAS.
|
||||||
type: boolean
|
type: boolean
|
||||||
SKIPUPDATE:
|
category: Gameplay & Rules
|
||||||
category: System & General
|
|
||||||
default: false
|
default: false
|
||||||
description: Skips automatic SteamCMD game file update checks on container startup.
|
Multihome:
|
||||||
title: Skip Update
|
title: Multihome Address
|
||||||
type: boolean
|
description: Bind the server to one IP address instead of all of them. Leave empty unless you know you need it.
|
||||||
STEAMBETA:
|
type: string
|
||||||
category: System & General
|
|
||||||
default: false
|
|
||||||
description: When enabled, updates and runs the Experimental branch instead of the stable release.
|
|
||||||
title: Steam Beta Branch
|
|
||||||
type: boolean
|
|
||||||
STEAMBETAID:
|
|
||||||
category: System & General
|
category: System & General
|
||||||
default: ""
|
default: ""
|
||||||
description: Specific beta branch identifier to download via SteamCMD.
|
ExternalReliablePort:
|
||||||
title: Steam Beta Branch Name
|
title: Reliable Messaging Port (public)
|
||||||
|
description: The port players' games use for reliable messaging. Filled in for you; only change it if you know you need to.
|
||||||
type: string
|
type: string
|
||||||
STEAMBETAKEY:
|
|
||||||
category: Security & Access
|
|
||||||
default: ""
|
|
||||||
description: Password required to access a private Steam beta branch.
|
|
||||||
title: Steam Beta Password
|
|
||||||
type: string
|
|
||||||
TIMEOUT:
|
|
||||||
category: System & General
|
category: System & General
|
||||||
default: 30
|
default: '{PUBLIC_PORT_8888}'
|
||||||
description: Client network connection timeout threshold in seconds.
|
|
||||||
title: Connection Timeout
|
|
||||||
type: integer
|
|
||||||
VMOVERRIDE:
|
|
||||||
category: System & General
|
|
||||||
default: false
|
|
||||||
description: Skips CPU model checking on hypervisors to prevent crash warnings on KVM/QEMU hosts.
|
|
||||||
title: VM CPU Check Override
|
|
||||||
type: boolean
|
|
||||||
required: []
|
required: []
|
||||||
type: object
|
maxPlayersConfigKey: MaxPlayers
|
||||||
configFileFormat: INI
|
logFilePath: FactoryGame/Saved/Logs/FactoryGame.log
|
||||||
configFormatOptions:
|
readyPattern: 'Engine is initialized'
|
||||||
assignment: =
|
|
||||||
falseValue: "false"
|
|
||||||
footer: ""
|
|
||||||
header: ""
|
|
||||||
quoteStrings: false
|
|
||||||
separator: "\n"
|
|
||||||
trueValue: "true"
|
|
||||||
unquotedKeys: []
|
|
||||||
uppercaseKeys: false
|
|
||||||
agnosticCommandConfig:
|
agnosticCommandConfig:
|
||||||
|
queryType: LOG_ONLY
|
||||||
|
playerJoinRegex: 'LogNet: Join succeeded: ([^\r\n]+)'
|
||||||
|
playerLeaveRegex: 'UNetConnection::Close:.*?\[PlayerName:\s*([^\]]+)\]'
|
||||||
actions:
|
actions:
|
||||||
Ban:
|
Ban:
|
||||||
type: UNSUPPORTED
|
type: UNSUPPORTED
|
||||||
@@ -169,22 +119,7 @@ agnosticCommandConfig:
|
|||||||
type: UNSUPPORTED
|
type: UNSUPPORTED
|
||||||
Save:
|
Save:
|
||||||
type: UNSUPPORTED
|
type: UNSUPPORTED
|
||||||
pingPort: null
|
|
||||||
pingPortKey: null
|
|
||||||
playerJoinRegex: '.*Join succeeded: (\S+).*'
|
|
||||||
playerLeaveRegex: .*UNetConnection::Close:.*?\[PlayerName:\s*([^\]]+)\].*
|
|
||||||
playersCommand: null
|
|
||||||
playersFormat: null
|
|
||||||
playersRegex: null
|
|
||||||
queryPortKey: null
|
|
||||||
queryType: LOG_ONLY
|
|
||||||
rconPasswordKey: null
|
|
||||||
rconPortKey: null
|
|
||||||
restEndpoint: null
|
|
||||||
restPasswordKey: null
|
|
||||||
restPortKey: null
|
|
||||||
restUser: null
|
|
||||||
backupPaths:
|
backupPaths:
|
||||||
- saved
|
- .config/Epic/FactoryGame/Saved/SaveGames
|
||||||
maxBackupSizeMb: 1024
|
- FactoryGame/Saved/Config
|
||||||
maxPlayersConfigKey: MAXPLAYERS
|
maxBackupSizeMb: 2048
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
# yaml-language-server: $schema=../../schema/test.schema.json
|
# yaml-language-server: $schema=../../schema/test.schema.json
|
||||||
# Values CI uses to boot-test this game. Never put real credentials here: map them under 'secrets'.
|
# Values CI uses to boot-test this game. Never put real credentials here: map them under 'secrets'.
|
||||||
timeoutSec: 900
|
# SteamCMD install of ~8 GB plus first start
|
||||||
|
timeoutSec: 2400
|
||||||
|
readyExample: "LogInit: Display: Engine is initialized. Leaving FEngineLoop::Init()"
|
||||||
@@ -69,6 +69,10 @@
|
|||||||
"maxBackupSizeMb": { "type": ["integer", "null"], "minimum": 1 },
|
"maxBackupSizeMb": { "type": ["integer", "null"], "minimum": 1 },
|
||||||
|
|
||||||
"publicIpConfigKey": { "description": "Config key that receives the server's public address.", "type": ["string", "null"] },
|
"publicIpConfigKey": { "description": "Config key that receives the server's public address.", "type": ["string", "null"] },
|
||||||
|
"portMode": {
|
||||||
|
"description": "MATCH_PUBLIC: for games that advertise the address their own traffic comes from. The container binds the port players use (the relay port behind Public Bypass) and, behind a relay, all its outgoing traffic leaves through the relay. Tell the game its port with {PUBLIC_PORT_<defaultPort>} in defaultStartupCommand.",
|
||||||
|
"enum": ["DEFAULT", "MATCH_PUBLIC", null]
|
||||||
|
},
|
||||||
"maxPlayersConfigKey": { "description": "Config key locked to the pricing tier's player count.", "type": ["string", "null"] },
|
"maxPlayersConfigKey": { "description": "Config key locked to the pricing tier's player count.", "type": ["string", "null"] },
|
||||||
"mapConfigKey": { "type": ["string", "null"] },
|
"mapConfigKey": { "type": ["string", "null"] },
|
||||||
|
|
||||||
|
|||||||
+7
-17
@@ -30,24 +30,14 @@ for id in "$@"; do
|
|||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
payload=$("$gbrgames" boottest-payload "$id")
|
# The node-agent's BootTest with the game's real payload. Steam games start from the runner's download
|
||||||
|
# cache (volumes gbr-steamcache-<app>, kept by RUNNER.md's prune), so Steam isn't asked for the whole
|
||||||
|
# game on every run
|
||||||
|
AGENT_IMAGE="$AGENT_IMAGE" "$gbrgames" boot-test "$id"
|
||||||
code=$?
|
code=$?
|
||||||
if [ $code -eq 3 ]; then
|
if [ $code -eq 3 ]; then skipped+=("$id")
|
||||||
skipped+=("$id")
|
elif [ $code -eq 0 ]; then passed+=("$id")
|
||||||
echo "::endgroup::"
|
else failed+=("$id"); fi
|
||||||
continue
|
|
||||||
elif [ $code -ne 0 ]; then
|
|
||||||
failed+=("$id")
|
|
||||||
echo "::endgroup::"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker run --rm -i \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
--entrypoint java "$AGENT_IMAGE" \
|
|
||||||
-Dloader.main=com.gbreed.gbr.nodeagent.BootTest -cp app.jar \
|
|
||||||
org.springframework.boot.loader.launch.PropertiesLauncher <<< "$payload"
|
|
||||||
if [ $? -eq 0 ]; then passed+=("$id"); else failed+=("$id"); fi
|
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user