From 4beb40da0f7581689aac407bc2b8a7fa73117d12 Mon Sep 17 00:00:00 2001 From: Virx Date: Sat, 5 Jul 2025 00:43:33 -0400 Subject: [PATCH 1/2] Fix `WaitForMatchReady` --- app.go | 163 ++++++++++++------ .../src/components/LoadoutEditor/Main.svelte | 14 +- loadout.go | 98 ++++------- rockethost.go | 2 +- 4 files changed, 150 insertions(+), 127 deletions(-) diff --git a/app.go b/app.go index 84afc4f..2b0b996 100644 --- a/app.go +++ b/app.go @@ -157,50 +157,94 @@ type StartMatchOptions struct { LauncherArg string `json:"launcherArg"` } +func ReadAllMessages(conn *rlbot.RLBotConnection, packetChan chan any) { + for { + packet, err := conn.RecvPacket() // This is the blocking call + if err != nil { + packetChan <- fmt.Errorf("error receiving packet: %w", err) // Send the error to the channel + return // Exit goroutine on error + } + packetChan <- packet.Value // Send the received packet to the channel + + switch packet.Type { + case flat.CoreMessageDisconnectSignal: + return // Exit goroutine on disconnect signal + } + } +} + func WaitForMatchReady( conn *rlbot.RLBotConnection, - expectedMatchConfig *flat.MatchConfigurationT, - matchStartDur time.Duration, + rlbot_address string, + matchLoadDur time.Duration, matchReadyDur time.Duration, ) error { - packetChan := make(chan interface{}) + packetChan := make(chan any) // Goroutine to continuously receive packets from the connection - go func() { - for { - packet, err := conn.RecvPacket() // This is the blocking call - if err != nil { - packetChan <- fmt.Errorf("error receiving packet: %w", err) // Send the error to the channel - return // Exit goroutine on error - } - packetChan <- packet // Send the received packet to the channel - } - }() - - var matchConfig *flat.MatchConfigurationT - var gamePacket *flat.GamePacketT + go ReadAllMessages(conn, packetChan) // First wait: for initial MatchConfigurationT and GamePacketT, or timeout (matchStartDur) - timer1 := time.NewTimer(matchStartDur) + timer1 := time.NewTimer(matchLoadDur) defer timer1.Stop() // Ensure timer is stopped when the function exits - for matchConfig == nil || gamePacket == nil { + // Wait for the previous match to ended, then reconnect + // We can then guarentee that the subsequent GamePackets are from our new match + reconnected := false + for !reconnected { select { case item := <-packetChan: // Receive either packet or error if err, ok := item.(error); ok { return err // Propagate the error from the goroutine } - packet := item // Otherwise, it's a packet - switch packet := packet.(type) { - case *flat.MatchConfigurationT: - matchConfig = packet + switch item.(type) { + case *flat.DisconnectSignalT: + conn2, err := rlbot.Connect(rlbot_address) + if err != nil { + return fmt.Errorf("Failed to reconnect to RLBotServer at %s", rlbot_address) + } + + conn = &conn2 + + // Close the connection if a new match is started in the middle of waiting for this one + conn.SendPacket(&flat.ConnectionSettingsT{ + AgentId: "", + WantsBallPredictions: false, + WantsComms: false, + CloseBetweenMatches: true, + }) + + // Start reading messages from the new connection + go ReadAllMessages(conn, packetChan) + + reconnected = true + } + case <-timer1.C: + conn.SendPacket(&flat.DisconnectSignalT{}) + return fmt.Errorf("Timed out waiting for match load after %s", matchLoadDur) + } + } + + var gamePacket *flat.GamePacketT + for gamePacket == nil { + select { + case item := <-packetChan: // Receive either packet or error + if err, ok := item.(error); ok { + return err // Propagate the error from the goroutine + } + + switch packet := item.(type) { + case *flat.FieldInfoT: + conn.SendPacket(&flat.InitCompleteT{}) case *flat.GamePacketT: gamePacket = packet + case *flat.DisconnectSignalT: + return fmt.Errorf("Match was ended while waiting for it to load") } case <-timer1.C: - conn.SendPacket(nil) - return fmt.Errorf("Timed out waiting for match start after %s", matchStartDur) + conn.SendPacket(&flat.DisconnectSignalT{}) + return fmt.Errorf("Timed out waiting for match load after %s", matchLoadDur) } } @@ -216,15 +260,15 @@ func WaitForMatchReady( if err, ok := item.(error); ok { return err // Propagate the error from the goroutine } - packet := item - switch packet := packet.(type) { + switch packet := item.(type) { case *flat.GamePacketT: gamePacket = packet - // Ignore other packet types in this phase + case *flat.DisconnectSignalT: + return fmt.Errorf("Match was ended while waiting for it to start") } case <-timer2.C: - conn.SendPacket(nil) + conn.SendPacket(&flat.DisconnectSignalT{}) return fmt.Errorf( "Timed out waiting for match ready after %s", matchReadyDur, @@ -232,19 +276,45 @@ func WaitForMatchReady( } } + conn.SendPacket(&flat.DisconnectSignalT{}) + return nil } -func (a *App) StartMatch(options StartMatchOptions) Result { - // TODO: Save this in App struct - conn, err := rlbot.Connect(a.rlbot_address) +func StartAndWaitForMatch(rlbot_address string, match *flat.MatchConfigurationT) error { + conn, err := rlbot.Connect(rlbot_address) if err != nil { - return Result{ - false, - "Failed to connect to RLBotServer at " + a.rlbot_address, - } + return fmt.Errorf("Failed to reconnect to RLBotServer at %s", rlbot_address) + } + + // Rely on RLBotServer closing this connection when the new match starts + // to differentiate between the new MatchConfigurationT and the old one. + conn.SendPacket(&flat.ConnectionSettingsT{ + AgentId: "", + WantsBallPredictions: false, + WantsComms: false, + CloseBetweenMatches: true, + }) + conn.SendPacket(&flat.InitCompleteT{}) + + conn.SendPacket(match) + + // Wait for the match to start, with timeouts + err = WaitForMatchReady( + &conn, + rlbot_address, + 120*time.Second, + 20*time.Second, + ) + if err != nil { + conn.SendPacket(&flat.DisconnectSignalT{}) + return err } + return nil +} + +func (a *App) StartMatch(options StartMatchOptions) Result { var gameMode flat.GameMode switch options.GameMode { case "Soccar": @@ -319,34 +389,15 @@ func (a *App) StartMatch(options StartMatchOptions) Result { ExistingMatchBehavior: flat.ExistingMatchBehavior(options.ExtraOptions.ExistingMatchBehavior), } - conn.SendPacket(&match) - - conn.SendPacket(&flat.ConnectionSettingsT{ - AgentId: "", - WantsBallPredictions: false, - WantsComms: false, - CloseBetweenMatches: false, - }) - conn.SendPacket(&flat.InitCompleteT{}) - // Using the new function with a 30-second timeout - err = WaitForMatchReady( - &conn, - &match, - 120*time.Second, - 20*time.Second, - ) + err := StartAndWaitForMatch(a.rlbot_address, &match) if err != nil { return Result{false, err.Error()} } - conn.SendPacket(nil) // Tell core that we want to disconnect - return Result{true, ""} } func (a *App) StopMatch(shutdownServer bool) Result { - // TODO: Save this in App struct - // TODO: Make dynamic, pull from env var? conn, err := rlbot.Connect(a.rlbot_address) if err != nil { return Result{false, "Failed to connect to rlbot"} @@ -355,7 +406,7 @@ func (a *App) StopMatch(shutdownServer bool) Result { conn.SendPacket(&flat.StopCommandT{ ShutdownServer: shutdownServer, }) - conn.SendPacket(nil) // Tell core that we want to disconnect + conn.SendPacket(&flat.DisconnectSignalT{}) return Result{true, ""} } diff --git a/frontend/src/components/LoadoutEditor/Main.svelte b/frontend/src/components/LoadoutEditor/Main.svelte index 588eae9..1903457 100644 --- a/frontend/src/components/LoadoutEditor/Main.svelte +++ b/frontend/src/components/LoadoutEditor/Main.svelte @@ -136,30 +136,38 @@ async function LaunchMatch( team: "blue" | "orange", ) { if (!previewMatchTeam) { + previewMatchTeam = team; + lastShowcaseType = selectedShowcaseType; + + let id = toast.loading(`Launching preview for ${team} car...`); await App.LaunchPreviewLoadout( options, ExistingMatchBehavior.ExistingMatchBehaviorRestart, ); - toast.success(`Launching preview for ${team} car`); + toast.success(`Launched preview for ${team} car`, { id }); } else { if ( lastShowcaseType !== selectedShowcaseType || previewMatchTeam !== team ) { + previewMatchTeam = team; + lastShowcaseType = selectedShowcaseType; + await App.LaunchPreviewLoadout( options, ExistingMatchBehavior.ExistingMatchBehaviorContinueAndSpawn, ); } else { + previewMatchTeam = team; + lastShowcaseType = selectedShowcaseType; + await App.SetLoadout(options); } toast.success(`Preview updated for ${team} car`); } - previewMatchTeam = team; - lastShowcaseType = selectedShowcaseType; App.SetShowcaseType(selectedShowcaseType, team === "blue" ? 0 : 1); } diff --git a/loadout.go b/loadout.go index 8610fae..7938522 100644 --- a/loadout.go +++ b/loadout.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "os" "path/filepath" @@ -96,20 +97,31 @@ func (options LoadoutPreviewOptions) GetPreviewMatch(existingMatchBehavior flat. } func (a *App) LaunchPreviewLoadout(options LoadoutPreviewOptions, existingMatchBehavior flat.ExistingMatchBehavior) error { - conn, err := rlbot.Connect(a.rlbot_address) - if err != nil { - return err - } - match, err := options.GetPreviewMatch(existingMatchBehavior) if err != nil { return err } - conn.SendPacket(match) - conn.SendPacket(nil) + return StartAndWaitForMatch(a.rlbot_address, match) +} - return nil +func WaitForGamePacket(conn *rlbot.RLBotConnection) (*flat.GamePacketT, error) { + var gamePacket *flat.GamePacketT + for gamePacket == nil || (gamePacket.MatchInfo.MatchPhase != flat.MatchPhaseKickoff && gamePacket.MatchInfo.MatchPhase != flat.MatchPhaseActive) { + packet, err := conn.RecvPacket() + if err != nil { + return nil, err + } + + switch packet := packet.Value.(type) { + case *flat.GamePacketT: + gamePacket = packet + case *flat.DisconnectSignalT: + return nil, fmt.Errorf("received disconnect signal while waiting for game packet") + } + } + + return gamePacket, nil } func (a *App) SetLoadout(options LoadoutPreviewOptions) error { @@ -127,18 +139,9 @@ func (a *App) SetLoadout(options LoadoutPreviewOptions) error { conn.SendPacket(&flat.InitCompleteT{}) - // wait for GamePacket - var gamePacket *flat.GamePacketT - for gamePacket == nil { - packet, err := conn.RecvPacket() - if err != nil { - return err - } - - switch packet := packet.Value.(type) { - case *flat.GamePacketT: - gamePacket = packet - } + gamePacket, err := WaitForGamePacket(&conn) + if err != nil { + return err } // if the match is over, launch a new match @@ -155,7 +158,7 @@ func (a *App) SetLoadout(options LoadoutPreviewOptions) error { } conn.SendPacket(match) - conn.SendPacket(nil) + conn.SendPacket(&flat.DisconnectSignalT{}) return nil } @@ -167,7 +170,7 @@ func (a *App) SetLoadout(options LoadoutPreviewOptions) error { } conn.SendPacket(match) - conn.SendPacket(nil) + conn.SendPacket(&flat.DisconnectSignalT{}) return nil } @@ -176,47 +179,13 @@ func (a *App) SetLoadout(options LoadoutPreviewOptions) error { Loadout: options.Loadout.ToPlayerLoadout(), }) - conn.SendPacket(nil) + conn.SendPacket(&flat.DisconnectSignalT{}) return nil } -func WaitForLoadoutMatchReady(conn *rlbot.RLBotConnection, team uint32) (*flat.GamePacketT, error) { - // wait for the correct match to start - var gamePacket *flat.GamePacketT - var matchConfig *flat.MatchConfigurationT - for matchConfig == nil || gamePacket == nil || len(matchConfig.PlayerConfigurations) != 1 || matchConfig.PlayerConfigurations[0].Team != team || matchConfig.PlayerConfigurations[0].Variety.Type != flat.PlayerClassCustomBot || matchConfig.PlayerConfigurations[0].Variety.Value.(*flat.CustomBotT).Name != "Showcase" { - packet, err := conn.RecvPacket() - if err != nil { - return nil, err - } - - switch packet := packet.Value.(type) { - case *flat.MatchConfigurationT: - matchConfig = packet - case *flat.GamePacketT: - gamePacket = packet - } - } - - // while the match isn't active or the car is on the wrong team - for !(gamePacket.MatchInfo.MatchPhase == flat.MatchPhaseActive || gamePacket.MatchInfo.MatchPhase == flat.MatchPhaseKickoff) || len(gamePacket.Players) != 1 || gamePacket.Players[0].Team != team { - packet, err := conn.RecvPacket() - if err != nil { - return nil, err - } - - switch packet := packet.Value.(type) { - case *flat.GamePacketT: - gamePacket = packet - } - } - - return gamePacket, nil -} - -func (a *App) StaticSetter(team uint32) error { - conn, err := rlbot.Connect(a.rlbot_address) +func StaticSetter(rlbot_address string, team uint32) error { + conn, err := rlbot.Connect(rlbot_address) if err != nil { return err } @@ -230,11 +199,6 @@ func (a *App) StaticSetter(team uint32) error { conn.SendPacket(&flat.InitCompleteT{}) - _, err = WaitForLoadoutMatchReady(&conn, team) - if err != nil { - return err - } - gameState := flat.DesiredGameStateT{ CarStates: []*flat.DesiredCarStateT{ { @@ -278,7 +242,7 @@ func (a *App) SetShowcaseType(showcaseType string, team uint32) error { conn.SendPacket(&flat.InitCompleteT{}) - gamePacket, err := WaitForLoadoutMatchReady(&conn, team) + gamePacket, err := WaitForGamePacket(&conn) if err != nil { return err } @@ -310,7 +274,7 @@ func (a *App) SetShowcaseType(showcaseType string, team uint32) error { case "static": controller.Boost = true - go a.StaticSetter(team) + go StaticSetter(a.rlbot_address, team) case "boost": controller.Boost = true controller.Steer = 1 @@ -353,7 +317,7 @@ func (a *App) SetShowcaseType(showcaseType string, team uint32) error { ControllerState: &controller, }) - conn.SendPacket(nil) + conn.SendPacket(&flat.DisconnectSignalT{}) return nil } diff --git a/rockethost.go b/rockethost.go index f61600e..9cc880e 100644 --- a/rockethost.go +++ b/rockethost.go @@ -266,7 +266,7 @@ outer: return "", errors.New("Couldn't send join message") } - conn.SendPacket(nil) // Tell core that we want to disconnect + conn.SendPacket(&flat.DisconnectSignalT{}) return result.Message, nil } From 425e87133a7c678b075ff1a275ba387db65af18b Mon Sep 17 00:00:00 2001 From: Virx Date: Sat, 5 Jul 2025 18:50:18 -0400 Subject: [PATCH 2/2] Update interface --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 78bd7de..a42afec 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/BurntSushi/toml v1.5.0 - github.com/RLBot/go-interface v0.0.0-20250622212621-386aff5bf548 + github.com/RLBot/go-interface v0.0.0-20250705224140-6c179505c975 github.com/ncruces/zenity v0.10.14 github.com/ulikunitz/xz v0.5.12 github.com/wailsapp/mimetype v1.4.1 diff --git a/go.sum b/go.sum index 13e043d..45c83a0 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/RLBot/go-interface v0.0.0-20250622212621-386aff5bf548 h1:oaBftapE2YFN6DPMXhVzFpvj27W1X4cUzVhg/bwlNko= -github.com/RLBot/go-interface v0.0.0-20250622212621-386aff5bf548/go.mod h1:zogQvXLJKb2EQ4bb/PUH168VPwxBdzG1SSFJt9uJM5c= +github.com/RLBot/go-interface v0.0.0-20250705224140-6c179505c975 h1:vw1+2F2mY7qlw1d8dCvW8r+f+zxyq1H03NL+XrmzBiM= +github.com/RLBot/go-interface v0.0.0-20250705224140-6c179505c975/go.mod h1:zogQvXLJKb2EQ4bb/PUH168VPwxBdzG1SSFJt9uJM5c= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=