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 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(); 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> TVCameraSets { get; private set; } public Dictionary> OtherCameraSets { get; private set; } public Dictionary> 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 CamTypeLastActive { get; } = new Dictionary(); public float TVCamLearningProgress { get => Get(); 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>(); OtherCameraSets = new Dictionary>(); AllCameraSets = new Dictionary>(); 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()); AllCameraSets.Add(tvCamSet.Key, new List()); 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()); AllCameraSets.Add(camSet.Key, new List()); 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 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 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 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>(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); } } } }