Compare commits

...

11 Commits

Author SHA1 Message Date
d197ff8d8a Merge pull request #23 from rmroc451/misc-fixes-and-caboose-additions
Misc fixes and caboose additions
2024-07-24 00:09:54 -05:00
89894890b4 build version increment 2024-07-24 00:07:59 -05:00
a13701c3d2 AutoHotboxSpotter Patch for caboose consist presence, making checks happen between every 15-30 seconds vs 60-300 (default) 2024-07-24 00:07:48 -05:00
b5be17703f Adding AutoOiler Patch that halves times to oil consists and the start delay, if a caboose is presentin the consist that the ai engineer is in. 2024-07-24 00:06:37 -05:00
35afa4520d log context adjustments for CarInspector patch class 2024-07-24 00:04:49 -05:00
80d064c950 remove SetActive call, which seems to be a performance gain when tags are displayed. 2024-07-24 00:03:11 -05:00
ec21effd30 Adding consist buttons from inspector to new context menu, and squashing bug that would occur if inspector open but tags weren't active, breaking the ui panel builder reload detection for handbrake/couple/air changes.
final change of Version 0.1.7
2024-07-13 13:09:42 -05:00
2242aaacde Merge pull request #22 from rmroc451/1.6.1
Fix issue with cars that are missing tagcallouts from throwing errors.
2024-07-13 11:12:24 -05:00
e26713688b Fix issue with cars that are missing tagcallouts from throwing errors. 2024-07-13 11:11:22 -05:00
8a228be5a8 Merge pull request #21 from rmroc451/20-webhook-settings-adds-a-new-blank-entry-every-time-the-settings-window-opens
misc refactor and fixes
2024-07-05 20:22:52 -05:00
2ed33465d9 reduce cyclomatic complexity of the cabeese hunting algorithm, and fix issue with webhook settings adding an empty row each time settings screen opens. 2024-07-05 20:21:41 -05:00
10 changed files with 272 additions and 52 deletions

View File

@@ -12,7 +12,7 @@
<PropertyGroup Condition="'$(AssemblyVersion)' == '' OR '$(MajorVersion)' != '' OR '$(MinorVersion)' != ''">
<MajorVersion Condition="'$(MajorVersion)' == ''">0</MajorVersion>
<MinorVersion Condition="'$(MinorVersion)' == ''">1</MinorVersion>
<PatchVersion Condition="'$(PatchVersion)' == ''">6</PatchVersion>
<PatchVersion Condition="'$(PatchVersion)' == ''">8</PatchVersion>
<AssemblyVersion>$(MajorVersion).$(MinorVersion).$(PatchVersion)</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<ProductVersion>$(AssemblyVersion)</ProductVersion>

View File

@@ -0,0 +1,93 @@
using Model.AI;
using System.Collections;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Extensions
{
internal static class AutoEngineer_Extensions
{
private static float CabooseHalvedFloat(this float input, bool hasCaboose) =>
hasCaboose ? input / 2 : input;
private static float CabooseAutoOilerLimit(this bool hasCaboose) =>
hasCaboose ? 0.99f : AutoOiler.OilIfBelow;
public static IEnumerator MrocAutoOilerLoop(this AutoOiler oiler, Serilog.ILogger _log)
{
int originIndex = oiler.FindOriginIndex();
bool hasCaboose = oiler._cars.CabooseInConsist();
if (originIndex < 0)
{
_log.Error("Couldn't find origin car {car}", oiler._originCar);
oiler._coroutine = null;
yield break;
}
oiler._reverse = originIndex > oiler._cars.Count - originIndex;
_log.Information(
"AutoOiler {name} starting, rev = {reverse}, caboose halving adjustment = {hasCaboose}, oil limit = {limit}",
oiler.name,
oiler._reverse,
hasCaboose,
hasCaboose.CabooseAutoOilerLimit()
);
while (true)
{
yield return new WaitForSeconds(AutoOiler.StartDelay.CabooseHalvedFloat(hasCaboose));
int carIndex = originIndex;
float adjustedTimeToWalk = AutoOiler.TimeToWalkCar.CabooseHalvedFloat(hasCaboose);
do
{
if (oiler.TryGetCar(carIndex, out var car))
{
float num = 0f;
float origOil = car.Oiled;
if (car.NeedsOiling && car.Oiled < hasCaboose.CabooseAutoOilerLimit())
{
float num2 = 1f - car.Oiled;
car.OffsetOiled(num2);
float num3 = num2 * AutoOiler.TimeToFullyOil.CabooseHalvedFloat(hasCaboose);
num += num3;
oiler._pendingRunDuration += num3;
oiler._oiledCount++;
_log.Information("AutoOiler {name}: oiled {car} from {orig} => {new}", oiler.name, car, origOil, car.Oiled);
}
num += adjustedTimeToWalk;
oiler._pendingRunDuration += adjustedTimeToWalk;
yield return new WaitForSeconds(num);
}
carIndex = oiler.NextIndex(carIndex);
}
while (oiler.InBounds(carIndex));
oiler._reverse = !oiler._reverse;
oiler.PayWages();
}
}
public static IEnumerator MrocAutoHotboxSpotterLoop(this AutoHotboxSpotter spotter, Serilog.ILogger _log)
{
while (true)
{
bool hasCaboose = spotter._cars.CabooseInConsist();
if (!spotter.HasCars)
{
yield return new WaitForSeconds(1f);
continue;
}
_log.Information("AutoHotboxSpotter {name}: Hotbox Spotter Running, Has Caboose => {hasCaboose}; Has Cars {hasCars}", spotter.name, hasCaboose, spotter.HasCars);
spotter.CheckForHotbox();
while (spotter.HasCars)
{
int num = Random.Range(60, 300);
if (hasCaboose)
{
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}", spotter.name, numOrig, num);
}
yield return new WaitForSeconds(num);
spotter.CheckForHotbox();
}
}
}
}
}

View File

@@ -58,10 +58,10 @@ public static class Car_Extensions
public static bool CabooseInConsist(this IEnumerable<Car> input) => input.FirstOrDefault(c => c.IsCaboose());
public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, bool decrement = false)
public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, HashSet<string> carIdsCheckedAlready, bool decrement = false)
{
Car? output = null;
if (!car.IsCaboose()) return null;
if (carIdsCheckedAlready.Contains(car.id) || !car.IsCaboose()) return null;
List<LoadSlot> loadSlots = car.Definition.LoadSlots;
for (int i = 0; i < loadSlots.Count; i++)
@@ -78,33 +78,44 @@ public static class Car_Extensions
return output;
}
public static Car? HuntingForCabeeseNearCar(this Car car, float timeNeeded, TrainController tc, bool decrement = false)
public static Car? HuntingForCabeeseNearCar(this Car car, float timeNeeded, TrainController tc, HashSet<string> carIdsCheckedAlready, bool decrement = false)
{
List<(string carId, float distance)> source =
CarsNearCurrentCar(car, timeNeeded, tc, carIdsCheckedAlready, decrement);
Car output = FindNearestCabooseFromNearbyCars(tc, source);
if (output != null) output.CabooseWithSufficientCrewHours(timeNeeded, carIdsCheckedAlready, decrement);
return output;
}
private static Car FindNearestCabooseFromNearbyCars(TrainController tc, List<(string carId, float distance)> source) =>
(
from t in source
where t.distance < 21f //todo: add setting slider for catchment
orderby t.distance ascending
select tc.CarForId(t.carId)
).FirstOrDefault();
private static List<(string carId, float distance)> CarsNearCurrentCar(Car car, float timeNeeded, TrainController tc, HashSet<string> carIdsCheckedAlready, bool decrement)
{
Vector3 position = car.GetMotionSnapshot().Position;
Vector3 center = WorldTransformer.WorldToGame(position);
Rect rect = new Rect(new Vector2(center.x - 30f, center.z - 30f), Vector2.one * 30f * 2f);
var cars = tc.CarIdsInRect(rect);
Log.Information($"{nameof(HuntingForCabeeseNearCar)} => {cars.Count()}");
bool decrementedAlready = false;
List<(string carId, float distance)> source = cars.Select(carId =>
{
Car car = tc.CarForId(carId);
if (car == null || !car.CabooseWithSufficientCrewHours(timeNeeded, decrement && !decrementedAlready))
{
return (carId: carId, distance: 1000f);
}
decrementedAlready = true;
Vector3 a = WorldTransformer.WorldToGame(car.GetMotionSnapshot().Position);
return (carId: carId, distance: Vector3.Distance(a, center));
}).ToList();
Car? output =
(from t in source
where t.distance < 21f
orderby t.distance ascending
select tc.CarForId(t.carId)
).FirstOrDefault();
return output;
List<(string carId, float distance)> source =
cars
.Select(carId =>
{
Car car = tc.CarForId(carId);
if (car == null || !car.CabooseWithSufficientCrewHours(timeNeeded, carIdsCheckedAlready))
{
return (carId: carId, distance: 1000f);
}
Vector3 a = WorldTransformer.WorldToGame(car.GetMotionSnapshot().Position);
return (carId: carId, distance: Vector3.Distance(a, center));
}).ToList();
return source;
}
}

View File

@@ -0,0 +1,26 @@
using HarmonyLib;
using Model.AI;
using Railloader;
using RMROC451.TweaksAndThings.Extensions;
using Serilog;
using System.Collections;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(AutoHotboxSpotter))]
[HarmonyPatch(nameof(AutoHotboxSpotter.SpotterLoop))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class AutoHotboxSpotter_SpotterLoop_Patch
{
private static ILogger _log => Log.ForContext<AutoHotboxSpotter_SpotterLoop_Patch>();
public static bool Prefix(AutoHotboxSpotter __instance, ref IEnumerator __result)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled) return true;
bool buttonsHaveCost = tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false;
if (buttonsHaveCost) __result = __instance.MrocAutoHotboxSpotterLoop(_log);
return !buttonsHaveCost; //only hit this if !buttonsHaveCost, since Loop is a coroutine
}
}

View File

@@ -0,0 +1,26 @@
using HarmonyLib;
using Model.AI;
using Railloader;
using RMROC451.TweaksAndThings.Extensions;
using Serilog;
using System.Collections;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(AutoOiler))]
[HarmonyPatch(nameof(AutoOiler.Loop))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class AutoOiler_Loop_Patch
{
private static ILogger _log => Log.ForContext<AutoOiler_Loop_Patch>();
public static bool Prefix(AutoOiler __instance, ref IEnumerator __result)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled) return true;
bool buttonsHaveCost = tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false;
if (buttonsHaveCost)__result = __instance.MrocAutoOilerLoop(_log);
return !buttonsHaveCost; //only hit this if !buttonsHaveCost, since Loop is a coroutine
}
}

View File

@@ -8,12 +8,14 @@ using Network;
using Railloader;
using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using UI.Builder;
using UI.CarInspector;
using UI.ContextMenu;
using UI.Tags;
using static Model.Car;
@@ -24,6 +26,7 @@ namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarInspector_PopulateCarPanel_Patch
{
private static ILogger _log => Log.ForContext<CarInspector_PopulateCarPanel_Patch>();
private static IEnumerable<LogicalEnd> ends = Enum.GetValues(typeof(LogicalEnd)).Cast<LogicalEnd>();
/// <summary>
@@ -70,7 +73,7 @@ internal class CarInspector_PopulateCarPanel_Patch
private static UIPanelBuilder AddCarConsistRebuildObservers(UIPanelBuilder builder, IEnumerable<Model.Car> consist)
{
TagController tagController = UnityEngine.Object.FindFirstObjectByType<TagController>();
foreach (Model.Car car in consist)
foreach (Model.Car car in consist.Where(c => c.Archetype != Model.Definition.CarArchetype.Tender))
{
builder = AddObserver(builder, car, PropertyChange.KeyForControl(PropertyChange.Control.Handbrake), tagController);
foreach (LogicalEnd logicalEnd in ends)
@@ -93,12 +96,13 @@ internal class CarInspector_PopulateCarPanel_Patch
{
try
{
tagController.UpdateTag(car, car.TagCallout, OpsController.Shared);
builder.Rebuild();
if (car.TagCallout != null) tagController.UpdateTags(CameraSelector.shared._currentCamera.GroundPosition, true); //tagController.UpdateTag(car, car.TagCallout, OpsController.Shared);
if (ContextMenu.IsShown && ContextMenu.Shared.centerLabel.text == car.DisplayName) CarPickable.HandleShowContextMenu(car);
}
catch(Exception ex)
{
Log.Warning(ex, $"{nameof(AddObserver)} {car} Exception logged for {key}");
_log.ForContext("car", car).Warning(ex, $"{nameof(AddObserver)} {car} Exception logged for {key}");
}
},
false
@@ -122,7 +126,7 @@ internal class CarInspector_PopulateCarPanel_Patch
{
TrainController tc = UnityEngine.Object.FindObjectOfType<TrainController>();
IEnumerable<Model.Car> consist = car.EnumerateCoupled(LogicalEnd.A);
//Log.Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
_log.ForContext("car", car).Verbose($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
CalculateCostIfEnabled(car, mrocHelperType, buttonsHaveCost, consist);
@@ -136,7 +140,6 @@ internal class CarInspector_PopulateCarPanel_Patch
else
{
consist = consist.Where(c => c is not BaseLocomotive && c.Archetype != Model.Definition.CarArchetype.Tender);
Log.Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
//when ApplyHandbrakesAsNeeded is called, and the consist contains an engine, it stops applying brakes.
tc.ApplyHandbrakesAsNeeded(consist.ToList(), PlaceTrainHandbrakes.Automatic);
}
@@ -163,7 +166,7 @@ internal class CarInspector_PopulateCarPanel_Patch
case MrocHelperType.BleedAirSystem:
consist = consist.Where(c => c.NotMotivePower());
Log.Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
_log.ForContext("car", car).Information($"{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));
@@ -184,31 +187,35 @@ internal class CarInspector_PopulateCarPanel_Patch
if (cabooseWithAvailCrew == null) timeCost *= 1.5f;
var cabooseFoundDisplay = cabooseWithAvailCrew?.DisplayName ?? "No caboose";
Log.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).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)");
Multiplayer.SendError(StateManager.Shared._playersManager.LocalPlayer, $"{(cabooseWithAvailCrew != null ? $"{cabooseWithAvailCrew.DisplayName} Hours Adjusted: ({tsString})\n" : string.Empty)}Wages: ~(${Math.Ceiling((decimal)(timeCost / 3600) * 5)})");
if (buttonsHaveCost) StateManager_OnDayDidChange_Patch.UnbilledAutoBrakeCrewRunDuration += timeCost;
StateManager_OnDayDidChange_Patch.UnbilledAutoBrakeCrewRunDuration += timeCost;
}
}
public static Car? NearbyCabooseWithAvailableCrew(Car car, float timeNeeded, bool decrement = false)
{
HashSet<string> carIdsCheckedAlready = new();
//check current car.
Car? output = car.CabooseWithSufficientCrewHours(timeNeeded, decrement);
Car? output = car.CabooseWithSufficientCrewHours(timeNeeded, carIdsCheckedAlready, decrement);
if (output != null) return output; //short out if we are good
carIdsCheckedAlready.Add(car.id);
//check consist, for cabeese
IEnumerable<Car> consist = car.EnumerateCoupled(LogicalEnd.A);
output = consist.FirstOrDefault(c => c.CabooseWithSufficientCrewHours(timeNeeded, decrement));
output = consist.FirstOrDefault(c => c.CabooseWithSufficientCrewHours(timeNeeded, carIdsCheckedAlready, decrement));
if (output != null) return output; //short out if we are good
carIdsCheckedAlready.UnionWith(consist.Select(c => c.id));
//then check near consist cars for cabeese
TrainController tc = UnityEngine.Object.FindObjectOfType<TrainController>();
foreach (var c in consist)
{
output = c.HuntingForCabeeseNearCar(timeNeeded, tc, decrement);
output = c.HuntingForCabeeseNearCar(timeNeeded, tc, carIdsCheckedAlready, decrement);
if (output != null) return output; //short out if we are good
}

View File

@@ -0,0 +1,51 @@
using HarmonyLib;
using Model;
using Railloader;
using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using System.Linq;
using UI;
using UI.ContextMenu;
using static Model.Car;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(CarPickable))]
[HarmonyPatch(nameof(CarPickable.HandleShowContextMenu), typeof(Car))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarPickable_HandleShowContextMenu_Patch
{
private static void Postfix(Car car)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled) return;
bool buttonsHaveCost = tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false;
ContextMenu shared = ContextMenu.Shared;
var consist = car.EnumerateCoupled(LogicalEnd.A);
shared.AddButton(ContextMenuQuadrant.Unused2, $"{(consist.Any(c => c.HandbrakeApplied()) ? "Release " : "Set ")} Consist", SpriteName.Handbrake, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Handbrake, buttonsHaveCost);
});
if (consist.Any(c => c.EndAirSystemIssue()))
{
shared.AddButton(ContextMenuQuadrant.Unused2, $"Air Up Consist", SpriteName.Select, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost);
});
}
if (consist.Any(c => c.SupportsBleed()))
{
shared.AddButton(ContextMenuQuadrant.Unused2, $"Bleed Consist", SpriteName.Bleed, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.BleedAirSystem, buttonsHaveCost);
});
}
shared.BuildItemAngles();
shared.StartCoroutine(shared.AnimateButtonsShown());
}
}

View File

@@ -3,9 +3,7 @@ using Model;
using Model.OpsNew;
using Railloader;
using RMROC451.TweaksAndThings.Extensions;
using UI;
using UI.Tags;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches;
@@ -19,7 +17,6 @@ internal class TagController_UpdateTag_Patch
private static void Postfix(Car car, TagCallout tagCallout)
{
TagController tagController = UnityEngine.Object.FindObjectOfType<TagController>();
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled || !tweaksAndThings.settings.HandBrakeAndAirTagModifiers)
@@ -27,24 +24,14 @@ internal class TagController_UpdateTag_Patch
return;
}
ProceedWithPostFix(car, tagCallout, tagController);
ProceedWithPostFix(car, tagCallout);
return;
}
private static void ProceedWithPostFix(Car car, TagCallout tagCallout, TagController tagController)
private static void ProceedWithPostFix(Car car, TagCallout tagCallout)
{
bool isAltDownWithCarIssue = GameInput.IsAltDown && car.CarOrEndGearIssue();
tagCallout.callout.Title = string.Format(tagTitleFormat, "{0}", car.DisplayName);
tagCallout.gameObject.SetActive(
tagCallout.gameObject.activeSelf &&
(!GameInput.IsAltDown || isAltDownWithCarIssue)
);
if (tagCallout.gameObject.activeSelf && isAltDownWithCarIssue)
{
tagController.ApplyImageColor(tagCallout, Color.black);
}
tagCallout.callout.Title =
(car.CarAndEndGearIssue(), car.EndAirSystemIssue(), car.HandbrakeApplied()) switch

View File

@@ -34,7 +34,7 @@ public class Settings
internal void AddAnotherRow()
{
WebhookSettingsList = !WebhookSettingsList?.Any() ?? false ? new[] { new WebhookSettings() }.ToList() : new List<WebhookSettings>();
WebhookSettingsList ??= new[] { new WebhookSettings() }.ToList();
if (!string.IsNullOrEmpty(WebhookSettingsList.OrderByDescending(wsl => wsl.WebhookUrl).Last().WebhookUrl))
{
WebhookSettingsList.Add(new());
@@ -76,4 +76,19 @@ public class RosterFuelColumnSettings
public bool EngineRosterShowsFuelStatusAlways;
public EngineRosterFuelDisplayColumn EngineRosterFuelStatusColumn;
}
public static class SettingsExtensions
{
public static List<WebhookSettings> SanitizeEmptySettings(this IEnumerable<WebhookSettings>? settings)
{
List<WebhookSettings> output =
settings?.Where(s => !string.IsNullOrEmpty(s.WebhookUrl))?.ToList() ??
new();
output.Add(new());
return output;
}
}

View File

@@ -78,6 +78,10 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
if (!settings?.WebhookSettingsList?.Any() ?? true) settings.WebhookSettingsList = new[] { new WebhookSettings() }.ToList();
if (settings?.EngineRosterFuelColumnSettings == null) settings.EngineRosterFuelColumnSettings = new();
settings.WebhookSettingsList =
settings?.WebhookSettingsList.SanitizeEmptySettings();
//WebhookUISection(ref builder);
//builder.AddExpandingVerticalSpacer();
WebhooksListUISection(ref builder);