using ksBroadcastingNetwork; using ksBroadcastingNetwork.Structs; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; namespace ksBroadcastingTestClient.Autopilot { public class AutopilotCarViewModel : KSObservableObject { public float Pressure { get => Get(); set { if (Set(value)) NotifyUpdate(nameof(PressureWidth)); } } public GridLength PressureWidth => new GridLength(Pressure, GridUnitType.Star); public GridLength PressureWidthRight => new GridLength(1f - Pressure / 2f, GridUnitType.Star); public int CarIndex { get; } public int RaceNumber { get => Get(); private set => Set(value); } public int CarModelEnum { get => Get(); private set => Set(value); } public string TeamName { get => Get(); private set => Set(value); } public int CupCategoryEnum { get => Get(); private set => Set(value); } public CarLocationEnum CarLocation { get => Get(); private set => Set(value); } public bool CrossedTheLineWithFocus { get; private set; } public int Delta { get => Get(); private set => Set(value); } public int Gear { get => Get(); private set => Set(value); } public int Kmh { get => Get(); private set => Set(value); } public int Position { get => Get(); private set => Set(value); } public int CupPosition { get => Get(); private set => Set(value); } public int TrackPosition { get => Get(); private set => Set(value); } public float SplinePosition { get => Get(); private set => Set(value); } public float WorldX { get => Get(); private set => Set(value); } public float WorldY { get => Get(); private set => Set(value); } public float Yaw { get => Get(); private set => Set(value); } public int Laps { get => Get(); private set => Set(value); } public string LocationHint { get => Get(); private set => Set(value); } public float GapFrontMeters { get => Get(); set => Set(value); } public float GapRearMeters { get => Get(); set => Set(value); } public float GapFrontSeconds { get => Get(); set => Set(value); } public float GapRearSeconds { get => Get(); set => Set(value); } public string CurrentDriverName { get => Get(); private set => Set(value); } public bool HasFocus { get; internal set; } public List PressureCategories { get; } = new List(); public int SessionPersonalBestLap { get => Get(); private set => Set(value); } public int PredictedLaptime { get => Get(); private set => Set(value); } public static int SessionBestLap { get; set; } public int CarsAroundMe30m { get; private set; } const float OFFSET = 0.001f; public AutopilotCarViewModel(ushort carIndex) { CarIndex = carIndex; foreach (var item in Enum.GetValues(typeof(CarWeightCategoryEnum)).Cast()) { PressureCategories.Add(new CarWeightCategoryViewModel(item)); } } internal void SetFocused(int focusedCarIndex) { if (CarIndex == focusedCarIndex) { HasFocus = true; // RowForeground = Brushes.Yellow; // RowBackground = Brushes.Black; } else { HasFocus = false; // RowForeground = Brushes.Black; // RowBackground = null; } } internal void Update(CarInfo carUpdate) { RaceNumber = carUpdate.RaceNumber; CarModelEnum = carUpdate.CarModelType; TeamName = carUpdate.TeamName; CupCategoryEnum = carUpdate.CupCategory; CurrentDriverName = carUpdate.GetCurrentDriverName(); } internal void Update(RealtimeCarUpdate carUpdate, float trackMeters) { if (carUpdate.CarIndex != CarIndex) { System.Diagnostics.Debug.WriteLine($"Wrong {nameof(RealtimeCarUpdate)}.CarIndex {carUpdate.CarIndex} for {nameof(AutopilotCarViewModel)}.CarIndex {CarIndex}"); return; } CarLocation = carUpdate.CarLocation; CrossedTheLineWithFocus = false; if (carUpdate.SplinePosition * trackMeters < 100 && HasFocus) CrossedTheLineWithFocus = true; if (CrossedTheLineWithFocus && carUpdate.SplinePosition * trackMeters > 100) CrossedTheLineWithFocus = false; if (carUpdate.SplinePosition * trackMeters > 500) Delta = carUpdate.Delta; Kmh = carUpdate.Kmh; Position = carUpdate.Position; CupPosition = carUpdate.CupPosition; TrackPosition = carUpdate.TrackPosition; SplinePosition = carUpdate.SplinePosition; WorldX = carUpdate.WorldPosX; WorldY = carUpdate.WorldPosY; Yaw = carUpdate.Yaw; Laps = carUpdate.Laps; SessionPersonalBestLap = carUpdate.BestSessionLap.LaptimeMS ?? -1; if (SessionPersonalBestLap > 0 && (SessionPersonalBestLap < SessionBestLap || SessionBestLap <= 0)) SessionBestLap = SessionPersonalBestLap; if (SessionBestLap > 0) PredictedLaptime = SessionBestLap + Delta; // The location hint will combine stuff like pits, in/outlap if (CarLocation == CarLocationEnum.PitEntry) LocationHint = "IN"; else if (CarLocation == CarLocationEnum.Pitlane) LocationHint = "PIT"; else if (CarLocation == CarLocationEnum.PitExit) LocationHint = "OUT"; else LocationHint = "OUT"; } internal void CalcPressure(IList trackPositionCarList, float trackMeters, float currentFocusSeconds, IDictionary categoryWeights, ICamManager camManager) { var OFFSET = 0.001f; // we'll use a (tiny) offset so even a zero doesn't eliminate our information by a zero division; var ONE = 1f + OFFSET; // this way even a multiple "bad, bad, very bad" combination will be greater than "bad, very bad, very bad" Pressure = 1f; foreach (var category in PressureCategories) { // Special case: Sitting in the pits or so if (CarLocation == CarLocationEnum.Pitlane) { category.RawValue = OFFSET; category.Hint = $"Pitlane"; } else { switch (category.Category) { case CarWeightCategoryEnum.Proximity: { // 1 Proximity - how close is this car to the next one, linearly scaled to 2.5 seconds // Now this is tricky for rear/front, as there are always two cars for the same distance. If we look at the front // or rear car has major impact on the pressure of rearwing or onboard cams, so we'll need a bit of variance here float frontBias = 1f; float rearBias = 0.95f; if (DateTime.Now.Minute % 2 == 0) { frontBias = 0.95f; rearBias = 1f; } var closestDistance = Math.Min(GapFrontSeconds * frontBias, GapRearSeconds * rearBias); category.RawValue = ONE - Math.Min(closestDistance, 2.5f) / 2.5f; category.Hint = $"{closestDistance:F1}"; } break; case CarWeightCategoryEnum.Pack: { // 2 PackFactor - how many cars are within ~10 and 30 meters? Linearly scaled to 5 cars CarsAroundMe30m = trackPositionCarList.Where(x => Math.Abs(x.SplinePosition - SplinePosition) * trackMeters < 30 && x != this).Count(); var carsAroundMe10m = trackPositionCarList.Where(x => Math.Abs(x.SplinePosition - SplinePosition) * trackMeters < 10 && x != this).Count(); // 10m counts twice category.RawValue = Math.Min(CarsAroundMe30m + carsAroundMe10m, 7) / 7f + OFFSET; category.Hint = $"{CarsAroundMe30m}|{carsAroundMe10m}"; } break; case CarWeightCategoryEnum.Position: { // 3 Race Position - leaders may be more interesting than anybody else // we have 2 positions; official and track - let's combine them var pos = (Position + TrackPosition) / 2f; category.RawValue = ONE - pos / (float)trackPositionCarList.Count; if (Position != TrackPosition) category.Hint = $"{Position}|{TrackPosition}"; else category.Hint = $"{TrackPosition}"; } break; case CarWeightCategoryEnum.FocusFast: { // 10 Focus seconds - how long since the last focus switch? This is the short time thing which allows (or denies) to quickly jump // action if there is something happening like a contact, closing in or whatever // First 5s are super critical, we won't really allow a jump there so we scale to 10f - then it's neutral to jump over if (!HasFocus) { category.RawValue = Math.Min(currentFocusSeconds, 10f) / 10f; category.Hint = $"{currentFocusSeconds:F1}s"; } else { // While we have focus, we signal that's it's uncritical to stick with us category.RawValue = 1f; category.Hint = $"Focus"; } } break; case CarWeightCategoryEnum.FocusSlow: { // Opposed to the "Fast" variant, this is about how long we want to generally stick to a car. // To not confuse the user, we basically want to aim for at least 30 seconds = 50% if (!HasFocus) { category.RawValue = Math.Min(currentFocusSeconds, 60f) / 60f; category.Hint = $"{currentFocusSeconds:F1}s"; } else { // While we have focus, we signal that's it's uncritical to stick with us. // though we could gradually lower the value when it's becoming way too long if (currentFocusSeconds > 5 * 60) { // we'll sloooowly reduce this so other cars may get into the focus. After a total of 15minutes the focus will forcibly go away (but most likely earlier) category.RawValue = ONE - Math.Min((Math.Max(currentFocusSeconds - 5f, 0f) * 60f) / 10f * 60f, 0f); category.Hint = $"Focus ({(currentFocusSeconds / 60f):F1}min)"; } else { category.RawValue = 1f; category.Hint = $"Focus"; } } } break; case CarWeightCategoryEnum.Pace: { // For pace, we either look at the delta (to express how much we're pushing and maybe hunting) or at the predicted laptime - which is gold for the P/Q modes // this way we should be able to focus cars that may end up on pole or high positions while they do so - something quite hard to achieve manually if (PredictedLaptime <= 0) { // No lap set yet, doesn't matter category.RawValue = OFFSET; category.Hint = $"No laptime"; } else { var splinePosFactor = SplinePosition; if (CrossedTheLineWithFocus) splinePosFactor = 1f; splinePosFactor = (splinePosFactor + 1f) / 2f; if (PredictedLaptime < SessionBestLap) { // oh on the way to purple! category.RawValue = 1f * splinePosFactor; category.Hint = $"{Delta:N0} (!)"; } else { category.RawValue = (1f - Math.Min(PredictedLaptime - SessionBestLap, 1500) / 1500) * splinePosFactor; category.Hint = $"{Delta:N0}"; } } } break; // Add handling for new enum types here default: { // Using a 1f default for unhandled categories will make them do (and break) nothing category.RawValue = 1f; category.Hint = $"Unmapped"; } break; } } category.WeightedValue = (float)Math.Pow(category.RawValue, categoryWeights[category.Category]); Pressure *= category.WeightedValue; } } public void CalcPreferredCamera(IList trackPositionCarList, float trackMeters, float cameraChangedBeforeSeconds, ICamManager camManager, out AnyCamera preferredCamera, out float rawWeight) { // This is going to be a bigger one. We will select the most interesting/suitable camera for this car var tvCam1Pressure = camManager.GetTVCamPressure(CameraTypeEnum.Tv1, this); var tvCam2Pressure = camManager.GetTVCamPressure(CameraTypeEnum.Tv2, this); // Additionally, we will look for cool other cams, like // a) If there is nothing in front, but lots of action in the rear, we like the rear view cam var carsBehindMe = trackPositionCarList.Where(x => Math.Abs(x.SplinePosition - SplinePosition) * trackMeters < 50 && x != this).Count(); var rearViewPressure = 0f; if (GapFrontSeconds > 1 && GapRearSeconds < 2.0f && GapRearMeters > 4) rearViewPressure = 1f - Math.Min(Math.Max(GapRearSeconds - 1f, 0f), 1.0f) / 1.0f; // b) reversed, if there is action in front of me we can do an onboard var onboardPressure = 0f; if (GapFrontSeconds < 0.5 && GapRearSeconds > 2f && GapFrontMeters > 4f) onboardPressure = 1f - Math.Min(Math.Max(GapFrontSeconds - 1f, 0f), 1.5f) / 1.5f; var pitlaneFactor = CarLocation == CarLocationEnum.Pitlane ? 0f : 1f; var camPressures = new Dictionary() { { CameraTypeEnum.Tv1, tvCam1Pressure * pitlaneFactor }, { CameraTypeEnum.Tv2, tvCam2Pressure * pitlaneFactor }, { CameraTypeEnum.RearWing, rearViewPressure * pitlaneFactor }, { CameraTypeEnum.Onboard, onboardPressure * pitlaneFactor}, { CameraTypeEnum.Pitlane, -pitlaneFactor }, { CameraTypeEnum.Helicam, 0.8f * pitlaneFactor }, }; // Any of the weights will go down by the time since the last camera switch, which makes hopping unlikely var camClippingPrevention = Math.Min(Math.Max(cameraChangedBeforeSeconds - 3f, 0f), 7f) / 7f + OFFSET; foreach (var camType in camPressures.Keys.ToArray()) { switch (camType) { // Additionally we have some edits to do case CameraTypeEnum.Tv1: // TV1 is suitable for close combat, but we shouldn't use it too much for packs if (CarsAroundMe30m > 1) camPressures[camType] *= 1f - Math.Min(CarsAroundMe30m - 1, 5) / 5f; break; case CameraTypeEnum.Tv2: // TV2 is always nice, but shouldn't be overused for low car numbers if (CarsAroundMe30m == 0) camPressures[camType] *= 0.5f; if (CarsAroundMe30m == 1) camPressures[camType] *= 0.75f; break; case CameraTypeEnum.Helicam: // helicam is great to catch many cars, and we also like that this will push them automatically for the start if (CarsAroundMe30m > 2) camPressures[camType] = Math.Min(CarsAroundMe30m, 5) / 5f; else // Otherwise this isn't too fancy and should be reserved to replays camPressures[camType] *= 0.002f; break; case CameraTypeEnum.Onboard: // we need to have a fallback if TV cams suck, otherwise the Helicam is too much of an option most of the time camPressures[camType] = Math.Max(camPressures[camType], 0.21f); break; } // Clipping prevention: we don't like jumping too quickly if (camClippingPrevention < 1f) camPressures[camType] *= camClippingPrevention; } camManager.GetPreferredCameraWithWeight(out preferredCamera, out rawWeight, camPressures, HasFocus); } } }