2014-06-24 22:51:10 -07:00
|
|
|
// Copyright 2014 Team 254. All Rights Reserved.
|
|
|
|
|
// Author: pat@patfairbank.com (Patrick Fairbank)
|
|
|
|
|
//
|
|
|
|
|
// Web routes for controlling match play.
|
|
|
|
|
|
2017-08-31 23:26:22 -07:00
|
|
|
package web
|
2014-06-24 22:51:10 -07:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2022-08-03 20:53:16 -07:00
|
|
|
"github.com/Team254/cheesy-arena-lite/bracket"
|
2022-07-24 14:36:44 -07:00
|
|
|
"github.com/Team254/cheesy-arena-lite/field"
|
2021-05-16 11:00:29 -07:00
|
|
|
"github.com/Team254/cheesy-arena-lite/game"
|
|
|
|
|
"github.com/Team254/cheesy-arena-lite/model"
|
|
|
|
|
"github.com/Team254/cheesy-arena-lite/tournament"
|
|
|
|
|
"github.com/Team254/cheesy-arena-lite/websocket"
|
2014-06-24 22:51:10 -07:00
|
|
|
"github.com/gorilla/mux"
|
2014-07-06 00:34:40 -07:00
|
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
|
"io"
|
|
|
|
|
"log"
|
2014-06-24 22:51:10 -07:00
|
|
|
"net/http"
|
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
2014-07-30 22:55:14 -07:00
|
|
|
"time"
|
2014-06-24 22:51:10 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type MatchPlayListItem struct {
|
2021-05-12 17:49:05 -07:00
|
|
|
Id int
|
2014-06-24 22:51:10 -07:00
|
|
|
DisplayName string
|
|
|
|
|
Time string
|
2020-03-29 00:04:15 -07:00
|
|
|
Status model.MatchStatus
|
2014-06-24 22:51:10 -07:00
|
|
|
ColorClass string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MatchPlayList []MatchPlayListItem
|
|
|
|
|
|
|
|
|
|
// Shows the match play control interface.
|
2017-08-28 20:14:32 -07:00
|
|
|
func (web *Web) matchPlayHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !web.userIsAdmin(w, r) {
|
2015-08-22 23:33:38 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-28 20:14:32 -07:00
|
|
|
practiceMatches, err := web.buildMatchPlayList("practice")
|
2014-06-24 22:51:10 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
qualificationMatches, err := web.buildMatchPlayList("qualification")
|
2014-06-24 22:51:10 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
eliminationMatches, err := web.buildMatchPlayList("elimination")
|
2014-06-24 22:51:10 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-31 23:26:22 -07:00
|
|
|
template, err := web.parseFiles("templates/match_play.html", "templates/base.html")
|
2014-06-24 22:51:10 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
matchesByType := map[string]MatchPlayList{"practice": practiceMatches,
|
|
|
|
|
"qualification": qualificationMatches, "elimination": eliminationMatches}
|
2018-09-22 01:10:12 -07:00
|
|
|
currentMatchType := web.arena.CurrentMatch.Type
|
|
|
|
|
if currentMatchType == "test" {
|
2014-06-24 22:51:10 -07:00
|
|
|
currentMatchType = "practice"
|
|
|
|
|
}
|
2022-07-26 18:58:38 -07:00
|
|
|
redOffFieldTeams, blueOffFieldTeams, err := web.arena.Database.GetOffFieldTeamIds(web.arena.CurrentMatch)
|
|
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
matchResult, err := web.arena.Database.GetMatchResultForMatch(web.arena.CurrentMatch.Id)
|
2014-07-10 23:03:03 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
isReplay := matchResult != nil
|
2014-06-24 22:51:10 -07:00
|
|
|
data := struct {
|
2017-08-23 22:41:56 -07:00
|
|
|
*model.EventSettings
|
2020-04-04 23:53:25 -07:00
|
|
|
PlcIsEnabled bool
|
|
|
|
|
MatchesByType map[string]MatchPlayList
|
|
|
|
|
CurrentMatchType string
|
|
|
|
|
Match *model.Match
|
2022-07-26 18:58:38 -07:00
|
|
|
RedOffFieldTeams []int
|
|
|
|
|
BlueOffFieldTeams []int
|
2020-04-14 19:38:14 -05:00
|
|
|
RedScore *game.Score
|
|
|
|
|
BlueScore *game.Score
|
2020-04-04 23:53:25 -07:00
|
|
|
AllowSubstitution bool
|
|
|
|
|
IsReplay bool
|
|
|
|
|
PlcArmorBlockStatuses map[string]bool
|
2022-07-26 18:58:38 -07:00
|
|
|
}{
|
|
|
|
|
web.arena.EventSettings,
|
|
|
|
|
web.arena.Plc.IsEnabled(),
|
|
|
|
|
matchesByType,
|
|
|
|
|
currentMatchType,
|
|
|
|
|
web.arena.CurrentMatch,
|
|
|
|
|
redOffFieldTeams,
|
|
|
|
|
blueOffFieldTeams,
|
|
|
|
|
web.arena.RedScore,
|
|
|
|
|
web.arena.BlueScore,
|
|
|
|
|
web.arena.CurrentMatch.ShouldAllowSubstitution(),
|
|
|
|
|
isReplay,
|
|
|
|
|
web.arena.Plc.GetArmorBlockStatuses(),
|
|
|
|
|
}
|
2014-06-24 22:51:10 -07:00
|
|
|
err = template.ExecuteTemplate(w, "base", data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-09-06 22:25:12 -07:00
|
|
|
// Loads the given match onto the arena in preparation for playing it.
|
2017-08-28 20:14:32 -07:00
|
|
|
func (web *Web) matchPlayLoadHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !web.userIsAdmin(w, r) {
|
2015-08-22 23:33:38 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2014-06-29 14:20:47 -07:00
|
|
|
vars := mux.Vars(r)
|
2021-05-12 17:49:05 -07:00
|
|
|
matchId, _ := strconv.Atoi(vars["matchId"])
|
2017-08-23 22:41:56 -07:00
|
|
|
var match *model.Match
|
2014-07-06 00:34:40 -07:00
|
|
|
var err error
|
|
|
|
|
if matchId == 0 {
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.LoadTestMatch()
|
2014-07-06 00:34:40 -07:00
|
|
|
} else {
|
2017-08-28 20:14:32 -07:00
|
|
|
match, err = web.arena.Database.GetMatchById(matchId)
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if match == nil {
|
|
|
|
|
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.LoadMatch(match)
|
2014-06-29 14:20:47 -07:00
|
|
|
}
|
2014-07-04 16:55:29 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2014-06-29 14:20:47 -07:00
|
|
|
|
2017-09-02 14:08:16 -07:00
|
|
|
http.Redirect(w, r, "/match_play", 303)
|
2014-07-27 16:41:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Loads the results for the given match into the display buffer.
|
2017-08-28 20:14:32 -07:00
|
|
|
func (web *Web) matchPlayShowResultHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !web.userIsAdmin(w, r) {
|
2015-08-22 23:33:38 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2014-07-27 16:41:09 -07:00
|
|
|
vars := mux.Vars(r)
|
2021-05-12 17:49:05 -07:00
|
|
|
matchId, _ := strconv.Atoi(vars["matchId"])
|
2017-08-28 20:14:32 -07:00
|
|
|
match, err := web.arena.Database.GetMatchById(matchId)
|
2014-08-02 19:43:45 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if match == nil {
|
|
|
|
|
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
matchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id)
|
2014-07-27 16:41:09 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if matchResult == nil {
|
|
|
|
|
handleWebErr(w, fmt.Errorf("No result found for match ID %d.", matchId))
|
|
|
|
|
return
|
|
|
|
|
}
|
2020-03-26 21:38:58 -07:00
|
|
|
if match.ShouldUpdateRankings() {
|
|
|
|
|
web.arena.SavedRankings, err = web.arena.Database.GetAllRankings()
|
|
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
web.arena.SavedRankings = game.Rankings{}
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
web.arena.SavedMatch = match
|
|
|
|
|
web.arena.SavedMatchResult = matchResult
|
2018-08-31 22:40:08 -07:00
|
|
|
web.arena.ScorePostedNotifier.Notify()
|
2014-07-27 16:41:09 -07:00
|
|
|
|
2017-09-02 14:08:16 -07:00
|
|
|
http.Redirect(w, r, "/match_play", 303)
|
2014-06-29 14:20:47 -07:00
|
|
|
}
|
|
|
|
|
|
2014-07-06 00:34:40 -07:00
|
|
|
// The websocket endpoint for the match play client to send control commands and receive status updates.
|
2017-08-28 20:14:32 -07:00
|
|
|
func (web *Web) matchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !web.userIsAdmin(w, r) {
|
2015-08-22 23:33:38 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-31 22:40:08 -07:00
|
|
|
ws, err := websocket.NewWebsocket(w, r)
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
|
|
|
|
handleWebErr(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-08-31 22:40:08 -07:00
|
|
|
defer ws.Close()
|
2014-07-06 00:34:40 -07:00
|
|
|
|
2018-08-31 22:40:08 -07:00
|
|
|
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
|
2018-09-18 23:58:33 -07:00
|
|
|
go ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.ArenaStatusNotifier, web.arena.MatchTimeNotifier,
|
2020-04-14 19:38:14 -05:00
|
|
|
web.arena.RealtimeScoreNotifier, web.arena.AudienceDisplayModeNotifier,
|
2020-03-28 18:32:46 -07:00
|
|
|
web.arena.AllianceStationDisplayModeNotifier, web.arena.EventStatusNotifier)
|
2014-07-06 00:34:40 -07:00
|
|
|
|
|
|
|
|
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
|
|
|
|
for {
|
2018-08-31 22:40:08 -07:00
|
|
|
messageType, data, err := ws.Read()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
// Client has closed the connection; nothing to do here.
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-08-31 22:40:08 -07:00
|
|
|
log.Println(err)
|
2014-07-06 00:34:40 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch messageType {
|
|
|
|
|
case "substituteTeam":
|
|
|
|
|
args := struct {
|
|
|
|
|
Team int
|
|
|
|
|
Position string
|
|
|
|
|
}{}
|
|
|
|
|
err = mapstructure.Decode(data, &args)
|
|
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.SubstituteTeam(args.Team, args.Position)
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
case "toggleBypass":
|
|
|
|
|
station, ok := data.(string)
|
|
|
|
|
if !ok {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
if _, ok := web.arena.AllianceStations[station]; !ok {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(fmt.Sprintf("Invalid alliance station '%s'.", station))
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
web.arena.AllianceStations[station].Bypass = !web.arena.AllianceStations[station].Bypass
|
2014-07-06 00:34:40 -07:00
|
|
|
case "startMatch":
|
2015-03-28 19:45:49 -07:00
|
|
|
args := struct {
|
2019-07-27 12:09:35 -07:00
|
|
|
MuteMatchSounds bool
|
2015-03-28 19:45:49 -07:00
|
|
|
}{}
|
|
|
|
|
err = mapstructure.Decode(data, &args)
|
|
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2015-03-28 19:45:49 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
web.arena.MuteMatchSounds = args.MuteMatchSounds
|
|
|
|
|
err = web.arena.StartMatch()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
case "abortMatch":
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.AbortMatch()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2022-07-24 14:36:44 -07:00
|
|
|
case "signalVolunteers":
|
|
|
|
|
if web.arena.MatchState != field.PostMatch {
|
|
|
|
|
// Don't allow clearing the field until the match is over.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
web.arena.FieldVolunteers = true
|
|
|
|
|
continue // Don't reload.
|
|
|
|
|
case "signalReset":
|
|
|
|
|
if web.arena.MatchState != field.PostMatch {
|
|
|
|
|
// Don't allow clearing the field until the match is over.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
web.arena.FieldReset = true
|
|
|
|
|
web.arena.AllianceStationDisplayMode = "fieldReset"
|
|
|
|
|
web.arena.AllianceStationDisplayModeNotifier.Notify()
|
|
|
|
|
continue // Don't reload.
|
2014-07-06 00:34:40 -07:00
|
|
|
case "commitResults":
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.commitCurrentMatchScore()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.ResetMatch()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.LoadNextMatch()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2018-08-31 22:40:08 -07:00
|
|
|
err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier)
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
log.Println(err)
|
2014-07-06 00:34:40 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
continue // Skip sending the status update, as the client is about to terminate and reload.
|
|
|
|
|
case "discardResults":
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.ResetMatch()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
err = web.arena.LoadNextMatch()
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(err.Error())
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2018-08-31 22:40:08 -07:00
|
|
|
err = ws.WriteNotifier(web.arena.ReloadDisplaysNotifier)
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
log.Println(err)
|
2014-07-06 00:34:40 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
continue // Skip sending the status update, as the client is about to terminate and reload.
|
2014-08-02 19:43:45 -07:00
|
|
|
case "setAudienceDisplay":
|
2022-04-30 14:40:39 -07:00
|
|
|
mode, ok := data.(string)
|
2014-08-02 19:43:45 -07:00
|
|
|
if !ok {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
2014-08-02 19:43:45 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2022-04-30 14:40:39 -07:00
|
|
|
web.arena.SetAudienceDisplayMode(mode)
|
2014-08-02 19:43:45 -07:00
|
|
|
continue
|
2014-08-08 12:39:08 -07:00
|
|
|
case "setAllianceStationDisplay":
|
2022-04-30 14:40:39 -07:00
|
|
|
mode, ok := data.(string)
|
2014-08-08 12:39:08 -07:00
|
|
|
if !ok {
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
2014-08-08 12:39:08 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2022-04-30 14:40:39 -07:00
|
|
|
web.arena.SetAllianceStationDisplayMode(mode)
|
2014-08-08 12:39:08 -07:00
|
|
|
continue
|
2018-09-18 23:58:33 -07:00
|
|
|
case "startTimeout":
|
|
|
|
|
durationSec, ok := data.(float64)
|
|
|
|
|
if !ok {
|
|
|
|
|
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
err = web.arena.StartTimeout(int(durationSec))
|
|
|
|
|
if err != nil {
|
|
|
|
|
ws.WriteError(err.Error())
|
|
|
|
|
continue
|
|
|
|
|
}
|
2022-07-24 16:50:41 -07:00
|
|
|
case "setTestMatchName":
|
|
|
|
|
if web.arena.CurrentMatch.Type != "test" {
|
|
|
|
|
// Don't allow changing the name of a non-test match.
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name, ok := data.(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
ws.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
web.arena.CurrentMatch.DisplayName = name
|
|
|
|
|
web.arena.MatchLoadNotifier.Notify()
|
|
|
|
|
continue
|
2020-04-14 19:38:14 -05:00
|
|
|
case "updateRealtimeScore":
|
|
|
|
|
args := data.(map[string]interface{})
|
|
|
|
|
web.arena.BlueScore.AutoPoints = int(args["blueAuto"].(float64))
|
|
|
|
|
web.arena.RedScore.AutoPoints = int(args["redAuto"].(float64))
|
|
|
|
|
web.arena.BlueScore.TeleopPoints = int(args["blueTeleop"].(float64))
|
|
|
|
|
web.arena.RedScore.TeleopPoints = int(args["redTeleop"].(float64))
|
|
|
|
|
web.arena.BlueScore.EndgamePoints = int(args["blueEndgame"].(float64))
|
|
|
|
|
web.arena.RedScore.EndgamePoints = int(args["redEndgame"].(float64))
|
|
|
|
|
web.arena.RealtimeScoreNotifier.Notify()
|
2014-07-06 00:34:40 -07:00
|
|
|
default:
|
2018-08-31 22:40:08 -07:00
|
|
|
ws.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
|
2014-07-06 00:34:40 -07:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send out the status again after handling the command, as it most likely changed as a result.
|
2018-08-31 22:40:08 -07:00
|
|
|
err = ws.WriteNotifier(web.arena.ArenaStatusNotifier)
|
2014-07-06 00:34:40 -07:00
|
|
|
if err != nil {
|
2018-08-31 22:40:08 -07:00
|
|
|
log.Println(err)
|
2014-07-06 00:34:40 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-03 20:59:07 -07:00
|
|
|
// Saves the given match and result to the database, supplanting any previous result for the match.
|
2020-03-26 21:38:58 -07:00
|
|
|
func (web *Web) commitMatchScore(match *model.Match, matchResult *model.MatchResult, isMatchReviewEdit bool) error {
|
|
|
|
|
var updatedRankings game.Rankings
|
|
|
|
|
|
2018-10-04 23:58:33 -07:00
|
|
|
if match.Type != "test" {
|
|
|
|
|
if matchResult.PlayNumber == 0 {
|
|
|
|
|
// Determine the play number for this new match result.
|
|
|
|
|
prevMatchResult, err := web.arena.Database.GetMatchResultForMatch(match.Id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if prevMatchResult != nil {
|
|
|
|
|
matchResult.PlayNumber = prevMatchResult.PlayNumber + 1
|
|
|
|
|
} else {
|
|
|
|
|
matchResult.PlayNumber = 1
|
|
|
|
|
}
|
2014-07-06 00:34:40 -07:00
|
|
|
|
2018-10-04 23:58:33 -07:00
|
|
|
// Save the match result record to the database.
|
|
|
|
|
err = web.arena.Database.CreateMatchResult(matchResult)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2014-06-28 22:03:30 -07:00
|
|
|
} else {
|
2018-10-04 23:58:33 -07:00
|
|
|
// We are updating a match result record that already exists.
|
2021-05-12 16:40:07 -07:00
|
|
|
err := web.arena.Database.UpdateMatchResult(matchResult)
|
2018-10-04 23:58:33 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2014-06-28 22:03:30 -07:00
|
|
|
}
|
|
|
|
|
|
2018-10-04 23:58:33 -07:00
|
|
|
// Update and save the match record to the database.
|
|
|
|
|
match.ScoreCommittedAt = time.Now()
|
2020-04-14 19:38:14 -05:00
|
|
|
redScore := matchResult.RedScoreSummary()
|
|
|
|
|
blueScore := matchResult.BlueScoreSummary()
|
2018-10-04 23:58:33 -07:00
|
|
|
if redScore.Score > blueScore.Score {
|
2020-03-29 00:04:15 -07:00
|
|
|
match.Status = model.RedWonMatch
|
2018-10-04 23:58:33 -07:00
|
|
|
} else if redScore.Score < blueScore.Score {
|
2020-03-29 00:04:15 -07:00
|
|
|
match.Status = model.BlueWonMatch
|
2018-10-04 23:58:33 -07:00
|
|
|
} else {
|
2020-03-29 00:04:15 -07:00
|
|
|
match.Status = model.TieMatch
|
2014-06-28 22:03:30 -07:00
|
|
|
}
|
2021-05-12 16:40:07 -07:00
|
|
|
err := web.arena.Database.UpdateMatch(match)
|
2014-06-28 22:03:30 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2014-08-20 03:07:34 -07:00
|
|
|
|
2019-07-20 22:42:56 -07:00
|
|
|
if match.ShouldUpdateRankings() {
|
2018-10-04 23:58:33 -07:00
|
|
|
// Recalculate all the rankings.
|
2020-03-26 21:38:58 -07:00
|
|
|
rankings, err := tournament.CalculateRankings(web.arena.Database, isMatchReviewEdit)
|
2018-10-04 23:58:33 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2020-03-26 21:38:58 -07:00
|
|
|
updatedRankings = rankings
|
2014-07-30 22:55:14 -07:00
|
|
|
}
|
|
|
|
|
|
2019-07-20 22:42:56 -07:00
|
|
|
if match.ShouldUpdateEliminationMatches() {
|
2022-08-03 20:53:16 -07:00
|
|
|
if err = web.arena.Database.UpdateAllianceFromMatch(
|
|
|
|
|
match.ElimRedAlliance, [3]int{match.Red1, match.Red2, match.Red3},
|
2022-07-31 17:21:33 -07:00
|
|
|
); err != nil {
|
2019-10-07 21:41:43 -07:00
|
|
|
return err
|
|
|
|
|
}
|
2022-08-03 20:53:16 -07:00
|
|
|
if err = web.arena.Database.UpdateAllianceFromMatch(
|
|
|
|
|
match.ElimBlueAlliance, [3]int{match.Blue1, match.Blue2, match.Blue3},
|
2022-07-31 17:21:33 -07:00
|
|
|
); err != nil {
|
2019-10-07 21:41:43 -07:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-04 23:58:33 -07:00
|
|
|
// Generate any subsequent elimination matches.
|
2022-08-03 20:53:16 -07:00
|
|
|
nextMatchTime := time.Now().Add(time.Second * bracket.ElimMatchSpacingSec)
|
|
|
|
|
if err = web.arena.UpdatePlayoffBracket(&nextMatchTime); err != nil {
|
2018-10-04 23:58:33 -07:00
|
|
|
return err
|
2016-11-05 15:51:50 -07:00
|
|
|
}
|
2019-08-09 23:13:45 -07:00
|
|
|
|
|
|
|
|
// Generate awards if the tournament is over.
|
2022-08-03 20:53:16 -07:00
|
|
|
if web.arena.PlayoffBracket.IsComplete() {
|
|
|
|
|
winnerAllianceId := web.arena.PlayoffBracket.Winner()
|
|
|
|
|
finalistAllianceId := web.arena.PlayoffBracket.Finalist()
|
|
|
|
|
if err = tournament.CreateOrUpdateWinnerAndFinalistAwards(
|
|
|
|
|
web.arena.Database, winnerAllianceId, finalistAllianceId,
|
|
|
|
|
); err != nil {
|
2019-08-09 23:13:45 -07:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-10-04 23:58:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if web.arena.EventSettings.TbaPublishingEnabled && match.Type != "practice" {
|
|
|
|
|
// Publish asynchronously to The Blue Alliance.
|
|
|
|
|
go func() {
|
2021-05-12 16:40:07 -07:00
|
|
|
if err = web.arena.TbaClient.PublishMatches(web.arena.Database); err != nil {
|
2018-10-04 23:58:33 -07:00
|
|
|
log.Printf("Failed to publish matches: %s", err.Error())
|
2016-11-05 15:51:50 -07:00
|
|
|
}
|
2019-07-20 22:42:56 -07:00
|
|
|
if match.ShouldUpdateRankings() {
|
2021-05-12 16:40:07 -07:00
|
|
|
if err = web.arena.TbaClient.PublishRankings(web.arena.Database); err != nil {
|
2018-10-04 23:58:33 -07:00
|
|
|
log.Printf("Failed to publish rankings: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Back up the database, but don't error out if it fails.
|
|
|
|
|
err = web.arena.Database.Backup(web.arena.EventSettings.Name,
|
|
|
|
|
fmt.Sprintf("post_%s_match_%s", match.Type, match.DisplayName))
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Println(err)
|
|
|
|
|
}
|
2016-11-05 15:51:50 -07:00
|
|
|
}
|
|
|
|
|
|
2020-03-26 21:38:58 -07:00
|
|
|
if !isMatchReviewEdit {
|
2018-10-04 23:58:33 -07:00
|
|
|
// Store the result in the buffer to be shown in the audience display.
|
|
|
|
|
web.arena.SavedMatch = match
|
|
|
|
|
web.arena.SavedMatchResult = matchResult
|
2020-03-26 21:38:58 -07:00
|
|
|
web.arena.SavedRankings = updatedRankings
|
2018-10-04 23:58:33 -07:00
|
|
|
web.arena.ScorePostedNotifier.Notify()
|
2014-08-28 19:32:18 -07:00
|
|
|
}
|
|
|
|
|
|
2014-06-24 22:51:10 -07:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-28 20:14:32 -07:00
|
|
|
func (web *Web) getCurrentMatchResult() *model.MatchResult {
|
|
|
|
|
return &model.MatchResult{MatchId: web.arena.CurrentMatch.Id, MatchType: web.arena.CurrentMatch.Type,
|
2020-04-14 19:38:14 -05:00
|
|
|
RedScore: web.arena.RedScore, BlueScore: web.arena.BlueScore}
|
2015-08-19 22:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Saves the realtime result as the final score for the match currently loaded into the arena.
|
2017-08-28 20:14:32 -07:00
|
|
|
func (web *Web) commitCurrentMatchScore() error {
|
2020-03-26 21:38:58 -07:00
|
|
|
return web.commitMatchScore(web.arena.CurrentMatch, web.getCurrentMatchResult(), false)
|
2014-08-03 20:59:07 -07:00
|
|
|
}
|
|
|
|
|
|
2014-09-06 22:25:12 -07:00
|
|
|
// Helper function to implement the required interface for Sort.
|
2014-06-24 22:51:10 -07:00
|
|
|
func (list MatchPlayList) Len() int {
|
|
|
|
|
return len(list)
|
|
|
|
|
}
|
|
|
|
|
|
2014-09-06 22:25:12 -07:00
|
|
|
// Helper function to implement the required interface for Sort.
|
2014-06-24 22:51:10 -07:00
|
|
|
func (list MatchPlayList) Less(i, j int) bool {
|
2020-03-29 00:04:15 -07:00
|
|
|
return list[i].Status == model.MatchNotPlayed && list[j].Status != model.MatchNotPlayed
|
2014-06-24 22:51:10 -07:00
|
|
|
}
|
|
|
|
|
|
2014-09-06 22:25:12 -07:00
|
|
|
// Helper function to implement the required interface for Sort.
|
2014-06-24 22:51:10 -07:00
|
|
|
func (list MatchPlayList) Swap(i, j int) {
|
|
|
|
|
list[i], list[j] = list[j], list[i]
|
|
|
|
|
}
|
|
|
|
|
|
2014-09-06 22:25:12 -07:00
|
|
|
// Constructs the list of matches to display on the side of the match play interface.
|
2017-08-28 20:14:32 -07:00
|
|
|
func (web *Web) buildMatchPlayList(matchType string) (MatchPlayList, error) {
|
|
|
|
|
matches, err := web.arena.Database.GetMatchesByType(matchType)
|
2014-06-24 22:51:10 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return MatchPlayList{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchPlayList := make(MatchPlayList, len(matches))
|
|
|
|
|
for i, match := range matches {
|
|
|
|
|
matchPlayList[i].Id = match.Id
|
2019-07-20 22:42:56 -07:00
|
|
|
matchPlayList[i].DisplayName = match.TypePrefix() + match.DisplayName
|
2014-08-21 00:12:49 -07:00
|
|
|
matchPlayList[i].Time = match.Time.Local().Format("3:04 PM")
|
2014-06-29 14:20:47 -07:00
|
|
|
matchPlayList[i].Status = match.Status
|
2020-03-29 00:04:15 -07:00
|
|
|
switch match.Status {
|
|
|
|
|
case model.RedWonMatch:
|
2015-05-31 19:31:10 -07:00
|
|
|
matchPlayList[i].ColorClass = "danger"
|
2020-03-29 00:04:15 -07:00
|
|
|
case model.BlueWonMatch:
|
2015-05-31 19:31:10 -07:00
|
|
|
matchPlayList[i].ColorClass = "info"
|
2020-03-29 00:04:15 -07:00
|
|
|
case model.TieMatch:
|
2015-05-31 19:31:10 -07:00
|
|
|
matchPlayList[i].ColorClass = "warning"
|
|
|
|
|
default:
|
|
|
|
|
matchPlayList[i].ColorClass = ""
|
|
|
|
|
}
|
2017-08-28 20:14:32 -07:00
|
|
|
if web.arena.CurrentMatch != nil && matchPlayList[i].Id == web.arena.CurrentMatch.Id {
|
2014-06-29 14:20:47 -07:00
|
|
|
matchPlayList[i].ColorClass = "success"
|
|
|
|
|
}
|
2014-06-24 22:51:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort the list to put all completed matches at the bottom.
|
|
|
|
|
sort.Stable(matchPlayList)
|
|
|
|
|
|
|
|
|
|
return matchPlayList, nil
|
|
|
|
|
}
|