public override Object ReadGameData(Boolean forSpotter) { CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper structWrapper = new CrewChiefV4.PCars.PCarsSharedMemoryReader.PCarsStructWrapper(); structWrapper.ticksWhenRead = DateTime.UtcNow.Ticks; lock (this) { if (!initialised) { if (!InitialiseInternal()) { throw new GameDataReadException("Failed to initialise UDP client"); } } previousGameState = StructHelper.Clone(currentGameState); currentGameState = StructHelper.Clone(workingGameState); if (forSpotter) { newSpotterData = false; } } structWrapper.data = currentGameState; if (!forSpotter && dumpToFile && dataToDump != null && currentGameState.mTrackLocation != null && currentGameState.mTrackLocation.Length > 0) { dataToDump.Add(structWrapper); } return(structWrapper); }
public static pCarsAPIStruct MergeWithExistingState(pCarsAPIStruct existingState, sParticipantInfoStringsAdditional udpAdditionalStrings) { int offset = udpAdditionalStrings.sOffset; if (existingState.mParticipantData == null) { existingState.mParticipantData = new pCarsAPIParticipantStruct[56]; } for (int i = offset; i < offset + 16 && i < existingState.mParticipantData.Length; i++) { existingState.mParticipantData[i].mName = udpAdditionalStrings.sName[i - offset].nameByteArray; } return(existingState); }
public static pCarsAPIStruct MergeWithExistingState(pCarsAPIStruct existingState, sParticipantInfoStrings udpParticipantStrings) { existingState.mCarClassName = udpParticipantStrings.sCarClassName; existingState.mCarName = udpParticipantStrings.sCarName; existingState.mTrackLocation = udpParticipantStrings.sTrackLocation; existingState.mTrackVariation = udpParticipantStrings.sTrackVariation; if (existingState.mParticipantData == null) { existingState.mParticipantData = new pCarsAPIParticipantStruct[56]; } for (int i = 0; i < udpParticipantStrings.sName.Count(); i++) { existingState.mParticipantData[i].mName = udpParticipantStrings.sName[i].nameByteArray; } return(existingState); }
public override Object ReadGameData(Boolean forSpotter) { lock (this) { pCarsAPIStruct _pcarsapistruct = new pCarsAPIStruct(); if (!initialised) { if (!InitialiseInternal()) { throw new GameDataReadException("Failed to initialise shared memory"); } } try { using (var sharedMemoryStreamView = memoryMappedFile.CreateViewStream()) { BinaryReader _SharedMemoryStream = new BinaryReader(sharedMemoryStreamView); sharedMemoryReadBuffer = _SharedMemoryStream.ReadBytes(sharedmemorysize); GCHandle handle = GCHandle.Alloc(sharedMemoryReadBuffer, GCHandleType.Pinned); try { _pcarsapistruct = (pCarsAPIStruct)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(pCarsAPIStruct)); } finally { handle.Free(); } } PCarsStructWrapper structWrapper = new PCarsStructWrapper(); structWrapper.ticksWhenRead = DateTime.UtcNow.Ticks; structWrapper.data = _pcarsapistruct; if (!forSpotter && dumpToFile && dataToDump != null && _pcarsapistruct.mTrackLocation != null && _pcarsapistruct.mTrackLocation.Length > 0) { dataToDump.Add(structWrapper); } return(structWrapper); } catch (Exception ex) { throw new GameDataReadException(ex.Message, ex); } } }
private int readFromOffset(int offset, byte[] rawData) { // the first 2 bytes are the version - discard it for now int frameTypeAndSequence = rawData[offset + 2]; int frameType = frameTypeAndSequence & 3; int sequence = frameTypeAndSequence >> 2; int frameLength = 0; if (frameType == 0) { telemPacketCount++; if (telemPacketCount > packetCountAtStartOfNextRateCheck) { lastPacketRateEstimate = (int)((float)TimeSpan.TicksPerSecond * (float)(telemPacketCount - packetCountAtStartOfCurrentRateCheck) / (float)(DateTime.UtcNow.Ticks - ticksAtStartOfCurrentPacketRateCheck)); Console.WriteLine("Packet rate = " + lastPacketRateEstimate + "Hz, totals: type0 = " + telemPacketCount + " type1 = " + stringsPacketCount + " type2 = " + additionalStringsPacketCount + " in sequence = " + inSequenceTelemCount + " oos accepted = " + acceptedOutOfSequenceTelemCount + " oos rejected = " + discardedTelemCount); packetCountAtStartOfCurrentRateCheck = telemPacketCount; packetCountAtStartOfNextRateCheck = packetCountAtStartOfCurrentRateCheck + packetRateCheckInterval; ticksAtStartOfCurrentPacketRateCheck = DateTime.UtcNow.Ticks; } frameLength = sTelemetryData_PacketSize; Boolean sequenceCheckOK = isNextInSequence(sequence); if (sequenceCheckOK) { inSequenceTelemCount++; } if (strictPacketOrdering && !sequenceCheckOK) { discardedTelemCount++; } else { GCHandle handle = GCHandle.Alloc(rawData.Skip(offset).Take(frameLength).ToArray(), GCHandleType.Pinned); try { sTelemetryData telem = (sTelemetryData)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(sTelemetryData)); if (sequenceCheckOK || !telemIsOutOfSequence(telem)) { buttonsState = ConvertBytesToBoolArray(telem.sDPad, telem.sJoyPad1, telem.sJoyPad2); lastSequenceNumberForTelemPacket = sequence; workingGameState = StructHelper.MergeWithExistingState(workingGameState, telem); newSpotterData = workingGameState.hasNewPositionData; } } finally { handle.Free(); } } } else if (frameType == 1) { stringsPacketCount++; frameLength = sParticipantInfoStrings_PacketSize; GCHandle handle = GCHandle.Alloc(rawData.Skip(offset).Take(frameLength).ToArray(), GCHandleType.Pinned); try { sParticipantInfoStrings strings = (sParticipantInfoStrings)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(sParticipantInfoStrings)); workingGameState = StructHelper.MergeWithExistingState(workingGameState, strings); } finally { handle.Free(); } } else if (frameType == 2) { additionalStringsPacketCount++; frameLength = sParticipantInfoStringsAdditional_PacketSize; GCHandle handle = GCHandle.Alloc(rawData.Skip(offset).Take(frameLength).ToArray(), GCHandleType.Pinned); try { sParticipantInfoStringsAdditional additional = (sParticipantInfoStringsAdditional)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(sParticipantInfoStringsAdditional)); workingGameState = StructHelper.MergeWithExistingState(workingGameState, additional); } finally { handle.Free(); } } else { Console.WriteLine("Unrecognised frame type " + frameType + " from byte " + rawData[offset + 2]); } return(frameLength + offset); }
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); } } } }
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); }
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; } } } }