diff --git a/Assembly.version b/Assembly.version index 45f4823..8829fb5 100644 --- a/Assembly.version +++ b/Assembly.version @@ -1,7 +1,7 @@ 2 - 0 - 1 + 1 + 7 \ No newline at end of file diff --git a/README.md b/README.md index 8911242..0255838 100644 --- a/README.md +++ b/README.md @@ -28,22 +28,191 @@ This mod requires Railloader by Zamu. ### What does this mod do? **PLEASE READ AS THE WAY THIS MOD FUNCTIONS HAS CHANGED FROM PRIOR VERSIONS** -1. Car Inspector : Handbrake & Air Line Helper - * Gives two buttons that will scan the current car's connections, including the whole consist, and automatically release hand brakes, set anglecocks and connect glad hands. -2. Car Tag Updates - * Shows an indication of which cars in the FOV have Air System or Handbrake issues. - * **hold SHIFT** to only show the tags in the FOV for cars with an issue! -3. Discord Webhooks - * Allows the console messages to post to a discord webhook. useful for those wanting to keep an eye on 24/7 hosted saves. - * Locomotive messages grab the locomotive `Ident.RoadNumber` and check the `CTC Panel Markers` if they exist. If found, they will use the red/green color and embed the locomotive as an image in the message. If no marker is found, it defaults to blue. - * Currently, One person per server should have this per discord webhook, otherwise you will get duplicate messages to the webhook. - * **Multiple hooks**: Allows for many different webhooks per client to be setup, and filtered to the `Ident.ReportingMark` so you can get messages to different hooks based on what save/server you are playing on. - * **Customizable** from the in-game Railloader settings, find `RMROC451.TweaksAndThings` +Basically, this mod has a couple zones of focus. Caboose tweaks and other QOL things. Some of those QOL things, I added the option for the cabeese to be required & charge you a "crew salary" to utilize, or pay a monetary penalty. + +I was disappointed the vanilla cabeese were largely for show, didn't provide any real reason to have them except for role playing. + +Enter Tweaks and Things. + +### QOL & Cabeese Modifications: + ### Does this work in Multiplayer? Yes, these are client side mods. Host doesn't need to have them. ### What version of Railroader does this mod work with? -2024.4.4 +2024.6 -> [Full Requirements](./TweaksAndThings/Definition.json) *Special thanks and credit to Zamu for creating Railloader and for help with making the mod a bit more robust.* diff --git a/TweaksAndThings/Commands/CrewUpdateCommand.cs b/TweaksAndThings/Commands/CrewUpdateCommand.cs index a949ac9..b1bec51 100644 --- a/TweaksAndThings/Commands/CrewUpdateCommand.cs +++ b/TweaksAndThings/Commands/CrewUpdateCommand.cs @@ -2,6 +2,7 @@ using Game.State; using Helpers; using Model.Ops; +using Model.Ops.Timetable; using Network; using RMROC451.TweaksAndThings.Extensions; using RMROC451.TweaksAndThings.Patches; @@ -46,9 +47,16 @@ public class EchoCommand : IConsoleCommand EntityReference loco = new EntityReference(EntityType.Car, car.id); if (comps[2] == "+") message = new Hyperlink(entityReference.URI(), string.Format(message, OpsController.Shared.ClosestArea(car)?.name ?? "???")); - car.PostNotice(nameof(EchoCommand), $"{message} :{StateManager.Shared._playersManager.LocalPlayer}"); - ExpandedConsole_Add_Patch.SendMs(null, $"{Hyperlink.To(car)} {message}"); - Multiplayer.Broadcast($"{StateManager.Shared._playersManager.LocalPlayer} {Hyperlink.To(car)}: \"{message}\""); + string hlt = Hyperlink.To(car); + hlt = car.TryGetTimetableTrain(out Timetable.Train t) ? hlt.Replace(car.DisplayName, t.DisplayStringLong) : hlt; + + if (StateManager.IsHost) + { + car.PostNotice(nameof(EchoCommand), $"{message} :{StateManager.Shared._playersManager.LocalPlayer}"); + + ExpandedConsole_Add_Patch.SendMs(null, $"{StateManager.Shared._playersManager.LocalPlayer} {hlt} {message}"); + } + if (!StateManager.IsHost) Multiplayer.Broadcast($"{StateManager.Shared._playersManager.LocalPlayer} {hlt}: \"{message}\""); return string.Empty; } diff --git a/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs b/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs index a71600c..e229595 100644 --- a/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs +++ b/TweaksAndThings/Extensions/AutoEngineer_Extensions.cs @@ -19,7 +19,7 @@ namespace RMROC451.TweaksAndThings.Extensions public static IEnumerator MrocAutoOilerLoop(this AutoOiler oiler, Serilog.ILogger _log, bool cabooseRequired) { int originIndex = oiler.FindOriginIndex(); - Model.Car? foundCaboose = oiler._originCar.FindMyCaboose(0.0f, false); + Model.Car? foundCaboose = oiler._originCar.FindMyCabooseSansLoadRequirement(); if (originIndex < 0) { _log.Error("Couldn't find origin car {car}", oiler._originCar); @@ -30,7 +30,7 @@ namespace RMROC451.TweaksAndThings.Extensions yield break; } oiler._reverse = originIndex > oiler._cars.Count - originIndex; - _log.Information( + _log.Debug( "AutoOiler {name} starting, rev = {reverse}, caboose required = {req}, caboose halving adjustment = {hasCaboose}, oil limit = {limit}", oiler.name, oiler._reverse, @@ -41,7 +41,7 @@ namespace RMROC451.TweaksAndThings.Extensions while (true) { yield return new WaitForSeconds(AutoOiler.StartDelay.CabooseHalvedFloat(foundCaboose)); - foundCaboose = oiler._originCar.FindMyCaboose(0.0f,false); + foundCaboose = oiler._originCar.FindMyCabooseSansLoadRequirement(); int carIndex = originIndex; float adjustedTimeToWalk = AutoOiler.TimeToWalkCar.CabooseHalvedFloat(foundCaboose); do @@ -58,11 +58,11 @@ namespace RMROC451.TweaksAndThings.Extensions num += num3; oiler._pendingRunDuration += num3; oiler._oiledCount++; - _log.Information("AutoOiler {name}: oiled {car} from {orig} => {new}", oiler.name, car, origOil, car.Oiled); + _log.Debug("AutoOiler {name}: oiled {car} from {orig} => {new}", oiler.name, car, origOil, car.Oiled); } if (car.HasHotbox && car.Oiled == 1f && cabooseRequired && foundCaboose) { - _log.Information("AutoOiler {name}: {foundCaboose} repaired hotbox {car}", oiler.name, foundCaboose, car); + _log.Debug("AutoOiler {name}: {foundCaboose} repaired hotbox {car}", oiler.name, foundCaboose, car); Multiplayer.Broadcast($"{Hyperlink.To(oiler._originCar)}: \"{Hyperlink.To(car)} hotbox repaired!\""); car.SendPropertyChange(PropertyChange.Control.Hotbox, false); } @@ -80,7 +80,7 @@ namespace RMROC451.TweaksAndThings.Extensions public static IEnumerator MrocAutoHotboxSpotterLoop(this AutoHotboxSpotter spotter, Serilog.ILogger _log, bool cabooseRequired) { - Func foundCaboose = () => spotter._locomotive.FindMyCaboose(0.0f, false); + Func foundCaboose = () => spotter._locomotive.FindMyCabooseSansLoadRequirement(); while (true) { if (!spotter.HasCars) @@ -89,7 +89,7 @@ namespace RMROC451.TweaksAndThings.Extensions continue; } var fc = foundCaboose(); - _log.Information("AutoHotboxSpotter {name}: Hotbox Spotter Running, Found Caboose => {hasCaboose}; Has Cars {hasCars}; Requires Caboose {requiresCaboose}", + _log.Debug("AutoHotboxSpotter {name}: Hotbox Spotter Running, Found Caboose => {hasCaboose}; Has Cars {hasCars}; Requires Caboose {requiresCaboose}", spotter.name, fc, spotter.HasCars, cabooseRequired); if (CabooseRequirementChecker(string.Format("{0} {1}", spotter.GetType().Name, spotter.name), cabooseRequired, fc, _log)) { @@ -104,7 +104,7 @@ namespace RMROC451.TweaksAndThings.Extensions { var numOrig = num; num = Random.Range(15, 30); - _log.Information("AutoHotboxSpotter {name}: Next check went from num(60,300) => {numOrig}; to num(15,30) => {hasCaboose}; Requires Caboose {requiresCaboose}", spotter.name, numOrig, num, fc, cabooseRequired); + _log.Debug("AutoHotboxSpotter {name}: Next check went from num(60,300) => {numOrig}; to num(15,30) => {hasCaboose}; Requires Caboose {requiresCaboose}", spotter.name, numOrig, num, fc, cabooseRequired); } yield return new WaitForSeconds(num); spotter.CheckForHotbox(); diff --git a/TweaksAndThings/Extensions/Car_Extensions.cs b/TweaksAndThings/Extensions/Car_Extensions.cs index 18f62c6..05aa655 100644 --- a/TweaksAndThings/Extensions/Car_Extensions.cs +++ b/TweaksAndThings/Extensions/Car_Extensions.cs @@ -7,6 +7,7 @@ using Model.Definition.Data; using Model.Ops; using Model.Ops.Timetable; using Railloader; +using RMROC451.TweaksAndThings.Patches; using Serilog; using System; using System.Collections.Generic; @@ -98,21 +99,28 @@ public static class Car_Extensions input.SelectedLocomotive.TryGetTimetableTrain(out Timetable.Train t) && t.TrainClass == Timetable.TrainClass.First; - public static Car? FindMyCaboose(this Car car, float timeNeeded, bool decrement = false) => + public static Car? FindMyCabooseSansLoadRequirement(this Car car) => + FindMyCaboose(car, 0f, decrement: false, requireLoad: false); + + public static Car? FindMyCabooseWithLoadRequirement(this Car car, float timeNeeded, bool decrement) => + FindMyCaboose(car, timeNeeded, decrement, requireLoad: true); + + private static Car? FindMyCaboose(this Car car, float timeNeeded, bool decrement = false, bool requireLoad = true) => ( car.CarCaboose() ?? car.CarsNearCurrentCar(timeNeeded, decrement).FindNearestCabooseFromNearbyCars() - )?.CabooseWithSufficientCrewHours(timeNeeded, decrement); + )?.CabooseWithSufficientCrewHours(timeNeeded: timeNeeded, requireLoad:requireLoad, decrement: decrement); - public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, bool decrement = false) + public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, bool requireLoad, bool decrement = false) { Car? output = null; if (car is null || !car.IsCaboose()) return null; + if (!requireLoad) return car; List loadSlots = car.Definition.LoadSlots; for (int i = 0; i < loadSlots.Count; i++) { CarLoadInfo? loadInfo = car.GetLoadInfo(i); - if (loadInfo.HasValue) + if (loadInfo.HasValue && loadInfo.Value.LoadId == OpsController_AnnounceCoalescedPayments_Patch.CrewLoadHours.id) { CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault(); output = valueOrDefault.Quantity >= timeNeeded ? car : null; @@ -140,7 +148,7 @@ public static class Car_Extensions .Union(car.EnumerateCoupled()) .Where(c => c.IsCaboose()); - //if (cabeese?.Any() ?? false) Log.Information($"{nameof(CarsNearCurrentCar)}[{car.DisplayName}] => {cabeese.Count()}"); + if (cabeese?.Any() ?? false) Log.Debug($"{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(); 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/AutoEngineerDestinationPicker_Loop_Patch.cs b/TweaksAndThings/Patches/AutoEngineerDestinationPicker_Loop_Patch.cs new file mode 100644 index 0000000..8b260f1 --- /dev/null +++ b/TweaksAndThings/Patches/AutoEngineerDestinationPicker_Loop_Patch.cs @@ -0,0 +1,60 @@ +using HarmonyLib; +using Helpers; +using Railloader; +using Serilog; +using System.Collections; +using Track; +using UI; +using UnityEngine; +using static UI.AutoEngineerDestinationPicker; + +namespace RMROC451.TweaksAndThings.Patches; + +[HarmonyPatch(typeof(AutoEngineerDestinationPicker))] +[HarmonyPatch(nameof(AutoEngineerDestinationPicker.Loop))] +[HarmonyPatchCategory("RMROC451TweaksAndThings")] +internal class AutoEngineerDestinationPicker_Loop_Patch +{ + static bool Prefix(AutoEngineerDestinationPicker __instance, ref IEnumerator __result) + { + TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; + if (!tweaksAndThings.IsEnabled()) return true; + + __result = Loop(__instance); + + return false; + } + + private static IEnumerator Loop(AutoEngineerDestinationPicker __instance) + { + Hit valueOrDefault; + Location location; + WaitForSecondsRealtime wait = new WaitForSecondsRealtime(1/60); + while (true) + { + Location? currentOrdersGotoLocation = __instance.GetCurrentOrdersGotoLocation(); + Hit? hit = __instance.HitLocation(); + if (hit.HasValue) + { + valueOrDefault = hit.GetValueOrDefault(); + location = valueOrDefault.Location; + Graph.PositionRotation positionRotation = __instance._graph.GetPositionRotation(location); + __instance.destinationMarker.position = WorldTransformer.GameToWorld(positionRotation.Position); + __instance.destinationMarker.rotation = positionRotation.Rotation; + __instance.destinationMarker.gameObject.SetActive(value: true); + if (!currentOrdersGotoLocation.Equals(location) && __instance.MouseClicked) + { + break; + } + } + else + { + __instance.destinationMarker.gameObject.SetActive(value: false); + } + yield return wait; + } + Log.Debug("DestinationPicker Hit: {hit} {car} {end}", valueOrDefault.Location, valueOrDefault.CarInfo?.car, valueOrDefault.CarInfo?.end); + __instance._ordersHelper.SetWaypoint(location, valueOrDefault.CarInfo?.car.id); + __instance.StopLoop(); + } +} diff --git a/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.cs b/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.cs deleted file mode 100644 index 623a66f..0000000 --- a/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.cs +++ /dev/null @@ -1,92 +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.Information($"{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); - - 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 = () => - { - var output = TrainController.Shared.SelectedLocomotive.FindMyCaboose(0.0f, false) == null; - logMessage += $"\ncaboose? {!output}"; - return output; - }; - - logMessage += $"\nCaboose Required {cabooseReq}"; - - bool output = - cabooseReq && - !firstClass() && - FreightConsist() && - noCaboose(); - - logMessage += $"\nGovern AE? {output}"; - - _log.Information(logMessage); - - return output; - } -} diff --git a/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SetWaypoint_patch.cs b/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SetWaypoint_patch.cs deleted file mode 100644 index a512189..0000000 --- a/TweaksAndThings/Patches/AutoEngineerOrdersHelper_SetWaypoint_patch.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Game.Notices; -using HarmonyLib; -using Model; -using Serilog; -using UI.EngineControls; -using UnityEngine; - -namespace RMROC451.TweaksAndThings.Patches; - - -[HarmonyPatch(typeof(AutoEngineerOrdersHelper))] -[HarmonyPatch(nameof(AutoEngineerOrdersHelper.SetWaypoint), typeof(Track.Location), typeof(string))] -[HarmonyPatchCategory("RMROC451TweaksAndThings")] -internal class AutoEngineerOrdersHelper_SetWaypoint_patch -{ - private static Serilog.ILogger _log => Log.ForContext(); - static void Postfix(AutoEngineerOrdersHelper __instance, Track.Location location, string coupleToCarId) - { - _log.Information($"start setWP"); - Car selectedLoco = __instance._locomotive; - _log.Information($"{selectedLoco?.DisplayName ?? ""} set WP"); - Vector3 gamePoint = location.GetPosition(); - EntityReference entityReference = new EntityReference(EntityType.Position, new Vector4(gamePoint.x, gamePoint.y, gamePoint.z, 0)); - selectedLoco.PostNotice("ai-wpt-rmroc451", new Hyperlink(entityReference.URI(), $"WP SET")); - } -} diff --git a/TweaksAndThings/Patches/AutoEngineerPlanner_HandleCommand_Patch.cs b/TweaksAndThings/Patches/AutoEngineerPlanner_HandleCommand_Patch.cs new file mode 100644 index 0000000..fa3edbc --- /dev/null +++ b/TweaksAndThings/Patches/AutoEngineerPlanner_HandleCommand_Patch.cs @@ -0,0 +1,159 @@ +using Game; +using Game.Messages; +using Game.Notices; +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 Track; +using UI.EngineControls; +using UI.EngineRoster; +using UnityEngine; +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; + LocoNoticeWPSet(__instance, command, sender); + if (!tweaksAndThings.IsEnabled() || !tweaksAndThings.SafetyFirst() || (sender.IsRemote && !tweaksAndThings.SafetyFirstClientEnforce()) || command.MaxSpeedMph <= governedSpeed) return true; + BaseLocomotive loco = __instance._locomotive; + + 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; + } + + private static void LocoNoticeWPSet(AutoEngineerPlanner __instance, AutoEngineerCommand command, IPlayer sender) + { + OrderWaypoint? wp = + string.IsNullOrEmpty(command.WaypointLocationString) ? + null : + new OrderWaypoint?(new OrderWaypoint(command.WaypointLocationString, command.WaypointCoupleToCarId)); + + if ( + wp.HasValue && + !string.IsNullOrEmpty(wp.Value.LocationString) && + !__instance._orders.Waypoint.Equals(wp) + ) + { + _log.Debug($"start setWP"); + Car selectedLoco = __instance._locomotive; + _log.Debug($"{selectedLoco?.DisplayName ?? ""} set WP"); + if (LocationPositionFromString(wp.Value, out Vector3 gamePoint)) + { + EntityReference entityReference = new EntityReference(EntityType.Position, new Vector4(gamePoint.x, gamePoint.y, gamePoint.z, 0)); + selectedLoco.PostNotice("ai-wpt-rmroc451", new Hyperlink(entityReference.URI(), $"WP SET [{sender.Name}]")); + } + } + } + + internal static bool LocationPositionFromString(OrderWaypoint waypoint, out Vector3 position) + { + Location location; + position = default; + try + { + location = Graph.Shared.ResolveLocationString(waypoint.LocationString); + position = location.GetPosition(); + return true; + } + catch (Exception exception) + { + Log.Error(exception, "Couldn't get location from waypoint: {locStr}", waypoint.LocationString); + return false; + } + } + + 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 new file mode 100644 index 0000000..6737d87 --- /dev/null +++ b/TweaksAndThings/Patches/AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch.cs @@ -0,0 +1,369 @@ +using Game.Messages; +using Game.State; +using HarmonyLib; +using Model; +using Model.AI; +using Model.Ops; +using Model.Ops.Timetable; +using Network; +using Network.Messages; +using Railloader; +using Serilog; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Track; +using Track.Search; +using UI; +using UI.EngineControls; +using UnityEngine; +using UnityEngine.UI; +using static Track.Search.RouteSearch; +using Location = Track.Location; + +namespace RMROC451.TweaksAndThings.Patches; + +[HarmonyPatch(typeof(LocomotiveControlsUIAdapter))] +[HarmonyPatch(nameof(LocomotiveControlsUIAdapter.UpdateCarText))] +[HarmonyPatchCategory("RMROC451TweaksAndThings")] +internal class LocomotiveControlsUIAdapter_UpdateCarText_Postfix() +{ + private static Serilog.ILogger _log => Log.ForContext(); + private static int lastSeenIntegrationSetCount = default; + private static string? lastLocoSeenCarId = default; + private static Coroutine? watchyWatchy = null; + private static HashSet locoConsistDestinations = []; + private static Game.GameDateTime? timetableSaveTime = null; + static string getDictKey(Car car) => car.DisplayName; + + static void Postfix(LocomotiveControlsUIAdapter __instance) + { + try + { + + if (lastLocoSeenCarId != null && lastLocoSeenCarId.Equals(TrainController.Shared?.SelectedLocomotive.id) && watchyWatchy != null) return; + if (watchyWatchy != null) ((MonoBehaviour)__instance).StopCoroutine(watchyWatchy); + watchyWatchy = null; + + if (__instance._persistence.Orders.Mode == AutoEngineerMode.Waypoint) watchyWatchy = ((MonoBehaviour)__instance).StartCoroutine(UpdateCogCoroutine(__instance)); + } + catch (Exception ex) + { + _log.Error(ex, "I have a very unique set of skills; I will find you and I will squash you."); + } + } + + public static IEnumerator UpdateCogCoroutine(LocomotiveControlsUIAdapter __instance) + { + TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; + WaitForSecondsRealtime wait = new WaitForSecondsRealtime(3f); + + while (true) + { + 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 = []; + if (!tweaksAndThings.IsEnabled() || !ShouldRecalc(__instance, selectedLoco, out destinations)) yield return wait; + timetableSaveTime = TimetableController.Shared.CurrentDocument.Modified; + lastSeenIntegrationSetCount = selectedLoco.set.NumberOfCars; + + IterateCarsDetectDestinations( + (AutoEngineerWaypointControls)__instance.aiWaypointControls, + ((AutoEngineerWaypointControls)__instance.aiWaypointControls).ConfigureOptionsDropdown(), + selectedLoco, + numberOfCars, + destinations: destinations, + out List rowDatas, + out Action func, + out int origCount, + out int maxRowOrig, + out AutoEngineerOrdersHelper aeoh + ); + + List<(string destinationId, string destination, float? distance, float sortDistance, Location? location)> jumpTos = + BuildJumpToOptions((AutoEngineerWaypointControls)__instance.aiWaypointControls, selectedLoco); + + var config = WireUpJumpTosToSettingMenu( + (AutoEngineerWaypointControls)__instance.aiWaypointControls, + selectedLoco, + rowDatas, + func, + origCount, + maxRowOrig, + aeoh, + ref jumpTos + ); + + List list = config.Rows; + Action action = config.OnRowSelected; + + __instance.optionsDropdown.Configure(list, action); + ((Selectable)__instance.optionsDropdown).interactable = list.Count > 0; + yield return wait; + } + } + + private static bool ShouldRecalc(LocomotiveControlsUIAdapter __instance, BaseLocomotive selectedLoco, out HashSet destinations) + { + bool output = false; + string locoKey = getDictKey(selectedLoco); + List consist = new List(); + consist = selectedLoco.EnumerateCoupled().ToList(); + destinations = consist.Where(c => GetCarDestinationIdentifier(c).HasValue).Select(GetCarDestinationIdentifier).ToHashSet(); + + //_log.Information($"{locoKey} --> [{destinations.Count}] -> Seen -> {string.Join(Environment.NewLine, destinations.Select(k => k.Value.DisplayName))}"); + //_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}"); + if (output) lastSeenIntegrationSetCount = default; + output |= lastSeenIntegrationSetCount != selectedLoco.set.NumberOfCars; + //_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}"); + + return output; + } + + 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, float sortDistance, Location? location)> jumpTos) + { + OptionsDropdownConfiguration __result; + jumpTos = jumpTos?.OrderBy(c => c.sortDistance)?.ToList() ?? default; + var localJumpTos = jumpTos.ToList(); + var safetyFirst = AutoEngineerPlanner_HandleCommand_Patch.SafetyFirstGoverningApplies(selectedLoco) && jumpTos.Any(); + + rowDatas.AddRange(jumpTos.Select(j => + new DropdownMenu.RowData( + $"{j.destination} ({(j.distance.HasValue ? Units.DistanceText(j.distance.Value) : "N/A")})", + !safetyFirst ? null : "Disabled; Safety First!" + ) + )); + + __result = new OptionsDropdownConfiguration( + rowDatas + , delegate (int row) + { + _log.Debug($"{__instance.Locomotive.DisplayName} row {row}/{localJumpTos.Count}/{rowDatas.Count}"); + if (row <= maxRowOrig) + { + func(row); + } + if (row > maxRowOrig && localJumpTos[row - origCount].location.HasValue) + { + if (safetyFirst) + { + Multiplayer.SendError(StateManager.Shared.PlayersManager.LocalPlayer, "Safety First, find yourself a caboose!", AlertLevel.Error); + return; + } + float trainMomentum = 0f; + Location end = localJumpTos[row - origCount].location.Value; + Location start = RouteStartLocation(__instance, selectedLoco); + HeuristicCosts autoEngineer = HeuristicCosts.AutoEngineer; + List list = new List(); + + var totLen = StateManager.IsHost ? selectedLoco.AutoEngineerPlanner.CalculateTotalLength() : CalculateTotalLength(selectedLoco); + if (!Graph.Shared.FindRoute(start, end, autoEngineer, list, out var metrics, checkForCars: false, totLen, trainMomentum)) + { + RouteSearch.Metrics metrics2; + bool flag = Graph.Shared.FindRoute(start, end, autoEngineer, null, out metrics2); + 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); + aeoh.SetOrdersValue(maybeWaypoint: mw); + } + } + } + ); + return __result; + } + + private static List<(string destinationId, string destination, float? distance, float sortDistance, Location? location)> BuildJumpToOptions(AutoEngineerWaypointControls __instance, BaseLocomotive selectedLoco) + { + List<(string destinationId, string destination, float? distance, float sortDistance, Location? location)> jumpTos = new(); + foreach (OpsCarPosition ocp in locoConsistDestinations) + { + string destName = ocp.DisplayName; + string destId = ocp.Identifier; + float? distance = null; + float sortdistance = 0f; + 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; + sortdistance = Graph.Shared.FindRoute(start, destLoc, autoEngineer, list, out metrics, checkForCars: false, 0f, trainMomentum) + ? metrics.Distance + : float.MaxValue; + } + ; + _log.Debug($"{getDictKey(selectedLoco)} -> {destName} {destId} {distance?.ToString()}"); + jumpTos.Add(( + destinationId: destId, + destination: $"WP> {destName}" + , distance: distance + , sortdistance: sortdistance + , location: (Location?)destLoc + )); + } + + if (selectedLoco.TryGetTimetableTrain(out Timetable.Train t)) + { + //_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}"); + if (stp != null) + { + try + { + string destName = t.TrainType == Timetable.TrainType.Passenger ? stp.passengerStop.DisplayName : stp.DisplayName; + string destId = t.TrainType == Timetable.TrainType.Passenger ? stp.passengerStop.identifier : stp.code; + float? distance = null; + float sortdistance = 0f; + if ( + Graph.Shared.TryGetLocationFromPoint( + stp.passengerStop.TrackSpans?.FirstOrDefault().GetSegments().FirstOrDefault(), + stp.passengerStop.TrackSpans?.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; + sortdistance = Graph.Shared.FindRoute(start, destLoc, autoEngineer, list, out metrics, checkForCars: false, 0f, trainMomentum) + ? metrics.Distance + : float.MaxValue; + } + ; + _log.Debug($"{getDictKey(selectedLoco)} -> {destName} {destId} {distance?.ToString()}"); + jumpTos.Add(( + destinationId: destId, + destination: $"{t.DisplayStringLong} > {destName}" + , distance: distance + , sortdistance: sortdistance + , location: (Location?)destLoc + )); + } + catch (Exception ex) + { + _log.Warning(ex, $"Timetable entry not added to AE gear cog options {stp}"); + } + } + } + } + + return jumpTos; + } + + private static void IterateCarsDetectDestinations( + AutoEngineerWaypointControls __instance, + OptionsDropdownConfiguration __result, + BaseLocomotive selectedLoco, + int numberOfCars, + HashSet destinations, + 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; + aeoh = new AutoEngineerOrdersHelper(persistence: new AutoEngineerPersistence(selectedLoco.KeyValueObject), locomotive: selectedLoco); + string locoKey = getDictKey(selectedLoco); + + var dropped = + locoConsistDestinations.Except(destinations).ToHashSet(); //are no longer here + + _log.Debug($"{locoKey} --> [{destinations.Count}] -> Seen -> {string.Join(Environment.NewLine, destinations.Select(k => k.Value.DisplayName))}"); + _log.Debug($"{locoKey} --> [{locoConsistDestinations.Count}] -> Cache -> {string.Join(Environment.NewLine, locoConsistDestinations.Select(k => $"{locoKey}:{k.Value.DisplayName}"))}"); + _log.Debug($"{locoKey} --> [{dropped.Count}] -> removed -> {string.Join(Environment.NewLine, dropped.Select(k => k.Value.DisplayName))}"); + locoConsistDestinations = destinations.ToList().ToHashSet(); //remove ones that are no longer here + } + + private static void PrepLocoUsage(AutoEngineerWaypointControls __instance, out BaseLocomotive selectedLoco, out int numberOfCars) + { + //wire up that loco + selectedLoco = __instance.Locomotive; + numberOfCars = selectedLoco?.set.NumberOfCars ?? -1; + _log.Debug($"{selectedLoco?.id} --> HI BOB[{numberOfCars}]"); + } + + 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) + { + bool num = _locomotive.IsStopped(); + bool? flag = (num ? null : new bool?(_locomotive.velocity >= 0f)); + + bool flag2 = flag ?? __instance.OrdersHelper.Orders.Forward; + if (_locomotive.EndToLogical((!flag2) ? Car.End.R : Car.End.F) == Car.LogicalEnd.A) + { + return _locomotive.EnumerateCoupled().First().WheelBoundsA; + } + + return _locomotive.EnumerateCoupled(Car.LogicalEnd.B).First().WheelBoundsB.Flipped(); + } + + private static float CalculateTotalLength(BaseLocomotive selectedLoco) + { + List coupledCarsCached = selectedLoco.EnumerateCoupled().ToList(); + float num = 0f; + foreach (Car item in coupledCarsCached) + { + num += item.carLength; + } + + return num + 1.04f * (float)(coupledCarsCached.Count - 1); + } +} + +[HarmonyPatch(typeof(LocomotiveControlsUIAdapter))] +[HarmonyPatch(nameof(LocomotiveControlsUIAdapter.UpdateOptionsDropdown))] +[HarmonyPatchCategory("RMROC451TweaksAndThings")] +internal class LocomotiveControlsUIAdapter_UpdateOptionsDropdown_Prefix +{ + static bool Prefix(LocomotiveControlsUIAdapter __instance) + { + return false; + } +} diff --git a/TweaksAndThings/Patches/BaseLocomotive_FindSourceLocomotive_Patch.cs b/TweaksAndThings/Patches/BaseLocomotive_FindSourceLocomotive_Patch.cs index bc2db78..887d234 100644 --- a/TweaksAndThings/Patches/BaseLocomotive_FindSourceLocomotive_Patch.cs +++ b/TweaksAndThings/Patches/BaseLocomotive_FindSourceLocomotive_Patch.cs @@ -43,3 +43,89 @@ internal class BaseLocomotive_FindSourceLocomotive_Patch return null; } } + +//[HarmonyPatch(typeof(LocomotiveAirSystem))] +//[HarmonyPatch(nameof(LocomotiveAirSystem._ShouldDeferToLocomotiveAir))] +//[HarmonyPatchCategory("RMROC451TweaksAndThings")] +//internal class LocomotiveAirSystem__ShouldDeferToLocomotiveAir_Patch +//{ +// private static void Postfix(LocomotiveAirSystem __instance, ref LocomotiveAirSystem locomotiveAirSystem, ref bool __result) +// { +// TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; +// if (!tweaksAndThings.IsEnabled()) return; + +// __result = _ShouldDeferToLocomotiveAir(__instance, out locomotiveAirSystem); +// } +// public static bool _ShouldDeferToLocomotiveAir(LocomotiveAirSystem __instance, out LocomotiveAirSystem locomotiveAirSystem) +// { +// locomotiveAirSystem = null; +// if (__instance.car.set == null) +// { +// return false; +// } +// if (!(__instance.car.air is LocomotiveAirSystem locomotiveAirSystem2) || !(__instance.car is BaseLocomotive baseLocomotive)) +// { +// return false; +// } +// if (!locomotiveAirSystem2.IsCutOut || locomotiveAirSystem2.IsMuEnabled) +// { +// return false; +// } +// BaseLocomotive baseLocomotive2 = baseLocomotive.FindMuSourceLocomotive(); +// if (baseLocomotive2 == null) +// { +// return false; +// } +// if (!(baseLocomotive2.air is LocomotiveAirSystem locomotiveAirSystem3)) +// { +// return false; +// } +// locomotiveAirSystem = locomotiveAirSystem3; +// return true; +// } +//} + +//[HarmonyPatch(typeof(CarAirSystem))] +//[HarmonyPatch(nameof(CarAirSystem.ShouldDeferToLocomotiveAir))] +//[HarmonyPatchCategory("RMROC451TweaksAndThings")] +//internal class CarAirSystem_ShouldDeferToLocomotiveAir_Patch +//{ +// private static void Postfix(CarAirSystem __instance, ref LocomotiveAirSystem locomotiveAirSystem, ref bool __result) +// { +// TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; +// if (!tweaksAndThings.IsEnabled()) return; + +// __result = ShouldDeferToLocomotiveAir(__instance, out locomotiveAirSystem); +// } + +// public static bool ShouldDeferToLocomotiveAir(CarAirSystem __instance, out LocomotiveAirSystem locomotiveAirSystem) +// { +// locomotiveAirSystem = null; +// if (__instance.car.set == null) +// { +// return false; +// } +// if (__instance.car.Archetype != CarArchetype.Tender) +// { +// return false; +// } +// if (!__instance.car.TryGetAdjacentCar(__instance.car.EndToLogical(Car.End.F), out var adjacent) || !adjacent.IsLocomotive) +// { +// return false; +// } +// if (!(adjacent.air is LocomotiveAirSystem locomotiveAirSystem2)) +// { +// return false; +// } +// locomotiveAirSystem = locomotiveAirSystem2; +// if (locomotiveAirSystem.IsMuEnabled) +// { +// return true; +// } +// if (!locomotiveAirSystem.IsCutOut) +// { +// return true; +// } +// return locomotiveAirSystem.ShouldDeferToLocomotiveAir(out locomotiveAirSystem); +// } +//} diff --git a/TweaksAndThings/Patches/CarInspector_PopulateCarPanel_Patch.cs b/TweaksAndThings/Patches/CarInspector_PopulateCarPanel_Patch.cs index ecd6ae8..235dd2f 100644 --- a/TweaksAndThings/Patches/CarInspector_PopulateCarPanel_Patch.cs +++ b/TweaksAndThings/Patches/CarInspector_PopulateCarPanel_Patch.cs @@ -28,7 +28,7 @@ namespace RMROC451.TweaksAndThings.Patches; internal class CarInspector_PopulateCarPanel_Patch { private static ILogger _log => Log.ForContext(); - private static IEnumerable ends = Enum.GetValues(typeof(LogicalEnd)).Cast(); + internal static IEnumerable ends = Enum.GetValues(typeof(LogicalEnd)).Cast(); /// /// If a caboose inspector is opened, it will auto set Anglecocks, gladhands and hand brakes @@ -88,7 +88,7 @@ internal class CarInspector_PopulateCarPanel_Patch int consistLength = consist.Count(); int tonnage = LocomotiveControlsHoverArea.CalculateTonnage(consist); int lengthInMeters = UnityEngine.Mathf.CeilToInt(LocomotiveControlsHoverArea.CalculateLengthInMeters(consist.ToList()) * 3.28084f); - var newSubTitle = () => string.Format("{0}, {1:N0}T, {2:N0}ft, {3:0.0} mph", consistLength.Pluralize("car"), tonnage, lengthInMeters, __instance._car.VelocityMphAbs); + var newSubTitle = () => string.Format("{0}, {1:N0}T, {2:N0}ft, {3:N0} mph", consistLength.Pluralize("car"), tonnage, lengthInMeters, __instance._car.VelocityMphAbs); field.AddLabel(() => newSubTitle(), UIPanelBuilder.Frequency.Fast) .Tooltip("Consist Info", "Reflects info about consist.").FlexibleWidth(); @@ -181,7 +181,7 @@ internal class CarInspector_PopulateCarPanel_Patch case MrocHelperType.BleedAirSystem: consist = consist.Where(c => !c.MotivePower()); - _log.ForContext("car", car).Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}"); + _log.ForContext("car", car).Debug($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}"); foreach (Model.Car bleed in consist) { StateManager.ApplyLocal(new PropertyChange(bleed.id, PropertyChange.Control.Bleed, 1)); @@ -190,7 +190,7 @@ internal class CarInspector_PopulateCarPanel_Patch case MrocHelperType.Oil: consist = consist.Where(c => c.NeedsOiling || c.HasHotbox); - _log.ForContext("car", car).Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}"); + _log.ForContext("car", car).Debug($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}"); foreach (Model.Car oil in consist) { StateManager.ApplyLocal(new PropertyChange(oil.id, nameof(Car.Oiled).ToLower(), new FloatPropertyValue(1))); @@ -227,12 +227,12 @@ internal class CarInspector_PopulateCarPanel_Patch float originalTimeCost = consist.CalculateCostForAutoEngineerEndGearSetting(); float timeCost = originalTimeCost; float crewCost = timeCost / 3600; //hours of time deducted from caboose. - var tsString = crewCost.FormatCrewHours(OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad().description); - Car? cabooseWithAvailCrew = car.FindMyCaboose(crewCost, buttonsHaveCost); + var tsString = crewCost.FormatCrewHours(OpsController_AnnounceCoalescedPayments_Patch.CrewLoadHours.description); + Car? cabooseWithAvailCrew = car.FindMyCabooseWithLoadRequirement(crewCost, buttonsHaveCost); if (cabooseWithAvailCrew == null) timeCost *= 1.5f; var cabooseFoundDisplay = cabooseWithAvailCrew?.DisplayName ?? "No caboose"; - _log.ForContext("car", car).Information($"{nameof(MrocConsistHelper)} {mrocHelperType} : [VACINITY CABEESE FOUND:{cabooseWithAvailCrew?.ToString() ?? "NONE"}] => Consist Length {consist.Count()} => costs {timeCost / 60} minutes of AI Engineer time, $5 per hour = ~${Math.Ceiling((decimal)(timeCost / 3600) * 5)} (*2 if no caboose nearby)"); + _log.ForContext("car", car).Debug($"{nameof(MrocConsistHelper)} {mrocHelperType} : [VACINITY CABEESE FOUND:{cabooseWithAvailCrew?.ToString() ?? "NONE"}] => Consist Length {consist.Count()} => costs {timeCost / 60} minutes of AI Engineer time, $5 per hour = ~${Math.Ceiling((decimal)(timeCost / 3600) * 5)} (*2 if no caboose nearby)"); Multiplayer.SendError(StateManager.Shared._playersManager.LocalPlayer, $"{(cabooseWithAvailCrew != null ? $"{cabooseWithAvailCrew.DisplayName} Hours Adjusted: ({tsString})\n" : string.Empty)}Wages: ~(${Math.Ceiling((decimal)(timeCost / 3600) * 5)})"); diff --git a/TweaksAndThings/Patches/CarPickable_Activate_Patch.cs b/TweaksAndThings/Patches/CarPickable_Activate_Patch.cs index f20795a..ae1678c 100644 --- a/TweaksAndThings/Patches/CarPickable_Activate_Patch.cs +++ b/TweaksAndThings/Patches/CarPickable_Activate_Patch.cs @@ -2,6 +2,7 @@ using HarmonyLib; using Model; using Model.Ops; +using Model.Physics; using Network; using Railloader; using RMROC451.TweaksAndThings.Enums; @@ -9,6 +10,7 @@ using RMROC451.TweaksAndThings.Extensions; using RollingStock; using Serilog; using System; +using System.Collections.Generic; using System.Linq; using UI; using UI.Tags; @@ -40,92 +42,179 @@ internal class CarPickable_Activate_Patch { TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; if (!tweaksAndThings.IsEnabled()) return true; - bool bCtrlAltHeld = GameInput.IsControlDown && GameInput.IsAltDown; - _log.ForContext("car", __instance.car).Information($"{GameInput.IsShiftDown} {GameInput.IsControlDown} {GameInput.IsAltDown} {bCtrlAltHeld} "); + _log.ForContext("car", __instance.car).Debug($"{GameInput.IsShiftDown} {GameInput.IsControlDown} {GameInput.IsAltDown}"); if (OnPointerDown(evt, PickableActivation.Primary)) { CameraSelector.shared.FollowCar(__instance.car); - _log.ForContext("car", __instance.car).Information("just click!"); + _log.ForContext("car", __instance.car).Debug("just click!"); return false; - } - //single click with keys pressed: - else if (evt.Activation == PickableActivation.Primary) - { - bool output = true; - var consist = __instance.car.EnumerateCoupled(); - bool handbrakesApplied = consist.Any(c => c.HandbrakeApplied()); - bool airSystemIssues = consist.Any(c => c.EndAirSystemIssue()); - Func cabooseNear = () => (bool)__instance.car.FindMyCaboose(0.0f, false); - bool needsOiling = GameInput.IsShiftDown && consist.All(c => c.IsStopped()) && consist.Any(c => c.NeedsOiling || c.HasHotbox) && (!tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() || cabooseNear()); - var chargeIt = handbrakesApplied || airSystemIssues || needsOiling; - //CTRL + ALT + SHIFT : BrakesAngleCocksAndOiling - //CTRL + ALT : Release Consist Brakes and Check AngleCocks - //ALT + SHIFT : toggle consist brakes - //CTRL + SHIFT : Check Consist Angle Cocks - //ALT : Toggle car brakes and & air up cars - //CTRL : NOTHING; BASE CAR INSPECTOR + } + return HandleCarOrTrainBrakeDisplayClick(__instance.car, tweaksAndThings, evt.Activation); + } + internal static bool HandleCarOrTrainBrakeDisplayClick(Car car, TweaksAndThingsPlugin tweaksAndThings, PickableActivation activation) + { + bool bCtrlAltHeld = GameInput.IsControlDown && GameInput.IsAltDown; + bool output = true; + var consist = car.EnumerateCoupled(); + bool handbrakesApplied = consist.Any(c => c.HandbrakeApplied()); + bool airSystemIssues = consist.Any(c => c.EndAirSystemIssue()); + Func cabooseNear = () => (bool)car.FindMyCabooseSansLoadRequirement(); + bool needsOiling = GameInput.IsShiftDown && consist.All(c => c.IsStopped()) && consist.Any(c => c.NeedsOiling || c.HasHotbox) && (!tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() || cabooseNear()); + var chargeIt = handbrakesApplied || airSystemIssues || needsOiling; + //CTRL + ALT + SHIFT : BrakesAngleCocksAndOiling + //CTRL + ALT : Release Consist Brakes and Check AngleCocks + //ALT + SHIFT : toggle consist brakes + //CTRL + SHIFT : Check Consist Angle Cocks + //ALT : Toggle car brakes and & air up cars + //CTRL : NOTHING; BASE CAR INSPECTOR + //SHIFT : FOLLOW + + if (activation == PickableActivation.Primary) + { if (bCtrlAltHeld) { - BrakesAngleCocksAndOiling(__instance, tweaksAndThings, GameInput.IsShiftDown, consist, handbrakesApplied, airSystemIssues, needsOiling, chargeIt); - _log.ForContext("car", __instance.car).Information($"ctrlAlt{(GameInput.IsShiftDown ? "shift" : string.Empty)}Held!"); + BrakesAngleCocksAndOiling(car, tweaksAndThings, GameInput.IsShiftDown, consist, handbrakesApplied, airSystemIssues, needsOiling, chargeIt); + _log.ForContext("car", car).Debug($"ctrlAlt{(GameInput.IsShiftDown ? "shift" : string.Empty)}Held!"); output = false; } else if (GameInput.IsAltDown && GameInput.IsShiftDown) { - CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.Handbrake, tweaksAndThings.EndGearHelpersRequirePayment()); - _log.ForContext("car", __instance.car).Information("ctrlShiftHeld!"); + CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Handbrake, tweaksAndThings.EndGearHelpersRequirePayment()); + _log.ForContext("car", car).Debug("ctrlShiftHeld!"); output = false; } else if (GameInput.IsControlDown && GameInput.IsShiftDown) { if (airSystemIssues) - CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.GladhandAndAnglecock, tweaksAndThings.EndGearHelpersRequirePayment()); - _log.ForContext("car", __instance.car).Information("altShiftHeld!"); + CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.GladhandAndAnglecock, tweaksAndThings.EndGearHelpersRequirePayment()); + _log.ForContext("car", car).Debug("altShiftHeld!"); output = false; } else if (GameInput.IsAltDown) { - __instance.car.SetHandbrake(!__instance.car.HandbrakeApplied()); - CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(__instance.car); - if (__instance.car.TryGetAdjacentCar(Car.LogicalEnd.A, out Model.Car cA)) CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(cA); - if (__instance.car.TryGetAdjacentCar(Car.LogicalEnd.B, out Model.Car cB)) CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(cB); - _log.ForContext("car", __instance.car).Information("ctrlHeld!"); - TagController.Shared.UpdateTag(__instance.car, __instance.car.TagCallout, OpsController.Shared); - __instance.car.TagCallout.Update(); + car.SetHandbrake(!car.HandbrakeApplied()); + CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(car); + if (car.TryGetAdjacentCar(Car.LogicalEnd.A, out Model.Car cA)) CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(cA); + if (car.TryGetAdjacentCar(Car.LogicalEnd.B, out Model.Car cB)) CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(cB); + _log.ForContext("car", car).Debug("ctrlHeld!"); + TagController.Shared.UpdateTag(car, car.TagCallout, OpsController.Shared); + car.TagCallout.Update(); output = false; } - return output; - - - //else if (ctrlHeld && shiftHeld) - //{ - // var selected = UI.CarInspector.CarInspector._instance._selectedTabState.Value; - // UI.CarInspector.CarInspector.Show(__instance.car); - // UI.CarInspector.CarInspector._instance._selectedTabState.Value = selected; - - //} + else if (GameInput.IsShiftDown) + { + CameraSelector.shared.FollowCar(car); + output = false; + } + } + else if ((GameInput.IsControlDown || GameInput.IsAltDown) && activation == PickableActivation.Secondary) + { + AltClickageMyBrosif(car, activation); + output = false; + } + else if (activation == PickableActivation.Secondary) + { + //AltClickageMyBrosif(car, activation); + CarPickable.HandleShowContextMenu(car); + output = false; } - return true; + return output; } - private static void BrakesAngleCocksAndOiling(CarPickable __instance, TweaksAndThingsPlugin tweaksAndThings, bool shiftHeld, System.Collections.Generic.IEnumerable consist, bool handbrakesApplied, bool airSystemIssues, bool needsOiling, bool chargeIt) + private static void AltClickageMyBrosif(Car car, PickableActivation activation) + { + //if (GameInput.IsControlDown) + //{ + + //} + //else if (GameInput.IsAltDown) + //{ + // logSet(car); + // //car.TryGetAdjacentCar(Car.LogicalEnd.A, out var OutCarA); + // //car.TryGetAdjacentCar(Car.LogicalEnd.B, out var OutCarB); + // output = false; + //} + List cars = TrainController.Shared.SelectedLocomotive.EnumerateCoupled().ToList(); + logSet(car); + //car.TryGetAdjacentCar(Car.LogicalEnd.A, out var OutCarA); + //car.TryGetAdjacentCar(Car.LogicalEnd.B, out var OutCarB); + //bool b4 = false; + //if ((Object)(object)OutCarA != (Object)null && OutCarA.id == cars[i - 1].id) + //{ + // cars[i - 1].ApplyEndGearChange(Car.LogicalEnd.B, Car.EndGearStateKey.Anglecock, 0f); + // ccar.ApplyEndGearChange(Car.LogicalEnd.A, Car.EndGearStateKey.Anglecock, 0f); + // ccar.HandleCouplerClick(ccar.EndGearA.Coupler); + // logger.Information("{indName}: Decoupled {ccarName} from {cars1Name} at NOA{FR}", new object[4] + // { + // ((Object)base.Industry).name, + // ccar.DisplayName, + // cars[i - 1].DisplayName, + // (ccar.LogicalToEnd(Car.LogicalEnd.A) == Car.End.F) ? "F" : "R" + // }); + // Storage.LastCarEnd = ccar.LogicalToEnd(Car.LogicalEnd.A); + // b4 = true; + //} + //if ((Object)(object)OutCarB != (Object)null && !b4 && OutCarB.id == cars[i - 1].id) + //{ + // cars[i - 1].ApplyEndGearChange(Car.LogicalEnd.A, Car.EndGearStateKey.Anglecock, 0f); + // ccar.ApplyEndGearChange(Car.LogicalEnd.B, Car.EndGearStateKey.Anglecock, 0f); + // ccar.HandleCouplerClick(ccar.EndGearB.Coupler); + // logger.Information("{indName}: Decoupled {ccarName} from {carsm1Name} at NOB{FR}", new object[4] + // { + // ((Object)base.Industry).name, + // ccar.DisplayName, + // cars[i - 1].DisplayName, + // (ccar.LogicalToEnd(Car.LogicalEnd.A) == Car.End.F) ? "F" : "R" + // }); + // Storage.LastCarEnd = ccar.LogicalToEnd(Car.LogicalEnd.B); + // b4 = true; + //} + //if (!b4) + //{ + // logger.Information("{IndName}: ERROR: NLC {ccarName} can't find LCC {carsm1Name} ({OutCarAName},{OutCarBName})", new object[5] + // { + // ((Object)base.Industry).name, + // ccar.DisplayName, + // cars[i - 1].DisplayName, + // ((Object)(object)OutCarA != (Object)null) ? OutCarA.DisplayName : "no Car", + // ((Object)(object)OutCarB != (Object)null) ? OutCarB.DisplayName : "no Car" + // }); + // return; + //} + } + + private static void logSet(Car refCar) + { + IntegrationSet set = refCar.set; + foreach (Car car in set.Cars) + { + Car acar; + string endA = (car.TryGetAdjacentCar(Car.LogicalEnd.A, out acar) ? acar.DisplayName : "none"); + Car bcar; + string endB = (car.TryGetAdjacentCar(Car.LogicalEnd.B, out bcar) ? bcar.DisplayName : "none"); + _log.Information("[Car: {id}:(EndA:{carA}),(EndB:{carB}),(FisA:{fisA})]", new object[4] { car.DisplayName, endA, endB, car.FrontIsA }); + } + } + + + private static void BrakesAngleCocksAndOiling(Car car, TweaksAndThingsPlugin tweaksAndThings, bool shiftHeld, System.Collections.Generic.IEnumerable consist, bool handbrakesApplied, bool airSystemIssues, bool needsOiling, bool chargeIt) { int hbFix = 0; if (handbrakesApplied) - CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.Handbrake, false); + CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Handbrake, false); if (airSystemIssues) - CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.GladhandAndAnglecock, false); + CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.GladhandAndAnglecock, false); if (needsOiling) - hbFix = CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.Oil, false); + hbFix = CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Oil, false); if (hbFix > 0) - Multiplayer.Broadcast($"Near {Hyperlink.To(__instance.car)}: \"{hbFix.Pluralize("hotbox") + " repaired!"}\""); + Multiplayer.Broadcast($"Near {Hyperlink.To(car)}: \"{hbFix.Pluralize("hotbox") + " repaired!"}\""); if (chargeIt) - CarInspector_PopulateCarPanel_Patch.CalculateCostIfEnabled(__instance.car, MrocHelperType.Handbrake, tweaksAndThings.EndGearHelpersRequirePayment(), consist); + CarInspector_PopulateCarPanel_Patch.CalculateCostIfEnabled(car, MrocHelperType.Handbrake, tweaksAndThings.EndGearHelpersRequirePayment(), consist); } public static bool OnPointerDown( diff --git a/TweaksAndThings/Patches/CarPickable_HandleShowContextMenu_Patch.cs b/TweaksAndThings/Patches/CarPickable_HandleShowContextMenu_Patch.cs index fc174b0..5cfde76 100644 --- a/TweaksAndThings/Patches/CarPickable_HandleShowContextMenu_Patch.cs +++ b/TweaksAndThings/Patches/CarPickable_HandleShowContextMenu_Patch.cs @@ -4,6 +4,7 @@ using Railloader; using RMROC451.TweaksAndThings.Enums; using RMROC451.TweaksAndThings.Extensions; using RollingStock; +using System; using System.Linq; using UI; using UI.ContextMenu; @@ -35,8 +36,6 @@ internal class CarPickable_HandleShowContextMenu_Patch }); if (GameInput.IsShiftDown) { - - if (!car.EnumerateCoupled().Any(c => !c.SupportsBleed())) { shared.AddButton(ContextMenuQuadrant.Brakes, $"Bleed Consist", SpriteName.Bleed, delegate @@ -44,23 +43,24 @@ internal class CarPickable_HandleShowContextMenu_Patch CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.BleedAirSystem, buttonsHaveCost); }); } - - shared.AddButton(ContextMenuQuadrant.Brakes, $"{(car.EnumerateCoupled().Any(c => c.HandbrakeApplied()) ? "Release " : "Set ")} Consist", SpriteName.Handbrake, delegate - { - CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Handbrake, buttonsHaveCost); - }); - if (car.EnumerateCoupled().Any(c => c.EndAirSystemIssue())) - { - shared.AddButton(ContextMenuQuadrant.Unused2, $"Air Up Consist", SpriteName.Select, delegate + shared.AddButton(ContextMenuQuadrant.Brakes, $"{(car.EnumerateCoupled().Any(c => c.HandbrakeApplied()) ? "Release " : "Set ")} Consist", SpriteName.Handbrake, delegate + { + CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Handbrake, buttonsHaveCost); + }); + + if (car.EnumerateCoupled().Any(c => c.EndAirSystemIssue())) + { + shared.AddButton(ContextMenuQuadrant.General, $"Air Up Consist", SpriteName.Select, delegate { CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost); }); + } } - } - else { - if (car.SupportsBleed()) + else { + if (car.SupportsBleed()) + { shared.AddButton(ContextMenuQuadrant.Brakes, "Bleed", SpriteName.Bleed, car.SetBleed); } shared.AddButton(ContextMenuQuadrant.Brakes, car.air.handbrakeApplied ? "Release Handbrake" : "Apply Handbrake", SpriteName.Handbrake, delegate @@ -75,7 +75,9 @@ internal class CarPickable_HandleShowContextMenu_Patch CameraSelector.shared.FollowCar(car); }); - shared.Show(car.DisplayName); + string secondaryLine = car.Waybill.HasValue ? $"{Environment.NewLine}{car.Waybill.Value.Destination.DisplayName}" : string.Empty; + secondaryLine = secondaryLine.Length > 10 + Environment.NewLine.Length ? $"{secondaryLine.Substring(0, 7+ Environment.NewLine.Length)}..." : secondaryLine; + shared.Show($"{car.DisplayName}{secondaryLine}"); shared.BuildItemAngles(); shared.StartCoroutine(shared.AnimateButtonsShown()); return false; diff --git a/TweaksAndThings/Patches/ContextMenu_Show_Patch.cs b/TweaksAndThings/Patches/ContextMenu_Show_Patch.cs deleted file mode 100644 index 007c018..0000000 --- a/TweaksAndThings/Patches/ContextMenu_Show_Patch.cs +++ /dev/null @@ -1,128 +0,0 @@ -using HarmonyLib; -using Helpers; -using Railloader; -using Serilog; -using System; -using System.Collections.Generic; -using UI; -using UI.ContextMenu; -using UnityEngine; -using UnityEngine.UI; - -namespace RMROC451.TweaksAndThings.Patches; - -[HarmonyPatch(typeof(UI.ContextMenu.ContextMenu))] -[HarmonyPatch(nameof(UI.ContextMenu.ContextMenu.Show), typeof(string))] -[HarmonyPatchCategory("RMROC451TweaksAndThings")] -internal class ContextMenu_Show_Patch -{ - static bool Prefix(UI.ContextMenu.ContextMenu __instance, string centerText) - { - TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; - if (!tweaksAndThings.IsEnabled()) return true; - - if (!__instance.GetRootCanvas(out var rootCanvas)) - { - Log.Warning("Couldn't get root canvas"); - return true; - } - __instance.CancelHideCoroutine(); - if (__instance.contentRectTransform.childCount >= 1) __instance.contentRectTransform.GetChild(0).DestroyAllChildren(); //YOINK DEM CIRCLES! - __instance.SetupTemplate(rootCanvas); - __instance.centerLabel.text = centerText; - Canvas componentInParent = ((Component)__instance.contentRectTransform).GetComponentInParent(); - Vector3 mousePosition = Input.mousePosition; - Vector2 val = componentInParent.ScreenToCanvasPosition(mousePosition).XY(); - Vector2 renderingDisplaySize = rootCanvas.renderingDisplaySize; - float num = __instance.radius + 50f; - if (val.x < num) - { - val.x = num; - } - if (val.x > renderingDisplaySize.x - num) - { - val.x = renderingDisplaySize.x - num; - } - if (val.y < num) - { - val.y = num; - } - if (val.y > renderingDisplaySize.y - num) - { - val.y = renderingDisplaySize.y - num; - } - __instance.contentRectTransform.anchoredPosition = val; - __instance.BuildItemAngles(); - - ((MonoBehaviour)__instance).StartCoroutine(__instance.AnimateButtonsShown()); - ((Component)__instance.contentRectTransform).gameObject.SetActive(true); - UI.ContextMenu.ContextMenu.IsShown = true; - __instance._blocker = __instance.CreateBlocker(rootCanvas); - GameInput.RegisterEscapeHandler(GameInput.EscapeHandler.Transient, delegate - { - __instance.Hide(); - return true; - }); - - return false; - } -} - - -[HarmonyPatch(typeof(UI.ContextMenu.ContextMenu))] -[HarmonyPatch(nameof(UI.ContextMenu.ContextMenu.DefaultAngleForItem), typeof(ContextMenuQuadrant), typeof(int), typeof(int))] -[HarmonyPatchCategory("RMROC451TweaksAndThings")] -internal class ContextMenu_DefaultAngleForItem_Patch -{ - static bool Prefix(UI.ContextMenu.ContextMenu __instance, ref float __result, ContextMenuQuadrant quadrant, int index, int quadrantItemCount) - { - TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; - if (!tweaksAndThings.IsEnabled()) return true; - - - int num = quadrant switch - { - ContextMenuQuadrant.General => 0, - ContextMenuQuadrant.Unused1 => 90, - ContextMenuQuadrant.Brakes => 180, - ContextMenuQuadrant.Unused2 => -90, - _ => throw new ArgumentOutOfRangeException("quadrant", quadrant, null), - }; - if (quadrantItemCount <= 1) - { - __result = num; - return false; - } - int num2 = ((quadrantItemCount <= 3) ? 30 : (90 / (quadrantItemCount - 1))); - __result = (float)num + -0.5f * (float)((quadrantItemCount - 1) * num2) + (float)(num2 * index); - return false; - } -} - - -[HarmonyPatch(typeof(UI.ContextMenu.ContextMenu))] -[HarmonyPatch(nameof(UI.ContextMenu.ContextMenu.AddButton), typeof(ContextMenuQuadrant), typeof(string), typeof(Sprite), typeof(Action))] -[HarmonyPatchCategory("RMROC451TweaksAndThings")] -internal class ContextMenu_AddButton_Patch -{ - static bool Prefix(UI.ContextMenu.ContextMenu __instance, ContextMenuQuadrant quadrant, string title, Sprite sprite, Action action) - { - TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; - if (!tweaksAndThings.IsEnabled()) return true; - - - List list = __instance._quadrants[(int)quadrant]; - int index = list.Count; - ContextMenuItem contextMenuItem = UnityEngine.Object.Instantiate(__instance.itemPrefab, (Transform)(object)__instance.contentRectTransform); - contextMenuItem.image.sprite = sprite; - contextMenuItem.label.text = title; - contextMenuItem.OnClick = delegate - { - action(); - __instance.Hide((quadrant, index)); - }; - ((Component)contextMenuItem).gameObject.AddComponent().preferredHeight = 30f; - list.Add(contextMenuItem); - return false; - } -} \ No newline at end of file diff --git a/TweaksAndThings/Patches/EngineRosterPanel_Populate_Patch.cs b/TweaksAndThings/Patches/EngineRosterPanel_Populate_Patch.cs index ccb5389..d9bc633 100644 --- a/TweaksAndThings/Patches/EngineRosterPanel_Populate_Patch.cs +++ b/TweaksAndThings/Patches/EngineRosterPanel_Populate_Patch.cs @@ -20,7 +20,7 @@ internal class EngineRosterPanel_Populate_Patch __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(); + var hiddenEntries = rows.Where(r => r.Engine.locomotiveControl.air.IsCutOut && !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()}]"); diff --git a/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs b/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs index b49e4b5..3e48c14 100644 --- a/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs +++ b/TweaksAndThings/Patches/EngineRosterRow_Refresh_Patch.cs @@ -15,6 +15,7 @@ using UI.EngineRoster; using UI.Tooltips; using UnityEngine; using Game.State; +using Game; namespace RMROC451.TweaksAndThings.Patches; @@ -34,6 +35,8 @@ internal class EngineRosterRow_Refresh_Patch string fuelInfoText = string.Empty; string fuelInfoTooltip = string.Empty; + TweakyTweakTweakers(__instance); + if (tweaksAndThings == null || rosterFuelColumnSettings == null || !tweaksAndThings.IsEnabled() || @@ -52,7 +55,7 @@ internal class EngineRosterRow_Refresh_Patch bool cabooseRequirementFulfilled = !tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() || consist.ConsistNoFreight() - || (bool)engineOrTender.FindMyCaboose(0.0f, false); + || (bool)engineOrTender.FindMyCabooseSansLoadRequirement(); float offendingPercentage = 100f; foreach (Car loco in locos) @@ -131,13 +134,54 @@ internal class EngineRosterRow_Refresh_Patch default: break; } - } catch (Exception ex) + } + catch (Exception ex) { rosterFuelColumnSettings.EngineRosterFuelStatusColumn = EngineRosterFuelDisplayColumn.None; Log.Error(ex, "Error Detecting fuel status for engine roster"); } } + private static void TweakyTweakTweakers(EngineRosterRow __instance) + { + var helperData = EngineTextHelper(__instance._engine); + + if (helperData.HasValue) + { + __instance.nameLabel.text = helperData.Value.nameLabel; + __instance.nameTooltip.tooltipText += helperData.Value.nameTooltip; + } + + __instance.crewLabel.text = string.Empty; + for (int i = __instance._crewComponents.Count - 1; i >= 0; i--) + { + string str = __instance._crewComponents[i]; + if ((new[] { "MU", "AE" }).Contains(str)) + str = $"{str} "; + __instance.crewLabel.text = $"{str}{__instance.crewLabel.text}"; + } + } + + internal static (string nameLabel, string nameTooltip, int selectedCount)? EngineTextHelper(Car loco, bool mapIcon = false) + { + (string nameLabel, string nameTooltip, int selectedCount)? output = null; + int selectedCount = 0; + Dictionary dictionary = StateManager.Shared.PlayersManager.AllPlayers.ToDictionary((IPlayer p) => p.PlayerId, (IPlayer p) => p); + List usersSelected = new(); + foreach (var kvp in dictionary) + { + if (new PlayerProperties(PlayerPropertiesManager.Shared._object[kvp.Key.ToString()]).SelectedCarId == loco.id) + { + usersSelected.Add(kvp.Value.Name); + selectedCount++; + } + } + + if (selectedCount > 0 && dictionary.Count > 1) + output = ($"{(mapIcon && loco is BaseLocomotive ? loco.Ident.RoadNumber : loco.DisplayName)}{selectedCount}", $"{Environment.NewLine}Selected by: {string.Join(", ", usersSelected)}", selectedCount); + return output; + } + private static void SetLabelAndTooltip(ref TMP_Text label, ref UITooltipProvider tooltip, string fuelInfoText, string fuelInfoTooltip) { label.text = $" {fuelInfoText} {label.text}"; diff --git a/TweaksAndThings/Patches/ExpandedConsole_Patch.cs b/TweaksAndThings/Patches/ExpandedConsole_Patch.cs index 50b2461..19fa469 100644 --- a/TweaksAndThings/Patches/ExpandedConsole_Patch.cs +++ b/TweaksAndThings/Patches/ExpandedConsole_Patch.cs @@ -1,7 +1,8 @@ using Game; using Game.State; using HarmonyLib; -using Helpers; +using Model; +using Model.Ops.Timetable; using Newtonsoft.Json; using Railloader; using Serilog; @@ -21,6 +22,7 @@ namespace RMROC451.TweaksAndThings.Patches; [HarmonyPatchCategory("RMROC451TweaksAndThings")] internal class ExpandedConsole_Add_Patch { + private static Serilog.ILogger _log => Log.ForContext(); private static void Prefix(ref UI.Console.Console.Entry entry) { entry.Text = $"{entry.Timestamp} : {entry.Text}"; @@ -28,12 +30,26 @@ internal class ExpandedConsole_Add_Patch SendMs((UI.Console.Console.Entry?)entry); } + + private static string hold => @"```ansi +■{loco}■ {msg} +```"; + + private static string west => @"```ansi +◀{loco}  {msg} +```"; + + private static string east => @"```ansi + {loco}▶ {msg} +```"; + internal static void SendMs(UI.Console.Console.Entry? entry, string? text = null) { try { if (entry is null && !String.IsNullOrEmpty(text)) entry = new() { Text = text }; var msgText = entry?.Text ?? string.Empty; + if (msgText.StartsWith("Usage:")) return; TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; StateManager shared = StateManager.Shared; GameStorage gameStorage = shared.Storage; @@ -50,31 +66,45 @@ internal class ExpandedConsole_Add_Patch var carId = t.IsMatch(msgText) ? Regex.Match(msgText, "car:(.*?)\"").Groups[1].Captures[0].ToString() : string.Empty; Model.Car? car = TrainController.Shared.CarForString(carId); + var data = UpdateCarText(car); bool engineInMessage = car?.IsLocomotive ?? false; - var image = engineInMessage ? - new - { - url = string.Empty - } : - null; + string msgToSend = string.Empty; + string desc = Regex.Replace(msgText, "<.*?>", ""); + desc = !!car ? desc.Replace(car?.DisplayName ?? string.Empty, string.Empty) : desc; + desc = desc.Trim();//.Replace(": ", "\n"); - if (engineInMessage) + if (!!car && engineInMessage) { CTCPanelMarkerManager cTCPanelMarkerManager = UnityEngine.Object.FindObjectOfType(); CTCPanelMarker marker = cTCPanelMarkerManager?._markers?.Values?.FirstOrDefault(v => v.TooltipInfo.Text.Contains(car.Ident.RoadNumber)); - - string color = CTCPanelMarker.InferColorFromText(car?.DisplayName).HexString().Replace("#", string.Empty); - if (marker != null) + + string markerText = marker?.TooltipInfo.Text ?? string.Empty; + if (markerText.StartsWith(">") || markerText.EndsWith(">") || data.Item2 == Timetable.Direction.East || msgText.Contains("*-")) { - color = CTCPanelMarker.InferColorFromText(marker.TooltipInfo.Text).HexString().Replace("#", string.Empty); + msgToSend = east.Replace("{loco}", car?.DisplayName.Replace(" ", string.Empty)).Replace("{msg}", desc); } - image = new + else if (markerText.StartsWith("<") || markerText.EndsWith("<") || data.Item2 == Timetable.Direction.West || msgText.Contains("-*")) { - url = $"https://img.shields.io/badge/{car.DisplayName.Replace(" ", "%20")}-%20-{color}.png" - }; - + msgToSend = west.Replace("{loco}", car?.DisplayName.Replace(" ", string.Empty)).Replace("{msg}", desc); + } + else + { + msgToSend = hold.Replace("{loco}", car?.DisplayName.Replace(" ", string.Empty)).Replace("{msg}", desc); + } } + else + { + msgToSend = desc; + } + msgToSend = msgToSend + .Replace(" , ", ", ") + .Replace(", :", " :") + .Replace(" ; ", "; ") + .Replace("*-", string.Empty) + .Replace("#", string.Empty) + .Replace("-*", string.Empty) + .Trim(); var SuccessWebHook = new { @@ -83,9 +113,7 @@ internal class ExpandedConsole_Add_Patch { new { - description= Regex.Replace(msgText, "<.*?>", "").Replace(": ", "\n"), - timestamp=DateTime.UtcNow, - image + description= msgToSend }, } }; @@ -103,6 +131,26 @@ internal class ExpandedConsole_Add_Patch } } + public static (string , Timetable.Direction?) UpdateCarText(Car car) + { + string output = string.Empty; + Timetable.Direction? dir = null; + if (car?.IsLocomotive ?? false) + { + if (StateManager.Shared.PlayersManager.TrainCrewForId(car.trainCrewId, out var trainCrew)) + { + output = trainCrew.Name; + if (TimetableController.Shared.TryGetTrainForTrainCrew(trainCrew, out Timetable.Train timetableTrain)) + { + dir = timetableTrain.Direction; + output += " (Train " + timetableTrain.DisplayStringShort + ")"; + } + } + } + return (output, dir); + } + + public static GameDateTime RealNow() { var now = DateTime.Now; diff --git a/TweaksAndThings/Patches/InterchangedIndustryLoader_ServiceInterchange_Patch.cs b/TweaksAndThings/Patches/InterchangedIndustryLoader_ServiceInterchange_Patch.cs index 14660a5..7c5e0d3 100644 --- a/TweaksAndThings/Patches/InterchangedIndustryLoader_ServiceInterchange_Patch.cs +++ b/TweaksAndThings/Patches/InterchangedIndustryLoader_ServiceInterchange_Patch.cs @@ -48,8 +48,7 @@ internal class InterchangedIndustryLoader_ServiceInterchange_Patch if (num4 > 0) { var canAfford = shared.CanAfford(num4); - penalty = Mathf.CeilToInt(!canAfford ? num4 * 0.2f : 0); - __instance.Industry.ApplyToBalance(-num4, __instance.ledgerCategory, null, 0, quiet: true); + penalty += Mathf.CeilToInt(!canAfford ? num4 * 0.2f : 0); num2 += num4; num3++; diff --git a/TweaksAndThings/Patches/MapWindow_OnClick_Patch.cs b/TweaksAndThings/Patches/MapWindow_OnClick_Patch.cs index 5619df3..c1aef99 100644 --- a/TweaksAndThings/Patches/MapWindow_OnClick_Patch.cs +++ b/TweaksAndThings/Patches/MapWindow_OnClick_Patch.cs @@ -39,7 +39,7 @@ internal class MapWindow_OnClick_Patch texture2D.wrapMode = TextureWrapMode.Clamp; if (!ImageConversion.LoadImage(texture2D, File.ReadAllBytes(path))) { - _log.Information($"Unable to load {name} icon!"); + _log.Debug($"Unable to load {name} icon!"); return null; } Sprite sprite = Sprite.Create(texture2D, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f)); diff --git a/TweaksAndThings/Patches/NoticeExtensions_PostNotice_Patch.cs b/TweaksAndThings/Patches/NoticeExtensions_PostNotice_Patch.cs index 27fe48c..17f0d19 100644 --- a/TweaksAndThings/Patches/NoticeExtensions_PostNotice_Patch.cs +++ b/TweaksAndThings/Patches/NoticeExtensions_PostNotice_Patch.cs @@ -1,4 +1,5 @@ using Game.Notices; +using Game.State; using HarmonyLib; using Model; using Serilog; @@ -14,10 +15,11 @@ internal class NoticeExtensions_PostNotice_Patch private static ILogger _log => Log.ForContext(); static void Postfix(Car car, string key, string content) { + if (!StateManager.IsHost) return; try { - //Log.Information($"{car.DisplayName} patch PostNotice"); + //Log.Debug($"{car.DisplayName} patch PostNotice"); if (!string.IsNullOrEmpty(content) && key.Equals("ai-wpt") && content.ToLower().Contains("Arrived at Waypoint".ToLower()) diff --git a/TweaksAndThings/Patches/OpsController_AnnounceCoalescedPayments_Patch.cs b/TweaksAndThings/Patches/OpsController_AnnounceCoalescedPayments_Patch.cs index 337045a..8ac62fd 100644 --- a/TweaksAndThings/Patches/OpsController_AnnounceCoalescedPayments_Patch.cs +++ b/TweaksAndThings/Patches/OpsController_AnnounceCoalescedPayments_Patch.cs @@ -24,7 +24,7 @@ internal class CarExtensions_LoadString_Patch { public static bool Prefix(CarLoadInfo info, Load load, ref string __result) { - bool output = load.id == OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad().id; + bool output = load.id == OpsController_AnnounceCoalescedPayments_Patch.CrewLoadHours.id; if (output) __result = info.Quantity.FormatCrewHours(load.description); return !output; @@ -38,7 +38,7 @@ internal class CarPrototypeLibrary_LoadForId_Patch { public static bool Prefix(string loadId, ref Load __result) { - Load load = OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad(); + Load load = OpsController_AnnounceCoalescedPayments_Patch.CrewLoadHours; if (loadId == load.id) __result = load; return __result == null; @@ -70,7 +70,9 @@ internal class OpsController_AnnounceCoalescedPayments_Patch typeof(RepairTrack) }; - public static Load CrewHoursLoad() + private static Load _crewLoadHours; + internal static Load CrewLoadHours => _crewLoadHours ?? CrewHoursLoad(); + private static Load CrewHoursLoad() { Load load = (Load)ScriptableObject.CreateInstance(typeof(Load)); load.name = "crew-hours"; @@ -96,6 +98,7 @@ internal class OpsController_AnnounceCoalescedPayments_Patch var data = car.QuantityCapacityOfLoad(CrewHoursLoad()); if ((data.quantity + quantityToLoad > data.capacity) && data.quantity < data.capacity) { + quantityToLoad = data.capacity; //ensure topping off Multiplayer.Broadcast($"{Hyperlink.To(car)}: \"Caboose crew topped off.\""); CrewCarDict[car.id] = (CrewCarDict[car.id].spotted, false); } @@ -113,18 +116,18 @@ internal class OpsController_AnnounceCoalescedPayments_Patch var passengerStops = OpsController.Shared.AllIndustries .SelectMany(i => i.TrackDisplayables.Where(t => refillLocations.Contains(t.GetType()))); - //Log.Information($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper => PassengerStops => {string.Join(",", passengerStops)}"); + //Log.Debug($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper => PassengerStops => {string.Join(",", passengerStops)}"); var cabeese = passengerStops .SelectMany(t => t.TrackSpans?.Select(s => (tc.CarsOnSpan(s) ?? Enumerable.Empty()).Where(c => c.IsCaboose()))?.SelectMany(c => c?.Select(c2 => (t, c2)))); - //Log.Information($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper => PassengerStops Cabeese => {string.Join(",", cabeese?.Select(c => $"{c.t} : {c.c2}") ?? [])}"); + //Log.Debug($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper => PassengerStops Cabeese => {string.Join(",", cabeese?.Select(c => $"{c.t} : {c.c2}") ?? [])}"); CrewCarDict = CrewCarDict.Where(kvp => cabeese.Select(c => c.c2.id).Contains(kvp.Key)).ToDictionary(k => k.Key, v => v.Value); var deltaTime = (float)(TimeWeather.Now.TotalSeconds - dateTime.TotalSeconds); foreach (var caboose in cabeese) { - //Log.Information($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper ({deltaTime}) => {caboose.t} : {caboose.c2}"); + //Log.Debug($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper ({deltaTime}) => {caboose.t} : {caboose.c2}"); CarLoadCrewHelper(caboose.c2, deltaTime); } dateTime = TimeWeather.Now; diff --git a/TweaksAndThings/Patches/StateManager_OnDayDidChange_Patch.cs b/TweaksAndThings/Patches/StateManager_OnDayDidChange_Patch.cs index a760448..ccd0406 100644 --- a/TweaksAndThings/Patches/StateManager_OnDayDidChange_Patch.cs +++ b/TweaksAndThings/Patches/StateManager_OnDayDidChange_Patch.cs @@ -36,11 +36,11 @@ internal class StateManager_OnDayDidChange_Patch { foreach (var car in TrainController.Shared.Cars.Where(Car_Extensions.IsCaboose)) { - var data = car.QuantityCapacityOfLoad(OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad()); + var data = car.QuantityCapacityOfLoad(OpsController_AnnounceCoalescedPayments_Patch.CrewLoadHours); if (data.quantity < data.capacity) { Multiplayer.Broadcast($"{Hyperlink.To(car)}: \"Caboose crew topped off.\""); - new OpsCarAdapter(car, OpsController.Shared).Load(OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad(), data.capacity - data.quantity); + new OpsCarAdapter(car, OpsController.Shared).Load(OpsController_AnnounceCoalescedPayments_Patch.CrewLoadHours, data.capacity - data.quantity); } } } diff --git a/TweaksAndThings/Patches/TagController_UpdateTag_Patch.cs b/TweaksAndThings/Patches/TagController_UpdateTag_Patch.cs index 724e5b1..dc46cd5 100644 --- a/TweaksAndThings/Patches/TagController_UpdateTag_Patch.cs +++ b/TweaksAndThings/Patches/TagController_UpdateTag_Patch.cs @@ -1,13 +1,17 @@ using Game.State; using HarmonyLib; +using KeyValue.Runtime; using Model; using Model.Ops; using Railloader; using RMROC451.TweaksAndThings.Extensions; +using RollingStock; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using UI.Tags; +using static Unity.IO.LowLevel.Unsafe.AsyncReadManagerMetrics; namespace RMROC451.TweaksAndThings.Patches; @@ -28,7 +32,7 @@ internal class TagController_UpdateTag_Patch return; } - ProceedWithPostFix(car, tagCallout, tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() || !tweaksAndThings.CabooseRequiredForLocoOilIndicator()); + ProceedWithPostFix(car, tagCallout, tweaksAndThings.CabooseRequiredForLocoOilIndicator()); return; } @@ -43,7 +47,7 @@ internal class TagController_UpdateTag_Patch //if (car.EnableOiling) tags.Add(car.HasHotbox ? TextSprites.Hotbox : $"{TextSprites.Warning}{car.Oiled.TriColorPiePercent(1)}"); if (car.EnableOiling) tags.Add(car.HasHotbox ? TextSprites.Hotbox : car.Oiled.TriColorPiePercent(1, oilSpriteName)); IEnumerable consist = car.EnumerateCoupled().Where(c => c.EnableOiling); - Func cabooseRequirementFulfilled = () => (!cabooseRequired || consist.ConsistNoFreight() || car.FindMyCaboose(0.0f, false)); + Func cabooseRequirementFulfilled = () => (!cabooseRequired || consist.ConsistNoFreight() || (bool)car.FindMyCabooseSansLoadRequirement()); if (StateManager.Shared.Storage.OilFeature && car.IsLocomotive && !car.NeedsOiling @@ -54,6 +58,18 @@ internal class TagController_UpdateTag_Patch if (car.EndAirSystemIssue()) tags.Add(TextSprites.CycleWaybills); if (car.HandbrakeApplied()) tags.Add(TextSprites.HandbrakeWheel); + if (car.IsPassengerCar()) + { + PassengerMarker? passengerMarker = car.GetPassengerMarker(); + if (passengerMarker.HasValue) + { + IEnumerable loadInfo = car.PassengerCountString(passengerMarker).Split('/'); + //string item4 = CarPickable.PassengerString(car, passengerMarker.Value); + string val = TextSprites.PiePercent(float.Parse(loadInfo.First()), float.Parse(loadInfo.Last())) + $" {car.PassengerCountString(passengerMarker)} Passengers"; + tagCallout.callout.Text = tagCallout.callout.Text.Contains("Empty") ? tagCallout.callout.Text.Replace("Empty", val) : tagCallout.callout.Text + $"\n{val}"; + } + } + tagCallout.callout.Title = tags.Any() switch { diff --git a/TweaksAndThings/Patches/TrainBrakeDisplay_GetCarImage_Patch.cs b/TweaksAndThings/Patches/TrainBrakeDisplay_GetCarImage_Patch.cs new file mode 100644 index 0000000..05d80a4 --- /dev/null +++ b/TweaksAndThings/Patches/TrainBrakeDisplay_GetCarImage_Patch.cs @@ -0,0 +1,42 @@ +using HarmonyLib; +using Model; +using Railloader; +using UI; +using UI.Tags; +using UnityEngine; + +namespace RMROC451.TweaksAndThings.Patches; + +[HarmonyPatch(typeof(TrainBrakeDisplay))] +[HarmonyPatch(nameof(TrainBrakeDisplay.Update))] +[HarmonyPatchCategory("RMROC451TweaksAndThings")] +internal class TrainBrakeDisplay_Update_Patch() +{ + private static bool Prefix(TrainBrakeDisplay __instance) + { + + TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; + if (!tweaksAndThings.IsEnabled()) return true; + + TweakedOriginalMethods.TrainBrakeDisplay.Update(__instance); + + return false; + } +} + +[HarmonyPatch(typeof(TrainBrakeDisplay))] +[HarmonyPatch(nameof(TrainBrakeDisplay.ColorForCar))] +[HarmonyPatchCategory("RMROC451TweaksAndThings")] +internal class TrainBrakeDisplay_ColorForCar_Patch +{ + private static bool Prefix(TrainBrakeDisplay __instance, Car car, ref Color __result) + { + TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; + if (!tweaksAndThings.IsEnabled() || !tweaksAndThings.TrainBrakeDisplayShowsColorsInCalloutMode() || !TagController.Shared.TagsVisible) return true; + + TweakedOriginalMethods.TrainBrakeDisplay.ColorForCar(__instance, car, ref __result); + + return false; + } +} + diff --git a/TweaksAndThings/Patches/WedgeImage_OnPopulateMesh.cs b/TweaksAndThings/Patches/WedgeImage_OnPopulateMesh.cs deleted file mode 100644 index dcc7121..0000000 --- a/TweaksAndThings/Patches/WedgeImage_OnPopulateMesh.cs +++ /dev/null @@ -1,20 +0,0 @@ -using HarmonyLib; -using Railloader; -using UI.ContextMenu; -using UnityEngine.UI; - -namespace RMROC451.TweaksAndThings.Patches; - -[HarmonyPatch(typeof(WedgeImage))] -[HarmonyPatch(nameof(WedgeImage.OnPopulateMesh), typeof(VertexHelper))] -[HarmonyPatchCategory("RMROC451TweaksAndThings")] -internal class WedgeImage_OnPopulateMesh_Patch -{ - private static void Postfix(VertexHelper vh) - { - TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase.Shared; - if (!tweaksAndThings.IsEnabled()) return; - - vh.Clear(); //clear the image backgrounds for now. - } -} diff --git a/TweaksAndThings/RMROC451.TweaksAndThings.csproj b/TweaksAndThings/RMROC451.TweaksAndThings.csproj index c4d70df..8069656 100644 --- a/TweaksAndThings/RMROC451.TweaksAndThings.csproj +++ b/TweaksAndThings/RMROC451.TweaksAndThings.csproj @@ -1,9 +1,8 @@  - + + - - False False diff --git a/TweaksAndThings/Settings/Settings.cs b/TweaksAndThings/Settings/Settings.cs index de4b2a1..c8e93af 100644 --- a/TweaksAndThings/Settings/Settings.cs +++ b/TweaksAndThings/Settings/Settings.cs @@ -28,8 +28,10 @@ public class Settings bool cabooseRequiredForLocoTagOilIndication, bool servicingFundPenalty, bool safetyFirst, + bool safetyFirstClientEnforce, CrewHourLoadMethod loadCrewHoursMethod, - float cabeeseSearchRadiusFtInMeters + float cabeeseSearchRadiusFtInMeters, + bool trainBrakeDisplayShowsColorsInCalloutMode ) { WebhookSettingsList = webhookSettingsList; @@ -41,8 +43,10 @@ public class Settings CabooseRequiredForLocoTagOilIndication = cabooseRequiredForLocoTagOilIndication; ServicingFundPenalty = servicingFundPenalty; SafetyFirst = safetyFirst; + SafetyFirstClientEnforce = safetyFirstClientEnforce; LoadCrewHoursMethod = loadCrewHoursMethod; CabeeseSearchRadiusFtInMeters = cabeeseSearchRadiusFtInMeters; + TrainBrakeDisplayShowsColorsInCalloutMode = trainBrakeDisplayShowsColorsInCalloutMode; } public readonly UIState _selectedTabState = new UIState(null); @@ -55,8 +59,10 @@ public class Settings public bool CabooseRequiredForLocoTagOilIndication; public bool ServicingFundPenalty; public bool SafetyFirst; + public bool SafetyFirstClientEnforce; public CrewHourLoadMethod LoadCrewHoursMethod; public float CabeeseSearchRadiusFtInMeters; + public bool TrainBrakeDisplayShowsColorsInCalloutMode; internal void AddAnotherRow() { @@ -64,7 +70,7 @@ public class Settings if (!string.IsNullOrEmpty(WebhookSettingsList.OrderByDescending(wsl => wsl.WebhookUrl).Last().WebhookUrl)) { WebhookSettingsList.Add(new()); - Log.Information($"Adding another {nameof(WebhookSettings)} list entry, last one was filled in"); + Log.Debug($"Adding another {nameof(WebhookSettings)} list entry, last one was filled in"); } } } @@ -126,16 +132,18 @@ public static class SettingsExtensions public static bool RequireConsistCabooseForOilerAndHotboxSpotter(this TweaksAndThingsPlugin input) => input?.settings?.RequireConsistCabooseForOilerAndHotboxSpotter ?? false; public static bool CabooseNonMotiveAllowedSetting(this TweaksAndThingsPlugin input, Car car) => - input.EndGearHelpersRequirePayment() && !car.MotivePower() && (bool)car.FindMyCaboose(0.0f, false); + input.EndGearHelpersRequirePayment() && !car.MotivePower() && (bool)car.FindMyCabooseSansLoadRequirement(); public static bool CabooseRequiredForLocoOilIndicator(this TweaksAndThingsPlugin input) => input?.settings?.CabooseRequiredForLocoTagOilIndication ?? false; public static bool ServiceFundPenalties(this TweaksAndThingsPlugin input) => 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 float CabeeseSearchRadiusInMeters(this TweaksAndThingsPlugin input) => - input?.settings?.CabeeseSearchRadiusFtInMeters ?? 21f; + public static bool TrainBrakeDisplayShowsColorsInCalloutMode(this TweaksAndThingsPlugin input) => + input?.settings?.TrainBrakeDisplayShowsColorsInCalloutMode ?? false; } \ No newline at end of file diff --git a/TweaksAndThings/TweakedOriginalMethods/TrainBrakeDisplay.cs b/TweaksAndThings/TweakedOriginalMethods/TrainBrakeDisplay.cs new file mode 100644 index 0000000..08ad136 --- /dev/null +++ b/TweaksAndThings/TweakedOriginalMethods/TrainBrakeDisplay.cs @@ -0,0 +1,177 @@ +using Model; +using Model.Ops; +using Model.Physics; +using Railloader; +using RMROC451.TweaksAndThings.Patches; +using UI; +using UI.CarInspector; +using UI.Common; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using static UnityEngine.UI.Image; +using Object = UnityEngine.Object; + +namespace RMROC451.TweaksAndThings.TweakedOriginalMethods +{ + internal static class TrainBrakeDisplay + { + internal static void ColorForCar(UI.TrainBrakeDisplay __instance, Car car, ref Color __result) + { + __result = Color.gray; + + if (car.IsDerailed) + { + __result = __instance.derailedColor; + return; + } + CarAirSystem air = car.air; + OpsController opsController = OpsController.Shared; + if (air.handbrakeApplied) + { + __result = __instance.handbrakeAppliedColor; + return; + } + else if ( + (Object)(object)opsController != (Object)null && + opsController.TryGetDestinationInfo(car, out var destinationName, out var isAtDestination, out var destinationPosition, out var destination) + ) + { + Area area = opsController.AreaForCarPosition(destination); + if ((Object)(object)area != (Object)null) + { + __result = area.tagColor; + } + if (!isAtDestination) + { + float num = 1f / __result.maxColorComponent; + __result *= num; + } + } + + if (Mathf.InverseLerp(0f, 72f, air.BrakeCylinder.Pressure) >= 10f) __result = (__result + __instance.ColorForPsi(air.BrakeCylinder.Pressure)) / 2f; + } + + internal static Image GetCarImage(UI.TrainBrakeDisplay __instance, int index, float xCyl, float yCyl, Car car) + { + Image output = null; + if (__instance._carImages.Count - 1 < index) + { + var tweaksAndThings = SingletonPluginBase.Shared; + GameObject val = new GameObject(); + val.transform.SetParent(((Component)__instance).transform, false); + ((Object)val).name = $"Car {index}"; + float height = 12f; + val.AddComponent().SetFrame(xCyl, yCyl, __instance._imageWidth, height); + EventTrigger trigger = val.AddComponent(); + EventTrigger.Entry entry = new EventTrigger.Entry(); + entry.eventID = EventTriggerType.PointerClick; + entry.callback.AddListener((eventData) => { + PointerEventData ped = (PointerEventData)eventData; + PickableActivation pa = PickableActivation.Primary; + if (ped.button == PointerEventData.InputButton.Right) pa = PickableActivation.Secondary; + + bool checkFurther = ped.button == PointerEventData.InputButton.Middle ? true : CarPickable_Activate_Patch.HandleCarOrTrainBrakeDisplayClick(car, tweaksAndThings, pa); + if (checkFurther & GameInput.IsControlDown) CarInspector.Show(car); + }); + trigger.triggers.Add(entry); + output = val.AddComponent(); + + output.type = (Type)1; + __instance._carImages.Add(output); + } + else + { + output = __instance._carImages[index]; + ((Component)output).gameObject.SetActive(true); + } + output.sprite = (car.IsLocomotive ? __instance.locomotiveTile : __instance.carTile); + return output; + } + + internal static void Update(UI.TrainBrakeDisplay __instance) + { + Car selectedCar = __instance._trainController.SelectedCar; + if (selectedCar == null || selectedCar.set == null) return; + + int numberOfCars = selectedCar.set.NumberOfCars; + if (numberOfCars != __instance._lastNumCars) + { + __instance.RemoveAllImages(); + int num = Mathf.Clamp(numberOfCars, 0, 100); + float num2 = (float)num * 8f + (float)(num - 1) * 1f; + if (num2 > __instance._rectTransform.rect.width) + { + float num3 = __instance._rectTransform.rect.width / num2; + __instance._imageWidth = 8f * num3; + __instance._spacing = 1f * num3; + } + else + { + __instance._imageWidth = 8f; + __instance._spacing = 1f; + } + + __instance._lastNumCars = numberOfCars; + } + + int num4 = 0; + float num5 = __instance._imageWidth / 2f; + float num6 = 0f; + float y = 12f - 2f * __instance._spacing; + Car.LogicalEnd logicalEnd = ((selectedCar.set.IndexOfCar(selectedCar).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 = selectedCar.set.StartIndexForConnected(selectedCar, logicalEnd, IntegrationSet.EnumerationCondition.Coupled); + Car car; + while (!stop && (car = selectedCar.set.NextCarConnected(ref carIndex, logicalEnd, IntegrationSet.EnumerationCondition.Coupled, out stop)) != null && !(num5 > __instance._rectTransform.rect.width)) + { + Image carImage = TweakedOriginalMethods.TrainBrakeDisplay.GetCarImage(__instance, num4, num5, 0f, car); + num5 += __instance._imageWidth + __instance._spacing; + carImage.color = __instance.ColorForCar(car); + Image airImage = __instance.GetAirImage(num4, num6, y); + num6 += __instance._imageWidth + __instance._spacing; + Color color; + if (!car[logicalEnd].IsCoupled) + { + color = ColorForOuterAnglecock(car[logicalEnd].AnglecockSetting); + } + else + { + Car car2 = car.CoupledTo(logicalEnd); + bool num7 = car2 != null && car2[end].AnglecockSetting > 0.9f; + bool flag = car[logicalEnd].AnglecockSetting > 0.9f; + color = ((num7 && flag && car[logicalEnd].IsAirConnected) ? Color.clear : Color.white); + } + + airImage.color = color; + if (!car[end].IsCoupled) + { + __instance.GetAirImage(num4 + 1, num6, y).color = ColorForOuterAnglecock(car[end].AnglecockSetting); + } + + num4++; + } + + for (int i = num4; i < __instance._carImages.Count; i++) + { + __instance._carImages[i].gameObject.SetActive(value: false); + } + + for (int j = num4 + 1; j < __instance._airImages.Count; j++) + { + __instance._airImages[j].gameObject.SetActive(value: false); + } + + static Color ColorForOuterAnglecock(float value) + { + if (!((double)value < 0.01)) + { + return Color.white; + } + + return Color.clear; + } + } + } +} diff --git a/TweaksAndThings/TweaksAndThingsPlugin.cs b/TweaksAndThings/TweaksAndThingsPlugin.cs index 7a623b2..e7382f5 100644 --- a/TweaksAndThings/TweaksAndThingsPlugin.cs +++ b/TweaksAndThings/TweaksAndThingsPlugin.cs @@ -40,7 +40,6 @@ public class TweaksAndThingsPlugin : SingletonPluginBase, static TweaksAndThingsPlugin() { - Log.Information("Hello! Static Constructor was called!"); } public TweaksAndThingsPlugin(IModdingContext moddingContext, IModDefinition self) @@ -49,7 +48,7 @@ public class TweaksAndThingsPlugin : SingletonPluginBase, this.moddingContext = moddingContext; - logger.Information("Hello! Constructor was called for {modId}/{modVersion}!", self.Id, self.Version); + logger.Debug("Hello! Constructor was called for {modId}/{modVersion}!", self.Id, self.Version); moddingContext.RegisterConsoleCommand(new EchoCommand()); @@ -58,7 +57,6 @@ public class TweaksAndThingsPlugin : SingletonPluginBase, public override void OnEnable() { - logger.Information("OnEnable() was called!"); var harmony = new Harmony(modDefinition.Id); harmony.PatchCategory(modDefinition.Id.Replace(".", string.Empty)); } @@ -72,13 +70,10 @@ public class TweaksAndThingsPlugin : SingletonPluginBase, public void Update() { - logger.Verbose("UPDATE()"); } public void ModTabDidOpen(UIPanelBuilder builder) { - logger.Information("Daytime!"); - if (settings == null) settings = new(); if (!settings?.WebhookSettingsList?.Any() ?? true) settings.WebhookSettingsList = new[] { new WebhookSettings() }.ToList(); if (settings?.EngineRosterFuelColumnSettings == null) settings.EngineRosterFuelColumnSettings = new(); @@ -88,7 +83,9 @@ public class TweaksAndThingsPlugin : SingletonPluginBase, settings?.WebhookSettingsList.SanitizeEmptySettings(); builder.AddSection("Adjustments To Base Game", (UIPanelBuilder builder) => { - builder.AddLabel("Repair tracks now require cars to be waybilled, or they will not be serviced/overhauled.\nThey will report on the company window's location section as 'No Work Order Assigned'."); + builder.AddLabel("1) Repair tracks now require cars to be waybilled, or they will not be serviced/overhauled.\nThey will report on the company window's location section as 'No Work Order Assigned'."); + builder.Spacer(spacing * spacing); + builder.AddLabel("2) You now have the same click options on the little car icons in the lower left engine controls ui, as you do with cars in the game. Ctrl click -> open car inspector, etc."); }); builder.Spacer(spacing * spacing); builder.AddTabbedPanels(settings._selectedTabState, delegate (UITabbedPanelBuilder tabBuilder) @@ -206,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) @@ -236,6 +250,18 @@ AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 - } ).Tooltip("Allow Insufficient Funds", $@"Will allow interchange service and repair shops to still function when you are insolvent, at a 20% overdraft fee."); + builder.Spacer(spacing); + builder.AddFieldToggle( + "Train Brake Color Mode", + () => this.TrainBrakeDisplayShowsColorsInCalloutMode(), + delegate (bool enabled) + { + if (settings == null) settings = new(); + settings.TrainBrakeDisplayShowsColorsInCalloutMode = enabled; + builder.Rebuild(); + } + ).Tooltip("Train Brake Color Mode", $@"When enabled/checked and car tag callout mode is enabled (showing car tags hovering over them), the train brake display of the selected locomotive will change the cars/engines to their destination area's color to help you visualize sets of cars at a glance."); + builder.Spacer(spacing); EngineRosterShowsFuelStatusUISection(builder); } @@ -256,7 +282,7 @@ AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 - builder.Rebuild(); } ) - ).Tooltip("Enable Fuel Display in Engine Roster", $"Will add reaming fuel indication to Engine Roster (with details in roster row tool tip), Examples : {string.Join(" ", Enumerable.Range(0, 4).Select(i => TextSprites.PiePercent(i, 4)))}"); + ).Tooltip("Enable Fuel Display in Engine Roster", $"Will add reamaing fuel indication to Engine Roster (with details in roster row tool tip), Examples : {string.Join(" ", Enumerable.Range(0, 4).Select(i => TextSprites.PiePercent(i, 4)))}"); builder.Spacer(spacing); builder.AddFieldToggle( @@ -333,7 +359,6 @@ AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 - public void ModTabDidClose() { - logger.Information("Nighttime..."); this.moddingContext.SaveSettingsData(this.modDefinition.Id, settings ?? new()); } }