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

View File

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

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

View File

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

View File

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