Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 107 additions & 56 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -216,35 +260,61 @@ 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,
)
}
}

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":
Expand Down Expand Up @@ -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"}
Expand All @@ -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, ""}
}
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/components/LoadoutEditor/Main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
</script>
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading