diff --git a/TLM/TLM/Lifecycle/SerializableDataExtension.cs b/TLM/TLM/Lifecycle/SerializableDataExtension.cs index b796b64d5..092180848 100644 --- a/TLM/TLM/Lifecycle/SerializableDataExtension.cs +++ b/TLM/TLM/Lifecycle/SerializableDataExtension.cs @@ -11,6 +11,12 @@ namespace TrafficManager.Lifecycle { using TrafficManager.Manager.Impl.LaneConnection; using TrafficManager.State; using Util; + using System.Linq; + using System.Text; + using System.Xml.Linq; + using TrafficManager.Persistence; + using System.Xml; + using ICSharpCode.SharpZipLib.GZip; [UsedImplicitly] public class SerializableDataExtension @@ -22,6 +28,8 @@ public class SerializableDataExtension private const string VERSION_INFO_DATA_ID = "TrafficManager_VersionInfo_v1.0"; private static ISerializableData SerializableData => SimulationManager.instance.m_SerializableDataWrapper; + private static Dictionary _domCollection = new Dictionary(ReferenceEqualityComparer.Instance); + private static HashSet _persistenceMigration = new HashSet(ReferenceEqualityComparer.Instance); private static Configuration _configuration; private static VersionInfoConfiguration _versionInfoConfiguration; @@ -64,15 +72,35 @@ public static void Load() { loadingSucceeded = false; } + try { + LoadDomCollection(); + } + catch (Exception e) { + Log.Error($"OnLoadData: Error while loading DOM collection: {e}"); + } + + bool loadedData = false; try { byte[] data = SerializableData.LoadData(DATA_ID); DeserializeData(data); + loadedData = true; } catch (Exception e) { Log.Error($"OnLoadData: Error while deserializing data: {e}"); + } + + bool loadedContainers = false; + try { + LoadDomElements(); + loadedContainers = true; + } + catch (Exception e) { + Log.Error($"OnLoadData: Error while deserializing containers: {e}"); loadingSucceeded = false; } + loadingSucceeded &= loadedData || loadedContainers; + // load options (empty byte array causes default options to be applied) try { if (TMPELifecycle.InGameOrEditor()) { @@ -153,11 +181,113 @@ private static void DeserializeVersionData(byte[] data) { } } + private static void LoadDomCollection() { + + _domCollection.Clear(); + _persistenceMigration.Clear(); + + foreach (var po in GlobalPersistence.PersistentObjects) { + + byte[] data; + try { + data = SerializableData.LoadData(po.DependencyTarget.FullName); + } + catch { + Log.Info($"No DOM found for {po.DependencyTarget.Name}"); + data = null; + } + + try { + if (data?.Length > 0) { + + Log.Info($"Attempting to load DOM for {po.DependencyTarget.Name}"); + + try { + using (var memoryStream = new MemoryStream(data)) { + using (var compressionStream = new GZipInputStream(memoryStream)) { + using (var streamReader = new StreamReader(compressionStream, Encoding.UTF8)) { + + XmlReaderSettings xmlReaderSettings = new XmlReaderSettings { + ProhibitDtd = false, + XmlResolver = null, + }; + using (var xmlReader = XmlReader.Create(streamReader, xmlReaderSettings)) + _domCollection[po] = XDocument.Load(xmlReader); + } + } + } + } + catch (Exception ex) { + + Log.Error("Load DOM failed, attempting without compression. " + ex); + + using (var memoryStream = new MemoryStream(data)) { + using (var streamReader = new StreamReader(memoryStream, Encoding.UTF8)) { + + XmlReaderSettings xmlReaderSettings = new XmlReaderSettings { + ProhibitDtd = false, + XmlResolver = null, + }; + using (var xmlReader = XmlReader.Create(streamReader, xmlReaderSettings)) + _domCollection[po] = XDocument.Load(xmlReader); + } + } + + Log.Info("Load DOM without compression succeeded."); + + } + + if (_domCollection[po]?.Root?.Elements(po.ElementName)?.Any(e => po.CanLoad(e)) == true) + _persistenceMigration.Add(po.DependencyTarget); +#if DEBUGLOAD + Log._Debug($"Loaded DOM for {po.DependencyTarget.Name}: {_domCollection[po]}\r"); +#endif + } + } + catch (Exception ex) { + Log.Error($"Error loading DOM for {po.DependencyTarget.Name}: {ex}"); + Log.Info(ex.StackTrace); + } + } + } + + private static void LoadDomElements() { + + try { + foreach (var e in _domCollection.OrderBy(e => e.Key)) { + var po = e.Key; + var dom = e.Value; + + var elements = dom.Root.Elements(po.ElementName)?.Where(po.CanLoad); + if (elements?.Any() == true) { + if (elements.Count() > 1) { + Log.Error($"More than one compatible element {po.ElementName} was found for {po.DependencyTarget.Name}. Using the first one."); + } + try { + XElement element = elements.First(); + Log.Info($"Calling LoadData of DOM element {po.ElementName} for {po.DependencyTarget.Name}, {element.Attribute("featuresRequired")} {element.Attribute("featuresForbidden")}"); + var result = po.LoadData(element, new PersistenceContext { Version = Version }); + result.LogMessage($"LoadData of DOM element {po.ElementName} for {po.DependencyTarget.Name} reported {result}."); + } + catch (Exception ex) { + Log.Error($"Error loading DOM element {po.ElementName} for {po.DependencyTarget.Name}: {ex}"); + Log.Info(ex.StackTrace); + } + } + } + } + catch (Exception ex) { + Log.Error($"Error loading DOM elements: {ex}"); + Log.Info(ex.StackTrace); + throw new ApplicationException("An error occurred while loading"); + } + } + private static void DeserializeData(byte[] data) { bool error = false; try { if (data != null && data.Length != 0) { - Log.Info($"Loading Data from New Load Routine! Length={data.Length}"); + Log.Info($"Loading Data from Old 'New' Load Routine! Length={data.Length}"); var memoryStream = new MemoryStream(); memoryStream.Write(data, 0, data.Length); memoryStream.Position = 0; @@ -202,145 +332,48 @@ private static void ReportVersionInfo(out bool error) { } } - private static void LoadDataState(out bool error) { - error = false; - - Log.Info("Loading State from Config"); - if (_configuration == null) { - Log.Warning("Configuration NULL, Couldn't load save data. Possibly a new game?"); - return; - } - - // load ext. citizens - if (_configuration.ExtCitizens != null) { - if (!ExtCitizenManager.Instance.LoadData(_configuration.ExtCitizens)) { - error = true; - } - } else { - Log.Info("Ext. citizen data structure undefined!"); - } - - // load ext. citizen instances - if (_configuration.ExtCitizenInstances != null) { - if (!ExtCitizenInstanceManager.Instance.LoadData(_configuration.ExtCitizenInstances)) { - error = true; - } - } else { - Log.Info("Ext. citizen instance data structure undefined!"); - } - - // load priority segments - if (_configuration.PrioritySegments != null) { - if (!TrafficPriorityManager.Instance.LoadData(_configuration.PrioritySegments)) { - error = true; - } - } else { - Log.Info("Priority segments data structure (old) undefined!"); - } - - if (_configuration.CustomPrioritySegments != null) { - if (!TrafficPriorityManager.Instance.LoadData(_configuration.CustomPrioritySegments)) { - error = true; - } - } else { - Log.Info("Priority segments data structure (new) undefined!"); - } - - // load parking restrictions - if (_configuration.ParkingRestrictions != null) { - if (!ParkingRestrictionsManager.Instance.LoadData(_configuration.ParkingRestrictions)) { - error = true; - } - } else { - Log.Info("Parking restrctions structure undefined!"); - } - - // load vehicle restrictions (warning: has to be done before loading timed lights!) - if (_configuration.LaneAllowedVehicleTypes != null) { - if (!VehicleRestrictionsManager.Instance.LoadData(_configuration.LaneAllowedVehicleTypes)) { - error = true; - } - } else { - Log.Info("Vehicle restrctions structure undefined!"); - } - - if (_configuration.TimedLights != null) { - if (!TrafficLightSimulationManager.Instance.LoadData(_configuration.TimedLights)) { - error = true; - } - } else { - Log.Info("Timed traffic lights data structure undefined!"); - } - - // load toggled traffic lights (old method) - if (_configuration.NodeTrafficLights != null) { - if (!TrafficLightManager.Instance.LoadData(_configuration.NodeTrafficLights)) { - error = true; + private static void LoadDataState(ICustomDataManager manager, T data, string description, ref bool error) { + if (_persistenceMigration?.Contains(manager.GetType()) == true) { + if (data != null) { + Log.Info($"{manager.GetType().FullName} is in migration to DOM. {description} data structure ignored."); } - } else { - Log.Info("Junction traffic lights data structure (old) undefined!"); } - - // load toggled traffic lights (new method) - if (_configuration.ToggledTrafficLights != null) { - if (!TrafficLightManager.Instance.LoadData(_configuration.ToggledTrafficLights)) { - error = true; + else if (data != null) { + if (GlobalPersistence.PersistentObjects.Any(o => o.DependencyTarget == manager.GetType())) { + Log.Info($"Reading legacy {description} data structure (no DOM element was found)."); } - } else { - Log.Info("Junction traffic lights data structure (new) undefined!"); - } - - // load lane arrrows (old method) - if (_configuration.LaneFlags != null) { - if (!LaneArrowManager.Instance.LoadData(_configuration.LaneFlags)) { - error = true; - } - } else { - Log.Info("Lane arrow data structure (old) undefined!"); - } - - // load lane arrows (new method) - if (_configuration.LaneArrows != null) { - if (!LaneArrowManager.Instance.LoadData(_configuration.LaneArrows)) { - error = true; - } - } else { - Log.Info("Lane arrow data structure (new) undefined!"); - } - - // load lane connections - if (_configuration.LaneConnections != null) { - if (!LaneConnectionManager.Instance.LoadData(_configuration.LaneConnections)) { + if (!manager.LoadData(data)) { error = true; } - } else { - Log.Info("Lane connection data structure undefined!"); + } else if (description != null) { + Log.Info($"{description} data structure undefined!"); } + } - // Load custom default speed limits - if (_configuration.CustomDefaultSpeedLimits != null) { - if (!SpeedLimitManager.Instance.LoadData(_configuration.CustomDefaultSpeedLimits)) { - error = true; - } - } + private static void LoadDataState(out bool error) { + error = false; - // load speed limits - if (_configuration.LaneSpeedLimits != null) { - if (!SpeedLimitManager.Instance.LoadData(_configuration.LaneSpeedLimits)) { - error = true; - } - } else { - Log.Info("Lane speed limit structure undefined!"); + Log.Info("Loading State from Config"); + if (_configuration == null) { + Log.Warning("Configuration NULL, Couldn't load save data. Possibly a new game?"); + return; } - // Load segment-at-node flags - if (_configuration.SegmentNodeConfs != null) { - if (!JunctionRestrictionsManager.Instance.LoadData(_configuration.SegmentNodeConfs)) { - error = true; - } - } else { - Log.Info("Segment-at-node structure undefined!"); - } + LoadDataState(ExtCitizenManager.Instance, _configuration.ExtCitizens, "Ext. citizen", ref error); + LoadDataState(ExtCitizenInstanceManager.Instance, _configuration.ExtCitizenInstances, "Ext. citizen instance", ref error); + LoadDataState(TrafficPriorityManager.Instance, _configuration.PrioritySegments, "Priority segments (old)", ref error); + LoadDataState(TrafficPriorityManager.Instance, _configuration.CustomPrioritySegments, "Priority segments (new)", ref error); + LoadDataState(ParkingRestrictionsManager.Instance, _configuration.ParkingRestrictions, "Parking restrctions", ref error); + LoadDataState(VehicleRestrictionsManager.Instance, _configuration.LaneAllowedVehicleTypes, "Vehicle restrctions", ref error); + LoadDataState(TrafficLightSimulationManager.Instance, _configuration.TimedLights, "Timed traffic lights", ref error); + LoadDataState(TrafficLightManager.Instance, _configuration.NodeTrafficLights, "Junction traffic lights (old)", ref error); + LoadDataState(TrafficLightManager.Instance, _configuration.ToggledTrafficLights, "Junction traffic lights (new)", ref error); + LoadDataState(LaneArrowManager.Instance, _configuration.LaneFlags, "Lane arrow (old)", ref error); + LoadDataState(LaneArrowManager.Instance, _configuration.LaneArrows, "Lane arrow (new)", ref error); + LoadDataState(LaneConnectionManager.Instance, _configuration.LaneConnections, "Lane connection", ref error); + LoadDataState(SpeedLimitManager.Instance, _configuration.CustomDefaultSpeedLimits, "Default speed limit", ref error); + LoadDataState(SpeedLimitManager.Instance, _configuration.LaneSpeedLimits, "Lane speed limit", ref error); + LoadDataState(JunctionRestrictionsManager.Instance, _configuration.SegmentNodeConfs, "Segment-at-node", ref error); } public static void Save() { @@ -436,10 +469,12 @@ public static void Save() { memoryStreamVersion.Position = 0; Log.Info($"Version data byte length {memoryStreamVersion.Length}"); SerializableData.SaveData(VERSION_INFO_DATA_ID, memoryStreamVersion.ToArray()); - } catch (Exception ex) { + } + catch (Exception ex) { Log.Error("Unexpected error while saving version data: " + ex); success = false; - } finally { + } + finally { memoryStreamVersion.Close(); } @@ -447,7 +482,8 @@ public static void Save() { if (TMPELifecycle.PlayMode) { SerializableData.SaveData("TMPE_Options", OptionsManager.Instance.SaveData(ref success)); } - } catch (Exception ex) { + } + catch (Exception ex) { Log.Error("Unexpected error while saving options: " + ex.Message); success = false; } @@ -460,13 +496,17 @@ public static void Save() { memoryStream.Position = 0; Log.Info($"Save data byte length {memoryStream.Length}"); SerializableData.SaveData(DATA_ID, memoryStream.ToArray()); - } catch (Exception ex) { + } + catch (Exception ex) { Log.Error("Unexpected error while saving data: " + ex); success = false; - } finally { + } + finally { memoryStream.Close(); } + SaveDom(ref success); + var reverseManagers = new List(TMPELifecycle.Instance.RegisteredManagers); reverseManagers.Reverse(); foreach (ICustomManager manager in reverseManagers) { @@ -480,7 +520,8 @@ public static void Save() { success = false; } } - } catch (Exception e) { + } + catch (Exception e) { success = false; Log.Error($"Error occurred while saving data: {e}"); @@ -491,5 +532,75 @@ public static void Save() { // the steps under 'In case problems arise'.", true); } } + + private static void SaveDom(ref bool success) { + + foreach (var po in GlobalPersistence.PersistentObjects.OrderBy(o => o)) { + + try { + var dom = new XDocument(); + dom.Add(new XElement(po.DependencyTarget.Name)); + + var result = po.SaveData(dom.Root, new PersistenceContext { Version = Version }); + + result.LogMessage($"SaveData of DOM element {po.ElementName} for {po.DependencyTarget.Name} reported {result}."); + +#if DEBUGSAVE + Log._Debug($"Saving DOM for {po.DependencyTarget.Name}: {dom}"); +#endif + + try { + using (var memoryStream = new MemoryStream()) { + + using (var compressionStream = new GZipOutputStream(memoryStream)) { + + using (var streamWriter = new StreamWriter(compressionStream, Encoding.UTF8)) { + + dom.Save(streamWriter); + + streamWriter.Flush(); + compressionStream.Finish(); + + memoryStream.Position = 0; + Log.Info($"Save DOM for {po.DependencyTarget.Name} byte length {memoryStream.Length}"); + + SerializableData.SaveData(po.DependencyTarget.FullName, memoryStream.ToArray()); + } + } + } + } + catch (Exception ex) { + + Log.Error("Save DOM failed, attempting without compression. " + ex); + + try { + SerializableData.EraseData(po.DependencyTarget.FullName); + } + catch { + } + + using (var memoryStream = new MemoryStream()) { + using (var streamWriter = new StreamWriter(memoryStream, Encoding.UTF8)) { + + dom.Save(streamWriter); + + memoryStream.Position = 0; + Log.Info($"Save DOM for {po.DependencyTarget.Name} byte length {memoryStream.Length}"); + + SerializableData.SaveData(po.DependencyTarget.FullName, memoryStream.ToArray()); + } + } + + Log.Info("Save DOM without compression succeeded."); + + } + } + catch (Exception ex) { + + Log.Error($"Unexpected error while saving DOM for {po.DependencyTarget.Name}: {ex}"); + success = false; + } + } + } } -} \ No newline at end of file +} diff --git a/TLM/TLM/Manager/Impl/JunctionRestrictionsManager.Persistence.cs b/TLM/TLM/Manager/Impl/JunctionRestrictionsManager.Persistence.cs new file mode 100644 index 000000000..89c18b841 --- /dev/null +++ b/TLM/TLM/Manager/Impl/JunctionRestrictionsManager.Persistence.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using TrafficManager.Manager.Model; +using TrafficManager.Persistence; + +namespace TrafficManager.Manager.Impl { + partial class JunctionRestrictionsManager { + + internal class Persistence : PersistentObject { + public override XName ElementName => "JunctionRestrictions"; + + public static readonly XName segmentElementName = "Segment"; + + public override Type DependencyTarget => typeof(JunctionRestrictionsManager); + + public override IEnumerable GetDependencies() { + yield return typeof(LaneConnection.LaneConnectionManager); + } + + protected override PersistenceResult OnLoadData(XElement element, ICollection featuresRequired, PersistenceContext context) { + + foreach (var segmentElement in element.Elements(segmentElementName)) { + Instance.AddSegmentModel(segmentElement.XDeserialize>()); + } + + return PersistenceResult.Success; + } + + protected override PersistenceResult OnSaveData(XElement element, ICollection featuresRequired, ICollection featuresForbidden, PersistenceContext context) { + var result = PersistenceResult.Success; + + foreach (var segment in Instance.EnumerateSegmentModels()) { + element.Add(segment.XSerialize(segmentElementName)); + } + + return result; + } + + public enum JunctionRestrictionsFeature { + + None = 0, + } + + } + } +} diff --git a/TLM/TLM/Manager/Impl/JunctionRestrictionsManager.cs b/TLM/TLM/Manager/Impl/JunctionRestrictionsManager.cs index be85b81a8..c338232eb 100644 --- a/TLM/TLM/Manager/Impl/JunctionRestrictionsManager.cs +++ b/TLM/TLM/Manager/Impl/JunctionRestrictionsManager.cs @@ -17,9 +17,11 @@ namespace TrafficManager.Manager.Impl { using TrafficManager.Util; using TrafficManager.Util.Extensions; using TrafficManager.Lifecycle; + using TrafficManager.Manager.Model; + using TrafficManager.Persistence; using TrafficManager.API.Traffic.Enums; - public class JunctionRestrictionsManager + public partial class JunctionRestrictionsManager : AbstractGeometryObservingManager, ICustomDataManager>, IJunctionRestrictionsManager @@ -37,8 +39,58 @@ public class JunctionRestrictionsManager private JunctionRestrictionsManager() { segmentRestrictions = new SegmentJunctionRestrictions[NetManager.MAX_SEGMENT_COUNT]; invalidSegmentRestrictions = new SegmentJunctionRestrictions[NetManager.MAX_SEGMENT_COUNT]; + + GlobalPersistence.PersistentObjects.Add(new Persistence()); + } + + private IEnumerable> EnumerateSegmentModels() { + + for (ushort segmentId = 1; segmentId < NetManager.MAX_SEGMENT_COUNT; segmentId++) { + + var netSegment = segmentId.ToSegment(); + + if (netSegment.IsValid() + && netSegment.m_startNode.ToNode().IsValid() + && netSegment.m_endNode.ToNode().IsValid() + && !segmentRestrictions[segmentId].IsDefault()) { + + yield return new SegmentEndPair { + segmentId = segmentId, + start = segmentRestrictions[segmentId].startNodeRestrictions.Model, + end = segmentRestrictions[segmentId].endNodeRestrictions.Model, + }; + } + } + } + + private void AddSegmentModel(SegmentEndPair model) { + + AddSegmentModel(model.start, model.segmentId, true); + AddSegmentModel(model.end, model.segmentId, false); } + private void AddSegmentModel(JunctionRestrictionsModel model, ushort segmentId, bool startNode) { + + ref var segment = ref segmentId.ToSegment(); + ref var node = ref (startNode ? ref segment.m_startNode.ToNode() : ref segment.m_endNode.ToNode()); + + SetFlag(JunctionRestrictionsFlags.AllowUTurn, IsUturnAllowedConfigurable, SetUturnAllowed, ref node); + SetFlag(JunctionRestrictionsFlags.AllowNearTurnOnRed, IsNearTurnOnRedAllowedConfigurable, SetNearTurnOnRedAllowed, ref node); + SetFlag(JunctionRestrictionsFlags.AllowFarTurnOnRed, IsFarTurnOnRedAllowedConfigurable, SetFarTurnOnRedAllowed, ref node); + SetFlag(JunctionRestrictionsFlags.AllowForwardLaneChange, IsLaneChangingAllowedWhenGoingStraightConfigurable, SetLaneChangingAllowedWhenGoingStraight, ref node); + SetFlag(JunctionRestrictionsFlags.AllowEnterWhenBlocked, IsEnteringBlockedJunctionAllowedConfigurable, SetEnteringBlockedJunctionAllowed, ref node); + SetFlag(JunctionRestrictionsFlags.AllowPedestrianCrossing, IsPedestrianCrossingAllowedConfigurable, SetPedestrianCrossingAllowed, ref node); + + void SetFlag(JunctionRestrictionsFlags property, InternalGet isConfigurable, Func set, ref NetNode node) { + + if ((model.mask & property) != 0 && isConfigurable(segmentId, startNode, ref node)) { + set(segmentId, startNode, (model.values & property) != 0); + } + } + } + + private delegate bool InternalGet(ushort segmentId, bool startNode, ref NetNode node); + private void AddInvalidSegmentJunctionRestrictions(ushort segmentId, bool startNode, ref JunctionRestrictions restrictions) { @@ -220,60 +272,60 @@ private void UpdateDefaults(ref ExtSegmentEnd segEnd, ref JunctionRestrictions restrictions, ref NetNode node) { if (!IsUturnAllowedConfigurable(segEnd.segmentId, segEnd.startNode, ref node)) { - restrictions.ClearValue(JunctionRestrictionFlags.AllowUTurn); + restrictions.ClearValue(JunctionRestrictionsFlags.AllowUTurn); } if (!IsNearTurnOnRedAllowedConfigurable(segEnd.segmentId, segEnd.startNode, ref node)) { - restrictions.ClearValue(JunctionRestrictionFlags.AllowNearTurnOnRed); + restrictions.ClearValue(JunctionRestrictionsFlags.AllowNearTurnOnRed); } if (!IsFarTurnOnRedAllowedConfigurable(segEnd.segmentId, segEnd.startNode, ref node)) { - restrictions.ClearValue(JunctionRestrictionFlags.AllowFarTurnOnRed); + restrictions.ClearValue(JunctionRestrictionsFlags.AllowFarTurnOnRed); } if (!IsLaneChangingAllowedWhenGoingStraightConfigurable( segEnd.segmentId, segEnd.startNode, ref node)) { - restrictions.ClearValue(JunctionRestrictionFlags.AllowForwardLaneChange); + restrictions.ClearValue(JunctionRestrictionsFlags.AllowForwardLaneChange); } if (!IsEnteringBlockedJunctionAllowedConfigurable( segEnd.segmentId, segEnd.startNode, ref node)) { - restrictions.ClearValue(JunctionRestrictionFlags.AllowEnterWhenBlocked); + restrictions.ClearValue(JunctionRestrictionsFlags.AllowEnterWhenBlocked); } if (!IsPedestrianCrossingAllowedConfigurable( segEnd.segmentId, segEnd.startNode, ref node)) { - restrictions.ClearValue(JunctionRestrictionFlags.AllowPedestrianCrossing); + restrictions.ClearValue(JunctionRestrictionsFlags.AllowPedestrianCrossing); } - restrictions.SetDefault(JunctionRestrictionFlags.AllowUTurn, GetDefaultUturnAllowed( + restrictions.SetDefault(JunctionRestrictionsFlags.AllowUTurn, GetDefaultUturnAllowed( segEnd.segmentId, segEnd.startNode, ref node)); - restrictions.SetDefault(JunctionRestrictionFlags.AllowNearTurnOnRed, GetDefaultNearTurnOnRedAllowed( + restrictions.SetDefault(JunctionRestrictionsFlags.AllowNearTurnOnRed, GetDefaultNearTurnOnRedAllowed( segEnd.segmentId, segEnd.startNode, ref node)); - restrictions.SetDefault(JunctionRestrictionFlags.AllowFarTurnOnRed, GetDefaultFarTurnOnRedAllowed( + restrictions.SetDefault(JunctionRestrictionsFlags.AllowFarTurnOnRed, GetDefaultFarTurnOnRedAllowed( segEnd.segmentId, segEnd.startNode, ref node)); - restrictions.SetDefault(JunctionRestrictionFlags.AllowForwardLaneChange, + restrictions.SetDefault(JunctionRestrictionsFlags.AllowForwardLaneChange, GetDefaultLaneChangingAllowedWhenGoingStraight( segEnd.segmentId, segEnd.startNode, ref node)); - restrictions.SetDefault(JunctionRestrictionFlags.AllowEnterWhenBlocked, GetDefaultEnteringBlockedJunctionAllowed( + restrictions.SetDefault(JunctionRestrictionsFlags.AllowEnterWhenBlocked, GetDefaultEnteringBlockedJunctionAllowed( segEnd.segmentId, segEnd.startNode, ref node)); - restrictions.SetDefault(JunctionRestrictionFlags.AllowPedestrianCrossing, GetDefaultPedestrianCrossingAllowed( + restrictions.SetDefault(JunctionRestrictionsFlags.AllowPedestrianCrossing, GetDefaultPedestrianCrossingAllowed( segEnd.segmentId, segEnd.startNode, ref node)); @@ -287,12 +339,12 @@ private void UpdateDefaults(ref ExtSegmentEnd segEnd, "defaultEnterWhenBlockedAllowed={6}, defaultPedestrianCrossingAllowed={7}", segEnd.segmentId, segEnd.startNode, - restrictions.GetDefault(JunctionRestrictionFlags.AllowUTurn), - restrictions.GetDefault(JunctionRestrictionFlags.AllowNearTurnOnRed), - restrictions.GetDefault(JunctionRestrictionFlags.AllowFarTurnOnRed), - restrictions.GetDefault(JunctionRestrictionFlags.AllowForwardLaneChange), - restrictions.GetDefault(JunctionRestrictionFlags.AllowEnterWhenBlocked), - restrictions.GetDefault(JunctionRestrictionFlags.AllowPedestrianCrossing)); + restrictions.GetDefault(JunctionRestrictionsFlags.AllowUTurn), + restrictions.GetDefault(JunctionRestrictionsFlags.AllowNearTurnOnRed), + restrictions.GetDefault(JunctionRestrictionsFlags.AllowFarTurnOnRed), + restrictions.GetDefault(JunctionRestrictionsFlags.AllowForwardLaneChange), + restrictions.GetDefault(JunctionRestrictionsFlags.AllowEnterWhenBlocked), + restrictions.GetDefault(JunctionRestrictionsFlags.AllowPedestrianCrossing)); } #endif Notifier.Instance.OnNodeModified(segEnd.nodeId, this); @@ -367,7 +419,7 @@ public bool GetDefaultUturnAllowed(ushort segmentId, bool startNode, ref NetNode } public bool IsUturnAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionFlags.AllowUTurn, startNode); + return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionsFlags.AllowUTurn, startNode); } public bool IsNearTurnOnRedAllowedConfigurable(ushort segmentId, @@ -450,11 +502,11 @@ public bool IsTurnOnRedAllowed(bool near, ushort segmentId, bool startNode) { } public bool IsNearTurnOnRedAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionFlags.AllowNearTurnOnRed, startNode); + return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionsFlags.AllowNearTurnOnRed, startNode); } public bool IsFarTurnOnRedAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionFlags.AllowFarTurnOnRed, startNode); + return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionsFlags.AllowFarTurnOnRed, startNode); } public bool IsLaneChangingAllowedWhenGoingStraightConfigurable( @@ -528,7 +580,7 @@ public bool GetDefaultLaneChangingAllowedWhenGoingStraight(ushort segmentId, boo } public bool IsLaneChangingAllowedWhenGoingStraight(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionFlags.AllowForwardLaneChange, startNode); + return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionsFlags.AllowForwardLaneChange, startNode); } public bool IsEnteringBlockedJunctionAllowedConfigurable( @@ -629,7 +681,7 @@ public bool GetDefaultEnteringBlockedJunctionAllowed( } public bool IsEnteringBlockedJunctionAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionFlags.AllowEnterWhenBlocked, startNode); + return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionsFlags.AllowEnterWhenBlocked, startNode); } public bool IsPedestrianCrossingAllowedConfigurable(ushort segmentId, bool startNode, ref NetNode node) { @@ -717,19 +769,19 @@ public bool GetDefaultPedestrianCrossingAllowed(ushort segmentId, bool startNode } public bool IsPedestrianCrossingAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionFlags.AllowPedestrianCrossing, startNode); + return segmentRestrictions[segmentId].GetValueOrDefault(JunctionRestrictionsFlags.AllowPedestrianCrossing, startNode); } public TernaryBool GetUturnAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionFlags.AllowUTurn, startNode); + return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionsFlags.AllowUTurn, startNode); } public TernaryBool GetNearTurnOnRedAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionFlags.AllowNearTurnOnRed, startNode); + return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionsFlags.AllowNearTurnOnRed, startNode); } public TernaryBool GetFarTurnOnRedAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionFlags.AllowFarTurnOnRed, startNode); + return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionsFlags.AllowFarTurnOnRed, startNode); } public TernaryBool GetTurnOnRedAllowed(bool near, ushort segmentId, bool startNode) { @@ -739,15 +791,15 @@ public TernaryBool GetTurnOnRedAllowed(bool near, ushort segmentId, bool startNo } public TernaryBool GetLaneChangingAllowedWhenGoingStraight(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionFlags.AllowForwardLaneChange, startNode); + return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionsFlags.AllowForwardLaneChange, startNode); } public TernaryBool GetEnteringBlockedJunctionAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionFlags.AllowEnterWhenBlocked, startNode); + return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionsFlags.AllowEnterWhenBlocked, startNode); } public TernaryBool GetPedestrianCrossingAllowed(ushort segmentId, bool startNode) { - return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionFlags.AllowPedestrianCrossing, startNode); + return segmentRestrictions[segmentId].GetTernaryBool(JunctionRestrictionsFlags.AllowPedestrianCrossing, startNode); } public bool ToggleUturnAllowed(ushort segmentId, bool startNode) { @@ -836,37 +888,37 @@ public bool ClearSegmentEnd(ushort segmentId, bool startNode) { } private void SetSegmentJunctionRestrictions(ushort segmentId, bool startNode, JunctionRestrictions restrictions) { - if (restrictions.HasValue(JunctionRestrictionFlags.AllowUTurn)) { - SetUturnAllowed(segmentId, startNode, restrictions.GetValueOrDefault(JunctionRestrictionFlags.AllowUTurn)); + if (restrictions.HasValue(JunctionRestrictionsFlags.AllowUTurn)) { + SetUturnAllowed(segmentId, startNode, restrictions.GetValueOrDefault(JunctionRestrictionsFlags.AllowUTurn)); } - if (restrictions.HasValue(JunctionRestrictionFlags.AllowNearTurnOnRed)) { - SetNearTurnOnRedAllowed(segmentId, startNode, restrictions.GetValueOrDefault(JunctionRestrictionFlags.AllowNearTurnOnRed)); + if (restrictions.HasValue(JunctionRestrictionsFlags.AllowNearTurnOnRed)) { + SetNearTurnOnRedAllowed(segmentId, startNode, restrictions.GetValueOrDefault(JunctionRestrictionsFlags.AllowNearTurnOnRed)); } - if (restrictions.HasValue(JunctionRestrictionFlags.AllowFarTurnOnRed)) { - SetFarTurnOnRedAllowed(segmentId, startNode, restrictions.GetValueOrDefault(JunctionRestrictionFlags.AllowFarTurnOnRed)); + if (restrictions.HasValue(JunctionRestrictionsFlags.AllowFarTurnOnRed)) { + SetFarTurnOnRedAllowed(segmentId, startNode, restrictions.GetValueOrDefault(JunctionRestrictionsFlags.AllowFarTurnOnRed)); } - if (restrictions.HasValue(JunctionRestrictionFlags.AllowForwardLaneChange)) { + if (restrictions.HasValue(JunctionRestrictionsFlags.AllowForwardLaneChange)) { SetLaneChangingAllowedWhenGoingStraight( segmentId, startNode, - restrictions.GetValueOrDefault(JunctionRestrictionFlags.AllowForwardLaneChange)); + restrictions.GetValueOrDefault(JunctionRestrictionsFlags.AllowForwardLaneChange)); } - if (restrictions.HasValue(JunctionRestrictionFlags.AllowEnterWhenBlocked)) { + if (restrictions.HasValue(JunctionRestrictionsFlags.AllowEnterWhenBlocked)) { SetEnteringBlockedJunctionAllowed( segmentId, startNode, - restrictions.GetValueOrDefault(JunctionRestrictionFlags.AllowEnterWhenBlocked)); + restrictions.GetValueOrDefault(JunctionRestrictionsFlags.AllowEnterWhenBlocked)); } - if (restrictions.HasValue(JunctionRestrictionFlags.AllowPedestrianCrossing)) { + if (restrictions.HasValue(JunctionRestrictionsFlags.AllowPedestrianCrossing)) { SetPedestrianCrossingAllowed( segmentId, startNode, - restrictions.GetValueOrDefault(JunctionRestrictionFlags.AllowPedestrianCrossing)); + restrictions.GetValueOrDefault(JunctionRestrictionsFlags.AllowPedestrianCrossing)); } } @@ -894,7 +946,7 @@ public bool SetUturnAllowed(ushort segmentId, bool startNode, TernaryBool value) return false; } - segmentRestrictions[segmentId].SetValue(JunctionRestrictionFlags.AllowUTurn, startNode, value); + segmentRestrictions[segmentId].SetValue(JunctionRestrictionsFlags.AllowUTurn, startNode, value); OnSegmentChange( segmentId, startNode, @@ -931,9 +983,9 @@ public bool SetTurnOnRedAllowed(bool near, ushort segmentId, bool startNode, Ter } if (near) { - segmentRestrictions[segmentId].SetValue(JunctionRestrictionFlags.AllowNearTurnOnRed, startNode, value); + segmentRestrictions[segmentId].SetValue(JunctionRestrictionsFlags.AllowNearTurnOnRed, startNode, value); } else { - segmentRestrictions[segmentId].SetValue(JunctionRestrictionFlags.AllowFarTurnOnRed, startNode, value); + segmentRestrictions[segmentId].SetValue(JunctionRestrictionsFlags.AllowFarTurnOnRed, startNode, value); } OnSegmentChange(segmentId, startNode, ref Constants.ManagerFactory.ExtSegmentManager.ExtSegments[segmentId], true); return true; @@ -955,7 +1007,7 @@ public bool SetLaneChangingAllowedWhenGoingStraight( return false; } - segmentRestrictions[segmentId].SetValue(JunctionRestrictionFlags.AllowForwardLaneChange, startNode, value); + segmentRestrictions[segmentId].SetValue(JunctionRestrictionsFlags.AllowForwardLaneChange, startNode, value); OnSegmentChange( segmentId, startNode, @@ -977,7 +1029,7 @@ public bool SetEnteringBlockedJunctionAllowed(ushort segmentId, bool startNode, return false; } - segmentRestrictions[segmentId].SetValue(JunctionRestrictionFlags.AllowEnterWhenBlocked, startNode, value); + segmentRestrictions[segmentId].SetValue(JunctionRestrictionsFlags.AllowEnterWhenBlocked, startNode, value); // recalculation not needed here because this is a simulation-time feature OnSegmentChange( @@ -1001,7 +1053,7 @@ public bool SetPedestrianCrossingAllowed(ushort segmentId, bool startNode, Terna return false; } - segmentRestrictions[segmentId].SetValue(JunctionRestrictionFlags.AllowPedestrianCrossing, startNode, value); + segmentRestrictions[segmentId].SetValue(JunctionRestrictionsFlags.AllowPedestrianCrossing, startNode, value); OnSegmentChange( segmentId, startNode, @@ -1323,15 +1375,15 @@ private struct SegmentJunctionRestrictions { public JunctionRestrictions startNodeRestrictions; public JunctionRestrictions endNodeRestrictions; - public bool GetValueOrDefault(JunctionRestrictionFlags flags, bool startNode) { + public bool GetValueOrDefault(JunctionRestrictionsFlags flags, bool startNode) { return (startNode ? startNodeRestrictions : endNodeRestrictions).GetValueOrDefault(flags); } - public TernaryBool GetTernaryBool(JunctionRestrictionFlags flags, bool startNode) { + public TernaryBool GetTernaryBool(JunctionRestrictionsFlags flags, bool startNode) { return (startNode ? startNodeRestrictions : endNodeRestrictions).GetTernaryBool(flags); } - public void SetValue(JunctionRestrictionFlags flags, bool startNode, TernaryBool value) { + public void SetValue(JunctionRestrictionsFlags flags, bool startNode, TernaryBool value) { if (startNode) startNodeRestrictions.SetValue(flags, value); else @@ -1362,60 +1414,59 @@ public override string ToString() { private struct JunctionRestrictions { - private JunctionRestrictionFlags values; - - private JunctionRestrictionFlags mask; + private JunctionRestrictionsModel model; - private JunctionRestrictionFlags defaults; + private JunctionRestrictionsFlags defaults; + public JunctionRestrictionsModel Model => model; - public void ClearValue(JunctionRestrictionFlags flags) { - values &= ~flags; - mask &= ~flags; + public void ClearValue(JunctionRestrictionsFlags flags) { + model.values &= ~flags; + model.mask &= ~flags; } - public void SetDefault(JunctionRestrictionFlags flags, bool value) { + public void SetDefault(JunctionRestrictionsFlags flags, bool value) { if (value) defaults |= flags; else defaults &= ~flags; } - public bool GetDefault(JunctionRestrictionFlags flags) { + public bool GetDefault(JunctionRestrictionsFlags flags) { return (defaults & flags) == flags; } - public bool HasValue(JunctionRestrictionFlags flags) { - return (mask & flags) == flags; + public bool HasValue(JunctionRestrictionsFlags flags) { + return (model.mask & flags) == flags; } - public TernaryBool GetTernaryBool(JunctionRestrictionFlags flags) { - return (mask & flags) == flags - ? (values & flags) == flags + public TernaryBool GetTernaryBool(JunctionRestrictionsFlags flags) { + return (model.mask & flags) == flags + ? (model.values & flags) == flags ? TernaryBool.True : TernaryBool.False : TernaryBool.Undefined; } - public bool GetValueOrDefault(JunctionRestrictionFlags flags) { - return ((values & flags & mask) | (defaults & flags & ~mask)) == flags; + public bool GetValueOrDefault(JunctionRestrictionsFlags flags) { + return ((model.values & flags & model.mask) | (defaults & flags & ~model.mask)) == flags; } - public void SetValue(JunctionRestrictionFlags flags, TernaryBool value) { + public void SetValue(JunctionRestrictionsFlags flags, TernaryBool value) { switch (value) { case TernaryBool.True: - values |= flags; - mask |= flags; + model.values |= flags; + model.mask |= flags; break; case TernaryBool.False: - values &= ~flags; - mask |= flags; + model.values &= ~flags; + model.mask |= flags; break; case TernaryBool.Undefined: - values &= ~flags; - mask &= ~flags; + model.values &= ~flags; + model.mask &= ~flags; break; default: @@ -1424,11 +1475,11 @@ public void SetValue(JunctionRestrictionFlags flags, TernaryBool value) { } public bool IsDefault() { - return ((values & mask) | (defaults & ~mask)) == defaults; + return ((model.values & model.mask) | (defaults & ~model.mask)) == defaults; } public void Reset(bool resetDefaults = true) { - values = mask = default; + model.values = model.mask = default; if (resetDefaults) { defaults = default; @@ -1437,7 +1488,7 @@ public void Reset(bool resetDefaults = true) { public override string ToString() { return string.Format( - $"[JunctionRestrictions\n\tvalues = {values}\n\tmask = {mask}\n" + + $"[JunctionRestrictions\n\tvalues = {model.values}\n\tmask = {model.mask}\n" + $"defaults = {defaults}\n" + "JunctionRestrictions]"); } diff --git a/TLM/TLM/Manager/Impl/TrafficLightSimulationManager.Persistence.cs b/TLM/TLM/Manager/Impl/TrafficLightSimulationManager.Persistence.cs new file mode 100644 index 000000000..a9ff197d9 --- /dev/null +++ b/TLM/TLM/Manager/Impl/TrafficLightSimulationManager.Persistence.cs @@ -0,0 +1,409 @@ +using CSUtil.Commons; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using TrafficManager.API.Traffic.Enums; +using TrafficManager.Persistence; +using TrafficManager.TrafficLight.Impl; +using TrafficManager.TrafficLight.Model; +using TrafficManager.Util; +using TrafficManager.Util.Extensions; +using static RoadBaseAI; + +namespace TrafficManager.Manager.Impl { + + partial class TrafficLightSimulationManager { + + internal class Persistence : PersistentObject { + + public enum TtlFeature { + + None = 0, + } + + public override Type DependencyTarget => typeof(TrafficLightSimulationManager); + + public override XName ElementName => "TimedTrafficLights"; + + private static readonly XName ttlNodeElementName = "TtlNode"; + private static readonly XName stepElementName = "Step"; + private static readonly XName segLightsElementName = "SegmentLights"; + private static readonly XName lightElementName = "Light"; + + public override IEnumerable GetDependencies() => null; + + protected override PersistenceResult OnLoadData(XElement element, ICollection featuresRequired, PersistenceContext context) { + + Log.Info($"Loading timed traffic lights (XML method)"); + + // This is really terrible. The way grouped traffic lights are modeled, there are situations + // where the data we've saved can't be used to load grouped traffic lights properly unless + // we scan for and build all the traffic light groups in advance. + + // TODO Legacy code just keeps going if this step fails. Is that okay? + var result = BuildTtlGroups(element, out var masterNodeLookup, out var ttlGroups); + + // Now we can load the traffic lights + + foreach (var ttlElement in element.Elements(ttlNodeElementName)) { + try { + var ttlNodeId = ttlElement.Attribute(nameof(ITimedTrafficLightsModel.NodeId)); + + if (!masterNodeLookup.ContainsKey(ttlNodeId)) { + continue; + } + + ushort masterNodeId = masterNodeLookup[ttlNodeId]; + List nodeGroup = ttlGroups[masterNodeId]; + +#if DEBUGLOAD + Log._Debug($"Adding timed light at node {ttlNodeId}. NodeGroup: " + + $"{string.Join(", ", nodeGroup.Select(x => x.ToString()).ToArray())}"); +#endif + + Instance.SetUpTimedTrafficLight(ttlNodeId, nodeGroup); + + int step = 0; + foreach (var stepElement in ttlElement.Elements(stepElementName)) { +#if DEBUGLOAD + Log._Debug($"Loading timed step {step} at node {ttlNodeId}"); +#endif + LoadStep(stepElement, ttlNodeId); + ++step; + } + } + catch (Exception e) { + // ignore, as it's probably corrupt save data. it'll be culled on next save + Log.Warning($"Error loading data from TimedNode (new method): {e}"); + result = PersistenceResult.Failure; + } + } + + // Since grouped traffic lights are stored in pieces, we can't start + // any of the traffic lights until all of them have been loaded. + + foreach (var ttlElement in element.Elements(ttlNodeElementName)) { + + var nodeId = ttlElement.Attribute(nameof(ITimedTrafficLightsModel.NodeId)); + + try { + TimedTrafficLights timedNode = + Instance.TrafficLightSimulations[nodeId].timedLight; + + timedNode.Housekeeping(); + if (ttlElement.Attribute(nameof(ITimedTrafficLightsModel.IsStarted))) { + timedNode.Start(ttlElement.Attribute(nameof(ITimedTrafficLightsModel.CurrentStep))); + } + } + catch (Exception e) { + Log.Warning($"Error starting timed light @ {nodeId}: {e}"); + result = PersistenceResult.Failure; + } + } + + return result; + } + + private static void LoadStep(XElement stepElement, ushort ttlNodeId) { + + TimedTrafficLightsStep step = + Instance.TrafficLightSimulations[ttlNodeId].timedLight.AddStep( + stepElement.Attribute(nameof(ITimedTrafficLightsStepModel.MinTime)), + stepElement.Attribute(nameof(ITimedTrafficLightsStepModel.MaxTime)), + stepElement.Attribute(nameof(ITimedTrafficLightsStepModel.ChangeMetric)), + stepElement.Attribute(nameof(ITimedTrafficLightsStepModel.WaitFlowBalance))); + + foreach (var segLightsElement in stepElement.Elements(segLightsElementName)) { + LoadSegLights(segLightsElement, step); + } + } + + private static void LoadSegLights(XElement segLightsElement, TimedTrafficLightsStep step) { + + var segmentId = segLightsElement.Attribute(nameof(ICustomSegmentLightsModel.SegmentId)); + ref NetSegment netSegment = ref segmentId.ToSegment(); + + if (netSegment.IsValid()) { + +#if DEBUGLOAD + Log._Debug($"Loading segment {segmentId} of for ttl step"); +#endif + + if (step.CustomSegmentLights.TryGetValue(segmentId, out var lights)) { + + var manualPedestrianMode = segLightsElement.Attribute(nameof(ICustomSegmentLightsModel.ManualPedestrianMode)); + var pedestrianLightState = segLightsElement.Attribute(nameof(ICustomSegmentLightsModel.PedestrianLightState)); + +#if DEBUGLOAD + Log._Debug($"Loading pedestrian light at segment {segmentId}: " + + $"{pedestrianLightState} {manualPedestrianMode}"); +#endif + + lights.ManualPedestrianMode = manualPedestrianMode; + lights.PedestrianLightState = pedestrianLightState; + + bool first = true; // v1.10.2 transitional code (dark arts that no one understands) + foreach (var lightElement in segLightsElement.Elements(lightElementName)) { + LoadLight(lightElement, lights, ref first); + } + } else { +#if DEBUGLOAD + Log._Debug($"No segment lights found for segment {segmentId}"); +#endif + + } + } else { +#if DEBUGLOAD + Log._Debug($"Invalid segment {segmentId} for ttl step"); +#endif + } + } + + private static void LoadLight(XElement lightElement, CustomSegmentLights lights, ref bool first) { + + var vehicleType = lightElement.Attribute(nameof(CustomSegmentLightModel.VehicleType)); + +#if DEBUGLOAD + Log._Debug($"Loading light: vehicleType={vehicleType}"); +#endif + + if (!lights.CustomLights.TryGetValue( + vehicleType, + out CustomSegmentLight light)) { + + // BEGIN dark arts that no one understands + // TODO learn the dark arts +#if DEBUGLOAD + Log._Debug($"No segment light found for vehicleType {vehicleType}"); +#endif + // v1.10.2 transitional code START + if (first) { + first = false; + if (!lights.CustomLights.TryGetValue( + CustomSegmentLights + .DEFAULT_MAIN_VEHICLETYPE, + out light)) { +#if DEBUGLOAD + Log._Debug($"No segment light found for DEFAULT vehicleType {CustomSegmentLights.DEFAULT_MAIN_VEHICLETYPE} "); +#endif + return; + } + } else { + // v1.10.2 transitional code END + return; + + // v1.10.2 transitional code START + } + + // v1.10.2 transitional code END + + // END dark arts + } + + light.InternalCurrentMode = lightElement.Attribute(nameof(CustomSegmentLightModel.CurrentMode)); // TODO improve & remove + light.SetStates( + lightElement.Attribute(nameof(CustomSegmentLightModel.LightMain)), + lightElement.Attribute(nameof(CustomSegmentLightModel.LightLeft)), + lightElement.Attribute(nameof(CustomSegmentLightModel.LightRight)), + false); + } + + private static PersistenceResult BuildTtlGroups(XElement element, out Dictionary masterNodeLookup, out Dictionary> ttlGroups) { + + var result = PersistenceResult.Success; + + var nodesWithSimulation = new HashSet(); + + foreach (var ttlElement in element.Elements(ttlNodeElementName)) { + nodesWithSimulation.Add(ttlElement.Attribute(nameof(TimedTrafficLights.NodeId))); + } + +#if DEBUGLOAD + Log._Debug($"TrafficLightSimulationManager.Persistence.BuildTtlGroups: " + + $"nodesWithSimulation=[{string.Join(",", nodesWithSimulation.Select(n => n.ToString()).ToArray())}]"); +#endif + + masterNodeLookup = new Dictionary(); + ttlGroups = new Dictionary>(); + foreach (var ttlElement in element.Elements(ttlNodeElementName)) { + + var ttlNodeId = ttlElement.Attribute(nameof(TimedTrafficLights.NodeId)); + + try { + // TODO most of this should not be necessary at all if the classes around TimedTrafficLights class were properly designed + // enforce uniqueness of node ids + List currentNodeGroup = ttlElement.Elements(nameof(TimedTrafficLights.NodeGroup)) + .Distinct().ToList(); + + if (!currentNodeGroup.Contains(ttlNodeId)) { + currentNodeGroup.Add(ttlNodeId); + } + + // remove any nodes that are not configured to have a simulation + currentNodeGroup = new List( + currentNodeGroup.Intersect(nodesWithSimulation)); + + // remove invalid nodes from the group; find if any of the nodes in the group is already a master node + ushort masterNodeId = 0; + int foundMasterNodes = 0; + + for (int i = 0; i < currentNodeGroup.Count;) { + ushort nodeId = currentNodeGroup[i]; + ref NetNode netNode = ref nodeId.ToNode(); + + if (!netNode.IsValid()) { + currentNodeGroup.RemoveAt(i); + continue; + } + + if (ttlGroups.ContainsKey(nodeId)) { + // this is a known master node + if (foundMasterNodes > 0) { + // we already found another master node. ignore this node. + currentNodeGroup.RemoveAt(i); + continue; + } + + // we found the first master node + masterNodeId = nodeId; + ++foundMasterNodes; + } + + ++i; + } + + if (masterNodeId == 0) { + // no master node defined yet, set the first node as a master node + masterNodeId = currentNodeGroup[0]; + } + + // ensure the master node is the first node in the list (TimedTrafficLights + // depends on this at the moment...) + currentNodeGroup.Remove(masterNodeId); + currentNodeGroup.Insert(0, masterNodeId); + + // update the saved node group and master-slave info + ttlGroups[masterNodeId] = currentNodeGroup; + + foreach (ushort nodeId in currentNodeGroup) { + masterNodeLookup[nodeId] = masterNodeId; + } + +#if DEBUGLOAD + Log._Debug($"Node {ttlNodeId}: masterNodeId={masterNodeId}, " + + $"currentNodeGroup=[{string.Join(",", currentNodeGroup.Select(n => n.ToString()).ToArray())}]"); +#endif + } + catch (Exception e) { + Log.WarningFormat( + "Error building timed traffic light group for TimedNode {0} (NodeGroup: {1}): {2}", + ttlNodeId, + string.Join(", ", ttlElement.Elements(nameof(TimedTrafficLights.NodeId)).Select(e => e.Value).ToArray()), + e); + result = PersistenceResult.Failure; + } + } + + return result; + } + + protected override PersistenceResult OnSaveData(XElement element, ICollection featuresRequired, ICollection featuresForbidden, PersistenceContext context) { + var result = PersistenceResult.Success; + + foreach (var timedNodeImpl in Instance.EnumerateTimedTrafficLights()) { + + ITimedTrafficLightsModel timedNode = timedNodeImpl; + + try { + // prepare ttl for write + + timedNodeImpl.OnGeometryUpdate(); + + // we don't save transition states; instead, we save the next step + int currentStep = timedNode.CurrentStep; + if (timedNode.IsStarted && + timedNodeImpl.GetStep(timedNode.CurrentStep).IsInEndTransition()) { + currentStep = (currentStep + 1) % timedNodeImpl.NumSteps(); + } + + // build ttl element + + var ttlNodeElement = new XElement(ttlNodeElementName); + + ttlNodeElement.AddAttribute(nameof(timedNode.NodeId), timedNode.NodeId); + ttlNodeElement.AddElements(nameof(timedNode.NodeGroup), timedNode.NodeGroup); + ttlNodeElement.AddAttribute(nameof(timedNode.IsStarted), timedNode.IsStarted); + ttlNodeElement.AddAttribute(nameof(timedNode.CurrentStep), currentStep); + + element.Add(ttlNodeElement); + + // add steps to the saved ttl + + for (var stepIndex = 0; stepIndex < timedNodeImpl.NumSteps(); stepIndex++) { + SaveStep(ttlNodeElement, timedNodeImpl.GetStep(stepIndex)); + } + } + catch (Exception e) { + Log.Error( + $"Exception occurred while saving timed traffic light @ {timedNode.NodeId}: {e}"); + result = PersistenceResult.Failure; + } + } + + return result; + } + + private static void SaveStep(XElement ttlNodeElement, ITimedTrafficLightsStepModel timedStep) { + + var stepElement = new XElement(stepElementName); + + stepElement.AddAttribute(nameof(timedStep.MinTime), timedStep.MinTime); + stepElement.AddAttribute(nameof(timedStep.MaxTime), timedStep.MaxTime); + stepElement.AddAttribute(nameof(timedStep.ChangeMetric), timedStep.ChangeMetric); + stepElement.AddAttribute(nameof(timedStep.WaitFlowBalance), timedStep.WaitFlowBalance); + + ttlNodeElement.Add(stepElement); + + foreach (var segLights in timedStep.EnumerateCustomSegmentLights()) { + SaveSegLights(stepElement, segLights); + } + } + + private static void SaveSegLights(XElement stepElement, ICustomSegmentLightsModel segLights) { + + var segLightsElement = new XElement(segLightsElementName); + + // build segment lights element + + segLightsElement.AddAttribute(nameof(segLights.NodeId), segLights.NodeId); + segLightsElement.AddAttribute(nameof(segLights.SegmentId), segLights.SegmentId); + segLightsElement.AddAttribute(nameof(segLights.PedestrianLightState), (int?)segLights.PedestrianLightState); + segLightsElement.AddAttribute(nameof(segLights.ManualPedestrianMode), segLights.ManualPedestrianMode); + + stepElement.Add(segLightsElement); + + // add lights to the saved segment lights collection + + foreach (var segLight in segLights) { + SaveLight(segLightsElement, segLight); + } + } + + private static void SaveLight(XElement segLightsElement, CustomSegmentLightModel segLight) { + + var lightElement = new XElement(lightElementName); + + lightElement.AddAttribute(nameof(segLight.VehicleType), segLight.VehicleType); + + lightElement.AddAttribute(nameof(segLight.CurrentMode), segLight.CurrentMode); + lightElement.AddAttribute(nameof(segLight.LightLeft), segLight.LightLeft); + lightElement.AddAttribute(nameof(segLight.LightMain), segLight.LightMain); + lightElement.AddAttribute(nameof(segLight.LightRight), segLight.LightRight); + + segLightsElement.Add(lightElement); + } + } + + } +} diff --git a/TLM/TLM/Manager/Impl/TrafficLightSimulationManager.cs b/TLM/TLM/Manager/Impl/TrafficLightSimulationManager.cs index d0d31f79a..fdd4afbb3 100644 --- a/TLM/TLM/Manager/Impl/TrafficLightSimulationManager.cs +++ b/TLM/TLM/Manager/Impl/TrafficLightSimulationManager.cs @@ -15,8 +15,9 @@ namespace TrafficManager.Manager.Impl { using TrafficManager.Util; using ColossalFramework; using TrafficManager.Util.Extensions; + using TrafficManager.Persistence; - public class TrafficLightSimulationManager + public partial class TrafficLightSimulationManager : AbstractGeometryObservingManager, ICustomDataManager>, ITrafficLightSimulationManager @@ -29,11 +30,22 @@ private TrafficLightSimulationManager() { for (int i = 0; i < TrafficLightSimulations.Length; ++i) { TrafficLightSimulations[i] = new TrafficLightSimulation((ushort)i); } + + GlobalPersistence.PersistentObjects.Add(new Persistence()); } public static readonly TrafficLightSimulationManager Instance = new TrafficLightSimulationManager(); + private IEnumerable EnumerateTimedTrafficLights() { + + for (int i = 0; i < NetManager.MAX_NODE_COUNT; i++) { + if (TrafficLightSimulations[i].IsTimedLight()) { + yield return TrafficLightSimulations[i].timedLight; + } + } + } + /// /// For each node id: traffic light simulation assigned to the node /// diff --git a/TLM/TLM/Manager/Model/JunctionRestrictionsModel.cs b/TLM/TLM/Manager/Model/JunctionRestrictionsModel.cs new file mode 100644 index 000000000..912bff413 --- /dev/null +++ b/TLM/TLM/Manager/Model/JunctionRestrictionsModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TrafficManager.API.Traffic.Enums; + +namespace TrafficManager.Manager.Model { + internal struct JunctionRestrictionsModel { + + public JunctionRestrictionsFlags values; + + public JunctionRestrictionsFlags mask; + } +} diff --git a/TLM/TLM/Manager/Model/SegmentEndPair.cs b/TLM/TLM/Manager/Model/SegmentEndPair.cs new file mode 100644 index 000000000..aa33eb671 --- /dev/null +++ b/TLM/TLM/Manager/Model/SegmentEndPair.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TrafficManager.Manager.Model { + internal struct SegmentEndPair { + + public ushort segmentId; + + public T start; + + public T end; + } +} diff --git a/TLM/TLM/Persistence/FeatureFilter.cs b/TLM/TLM/Persistence/FeatureFilter.cs new file mode 100644 index 000000000..12866e6fb --- /dev/null +++ b/TLM/TLM/Persistence/FeatureFilter.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace TrafficManager.Persistence { + internal sealed class FeatureFilter + where TFeature : struct { + + private const string featuresRequiredAttributeName = "featuresRequired"; + private const string featuresForbiddenAttributeName = "featuresForbidden"; + + private enum FeatureStatus { + Forbidden, + Required, + } + + static FeatureFilter() { + Type tFeature = typeof(TFeature); + if (!tFeature.IsEnum) + throw new PersistenceException($"FeatureSet<{tFeature.FullName}>: Type {tFeature.Name} is not an enum"); + } + + private readonly Dictionary filter; + + public static bool CanLoad(XElement element, out FeatureFilter result) { + return CanLoad(element.Attribute(featuresRequiredAttributeName)?.Value, + element.Attribute(featuresForbiddenAttributeName)?.Value, + out result); + } + + private static bool CanLoad(string requiredAttribute, string forbiddenAttribute, out FeatureFilter result) { + + result = null; + + if (!string.IsNullOrEmpty(forbiddenAttribute)) { + foreach (var featureName in forbiddenAttribute.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) { + try { + Enum.Parse(typeof(TFeature), featureName); + return false; + } + catch { + } + } + } + var filter = new Dictionary(); + if (!string.IsNullOrEmpty(requiredAttribute)) { + foreach (var featureName in requiredAttribute.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) { + try { + filter[(TFeature)Enum.Parse(typeof(TFeature), featureName)] = FeatureStatus.Required; + } + catch { + return false; + } + } + } + result = new FeatureFilter(filter); + return true; + } + + public FeatureFilter() => filter = new Dictionary(); + + public FeatureFilter(IEnumerable required, IEnumerable forbidden) + : this() + { + + if (required != null) { + if (forbidden != null) { + IEnumerable conflictingFeatures = required.Intersect(forbidden); + if (conflictingFeatures.Any()) { + throw new ArgumentException($"{conflictingFeatures.Count()} features including {conflictingFeatures.First()} were found in both the required and forbidden collections"); + } + } + + foreach (var f in required) { + Require(f); + } + } + + if (forbidden != null) { + foreach (var f in forbidden) { + Forbid(f); + } + } + } + + private FeatureFilter(Dictionary filter) => this.filter = filter; + + public void Clear() => filter.Clear(); + + public void Require(TFeature feature) => filter[feature] = FeatureStatus.Required; + + public void Forbid(TFeature feature) => filter[feature] = FeatureStatus.Forbidden; + + public void Disregard(TFeature feature) => filter.Remove(feature); + + public bool IsRequired(TFeature feature) { + return filter.TryGetValue(feature, out var status) + && status == FeatureStatus.Required; + } + + public bool IsForbidden(TFeature feature) { + return filter.TryGetValue(feature, out var status) + && status == FeatureStatus.Forbidden; + } + + public bool IsAnyRequired() => filter.ContainsValue(FeatureStatus.Required); + + public bool IsAnyForbidden() => filter.ContainsValue(FeatureStatus.Forbidden); + + private IEnumerable Enumerate(FeatureStatus status) + => filter.Where(e => e.Value == status).Select(e => e.Key).OrderByDescending(f => f); + + public IEnumerable GetRequired() + => Enumerate(FeatureStatus.Required); + + public IEnumerable GetForbidden() + => Enumerate(FeatureStatus.Forbidden); + + public ICollection GetRequiredCollection(bool readOnly) + => new FeatureCollection(this, FeatureStatus.Required, readOnly); + + public ICollection GetForbiddenCollection() + => new FeatureCollection(this, FeatureStatus.Forbidden, true); + + private class FeatureCollection : ICollection { + private readonly FeatureFilter featureFilter; + private readonly FeatureStatus collectionFeatureStatus; + private readonly bool isReadOnly; + + public FeatureCollection(FeatureFilter featureFilter, FeatureStatus collectionFeatureStatus, bool isReadOnly) { + this.featureFilter = featureFilter; + this.collectionFeatureStatus = collectionFeatureStatus; + this.isReadOnly = isReadOnly; + } + + public int Count => featureFilter.filter.Values.Where(v => v == collectionFeatureStatus).Count(); + + public bool IsReadOnly => isReadOnly; + + public void Add(TFeature item) { + if (isReadOnly) + throw new NotSupportedException(); + if (featureFilter.filter.TryGetValue(item, out var existingStatus) && existingStatus != collectionFeatureStatus) + throw new PersistenceException($"{item} could not be added to the required collection because it is already forbidden."); + featureFilter.filter[item] = collectionFeatureStatus; + } + + public void Clear() { + if (isReadOnly) + throw new NotSupportedException(); + foreach (var e in featureFilter.filter.Where(e => e.Value == collectionFeatureStatus).ToArray()) + featureFilter.filter.Remove(e.Key); + } + + public bool Contains(TFeature item) { + return featureFilter.filter.TryGetValue(item, out var status) + && status == collectionFeatureStatus; + } + + public void CopyTo(TFeature[] array, int arrayIndex) { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + => featureFilter.Enumerate(collectionFeatureStatus).GetEnumerator(); + + public bool Remove(TFeature item) { + if (isReadOnly) + throw new NotSupportedException(); + + return featureFilter.filter.TryGetValue(item, out var status) + && status == collectionFeatureStatus + && featureFilter.filter.Remove(item); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public object[] GetAttributes() { + var result = new ArrayList(); + + if (IsAnyRequired()) { + result.Add(new XAttribute(featuresRequiredAttributeName, + string.Join(" ", GetRequired().Select(f => f.ToString()).ToArray()))); + } + + if (IsAnyForbidden()) { + result.Add(new XAttribute(featuresForbiddenAttributeName, + string.Join(" ", GetForbidden().Select(f => f.ToString()).ToArray()))); + } + + return result.ToArray(); + } + } +} diff --git a/TLM/TLM/Persistence/GlobalPersistence.cs b/TLM/TLM/Persistence/GlobalPersistence.cs new file mode 100644 index 000000000..c9ed34529 --- /dev/null +++ b/TLM/TLM/Persistence/GlobalPersistence.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TrafficManager.Persistence { + internal static class GlobalPersistence { + + public static List PersistentObjects { get; } = new List(); + } +} diff --git a/TLM/TLM/Persistence/IPersistentObject.cs b/TLM/TLM/Persistence/IPersistentObject.cs new file mode 100644 index 000000000..19a23e390 --- /dev/null +++ b/TLM/TLM/Persistence/IPersistentObject.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace TrafficManager.Persistence { + internal interface IPersistentObject : IComparable, IComparable { + + Type DependencyTarget { get; } + + IEnumerable GetDependencies(); + + XName ElementName { get; } + + bool CanLoad(XElement element); + + PersistenceResult LoadData(XElement element, PersistenceContext context); + + PersistenceResult SaveData(XElement container, PersistenceContext context); + } +} \ No newline at end of file diff --git a/TLM/TLM/Persistence/PersistenceContext.cs b/TLM/TLM/Persistence/PersistenceContext.cs new file mode 100644 index 000000000..ab2853c03 --- /dev/null +++ b/TLM/TLM/Persistence/PersistenceContext.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace TrafficManager.Persistence { + internal class PersistenceContext { + + public int Version { get; set; } + } +} diff --git a/TLM/TLM/Persistence/PersistenceException.cs b/TLM/TLM/Persistence/PersistenceException.cs new file mode 100644 index 000000000..78195675f --- /dev/null +++ b/TLM/TLM/Persistence/PersistenceException.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TrafficManager.Persistence { + internal class PersistenceException : Exception { + public PersistenceException(string message) + : base(message) { } + } +} diff --git a/TLM/TLM/Persistence/PersistenceResult.cs b/TLM/TLM/Persistence/PersistenceResult.cs new file mode 100644 index 000000000..3903b4c18 --- /dev/null +++ b/TLM/TLM/Persistence/PersistenceResult.cs @@ -0,0 +1,29 @@ +using CSUtil.Commons; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TrafficManager.Persistence { + internal enum PersistenceResult { + Success = 0, + Failure = 1, + Skip = 2, + } + + internal static class PersistenceResultExtensions { + public static void LogMessage(this PersistenceResult result, string message) { + switch (result) { + case PersistenceResult.Failure: + default: + Log.Warning(message); + break; + + case PersistenceResult.Success: + case PersistenceResult.Skip: + Log.Info(message); + break; + } + } + } +} diff --git a/TLM/TLM/Persistence/PersistentObject.cs b/TLM/TLM/Persistence/PersistentObject.cs new file mode 100644 index 000000000..feb839170 --- /dev/null +++ b/TLM/TLM/Persistence/PersistentObject.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace TrafficManager.Persistence { + + internal abstract class PersistentObject + : IPersistentObject + where TFeature : struct { + + public abstract XName ElementName { get; } + + /// + /// Moves one or more of 's required features to the forbidden list, + /// then clears any remaining required features. + /// + /// + protected virtual void ChooseVictim(FeatureFilter featureFilter) { + featureFilter.Forbid(featureFilter.GetRequired().First()); + featureFilter.GetRequiredCollection(false).Clear(); + } + + /// + /// Checks whether the element is feature-compatible with this build. + /// + /// + /// true if feature-compatible, otherwise false + public bool CanLoad(XElement element) => FeatureFilter.CanLoad(element, out var _); + + protected PersistenceResult LoadData(XElement element, Func, PersistenceResult> loadData) { + return FeatureFilter.CanLoad(element, out var featureFilter) + ? loadData(featureFilter.GetRequiredCollection(true)) + : PersistenceResult.Skip; + } + + protected PersistenceResult SaveData(XElement container, ICollection featuresForbidden, Func, ICollection, PersistenceResult> saveData) { + + var featureFilter = new FeatureFilter(); + if (featuresForbidden != null) { + foreach (var feature in featuresForbidden) { + featureFilter.Forbid(feature); + } + } + + var element = new XElement(ElementName); + var result = saveData(element, + featureFilter.GetRequiredCollection(false), + featureFilter.GetForbiddenCollection()); + + if (result == PersistenceResult.Success) { + + element.Add(featureFilter.GetAttributes()); + container.Add(element); + + while (result == PersistenceResult.Success && featureFilter.IsAnyRequired()) { + + ChooseVictim(featureFilter); + + if (featureFilter.GetForbiddenCollection().Count == Enum.GetValues(typeof(TFeature)).Length) + break; + + element = new XElement(ElementName); + result = saveData(element, + featureFilter.GetRequiredCollection(false), + featureFilter.GetForbiddenCollection()); + + if (result == PersistenceResult.Success) { + element.Add(featureFilter.GetAttributes()); + container.Add(element); + } + } + if (result == PersistenceResult.Skip) + result = PersistenceResult.Success; + } + + return result; + } + + public abstract Type DependencyTarget { get; } + + int IComparable.CompareTo(object obj) => CompareTo((IPersistentObject)obj); + + public int CompareTo(IPersistentObject other) { + + bool thisDependsOnOther = GetDependencies()?.Contains(other.DependencyTarget) == true; + bool otherDependsOnThis = other.GetDependencies()?.Contains(DependencyTarget) == true; + + return + thisDependsOnOther == otherDependsOnThis ? ElementName.ToString().CompareTo(other.ElementName.ToString()) + : thisDependsOnOther ? 1 + : -1; + } + + public abstract IEnumerable GetDependencies(); + + /// + /// Override to load data + /// + /// An XML element that was filled by + /// A read-only collection of features that were known to the build that created this element. + /// When a particular feature caused a breaking change in the save data, the absence of that feature from this collection + /// means that the data must be read the old way. + /// The persistence context of this load operation + /// + protected abstract PersistenceResult OnLoadData(XElement element, ICollection featuresRequired, PersistenceContext context); + + /// + /// Verifies feature compatibility and conditionally calls the overridable + /// . + /// + /// + /// + /// + public PersistenceResult LoadData(XElement element, PersistenceContext context) { + return LoadData(element, f => OnLoadData(element, f, context)); + } + + /// + /// Override to save data + /// + /// An XML element to be filled with save data + /// A collection of features this save data depends on. When a new feature introduces a + /// breaking change, the feature must be added to this collection to avoid data loss when if the player reverts to an earlier build. + /// A collection of features that are to be omitted from the save data for backward compatibility. + /// If the implementation cannot write backward-compatible save data that omits the features in this collection, + /// it must return . + /// The persistence context of this save operation + /// + protected abstract PersistenceResult OnSaveData(XElement element, ICollection featuresRequired, ICollection featuresForbidden, PersistenceContext context); + + /// + /// Calls . + /// When that method reports the use of features that introduced breaking changes, successive calls are made to request + /// backward-compatible save data. + /// + /// + /// + /// + public PersistenceResult SaveData(XElement container, PersistenceContext context) { + return SaveData(container, null, (e, r, f) => OnSaveData(e, r, f, context)); + } + } +} diff --git a/TLM/TLM/Persistence/XSerializer.cs b/TLM/TLM/Persistence/XSerializer.cs new file mode 100644 index 000000000..641736a59 --- /dev/null +++ b/TLM/TLM/Persistence/XSerializer.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; + +namespace TrafficManager.Persistence { + public static class XSerializer { + + public static XElement XSerialize(this T value, XName name) + where T : new() + => value.XSerialize(typeof(T), name); + + public static XElement XSerialize(this object value, Type type, XName name) { + + XElement element = null; + + if (value != null) { + + if (Type.GetTypeCode(type) != TypeCode.Object) { + throw new ArgumentException("Non-primitive type required"); + } + + element = new XElement(name); + + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.NonPublic)) { + + var fieldValue = field.GetValue(value); + + if (fieldValue != null) { + + if (Type.GetTypeCode(field.FieldType) == TypeCode.Object) { + element.Add(fieldValue.XSerialize(field.FieldType, field.Name)); + } else { + element.AddAttribute(field.Name, fieldValue); + } + } + } + } + return element; + } + + public static T XDeserialize(this XElement element) + where T : new() + => (T)element.XDeserialize(typeof(T)); + + public static object XDeserialize(this XElement element, Type type) { + + object result = null; + + if (element != null) { + + if (Type.GetTypeCode(type) != TypeCode.Object) { + throw new ArgumentException("Non-primitive type required"); + } + + result = Activator.CreateInstance(type); + + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.NonPublic)) { + + if (Type.GetTypeCode(field.FieldType) == TypeCode.Object) { + var memberElement = element.Element(field.Name); + if (memberElement != null) { + field.SetValue(result, memberElement.XDeserialize(field.FieldType)); + } + } else { + field.SetValue(result, element.Attribute(field.FieldType, field.Name)); + } + } + } + + return result; + } + } +} diff --git a/TLM/TLM/Persistence/XmlLinqExtensions.cs b/TLM/TLM/Persistence/XmlLinqExtensions.cs new file mode 100644 index 000000000..224bb3abd --- /dev/null +++ b/TLM/TLM/Persistence/XmlLinqExtensions.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace TrafficManager.Persistence { + internal static class XmlLinqExtensions { + + public static XElement NewElement(this XElement element, XName name) { + var result = new XElement(name); + element.Add(result); + return result; + } + + public static void AddAttribute(this XElement element, XName name, T value) { + if (!(typeof(T).IsClass && ReferenceEquals(value, default(T)))) { + element.Add(new XAttribute(name, ConvertToXml(value))); + } + } + + public static void AddAttribute(this XElement element, XName name, object value) { + if (value != null) { + element.Add(new XAttribute(name, ConvertToXml(value))); + } + } + + public static void AddElement(this XElement element, XName name, object value) { + if (value != null) { + element.Add(new XElement(name, ConvertToXml(value))); + } + } + + public static void AddElement(this XElement element, XName name, T value) { + if (!(typeof(T).IsClass && ReferenceEquals(value, default(T)))) { + element.Add(new XElement(name, ConvertToXml(value))); + } + } + + public static void AddElements(this XElement element, XName name, IEnumerable values) { + foreach (var value in values) { + element.AddElement(name, value); + } + } + + public static void AddElements(this XElement element, XName name, params object[] values) { + foreach (var value in values) { + element.AddElement(name, value); + } + } + + public static void AddElements(this XElement element, XName name, params T[] values) + where T : struct + { + + foreach (var value in values) { + element.AddElement(name, value); + } + } + + private static string ConvertToXml(object value) { + switch (value) { + case DateTime dateTime: + return dateTime.ToString("O", CultureInfo.InvariantCulture); + + default: + if (value.GetType().IsEnum) { + return ConvertToXml(Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()))); + } else { + return (string)Convert.ChangeType(value, typeof(string), CultureInfo.InvariantCulture); + } + } + } + + public static void AddAttribute(this XElement element, XName name, T? value) + where T : struct + { + + if (value.HasValue) { + element.AddAttribute(name, value.Value); + } + } + + public static T Attribute(this XElement element, XName name) { + + var value = element.Attribute(name)?.Value; + return value == null ? default : ConvertFromXml(value); + } + + public static object Attribute(this XElement element, Type type, XName name) { + var value = element.Attribute(name)?.Value; + return value == null + ? type.IsValueType + ? Activator.CreateInstance(type) + : null + : ConvertFromXml(value, type); + } + + public static T Element(this XElement element, XName name) { + + var value = element.Element(name)?.Value; + return value == null ? default : ConvertFromXml(value); + } + + public static object Element(this XElement element, Type type, XName name) { + + var value = element.Element(name)?.Value; + return value == null + ? type.IsValueType + ? Activator.CreateInstance(type) + : null + : ConvertFromXml(value, type); + } + + public static IEnumerable Elements(this XElement element, XName name) + => element.Elements(name).Select(e => ConvertFromXml(e.Value)); + + public static T Value(this XElement element) => ConvertFromXml(element.Value); + + public static object Value(this XElement element, Type type) => ConvertFromXml(element.Value, type); + + private static T ConvertFromXml(string value) => (T)ConvertFromXml(value, typeof(T)); + + private static object ConvertFromXml(string value, Type type) { + + if (type.IsValueType) { + type = Nullable.GetUnderlyingType(type) ?? type; + } + + switch (Type.GetTypeCode(type)) { + + case TypeCode.DateTime: + return DateTime.ParseExact(value, "O", CultureInfo.InvariantCulture); + + default: + if (type.IsEnum) { + return Enum.Parse(type, value); + } else { + return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + } + } + } + } +} diff --git a/TLM/TLM/TLM.csproj b/TLM/TLM/TLM.csproj index efe8b8686..5ce2554c0 100644 --- a/TLM/TLM/TLM.csproj +++ b/TLM/TLM/TLM.csproj @@ -1,4 +1,4 @@ - + @@ -104,6 +104,10 @@ $(MangedDLLPath)\ICities.dll False + + False + $(MangedDLLPath)\ICSharpCode.SharpZipLib.dll + ..\libs\MoveItIntegration.dll @@ -113,6 +117,9 @@ + + True + ..\packages\UnifiedUILib.2.2.1\lib\net35\UnifiedUILib.dll @@ -148,13 +155,24 @@ + + + + + + + + + + + @@ -171,9 +189,15 @@ + + + + + + @@ -189,6 +213,7 @@ + @@ -471,6 +496,7 @@ + @@ -1268,6 +1294,7 @@ xcopy /y "$(TargetDir)CSUtil.Commons.dll" "%25DEPLOYDIR%25" xcopy /y "$(TargetDir)CitiesHarmony.API.dll" "%25DEPLOYDIR%25" xcopy /y "$(TargetDir)MoveItIntegration.dll" "%25DEPLOYDIR%25" xcopy /y "$(TargetDir)UnifiedUILib.dll" "%25DEPLOYDIR%25" +xcopy /y "$(TargetDir)System.Xml.Linq.dll" "%25DEPLOYDIR%25" rem To avoid double hot reload, TrafficManager.dll must be replaced last and fast. rem Once TrafficManager.dll is re-loaded, all other dlls will be reloaded as well diff --git a/TLM/TLM/TrafficLight/Impl/CustomSegmentLight.cs b/TLM/TLM/TrafficLight/Impl/CustomSegmentLight.cs index a410ca3af..ba0ade5ef 100644 --- a/TLM/TLM/TrafficLight/Impl/CustomSegmentLight.cs +++ b/TLM/TLM/TrafficLight/Impl/CustomSegmentLight.cs @@ -10,6 +10,7 @@ namespace TrafficManager.TrafficLight.Impl { using TrafficManager.API.Traffic.Enums; using TrafficManager.Manager.Impl; using TrafficManager.State.ConfigData; + using TrafficManager.TrafficLight.Model; using TrafficManager.Util; using TrafficManager.Util.Extensions; diff --git a/TLM/TLM/TrafficLight/Impl/CustomSegmentLights.cs b/TLM/TLM/TrafficLight/Impl/CustomSegmentLights.cs index 33fa2fb21..614653f16 100644 --- a/TLM/TLM/TrafficLight/Impl/CustomSegmentLights.cs +++ b/TLM/TLM/TrafficLight/Impl/CustomSegmentLights.cs @@ -14,12 +14,14 @@ namespace TrafficManager.TrafficLight.Impl { using TrafficManager.Util; using TrafficManager.Manager.Impl; using TrafficManager.Util.Extensions; + using TrafficManager.TrafficLight.Model; + using System.Collections; /// /// Represents the set of custom traffic lights located at a node /// public class CustomSegmentLights - : SegmentEndId + : SegmentEndId, ICustomSegmentLightsModel { // private static readonly ExtVehicleType[] SINGLE_LANE_VEHICLETYPES // = new ExtVehicleType[] { ExtVehicleType.Tram, ExtVehicleType.Service, @@ -855,5 +857,19 @@ public void Housekeeping(bool mayDelete, bool calculateAutoPedLight) { CustomLights.DictionaryToString()); } } // end Housekeeping() + + IEnumerator IEnumerable.GetEnumerator() { + foreach (var light in CustomLights) { + yield return new CustomSegmentLightModel() { + VehicleType = light.Key, + CurrentMode = light.Value.CurrentMode, + LightLeft = light.Value.LightLeft, + LightMain = light.Value.LightMain, + LightRight = light.Value.LightRight, + }; + } + } + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); } // end class -} \ No newline at end of file +} diff --git a/TLM/TLM/TrafficLight/Impl/TimedTrafficLights.cs b/TLM/TLM/TrafficLight/Impl/TimedTrafficLights.cs index 5b676d874..e6b3429f1 100644 --- a/TLM/TLM/TrafficLight/Impl/TimedTrafficLights.cs +++ b/TLM/TLM/TrafficLight/Impl/TimedTrafficLights.cs @@ -14,9 +14,10 @@ namespace TrafficManager.TrafficLight.Impl { using TrafficManager.Util; using UnityEngine; using TrafficManager.Util.Extensions; + using TrafficManager.TrafficLight.Model; // TODO define TimedTrafficLights per node group, not per individual nodes - public class TimedTrafficLights { + public class TimedTrafficLights : ITimedTrafficLightsModel { public TimedTrafficLights(ushort nodeId, IEnumerable nodeGroup) { NodeId = nodeId; NodeGroup = new List(nodeGroup); @@ -29,6 +30,16 @@ public TimedTrafficLights(ushort nodeId, IEnumerable nodeGroup) { started = false; } + private TimedTrafficLights(ushort nodeId, IEnumerable nodeGroup, bool started) + : this(nodeId, nodeGroup) + { + + this.started = started; + } + + internal static TimedTrafficLights CreateLoadingInstance(ushort nodeId, IEnumerable nodeGroup, bool started) + => new TimedTrafficLights(nodeId, nodeGroup, started); + public ushort NodeId { get; } @@ -59,6 +70,8 @@ public ushort MasterNodeId { public IDictionary> Directions { get; private set; } + bool ITimedTrafficLightsModel.IsStarted => IsStarted(); + /// /// Segment ends that were set up for this timed traffic light /// @@ -324,6 +337,13 @@ public TimedTrafficLightsStep AddStep(int minTime, return step; } + internal void AddLoadingStep(TimedTrafficLightsStep step) { + // TODO PERSISTENCE set this up for hot swapping ttl at load time + + step.Initialize(this); + Steps.Add(step); + } + public void Start() { Start(0); } diff --git a/TLM/TLM/TrafficLight/Impl/TimedTrafficLightsStep.cs b/TLM/TLM/TrafficLight/Impl/TimedTrafficLightsStep.cs index 2b5be2a7a..a6df9f701 100644 --- a/TLM/TLM/TrafficLight/Impl/TimedTrafficLightsStep.cs +++ b/TLM/TLM/TrafficLight/Impl/TimedTrafficLightsStep.cs @@ -14,27 +14,46 @@ namespace TrafficManager.TrafficLight.Impl { using TrafficManager.Util; using ColossalFramework; using TrafficManager.Util.Extensions; + using TrafficManager.TrafficLight.Model; // TODO class should be completely reworked, approx. in version 1.10 - public class TimedTrafficLightsStep : ITrafficLightContainer + public class TimedTrafficLightsStep : ITrafficLightContainer, ITimedTrafficLightsStepModel { public TimedTrafficLightsStep(TimedTrafficLights timedNode, int minTime, int maxTime, StepChangeMetric stepChangeMode, float waitFlowBalance, - bool makeRed = false) { + bool makeRed = false) + : this() + { + MinTime = minTime; MaxTime = maxTime; ChangeMetric = stepChangeMode; WaitFlowBalance = waitFlowBalance; - this.timedNode = timedNode; CurrentFlow = Single.NaN; CurrentWait = Single.NaN; + Initialize(timedNode, makeRed); + } + + private TimedTrafficLightsStep() { + endTransitionStart = null; stepDone = false; + } + + internal static TimedTrafficLightsStep CreateLoadingInstance() => new TimedTrafficLightsStep(); + + internal void Initialize(TimedTrafficLights timedNode, bool makeRed = false) { + + if (this.timedNode != null) { + throw new InvalidOperationException($"TimedTrafficLightsStep instance is already initialized for node {this.timedNode.NodeId}."); + } + + this.timedNode = timedNode; ref NetNode node = ref timedNode.NodeId.ToNode(); @@ -1294,6 +1313,12 @@ internal bool AddSegment(ushort segmentId, bool startNode, bool makeRed) { return customSegLightsMan.ApplyLightModes(segmentId, startNode, clonedLights); } + internal void AddLoadingSegmentLights(CustomSegmentLights segLights) { + // TODO PERSISTENCE set this up for hot swapping ttl at load time + + CustomSegmentLights.Add(segLights.SegmentId, segLights); + } + public bool SetSegmentLights(ushort nodeId, ushort segmentId, CustomSegmentLights lights) { #if DEBUG if (DebugSwitch.TimedTrafficLights.Get() && DebugSettings.NodeId == timedNode.NodeId) { @@ -1393,5 +1418,10 @@ public void RemoveSegmentLight(ushort segmentId, bool startNode) { public bool IsSegmentLight(ushort segmentId, bool startNode) { throw new NotImplementedException(); } + + IEnumerable ITimedTrafficLightsStepModel.EnumerateCustomSegmentLights() { + foreach (var segLights in CustomSegmentLights.Values) + yield return segLights; + } } } \ No newline at end of file diff --git a/TLM/TLM/TrafficLight/Model/CustomSegmentLightModel.cs b/TLM/TLM/TrafficLight/Model/CustomSegmentLightModel.cs new file mode 100644 index 000000000..02b67df82 --- /dev/null +++ b/TLM/TLM/TrafficLight/Model/CustomSegmentLightModel.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TrafficManager.API.Traffic.Enums; + +namespace TrafficManager.TrafficLight.Model { + internal class CustomSegmentLightModel { + + public ExtVehicleType VehicleType { get; set; } + + public LightMode CurrentMode { get; set; } + + public RoadBaseAI.TrafficLightState LightLeft { get; set; } + + public RoadBaseAI.TrafficLightState LightMain { get; set; } + + public RoadBaseAI.TrafficLightState LightRight { get; set; } + } +} diff --git a/TLM/TLM/TrafficLight/Model/ICustomSegmentLightsModel.cs b/TLM/TLM/TrafficLight/Model/ICustomSegmentLightsModel.cs new file mode 100644 index 000000000..007630eb3 --- /dev/null +++ b/TLM/TLM/TrafficLight/Model/ICustomSegmentLightsModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TrafficManager.TrafficLight.Model { + internal interface ICustomSegmentLightsModel : IEnumerable { + + ushort NodeId { get; } + + ushort SegmentId { get; } + + RoadBaseAI.TrafficLightState? PedestrianLightState { get; } + + bool ManualPedestrianMode { get; } + } +} diff --git a/TLM/TLM/TrafficLight/Model/ITimedTrafficLightsModel.cs b/TLM/TLM/TrafficLight/Model/ITimedTrafficLightsModel.cs new file mode 100644 index 000000000..626cb5fc0 --- /dev/null +++ b/TLM/TLM/TrafficLight/Model/ITimedTrafficLightsModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TrafficManager.TrafficLight.Model { + internal interface ITimedTrafficLightsModel { + + int CurrentStep { get; } + + bool IsStarted { get; } + + ushort NodeId { get; } + + IList NodeGroup { get; } + } +} diff --git a/TLM/TLM/TrafficLight/Model/ITimedTrafficLightsStepModel.cs b/TLM/TLM/TrafficLight/Model/ITimedTrafficLightsStepModel.cs new file mode 100644 index 000000000..b0ffccabc --- /dev/null +++ b/TLM/TLM/TrafficLight/Model/ITimedTrafficLightsStepModel.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TrafficManager.API.Traffic.Enums; + +namespace TrafficManager.TrafficLight.Model { + internal interface ITimedTrafficLightsStepModel { + + int MinTime { get; } + + int MaxTime { get; } + + StepChangeMetric ChangeMetric { get; } + + float WaitFlowBalance { get; } + + IEnumerable EnumerateCustomSegmentLights(); + } +} diff --git a/TLM/TLM/UI/Textures/RoadSignTheme.cs b/TLM/TLM/UI/Textures/RoadSignTheme.cs index 4f4512d46..c036ff67c 100644 --- a/TLM/TLM/UI/Textures/RoadSignTheme.cs +++ b/TLM/TLM/UI/Textures/RoadSignTheme.cs @@ -150,22 +150,22 @@ public Texture2D GetOtherRestriction(OtherRestriction type, bool allow) { : this.ParentTheme.GetOtherRestriction(type, allow: false); } - public Texture2D JunctionRestriction(JunctionRestrictionFlags rule, bool allowed) { + public Texture2D JunctionRestriction(JunctionRestrictionsFlags rule, bool allowed) { bool rht = Shortcuts.RHT; switch (rule) { - case JunctionRestrictionFlags.AllowPedestrianCrossing: + case JunctionRestrictionsFlags.AllowPedestrianCrossing: return GetOtherRestriction(OtherRestriction.Crossing, allowed); - case JunctionRestrictionFlags.AllowUTurn: + case JunctionRestrictionsFlags.AllowUTurn: return GetOtherRestriction(OtherRestriction.UTurn, allowed); - case JunctionRestrictionFlags.AllowEnterWhenBlocked: + case JunctionRestrictionsFlags.AllowEnterWhenBlocked: return GetOtherRestriction(OtherRestriction.EnterBlockedJunction, allowed); - case JunctionRestrictionFlags.AllowForwardLaneChange: + case JunctionRestrictionsFlags.AllowForwardLaneChange: return GetOtherRestriction(OtherRestriction.LaneChange, allowed); - case JunctionRestrictionFlags.AllowFarTurnOnRed when rht: - case JunctionRestrictionFlags.AllowNearTurnOnRed when !rht: + case JunctionRestrictionsFlags.AllowFarTurnOnRed when rht: + case JunctionRestrictionsFlags.AllowNearTurnOnRed when !rht: return GetOtherRestriction(OtherRestriction.LeftOnRed, allowed); - case JunctionRestrictionFlags.AllowNearTurnOnRed when rht: - case JunctionRestrictionFlags.AllowFarTurnOnRed when !rht: + case JunctionRestrictionsFlags.AllowNearTurnOnRed when rht: + case JunctionRestrictionsFlags.AllowFarTurnOnRed when !rht: return GetOtherRestriction(OtherRestriction.RightOnRed, allowed); default: Log.Error($"could not get texture for {rule}."); diff --git a/TLM/TLM/Util/AnonymousDisposable.cs b/TLM/TLM/Util/AnonymousDisposable.cs new file mode 100644 index 000000000..66e645056 --- /dev/null +++ b/TLM/TLM/Util/AnonymousDisposable.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; + +namespace TrafficManager.Util { + /// + /// Implements IDisposable as a proxy to an delegate. + /// + public sealed class AnonymousDisposable : IDisposable { + private volatile Action dispose; + + /// + /// An IDisposable object that does nothing. This may be returned when an overriding + /// method is required to return IDisposable but has no need for its functionality. + /// It may also be returned as a placeholder when anticipating a future need for + /// IDisposable. + /// + public static readonly IDisposable Empty = new AnonymousDisposable(null); + + /// + /// Constructs an object that invokes the specified action when its + /// method is called. + /// + /// + public AnonymousDisposable(Action dispose) => this.dispose = dispose; + + /// + /// On the first call, invokes the delegate that was provided to the constructor. + /// On subsequent calls, does nothing. + /// + public void Dispose() => Interlocked.Exchange(ref dispose, null)?.Invoke(); + } +} diff --git a/TLM/TLM/Util/ReferenceEqualityComparer.cs b/TLM/TLM/Util/ReferenceEqualityComparer.cs new file mode 100644 index 000000000..add2d2187 --- /dev/null +++ b/TLM/TLM/Util/ReferenceEqualityComparer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace TrafficManager.Util { + internal sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer { + + public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + public bool Equals(T x, T y) => ReferenceEquals(x, y); + + public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + + bool IEqualityComparer.Equals(object x, object y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/TLM/TMPE.API/TMPE.API.csproj b/TLM/TMPE.API/TMPE.API.csproj index 0c1b71785..d361da638 100644 --- a/TLM/TMPE.API/TMPE.API.csproj +++ b/TLM/TMPE.API/TMPE.API.csproj @@ -145,7 +145,7 @@ - + diff --git a/TLM/TMPE.API/Traffic/Enums/JunctionRestrictionFlags.cs b/TLM/TMPE.API/Traffic/Enums/JunctionRestrictionsFlags.cs similarity index 86% rename from TLM/TMPE.API/Traffic/Enums/JunctionRestrictionFlags.cs rename to TLM/TMPE.API/Traffic/Enums/JunctionRestrictionsFlags.cs index ba8c86698..305c89855 100644 --- a/TLM/TMPE.API/Traffic/Enums/JunctionRestrictionFlags.cs +++ b/TLM/TMPE.API/Traffic/Enums/JunctionRestrictionsFlags.cs @@ -1,5 +1,5 @@ namespace TrafficManager.API.Traffic.Enums { - public enum JunctionRestrictionFlags { + public enum JunctionRestrictionsFlags { AllowUTurn = 1 << 0, AllowNearTurnOnRed = 1 << 1, AllowFarTurnOnRed = 1 << 2, diff --git a/TLM/TMPE.API/UI/ITheme.cs b/TLM/TMPE.API/UI/ITheme.cs index 7694cd2fd..9ad02a351 100644 --- a/TLM/TMPE.API/UI/ITheme.cs +++ b/TLM/TMPE.API/UI/ITheme.cs @@ -6,7 +6,7 @@ namespace TrafficManager.API.UI { /// gets the texture for overlay sprite for each traffic rule according to the current theme. /// public interface ITheme { - Texture2D JunctionRestriction(JunctionRestrictionFlags rule, bool allowed); + Texture2D JunctionRestriction(JunctionRestrictionsFlags rule, bool allowed); Texture2D Parking(bool allowed);