Compare commits

...

21 Commits

Author SHA1 Message Date
8a21ec2e79 Support fallback to vanilla icons if files are not found. Allow configuration for context menu pagination between car/consist option preferences by player 2025-09-06 22:03:25 -05:00
3a04405dad rework context menu and add custom sprites, not registered with in game library though :( 2025-09-05 17:07:23 -05:00
ba94d80401 allows "deprecated" model metadata tag to exlude cars from being utilized for ordering, but will allow them to run out their lives on the rails until they export. Useful for when you mod in a car, and want to remove it without hunting around to find all of them and deleting them. 2025-09-05 10:51:03 -05:00
d692567d95 put everything back in context menu as one big happy family. 2025-09-05 10:50:00 -05:00
32f3e8b2db tweaks to engine roster to show who has engines selected and throttling on AE Waypoint picker to 60fps. 2025-09-02 22:19:41 -05:00
a7ae1d479c limit AutoEngineerDestinationPicker.Loop to running max of 10x per second rather than as fast as it can (caused stuttering in areas with lots of cars). 2025-09-02 10:04:58 -05:00
b562424f8b move loco post notice about WP set to host, so clients get them 2025-09-02 09:42:14 -05:00
f1b68d2827 one more of the ill fated traincontroller shared usages while on host processing. 2025-09-01 23:53:23 -05:00
b911ee6f6f version -> 2.1.7 2025-09-01 12:54:16 -05:00
cc18579507 host now can assert safety first; setting added to allow clients to override. Client does not require mod for this to be enforced! 2025-09-01 12:53:52 -05:00
1dadb04cbe round to whole number vs showing partial full mph in car inspector consist info (when caboose is nearby) 2025-09-01 12:51:29 -05:00
1032edb7ef switch roster MU hiding to be based on cutout not MU 2025-09-01 12:50:50 -05:00
c0e75c3c39 version++ 2025-08-31 18:33:06 -05:00
bd215d0dcc coroutine vs observers 2025-08-31 18:32:48 -05:00
554ea11790 2.1.5 updates
passenger car load info in tags; refactor cog logic again; formatting updates for webhook messages
2025-08-30 22:38:31 -05:00
c114aec3b3 fixes #70; forgot to pass boolean after refactor. 2025-08-29 09:17:24 -05:00
38a43276f7 fixes #69; track loading wasn't fully topping off. 2025-08-29 09:15:36 -05:00
afd18b9d27 fixes #68; bugged 120% penalty adjusted to intended 20% 2025-08-28 11:12:07 -05:00
52e9fe1cab additional refactor for #56; fixing AEPlanner spamming recalc of gear when coupling in waypoint mode and reporting stationary endlessly. 2025-08-25 17:08:18 -05:00
ec9aadd6e2 refactor for #56; improved caching, observer detection of waybill/repair destination changing debouncing, and coupled car detection fixes. increment version. 2025-08-25 08:55:19 -05:00
af8e1bd6ca Fix issue with inverted boolean and params out of order; version increment 2025-08-24 20:25:44 -05:00
34 changed files with 783 additions and 312 deletions

View File

@@ -2,6 +2,6 @@
<PropertyGroup>
<MajorVersion>2</MajorVersion>
<MinorVersion>1</MinorVersion>
<PatchVersion>1</PatchVersion>
<PatchVersion>8</PatchVersion>
</PropertyGroup>
</Project>

View File

@@ -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 ?? "???"));
if (StateManager.IsHost) car.PostNotice(nameof(EchoCommand), $"{message} :{StateManager.Shared._playersManager.LocalPlayer}");
ExpandedConsole_Add_Patch.SendMs(null, $"{Hyperlink.To(car)} {message}");
if (!StateManager.IsHost) 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;
}

View File

@@ -103,12 +103,12 @@ public static class Car_Extensions
FindMyCaboose(car, 0f, decrement: false, requireLoad: false);
public static Car? FindMyCabooseWithLoadRequirement(this Car car, float timeNeeded, bool decrement) =>
FindMyCaboose(car, timeNeeded, decrement, requireLoad: false);
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 requireLoad, bool decrement = false)
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

View File

@@ -13,7 +13,7 @@ internal class AutoEngineerControlSetBase_UpdateStatusLabel_Patch
static void Postfix(AutoEngineerControlSetBase __instance)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.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}; <b>Safety</b>";

View File

@@ -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<TweaksAndThingsPlugin>.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();
}
}

View File

@@ -1,94 +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<AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch>();
static bool Prefix(AutoEngineerMode mode, ref int maxSpeedMph)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled() || !tweaksAndThings.SafetyFirst()) return true;
if (SafetyFirstGoverningApplies())
{
int orig = maxSpeedMph;
int limitedSpeed = Math.Min(maxSpeedMph, 20);
maxSpeedMph = mode switch
{
AutoEngineerMode.Road => limitedSpeed,
AutoEngineerMode.Waypoint => limitedSpeed,
_ => maxSpeedMph,
};
if (orig != maxSpeedMph)
{
_log.Debug($"{Enum.GetName(typeof(AutoEngineerMode), mode)}[{TrainController.Shared.SelectedLocomotive.DisplayName}] {nameof(AutoEngineerOrdersExtensions.MaxSpeedMph)} limited to {limitedSpeed} from {orig}; No Caboose in Consist;");
}
}
return true;
}
internal static bool SafetyFirstGoverningApplies()
{
var _persistence = new AutoEngineerPersistence(TrainController.Shared.SelectedLocomotive.KeyValueObject);
var OrdersHelper = new AutoEngineerOrdersHelper(TrainController.Shared.SelectedLocomotive, _persistence);
if (TrainController.Shared.SelectedLocomotive.EnumerateCoupled().All(c => c.IsCaboose() || c.MotivePower())) return false;
bool cabooseReq = SingletonPluginBase<TweaksAndThingsPlugin>.Shared.RequireConsistCabooseForOilerAndHotboxSpotter();
string logMessage = $"\n{nameof(SafetyFirstGoverningApplies)}:{Enum.GetName(typeof(AutoEngineerMode), OrdersHelper.Mode)}[{TrainController.Shared.SelectedLocomotive.DisplayName}] ";
Func<bool> firstClass = () =>
{
var output = TrainController.Shared.SelectedEngineExpress();
logMessage += $"\nfirst class {output}";
return output;
};
Func<bool> 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<bool> noCaboose = () =>
{
bool output = (bool)TrainController.Shared.SelectedLocomotive.FindMyCabooseSansLoadRequirement();
logMessage += $"\ncaboose? {!output}";
return output;
};
logMessage += $"\nCaboose Required {cabooseReq}";
bool output =
cabooseReq &&
!firstClass() &&
FreightConsist() &&
noCaboose();
logMessage += $"\nGovern AE? {output}";
_log.Debug(logMessage);
return output;
}
}

View File

@@ -1,31 +0,0 @@
using Game.Notices;
using Game.State;
using HarmonyLib;
using Model;
using Network;
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<AutoEngineerOrdersHelper_SetWaypoint_patch>();
static void Postfix(AutoEngineerOrdersHelper __instance, Track.Location location, string coupleToCarId)
{
if (StateManager.IsHost)
{
_log.Debug($"start setWP");
Car selectedLoco = __instance._locomotive;
_log.Debug($"{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"));
}
}
}

View File

@@ -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<AutoEngineerPlanner_HandleCommand_Patch>();
private static int governedSpeed = 20;
static bool Prefix(AutoEngineerPlanner __instance, ref AutoEngineerCommand command, ref IPlayer sender)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.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<TweaksAndThingsPlugin>.Shared.RequireConsistCabooseForOilerAndHotboxSpotter();
string logMessage = $"\n{nameof(SafetyFirstGoverningApplies)}:{Enum.GetName(typeof(AutoEngineerMode), OrdersHelper.Mode)}[{loco.DisplayName}] ";
Func<bool> firstClass = () =>
{
var output = TrainController.Shared.SelectedEngineExpress();
logMessage += $"\nfirst class {output}";
return output;
};
Func<bool> 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<bool> 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;
}
}

View File

@@ -1,14 +1,16 @@
using Game.State;
using Game.Messages;
using Game.State;
using HarmonyLib;
using KeyValue.Runtime;
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;
@@ -16,97 +18,123 @@ 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(AutoEngineerWaypointControls))]
[HarmonyPatch(nameof(AutoEngineerWaypointControls.ConfigureOptionsDropdown))]
[HarmonyPatch(typeof(LocomotiveControlsUIAdapter))]
[HarmonyPatch(nameof(LocomotiveControlsUIAdapter.UpdateCarText))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch
internal class LocomotiveControlsUIAdapter_UpdateCarText_Postfix()
{
private static Serilog.ILogger _log => Log.ForContext<AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch>();
private static Serilog.ILogger _log => Log.ForContext<LocomotiveControlsUIAdapter_UpdateCarText_Postfix>();
private static int lastSeenIntegrationSetCount = default;
private static string? lastLocoSeenCarId = default;
private static Coroutine? watchyWatchy = null;
private static HashSet<OpsCarPosition?> locoConsistDestinations = [];
private static Game.GameDateTime? timetableSaveTime = null;
static string getDictKey(Car car) => car.DisplayName;
private static readonly HashSet<IDisposable> _keyChangeObservers = new HashSet<IDisposable>();
private static readonly HashSet<string> destinations = new HashSet<string>();
private static readonly HashSet<Car> consist = new HashSet<Car>();
private static string _lastLocoCarId = string.Empty;
private static bool recalcing = false;
static void Postfix(AutoEngineerWaypointControls __instance, ref OptionsDropdownConfiguration __result)
static void Postfix(LocomotiveControlsUIAdapter __instance)
{
try
{
_log.Information($"HI BOB");
foreach(var o in _keyChangeObservers) o.Dispose();
_keyChangeObservers.Clear();
destinations.Clear();
recalcing = false;
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<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return;
//_log.Information($"HI2");
WaitForSecondsRealtime wait = new WaitForSecondsRealtime(3f);
List<DropdownMenu.RowData> rowDatas = __result.Rows.ToList();
var func = __result.OnRowSelected;
int origCount = rowDatas.Count;
int maxRowOrig = origCount - 1;
var selectedLoco = TrainController.Shared.SelectedLocomotive;
if (selectedLoco.id != _lastLocoCarId || !(consist?.Any() ?? false))
while (true)
{
_lastLocoCarId = selectedLoco.id;
consist.Clear();
consist.UnionWith(selectedLoco.EnumerateCoupled()?.ToList() ?? Enumerable.Empty<Car>());
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<OpsCarPosition?> 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<DropdownMenu.RowData> rowDatas,
out Action<int> 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<DropdownMenu.RowData> list = config.Rows;
Action<int> action = config.OnRowSelected;
__instance.optionsDropdown.Configure(list, action);
((Selectable)__instance.optionsDropdown).interactable = list.Count > 0;
yield return wait;
}
var aeoh = new AutoEngineerOrdersHelper(persistence: new AutoEngineerPersistence(selectedLoco.KeyValueObject), locomotive: selectedLoco);
List<(string destinationId, string destination, float? distance, Location? location)> jumpTos = new();
}
foreach(var c in consist)
{
AddObserversToCar(__instance, c);
OpsCarPosition? destination = c.Waybill.HasValue && !c.Waybill.Value.Completed ? c.Waybill.Value.Destination : null;
bool completed = c.Waybill?.Completed ?? false;
if (!destination.HasValue && c.TryGetOverrideDestination(OverrideDestination.Repair, OpsController.Shared, out (OpsCarPosition, string)? result)) destination = result.Value.Item1;
_log.Information($"{c.DisplayName} -> {destination.HasValue}");
private static bool ShouldRecalc(LocomotiveControlsUIAdapter __instance, BaseLocomotive selectedLoco, out HashSet<OpsCarPosition?> destinations)
{
bool output = false;
string locoKey = getDictKey(selectedLoco);
List<Car> consist = new List<Car>();
consist = selectedLoco.EnumerateCoupled().ToList();
destinations = consist.Where(c => GetCarDestinationIdentifier(c).HasValue).Select(GetCarDestinationIdentifier).ToHashSet();
string destId = destination?.Identifier ?? string.Empty;
//_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}"))}");
if (destinations.Contains(destId)) continue;
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}");
if (destination.HasValue && !completed)
{
string destName = destination.Value.DisplayName;
float? distance = null;
return output;
}
if (Graph.Shared.TryGetLocationFromPoint(destination.Value.Spans?.FirstOrDefault().GetSegments().FirstOrDefault(), destination.Value.Spans?.FirstOrDefault()?.GetCenterPoint() ?? default, 200f, out Location destLoc))
{
float trainMomentum = 0f;
Location start = StateManager.IsHost ? selectedLoco.AutoEngineerPlanner.RouteStartLocation(out trainMomentum) : RouteStartLocation(__instance, selectedLoco);
HeuristicCosts autoEngineer = HeuristicCosts.AutoEngineer;
List<RouteSearch.Step> list = new List<RouteSearch.Step>();
var totLen = StateManager.IsHost ? selectedLoco.AutoEngineerPlanner.CalculateTotalLength() : CalculateTotalLength(selectedLoco);
distance = Graph.Shared.FindRoute(start, destLoc, autoEngineer, list, out var metrics, checkForCars: false, totLen, trainMomentum)
? metrics.Distance
: null;
};
_log.Information($"{c.DisplayName} -> {destName} {destId} {distance?.ToString()}");
if (distance.HasValue)
{
destinations.Add(destId);
jumpTos.Add((
destinationId: destId,
destination: $"WP> {destName}"
, distance: distance
, location: (Location?)destLoc
));
}
}
}
jumpTos = jumpTos?.OrderBy(c => c.distance)?.ToList() ?? [];
var safetyFirst = AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.SafetyFirstGoverningApplies() && jumpTos.Any();
private static OptionsDropdownConfiguration WireUpJumpTosToSettingMenu(AutoEngineerWaypointControls __instance, BaseLocomotive selectedLoco, List<DropdownMenu.RowData> rowDatas, Action<int> 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(
@@ -119,12 +147,12 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch
rowDatas
, delegate (int row)
{
_log.Information($"{TrainController.Shared.SelectedLocomotive.DisplayName} row {row}/{jumpTos.Count}/{rowDatas.Count}");
_log.Debug($"{__instance.Locomotive.DisplayName} row {row}/{localJumpTos.Count}/{rowDatas.Count}");
if (row <= maxRowOrig)
{
func(row);
}
if (row > maxRowOrig && jumpTos[row - origCount].location.HasValue)
if (row > maxRowOrig && localJumpTos[row - origCount].location.HasValue)
{
if (safetyFirst)
{
@@ -132,7 +160,7 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch
return;
}
float trainMomentum = 0f;
Location end = jumpTos[row - origCount].location.Value;
Location end = localJumpTos[row - origCount].location.Value;
Location start = RouteStartLocation(__instance, selectedLoco);
HeuristicCosts autoEngineer = HeuristicCosts.AutoEngineer;
List<RouteSearch.Step> list = new List<RouteSearch.Step>();
@@ -142,8 +170,9 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch
{
RouteSearch.Metrics metrics2;
bool flag = Graph.Shared.FindRoute(start, end, autoEngineer, null, out metrics2);
Multiplayer.SendError(StateManager.Shared.PlayersManager.LocalPlayer, flag ? (selectedLoco.DisplayName + " Train too long to navigate to waypoint.") : (selectedLoco.DisplayName + " Unable to find a path to waypoint."), AlertLevel.Error);
} else
Multiplayer.SendError(StateManager.Shared.PlayersManager.LocalPlayer, flag ? (getDictKey(selectedLoco) + " Train too long to navigate to waypoint.") : (getDictKey(selectedLoco) + " Unable to find a path to waypoint."), AlertLevel.Error);
}
else
{
var mw = (location: end, carId: string.Empty);
aeoh.SetWaypoint(mw.location, mw.carId);
@@ -152,10 +181,157 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch
}
}
);
return __result;
}
private static Location RouteStartLocation(AutoEngineerWaypointControls __instance, BaseLocomotive _locomotive) {
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<RouteSearch.Step> list = new List<RouteSearch.Step>();
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<RouteSearch.Step> list = new List<RouteSearch.Step>();
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<OpsCarPosition?> destinations,
out List<DropdownMenu.RowData> rowDatas,
out Action<int> 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));
@@ -178,47 +354,16 @@ internal class AutoEngineerWaypointControls_ConfigureOptionsDropdown_Patch
}
return num + 1.04f * (float)(coupledCarsCached.Count - 1);
}
}
}
private static void AddObserversToCar(AutoEngineerWaypointControls __instance, Car c)
[HarmonyPatch(typeof(LocomotiveControlsUIAdapter))]
[HarmonyPatch(nameof(LocomotiveControlsUIAdapter.UpdateOptionsDropdown))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class LocomotiveControlsUIAdapter_UpdateOptionsDropdown_Prefix
{
static bool Prefix(LocomotiveControlsUIAdapter __instance)
{
AddObserver(__instance, c, Car.KeyOpsWaybill);
AddObserver(__instance, c, Car.KeyOpsRepairDestination);
foreach (Car.LogicalEnd logicalEnd in CarInspector_PopulateCarPanel_Patch.ends)
{
AddObserver(__instance, c, Car.KeyValueKeyFor(Car.EndGearStateKey.IsCoupled, c.LogicalToEnd(logicalEnd)), true);
}
}
private static void AddObserver(AutoEngineerWaypointControls __instance, Model.Car car, string key, bool clearCarCache = false)
{
_keyChangeObservers.Add(
car.KeyValueObject.Observe(
key,
delegate (Value value)
{
_log.Information($"{car.DisplayName} OBSV {key}: {value}; recalcing {recalcing}");
if (recalcing) return;
try
{
foreach(var o in _keyChangeObservers)
{
o.Dispose();
}
var loco = TrainController.Shared.SelectedLocomotive;
if (!TrainController.Shared.SelectRecall()) TrainController.Shared.SelectedCar = null;
_keyChangeObservers.Clear();
recalcing = true;
new WaitForSeconds(0.25f);
TrainController.Shared.SelectedCar = loco;
}
catch (Exception ex)
{
_log.ForContext("car", car).Warning(ex, $"{nameof(AddObserver)} {car} Exception logged for {key}");
}
},
false
)
);
return false;
}
}

View File

@@ -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();

View File

@@ -1,4 +1,5 @@
using HarmonyLib;
using Game.State;
using HarmonyLib;
using Model;
using Railloader;
using RMROC451.TweaksAndThings.Enums;
@@ -8,6 +9,8 @@ using System;
using System.Linq;
using UI;
using UI.ContextMenu;
using UnityEngine;
using ContextMenu = UI.ContextMenu.ContextMenu;
namespace RMROC451.TweaksAndThings.Patches;
@@ -34,50 +37,79 @@ internal class CarPickable_HandleShowContextMenu_Patch
{
trainController.SelectedCar = ((trainController.SelectedCar == car) ? null : car);
});
if (GameInput.IsShiftDown)
bool shiftPagination = (tweaksAndThings.ShiftForPagination() && GameInput.IsShiftDown) || !tweaksAndThings.ShiftForPagination();
bool nonShiftShow = (tweaksAndThings.ShiftForPagination() && !GameInput.IsShiftDown) || !tweaksAndThings.ShiftForPagination();
if (shiftPagination && car.EnumerateCoupled().Any(c => c.SupportsBleed()))
{
if (!car.EnumerateCoupled().Any(c => !c.SupportsBleed()))
Sprite? bleedConsist = MapWindow_OnClick_Patch.LoadTexture("BleedConsist.png", "BleedConsist");
if (bleedConsist == null) bleedConsist = SpriteName.Bleed.Sprite();
shared.AddButton(ContextMenuQuadrant.Brakes, $"Bleed Consist", bleedConsist, delegate
{
shared.AddButton(ContextMenuQuadrant.Brakes, $"Bleed Consist", SpriteName.Bleed, delegate
{
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);
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.BleedAirSystem, buttonsHaveCost);
});
}
if (nonShiftShow && car.SupportsBleed())
{
Sprite? bleedCar = MapWindow_OnClick_Patch.LoadTexture("BleedCar.png", "BleedCar");
if (bleedCar == null) bleedCar = SpriteName.Bleed.Sprite();
shared.AddButton(ContextMenuQuadrant.Brakes, "Bleed", bleedCar, car.SetBleed);
}
if (car.EnumerateCoupled().Any(c => c.EndAirSystemIssue()))
{
shared.AddButton(ContextMenuQuadrant.General, $"Air Up Consist", SpriteName.Select, delegate
if (shiftPagination)
{
string text = car.EnumerateCoupled().Any(c => c.HandbrakeApplied()) ? "Release " : "Set ";
Sprite? consistBrakes = MapWindow_OnClick_Patch.LoadTexture($"Consist{text.Trim()}Brake.png", $"{text.Trim()}Consist");
if (consistBrakes == null) consistBrakes = SpriteName.Handbrake.Sprite();
shared.AddButton(ContextMenuQuadrant.Brakes, $"{text}Consist", consistBrakes, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Handbrake, buttonsHaveCost);
});
}
if (nonShiftShow)
{
string textCar = car.HandbrakeApplied() ? "Release " : "Set ";
Sprite? carBrakes = MapWindow_OnClick_Patch.LoadTexture($"{textCar.Trim()}Brake.png", $"{textCar.Trim()}Brake");
if (carBrakes == null) carBrakes = SpriteName.Handbrake.Sprite();
shared.AddButton(ContextMenuQuadrant.Brakes, $"{textCar}Handbrake", carBrakes, delegate
{
bool apply = !car.air.handbrakeApplied;
car.SetHandbrake(apply);
});
}
if (shiftPagination && car.EnumerateCoupled().Any(c => c.EndAirSystemIssue()))
{
Sprite? connectAir = MapWindow_OnClick_Patch.LoadTexture($"ConnectAir.png", "ConnectAir");
if (connectAir == null) connectAir = SpriteName.Select.Sprite();
shared.AddButton(ContextMenuQuadrant.General, $"Air Up Consist", connectAir, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost);
});
}
}
else
if (StateManager.IsHost && shiftPagination && car.EnumerateCoupled().Any(c => c.NeedsOiling || c.HasHotbox))
{
if (car.SupportsBleed())
Sprite? oilCan = MapWindow_OnClick_Patch.LoadTexture("OilCan.png", "OilCan");
if (oilCan == null) oilCan = SpriteName.Select.Sprite();
shared.AddButton(ContextMenuQuadrant.General, $"Oil Consist", oilCan, delegate
{
shared.AddButton(ContextMenuQuadrant.Brakes, "Bleed", SpriteName.Bleed, car.SetBleed);
}
shared.AddButton(ContextMenuQuadrant.Brakes, car.air.handbrakeApplied ? "Release Handbrake" : "Apply Handbrake", SpriteName.Handbrake, delegate
{
bool apply = !car.air.handbrakeApplied;
car.SetHandbrake(apply);
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Oil, buttonsHaveCost);
});
}
shared.AddButton(ContextMenuQuadrant.General, $"Follow", SpriteName.Inspect, delegate
Sprite? follow = MapWindow_OnClick_Patch.LoadTexture($"Follow.png", "ConnectAir");
if (follow == null) follow = SpriteName.Inspect.Sprite();
shared.AddButton(ContextMenuQuadrant.General, $"Follow", follow, delegate
{
CameraSelector.shared.FollowCar(car);
});
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}");
secondaryLine = secondaryLine.Length > 10 + Environment.NewLine.Length ? $"{secondaryLine.Substring(0, 7 + Environment.NewLine.Length)}..." : secondaryLine;
shared.Show($"{car.EnumerateCoupled().Count()} Cars{Environment.NewLine}{car.DisplayName}{secondaryLine}");
shared.BuildItemAngles();
shared.StartCoroutine(shared.AnimateButtonsShown());
return false;

View File

@@ -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<string>();
var hiddenEntries = rows.Where(r => r.Engine.locomotiveControl.air.IsCutOut && !r.IsSelected && !r.IsFavorite).Select(r => r.Engine.id) ?? Enumerable.Empty<string>();
if (hiddenEntries.Any()) __instance._window.Title =string.Format("{0} : {1}", __instance._window.Title, $"Hidden MU Count [{hiddenEntries.Count()}]");

View File

@@ -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.FindMyCabooseSansLoadRequirement();
|| (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 = $"<sup>{str} </sup>";
__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<PlayerId, IPlayer> dictionary = StateManager.Shared.PlayersManager.AllPlayers.ToDictionary((IPlayer p) => p.PlayerId, (IPlayer p) => p);
List<string> 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)}<sub>{selectedCount}</sub>", $"{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}";

View File

@@ -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<ExpandedConsole_Add_Patch>();
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<TweaksAndThingsPlugin>.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<CTCPanelMarkerManager>();
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;

View File

@@ -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++;

View File

@@ -34,6 +34,11 @@ internal class MapWindow_OnClick_Patch
public static Sprite? LoadTexture(string fileName, string name)
{
string path = Path.Combine(SingletonPluginBase<TweaksAndThingsPlugin>.Shared.ModDirectory, fileName);
if (!File.Exists(path))
{
_log.Debug($"Unable to find {name} icon at {path}!");
return null;
}
Texture2D texture2D = new Texture2D(128, 128, TextureFormat.DXT5, mipChain: false);
texture2D.name = name;
texture2D.wrapMode = TextureWrapMode.Clamp;

View File

@@ -98,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);
}

View File

@@ -0,0 +1,42 @@
using HarmonyLib;
using Helpers;
using Model.Database;
using Model.Definition;
using Model.Definition.Data;
using Model.Ops;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(PrefabStoreExtensions))]
[HarmonyPatch(nameof(PrefabStoreExtensions.Random))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal static class PrefabStoreExtensions_Random_Patch
{
public static bool Prefix(IPrefabStore prefabStore, CarTypeFilter carTypeFilter, IndustryContext.CarSizePreference sizePreference, Random rnd, ref TypedContainerItem<CarDefinition> __result)
{
List<TypedContainerItem<CarDefinition>> list =
(from p in prefabStore.AllCarDefinitionInfos.ToList().FindAll((TypedContainerItem<CarDefinition> p) =>
carTypeFilter.Matches(p.Definition.CarType) && !p.Metadata.Tags.Any(t => t.Equals("deprecated")))
orderby p.Definition.WeightEmpty
select p).ToList();
if (list.Count == 0)
{
Log.Error($"Couldn't find car for condition: {carTypeFilter}");
__result = null;
}
__result = list.RandomElementUsingNormalDistribution(sizePreference switch
{
IndustryContext.CarSizePreference.Small => 0.2f,
IndustryContext.CarSizePreference.Medium => 0.4f,
IndustryContext.CarSizePreference.Large => 0.6f,
IndustryContext.CarSizePreference.ExtraLarge => 0.8f,
_ => 0.5f,
}, rnd);
return false;
}
}

View File

@@ -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;
@@ -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<string> 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
{

View File

@@ -55,4 +55,8 @@
<ItemGroup>
<Publicize Include="Assembly-CSharp" />
</ItemGroup>
<ItemGroup>
<None Include="Images\**" CopyToOutputDirectory="Always"/>
</ItemGroup>
</Project>

View File

@@ -28,9 +28,11 @@ public class Settings
bool cabooseRequiredForLocoTagOilIndication,
bool servicingFundPenalty,
bool safetyFirst,
bool safetyFirstClientEnforce,
CrewHourLoadMethod loadCrewHoursMethod,
float cabeeseSearchRadiusFtInMeters,
bool trainBrakeDisplayShowsColorsInCalloutMode
bool trainBrakeDisplayShowsColorsInCalloutMode,
bool shiftPaginationOnContextMenu
)
{
WebhookSettingsList = webhookSettingsList;
@@ -42,9 +44,11 @@ public class Settings
CabooseRequiredForLocoTagOilIndication = cabooseRequiredForLocoTagOilIndication;
ServicingFundPenalty = servicingFundPenalty;
SafetyFirst = safetyFirst;
SafetyFirstClientEnforce = safetyFirstClientEnforce;
LoadCrewHoursMethod = loadCrewHoursMethod;
CabeeseSearchRadiusFtInMeters = cabeeseSearchRadiusFtInMeters;
TrainBrakeDisplayShowsColorsInCalloutMode = trainBrakeDisplayShowsColorsInCalloutMode;
ShiftPaginationOnContextMenu = shiftPaginationOnContextMenu;
}
public readonly UIState<string> _selectedTabState = new UIState<string>(null);
@@ -57,9 +61,11 @@ public class Settings
public bool CabooseRequiredForLocoTagOilIndication;
public bool ServicingFundPenalty;
public bool SafetyFirst;
public bool SafetyFirstClientEnforce;
public CrewHourLoadMethod LoadCrewHoursMethod;
public float CabeeseSearchRadiusFtInMeters;
public bool TrainBrakeDisplayShowsColorsInCalloutMode;
public bool ShiftPaginationOnContextMenu;
internal void AddAnotherRow()
{
@@ -136,9 +142,13 @@ public static class SettingsExtensions
input?.settings?.ServicingFundPenalty ?? false;
public static bool SafetyFirst(this TweaksAndThingsPlugin input) =>
input?.settings?.SafetyFirst ?? false;
public static bool SafetyFirstClientEnforce(this TweaksAndThingsPlugin input) =>
input?.settings?.SafetyFirstClientEnforce ?? true;
public static bool DayLoadCrewHours(this TweaksAndThingsPlugin input) =>
(input?.settings?.LoadCrewHoursMethod ?? CrewHourLoadMethod.Tracks) == CrewHourLoadMethod.Daily;
public static bool TrainBrakeDisplayShowsColorsInCalloutMode(this TweaksAndThingsPlugin input) =>
input?.settings?.TrainBrakeDisplayShowsColorsInCalloutMode ?? false;
public static bool ShiftForPagination(this TweaksAndThingsPlugin input) =>
input?.settings?.ShiftPaginationOnContextMenu ?? false;
}

View File

@@ -40,7 +40,6 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
static TweaksAndThingsPlugin()
{
Log.Debug("Hello! Static Constructor was called!");
}
public TweaksAndThingsPlugin(IModdingContext moddingContext, IModDefinition self)
@@ -58,7 +57,6 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
public override void OnEnable()
{
logger.Debug("OnEnable() was called!");
var harmony = new Harmony(modDefinition.Id);
harmony.PatchCategory(modDefinition.Id.Replace(".", string.Empty));
}
@@ -72,13 +70,10 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
public void Update()
{
logger.Verbose("UPDATE()");
}
public void ModTabDidOpen(UIPanelBuilder builder)
{
logger.Debug("Daytime!");
if (settings == null) settings = new();
if (!settings?.WebhookSettingsList?.Any() ?? true) settings.WebhookSettingsList = new[] { new WebhookSettings() }.ToList();
if (settings?.EngineRosterFuelColumnSettings == null) settings.EngineRosterFuelColumnSettings = new();
@@ -208,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)
@@ -250,6 +262,18 @@ AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 -
}
).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);
builder.AddFieldToggle(
"Context Menu Shift Modifier",
() => this.ShiftForPagination(),
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.ShiftPaginationOnContextMenu = enabled;
builder.Rebuild();
}
).Tooltip("Context Menu Shift Modifier", $@"When enabled/checked, utilizing `SHIFT` while initiating the context menu of a car will show consist level options, and without shows only car level. If this is unchecked, all car/consist options show up in a happy goulash!");
builder.Spacer(spacing);
EngineRosterShowsFuelStatusUISection(builder);
}
@@ -347,7 +371,6 @@ AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 -
public void ModTabDidClose()
{
logger.Debug("Nighttime...");
this.moddingContext.SaveSettingsData(this.modDefinition.Id, settings ?? new());
}
}