diff --git a/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs b/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs index 1fc94ad..a71600c 100644 --- a/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs +++ b/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs @@ -1,4 +1,6 @@ -using Model.AI; +using Game.Messages; +using Model.AI; +using Network; using System; using System.Collections; using UnityEngine; @@ -61,7 +63,8 @@ namespace RMROC451.TweaksAndThings.Extensions if (car.HasHotbox && car.Oiled == 1f && cabooseRequired && foundCaboose) { _log.Information("AutoOiler {name}: {foundCaboose} repaired hotbox {car}", oiler.name, foundCaboose, car); - car.AdjustHotboxValue(); + Multiplayer.Broadcast($"{Hyperlink.To(oiler._originCar)}: \"{Hyperlink.To(car)} hotbox repaired!\""); + car.SendPropertyChange(PropertyChange.Control.Hotbox, false); } num += adjustedTimeToWalk; oiler._pendingRunDuration += adjustedTimeToWalk; diff --git a/TweaksAndThings/Extensions/Car_Extensions.cs b/TweaksAndThings/Extensions/Car_Extensions.cs index 5bf77fc..18f62c6 100644 --- a/TweaksAndThings/Extensions/Car_Extensions.cs +++ b/TweaksAndThings/Extensions/Car_Extensions.cs @@ -1,4 +1,5 @@ using Game.Messages; +using Game.State; using Helpers; using Model; using Model.Definition; @@ -64,6 +65,8 @@ public static class Car_Extensions public static bool IsCaboose(this Car car) => car.Archetype == Model.Definition.CarArchetype.Caboose; + public static Car? CarCaboose(this Car car) => car.IsCaboose() ? car : null; + public static bool IsCabooseAndStoppedForLoadRefresh(this Car car, bool isFull) => car.IsCaboose() && car.IsStopped(30f) && !isFull; public static Car? CabooseInConsist(this IEnumerable input) => input.FirstOrDefault(IsCaboose); @@ -96,12 +99,14 @@ public static class Car_Extensions t.TrainClass == Timetable.TrainClass.First; public static Car? FindMyCaboose(this Car car, float timeNeeded, bool decrement = false) => - car.CarsNearCurrentCar(timeNeeded, decrement).FindNearestCabooseFromNearbyCars(); + ( + car.CarCaboose() ?? car.CarsNearCurrentCar(timeNeeded, decrement).FindNearestCabooseFromNearbyCars() + )?.CabooseWithSufficientCrewHours(timeNeeded, decrement); public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, bool decrement = false) { Car? output = null; - if (!car.IsCaboose()) return null; + if (car is null || !car.IsCaboose()) return null; List loadSlots = car.Definition.LoadSlots; for (int i = 0; i < loadSlots.Count; i++) @@ -118,29 +123,24 @@ public static class Car_Extensions return output; } - private static Car FindNearestCabooseFromNearbyCars(this List<(Car car, bool crewCar, float distance)> source) => + private static Car? FindNearestCabooseFromNearbyCars(this IEnumerable<(Car car, bool crewCar, float distance)> source) => source - .OrderBy(c => c.crewCar ? 0 : 1) - .ThenBy(c => c.distance) - .Select(c => c.car) - .FirstOrDefault(); + ?.OrderBy(c => c.crewCar ? 0 : 1) + ?.ThenBy(c => c.distance) + ?.Select(c => c.car) + ?.FirstOrDefault(); - private static List<(Car car, bool crewCar, float distance)> CarsNearCurrentCar(this Car car, float timeNeeded, bool decrement) + private static IEnumerable<(Car car, bool crewCar, float distance)> CarsNearCurrentCar(this Car car, float timeNeeded, bool decrement) { + Area carArea = OpsController.Shared.ClosestArea(car); - var cabeese = - car.EnumerateCoupled().SelectMany(consistCar => - { - Vector3 position = consistCar.GetMotionSnapshot().Position; - Vector3 center = WorldTransformer.WorldToGame(position); - var o = TrainController.Shared - .CarIdsInRadius(center, SingletonPluginBase.Shared.CabeeseSearchRadiusInMeters()) - .Where(c => TrainController.Shared.CarForId(c).IsCaboose()); - return o; - }).Distinct().Select(c => TrainController.Shared.CarForId(c)); + var cabeese = OpsController.Shared + .CarsInArea(carArea) + .Select(c => TrainController.Shared.CarForId(c.Id)) + .Union(car.EnumerateCoupled()) + .Where(c => c.IsCaboose()); - - Log.Information($"{nameof(CarsNearCurrentCar)} => {cabeese.Count()}"); + //if (cabeese?.Any() ?? false) Log.Information($"{nameof(CarsNearCurrentCar)}[{car.DisplayName}] => {cabeese.Count()}"); List<(Car car, bool crewCar, float distance)> source = cabeese.Select(c => (car: c, crewCar: c.IsCrewCar(), distance: car.Distance(c))).ToList(); @@ -158,9 +158,16 @@ public static class Car_Extensions } public static bool IsCrewCar(this Car car) => - !string.IsNullOrEmpty(TrainController.Shared.SelectedLocomotive.trainCrewId) && - car.trainCrewId == TrainController.Shared.SelectedLocomotive.trainCrewId; + !string.IsNullOrEmpty(TrainController.Shared.SelectedLocomotive?.trainCrewId) && + car.trainCrewId == TrainController.Shared.SelectedLocomotive?.trainCrewId; - public static void AdjustHotboxValue(this Car car) => car.ControlProperties[PropertyChange.Control.Hotbox] = null; + //public static void AdjustHotboxValue(this Car car) => car.ControlProperties[PropertyChange.Control.Hotbox] = null; + public static void AdjustHotboxValue(this Car car, float hotboxValue = 0f) => + StateManager.ApplyLocal( + new PropertyChange( + car.id, PropertyChange.KeyForControl(PropertyChange.Control.Hotbox), + new FloatPropertyValue(hotboxValue) + ) + ); } diff --git a/TweaksAndThings/Patches/EngineRosterPanel_Populate_Patch.cs b/TweaksAndThings/Patches/EngineRosterPanel_Populate_Patch.cs new file mode 100644 index 0000000..ccb5389 --- /dev/null +++ b/TweaksAndThings/Patches/EngineRosterPanel_Populate_Patch.cs @@ -0,0 +1,31 @@ +using HarmonyLib; +using Railloader; +using System.Collections.Generic; +using System.Linq; +using UI; +using UI.EngineRoster; + +namespace RMROC451.TweaksAndThings.Patches; + + +[HarmonyPatch(typeof(EngineRosterPanel))] +[HarmonyPatch(nameof(EngineRosterPanel.Populate))] +[HarmonyPatchCategory("RMROC451TweaksAndThings")] +internal class EngineRosterPanel_Populate_Patch +{ + private static bool Prefix(EngineRosterPanel __instance, ref List rows) + { + TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; + + __instance._window.Title = __instance._window.Title.Split(':')[0].Trim(); + if (!tweaksAndThings.IsEnabled()) return true; + + var hiddenEntries = rows.Where(r => r.Engine.IsMuEnabled && !r.IsSelected && !r.IsFavorite).Select(r => r.Engine.id) ?? Enumerable.Empty(); + + if (hiddenEntries.Any()) __instance._window.Title =string.Format("{0} : {1}", __instance._window.Title, $"Hidden MU Count [{hiddenEntries.Count()}]"); + + rows = rows.Where(r => !hiddenEntries.Contains(r.Engine.id)).ToList(); + + return true; + } +} diff --git a/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs b/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs index 5d21336..b49e4b5 100644 --- a/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs +++ b/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs @@ -24,6 +24,8 @@ namespace RMROC451.TweaksAndThings.Patches; [HarmonyPatchCategory("RMROC451TweaksAndThings")] internal class EngineRosterRow_Refresh_Patch { + private static Serilog.ILogger _log => Log.ForContext(); + public static void Postfix(EngineRosterRow __instance) { TweaksAndThingsPlugin? tweaksAndThings = SingletonPluginBase.Shared; @@ -35,46 +37,60 @@ internal class EngineRosterRow_Refresh_Patch if (tweaksAndThings == null || rosterFuelColumnSettings == null || !tweaksAndThings.IsEnabled() || - rosterFuelColumnSettings.EngineRosterFuelStatusColumn == EngineRosterFuelDisplayColumn.None || (!GameInput.IsAltDown && !rosterFuelColumnSettings.EngineRosterShowsFuelStatusAlways)) + rosterFuelColumnSettings.EngineRosterFuelStatusColumn == EngineRosterFuelDisplayColumn.None || (!GameInput.IsAltDown && !rosterFuelColumnSettings.EngineRosterShowsFuelStatusAlways) || + __instance._engine.IsMuEnabled + ) { return; } try { - IEnumerable consist = __instance._engine.EnumerateCoupled().Where(c => c.EnableOiling); + Car engineOrTender = __instance._engine; + IEnumerable locos = engineOrTender.EnumerateCoupled().Where(c => c.IsLocomotive).ToList(); + IEnumerable consist = engineOrTender.EnumerateCoupled().Where(c => c.EnableOiling).ToList(); bool cabooseRequirementFulfilled = !tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() || consist.ConsistNoFreight() - || (bool)__instance._engine.FindMyCaboose(0.0f, false); - float offendingPercentage = 0; - Car engineOrTender = __instance._engine; - List loadSlots = __instance._engine.Definition.LoadSlots; - if (!loadSlots.Any()) - { - engineOrTender = __instance._engine.DetermineFuelCar()!; - loadSlots = engineOrTender != null ? engineOrTender.Definition.LoadSlots : Enumerable.Empty().ToList(); - } + || (bool)engineOrTender.FindMyCaboose(0.0f, false); + float offendingPercentage = 100f; - var offender = loadSlots.OrderBy(ls => (engineOrTender.GetLoadInfo(ls.RequiredLoadIdentifier, out int slotIndex)?.Quantity ?? 0) / loadSlots[slotIndex].MaximumCapacity).FirstOrDefault().RequiredLoadIdentifier; - - for (int i = 0; i < loadSlots.Count; i++) + foreach (Car loco in locos) { - CarLoadInfo? loadInfo = engineOrTender.GetLoadInfo(i); - if (loadInfo.HasValue) + var investigate = loco; + List loadSlots = investigate.Definition.LoadSlots; + if (!loadSlots.Any()) { - CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault(); - var fuelLevel = FuelLevel(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity); - offendingPercentage = CalcPercentLoad(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity); - fuelInfoText += loadSlots[i].RequiredLoadIdentifier == offender ? fuelLevel + " " : string.Empty; - //fuelInfoText += TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity) + " "; - fuelInfoTooltip += $"{TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity)} {valueOrDefault.LoadString(CarPrototypeLibrary.instance.LoadForId(valueOrDefault.LoadId))}\n"; + investigate = investigate.DetermineFuelCar()!; + loadSlots = investigate != null ? investigate.Definition.LoadSlots : Enumerable.Empty().ToList(); + } + + var offender = loadSlots.OrderBy(ls => (investigate.GetLoadInfo(ls.RequiredLoadIdentifier, out int slotIndex)?.Quantity ?? 0) / loadSlots[slotIndex].MaximumCapacity).FirstOrDefault().RequiredLoadIdentifier; + + + for (int i = 0; i < loadSlots.Count; i++) + { + CarLoadInfo? loadInfo = investigate.GetLoadInfo(i); + if (loadInfo.HasValue) + { + CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault(); + var fuelLevel = FuelLevel(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity); + var offenderCheck = CalcPercentLoad(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity); + + if (offenderCheck < offendingPercentage) + { + offendingPercentage = offenderCheck; + fuelInfoText = loadSlots[i].RequiredLoadIdentifier == offender ? $"{fuelLevel} " : string.Empty; + } + + fuelInfoTooltip += $"{TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity)} {valueOrDefault.LoadString(CarPrototypeLibrary.instance.LoadForId(valueOrDefault.LoadId))} {(!loco.id.Equals(__instance._engine.id) ? $"{loco.DisplayName}" : "")}\n"; + } } } try { - if (cabooseRequirementFulfilled && StateManager.Shared.Storage.OilFeature) + if (cabooseRequirementFulfilled && StateManager.Shared.Storage.OilFeature && consist.Any()) { float lowestOilLevel = consist.OrderBy(c => c.Oiled).FirstOrDefault().Oiled; var oilLevel = FuelLevel(lowestOilLevel, 1); diff --git a/TweaksAndThings/RMROC451.TweaksAndThings.csproj b/TweaksAndThings/RMROC451.TweaksAndThings.csproj index d35475b..c4d70df 100644 --- a/TweaksAndThings/RMROC451.TweaksAndThings.csproj +++ b/TweaksAndThings/RMROC451.TweaksAndThings.csproj @@ -1,8 +1,13 @@  - - + + + False + + + False + portable diff --git a/TweaksAndThings/TweaksAndThingsPlugin.cs b/TweaksAndThings/TweaksAndThingsPlugin.cs index 271ed19..7a623b2 100644 --- a/TweaksAndThings/TweaksAndThingsPlugin.cs +++ b/TweaksAndThings/TweaksAndThingsPlugin.cs @@ -1,6 +1,7 @@ // Ignore Spelling: RMROC using GalaSoft.MvvmLight.Messaging; +using Game; using Game.Messages; using Game.State; using HarmonyLib; @@ -205,24 +206,6 @@ AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 - ).Tooltip("Safety First", $@"On non-express timetabled consists, a caboose is required in the consist increase AE max speed > 20 in {Enum.GetName(typeof(AutoEngineerMode), AutoEngineerMode.Road)}/{Enum.GetName(typeof(AutoEngineerMode), AutoEngineerMode.Waypoint)} mode."); #endregion - #region CabeeseSearchRadius - builder.Spacer(spacing); - builder.AddField( - "Cabeese Search Radius", - builder.AddSlider( - () => this.CabeeseSearchRadiusInMeters(), - () => $"{string.Format(Mathf.CeilToInt(this.CabeeseSearchRadiusInMeters() * 3.28084f).ToString(), "N0")}ft", - delegate (float input) { - settings = settings ?? new(); - settings.CabeeseSearchRadiusFtInMeters = Mathf.CeilToInt(input); - builder.Rebuild(); - }, - minValue: 1f, - maxValue: Mathf.CeilToInt(5280f / 2f / 3.28084f), - wholeNumbers: true - ) - ).Tooltip("Cabeese Catchment Area", "How far should the cabeese hunting logic look away from the cars in the area to find a caboose?"); - #endregion } private void UiUpdates(UIPanelBuilder builder)