import {Delete, OpenInNew} from "@mui/icons-material"; import { Button, Chip, Container, Divider, FormControl, Grid, InputLabel, MenuItem, Paper, Select, Slider, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography } from "@mui/material"; import {DataGrid} from "@mui/x-data-grid"; import * as Sentry from "@sentry/nextjs"; import dynamic from "next/dynamic"; import Image from "next/image"; import {useSnackbar} from "notistack"; import {useState} from "react"; import {useDispatch} from "react-redux"; import {BiasParams, CarSetupParams} from "../../consts/params"; import {AllPossibleSetups, FeedbackColorForMUI} from "../../consts/setup"; import {tracks} from "../../consts/tracks"; import {validateSetupArray} from "../../consts/validator"; import {updateSlot} from "../../libs/reducers/configReducer"; import {arrayFloatEqual, biasToSetup, eps, nearestSetup, randomSetup, setupToBias} from "../../libs/setup"; import {ClearFeedbackDialog} from "./ClearFeedbackDialog"; import {MuiOtpInput} from "mui-one-time-password-input"; import {HtmlTooltip} from "../Tooltip"; import styles from "./Calculator.module.css" const trackMap = {}; tracks.map(x => trackMap[x.id] = x); const shortAlphabet = "ogdb+-u12345 "; const feedbackShortMapping = { "optimal": "o", "great": "g", "good": "d", "bad": "b", "bad+": "+", "bad-": "-", "unknown": "u", } const feedbackShortUnmapping = { "o": "optimal", "g": "great", "d": "good", "b": "bad", "+": "bad+", "-": "bad-", "u": "unknown", "1": "optimal", "2": "great", "3": "good", "4": "bad", "5": "unknown", " ": "unknown", } export function Calculator({ slot }) { const { enqueueSnackbar } = useSnackbar(); const dispatch = useDispatch(); const [carSetupList, setCarSetupList] = useState([]); const [possibleSetups, setPossibleSetups] = useState(AllPossibleSetups); const [openClearFeedback, setOpenClearFeedback] = useState(false); const update = (payload) => dispatch(updateSlot({ id: slot.id, payload })); let { track, carSetup, prevCarSetup, feedback, previousRuns, } = slot; if ( slot.id && !( validateSetupArray(carSetup) && validateSetupArray(prevCarSetup) && feedback && track && previousRuns ) ) { update({ carSetup: [0.5, 0.5, 0.5, 0.5, 0.5], prevCarSetup: [0.5, 0.5, 0.5, 0.5, 0.5], feedback: [[], [], [], [], []], track: "XX", previousRuns: [], }); } const biasParam = setupToBias(carSetup); const prevBiasParam = setupToBias(prevCarSetup); const isValidSetup = CarSetupParams.map(p => { try { const val = carSetup ? carSetup[p.index] : 0.5; if (val < -1e-6 || val >= 1+1e-6) { return false; } const roundValue = val * (p.max - p.min) / p.step; return Math.abs(Math.round(roundValue) - roundValue) <= 1e-6; } catch { Sentry.captureMessage("invalid car setup found: " + JSON.stringify(carSetup)); update({ carSetup: [0.5, 0.5, 0.5, 0.5, 0.5], prevCarSetup: [0.5, 0.5, 0.5, 0.5, 0.5], feedback: [[], [], [], [], []], track: "XX", previousRuns: [], }); } }); const createFeedback = (idx, biasValue, v) => { const matchedRuns = previousRuns.filter(x => ( x.track === track && arrayFloatEqual(x.carSetup, carSetup) )) if (v === "unknown") { // setLastCarSetup(carSetup) update({ previousRuns: matchedRuns.length ? ( previousRuns.map(x => x.id === matchedRuns[0].id ? { ...x, ["feedback_" + idx]: null, } : x) ) : ([{ track, carSetup: JSON.parse(JSON.stringify(carSetup)), ["feedback_" + idx]: null, id: +new Date(), }, ...previousRuns]), feedback: feedback.map((x, i) => idx === i ? x.filter(x => x.value !== biasValue): x), }); } else { // setLastCarSetup(carSetup) update({ previousRuns: matchedRuns.length ? ( previousRuns.map(x => x.id === matchedRuns[0].id ? { ...x, ["feedback_" + idx]: { value: biasValue, timestamp: +new Date(), feedback: v }, } : x) ) : ([{ track, carSetup: JSON.parse(JSON.stringify(carSetup)), ["feedback_" + idx]: { value: biasValue, timestamp: +new Date(), feedback: v }, id: +new Date(), }, ...previousRuns]), feedback: feedback.map((x, i) => idx === i ? [ ...x.filter(x => x.value !== biasValue), { value: biasValue, timestamp: +new Date(), feedback: v } ]: x), }); } // if (v === "optimal" && Object.keys(preset).length) { // axios.post(`/api/report`, { // track, // value: biasValue, // feedback: v, // index: row.index, // }); // } } const currentTrack = trackMap[track]; const currentFeedbacks = [0, 1, 2, 3, 4].map(i => { for(const fb of feedback[i]) { if (fb.value === biasParam[i]) { return fb.feedback; } } return "unknown"; }) const currentShortFeedbacks = currentFeedbacks.map(x => feedbackShortMapping[x]) const setShortFeedbacks = (_val) => { const val = _val.toLowerCase(); [0, 1, 2, 3, 4].map(i => { if (val[i] && (currentShortFeedbacks[i] !== val[i])) { createFeedback(i, biasParam[i], feedbackShortUnmapping[val[i]]) } }) } const setCarSetup = (carSetup) => { update({ carSetup }); } const findNearest = () => { update({ prevCarSetup: carSetup, }); const {setup, possibleSetups, lowestRuleBreak, possibleSetupList} = nearestSetup( biasParam, feedback, [[0,1],[0,1],[0,1],[0,1],[0,1]], // currentTrack.perfectSetups, // , // [[0,1],[0,1],[0,1],[0,1],[0,1]], // currentTrack.perfectEffects, // , // ) if (setup) { if (lowestRuleBreak > 0) { enqueueSnackbar( 'Unable to find a valid setup matching all feedbacks. This is the closest we could get.', { variant: "warning" } ); setPossibleSetups(0); } else { setPossibleSetups(possibleSetups); } setCarSetup(setup); setCarSetupList(possibleSetupList); } else { enqueueSnackbar( 'Unable to find a valid setup matching all feedbacks. Try deleting some feedbacks.', { variant: "error" } ); } } const clearFeedbacks = () => { update({ feedback: [[], [], [], [], []] }); setPossibleSetups(AllPossibleSetups); } const loadPreset = () => setCarSetup(currentTrack.setup); return ( { clearFeedbacks(); loadPreset(); }} isOpen={openClearFeedback} setIsOpen={setOpenClearFeedback} /> Track Select: Setup Values Compare { CarSetupParams.map(row => { let carSetupDiff = carSetup[row.index] - prevCarSetup[row.index]; if (Math.abs(carSetupDiff) < eps) { carSetupDiff = 0; } const perfectRange = currentTrack.perfectSetups[row.index]; return ( {row.name} { currentTrack.perfectSetups[row.index][1] <= 1 && ( perfectRange[1] || carSetup[row.index] + eps < perfectRange[0] ? "#ffaa00": "#77ff77", fontSize: 14 }}> { row.render( perfectRange[0] * (row.max - row.min) + row.min ) }~{ row.render( perfectRange[1] * (row.max - row.min) + row.min ) } ) } 1 || carSetup[row.index] < 0) ? "error" : (isValidSetup[row.index] ? "primary" : "warning") } step={row.step / (row.max - row.min)} max={Math.max(1, carSetup[row.index])} min={Math.min(0, carSetup[row.index])} valueLabelFormat={v => row.render(v * (row.max - row.min) + row.min)} valueLabelDisplay="on" value={carSetup[row.index]} onChange={(e, value) => { const setup = carSetup.map((x, idx) => idx === row.index ? ( Math.round(value * 560) / 560 ): x); setCarSetup(setup) }} /> 0 ? "#ff6383" : carSetupDiff < 0 ? "#76ff03" : "white" }}>{carSetupDiff > 0 ? "▲" : carSetupDiff < 0 ? "▼" : ""} { row.render(carSetup[row.index] * (row.max - row.min) + row.min) } Prev: { row.render(prevCarSetup[row.index] * (row.max - row.min) + row.min) } ) }) }
{possibleSetups} Possibilities
{/* */}
Quick Input Use these shortcuts to input the feedbacks quicker.
shortmeaningnumber
ooptimal1
ggreat2
dgood3
bbad4
uunknown5
Additionally, +/- for bad(+)/(-). } > setShortFeedbacks(v)} validateChar={(ch) => { return shortAlphabet.indexOf(ch.toLowerCase()) !== -1; }} /> Feedback Bias Value { BiasParams.map(row => { let feedbacks = JSON.parse(JSON.stringify(feedback[row.index])); const biasValue = biasParam[row.index]; const k = row.name + ":" + slot.slotNaming; return [( {row.name} v.toFixed(6)} valueLabelDisplay="on" value={biasParam[row.index]} onChange={(e, value) => { const bias = biasParam.map((x, idx) => idx === row.index ? value : x); setCarSetup(biasToSetup(bias)) }} /> { const val = e.target.value; const nVal = Number(val); if (0 <= nVal && nVal <= 1) { const b = biasParam.map((x, idx) => idx === row.index ? nVal : x); setCarSetup(biasToSetup(b)) } } } /> ),( { feedbacks.sort( (x, y) => x.value - y.value ).map((f, _idx) => ( { const bias = biasParam.map((x, idx) => idx === row.index ? f.value : x); setCarSetup(biasToSetup(bias)) }} onDelete={() => { update({ feedback: feedback.map((x, idx) => idx === row.index ? x.filter(x => x.value !== f.value) : x ) }) }} /> )) } )]; }).flat() }
{ const biasParams = setupToBias(x.arr); return {...x, biasParams, id: id + 1} }) ]} columns={[ { field: 'id', headerName: 'Setup #', renderCell : ({ row, value }) => }, { field: 'diff', headerName: '%', valueGetter: ({ value }) => value.toFixed(1) + "%", }, ...CarSetupParams.map(param => { const idx = param.index; return { field: 'arr_' + idx, headerName: param.name, valueGetter: ({ row }) => row.arr ? row.arr[idx] : 0, renderCell: ({ row }) => { const value = row.arr ? row.arr[idx] : 0; const carSetupDiff = value - (prevCarSetup ? prevCarSetup[idx] : 0); return 0 ? "#ff6383" : carSetupDiff < 0 ? "#76ff03" : "white" }} > {carSetupDiff > 0 ? "▲" : carSetupDiff < 0 ? "▼" : ""} { param.render(value * (param.max - param.min) + param.min) } }, } }), { field: 'arr', headerName: '⇒', width: 8, renderCell: () => "⇒", }, ...BiasParams.map(param => { const idx = param.index; return { field: 'biasArr_' + idx, headerName: param.name, valueGetter: ({ row }) => row.biasParams ? row.biasParams[idx] : 0, renderCell: ({ row }) => { const value = row.biasParams ? row.biasParams[idx] : 0; const carSetupDiff = value - (prevBiasParam ? prevBiasParam[idx] : 0); return 0 ? "#ff6383" : carSetupDiff < 0 ? "#76ff03" : "white" }} > {value.toFixed(4)} {carSetupDiff > 0 ? "▲" : carSetupDiff < 0 ? "▼" : ""} }, } }) ]} density="compact" initialState={{ pagination: { paginationModel: { pageSize: 20 } }, }} pageSizeOptions={[20, 50, 100]} />
Previous Runs FWA RWA ARD TC TO Oversteer Braking Cornering Traction Straights { previousRuns.map(x => { return ( {trackMap[x.track]?.name} { [0, 1, 2, 3, 4].map(idx => ( { CarSetupParams[idx].render( x.carSetup[idx] * ( CarSetupParams[idx].max - CarSetupParams[idx].min ) + CarSetupParams[idx].min ) } )) } { [0, 1, 2, 3, 4].map(idx => ( { x["feedback_" + idx] && ( ) } )) } ) }) }
) } export default dynamic(() => Promise.resolve(Calculator), { ssr: false, });