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; + } + } +}