From b9d445c57dbb04efa1000de34738587bdb4595c9 Mon Sep 17 00:00:00 2001 From: git_pipeline Date: Sat, 7 Nov 2026 01:45:00 +0000 Subject: [PATCH 1/4] fix(satisfactory): run the official SteamCMD server instead of wolveix/satisfactory-server The wolveix image keeps its game files and saves in /config and /home/steam, outside the server volume, so they were lost on every start and the weekly boot test failed. Now: cm2network/steamcmd with Steam app 1690800, HOME=/server, ports 7777/udp+tcp and 8888/tcp, MaxPlayers in Game.ini, -ExternalReliablePort for the public reliable port. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MZxvxViV6QwAtVWT3saGvs --- games/satisfactory-vanilla-stable/game.yaml | 214 +++++--------------- 1 file changed, 48 insertions(+), 166 deletions(-) diff --git a/games/satisfactory-vanilla-stable/game.yaml b/games/satisfactory-vanilla-stable/game.yaml index ebbd6a0..82a70ce 100644 --- a/games/satisfactory-vanilla-stable/game.yaml +++ b/games/satisfactory-vanilla-stable/game.yaml @@ -1,165 +1,62 @@ # 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. id: satisfactory-vanilla-stable -name: Satisfactory (Vanilla Stable) -defaultDockerImage: wolveix/satisfactory-server:latest +name: Satisfactory +defaultDockerImage: cm2network/steamcmd +defaultStartupCommand: >- + export HOME=/server; + mkdir -p "$HOME/.steam/sdk64"; + cp -f /home/steam/steamcmd/linux64/steamclient.so "$HOME/.steam/sdk64/" 2>/dev/null; + exec ./FactoryServer.sh -Port=7777 -ReliablePort=8888 -ExternalReliablePort="${EXTERNAL_RELIABLE_PORT:-8888}" -log -unattended requiredMemoryMb: 8192 -requiredDiskMb: 16384 +requiredDiskMb: 20480 defaultPorts: - - 7777/tcp - 7777/udp + - 7777/tcp - 8888/tcp -installMethod: NONE +installMethod: STEAMCMD +installConfig: + appId: "1690800" defaultConfig: - AUTOSAVENUM: 5 - DEBUG: false - DISABLESEASONALEVENTS: false - LOG: false - MAXOBJECTS: 2162688 - MAXPLAYERS: 4 - MAXTICKRATE: 30 - MULTIHOME: '::' - PGID: 1000 - PUID: 1000 - SERVERGAMEPORT: 7777 - SERVERMESSAGINGPORT: 8888 - SERVERSTREAMING: true - SKIPUPDATE: false - STEAMBETA: false - STEAMBETAID: "" - STEAMBETAKEY: "" - TIMEOUT: 30 - VMOVERRIDE: false + MaxPlayers: 4 + ExternalReliablePort: '{PUBLIC_PORT_8888}' configSchema: $schema: http://json-schema.org/draft-07/schema# - properties: - AUTOSAVENUM: - category: World & Environment - default: 5 - description: The number of rotating autosave files to keep in the save folder. - title: Autosave Rotation Count - type: integer - DEBUG: - category: System & General - default: false - description: Prints diagnostic environment and system details on startup, then stops. - title: Debug Mode - type: boolean - DISABLESEASONALEVENTS: - category: Gameplay & Rules - default: false - description: Disables seasonal in-game events such as FICSMAS holiday events. - title: Disable Seasonal Events - type: boolean - LOG: - category: System & General - default: false - description: Disables automatic pruning of previous Satisfactory server log files on container startup. - title: Retain Old Logs - type: boolean - MAXOBJECTS: - category: World & Environment - default: 2162688 - description: Unreal Engine garbage collector UObject limit to support large late-game factories. - title: Max Objects Limit - type: integer - MAXPLAYERS: - 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 - default: true - description: Toggles whether the game server utilizes Unreal Engine World Partition asset streaming. - title: Server Streaming - type: boolean - SKIPUPDATE: - category: System & General - default: false - description: Skips automatic SteamCMD game file update checks on container startup. - title: Skip Update - type: boolean - STEAMBETA: - 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 - default: "" - description: Specific beta branch identifier to download via SteamCMD. - title: Steam Beta Branch Name - 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 - default: 30 - 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: [] type: object -configFileFormat: INI + properties: + 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 + ExternalReliablePort: + 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 + category: System & General + default: '{PUBLIC_PORT_8888}' + required: [] +configFilePath: FactoryGame/Saved/Config/LinuxServer/Game.ini +configFileFormat: STANDARD configFormatOptions: - assignment: = - falseValue: "false" - footer: "" - header: "" - quoteStrings: false + header: | + [/Script/Engine.GameSession] + footer: "\n" separator: "\n" - trueValue: "true" - unquotedKeys: [] + assignment: '=' + quoteStrings: false + trueValue: "True" + falseValue: "False" uppercaseKeys: false +maxPlayersConfigKey: MaxPlayers +logFilePath: FactoryGame/Saved/Logs/FactoryGame.log agnosticCommandConfig: + queryType: LOG_ONLY + playerJoinRegex: 'LogNet: Join succeeded: ([^\r\n]+)' + playerLeaveRegex: 'UNetConnection::Close:.*?\[PlayerName:\s*([^\]]+)\]' actions: Ban: type: UNSUPPORTED @@ -169,22 +66,7 @@ agnosticCommandConfig: type: UNSUPPORTED Save: 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: - - saved -maxBackupSizeMb: 1024 -maxPlayersConfigKey: MAXPLAYERS + - .config/Epic/FactoryGame/Saved/SaveGames + - FactoryGame/Saved/Config +maxBackupSizeMb: 2048 From 5c192127f4875a51aae301d6788a204fc96ba34f Mon Sep 17 00:00:00 2001 From: git_pipeline Date: Sat, 7 Nov 2026 01:45:59 +0000 Subject: [PATCH 2/4] test(satisfactory): allow 40 minutes for the SteamCMD install and first start Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MZxvxViV6QwAtVWT3saGvs --- games/satisfactory-vanilla-stable/test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/games/satisfactory-vanilla-stable/test.yaml b/games/satisfactory-vanilla-stable/test.yaml index 1b49f91..31b8db2 100644 --- a/games/satisfactory-vanilla-stable/test.yaml +++ b/games/satisfactory-vanilla-stable/test.yaml @@ -1,3 +1,4 @@ # 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'. -timeoutSec: 900 +# SteamCMD install of ~8 GB plus first start +timeoutSec: 2400 From a918704ec6448786a94c12d04a0965afad2cceb6 Mon Sep 17 00:00:00 2001 From: git_pipeline Date: Sat, 7 Nov 2026 01:54:26 +0000 Subject: [PATCH 3/4] fix(satisfactory): restore server settings, written to Game.ini/Engine.ini each start The official server reads no env vars, so the startup command writes Max Players, autosave count, tick rate, timeout, object limit and server streaming into its ini files, and passes seasonal events and multihome as launch options. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MZxvxViV6QwAtVWT3saGvs --- games/satisfactory-vanilla-stable/game.yaml | 80 +++++++++++++++++---- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/games/satisfactory-vanilla-stable/game.yaml b/games/satisfactory-vanilla-stable/game.yaml index 82a70ce..fe2f801 100644 --- a/games/satisfactory-vanilla-stable/game.yaml +++ b/games/satisfactory-vanilla-stable/game.yaml @@ -2,14 +2,25 @@ # 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 name: Satisfactory defaultDockerImage: cm2network/steamcmd defaultStartupCommand: >- export HOME=/server; - mkdir -p "$HOME/.steam/sdk64"; + 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; - exec ./FactoryServer.sh -Port=7777 -ReliablePort=8888 -ExternalReliablePort="${EXTERNAL_RELIABLE_PORT:-8888}" -log -unattended + 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 requiredDiskMb: 20480 defaultPorts: @@ -21,6 +32,13 @@ installConfig: appId: "1690800" defaultConfig: MaxPlayers: 4 + AutosaveNum: 5 + MaxTickRate: 30 + ConnectionTimeout: 30 + MaxObjects: 2162688 + ServerStreaming: true + DisableSeasonalEvents: false + Multihome: "" ExternalReliablePort: '{PUBLIC_PORT_8888}' configSchema: $schema: http://json-schema.org/draft-07/schema# @@ -32,6 +50,52 @@ configSchema: 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 + default: 5 + MaxTickRate: + title: Max Tick Rate + description: The highest simulation tick rate the server runs at, in Hz. + type: integer + minimum: 10 + maximum: 120 + category: System & General + default: 30 + ConnectionTimeout: + title: Connection Timeout + description: Seconds before a player who stops responding is disconnected. + type: integer + minimum: 5 + category: System & General + default: 30 + MaxObjects: + title: Max Objects Limit + description: Unreal Engine object limit. Raise it for very large late-game factories. + type: integer + category: World & Environment + default: 2162688 + ServerStreaming: + title: Server Streaming + description: Lets the server stream world data in and out instead of keeping it all loaded. + type: boolean + category: System & General + default: true + DisableSeasonalEvents: + title: Disable Seasonal Events + description: Turns off seasonal events such as FICSMAS. + type: boolean + category: Gameplay & Rules + default: false + Multihome: + title: Multihome Address + description: Bind the server to one IP address instead of all of them. Leave empty unless you know you need it. + type: string + category: System & General + default: "" ExternalReliablePort: 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. @@ -39,18 +103,6 @@ configSchema: category: System & General default: '{PUBLIC_PORT_8888}' required: [] -configFilePath: FactoryGame/Saved/Config/LinuxServer/Game.ini -configFileFormat: STANDARD -configFormatOptions: - header: | - [/Script/Engine.GameSession] - footer: "\n" - separator: "\n" - assignment: '=' - quoteStrings: false - trueValue: "True" - falseValue: "False" - uppercaseKeys: false maxPlayersConfigKey: MaxPlayers logFilePath: FactoryGame/Saved/Logs/FactoryGame.log agnosticCommandConfig: From 1902c623660169326294fa0fbba8e41c904cfcad Mon Sep 17 00:00:00 2001 From: Greg Reed Date: Sat, 7 Nov 2026 01:58:27 +0000 Subject: [PATCH 4/4] fix(builder): SteamCMD is the only option for Linux Steam servers; settings must reach the server --- AUTHORING.md | 30 ++++++++++++++++++++++++++++++ cmd/gbrgames/research.go | 32 +++++++++++++++++++++++++------- cmd/gbrgames/research_test.go | 18 +++++++++++++++--- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/AUTHORING.md b/AUTHORING.md index b35efc2..e819723 100644 --- a/AUTHORING.md +++ b/AUTHORING.md @@ -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 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/`), 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//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//Saved/SaveGames`). + ### Ports - `defaultPorts` lists **only what players connect to** (game and query ports). RCON, REST and admin ports are **never** published. - 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_}` 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). 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_}`. @@ -77,6 +93,20 @@ Know this before writing anything: - Take keys and default values from what the server really writes: run `gbrgames first-boot -game ` and read the settings in its report. - 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 + `/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 `defaultConfig` value, and a `category`: one of `Security & Access`, `Gameplay & Rules`, `World & Environment`, `System & General`. Mark the few that matter when deploying with diff --git a/cmd/gbrgames/research.go b/cmd/gbrgames/research.go index 19b9821..70f981c 100644 --- a/cmd/gbrgames/research.go +++ b/cmd/gbrgames/research.go @@ -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, " + "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, " + - "cm2network/steamcmd (the plain SteamCMD route, installing the server into /server) always comes first and that " + - "variant is the recommended one. Then Docker Hub images that appear in the pages and run as a non-root user " + - "(needed for servers that only exist for Windows).\n" + + "cm2network/steamcmd (the plain SteamCMD route, installing the server into /server) is its only image, and " + + "there is no separate variant for a community image of the same server: those keep their saves outside /server. " + + "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" + "- 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" + @@ -311,7 +312,7 @@ func verifyVariants(res *researchResult) { } } apps := steamApps(appIDs) - anyRunnable := false + anyRunnable, linuxSteam := false, false for i := range res.Variants { v := &res.Variants[i] 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.Images = withSteamCMD(v.Images, 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 { 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.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) 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 // server with SteamCMD comes before the model's choice: it keeps everything under /server, as nodes need. 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 -// 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. func withSteamCMD(images []string, linux bool) []string { var others []string @@ -432,7 +450,7 @@ func withSteamCMD(images []string, linux bool) []string { } } if linux { - return append([]string{"cm2network/steamcmd"}, others...) + return []string{"cm2network/steamcmd"} } return append(others, "cm2network/steamcmd") } diff --git a/cmd/gbrgames/research_test.go b/cmd/gbrgames/research_test.go index c76d54e..235e0fa 100644 --- a/cmd/gbrgames/research_test.go +++ b/cmd/gbrgames/research_test.go @@ -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"} 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) } 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) { + // A variant runnable two ways becomes one option per way, SteamCMD recommended v := oneOptionPerImage([]variant{{ 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\""}, }}) recommend(v)