From dd4044c6c931e4f2c30ac4fcc33ba05bc5f96941 Mon Sep 17 00:00:00 2001 From: Kyle Flynn Date: Wed, 25 Sep 2024 14:12:44 +0300 Subject: [PATCH 1/2] Initial fixes and event monitor --- back-end/api/sql/seasons/fgc_2024.sql | 3 +- .../src/api/events/match-update-event.ts | 19 +-- front-end/src/api/use-match-data.ts | 2 +- front-end/src/app-routes.tsx | 12 +- .../displays/seasons/fgc_2024/index.tsx | 7 +- .../src/apps/event-monitor/event-monitor.tsx | 117 ++++++++++++++++++ front-end/src/apps/event-monitor/index.tsx | 3 + .../seasons/fgc-2024/match-detail-info.tsx | 40 +++--- .../src/seasons/fgc-2024/referee/HRExtra.tsx | 33 +++++ .../fgc-2024/referee/TeleOpScoreSheet.tsx | 46 ------- .../src/seasons/fgc-2024/score-breakdowns.tsx | 8 +- front-end/src/stores/recoil/ui-settings.ts | 6 +- lib/models/src/seasons/FeedingTheFuture.ts | 26 ++-- 13 files changed, 207 insertions(+), 115 deletions(-) create mode 100644 front-end/src/apps/event-monitor/event-monitor.tsx create mode 100644 front-end/src/apps/event-monitor/index.tsx diff --git a/back-end/api/sql/seasons/fgc_2024.sql b/back-end/api/sql/seasons/fgc_2024.sql index 722296d8..7ecc25be 100644 --- a/back-end/api/sql/seasons/fgc_2024.sql +++ b/back-end/api/sql/seasons/fgc_2024.sql @@ -4,7 +4,6 @@ ALTER TABLE "ranking" ADD COLUMN foodSecuredPoints INT; ALTER TABLE "match_detail" ADD COLUMN redResevoirConserved; ALTER TABLE "match_detail" ADD COLUMN redFoodProduced; -ALTER TABLE "match_detail" ADD COLUMN redFoodSecured; ALTER TABLE "match_detail" ADD COLUMN redRobotOneParked; ALTER TABLE "match_detail" ADD COLUMN redRobotTwoParked; ALTER TABLE "match_detail" ADD COLUMN redRobotThreeParked; @@ -23,7 +22,6 @@ ALTER TABLE "match_detail" ADD COLUMN redEc6 INT; ALTER TABLE "match_detail" ADD COLUMN blueResevoirConserved; ALTER TABLE "match_detail" ADD COLUMN blueFoodProduced; -ALTER TABLE "match_detail" ADD COLUMN blueFoodSecured; ALTER TABLE "match_detail" ADD COLUMN blueRobotOneParked; ALTER TABLE "match_detail" ADD COLUMN blueRobotTwoParked; ALTER TABLE "match_detail" ADD COLUMN blueRobotThreeParked; @@ -42,3 +40,4 @@ ALTER TABLE "match_detail" ADD COLUMN blueEc6 INT; ALTER TABLE "match_detail" ADD COLUMN coopertition; ALTER TABLE "match_detail" ADD COLUMN fieldBalanced; +ALTER TABLE "match_detail" ADD COLUMN foodSecured; diff --git a/front-end/src/api/events/match-update-event.ts b/front-end/src/api/events/match-update-event.ts index ab71eb6e..dfca5764 100644 --- a/front-end/src/api/events/match-update-event.ts +++ b/front-end/src/api/events/match-update-event.ts @@ -1,18 +1,9 @@ -import { Match, MatchState } from '@toa-lib/models'; +import { Match } from '@toa-lib/models'; import { useRecoilCallback } from 'recoil'; -import { matchStateAtom, matchOccurringAtom } from 'src/stores/recoil'; +import { matchOccurringAtom } from 'src/stores/recoil'; export const useMatchUpdateEvent = () => { - return useRecoilCallback( - ({ snapshot, set }) => - async (newMatch: Match) => { - const state = await snapshot.getPromise(matchStateAtom); - if (state >= MatchState.MATCH_COMPLETE) { - // Don't update anything. - return; - } else { - set(matchOccurringAtom, newMatch); - } - } - ); + return useRecoilCallback(({ set }) => async (newMatch: Match) => { + set(matchOccurringAtom, newMatch); + }); }; diff --git a/front-end/src/api/use-match-data.ts b/front-end/src/api/use-match-data.ts index 201839d6..fcca2e17 100644 --- a/front-end/src/api/use-match-data.ts +++ b/front-end/src/api/use-match-data.ts @@ -77,7 +77,7 @@ export const deleteMatches = async ( ): Promise => apiFetcher(`match/${eventKey}/${tournamentKey}`, 'DELETE'); export const useMatchAll = ( - key?: MatchKey + key?: MatchKey | null ): SWRResponse, ApiResponseError> => useSWR>( key ? `match/all/${key.eventKey}/${key.tournamentKey}/${key.id}` : '', diff --git a/front-end/src/app-routes.tsx b/front-end/src/app-routes.tsx index 228c7b0f..a82b2cd3 100644 --- a/front-end/src/app-routes.tsx +++ b/front-end/src/app-routes.tsx @@ -96,7 +96,6 @@ const RefereeApp = lazy(() => const RedReferee = lazy(() => import('./apps/referee').then((m) => ({ default: m.RedReferee })) ); -0; const BlueReferee = lazy(() => import('./apps/referee').then((m) => ({ default: m.BlueReferee })) ); @@ -114,6 +113,11 @@ const AudienceDisplay = lazy(() => })) ); +// Misc routes +const EventMonitor = lazy(() => + import('./apps/event-monitor').then((m) => ({ default: m.EventMonitor })) +); + // Unised Routes // const AccountManager = lazy(() => import('./apps/AccountManager')); // const RefereeApp = lazy(() => import('./apps/Referee/Referee')); @@ -320,6 +324,12 @@ const AppRoutes: AppRoute[] = [ path: '/audience-display-manager', group: 0, element: AudienceDisplayManager + }, + { + name: 'Event Monitor', + path: '/event-monitor', + group: 0, + element: EventMonitor } // { // name: 'Account Manager', diff --git a/front-end/src/apps/audience-display/displays/seasons/fgc_2024/index.tsx b/front-end/src/apps/audience-display/displays/seasons/fgc_2024/index.tsx index 9a25a772..8c502e5d 100644 --- a/front-end/src/apps/audience-display/displays/seasons/fgc_2024/index.tsx +++ b/front-end/src/apps/audience-display/displays/seasons/fgc_2024/index.tsx @@ -102,12 +102,9 @@ export const Breakdown: ResultsBreakdown[] = [ icon: , title: 'Food Secured Points', color: '#000000', - resultCalc: (match, alliance) => { + resultCalc: (match) => { if (!match.details) return '0'; - const [redFoodSecured, blueFoodSecured] = getFoodSecuredPoints( - match.details - ); - const pts = alliance === 'red' ? redFoodSecured : blueFoodSecured; + const pts = getFoodSecuredPoints(match.details); return pts > 0 ? `+${pts}` : `${pts}`; } }, diff --git a/front-end/src/apps/event-monitor/event-monitor.tsx b/front-end/src/apps/event-monitor/event-monitor.tsx new file mode 100644 index 00000000..bd323da1 --- /dev/null +++ b/front-end/src/apps/event-monitor/event-monitor.tsx @@ -0,0 +1,117 @@ +import { Card, CardContent, CardHeader, Grid } from '@mui/material'; +import { FC, useEffect, useState } from 'react'; +import { DefaultLayout } from 'src/layouts/default-layout'; +import { MatchSocketEvent, MatchKey } from '@toa-lib/models'; +import { io } from 'socket.io-client'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import { useMatchAll } from 'src/api/use-match-data'; + +interface MonitorCardProps { + field: number; + url: string; +} + +const MonitorCard: FC = ({ field, url }) => { + const [connected, setConnected] = useState(false); + const [key, setKey] = useState(null); + const [status, setStatus] = useState('STANDBY'); + const { data: match } = useMatchAll(key); + useEffect(() => { + const socket = createSocket(); + socket.on('connect', handleConnect); + socket.on('disconnect', handleDisconnect); + socket.on(MatchSocketEvent.PRESTART, handlePrestart); + socket.on(MatchSocketEvent.START, handleStart); + socket.on(MatchSocketEvent.ABORT, handleAbort); + socket.on(MatchSocketEvent.END, handleEnd); + socket.on(MatchSocketEvent.COMMIT, handleCommit); + socket.connect(); + socket.emit('rooms', ['match']); + return () => { + socket.off(MatchSocketEvent.PRESTART, handlePrestart); + socket.off(MatchSocketEvent.START, handleStart); + socket.off(MatchSocketEvent.ABORT, handleAbort); + socket.off(MatchSocketEvent.END, handleEnd); + socket.off(MatchSocketEvent.COMMIT, handleCommit); + }; + }, []); + + const handleConnect = () => setConnected(true); + const handleDisconnect = () => setConnected(false); + + const handlePrestart = (key: MatchKey) => { + setKey(key); + setStatus('PRESTART'); + }; + const handleStart = () => { + setStatus('IN PROGRESS'); + }; + const handleAbort = () => { + setStatus('ABORTED'); + }; + const handleEnd = () => { + setStatus('COMPLETE'); + }; + const handleCommit = () => { + setStatus('COMMITTED'); + }; + + const createSocket = (autoConnect: boolean = false, token: string = '') => { + return io(`ws://${url}`, { + rejectUnauthorized: false, + transports: ['websocket'], + query: { token }, + autoConnect + }); + }; + return ( + + + ) : ( + + ) + } + /> + Event Monitor {connected} + + ); +}; + +export const EventMonitor: FC = () => { + return ( + + + + + + + + + + + + + + + +
HERE
+
+ + + +
+
+ ); +}; diff --git a/front-end/src/apps/event-monitor/index.tsx b/front-end/src/apps/event-monitor/index.tsx new file mode 100644 index 00000000..d83377d4 --- /dev/null +++ b/front-end/src/apps/event-monitor/index.tsx @@ -0,0 +1,3 @@ +import { EventMonitor } from './event-monitor'; + +export { EventMonitor }; diff --git a/front-end/src/seasons/fgc-2024/match-detail-info.tsx b/front-end/src/seasons/fgc-2024/match-detail-info.tsx index 918839f6..260d7693 100644 --- a/front-end/src/seasons/fgc-2024/match-detail-info.tsx +++ b/front-end/src/seasons/fgc-2024/match-detail-info.tsx @@ -40,7 +40,7 @@ export const MatchDetailInfo: FC< spacing={3} sx={{ border: '2px solid red', borderRadius: '1rem', mb: 4, p: 1 }} > - + - + - - - - + - + - - - {/* COOPERTITION */} - + Field Balanced} states={['N', 'Y']} @@ -230,6 +210,16 @@ export const MatchDetailInfo: FC< fullWidth /> + + + ); diff --git a/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx b/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx index c95edb9c..aa0bd2f4 100644 --- a/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx +++ b/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx @@ -101,6 +101,23 @@ const HeadRefereeExtra: React.FC = () => { ); }; + const handleFoodSecuredChange = ( + newValue: number, + manuallyTyped: boolean + ) => { + if (manuallyTyped) { + handleMatchDetailsUpdate('foodSecured', newValue); + } + }; + + const handleFoodSecuredDecrement = () => { + handleMatchDetailsUpdate('foodSecured', -1); + }; + + const handleFoodSecuredIncrement = () => { + handleMatchDetailsUpdate('foodSecured', 1); + }; + return ( { side='near' /> + + + Food Secured + + + ); diff --git a/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx b/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx index 5faeda00..0f2436fe 100644 --- a/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx +++ b/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx @@ -71,32 +71,6 @@ const TeleScoreSheet: FC = ({ ); }; - const handleFoodSecuredChange = ( - newValue: number, - manuallyTyped: boolean - ) => { - if (manuallyTyped) { - onMatchDetailsUpdate( - alliance === 'red' ? 'redFoodSecured' : 'blueFoodSecured', - newValue - ); - } - }; - - const handleFoodSecuredDecrement = () => { - onMatchDetailsAdjustment( - alliance === 'red' ? 'redFoodSecured' : 'blueFoodSecured', - -1 - ); - }; - - const handleFoodSecuredIncrement = () => { - onMatchDetailsAdjustment( - alliance === 'red' ? 'redFoodSecured' : 'blueFoodSecured', - 1 - ); - }; - const getBalanceStatus = (station: number): number | undefined => { switch (station) { case 11: @@ -183,26 +157,6 @@ const TeleScoreSheet: FC = ({ onDecrement={handleResevoirDecrement} /> - - - {alliance} Food Secured - - - ({ default: false, effects: [localStorageEffect('syncApiKey')] }); - +export const monitorAddressesAtom = atom({ + key: 'ui.monitorAddressesAtom', + default: [], + effects: [localStorageEffect('monitorAddresses')] +}); /** * @section UI STATE * Recoil state management for global UI interactions diff --git a/lib/models/src/seasons/FeedingTheFuture.ts b/lib/models/src/seasons/FeedingTheFuture.ts index 853d6aa0..22e35c4b 100644 --- a/lib/models/src/seasons/FeedingTheFuture.ts +++ b/lib/models/src/seasons/FeedingTheFuture.ts @@ -39,20 +39,19 @@ const functions: SeasonFunctions = { export interface MatchDetails extends MatchDetailBase { redResevoirConserved: number; redFoodProduced: number; - redFoodSecured: number; redRobotOneParked: number; redRobotTwoParked: number; redRobotThreeParked: number; redNexusState: AllianceNexusGoalState; blueResevoirConserved: number; blueFoodProduced: number; - blueFoodSecured: number; blueRobotOneParked: number; blueRobotTwoParked: number; blueRobotThreeParked: number; blueNexusState: AllianceNexusGoalState; coopertition: number; fieldBalanced: number; + foodSecured: number; } export enum NexusGoalState { @@ -98,20 +97,19 @@ export const defaultMatchDetails: MatchDetails = { tournamentKey: '', redResevoirConserved: 0, redFoodProduced: 0, - redFoodSecured: 0, redRobotOneParked: 0, redRobotTwoParked: 0, redRobotThreeParked: 0, redNexusState: { ...defaultNexusGoalState }, blueResevoirConserved: 0, blueFoodProduced: 0, - blueFoodSecured: 0, blueRobotOneParked: 0, blueRobotTwoParked: 0, blueRobotThreeParked: 0, blueNexusState: { ...defaultNexusGoalState }, coopertition: 0, - fieldBalanced: 1 + fieldBalanced: 1, + foodSecured: 0 }; export const isFeedingTheFutureDetails = (obj: unknown): obj is MatchDetails => @@ -264,7 +262,7 @@ function calculateRankings( if (participant.cardStatus <= CardStatus.YELLOW_CARD) { scoresMap.set(participant.teamKey, [...scores, match.redScore]); ranking.foodSecuredPoints += - match.details.redFoodSecured * ScoreTable.FoodSecured; + match.details.foodSecured * ScoreTable.FoodSecured; if (ranking.highestScore < match.redScore) { ranking.highestScore = match.redScore; } @@ -279,7 +277,7 @@ function calculateRankings( if (participant.cardStatus <= CardStatus.YELLOW_CARD) { scoresMap.set(participant.teamKey, [...scores, match.blueScore]); ranking.foodSecuredPoints += - match.details.blueFoodSecured * ScoreTable.FoodSecured; + match.details.foodSecured * ScoreTable.FoodSecured; if (ranking.highestScore < match.blueScore) { ranking.highestScore = match.blueScore; } @@ -436,18 +434,17 @@ export function calculateScore(match: Match): [number, number] { const [redResevoirPoints, blueResevoirPoints] = getResevoirPoints(details); const [redNexusPoints, blueNexusPoints] = getNexusPoints(details); const [redFoodProduced, blueFoodProduced] = getFoodProducedPoints(details); - const [redFoodSecuredPoints, blueFoodSecuredPoints] = - getFoodSecuredPoints(details); + const foodSecuredPoints = getFoodSecuredPoints(details); const coopertitionPoints = getCoopertitionPoints(details); const redScore = (redResevoirPoints + redNexusPoints + redFoodProduced) * ScoreTable.BalanceMultiplier(nBalanced) + - redFoodSecuredPoints + + foodSecuredPoints + coopertitionPoints; const blueScore = (blueResevoirPoints + blueNexusPoints + blueFoodProduced) * ScoreTable.BalanceMultiplier(nBalanced) + - blueFoodSecuredPoints + + foodSecuredPoints + coopertitionPoints; const redPenalty = Math.round(match.redMinPen * ScoreTable.Foul * redScore); const bluePenalty = Math.round( @@ -497,11 +494,8 @@ export function getFoodProducedPoints(details: MatchDetails): [number, number] { ]; } -export function getFoodSecuredPoints(details: MatchDetails): [number, number] { - return [ - details.redFoodSecured * ScoreTable.FoodSecured, - details.blueFoodSecured * ScoreTable.FoodSecured - ]; +export function getFoodSecuredPoints(details: MatchDetails): number { + return details.foodSecured * ScoreTable.FoodSecured; } export function getBalancedRobots(details: MatchDetails): number { From 8054ae4d5dfd13602f55cb8f027f2b43929d3b68 Mon Sep 17 00:00:00 2001 From: Kyle Flynn Date: Wed, 25 Sep 2024 14:57:44 +0300 Subject: [PATCH 2/2] More event stuff --- .../src/apps/event-monitor/event-monitor.tsx | 122 +++++++++++++++++- .../apps/scorekeeper/hooks/use-prestart.ts | 7 +- front-end/src/hooks/use-team-identifier.ts | 16 +++ 3 files changed, 139 insertions(+), 6 deletions(-) diff --git a/front-end/src/apps/event-monitor/event-monitor.tsx b/front-end/src/apps/event-monitor/event-monitor.tsx index bd323da1..ed5e181d 100644 --- a/front-end/src/apps/event-monitor/event-monitor.tsx +++ b/front-end/src/apps/event-monitor/event-monitor.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardHeader, Grid } from '@mui/material'; +import { Card, CardContent, CardHeader, Grid, Typography } from '@mui/material'; import { FC, useEffect, useState } from 'react'; import { DefaultLayout } from 'src/layouts/default-layout'; import { MatchSocketEvent, MatchKey } from '@toa-lib/models'; @@ -6,6 +6,7 @@ import { io } from 'socket.io-client'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import ErrorIcon from '@mui/icons-material/Error'; import { useMatchAll } from 'src/api/use-match-data'; +import { useTeamIdentifiersForEventKey } from 'src/hooks/use-team-identifier'; interface MonitorCardProps { field: number; @@ -17,6 +18,7 @@ const MonitorCard: FC = ({ field, url }) => { const [key, setKey] = useState(null); const [status, setStatus] = useState('STANDBY'); const { data: match } = useMatchAll(key); + const identifiers = useTeamIdentifiersForEventKey(key?.eventKey); useEffect(() => { const socket = createSocket(); socket.on('connect', handleConnect); @@ -84,7 +86,117 @@ const MonitorCard: FC = ({ field, url }) => { ) } /> - Event Monitor {connected} + + + + + + + {match && match.participants ? ( + <> + {' '} + {identifiers[match?.participants?.[0].teamKey]} + + ) : ( + '---' + )} + + + + + + {match && match.participants ? ( + <> + {identifiers[match?.participants?.[3].teamKey]}{' '} + + + ) : ( + '---' + )} + + + + + + {match && match.participants ? ( + <> + {' '} + {identifiers[match?.participants?.[1].teamKey]} + + ) : ( + '---' + )} + + + + vs. + + + + {match && match.participants ? ( + <> + {identifiers[match?.participants?.[4].teamKey]}{' '} + + + ) : ( + '---' + )} + + + + + + {match && match.participants ? ( + <> + {' '} + {identifiers[match?.participants?.[2].teamKey]} + + ) : ( + '---' + )} + + + + + + {match && match.participants ? ( + <> + {identifiers[match?.participants?.[5].teamKey]}{' '} + + + ) : ( + '---' + )} + + + + + + {match ? match.redScore : '--'} + + + + + + {match ? match.blueScore : '--'} + + + + + + ); }; @@ -93,9 +205,6 @@ export const EventMonitor: FC = () => { return ( - - - @@ -105,6 +214,9 @@ export const EventMonitor: FC = () => { + + +
HERE
diff --git a/front-end/src/apps/scorekeeper/hooks/use-prestart.ts b/front-end/src/apps/scorekeeper/hooks/use-prestart.ts index 21b5573f..f61c7f81 100644 --- a/front-end/src/apps/scorekeeper/hooks/use-prestart.ts +++ b/front-end/src/apps/scorekeeper/hooks/use-prestart.ts @@ -5,7 +5,11 @@ import { MatchSocketEvent, MatchState } from '@toa-lib/models'; -import { matchOccurringAtom, socketConnectedAtom } from 'src/stores/recoil'; +import { + currentTournamentKeyAtom, + matchOccurringAtom, + socketConnectedAtom +} from 'src/stores/recoil'; import { patchMatch, patchMatchParticipants } from 'src/api/use-match-data'; import { DateTime } from 'luxon'; import { once, sendPrestart, sendUpdate } from 'src/api/use-socket'; @@ -54,6 +58,7 @@ export const usePrestartCallback = () => { } }; set(matchOccurringAtom, currentMatch); + set(currentTournamentKeyAtom, currentMatch.tournamentKey); } fieldControl?.prestartField?.(); // Once we recieve the prestart response, immediately send update to load socket with match diff --git a/front-end/src/hooks/use-team-identifier.ts b/front-end/src/hooks/use-team-identifier.ts index bd3cf42c..aed37283 100644 --- a/front-end/src/hooks/use-team-identifier.ts +++ b/front-end/src/hooks/use-team-identifier.ts @@ -20,6 +20,22 @@ export const useTeamIdentifiers = (): Record => { ); }; +export const useTeamIdentifiersForEventKey = ( + eventKey: string | null | undefined +): Record => { + const identifier = useRecoilValue(teamIdentifierAtom); + const { data: teams } = useTeamsForEvent(eventKey); + return useMemo( + () => + teams + ? Object.fromEntries( + teams.map((t) => [t.teamKey, String(t[identifier])]) + ) + : {}, + [teams, identifier] + ); +}; + export const useTeamIdentifierRecord = ( teams: Team[] ): Record => {