Skip to content

Commit 4bd0b8d

Browse files
committed
Warn when launcher modes ignore configured args
1 parent b7528a1 commit 4bd0b8d

3 files changed

Lines changed: 114 additions & 40 deletions

File tree

docs/CONFIGURATION.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ Best for games installed through Steam.
173173
You can find the App ID in the game's Steam store URL. `stopProcessName` is
174174
required for Steam games.
175175

176+
GABS starts Steam games through the platform launcher URL. Configured `args` are
177+
not passed to the game in this mode. Put launch options such as
178+
`-savedatafolder=...` in Steam's own launch options, or use `DirectPath` /
179+
`CustomCommand` when GABS must control the process arguments directly.
180+
176181
### EpicAppId
177182
Best for games installed through Epic Games Store.
178183
```json
@@ -184,6 +189,10 @@ Best for games installed through Epic Games Store.
184189
```
185190
`stopProcessName` is required for Epic games.
186191

192+
As with Steam, configured `args` are not passed to the game in this mode. Use
193+
the game launcher's own launch options, `DirectPath`, or `CustomCommand` for
194+
process arguments.
195+
187196
### CustomCommand
188197
Best for complex launch setups or special requirements.
189198
```json

internal/mcp/game_commands_test.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,17 @@ func TestCurrentGameCommandBehavior(t *testing.T) {
9898
},
9999
},
100100
}
101-
101+
102102
response := server.HandleMessage(showMsg)
103103
if response == nil {
104104
t.Fatal("Expected response from games.show")
105105
}
106-
106+
107107
// Check that response contains detailed game information with validation indicators
108108
respBytes, _ := json.Marshal(response)
109109
responseStr := string(respBytes)
110110
t.Logf("games.show output: %s", responseStr)
111-
111+
112112
// The output should contain the game ID and all configuration details
113113
if !strings.Contains(responseStr, "rimworld") {
114114
t.Error("Expected to see game ID 'rimworld' in output")
@@ -126,7 +126,7 @@ func TestCurrentGameCommandBehavior(t *testing.T) {
126126
t.Error("Expected Steam App ID '294100' to be shown in detailed view")
127127
}
128128
})
129-
129+
130130
// Test games.start with correct ID (should work)
131131
t.Run("GamesStartWithCorrectId", func(t *testing.T) {
132132
startCorrectMsg := &Message{
@@ -193,6 +193,35 @@ func TestCurrentGameCommandBehavior(t *testing.T) {
193193
})
194194
}
195195

196+
func TestGameValidationWarningsForLauncherArgs(t *testing.T) {
197+
steamGame := config.GameConfig{
198+
ID: "rimworld",
199+
Name: "RimWorld",
200+
LaunchMode: "SteamAppId",
201+
Target: "294100",
202+
Args: []string{"-savedatafolder=/tmp/rimworld-test"},
203+
StopProcessName: "RimWorldWin64.exe",
204+
}
205+
206+
warnings := gameValidationWarnings(steamGame)
207+
if len(warnings) != 1 {
208+
t.Fatalf("expected one launcher-args warning, got %d: %#v", len(warnings), warnings)
209+
}
210+
if !strings.Contains(warnings[0], "does not pass configured args") {
211+
t.Fatalf("expected ignored args warning, got %q", warnings[0])
212+
}
213+
if !strings.Contains(warnings[0], "-savedatafolder") {
214+
t.Fatalf("expected savedatafolder hint, got %q", warnings[0])
215+
}
216+
217+
directGame := steamGame
218+
directGame.LaunchMode = "DirectPath"
219+
directGame.Target = "/games/RimWorld/RimWorldWin64.exe"
220+
if warnings := gameValidationWarnings(directGame); len(warnings) != 0 {
221+
t.Fatalf("direct launch args should not warn, got %#v", warnings)
222+
}
223+
}
224+
196225
// TestGameIdResolution tests the new forgiving resolution logic
197226
func TestGameIdResolution(t *testing.T) {
198227
// Create a fresh temporary config

internal/mcp/stdio_server.go

Lines changed: 72 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,12 @@ func (s *Server) RegisterGameManagementTools(gamesConfig *config.GamesConfig, ba
411411

412412
status := s.checkGameStatus(game.ID)
413413
validationWarnings := gameValidationWarnings(*game)
414+
if len(validationWarnings) > 0 {
415+
content.WriteString("\nConfiguration Warnings:\n")
416+
for _, warning := range validationWarnings {
417+
content.WriteString(fmt.Sprintf(" - %s\n", warning))
418+
}
419+
}
414420
structured := map[string]interface{}{
415421
"game": gameConfigStructured(*game),
416422
"status": status,
@@ -527,6 +533,7 @@ func (s *Server) RegisterGameManagementTools(gamesConfig *config.GamesConfig, ba
527533
}, nil
528534
}
529535

536+
validationWarnings := gameValidationWarnings(*game)
530537
startResult, err := s.startGame(*game, gamesConfig, backoffMin, backoffMax)
531538
if err != nil {
532539
var activeErr *gameAlreadyActiveError
@@ -536,14 +543,16 @@ func (s *Server) RegisterGameManagementTools(gamesConfig *config.GamesConfig, ba
536543
status = s.checkGameStatus(game.ID)
537544
}
538545
toolCount := len(s.getGameSpecificTools(game.ID))
546+
structured := map[string]interface{}{
547+
"gameId": game.ID,
548+
"status": status,
549+
"toolCount": toolCount,
550+
"nextActions": s.nextActionsForGameStatus(*game, status, toolCount),
551+
}
552+
addValidationWarnings(structured, validationWarnings)
539553
return &ToolResult{
540-
Content: []Content{{Type: "text", Text: activeErr.ToolMessage(*game)}},
541-
StructuredContent: map[string]interface{}{
542-
"gameId": game.ID,
543-
"status": status,
544-
"toolCount": toolCount,
545-
"nextActions": s.nextActionsForGameStatus(*game, status, toolCount),
546-
},
554+
Content: []Content{{Type: "text", Text: activeErr.ToolMessage(*game)}},
555+
StructuredContent: structured,
547556
}, nil
548557
}
549558

@@ -559,39 +568,46 @@ func (s *Server) RegisterGameManagementTools(gamesConfig *config.GamesConfig, ba
559568
message = fmt.Sprintf("%s: %v", message, startResult.GABPConnectError)
560569
}
561570
message = fmt.Sprintf("%s. The game may still be loading or the mod may be missing. Use games_status, then games_connect once the mod is ready.", message)
562-
return &ToolResult{
563-
Content: []Content{{Type: "text", Text: message}},
564-
StructuredContent: map[string]interface{}{
565-
"gameId": game.ID,
566-
"processStarted": startResult.ProcessStarted,
567-
"gabpConnected": startResult.GABPConnected,
568-
"gameStillRunning": startResult.GameStillRunning,
569-
"gabpWaitMs": startResult.GABPConnectWait.Milliseconds(),
570-
"gabpError": func() interface{} {
571-
if startResult.GABPConnectError == nil {
572-
return nil
573-
}
574-
return startResult.GABPConnectError.Error()
575-
}(),
576-
"nextActions": []map[string]interface{}{
577-
mcpNextAction("games_status", map[string]interface{}{"gameId": game.ID}, "Verify whether the game is still running."),
578-
mcpNextAction("games_connect", map[string]interface{}{"gameId": game.ID}, "Connect after the mod finishes loading."),
579-
},
571+
message = appendValidationWarningText(message, validationWarnings)
572+
structured := map[string]interface{}{
573+
"gameId": game.ID,
574+
"processStarted": startResult.ProcessStarted,
575+
"gabpConnected": startResult.GABPConnected,
576+
"gameStillRunning": startResult.GameStillRunning,
577+
"gabpWaitMs": startResult.GABPConnectWait.Milliseconds(),
578+
"gabpError": func() interface{} {
579+
if startResult.GABPConnectError == nil {
580+
return nil
581+
}
582+
return startResult.GABPConnectError.Error()
583+
}(),
584+
"nextActions": []map[string]interface{}{
585+
mcpNextAction("games_status", map[string]interface{}{"gameId": game.ID}, "Verify whether the game is still running."),
586+
mcpNextAction("games_connect", map[string]interface{}{"gameId": game.ID}, "Connect after the mod finishes loading."),
580587
},
588+
}
589+
addValidationWarnings(structured, validationWarnings)
590+
return &ToolResult{
591+
Content: []Content{{Type: "text", Text: message}},
592+
StructuredContent: structured,
581593
}, nil
582594
}
583595

584-
return &ToolResult{
585-
Content: []Content{{Type: "text", Text: fmt.Sprintf("Game '%s' (%s) started successfully and connected via GABP.", game.ID, game.Name)}},
586-
StructuredContent: map[string]interface{}{
587-
"gameId": game.ID,
588-
"processStarted": true,
589-
"gabpConnected": true,
590-
"gameStillRunning": true,
591-
"nextActions": []map[string]interface{}{
592-
mcpNextAction("games_tool_names", map[string]interface{}{"gameId": game.ID, "brief": true}, "Discover connected game-specific tools."),
593-
},
596+
message := fmt.Sprintf("Game '%s' (%s) started successfully and connected via GABP.", game.ID, game.Name)
597+
message = appendValidationWarningText(message, validationWarnings)
598+
structured := map[string]interface{}{
599+
"gameId": game.ID,
600+
"processStarted": true,
601+
"gabpConnected": true,
602+
"gameStillRunning": true,
603+
"nextActions": []map[string]interface{}{
604+
mcpNextAction("games_tool_names", map[string]interface{}{"gameId": game.ID, "brief": true}, "Discover connected game-specific tools."),
594605
},
606+
}
607+
addValidationWarnings(structured, validationWarnings)
608+
return &ToolResult{
609+
Content: []Content{{Type: "text", Text: message}},
610+
StructuredContent: structured,
595611
}, nil
596612
}, normalizationConfig)
597613

@@ -2084,13 +2100,33 @@ func gameConfigStructured(game config.GameConfig) map[string]interface{} {
20842100
}
20852101

20862102
func gameValidationWarnings(game config.GameConfig) []string {
2087-
warnings := make([]string, 0, 1)
2103+
warnings := make([]string, 0, 2)
20882104
if (game.LaunchMode == "SteamAppId" || game.LaunchMode == "EpicAppId") && game.StopProcessName == "" {
20892105
warnings = append(warnings, fmt.Sprintf("%s games need stopProcessName for reliable games_stop and games_kill.", game.LaunchMode))
20902106
}
2107+
if launcherModeIgnoresConfiguredArgs(game) {
2108+
warnings = append(warnings, fmt.Sprintf("%s launch mode does not pass configured args to the game; use DirectPath, CustomCommand, or the game launcher's own launch options for arguments such as -savedatafolder=...", game.LaunchMode))
2109+
}
20912110
return warnings
20922111
}
20932112

2113+
func launcherModeIgnoresConfiguredArgs(game config.GameConfig) bool {
2114+
return (game.LaunchMode == "SteamAppId" || game.LaunchMode == "EpicAppId") && len(game.Args) > 0
2115+
}
2116+
2117+
func addValidationWarnings(structured map[string]interface{}, warnings []string) {
2118+
if len(warnings) > 0 {
2119+
structured["validationWarnings"] = warnings
2120+
}
2121+
}
2122+
2123+
func appendValidationWarningText(message string, warnings []string) string {
2124+
if len(warnings) == 0 {
2125+
return message
2126+
}
2127+
return fmt.Sprintf("%s Configuration warning: %s", message, strings.Join(warnings, " "))
2128+
}
2129+
20942130
func (s *Server) gameStatusStructured(game config.GameConfig, status string) map[string]interface{} {
20952131
toolCount := len(s.getGameSpecificTools(game.ID))
20962132
item := map[string]interface{}{

0 commit comments

Comments
 (0)