diff --git a/TweaksAndThings/Patches/AutoEngineerControlSetBase_UpdateStatusLabel_Patch.cs b/TweaksAndThings/Patches/AutoEngineerControlSetBase_UpdateStatusLabel_Patch.cs index d48abda..ef57970 100644 --- a/TweaksAndThings/Patches/AutoEngineerControlSetBase_UpdateStatusLabel_Patch.cs +++ b/TweaksAndThings/Patches/AutoEngineerControlSetBase_UpdateStatusLabel_Patch.cs @@ -13,7 +13,7 @@ internal class AutoEngineerControlSetBase_UpdateStatusLabel_Patch static void Postfix(AutoEngineerControlSetBase __instance) { TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; - if (!tweaksAndThings.IsEnabled() || !AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.SafetyFirstGoverningApplies()) return; + if (!tweaksAndThings.IsEnabled() || !AutoEngineerPlanner_HandleCommand_Patch.SafetyFirstGoverningApplies(__instance.Locomotive)) return; string orig = __instance.statusLabel.text; __instance.statusLabel.text = $"{orig}; Safety"; diff --git a/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.cs b/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.cs deleted file mode 100644 index ec56c13..0000000 --- a/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Game.Messages; -using HarmonyLib; -using Model.AI; -using Model.Definition; -using Railloader; -using RMROC451.TweaksAndThings.Extensions; -using Serilog; -using System; -using System.Linq; -using UI.EngineControls; - -namespace RMROC451.TweaksAndThings.Patches; - - -[HarmonyPatch(typeof(AutoEngineerOrdersHelper))] -[HarmonyPatch(nameof(AutoEngineerOrdersHelper.SendAutoEngineerCommand), typeof(AutoEngineerMode), typeof(bool), typeof(int), typeof(float), typeof(OrderWaypoint))] -[HarmonyPatchCategory("RMROC451TweaksAndThings")] -internal class AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch -{ - - private static Serilog.ILogger _log => Log.ForContext(); - - static bool Prefix(AutoEngineerMode mode, ref int maxSpeedMph) - { - TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; - if (!tweaksAndThings.IsEnabled() || !tweaksAndThings.SafetyFirst()) return true; - - if (SafetyFirstGoverningApplies()) - { - int orig = maxSpeedMph; - int limitedSpeed = Math.Min(maxSpeedMph, 20); - maxSpeedMph = mode switch - { - AutoEngineerMode.Road => limitedSpeed, - AutoEngineerMode.Waypoint => limitedSpeed, - _ => maxSpeedMph, - }; - - if (orig != maxSpeedMph) - { - _log.Debug($"{Enum.GetName(typeof(AutoEngineerMode), mode)}[{TrainController.Shared.SelectedLocomotive.DisplayName}] {nameof(AutoEngineerOrdersExtensions.MaxSpeedMph)} limited to {limitedSpeed} from {orig}; No Caboose in Consist;"); - } - } - - return true; - - } - - internal static bool SafetyFirstGoverningApplies() - { - var _persistence = new AutoEngineerPersistence(TrainController.Shared.SelectedLocomotive.KeyValueObject); - var OrdersHelper = new AutoEngineerOrdersHelper(TrainController.Shared.SelectedLocomotive, _persistence); - - if (TrainController.Shared.SelectedLocomotive.EnumerateCoupled().All(c => c.IsCaboose() || c.MotivePower())) return false; - - bool cabooseReq = SingletonPluginBase.Shared.RequireConsistCabooseForOilerAndHotboxSpotter(); - string logMessage = $"\n{nameof(SafetyFirstGoverningApplies)}:{Enum.GetName(typeof(AutoEngineerMode), OrdersHelper.Mode)}[{TrainController.Shared.SelectedLocomotive.DisplayName}] "; - Func firstClass = () => - { - var output = TrainController.Shared.SelectedEngineExpress(); - logMessage += $"\nfirst class {output}"; - return output; - }; - - Func FreightConsist = () => - { - bool output = !TrainController.Shared.SelectedLocomotive.EnumerateCoupled().ConsistNoFreight(); - logMessage += $"\nFreightConsist? {output}"; - logMessage += " " + string.Join(" / ", TrainController.Shared.SelectedLocomotive.EnumerateCoupled().Where(c => !c.MotivePower()).Select(c => $"{c.id} {Enum.GetName(typeof(CarArchetype), c.Archetype)}")); - return output; - }; - - Func noCaboose = () => - { - bool output = TrainController.Shared.SelectedLocomotive.FindMyCabooseSansLoadRequirement() == null; - logMessage += $"\ncaboose? {!output}"; - return output; - }; - - logMessage += $"\nCaboose Required {cabooseReq}"; - - bool output = - cabooseReq && - !firstClass() && - FreightConsist() && - noCaboose(); - - logMessage += $"\nGovern AE? {output}"; - if (_log.IsEnabled(Serilog.Events.LogEventLevel.Verbose)) - logMessage += $"\n{Environment.StackTrace}"; - - _log.Debug(logMessage); - - return output; - } -} diff --git a/TweaksAndThings/Patches/AutoEngineerPlanner_HandleCommand_Patch.cs b/TweaksAndThings/Patches/AutoEngineerPlanner_HandleCommand_Patch.cs new file mode 100644 index 0000000..a750b54 --- /dev/null +++ b/TweaksAndThings/Patches/AutoEngineerPlanner_HandleCommand_Patch.cs @@ -0,0 +1,114 @@ +using Game; +using Game.Messages; +using Game.State; +using HarmonyLib; +using Model; +using Model.AI; +using Model.Definition; +using Network; +using Network.Messages; +using Railloader; +using RMROC451.TweaksAndThings.Extensions; +using Serilog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UI.EngineControls; +using UI.EngineRoster; +using static Unity.IO.LowLevel.Unsafe.AsyncReadManagerMetrics; +using static UnityEngine.InputSystem.InputRemoting; + +namespace RMROC451.TweaksAndThings.Patches; + +[HarmonyPatch(typeof(AutoEngineerPlanner))] +[HarmonyPatch(nameof(AutoEngineerPlanner.HandleCommand))] +[HarmonyPatchCategory("RMROC451TweaksAndThings")] +internal class AutoEngineerPlanner_HandleCommand_Patch +{ + private static Serilog.ILogger _log => Log.ForContext(); + private static int governedSpeed = 20; + + static bool Prefix(AutoEngineerPlanner __instance, ref AutoEngineerCommand command, ref IPlayer sender) + { + TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; + if (!tweaksAndThings.IsEnabled() || !tweaksAndThings.SafetyFirst() || (sender.IsRemote && !tweaksAndThings.SafetyFirstClientEnforce()) || command.MaxSpeedMph <= governedSpeed) return true; + BaseLocomotive loco = TrainController.Shared.SelectedLocomotive; + if (TrainController.Shared.TryGetCarForId(command.LocomotiveId, out Car c)) loco = (BaseLocomotive)c; + + if (SafetyFirstGoverningApplies(loco)) + { + int orig = command.MaxSpeedMph; + int limitedSpeed = Math.Min(command.MaxSpeedMph, governedSpeed); + command.MaxSpeedMph = command.Mode switch + { + AutoEngineerMode.Road => limitedSpeed, + AutoEngineerMode.Waypoint => limitedSpeed, + _ => command.MaxSpeedMph, + }; + + string message = $"{Enum.GetName(typeof(AutoEngineerMode), command.Mode)}[{loco.DisplayName}] governed{{0}}due to Safety First rules."; + if (orig != command.MaxSpeedMph) + { + message = string.Format(message, $" from {orig} to {command.MaxSpeedMph} MPH "); + }else + { + message = string.Format(message, " "); + } + _log.Debug(message); + Multiplayer.SendError(sender, message, AlertLevel.Info); + } + + return true; + } + + internal static bool SafetyFirstGoverningApplies(BaseLocomotive loco) + { + var _persistence = new AutoEngineerPersistence(loco.KeyValueObject); + var OrdersHelper = new AutoEngineerOrdersHelper(loco, _persistence); + + if (loco.EnumerateCoupled().All(c => c.IsCaboose() || c.MotivePower())) return false; + + bool cabooseReq = SingletonPluginBase.Shared.RequireConsistCabooseForOilerAndHotboxSpotter(); + string logMessage = $"\n{nameof(SafetyFirstGoverningApplies)}:{Enum.GetName(typeof(AutoEngineerMode), OrdersHelper.Mode)}[{loco.DisplayName}] "; + Func firstClass = () => + { + var output = TrainController.Shared.SelectedEngineExpress(); + logMessage += $"\nfirst class {output}"; + return output; + }; + + Func FreightConsist = () => + { + bool output = !loco.EnumerateCoupled().ConsistNoFreight(); + logMessage += $"\nFreightConsist? {output}"; + logMessage += " " + string.Join(" / ", loco.EnumerateCoupled().Where(c => !c.MotivePower()).Select(c => $"{c.id} {Enum.GetName(typeof(CarArchetype), c.Archetype)}")); + return output; + }; + + Func noCaboose = () => + { + bool output = loco.FindMyCabooseSansLoadRequirement() == null; + logMessage += $"\ncaboose? {!output}"; + return output; + }; + + logMessage += $"\nCaboose Required {cabooseReq}"; + + bool output = + cabooseReq && + !firstClass() && + FreightConsist() && + noCaboose(); + + logMessage += $"\nGovern AE? {output}"; + if (_log.IsEnabled(Serilog.Events.LogEventLevel.Verbose)) + logMessage += $"\n{Environment.StackTrace}"; + + _log.Debug(logMessage); + + return output; + } +} + diff --git a/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs b/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs index 8dd2ec8..6737d87 100644 --- a/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs +++ b/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs @@ -61,7 +61,7 @@ internal class LocomotiveControlsUIAdapter_UpdateCarText_Postfix() while (true) { - if (__instance._persistence.Orders.Mode != AutoEngineerMode.Waypoint) yield return wait; + if (__instance._persistence.Orders.Mode != AutoEngineerMode.Waypoint || ((AutoEngineerWaypointControls)__instance.aiWaypointControls).Locomotive == null) yield return wait; PrepLocoUsage((AutoEngineerWaypointControls)__instance.aiWaypointControls, out BaseLocomotive selectedLoco, out int numberOfCars); HashSet destinations = []; @@ -117,14 +117,14 @@ internal class LocomotiveControlsUIAdapter_UpdateCarText_Postfix() //_log.Information($"{locoKey} --> [{locoConsistDestinations.Count}] -> Cache -> {string.Join(Environment.NewLine, locoConsistDestinations.Select(k => $"{locoKey}:{k.Value.DisplayName}"))}"); output |= !locoConsistDestinations.SetEquals(destinations); - _log.Information($"{locoKey} 1-> {output}"); + //_log.Information($"{locoKey} 1-> {output}"); if (output) lastSeenIntegrationSetCount = default; output |= lastSeenIntegrationSetCount != selectedLoco.set.NumberOfCars; - _log.Information($"{locoKey} 2-> {output}"); + //_log.Information($"{locoKey} 2-> {output}"); //output |= __instance.optionsDropdown.scrollRect.content.childCount != (destinations.Count + timetableDestinations.Count + 1); //+1 for the default "JumpTo" entry) //_log.Information($"{locoKey} 2.5-> {output} {__instance.optionsDropdown.scrollRect.content.childCount} {(destinations.Count)} {timetableDestinations.Count}"); output |= selectedLoco.TryGetTimetableTrain(out _) && TimetableController.Shared.CurrentDocument.Modified != timetableSaveTime; - _log.Information($"{locoKey} 3-> {output}"); + //_log.Information($"{locoKey} 3-> {output}"); return output; } @@ -134,7 +134,7 @@ internal class LocomotiveControlsUIAdapter_UpdateCarText_Postfix() OptionsDropdownConfiguration __result; jumpTos = jumpTos?.OrderBy(c => c.sortDistance)?.ToList() ?? default; var localJumpTos = jumpTos.ToList(); - var safetyFirst = AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.SafetyFirstGoverningApplies() && jumpTos.Any(); + var safetyFirst = AutoEngineerPlanner_HandleCommand_Patch.SafetyFirstGoverningApplies(selectedLoco) && jumpTos.Any(); rowDatas.AddRange(jumpTos.Select(j => new DropdownMenu.RowData( @@ -227,11 +227,11 @@ internal class LocomotiveControlsUIAdapter_UpdateCarText_Postfix() if (selectedLoco.TryGetTimetableTrain(out Timetable.Train t)) { - _log.Information($"{getDictKey(selectedLoco)} -> {t.DisplayStringLong}"); + //_log.Information($"{getDictKey(selectedLoco)} -> {t.DisplayStringLong}"); foreach (var e in t.Entries) { var stp = TimetableController.Shared.GetAllStations().FirstOrDefault(ps => ps.code == e.Station); - _log.Information($"{getDictKey(selectedLoco)} -> {t.DisplayStringLong} -> {e.Station} {stp}"); + //_log.Information($"{getDictKey(selectedLoco)} -> {t.DisplayStringLong} -> {e.Station} {stp}"); if (stp != null) { try @@ -315,8 +315,8 @@ internal class LocomotiveControlsUIAdapter_UpdateCarText_Postfix() { //wire up that loco selectedLoco = __instance.Locomotive; - numberOfCars = selectedLoco.set.NumberOfCars; - _log.Debug($"{selectedLoco.id} --> HI BOB[{numberOfCars}]"); + numberOfCars = selectedLoco?.set.NumberOfCars ?? -1; + _log.Debug($"{selectedLoco?.id} --> HI BOB[{numberOfCars}]"); } private static OpsCarPosition? GetCarDestinationIdentifier(Car c) diff --git a/TweaksAndThings/Settings/Settings.cs b/TweaksAndThings/Settings/Settings.cs index 79eae63..c8e93af 100644 --- a/TweaksAndThings/Settings/Settings.cs +++ b/TweaksAndThings/Settings/Settings.cs @@ -28,6 +28,7 @@ public class Settings bool cabooseRequiredForLocoTagOilIndication, bool servicingFundPenalty, bool safetyFirst, + bool safetyFirstClientEnforce, CrewHourLoadMethod loadCrewHoursMethod, float cabeeseSearchRadiusFtInMeters, bool trainBrakeDisplayShowsColorsInCalloutMode @@ -42,6 +43,7 @@ public class Settings CabooseRequiredForLocoTagOilIndication = cabooseRequiredForLocoTagOilIndication; ServicingFundPenalty = servicingFundPenalty; SafetyFirst = safetyFirst; + SafetyFirstClientEnforce = safetyFirstClientEnforce; LoadCrewHoursMethod = loadCrewHoursMethod; CabeeseSearchRadiusFtInMeters = cabeeseSearchRadiusFtInMeters; TrainBrakeDisplayShowsColorsInCalloutMode = trainBrakeDisplayShowsColorsInCalloutMode; @@ -57,6 +59,7 @@ public class Settings public bool CabooseRequiredForLocoTagOilIndication; public bool ServicingFundPenalty; public bool SafetyFirst; + public bool SafetyFirstClientEnforce; public CrewHourLoadMethod LoadCrewHoursMethod; public float CabeeseSearchRadiusFtInMeters; public bool TrainBrakeDisplayShowsColorsInCalloutMode; @@ -136,6 +139,8 @@ public static class SettingsExtensions input?.settings?.ServicingFundPenalty ?? false; public static bool SafetyFirst(this TweaksAndThingsPlugin input) => input?.settings?.SafetyFirst ?? false; + public static bool SafetyFirstClientEnforce(this TweaksAndThingsPlugin input) => + input?.settings?.SafetyFirstClientEnforce ?? true; public static bool DayLoadCrewHours(this TweaksAndThingsPlugin input) => (input?.settings?.LoadCrewHoursMethod ?? CrewHourLoadMethod.Tracks) == CrewHourLoadMethod.Daily; public static bool TrainBrakeDisplayShowsColorsInCalloutMode(this TweaksAndThingsPlugin input) => diff --git a/TweaksAndThings/TweaksAndThingsPlugin.cs b/TweaksAndThings/TweaksAndThingsPlugin.cs index c5b9363..e7382f5 100644 --- a/TweaksAndThings/TweaksAndThingsPlugin.cs +++ b/TweaksAndThings/TweaksAndThingsPlugin.cs @@ -203,6 +203,23 @@ 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 SafetyFirstClient + if (settings?.SafetyFirst ?? false) + { + builder.Spacer(spacing); + builder.AddFieldToggle( + "Safety First! (Enforce Client Speed Restrictions)", + () => settings?.SafetyFirstClientEnforce ?? false, + delegate (bool enabled) + { + if (settings == null) settings = new(); + settings.SafetyFirstClientEnforce = enabled; + builder.Rebuild(); + } + ).Tooltip("Safety First! (Enforce Client Speed Restrictions)", $@"Enforce cabeese dominance on clients; uncheck to allow clients to override the 20mph restriction."); + } + #endregion + } private void UiUpdates(UIPanelBuilder builder)