feat(research): one option per way to run a game, SteamCMD recommended
Validate game templates / validate (push) Successful in 19s
Validate game templates / boot-test (push) Skipped
Validate game templates / publish (push) Skipped

A draft built on a ready-made image whose files all live outside /server
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 build picked among the variant's images on its own.

- research reads each Steam app's OS list. When it ships a Linux server,
  the SteamCMD route is the recommended option; for Windows-only servers,
  images that bring Proton or Wine come first. The research prompt says
  the same.
- A variant that can run on several images becomes one option per image
  ("... (SteamCMD)", "... (image x/y)"), so the admin picks exactly how the
  server runs.
- build uses exactly the chosen option and never switches to another; if it
  can't be made to work, the PR says so and another option can be picked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gregandClaude Opus 5.5 committed 2026-10-25 23:10:21 -04:00
1 parent 274ed68cc5
commit f2ffed5c98
3 files changed
+202 -61

No files matched your search

+26 -23
View File
@@ -93,26 +93,35 @@ type buildOptions struct {
agentImage string
}
// build drafts the template for exactly the option the admin chose: one image (research lists each way to
// run a game as its own option). It never switches to another image; if the chosen one can't be made to
// work, the result says so and the admin can pick a different option.
func build(o buildOptions) (*buildResult, error) {
if len(o.variant.Images) == 0 {
return nil, errors.New("the variant has no image to build from")
}
res := &buildResult{GameID: o.id, Dir: filepath.Join(o.gamesDir, o.id)}
res.Verified = append(res.Verified, o.variant.Verified...)
// 1. First boot of each candidate image until one runs as a non-root user
var report *bootReport
for _, image := range o.variant.Images {
steam := isSteamCMDImage(image) && o.variant.SteamAppID != ""
if isSteamCMDImage(image) && !steam {
if isSteamCMDImage(image) && o.variant.SteamAppID == "" {
continue // the SteamCMD image alone runs nothing
}
res.Image = image
if steam && o.variant.StartupHint == "" {
fmt.Printf("[build] no startup command known for Steam app %s; drafting without a first boot\n", o.variant.SteamAppID)
report = nil
break
res, err := buildWithImage(o, image)
if res != nil {
res.Summary = buildSummary(o, res)
}
return res, err
}
return nil, errors.New("the variant has no image to build from")
}
// buildWithImage drafts the template for one image: first boot, draft, then boots of the draft with
// failures going back to the model.
func buildWithImage(o buildOptions, image string) (*buildResult, error) {
res := &buildResult{GameID: o.id, Dir: filepath.Join(o.gamesDir, o.id), Image: image}
res.Verified = append(res.Verified, o.variant.Verified...)
steam := isSteamCMDImage(image)
// 1. First boot of the image untouched: its log, files and settings are the draft's facts
var report *bootReport
if steam && o.variant.StartupHint == "" {
fmt.Printf("[build] no startup command known for Steam app %s; drafting without a first boot\n", o.variant.SteamAppID)
} else {
fmt.Printf("[build] first boot of %s\n", image)
r, err := firstBoot(o.id+"-probe", image, o.variant.SteamAppID, steam, o.variant.StartupHint)
if err != nil {
@@ -120,18 +129,12 @@ func build(o buildOptions) (*buildResult, error) {
}
if why := imageUnusable(r); why != "" {
fmt.Printf("[build] %s can't be used: %s\n", image, why)
res.Problems = append(res.Problems, why)
res.Image = ""
continue
res.Problems = []string{why}
return res, nil
}
report = r
res.Verified = append(res.Verified, fmt.Sprintf("first boot of %s: %s", image, r.Outcome))
break
}
if res.Image == "" {
return res, errors.New("no image of this variant runs as a non-root user")
}
res.Problems = nil
// 2. Draft until validation and the image checks pass
steamApp := ""