feat(builder): SteamCMD first for Linux servers, and fall back to the next image
A draft built on a ready-made image whose files all live outside /server (nodes keep only /server and /data) went round three times and ended as WIP, although research had also verified the game's Steam app, which SteamCMD installs straight into /server. The builder never tried it. - research reads each Steam app's OS list. When it ships a Linux server, cm2network/steamcmd goes first in the variant's images and that variant is recommended. Windows-only servers keep images with Proton or Wine first. The research prompt says the same. - build tries the variant's images in order: when no draft passes with one, the next image gets a fresh start. The first draft that boots wins; if none does, the PR gets the draft that got furthest, and its summary lists each image tried and how it went. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
274ed68cc5
commit
9f6e26bc0c
3 files changed
+196
-61
No files matched your search
+68
-38
@@ -173,9 +173,10 @@ const researchPrompt = "You research how to host a game's dedicated server for G
|
||||
"in Docker on Linux, as a non-root user, with a volume at /server. From the search results and pages given, list " +
|
||||
"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: Docker Hub images that run this server, best first (prefer images that run as a non-root user, and the " +
|
||||
"plain SteamCMD route cm2network/steamcmd when the game ships a Linux dedicated server on Steam). Only images that " +
|
||||
"appear in the pages.\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" +
|
||||
"- 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" +
|
||||
@@ -299,13 +300,13 @@ func trimToJSON(answer string) string {
|
||||
// Steam apps must exist and be readable anonymously (what SteamCMD installs need). What fails is dropped
|
||||
// and noted; a variant left with nothing runnable can't be recommended.
|
||||
func verifyVariants(res *researchResult) {
|
||||
var apps []string
|
||||
var appIDs []string
|
||||
for _, v := range res.Variants {
|
||||
if v.SteamAppID != "" {
|
||||
apps = append(apps, v.SteamAppID)
|
||||
appIDs = append(appIDs, v.SteamAppID)
|
||||
}
|
||||
}
|
||||
names := steamAppNames(apps)
|
||||
apps := steamApps(appIDs)
|
||||
anyRunnable := false
|
||||
for i := range res.Variants {
|
||||
v := &res.Variants[i]
|
||||
@@ -324,10 +325,11 @@ func verifyVariants(res *researchResult) {
|
||||
}
|
||||
v.Images = images
|
||||
if v.SteamAppID != "" {
|
||||
if name, ok := names[v.SteamAppID]; ok {
|
||||
v.Verified = append(v.Verified, fmt.Sprintf("Steam app %s is %q", v.SteamAppID, name))
|
||||
if !hasImage(v.Images, "cm2network/steamcmd") {
|
||||
v.Images = append(v.Images, "cm2network/steamcmd")
|
||||
if app, ok := apps[v.SteamAppID]; ok {
|
||||
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 SteamCMD (installing it into /server) is tried first")
|
||||
}
|
||||
} else {
|
||||
v.Problems = append(v.Problems, "Steam app "+v.SteamAppID+" not found for anonymous SteamCMD")
|
||||
@@ -346,44 +348,56 @@ func verifyVariants(res *researchResult) {
|
||||
res.Reason = "unknown"
|
||||
res.Message = "No variant has a Docker image or Steam app that could be confirmed. " + res.Message
|
||||
}
|
||||
// Exactly one recommendation, among the runnable ones
|
||||
recommend(res.Variants)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
runnable := func(v variant) bool { return len(v.Images) > 0 }
|
||||
steamFirst := func(v variant) bool { return runnable(v) && isSteamCMDImage(v.Images[0]) }
|
||||
rec := -1
|
||||
for i, v := range res.Variants {
|
||||
if v.Recommended && rec < 0 && len(v.Images) > 0 {
|
||||
rec = i
|
||||
}
|
||||
res.Variants[i].Recommended = false
|
||||
}
|
||||
if rec < 0 {
|
||||
for i, v := range res.Variants {
|
||||
if len(v.Images) > 0 {
|
||||
for _, want := range []func(variant) bool{
|
||||
func(v variant) bool { return v.Recommended && steamFirst(v) },
|
||||
steamFirst,
|
||||
func(v variant) bool { return v.Recommended && runnable(v) },
|
||||
runnable,
|
||||
} {
|
||||
for i, v := range variants {
|
||||
if rec < 0 && want(v) {
|
||||
rec = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if rec >= 0 {
|
||||
res.Variants[rec].Recommended = true
|
||||
for i := range variants {
|
||||
variants[i].Recommended = i == rec
|
||||
}
|
||||
}
|
||||
|
||||
func hasImage(images []string, name string) bool {
|
||||
for _, i := range images {
|
||||
if i == name || strings.HasPrefix(i, name+":") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
var (
|
||||
appName = regexp.MustCompile(`"common"\s*\{[^}]*?"name"\s+"([^"]+)"`)
|
||||
appLinux = regexp.MustCompile(`"oslist"\s+"[^"]*linux`)
|
||||
)
|
||||
|
||||
type steamApp struct {
|
||||
name string
|
||||
linux bool // ships a Linux server: SteamCMD installs it straight into /server
|
||||
}
|
||||
|
||||
var appName = regexp.MustCompile(`"common"\s*\{[^}]*?"name"\s+"([^"]+)"`)
|
||||
func parseAppInfo(out string) (steamApp, bool) {
|
||||
m := appName.FindStringSubmatch(out)
|
||||
if m == nil {
|
||||
return steamApp{}, false
|
||||
}
|
||||
return steamApp{name: m[1], linux: appLinux.MatchString(out)}, true
|
||||
}
|
||||
|
||||
// steamAppNames asks SteamCMD (anonymously, in its official image) for each app's info: an app that
|
||||
// steamApps asks SteamCMD (anonymously, in its official image) for each app's info: an app that
|
||||
// anonymous SteamCMD can't see can't be installed by the node-agent either.
|
||||
func steamAppNames(apps []string) map[string]string {
|
||||
names := map[string]string{}
|
||||
func steamApps(apps []string) map[string]steamApp {
|
||||
found := map[string]steamApp{}
|
||||
if len(apps) == 0 {
|
||||
return names
|
||||
return found
|
||||
}
|
||||
for _, app := range apps {
|
||||
if !regexp.MustCompile(`^\d+$`).MatchString(app) {
|
||||
@@ -395,9 +409,25 @@ func steamAppNames(apps []string) map[string]string {
|
||||
fmt.Printf("[research] steamcmd for app %s: %v\n", app, err)
|
||||
continue
|
||||
}
|
||||
if m := appName.FindStringSubmatch(out); m != nil {
|
||||
names[app] = m[1]
|
||||
if info, ok := parseAppInfo(out); ok {
|
||||
found[app] = info
|
||||
}
|
||||
}
|
||||
return names
|
||||
return found
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
for _, i := range images {
|
||||
if !isSteamCMDImage(i) {
|
||||
others = append(others, i)
|
||||
}
|
||||
}
|
||||
if linux {
|
||||
return append([]string{"cm2network/steamcmd"}, others...)
|
||||
}
|
||||
return append(others, "cm2network/steamcmd")
|
||||
}
|
||||
Reference in new issue
Block a user