diff --git a/TLM/CSUtil.Commons/ArrowDirection.cs b/TLM/CSUtil.Commons/ArrowDirection.cs
index 0c3068f6e..fcf2e295c 100644
--- a/TLM/CSUtil.Commons/ArrowDirection.cs
+++ b/TLM/CSUtil.Commons/ArrowDirection.cs
@@ -1,9 +1,9 @@
-namespace CSUtil.Commons {
+namespace CSUtil.Commons {
public enum ArrowDirection {
None = 0,
Left = 1,
Forward = 2,
Right = 3,
- Turn = 4
+ Turn = 4,
}
}
\ No newline at end of file
diff --git a/TLM/TLM/TLM.csproj b/TLM/TLM/TLM.csproj
index 2ccd172fb..8dade59d5 100644
--- a/TLM/TLM/TLM.csproj
+++ b/TLM/TLM/TLM.csproj
@@ -268,6 +268,7 @@
+
diff --git a/TLM/TLM/UI/MainMenu/TimedTrafficLightsButton.cs b/TLM/TLM/UI/MainMenu/TimedTrafficLightsButton.cs
index bef9131a3..caa79d24e 100644
--- a/TLM/TLM/UI/MainMenu/TimedTrafficLightsButton.cs
+++ b/TLM/TLM/UI/MainMenu/TimedTrafficLightsButton.cs
@@ -6,7 +6,7 @@ public class TimedTrafficLightsButton : MenuToolModeButton {
protected override ButtonFunction Function => ButtonFunction.TimedTrafficLights;
- public override string Tooltip => Translation.Menu.Get("Tooltip:Timed traffic lights");
+ public override string Tooltip => Translation.Menu.Get("Tooltip:Timed traffic lights") + "\n" + Translation.Menu.Get("Tooltip.Keybinds:Auto TL");
public override bool Visible => Options.timedLightsEnabled;
}
diff --git a/TLM/TLM/UI/SubTools/TimedTrafficLightsTool.cs b/TLM/TLM/UI/SubTools/TimedTrafficLightsTool.cs
index 8c506bf69..5a2609042 100644
--- a/TLM/TLM/UI/SubTools/TimedTrafficLightsTool.cs
+++ b/TLM/TLM/UI/SubTools/TimedTrafficLightsTool.cs
@@ -1,4 +1,5 @@
-namespace TrafficManager.UI.SubTools {
+namespace TrafficManager.UI.SubTools {
+ using TrafficManager.Util;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -99,9 +100,36 @@ public override void OnSecondaryClickOverlay() {
}
public override void OnPrimaryClickOverlay() {
- if (HoveredNodeId <= 0 || nodeSelectionLocked) {
+ if (HoveredNodeId <= 0 || nodeSelectionLocked || !Flags.MayHaveTrafficLight(HoveredNodeId)) {
return;
}
+ bool ctrlDown = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
+ if(ctrlDown) {
+ AutoTimedTrafficLights.ErrorResult res = AutoTimedTrafficLights.Setup(HoveredNodeId);
+ if (res != AutoTimedTrafficLights.ErrorResult.Success) {
+ string message;
+ switch (res) {
+ case AutoTimedTrafficLights.ErrorResult.NotSupported:
+ message = "Dialog.Text:Auto TL no need";
+ break;
+ case AutoTimedTrafficLights.ErrorResult.TTLExists:
+ message = "Dialog.Text:Node has timed TL script";
+ break;
+ default: //Unreachable code
+ message = $"error = {res}";
+ break;
+ }
+ message =
+ Translation.TrafficLights.Get("Dialog.Text:Auto TL create failed because") +
+ "\n" +
+ Translation.TrafficLights.Get(message);
+ MainTool.ShowError(message);
+ return;
+ }
+ RefreshCurrentTimedNodeIds(HoveredNodeId);
+ MainTool.SetToolMode(ToolMode.TimedLightsShowLights);
+ }
+
TrafficLightSimulationManager tlsMan = TrafficLightSimulationManager.Instance;
@@ -249,7 +277,10 @@ public override void OnToolGUI(Event e) {
switch (MainTool.GetToolMode()) {
case ToolMode.TimedLightsSelectNode: {
- GuiTimedTrafficLightsNode();
+ bool ctrlDown = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
+ if (!ctrlDown) {
+ GuiTimedTrafficLightsNode();
+ }
break;
}
diff --git a/TLM/TLM/Util/AutoTimedTrafficLights.cs b/TLM/TLM/Util/AutoTimedTrafficLights.cs
new file mode 100644
index 000000000..d59bde3da
--- /dev/null
+++ b/TLM/TLM/Util/AutoTimedTrafficLights.cs
@@ -0,0 +1,430 @@
+namespace TrafficManager.Util {
+ using TrafficManager.Manager.Impl;
+ using System.Collections.Generic;
+ using ColossalFramework;
+ using UnityEngine;
+ using GenericGameBridge.Service;
+ using API.TrafficLight;
+ using API.TrafficLight.Data;
+ using API.Traffic.Enums;
+ using TrafficManager.API.Manager;
+ using TrafficManager.API.Traffic.Data;
+ using CSUtil.Commons;
+
+ public static class AutoTimedTrafficLights {
+ ///
+ /// allocate dedicated turning lanes.
+ ///
+ private static readonly bool SeparateLanes = true;
+
+ ///
+ /// allow cars to take the short turn whenever there is the opportunity. LHT is opposite
+ ///
+ private static readonly bool AllowShortTurns = true;
+
+ ///
+ /// Due to game limitations, sometimes allowing short turn can lead to car collisions, unless if
+ /// timed traffic lights interface changes. should we currently do not setup lane connector. Should we
+ /// allow short turns in such situations anyways?
+ ///
+ private static readonly bool AllowCollidingShortTurns = false;
+
+ //Shortcuts:
+ private static bool RHT => !Constants.ServiceFactory.SimulationService.TrafficDrivesOnLeft;
+ private static TrafficLightSimulationManager tlsMan = TrafficLightSimulationManager.Instance;
+ private static INetService netService = Constants.ServiceFactory.NetService;
+ private static CustomSegmentLightsManager customTrafficLightsManager = CustomSegmentLightsManager.Instance;
+ private static IExtSegmentManager segMan = Constants.ManagerFactory.ExtSegmentManager;
+ private static IExtSegmentEndManager segEndMan = Constants.ManagerFactory.ExtSegmentEndManager;
+ private static ref TrafficLightSimulation Sim(ushort nodeId) => ref tlsMan.TrafficLightSimulations[nodeId];
+ private static ref ITimedTrafficLights TimedLight(ushort nodeId) => ref Sim(nodeId).timedLight;
+
+ ///
+ /// The directions toward which the traffic light is green
+ ///
+ private enum GreenDir {
+ AllRed,
+ AllGreen,
+ ShortOnly
+ }
+
+ public enum ErrorResult {
+ Success = 0,
+ NoJunction,
+ NotSupported,
+ TTLExists,
+ Other,
+ }
+
+ ///
+ /// creats a sorted list of segmetns connected to nodeId.
+ /// roads without outgoing lanes are excluded as they do not need traffic lights
+ /// the segments are arranged in a clockwise direction (Counter clock wise for LHT).
+ ///
+ /// the junction
+ /// a list of segments aranged in counter clockwise direction.
+ private static List ArrangedSegments(ushort nodeId) {
+ ClockDirection clockDir = RHT ? ClockDirection.CounterClockwise : ClockDirection.CounterClockwise;
+ List segList = new List();
+ netService.IterateNodeSegments(
+ nodeId,
+ clockDir,
+ (ushort segId, ref NetSegment _) => {
+ if (CountOutgoingLanes(segId, nodeId) > 0) {
+ segList.Add(segId);
+ }
+ return true;
+ });
+;
+ return segList;
+ }
+
+ ///
+ /// Adds an empty timed traffic light if it does not already exists.
+ /// additionally allocates dedicated turning lanes if possible.
+ ///
+ /// the junction for which we want a traffic light
+ /// true if sucessful
+ public static bool Add(ushort nodeId) {
+ List nodeGroup = new List(1);
+ nodeGroup.Add(nodeId);
+ return tlsMan.SetUpTimedTrafficLight(nodeId, nodeGroup);
+ }
+
+ ///
+ /// Creates and configures default traffic the input junction
+ ///
+ /// input junction
+ /// true if successful
+ public static ErrorResult Setup(ushort nodeId) {
+ if(tlsMan.HasTimedSimulation(nodeId)) {
+ return ErrorResult.TTLExists;
+ }
+
+ // issue #575: Support level crossings.
+ NetNode.Flags flags = Singleton.instance.m_nodes.m_buffer[nodeId].m_flags;
+ if((flags & NetNode.Flags.LevelCrossing) != 0) {
+ return ErrorResult.NotSupported;
+ }
+
+ var segList = ArrangedSegments(nodeId);
+ int n = segList.Count;
+
+ if (n < 3) {
+ return ErrorResult.NotSupported;
+ }
+
+ if (!Add(nodeId)) {
+ return ErrorResult.Other;
+ }
+
+ if (SeparateLanes) {
+ LaneArrowManager.SeparateTurningLanes.SeparateNode(nodeId, out _);
+ }
+
+ //Is it special case:
+ {
+ var segList2Way = TwoWayRoads(segList, out int n2);
+ if (n2 < 2) {
+ return ErrorResult.NotSupported;
+ }
+ bool b = HasIncommingOneWaySegment(nodeId);
+ if (n2 == 2 && !b) {
+ return SetupSpecial(nodeId, segList2Way);
+ }
+ }
+
+ for (int i = 0; i < n; ++i) {
+ ITimedTrafficLightsStep step = TimedLight(nodeId).AddStep(
+ minTime: 3,
+ maxTime: 8,
+ changeMetric: StepChangeMetric.Default,
+ waitFlowBalance: 0.3f,
+ makeRed:true);
+
+ SetupHelper(step, nodeId, segList[i], GreenDir.AllGreen);
+
+ ushort nextSegmentId = segList[(i + 1) % n];
+ if ( NeedsShortOnly(nextSegmentId, nodeId)) {
+ SetupHelper(step, nodeId, nextSegmentId, GreenDir.ShortOnly);
+ } else {
+ SetupHelper(step, nodeId, nextSegmentId, GreenDir.AllRed);
+ }
+ for (int j = 2; j < n; ++j) {
+ SetupHelper(step, nodeId, segList[(i + j) % n], GreenDir.AllRed);
+ }
+ }
+
+ Sim(nodeId).Housekeeping();
+ TimedLight(nodeId).Start();
+ return ErrorResult.Success;
+ }
+
+ ///
+ /// speical case where:
+ /// multiple outgoing one way roads. only two 2way roads.
+ /// - each 1-way road gets a go
+ /// - then the two 2-way roads get a go.
+ /// this way we can save one step.
+ ///
+ ///
+ ///
+ ///
+ private static ErrorResult SetupSpecial(ushort nodeId, List segList2Way) {
+ var segList1Way = OneWayRoads(nodeId, out var n1);
+
+ // the two 2-way roads get a go.
+ {
+ ITimedTrafficLightsStep step = TimedLight(nodeId).AddStep(
+ minTime: 3,
+ maxTime: 8,
+ changeMetric: StepChangeMetric.Default,
+ waitFlowBalance: 0.3f,
+ makeRed: true);
+
+ SetupHelper(step, nodeId, segList2Way[0], GreenDir.AllGreen);
+ SetupHelper(step, nodeId, segList2Way[1], GreenDir.AllGreen);
+ foreach (var segId in segList1Way) {
+ SetupHelper(step, nodeId, segId, GreenDir.AllRed);
+ }
+ }
+
+ //each 1-way road gets a go
+ for (int i = 0; i < n1; ++i) {
+ ITimedTrafficLightsStep step = TimedLight(nodeId).AddStep(
+ minTime: 3,
+ maxTime: 8,
+ changeMetric: StepChangeMetric.Default,
+ waitFlowBalance: 0.3f,
+ makeRed: true);
+
+ SetupHelper(step, nodeId, segList1Way[i], GreenDir.AllGreen);
+ for (int j = 1; j < n1; ++j) {
+ SetupHelper(step, nodeId, segList1Way[(i + j) % n1], GreenDir.AllRed);
+ }
+ foreach (var segId in segList2Way) {
+ SetupHelper(step, nodeId, segId, GreenDir.AllRed);
+ }
+ }
+
+ Sim(nodeId).Housekeeping();
+ TimedLight(nodeId).Start();
+ return ErrorResult.Success;
+ }
+
+ ///
+ /// Configures traffic light for and for all lane types at input segmentId, nodeId, and step.
+ ///
+ ///
+ ///
+ ///
+ /// Determines which directions are green
+ private static void SetupHelper(ITimedTrafficLightsStep step, ushort nodeId, ushort segmentId, GreenDir m) {
+ bool startNode = (bool)netService.IsStartNode(segmentId, nodeId);
+
+ //get step data for side seg
+ ICustomSegmentLights liveSegmentLights = customTrafficLightsManager.GetSegmentLights(segmentId, startNode);
+
+ //for each lane type
+ foreach (ExtVehicleType vehicleType in liveSegmentLights.VehicleTypes) {
+ //set light mode
+ ICustomSegmentLight liveSegmentLight = liveSegmentLights.GetCustomLight(vehicleType);
+ liveSegmentLight.CurrentMode = LightMode.All;
+
+ TimedLight(nodeId).ChangeLightMode(
+ segmentId,
+ vehicleType,
+ liveSegmentLight.CurrentMode);
+
+ // set light states
+ var green = RoadBaseAI.TrafficLightState.Green;
+ var red = RoadBaseAI.TrafficLightState.Red;
+ switch (m) {
+ case GreenDir.AllRed:
+ liveSegmentLight.SetStates(red, red, red);
+ break;
+
+ case GreenDir.AllGreen:
+ liveSegmentLight.SetStates(green, green, green);
+ break;
+
+ case GreenDir.ShortOnly: {
+ // calculate directions
+ ref ExtSegmentEnd segEnd = ref segEndMan.ExtSegmentEnds[segEndMan.GetIndex(segmentId, nodeId)];
+ ref NetNode node = ref Singleton.instance.m_nodes.m_buffer[nodeId];
+ segEndMan.CalculateOutgoingLeftStraightRightSegments(ref segEnd, ref node, out bool bLeft, out bool bForward, out bool bRight);
+ bool bShort = RHT ? bRight : bLeft;
+ bool bLong = RHT ? bLeft : bRight;
+
+ if (bShort) {
+ SetStates(liveSegmentLight, red, red, green);
+ } else if (bLong) {
+ // go forward instead of short
+ SetStates(liveSegmentLight, green, red, red);
+ } else {
+ Debug.LogAssertion("Unreachable code.");
+ liveSegmentLight.SetStates(green, green, green);
+ }
+ break;
+ }
+ default:
+ Debug.LogAssertion("Unreachable code.");
+ liveSegmentLight.SetStates(green, green, green);
+ break;
+ } // end switch
+ } // end foreach
+ step.UpdateLights(); //save
+ }
+
+ ///
+ /// converst forward, short-turn and far-turn to mainLight, leftLigh, rightLight respectively according to
+ /// whether the traffic is RHT or LHT
+ ///
+ private static void SetStates(
+ ICustomSegmentLight liveSegmentLight,
+ RoadBaseAI.TrafficLightState sForard,
+ RoadBaseAI.TrafficLightState sFar,
+ RoadBaseAI.TrafficLightState sShort) {
+ if (RHT) {
+ liveSegmentLight.SetStates(mainLight:sForard, leftLight: sFar, rightLight:sShort);
+ } else {
+ liveSegmentLight.SetStates(mainLight: sForard, leftLight: sShort, rightLight: sFar);
+ }
+ }
+
+ private static bool HasIncommingOneWaySegment(ushort nodeId) {
+ ref NetNode node = ref Singleton.instance.m_nodes.m_buffer[nodeId];
+ for (int i = 0; i < 8; ++i) {
+ var segId = node.GetSegment(i);
+ if (segId != 0 && segMan.CalculateIsOneWay(segId)) {
+ int n = CountIncomingLanes(segId, nodeId);
+ int dummy = CountOutgoingLanes(segId, nodeId);
+ if (n > 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /// filters out oneway roads from segList. Assumes all segments in seglist have outgoing lanes.
+ /// List of segments returned by SWSegments.
+ /// number of two way roads connected to the junction
+ /// A list of all two way roads connected to the junction
+ private static List TwoWayRoads(List segList, out int count) {
+ List segList2 = new List();
+ foreach(var segId in segList) {
+ if (!segMan.CalculateIsOneWay(segId)) {
+ segList2.Add(segId);
+ }
+ }
+ count = segList2.Count;
+ return segList2;
+ }
+
+ ///
+ /// number of all one way roads at input junction.
+ /// list of all oneway roads connected to input junction
+ private static List OneWayRoads(ushort nodeId, out int count) {
+ List segList2 = new List();
+ ref NetNode node = ref Singleton.instance.m_nodes.m_buffer[nodeId];
+ for (int i = 0; i < 8; ++i) {
+ var segId = node.GetSegment(i);
+ if (segMan.CalculateIsOneWay(segId)) {
+ segList2.Add(segId);
+ }
+ }
+ count = segList2.Count;
+ return segList2;
+ }
+
+ ///
+ /// wierd road connections where short turn is possible only if:
+ /// - timed traffic light gives more control over directions to turn to.
+ /// - lane connector is used.
+ /// Note: for other more normal cases furthur chaking is performed in SetupHelper() to determine if a short turn is necessary.
+ ///
+ ///
+ ///
+ ///
+ /// false AllowShortTurns == false
+ /// otherwise true if AllowCollidingShortTurns == true
+ /// otherwise false if it is special case described above.
+ /// otherwise true if short turn is easily possible, without complications.
+ /// otherwise false.
+ ///
+ private static bool NeedsShortOnly(ushort segmentId, ushort nodeId) {
+ if (!AllowShortTurns) {
+ return false;
+ }
+ if (AllowCollidingShortTurns) {
+ return true;
+ }
+ ref NetSegment seg = ref Singleton.instance.m_segments.m_buffer[segmentId];
+ ArrowDirection shortDir = RHT ? ArrowDirection.Right : ArrowDirection.Left;
+ int nShort = CountDirSegs(segmentId, nodeId, shortDir);
+
+ if (nShort > 1) {
+ return false;
+ }
+ if (nShort == 1) {
+ ushort nextSegmentId = RHT? seg.GetRightSegment(nodeId) : seg.GetLeftSegment(nodeId);
+ return !segMan.CalculateIsOneWay(nextSegmentId);
+ }
+ int nForward = CountDirSegs(segmentId, nodeId, ArrowDirection.Forward);
+ if (nForward > 1) {
+ return false;
+ }
+ if (nForward == 1) {
+ // RHT: if there are not segments to the right GetRightSegment() returns the forward segment.
+ // LHT: if there are not segments to the left GetLeftSegment() returns the forward segment.
+ ushort nextSegmentId = RHT ? seg.GetRightSegment(nodeId) : seg.GetLeftSegment(nodeId);
+ return !segMan.CalculateIsOneWay(nextSegmentId);
+ }
+ return false;
+ }
+
+ ///
+ /// count the lanes going out or comming in a segment from inpout junctions.
+ ///
+ ///
+ ///
+ /// true if lanes our going out toward the junction
+ ///
+ private static int CountLanes(ushort segmentId, ushort nodeId, bool outgoing = true) {
+ return netService.GetSortedLanes(
+ segmentId,
+ ref Singleton.instance.m_segments.m_buffer[segmentId],
+ netService.IsStartNode(segmentId, nodeId) ^ (!outgoing),
+ LaneArrowManager.LANE_TYPES,
+ LaneArrowManager.VEHICLE_TYPES,
+ true
+ ).Count;
+ }
+ private static int CountOutgoingLanes(ushort segmentId, ushort nodeId) => CountLanes(segmentId, nodeId, true);
+ private static int CountIncomingLanes(ushort segmentId, ushort nodeId) => CountLanes(segmentId, nodeId, false);
+
+
+ ///
+ /// Counts the number of roads toward the given directions.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static int CountDirSegs(ushort segmentId, ushort nodeId, ArrowDirection dir) {
+ ExtSegmentEnd segEnd = segEndMan.ExtSegmentEnds[segEndMan.GetIndex(segmentId, nodeId)];
+ int ret = 0;
+
+ netService.IterateNodeSegments(
+ nodeId,
+ (ushort segId, ref NetSegment seg) => {
+ if (segEndMan.GetDirection(ref segEnd, segId) == dir) {
+ ret++;
+ }
+ return true;
+ });
+ return ret;
+ }
+ }
+}