Jack 4d94fbf54f
All checks were successful
Build and push image / Build (push) Successful in 49s
i live in spain but the S is silent
2023-09-28 00:53:27 +01:00

357 lines
20 KiB
C#
Executable File

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<float>(); 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<int>(); private set => Set(value); }
public int CarModelEnum { get => Get<int>(); private set => Set(value); }
public string TeamName { get => Get<string>(); private set => Set(value); }
public int CupCategoryEnum { get => Get<int>(); private set => Set(value); }
public CarLocationEnum CarLocation { get => Get<CarLocationEnum>(); private set => Set(value); }
public bool CrossedTheLineWithFocus { get; private set; }
public int Delta { get => Get<int>(); private set => Set(value); }
public int Gear { get => Get<int>(); private set => Set(value); }
public int Kmh { get => Get<int>(); private set => Set(value); }
public int Position { get => Get<int>(); private set => Set(value); }
public int CupPosition { get => Get<int>(); private set => Set(value); }
public int TrackPosition { get => Get<int>(); private set => Set(value); }
public float SplinePosition { get => Get<float>(); private set => Set(value); }
public float WorldX { get => Get<float>(); private set => Set(value); }
public float WorldY { get => Get<float>(); private set => Set(value); }
public float Yaw { get => Get<float>(); private set => Set(value); }
public int Laps { get => Get<int>(); private set => Set(value); }
public string LocationHint { get => Get<string>(); private set => Set(value); }
public float GapFrontMeters { get => Get<float>(); set => Set(value); }
public float GapRearMeters { get => Get<float>(); set => Set(value); }
public float GapFrontSeconds { get => Get<float>(); set => Set(value); }
public float GapRearSeconds { get => Get<float>(); set => Set(value); }
public string CurrentDriverName { get => Get<string>(); private set => Set(value); }
public bool HasFocus { get; internal set; }
public List<CarWeightCategoryViewModel> PressureCategories { get; } = new List<CarWeightCategoryViewModel>();
public int SessionPersonalBestLap { get => Get<int>(); private set => Set(value); }
public int PredictedLaptime { get => Get<int>(); 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<CarWeightCategoryEnum>())
{
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<AutopilotCarViewModel> trackPositionCarList, float trackMeters, float currentFocusSeconds, IDictionary<CarWeightCategoryEnum, float> 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<AutopilotCarViewModel> 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, float>()
{
{ 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);
}
}
}