Compare commits

...

12 Commits

Author SHA1 Message Date
ec21effd30 Adding consist buttons from inspector to new context menu, and squashing bug that would occur if inspector open but tags weren't active, breaking the ui panel builder reload detection for handbrake/couple/air changes.
final change of Version 0.1.7
2024-07-13 13:09:42 -05:00
2242aaacde Merge pull request #22 from rmroc451/1.6.1
Fix issue with cars that are missing tagcallouts from throwing errors.
2024-07-13 11:12:24 -05:00
e26713688b Fix issue with cars that are missing tagcallouts from throwing errors. 2024-07-13 11:11:22 -05:00
8a228be5a8 Merge pull request #21 from rmroc451/20-webhook-settings-adds-a-new-blank-entry-every-time-the-settings-window-opens
misc refactor and fixes
2024-07-05 20:22:52 -05:00
2ed33465d9 reduce cyclomatic complexity of the cabeese hunting algorithm, and fix issue with webhook settings adding an empty row each time settings screen opens. 2024-07-05 20:21:41 -05:00
67d39801de Merge pull request #18 from rmroc451/13-add-buttonconsole-command-to-initiate-a-console-message-logging-with-selected-engine
Engine based status messages & Caboose Crew time Resource
2024-06-27 00:11:54 -05:00
8dc87f312d #5 adding Crew as a resource to cabeese, and having the new buttons added by this mod, cost resources if opting in. 2024-06-27 00:09:40 -05:00
9cb788d86a Merge remote-tracking branch 'origin/main' into 13-add-buttonconsole-command-to-initiate-a-console-message-logging-with-selected-engine 2024-06-23 13:48:14 -05:00
2e47536028 added Bleed Consist button 2024-06-23 02:30:13 -05:00
b6edc93636 renaming main class to have plugin suffix to avoid namespace collision issues. 2024-06-23 02:10:47 -05:00
6693a762c9 rather than wait for the next callout refresh ever 1s, call update if the inspector observers fire off. 2024-06-23 02:04:53 -05:00
fc43d54815 initial work for 13 2024-06-21 00:23:45 -05:00
19 changed files with 628 additions and 84 deletions

1
.gitignore vendored
View File

@@ -396,4 +396,3 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
/TweaksAndThings/Commands/EchoCommand.cs

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
using Game.State;
using Helpers;
using Model.OpsNew;
using Network;
using RMROC451.TweaksAndThings.Extensions;
using System.Linq;
using UI.Console;
namespace RMROC451.TweaksAndThings.Commands;
[ConsoleCommand("/cu", "generate a formatted message about a locomotive's status.")]
public class EchoCommand : IConsoleCommand
{
public string Execute(string[] comps)
{
if (comps.Length < 4)
{
return "Usage: /cu <car>|. +|- <message>";
}
string query = comps[1];
Model.Car car = query == "." ? TrainController.Shared.SelectedLocomotive : TrainController.Shared.CarForString(query);
if (car.DetermineFuelCar() == null)
{
return "Car not found.";
}
string message = string.Join(" ", comps.Skip(3)).Truncate(512);
switch (comps[2])
{
case "+":
break;
case "-":
break;
default:
return "+ to include area or - to leave it out";
}
if (comps[2] == "+") message += $" {OpsController.Shared.ClosestArea(car)?.name ?? "???"}";
Multiplayer.Broadcast($"{StateManager.Shared._playersManager.LocalPlayer} {Hyperlink.To(car)}: \"{message}\"");
return string.Empty;
}
}

View File

@@ -7,7 +7,13 @@
{
"id": "railloader",
"notBefore": "1.8.1"
}
},
"Zamu.StrangeCustoms"
],
"assemblies": [ "RMROC451.TweaksAndThings" ]
"assemblies": [ "RMROC451.TweaksAndThings" ],
"mixintos": {
"container:ne-caboose01": "file(mroc-cabeese.json)",
"container:ne-caboose02": "file(mroc-cabeese.json)",
"container:ne-caboose03": "file(mroc-cabeese.json)"
}
}

View File

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

View File

@@ -1,40 +1,121 @@
using Model;
using Helpers;
using Model;
using Model.Definition.Data;
using Model.OpsNew;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Extensions
namespace RMROC451.TweaksAndThings.Extensions;
public static class Car_Extensions
{
public static class Car_Extensions
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 BEndAirSystemIssue = car[Car.LogicalEnd.B].IsCoupled && !car[Car.LogicalEnd.B].IsAirConnectedAndOpen;
bool EndAirSystemIssue = AEndAirSystemIssue || BEndAirSystemIssue;
return EndAirSystemIssue;
}
public static bool HandbrakeApplied(this Model.Car car) =>
car.air.handbrakeApplied;
public static bool CarOrEndGearIssue(this Model.Car car) =>
car.EndAirSystemIssue() || car.HandbrakeApplied();
public static bool CarAndEndGearIssue(this Model.Car car) =>
car.EndAirSystemIssue() && car.HandbrakeApplied();
public static Car? DetermineFuelCar(this Car engine, bool returnEngineIfNull = false)
{
if (engine == null) return null;
Car? car;
if (engine is SteamLocomotive steamLocomotive && new Func<Car>(steamLocomotive.FuelCar) != null)
{
bool AEndAirSystemIssue = car[Car.LogicalEnd.A].IsCoupled && !car[Car.LogicalEnd.A].IsAirConnectedAndOpen;
bool BEndAirSystemIssue = car[Car.LogicalEnd.B].IsCoupled && !car[Car.LogicalEnd.B].IsAirConnectedAndOpen;
bool EndAirSystemIssue = AEndAirSystemIssue || BEndAirSystemIssue;
return EndAirSystemIssue;
car = steamLocomotive.FuelCar();
}
public static bool HandbrakeApplied(this Model.Car car) =>
car.air.handbrakeApplied;
public static bool CarOrEndGearIssue(this Model.Car car) =>
car.EndAirSystemIssue() || car.HandbrakeApplied();
public static bool CarAndEndGearIssue(this Model.Car car) =>
car.EndAirSystemIssue() && car.HandbrakeApplied();
public static Car? DetermineFuelCar(this Car engine, bool returnEngineIfNull = false)
else
{
Car? car;
if (engine is SteamLocomotive steamLocomotive && new Func<Car>(steamLocomotive.FuelCar) != null)
{
car = steamLocomotive.FuelCar();
}
else
{
car = engine is DieselLocomotive ? engine : null;
if (returnEngineIfNull && car == null) car = engine;
}
return car;
car = engine is DieselLocomotive ? engine : null;
if (returnEngineIfNull && car == null) car = engine;
}
return car;
}
public static bool NotMotivePower(this Car car) => car is not BaseLocomotive && car.Archetype != Model.Definition.CarArchetype.Tender;
/// <summary>
/// For every car in the consist, cost 1 minute of AI Engineer time.
/// </summary>
/// <param name="consist"></param>
public static int CalculateCostForAutoEngineerEndGearSetting(this IEnumerable<Car> consist) =>
consist.Count() * 60;
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? CabooseWithSufficientCrewHours(this Car car, float timeNeeded, HashSet<string> carIdsCheckedAlready, bool decrement = false)
{
Car? output = null;
if (carIdsCheckedAlready.Contains(car.id) || !car.IsCaboose()) return null;
List<LoadSlot> loadSlots = car.Definition.LoadSlots;
for (int i = 0; i < loadSlots.Count; i++)
{
CarLoadInfo? loadInfo = car.GetLoadInfo(i);
if (loadInfo.HasValue)
{
CarLoadInfo valueOrDefault = loadInfo.GetValueOrDefault();
output = valueOrDefault.Quantity >= timeNeeded ? car : null;
if (decrement && output != null) output.SetLoadInfo(i, valueOrDefault with { Quantity = valueOrDefault.Quantity - timeNeeded });
break;
}
}
return output;
}
public static Car? HuntingForCabeeseNearCar(this Car car, float timeNeeded, TrainController tc, HashSet<string> carIdsCheckedAlready, bool decrement = false)
{
List<(string carId, float distance)> source =
CarsNearCurrentCar(car, timeNeeded, tc, carIdsCheckedAlready, decrement);
Car output = FindNearestCabooseFromNearbyCars(tc, source);
if (output != null) output.CabooseWithSufficientCrewHours(timeNeeded, carIdsCheckedAlready, decrement);
return output;
}
private static Car FindNearestCabooseFromNearbyCars(TrainController tc, List<(string carId, float distance)> source) =>
(
from t in source
where t.distance < 21f //todo: add setting slider for catchment
orderby t.distance ascending
select tc.CarForId(t.carId)
).FirstOrDefault();
private static List<(string carId, float distance)> CarsNearCurrentCar(Car car, float timeNeeded, TrainController tc, HashSet<string> carIdsCheckedAlready, bool decrement)
{
Vector3 position = car.GetMotionSnapshot().Position;
Vector3 center = WorldTransformer.WorldToGame(position);
Rect rect = new Rect(new Vector2(center.x - 30f, center.z - 30f), Vector2.one * 30f * 2f);
var cars = tc.CarIdsInRect(rect);
Log.Information($"{nameof(HuntingForCabeeseNearCar)} => {cars.Count()}");
List<(string carId, float distance)> source =
cars
.Select(carId =>
{
Car car = tc.CarForId(carId);
if (car == null || !car.CabooseWithSufficientCrewHours(timeNeeded, carIdsCheckedAlready))
{
return (carId: carId, distance: 1000f);
}
Vector3 a = WorldTransformer.WorldToGame(car.GetMotionSnapshot().Position);
return (carId: carId, distance: Vector3.Distance(a, center));
}).ToList();
return source;
}
}

View File

@@ -0,0 +1,28 @@
using Core;
using System;
namespace RMROC451.TweaksAndThings.Extensions;
public static class Load_Extensions
{
//https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings?redirectedfrom=MSDN#the--section-separator
//https://dotnetfiddle.net/iHVevM
public static string FormatCrewHours(this float quantity, string description)
{
var ts = TimeSpan.FromHours(quantity);
float minutes = ts.Minutes - (ts.Minutes % 15);
string output = string.Format("{0:;;No}{1:##}{2:\\.##;\\.##;.} {3} {4}",
ts.Hours + minutes,
ts.Hours, (minutes / 60.0f) * 100,
description,
"Hour".Pluralize(quantity == 1 ? 1 : 0)
).Trim();
if (ts.Hours < 1)
{
output = string.Format("{0} {1} Minutes", ts.Minutes, description).Trim();
}
return output;
}
}

View File

@@ -2,15 +2,21 @@
using Game.State;
using HarmonyLib;
using KeyValue.Runtime;
using Model;
using Model.OpsNew;
using Network;
using Railloader;
using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using UI.Builder;
using UI.CarInspector;
using UI.ContextMenu;
using UI.Tags;
using static Model.Car;
namespace RMROC451.TweaksAndThings.Patches;
@@ -18,7 +24,7 @@ namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(CarInspector))]
[HarmonyPatch(nameof(CarInspector.PopulateCarPanel), typeof(UIPanelBuilder))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
public class CarInspector_PopulateCarPanel_Patch
internal class CarInspector_PopulateCarPanel_Patch
{
private static IEnumerable<LogicalEnd> ends = Enum.GetValues(typeof(LogicalEnd)).Cast<LogicalEnd>();
@@ -31,8 +37,9 @@ public class CarInspector_PopulateCarPanel_Patch
private static bool Prefix(CarInspector __instance, UIPanelBuilder builder)
{
TweaksAndThings tweaksAndThings = SingletonPluginBase<TweaksAndThings>.Shared;
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled) return true;
bool buttonsHaveCost = tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false;
var consist = __instance._car.EnumerateCoupled(LogicalEnd.A);
builder = AddCarConsistRebuildObservers(builder, consist);
@@ -40,20 +47,23 @@ public class CarInspector_PopulateCarPanel_Patch
builder.HStack(delegate (UIPanelBuilder hstack)
{
var buttonName = $"{(consist.Any(c => c.HandbrakeApplied()) ? "Release " : "Set ")} {TextSprites.HandbrakeWheel}";
hstack.AddButtonCompact(buttonName, delegate
{
MrocConsistHelper(__instance._car, MrocHelperType.Handbrake);
hstack.AddButtonCompact(buttonName, delegate {
MrocConsistHelper(__instance._car, MrocHelperType.Handbrake, buttonsHaveCost);
hstack.Rebuild();
}).Tooltip(buttonName, $"Iterates over cars in this consist and {(consist.Any(c => c.HandbrakeApplied()) ? "releases" : "sets")} {TextSprites.HandbrakeWheel}.");
if (consist.Any(c => c.EndAirSystemIssue()))
{
hstack.AddButtonCompact("Connect Air Lines", delegate
{
MrocConsistHelper(__instance._car, MrocHelperType.GladhandAndAnglecock);
hstack.AddButtonCompact("Connect Air", delegate {
MrocConsistHelper(__instance._car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost);
hstack.Rebuild();
}).Tooltip("Connect Consist Air Lines", "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 {
MrocConsistHelper(__instance._car, MrocHelperType.BleedAirSystem, buttonsHaveCost);
hstack.Rebuild();
}).Tooltip("Bleed Air Lines", "Iterates over each car in this consist and bleeds the air out of the lines.");
});
return true;
@@ -61,28 +71,38 @@ public class CarInspector_PopulateCarPanel_Patch
private static UIPanelBuilder AddCarConsistRebuildObservers(UIPanelBuilder builder, IEnumerable<Model.Car> consist)
{
foreach (Model.Car car in consist)
TagController tagController = UnityEngine.Object.FindFirstObjectByType<TagController>();
foreach (Model.Car car in consist.Where(c => c.Archetype != Model.Definition.CarArchetype.Tender))
{
builder = AddObserver(builder, car, PropertyChange.KeyForControl(PropertyChange.Control.Handbrake));
builder = AddObserver(builder, car, PropertyChange.KeyForControl(PropertyChange.Control.Handbrake), tagController);
foreach (LogicalEnd logicalEnd in ends)
{
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsCoupled, car.LogicalToEnd(logicalEnd)));
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsAirConnected, car.LogicalToEnd(logicalEnd)));
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.Anglecock, car.LogicalToEnd(logicalEnd)));
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsCoupled, car.LogicalToEnd(logicalEnd)), tagController);
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.IsAirConnected, car.LogicalToEnd(logicalEnd)), tagController);
builder = AddObserver(builder, car, KeyValueKeyFor(EndGearStateKey.Anglecock, car.LogicalToEnd(logicalEnd)), tagController);
}
}
return builder;
}
private static UIPanelBuilder AddObserver(UIPanelBuilder builder, Model.Car car, string key)
private static UIPanelBuilder AddObserver(UIPanelBuilder builder, Model.Car car, string key, TagController tagController)
{
builder.AddObserver(
car.KeyValueObject.Observe(
key,
delegate (Value value)
{
builder.Rebuild();
try
{
if (car.TagCallout != null) tagController.UpdateTag(car, car.TagCallout, OpsController.Shared);
if (ContextMenu.IsShown && ContextMenu.Shared.centerLabel.text == car.DisplayName) CarPickable.HandleShowContextMenu(car);
builder.Rebuild();
}
catch(Exception ex)
{
Log.Warning(ex, $"{nameof(AddObserver)} {car} Exception logged for {key}");
}
},
false
)
@@ -101,21 +121,24 @@ public class CarInspector_PopulateCarPanel_Patch
// }
//}
public static void MrocConsistHelper(Model.Car car, MrocHelperType mrocHelperType)
public static void MrocConsistHelper(Model.Car car, MrocHelperType mrocHelperType, bool buttonsHaveCost)
{
TrainController tc = UnityEngine.Object.FindObjectOfType<TrainController>();
IEnumerable<Model.Car> consist = car.EnumerateCoupled(LogicalEnd.A);
//Log.Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
CalculateCostIfEnabled(car, mrocHelperType, buttonsHaveCost, consist);
Log.Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
switch (mrocHelperType)
{
case MrocHelperType.Handbrake:
if (consist.Any(c => c.HandbrakeApplied()))
{
consist.Do(c => c.SetHandbrake(false));
} else
}
else
{
TrainController tc = UnityEngine.Object.FindObjectOfType<TrainController>();
consist = consist.Where(c => c.DetermineFuelCar(true) != null);
consist = consist.Where(c => c is not BaseLocomotive && c.Archetype != Model.Definition.CarArchetype.Tender);
Log.Information($"{car} => {mrocHelperType} => {string.Join("/", consist.Select(c => c.ToString()))}");
//when ApplyHandbrakesAsNeeded is called, and the consist contains an engine, it stops applying brakes.
tc.ApplyHandbrakesAsNeeded(consist.ToList(), PlaceTrainHandbrakes.Automatic);
@@ -130,20 +153,72 @@ public class CarInspector_PopulateCarPanel_Patch
StateManager.ApplyLocal(
new PropertyChange(
c.id,
c.id,
KeyValueKeyFor(EndGearStateKey.Anglecock, c.LogicalToEnd(end)),
new FloatPropertyValue(endGear.IsCoupled ? 1f : 0f)
)
);
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)
{
if (buttonsHaveCost)
{
float originalTimeCost = consist.CalculateCostForAutoEngineerEndGearSetting();
float timeCost = originalTimeCost;
float crewCost = timeCost / 3600; //hours of time deducted from caboose.
var tsString = crewCost.FormatCrewHours(IndustryComponent_Service_Patch.CrewHoursLoad().description);
Car? cabooseWithAvailCrew = NearbyCabooseWithAvailableCrew(car, crewCost, buttonsHaveCost);
if (cabooseWithAvailCrew == null) timeCost *= 1.5f;
var cabooseFoundDisplay = cabooseWithAvailCrew?.DisplayName ?? "No caboose";
Log.Information($"{nameof(MrocConsistHelper)} {mrocHelperType} : [VACINITY CABEESE FOUND:{cabooseWithAvailCrew?.ToString() ?? "NONE"}] => Consist Length {consist.Count()} => costs {timeCost / 60} minutes of AI Engineer time, $5 per hour = ~${Math.Ceiling((decimal)(timeCost / 3600) * 5)} (*2 if no caboose nearby)");
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;
}
}
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,51 @@
using HarmonyLib;
using Model;
using Railloader;
using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Extensions;
using RollingStock;
using System.Linq;
using UI;
using UI.ContextMenu;
using static Model.Car;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(CarPickable))]
[HarmonyPatch(nameof(CarPickable.HandleShowContextMenu), typeof(Car))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class CarPickable_HandleShowContextMenu_Patch
{
private static void Postfix(Car car)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled) return;
bool buttonsHaveCost = tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false;
ContextMenu shared = ContextMenu.Shared;
var consist = car.EnumerateCoupled(LogicalEnd.A);
shared.AddButton(ContextMenuQuadrant.Unused2, $"{(consist.Any(c => c.HandbrakeApplied()) ? "Release " : "Set ")} Consist", SpriteName.Handbrake, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.Handbrake, buttonsHaveCost);
});
if (consist.Any(c => c.EndAirSystemIssue()))
{
shared.AddButton(ContextMenuQuadrant.Unused2, $"Air Up Consist", SpriteName.Select, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.GladhandAndAnglecock, buttonsHaveCost);
});
}
if (consist.Any(c => c.SupportsBleed()))
{
shared.AddButton(ContextMenuQuadrant.Unused2, $"Bleed Consist", SpriteName.Bleed, delegate
{
CarInspector_PopulateCarPanel_Patch.MrocConsistHelper(car, MrocHelperType.BleedAirSystem, buttonsHaveCost);
});
}
shared.BuildItemAngles();
shared.StartCoroutine(shared.AnimateButtonsShown());
}
}

View File

@@ -21,11 +21,11 @@ namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(EngineRosterRow))]
[HarmonyPatch(nameof(EngineRosterRow.Refresh))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
public class EngineRosterRow_Refresh_Patch
internal class EngineRosterRow_Refresh_Patch
{
public static void Postfix(EngineRosterRow __instance)
{
TweaksAndThings? tweaksAndThings = SingletonPluginBase<TweaksAndThings>.Shared;
TweaksAndThingsPlugin? tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
RosterFuelColumnSettings? rosterFuelColumnSettings = tweaksAndThings?.settings?.EngineRosterFuelColumnSettings;
if (tweaksAndThings == null ||

View File

@@ -19,7 +19,7 @@ namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(ExpandedConsole))]
[HarmonyPatch(nameof(ExpandedConsole.Add))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
public class ExpandedConsole_Add_Patch
internal class ExpandedConsole_Add_Patch
{
private static void Prefix(ref UI.Console.Console.Entry entry)
{
@@ -32,7 +32,7 @@ public class ExpandedConsole_Add_Patch
{
try
{
TweaksAndThings tweaksAndThings = SingletonPluginBase<TweaksAndThings>.Shared;
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
StateManager shared = StateManager.Shared;
GameStorage gameStorage = shared.Storage;
WebhookSettings settings = tweaksAndThings?.settings?.WebhookSettingsList?.FirstOrDefault(ws => ws.RailroadMark == gameStorage.RailroadMark);

View File

@@ -0,0 +1,139 @@
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,50 @@
using Game.Events;
using Game.State;
using HarmonyLib;
using KeyValue.Runtime;
using Network;
using Railloader;
using UnityEngine;
namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(StateManager))]
[HarmonyPatch(nameof(StateManager.OnDayDidChange), typeof(TimeDayDidChange))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
internal class StateManager_OnDayDidChange_Patch
{
private const string unbilledBrakeCrewDuration = "unbilledBrakeCrewDuration";
private static void Postfix(StateManager __instance)
{
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled || !(tweaksAndThings?.settings?.EndGearHelpersRequirePayment ?? false)) return;
if (StateManager.IsHost) PayAutoBrakeCrewWages(__instance);
}
private static void PayAutoBrakeCrewWages(StateManager __instance)
{
float unbilledRunDuration = UnbilledAutoBrakeCrewRunDuration;
int num = Mathf.FloorToInt(unbilledRunDuration / 3600f * 5f);
float num2 = (float)num / 5f;
float unbilledAutoEngineerRunDuration2 = unbilledRunDuration - num2 * 3600f;
if (num > 0)
{
__instance.ApplyToBalance(-num, Ledger.Category.WagesAI, null, memo: "AI Brake Crew");
Multiplayer.Broadcast($"Paid {num:C0} for {num2:F1} hours of Brake Crew services.");
UnbilledAutoBrakeCrewRunDuration = unbilledAutoEngineerRunDuration2;
}
}
public static float UnbilledAutoBrakeCrewRunDuration
{
get
{
return StateManager.Shared._storage._gameKeyValueObject[unbilledBrakeCrewDuration].FloatValue;
}
set
{
StateManager.Shared._storage._gameKeyValueObject[unbilledBrakeCrewDuration] = Value.Float(value);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace RMROC451.TweaksAndThings.Patches;
[HarmonyPatch(typeof(TagController))]
[HarmonyPatch(nameof(TagController.UpdateTag), typeof(Car), typeof(TagCallout), typeof(OpsController))]
[HarmonyPatchCategory("RMROC451TweaksAndThings")]
public class TagController_UpdateTag_Patch
internal class TagController_UpdateTag_Patch
{
private const string tagTitleAndIconDelimeter = "\n<width=100%><align=\"right\">";
private const string tagTitleFormat = "<align=left><margin-right={0}.5em>{1}</margin><line-height=0>";
@@ -20,7 +20,7 @@ public class TagController_UpdateTag_Patch
private static void Postfix(Car car, TagCallout tagCallout)
{
TagController tagController = UnityEngine.Object.FindObjectOfType<TagController>();
TweaksAndThings tweaksAndThings = SingletonPluginBase<TweaksAndThings>.Shared;
TweaksAndThingsPlugin tweaksAndThings = SingletonPluginBase<TweaksAndThingsPlugin>.Shared;
if (!tweaksAndThings.IsEnabled || !tweaksAndThings.settings.HandBrakeAndAirTagModifiers)
{

View File

@@ -4,6 +4,14 @@
<!-- <MajorVersion>1</MajorVersion> -->
<!-- <MinorVersion>0</MinorVersion> -->
</PropertyGroup>
<ItemGroup>
<None Remove="mroc-cabeese.json" />
</ItemGroup>
<ItemGroup>
<Content Include="mroc-cabeese.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<GameAssembly Include="Assembly-CSharp" />
<GameAssembly Include="Railloader.Interchange" />
@@ -12,12 +20,14 @@
<GameAssembly Include="KeyValue.Runtime" />
<GameAssembly Include="Definition" />
<GameAssembly Include="Ops" />
<GameAssembly Include="StrangeCustoms" />
<GameAssembly Include="UnityEngine.CoreModule" />
<GameAssembly Include="UnityEngine.UI" />
<GameAssembly Include="Unity.TextMeshPro" />
<GameAssembly Include="System.Net.Http" />
<GameAssembly Include="Core" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Krafs.Publicizer" Version="2.2.1">

View File

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

View File

@@ -11,10 +11,11 @@ using System.Net.Http;
using UI.Builder;
using RMROC451.TweaksAndThings.Enums;
using RMROC451.TweaksAndThings.Commands;
namespace RMROC451.TweaksAndThings;
public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHandler, IModTabHandler
public class TweaksAndThingsPlugin : SingletonPluginBase<TweaksAndThingsPlugin>, IUpdateHandler, IModTabHandler
{
private HttpClient client;
internal HttpClient Client
@@ -28,24 +29,24 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
}
}
internal Settings? settings { get; private set; } = null;
readonly ILogger logger = Log.ForContext<TweaksAndThings>();
readonly ILogger logger = Log.ForContext<TweaksAndThingsPlugin>();
IModdingContext moddingContext { get; set; }
IModDefinition modDefinition { get; set; }
static TweaksAndThings()
static TweaksAndThingsPlugin()
{
Log.Information("Hello! Static Constructor was called!");
}
public TweaksAndThings(IModdingContext moddingContext, IModDefinition self)
public TweaksAndThingsPlugin(IModdingContext moddingContext, IModDefinition self)
{
this.modDefinition = self;
this.moddingContext = moddingContext;
logger.Information("Hello! Constructor was called for {modId}/{modVersion}!", self.Id, self.Version);
//moddingContext.RegisterConsoleCommand(new EchoCommand());
moddingContext.RegisterConsoleCommand(new EchoCommand());
settings = moddingContext.LoadSettingsData<Settings>(self.Id);
}
@@ -54,7 +55,7 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
{
logger.Information("OnEnable() was called!");
var harmony = new Harmony(modDefinition.Id);
harmony.PatchCategory(modDefinition.Id.Replace(".",string.Empty));
harmony.PatchCategory(modDefinition.Id.Replace(".", string.Empty));
}
public override void OnDisable()
@@ -77,6 +78,10 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
if (!settings?.WebhookSettingsList?.Any() ?? true) settings.WebhookSettingsList = new[] { new WebhookSettings() }.ToList();
if (settings?.EngineRosterFuelColumnSettings == null) settings.EngineRosterFuelColumnSettings = new();
settings.WebhookSettingsList =
settings?.WebhookSettingsList.SanitizeEmptySettings();
//WebhookUISection(ref builder);
//builder.AddExpandingVerticalSpacer();
WebhooksListUISection(ref builder);
@@ -96,12 +101,12 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
builder.AddDropdown(columns, (int)(settings?.EngineRosterFuelColumnSettings?.EngineRosterFuelStatusColumn ?? EngineRosterFuelDisplayColumn.None),
delegate (int column)
{
if (settings == null) settings = new() { WebhookSettingsList = new[] { new WebhookSettings() }.ToList(), EngineRosterFuelColumnSettings = new() };
if (settings == null) settings = new();
settings.EngineRosterFuelColumnSettings.EngineRosterFuelStatusColumn = (EngineRosterFuelDisplayColumn)column;
builder.Rebuild();
}
)
).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(
"Always Visible?",
@@ -109,7 +114,7 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
() => settings?.EngineRosterFuelColumnSettings?.EngineRosterShowsFuelStatusAlways ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new() { WebhookSettingsList = new[] { new WebhookSettings() }.ToList(), EngineRosterFuelColumnSettings = new() };
if (settings == null) settings = new();
settings.EngineRosterFuelColumnSettings.EngineRosterShowsFuelStatusAlways = enabled;
builder.Rebuild();
}
@@ -128,12 +133,25 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
() => settings?.HandBrakeAndAirTagModifiers ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new() { WebhookSettingsList = new[] { new WebhookSettings() }.ToList() };
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.");
});
}
@@ -152,7 +170,7 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
() => settings?.WebhookSettingsList[z]?.WebhookEnabled ?? false,
delegate (bool enabled)
{
if (settings == null) settings = new() { WebhookSettingsList = new[] { new WebhookSettings() }.ToList() };
if (settings == null) settings = new();
settings.WebhookSettingsList[z].WebhookEnabled = enabled;
settings.AddAnotherRow();
builder.Rebuild();
@@ -168,7 +186,7 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
settings?.WebhookSettingsList[z]?.RailroadMark,
delegate (string railroadMark)
{
if (settings == null) settings = new() { WebhookSettingsList = new[] { new WebhookSettings() }.ToList() };
if (settings == null) settings = new();
settings.WebhookSettingsList[z].RailroadMark = railroadMark;
settings.AddAnotherRow();
builder.Rebuild();
@@ -184,7 +202,7 @@ public class TweaksAndThings : SingletonPluginBase<TweaksAndThings>, IUpdateHand
settings?.WebhookSettingsList[z]?.WebhookUrl,
delegate (string webhookUrl)
{
if (settings == null) settings = new() { WebhookSettingsList = new[] { new WebhookSettings() }.ToList() };
if (settings == null) settings = new();
settings.WebhookSettingsList[z].WebhookUrl = webhookUrl;
settings.AddAnotherRow();
builder.Rebuild();

View File

@@ -0,0 +1,23 @@
{
"objects": [
{
"$find": [
{
"path": "definition.archetype",
"value": "Caboose"
}
],
"definition": {
"loadSlots": [
{
"$add": {
"maximumCapacity": 8,
"loadUnits": "Quantity",
"requiredLoadIdentifier": "crew-hours"
}
}
]
}
}
]
}