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
{
///
/// 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
///
public class AutopilotViewModel : KSObservableObject
{
public AutopilotWeightsViewModel WeightsVM { get; } = new AutopilotWeightsViewModel();
public ObservableCollection Cars { get; } = new ObservableCollection();
public TrackViewModel TrackVM { get => Get(); private set => Set(value); }
public CameraManagementViewModel CamManagerVM { get; } = new CameraManagementViewModel();
public KSRelayCommand RequestFocusedCarCommand { get; }
public KSRelayCommand ToggleAutopilotCommand { get; }
public bool IsAutopilotActive { get => Get(); private set { Set(value); NotifyUpdate(nameof(AutopilotStateText)); } }
public string AutopilotStateText { get => IsAutopilotActive ? "Stop" : "Auto"; }
private List _clients = new List();
public List PressureCategoryWeights { get; } = new List();
public DateTime LastFocusChange { get => Get(); private set => Set(value); }
public DateTime LastCameraSetChange { get => Get(); private set => Set(value); }
public DateTime LastCameraChange { get => Get(); 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(); set => Set(value); }
public string CurrentHudPage { get; private set; }
public string AutopilotState { get => Get(); private set => Set(value); }
public ObservableCollection MiniLog { get; } = new ObservableCollection();
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())
{
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);
}
}
}