Compare commits

...

38 Commits

Author SHA1 Message Date
5affe27323 Merge pull request #41 from rmroc451/car-click-actions-and-other-misc-updates
Car click actions and other misc updates
2025-08-16 11:31:10 -05:00
d19a1a2995 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. 2025-08-16 11:23:40 -05:00
7c8becd471 2.0.0 feature updates 2025-08-11 00:25:30 -05:00
26a3bdaaa1 updates 2025-07-19 13:36:26 -05:00
3f2e3dee80 remove "experimental" from build target output 2025-03-08 21:21:38 -06:00
ed44ea829e Merge pull request #40 from rmroc451/experimental-2024-6
Experimental 2024.6 to main
2025-03-08 21:18:23 -06:00
bce1d8bd58 fixing issues with build angles on context menu for select/deselect on consists without an engine 2025-03-08 21:17:29 -06:00
c25271ee2d begone concentric circles! 2025-03-02 13:23:15 -06:00
80f0847587 reverting updated context menu visualizations, and allowing the even spacing of items vs quadrant concept. Pesky double circle context menu borders still remain, but I'm working on it. 2025-03-02 12:43:08 -06:00
7552422b6e disabling context menu dividers and expanding the context menu radius to 200f to help spread things out. 2025-02-22 23:43:00 -06:00
58dc7efac0 adjusting CarIdsInRect -> CarIdsInRadius 2025-02-16 09:41:58 -06:00
7ebd14db2a adjusting railroader usage of namespace Model.OpsNew -> Model.Ops and some adjustments to the version files. 2024-12-24 22:11:22 -06:00
ca0e78b971 hotfix for car inspector rebuild 2024-08-03 00:55:43 -05:00
9f210b0b8a increasing version number. 2024-08-01 15:55:29 -05:00
840f35cf62 Merge pull request #38 from rmroc451/37-passenger-stops-not-filling-crew-hours-on-cabeese
#37 refactored caboose crew hours load refill logic, condensing and a…
2024-07-28 12:11:16 -05:00
786db49b68 #37 refactored caboose crew hours load refill logic, condensing and also allowing passenger stops to work as they were expected to. 2024-07-28 08:13:21 -05:00
108900b026 Merge pull request #36 from rmroc451/33-double-click-to-follow-car
#33 double primary click any car/loco to follow
2024-07-27 22:33:18 -05:00
9e8c38e6f4 Merge pull request #35 from rmroc451/26-caboose-auto-heal-hotbox-when-fully-oiled
#26 auto heal hotbox when found if caboose use is enabled and caboose…
2024-07-27 22:32:49 -05:00
89f2490f14 Merge pull request #34 from rmroc451/32-buttons-dont-adjust-when-decoupling-as-expected
#32 swap from integration set that was giving inconsistent results, a…
2024-07-27 22:32:20 -05:00
fbd08b007c #33 double primary click any car/loco to follow 2024-07-27 22:31:33 -05:00
27d3432cbf #26 auto heal hotbox when found if caboose use is enabled and caboose is present in consist. 2024-07-27 22:30:40 -05:00
cf1d5e32e5 #32 swap from integration set that was giving inconsistent results, and adding in a panel.reubuildOnInterval when an observed value changes. 2024-07-27 22:25:23 -05:00
92317b29b2 Merge pull request #31 from rmroc451/v1.0.0-prep
Closes #27 fix issue with last car end gear issue detection
Closes #28 add hotbox icon to tag update info
Closes #25 adding follow to car context menu
Closes #24 adding consist info to non motive power cars, that have a caboose when Caboose Use mod setting is enabled.
Closes #29, #30 Allow caboose setting to require that a caboose is present in the consist to use the AI Engineer's AutoOiler & - AutoHotboxSpotter.
2024-07-26 12:40:38 -05:00
1f48bf04aa increment version number 2024-07-26 12:35:52 -05:00
5774c7f04c #29 #30 Allow caboose setting to require that a caboose is present in the consist to use the AI Engineer's AutoOiler & AutoHotboxSpotter. 2024-07-26 12:25:03 -05:00
5eed492b47 #24 adding consist info to non motive power cars, that have a caboose, when Caboose Use mod setting is enabled. 2024-07-26 12:20:52 -05:00
d4d18d8c92 #25 adding follow to car context menu 2024-07-26 10:17:09 -05:00
d7e35828b8 #28 add hotbox icon to tag update info 2024-07-26 10:15:09 -05:00
0b444d6364 #27 fix issue with last car end gear issue detection 2024-07-26 10:13:35 -05:00
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
39 changed files with 2090 additions and 368 deletions

7
Assembly.version Normal file
View File

@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<MajorVersion>2</MajorVersion>
<MinorVersion>0</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>
</Project>

View File

@@ -1,5 +1,6 @@
<Project> <Project>
<Import Project="Paths.user" Condition="Exists('Paths.user')" /> <Import Project="Paths.user" Condition="Exists('Paths.user')" />
<Import Project="Assembly.version" Condition="Exists('Assembly.version')" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>net48</TargetFramework> <TargetFramework>net48</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>

View File

@@ -6,13 +6,14 @@
<!-- Copy the mod to the game directory --> <!-- Copy the mod to the game directory -->
<GameModDir Condition="'$(GameModDir)' == ''">$(GameDir)/Mods/$(AssemblyName)</GameModDir> <GameModDir Condition="'$(GameModDir)' == ''">$(GameDir)/Mods/$(AssemblyName)</GameModDir>
<OutDir Condition="'$(Configuration)' == 'Debug'">$(GameModDir)/</OutDir> <OutDir Condition="'$(Configuration)' == 'Debug'">$(GameModDir)/</OutDir>
<VersionTimestamp>$([System.DateTime]::UtcNow.ToString(`o`))</VersionTimestamp>
</PropertyGroup> </PropertyGroup>
<!-- Replace the default version if something was set for it --> <!-- Replace the default version if something was set for it -->
<PropertyGroup Condition="'$(AssemblyVersion)' == '' OR '$(MajorVersion)' != '' OR '$(MinorVersion)' != ''"> <PropertyGroup Condition="'$(AssemblyVersion)' == '' OR '$(MajorVersion)' != '' OR '$(MinorVersion)' != ''">
<MajorVersion Condition="'$(MajorVersion)' == ''">0</MajorVersion> <MajorVersion Condition="'$(MajorVersion)' == ''">1</MajorVersion>
<MinorVersion Condition="'$(MinorVersion)' == ''">1</MinorVersion> <MinorVersion Condition="'$(MinorVersion)' == ''">1</MinorVersion>
<PatchVersion Condition="'$(PatchVersion)' == ''">6</PatchVersion> <PatchVersion Condition="'$(PatchVersion)' == ''">1</PatchVersion>
<AssemblyVersion>$(MajorVersion).$(MinorVersion).$(PatchVersion)</AssemblyVersion> <AssemblyVersion>$(MajorVersion).$(MinorVersion).$(PatchVersion)</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion> <FileVersion>$(AssemblyVersion)</FileVersion>
<ProductVersion>$(AssemblyVersion)</ProductVersion> <ProductVersion>$(AssemblyVersion)</ProductVersion>
@@ -30,7 +31,7 @@
<!-- Publish the mod as a neat zip file --> <!-- Publish the mod as a neat zip file -->
<Target Name="PrepareForPublishing" AfterTargets="AfterBuild" Condition="'$(Configuration)' == 'Release'"> <Target Name="PrepareForPublishing" AfterTargets="AfterBuild" Condition="'$(Configuration)' == 'Release'">
<!-- Replace $(AssemblyVersion) with the actual version --> <!-- Replace $(AssemblyVersion) with the actual version -->
<Exec Command="powershell -Command &quot;(Get-Content '$(OutputPath)Definition.json') -replace '\$\(AssemblyVersion\)', '$(AssemblyVersion)' | Set-Content '$(OutputPath)Definition.json'&quot;" /> <Exec Command="powershell -Command &quot;(Get-Content '$(OutputPath)Definition.json') -replace '\$\(AssemblyVersion\)', '$(AssemblyVersion)_$(VersionTimestamp)' | Set-Content '$(OutputPath)Definition.json'&quot;" />
<PropertyGroup> <PropertyGroup>
<ModsDirectory>$(OutputPath)/Mods</ModsDirectory> <ModsDirectory>$(OutputPath)/Mods</ModsDirectory>

6
Paths.user.example Normal file
View File

@@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<!-- Directory that the game (Railroader.exe) is in -->
<GameDir></GameDir>
</PropertyGroup>
</Project>

View File

@@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{452A23A6-81C8-49C6-A7EE-95FD9377F896}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{452A23A6-81C8-49C6-A7EE-95FD9377F896}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore .gitignore = .gitignore
Assembly.version = Assembly.version
Directory.Build.props = Directory.Build.props Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets Directory.Build.targets = Directory.Build.targets
Paths.user = Paths.user Paths.user = Paths.user

View File

@@ -1,10 +1,14 @@
using Game.State; using Game.Notices;
using Game.State;
using Helpers; using Helpers;
using Model.OpsNew; using Model.Ops;
using Network; using Network;
using RMROC451.TweaksAndThings.Extensions; using RMROC451.TweaksAndThings.Extensions;
using RMROC451.TweaksAndThings.Patches;
using System.Linq; using System.Linq;
using Track;
using UI.Console; using UI.Console;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Commands; namespace RMROC451.TweaksAndThings.Commands;
@@ -37,8 +41,15 @@ public class EchoCommand : IConsoleCommand
return "+ to include area or - to leave it out"; return "+ to include area or - to leave it out";
} }
if (comps[2] == "+") message += $" {OpsController.Shared.ClosestArea(car)?.name ?? "???"}"; var gamePoint = car.GetCenterPosition(Graph._graph);
EntityReference entityReference = new EntityReference(EntityType.Position, new Vector4( gamePoint.x, gamePoint.y, gamePoint.z, 0));
EntityReference loco = new EntityReference(EntityType.Car, car.id);
if (comps[2] == "+") message = new Hyperlink(entityReference.URI(), string.Format(message, OpsController.Shared.ClosestArea(car)?.name ?? "???"));
car.PostNotice(nameof(EchoCommand), $"{message} :{StateManager.Shared._playersManager.LocalPlayer}");
ExpandedConsole_Add_Patch.SendMs(null, $"{Hyperlink.To(car)} {message}");
Multiplayer.Broadcast($"{StateManager.Shared._playersManager.LocalPlayer} {Hyperlink.To(car)}: \"{message}\""); Multiplayer.Broadcast($"{StateManager.Shared._playersManager.LocalPlayer} {Hyperlink.To(car)}: \"{message}\"");
return string.Empty; return string.Empty;
} }
} }

View File

@@ -5,10 +5,17 @@
"version": "$(AssemblyVersion)", "version": "$(AssemblyVersion)",
"requires": [ "requires": [
{ {
"id": "railloader", "id": "railroader",
"notBefore": "1.8.1" "notBefore": "2024.6"
}, },
"Zamu.StrangeCustoms" {
"id": "railloader",
"notBefore": "1.9.6.14"
},
{
"id": "Zamu.StrangeCustoms",
"notBefore": "1.10.25017.313"
}
], ],
"assemblies": [ "RMROC451.TweaksAndThings" ], "assemblies": [ "RMROC451.TweaksAndThings" ],
"mixintos": { "mixintos": {

View File

@@ -0,0 +1,7 @@
namespace RMROC451.TweaksAndThings.Enums;
public enum CrewHourLoadMethod
{
Tracks,
Daily
}

View File

@@ -4,5 +4,6 @@ public enum MrocHelperType
{ {
Handbrake, Handbrake,
GladhandAndAnglecock, GladhandAndAnglecock,
BleedAirSystem BleedAirSystem,
Oil
} }

View File

@@ -0,0 +1,124 @@
using Game.Messages;
using Model.AI;
using Network;
using System;
using System.Collections;
using UnityEngine;
using Random = UnityEngine.Random;
namespace RMROC451.TweaksAndThings.Extensions
{
internal static class AutoEngineer_Extensions
{
private static float CabooseHalvedFloat(this float input, Model.Car? hasCaboose) =>
hasCaboose ? input / 2 : input;
private static float CabooseAutoOilerLimit(this Model.Car? caboose) =>
caboose ? 0.99f : AutoOiler.OilIfBelow;
public static IEnumerator MrocAutoOilerLoop(this AutoOiler oiler, Serilog.ILogger _log, bool cabooseRequired)
{
int originIndex = oiler.FindOriginIndex();
Model.Car? foundCaboose = oiler._originCar.FindMyCaboose(0.0f, false);
if (originIndex < 0)
{
_log.Error("Couldn't find origin car {car}", oiler._originCar);
oiler._coroutine = null;
yield break;
} else if (CabooseRequirementChecker(string.Format("{0} {1}", oiler.GetType().Name, oiler.name), cabooseRequired, foundCaboose, _log))
{
yield break;
}
oiler._reverse = originIndex > oiler._cars.Count - originIndex;
_log.Information(
"AutoOiler {name} starting, rev = {reverse}, caboose required = {req}, caboose halving adjustment = {hasCaboose}, oil limit = {limit}",
oiler.name,
oiler._reverse,
cabooseRequired,
foundCaboose,
foundCaboose.CabooseAutoOilerLimit()
);
while (true)
{
yield return new WaitForSeconds(AutoOiler.StartDelay.CabooseHalvedFloat(foundCaboose));
foundCaboose = oiler._originCar.FindMyCaboose(0.0f,false);
int carIndex = originIndex;
float adjustedTimeToWalk = AutoOiler.TimeToWalkCar.CabooseHalvedFloat(foundCaboose);
do
{
if (oiler.TryGetCar(carIndex, out var car))
{
float num = 0f;
float origOil = car.Oiled;
if (car.NeedsOiling && car.Oiled < foundCaboose.CabooseAutoOilerLimit())
{
float num2 = 1f - car.Oiled;
car.OffsetOiled(num2);
float num3 = num2 * AutoOiler.TimeToFullyOil.CabooseHalvedFloat(foundCaboose);
num += num3;
oiler._pendingRunDuration += num3;
oiler._oiledCount++;
_log.Information("AutoOiler {name}: oiled {car} from {orig} => {new}", oiler.name, car, origOil, car.Oiled);
}
if (car.HasHotbox && car.Oiled == 1f && cabooseRequired && foundCaboose)
{
_log.Information("AutoOiler {name}: {foundCaboose} repaired hotbox {car}", oiler.name, foundCaboose, car);
Multiplayer.Broadcast($"{Hyperlink.To(oiler._originCar)}: \"{Hyperlink.To(car)} hotbox repaired!\"");
car.SendPropertyChange(PropertyChange.Control.Hotbox, false);
}
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, bool cabooseRequired)
{
Func<Model.Car?> foundCaboose = () => spotter._locomotive.FindMyCaboose(0.0f, false);
while (true)
{
if (!spotter.HasCars)
{
yield return new WaitForSeconds(1f);
continue;
}
var fc = foundCaboose();
_log.Information("AutoHotboxSpotter {name}: Hotbox Spotter Running, Found Caboose => {hasCaboose}; Has Cars {hasCars}; Requires Caboose {requiresCaboose}",
spotter.name, fc, spotter.HasCars, cabooseRequired);
if (CabooseRequirementChecker(string.Format("{0} {1}", spotter.GetType().Name, spotter.name), cabooseRequired, fc, _log))
{
yield break;
}
spotter.CheckForHotbox();
while (spotter.HasCars)
{
int num = Random.Range(60, 300);
fc = foundCaboose();
if (fc)
{
var numOrig = num;
num = Random.Range(15, 30);
_log.Information("AutoHotboxSpotter {name}: Next check went from num(60,300) => {numOrig}; to num(15,30) => {hasCaboose}; Requires Caboose {requiresCaboose}", spotter.name, numOrig, num, fc, cabooseRequired);
}
yield return new WaitForSeconds(num);
spotter.CheckForHotbox();
}
}
}
private static bool CabooseRequirementChecker(string name, bool cabooseRequired, Model.Car? foundCaboose, Serilog.ILogger _log)
{
bool error = cabooseRequired && foundCaboose == null;
if (error) {
_log.Debug("{name}: Couldn't find required caboose!", name);
}
return error;
}
}
}

View File

@@ -1,7 +1,12 @@
using Helpers; using Game.Messages;
using Game.State;
using Helpers;
using Model; using Model;
using Model.Definition;
using Model.Definition.Data; using Model.Definition.Data;
using Model.OpsNew; using Model.Ops;
using Model.Ops.Timetable;
using Railloader;
using Serilog; using Serilog;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -12,10 +17,14 @@ namespace RMROC451.TweaksAndThings.Extensions;
public static class Car_Extensions public static class Car_Extensions
{ {
private static bool EndGearIssue(this Car car, Car.LogicalEnd end) =>
(!car[end].IsCoupled && car[end].IsAnglecockOpen) ||
(car[end].IsCoupled && !car[end].IsAirConnectedAndOpen);
public static bool EndAirSystemIssue(this Car car) public static bool EndAirSystemIssue(this Car car)
{ {
bool AEndAirSystemIssue = car[Car.LogicalEnd.A].IsCoupled && !car[Car.LogicalEnd.A].IsAirConnectedAndOpen; bool AEndAirSystemIssue = car.EndGearIssue(Car.LogicalEnd.A);
bool BEndAirSystemIssue = car[Car.LogicalEnd.B].IsCoupled && !car[Car.LogicalEnd.B].IsAirConnectedAndOpen; bool BEndAirSystemIssue = car.EndGearIssue(Car.LogicalEnd.B);
bool EndAirSystemIssue = AEndAirSystemIssue || BEndAirSystemIssue; bool EndAirSystemIssue = AEndAirSystemIssue || BEndAirSystemIssue;
return EndAirSystemIssue; return EndAirSystemIssue;
} }
@@ -45,7 +54,7 @@ public static class Car_Extensions
return car; return car;
} }
public static bool NotMotivePower(this Car car) => car is not BaseLocomotive && car.Archetype != Model.Definition.CarArchetype.Tender; public static bool MotivePower(this Car car) => car is BaseLocomotive || car.Archetype == Model.Definition.CarArchetype.Tender;
/// <summary> /// <summary>
/// For every car in the consist, cost 1 minute of AI Engineer time. /// For every car in the consist, cost 1 minute of AI Engineer time.
@@ -56,12 +65,48 @@ 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 bool CabooseInConsist(this IEnumerable<Car> input) => input.FirstOrDefault(c => c.IsCaboose()); public static Car? CarCaboose(this Car car) => car.IsCaboose() ? car : null;
public static Car? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, HashSet<string> carIdsCheckedAlready, bool decrement = false) 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 bool ConsistNoFreight(this IEnumerable<Car> input) =>
input.Where(c => !c.MotivePower()).Any() &&
input
.Where(c => !c.MotivePower())
.All(c => !c.MrocIsFreight());
public static bool ConsistFreight(this IEnumerable<Car> input) =>
input.Where(c => !c.MotivePower()).Any() &&
input
.Where(c => !c.MotivePower())
.All(c => c.MrocIsFreight());
public static bool MrocIsFreight(this Car c) =>
c.Archetype.IsFreight() || c.Archetype switch
{
CarArchetype.LocomotiveDiesel => false,
CarArchetype.LocomotiveSteam => false,
CarArchetype.Coach => false,
CarArchetype.Baggage => false,
_ => true
};
public static bool SelectedEngineExpress(this TrainController input) =>
input.SelectedLocomotive.TryGetTimetableTrain(out Timetable.Train t) &&
t.TrainClass == Timetable.TrainClass.First;
public static Car? FindMyCaboose(this Car car, float timeNeeded, bool decrement = false) =>
(
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; Car? output = null;
if (carIdsCheckedAlready.Contains(car.id) || !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++)
@@ -78,44 +123,51 @@ public static class Car_Extensions
return output; return output;
} }
public static Car? HuntingForCabeeseNearCar(this Car car, float timeNeeded, TrainController tc, HashSet<string> carIdsCheckedAlready, bool decrement = false) private static Car? FindNearestCabooseFromNearbyCars(this IEnumerable<(Car car, bool crewCar, float distance)> source) =>
{ source
List<(string carId, float distance)> source = ?.OrderBy(c => c.crewCar ? 0 : 1)
CarsNearCurrentCar(car, timeNeeded, tc, carIdsCheckedAlready, decrement); ?.ThenBy(c => c.distance)
?.Select(c => c.car)
?.FirstOrDefault();
Car output = FindNearestCabooseFromNearbyCars(tc, source); private static IEnumerable<(Car car, bool crewCar, float distance)> CarsNearCurrentCar(this Car car, float timeNeeded, bool decrement)
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; Area carArea = OpsController.Shared.ClosestArea(car);
Vector3 center = WorldTransformer.WorldToGame(position);
Rect rect = new Rect(new Vector2(center.x - 30f, center.z - 30f), Vector2.one * 30f * 2f); var cabeese = OpsController.Shared
var cars = tc.CarIdsInRect(rect); .CarsInArea(carArea)
Log.Information($"{nameof(HuntingForCabeeseNearCar)} => {cars.Count()}"); .Select(c => TrainController.Shared.CarForId(c.Id))
List<(string carId, float distance)> source = .Union(car.EnumerateCoupled())
cars .Where(c => c.IsCaboose());
.Select(carId =>
{ //if (cabeese?.Any() ?? false) Log.Information($"{nameof(CarsNearCurrentCar)}[{car.DisplayName}] => {cabeese.Count()}");
Car car = tc.CarForId(carId);
if (car == null || !car.CabooseWithSufficientCrewHours(timeNeeded, carIdsCheckedAlready)) List<(Car car, bool crewCar, float distance)> source =
{ cabeese.Select(c => (car: c, crewCar: c.IsCrewCar(), distance: car.Distance(c))).ToList();
return (carId: carId, distance: 1000f);
}
Vector3 a = WorldTransformer.WorldToGame(car.GetMotionSnapshot().Position);
return (carId: carId, distance: Vector3.Distance(a, center));
}).ToList();
return source; return source;
} }
public static float Distance(this Car car1, Car car2)
{
Vector3 position = car1.GetMotionSnapshot().Position;
Vector3 center = WorldTransformer.WorldToGame(position);
Vector3 a = WorldTransformer.WorldToGame(car2.GetMotionSnapshot().Position);
return Vector3.Distance(a, center);
}
public static bool IsCrewCar(this Car car) =>
!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, float hotboxValue = 0f) =>
StateManager.ApplyLocal(
new PropertyChange(
car.id, PropertyChange.KeyForControl(PropertyChange.Control.Hotbox),
new FloatPropertyValue(hotboxValue)
)
);
} }

View File

@@ -0,0 +1,36 @@
using RMROC451.TweaksAndThings.Patches;
using Serilog;
using UnityEngine;
using ILogger = Serilog.ILogger;
namespace RMROC451.TweaksAndThings.Extensions;
public static class TextSprite_Extensions
{
public static string TriColorPiePercent(this float quantity, float capacity, string name = "")
{
int num;
if (capacity <= 0f)
{
num = 0;
}
else
{
float num2 = Mathf.Clamp01(quantity / capacity);
int num3 = ((!(num2 < 0.01f)) ? ((!(num2 > 0.99f)) ? (Mathf.FloorToInt(num2 * 15f) + 1) : 16) : 0);
num = num3;
}
string color = "#219106"; //Green
if (num > 5 && num <= 10)
{
color = "#CE8326"; //orange
} else if (num <= 5)
{
color = "#D53427"; //Red
}
string sprite = string.IsNullOrEmpty(name) ? $"Pie{num:D2}" : name;
return $"<sprite tint=1 color={color} name={sprite}>";
}
}

View File

@@ -0,0 +1,22 @@
using HarmonyLib;
using Railloader;
using UI.Builder;
using UI.EngineControls;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(AutoEngineerControlSetBase))]
[HarmonyPatch(nameof(AutoEngineerControlSetBase.UpdateStatusLabel))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class AutoEngineerControlSetBase_UpdateStatusLabel_Patch
{
static void Postfix(AutoEngineerControlSetBase __instance)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled() || !AutoEngineerOrdersHelper_SendAutoEngineerCommand_Patch.SafetyFirstGoverningApplies()) return;
string orig = __instance.statusLabel.text;
__instance.statusLabel.text = $"{orig}; <b>Safety</b>";
__instance.statusLabel.rectTransform.Tooltip("Status", $"<b>Safety First, Speed Limited</b>\n\n{orig}");
}
}

View File

@@ -0,0 +1,92 @@
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.Information($"{Enum.GetName(typeof(AutoEngineerMode), mode)}[{TrainController.Shared.SelectedLocomotive.DisplayName}] {nameof(AutoEngineerOrdersExtensions.MaxSpeedMph)} limited to {limitedSpeed} from {orig}; No Caboose in Consist;");
}
}
return true;
}
internal static bool SafetyFirstGoverningApplies()
{
var _persistence = new AutoEngineerPersistence(TrainController.Shared.SelectedLocomotive.KeyValueObject);
var OrdersHelper = new AutoEngineerOrdersHelper(TrainController.Shared.SelectedLocomotive, _persistence);
bool cabooseReq = SingletonPluginBase<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 = () =>
{
var output = TrainController.Shared.SelectedLocomotive.FindMyCaboose(0.0f, false) == null;
logMessage += $"\ncaboose? {!output}";
return output;
};
logMessage += $"\nCaboose Required {cabooseReq}";
bool output =
cabooseReq &&
!firstClass() &&
FreightConsist() &&
noCaboose();
logMessage += $"\nGovern AE? {output}";
_log.Information(logMessage);
return output;
}
}

View File

@@ -0,0 +1,26 @@
using Game.Notices;
using HarmonyLib;
using Model;
using Serilog;
using UI.EngineControls;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(AutoEngineerOrdersHelper))]
[HarmonyPatch(nameof(AutoEngineerOrdersHelper.SetWaypoint), typeof(Track.Location), typeof(string))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class AutoEngineerOrdersHelper_SetWaypoint_patch
{
private static Serilog.ILogger _log => Log.ForContext<AutoEngineerOrdersHelper_SetWaypoint_patch>();
static void Postfix(AutoEngineerOrdersHelper __instance, Track.Location location, string coupleToCarId)
{
_log.Information($"start setWP");
Car selectedLoco = __instance._locomotive;
_log.Information($"{selectedLoco?.DisplayName ?? ""} set WP");
Vector3 gamePoint = location.GetPosition();
EntityReference entityReference = new EntityReference(EntityType.Position, new Vector4(gamePoint.x, gamePoint.y, gamePoint.z, 0));
selectedLoco.PostNotice("ai-wpt-rmroc451", new Hyperlink(entityReference.URI(), $"WP SET"));
}
}

View File

@@ -0,0 +1,27 @@
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.EndGearHelpersRequirePayment();
bool cabooseRequired = tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter();
if (buttonsHaveCost) __result = __instance.MrocAutoHotboxSpotterLoop(_log, cabooseRequired);
return !buttonsHaveCost; //only hit this if !buttonsHaveCost, since Loop is a coroutine
}
}

View File

@@ -0,0 +1,27 @@
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.EndGearHelpersRequirePayment();
bool cabooseRequired = tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() && !__instance._cars.ConsistNoFreight();
if (buttonsHaveCost) __result = __instance.MrocAutoOilerLoop(_log, cabooseRequired);
return !buttonsHaveCost; //only hit this if !buttonsHaveCost, since Loop is a coroutine
}
}

View File

@@ -0,0 +1,52 @@
using HarmonyLib;
using Railloader;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UI.Builder;
using UI.PreferencesWindow;
using UnityEngine.InputSystem;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(BindingsWindow))]
[HarmonyPatch(nameof(BindingsWindow.Build), typeof(UIPanelBuilder))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class BindingsWindow_Build_Patch
{
public static bool Prefix(BindingsWindow __instance, UIPanelBuilder builder)
{
return true;
//TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
//if (!tweaksAndThings.IsEnabled) return true;
//(string title, InputAction[] actions)[] rebindableActions = BindingsWindow.RebindableActions;
//HashSet<InputAction> conflicts = BindingsWindow.FindConflicts(rebindableActions.SelectMany(((string title, InputAction[] actions) t) => t.actions));
//__instance._conflicts = conflicts;
//__instance._builder = builder;
//builder.AddTabbedPanels(__instance._selectedTabState, delegate (UITabbedPanelBuilder uITabbedPanelBuilder)
//{
// (string, InputAction[])[] array = rebindableActions;
// for (int i = 0; i < array.Length; i++)
// {
// var (text, actions) = array[i];
// uITabbedPanelBuilder.AddTab(text, text, delegate (UIPanelBuilder uIPanelBuilder)
// {
// uIPanelBuilder.VScrollView(delegate (UIPanelBuilder uIPanelBuilder2)
// {
// InputAction[] array2 = actions;
// foreach (InputAction val in array2)
// {
// //uIPanelBuilder2.AddInputBindingControl(val, conflicts.Contains(val), DidRebind);
// }
// });
// });
// }
//});
//return false;
}
}

View File

@@ -1,19 +1,22 @@
using Game.Messages; using Core;
using Game.Messages;
using Game.State; using Game.State;
using HarmonyLib; using HarmonyLib;
using KeyValue.Runtime; using KeyValue.Runtime;
using Model; using Model;
using Model.OpsNew;
using Network; using Network;
using Railloader; using Railloader;
using RMROC451.TweaksAndThings.Enums; using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Extensions; using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using Serilog; using Serilog;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using UI;
using UI.Builder; using UI.Builder;
using UI.CarInspector; using UI.CarInspector;
using UI.ContextMenu;
using UI.Tags; using UI.Tags;
using static Model.Car; using static Model.Car;
@@ -24,6 +27,7 @@ namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatchCategory("RMROC451TweaksAndThings")] [HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarInspector_PopulateCarPanel_Patch 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>(); private static IEnumerable<LogicalEnd> ends = Enum.GetValues(typeof(LogicalEnd)).Cast<LogicalEnd>();
/// <summary> /// <summary>
@@ -36,48 +40,74 @@ internal class CarInspector_PopulateCarPanel_Patch
{ {
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared; TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled) return true; if (!tweaksAndThings.IsEnabled()) return true;
bool buttonsHaveCost = tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false; bool buttonsHaveCost = tweaksAndThings.EndGearHelpersRequirePayment();
var consist = __instance._car.EnumerateCoupled(LogicalEnd.A); var consist = __instance._car.EnumerateCoupled();
builder = AddCarConsistRebuildObservers(builder, consist);
builder.HStack(delegate (UIPanelBuilder hstack) builder.HStack(delegate (UIPanelBuilder hstack)
{ {
hstack = AddCarConsistRebuildObservers(hstack, consist);
var buttonName = $"{(consist.Any(c => c.HandbrakeApplied()) ? "Release " : "Set ")} {TextSprites.HandbrakeWheel}"; var buttonName = $"{(consist.Any(c => c.HandbrakeApplied()) ? "Release " : "Set ")} {TextSprites.HandbrakeWheel}";
hstack.AddButtonCompact(buttonName, delegate { hstack.AddButtonCompact(buttonName, delegate
{
MrocConsistHelper(__instance._car, MrocHelperType.Handbrake, buttonsHaveCost); MrocConsistHelper(__instance._car, MrocHelperType.Handbrake, buttonsHaveCost);
hstack.Rebuild(); hstack.Rebuild();
}).Tooltip(buttonName, $"Iterates over cars in this consist and {(consist.Any(c => c.HandbrakeApplied()) ? "releases" : "sets")} {TextSprites.HandbrakeWheel}."); }).Tooltip(buttonName, $"Iterates over cars in this consist and {(consist.Any(c => c.HandbrakeApplied()) ? "releases" : "sets")} {TextSprites.HandbrakeWheel}.");
if (consist.Any(c => c.EndAirSystemIssue())) if (consist.Any(c => c.EndAirSystemIssue()))
{ {
hstack.AddButtonCompact("Connect Air", delegate { hstack.AddButtonCompact("Connect Air", delegate
{
MrocConsistHelper(__instance._car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost); MrocConsistHelper(__instance._car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost);
hstack.Rebuild(); hstack.Rebuild();
}).Tooltip("Connect Consist Air", "Iterates over each car in this consist and connects gladhands and opens anglecocks."); }).Tooltip("Connect Consist Air", "Iterates over each car in this consist and connects gladhands and opens anglecocks.");
} }
hstack.AddButtonCompact("Bleed Consist", delegate { hstack.AddButtonCompact("Bleed Consist", delegate
{
MrocConsistHelper(__instance._car, MrocHelperType.BleedAirSystem, buttonsHaveCost); MrocConsistHelper(__instance._car, MrocHelperType.BleedAirSystem, buttonsHaveCost);
hstack.Rebuild(); hstack.Rebuild();
}).Tooltip("Bleed Air Lines", "Iterates over each car in this consist and bleeds the air out of the lines."); }).Tooltip("Bleed Air Lines", "Iterates over each car in this consist and bleeds the air out of the lines.");
}); });
CabooseUiEnhancer(__instance, builder, consist, tweaksAndThings);
return true; return true;
} }
private static UIPanelBuilder AddCarConsistRebuildObservers(UIPanelBuilder builder, IEnumerable<Model.Car> consist) private static void CabooseUiEnhancer(CarInspector __instance, UIPanelBuilder builder, IEnumerable<Car> consist, TweaksAndThingsPlugin plugin)
{
if (plugin.CabooseNonMotiveAllowedSetting(__instance._car))
{
builder.HStack(delegate (UIPanelBuilder hstack)
{
hstack = AddCarConsistRebuildObservers(hstack, consist, all: false);
hstack.AddField("Consist Info", hstack.HStack(delegate (UIPanelBuilder field)
{
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);
field.AddLabel(() => newSubTitle(), UIPanelBuilder.Frequency.Fast)
.Tooltip("Consist Info", "Reflects info about consist.").FlexibleWidth();
}));
});
}
}
private static UIPanelBuilder AddCarConsistRebuildObservers(UIPanelBuilder builder, IEnumerable<Model.Car> consist, bool all = true)
{ {
TagController tagController = UnityEngine.Object.FindFirstObjectByType<TagController>(); 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); builder = AddObserver(builder, car, PropertyChange.KeyForControl(PropertyChange.Control.Handbrake), tagController);
foreach (LogicalEnd logicalEnd in ends) foreach (LogicalEnd logicalEnd in ends)
{ {
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsCoupled, car.LogicalToEnd(logicalEnd)), tagController); builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsCoupled, car.LogicalToEnd(logicalEnd)), tagController);
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsAirConnected, car.LogicalToEnd(logicalEnd)), tagController); if (all) builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsAirConnected, car.LogicalToEnd(logicalEnd)), tagController);
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.Anglecock, car.LogicalToEnd(logicalEnd)), tagController); if (all) builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.Anglecock, car.LogicalToEnd(logicalEnd)), tagController);
} }
} }
@@ -93,12 +123,13 @@ internal class CarInspector_PopulateCarPanel_Patch
{ {
try try
{ {
tagController.UpdateTag(car, car.TagCallout, OpsController.Shared);
builder.Rebuild(); builder.Rebuild();
if (car.TagCallout != null) tagController.UpdateTags(CameraSelector.shared._currentCamera.GroundPosition, true);
if (ContextMenu.IsShown && ContextMenu.Shared.centerLabel.text == car.DisplayName) CarPickable.HandleShowContextMenu(car);
} }
catch(Exception ex) 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 false
@@ -118,11 +149,12 @@ internal class CarInspector_PopulateCarPanel_Patch
// } // }
//} //}
public static void MrocConsistHelper(Model.Car car, MrocHelperType mrocHelperType, bool buttonsHaveCost) public static int MrocConsistHelper(Model.Car car, MrocHelperType mrocHelperType, bool buttonsHaveCost)
{ {
int output = 0;
TrainController tc = UnityEngine.Object.FindObjectOfType<TrainController>(); TrainController tc = UnityEngine.Object.FindObjectOfType<TrainController>();
IEnumerable<Model.Car> consist = car.EnumerateCoupled(LogicalEnd.A); IEnumerable<Model.Car> consist = car.EnumerateCoupled();
//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); CalculateCostIfEnabled(car, mrocHelperType, buttonsHaveCost, consist);
@@ -136,7 +168,6 @@ internal class CarInspector_PopulateCarPanel_Patch
else else
{ {
consist = consist.Where(c => c is not BaseLocomotive && c.Archetype != Model.Definition.CarArchetype.Tender); 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. //when ApplyHandbrakesAsNeeded is called, and the consist contains an engine, it stops applying brakes.
tc.ApplyHandbrakesAsNeeded(consist.ToList(), PlaceTrainHandbrakes.Automatic); tc.ApplyHandbrakesAsNeeded(consist.ToList(), PlaceTrainHandbrakes.Automatic);
} }
@@ -144,6 +175,35 @@ internal class CarInspector_PopulateCarPanel_Patch
case MrocHelperType.GladhandAndAnglecock: case MrocHelperType.GladhandAndAnglecock:
consist.Do(c => consist.Do(c =>
CarEndAirUpdate(c)
);
break;
case MrocHelperType.BleedAirSystem:
consist = consist.Where(c => !c.MotivePower());
_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));
}
break;
case MrocHelperType.Oil:
consist = consist.Where(c => c.NeedsOiling || c.HasHotbox);
_log.ForContext("car", car).Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
foreach (Model.Car oil in consist)
{
StateManager.ApplyLocal(new PropertyChange(oil.id, nameof(Car.Oiled).ToLower(), new FloatPropertyValue(1)));
output += car.HasHotbox ? 1 : 0;
if (car.HasHotbox) car.AdjustHotboxValue();
}
break;
}
return output;
}
internal static void CarEndAirUpdate(Car c)
{
ends.Do(end => ends.Do(end =>
{ {
EndGear endGear = c[end]; EndGear endGear = c[end];
@@ -157,65 +217,27 @@ internal class CarInspector_PopulateCarPanel_Patch
); );
if (c.TryGetAdjacentCar(end, out Model.Car c2)) StateManager.ApplyLocal(new SetGladhandsConnected(c.id, c2.id, true)); if (c.TryGetAdjacentCar(end, out Model.Car c2)) StateManager.ApplyLocal(new SetGladhandsConnected(c.id, c2.id, true));
}) });
);
break;
case MrocHelperType.BleedAirSystem:
consist = consist.Where(c => c.NotMotivePower());
Log.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));
}
break;
}
} }
private static void CalculateCostIfEnabled(Car car, MrocHelperType mrocHelperType, bool buttonsHaveCost, IEnumerable<Car> consist) internal static void CalculateCostIfEnabled(Car car, MrocHelperType mrocHelperType, bool buttonsHaveCost, IEnumerable<Car> consist)
{ {
if (buttonsHaveCost) if (buttonsHaveCost)
{ {
float originalTimeCost = consist.CalculateCostForAutoEngineerEndGearSetting(); float originalTimeCost = consist.CalculateCostForAutoEngineerEndGearSetting();
float timeCost = originalTimeCost; float timeCost = originalTimeCost;
float crewCost = timeCost / 3600; //hours of time deducted from caboose. float crewCost = timeCost / 3600; //hours of time deducted from caboose.
var tsString = crewCost.FormatCrewHours(IndustryComponent_Service_Patch.CrewHoursLoad().description); var tsString = crewCost.FormatCrewHours(OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad().description);
Car? cabooseWithAvailCrew = NearbyCabooseWithAvailableCrew(car, crewCost, buttonsHaveCost); Car? cabooseWithAvailCrew = car.FindMyCaboose(crewCost, buttonsHaveCost);
if (cabooseWithAvailCrew == null) timeCost *= 1.5f; if (cabooseWithAvailCrew == null) timeCost *= 1.5f;
var cabooseFoundDisplay = cabooseWithAvailCrew?.DisplayName ?? "No caboose"; 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)})"); 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, 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, 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, carIdsCheckedAlready, decrement);
if (output != null) return output; //short out if we are good
}
return output;
}
} }

View File

@@ -0,0 +1,153 @@
using Core;
using HarmonyLib;
using Model;
using Model.Ops;
using Network;
using Railloader;
using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using Serilog;
using System;
using System.Linq;
using UI;
using UI.Tags;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches;
enum MrocAction
{
Follow,
Inspect,
ConnectConsistAir,
ToggleConsistBrakes
}
[HarmonyPatch(typeof(CarPickable))]
[HarmonyPatch(nameof(CarPickable.Activate), typeof(PickableActivateEvent))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarPickable_Activate_Patch
{
private static Serilog.ILogger _log => Log.ForContext<CarPickable_Activate_Patch>();
static float clicked = 0;
static float clicktime = 0;
static float clickdelay = 0.5f;
private static bool Prefix(CarPickable __instance, PickableActivateEvent evt)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return true;
bool bCtrlAltHeld = GameInput.IsControlDown && GameInput.IsAltDown;
_log.ForContext("car", __instance.car).Information($"{GameInput.IsShiftDown} {GameInput.IsControlDown} {GameInput.IsAltDown} {bCtrlAltHeld} ");
if (OnPointerDown(evt, PickableActivation.Primary))
{
CameraSelector.shared.FollowCar(__instance.car);
_log.ForContext("car", __instance.car).Information("just click!");
return false;
}
//single click with keys pressed:
else if (evt.Activation == PickableActivation.Primary)
{
bool output = true;
var consist = __instance.car.EnumerateCoupled();
bool handbrakesApplied = consist.Any(c => c.HandbrakeApplied());
bool airSystemIssues = consist.Any(c => c.EndAirSystemIssue());
Func<bool> cabooseNear = () => (bool)__instance.car.FindMyCaboose(0.0f, false);
bool needsOiling = GameInput.IsShiftDown && consist.All(c => c.IsStopped()) && consist.Any(c => c.NeedsOiling || c.HasHotbox) && (!tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() || cabooseNear());
var chargeIt = handbrakesApplied || airSystemIssues || needsOiling;
//CTRL + ALT + SHIFT : BrakesAngleCocksAndOiling
//CTRL + ALT : Release Consist Brakes and Check AngleCocks
//ALT + SHIFT : toggle consist brakes
//CTRL + SHIFT : Check Consist Angle Cocks
//ALT : Toggle car brakes and & air up cars
//CTRL : NOTHING; BASE CAR INSPECTOR
if (bCtrlAltHeld)
{
BrakesAngleCocksAndOiling(__instance, tweaksAndThings, GameInput.IsShiftDown, consist, handbrakesApplied, airSystemIssues, needsOiling, chargeIt);
_log.ForContext("car", __instance.car).Information($"ctrlAlt{(GameInput.IsShiftDown ? "shift" : string.Empty)}Held!");
output = false;
}
else if (GameInput.IsAltDown && GameInput.IsShiftDown)
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.Handbrake, tweaksAndThings.EndGearHelpersRequirePayment());
_log.ForContext("car", __instance.car).Information("ctrlShiftHeld!");
output = false;
}
else if (GameInput.IsControlDown && GameInput.IsShiftDown)
{
if (airSystemIssues)
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.GladhandAndAnglecock, tweaksAndThings.EndGearHelpersRequirePayment());
_log.ForContext("car", __instance.car).Information("altShiftHeld!");
output = false;
}
else if (GameInput.IsAltDown)
{
__instance.car.SetHandbrake(!__instance.car.HandbrakeApplied());
CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(__instance.car);
if (__instance.car.TryGetAdjacentCar(Car.LogicalEnd.A, out Model.Car cA)) CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(cA);
if (__instance.car.TryGetAdjacentCar(Car.LogicalEnd.B, out Model.Car cB)) CarInspector_PopulateCarPanel_Patch.CarEndAirUpdate(cB);
_log.ForContext("car", __instance.car).Information("ctrlHeld!");
TagController.Shared.UpdateTag(__instance.car, __instance.car.TagCallout, OpsController.Shared);
__instance.car.TagCallout.Update();
output = false;
}
return output;
//else if (ctrlHeld && shiftHeld)
//{
// var selected = UI.CarInspector.CarInspector._instance._selectedTabState.Value;
// UI.CarInspector.CarInspector.Show(__instance.car);
// UI.CarInspector.CarInspector._instance._selectedTabState.Value = selected;
//}
}
return true;
}
private static void BrakesAngleCocksAndOiling(CarPickable __instance, TweaksAndThingsPlugin tweaksAndThings, bool shiftHeld, System.Collections.Generic.IEnumerable<Car> consist, bool handbrakesApplied, bool airSystemIssues, bool needsOiling, bool chargeIt)
{
int hbFix = 0;
if (handbrakesApplied)
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.Handbrake, false);
if (airSystemIssues)
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.GladhandAndAnglecock, false);
if (needsOiling)
hbFix = CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(__instance.car, MrocHelperType.Oil, false);
if (hbFix > 0)
Multiplayer.Broadcast($"Near {Hyperlink.To(__instance.car)}: \"{hbFix.Pluralize("hotbox") + " repaired!"}\"");
if (chargeIt)
CarInspector_PopulateCarPanel_Patch.CalculateCostIfEnabled(__instance.car, MrocHelperType.Handbrake, tweaksAndThings.EndGearHelpersRequirePayment(), consist);
}
public static bool OnPointerDown(
PickableActivateEvent evt,
PickableActivation btn = PickableActivation.Primary
)
{
bool output = false;
if (evt.Activation == btn)
{
clicked++;
if (clicked == 1) clicktime = Time.time;
if (clicked > 1 && Time.time - clicktime < clickdelay)
{
clicked = 0;
clicktime = 0;
output = true;
}
else if (clicked > 2 || Time.time - clicktime > 1) clicked = 0;
}
return output;
}
}

View File

@@ -0,0 +1,83 @@
using HarmonyLib;
using Model;
using Railloader;
using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using System.Linq;
using UI;
using UI.ContextMenu;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(CarPickable))]
[HarmonyPatch(nameof(CarPickable.HandleShowContextMenu), typeof(Car))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarPickable_HandleShowContextMenu_Patch
{
private static bool Prefix(Car car)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return true;
bool buttonsHaveCost = tweaksAndThings.EndGearHelpersRequirePayment();
TrainController trainController = TrainController.Shared;
ContextMenu shared = ContextMenu.Shared;
if (ContextMenu.IsShown)
{
shared.Hide();
}
shared.Clear();
shared.AddButton(ContextMenuQuadrant.General, (trainController.SelectedCar == car) ? "Deselect" : "Select", SpriteName.Select, delegate
{
trainController.SelectedCar = ((trainController.SelectedCar == car) ? null : car);
});
if (GameInput.IsShiftDown)
{
if (!car.EnumerateCoupled().Any(c => !c.SupportsBleed()))
{
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);
});
if (car.EnumerateCoupled().Any(c => c.EndAirSystemIssue()))
{
shared.AddButton(ContextMenuQuadrant.Unused2, $"Air Up Consist", SpriteName.Select, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost);
});
}
}
else {
if (car.SupportsBleed())
{
shared.AddButton(ContextMenuQuadrant.Brakes, "Bleed", SpriteName.Bleed, car.SetBleed);
}
shared.AddButton(ContextMenuQuadrant.Brakes, car.air.handbrakeApplied ? "Release Handbrake" : "Apply Handbrake", SpriteName.Handbrake, delegate
{
bool apply = !car.air.handbrakeApplied;
car.SetHandbrake(apply);
});
}
shared.AddButton(ContextMenuQuadrant.General, $"Follow", SpriteName.Inspect, delegate
{
CameraSelector.shared.FollowCar(car);
});
shared.Show(car.DisplayName);
shared.BuildItemAngles();
shared.StartCoroutine(shared.AnimateButtonsShown());
return false;
}
}

View File

@@ -0,0 +1,128 @@
using HarmonyLib;
using Helpers;
using Railloader;
using Serilog;
using System;
using System.Collections.Generic;
using UI;
using UI.ContextMenu;
using UnityEngine;
using UnityEngine.UI;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(UI.ContextMenu.ContextMenu))]
[HarmonyPatch(nameof(UI.ContextMenu.ContextMenu.Show), typeof(string))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class ContextMenu_Show_Patch
{
static bool Prefix(UI.ContextMenu.ContextMenu __instance, string centerText)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return true;
if (!__instance.GetRootCanvas(out var rootCanvas))
{
Log.Warning("Couldn't get root canvas");
return true;
}
__instance.CancelHideCoroutine();
if (__instance.contentRectTransform.childCount >= 1) __instance.contentRectTransform.GetChild(0).DestroyAllChildren(); //YOINK DEM CIRCLES!
__instance.SetupTemplate(rootCanvas);
__instance.centerLabel.text = centerText;
Canvas componentInParent = ((Component)__instance.contentRectTransform).GetComponentInParent<Canvas>();
Vector3 mousePosition = Input.mousePosition;
Vector2 val = componentInParent.ScreenToCanvasPosition(mousePosition).XY();
Vector2 renderingDisplaySize = rootCanvas.renderingDisplaySize;
float num = __instance.radius + 50f;
if (val.x < num)
{
val.x = num;
}
if (val.x > renderingDisplaySize.x - num)
{
val.x = renderingDisplaySize.x - num;
}
if (val.y < num)
{
val.y = num;
}
if (val.y > renderingDisplaySize.y - num)
{
val.y = renderingDisplaySize.y - num;
}
__instance.contentRectTransform.anchoredPosition = val;
__instance.BuildItemAngles();
((MonoBehaviour)__instance).StartCoroutine(__instance.AnimateButtonsShown());
((Component)__instance.contentRectTransform).gameObject.SetActive(true);
UI.ContextMenu.ContextMenu.IsShown = true;
__instance._blocker = __instance.CreateBlocker(rootCanvas);
GameInput.RegisterEscapeHandler(GameInput.EscapeHandler.Transient, delegate
{
__instance.Hide();
return true;
});
return false;
}
}
[HarmonyPatch(typeof(UI.ContextMenu.ContextMenu))]
[HarmonyPatch(nameof(UI.ContextMenu.ContextMenu.DefaultAngleForItem), typeof(ContextMenuQuadrant), typeof(int), typeof(int))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class ContextMenu_DefaultAngleForItem_Patch
{
static bool Prefix(UI.ContextMenu.ContextMenu __instance, ref float __result, ContextMenuQuadrant quadrant, int index, int quadrantItemCount)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return true;
int num = quadrant switch
{
ContextMenuQuadrant.General => 0,
ContextMenuQuadrant.Unused1 => 90,
ContextMenuQuadrant.Brakes => 180,
ContextMenuQuadrant.Unused2 => -90,
_ => throw new ArgumentOutOfRangeException("quadrant", quadrant, null),
};
if (quadrantItemCount <= 1)
{
__result = num;
return false;
}
int num2 = ((quadrantItemCount <= 3) ? 30 : (90 / (quadrantItemCount - 1)));
__result = (float)num + -0.5f * (float)((quadrantItemCount - 1) * num2) + (float)(num2 * index);
return false;
}
}
[HarmonyPatch(typeof(UI.ContextMenu.ContextMenu))]
[HarmonyPatch(nameof(UI.ContextMenu.ContextMenu.AddButton), typeof(ContextMenuQuadrant), typeof(string), typeof(Sprite), typeof(Action))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class ContextMenu_AddButton_Patch
{
static bool Prefix(UI.ContextMenu.ContextMenu __instance, ContextMenuQuadrant quadrant, string title, Sprite sprite, Action action)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return true;
List<ContextMenuItem> list = __instance._quadrants[(int)quadrant];
int index = list.Count;
ContextMenuItem contextMenuItem = UnityEngine.Object.Instantiate<ContextMenuItem>(__instance.itemPrefab, (Transform)(object)__instance.contentRectTransform);
contextMenuItem.image.sprite = sprite;
contextMenuItem.label.text = title;
contextMenuItem.OnClick = delegate
{
action();
__instance.Hide((quadrant, index));
};
((Component)contextMenuItem).gameObject.AddComponent<LayoutElement>().preferredHeight = 30f;
list.Add(contextMenuItem);
return false;
}
}

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

@@ -1,7 +1,7 @@
using HarmonyLib; using HarmonyLib;
using Model; using Model;
using Model.Definition.Data; using Model.Definition.Data;
using Model.OpsNew; using Model.Ops;
using Railloader; using Railloader;
using RMROC451.TweaksAndThings.Extensions; using RMROC451.TweaksAndThings.Extensions;
using Serilog; using Serilog;
@@ -14,6 +14,7 @@ using UI;
using UI.EngineRoster; using UI.EngineRoster;
using UI.Tooltips; using UI.Tooltips;
using UnityEngine; using UnityEngine;
using Game.State;
namespace RMROC451.TweaksAndThings.Patches; namespace RMROC451.TweaksAndThings.Patches;
@@ -23,44 +24,97 @@ 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;
RosterFuelColumnSettings? rosterFuelColumnSettings = tweaksAndThings?.settings?.EngineRosterFuelColumnSettings; RosterFuelColumnSettings? rosterFuelColumnSettings = tweaksAndThings?.settings?.EngineRosterFuelColumnSettings;
string fuelInfoText = string.Empty;
string fuelInfoTooltip = string.Empty;
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
{ {
string fuelInfoText = string.Empty;
string fuelInfoTooltip = string.Empty;
Car engineOrTender = __instance._engine; Car engineOrTender = __instance._engine;
List<LoadSlot> loadSlots = __instance._engine.Definition.LoadSlots; 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)engineOrTender.FindMyCaboose(0.0f, false);
float offendingPercentage = 100f;
foreach (Car loco in locos)
{
var investigate = loco;
List<LoadSlot> loadSlots = investigate.Definition.LoadSlots;
if (!loadSlots.Any()) if (!loadSlots.Any())
{ {
engineOrTender = __instance._engine.DetermineFuelCar()!; investigate = investigate.DetermineFuelCar()!;
loadSlots = engineOrTender != null ? engineOrTender.Definition.LoadSlots : Enumerable.Empty<LoadSlot>().ToList(); loadSlots = investigate != null ? investigate.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; 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++) for (int i = 0; i < loadSlots.Count; i++)
{ {
CarLoadInfo? loadInfo = engineOrTender.GetLoadInfo(i); CarLoadInfo? loadInfo = investigate.GetLoadInfo(i);
if (loadInfo.HasValue) if (loadInfo.HasValue)
{ {
CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault(); CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault();
var fuelLevel = FuelLevel(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity); var fuelLevel = FuelLevel(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity);
fuelInfoText += loadSlots[i].RequiredLoadIdentifier == offender ? fuelLevel + " " : string.Empty; var offenderCheck = CalcPercentLoad(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity);
//fuelInfoText += TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity) + " ";
fuelInfoTooltip += $"{TextSprites.PiePercent(valueOrDefault.Quantity, loadSlots[i].MaximumCapacity)} {valueOrDefault.LoadString(CarPrototypeLibrary.instance.LoadForId(valueOrDefault.LoadId))}\n"; 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 && consist.Any())
{
float lowestOilLevel = consist.OrderBy(c => c.Oiled).FirstOrDefault().Oiled;
var oilLevel = FuelLevel(lowestOilLevel, 1);
fuelInfoTooltip += $"{lowestOilLevel.TriColorPiePercent(1)} {oilLevel} Consist Oil Lowest Level\n";
if (CalcPercentLoad(lowestOilLevel, 1) < offendingPercentage)
{
fuelInfoText = $"{oilLevel} ";
}
if (consist.Any(c => c.HasHotbox))
{
fuelInfoText = $"{TextSprites.Hotbox} ";
fuelInfoTooltip = $"{TextSprites.Hotbox} Hotbox detected!\n{fuelInfoTooltip}";
}
}
else if (!cabooseRequirementFulfilled && StateManager.Shared.Storage.OilFeature)
{
fuelInfoTooltip += $"Add Caboose To Consist For Consist Oil Level Reporting\n";
}
}
catch (Exception ex)
{
Log.Error(ex, "Error Detecting oiling consist status for engine roster");
} }
switch (rosterFuelColumnSettings.EngineRosterFuelStatusColumn) switch (rosterFuelColumnSettings.EngineRosterFuelStatusColumn)
@@ -77,7 +131,6 @@ internal class EngineRosterRow_Refresh_Patch
default: default:
break; break;
} }
} catch (Exception ex) } catch (Exception ex)
{ {
rosterFuelColumnSettings.EngineRosterFuelStatusColumn = EngineRosterFuelDisplayColumn.None; rosterFuelColumnSettings.EngineRosterFuelStatusColumn = EngineRosterFuelDisplayColumn.None;
@@ -91,10 +144,15 @@ internal class EngineRosterRow_Refresh_Patch
tooltip.TooltipInfo = new TooltipInfo(tooltip.tooltipTitle, fuelInfoTooltip); tooltip.TooltipInfo = new TooltipInfo(tooltip.tooltipTitle, fuelInfoTooltip);
} }
public static string FuelLevel(float quantity, float capacity) public static float CalcPercentLoad(float quantity, float capacity)
{ {
float num = capacity <= 0f ? 0 : Mathf.Clamp(quantity / capacity * 100, 0, 100); float num = capacity <= 0f ? 0 : Mathf.Clamp(quantity / capacity * 100, 0, 100);
return $"{Mathf.FloorToInt(num):D2}%"; return num;
}
public static string FuelLevel(float quantity, float capacity)
{
return $"{Mathf.FloorToInt(CalcPercentLoad(quantity, capacity)):D2}%";
} }
} }

View File

@@ -0,0 +1,15 @@
using HarmonyLib;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(EntityReference))]
[HarmonyPatch(nameof(EntityReference.Text))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class EntityReference_Text_Patch
{
static void Postfix(ref string __result)
{
if (__result == "Unknown")
__result = string.Empty;
}
}

View File

@@ -25,13 +25,15 @@ internal class ExpandedConsole_Add_Patch
{ {
entry.Text = $"{entry.Timestamp} : {entry.Text}"; entry.Text = $"{entry.Timestamp} : {entry.Text}";
entry.Timestamp = RealNow(); entry.Timestamp = RealNow();
SendMs(ref entry); SendMs((UI.Console.Console.Entry?)entry);
} }
static void SendMs(ref UI.Console.Console.Entry entry) internal static void SendMs(UI.Console.Console.Entry? entry, string? text = null)
{ {
try try
{ {
if (entry is null && !String.IsNullOrEmpty(text)) entry = new() { Text = text };
var msgText = entry?.Text ?? string.Empty;
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared; TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
StateManager shared = StateManager.Shared; StateManager shared = StateManager.Shared;
GameStorage gameStorage = shared.Storage; GameStorage gameStorage = shared.Storage;
@@ -46,7 +48,7 @@ internal class ExpandedConsole_Add_Patch
{ {
var t = new Regex("car:(.*)\""); var t = new Regex("car:(.*)\"");
var carId = t.IsMatch(entry.Text) ? Regex.Match(entry.Text, "car:(.*?)\"").Groups[1].Captures[0].ToString() : string.Empty; var carId = t.IsMatch(msgText) ? Regex.Match(msgText, "car:(.*?)\"").Groups[1].Captures[0].ToString() : string.Empty;
Model.Car? car = TrainController.Shared.CarForString(carId); Model.Car? car = TrainController.Shared.CarForString(carId);
bool engineInMessage = car?.IsLocomotive ?? false; bool engineInMessage = car?.IsLocomotive ?? false;
var image = engineInMessage ? var image = engineInMessage ?
@@ -81,7 +83,7 @@ internal class ExpandedConsole_Add_Patch
{ {
new new
{ {
description= Regex.Replace(entry.Text, "<.*?>", "").Replace(": ", "\n"), description= Regex.Replace(msgText, "<.*?>", "").Replace(": ", "\n"),
timestamp=DateTime.UtcNow, timestamp=DateTime.UtcNow,
image image
}, },

View File

@@ -1,139 +0,0 @@
using Game.State;
using HarmonyLib;
using Model;
using Model.Definition.Data;
using Model.Ops.Definition;
using Model.OpsNew;
using Railloader;
using RMROC451.TweaksAndThings.Extensions;
using Serilog;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(CarExtensions))]
[HarmonyPatch(nameof(CarExtensions.LoadString), typeof(CarLoadInfo), typeof(Load))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarExtensions_LoadString_Patch
{
public static bool Prefix(CarLoadInfo info, Load load, ref string __result)
{
bool output = load.id == IndustryComponent_Service_Patch.CrewHoursLoad().id;
if (output) __result = info.Quantity.FormatCrewHours(load.description);
return !output;
}
}
[HarmonyPatch(typeof(CarPrototypeLibrary))]
[HarmonyPatch(nameof(CarPrototypeLibrary.LoadForId), typeof(string))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarPrototypeLibrary_LoadForId_Patch
{
public static bool Prefix(string loadId, ref Load __result)
{
Load load = IndustryComponent_Service_Patch.CrewHoursLoad();
if (loadId == load.id) __result = load;
return __result == null;
}
}
[HarmonyPatch(typeof(TeamTrack))]
[HarmonyPatch(nameof(TeamTrack.Service), typeof(IIndustryContext))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class TeamTrack_Service_Patch
{
public static bool Prefix(IndustryComponent __instance, IIndustryContext ctx)
{
//Log.Information($"{nameof(SimplePassengerStop_Service_Patch)} => {((IndustryContext)ctx)._industry.name}");
return IndustryComponent_Service_Patch.Prefix(__instance, ctx);
}
}
[HarmonyPatch(typeof(RepairTrack))]
[HarmonyPatch(nameof(RepairTrack.Service), typeof(IIndustryContext))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class RepairTrack_Service_Patch
{
public static bool Prefix(IndustryComponent __instance, IIndustryContext ctx)
{
//Log.Information($"{nameof(SimplePassengerStop_Service_Patch)} => {((IndustryContext)ctx)._industry.name}");
return IndustryComponent_Service_Patch.Prefix(__instance, ctx);
}
}
[HarmonyPatch(typeof(SimplePassengerStop))]
[HarmonyPatch(nameof(SimplePassengerStop.Service), typeof(IIndustryContext))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class SimplePassengerStop_Service_Patch
{
public static bool Prefix(IndustryComponent __instance, IIndustryContext ctx)
{
Log.Information($"{nameof(SimplePassengerStop_Service_Patch)} => {((IndustryContext)ctx)._industry.name}");
return IndustryComponent_Service_Patch.Prefix(__instance, ctx);
}
}
internal static class IndustryComponent_Service_Patch
{
public static Load CrewHoursLoad()
{
Load load = (Load)ScriptableObject.CreateInstance(typeof(Load));
load.name = "crew-hours";
load.description = "Crew";
load.units = LoadUnits.Quantity;
return load;
}
public static bool Prefix(IndustryComponent __instance, IIndustryContext ctx)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!StateManager.IsHost || !tweaksAndThings.IsEnabled || !(tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false)) return true;
Load load = CrewHoursLoad();
float rate2 = 24 * 2 * 8;// carLoadRate = 8 crew hours in 30 min loading; (24h * 2 to get half hour chunks * 8 hours to load in those chunks)
float num2 = 99999999; //QuantityInStorage for crew-hours (infinite where crew can be shuffling about)
float quantityToLoad = Mathf.Min(num2, IndustryComponent.RateToValue(rate2, ctx.DeltaTime));
var carsAtPosition = ctx.CarsAtPosition();
var cabeese = from car in carsAtPosition.Where(c => c.CarType == "NE")
where car.IsEmptyOrContains(load)
orderby car.QuantityOfLoad(load).quantity descending
select car;
foreach (IOpsCar item in cabeese)
{
TrainController tc = UnityEngine.Object.FindAnyObjectByType<TrainController>();
if (tc.TryGetCarForId(item.Id, out Car car))
{
List<LoadSlot> loadSlots = car.Definition.LoadSlots;
float quantity = 0f;
float max = 0f;
for (int i = 0; i < loadSlots.Count; i++)
{
LoadSlot loadSlot = loadSlots[i];
if (loadSlot.LoadRequirementsMatch(load) && loadSlot.LoadUnits == load.units)
{
CarLoadInfo? loadInfo = car.GetLoadInfo(i);
quantity = loadInfo.HasValue ? loadInfo.Value.Quantity : 0f;
max = loadSlots[i].MaximumCapacity;
break;
}
}
//Log.Information($"{nameof(IndustryComponent_Service_Patch)} {car} => {car.StoppedDuration} => {quantityToLoad} => {quantity}/{max}");
if (car.StoppedDuration > 30) item.Load(load, quantityToLoad);
}
//todo:crew refresh message?
}
return true;
}
}

View File

@@ -0,0 +1,104 @@
using Core;
using Game;
using Game.State;
using HarmonyLib;
using Model.Ops;
using Network;
using Railloader;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(InterchangedIndustryLoader))]
[HarmonyPatch(nameof(InterchangedIndustryLoader.ServeInterchange), typeof(IIndustryContext), typeof(Interchange))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class InterchangedIndustryLoader_ServiceInterchange_Patch
{
private static Serilog.ILogger _log => Log.ForContext<InterchangedIndustryLoader_ServiceInterchange_Patch>();
/// <summary>
/// Allow Interchange service for buying parts/fuel when negative balance, adding an additional 20% penalty
/// </summary>
/// <param name="__instance"></param>
/// <param name="ctx"></param>
/// <param name="interchange"></param>
/// <returns></returns>
public static bool Prefix(InterchangedIndustryLoader __instance, IIndustryContext ctx, Interchange interchange)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!StateManager.IsHost || !tweaksAndThings.IsEnabled() || !tweaksAndThings.ServiceFundPenalties()) return true;
StateManager shared = StateManager.Shared;
List<IOpsCar> list = (from car in EnumerateCars(__instance, ctx, requireWaybill: true)
where car.IsEmptyOrContains(__instance.load)
select car).ToList();
int num2 = 0;
int num3 = 0;
int penalty = 0;
GameDateTime returnTime = ctx.Now.AddingDays(23f / 24f);
foreach (IOpsCar item3 in list)
{
(float quantity, float capacity) tuple = item3.QuantityOfLoad(__instance.load);
float item = tuple.quantity;
float item2 = tuple.capacity;
int num4 = Mathf.RoundToInt((item2 - item) * __instance.load.costPerUnit);
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);
num2 += num4;
num3++;
}
item3.Load(__instance.load, item2);
item3.SetWaybill(null, __instance, "Full");
ctx.MoveToBardo(item3);
__instance.ScheduleReturnFromBardo(item3, returnTime);
}
if (num2 > 0)
{
__instance.Industry.ApplyToBalance(-num2, __instance.ledgerCategory, null, num3, quiet: true);
string penaltyText = penalty > 0 ? $"; Overdraft fee of {penalty:C0}" : string.Empty;
Multiplayer.Broadcast(string.Format("{6}: Ordered {0} of {1} at {2} for {3:C0}{5}. Expected return: {4}.", num3.Pluralize("car"), __instance.load.description, __instance.HyperlinkToThis, num2, 1.Pluralize("day"), penaltyText, __instance.HyperlinkToThis));
if (penalty > 0)
StateManager.Shared.ApplyToBalance(-penalty, __instance.ledgerCategory, null, $"Overdraft: {__instance.Industry.name}", 0, quiet: true);
}
return false;
}
private static IEnumerable<IOpsCar> EnumerateCars(InterchangedIndustryLoader __instance, IIndustryContext ctx, bool requireWaybill = false)
{
foreach (IOpsCar item in ctx.CarsAtPosition())
{
if (!CarTypeMatches(__instance.Interchange, item)) continue;
if (requireWaybill)
{
try
{
Waybill? waybill = item.Waybill;
string destId = waybill?.Destination.Identifier ?? string.Empty;
if (!waybill.HasValue || !destId.Equals(__instance.Identifier)) continue;
} catch (Exception ex)
{
_log.Error(ex, $"{item.DisplayName} issue detecting waybill");
continue;
}
}
yield return item;
}
}
private static bool CarTypeMatches(Interchange interchange, IOpsCar car)
{
string carType = car.CarType;
return interchange.carTypeFilter.Matches(carType);
}
}

View File

@@ -0,0 +1,183 @@
using HarmonyLib;
using Helpers;
using Map.Runtime;
using Model;
using Model.AI;
using Railloader;
using Serilog;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Track;
using UI;
using UI.EngineControls;
using UI.Map;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;
using static UI.AutoEngineerDestinationPicker;
using Vector2 = UnityEngine.Vector2;
using Vector3 = UnityEngine.Vector3;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(MapWindow))]
[HarmonyPatch(nameof(MapWindow.OnClick), typeof(Vector2))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class MapWindow_OnClick_Patch
{
private static Serilog.ILogger _log => Log.ForContext<MapWindow_OnClick_Patch>();
private static GameObject _prefabHolder;
private static MapIcon _waypointPrefab;
private static Dictionary<string, MapIcon> wps = new Dictionary<string, MapIcon>();
public static Sprite? LoadTexture(string fileName, string name)
{
string path = Path.Combine(SingletonPluginBase<TweaksAndThingsPlugin>.Shared.ModDirectory, fileName);
Texture2D texture2D = new Texture2D(128, 128, TextureFormat.DXT5, mipChain: false);
texture2D.name = name;
texture2D.wrapMode = TextureWrapMode.Clamp;
if (!ImageConversion.LoadImage(texture2D, File.ReadAllBytes(path)))
{
_log.Information($"Unable to load {name} icon!");
return null;
}
Sprite sprite = Sprite.Create(texture2D, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f));
sprite.name = name;
//if (!SpriteLibrary.Shared.entries.Any(s => s.name.ToString() == name)) SpriteLibrary.Shared.entries.Add(new() { name = SpriteName.Meh, sprite = sprite });
return sprite;
}
public static MapIcon? waypointPrefab
{
get
{
if (_waypointPrefab == null)
{
CreateWaypointPrefab();
}
return _waypointPrefab;
}
}
internal static GameObject prefabHolder
{
get
{
if (_prefabHolder == null)
{
_prefabHolder = new GameObject("Prefab Holder");
_prefabHolder.hideFlags = HideFlags.HideAndDontSave;
_prefabHolder.SetActive(value: false);
}
return _prefabHolder;
}
}
private static void CreateWaypointPrefab()
{
float num = 0.6f; //come back
Sprite sprite = LoadTexture("Map_pin_icon.png", "MapPin");
_waypointPrefab = Object.Instantiate(TrainController.Shared.locomotiveMapIconPrefab, prefabHolder.transform);
GameObject obj = _waypointPrefab.gameObject;
obj.hideFlags = HideFlags.HideAndDontSave;
obj.name = "Map Waypoint Icon";
if ((bool)_waypointPrefab.Text)
{
Object.DestroyImmediate(_waypointPrefab.Text.gameObject);
}
Image componentInChildren = _waypointPrefab.GetComponentInChildren<Image>();
componentInChildren.sprite = sprite;
componentInChildren.transform.localScale = Vector3.one * num;
}
static bool Prefix(MapWindow __instance, Vector2 viewportNormalizedPoint)
{
if (GameInput.IsControlDown && GameInput.IsAltDown)
{
Ray ray = RayForViewportNormalizedPoint(__instance, viewportNormalizedPoint);
Vector3 gamePoint = MapManager.Instance.FindTerrainPointForXZ(WorldTransformer.WorldToGame(ray.origin));
var selectedLoco = TrainController.Shared?.SelectedLocomotive;
if (selectedLoco != null)
{
Hit? valueOrDefault = null;
Camera _camera = null;
if (MainCameraHelper.TryGetIfNeeded(ref _camera))
{
float rad = 200f;
if (Graph.Shared.TryGetLocationFromGamePoint(gamePoint, rad, out Location location))
{
Hit? hit = HitLocation(location, selectedLoco);
if (hit.HasValue)
{
valueOrDefault = hit.GetValueOrDefault();
location = valueOrDefault.Value.Location;
}
var aeoh = new AutoEngineerOrdersHelper(persistence: new AutoEngineerPersistence(selectedLoco.KeyValueObject), locomotive: selectedLoco);
var mw = (location: (Location)location, carId: valueOrDefault?.CarInfo?.car?.id ?? string.Empty);
aeoh.SetWaypoint(mw.location, mw.carId);
aeoh.SetOrdersValue(maybeWaypoint: mw);
AutoEngineerDestinationPicker.Shared.Cancel();
}
}
}
return false;
}
return true;
}
public static Ray RayForViewportNormalizedPoint(MapWindow i, Vector2 v2) =>
i.mapBuilder.mapCamera.ViewportPointToRay(new Vector3(v2.x, v2.y, 0f));
private static Hit? HitLocation(Location? location, BaseLocomotive selectedLoco)
{
var _graph = TrainController.Shared.graph;
if (location.HasValue)
{
Location valueOrDefault = location.GetValueOrDefault();
TrainController shared = TrainController.Shared;
Vector3 position = _graph.GetPosition(valueOrDefault);
float num = 2f;
Hit? result = null;
HashSet<Car> value;
using (CollectionPool<HashSet<Car>, Car>.Get(out value))
{
shared.CheckForCarsAtPoint(position, 2f, value, valueOrDefault);
foreach (Car item in value)
{
if (selectedLoco.EnumerateCoupled().ToHashSet().Contains(item))
{
continue;
}
if (!item[item.EndToLogical(Car.End.F)].IsCoupled)
{
Location location2 = _graph.LocationByMoving(item.LocationF, 0.5f, checkSwitchAgainstMovement: false, stopAtEndOfTrack: true);
float distanceBetweenClose = _graph.GetDistanceBetweenClose(valueOrDefault, location2);
if (distanceBetweenClose < num)
{
num = distanceBetweenClose;
result = new Hit(location2, (item, Car.End.F));
}
}
if (!item[item.EndToLogical(Car.End.R)].IsCoupled)
{
Location location3 = _graph.LocationByMoving(item.LocationR, -0.5f, checkSwitchAgainstMovement: false, stopAtEndOfTrack: true).Flipped();
float distanceBetweenClose2 = _graph.GetDistanceBetweenClose(valueOrDefault, location3);
if (distanceBetweenClose2 < num)
{
num = distanceBetweenClose2;
result = new Hit(location3, (item, Car.End.R));
}
}
}
if (value.Count > 0)
{
return result;
}
}
return new Hit(valueOrDefault, null);
}
return null;
}
}

View File

@@ -0,0 +1,34 @@
using Game.Notices;
using HarmonyLib;
using Model;
using Serilog;
using System;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(NoticeExtensions))]
[HarmonyPatch(nameof(NoticeExtensions.PostNotice), typeof(Car), typeof(string), typeof(string))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class NoticeExtensions_PostNotice_Patch
{
private static ILogger _log => Log.ForContext<NoticeExtensions_PostNotice_Patch>();
static void Postfix(Car car, string key, string content)
{
try
{
//Log.Information($"{car.DisplayName} patch PostNotice");
if (!string.IsNullOrEmpty(content) &&
key.Equals("ai-wpt") &&
content.ToLower().Contains("Arrived at Waypoint".ToLower())
)
{
car.PostNotice("ai-wpt-rmroc451", null);
}
} catch (Exception ex)
{
_log.ForContext("car", car).Error(ex, "woops");
}
}
}

View File

@@ -0,0 +1,138 @@
using Game;
using Game.State;
using HarmonyLib;
using Model;
using Model.Definition.Data;
using Model.Ops.Definition;
using Model.Ops;
using Network;
using Railloader;
using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(CarExtensions))]
[HarmonyPatch(nameof(CarExtensions.LoadString), typeof(CarLoadInfo), typeof(Load))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarExtensions_LoadString_Patch
{
public static bool Prefix(CarLoadInfo info, Load load, ref string __result)
{
bool output = load.id == OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad().id;
if (output) __result = info.Quantity.FormatCrewHours(load.description);
return !output;
}
}
[HarmonyPatch(typeof(CarPrototypeLibrary))]
[HarmonyPatch(nameof(CarPrototypeLibrary.LoadForId), typeof(string))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarPrototypeLibrary_LoadForId_Patch
{
public static bool Prefix(string loadId, ref Load __result)
{
Load load = OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad();
if (loadId == load.id) __result = load;
return __result == null;
}
}
[HarmonyPatch(typeof(OpsController))]
[HarmonyPatch(nameof(OpsController.AnnounceCoalescedPayments))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class OpsController_AnnounceCoalescedPayments_Patch
{
static Dictionary<string, (bool spotted, bool filling)> CrewCarDict = new();
public static (bool spotted, bool filling) CrewCarStatus(Car car)
{
bool found = CrewCarDict.TryGetValue(car.id, out (bool spotted, bool filling) val);
if (!found) CrewCarDict.Add(car.id, (false, false));
return val;
}
static GameDateTime dateTime = TimeWeather.Now;
static readonly IEnumerable<Type> refillLocations =
new List<Type>() {
typeof(PassengerStop),
typeof(SimplePassengerStop),
typeof(TeamTrack),
typeof(RepairTrack)
};
public static Load CrewHoursLoad()
{
Load load = (Load)ScriptableObject.CreateInstance(typeof(Load));
load.name = "crew-hours";
load.description = "Crew";
load.units = LoadUnits.Quantity;
return load;
}
private static void CarLoadCrewHelper(Car car, float deltaTime)
{
float rate2 = 24 * 2 * 8;// carLoadRate = 8 crew hours in 30 min loading; (24h * 2 to get half hour chunks * 8 hours to load in those chunks)
float num2 = 99999999; //QuantityInStorage for crew-hours (infinite where crew can be shuffling about)
float quantityToLoad = Mathf.Min(num2, IndustryComponent.RateToValue(rate2, deltaTime));
if (car.IsCaboose() && !CrewCarStatus(car).spotted)
{
CrewCarDict[car.id] = (true, CrewCarDict[car.id].filling);
}
if (car.IsCabooseAndStoppedForLoadRefresh(new OpsCarAdapter(car, OpsController.Shared).IsFull(CrewHoursLoad())))
{
if (!CrewCarDict[car.id].filling) Multiplayer.Broadcast($"{Hyperlink.To(car)}: \"Topping off caboose crew.\"");
CrewCarDict[car.id] = (CrewCarDict[car.id].spotted, true);
var data = car.QuantityCapacityOfLoad(CrewHoursLoad());
if ((data.quantity + quantityToLoad > data.capacity) && data.quantity < data.capacity)
{
Multiplayer.Broadcast($"{Hyperlink.To(car)}: \"Caboose crew topped off.\"");
CrewCarDict[car.id] = (CrewCarDict[car.id].spotted, false);
}
new OpsCarAdapter(car, OpsController.Shared).Load(CrewHoursLoad(), quantityToLoad);
}
}
public static bool Prefix(IndustryComponent __instance)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!StateManager.IsHost || !tweaksAndThings.IsEnabled() || !tweaksAndThings.EndGearHelpersRequirePayment() || tweaksAndThings.DayLoadCrewHours()) return true;
TrainController tc = UnityEngine.Object.FindAnyObjectByType<TrainController>();
try {
var passengerStops = OpsController.Shared.AllIndustries
.SelectMany(i => i.TrackDisplayables.Where(t => refillLocations.Contains(t.GetType())));
//Log.Information($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper => PassengerStops => {string.Join(",", passengerStops)}");
var cabeese = passengerStops
.SelectMany(t => t.TrackSpans?.Select(s => (tc.CarsOnSpan(s) ?? Enumerable.Empty<Car>()).Where(c => c.IsCaboose()))?.SelectMany(c => c?.Select(c2 => (t, c2))));
//Log.Information($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper => PassengerStops Cabeese => {string.Join(",", cabeese?.Select(c => $"{c.t} : {c.c2}") ?? [])}");
CrewCarDict = CrewCarDict.Where(kvp => cabeese.Select(c => c.c2.id).Contains(kvp.Key)).ToDictionary(k => k.Key, v => v.Value);
var deltaTime = (float)(TimeWeather.Now.TotalSeconds - dateTime.TotalSeconds);
foreach (var caboose in cabeese)
{
//Log.Information($"{nameof(OpsController_AnnounceCoalescedPayments_Patch)} => Caboose Helper ({deltaTime}) => {caboose.t} : {caboose.c2}");
CarLoadCrewHelper(caboose.c2, deltaTime);
}
dateTime = TimeWeather.Now;
} catch (System.Exception ex)
{
Log.Error(ex, "error with announce caboose helper");
}
return true;
}
}

View File

@@ -0,0 +1,169 @@
using Game;
using Game.State;
using HarmonyLib;
using Model;
using Model.Ops;
using Network;
using Railloader;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UI.Builder;
using UnityEngine;
using static Model.Ops.RepairTrack;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(RepairTrack))]
[HarmonyPatch(nameof(RepairTrack.DailyPayables), typeof(GameDateTime), typeof(IIndustryContext))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class RepairTrack_DailyPayables_Patch
{
private static Serilog.ILogger _log => Log.ForContext<RepairTrack_DailyPayables_Patch>();
private static Serilog.ILogger ContextualLogger(RepairTrack __instance) =>
_log.ForContext("RepairTrackIdentifier", __instance?.Identifier ?? "???")
.ForContext("RepairTrackName", __instance?.name ?? "???")
.ForContext("RepairTrackDisplayName", __instance?.DisplayName ?? "???");
public static bool Prefix(RepairTrack __instance, GameDateTime now, IIndustryContext ctx)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled() || !tweaksAndThings.ServiceFundPenalties()) return true;
RepairRateState rateState = __instance.RateState;
StateManager shared = StateManager.Shared;
if (rateState.PayDue < 1E-06f)
{
rateState.PayDue = 0f;
rateState.PaidCurrent = true;
}
else
{
int num = Mathf.CeilToInt(rateState.PayDue);
rateState.PaidCurrent = shared.CanAfford(num);
int penalty = Mathf.CeilToInt(!rateState.PaidCurrent ? num * 0.2f : 0);
Multiplayer.Broadcast($"{Hyperlink.To(__instance.Industry)}: Paid {num:C0} wages for shop crew${(penalty > 0 ? $"; Overdraft fee of {penalty:C0}" : ".")}");
__instance.Industry.ApplyToBalance(-num, Ledger.Category.WagesRepair, null, 0, quiet: true);
if (penalty > 0)
StateManager.Shared.ApplyToBalance(-penalty, Ledger.Category.WagesRepair, null, $"Overdraft: {__instance.Industry.name}", 0, quiet: true);
rateState.PayDue = 0f;
rateState.PaidCurrent = true;
}
__instance.RateState = rateState;
return false;
}
}
[HarmonyPatch(typeof(RepairTrack))]
[HarmonyPatch(nameof(RepairTrack.EnumerateCarsActual), typeof(IIndustryContext))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class RepairTrack_NeedsRepair_Patch
{
private static Serilog.ILogger _log => Log.ForContext<RepairTrack_NeedsRepair_Patch>();
public static void Postfix(RepairTrack __instance, ref IEnumerable<Car> __result)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return;
__result = EnumerateCarsActualPatched(__result);
}
internal static IEnumerable<Car> EnumerateCarsActualPatched(IEnumerable<Car> __result, bool usePatchedVersion = true) =>
usePatchedVersion ? __result.Where(NeedsRepairPatched) : __result;
internal static bool NeedsRepairPatched(Car car)
{
bool result = TryGetRepairDestination(car, out var overrideTag);
return result;
}
}
[HarmonyPatch(typeof(RepairTrack))]
[HarmonyPatch(nameof(RepairTrack.BuildCars), typeof(UIPanelBuilder))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class RepairTrack_BuildCars_Patch
{
public static bool Prefix(RepairTrack __instance, UIPanelBuilder builder)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return true;
BuildCarsPatched(__instance, builder);
return false;
}
public static IEnumerable<Car> EnumerateCarsActualOrig(RepairTrack __instance, IIndustryContext ctx)
{
TrainController trainController = TrainController.Shared;
return from car in __instance.EnumerateCars(ctx)
select trainController.CarForId(car.Id);
}
public static void BuildCarsPatched(RepairTrack __instance, UIPanelBuilder builder)
{
IndustryContext industryContext = __instance.CreateContext(TimeWeather.Now, 0f);
IOrderedEnumerable<IGrouping<RepairGroup, Car>> carGroups = from @group in EnumerateCarsActualOrig(__instance, industryContext).GroupBy(delegate (Car car)
{
if (InForOverhaul(car))
{
return RepairGroup.Overhaul;
}
return RepairTrack_NeedsRepair_Patch.NeedsRepairPatched(car) ? RepairGroup.NeedsRepair : RepairGroup.None;
})
orderby @group.Key
select @group;
builder.VStack(delegate (UIPanelBuilder uIPanelBuilder)
{
float repairRateMultiplier = __instance.RateState.PayRateMultiplier;
foreach (IGrouping<RepairGroup, Car> item in carGroups)
{
RepairGroup repairGroup = item.Key;
uIPanelBuilder.AddSection(repairGroup switch
{
RepairGroup.Overhaul => "Overhauling",
RepairGroup.NeedsRepair => "Repairing",
RepairGroup.None => "No Work Order Assigned",
_ => throw new ArgumentOutOfRangeException(),
});
foreach (Car car in item.OrderBy((Car car2) => car2.SortName))
{
uIPanelBuilder.HStack(delegate (UIPanelBuilder uIPanelBuilder2)
{
uIPanelBuilder2.AddLabel(Hyperlink.To(car)).Width(130f);
uIPanelBuilder2.AddLabel($"{Mathf.RoundToInt(car.Condition * 100f)}%").Width(60f).HorizontalTextAlignment(HorizontalAlignmentOptions.Right);
string text = "";
string title = "";
if (repairGroup == RepairGroup.Overhaul)
{
float overhaulProgress = car.OverhaulProgress;
if (overhaulProgress > 0f)
{
text = $"{(int)(overhaulProgress * 100f)}%";
title = "Overhaul Progress";
}
}
uIPanelBuilder2.AddLabel(text).Width(60f).HorizontalTextAlignment(HorizontalAlignmentOptions.Right)
.Tooltip(title, null);
if (repairGroup != RepairGroup.None)
{
float num = CalculateRepairWorkOverall(car);
float num2 = num * 12000f * 0.0005f;
string text2 = ((!(repairRateMultiplier > 0f)) ? "Never" : GameDateTimeInterval.DeltaStringMinutes((int)(num / repairRateMultiplier * 60f * 24f), GameDateTimeInterval.Style.Short));
uIPanelBuilder2.AddLabel(text2).Width(80f).HorizontalTextAlignment(HorizontalAlignmentOptions.Right)
.Tooltip("Time Remaining", "D:HH:MM or H:MM");
uIPanelBuilder2.AddLabel($"{num2:F1}T").Width(80f).HorizontalTextAlignment(HorizontalAlignmentOptions.Right)
.Tooltip("Repair Parts Needed", null);
}
});
}
}
}).Padding(new RectOffset(20, 0, 0, 0));
}
}

View File

@@ -2,8 +2,11 @@
using Game.State; using Game.State;
using HarmonyLib; using HarmonyLib;
using KeyValue.Runtime; using KeyValue.Runtime;
using Model.Ops;
using Network; using Network;
using Railloader; using Railloader;
using RMROC451.TweaksAndThings.Extensions;
using System.Linq;
using UnityEngine; using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches; namespace RMROC451.TweaksAndThings.Patches;
@@ -18,8 +21,28 @@ internal class StateManager_OnDayDidChange_Patch
private static void Postfix(StateManager __instance) private static void Postfix(StateManager __instance)
{ {
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared; TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled || !(tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false)) return; if (!tweaksAndThings.IsEnabled()) return;
if (StateManager.IsHost) PayAutoBrakeCrewWages(__instance);
if (StateManager.IsHost) DoNewDayActivites(tweaksAndThings, __instance);
}
private static void DoNewDayActivites(TweaksAndThingsPlugin tweaksAndThings, StateManager __instance)
{
if (tweaksAndThings.EndGearHelpersRequirePayment() && tweaksAndThings.DayLoadCrewHours()) LoadCabeese(__instance);
if (tweaksAndThings.EndGearHelpersRequirePayment()) PayAutoBrakeCrewWages(__instance);
}
private static void LoadCabeese(StateManager __instance)
{
foreach (var car in TrainController.Shared.Cars.Where(Car_Extensions.IsCaboose))
{
var data = car.QuantityCapacityOfLoad(OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad());
if (data.quantity < data.capacity)
{
Multiplayer.Broadcast($"{Hyperlink.To(car)}: \"Caboose crew topped off.\"");
new OpsCarAdapter(car, OpsController.Shared).Load(OpsController_AnnounceCoalescedPayments_Patch.CrewHoursLoad(), data.capacity - data.quantity);
}
}
} }
private static void PayAutoBrakeCrewWages(StateManager __instance) private static void PayAutoBrakeCrewWages(StateManager __instance)

View File

@@ -1,11 +1,13 @@
using HarmonyLib; using Game.State;
using HarmonyLib;
using Model; using Model;
using Model.OpsNew; using Model.Ops;
using Railloader; using Railloader;
using RMROC451.TweaksAndThings.Extensions; using RMROC451.TweaksAndThings.Extensions;
using UI; using System;
using System.Collections.Generic;
using System.Linq;
using UI.Tags; using UI.Tags;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches; namespace RMROC451.TweaksAndThings.Patches;
@@ -19,39 +21,43 @@ internal class TagController_UpdateTag_Patch
private static void Postfix(Car car, TagCallout tagCallout) private static void Postfix(Car car, TagCallout tagCallout)
{ {
TagController tagController = UnityEngine.Object.FindObjectOfType<TagController>();
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared; TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled || !tweaksAndThings.settings.HandBrakeAndAirTagModifiers) if (!tweaksAndThings.IsEnabled() || !tweaksAndThings.settings.HandBrakeAndAirTagModifiers)
{ {
return; return;
} }
ProceedWithPostFix(car, tagCallout, tagController); ProceedWithPostFix(car, tagCallout, tweaksAndThings.RequireConsistCabooseForOilerAndHotboxSpotter() || !tweaksAndThings.CabooseRequiredForLocoOilIndicator());
return; return;
} }
private static void ProceedWithPostFix(Car car, TagCallout tagCallout, TagController tagController) private static void ProceedWithPostFix(Car car, TagCallout tagCallout, bool cabooseRequired)
{ {
bool isAltDownWithCarIssue = GameInput.IsAltDown && car.CarOrEndGearIssue(); tagCallout.callout.Title = string.Format(tagTitleFormat, "{0}", Hyperlink.To(car));
tagCallout.callout.Title = string.Format(tagTitleFormat, "{0}", car.DisplayName); List<string> tags = new();
tagCallout.gameObject.SetActive( string oilSpriteName = string.Empty;// "OilCan";
tagCallout.gameObject.activeSelf &&
(!GameInput.IsAltDown || isAltDownWithCarIssue)
);
if (tagCallout.gameObject.activeSelf && isAltDownWithCarIssue) if (OpsController_AnnounceCoalescedPayments_Patch.CrewCarStatus(car).spotted) tags.Add("+");
{ //if (car.EnableOiling) tags.Add(car.HasHotbox ? TextSprites.Hotbox : $"<cspace=-1em>{TextSprites.Warning}{car.Oiled.TriColorPiePercent(1)}</cspace>");
tagController.ApplyImageColor(tagCallout, Color.black); if (car.EnableOiling) tags.Add(car.HasHotbox ? TextSprites.Hotbox : car.Oiled.TriColorPiePercent(1, oilSpriteName));
} IEnumerable<Car> consist = car.EnumerateCoupled().Where(c => c.EnableOiling);
Func<bool> cabooseRequirementFulfilled = () => (!cabooseRequired || consist.ConsistNoFreight() || car.FindMyCaboose(0.0f, false));
if (StateManager.Shared.Storage.OilFeature
&& car.IsLocomotive
&& !car.NeedsOiling
&& (consist.Any(c => c.NeedsOiling) || consist.Any(c => c.HasHotbox)
&& cabooseRequirementFulfilled())
)
tags.Add(consist.Any(c => c.HasHotbox) ? TextSprites.Hotbox : consist.OrderBy(c => c.Oiled).FirstOrDefault().Oiled.TriColorPiePercent(1, oilSpriteName));
if (car.EndAirSystemIssue()) tags.Add(TextSprites.CycleWaybills);
if (car.HandbrakeApplied()) tags.Add(TextSprites.HandbrakeWheel);
tagCallout.callout.Title = tagCallout.callout.Title =
(car.CarAndEndGearIssue(), car.EndAirSystemIssue(), car.HandbrakeApplied()) switch tags.Any() switch
{ {
(true, _, _) => $"{tagCallout.callout.Title}{tagTitleAndIconDelimeter}{TextSprites.CycleWaybills}{TextSprites.HandbrakeWheel}".Replace("{0}", "2"), true => $"{tagCallout.callout.Title}{tagTitleAndIconDelimeter}{string.Join("", tags)}".Replace("{0}", tags.Count().ToString()),
(_, true, _) => $"{tagCallout.callout.Title}{tagTitleAndIconDelimeter}{TextSprites.CycleWaybills}".Replace("{0}", "1"),
(_, _, true) => $"{tagCallout.callout.Title}{tagTitleAndIconDelimeter}{TextSprites.HandbrakeWheel}".Replace("{0}", "1"),
_ => car.DisplayName _ => car.DisplayName
}; };
} }

View File

@@ -0,0 +1,20 @@
using HarmonyLib;
using Railloader;
using UI.ContextMenu;
using UnityEngine.UI;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(WedgeImage))]
[HarmonyPatch(nameof(WedgeImage.OnPopulateMesh), typeof(VertexHelper))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class WedgeImage_OnPopulateMesh_Patch
{
private static void Postfix(VertexHelper vh)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled()) return;
vh.Clear(); //clear the image backgrounds for now.
}
}

View File

@@ -1,9 +1,17 @@
<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>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<Optimize>False</Optimize>
<DebugType>portable</DebugType>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Patches\BindingsWindow_Patches.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="mroc-cabeese.json" /> <None Remove="mroc-cabeese.json" />
</ItemGroup> </ItemGroup>
@@ -22,9 +30,17 @@
<GameAssembly Include="Ops" /> <GameAssembly Include="Ops" />
<GameAssembly Include="StrangeCustoms" /> <GameAssembly Include="StrangeCustoms" />
<GameAssembly Include="Unity.InputSystem" />
<GameAssembly Include="UnityEngine.CoreModule" /> <GameAssembly Include="UnityEngine.CoreModule" />
<GameAssembly Include="UnityEngine.UI" /> <GameAssembly Include="UnityEngine.UI" />
<GameAssembly Include="UnityEngine.UIModule" />
<GameAssembly Include="UnityEngine.InputLegacyModule" />
<GameAssembly Include="UnityEngine.InputModule" />
<GameAssembly Include="Unity.TextMeshPro" /> <GameAssembly Include="Unity.TextMeshPro" />
<GameAssembly Include="Map.Runtime" />
<GameAssembly Include="UnityEngine.PhysicsModule" />
<GameAssembly Include="UnityEngine.ImageConversionModule" />
<GameAssembly Include="UnityEngine.IMGUIModule" />
<GameAssembly Include="System.Net.Http" /> <GameAssembly Include="System.Net.Http" />
<GameAssembly Include="Core" /> <GameAssembly Include="Core" />

View File

@@ -2,6 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using RMROC451.TweaksAndThings.Enums; using RMROC451.TweaksAndThings.Enums;
using UI.Builder;
using Model;
using RMROC451.TweaksAndThings.Extensions;
using UnityEngine.InputSystem;
namespace RMROC451.TweaksAndThings; namespace RMROC451.TweaksAndThings;
@@ -18,19 +22,41 @@ public class Settings
List<WebhookSettings> webhookSettingsList, List<WebhookSettings> webhookSettingsList,
bool handBrakeAndAirTagModifiers, bool handBrakeAndAirTagModifiers,
RosterFuelColumnSettings engineRosterFuelColumnSettings, RosterFuelColumnSettings engineRosterFuelColumnSettings,
bool endGearHelpersRequirePayment bool endGearHelpersRequirePayment,
bool requireConsistCabooseForOilerAndHotboxSpotter,
bool cabooseAllowsConsistInfo,
bool cabooseRequiredForLocoTagOilIndication,
bool servicingFundPenalty,
bool safetyFirst,
CrewHourLoadMethod loadCrewHoursMethod,
float cabeeseSearchRadiusFtInMeters
) )
{ {
WebhookSettingsList = webhookSettingsList; WebhookSettingsList = webhookSettingsList;
HandBrakeAndAirTagModifiers = handBrakeAndAirTagModifiers; HandBrakeAndAirTagModifiers = handBrakeAndAirTagModifiers;
EngineRosterFuelColumnSettings = engineRosterFuelColumnSettings; EngineRosterFuelColumnSettings = engineRosterFuelColumnSettings;
EndGearHelpersRequirePayment = endGearHelpersRequirePayment; EndGearHelpersRequirePayment = endGearHelpersRequirePayment;
RequireConsistCabooseForOilerAndHotboxSpotter = requireConsistCabooseForOilerAndHotboxSpotter;
CabooseAllowsConsistInfo = cabooseAllowsConsistInfo;
CabooseRequiredForLocoTagOilIndication = cabooseRequiredForLocoTagOilIndication;
ServicingFundPenalty = servicingFundPenalty;
SafetyFirst = safetyFirst;
LoadCrewHoursMethod = loadCrewHoursMethod;
CabeeseSearchRadiusFtInMeters = cabeeseSearchRadiusFtInMeters;
} }
public readonly UIState<string> _selectedTabState = new UIState<string>(null);
public List<WebhookSettings>? WebhookSettingsList; public List<WebhookSettings>? WebhookSettingsList;
public bool HandBrakeAndAirTagModifiers; public bool HandBrakeAndAirTagModifiers;
public RosterFuelColumnSettings? EngineRosterFuelColumnSettings; public RosterFuelColumnSettings? EngineRosterFuelColumnSettings;
public bool EndGearHelpersRequirePayment; public bool EndGearHelpersRequirePayment;
public bool RequireConsistCabooseForOilerAndHotboxSpotter;
public bool CabooseAllowsConsistInfo;
public bool CabooseRequiredForLocoTagOilIndication;
public bool ServicingFundPenalty;
public bool SafetyFirst;
public CrewHourLoadMethod LoadCrewHoursMethod;
public float CabeeseSearchRadiusFtInMeters;
internal void AddAnotherRow() internal void AddAnotherRow()
{ {
@@ -91,4 +117,25 @@ public static class SettingsExtensions
return output; return output;
} }
public static bool IsEnabled(this TweaksAndThingsPlugin input) =>
input?.IsEnabled ?? false;
public static bool CabooseAllowsConsistInfo(this TweaksAndThingsPlugin input) =>
input?.settings?.CabooseAllowsConsistInfo ?? false;
public static bool EndGearHelpersRequirePayment(this TweaksAndThingsPlugin input) =>
input?.settings?.EndGearHelpersRequirePayment ?? false;
public static bool RequireConsistCabooseForOilerAndHotboxSpotter(this TweaksAndThingsPlugin input) =>
input?.settings?.RequireConsistCabooseForOilerAndHotboxSpotter ?? false;
public static bool CabooseNonMotiveAllowedSetting(this TweaksAndThingsPlugin input, Car car) =>
input.EndGearHelpersRequirePayment() && !car.MotivePower() && (bool)car.FindMyCaboose(0.0f, false);
public static bool CabooseRequiredForLocoOilIndicator(this TweaksAndThingsPlugin input) =>
input?.settings?.CabooseRequiredForLocoTagOilIndication ?? false;
public static bool ServiceFundPenalties(this TweaksAndThingsPlugin input) =>
input?.settings?.ServicingFundPenalty ?? false;
public static bool SafetyFirst(this TweaksAndThingsPlugin input) =>
input?.settings?.SafetyFirst ?? false;
public static bool DayLoadCrewHours(this TweaksAndThingsPlugin input) =>
(input?.settings?.LoadCrewHoursMethod ?? CrewHourLoadMethod.Tracks) == CrewHourLoadMethod.Daily;
public static float CabeeseSearchRadiusInMeters(this TweaksAndThingsPlugin input) =>
input?.settings?.CabeeseSearchRadiusFtInMeters ?? 21f;
} }

View File

@@ -1,17 +1,20 @@
// Ignore Spelling: RMROC // Ignore Spelling: RMROC
using GalaSoft.MvvmLight.Messaging; using GalaSoft.MvvmLight.Messaging;
using Game;
using Game.Messages;
using Game.State; using Game.State;
using HarmonyLib; using HarmonyLib;
using Railloader; using Railloader;
using RMROC451.TweaksAndThings.Commands;
using RMROC451.TweaksAndThings.Enums;
using Serilog; using Serilog;
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using UI.Builder; using UI.Builder;
using UnityEngine;
using RMROC451.TweaksAndThings.Enums; using ILogger = Serilog.ILogger;
using RMROC451.TweaksAndThings.Commands;
namespace RMROC451.TweaksAndThings; namespace RMROC451.TweaksAndThings;
@@ -33,6 +36,8 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
IModdingContext moddingContext { get; set; } IModdingContext moddingContext { get; set; }
IModDefinition modDefinition { get; set; } IModDefinition modDefinition { get; set; }
public string ModDirectory => modDefinition.Directory;
static TweaksAndThingsPlugin() static TweaksAndThingsPlugin()
{ {
Log.Information("Hello! Static Constructor was called!"); Log.Information("Hello! Static Constructor was called!");
@@ -48,7 +53,7 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
moddingContext.RegisterConsoleCommand(new EchoCommand()); moddingContext.RegisterConsoleCommand(new EchoCommand());
settings = moddingContext.LoadSettingsData<Settings>(self.Id); settings = moddingContext.LoadSettingsData<Settings>(self.Id) ?? new();
} }
public override void OnEnable() public override void OnEnable()
@@ -82,20 +87,165 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
settings.WebhookSettingsList = settings.WebhookSettingsList =
settings?.WebhookSettingsList.SanitizeEmptySettings(); settings?.WebhookSettingsList.SanitizeEmptySettings();
//WebhookUISection(ref builder); builder.AddSection("Adjustments To Base Game", (UIPanelBuilder builder) => {
//builder.AddExpandingVerticalSpacer(); builder.AddLabel("Repair tracks now require cars to be waybilled, or they will not be serviced/overhauled.\nThey will report on the company window's location section as 'No Work Order Assigned'.");
WebhooksListUISection(ref builder); });
builder.AddExpandingVerticalSpacer(); builder.Spacer(spacing * spacing);
HandbrakesAndAnglecocksUISection(ref builder); builder.AddTabbedPanels(settings._selectedTabState, delegate (UITabbedPanelBuilder tabBuilder)
builder.AddExpandingVerticalSpacer(); {
EnginRosterShowsFuelStatusUISection(ref builder); tabBuilder.AddTab("Caboose Mods", "cabooseUpdates", CabooseMods);
tabBuilder.AddTab("UI", "rosterUi", UiUpdates);
tabBuilder.AddTab("Webhooks", "webhooks", WebhooksListUISection);
});
} }
private void EnginRosterShowsFuelStatusUISection(ref UIPanelBuilder builder) private static string cabooseUse => "Caboose Use";
private static string autoAiRequirment => "AutoAI Requirement";
private static string locoConsistOilIndication => "Consist Oil Indication";
private static float spacing => 2f;
private void CabooseMods(UIPanelBuilder builder)
{
//UI.GameInput
//builder.AddField("Meow",
// builder.AddInputBindingControl(
// )
//)
// InputAction a = new InputAction("connectCarsAndGladhands", InputActionType.Button)
//var connectCarsAndGladhands = new InputAction("connectCarsAndGladhands");
//connectCarsAndGladhands.AddCompositeBinding("connectCarsAndGladhandsComposite")
// .With("modifier", "<Keyboard>/leftCtrl")
// .With("modifier", "<Keyboard>/leftAlt");
//builder.AddField("meow", builder.AddInputBindingControl(connectCarsAndGladhands, conflict: true, ()=>))
#region EndGearHelperCost
builder.AddFieldToggle(
cabooseUse,
() => this.EndGearHelpersRequirePayment(),
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.EndGearHelpersRequirePayment = enabled;
builder.Rebuild();
}
).Tooltip("Enable End Gear Helper Cost", @$"Will cost 1 minute of AI Brake Crew & Caboose Crew time per car in the consist when the new inspector buttons are utilized.
1.5x multiplier penalty to AI Brake Crew cost if no sufficiently crewed caboose nearby.
Caboose starts reloading `Crew Hours` at any Team or Repair track (no waybill), after being stationary for 30 seconds.
AutoOiler Update: Increases limit that crew will oiling a car from 75% -> 99%, also halves the time it takes (simulating crew from lead end and caboose handling half the train).
AutoOiler Update: if `{cabooseUse}` & `{autoAiRequirment.Replace("\n", " ")}` checked, then when a caboose is present, the AutoOiler will repair hotboxes afer oiling them to 100%.
AutoHotboxSpotter Update: decrease the random wait from 30 - 300 seconds to 15 - 30 seconds (Safety Is Everyone's Job)");
#endregion
#region CabeeseLoadOptions
if (this.EndGearHelpersRequirePayment())
{
var columns = Enum.GetValues(typeof(CrewHourLoadMethod)).Cast<CrewHourLoadMethod>().Select(i => i.ToString()).ToList();
builder.Spacer(spacing);
builder.AddField("Refill Option",
builder.AddDropdown(
columns,
(int)(settings?.LoadCrewHoursMethod ?? CrewHourLoadMethod.Tracks),
delegate (int column)
{
if (settings == null) settings = new();
settings.LoadCrewHoursMethod = (CrewHourLoadMethod)column;
builder.Rebuild();
}
)
).Tooltip("Crew Hours Load Option", "Select whether you want to manually reload cabeese via:\n\ntrack method - (team/repair/passenger stop/interchange)\n\ndaily caboose top off - refill to 8h at new day.");
}
#endregion
#region RequireCabeeseForOiler/HotboxDetection
builder.Spacer(spacing);
builder.AddFieldToggle(
autoAiRequirment,
() => this.RequireConsistCabooseForOilerAndHotboxSpotter(),
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.RequireConsistCabooseForOilerAndHotboxSpotter = enabled;
builder.Rebuild();
}
).Tooltip("AI Hotbox\\Oiler Requires Caboose", $@"A caboose is required in the consist to check for Hotboxes and perform Auto Oiler, if checked.");
#endregion
#region ShowLocomotiveConsistOilIndicator
builder.Spacer(spacing);
builder.AddFieldToggle(
locoConsistOilIndication,
() => settings?.CabooseRequiredForLocoTagOilIndication ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.CabooseRequiredForLocoTagOilIndication = enabled;
builder.Rebuild();
}
).Tooltip(locoConsistOilIndication, $@"A caboose is required in the consist to report the lowest oil level in the consist in the locomotive's tag & roster entry.");
#endregion
#region SafetyFirst
builder.Spacer(spacing);
builder.AddFieldToggle(
"Safety First!",
() => settings?.SafetyFirst ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.SafetyFirst = enabled;
builder.Rebuild();
}
).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
}
private void UiUpdates(UIPanelBuilder builder)
{
builder.AddFieldToggle(
"Enable Tag Updates",
() => settings?.HandBrakeAndAirTagModifiers ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.HandBrakeAndAirTagModifiers = enabled;
builder.Rebuild();
}
).Tooltip("Enable Tag Updates", $@"Will suffix tag title with:
{TextSprites.CycleWaybills} if Air System issue.
{TextSprites.HandbrakeWheel} if there is a handbrake set.
{TextSprites.Hotbox} if a hotbox.");
builder.Spacer(spacing);
builder.AddFieldToggle(
"Debt Allowance",
() => settings?.ServicingFundPenalty ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.ServicingFundPenalty = enabled;
builder.Rebuild();
}
).Tooltip("Allow Insufficient Funds", $@"Will allow interchange service and repair shops to still function when you are insolvent, at a 20% overdraft fee.");
builder.Spacer(spacing);
EngineRosterShowsFuelStatusUISection(builder);
}
private void EngineRosterShowsFuelStatusUISection(UIPanelBuilder builder)
{ {
var columns = Enum.GetValues(typeof(EngineRosterFuelDisplayColumn)).Cast<EngineRosterFuelDisplayColumn>().Select(i => i.ToString()).ToList(); var columns = Enum.GetValues(typeof(EngineRosterFuelDisplayColumn)).Cast<EngineRosterFuelDisplayColumn>().Select(i => i.ToString()).ToList();
builder.AddSection("Fuel Display in Engine Roster", delegate (UIPanelBuilder builder) builder.AddSection("Fuel Display in Engine Roster", delegate (UIPanelBuilder builder)
{ {
builder.Spacer(spacing);
builder.AddField( builder.AddField(
"Enable", "Enable",
builder.AddDropdown(columns, (int)(settings?.EngineRosterFuelColumnSettings?.EngineRosterFuelStatusColumn ?? EngineRosterFuelDisplayColumn.None), builder.AddDropdown(columns, (int)(settings?.EngineRosterFuelColumnSettings?.EngineRosterFuelStatusColumn ?? EngineRosterFuelDisplayColumn.None),
@@ -108,9 +258,9 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
) )
).Tooltip("Enable Fuel Display in Engine Roster", $"Will add reaming fuel indication to Engine Roster (with details in roster row tool tip), Examples : {string.Join(" ", Enumerable.Range(0, 4).Select(i => TextSprites.PiePercent(i, 4)))}"); ).Tooltip("Enable Fuel Display in Engine Roster", $"Will add reaming fuel indication to Engine Roster (with details in roster row tool tip), Examples : {string.Join(" ", Enumerable.Range(0, 4).Select(i => TextSprites.PiePercent(i, 4)))}");
builder.AddField( builder.Spacer(spacing);
builder.AddFieldToggle(
"Always Visible?", "Always Visible?",
builder.AddToggle(
() => settings?.EngineRosterFuelColumnSettings?.EngineRosterShowsFuelStatusAlways ?? false, () => settings?.EngineRosterFuelColumnSettings?.EngineRosterShowsFuelStatusAlways ?? false,
delegate (bool enabled) delegate (bool enabled)
{ {
@@ -118,44 +268,11 @@ public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>,
settings.EngineRosterFuelColumnSettings.EngineRosterShowsFuelStatusAlways = enabled; settings.EngineRosterFuelColumnSettings.EngineRosterShowsFuelStatusAlways = enabled;
builder.Rebuild(); builder.Rebuild();
} }
)
).Tooltip("Fuel Display in Engine Roster Always Visible", $"Always displayed, if you want it hidden and only shown when you care to see, uncheck this, and then you can press ALT for it to populate on the next UI refresh cycle."); ).Tooltip("Fuel Display in Engine Roster Always Visible", $"Always displayed, if you want it hidden and only shown when you care to see, uncheck this, and then you can press ALT for it to populate on the next UI refresh cycle.");
}); });
} }
private void HandbrakesAndAnglecocksUISection(ref UIPanelBuilder builder) private void WebhooksListUISection(UIPanelBuilder builder)
{
builder.AddSection("Tag Callout Handbrake and Air System Helper", delegate (UIPanelBuilder builder)
{
builder.AddField(
"Enable Tag Updates",
builder.AddToggle(
() => settings?.HandBrakeAndAirTagModifiers ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.HandBrakeAndAirTagModifiers = enabled;
builder.Rebuild();
}
)
).Tooltip("Enable Tag Updates", $"Will add {TextSprites.CycleWaybills} to the car tag title having Air System issues. Also prepends {TextSprites.HandbrakeWheel} if there is a handbrake set.\n\nHolding Left Alt while tags are displayed only shows tag titles that have issues.");
builder.AddField(
"Caboose Use",
builder.AddToggle(
() => settings?.EndGearHelpersRequirePayment ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new();
settings.EndGearHelpersRequirePayment = enabled;
builder.Rebuild();
}
)
).Tooltip("Enable End Gear Helper Cost", $"Will cost 1 minute of AI Brake Crew & Caboose Crew time per car in the consist when the new inspector buttons are utilized.\n\n1.5x multiplier penalty to AI Brake Crew cost if no sufficiently crewed caboose nearby.\n\nCaboose starts reloading `Crew Hours` at any Team or Repair track (no waybill), after being stationary for 30 seconds.");
});
}
private void WebhooksListUISection(ref UIPanelBuilder builder)
{ {
builder.AddSection("Webhooks List", delegate (UIPanelBuilder builder) builder.AddSection("Webhooks List", delegate (UIPanelBuilder builder)
{ {

12
updates.json Normal file
View File

@@ -0,0 +1,12 @@
{
"RMROC451.TweaksAndThings": {
"version": "1.0.0",
"changelog": [
{
"version": "*",
"date": "2024-07-26T14:31:49.0948925Z",
"desc": "https://github.com/rmroc451/TweaksAndThings/releases"
}
]
}
}