diff --git a/Assembly.version b/Assembly.version index df0e54f..b9ef449 100644 --- a/Assembly.version +++ b/Assembly.version @@ -2,6 +2,6 @@ 2 1 - 2 + 3 \ No newline at end of file diff --git a/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs b/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs index 560f74b..6833a81 100644 --- a/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs +++ b/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs @@ -4,6 +4,7 @@ using KeyValue.Runtime; using Model; using Model.AI; using Model.Ops; +using Model.Physics; using Network; using Network.Messages; using Railloader; @@ -15,7 +16,6 @@ using Track; using Track.Search; using UI; using UI.EngineControls; -using UnityEngine; using static Track.Search.RouteSearch; using Location = Track.Location; @@ -28,84 +28,51 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch { private static Serilog.ILogger _log => Log.ForContext(); - private static readonly HashSet _keyChangeObservers = new HashSet(); - private static readonly HashSet destinations = new HashSet(); - private static readonly HashSet consist = new HashSet(); + private static readonly HashSet _keyChangeObservers = new(); + private static readonly Dictionary> locoConsistDestinations = new(); + private static readonly HashSet consist = new(); private static string _lastLocoCarId = string.Empty; private static bool recalcing = false; + static string getDictKey(Car car) => car.DisplayName; + static void Postfix(AutoEngineerWaypointControls __instance, ref OptionsDropdownConfiguration __result) { - - _log.Information($"HI BOB"); - foreach(var o in _keyChangeObservers) o.Dispose(); - _keyChangeObservers.Clear(); - destinations.Clear(); - recalcing = false; + PrepLocoUsage(out BaseLocomotive selectedLoco, out int numberOfCars); TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; if (!tweaksAndThings.IsEnabled()) return; - //_log.Information($"HI2"); + IterateCarsDetectDestinations( + __instance, + __result, + selectedLoco, + numberOfCars, + out List rowDatas, + out Action func, + out int origCount, + out int maxRowOrig, + out AutoEngineerOrdersHelper aeoh + ); - List rowDatas = __result.Rows.ToList(); - var func = __result.OnRowSelected; - int origCount = rowDatas.Count; - int maxRowOrig = origCount - 1; + List<(string destinationId, string destination, float? distance, Location? location)> jumpTos = BuildJumpToOptions(__instance, selectedLoco); - var selectedLoco = TrainController.Shared.SelectedLocomotive; - if (selectedLoco.id != _lastLocoCarId || !(consist?.Any() ?? false)) - { - _lastLocoCarId = selectedLoco.id; - consist.Clear(); - consist.UnionWith(selectedLoco.EnumerateCoupled()?.ToList() ?? Enumerable.Empty()); - } - var aeoh = new AutoEngineerOrdersHelper(persistence: new AutoEngineerPersistence(selectedLoco.KeyValueObject), locomotive: selectedLoco); - List<(string destinationId, string destination, float? distance, Location? location)> jumpTos = new(); + __result = WireUpJumpTosToSettingMenu( + __instance, + selectedLoco, + rowDatas, + func, + origCount, + maxRowOrig, + aeoh, + ref jumpTos + ); + } - foreach(var c in consist) - { - AddObserversToCar(__instance, c); - OpsCarPosition? destination = c.Waybill.HasValue && !c.Waybill.Value.Completed ? c.Waybill.Value.Destination : null; - bool completed = c.Waybill?.Completed ?? false; - if (!destination.HasValue && c.TryGetOverrideDestination(OverrideDestination.Repair, OpsController.Shared, out (OpsCarPosition, string)? result)) destination = result.Value.Item1; - _log.Information($"{c.DisplayName} -> {destination.HasValue}"); - - string destId = destination?.Identifier ?? string.Empty; - - if (destinations.Contains(destId)) continue; - - if (destination.HasValue && !completed) - { - string destName = destination.Value.DisplayName; - float? distance = null; - - if (Graph.Shared.TryGetLocationFromPoint(destination.Value.Spans?.FirstOrDefault().GetSegments().FirstOrDefault(), destination.Value.Spans?.FirstOrDefault()?.GetCenterPoint() ?? default, 200f, out Location destLoc)) - { - - float trainMomentum = 0f; - Location start = StateManager.IsHost ? selectedLoco.AutoEngineerPlanner.RouteStartLocation(out trainMomentum) : RouteStartLocation(__instance, selectedLoco); - HeuristicCosts autoEngineer = HeuristicCosts.AutoEngineer; - List list = new List(); - var totLen = StateManager.IsHost ? selectedLoco.AutoEngineerPlanner.CalculateTotalLength() : CalculateTotalLength(selectedLoco); - distance = Graph.Shared.FindRoute(start, destLoc, autoEngineer, list, out var metrics, checkForCars: false, totLen, trainMomentum) - ? metrics.Distance - : null; - }; - _log.Information($"{c.DisplayName} -> {destName} {destId} {distance?.ToString()}"); - if (distance.HasValue) - { - destinations.Add(destId); - jumpTos.Add(( - destinationId: destId, - destination: $"WP> {destName}" - , distance: distance - , location: (Location?)destLoc - )); - } - } - } - - jumpTos = jumpTos?.OrderBy(c => c.distance)?.ToList() ?? []; + private static OptionsDropdownConfiguration WireUpJumpTosToSettingMenu(AutoEngineerWaypointControls __instance, BaseLocomotive selectedLoco, List rowDatas, Action func, int origCount, int maxRowOrig, AutoEngineerOrdersHelper aeoh, ref List<(string destinationId, string destination, float? distance, Location? location)> jumpTos) + { + OptionsDropdownConfiguration __result; + jumpTos = jumpTos?.OrderBy(c => c.distance ?? float.MaxValue)?.ToList() ?? []; + var localJumpTos = jumpTos.ToList(); var safetyFirst = AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.SafetyFirstGoverningApplies() && jumpTos.Any(); rowDatas.AddRange(jumpTos.Select(j => @@ -119,12 +86,12 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch rowDatas , delegate (int row) { - _log.Information($"{TrainController.Shared.SelectedLocomotive.DisplayName} row {row}/{jumpTos.Count}/{rowDatas.Count}"); + _log.Debug($"{TrainController.Shared.SelectedLocomotive.DisplayName} row {row}/{localJumpTos.Count}/{rowDatas.Count}"); if (row <= maxRowOrig) { func(row); } - if (row > maxRowOrig && jumpTos[row - origCount].location.HasValue) + if (row > maxRowOrig && localJumpTos[row - origCount].location.HasValue) { if (safetyFirst) { @@ -132,7 +99,7 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch return; } float trainMomentum = 0f; - Location end = jumpTos[row - origCount].location.Value; + Location end = localJumpTos[row - origCount].location.Value; Location start = RouteStartLocation(__instance, selectedLoco); HeuristicCosts autoEngineer = HeuristicCosts.AutoEngineer; List list = new List(); @@ -142,8 +109,9 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch { RouteSearch.Metrics metrics2; bool flag = Graph.Shared.FindRoute(start, end, autoEngineer, null, out metrics2); - Multiplayer.SendError(StateManager.Shared.PlayersManager.LocalPlayer, flag ? (selectedLoco.DisplayName + " Train too long to navigate to waypoint.") : (selectedLoco.DisplayName + " Unable to find a path to waypoint."), AlertLevel.Error); - } else + Multiplayer.SendError(StateManager.Shared.PlayersManager.LocalPlayer, flag ? (getDictKey(selectedLoco) + " Train too long to navigate to waypoint.") : (getDictKey(selectedLoco) + " Unable to find a path to waypoint."), AlertLevel.Error); + } + else { var mw = (location: end, carId: string.Empty); aeoh.SetWaypoint(mw.location, mw.carId); @@ -152,7 +120,118 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch } } ); + return __result; + } + private static List<(string destinationId, string destination, float? distance, Location? location)> BuildJumpToOptions(AutoEngineerWaypointControls __instance, BaseLocomotive selectedLoco) + { + List<(string destinationId, string destination, float? distance, Location? location)> jumpTos = new(); + foreach (var ocp in locoConsistDestinations[getDictKey(selectedLoco)].Values.Distinct()) + { + string destName = ocp.DisplayName; + string destId = ocp.Identifier; + float? distance = null; + + if ( + Graph.Shared.TryGetLocationFromPoint( + ocp.Spans?.FirstOrDefault().GetSegments().FirstOrDefault(), + ocp.Spans?.FirstOrDefault()?.GetCenterPoint() ?? default, + 200f, + out Location destLoc + ) + ) + { + float trainMomentum = 0f; + Location start = StateManager.IsHost ? selectedLoco.AutoEngineerPlanner.RouteStartLocation(out trainMomentum) : RouteStartLocation(__instance, selectedLoco); + HeuristicCosts autoEngineer = HeuristicCosts.AutoEngineer; + List list = new List(); + var totLen = StateManager.IsHost ? selectedLoco.AutoEngineerPlanner.CalculateTotalLength() : CalculateTotalLength(selectedLoco); + distance = Graph.Shared.FindRoute(start, destLoc, autoEngineer, list, out var metrics, checkForCars: false, totLen, trainMomentum) + ? metrics.Distance + : null; + }; + _log.Debug($"{getDictKey(selectedLoco)} -> {destName} {destId} {distance?.ToString()}"); + jumpTos.Add(( + destinationId: destId, + destination: $"WP> {destName}" + , distance: distance + , location: (Location?)destLoc + )); + } + + return jumpTos; + } + + private static void IterateCarsDetectDestinations(AutoEngineerWaypointControls __instance, OptionsDropdownConfiguration __result, BaseLocomotive selectedLoco, int numberOfCars, out List rowDatas, out Action func, out int origCount, out int maxRowOrig, out AutoEngineerOrdersHelper aeoh) + { + rowDatas = __result.Rows.ToList(); + func = __result.OnRowSelected; + origCount = rowDatas.Count; + maxRowOrig = origCount - 1; + if (!locoConsistDestinations.ContainsKey(getDictKey(selectedLoco))) locoConsistDestinations.Add(getDictKey(selectedLoco), new()); + + Dictionary seen = new(); + aeoh = new AutoEngineerOrdersHelper(persistence: new AutoEngineerPersistence(selectedLoco.KeyValueObject), locomotive: selectedLoco); + Car.LogicalEnd logicalEnd = ((selectedLoco.set.IndexOfCar(selectedLoco).GetValueOrDefault(0) >= numberOfCars / 2) ? Car.LogicalEnd.B : Car.LogicalEnd.A); + Car.LogicalEnd end = ((logicalEnd == Car.LogicalEnd.A) ? Car.LogicalEnd.B : Car.LogicalEnd.A); + bool stop = false; + int carIndex = selectedLoco.set.StartIndexForConnected(selectedLoco, logicalEnd, IntegrationSet.EnumerationCondition.Coupled); + Car car; + while (!stop && (car = selectedLoco.set.NextCarConnected(ref carIndex, logicalEnd, IntegrationSet.EnumerationCondition.Coupled, out stop)) != null) + { + AddObserversToCar(__instance, car); + var ocp = GetCarDestinationIdentifier(car); + _log.Debug($"{getDictKey(selectedLoco)} --> {getDictKey(car)} -> {ocp.HasValue} {ocp?.DisplayName}"); + + if (ocp.HasValue) + { + seen.Add(getDictKey(car), ocp.Value); + if (locoConsistDestinations[getDictKey(selectedLoco)].TryGetValue(getDictKey(car), out _)) + locoConsistDestinations[getDictKey(selectedLoco)][getDictKey(car)] = ocp.Value; + else + locoConsistDestinations[getDictKey(selectedLoco)].Add(getDictKey(car), ocp.Value); + } + } + + _log.Debug($"{getDictKey(selectedLoco)} --> [{seen.Keys.Count}] -> Seen -> {string.Join(Environment.NewLine, seen.Select(k => $"{k.Key}:{k.Value.DisplayName}"))}"); + _log.Debug($"{getDictKey(selectedLoco)} --> [{locoConsistDestinations[getDictKey(selectedLoco)].Keys.Count}] -> Cache -> {string.Join(Environment.NewLine, locoConsistDestinations[getDictKey(selectedLoco)].Select(k => $"{k.Key}:{k.Value.DisplayName}"))}"); + + var dropped = + locoConsistDestinations[getDictKey(selectedLoco)].Keys.Except(seen.Keys).ToDictionary( + t => t, + t => locoConsistDestinations[getDictKey(selectedLoco)][t] + ); //are no longer here + + _log.Debug($"{getDictKey(selectedLoco)} --> [{dropped.Keys.Count}] -> removed -> {string.Join(Environment.NewLine, dropped.Select(k => $"{k.Key}:{k.Value.DisplayName}"))}"); + + locoConsistDestinations[getDictKey(selectedLoco)] = + locoConsistDestinations[getDictKey(selectedLoco)].Keys.Intersect(seen.Keys).ToDictionary( + t => t, + t => locoConsistDestinations[getDictKey(selectedLoco)][t] + ); //remove ones that are no longer here + seen.Clear(); + } + + private static void PrepLocoUsage(out BaseLocomotive selectedLoco, out int numberOfCars) + { + //wire up that loco + selectedLoco = TrainController.Shared.SelectedLocomotive; + numberOfCars = selectedLoco.set.NumberOfCars; + _log.Debug($"{getDictKey(selectedLoco)} --> HI BOB[{numberOfCars}]"); + foreach (var o in _keyChangeObservers) o.Dispose(); + _keyChangeObservers.Clear(); + recalcing = false; + } + + private static OpsCarPosition? GetCarDestinationIdentifier(Car c) + { + OpsCarPosition? destination = null; + if (c.TryGetOverrideDestination(OverrideDestination.Repair, OpsController.Shared, out (OpsCarPosition, string)? result)) + destination = result.Value.Item1; + + if (!destination.HasValue && c.Waybill.HasValue && !c.Waybill.Value.Completed) + destination = c.Waybill.Value.Destination; + return destination; } private static Location RouteStartLocation(AutoEngineerWaypointControls __instance, BaseLocomotive _locomotive) { @@ -197,8 +276,28 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch key, delegate (Value value) { - _log.Information($"{car.DisplayName} OBSV {key}: {value}; recalcing {recalcing}"); if (recalcing) return; + bool waybillChng = (new[] { Car.KeyOpsWaybill, Car.KeyOpsRepairDestination }).Contains(key); + string? destId = + waybillChng ? + GetCarDestinationIdentifier(car)?.Identifier ?? null : + null; + if (waybillChng) + { + if (locoConsistDestinations.TryGetValue(getDictKey(TrainController.Shared.SelectedLocomotive), out Dictionary cars) + && cars.TryGetValue(getDictKey(car), out OpsCarPosition pos) + && pos.Identifier == destId) + { + return; + } else + { + _log.Debug($"{getDictKey(car)} OBSV {key}: destNew; {destId}; reload"); + } + } else + { + _log.Debug($"{getDictKey(car)} OBSV {key}: {value}"); + } + try { foreach(var o in _keyChangeObservers) @@ -209,7 +308,6 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch if (!TrainController.Shared.SelectRecall()) TrainController.Shared.SelectedCar = null; _keyChangeObservers.Clear(); recalcing = true; - new WaitForSeconds(0.25f); TrainController.Shared.SelectedCar = loco; } catch (Exception ex) @@ -221,4 +319,24 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch ) ); } + + private static Waybill? GetWaybill(Car car, Value waybillValue) + { + Waybill? _waybill = null; + try + { + _waybill = Model.Ops.Waybill.FromPropertyValue(waybillValue, OpsController.Shared); + } + catch (OpsController.InvalidOpsCarPositionException ex) + { + Log.Error(ex, "Waybill for car {car} contains an invalid ops position: {pos}", car, ex.Identifier); + _waybill = null; + } + catch (Exception exception) + { + Log.Warning(exception, "{car} Exception in Waybill.FromPropertyValue", car); + _waybill = null; + } + return _waybill; + } }