Exemple #1
0
        private Boolean opponentIsRacing(float currentOpponentX, float currentOpponentY, float previousOpponentX, float previousOpponentY,
                                         pCarsAPIParticipantStruct playerData, pCarsAPIParticipantStruct previousPlayerData, float interval)
        {
            float deltaX = Math.Abs(currentOpponentX - playerData.mWorldPosition[0]);
            float deltaY = Math.Abs(currentOpponentY - playerData.mWorldPosition[2]);

            if (deltaX > trackWidth || deltaY > trackWidth)
            {
                return(false);
            }
            float opponentVelocityX = Math.Abs(currentOpponentX - previousOpponentX) / interval;
            float opponentVelocityY = Math.Abs(currentOpponentY - previousOpponentY) / interval;

            // hard code this - if the opponent car is going < 4m/s on both axis we're not interested
            if (opponentVelocityX < 4 && opponentVelocityY < 4)
            {
                return(false);
            }

            float playerVelocityX = Math.Abs(playerData.mWorldPosition[0] - previousPlayerData.mWorldPosition[0]) / interval;
            float playerVelocityY = Math.Abs(playerData.mWorldPosition[2] - previousPlayerData.mWorldPosition[2]) / interval;

            if (Math.Abs(playerVelocityX - opponentVelocityX) > maxClosingSpeed || Math.Abs(playerVelocityY - opponentVelocityY) > maxClosingSpeed)
            {
                // Console.WriteLine("high closing speed: x = " + (playerVelocityX - opponentVelocityX) + " y = " + (playerVelocityY - opponentVelocityY));
                return(false);
            }
            return(true);
        }
        public void trigger(Object lastStateObj, Object currentStateObj)
        {
            if (paused)
            {
                return;
            }
            CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper currentWrapper = (CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper)currentStateObj;
            pCarsAPIStruct currentState = currentWrapper.data;

            // game state is 3 for paused, 5 for replay. No idea what 4 is...
            if (currentState.mGameState == (uint)eGameState.GAME_FRONT_END ||
                (currentState.mGameState == (uint)eGameState.GAME_INGAME_PAUSED && !System.Diagnostics.Debugger.IsAttached) ||
                currentState.mGameState == (uint)eGameState.GAME_VIEWING_REPLAY || currentState.mGameState == (uint)eGameState.GAME_EXITED)
            {
                // don't ignore the paused game updates if we're in debug mode
                return;
            }
            CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper previousWrapper = (CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper)lastStateObj;
            pCarsAPIStruct lastState = previousWrapper.data;

            DateTime now = new DateTime(currentWrapper.ticksWhenRead);
            float interval = (float)(((double)currentWrapper.ticksWhenRead - (double)previousWrapper.ticksWhenRead) / (double)TimeSpan.TicksPerSecond);
            if (currentState.mRaceState == (int)eRaceState.RACESTATE_RACING &&
                lastState.mRaceState != (int)eRaceState.RACESTATE_RACING)
            {
                timeToStartSpotting = now.Add(TimeSpan.FromSeconds(timeAfterRaceStartToActivate));
            }
            // this check looks a bit funky... whe we start a practice session, the raceState is not_started
            // until we cross the line for the first time. Which is retarded really.
            if (currentState.mRaceState == (int)eRaceState.RACESTATE_INVALID || now < timeToStartSpotting ||
                (currentState.mSessionState == (int)eSessionState.SESSION_RACE && currentState.mRaceState == (int) eRaceState.RACESTATE_NOT_STARTED))
            {
                return;
            }

            if (enabled && currentState.mNumParticipants > 1 &&
                (enableSpotterInTimetrial || currentState.mSessionState != (uint)eSessionState.SESSION_TIME_ATTACK))
            {
                Tuple<int, pCarsAPIParticipantStruct> playerDataWithIndex = PCarsGameStateMapper.getPlayerDataStruct(currentState.mParticipantData, currentState.mViewedParticipantIndex);
                int playerIndex = playerDataWithIndex.Item1;
                pCarsAPIParticipantStruct playerData = playerDataWithIndex.Item2;
                float[] currentPlayerPosition = new float[] { playerData.mWorldPosition[0], playerData.mWorldPosition[2] };

                if (currentState.mPitMode == (uint)ePitMode.PIT_MODE_NONE)
                {
                    List<float[]> currentOpponentPositions = new List<float[]>();
                    float[] playerVelocityData = new float[3];
                    playerVelocityData[0] = currentState.mSpeed;
                    playerVelocityData[1] = currentState.mWorldVelocity[0];
                    playerVelocityData[2] = currentState.mWorldVelocity[2];

                    for (int i = 0; i < currentState.mParticipantData.Count(); i++)
                    {
                        if (i == playerIndex)
                        {
                            continue;
                        }
                        pCarsAPIParticipantStruct opponentData = currentState.mParticipantData[i];
                        if (opponentData.mIsActive)
                        {
                            float[] currentPositions = new float[] { opponentData.mWorldPosition[0], opponentData.mWorldPosition[2] };
                            currentOpponentPositions.Add(currentPositions);
                        }
                    }
                    if (currentOpponentPositions.Count() > 0)
                    {
                        float playerRotation = currentState.mOrientation[1];
                        if (playerRotation < 0)
                        {
                            playerRotation = (float)(2 * Math.PI) + playerRotation;
                        }
                        playerRotation = (float)(2 * Math.PI) - playerRotation;
                        internalSpotter.triggerInternal(playerRotation, currentPlayerPosition, playerVelocityData, currentOpponentPositions);
                    }
                }
            }
        }
Exemple #3
0
        public static pCarsAPIStruct MergeWithExistingState(pCarsAPIStruct existingState, sTelemetryData udpTelemetryData)
        {
            if (existingState.isSameClassAsPlayer == null)
            {
                existingState.isSameClassAsPlayer = new Boolean[(int)eAPIStructLengths.NUM_PARTICIPANTS];
            }
            existingState.hasOpponentClassData = false;
            existingState.hasNewPositionData   = false;
            existingState.mGameState           = (uint)udpTelemetryData.sGameSessionState & 7;
            existingState.mSessionState        = (uint)udpTelemetryData.sGameSessionState >> 4;
            existingState.mRaceState           = (uint)udpTelemetryData.sRaceStateFlags & 7;

            // Participant Info
            existingState.mViewedParticipantIndex = udpTelemetryData.sViewedParticipantIndex;
            existingState.mNumParticipants        = udpTelemetryData.sNumParticipants;

            // Unfiltered Input
            existingState.mUnfilteredThrottle = (float)udpTelemetryData.sUnfilteredThrottle / 255f;
            existingState.mUnfilteredBrake    = (float)udpTelemetryData.sUnfilteredBrake / 255f;
            existingState.mUnfilteredSteering = (float)udpTelemetryData.sUnfilteredSteering / 127f;
            existingState.mUnfilteredClutch   = (float)udpTelemetryData.sUnfilteredClutch / 255f;

            existingState.mLapsInEvent = udpTelemetryData.sLapsInEvent;
            existingState.mTrackLength = udpTelemetryData.sTrackLength;

            // Timing & Scoring
            existingState.mLapInvalidated             = (udpTelemetryData.sRaceStateFlags >> 3 & 1) == 1;
            existingState.mSessionFastestLapTime      = udpTelemetryData.sBestLapTime;
            existingState.mLastLapTime                = udpTelemetryData.sLastLapTime;
            existingState.mCurrentTime                = udpTelemetryData.sCurrentTime;
            existingState.mSplitTimeAhead             = udpTelemetryData.sSplitTimeAhead;
            existingState.mSplitTimeBehind            = udpTelemetryData.sSplitTimeBehind;
            existingState.mSplitTime                  = udpTelemetryData.sSplitTime;
            existingState.mEventTimeRemaining         = udpTelemetryData.sEventTimeRemaining;
            existingState.mPersonalFastestLapTime     = udpTelemetryData.sPersonalFastestLapTime;
            existingState.mWorldFastestLapTime        = udpTelemetryData.sWorldFastestLapTime;
            existingState.mCurrentSector1Time         = udpTelemetryData.sCurrentSector1Time;
            existingState.mCurrentSector2Time         = udpTelemetryData.sCurrentSector2Time;
            existingState.mCurrentSector3Time         = udpTelemetryData.sCurrentSector3Time;
            existingState.mSessionFastestSector1Time  = udpTelemetryData.sFastestSector1Time;
            existingState.mSessionFastestSector2Time  = udpTelemetryData.sFastestSector2Time;
            existingState.mSessionFastestSector3Time  = udpTelemetryData.sFastestSector3Time;
            existingState.mPersonalFastestSector1Time = udpTelemetryData.sPersonalFastestSector1Time;
            existingState.mPersonalFastestSector2Time = udpTelemetryData.sPersonalFastestSector2Time;
            existingState.mPersonalFastestSector3Time = udpTelemetryData.sPersonalFastestSector3Time;
            existingState.mWorldFastestSector1Time    = udpTelemetryData.sWorldFastestSector1Time;
            existingState.mWorldFastestSector2Time    = udpTelemetryData.sWorldFastestSector2Time;
            existingState.mWorldFastestSector3Time    = udpTelemetryData.sWorldFastestSector3Time;

            // Flags
            existingState.mHighestFlagColour = (uint)udpTelemetryData.sHighestFlag & 7;
            existingState.mHighestFlagReason = (uint)udpTelemetryData.sHighestFlag >> 3 & 3;

            // Pit Info
            existingState.mPitMode     = (uint)udpTelemetryData.sPitModeSchedule & 7;
            existingState.mPitSchedule = (uint)udpTelemetryData.sPitModeSchedule >> 3 & 3;

            // Car State
            existingState.mCarFlags         = udpTelemetryData.sCarFlags;
            existingState.mOilTempCelsius   = udpTelemetryData.sOilTempCelsius;
            existingState.mOilPressureKPa   = udpTelemetryData.sOilPressureKPa;
            existingState.mWaterTempCelsius = udpTelemetryData.sWaterTempCelsius;
            existingState.mWaterPressureKPa = udpTelemetryData.sWaterPressureKpa;
            existingState.mFuelPressureKPa  = udpTelemetryData.sFuelPressureKpa;
            existingState.mFuelLevel        = udpTelemetryData.sFuelLevel;
            existingState.mFuelCapacity     = udpTelemetryData.sFuelCapacity;
            existingState.mSpeed            = udpTelemetryData.sSpeed;
            existingState.mRPM            = udpTelemetryData.sRpm;
            existingState.mMaxRPM         = udpTelemetryData.sMaxRpm;
            existingState.mBrake          = (float)udpTelemetryData.sBrake / 255f;
            existingState.mThrottle       = (float)udpTelemetryData.sThrottle / 255f;
            existingState.mClutch         = (float)udpTelemetryData.sClutch / 255f;
            existingState.mSteering       = (float)udpTelemetryData.sSteering / 127f;
            existingState.mGear           = udpTelemetryData.sGearNumGears & 15;
            existingState.mNumGears       = udpTelemetryData.sGearNumGears >> 4;
            existingState.mOdometerKM     = udpTelemetryData.sOdometerKM;
            existingState.mAntiLockActive = (udpTelemetryData.sRaceStateFlags >> 4 & 1) == 1;
            existingState.mBoostActive    = (udpTelemetryData.sRaceStateFlags >> 5 & 1) == 1;
            existingState.mBoostAmount    = udpTelemetryData.sBoostAmount;

            // Motion & Device Related
            existingState.mOrientation       = udpTelemetryData.sOrientation;
            existingState.mLocalVelocity     = udpTelemetryData.sLocalVelocity;
            existingState.mWorldVelocity     = udpTelemetryData.sWorldVelocity;
            existingState.mAngularVelocity   = udpTelemetryData.sAngularVelocity;
            existingState.mLocalAcceleration = udpTelemetryData.sLocalAcceleration;
            existingState.mWorldAcceleration = udpTelemetryData.sWorldAcceleration;
            existingState.mExtentsCentre     = udpTelemetryData.sExtentsCentre;


            existingState.mTyreFlags             = toUIntArray(udpTelemetryData.sTyreFlags);
            existingState.mTerrain               = toUIntArray(udpTelemetryData.sTerrain);
            existingState.mTyreY                 = udpTelemetryData.sTyreY;
            existingState.mTyreRPS               = udpTelemetryData.sTyreRPS;
            existingState.mTyreSlipSpeed         = udpTelemetryData.sTyreSlipSpeed;
            existingState.mTyreTemp              = toFloatArray(udpTelemetryData.sTyreTemp, 255);
            existingState.mTyreGrip              = toFloatArray(udpTelemetryData.sTyreGrip, 255);
            existingState.mTyreHeightAboveGround = udpTelemetryData.sTyreHeightAboveGround;
            existingState.mTyreLateralStiffness  = udpTelemetryData.sTyreLateralStiffness;
            existingState.mTyreWear              = toFloatArray(udpTelemetryData.sTyreWear, 255);
            existingState.mBrakeDamage           = toFloatArray(udpTelemetryData.sBrakeDamage, 255);
            existingState.mSuspensionDamage      = toFloatArray(udpTelemetryData.sSuspensionDamage, 255);
            existingState.mBrakeTempCelsius      = toFloatArray(udpTelemetryData.sBrakeTempCelsius, 1);
            existingState.mTyreTreadTemp         = toFloatArray(udpTelemetryData.sTyreTreadTemp, 1);
            existingState.mTyreLayerTemp         = toFloatArray(udpTelemetryData.sTyreLayerTemp, 1);
            existingState.mTyreCarcassTemp       = toFloatArray(udpTelemetryData.sTyreCarcassTemp, 1);
            existingState.mTyreRimTemp           = toFloatArray(udpTelemetryData.sTyreRimTemp, 1);
            existingState.mTyreInternalAirTemp   = toFloatArray(udpTelemetryData.sTyreInternalAirTemp, 1);
            existingState.mWheelLocalPosition    = udpTelemetryData.sWheelLocalPositionY;
            existingState.mRideHeight            = udpTelemetryData.sRideHeight;
            existingState.mSuspensionTravel      = udpTelemetryData.sSuspensionTravel;
            existingState.mSuspensionVelocity    = udpTelemetryData.sSuspensionVelocity;
            existingState.mAirPressure           = toFloatArray(udpTelemetryData.sAirPressure, 1);

            existingState.mEngineSpeed        = udpTelemetryData.sEngineSpeed;
            existingState.mEngineTorque       = udpTelemetryData.sEngineTorque;
            existingState.mEnforcedPitStopLap = udpTelemetryData.sEnforcedPitStopLap;

            // Car Damage
            existingState.mCrashState   = udpTelemetryData.sCrashState;
            existingState.mAeroDamage   = (float)udpTelemetryData.sAeroDamage / 255f;
            existingState.mEngineDamage = (float)udpTelemetryData.sEngineDamage / 255f;

            // Weather
            existingState.mAmbientTemperature = udpTelemetryData.sAmbientTemperature;
            existingState.mTrackTemperature   = udpTelemetryData.sTrackTemperature;
            existingState.mRainDensity        = (float)udpTelemetryData.sRainDensity / 255f;
            existingState.mWindSpeed          = udpTelemetryData.sWindSpeed * 2;
            existingState.mWindDirectionX     = (float)udpTelemetryData.sWindDirectionX / 127f;
            existingState.mWindDirectionY     = (float)udpTelemetryData.sWindDirectionY / 127f;
            //existingState.mCloudBrightness = udpTelemetryData.sCloudBrightness / 255;

            if (existingState.mParticipantData == null)
            {
                existingState.mParticipantData = new pCarsAPIParticipantStruct[56];
            }

            if (existingState.mLastSectorData == null)
            {
                existingState.mLastSectorData = new float[56];
            }

            if (existingState.mLapInvalidatedData == null)
            {
                existingState.mLapInvalidatedData = new Boolean[56];
            }
            for (int i = 0; i < udpTelemetryData.sParticipantInfo.Count(); i++)
            {
                sParticipantInfo          newPartInfo      = udpTelemetryData.sParticipantInfo[i];
                Boolean                   isActive         = (newPartInfo.sRacePosition >> 7) == 1;
                pCarsAPIParticipantStruct existingPartInfo = existingState.mParticipantData[i];

                if (isActive)
                {
                    existingPartInfo.mIsActive           = i < existingState.mNumParticipants;
                    existingPartInfo.mCurrentLap         = newPartInfo.sCurrentLap;
                    existingPartInfo.mCurrentLapDistance = newPartInfo.sCurrentLapDistance;
                    existingPartInfo.mLapsCompleted      = (uint)newPartInfo.sLapsCompleted & 127;
                    Boolean lapInvalidated = (newPartInfo.sLapsCompleted >> 7) == 1;
                    existingPartInfo.mRacePosition  = (uint)newPartInfo.sRacePosition & 127;
                    existingPartInfo.mCurrentSector = (uint)newPartInfo.sSector & 7;
                    Boolean sameClassAsPlayer = (newPartInfo.sSector >> 3 & 1) == 1;
                    if (sameClassAsPlayer)
                    {
                        existingState.hasOpponentClassData = true;
                    }
                    existingState.isSameClassAsPlayer[i] = sameClassAsPlayer;


                    // and now the bit magic for the extra position precision...
                    float[] newWorldPositions = toFloatArray(newPartInfo.sWorldPosition, 1);
                    float   xAdjustment       = ((float)((uint)newPartInfo.sSector >> 6 & 3)) / 4f;
                    float   zAdjustment       = ((float)((uint)newPartInfo.sSector >> 4 & 3)) / 4f;

                    newWorldPositions[0] = newWorldPositions[0] + xAdjustment;
                    newWorldPositions[2] = newWorldPositions[2] + zAdjustment;
                    if (!existingState.hasNewPositionData && i != udpTelemetryData.sViewedParticipantIndex &&
                        (existingPartInfo.mWorldPosition == null || (newWorldPositions[0] != existingPartInfo.mWorldPosition[0] || newWorldPositions[2] != existingPartInfo.mWorldPosition[2])))
                    {
                        existingState.hasNewPositionData = true;
                    }
                    existingPartInfo.mWorldPosition = newWorldPositions;

                    // LastSectorTime is now in the UDP data, but there's no slot for this in the participants struct
                    // so bung it in a separate array at the end
                    existingState.mLastSectorData[i]     = newPartInfo.sLastSectorTime;
                    existingState.mLapInvalidatedData[i] = lapInvalidated;
                }
                else
                {
                    existingPartInfo.mWorldPosition = new float[] { 0, 0, 0 };
                    existingPartInfo.mIsActive      = false;
                }
                existingState.mParticipantData[i] = existingPartInfo;
            }

            return(existingState);
        }
Exemple #4
0
        public void trigger(Object lastStateObj, Object currentStateObj)
        {
            if (paused)
            {
                audioPlayer.closeChannel();
                return;
            }
            CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper currentWrapper = (CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper)currentStateObj;
            pCarsAPIStruct currentState = currentWrapper.data;

            // game state is 3 for paused, 5 for replay. No idea what 4 is...
            if (currentState.mGameState == (uint)eGameState.GAME_FRONT_END ||
                (currentState.mGameState == (uint)eGameState.GAME_INGAME_PAUSED && !System.Diagnostics.Debugger.IsAttached) ||
                currentState.mGameState == (uint)eGameState.GAME_VIEWING_REPLAY || currentState.mGameState == (uint)eGameState.GAME_EXITED)
            {
                // don't ignore the paused game updates if we're in debug mode
                audioPlayer.closeChannel();
                return;
            }
            CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper previousWrapper = (CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper)lastStateObj;
            pCarsAPIStruct lastState = previousWrapper.data;

            DateTime now      = new DateTime(currentWrapper.ticksWhenRead);
            float    interval = (float)(((double)currentWrapper.ticksWhenRead - (double)previousWrapper.ticksWhenRead) / (double)TimeSpan.TicksPerSecond);

            if (currentState.mRaceState == (int)eRaceState.RACESTATE_RACING &&
                lastState.mRaceState != (int)eRaceState.RACESTATE_RACING)
            {
                timeToStartSpotting = now.Add(TimeSpan.FromSeconds(timeAfterRaceStartToActivate));
            }
            // this check looks a bit funky... whe we start a practice session, the raceState is not_started
            // until we cross the line for the first time. Which is retarded really.
            if (currentState.mRaceState == (int)eRaceState.RACESTATE_INVALID || now < timeToStartSpotting ||
                (currentState.mSessionState == (int)eSessionState.SESSION_RACE && currentState.mRaceState == (int)eRaceState.RACESTATE_NOT_STARTED))
            {
                return;
            }

            float currentSpeed  = currentState.mSpeed;
            float previousSpeed = lastState.mSpeed;

            if (enabled && currentState.mParticipantData.Count() > 1)
            {
                Tuple <int, pCarsAPIParticipantStruct> playerDataWithIndex = PCarsGameStateMapper.getPlayerDataStruct(currentState.mParticipantData, currentState.mViewedParticipantIndex);
                int playerIndex = playerDataWithIndex.Item1;
                pCarsAPIParticipantStruct playerData = playerDataWithIndex.Item2;
                float playerX = playerData.mWorldPosition[0];
                float playerY = playerData.mWorldPosition[2];
                if (playerX == 0 || playerY == 0 || playerX == -1 || playerY == -1 ||
                    lastState.mParticipantData == null || lastState.mParticipantData.Length == 0 || lastState.mViewedParticipantIndex < 0)
                {
                    return;
                }
                Tuple <int, pCarsAPIParticipantStruct> previousPlayerDataWithIndex = PCarsGameStateMapper.getPlayerDataStruct(lastState.mParticipantData, lastState.mViewedParticipantIndex);
                pCarsAPIParticipantStruct previousPlayerData = previousPlayerDataWithIndex.Item2;

                if (currentSpeed > minSpeedForSpotterToOperate && currentState.mPitMode == (uint)ePitMode.PIT_MODE_NONE)
                {
                    int carsOnLeft  = 0;
                    int carsOnRight = 0;
                    for (int i = 0; i < currentState.mParticipantData.Count(); i++)
                    {
                        if (i == playerIndex)
                        {
                            continue;
                        }
                        if (carsOnLeft >= 1 && carsOnRight >= 1)
                        {
                            // stop processing - we already know there's a car on both sides
                            break;
                        }

                        pCarsAPIParticipantStruct opponentData = currentState.mParticipantData[i];

                        float previousOpponentX = 0;
                        float previousOpponentY = 0;
                        try
                        {
                            pCarsAPIParticipantStruct previousOpponentData = PCarsGameStateMapper.getParticipantDataForName(lastState.mParticipantData, opponentData.mName, i);
                            previousOpponentX = previousOpponentData.mWorldPosition[0];
                            previousOpponentY = previousOpponentData.mWorldPosition[2];
                        }
                        catch (Exception)
                        {
                            // ignore - the mParticipantData array is frequently full of crap
                        }
                        float currentOpponentX = opponentData.mWorldPosition[0];
                        float currentOpponentY = opponentData.mWorldPosition[2];

                        if (opponentData.mIsActive)
                        {
                            if (currentOpponentX != 0 && currentOpponentY != 0 &&
                                currentOpponentX != -1 && currentOpponentY != -1 &&
                                previousOpponentX != 0 && previousOpponentY != 0 &&
                                previousOpponentX != -1 && previousOpponentY != -1 &&
                                opponentIsRacing(currentOpponentX, currentOpponentY, previousOpponentX, previousOpponentY, playerData, previousPlayerData, interval))
                            {
                                Side side = getSide(currentState.mOrientation[1], playerX, playerY, currentOpponentX, currentOpponentY);
                                if (side == Side.left)
                                {
                                    carsOnLeft++;
                                    if (lastKnownOpponentState.ContainsKey(opponentData.mName))
                                    {
                                        lastKnownOpponentState[opponentData.mName] = Side.left;
                                    }
                                    else
                                    {
                                        lastKnownOpponentState.Add(opponentData.mName, Side.left);
                                    }
                                }
                                else if (side == Side.right)
                                {
                                    carsOnRight++;
                                    if (lastKnownOpponentState.ContainsKey(opponentData.mName))
                                    {
                                        lastKnownOpponentState[opponentData.mName] = Side.right;
                                    }
                                    else
                                    {
                                        lastKnownOpponentState.Add(opponentData.mName, Side.right);
                                    }
                                }
                                else
                                {
                                    if (lastKnownOpponentState.ContainsKey(opponentData.mName))
                                    {
                                        lastKnownOpponentState[opponentData.mName] = Side.none;
                                    }
                                    else
                                    {
                                        lastKnownOpponentState.Add(opponentData.mName, Side.none);
                                    }
                                }
                            }
                            else
                            {
                                // no usable position data, use the last known state
                                if (lastKnownOpponentState.ContainsKey(opponentData.mName))
                                {
                                    int lastStateUseCount = 1;
                                    if (lastKnownOpponentStateUseCounter.ContainsKey(opponentData.mName))
                                    {
                                        lastStateUseCount = lastKnownOpponentStateUseCounter[opponentData.mName] + 1;
                                    }
                                    else
                                    {
                                        lastKnownOpponentStateUseCounter.Add(opponentData.mName, 0);
                                    }
                                    if (lastStateUseCount < maxSavedStateReuse)
                                    {
                                        lastKnownOpponentStateUseCounter[opponentData.mName] = lastStateUseCount;
                                        if (lastKnownOpponentState[opponentData.mName] == Side.left)
                                        {
                                            carsOnLeft++;
                                        }
                                        else if (lastKnownOpponentState[opponentData.mName] == Side.right)
                                        {
                                            carsOnRight++;
                                        }
                                    }
                                    else
                                    {
                                        // we've used too many saved states for this missing opponent position
                                        lastKnownOpponentState.Remove(opponentData.mName);
                                        lastKnownOpponentStateUseCounter.Remove(opponentData.mName);
                                    }
                                }
                            }
                        }
                    }
                    getNextMessage(carsOnLeft, carsOnRight, now);
                    playNextMessage(carsOnLeft, carsOnRight, now);
                    hasCarLeft  = carsOnLeft > 0;
                    hasCarRight = carsOnRight > 0;
                }
                else if (hasCarLeft || hasCarRight)
                {
                    if (!channelLeftOpenTimerStarted)
                    {
                        timeWhenChannelShouldBeClosed = now.Add(timeToWaitBeforeClosingChannelLeftOpen);
                        channelLeftOpenTimerStarted   = true;
                    }
                    if (now > timeWhenChannelShouldBeClosed)
                    {
                        Console.WriteLine("Closing channel left open in spotter");
                        timeWhenChannelShouldBeClosed = DateTime.MaxValue;
                        hasCarLeft  = false;
                        hasCarRight = false;
                        audioPlayer.closeChannel();
                        channelLeftOpenTimerStarted = false;
                    }
                }
            }
        }