i live in spain but the S is silent
All checks were successful
Build and push image / Build (push) Successful in 49s
All checks were successful
Build and push image / Build (push) Successful in 49s
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
<UserControl x:Class="ksBroadcastingTestClient.Autopilot.AutopilotCarView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:ksBroadcastingTestClient.Autopilot"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="{Binding PressureWidth}" />
|
||||
<ColumnDefinition Width="0.001*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="30" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<!--<Grid Background="DarkGray" Grid.Row="0" Grid.Column="0" />-->
|
||||
<ProgressBar Value="{Binding Pressure}" Foreground="DarkGray" Grid.Row="0" Grid.ColumnSpan="2" />
|
||||
|
||||
<StackPanel Margin="3" Orientation="Horizontal" Grid.ColumnSpan="2" Grid.RowSpan="2">
|
||||
<TextBlock Width="30" Text="{Binding Position}" FontWeight="Bold" HorizontalAlignment="Left" />
|
||||
<TextBlock Width="60" Text="{Binding RaceNumber, StringFormat={}#{0}}" TextAlignment="Center" HorizontalAlignment="Left" />
|
||||
<TextBlock Width="80" Text="{Binding CurrentDriverName, StringFormat={}#{0}}" TextAlignment="Center" HorizontalAlignment="Left" />
|
||||
<TextBlock Width="30" Text="{Binding Pressure, StringFormat=P1}" FontWeight="Bold" HorizontalAlignment="Left" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- And the weights -->
|
||||
<ItemsControl ItemsSource="{Binding PressureCategories}" Grid.Row="1" Grid.ColumnSpan="2">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Category}" />
|
||||
<Grid Grid.Column="1">
|
||||
<ProgressBar Value="{Binding WeightedValue}" Minimum="0" Maximum="1" />
|
||||
<TextBlock Text="{Binding Hint}" Foreground="DarkGray" TextAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for AutopilotCarView.xaml
|
||||
/// </summary>
|
||||
public partial class AutopilotCarView : UserControl
|
||||
{
|
||||
public AutopilotCarView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,356 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
<UserControl x:Class="ksBroadcastingTestClient.Autopilot.AutopilotView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:ksBroadcastingTestClient.Autopilot"
|
||||
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800" Name="viewAutopilot">
|
||||
<UserControl.Resources>
|
||||
<CollectionViewSource x:Key="src" Source="{Binding Cars}">
|
||||
<CollectionViewSource.SortDescriptions>
|
||||
<scm:SortDescription PropertyName="Position" Direction="Ascending" />
|
||||
</CollectionViewSource.SortDescriptions>
|
||||
</CollectionViewSource>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<Grid Grid.ColumnSpan="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="230" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Content="{Binding AutopilotStateText}" Command="{Binding ToggleAutopilotCommand}" Width="60" Height="24" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="3,3,0,0" />
|
||||
<Expander IsExpanded="True" Grid.Column="1" Grid.ColumnSpan="99">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!-- Pressure weight sliders-->
|
||||
<ItemsControl ItemsSource="{Binding PressureCategoryWeights}" Width="200">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="40" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Category}" />
|
||||
<Slider Value="{Binding RawValue}" Minimum="0" Maximum="2" Grid.Column="1" />
|
||||
<TextBlock Text="{Binding RawValue, StringFormat=P0}" Grid.Column="2" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!-- Camera panel -->
|
||||
<local:CameraManagementView DataContext="{Binding CamManagerVM}" />
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
<TextBlock Text="{Binding CamManagerVM.CameraState}" Grid.Column="3" />
|
||||
<TextBlock Text="{Binding AutopilotState}" Grid.Column="4" />
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Car list -->
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ItemsControl Name="listViewCars" ItemsSource="{Binding Source={StaticResource src}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<local:AutopilotCarView Margin="0,3,0,0" >
|
||||
<local:AutopilotCarView.InputBindings>
|
||||
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding ElementName=viewAutopilot, Path=DataContext.RequestFocusedCarCommand}" CommandParameter="{Binding}" />
|
||||
</local:AutopilotCarView.InputBindings>
|
||||
</local:AutopilotCarView>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- MiniLog -->
|
||||
<ScrollViewer Grid.Row="1" Grid.Column="1">
|
||||
<ItemsControl ItemsSource="{Binding MiniLog}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for AutopilotView.xaml
|
||||
/// </summary>
|
||||
public partial class AutopilotView : UserControl
|
||||
{
|
||||
public AutopilotView()
|
||||
{
|
||||
InitializeComponent();
|
||||
AutorefreshSorting();
|
||||
}
|
||||
|
||||
private async void AutorefreshSorting()
|
||||
{
|
||||
var sorter = FindResource("src") as CollectionViewSource;
|
||||
|
||||
while (sorter != null)
|
||||
{
|
||||
sorter?.View?.Refresh();
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,415 @@
|
||||
using ksBroadcastingNetwork;
|
||||
using ksBroadcastingNetwork.Structs;
|
||||
using ksBroadcastingTestClient.Broadcasting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
/// <summary>
|
||||
/// Similar to the Broadcasting view model, but is meant to debug and understand decisions made by the autopilot.
|
||||
/// Is a bit of code duplication, but I wanted this to act stand-alone so it can serve as a base for a good auto-cam mod
|
||||
/// </summary>
|
||||
public class AutopilotViewModel : KSObservableObject
|
||||
{
|
||||
public AutopilotWeightsViewModel WeightsVM { get; } = new AutopilotWeightsViewModel();
|
||||
public ObservableCollection<AutopilotCarViewModel> Cars { get; } = new ObservableCollection<AutopilotCarViewModel>();
|
||||
|
||||
public TrackViewModel TrackVM { get => Get<TrackViewModel>(); private set => Set(value); }
|
||||
public CameraManagementViewModel CamManagerVM { get; } = new CameraManagementViewModel();
|
||||
public KSRelayCommand RequestFocusedCarCommand { get; }
|
||||
public KSRelayCommand ToggleAutopilotCommand { get; }
|
||||
public bool IsAutopilotActive { get => Get<bool>(); private set { Set(value); NotifyUpdate(nameof(AutopilotStateText)); } }
|
||||
public string AutopilotStateText { get => IsAutopilotActive ? "Stop" : "Auto"; }
|
||||
|
||||
private List<BroadcastingNetworkProtocol> _clients = new List<BroadcastingNetworkProtocol>();
|
||||
public List<CarWeightCategoryViewModel> PressureCategoryWeights { get; } = new List<CarWeightCategoryViewModel>();
|
||||
public DateTime LastFocusChange { get => Get<DateTime>(); private set => Set(value); }
|
||||
public DateTime LastCameraSetChange { get => Get<DateTime>(); private set => Set(value); }
|
||||
public DateTime LastCameraChange { get => Get<DateTime>(); private set => Set(value); }
|
||||
public int FocusedCarIndex { get; private set; }
|
||||
public string ActiveCameraSet { get; private set; }
|
||||
public string ActiveCamera { get; private set; }
|
||||
public RaceSessionType SessionType { get => Get<RaceSessionType>(); set => Set(value); }
|
||||
public string CurrentHudPage { get; private set; }
|
||||
public string AutopilotState { get => Get<string>(); private set => Set(value); }
|
||||
public ObservableCollection<string> MiniLog { get; } = new ObservableCollection<string>();
|
||||
|
||||
public AutopilotViewModel()
|
||||
{
|
||||
RequestFocusedCarCommand = new KSRelayCommand(RequestFocusedCar);
|
||||
ToggleAutopilotCommand = new KSRelayCommand((o) => IsAutopilotActive = !IsAutopilotActive);
|
||||
LastFocusChange = DateTime.Now.AddSeconds(-10);
|
||||
|
||||
foreach (var item in Enum.GetValues(typeof(CarWeightCategoryEnum)).Cast<CarWeightCategoryEnum>())
|
||||
{
|
||||
var weight = new CarWeightCategoryViewModel(item);
|
||||
weight.RawValue = GetDefaultCategoryWeight(item);
|
||||
PressureCategoryWeights.Add(weight);
|
||||
}
|
||||
}
|
||||
|
||||
private float GetDefaultCategoryWeight(CarWeightCategoryEnum item)
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
// Add cases for overrides, leave them out for a default of 100%
|
||||
default:
|
||||
return 1f;
|
||||
}
|
||||
}
|
||||
|
||||
int cycles = 0;
|
||||
private void CalcCarRanks()
|
||||
{
|
||||
cycles++;
|
||||
// This is the one big responsibility. Once each realtime update cycle, we will walk through all the cars and try to figure out
|
||||
// what is the best car (and camera and everything) to focus right now
|
||||
|
||||
// The obvious stuff is that we want a priority mix of
|
||||
// - close action
|
||||
// - watching the front guys
|
||||
// - distribute screentime across the field
|
||||
// - contacts and accidents
|
||||
|
||||
// but also take more sophisticated stuff into consideration
|
||||
// - camera clipping, avoid too furious jumps
|
||||
// - action clipping - once we watch something, we want to stick there for moment at least
|
||||
// - camera selection: we should offer a healthy mix and shuffle in onboards, bonus points for understanding which camera would be nice for a given situations
|
||||
// - instant replays - in theory we should be able to jump to any contact happening, and selectively offer a replay of the seconds before. Same for overtakes we didn't cover
|
||||
|
||||
// I do see multiple ways to implement this, the regular way would be to use any default decision making method - in the easiest way we could stack up categories and weights.
|
||||
// Due to the nature of the amount solutions we have, I spot a loophole and will (try to) abuse the big trick in genetic algorithms, just without the CPU intensive use.
|
||||
#region deeper thoughts about this
|
||||
// GA's have a black magic component where you only need to describe the fitness function of a given solution. If you do this well, you do not need to understand/implement tactics,
|
||||
// the GA will explore this. But as we only have a number of cars = number of solutions, we can simply bruteforce all scenarios (= amount of cars) and decide by fitness function.
|
||||
// That one has to express either the gain or pain if we focus that car right now. Sounds a lot more fancy than it is, but it's important to understand the perspective and feed the
|
||||
// data we need
|
||||
#endregion
|
||||
|
||||
var trackPositionCarList = Cars.OrderByDescending(x=>x.TrackPosition).ToList();
|
||||
// for the ease of use, we'll set the lead car to the front
|
||||
|
||||
var maxRank = 0f;
|
||||
AutopilotCarViewModel maxRankCar = null;
|
||||
var focusChangedBeforeSeconds = (float)(DateTime.Now - LastFocusChange).TotalSeconds;
|
||||
var cameraChangedBeforeSeconds = (float)(DateTime.Now - LastCameraChange).TotalSeconds;
|
||||
|
||||
var weightDict = PressureCategoryWeights.ToDictionary(x => x.Category, x => x.RawValue);
|
||||
var avgPressure = 0f;
|
||||
foreach (var item in trackPositionCarList)
|
||||
{
|
||||
item.CalcPressure(trackPositionCarList, TrackVM.TrackMeters, focusChangedBeforeSeconds, weightDict, CamManagerVM);
|
||||
|
||||
if(maxRank < item.Pressure)
|
||||
{
|
||||
maxRank = item.Pressure;
|
||||
maxRankCar = item;
|
||||
}
|
||||
|
||||
avgPressure += item.Pressure;
|
||||
}
|
||||
|
||||
if (trackPositionCarList.Count > 0)
|
||||
avgPressure /= trackPositionCarList.Count;
|
||||
|
||||
if (IsAutopilotActive)
|
||||
{
|
||||
// additionally, we will see what cam we want to use
|
||||
// In the first approach, we will ask if there is something to force - eg. TV cams we need to learn
|
||||
var forcedCamSet = CamManagerVM.GetForcedCameraSet();
|
||||
if (forcedCamSet != null)
|
||||
{
|
||||
RequestCameraChange(forcedCamSet.Set, forcedCamSet.Name);
|
||||
}
|
||||
else if(CamManagerVM.TVCamLearningProgress == 1f)
|
||||
{
|
||||
AnyCamera preferredCamera;
|
||||
float rawWeight;
|
||||
maxRankCar.CalcPreferredCamera(trackPositionCarList, TrackVM.TrackMeters, cameraChangedBeforeSeconds, CamManagerVM, out preferredCamera, out rawWeight);
|
||||
var stateStr = "";
|
||||
if (maxRankCar != null && !maxRankCar.HasFocus)
|
||||
stateStr = $"Want: {maxRankCar.CurrentDriverName} ({maxRank:P0})";
|
||||
stateStr += preferredCamera != null ? $" Cam {rawWeight:P0}" : $"-";
|
||||
AutopilotState = stateStr;
|
||||
|
||||
var isFocusChange = maxRankCar != null && !maxRankCar.HasFocus && maxRankCar.Pressure > avgPressure * 0.15f && rawWeight > 0.2f;
|
||||
var isCameraChange = preferredCamera != null && rawWeight > 0.2f;
|
||||
var isHudChange = false;
|
||||
var targetHUD = "";
|
||||
if(isCameraChange)
|
||||
{
|
||||
switch (preferredCamera.CamType)
|
||||
{
|
||||
// Onboards will get the basic hud, with rpm and such
|
||||
case CameraTypeEnum.RearWing:
|
||||
case CameraTypeEnum.Onboard:
|
||||
targetHUD = "Basic HUD";
|
||||
isHudChange = true;
|
||||
break;
|
||||
|
||||
// Default is the broadcasting hud
|
||||
case CameraTypeEnum.Tv1:
|
||||
case CameraTypeEnum.Tv2:
|
||||
case CameraTypeEnum.Helicam:
|
||||
case CameraTypeEnum.Pitlane:
|
||||
case CameraTypeEnum.Unknown:
|
||||
default:
|
||||
targetHUD = "Broadcasting";
|
||||
isHudChange = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetHUD == CurrentHudPage)
|
||||
isHudChange = false;
|
||||
}
|
||||
|
||||
if(isFocusChange && isHudChange)
|
||||
{
|
||||
RequestFocusedCarAndCamera(maxRankCar, preferredCamera.Set, preferredCamera.Name);
|
||||
Log($"Requested Focus: {maxRankCar.CurrentDriverName} ({maxRank:P0}) with cam {preferredCamera.Set}/{preferredCamera.Name} ({rawWeight:P0})");
|
||||
}
|
||||
else if(isFocusChange)
|
||||
{
|
||||
RequestFocusedCar(maxRankCar);
|
||||
Log($"Requested Focus: {maxRankCar.CurrentDriverName} ({maxRank:P0})");
|
||||
}
|
||||
else if(isCameraChange)
|
||||
{
|
||||
RequestCameraChange(preferredCamera.Set, preferredCamera.Name);
|
||||
}
|
||||
|
||||
if(false && isHudChange)
|
||||
{
|
||||
RequestHudPageChange(targetHUD);
|
||||
Log($"Requested HUD change: {targetHUD}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestFocusedCarAndCamera(object obj, string camSet, string camera)
|
||||
{
|
||||
var car = obj as AutopilotCarViewModel;
|
||||
if (car != null)
|
||||
{
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
// mssing readonly check, will skip this as the ACC client has to handle this as well
|
||||
client.SetFocus(Convert.ToUInt16(car.CarIndex), camSet, camera);
|
||||
}
|
||||
|
||||
LastFocusChange = DateTime.Now;
|
||||
LastCameraChange = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestFocusedCar(object obj)
|
||||
{
|
||||
var car = obj as AutopilotCarViewModel;
|
||||
if (car != null)
|
||||
{
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
// mssing readonly check, will skip this as the ACC client has to handle this as well
|
||||
client.SetFocus(Convert.ToUInt16(car.CarIndex));
|
||||
}
|
||||
|
||||
LastFocusChange = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestHudPageChange(string requestedHudPage)
|
||||
{
|
||||
if(requestedHudPage != CurrentHudPage)
|
||||
{
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
// mssing readonly check, will skip this as the ACC client has to handle this as well
|
||||
client.RequestHUDPage(requestedHudPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestCameraChange(string camSet, string camera)
|
||||
{
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
// mssing readonly check, will skip this as the ACC client has to handle this as well
|
||||
client.SetCamera(camSet, camera);
|
||||
}
|
||||
|
||||
LastCameraChange = DateTime.Now;
|
||||
}
|
||||
|
||||
internal void RegisterNewClient(ACCUdpRemoteClient newClient)
|
||||
{
|
||||
if (newClient.MsRealtimeUpdateInterval > 0)
|
||||
{
|
||||
// This client will send realtime updates, we should listen
|
||||
newClient.MessageHandler.OnTrackDataUpdate += MessageHandler_OnTrackDataUpdate;
|
||||
newClient.MessageHandler.OnEntrylistUpdate += MessageHandler_OnEntrylistUpdate;
|
||||
newClient.MessageHandler.OnRealtimeUpdate += MessageHandler_OnRealtimeUpdate;
|
||||
newClient.MessageHandler.OnRealtimeCarUpdate += MessageHandler_OnRealtimeCarUpdate;
|
||||
}
|
||||
|
||||
_clients.Add(newClient.MessageHandler);
|
||||
}
|
||||
|
||||
private void MessageHandler_OnTrackDataUpdate(string sender, TrackData trackUpdate)
|
||||
{
|
||||
if (TrackVM?.TrackId != trackUpdate.TrackId)
|
||||
{
|
||||
if (TrackVM != null)
|
||||
{
|
||||
TrackVM.OnRequestCameraChange -= RequestCameraChange;
|
||||
TrackVM.OnRequestHudPageChange -= RequestHudPageChange;
|
||||
}
|
||||
|
||||
TrackVM = new TrackViewModel(trackUpdate.TrackId, trackUpdate.TrackName, trackUpdate.TrackMeters);
|
||||
TrackVM.OnRequestCameraChange += RequestCameraChange;
|
||||
TrackVM.OnRequestHudPageChange += RequestHudPageChange;
|
||||
}
|
||||
|
||||
// The track cams may update in between
|
||||
TrackVM.Update(trackUpdate);
|
||||
CamManagerVM.Update(trackUpdate);
|
||||
}
|
||||
|
||||
private void MessageHandler_OnEntrylistUpdate(string sender, CarInfo carUpdate)
|
||||
{
|
||||
AutopilotCarViewModel vm = Cars.SingleOrDefault(x => x.CarIndex == carUpdate.CarIndex);
|
||||
if (vm == null)
|
||||
{
|
||||
vm = new AutopilotCarViewModel(carUpdate.CarIndex);
|
||||
Cars.Add(vm);
|
||||
}
|
||||
|
||||
vm.Update(carUpdate);
|
||||
}
|
||||
|
||||
private void MessageHandler_OnRealtimeUpdate(string sender, RealtimeUpdate update)
|
||||
{
|
||||
if (TrackVM != null)
|
||||
TrackVM.Update(update);
|
||||
|
||||
SessionType = update.SessionType;
|
||||
|
||||
foreach (var carVM in Cars)
|
||||
{
|
||||
carVM.SetFocused(update.FocusedCarIndex);
|
||||
}
|
||||
|
||||
if(FocusedCarIndex != update.FocusedCarIndex)
|
||||
{
|
||||
FocusedCarIndex = update.FocusedCarIndex;
|
||||
LastFocusChange = DateTime.Now;
|
||||
}
|
||||
|
||||
if(ActiveCameraSet != update.ActiveCameraSet)
|
||||
{
|
||||
ActiveCameraSet = update.ActiveCameraSet;
|
||||
LastCameraSetChange = DateTime.Now;
|
||||
}
|
||||
|
||||
if(ActiveCamera != update.ActiveCamera || ActiveCameraSet != update.ActiveCameraSet)
|
||||
{
|
||||
ActiveCamera = update.ActiveCamera;
|
||||
LastCameraChange = DateTime.Now;
|
||||
}
|
||||
|
||||
CurrentHudPage = update.CurrentHudPage;
|
||||
|
||||
CamManagerVM.RealtimeUpdate(update);
|
||||
try
|
||||
{
|
||||
if (TrackVM?.TrackMeters > 0)
|
||||
{
|
||||
var sortedCars = Cars.OrderByDescending(x => x.SplinePosition).ToArray();
|
||||
if(sortedCars.Count() > 1)
|
||||
{
|
||||
for (int i = 1; i < sortedCars.Length; i++)
|
||||
{
|
||||
var carAhead = sortedCars[i - 1];
|
||||
var carBehind = sortedCars[i];
|
||||
|
||||
var distance = calcMetersDistance(carAhead, carBehind, TrackVM.TrackMeters);
|
||||
carBehind.GapFrontMeters = distance;
|
||||
carAhead.GapRearMeters = distance;
|
||||
|
||||
var combinedSpeedMS = (carAhead.Kmh + carBehind.Kmh) / 2f / 3.6f;
|
||||
if(combinedSpeedMS > 0.0001f)
|
||||
{
|
||||
carBehind.GapFrontSeconds = distance / combinedSpeedMS;
|
||||
carAhead.GapRearSeconds = distance / combinedSpeedMS;
|
||||
}
|
||||
else
|
||||
{
|
||||
carBehind.GapFrontSeconds = 999;
|
||||
carAhead.GapRearSeconds = 999;
|
||||
}
|
||||
}
|
||||
|
||||
// then the first and last cars
|
||||
var distance2 = calcMetersDistance(sortedCars.First(), sortedCars.Last(), TrackVM.TrackMeters);
|
||||
sortedCars.First().GapFrontMeters = distance2;
|
||||
sortedCars.Last().GapRearMeters = distance2;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in sortedCars)
|
||||
{
|
||||
item.GapFrontMeters = TrackVM.TrackMeters;
|
||||
item.GapRearMeters = TrackVM.TrackMeters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A bit unfortunate, the RealtimeUpdate is happening before the carUpdates so we will lag a bit
|
||||
// but still it's the one place we know the carupdates are synched and the states are the most comparable
|
||||
// more sophisticated solution would be better, like resetting a counter here and push the calculation once we received all cars
|
||||
CalcCarRanks();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static float calcMetersDistance(AutopilotCarViewModel carAhead, AutopilotCarViewModel carBehind, float trackMeters)
|
||||
{
|
||||
var splineDistance = carAhead.SplinePosition - carBehind.SplinePosition;
|
||||
while (splineDistance < 0f)
|
||||
splineDistance += 1f;
|
||||
|
||||
return splineDistance * trackMeters;
|
||||
}
|
||||
|
||||
private void MessageHandler_OnRealtimeCarUpdate(string sender, RealtimeCarUpdate carUpdate)
|
||||
{
|
||||
var vm = Cars.FirstOrDefault(x => x.CarIndex == carUpdate.CarIndex);
|
||||
if (vm == null)
|
||||
{
|
||||
// Oh, we don't have this car yet. In this implementation, the Network protocol will take care of this
|
||||
// so hopefully we will display this car in the next cycles
|
||||
return;
|
||||
}
|
||||
|
||||
vm.Update(carUpdate, TrackVM.TrackMeters);
|
||||
if(vm.HasFocus)
|
||||
CamManagerVM.RealtimeUpdateFocusedCar(vm.CarIndex, vm.SplinePosition, vm.Kmh);
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
MiniLog.Insert(0, msg);
|
||||
while (MiniLog.Count > 15)
|
||||
MiniLog.RemoveAt(MiniLog.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
<UserControl x:Class="ksBroadcastingTestClient.Autopilot.AutopilotWeightsView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:ksBroadcastingTestClient.Autopilot"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for AutopilotWeightsView.xaml
|
||||
/// </summary>
|
||||
public partial class AutopilotWeightsView : UserControl
|
||||
{
|
||||
public AutopilotWeightsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
public class AutopilotWeightsViewModel : KSObservableObject
|
||||
{
|
||||
public string JumpinessHint { get; } = "How willingly will the autopilot jump between cars based on recent action.\nReduce this if it's becoming confusing for the viewer, increase it if we it sticks too long with one car and we miss other things";
|
||||
public float Jumpiness { get => Get<float>(); private set => Set(value); }
|
||||
|
||||
public string RacePositionHint { get; } = "How likely the autopilot will focus lead cars. If this is too high, it would follow a solo leader while the pack behind is fighting for it's life; increase this if you start to see insignificant stuff";
|
||||
public float RacePosition { get => Get<float>(); private set => Set(value); }
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for CameraManagementView.xaml
|
||||
/// </summary>
|
||||
public partial class CameraManagementView : UserControl
|
||||
{
|
||||
public CameraManagementView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,544 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ksBroadcastingNetwork.Structs;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
public enum CameraTypeEnum { Tv1, Tv2, RearWing, Onboard, Helicam, Pitlane, Unknown }
|
||||
|
||||
public interface ICamManager
|
||||
{
|
||||
float GetTVCamPressure(CameraTypeEnum cameraType, AutopilotCarViewModel car);
|
||||
void GetPreferredCameraWithWeight(out AnyCamera preferredCamera, out float rawValue, Dictionary<CameraTypeEnum, float> camPressures, bool isFocusedCar);
|
||||
}
|
||||
|
||||
public class AnyCamera : KSObservableObject
|
||||
{
|
||||
public string Set { get; }
|
||||
public string Name { get; }
|
||||
public CameraTypeEnum CamType { get; set; }
|
||||
public bool IsActive { get => Get<bool>(); set { Set(value); NotifyUpdate(nameof(IndicatorColor)); } }
|
||||
public virtual System.Windows.Media.Brush IndicatorColor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsActive)
|
||||
return System.Windows.Media.Brushes.Green;
|
||||
return System.Windows.Media.Brushes.Gray;
|
||||
}
|
||||
}
|
||||
|
||||
public AnyCamera(string set, string name)
|
||||
{
|
||||
Set = set;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class TVCamJsonData
|
||||
{
|
||||
public string SetName { get; set; }
|
||||
public string CamName { get; set; }
|
||||
public float SplinePosStart { get; set; }
|
||||
public float SplinePosEnd { get; set; }
|
||||
public int EntrySpeed { get; set; }
|
||||
public int ExitSpeed { get; set; }
|
||||
|
||||
public static TVCamJsonData FromCam(TVCam cam)
|
||||
{
|
||||
return new TVCamJsonData()
|
||||
{
|
||||
SetName = cam.Set,
|
||||
CamName = cam.Name,
|
||||
SplinePosStart = cam.SplinePosStart,
|
||||
SplinePosEnd = cam.SplinePosEnd,
|
||||
EntrySpeed = cam.EntrySpeed,
|
||||
ExitSpeed = cam.ExitSpeed
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateTVCam(TVCam cam)
|
||||
{
|
||||
if (string.Equals(SetName, cam.Set) && string.Equals(CamName, cam.Name))
|
||||
{
|
||||
if (cam.SplinePosStart < 0f)
|
||||
cam.SplinePosStart = SplinePosStart;
|
||||
if (cam.SplinePosEnd < 0f)
|
||||
cam.SplinePosEnd = SplinePosEnd;
|
||||
if (cam.EntrySpeed <= 0f)
|
||||
cam.EntrySpeed = EntrySpeed;
|
||||
if (cam.ExitSpeed <= 0f)
|
||||
cam.ExitSpeed = ExitSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TVCam : AnyCamera
|
||||
{
|
||||
public float SplinePosStart { get; internal set; } = -1f;
|
||||
public float SplinePosEnd { get; internal set; } = -1f;
|
||||
|
||||
public override System.Windows.Media.Brush IndicatorColor
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SplinePosStart < 0f)
|
||||
return IsActive ? System.Windows.Media.Brushes.Brown : System.Windows.Media.Brushes.Red;
|
||||
if (SplinePosEnd < 0f)
|
||||
return IsActive ? System.Windows.Media.Brushes.RosyBrown : System.Windows.Media.Brushes.Orange;
|
||||
return base.IndicatorColor;
|
||||
}
|
||||
}
|
||||
|
||||
public int EntrySpeed { get; internal set; }
|
||||
public int ExitSpeed { get; internal set; }
|
||||
public TVCam PreviousCam { get; internal set; }
|
||||
|
||||
public TVCam(string set, string name)
|
||||
: base(set, name)
|
||||
{
|
||||
}
|
||||
|
||||
internal void SetEntry(float splinePosition, int kmh)
|
||||
{
|
||||
SplinePosStart = splinePosition;
|
||||
EntrySpeed = kmh;
|
||||
NotifyUpdate(nameof(IndicatorColor));
|
||||
}
|
||||
|
||||
internal void SetExit(float splinePosition, int kmh)
|
||||
{
|
||||
SplinePosEnd = splinePosition;
|
||||
ExitSpeed = kmh;
|
||||
NotifyUpdate(nameof(IndicatorColor));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var startIsSet = SplinePosStart < 0f ? "-" : "|";
|
||||
var endIsSet = SplinePosStart < 0f ? "|" : "|";
|
||||
return $"{startIsSet}{Name}{endIsSet}";
|
||||
}
|
||||
}
|
||||
|
||||
public class CameraManagementViewModel : KSObservableObject, ICamManager
|
||||
{
|
||||
public Dictionary<string, List<TVCam>> TVCameraSets { get; private set; }
|
||||
public Dictionary<string, List<AnyCamera>> OtherCameraSets { get; private set; }
|
||||
public Dictionary<string, List<AnyCamera>> AllCameraSets { get; private set; }
|
||||
|
||||
TVCam OldTVCam = null;
|
||||
float LastFocusedCarSplinePosition = -1f;
|
||||
int LastFocusedCarSpeed = -1;
|
||||
int LastFocusedCarId = -1;
|
||||
private AnyCamera CurrentCam;
|
||||
|
||||
public DateTime CurrentCamSetActiveSince { get; private set; }
|
||||
|
||||
private Random R = new Random();
|
||||
|
||||
private Dictionary<CameraTypeEnum, DateTime> CamTypeLastActive { get; } = new Dictionary<CameraTypeEnum, DateTime>();
|
||||
|
||||
public float TVCamLearningProgress { get => Get<float>(); private set { if (Set(value)) NotifyUpdate(nameof(CameraState)); } }
|
||||
public string CameraState
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TVCameraSets == null || TVCameraSets.Any())
|
||||
return $"Waiting for camera definitions";
|
||||
else if (TVCamLearningProgress < 1f)
|
||||
return $"Learning TV cams {TVCamLearningProgress:P0}";
|
||||
else
|
||||
{
|
||||
return $"Current camera xy";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string TrackName { get; private set; }
|
||||
public float TrackMeters { get; private set; }
|
||||
|
||||
internal void Update(TrackData trackUpdate)
|
||||
{
|
||||
TrackName = trackUpdate.TrackName;
|
||||
TrackMeters = trackUpdate.TrackMeters;
|
||||
if (TVCameraSets == null)
|
||||
{
|
||||
OldTVCam = null;
|
||||
TVCameraSets = new Dictionary<string, List<TVCam>>();
|
||||
OtherCameraSets = new Dictionary<string, List<AnyCamera>>();
|
||||
AllCameraSets = new Dictionary<string, List<AnyCamera>>();
|
||||
|
||||
foreach (var tvCamSet in trackUpdate.CameraSets.Where(x => x.Key.StartsWith("set")))
|
||||
{
|
||||
// we'll exclude the VR sets, usually can't use them but confuses the learning of TV cams
|
||||
if (tvCamSet.Key.EndsWith("vr", StringComparison.InvariantCultureIgnoreCase))
|
||||
continue;
|
||||
|
||||
TVCameraSets.Add(tvCamSet.Key, new List<TVCam>());
|
||||
AllCameraSets.Add(tvCamSet.Key, new List<AnyCamera>());
|
||||
|
||||
var lastTvCam = (TVCam)null;
|
||||
foreach (var tvCamName in tvCamSet.Value)
|
||||
{
|
||||
var cam = new TVCam(tvCamSet.Key, tvCamName);
|
||||
if (lastTvCam != null)
|
||||
cam.PreviousCam = lastTvCam;
|
||||
lastTvCam = cam;
|
||||
TVCameraSets[tvCamSet.Key].Add(cam);
|
||||
AllCameraSets[tvCamSet.Key].Add(cam);
|
||||
}
|
||||
|
||||
if (TVCameraSets[tvCamSet.Key].Count > 1)
|
||||
TVCameraSets[tvCamSet.Key].First().PreviousCam = TVCameraSets[tvCamSet.Key].Last();
|
||||
}
|
||||
|
||||
foreach (var camSet in trackUpdate.CameraSets.Where(x => !x.Key.StartsWith("set")))
|
||||
{
|
||||
OtherCameraSets.Add(camSet.Key, new List<AnyCamera>());
|
||||
AllCameraSets.Add(camSet.Key, new List<AnyCamera>());
|
||||
|
||||
foreach (var otherCamName in camSet.Value)
|
||||
{
|
||||
var cam = new AnyCamera(camSet.Key, otherCamName);
|
||||
OtherCameraSets[camSet.Key].Add(cam);
|
||||
AllCameraSets[camSet.Key].Add(cam);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var camera in AllCameraSets.SelectMany(x => x.Value))
|
||||
{
|
||||
camera.CamType = EvaluateCameraType(trackUpdate.TrackName, camera.Set, camera.Name);
|
||||
}
|
||||
|
||||
foreach (var camType in AllCameraSets.SelectMany(x => x.Value).Select(x => x.CamType).Distinct())
|
||||
{
|
||||
CamTypeLastActive.Add(camType, DateTime.Now.AddMinutes(-10));
|
||||
}
|
||||
|
||||
TryLoadTVCameraDefs(TVCameraSets.SelectMany(x => x.Value), TrackName);
|
||||
UpdateTVCamLearningProgress();
|
||||
|
||||
NotifyUpdate(nameof(AllCameraSets));
|
||||
NotifyUpdate(nameof(CameraState));
|
||||
}
|
||||
}
|
||||
|
||||
private CameraTypeEnum EvaluateCameraType(string trackName, string set, string name)
|
||||
{
|
||||
// This should go to a cfg file or so
|
||||
if (set.Contains("set1"))
|
||||
return CameraTypeEnum.Tv1;
|
||||
if (set.Contains("set2"))
|
||||
return CameraTypeEnum.Tv2;
|
||||
if (set.Contains("heli") || set.Contains("Heli"))
|
||||
return CameraTypeEnum.Helicam;
|
||||
if (set == "pitlane")
|
||||
return CameraTypeEnum.Pitlane;
|
||||
|
||||
// the rest should be some kind of onboard, we only look for the rear wing one precisely
|
||||
if (name == "Onboard3")
|
||||
return CameraTypeEnum.RearWing;
|
||||
|
||||
// aeh nobody wants to see chasecams in br
|
||||
if (name.Contains("chase") || name.Contains("chase"))
|
||||
return CameraTypeEnum.Unknown;
|
||||
|
||||
return CameraTypeEnum.Onboard;
|
||||
}
|
||||
|
||||
internal void RealtimeUpdate(RealtimeUpdate update)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (AllCameraSets.ContainsKey(update.ActiveCameraSet))
|
||||
{
|
||||
var cam = AllCameraSets[update.ActiveCameraSet].Single(x => x.Name == update.ActiveCamera);
|
||||
if (!cam.IsActive)
|
||||
{
|
||||
// Cam changed!
|
||||
var lastCams = AllCameraSets.SelectMany(x => x.Value).Where(x => x.IsActive);
|
||||
if (lastCams.Count() == 1)
|
||||
{
|
||||
// regular case, we have a last and new cam
|
||||
var oldCam = lastCams.Single();
|
||||
|
||||
// To learn the stuff, we need to memorize the old cam
|
||||
OldTVCam = (oldCam as TVCam);
|
||||
}
|
||||
else if (lastCams.Count() > 1)
|
||||
Debug.WriteLine($"There are {lastCams.Count()} active cams, something went horribly wrong");
|
||||
|
||||
foreach (var item in lastCams)
|
||||
item.IsActive = false;
|
||||
|
||||
cam.IsActive = true;
|
||||
if (cam.CamType != CurrentCam?.CamType)
|
||||
CurrentCamSetActiveSince = DateTime.Now;
|
||||
CurrentCam = cam;
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentCam != null)
|
||||
CamTypeLastActive[CurrentCam.CamType] = DateTime.Now;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
internal TVCam GetForcedCameraSet()
|
||||
{
|
||||
if (TVCamLearningProgress < 1f)
|
||||
{
|
||||
foreach (var item in TVCameraSets.SelectMany(x => x.Value))
|
||||
{
|
||||
if (item.SplinePosEnd < 0f || item.SplinePosStart < 0f)
|
||||
if (item.Set == CurrentCam?.Set)
|
||||
return null;
|
||||
else
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal AnyCamera GetTVCamByIndex(int camIndex)
|
||||
{
|
||||
foreach (var set in TVCameraSets?.Skip(camIndex))
|
||||
return set.Value.FirstOrDefault();
|
||||
return null;
|
||||
}
|
||||
|
||||
internal void RealtimeUpdateFocusedCar(int focusedCarId, float splinePosition, int kmh)
|
||||
{
|
||||
if (OldTVCam != null && !OldTVCam.IsActive && TVCamLearningProgress < 1f && LastFocusedCarId == focusedCarId)
|
||||
{
|
||||
var currentTVCam = CurrentCam as TVCam;
|
||||
// Looks like we can update the end of the old cam
|
||||
// BUT we need to make sure it's the one before the current cam
|
||||
if (currentTVCam != null)
|
||||
{
|
||||
if (currentTVCam.PreviousCam == OldTVCam)
|
||||
{
|
||||
// Cool, now we can learn where the new one begins
|
||||
if (currentTVCam.SplinePosStart < 0f)
|
||||
currentTVCam.SetEntry(splinePosition, kmh);
|
||||
|
||||
// Additionally, the old cam may learn the exit
|
||||
if (OldTVCam.SplinePosEnd < 0f)
|
||||
OldTVCam.SetExit(LastFocusedCarSplinePosition, LastFocusedCarSpeed);
|
||||
|
||||
var oldProgress = TVCamLearningProgress;
|
||||
UpdateTVCamLearningProgress();
|
||||
if (oldProgress != TVCamLearningProgress && TVCamLearningProgress == 1f)
|
||||
SaveTVCameraDefs(TVCameraSets.SelectMany(x => x.Value), TrackName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LastFocusedCarSplinePosition = splinePosition;
|
||||
LastFocusedCarSpeed = kmh;
|
||||
LastFocusedCarId = focusedCarId;
|
||||
}
|
||||
|
||||
private void UpdateTVCamLearningProgress()
|
||||
{
|
||||
// Now we may have an update to the LearningProcess
|
||||
int tvCams = 0;
|
||||
int finalizedTvCams = 0;
|
||||
foreach (var item in TVCameraSets.SelectMany(x => x.Value))
|
||||
{
|
||||
if (item.SplinePosEnd > -1f && item.SplinePosStart > -1f)
|
||||
{
|
||||
// ready to go
|
||||
finalizedTvCams++;
|
||||
}
|
||||
tvCams++;
|
||||
}
|
||||
if (tvCams > 0)
|
||||
TVCamLearningProgress = finalizedTvCams / (float)tvCams;
|
||||
else
|
||||
TVCamLearningProgress = 0;
|
||||
}
|
||||
|
||||
public float GetTVCamPressure(CameraTypeEnum cameraType, AutopilotCarViewModel car)
|
||||
{
|
||||
if (TVCameraSets == null)
|
||||
{
|
||||
Debug.WriteLine($"There are no TV cams (yet?)");
|
||||
return 1f;
|
||||
}
|
||||
|
||||
if (TVCamLearningProgress < 1f)
|
||||
{
|
||||
// while we learn, it's better to stick to a car and let us see all the cams as quick as possible
|
||||
return 0.1f;
|
||||
}
|
||||
|
||||
var setName = TVCameraSets.SelectMany(x => x.Value).FirstOrDefault(x => x.CamType == cameraType)?.Set;
|
||||
|
||||
// Complicated stuff. So the big targets are
|
||||
// 1) avoid changing focus to a car inside the same TV cam, that looks just weird
|
||||
// 2) avoid changing focus to a car that is at the end of the given camera zone
|
||||
// Third is avoid changing focus from a car that just entered a new cam, but this is handled in the car view model
|
||||
|
||||
// let's find the camera this car would be in
|
||||
var potentialTVCam = TVCameraSets[setName].FirstOrDefault(x => x.SplinePosStart < car.SplinePosition && x.SplinePosEnd > car.SplinePosition);
|
||||
if (potentialTVCam == null)
|
||||
{
|
||||
// mh what's this?
|
||||
return 0.1f;
|
||||
}
|
||||
|
||||
var timeToEnd = (potentialTVCam.SplinePosEnd - car.SplinePosition) * TrackMeters / (car.Kmh / 3.6f);
|
||||
// then the pressure is a function of "at least 2s", then we'll blend into full green light at 8s
|
||||
var pressure = Math.Min(Math.Max(timeToEnd - 2, 0), 6) / 6f;
|
||||
|
||||
// Now 1): is this cars not focusing, but we would use the same cam when focused?
|
||||
if (CurrentCam == potentialTVCam)
|
||||
// We are the focused car, always - even if this is called during a pre-step
|
||||
return Math.Max(0.8f, pressure);
|
||||
|
||||
// 2) How much time will we have until this camera ends?
|
||||
if (car.Kmh < 20)
|
||||
return 0.1f;
|
||||
|
||||
|
||||
// TODO: we could edit the pressure to have a healthy ratio between the cam sets, otherwise it'd be random
|
||||
// and set2 may win over proportionally often due to the longer scenes
|
||||
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public void GetPreferredCameraWithWeight(out AnyCamera preferredCamera, out float rawValue, Dictionary<CameraTypeEnum, float> camPressures, bool isFocusedCar)
|
||||
{
|
||||
// First we'll edit the camPressures based on settings, recent cameras and strict rules - the car didn't know shit about cams after all
|
||||
foreach (var item in camPressures)
|
||||
{
|
||||
// apply modifiers
|
||||
}
|
||||
|
||||
// now strict rules
|
||||
var camSetActiveSinceMinutes = Convert.ToSingle(Math.Max((DateTime.Now - CurrentCamSetActiveSince).TotalMinutes, 0.5) - 0.5);
|
||||
var lastCamSetSwitchSeconds = Convert.ToSingle((DateTime.Now - CurrentCamSetActiveSince).TotalSeconds);
|
||||
foreach (var camType in camPressures.Keys.ToArray())
|
||||
{
|
||||
var isCurrentCam = camType == CurrentCam?.CamType;
|
||||
var thisCamLastActiveMinutes = (DateTime.Now - CamTypeLastActive[camType]).TotalMinutes;
|
||||
if (thisCamLastActiveMinutes < 5.0 && !isCurrentCam)
|
||||
camPressures[camType] *= Math.Min(Convert.ToSingle(thisCamLastActiveMinutes / 5.0), 1f);
|
||||
|
||||
switch (camType)
|
||||
{
|
||||
case CameraTypeEnum.RearWing:
|
||||
case CameraTypeEnum.Onboard:
|
||||
{
|
||||
// Additionally these are nice, but shouldn't be in too often, so we'll doubledip this calculation
|
||||
if (thisCamLastActiveMinutes < 5.0 && !isCurrentCam)
|
||||
{
|
||||
camPressures[camType] *= Convert.ToSingle(thisCamLastActiveMinutes / 5.0);
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (isCurrentCam && isFocusedCar)
|
||||
{
|
||||
var camIsGettingOldFactor = 1f;
|
||||
if (camType == CameraTypeEnum.Tv1 || camType == CameraTypeEnum.Tv2)
|
||||
camIsGettingOldFactor = 1f - Math.Min(Math.Max(camSetActiveSinceMinutes - 0.5f, 0f) / 3f, 1f);
|
||||
else
|
||||
camIsGettingOldFactor = 1f - Math.Min(Math.Max(camSetActiveSinceMinutes - 0.3f, 0f) / 2f, 2f);
|
||||
|
||||
camPressures[camType] *= camIsGettingOldFactor;
|
||||
}
|
||||
|
||||
if (!isCurrentCam && isFocusedCar)
|
||||
{
|
||||
// on the other hand we don't want to switch too quickly;
|
||||
if (lastCamSetSwitchSeconds < 20)
|
||||
{
|
||||
var camIsYoungFactor = 1f;
|
||||
if (CurrentCam?.CamType == CameraTypeEnum.Tv1 || CurrentCam?.CamType == CameraTypeEnum.Tv2)
|
||||
camIsYoungFactor = Math.Min(Math.Max(lastCamSetSwitchSeconds - 10f, 0f) / 30f, 1f);
|
||||
else
|
||||
camIsYoungFactor = Math.Min(Math.Max(lastCamSetSwitchSeconds - 5f, 0f) / 15f, 1f);
|
||||
camPressures[camType] = Math.Min(camIsYoungFactor + camPressures[camType], 1f);
|
||||
}
|
||||
}
|
||||
|
||||
// In any way we do not want to focus a new car in the same camera, except for TV cams - but here we don't want to jump
|
||||
// into the same or one we recently saw
|
||||
if (!isFocusedCar)
|
||||
{
|
||||
if (camType == CurrentCam?.CamType)
|
||||
camPressures[camType] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
var winner = camPressures.OrderByDescending(x => x.Value).First();
|
||||
|
||||
// now we'll locate the camera for that, if it's not the same
|
||||
if (winner.Key == CurrentCam?.CamType || winner.Value < 0.1)
|
||||
preferredCamera = null; // null means stick witch it
|
||||
else
|
||||
{
|
||||
// otherwise, we'll select a random cam that matches the type
|
||||
var candidates = AllCameraSets.SelectMany(x => x.Value).Where(x => x.CamType == winner.Key).ToArray();
|
||||
if (!candidates.Any())
|
||||
{
|
||||
Debug.WriteLine($"Requested cam type {winner.Key} isn't available");
|
||||
preferredCamera = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
preferredCamera = candidates[R.Next(candidates.Length)];
|
||||
}
|
||||
}
|
||||
|
||||
rawValue = winner.Value;
|
||||
}
|
||||
|
||||
private static void SaveTVCameraDefs(IEnumerable<TVCam> tVCameraSets, string track)
|
||||
{
|
||||
var defs = tVCameraSets.Select(x => TVCamJsonData.FromCam(x));
|
||||
|
||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(defs);
|
||||
if (!System.IO.Directory.Exists("camDefs"))
|
||||
System.IO.Directory.CreateDirectory("camDefs");
|
||||
System.IO.File.WriteAllText($"camDefs/{track}.json", json);
|
||||
}
|
||||
|
||||
private static void TryLoadTVCameraDefs(IEnumerable<TVCam> tVCameraSets, string track)
|
||||
{
|
||||
if (!System.IO.File.Exists($"camDefs/{track}.json"))
|
||||
return;
|
||||
|
||||
var json = System.IO.File.ReadAllText($"camDefs/{track}.json");
|
||||
try
|
||||
{
|
||||
var camDefs = Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<TVCamJsonData>>(json);
|
||||
foreach (var item in tVCameraSets)
|
||||
{
|
||||
var camDef = camDefs.FirstOrDefault(x => string.Equals(x.SetName, item.Set) && string.Equals(x.CamName, item.Name));
|
||||
if (camDef != null)
|
||||
camDef.UpdateTVCam(item);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
<UserControl x:Class="ksBroadcastingTestClient.Autopilot.CarWeightCategoryView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:ksBroadcastingTestClient.Autopilot"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid>
|
||||
<ItemsControl ItemsSource="{Binding}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Category}" />
|
||||
<Slider Value="{Binding RawValue}" Minimum="0" Maximum="2" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</UserControl>
|
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for CarWeightCategoryView.xaml
|
||||
/// </summary>
|
||||
public partial class CarWeightCategoryView : UserControl
|
||||
{
|
||||
public CarWeightCategoryView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ksBroadcastingTestClient.Autopilot
|
||||
{
|
||||
public enum CarWeightCategoryEnum { Proximity, Pack, Position, FocusFast, FocusSlow, Pace }
|
||||
public class CarWeightCategoryViewModel : KSObservableObject
|
||||
{
|
||||
public CarWeightCategoryEnum Category { get; }
|
||||
public float RawValue { get => Get<float>(); set => Set(value); }
|
||||
public float WeightedValue { get => Get<float>(); set => Set(value); }
|
||||
public string Hint { get => Get<string>(); set => Set(value); }
|
||||
|
||||
public CarWeightCategoryViewModel(CarWeightCategoryEnum category)
|
||||
{
|
||||
Category = category;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user