diff --git a/TLM/CSModLib.Test/CSModLib.Test.csproj b/TLM/CSModLib.Test/CSModLib.Test.csproj
new file mode 100644
index 000000000..aa436a36f
--- /dev/null
+++ b/TLM/CSModLib.Test/CSModLib.Test.csproj
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Debug
+ AnyCPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}
+ Library
+ Properties
+ CSModLib.Test
+ CSModLib.Test
+ v4.8
+ 512
+ {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 15.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+ $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
+ False
+ UnitTest
+
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ ~/Library/Application Support/Steam/
+ $(ProgramFiles)\Steam
+ $(Registry:HKEY_CURRENT_USER\Software\Valve\Steam@SteamPath)
+ $(SteamPath)\steamapps\common\Cities_Skylines
+ $(CSPath)\Cities_Data\Managed
+ ..\dependencies
+ $(MSBuildExtensionsPath64)\..\Unity\
+ ..\Unity\
+
+
+
+ $(MangedDLLPath)\Assembly-CSharp.dll
+ True
+
+
+ $(MangedDLLPath)\ColossalManaged.dll
+ True
+
+
+ $(MangedDLLPath)\ICities.dll
+ True
+
+
+ $(MangedDLLPath)\UnityEngine.dll
+ True
+
+
+ $(MangedDLLPath)\UnityEngine.Networking.dll
+ True
+
+
+ $(MangedDLLPath)\UnityEngine.UI.dll
+ True
+
+
+ ..\packages\MSTest.TestFramework.2.2.7\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll
+
+
+ ..\packages\MSTest.TestFramework.2.2.7\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {5d3b2f48-3f16-424d-8c87-d59cdf420ea9}
+ CSModLib
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TLM/CSModLib.Test/GameObjects/ExtNetInfoTest.cs b/TLM/CSModLib.Test/GameObjects/ExtNetInfoTest.cs
new file mode 100644
index 000000000..b0a053eca
--- /dev/null
+++ b/TLM/CSModLib.Test/GameObjects/ExtNetInfoTest.cs
@@ -0,0 +1,692 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Linq;
+using CSModLib.GameObjects;
+using static CSModLib.GameObjects.ExtNetInfo;
+
+namespace CSModLib.Test.GameObjects {
+ [TestClass]
+ public class ExtNetInfoTest {
+
+ [TestMethod]
+ public void TestTwoLane() {
+
+ var lanes = new[] {
+ PedestrianLane(-6.5f, 3f),
+ PedestrianLane(6.5f, 3f),
+ ParkingLane(-4f, true),
+ ParkingLane(4f, false),
+ CarLane(-1.5f, true),
+ CarLane(1.5f, false),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.TwoWay, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 2, 4, 5, 3, 1);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(2, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward, 4);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.OuterForward, 5);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.OuterForward);
+ }
+
+ [TestMethod]
+ public void TestTwoLaneOneWay() {
+
+ var lanes = new[] {
+ PedestrianLane(-6.5f, 3f),
+ PedestrianLane(6.5f, 3f),
+ ParkingLane(-4f, false),
+ ParkingLane(4f, false),
+ CarLane(-1.5f, false),
+ CarLane(1.5f, false),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.OneWay, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 2, 4, 5, 3, 1);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(1, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterForward, 4, 5);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.OuterForward);
+ }
+
+ [TestMethod]
+ public void TestFourLane() {
+
+ var lanes = new[] {
+ PedestrianLane(-13.5f, 5f),
+ PedestrianLane(13.5f, 5f),
+ ParkingLane(-10f, true),
+ ParkingLane(10f, false),
+ CarLane(-7.5f, true),
+ CarLane(7.5f, false),
+ CarLane(-4.5f, true),
+ CarLane(4.5f, false),
+ RaisedMedianLane(0f),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.TwoWay, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 2, 4, 6, 8, 7, 5, 3, 1);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(2, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward, 4, 6);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.OuterForward, 7, 5);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 8, ExtLaneFlags.None);
+ }
+
+ [TestMethod]
+ public void TestFourLaneInverted() {
+
+ var lanes = new[] {
+ PedestrianLane(-13.5f, 5f),
+ PedestrianLane(13.5f, 5f),
+ ParkingLane(-10f, false),
+ ParkingLane(10f, true),
+ CarLane(-7.5f, false),
+ CarLane(7.5f, true),
+ CarLane(-4.5f, false),
+ CarLane(4.5f, true),
+ RaisedMedianLane(0f),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Inverted, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 2, 4, 6, 8, 7, 5, 3, 1);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(2, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterForward, 4, 6);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.OuterBackward, 7, 5);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 8, ExtLaneFlags.None);
+ }
+
+ [TestMethod]
+ public void TestCFI() {
+
+ var lanes = new[] {
+ CarLane(0f, true),
+ ThruCarLane(3f, false),
+ ThruCarLane(6f, false),
+ ThruCarLane(9f, true),
+ ThruCarLane(12f, true),
+ CarLane(15f, false),
+ CarLane(18f, false),
+ CarLane(21f, false),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4, 5, 6, 7);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(4, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward, 0);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.DisplacedInnerForward | ExtLaneFlags.AllowCFI, 1, 2);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.DisplacedInnerBackward | ExtLaneFlags.AllowCFI, 3, 4);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.OuterForward, 5, 6, 7);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.DisplacedInnerForward | ExtLaneFlags.AllowCFI);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.DisplacedInnerForward | ExtLaneFlags.AllowCFI);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.DisplacedInnerBackward | ExtLaneFlags.AllowCFI);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.DisplacedInnerBackward | ExtLaneFlags.AllowCFI);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.OuterForward);
+ }
+
+ [TestMethod]
+ public void TestUncontrolledDisplaced() {
+
+ var lanes = new[] {
+ CarLane(0f, true),
+ CarLane(3f, false),
+ CarLane(6f, true),
+ CarLane(9f, false),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(4, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward, 0);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.DisplacedInnerForward | ExtLaneFlags.ForbidControlledLanes, 1);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.DisplacedInnerBackward | ExtLaneFlags.ForbidControlledLanes, 2);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.OuterForward, 3);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.DisplacedInnerForward | ExtLaneFlags.ForbidControlledLanes);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.DisplacedInnerBackward | ExtLaneFlags.ForbidControlledLanes);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.OuterForward);
+ }
+
+ [TestMethod]
+ public void TestForwardTurnaround() {
+
+ var lanes = new[] {
+ CarLane(0f, false),
+ CarLane(3f, true),
+ CarLane(6f, true),
+ CarLane(9f, false),
+ CarLane(12f, false),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(3, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.DisplacedOuterForward, 0);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.OuterBackward, 1, 2);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.OuterForward, 3, 4);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.DisplacedOuterForward);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.OuterForward);
+ }
+
+ [TestMethod]
+ public void TestBackwardTurnaround() {
+
+ var lanes = new[] {
+ CarLane(0f, true),
+ CarLane(3f, true),
+ CarLane(6f, false),
+ CarLane(9f, false),
+ CarLane(12f, true),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(3, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward, 0, 1);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.OuterForward, 2, 3);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.DisplacedOuterBackward, 4);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.DisplacedOuterBackward);
+ }
+
+ [TestMethod]
+ public void TestDualTurnaround() {
+
+ var lanes = new[] {
+ CarLane(0f, false),
+ CarLane(3f, true),
+ CarLane(6f, true),
+ CarLane(9f, false),
+ CarLane(12f, false),
+ CarLane(15f, true),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4, 5);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(4, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.DisplacedOuterForward, 0);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.OuterBackward, 1, 2);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.OuterForward, 3, 4);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.DisplacedOuterBackward, 5);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.DisplacedOuterForward);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.DisplacedOuterBackward);
+ }
+
+
+ [TestMethod]
+ public void TestDoubleDualTurnaround() {
+
+ var lanes = new[] {
+ CarLane(0f, false),
+ CarLane(3f, false),
+ CarLane(6f, true),
+ CarLane(9f, true),
+ CarLane(12f, false),
+ CarLane(15f, false),
+ CarLane(18f, true),
+ CarLane(21f, true),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4, 5, 6, 7);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(4, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.DisplacedOuterForward, 0, 1);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.OuterBackward, 2, 3);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.OuterForward, 4, 5);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.DisplacedOuterBackward, 6, 7);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.DisplacedOuterForward);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.DisplacedOuterForward);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.DisplacedOuterBackward);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.DisplacedOuterBackward);
+ }
+
+ [TestMethod]
+ public void TestInnerDisplacedBusLanes() {
+
+ var lanes = new[] {
+ CarLane(0f, true),
+ CarLane(3f, true),
+ ThruBusLane(6f, false),
+ ThruBusLane(9f, false),
+ PedestrianLane(13.5f, 6f),
+ ThruBusLane(18f, true),
+ ThruBusLane(21f, true),
+ CarLane(24f, false),
+ CarLane(27f, false),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4, 5, 6, 7, 8);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(4, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward, 0, 1);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.DisplacedInnerForward, 2, 3);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.DisplacedInnerBackward, 5, 6);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.OuterForward, 7, 8);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.OuterBackward);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.DisplacedInnerForward);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.DisplacedInnerForward);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.DisplacedInnerBackward);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.DisplacedInnerBackward);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.OuterForward);
+ VerifyLane(extNetInfo, 8, ExtLaneFlags.OuterForward);
+ }
+
+ [TestMethod]
+ public void TestFourCarriageway() {
+
+ var lanes = new[] {
+ PedestrianLane(0f, 3f),
+ ParkingLane(3f, true, 3f),
+ CarLane(6f, true),
+ PedestrianLane(9f, 3f),
+ ThruCarLane(12f, true),
+ ThruCarLane(15f, true),
+ RaisedMedianLane(18f),
+ ThruCarLane(21f, false),
+ ThruCarLane(24f, false),
+ PedestrianLane(27f, 3f),
+ CarLane(30f, false),
+ ParkingLane(33f, false, 3f),
+ PedestrianLane(36f, 3f),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.TwoWay, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(4, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane, 2);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane, 4, 5);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane, 7, 8);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane, 10);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 8, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 9, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 10, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 11, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 12, ExtLaneFlags.None);
+ }
+
+ [TestMethod]
+ public void TestThreeCarriageway() {
+
+ var lanes = new[] {
+ PedestrianLane(0f, 3f),
+ ParkingLane(3f, true, 3f),
+ CarLane(6f, true),
+ PedestrianLane(9f, 3f),
+ ThruCarLane(12f, true),
+ ThruCarLane(15f, true),
+ ThruCarLane(18f, false),
+ ThruCarLane(21f, false),
+ PedestrianLane(24f, 3f),
+ CarLane(27f, false),
+ ParkingLane(30f, false, 3f),
+ PedestrianLane(33f, 3f),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.TwoWay, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(4, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane, 2);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane, 4, 5);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane, 6, 7);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane, 9);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 8, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 9, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 10, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 11, ExtLaneFlags.None);
+ }
+
+ [TestMethod]
+ public void TestSixCarriageway() {
+
+ var lanes = new[] {
+ CarLane(0f, true),
+ CarLane(3f, true),
+ RaisedMedianLane(6f),
+ ThruCarLane(9f, true),
+ ThruCarLane(12f, true),
+ ThruBusLane(15f, false),
+ ThruBusLane(18f, false),
+ PedestrianLane(22.5f, 6f),
+ ThruBusLane(27f, true),
+ ThruBusLane(30f, true),
+ ThruCarLane(33f, false),
+ ThruCarLane(36f, false),
+ RaisedMedianLane(39f),
+ CarLane(42f, false),
+ CarLane(45f, false),
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(6, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane, 0, 1);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane, 3, 4);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.DisplacedInnerForward, 5, 6);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.DisplacedInnerBackward, 8, 9);
+ VerifyGroup(extNetInfo, 4, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane, 10, 11);
+ VerifyGroup(extNetInfo, 5, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane, 13, 14);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.InnerBackward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.DisplacedInnerForward);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.DisplacedInnerForward);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 8, ExtLaneFlags.DisplacedInnerBackward);
+ VerifyLane(extNetInfo, 9, ExtLaneFlags.DisplacedInnerBackward);
+ VerifyLane(extNetInfo, 11, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 10, ExtLaneFlags.InnerForward | ExtLaneFlags.AllowExpressLane);
+ VerifyLane(extNetInfo, 12, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 13, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 14, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane);
+ }
+
+ [TestMethod]
+ public void TestLargeCargoRoad() {
+
+ var lanes = new[] {
+ FlushMedianLane(-16.4f), // 0
+ FlushMedianLane(16.4f), // 1
+ FlushMedianLane(5.5f), // 2
+ PedestrianLane(-27f, 2f), // 3
+ FlushMedianLane(-5.5f), // 4
+ PedestrianLane(27f, 2f), // 5
+ CarLane(-11.1f, true), // 6
+ CarLane(11.1f, false), // 7
+ CarLane(-22.5f, true), // 8
+ CarLane(22.5f, false), // 9
+ CarLane(8.1f, true), // 10
+ CarLane(-8.1f, false), // 11
+ };
+
+ var extNetInfo = new ExtNetInfo(lanes);
+
+ Assert.AreEqual(LaneConfiguration.Complex, extNetInfo.m_roadLaneConfiguration);
+ Assert.AreEqual(lanes.Length, extNetInfo.m_extLanes.Length);
+ VerifySequence(extNetInfo.m_sortedLanes, 3, 8, 0, 6, 11, 4, 2, 10, 7, 1, 9, 5);
+
+ VerifyAggregations(extNetInfo);
+
+ Assert.AreEqual(6, extNetInfo.m_laneGroups.Length);
+ VerifyGroup(extNetInfo, 0, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane, 8);
+ VerifyGroup(extNetInfo, 1, ExtLaneFlags.InnerBackward | ExtLaneFlags.ForbidControlledLanes, 6);
+ VerifyGroup(extNetInfo, 2, ExtLaneFlags.DisplacedInnerForward | ExtLaneFlags.ForbidControlledLanes, 11);
+ VerifyGroup(extNetInfo, 3, ExtLaneFlags.DisplacedInnerBackward | ExtLaneFlags.ForbidControlledLanes, 10);
+ VerifyGroup(extNetInfo, 4, ExtLaneFlags.InnerForward | ExtLaneFlags.ForbidControlledLanes, 7);
+ VerifyGroup(extNetInfo, 5, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane, 9);
+
+ VerifyLane(extNetInfo, 0, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 1, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 2, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 3, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 4, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 5, ExtLaneFlags.None);
+ VerifyLane(extNetInfo, 6, ExtLaneFlags.InnerBackward | ExtLaneFlags.ForbidControlledLanes);
+ VerifyLane(extNetInfo, 7, ExtLaneFlags.InnerForward | ExtLaneFlags.ForbidControlledLanes);
+ VerifyLane(extNetInfo, 8, ExtLaneFlags.OuterBackward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 9, ExtLaneFlags.OuterForward | ExtLaneFlags.AllowServiceLane);
+ VerifyLane(extNetInfo, 10, ExtLaneFlags.DisplacedInnerBackward | ExtLaneFlags.ForbidControlledLanes);
+ VerifyLane(extNetInfo, 11, ExtLaneFlags.DisplacedInnerForward | ExtLaneFlags.ForbidControlledLanes);
+ }
+
+ private void VerifyAggregations(ExtNetInfo extNetInfo) {
+
+ var expectedForwardFlags = extNetInfo.m_extLanes.Select(l => l.m_extFlags).Where(f => (f & ExtLaneFlags.ForwardGroup) != 0).DefaultIfEmpty().Aggregate((x, y) => x | y);
+ var expectedBackwardFlags = extNetInfo.m_extLanes.Select(l => l.m_extFlags).Where(f => (f & ExtLaneFlags.BackwardGroup) != 0).DefaultIfEmpty().Aggregate((x, y) => x | y);
+ var expectedFlags = extNetInfo.m_extLanes.Select(l => l.m_extFlags).DefaultIfEmpty().Aggregate((x, y) => x | y);
+
+ Assert.AreEqual(expectedForwardFlags, extNetInfo.m_forwardExtLaneFlags);
+ Assert.AreEqual(expectedBackwardFlags, extNetInfo.m_backwardExtLaneFlags);
+ Assert.AreEqual(expectedFlags, extNetInfo.m_extLaneFlags);
+ }
+
+ private void VerifyGroup(ExtNetInfo extNetInfo, int groupIndex, ExtLaneFlags laneFlags, params int[] sortedLanes) {
+ Assert.AreEqual(laneFlags, extNetInfo.m_laneGroups[groupIndex].m_extLaneFlags);
+ VerifySequence(extNetInfo.m_laneGroups[groupIndex].m_sortedLanes, sortedLanes);
+ }
+
+ private void VerifyLane(ExtNetInfo extNetInfo, int laneIndex, ExtLaneFlags flags) {
+ Assert.AreEqual(flags, extNetInfo.m_extLanes[laneIndex].m_extFlags);
+ }
+
+ private void VerifySequence(int[] actual, params int[] expected) {
+ CollectionAssert.AreEqual(expected, actual);
+ }
+
+ private NetInfo.Lane PedestrianLane(float position, float width) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = width,
+ m_verticalOffset = 0f,
+ m_laneType = NetInfo.LaneType.Pedestrian,
+ m_direction = NetInfo.Direction.Both,
+ };
+
+ private NetInfo.Lane ParkingLane(float position, bool backward, float width = 2f) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = width,
+ m_verticalOffset = -.3f,
+ m_laneType = NetInfo.LaneType.Parking,
+ m_vehicleType = VehicleInfo.VehicleType.Car,
+ m_direction = backward ? NetInfo.Direction.Backward : NetInfo.Direction.Forward,
+ };
+
+ private NetInfo.Lane CarLane(float position, bool backward) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = 3f,
+ m_verticalOffset = -.3f,
+ m_laneType = NetInfo.LaneType.Vehicle,
+ m_vehicleType = VehicleInfo.VehicleType.Car,
+ m_direction = backward ? NetInfo.Direction.Backward : NetInfo.Direction.Forward,
+ m_allowConnect = true,
+ };
+
+ private NetInfo.Lane ThruCarLane(float position, bool backward) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = 3f,
+ m_verticalOffset = -.3f,
+ m_laneType = NetInfo.LaneType.Vehicle,
+ m_vehicleType = VehicleInfo.VehicleType.Car,
+ m_direction = backward ? NetInfo.Direction.Backward : NetInfo.Direction.Forward,
+ m_allowConnect = false,
+ };
+
+ private NetInfo.Lane BusLane(float position, bool backward) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = 3f,
+ m_verticalOffset = -.3f,
+ m_laneType = NetInfo.LaneType.TransportVehicle,
+ m_vehicleType = VehicleInfo.VehicleType.Car,
+ m_direction = backward ? NetInfo.Direction.Backward : NetInfo.Direction.Forward,
+ m_allowConnect = true,
+ };
+
+ private NetInfo.Lane ThruBusLane(float position, bool backward) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = 3f,
+ m_verticalOffset = -.3f,
+ m_laneType = NetInfo.LaneType.TransportVehicle,
+ m_vehicleType = VehicleInfo.VehicleType.Car,
+ m_direction = backward ? NetInfo.Direction.Backward : NetInfo.Direction.Forward,
+ m_allowConnect = false,
+ };
+
+ private NetInfo.Lane RaisedMedianLane(float position, float width = 0f) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = width,
+ m_verticalOffset = 0f,
+ m_direction = NetInfo.Direction.Both,
+ };
+
+ private NetInfo.Lane FlushMedianLane(float position, float width = 0f) =>
+ new NetInfo.Lane {
+ m_position = position,
+ m_width = width,
+ m_verticalOffset = -.3f,
+ m_direction = NetInfo.Direction.Both,
+ };
+ }
+}
diff --git a/TLM/CSModLib.Test/Properties/AssemblyInfo.cs b/TLM/CSModLib.Test/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..c4d5fb26c
--- /dev/null
+++ b/TLM/CSModLib.Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,20 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("CSModLib.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("CSModLib.Test")]
+[assembly: AssemblyCopyright("Copyright © 2022")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+[assembly: ComVisible(false)]
+
+[assembly: Guid("41f0631c-3ca5-4a0f-9fe8-938897b2f577")]
+
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/TLM/CSModLib.Test/packages.config b/TLM/CSModLib.Test/packages.config
new file mode 100644
index 000000000..d35ea08e3
--- /dev/null
+++ b/TLM/CSModLib.Test/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/TLM/CSModLib/CSModLib.csproj b/TLM/CSModLib/CSModLib.csproj
new file mode 100644
index 000000000..4b9d3e655
--- /dev/null
+++ b/TLM/CSModLib/CSModLib.csproj
@@ -0,0 +1,94 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}
+ Library
+ Properties
+ CSModLib
+ CSModLib
+ v3.5
+ 512
+ latest
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ true
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ ~/Library/Application Support/Steam/
+ $(ProgramFiles)\Steam
+ $(Registry:HKEY_CURRENT_USER\Software\Valve\Steam@SteamPath)
+ $(SteamPath)\steamapps\common\Cities_Skylines
+ $(CSPath)\Cities_Data\Managed
+ ..\dependencies
+ $(MSBuildExtensionsPath64)\..\Unity\
+ ..\Unity\
+
+
+
+ $(MangedDLLPath)\Assembly-CSharp.dll
+ False
+
+
+ $(MangedDLLPath)\ColossalManaged.dll
+ False
+
+
+ $(MangedDLLPath)\ICities.dll
+ False
+
+
+ $(MangedDLLPath)\UnityEngine.dll
+ False
+
+
+ $(MangedDLLPath)\UnityEngine.Networking.dll
+ False
+
+
+ $(MangedDLLPath)\UnityEngine.UI.dll
+ False
+
+
+ ..\packages\Portable.ConcurrentDictionary.1.0.4\lib\net35\Portable.ConcurrentDictionary.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TLM/CSModLib/Core/ReferenceEqualityComparer.cs b/TLM/CSModLib/Core/ReferenceEqualityComparer.cs
new file mode 100644
index 000000000..9814ceded
--- /dev/null
+++ b/TLM/CSModLib/Core/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 CSModLib.Core {
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/TLM/CSModLib/GameObjects/ExtNetInfo.LaneInspector.cs b/TLM/CSModLib/GameObjects/ExtNetInfo.LaneInspector.cs
new file mode 100644
index 000000000..2826b850e
--- /dev/null
+++ b/TLM/CSModLib/GameObjects/ExtNetInfo.LaneInspector.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace CSModLib.GameObjects {
+ partial class ExtNetInfo {
+
+ private class LaneInspector {
+
+ private readonly NetInfo.Lane[] lanes;
+ private readonly ExtNetInfo extNetInfo;
+
+ public LaneInspector(ExtNetInfo extNetInfo, NetInfo.Lane[] lanes) {
+ this.lanes = lanes;
+ this.extNetInfo = extNetInfo;
+ }
+
+ private int GetDirectionalSortKey(ExtLaneFlags flags) {
+ switch (flags & LaneGroupDirection) {
+ case ExtLaneFlags.BackwardGroup:
+ return 1;
+ default:
+ return 2;
+ case ExtLaneFlags.ForwardGroup:
+ return 3;
+ }
+ }
+
+ // TODO: Boundary conditions on sorting need work
+ public void CalculateSortedLanes() {
+ extNetInfo.m_sortedLanes = extNetInfo.m_extLanes.Select((extInfo, index) => index)
+ .OrderBy(i => lanes[i].m_position)
+ .ThenBy(i => GetDirectionalSortKey(extNetInfo.m_extLanes[i].m_extFlags))
+ .ToArray();
+ }
+
+ public LaneConfiguration GetLaneConfiguration() {
+
+ var minForward = float.MaxValue;
+ var maxForward = float.MinValue;
+ var minBackward = float.MaxValue;
+ var maxBackward = float.MinValue;
+
+ for (int i = 0; i < lanes.Length; i++) {
+ var flags = extNetInfo.m_extLanes[i].m_extFlags;
+ if ((flags & ExtLaneFlags.ForwardGroup) != 0) {
+ float position = lanes[i].m_position;
+ minForward = Math.Min(minForward, (float)position);
+ maxForward = Math.Max(maxForward, (float)position);
+
+ } else if ((flags & ExtLaneFlags.BackwardGroup) != 0) {
+ float position = lanes[i].m_position;
+ minBackward = Math.Min(minBackward, (float)position);
+ maxBackward = Math.Max(maxBackward, (float)position);
+ }
+ }
+
+ if (minBackward == float.MaxValue)
+ return minForward == float.MaxValue ? LaneConfiguration.Undefined : LaneConfiguration.OneWay;
+ else if (minForward == float.MaxValue)
+ return LaneConfiguration.InvertedOneWay;
+ else if (maxForward < minBackward || (minForward < minBackward && maxForward == minBackward))
+ return LaneConfiguration.Inverted;
+ else if (minForward < maxBackward)
+ return LaneConfiguration.Complex;
+ else
+ return LaneConfiguration.TwoWay;
+ }
+
+ public void FindDisplacedOuter(ExtLaneFlags workingDirection, ref int lastOuterDisplacedIndex) {
+
+ bool backward = workingDirection == ExtLaneFlags.BackwardGroup;
+
+ int scanStart = backward ? lanes.Length - 1 : 0;
+ int scanEnd = backward ? -1 : lanes.Length;
+ int step = backward ? -1 : 1;
+
+ for (int sortedIndex = scanStart; sortedIndex != scanEnd; sortedIndex += step) {
+ ExtLaneInfo extLane = extNetInfo.m_extLanes[extNetInfo.m_sortedLanes[sortedIndex]];
+ var direction = extLane.m_extFlags & LaneGroupDirection;
+ if ((direction & workingDirection) != 0) {
+ extLane.m_extFlags |= ExtLaneFlags.DisplacedOuter;
+ lastOuterDisplacedIndex = sortedIndex;
+ } else if (direction != 0) {
+ break;
+ }
+ }
+ }
+
+ public void FindOuterAndDisplacedInner(ExtLaneFlags workingDirection) {
+
+ bool backward = workingDirection == ExtLaneFlags.BackwardGroup;
+
+ var oppositeDirection = backward ? ExtLaneFlags.ForwardGroup : ExtLaneFlags.BackwardGroup;
+ int scanStart = backward ? 0 : lanes.Length - 1;
+ int scanEnd = backward ? lanes.Length : -1;
+ int step = backward ? 1 : -1;
+
+ int sortedIndex;
+
+ // skip opposite direction DisplacedOuter
+ for (sortedIndex = scanStart; sortedIndex != scanEnd; sortedIndex += step) {
+ if ((extNetInfo.m_extLanes[extNetInfo.m_sortedLanes[sortedIndex]].m_extFlags & workingDirection) != 0)
+ break;
+ }
+
+ // scan for Outer (may convert some to Inner later)
+ for (; sortedIndex != scanEnd; sortedIndex += step) {
+ var extLane = extNetInfo.m_extLanes[extNetInfo.m_sortedLanes[sortedIndex]];
+ var direction = extLane.m_extFlags & LaneGroupDirection;
+ if ((direction & workingDirection) != 0)
+ extLane.m_extFlags |= ExtLaneFlags.Outer;
+ if ((direction & oppositeDirection) != 0)
+ break;
+ }
+
+ // skip opposite direction
+ for (; sortedIndex != scanEnd; sortedIndex += step) {
+ if ((extNetInfo.m_extLanes[extNetInfo.m_sortedLanes[sortedIndex]].m_extFlags & workingDirection) != 0)
+ break;
+ }
+
+ // scan for DisplacedInner|ForwardGroup
+ for (; sortedIndex != scanEnd; sortedIndex += step) {
+ int laneIndex = extNetInfo.m_sortedLanes[sortedIndex];
+ var extLane = extNetInfo.m_extLanes[laneIndex];
+ if ((extLane.m_extFlags & workingDirection) != 0) {
+
+ // if we find DisplacedOuter lanes, we've reached the other side and we're done
+ if ((extLane.m_extFlags & ExtLaneFlags.DisplacedOuter) != 0)
+ break;
+
+ var lane = lanes[laneIndex];
+ extLane.m_extFlags |= ExtLaneFlags.DisplacedInner;
+ if (lane.m_allowConnect)
+ extLane.m_extFlags |= ExtLaneFlags.ForbidControlledLanes;
+ else if (lane.m_laneType == NetInfo.LaneType.Vehicle)
+ extLane.m_extFlags |= ExtLaneFlags.AllowCFI;
+ }
+ }
+ }
+
+ public void FindInner(ExtLaneFlags workingDirection) {
+
+ bool backward = workingDirection == ExtLaneFlags.BackwardGroup;
+
+ var oppositeDirection = backward ? ExtLaneFlags.ForwardGroup : ExtLaneFlags.BackwardGroup;
+ int scanStart = backward ? 0 : lanes.Length - 1;
+ int scanEnd = backward ? lanes.Length : -1;
+ int step = backward ? 1 : -1;
+
+ // The position multiplier causes lane positions to appear to this logic in ascending order
+ // regardless of the direction in which we are evaluating them. This keeps the spacial
+ // calculations simple on median detection.
+ int positionMultiplier = backward ? 1 : -1;
+
+ int sortedIndex;
+
+ // advance to first Outer
+
+ float laneWidth = float.MaxValue;
+ float laneEdge = float.MaxValue;
+ float laneElevation = float.MaxValue;
+ float medianEdge = float.MaxValue;
+ float medianElevation = float.MinValue;
+
+ int firstOuterLane;
+ int firstInnerLane = -1;
+
+ for (firstOuterLane = scanStart; firstOuterLane != scanEnd; firstOuterLane += step) {
+
+ var laneIndex = extNetInfo.m_sortedLanes[firstOuterLane];
+ if ((extNetInfo.m_extLanes[laneIndex].m_extFlags & workingDirection) != 0) {
+ var lane = lanes[laneIndex];
+ laneWidth = lane.m_width;
+ laneEdge = lane.m_position * positionMultiplier + laneWidth / 2;
+ laneElevation = lane.m_verticalOffset;
+ break;
+ }
+ }
+
+ // scan for median
+
+ for (sortedIndex = firstOuterLane + step; sortedIndex != scanEnd; sortedIndex += step) {
+ var laneIndex = extNetInfo.m_sortedLanes[sortedIndex];
+ var extLane = extNetInfo.m_extLanes[laneIndex];
+ var lane = lanes[laneIndex];
+ var position = lane.m_position * positionMultiplier;
+ if ((extLane.m_extFlags & workingDirection) != 0) {
+
+ if (((position - lane.m_width / 2) >= medianEdge
+ && lane.m_verticalOffset < medianElevation
+ && medianElevation - lane.m_verticalOffset < 3f)
+ || (position - lane.m_width - laneWidth) >= laneEdge) {
+ firstInnerLane = sortedIndex;
+ break;
+ }
+
+ medianEdge = float.MaxValue;
+ medianElevation = float.MinValue;
+ laneWidth = lane.m_width;
+ laneEdge = position + laneWidth / 2;
+ laneElevation = lane.m_verticalOffset;
+ } else if ((extLane.m_extFlags & oppositeDirection) != 0) {
+ break;
+ } else {
+ if (!lane.IsRoadLane() && lane.m_verticalOffset > laneElevation && lane.m_verticalOffset - laneElevation < 3f) {
+ if ((position - lane.m_width / 2) >= laneEdge) {
+ medianEdge = Math.Min(medianEdge, position + lane.m_width / 2);
+ medianElevation = Math.Max(medianElevation, lane.m_verticalOffset);
+ }
+ }
+ }
+ }
+
+ if (firstInnerLane >= 0) {
+ // apply AllowServiceLane
+
+ for (sortedIndex = scanStart; sortedIndex != firstInnerLane; sortedIndex += step) {
+ var laneIndex = extNetInfo.m_sortedLanes[sortedIndex];
+ var extLane = extNetInfo.m_extLanes[laneIndex];
+ if ((extLane.m_extFlags & (ExtLaneFlags.Outer | workingDirection)) == (ExtLaneFlags.Outer | workingDirection)
+ && lanes[laneIndex].IsCarLane()) {
+ extLane.m_extFlags |= ExtLaneFlags.AllowServiceLane;
+ }
+ }
+
+ // apply Inner
+
+ for (; sortedIndex != scanEnd; sortedIndex += step) {
+ var laneIndex = extNetInfo.m_sortedLanes[sortedIndex];
+ var extLane = extNetInfo.m_extLanes[laneIndex];
+ if ((extLane.m_extFlags & (ExtLaneFlags.Outer | workingDirection)) == (ExtLaneFlags.Outer | workingDirection)) {
+ extLane.m_extFlags &= ~ExtLaneFlags.Outer;
+ extLane.m_extFlags |= ExtLaneFlags.Inner;
+ var lane = lanes[laneIndex];
+ if (lane.m_allowConnect)
+ extLane.m_extFlags |= ExtLaneFlags.ForbidControlledLanes;
+ else if (lane.IsCarLane())
+ extLane.m_extFlags |= ExtLaneFlags.AllowExpressLane;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/TLM/CSModLib/GameObjects/ExtNetInfo.cs b/TLM/CSModLib/GameObjects/ExtNetInfo.cs
new file mode 100644
index 000000000..1923cf73e
--- /dev/null
+++ b/TLM/CSModLib/GameObjects/ExtNetInfo.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace CSModLib.GameObjects {
+ public partial class ExtNetInfo : ExtPrefabInfo {
+
+ [Flags]
+ public enum ExtLaneFlags {
+
+ None = 0,
+
+ ///
+ /// Lanes outside a median (if any) that divides lanes going the same direction.
+ /// In the absence of such a median, these are simply lanes that are not displaced.
+ ///
+ Outer = 1 << 0,
+
+ ///
+ /// Lanes inside a median that divides lanes going the same direction.
+ ///
+ Inner = 1 << 1,
+
+ ///
+ /// Displaced lanes that are between lanes going the opposite direction.
+ ///
+ DisplacedInner = 1 << 2,
+
+ ///
+ /// Displaced lanes that are located on the far side of the road from what is normal for their direction.
+ ///
+ DisplacedOuter = 1 << 3,
+
+ ///
+ /// Lanes in a group whose prevailing direction is forward.
+ ///
+ ForwardGroup = 1 << 4,
+
+ ///
+ /// Lanes in a group whose prevailing direction is backward.
+ ///
+ BackwardGroup = 1 << 5,
+
+ ///
+ /// Lanes that may be treated as service lanes in controlled lane routing.
+ ///
+ AllowServiceLane = 1 << 6,
+
+ ///
+ /// Lanes that may be treated as express lanes in controlled lane routing.
+ ///
+ AllowExpressLane = 1 << 7,
+
+ ///
+ /// Lanes that may be treated as displaced far turn lanes in controlled lane routing.
+ ///
+ AllowCFI = 1 << 8,
+
+ ///
+ /// Lanes whose properties cause controlled lane routing to be disabled for the segment.
+ ///
+ ForbidControlledLanes = 1 << 9,
+
+ OuterForward = Outer | ForwardGroup,
+ InnerForward = Inner | ForwardGroup,
+ DisplacedInnerForward = DisplacedInner | ForwardGroup,
+ DisplacedOuterForward = DisplacedOuter | ForwardGroup,
+
+ OuterBackward = Outer | BackwardGroup,
+ InnerBackward = Inner | BackwardGroup,
+ DisplacedInnerBackward = DisplacedInner | BackwardGroup,
+ DisplacedOuterBackward = DisplacedOuter | BackwardGroup,
+ }
+
+ ///
+ /// Mask to obtain the lane grouping key.
+ ///
+ public const ExtLaneFlags LaneGroupingKey =
+ ExtLaneFlags.Outer | ExtLaneFlags.Inner
+ | ExtLaneFlags.DisplacedInner | ExtLaneFlags.DisplacedOuter
+ | ExtLaneFlags.ForwardGroup | ExtLaneFlags.BackwardGroup;
+
+ ///
+ /// Mask to test a segment for the presence of service lanes.
+ ///
+ public const ExtLaneFlags ServiceLaneRule = ExtLaneFlags.AllowServiceLane | ExtLaneFlags.ForbidControlledLanes;
+
+ ///
+ /// Mask to test a segment for the presence of express lanes.
+ ///
+ public const ExtLaneFlags ExpressLaneRule = ExtLaneFlags.AllowExpressLane | ExtLaneFlags.ForbidControlledLanes;
+
+ ///
+ /// Mask to test a segment for the presence of displaced far turn lanes.
+ ///
+ public const ExtLaneFlags CFIRule = ExtLaneFlags.AllowCFI | ExtLaneFlags.ForbidControlledLanes;
+
+ ///
+ /// Mask to test the prevailing direction of a lane group.
+ ///
+ public const ExtLaneFlags LaneGroupDirection = ExtLaneFlags.ForwardGroup | ExtLaneFlags.BackwardGroup;
+
+ ///
+ /// Mask to test a segment for the presence of any displaced lanes.
+ ///
+ public const ExtLaneFlags DisplacedLanes = ExtLaneFlags.DisplacedInner | ExtLaneFlags.DisplacedOuter;
+
+ ///
+ /// Extended lane prefab data, directly corresponding to .
+ ///
+ public ExtLaneInfo[] m_extLanes;
+
+ ///
+ /// Lane group prefab data.
+ ///
+ public LaneGroupInfo[] m_laneGroups;
+
+ ///
+ /// Lane indices sorted by position, then by direction.
+ ///
+ public int[] m_sortedLanes;
+
+ ///
+ /// Aggregate lane flags for forward groups.
+ ///
+ public ExtLaneFlags m_forwardExtLaneFlags;
+
+ ///
+ /// Aggregate lane flags for backward groups.
+ ///
+ public ExtLaneFlags m_backwardExtLaneFlags;
+
+ ///
+ /// Aggregate of all lane flags.
+ ///
+ public ExtLaneFlags m_extLaneFlags;
+
+ public LaneConfiguration m_roadLaneConfiguration;
+
+ public ExtNetInfo(NetInfo info)
+ : this(info.m_lanes) {
+ }
+
+ public enum LaneConfiguration {
+ Undefined = 0,
+ TwoWay = 1 << 0,
+ OneWay = 1 << 1,
+ Inverted = 1 << 2,
+ Complex = 1 << 3,
+
+ InvertedOneWay = OneWay | Inverted,
+ }
+
+ private const LaneConfiguration GeneralTwoWayConfiguration = LaneConfiguration.TwoWay | LaneConfiguration.Complex;
+
+ public ExtNetInfo(NetInfo.Lane[] lanes) {
+
+ m_extLanes = lanes.Select(l => new ExtLaneInfo(l)).ToArray();
+
+ var inspector = new LaneInspector(this, lanes);
+
+ inspector.CalculateSortedLanes();
+
+ m_roadLaneConfiguration = inspector.GetLaneConfiguration();
+
+ var lastDisplacedOuterBackward = lanes.Length;
+ var lastDisplacedOuterForward = -1;
+
+ if (m_roadLaneConfiguration != LaneConfiguration.Complex) {
+ for (int i = 0; i < m_extLanes.Length; i++) {
+ if (lanes[i].IsRoadLane())
+ m_extLanes[i].m_extFlags |= ExtLaneFlags.Outer;
+ }
+ } else {
+
+ inspector.FindDisplacedOuter(ExtLaneFlags.ForwardGroup, ref lastDisplacedOuterForward);
+ inspector.FindDisplacedOuter(ExtLaneFlags.BackwardGroup, ref lastDisplacedOuterBackward);
+
+ inspector.FindOuterAndDisplacedInner(ExtLaneFlags.ForwardGroup);
+ inspector.FindOuterAndDisplacedInner(ExtLaneFlags.BackwardGroup);
+ }
+
+ if ((m_roadLaneConfiguration & GeneralTwoWayConfiguration) != 0) {
+
+ inspector.FindInner(ExtLaneFlags.ForwardGroup);
+ inspector.FindInner(ExtLaneFlags.BackwardGroup);
+ }
+
+ m_laneGroups = Enumerable.Range(0, lanes.Length)
+ .Select(index => new { index, lane = lanes[index], extLane = m_extLanes[index] })
+ .GroupBy(l => l.extLane.m_extFlags & LaneGroupingKey)
+ .Where(g => g.Key != 0)
+ .Select(g => new LaneGroupInfo {
+ m_extLaneFlags = g.Select(l => l.extLane.m_extFlags).Aggregate((ExtLaneFlags x, ExtLaneFlags y) => x | y),
+ m_sortedLanes = g.OrderBy(g => g.lane.m_position).Select(g => g.index).ToArray(),
+ })
+ .OrderBy(lg => lanes[lg.m_sortedLanes[0]].m_position)
+ .ToArray();
+
+ m_forwardExtLaneFlags = m_extLanes.Select(l => l.m_extFlags).Where(f => (f & ExtLaneFlags.ForwardGroup) != 0).DefaultIfEmpty().Aggregate((x, y) => x | y);
+ m_backwardExtLaneFlags = m_extLanes.Select(l => l.m_extFlags).Where(f => (f & ExtLaneFlags.BackwardGroup) != 0).DefaultIfEmpty().Aggregate((x, y) => x | y);
+
+ m_extLaneFlags = m_forwardExtLaneFlags | m_backwardExtLaneFlags;
+ }
+
+ public class ExtLaneInfo {
+
+ public ExtLaneFlags m_extFlags;
+
+ public ExtLaneInfo(NetInfo.Lane lane) {
+ if (lane.IsRoadLane()) {
+
+ switch (lane.m_direction) {
+ case NetInfo.Direction.Forward:
+ case NetInfo.Direction.AvoidBackward:
+ m_extFlags |= ExtLaneFlags.ForwardGroup;
+ break;
+
+ case NetInfo.Direction.Backward:
+ case NetInfo.Direction.AvoidForward:
+ m_extFlags |= ExtLaneFlags.BackwardGroup;
+ break;
+
+ case NetInfo.Direction.Both:
+ case NetInfo.Direction.AvoidBoth:
+ m_extFlags |= ExtLaneFlags.ForwardGroup | ExtLaneFlags.BackwardGroup;
+ break;
+ }
+ }
+ }
+ }
+
+ public class LaneGroupInfo {
+
+ ///
+ /// Indices of the lanes in this group, sorted by position.
+ ///
+ public int[] m_sortedLanes;
+
+ ///
+ /// Aggregate lane flags.
+ ///
+ public ExtLaneFlags m_extLaneFlags;
+ }
+ }
+}
diff --git a/TLM/CSModLib/GameObjects/ExtPrefabInfo.cs b/TLM/CSModLib/GameObjects/ExtPrefabInfo.cs
new file mode 100644
index 000000000..c88297800
--- /dev/null
+++ b/TLM/CSModLib/GameObjects/ExtPrefabInfo.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Concurrent;
+using CSModLib.Core;
+
+namespace CSModLib.GameObjects {
+ public abstract class ExtPrefabInfo
+ where TInfo : PrefabInfo
+ where TExtInfo : ExtPrefabInfo
+ {
+ private static readonly ConcurrentDictionary extInfos;
+
+ static ExtPrefabInfo() {
+ extInfos = new ConcurrentDictionary(ReferenceEqualityComparer.Instance);
+ }
+
+ public static TExtInfo GetInstance(TInfo info) {
+ return extInfos.GetOrAdd(info, i => (TExtInfo)Activator.CreateInstance(typeof(TExtInfo), i));
+ }
+ }
+}
diff --git a/TLM/CSModLib/GameObjects/FlagExtensions.cs b/TLM/CSModLib/GameObjects/FlagExtensions.cs
new file mode 100644
index 000000000..109248971
--- /dev/null
+++ b/TLM/CSModLib/GameObjects/FlagExtensions.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace CSModLib.GameObjects {
+ public static class FlagExtensions {
+
+ public static bool IsFlagSet(this NetInfo.LaneType laneType, NetInfo.LaneType flag)
+ => (laneType & flag) != 0;
+
+ public static bool IsFlagSet(this VehicleInfo.VehicleType vehicleType, VehicleInfo.VehicleType flag)
+ => (vehicleType & flag) != 0;
+ }
+}
diff --git a/TLM/CSModLib/GameObjects/NetInfoExtensions.cs b/TLM/CSModLib/GameObjects/NetInfoExtensions.cs
new file mode 100644
index 000000000..f293cb4d0
--- /dev/null
+++ b/TLM/CSModLib/GameObjects/NetInfoExtensions.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace CSModLib.GameObjects {
+ public static class NetInfoExtensions {
+
+ public static bool IsRoadLane(this NetInfo.Lane lane) =>
+ lane.m_laneType.IsFlagSet(NetInfo.LaneType.Vehicle | NetInfo.LaneType.TransportVehicle)
+ && lane.m_vehicleType.IsFlagSet(VehicleInfo.VehicleType.Car | VehicleInfo.VehicleType.Trolleybus);
+
+ public static bool IsCarLane(this NetInfo.Lane lane) =>
+ lane.m_laneType.IsFlagSet(NetInfo.LaneType.Vehicle)
+ && lane.m_vehicleType.IsFlagSet(VehicleInfo.VehicleType.Car);
+ }
+}
diff --git a/TLM/CSModLib/Properties/AssemblyInfo.cs b/TLM/CSModLib/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..532a7281e
--- /dev/null
+++ b/TLM/CSModLib/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("CSModLib")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("CSModLib")]
+[assembly: AssemblyCopyright("Copyright © 2022")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("5d3b2f48-3f16-424d-8c87-d59cdf420ea9")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/TLM/CSModLib/packages.config b/TLM/CSModLib/packages.config
new file mode 100644
index 000000000..b3303b846
--- /dev/null
+++ b/TLM/CSModLib/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/TLM/TLM/TLM.csproj b/TLM/TLM/TLM.csproj
index 69d877070..230b9cd93 100644
--- a/TLM/TLM/TLM.csproj
+++ b/TLM/TLM/TLM.csproj
@@ -1,4 +1,4 @@
-
+
@@ -107,6 +107,9 @@
..\libs\MoveItIntegration.dll
+
+ ..\packages\Portable.ConcurrentDictionary.1.0.4\lib\net35\Portable.ConcurrentDictionary.dll
+
$(MangedDLLPath)\System.Core.dll
@@ -882,6 +885,10 @@
+
+ {5d3b2f48-3f16-424d-8c87-d59cdf420ea9}
+ CSModLib
+
{f8759084-df5b-4a54-b73c-824640a8fa3f}
CSUtil.CameraControl
@@ -1270,6 +1277,8 @@ 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)CSModLib.dll" "%25DEPLOYDIR%25"
+xcopy /y "$(TargetDir)Portable.ConcurrentDictionary.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/packages.config b/TLM/TLM/packages.config
index 0604303d0..49e5d6c4e 100644
--- a/TLM/TLM/packages.config
+++ b/TLM/TLM/packages.config
@@ -3,6 +3,7 @@
+
diff --git a/TLM/TMPE.UnitTest/TMPE.UnitTest.csproj b/TLM/TMPE.UnitTest/TMPE.UnitTest.csproj
index c50ac9dc4..887948442 100644
--- a/TLM/TMPE.UnitTest/TMPE.UnitTest.csproj
+++ b/TLM/TMPE.UnitTest/TMPE.UnitTest.csproj
@@ -1,4 +1,4 @@
-
+
Debug
diff --git a/TLM/TMPE.sln b/TLM/TMPE.sln
index 0a18b4fcc..44b914964 100644
--- a/TLM/TMPE.sln
+++ b/TLM/TMPE.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29503.13
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32519.379
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TLM", "TLM\TLM.csproj", "{7422AE58-8B0A-401C-9404-F4A438EFFE10}"
ProjectSection(ProjectDependencies) = postProject
@@ -27,6 +27,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TMPE.API", "TMPE.API\TMPE.A
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{A38B1D43-3557-4D42-973C-90C7F5E10C02}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSModLib", "CSModLib\CSModLib.csproj", "{5D3B2F48-3F16-424D-8C87-D59CDF420EA9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSModLib.Test", "CSModLib.Test\CSModLib.Test.csproj", "{41F0631C-3CA5-4A0F-9FE8-938897B2F577}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Benchmark|Any CPU = Benchmark|Any CPU
@@ -121,6 +125,30 @@ Global
{A38B1D43-3557-4D42-973C-90C7F5E10C02}.Release TEST|Any CPU.Build.0 = Release|Any CPU
{A38B1D43-3557-4D42-973C-90C7F5E10C02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A38B1D43-3557-4D42-973C-90C7F5E10C02}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Benchmark|Any CPU.ActiveCfg = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Benchmark|Any CPU.Build.0 = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.FullDebug|Any CPU.Build.0 = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.PF2_Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.PF2_Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Release TEST|Any CPU.ActiveCfg = Release|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Release TEST|Any CPU.Build.0 = Release|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5D3B2F48-3F16-424D-8C87-D59CDF420EA9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Benchmark|Any CPU.ActiveCfg = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Benchmark|Any CPU.Build.0 = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.FullDebug|Any CPU.Build.0 = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.PF2_Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.PF2_Debug|Any CPU.Build.0 = Debug|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Release TEST|Any CPU.ActiveCfg = Release|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Release TEST|Any CPU.Build.0 = Release|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {41F0631C-3CA5-4A0F-9FE8-938897B2F577}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE