using "area" cabeese search rather than iterating nearby cars in a catchment area. Adding MU auto filter for engine roster and including other loco fuel levels in single engine when in same consist/mu'd.

This commit is contained in:
2025-08-16 11:23:40 -05:00
parent 7c8becd471
commit d19a1a2995
6 changed files with 113 additions and 68 deletions

View File

@@ -1,4 +1,6 @@
using Model.AI; using Game.Messages;
using Model.AI;
using Network;
using System; using System;
using System.Collections; using System.Collections;
using UnityEngine; using UnityEngine;
@@ -61,7 +63,8 @@ namespace RMROC451.TweaksAndThings.Extensions
if (car.HasHotbox && car.Oiled == 1f && cabooseRequired && foundCaboose) if (car.HasHotbox && car.Oiled == 1f && cabooseRequired && foundCaboose)
{ {
_log.Information("AutoOiler {name}: {foundCaboose} repaired hotbox {car}", oiler.name, foundCaboose, car); _log.Information("AutoOiler {name}: {foundCaboose} repaired hotbox {car}", oiler.name, foundCaboose, car);
car.AdjustHotboxValue(); Multiplayer.Broadcast($"{Hyperlink.To(oiler._originCar)}: \"{Hyperlink.To(car)} hotbox repaired!\"");
car.SendPropertyChange(PropertyChange.Control.Hotbox, false);
} }
num += adjustedTimeToWalk; num += adjustedTimeToWalk;
oiler._pendingRunDuration += adjustedTimeToWalk; oiler._pendingRunDuration += adjustedTimeToWalk;

View File

@@ -1,4 +1,5 @@
using Game.Messages; using Game.Messages;
using Game.State;
using Helpers; using Helpers;
using Model; using Model;
using Model.Definition; using Model.Definition;
@@ -64,6 +65,8 @@ public static class Car_Extensions
public static bool IsCaboose(this Car car) => car.Archetype == Model.Definition.CarArchetype.Caboose; public static bool IsCaboose(this Car car) => car.Archetype == Model.Definition.CarArchetype.Caboose;
public static Car? CarCaboose(this Car car) => car.IsCaboose() ? car : null;
public static bool IsCabooseAndStoppedForLoadRefresh(this Car car, bool isFull) => car.IsCaboose() && car.IsStopped(30f) && !isFull; public static bool IsCabooseAndStoppedForLoadRefresh(this Car car, bool isFull) => car.IsCaboose() && car.IsStopped(30f) && !isFull;
public static Car? CabooseInConsist(this IEnumerable<Car> input) => input.FirstOrDefault(IsCaboose); public static Car? CabooseInConsist(this IEnumerable<Car> input) => input.FirstOrDefault(IsCaboose);
@@ -96,12 +99,14 @@ public static class Car_Extensions
t.TrainClass == Timetable.TrainClass.First; t.TrainClass == Timetable.TrainClass.First;
public static Car? FindMyCaboose(this Car car, float timeNeeded, bool decrement = false) => public static Car? FindMyCaboose(this Car car, float timeNeeded, bool decrement = false) =>
car.CarsNearCurrentCar(timeNeeded, decrement).FindNearestCabooseFromNearbyCars(); (
car.CarCaboose() ?? car.CarsNearCurrentCar(timeNeeded, decrement).FindNearestCabooseFromNearbyCars()
)?.CabooseWithSufficientCrewHours(timeNeeded, decrement);
public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, bool decrement = false) public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, bool decrement = false)
{ {
Car? output = null; Car? output = null;
if (!car.IsCaboose()) return null; if (car is null || !car.IsCaboose()) return null;
List<LoadSlot> loadSlots = car.Definition.LoadSlots; List<LoadSlot> loadSlots = car.Definition.LoadSlots;
for (int i = 0; i < loadSlots.Count; i++) for (int i = 0; i < loadSlots.Count; i++)
@@ -118,29 +123,24 @@ public static class Car_Extensions
return output; return output;
} }
private static Car FindNearestCabooseFromNearbyCars(this List<(Car car, bool crewCar, float distance)> source) => private static Car? FindNearestCabooseFromNearbyCars(this IEnumerable<(Car car, bool crewCar, float distance)> source) =>
source source
.OrderBy(c => c.crewCar ? 0 : 1) ?.OrderBy(c => c.crewCar ? 0 : 1)
.ThenBy(c => c.distance) ?.ThenBy(c => c.distance)
.Select(c => c.car) ?.Select(c => c.car)
.FirstOrDefault(); ?.FirstOrDefault();
private static List<(Car car, bool crewCar, float distance)> CarsNearCurrentCar(this Car car, float timeNeeded, bool decrement) private static IEnumerable<(Car car, bool crewCar, float distance)> CarsNearCurrentCar(this Car car, float timeNeeded, bool decrement)
{ {
Area carArea = OpsController.Shared.ClosestArea(car);
var cabeese = var cabeese = OpsController.Shared
car.EnumerateCoupled().SelectMany(consistCar => .CarsInArea(carArea)
{ .Select(c => TrainController.Shared.CarForId(c.Id))
Vector3 position = consistCar.GetMotionSnapshot().Position; .Union(car.EnumerateCoupled())
Vector3 center = WorldTransformer.WorldToGame(position); .Where(c => c.IsCaboose());
var o = TrainController.Shared
.CarIdsInRadius(center, SingletonPluginBase<TweaksAndThingsPlugin>.Shared.CabeeseSearchRadiusInMeters())
.Where(c => TrainController.Shared.CarForId(c).IsCaboose());
return o;
}).Distinct().Select(c => TrainController.Shared.CarForId(c));
//if (cabeese?.Any() ?? false) Log.Information($"{nameof(CarsNearCurrentCar)}[{car.DisplayName}] => {cabeese.Count()}");
Log.Information($"{nameof(CarsNearCurrentCar)} => {cabeese.Count()}");
List<(Car car, bool crewCar, float distance)> source = List<(Car car, bool crewCar, float distance)> source =
cabeese.Select(c => (car: c, crewCar: c.IsCrewCar(), distance: car.Distance(c))).ToList(); cabeese.Select(c => (car: c, crewCar: c.IsCrewCar(), distance: car.Distance(c))).ToList();
@@ -158,9 +158,16 @@ public static class Car_Extensions
} }
public static bool IsCrewCar(this Car car) => public static bool IsCrewCar(this Car car) =>
!string.IsNullOrEmpty(TrainController.Shared.SelectedLocomotive.trainCrewId) && !string.IsNullOrEmpty(TrainController.Shared.SelectedLocomotive?.trainCrewId) &&
car.trainCrewId == TrainController.Shared.SelectedLocomotive.trainCrewId; car.trainCrewId == TrainController.Shared.SelectedLocomotive?.trainCrewId;
public static void AdjustHotboxValue(this Car car) => car.ControlProperties[PropertyChange.Control.Hotbox] = null; //public static void AdjustHotboxValue(this Car car) => car.ControlProperties[PropertyChange.Control.Hotbox] = null;
public static void AdjustHotboxValue(this Car car, float hotboxValue = 0f) =>
StateManager.ApplyLocal(
new PropertyChange(
car.id, PropertyChange.KeyForControl(PropertyChange.Control.Hotbox),
new FloatPropertyValue(hotboxValue)
)
);
} }

View File

@@ -0,0 +1,31 @@
using HarmonyLib;
using Railloader;
using System.Collections.Generic;
using System.Linq;
using UI;
using UI.EngineRoster;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(EngineRosterPanel))]
[HarmonyPatch(nameof(EngineRosterPanel.Populate))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class EngineRosterPanel_Populate_Patch
{
private static bool Prefix(EngineRosterPanel __instance, ref List<RosterRowData> rows)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
__instance._window.Title = __instance._window.Title.Split(':')[0].Trim();
if (!tweaksAndThings.IsEnabled()) return true;
var hiddenEntries = rows.Where(r => r.Engine.IsMuEnabled && !r.IsSelected && !r.IsFavorite).Select(r => r.Engine.id) ?? Enumerable.Empty<string>();
if (hiddenEntries.Any()) __instance._window.Title =string.Format("{0} : {1}", __instance._window.Title, $"Hidden MU Count [{hiddenEntries.Count()}]");
rows = rows.Where(r => !hiddenEntries.Contains(r.Engine.id)).ToList();
return true;
}
}

View File

@@ -24,6 +24,8 @@ namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatchCategory("RMROC451TweaksAndThings")] [HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class EngineRosterRow_Refresh_Patch internal class EngineRosterRow_Refresh_Patch
{ {
private static Serilog.ILogger _log => Log.ForContext<EngineRosterRow_Refresh_Patch>();
public static void Postfix(EngineRosterRow __instance) public static void Postfix(EngineRosterRow __instance)
{ {
TweaksAndThingsPlugin? tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared; TweaksAndThingsPlugin? tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
@@ -35,46 +37,60 @@ internal class EngineRosterRow_Refresh_Patch
if (tweaksAndThings == null || if (tweaksAndThings == null ||
rosterFuelColumnSettings == null || rosterFuelColumnSettings == null ||
!tweaksAndThings.IsEnabled() || !tweaksAndThings.IsEnabled() ||
rosterFuelColumnSettings.EngineRosterFuelStatusColumn == EngineRosterFuelDisplayColumn.None || (!GameInput.IsAltDown && !rosterFuelColumnSettings.EngineRosterShowsFuelStatusAlways)) rosterFuelColumnSettings.EngineRosterFuelStatusColumn == EngineRosterFuelDisplayColumn.None || (!GameInput.IsAltDown && !rosterFuelColumnSettings.EngineRosterShowsFuelStatusAlways) ||
__instance._engine.IsMuEnabled
)
{ {
return; return;
} }
try try
{ {
IEnumerable<Car> consist = __instance._engine.EnumerateCoupled().Where(c => c.EnableOiling); Car engineOrTender = __instance._engine;
IEnumerable<Car> locos = engineOrTender.EnumerateCoupled().Where(c => c.IsLocomotive).ToList();
IEnumerable<Car> consist = engineOrTender.EnumerateCoupled().Where(c => c.EnableOiling).ToList();
bool cabooseRequirementFulfilled = bool cabooseRequirementFulfilled =
!tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() !tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter()
|| consist.ConsistNoFreight() || consist.ConsistNoFreight()
|| (bool)__instance._engine.FindMyCaboose(0.0f, false); || (bool)engineOrTender.FindMyCaboose(0.0f, false);
float offendingPercentage = 0; float offendingPercentage = 100f;
Car engineOrTender = __instance._engine;
List<LoadSlot> loadSlots = __instance._engine.Definition.LoadSlots;
if (!loadSlots.Any())
{
engineOrTender = __instance._engine.DetermineFuelCar()!;
loadSlots = engineOrTender != null ? engineOrTender.Definition.LoadSlots : Enumerable.Empty<LoadSlot>().ToList();
}
var offender = loadSlots.OrderBy(ls => (engineOrTender.GetLoadInfo(ls.RequiredLoadIdentifier, out int slotIndex)?.Quantity ?? 0) / loadSlots[slotIndex].MaximumCapacity).FirstOrDefault().RequiredLoadIdentifier; foreach (Car loco in locos)
for (int i = 0; i < loadSlots.Count; i++)
{ {
CarLoadInfo? loadInfo = engineOrTender.GetLoadInfo(i); var investigate = loco;
if (loadInfo.HasValue) List<LoadSlot> loadSlots = investigate.Definition.LoadSlots;
if (!loadSlots.Any())
{ {
CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault(); investigate = investigate.DetermineFuelCar()!;
var fuelLevel = FuelLevel(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity); loadSlots = investigate != null ? investigate.Definition.LoadSlots : Enumerable.Empty<LoadSlot>().ToList();
offendingPercentage = CalcPercentLoad(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity); }
fuelInfoText += loadSlots[i].RequiredLoadIdentifier == offender ? fuelLevel + " " : string.Empty;
//fuelInfoText += TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity) + " "; var offender = loadSlots.OrderBy(ls => (investigate.GetLoadInfo(ls.RequiredLoadIdentifier, out int slotIndex)?.Quantity ?? 0) / loadSlots[slotIndex].MaximumCapacity).FirstOrDefault().RequiredLoadIdentifier;
fuelInfoTooltip += $"{TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity)} {valueOrDefault.LoadString(CarPrototypeLibrary.instance.LoadForId(valueOrDefault.LoadId))}\n";
for (int i = 0; i < loadSlots.Count; i++)
{
CarLoadInfo? loadInfo = investigate.GetLoadInfo(i);
if (loadInfo.HasValue)
{
CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault();
var fuelLevel = FuelLevel(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity);
var offenderCheck = CalcPercentLoad(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity);
if (offenderCheck < offendingPercentage)
{
offendingPercentage = offenderCheck;
fuelInfoText = loadSlots[i].RequiredLoadIdentifier == offender ? $"{fuelLevel} " : string.Empty;
}
fuelInfoTooltip += $"{TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity)} {valueOrDefault.LoadString(CarPrototypeLibrary.instance.LoadForId(valueOrDefault.LoadId))} {(!loco.id.Equals(__instance._engine.id) ? $"{loco.DisplayName}" : "")}\n";
}
} }
} }
try try
{ {
if (cabooseRequirementFulfilled && StateManager.Shared.Storage.OilFeature) if (cabooseRequirementFulfilled && StateManager.Shared.Storage.OilFeature && consist.Any())
{ {
float lowestOilLevel = consist.OrderBy(c => c.Oiled).FirstOrDefault().Oiled; float lowestOilLevel = consist.OrderBy(c => c.Oiled).FirstOrDefault().Oiled;
var oilLevel = FuelLevel(lowestOilLevel, 1); var oilLevel = FuelLevel(lowestOilLevel, 1);

View File

@@ -1,8 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup><!-- Optionally, set a few things to your liking -->
<!-- Optionally, set a few things to your liking -->
<!-- <MajorVersion>1</MajorVersion> --> <!-- <MajorVersion>1</MajorVersion> -->
<!-- <MinorVersion>0</MinorVersion> --> <!-- <MinorVersion>0</MinorVersion> -->
<SignAssembly>False</SignAssembly>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<Optimize>False</Optimize>
<DebugType>portable</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="Patches\BindingsWindow_Patches.cs" /> <Compile Remove="Patches\BindingsWindow_Patches.cs" />

View File

@@ -1,6 +1,7 @@
// Ignore Spelling: RMROC // Ignore Spelling: RMROC
using GalaSoft.MvvmLight.Messaging; using GalaSoft.MvvmLight.Messaging;
using Game;
using Game.Messages; using Game.Messages;
using Game.State; using Game.State;
using HarmonyLib; using HarmonyLib;
@@ -205,24 +206,6 @@ AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 -
).Tooltip("Safety First", $@"On non-express timetabled consists, a caboose is required in the consist increase AE max speed > 20 in {Enum.GetName(typeof(AutoEngineerMode), AutoEngineerMode.Road)}/{Enum.GetName(typeof(AutoEngineerMode), AutoEngineerMode.Waypoint)} mode."); ).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 #endregion
#region CabeeseSearchRadius
builder.Spacer(spacing);
builder.AddField(
"Cabeese Search Radius",
builder.AddSlider(
() => this.CabeeseSearchRadiusInMeters(),
() => $"{string.Format(Mathf.CeilToInt(this.CabeeseSearchRadiusInMeters() * 3.28084f).ToString(), "N0")}ft",
delegate (float input) {
settings = settings ?? new();
settings.CabeeseSearchRadiusFtInMeters = Mathf.CeilToInt(input);
builder.Rebuild();
},
minValue: 1f,
maxValue: Mathf.CeilToInt(5280f / 2f / 3.28084f),
wholeNumbers: true
)
).Tooltip("Cabeese Catchment Area", "How far should the cabeese hunting logic look away from the cars in the area to find a caboose?");
#endregion
} }
private void UiUpdates(UIPanelBuilder builder) private void UiUpdates(UIPanelBuilder builder)