├── libs ├── TMPE.API.dll ├── CSURToolBox.dll ├── CSUtil.Commons.dll ├── HideCrosswalks.dll ├── TrafficManager.dll └── MoveItIntegration.dll ├── NodeController ├── Resources │ ├── sprites.png │ ├── PreviewImage.png │ ├── cursor_edit.png │ ├── cursor_error.png │ ├── cursor_move.png │ ├── cursor_insert.png │ ├── cursor_searching.png │ ├── uui_node_controller.png │ └── cursor_insert_crossing.png ├── Patches │ ├── PreloadPatch.cs │ ├── NetManager │ │ ├── AfterDeserialize.cs │ │ ├── UpdateNodeSegmentPatch.cs │ │ ├── ReleaseNodeImplementationPatch.cs │ │ ├── ReleaseSegmentImplementationPatch.cs │ │ └── CreateSegmentPatch.cs │ ├── TMPE │ │ ├── PrefixUtils.cs │ │ ├── GetDefaultUturnAllowed.cs │ │ ├── IsUturnAllowedConfigurable.cs │ │ ├── CanToggleTrafficLight.cs │ │ ├── GetDefaultPedestrianCrossingAllowed.cs │ │ ├── IsPedestrianCrossingAllowedConfigurable.cs │ │ ├── GetDefaultEnteringBlockedJunctionAllowed.cs │ │ └── IsEnteringBlockedJunctionAllowedConfigurable.cs │ ├── UpdateLanes.cs │ ├── NetNodePatches │ │ ├── UpdateBuilding.cs │ │ ├── RenderInstance.cs │ │ ├── CalculateNode.cs │ │ ├── RefreshJunctionData1Patch.cs │ │ └── RefreshJunctionData0.cs │ ├── BuilidingManger_SimulationStep_Patch.cs │ ├── Corner │ │ ├── FlatJunctions │ │ │ ├── FindDirectionPatch.cs │ │ │ └── FlatJunctionCommons.cs │ │ ├── CalculateCorner_MinCornerOffsetPatch.cs │ │ └── StretchPatches.cs │ ├── NetLanePatches │ │ ├── RefreshInstance.cs │ │ ├── PopulateGroupData.cs │ │ ├── RenderDestroyedInstance.cs │ │ ├── ANCalculatePropPosPatch.cs │ │ ├── RenderInstance.cs │ │ └── Commons.cs │ ├── HideCrosswalksMod │ │ └── ShouldHideCrossing.cs │ ├── UpdateNodeFlags.cs │ ├── NetTool │ │ ├── SplitSegmentPatch.cs │ │ └── MoveMiddleNodePatch.cs │ ├── NetSegmentPatches │ │ ├── UpdateSegments.cs │ │ └── RenderInstance.cs │ ├── GetPathTargetPositionPatch.cs │ ├── Nodeless │ │ ├── ClipSegmentEndPatch.cs │ │ ├── ClipSegmentEndPatch2.cs │ │ └── NodesLengthPatch.cs │ ├── HotReload │ │ └── ReadTypeMetadataPatch.cs │ └── VehicleSuperElevation │ │ ├── SimulationStepPatches.cs │ │ └── SuperElevationCommons.cs ├── Tool │ └── IToolUpdate.cs ├── TODO ├── Properties │ └── AssemblyInfo.cs ├── GUI │ ├── Panel │ │ ├── Corner │ │ │ ├── FlattenButton.cs │ │ │ ├── SlopeButton.cs │ │ │ ├── EmbankmentSlider.cs │ │ │ ├── StretchSlider.cs │ │ │ ├── ShiftSlider.cs │ │ │ ├── SlopeSlider.cs │ │ │ ├── UIOffsetSlider.cs │ │ │ ├── MassFlattenNodeCheckbox.cs │ │ │ ├── LockDirCheckbox.cs │ │ │ ├── FlatJunctionsCheckbox.cs │ │ │ ├── SharpCornersCheckbox.cs │ │ │ ├── NodelessCheckbox.cs │ │ │ └── TwistCheckbox.cs │ │ ├── IDataControllerUI.cs │ │ ├── CollapsorButton.cs │ │ ├── ButtonBase.cs │ │ ├── UIResetButton.cs │ │ ├── HintBox.cs │ │ └── HideMarlkingsCheckbox.cs │ ├── CornerMarker.cs │ ├── NodeControllerButton.cs │ └── Settings.cs ├── GlobalSuppressions.cs ├── path notes.txt ├── Util │ ├── Timer.cs │ ├── CSURUtil.cs │ ├── TMPEUtils.cs │ ├── HTCUtil.cs │ ├── MaterialUtils.cs │ ├── TextureUTILS.cs │ └── BuildingUtil.cs ├── Manager │ ├── INetworkData.cs │ └── LaneCache.cs ├── .editorconfig ├── LifeCycle │ ├── sequence logger.txt │ └── SerializableDataExtension.cs └── DecompiledSources │ └── NetAI.cs ├── .gitmodules ├── NodeController.sln ├── .gitattributes └── .github └── workflows └── Main.yml /libs/TMPE.API.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/libs/TMPE.API.dll -------------------------------------------------------------------------------- /libs/CSURToolBox.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/libs/CSURToolBox.dll -------------------------------------------------------------------------------- /libs/CSUtil.Commons.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/libs/CSUtil.Commons.dll -------------------------------------------------------------------------------- /libs/HideCrosswalks.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/libs/HideCrosswalks.dll -------------------------------------------------------------------------------- /libs/TrafficManager.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/libs/TrafficManager.dll -------------------------------------------------------------------------------- /libs/MoveItIntegration.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/libs/MoveItIntegration.dll -------------------------------------------------------------------------------- /NodeController/Resources/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/sprites.png -------------------------------------------------------------------------------- /NodeController/Resources/PreviewImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/PreviewImage.png -------------------------------------------------------------------------------- /NodeController/Resources/cursor_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/cursor_edit.png -------------------------------------------------------------------------------- /NodeController/Resources/cursor_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/cursor_error.png -------------------------------------------------------------------------------- /NodeController/Resources/cursor_move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/cursor_move.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "NodeController/KianCommons"] 2 | path = NodeController/KianCommons 3 | url = https://github.com/kianzarrin/KianCommons 4 | -------------------------------------------------------------------------------- /NodeController/Resources/cursor_insert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/cursor_insert.png -------------------------------------------------------------------------------- /NodeController/Resources/cursor_searching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/cursor_searching.png -------------------------------------------------------------------------------- /NodeController/Resources/uui_node_controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/uui_node_controller.png -------------------------------------------------------------------------------- /NodeController/Resources/cursor_insert_crossing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kianzarrin/NodeController/HEAD/NodeController/Resources/cursor_insert_crossing.png -------------------------------------------------------------------------------- /NodeController/Patches/PreloadPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches { 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | public class HotReloadPatchAttribute: Attribute { } 8 | } 9 | -------------------------------------------------------------------------------- /NodeController/Tool/IToolUpdate.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Tool { 2 | public interface IToolUpdate { 3 | /// 4 | /// this mehtod is called when ToolUpdate is called. 5 | /// 6 | void OnToolUpdate(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /NodeController/Patches/NetManager/AfterDeserialize.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches._NetManager; 2 | using NodeController.LifeCycle; 3 | using HarmonyLib; 4 | 5 | [HarmonyPatch(typeof(NetManager.Data), nameof(NetManager.Data.AfterDeserialize))] 6 | static class AfterDeserialize { 7 | static void Prefix() => SerializableDataExtension.BeforeNetMangerAfterDeserialize(); 8 | } 9 | -------------------------------------------------------------------------------- /NodeController/TODO: -------------------------------------------------------------------------------- 1 | - options for default universal behaviours: 2 | - auto embankment for curved segments. 3 | 4 | - new Corner table organisation 5 | 6 | - corner offset to increase chances of train track connection. 7 | - lod support (not now) 8 | 9 | bug : terrain height when setting middle node at tunnel entrance 10 | bug: dark texture when rendering middle node of a bridge where it meets the ground. 11 | -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/PrefixUtils.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches { 2 | using CSUtil.Commons; 3 | public static class PrefixUtils { 4 | public static bool HandleTernaryBool(TernaryBool? res, ref bool __result) { 5 | if (res == null || res == TernaryBool.Undefined) 6 | return true; // do nothing 7 | 8 | __result = res == TernaryBool.True; 9 | return false; // override 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NodeController/Patches/UpdateLanes.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController { 2 | using HarmonyLib; 3 | using KianCommons; 4 | using NodeController.Manager; 5 | 6 | [HarmonyPatch(typeof(RoadBaseAI))] 7 | [HarmonyPatch(nameof(RoadBaseAI.UpdateLanes))] 8 | [HarmonyAfter("me.tmpe")] 9 | [HarmonyPriority(Priority.Low)] 10 | class UpdateLanes { 11 | static void Postfix(ushort segmentID) { 12 | LaneCache.Instance.UpdateLanes(segmentID); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /NodeController/Patches/NetNodePatches/UpdateBuilding.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches; 2 | using ColossalFramework; 3 | using HarmonyLib; 4 | using KianCommons; 5 | using UnityEngine.Networking.Types; 6 | 7 | [HarmonyPatch(typeof(NetNode), nameof(NetNode.UpdateBuilding))] 8 | class UpdateBuilding { 9 | /// in case another mod updated node building without performing a full update 10 | static void Postfix(ushort nodeID) { 11 | BuilidingManger_SimulationStep_Patch.FixPillarNodeIDs.Add(nodeID); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /NodeController/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Setting ComVisible to false makes the types in this assembly not visible 6 | // to COM components. If you need to access a type in this assembly from 7 | // COM, set the ComVisible attribute to true on that type. 8 | [assembly: ComVisible(false)] 9 | 10 | // The following GUID is for the ID of the typelib if this project is exposed to COM 11 | [assembly: Guid("4e0bb9b3-efb1-4d63-99d8-a3b82d33e4fe")] 12 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/FlattenButton.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | public class FlattenButton : ButtonBase { 3 | public new NodeData Data => base.Data as NodeData; 4 | public override string HintHotkeys => null; 5 | public override bool ShouldShow => Data?.CanModifyFlatJunctions() ?? false; 6 | public override void Action(INetworkData data) => Data.Flatten(); 7 | 8 | public override void Awake() { 9 | base.Awake(); 10 | text = "Make flat"; 11 | } 12 | 13 | public override string HintDescription => "flatten the intersection"; 14 | } 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /NodeController/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Performance", "HAA0601:Value type to reference type conversion causing boxing allocation", Justification = "", Scope = "member", Target = "~M:NodeController.GUI.HintBox.Start")] 9 | [assembly: SuppressMessage("Style", "IDE0003:Remove qualification", Justification = "", Scope = "member", Target = "~M:NodeController.GUI.HintBox.Awake")] 10 | -------------------------------------------------------------------------------- /NodeController/Patches/BuilidingManger_SimulationStep_Patch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches { 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using HarmonyLib; 7 | 8 | [HarmonyPatch(typeof(BuildingManager),"SimulationStepImpl")] 9 | internal static class BuilidingManger_SimulationStep_Patch { 10 | // must be read/write from simulation thread. 11 | internal static HashSet FixPillarNodeIDs = new(); 12 | static void Prefix() { 13 | foreach(var nodeID in FixPillarNodeIDs) { 14 | NodeData.FixPillar(nodeID); 15 | } 16 | FixPillarNodeIDs.Clear(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /NodeController/path notes.txt: -------------------------------------------------------------------------------- 1 | VehicleAI.SimulationStep(ushort, ref Vehicle, ushort, ref Vehicle, int) : void @060042E3 2 | lastFrameData = vehicleData.GetLastFrameData(); 3 | CarAI.SimulationStep(ref lastFrameData) // modifies frame data. 4 | vehicleData.SetFrameData(Singleton.instance.m_currentFrameIndex, lastFrameData); 5 | 6 | CarAI.SimulationStep(ushort, ref Vehicle, ref Vehicle.Frame, ushort, ref Vehicle, int) : void @06003E95 7 | 8 | Vehicle.RenderInstance(...) 9 | uint targetFrame = this.GetTargetFrame(info, vehicleID); 10 | Vehicle.Frame frameData = this.GetFrameData(targetFrame - 32u); 11 | 12 | for simplicistic superlevation: 13 | CarAI.SimulationStep().PostFix() 14 | frame.rotation *= Euler(0,0,super elevation) 15 | -------------------------------------------------------------------------------- /NodeController/Util/Timer.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Util { 2 | using KianCommons; 3 | using System.Diagnostics; 4 | 5 | internal class Timer { 6 | private int counter; 7 | private Stopwatch sw = new(); 8 | private string name; 9 | long last; 10 | long step; 11 | 12 | public Timer(string name, long step = 1000) { 13 | this.name = name; 14 | this.step = step; 15 | } 16 | 17 | public void Start() { 18 | sw.Start(); 19 | counter++; 20 | } 21 | 22 | public void End() { 23 | sw.Stop(); 24 | var ms = sw.ElapsedMilliseconds; 25 | if (ms > last + step) { 26 | last = ms; 27 | Log.Info($"{name}: {counter} times took {ms}ms: {(float)ms / counter}ms/iteration"); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/SlopeButton.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | public class SlopeButton : ButtonBase{ 3 | public new NodeData Data => base.Data as NodeData; 4 | public override string HintHotkeys => null; 5 | 6 | public override bool ShouldShow => Data?.CanModifyFlatJunctions() ?? false; 7 | 8 | public override void Action(INetworkData data) => Data.UnFlatten(); 9 | 10 | public override void Awake() { 11 | base.Awake(); 12 | text = "Make Sloped"; 13 | } 14 | 15 | public override string HintDescription { 16 | get { 17 | if (Data.SegmentCount <= 2) 18 | return "make node follow road's slope"; 19 | return "make node follow main road's slope and twist side segments to match the slope"; 20 | } 21 | } 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /NodeController/Patches/Corner/FlatJunctions/FindDirectionPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches { 2 | using HarmonyLib; 3 | using JetBrains.Annotations; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | using System.Reflection.Emit; 7 | using UnityEngine; 8 | using static KianCommons.ReflectionHelpers; 9 | 10 | [UsedImplicitly] 11 | [HarmonyPatch] 12 | static class FindDirectionPatch { 13 | [UsedImplicitly] 14 | static MethodBase TargetMethod() => 15 | GetMethod(typeof(NetSegment), nameof(NetSegment.FindDirection)); 16 | 17 | [UsedImplicitly] 18 | public static IEnumerable Transpiler( 19 | IEnumerable instructions, MethodBase original) { 20 | return FlatJunctionsCommons.ModifyFlatJunctionsTranspiler(instructions, original); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NodeController/Patches/NetLanePatches/RefreshInstance.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.NetLanePatches { 2 | using HarmonyLib; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | 7 | [HarmonyPatch] 8 | public static class RefreshInstance { 9 | // public void RefreshInstance(uint laneID, NetInfo.Lane laneInfo, float startAngle, float endAngle, bool invert, ref RenderManager.Instance data, ref int propIndex) 10 | static MethodInfo Target = typeof(NetLane).GetMethod(nameof(NetLane.RefreshInstance), BindingFlags.Public | BindingFlags.Instance); 11 | static MethodBase TargetMethod() => Target; 12 | 13 | public static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions) 14 | => PropDisplacementCommons.Patch(instructions, Target); 15 | } // end class 16 | } // end name space -------------------------------------------------------------------------------- /NodeController/Patches/NetLanePatches/PopulateGroupData.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.NetLanePatches { 2 | using HarmonyLib; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | 7 | [HarmonyPatch] 8 | public static class PopulateGroupData { 9 | //public void PopulateGroupData(ushort segmentID, uint laneID, NetInfo.Lane laneInfo, bool destroyed, NetNode.Flags startFlags, NetNode.Flags endFlags, float startAngle, float 10 | static MethodInfo Target = typeof(NetLane).GetMethod(nameof(NetLane.PopulateGroupData), BindingFlags.Public | BindingFlags.Instance); 11 | static MethodBase TargetMethod() => Target; 12 | 13 | public static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions) 14 | => PropDisplacementCommons.Patch(instructions, Target); 15 | } // end class 16 | } // end name space -------------------------------------------------------------------------------- /NodeController/Patches/NetLanePatches/RenderDestroyedInstance.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.NetLanePatches { 2 | using HarmonyLib; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | 7 | [HarmonyPatch] 8 | public static class RenderDestroyedInstance { 9 | // public void RenderDestroyedInstance(RenderManager.CameraInfo cameraInfo, ushort segmentID, uint laneID, NetInfo netInfo, NetInfo.Lane laneInfo, NetNode.Flags startFlags, 10 | static MethodInfo Target = typeof(NetLane).GetMethod(nameof(NetLane.RenderDestroyedInstance), BindingFlags.Public | BindingFlags.Instance); 11 | static MethodBase TargetMethod() => Target; 12 | 13 | public static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions) 14 | => PropDisplacementCommons.Patch(instructions, Target); 15 | } // end class 16 | } // end name space -------------------------------------------------------------------------------- /NodeController/Patches/NetManager/UpdateNodeSegmentPatch.cs: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | namespace NodeController.Patches._NetManager; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using HarmonyLib; 8 | using KianCommons; 9 | using KianCommons.Patches; 10 | 11 | //[HarmonyPatch2(typeof(NetManager), typeof(UpdateNode))] 12 | static class UpdateNodePatch { 13 | delegate void UpdateNode(ushort node, ushort fromSegment, int level); 14 | static void Prefix(ushort node, ushort fromSegment, int level) { 15 | Log.Called("node:" + node, "fromSegment:" + fromSegment, "level:" + level); 16 | Log.Stack(); 17 | } 18 | } 19 | 20 | //[HarmonyPatch2(typeof(NetManager), typeof(UpdateSegment))] 21 | static class UpdateSegmentPatch { 22 | delegate void UpdateSegment(ushort segment); 23 | static void Prefix(ushort segment) { 24 | Log.Called("segment:" + segment); 25 | Log.Stack(); 26 | } 27 | } 28 | #endif -------------------------------------------------------------------------------- /NodeController/Patches/NetManager/ReleaseNodeImplementationPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using HarmonyLib; 4 | using ColossalFramework; 5 | 6 | namespace NodeController.Patches._NetManager 7 | { 8 | [HarmonyPatch] 9 | public static class ReleaseNodeImplementationPatch 10 | { 11 | public static MethodBase TargetMethod() 12 | { 13 | // ReleaseNodeImplementation(ushort node, ref NetNode data) 14 | return typeof(global::NetManager).GetMethod( 15 | "ReleaseNodeImplementation", 16 | BindingFlags.NonPublic | BindingFlags.Instance, 17 | Type.DefaultBinder, 18 | new[] { 19 | typeof(ushort), typeof(global::NetNode).MakeByRefType(), 20 | }, null); 21 | } 22 | 23 | public static void Prefix(ushort node) 24 | { 25 | NodeManager.Instance.SetNullNodeAndSegmentEnds(node); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /NodeController/Util/CSURUtil.cs: -------------------------------------------------------------------------------- 1 | using KianCommons; 2 | using System; 3 | using System.Runtime.CompilerServices; 4 | using KianCommons.Plugins; 5 | 6 | namespace NodeController.Util { 7 | public static class CSURUtil { 8 | public const string HARMONY_ID = "csur.toolbox"; 9 | internal static bool CSUREnabled; 10 | public static void Init() => CSUREnabled = PluginUtil.GetCSUR().IsActive(); 11 | 12 | public static float GetMinCornerOffset(ushort segmentID, ushort nodeID) { 13 | NetInfo info = nodeID.ToNode().Info; 14 | if (CSUREnabled && info.m_netAI is RoadBaseAI && info.name.Contains("CSUR")) { 15 | return GetMinCornerOffset_(info.m_minCornerOffset ,nodeID); 16 | } 17 | return segmentID.ToSegment().Info.m_minCornerOffset; 18 | } 19 | 20 | [MethodImplAttribute(MethodImplOptions.NoInlining)] 21 | private static float GetMinCornerOffset_(float cornerOffset0, ushort nodeID) { 22 | return CSURToolBox.Util.CSURUtil.GetMinCornerOffset(cornerOffset0,nodeID); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /NodeController/Patches/HideCrosswalksMod/ShouldHideCrossing.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.HideCrosswalksMod { 2 | using System.Reflection; 3 | using NodeController; 4 | using KianCommons.Patches; 5 | using HarmonyLib; 6 | using KianCommons; 7 | using KianCommons.Plugins; 8 | using NodeController.Util; 9 | using HideCrosswalks.Patches; 10 | 11 | [HarmonyPatch] 12 | public static class ShouldHideCrossing { 13 | static bool Prepare() => HTCUtil.IsActive; 14 | 15 | public static MethodBase TargetMethod() { 16 | return HTCUtil.Type_CalculateMaterialCommons. 17 | GetMethod(nameof(CalculateMaterialCommons.ShouldHideCrossing), throwOnError: true); 18 | } 19 | 20 | public static bool Prefix(ushort nodeID, ushort segmentID, ref bool __result) { 21 | var data = SegmentEndManager.Instance.GetAt( 22 | segmentID: segmentID, nodeID: nodeID); 23 | return PrefixUtils.HandleTernaryBool( 24 | data?.ShouldHideCrossingTexture(), 25 | ref __result); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/GetDefaultUturnAllowed.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.TMPE { 2 | using System.Reflection; 3 | using TrafficManager.Manager.Impl; 4 | using KianCommons.Patches; 5 | using KianCommons; 6 | using NodeController; 7 | using HarmonyLib; 8 | using KianCommons.Plugins; 9 | 10 | [HarmonyPatch] 11 | static class GetDefaultUturnAllowed { 12 | static bool Prepare() => PluginUtil.GetTrafficManager().IsActive(); 13 | public static MethodBase TargetMethod() { 14 | return typeof(JunctionRestrictionsManager). 15 | GetMethod(nameof(JunctionRestrictionsManager.GetDefaultUturnAllowed)); 16 | } 17 | 18 | public static bool Prefix(ushort segmentId, bool startNode, ref bool __result) { 19 | ushort nodeID = startNode ? segmentId.ToSegment().m_startNode : segmentId.ToSegment().m_endNode; 20 | var data = NodeManager.Instance.buffer[nodeID]; 21 | return PrefixUtils.HandleTernaryBool( 22 | data?.GetDefaultUturnAllowed(), 23 | ref __result); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/IsUturnAllowedConfigurable.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.TMPE { 2 | using System.Reflection; 3 | using TrafficManager.Manager.Impl; 4 | using NodeController; 5 | using KianCommons; 6 | using HarmonyLib; 7 | using KianCommons.Patches; 8 | using KianCommons.Plugins; 9 | 10 | [HarmonyPatch] 11 | static class IsUturnAllowedConfigurable { 12 | static bool Prepare() => PluginUtil.GetTrafficManager().IsActive(); 13 | 14 | public static MethodBase TargetMethod() { 15 | return typeof(JunctionRestrictionsManager). 16 | GetMethod(nameof(JunctionRestrictionsManager.IsUturnAllowedConfigurable)); 17 | } 18 | 19 | public static bool Prefix(ushort segmentId, bool startNode, ref bool __result) { 20 | ushort nodeID = startNode ? segmentId.ToSegment().m_startNode : segmentId.ToSegment().m_endNode; 21 | var data = NodeManager.Instance.buffer[nodeID]; 22 | return PrefixUtils.HandleTernaryBool( 23 | data?.IsUturnAllowedConfigurable(), 24 | ref __result); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/CanToggleTrafficLight.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.TMPE { 2 | using System.Reflection; 3 | using TrafficManager.Manager.Impl; 4 | using NodeController; 5 | using HarmonyLib; 6 | using TrafficManager.API.Traffic.Enums; 7 | using KianCommons.Patches; 8 | using KianCommons; 9 | using KianCommons.Plugins; 10 | 11 | [HarmonyPatch] 12 | static class CanToggleTrafficLightPatch { 13 | public delegate bool CanToggleTrafficLight(ushort nodeId, bool flag, ref NetNode node, out ToggleTrafficLightError reason); 14 | public static MethodBase TargetMethod() => 15 | typeof(TrafficLightManager).GetMethod(throwOnError: true); 16 | 17 | static bool Prepare() => PluginUtil.GetTrafficManager().IsActive(); 18 | 19 | public static bool Prefix(ref bool __result, ushort nodeId, ref ToggleTrafficLightError reason) { 20 | var nodeData = NodeManager.Instance.buffer[nodeId]; 21 | return PrefixUtils.HandleTernaryBool( 22 | nodeData?.CanHaveTrafficLights(out reason), 23 | ref __result); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/GetDefaultPedestrianCrossingAllowed.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.TMPE { 2 | using System.Reflection; 3 | using TrafficManager.Manager.Impl; 4 | using KianCommons.Patches; 5 | using KianCommons; 6 | using NodeController; 7 | using HarmonyLib; 8 | using KianCommons.Plugins; 9 | 10 | [HarmonyPatch] 11 | static class GetDefaultPedestrianCrossingAllowed { 12 | static bool Prepare() => PluginUtil.GetTrafficManager().IsActive(); 13 | public static MethodBase TargetMethod() { 14 | return typeof(JunctionRestrictionsManager). 15 | GetMethod(nameof(JunctionRestrictionsManager.GetDefaultPedestrianCrossingAllowed)); 16 | } 17 | 18 | public static bool Prefix(ushort segmentId, bool startNode, ref bool __result) { 19 | ushort nodeID = startNode ? segmentId.ToSegment().m_startNode : segmentId.ToSegment().m_endNode; 20 | NodeData data = NodeManager.Instance.buffer[nodeID]; 21 | 22 | return PrefixUtils.HandleTernaryBool( 23 | data?.GetDefaultPedestrianCrossingAllowed(), 24 | ref __result); 25 | 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /NodeController/Patches/UpdateNodeFlags.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController { 2 | using HarmonyLib; 3 | using CSUtil.Commons; 4 | using KianCommons; 5 | using TrafficManager.API.Manager; 6 | using TrafficManager.API.Traffic.Enums; 7 | using NodeController.Util; 8 | 9 | [HarmonyPatch(typeof(RoadBaseAI))] 10 | [HarmonyPatch(nameof(RoadBaseAI.UpdateNodeFlags))] 11 | static class UpdateNodeFlags { 12 | static ITrafficLightManager TL => TrafficManager.API.Implementations.ManagerFactory.TrafficLightManager; 13 | static void Postfix(ref NetNode data) { 14 | if (data.CountSegments() != 2)return; 15 | 16 | ushort nodeID = data.GetID(); 17 | NodeData nodeData = NodeManager.Instance.buffer[nodeID]; 18 | 19 | if (nodeData == null) return; 20 | 21 | if (nodeData.FirstTimeTrafficLight) { 22 | TMPEUtils.TryEnableTL(nodeID); 23 | nodeData.FirstTimeTrafficLight = false; 24 | } else if (nodeData.CanHaveTrafficLights(out _) == TernaryBool.False) { 25 | data.m_flags &= ~NetNode.Flags.TrafficLights; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/IsPedestrianCrossingAllowedConfigurable.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.TMPE { 2 | using System.Reflection; 3 | using TrafficManager.Manager.Impl; 4 | using NodeController; 5 | using KianCommons.Patches; 6 | using KianCommons; 7 | using HarmonyLib; 8 | using KianCommons.Plugins; 9 | 10 | [HarmonyPatch] 11 | static class IsPedestrianCrossingAllowedConfigurable { 12 | static bool Prepare() => PluginUtil.GetTrafficManager().IsActive(); 13 | 14 | public static MethodBase TargetMethod() { 15 | return typeof(JunctionRestrictionsManager). 16 | GetMethod(nameof(JunctionRestrictionsManager.IsPedestrianCrossingAllowedConfigurable)); 17 | } 18 | 19 | public static bool Prefix(ushort segmentId, bool startNode, ref bool __result) { 20 | ushort nodeID = startNode ? segmentId.ToSegment().m_startNode : segmentId.ToSegment().m_endNode; 21 | var data = NodeManager.Instance.buffer[nodeID]; 22 | return PrefixUtils.HandleTernaryBool( 23 | data?.IsPedestrianCrossingAllowedConfigurable(), 24 | ref __result); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/GetDefaultEnteringBlockedJunctionAllowed.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.TMPE { 2 | using System.Reflection; 3 | using TrafficManager.Manager.Impl; 4 | using NodeController; 5 | using HarmonyLib; 6 | using KianCommons.Patches; 7 | using KianCommons; 8 | using KianCommons.Plugins; 9 | 10 | [HarmonyPatch] 11 | static class GetDefaultEnteringBlockedJunctionAllowed { 12 | static bool Prepare() => PluginUtil.GetTrafficManager().IsActive(); 13 | 14 | public static MethodBase TargetMethod() { 15 | return typeof(JunctionRestrictionsManager). 16 | GetMethod(nameof(JunctionRestrictionsManager.GetDefaultEnteringBlockedJunctionAllowed)); 17 | } 18 | 19 | public static bool Prefix(ushort segmentId, bool startNode, ref bool __result) { 20 | ushort nodeID = startNode ? segmentId.ToSegment().m_startNode : segmentId.ToSegment().m_endNode; 21 | var data = NodeManager.Instance.buffer[nodeID]; 22 | return PrefixUtils.HandleTernaryBool( 23 | data?.GetDefaultEnteringBlockedJunctionAllowed(), 24 | ref __result); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/EmbankmentSlider.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using UnityEngine; 3 | using KianCommons; 4 | using System; 5 | 6 | public class EmbankmentSlider : UISliderBase { 7 | public override void Start() { 8 | base.Start(); 9 | minValue = -180; 10 | maxValue = 180; 11 | } 12 | 13 | public override void ApplyNode(NodeData data) 14 | => data.EmbankmentAngle = value; 15 | 16 | public override void ApplySegmentEnd(SegmentEndData data) 17 | => data.EmbankmentAngleDeg = value; 18 | 19 | public override string TooltipPostfix => " degrees"; 20 | 21 | public override void RefreshNode(NodeData data) => 22 | MixedValues = !data.HasUniformEmbankmentAngle(); 23 | 24 | public override void RefreshNodeValues(NodeData data) { 25 | isEnabled = data.CanMassEditNodeCorners(); 26 | if (isEnabled) 27 | value = data.EmbankmentAngle; 28 | } 29 | 30 | public override void RefreshSegmentEndValues(SegmentEndData data) { 31 | isEnabled = data.CanModifyCorners(); 32 | if (isEnabled) 33 | value = data.EmbankmentAngleDeg; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/IDataControllerUI.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | public enum NetworkTypeT { 3 | None, 4 | Node, 5 | Segment, 6 | SegmentEnd, 7 | Lane, 8 | } 9 | 10 | public interface IDataControllerUI { 11 | /// 12 | /// Apply values to custom data. 13 | /// may call or as appropriate. 14 | /// 15 | void Apply(); 16 | 17 | /// 18 | /// read values from custom data and refresh GUI elements accordingly. 19 | /// this will resized/rearrange panel, change the visibility of GUI elements and invalidates them. 20 | /// this will not update/modify custom data but it does call *Manager.GetOrCreate(). 21 | /// 22 | void Refresh(); 23 | 24 | 25 | /// 26 | /// Fast version of . it only reads values from custom data. 27 | /// however it does not resize panel, change elements visibility, or invalidate. 28 | /// 29 | void RefreshValues(); 30 | 31 | void Reset(); 32 | 33 | string HintHotkeys { get; } 34 | 35 | string HintDescription { get; } 36 | 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/StretchSlider.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using System; 3 | using ColossalFramework.UI; 4 | using KianCommons; 5 | using static KianCommons.HelpersExtensions; 6 | 7 | public class StretchSlider : UISliderBase { 8 | public override void Start() { 9 | base.Start(); 10 | minValue = 0; 11 | maxValue = +200; 12 | } 13 | 14 | public override void ApplyNode(NodeData data) 15 | => data.Stretch = value - 100; 16 | 17 | public override void ApplySegmentEnd(SegmentEndData data) 18 | => data.Stretch = value - 100; 19 | 20 | public override string TooltipPostfix => "%"; 21 | 22 | 23 | public override void RefreshNode(NodeData data) => 24 | MixedValues = !data.HasUniformStretch(); 25 | 26 | public override void RefreshNodeValues(NodeData data) { 27 | isEnabled = data.CanMassEditNodeCorners(); 28 | if (isEnabled) 29 | value = data.Stretch + 100; 30 | } 31 | 32 | public override void RefreshSegmentEndValues(SegmentEndData data) { 33 | isEnabled = data.CanModifyCorners(); 34 | if (isEnabled) 35 | value = data.Stretch + 100; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NodeController/Patches/NetTool/SplitSegmentPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches._NetTool { 2 | using HarmonyLib; 3 | using NodeController.LifeCycle; 4 | using static KianCommons.HelpersExtensions; 5 | using KianCommons; 6 | using System; 7 | 8 | [HarmonyPatch(typeof(global::NetTool), "SplitSegment")] 9 | public class SplitSegmentPatch 10 | { 11 | internal static MoveItSegmentData SegmentData3 { get; set; } // by move middle node 12 | internal static MoveItSegmentData SegmentData2 { get; set; } // by move middle node 13 | internal static MoveItSegmentData SegmentData { get; private set; } 14 | internal static bool CopyData => SegmentData != null || SegmentData2 !=null || SegmentData3 != null; 15 | 16 | public static void Prefix(ushort segment) 17 | { 18 | if (!InSimulationThread()) return; 19 | Log.Info($"SplitSegment.Prefix() segment:{segment}"/*\n" + Environment.StackTrace*/, true); 20 | SegmentData = MoveItIntegration.CopySegment(segment); 21 | } 22 | 23 | public static void Postfix() 24 | { 25 | if (!InSimulationThread()) return; 26 | Log.Debug($"SplitSegment.Postfix()\n" + Environment.StackTrace, false); 27 | SegmentData = SegmentData2 = SegmentData3 = null; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NodeController/Patches/NetSegmentPatches/UpdateSegments.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches; 2 | using HarmonyLib; 3 | using KianCommons; 4 | using System; 5 | using NodeController.LifeCycle; 6 | 7 | // not called in after deserialize 8 | internal static class UpdateSegmentsCommons { 9 | internal static void Postfix(ushort segmentID, bool startNode) { 10 | try { 11 | if (!NetUtil.IsSegmentValid(segmentID)) return; 12 | SegmentEndData segStart = SegmentEndManager.Instance.GetAt(segmentID, startNode); 13 | segStart?.OnAfterCalculate(); 14 | 15 | ref NetSegment segment = ref segmentID.ToSegment(); 16 | ushort nodeID = segment.GetNode(startNode); 17 | BuilidingManger_SimulationStep_Patch.FixPillarNodeIDs.Add(nodeID); 18 | } catch (Exception ex) { ex.Log($"segment:{segmentID}"); } 19 | } 20 | } 21 | 22 | 23 | // also called in after deserialize 24 | [HarmonyPatch(typeof(NetSegment), nameof(NetSegment.UpdateStartSegments))] 25 | static class UpdateStartSegments { 26 | static void Postfix(ushort segmentID) => UpdateSegmentsCommons.Postfix(segmentID, true); 27 | } 28 | 29 | // also called in after deserialize 30 | [HarmonyPatch(typeof(NetSegment), nameof(NetSegment.UpdateEndSegments))] 31 | static class UpdateEndSegments { 32 | static void Postfix(ushort segmentID) => UpdateSegmentsCommons.Postfix(segmentID, false); 33 | } 34 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/ShiftSlider.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using System; 3 | using ColossalFramework.UI; 4 | using KianCommons; 5 | using static KianCommons.HelpersExtensions; 6 | 7 | public class ShiftSlider : UISliderBase { 8 | public override void Start() { 9 | base.Start(); 10 | minValue = -20; 11 | maxValue = +20; 12 | LargeScrollStep = 1; 13 | LargeDragStep = 0.5f; 14 | CourseScrollStep = 0.1f; 15 | CourseDragStep = 0.1f; 16 | } 17 | 18 | public override void ApplyNode(NodeData data) 19 | => data.Shift = value; 20 | 21 | public override void ApplySegmentEnd(SegmentEndData data) 22 | => data.Shift = value; 23 | 24 | public override string TooltipPostfix => "m"; 25 | 26 | 27 | public override void RefreshNode(NodeData data) => 28 | MixedValues = !data.HasUniformShift(); 29 | 30 | public override void RefreshNodeValues(NodeData data) { 31 | isEnabled = data.CanMassEditNodeCorners(); 32 | if (isEnabled) 33 | value = data.Shift; 34 | } 35 | 36 | public override void RefreshSegmentEndValues(SegmentEndData data) { 37 | isEnabled = data.CanModifyCorners(); 38 | if (isEnabled) 39 | value = data.Shift; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /NodeController/Patches/TMPE/IsEnteringBlockedJunctionAllowedConfigurable.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.TMPE { 2 | using System.Reflection; 3 | using TrafficManager.Manager.Impl; 4 | using KianCommons.Patches; 5 | using KianCommons; 6 | using NodeController; 7 | using HarmonyLib; 8 | using ColossalFramework; 9 | using KianCommons.Plugins; 10 | 11 | [HarmonyPatch] 12 | static class IsEnteringBlockedJunctionAllowedConfigurable { 13 | static bool Prepare() => PluginUtil.GetTrafficManager().IsActive(); 14 | public static MethodBase TargetMethod() { 15 | return typeof(JunctionRestrictionsManager). 16 | GetMethod(nameof(JunctionRestrictionsManager.IsEnteringBlockedJunctionAllowedConfigurable)); 17 | } 18 | 19 | public static bool Prefix(ushort segmentId, bool startNode, ref bool __result) { 20 | ushort nodeID = startNode ? segmentId.ToSegment().m_startNode : segmentId.ToSegment().m_endNode; 21 | var data = NodeManager.Instance.buffer[nodeID]; 22 | if (data == null) { 23 | var flags = nodeID.ToNode().m_flags; 24 | bool oneway = flags.IsFlagSet(NetNode.Flags.OneWayIn) & flags.IsFlagSet(NetNode.Flags.OneWayOut); 25 | if (oneway & !segmentId.ToSegment().Info.m_hasPedestrianLanes) { 26 | __result = false; 27 | return false; 28 | } 29 | } 30 | 31 | 32 | return PrefixUtils.HandleTernaryBool( 33 | data?.IsEnteringBlockedJunctionAllowedConfigurable(), 34 | ref __result); 35 | 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /NodeController/Patches/NetNodePatches/RenderInstance.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Reflection; 4 | using System.Reflection.Emit; 5 | using System.Collections.Generic; 6 | 7 | namespace NodeController.Patches.NetNodePatches { 8 | using KianCommons; 9 | using KianCommons.Patches; 10 | 11 | [HarmonyPatch()] 12 | public static class RenderInstance { 13 | static void Log(string m) => KianCommons.Log.Info("NetNode_RenderInstance Transpiler: " + m); 14 | 15 | // RenderInstance(RenderManager.CameraInfo cameraInfo, ushort nodeID, NetInfo info, int iter, Flags flags, ref uint instanceIndex, ref RenderManager.Instance data) 16 | static MethodInfo Target => typeof(global::NetNode).GetMethod("RenderInstance", BindingFlags.NonPublic | BindingFlags.Instance); 17 | static MethodBase TargetMethod() { 18 | var ret = Target; 19 | Assertion.Assert(ret != null, "did not manage to find original function to patch"); 20 | Log("acquired method " + ret); 21 | return ret; 22 | } 23 | 24 | public static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions) { 25 | try { 26 | var codes = TranspilerUtils.ToCodeList(instructions); 27 | CalculateMaterialCommons.PatchCheckFlags(codes, occurance: 2, Target); 28 | 29 | Log("successfully patched NetNode.RenderInstance"); 30 | return codes; 31 | }catch(Exception e) { 32 | KianCommons.Log.Error(e.ToString()); 33 | throw e; 34 | } 35 | } 36 | } // end class 37 | } // end name space -------------------------------------------------------------------------------- /NodeController.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29926.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NodeController", "NodeController\NodeController.csproj", "{4E0BB9B3-EFB1-4D63-99D8-A3B82D33E4FE}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B14D55AE-A6B9-4110-B057-33FE2FA2E886}" 9 | ProjectSection(SolutionItems) = preProject 10 | .github\workflows\Main.yml = .github\workflows\Main.yml 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | Workshop|Any CPU = Workshop|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {4E0BB9B3-EFB1-4D63-99D8-A3B82D33E4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {4E0BB9B3-EFB1-4D63-99D8-A3B82D33E4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {4E0BB9B3-EFB1-4D63-99D8-A3B82D33E4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {4E0BB9B3-EFB1-4D63-99D8-A3B82D33E4FE}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {4E0BB9B3-EFB1-4D63-99D8-A3B82D33E4FE}.Workshop|Any CPU.ActiveCfg = Workshop|Any CPU 25 | {4E0BB9B3-EFB1-4D63-99D8-A3B82D33E4FE}.Workshop|Any CPU.Build.0 = Workshop|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ExtensibilityGlobals) = postSolution 31 | SolutionGuid = {3E70877A-D9DF-4D17-837F-E4C4A7B2B269} 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/SlopeSlider.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | public class SlopeSlider : UISliderBase { 3 | public override void Start() { 4 | base.Start(); 5 | minValue = -180; 6 | maxValue = 180; 7 | } 8 | 9 | public override void Reset() { 10 | var data = Root?.GetData(); 11 | if (data == null) return; 12 | 13 | if (data is SegmentEndData segmentEndData) { 14 | segmentEndData.DeltaSlopeAngleDeg = 0; 15 | } else if (data is NodeData nodeData) { 16 | foreach (var segEndData in nodeData.IterateSegmentEndDatas()) { 17 | segEndData.DeltaSlopeAngleDeg = 0; 18 | } 19 | } 20 | data.Update(); 21 | Root.Refresh(); 22 | } 23 | 24 | public override void ApplyNode(NodeData data) 25 | => data.SlopeAngleDeg = value; 26 | 27 | public override void ApplySegmentEnd(SegmentEndData data) 28 | => data.SlopeAngleDeg = value; 29 | 30 | public override string TooltipPostfix => " degrees"; 31 | 32 | public override void RefreshNode(NodeData data) => 33 | MixedValues = !data.HasUniformSlopeAngle(); 34 | 35 | public override void RefreshNodeValues(NodeData data) { 36 | isEnabled = data.CanMassEditNodeCorners(); 37 | if (isEnabled) 38 | value = data.SlopeAngleDeg; 39 | } 40 | 41 | public override void RefreshSegmentEndValues(SegmentEndData data) { 42 | isEnabled = data.CanModifyCorners(); 43 | if (isEnabled) 44 | value = data.SlopeAngleDeg; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NodeController/Patches/NetManager/ReleaseSegmentImplementationPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches._NetManager 2 | { 3 | using System; 4 | using System.Reflection; 5 | using HarmonyLib; 6 | using ColossalFramework; 7 | using NodeController.LifeCycle; 8 | using KianCommons; 9 | using static KianCommons.ReflectionHelpers; 10 | 11 | [HarmonyPatch] 12 | public static class ReleaseSegmentImplementationPatch 13 | { 14 | //private void ReleaseSegmentImplementation(ushort segment, ref NetSegment data, bool keepNodes) 15 | public static MethodBase TargetMethod() 16 | { 17 | return AccessTools.DeclaredMethod( 18 | typeof(NetManager), 19 | "ReleaseSegmentImplementation", 20 | new[] {typeof(ushort), typeof(NetSegment).MakeByRefType(), typeof(bool) }, 21 | null); 22 | } 23 | 24 | public static MoveItSegmentData UpgradingSegmentData; 25 | public static bool m_upgrading => 26 | (bool)GetFieldValue(NetUtil.netTool, "m_upgrading"); 27 | 28 | public static void Prefix(ushort segment) 29 | { 30 | if (UpgradingSegmentData != null) { 31 | KianCommons.Log.Error("Unexpected UpgradingSegmentData != null"); 32 | UpgradingSegmentData = null; 33 | } 34 | if (m_upgrading) { 35 | UpgradingSegmentData = MoveItIntegration.CopySegment(segment); 36 | } 37 | Log.Debug($"ReleaseSegment.Prefix({segment})\n"+Environment.StackTrace); 38 | SegmentEndManager.Instance.SetAt(segmentID: segment, true, value: null); 39 | SegmentEndManager.Instance.SetAt(segmentID: segment, false, value: null); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /NodeController/Patches/GetPathTargetPositionPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches { 2 | using HarmonyLib; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using UnityEngine; 7 | using NodeController; 8 | using KianCommons; 9 | 10 | [HarmonyPatch(typeof(CitizenAI), "GetPathTargetPosition")] 11 | static class GetPathTargetPositionPatch { 12 | public static IEnumerable Transpiler(IEnumerable instructions) { 13 | int replacements = 0; 14 | foreach (var instruction in instructions) { 15 | if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float value && value == 128) { 16 | replacements++; 17 | yield return new CodeInstruction(OpCodes.Ldloc, 4); 18 | yield return new CodeInstruction(OpCodes.Call, mGetGap_); 19 | } else { 20 | yield return instruction; 21 | } 22 | } 23 | Assertion.GT(replacements, 0, "no replacements could be made."); 24 | } 25 | 26 | private static MethodInfo mGetGap_ => 27 | AccessTools.Method(typeof(GetPathTargetPositionPatch), nameof(GetPathTargetPositionPatch.GetGap)); 28 | 29 | private static float GetGap(PathUnit.Position pathPos) { 30 | ref var segment = ref pathPos.m_segment.ToSegment(); 31 | bool startNode = pathPos.m_offset == 0; 32 | var nodeId = segment.GetNode(startNode); 33 | NodeData nodeData = NodeManager.Instance.buffer[nodeId]; 34 | return nodeData?.Gap ?? Mathf.Max(64f, segment.Info.Gap()); 35 | } 36 | 37 | private static float Gap(this NetInfo info) => Mathf.Max(info.m_minCornerOffset, info.m_halfWidth * 2); 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NodeController/Patches/NetLanePatches/ANCalculatePropPosPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.NetLanePatches { 2 | using HarmonyLib; 3 | using KianCommons; 4 | using KianCommons.Patches; 5 | using System; 6 | using System.Reflection; 7 | using UnityEngine; 8 | 9 | [HarmonyPatch] 10 | static class ANCalculatePropPosPatch { 11 | static MethodBase TargetMethod() => 12 | Type.GetType("AdaptiveRoads.Data.NetworkExtensions.PropRenderData, AdaptiveRoads", throwOnError:false)?. 13 | GetMethod("CalculatePropPos"); 14 | 15 | static bool Prepare() => TargetMethod() != null; 16 | 17 | static void Prefix( ref Vector3 __result, 18 | Vector3 pos, Vector3 tan, float t, 19 | ushort nodeId, ushort startSegmentId, ushort endSegmentId, 20 | bool isCatenary) { 21 | var start = SegmentEndManager.Instance.GetAt(startSegmentId, nodeID: nodeId); 22 | var end = SegmentEndManager.Instance.GetAt(endSegmentId, nodeID: nodeId); 23 | 24 | float stretchStart = start?.Stretch ?? 0; 25 | float stretchEnd = end?.Stretch ?? 0; 26 | float stretch = Mathf.Lerp(stretchStart, stretchEnd, t); 27 | stretch = 1 + stretch * 0.01f; // convert delta-percent to ratio 28 | pos.x *= stretch; 29 | 30 | float embankStart = start?.EmbankmentPercent ?? 0; 31 | float embankEnd = start?.EmbankmentPercent ?? 0; 32 | float embankment = Mathf.Lerp(embankStart, embankEnd, t); 33 | embankment *= 0.01f; //convert percent to ratio. 34 | float deltaY = pos.x * embankment; 35 | 36 | const bool reverse = false; // AN tracks are always forward. 37 | if (reverse) 38 | pos.y += deltaY; 39 | else 40 | pos.y -= deltaY; 41 | __result = pos; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NodeController/Patches/NetNodePatches/CalculateNode.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches { 2 | using ColossalFramework; 3 | using HarmonyLib; 4 | using KianCommons; 5 | 6 | [HarmonyPatch(typeof(NetNode), nameof(NetNode.CalculateNode))] 7 | class CalculateNode { 8 | static void Postfix(ushort nodeID) { 9 | NodeManager.Instance.OnBeforeCalculateNodePatch(nodeID); // invalid/unsupported nodes are set to null. 10 | NodeData nodeData = NodeManager.Instance.buffer[nodeID]; 11 | if (nodeData == null) 12 | return; 13 | 14 | ref NetNode node = ref nodeID.ToNode(); 15 | bool outside = node.m_flags.IsFlagSet(NetNode.Flags.Outside); 16 | if (nodeData.SegmentCount == 2 && !outside) { 17 | if (nodeData.NeedsTransitionFlag()) { 18 | node.m_flags |= NetNode.Flags.Transition; 19 | } else { 20 | node.m_flags &= ~NetNode.Flags.Transition; 21 | } 22 | if (nodeData.NeedMiddleFlag()) { 23 | node.m_flags &= ~(NetNode.Flags.Junction | NetNode.Flags.AsymForward | NetNode.Flags.AsymBackward); 24 | node.m_flags |= NetNode.Flags.Middle; 25 | } 26 | if (nodeData.NeedBendFlag()) { 27 | node.m_flags &= ~(NetNode.Flags.Junction | NetNode.Flags.Middle); 28 | node.m_flags |= NetNode.Flags.Bend; // TODO set asymForward and asymBackward 29 | } 30 | if (nodeData.NeedJunctionFlag()) { 31 | node.m_flags |= NetNode.Flags.Junction; 32 | node.m_flags &= ~(NetNode.Flags.Middle | NetNode.Flags.AsymForward | NetNode.Flags.AsymBackward | NetNode.Flags.Bend | NetNode.Flags.End); 33 | } 34 | node.m_flags &= ~NetNode.Flags.Moveable; 35 | } 36 | } // end postfix 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/UIOffsetSlider.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace NodeController.GUI { 4 | public class UIOffsetSlider : UISliderBase { 5 | public override void Awake() { 6 | base.Awake(); 7 | CourseDragStep = CourseScrollStep = 0.1f; 8 | LargeDragStep = LargeScrollStep = 1f; 9 | } 10 | 11 | public override void ApplyNode(NodeData data) => 12 | data.CornerOffset = value; 13 | 14 | public override void Reset() { 15 | var data = Root?.GetData(); 16 | if (data is SegmentEndData segmentEndData) { 17 | value = segmentEndData.DefaultCornerOffset; 18 | } else if (data is NodeData nodeData) { 19 | foreach (var segEndData in nodeData.IterateSegmentEndDatas()) { 20 | value = segEndData.DefaultCornerOffset; 21 | } 22 | } 23 | } 24 | 25 | public override void ApplySegmentEnd(SegmentEndData data) => 26 | data.CornerOffset = value; 27 | 28 | public override void RefreshNode(NodeData data) => 29 | MixedValues = !data.HasUniformCornerOffset(); 30 | 31 | public override string TooltipPostfix => "m"; 32 | 33 | public override void RefreshNodeValues(NodeData data) { 34 | isEnabled = data.CanModifyOffset(); 35 | if (isEnabled) { 36 | value = data.CornerOffset; 37 | } 38 | } 39 | 40 | public override void RefreshSegmentEndValues(SegmentEndData data) { 41 | isEnabled = data.CanModifyOffset(); 42 | if (isEnabled) { 43 | value = data.CornerOffset; 44 | bool mixed = !data.HasUniformCornerOffset(); 45 | if (MixedValues != mixed) { 46 | MixedValues = mixed; 47 | Invalidate(); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /NodeController/Util/TMPEUtils.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using KianCommons; 3 | using NodeController.Tool; 4 | using System; 5 | using TrafficManager.API.Manager; 6 | using UnityEngine; 7 | 8 | namespace NodeController.Util { 9 | public static class TMPEUtils { 10 | public static ITrafficLightManager TL => 11 | TrafficManager.API.Implementations.ManagerFactory.TrafficLightManager; 12 | 13 | internal static void TryEnableTL(ushort nodeID) { 14 | if (!TL.HasTrafficLight(nodeID) && TL.CanToggleTrafficLight(nodeID)) { 15 | TL.ToggleTrafficLight(nodeID); 16 | } 17 | } 18 | 19 | internal static bool WorldToScreenPoint(Vector3 worldPos, out Vector3 screenPos) { 20 | screenPos = NodeControllerTool.Camera.WorldToScreenPoint(worldPos); 21 | screenPos.y = Screen.height - screenPos.y; 22 | 23 | return screenPos.z >= 0; 24 | } 25 | internal static bool IsMouseOver(Rect boundingBox) { 26 | return boundingBox.Contains(Event.current.mousePosition); 27 | } 28 | 29 | delegate float dGetHandleAlphaT_(bool hovered); 30 | static dGetHandleAlphaT_ dGetHandleAlpha_; 31 | internal static float GetHandleAlpha(bool hovered) { 32 | if (dGetHandleAlpha_ == null) { 33 | var mGetHandleAlpha = AccessTools.DeclaredMethod( 34 | typeof(TrafficManager.UI.TrafficManagerTool), 35 | "GetHandleAlpha"); 36 | dGetHandleAlpha_ = (dGetHandleAlphaT_)Delegate.CreateDelegate( 37 | typeof(dGetHandleAlphaT_), mGetHandleAlpha); 38 | } 39 | return dGetHandleAlpha_(hovered); 40 | } 41 | 42 | internal static float GetBaseZoom() { 43 | return Screen.height / 1200f; 44 | } 45 | 46 | internal static Version TMPEVersion => 47 | TrafficManager.API.Implementations.ManagerFactory.VersionOf(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NodeController/Manager/INetworkData.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController { 2 | public interface INetworkData { 3 | /// 4 | /// marks the network for update in the next simulation step. 5 | /// once this node is updated, its recalculated. 6 | /// if the network is default/unsupported/invalid, manager will remove customisation. 7 | /// 8 | void Update(); 9 | 10 | /// 11 | /// Respond to external changes: 12 | /// - calculate new default values. (required by and ) 13 | /// - refresh node type, values. 14 | /// external changes includes: 15 | /// - segment added/remvoed 16 | /// - MoveIT moves segment/node. 17 | /// 18 | /// Call this: 19 | /// - after initialization 20 | /// - after CS has calcualted node but before custom modifications has been made 21 | /// 22 | /// Note: this does not mark network for update but rather responds to network update. 23 | /// 24 | void Calculate(); 25 | 26 | bool IsDefault(); 27 | 28 | void ResetToDefault(); 29 | 30 | /// 31 | /// Refreshes node state then marks the node for update. 32 | /// After major changes node must be refreshed before update so that the values are corrected. for example if node changes uturn corner offset must be a 33 | /// before calling update. 34 | /// its possible to call panel.Refresh() right after calling this method. in that case: 35 | /// - RefreshAndUpdate() prepairs node state prepairing it for panel.Refresh() 36 | /// - panel.Refresh() show/hide/enable/disable elements based on the current node state. 37 | /// - then simulation thread recalculates values. 38 | /// - OnAfterCalculate() will then Refreshes panel values. 39 | /// 40 | void RefreshAndUpdate(); 41 | 42 | bool IsSelected(); 43 | } 44 | 45 | public interface INetworkData { 46 | T Clone(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /NodeController/Patches/NetLanePatches/RenderInstance.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.NetLanePatches { 2 | using HarmonyLib; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using KianCommons; 7 | using static KianCommons.ReflectionHelpers; 8 | using KianCommons.Patches; 9 | using NodeController.Manager; 10 | 11 | [HarmonyPatch] 12 | public static class RenderInstance { 13 | // public void NetLane.RenderInstance(RenderManager.CameraInfo cameraInfo, ushort segmentID, uint laneID, NetInfo.Lane laneInfo, NetNode.Flags startFlags, NetNode.Flags endFlags, Color static MethodInfo Target => typeof(global::NetLane).GetMethod("RenderInstance", BindingFlags.NonPublic | BindingFlags.Instance); 14 | static MethodInfo Target = typeof(NetLane).GetMethod(nameof(NetLane.RenderInstance), BindingFlags.Public | BindingFlags.Instance); 15 | static MethodBase TargetMethod() => Target; 16 | 17 | static FieldInfo f_flags = GetField(nameof(NetLane.m_flags)); 18 | static MethodInfo mProcessFlags = GetMethod(typeof(RenderInstance), nameof(ProcessFlags)); 19 | 20 | public static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions, MethodBase original) { 21 | instructions = PropDisplacementCommons.Patch(instructions, Target); 22 | foreach (var code in instructions) { 23 | yield return code; 24 | if (code.LoadsField(f_flags)) { 25 | yield return TranspilerUtils.GetLDArg(original, "laneID"); 26 | yield return new CodeInstruction(OpCodes.Call, mProcessFlags); 27 | } 28 | } 29 | } 30 | 31 | public static NetLane.Flags ProcessFlags(NetLane.Flags flags, uint laneID) { 32 | if (LaneCache.Instance.ShouldHideArrows(laneID)) { 33 | return flags & ~(NetLane.Flags.LeftForwardRight); 34 | } else { 35 | return flags; 36 | } 37 | } 38 | } // end class 39 | } // end name space -------------------------------------------------------------------------------- /NodeController/Util/HTCUtil.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Util; 2 | using ColossalFramework.Plugins; 3 | using KianCommons; 4 | using KianCommons.Plugins; 5 | using System; 6 | using System.Reflection; 7 | using static ColossalFramework.Plugins.PluginManager; 8 | 9 | internal static class HTCUtil { 10 | static class Delegates { 11 | public delegate bool ShouldHideCrossing(ushort nodeID, ushort segmentID); 12 | } 13 | 14 | static HTCUtil() { 15 | Init(); 16 | PluginManager.instance.eventPluginsStateChanged += Init; 17 | PluginManager.instance.eventPluginsChanged += Init; 18 | LoadingManager.instance.m_levelPreLoaded += Init; 19 | } 20 | 21 | private static void Init() { 22 | try { 23 | Log.Stack(); 24 | Plugin = PluginUtil.GetHideCrossings(); 25 | IsActive = Plugin.IsActive(); 26 | 27 | if (IsActive) { 28 | asm = Plugin.GetMainAssembly(); 29 | var version = Plugin.userModInstance.VersionOf() ?? new Version(0, 0); 30 | Log.Info("HTC Version=" + version); 31 | shouldHideCrossing_ = DelegateUtil.CreateDelegate(Type_CalculateMaterialCommons); 32 | } else { 33 | Log.Info("HTC not found."); 34 | asm = null; 35 | } 36 | } catch(Exception ex) { ex.Log(); } 37 | } 38 | 39 | public static PluginInfo Plugin { get; private set; } 40 | 41 | public static bool IsActive { get; private set; } 42 | 43 | public static Assembly asm { get; private set; } 44 | 45 | /// 46 | /// type of 47 | /// 48 | public static Type Type_CalculateMaterialCommons => asm.GetType("HideCrosswalks.Patches.CalculateMaterialCommons"); 49 | 50 | static Delegates.ShouldHideCrossing shouldHideCrossing_; 51 | public static bool ShouldHideCrossing(ushort nodeID, ushort segmentID) { 52 | return shouldHideCrossing_?.Invoke(nodeID: nodeID, segmentID: segmentID) ?? false; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /NodeController/Patches/NetNodePatches/RefreshJunctionData1Patch.cs: -------------------------------------------------------------------------------- 1 | using KianCommons; 2 | using HarmonyLib; 3 | using System; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using JetBrains.Annotations; 7 | using KianCommons.Patches; 8 | 9 | namespace NodeController.Patches { 10 | 11 | [UsedImplicitly] 12 | [HarmonyPatch2(typeof(NetNode), typeof(RefreshJunctionData))] 13 | static class RefreshJunctionData1Patch 14 | { 15 | // non-DC 16 | delegate void RefreshJunctionData(ushort nodeID, int segmentIndex, ushort nodeSegment, Vector3 centerPos, ref uint instanceIndex, ref RenderManager.Instance data); 17 | 18 | [UsedImplicitly] 19 | static void Postfix(ushort nodeID, ref RenderManager.Instance data, ref Vector3 centerPos) 20 | { 21 | if(NodeManager.Instance.buffer[nodeID] is not NodeData blendData) return; 22 | 23 | centerPos = blendData.GetPosition(); // fix center pos. 24 | 25 | if(blendData.ShouldRenderCenteralCrossingTexture()) 26 | data.m_dataVector1.w = 0.01f; // puts crossings in the center. 27 | 28 | if(blendData.NodeType == NodeTypeT.Stretch) { 29 | ushort segmentID = nodeID.ToNode().GetSegment(data.m_dataInt0 & 7); 30 | var invert = segmentID.ToSegment().m_flags.IsFlagSet(NetSegment.Flags.Invert); 31 | var startNode = NetUtil.IsStartNode(segmentId: segmentID, nodeId: nodeID); 32 | bool turnAround = (startNode == !invert); 33 | if (turnAround) { 34 | // for segments it works like this: 35 | // 1- data.m_dataVector0.x *= -1 (can't do this for nodes) 36 | // 2- data.m_dataVector0.y *= -1 (can do this for nodes) 37 | 38 | // 1- data.m_dataVector0.x *= -1 does not work for node shader. so we do the equivalent of swapping left/right matrices: 39 | Helpers.Swap(ref data.m_dataMatrix0, ref data.m_dataMatrix1); 40 | Helpers.Swap(ref data.m_extraData.m_dataMatrix2, ref data.m_extraData.m_dataMatrix3); 41 | 42 | // 2- 43 | data.m_dataVector0.y *= -1; 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /NodeController/GUI/Panel/CollapsorButton.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework; 3 | using ColossalFramework.UI; 4 | using KianCommons.UI; 5 | using System.Collections; 6 | using UnityEngine; 7 | 8 | public class CollapsorButton : UIButton { 9 | private static SavedBool showAdvanced_ = new SavedBool("ShowAdvanced", NCSettings.FileName, false, true); 10 | private UIPanel targetPanel_; 11 | 12 | public override void Awake() { 13 | base.Awake(); 14 | name = GetType().FullName; 15 | autoSize = true; 16 | canFocus = false; 17 | 18 | normalBgSprite = ""; 19 | hoveredBgSprite = ""; 20 | pressedBgSprite = ""; 21 | disabledBgSprite = ""; 22 | atlas = TextureUtil.Ingame; 23 | 24 | text = " ▼ ▼ ▼ Advanced ▼ ▼ ▼"; 25 | height = 30f; 26 | textScale = 0.9f; 27 | m_TextVerticalAlign = UIVerticalAlignment.Bottom; 28 | horizontalAlignment = UIHorizontalAlignment.Center; 29 | textPadding = new RectOffset(5, 5, 5, 5); 30 | textColor = Color.white; 31 | hoveredTextColor = Color.Lerp(Color.blue, Color.white, .5f); 32 | pressedTextColor = Color.Lerp(Color.green, Color.white, 0); 33 | disabledTextColor = Color.Lerp(Color.black, Color.white, .5f); 34 | } 35 | 36 | public void SetTarget(UIPanel panel) { 37 | targetPanel_ = panel; 38 | if (!showAdvanced_.value) { 39 | targetPanel_.Hide(); 40 | } 41 | } 42 | public void Collapse() { 43 | targetPanel_.Hide(); 44 | text = text.Replace("▲", "▼"); 45 | GetComponentInParent().Refresh(); 46 | showAdvanced_.value = false; 47 | } 48 | 49 | public void Open() { 50 | targetPanel_.Show(); 51 | text = text.Replace("▼", "▲"); 52 | GetComponentInParent().Refresh(); 53 | showAdvanced_.value = true; 54 | } 55 | 56 | protected override void OnClick(UIMouseEventParameter p) { 57 | base.OnClick(p); 58 | if (targetPanel_.isVisible) { 59 | Collapse(); 60 | } else { 61 | Open(); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /NodeController/Patches/Nodeless/ClipSegmentEndPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.Nodeless { 2 | using HarmonyLib; 3 | using JetBrains.Annotations; 4 | using KianCommons; 5 | using NodeController.Util; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Reflection; 9 | using System.Reflection.Emit; 10 | using KianCommons.Patches; 11 | using static KianCommons.Patches.TranspilerUtils; 12 | 13 | [UsedImplicitly] 14 | [HarmonyPatch] 15 | internal static class ClipSegmentEndPatch { 16 | internal static bool GetClipSegmentEnd(bool clipSegmentEnd0, ushort nodeID, ushort segmentID) { 17 | var segmentData = SegmentEndManager.Instance.GetAt(segmentID: segmentID, nodeID: nodeID); 18 | bool nodeless = segmentData?.IsNodeless ?? false; 19 | return clipSegmentEnd0 && !nodeless; 20 | } 21 | 22 | [UsedImplicitly] 23 | static MethodBase TargetMethod() { 24 | return typeof(NetSegment).GetMethod( 25 | nameof(NetSegment.CalculateCorner), 26 | BindingFlags.Public | BindingFlags.Static, 27 | throwOnError: true); 28 | } 29 | 30 | static FieldInfo f_clipSegmentEnds => 31 | ReflectionHelpers.GetField(nameof(NetInfo.m_clipSegmentEnds)); 32 | 33 | static MethodInfo mGetClipSegmentEnd = ReflectionHelpers.GetMethod( 34 | typeof(ClipSegmentEndPatch), nameof(GetClipSegmentEnd)); 35 | 36 | public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) { 37 | CodeInstruction ldNodeID = GetLDArg(original, "startNodeID"); 38 | CodeInstruction ldSegmentID = GetLDArg(original, "ignoreSegmentID"); 39 | CodeInstruction callGetClipSegmentEnd = new CodeInstruction(OpCodes.Call, mGetClipSegmentEnd); 40 | 41 | int n = 0; 42 | foreach (var instruction in instructions) { 43 | yield return instruction; 44 | if (instruction.LoadsField(f_clipSegmentEnds)) { 45 | n++; 46 | yield return ldNodeID.Clone(); 47 | yield return ldSegmentID.Clone(); 48 | yield return callGetClipSegmentEnd.Clone(); 49 | } 50 | } 51 | 52 | Log.Succeeded($"patched {n} instances of {f_clipSegmentEnds} in {original}"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/ButtonBase.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework.UI; 3 | using System; 4 | using KianCommons; 5 | using KianCommons.UI; 6 | 7 | public abstract class ButtonBase : UIButtonExt, IDataControllerUI { 8 | public abstract string HintHotkeys { get; } 9 | 10 | public abstract string HintDescription { get; } 11 | 12 | public INetworkData Data => root_.Alive()?.GetData(); 13 | 14 | UIPanelBase root_; 15 | public override void Awake() { 16 | //Log.Debug("ButtonBase.Awake() called",false); 17 | base.Awake(); 18 | ParentWith = false; 19 | } 20 | 21 | public override void Start() { 22 | base.Start(); 23 | root_ = GetRootContainer() as UIPanelBase; 24 | } 25 | 26 | protected override void OnClick(UIMouseEventParameter p) { 27 | base.OnClick(p); 28 | if (refreshing_) 29 | return; 30 | Apply(); 31 | } 32 | 33 | public void Apply() { 34 | if (Log.VERBOSE) Log.Debug("UIResetButton.Apply called()\n" + Environment.StackTrace); 35 | var data = root_.Alive()?.GetData(); 36 | Action(data); 37 | data.RefreshAndUpdate(); 38 | Assertion.Assert(!refreshing_, "!refreshing_"); 39 | root_.Refresh(); 40 | } 41 | 42 | // protection against unnecessary apply/refresh/infinite recursion. 43 | bool refreshing_ = false; 44 | 45 | public void Refresh() { 46 | if (Log.VERBOSE) Log.Debug("Refresh called()\n" + Environment.StackTrace); 47 | RefreshValues(); 48 | refreshing_ = true; 49 | parent.isVisible = isVisible = isEnabled; 50 | Invalidate(); 51 | //Log.Debug("ButtonBase.Refresh(): isVisible set to " + isVisible); 52 | refreshing_ = false; 53 | } 54 | 55 | public void RefreshValues() { 56 | refreshing_ = true; 57 | INetworkData data = root_.Alive().GetData(); 58 | if (data == null) { 59 | Disable(); 60 | return; 61 | } 62 | isEnabled = ShouldShow; 63 | refreshing_ = false; 64 | } 65 | 66 | public abstract bool ShouldShow { get; } 67 | public abstract void Action(INetworkData data); 68 | 69 | public void Reset() { } //can't be reset. 70 | } 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /NodeController/GUI/CornerMarker.cs: -------------------------------------------------------------------------------- 1 | using NodeController.Tool; 2 | using UnityEngine; 3 | using KianCommons; 4 | using NodeController.Util; 5 | 6 | namespace NodeController.GUI { 7 | class CornerMarker { 8 | internal Vector3 TerrainPosition; // projected on terrain 9 | internal Vector3 Position; // original height. 10 | internal static float Radius = 2.5f; 11 | 12 | /// 13 | /// Intersects mouse ray with marker bounds. 14 | /// 15 | /// trueif mouse ray intersects with marker false otherwise 16 | internal bool IntersectRay() { 17 | Ray mouseRay = NodeControllerTool.Camera.ScreenPointToRay(Input.mousePosition); 18 | NodeControllerTool nctool = NodeControllerTool.Instance; 19 | float hitH = nctool.GetAccurateHitHeight(); 20 | 21 | Vector3 pos = Position; 22 | float mouseH = nctool.m_mousePosition.y; 23 | if (hitH < mouseH - KianToolBase.MAX_HIT_ERROR) { 24 | // For metros use projection on the terrain. 25 | pos = TerrainPosition; 26 | } else if (hitH - pos.y > KianToolBase.MAX_HIT_ERROR) { 27 | // if marker is projected on road plane above then modify its height 28 | pos.y = hitH; 29 | } 30 | Bounds bounds = new Bounds(center: pos, size: Vector3.one * Radius); 31 | return bounds.IntersectRay(mouseRay); 32 | } 33 | 34 | public void RenderOverlay(RenderManager.CameraInfo cameraInfo, Color color, bool hovered = false, bool selected = false) { 35 | float magnification = hovered ? 2f : 1f; 36 | if (selected) magnification = 2.5f; 37 | 38 | RenderManager.instance.OverlayEffect.DrawCircle( 39 | cameraInfo, 40 | color, 41 | TerrainPosition, 42 | Radius * magnification, 43 | TerrainPosition.y - 100f, // through all the geometry -100..100 44 | TerrainPosition.y + 100f, 45 | false, 46 | true); 47 | 48 | RenderManager.instance.OverlayEffect.DrawCircle( 49 | cameraInfo, 50 | selected ? Color.white : Color.black, 51 | TerrainPosition, 52 | Radius * 0.75f * magnification, // inner circle 53 | TerrainPosition.y - 100f, // through all the geometry -100..100 54 | TerrainPosition.y + 100f, 55 | false, 56 | false); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /NodeController/Patches/NetTool/MoveMiddleNodePatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches._NetTool { 2 | using HarmonyLib; 3 | using KianCommons; 4 | using System; 5 | using NodeController.LifeCycle; 6 | using static KianCommons.HelpersExtensions; 7 | using static KianCommons.Assertion; 8 | 9 | [HarmonyPatch(typeof(global::NetTool), "MoveMiddleNode")] 10 | public static class MoveMiddleNodePatch { 11 | internal static MoveItSegmentData SegmentData { get; private set; } 12 | internal static bool CopyData => SegmentData != null; 13 | internal static ushort NodeID, NodeID2; 14 | /// 15 | /// scenario 1: no change - returns the input node. 16 | /// scenario 2: move node : segment is released and a smaller segment is created - returns the moved node. 17 | /// scenario 3: merge node: segment is released and the other node is returned. 18 | /// 19 | /// How to handle: 20 | /// 1: skip (DONE) 21 | /// 2: copy segment end for the node that didn't move (moved node cannot have customisations) (DONE) 22 | /// 3: when split-segment creates a new segment, that copy segment end to it. 23 | /// 24 | /// input node 25 | 26 | 27 | public static void Prefix(ref ushort node) // TODO remove ref when in lates harmony. 28 | { 29 | if (!InSimulationThread()) return; 30 | NodeID = node; 31 | AssertEqual(NodeID.ToNode().CountSegments(), 1, "CountSegments"); 32 | ushort segmentID = NetUtil.GetFirstSegment(NodeID); 33 | Log.Info($"MoveMiddleNode.Prefix() node:{NodeID} segment:{segmentID}"/*+"\n" + Environment.StackTrace*/, true); 34 | SegmentData = MoveItIntegration.CopySegment(segmentID); 35 | NodeID2 = segmentID.ToSegment().GetOtherNode(NodeID); 36 | } 37 | 38 | /// output node 39 | public static void Postfix(ref ushort node) { 40 | if (!InSimulationThread()) return; 41 | if (SegmentData?.Start != null || SegmentData?.End != null) { 42 | Log.Debug($"MoveMiddleNode.Postfix()\n" + Environment.StackTrace,false); 43 | 44 | // scenario 3. 45 | if (node == NodeID2) { 46 | if (SplitSegmentPatch.SegmentData2 == null) { 47 | SplitSegmentPatch.SegmentData2 = SegmentData; 48 | } else { 49 | SplitSegmentPatch.SegmentData3 = SegmentData; 50 | } 51 | } 52 | } 53 | SegmentData = null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /NodeController/Patches/HotReload/ReadTypeMetadataPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.HotReload { 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | using HarmonyLib; 7 | using System.Text.RegularExpressions; 8 | using KianCommons; 9 | using NodeController.LifeCycle; 10 | 11 | [HarmonyPatch] 12 | [HotReloadPatch] 13 | /// 14 | /// Problem: object graph and type converter use type from different assembly versions 15 | /// (one gets type from first assembly while the other uses last assembly) which creates a conflict. 16 | /// Solution: Here we make sure both get type from last assembly by removing assembly version from type string. 17 | /// 18 | public static class ReadTypeMetadataPatch { 19 | private delegate Type GetType(string typeName, bool throwOnError); 20 | private static string assemblyName_ = typeof(ReadTypeMetadataPatch).Assembly.GetName().Name; 21 | 22 | private static bool Prepare() => LoadingManager.instance.m_loadingComplete = true; // hot-reload 23 | private static MethodBase TargetMethod() { 24 | var t = Type.GetType("System.Runtime.Serialization.Formatters.Binary.ObjectReader"); 25 | return AccessTools.DeclaredMethod(t, "ReadTypeMetadata"); 26 | } 27 | 28 | /// 29 | /// searches for call to GetType(typeString, true) and removes version data from type string. 30 | /// 31 | private static IEnumerable Transpiler(IEnumerable instructions) { 32 | MethodInfo mType_GetType = DelegateUtil.GetMethod(typeof(Type), nameof(GetType)); 33 | MethodInfo mReplaceAssemblyVersion = AccessTools.DeclaredMethod(typeof(ReadTypeMetadataPatch), nameof(ReplaceAssemblyVersion)); 34 | 35 | foreach (var code in instructions) { 36 | if (code.Calls(mType_GetType)) { 37 | yield return new CodeInstruction(OpCodes.Call, mReplaceAssemblyVersion); 38 | yield return new CodeInstruction(OpCodes.Ldc_I4_1); // load true again 39 | } 40 | yield return code; 41 | } 42 | } 43 | 44 | private static string ReplaceAssemblyVersion(string s, bool throwOnError) => ReplaceAssemblyVersionImpl(s); 45 | 46 | private static string ReplaceAssemblyVersionImpl(string s) { 47 | string num = "\\d+"; // matches ### 48 | string d = "\\."; // matches . 49 | string pattern = $"{assemblyName_}, Version={num}{d}{num}{d}{num}{d}{num}, Culture=neutral, PublicKeyToken=null"; 50 | var s2 = Regex.Replace(s, pattern, assemblyName_); 51 | return s2; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NodeController/Util/MaterialUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEngine; 4 | 5 | // TODO check out material.MainTextureScale 6 | // regarding weird nodes, what if we return a copy of the material? 7 | // Loading screens Mod owner wrote this about LODs: https://steamcommunity.com/workshop/filedetails/discussion/667342976/1636416951459546732/ 8 | namespace NodeController.Util { 9 | using static TextureUtils; 10 | public static class MaterialUtils { 11 | public static Texture2D TryGetTexture2D(this Material material, int textureID) { 12 | try { 13 | if (material.HasProperty(textureID)) 14 | { 15 | Texture texture = material.GetTexture(textureID); 16 | if (texture is Texture2D) 17 | return texture as Texture2D; 18 | } 19 | } 20 | catch { } 21 | //Log.Info($"Warning: failed to get {getTexName(textureID)} texture from material :" + material.name); 22 | return null; 23 | } 24 | 25 | public static NetInfo.Segment GetSegment(NetInfo info, int textureID) { 26 | NetInfo.Segment segmentInfo = null; 27 | foreach (var segmentInfo2 in info.m_segments ?? Enumerable.Empty()) { 28 | if (segmentInfo2.m_segmentMaterial.TryGetTexture2D(textureID) != null) { 29 | segmentInfo = segmentInfo2; 30 | break; 31 | } 32 | } 33 | return segmentInfo; 34 | } 35 | 36 | public static Material ContinuesMedian(Material material, NetInfo info, bool lod = false) { 37 | if (material == null) throw new ArgumentNullException("material"); 38 | if (info == null) throw new ArgumentNullException("info"); 39 | var segment = GetSegment(info, ID_APRMap); 40 | var segMaterial = segment.m_material; 41 | 42 | material = new Material(material); 43 | 44 | Texture2D tex; 45 | tex = segMaterial?.TryGetTexture2D(ID_Defuse); 46 | if (tex != null) material.SetTexture(ID_Defuse, tex); 47 | tex = segMaterial?.TryGetTexture2D(ID_APRMap); 48 | if (tex != null) material.SetTexture(ID_APRMap, tex); 49 | tex = segMaterial?.TryGetTexture2D(ID_XYSMap); 50 | if (tex != null) material.SetTexture(ID_XYSMap, tex); 51 | 52 | return material; 53 | } 54 | 55 | public static Mesh ContinuesMedian(Mesh mesh, NetInfo info, bool lod = false) { 56 | if (mesh == null) throw new ArgumentNullException("mesh"); 57 | if (info == null) throw new ArgumentNullException("info"); 58 | var segment = GetSegment(info, ID_APRMap); 59 | return segment?.m_mesh??mesh; 60 | } 61 | } // end class 62 | } // end namesapce 63 | 64 | -------------------------------------------------------------------------------- /NodeController/Patches/Corner/FlatJunctions/FlatJunctionCommons.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches { 2 | using HarmonyLib; 3 | using JetBrains.Annotations; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | using System.Reflection.Emit; 8 | using KianCommons; 9 | using static KianCommons.Assertion; 10 | using static KianCommons.Patches.TranspilerUtils; 11 | 12 | 13 | static class FlatJunctionsCommons { 14 | internal static bool GetFlatJunctions(bool flatJunctions0, ushort segmentID, ushort nodeID) { 15 | var data = SegmentEndManager.Instance.GetAt(segmentID,nodeID); 16 | return data?.FlatJunctions ?? flatJunctions0; 17 | } 18 | 19 | static FieldInfo f_flatJunctions = 20 | typeof(NetInfo).GetField(nameof(NetInfo.m_flatJunctions)) ?? 21 | throw new Exception("f_flatJunctions is null"); 22 | 23 | static MethodInfo mGetFlatJunctions = ReflectionHelpers.GetMethod( 24 | typeof(FlatJunctionsCommons), nameof(GetFlatJunctions)); 25 | 26 | public static IEnumerable ModifyFlatJunctionsTranspiler( 27 | IEnumerable instructions, 28 | MethodBase targetMethod) { 29 | AssertNotNull(targetMethod, "targetMethod"); 30 | //Log.Debug("targetMethod=" + targetMethod); 31 | CodeInstruction ldarg_nodeID = 32 | GetLDArg(targetMethod, "startNodeID", throwOnError:false) // CalculateCorner 33 | ?? GetLDArg(targetMethod, "nodeID"); // FindDirection 34 | CodeInstruction ldarg_segmentID = 35 | GetLDArg(targetMethod, "ignoreSegmentID", throwOnError: false) // CalculateCorner 36 | ?? GetLDArg(targetMethod, "segmentID"); // FindDirection 37 | AssertNotNull(ldarg_nodeID, "ldarg_nodeID"); 38 | 39 | CodeInstruction call_GetFlatJunctions = new CodeInstruction(OpCodes.Call, mGetFlatJunctions); 40 | Log.Debug("ldarg_nodeID=" + ldarg_nodeID); 41 | //Log.Debug("call_GetFlatJunctions=" + call_GetFlatJunctions); 42 | 43 | int n = 0; 44 | foreach (var instruction in instructions) { 45 | yield return instruction; 46 | bool is_ldfld_flatJunctions = 47 | instruction.opcode == OpCodes.Ldfld && instruction.operand == f_flatJunctions; 48 | if (is_ldfld_flatJunctions) { 49 | n++; 50 | yield return ldarg_segmentID; 51 | yield return ldarg_nodeID; 52 | yield return call_GetFlatJunctions; 53 | //Log.Debug("new instructions are:\n"+ instruction + "\n" + ldarg_nodeID + "\n" + call_GetFlatJunctions); 54 | } 55 | } 56 | 57 | Log.Debug($"TRANSPILER FlatJunctionsCommons: Successfully patched {targetMethod}. " + 58 | $"found {n} instances of Ldfld NetInfo.m_flatJunctions"); 59 | yield break; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /NodeController/Patches/Nodeless/ClipSegmentEndPatch2.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.Nodeless { 2 | using HarmonyLib; 3 | using KianCommons; 4 | using KianCommons.Patches; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Reflection; 8 | using System.Reflection.Emit; 9 | using UnityEngine; 10 | using static KianCommons.Patches.TranspilerUtils; 11 | 12 | [HarmonyPatch] 13 | static class ClipSegmentEndPatch2 { 14 | static IEnumerable TargetMethods() { 15 | yield return typeof(NetSegment).GetMethod(nameof(NetSegment.OverlapQuad), throwOnError: true); 16 | yield return typeof(ZoneBlock).GetMethod("CalculateImplementation1", throwOnError: true); 17 | } 18 | 19 | static bool GetClipSegmentStart(bool clipSegmentEnd0, ushort segmentID) => 20 | ClipSegmentEndPatch.GetClipSegmentEnd(clipSegmentEnd0, segmentID.ToSegment().m_startNode, segmentID); 21 | 22 | static bool GetClipSegmentEnd(bool clipSegmentEnd0, ushort segmentID) => 23 | ClipSegmentEndPatch.GetClipSegmentEnd(clipSegmentEnd0, segmentID.ToSegment().m_endNode, segmentID); 24 | 25 | static FieldInfo f_clipSegmentEnds => 26 | ReflectionHelpers.GetField(nameof(NetInfo.m_clipSegmentEnds)); 27 | 28 | static MethodInfo mGetClipSegmentStart = ReflectionHelpers.GetMethod( 29 | typeof(ClipSegmentEndPatch2), nameof(GetClipSegmentStart)); 30 | 31 | static MethodInfo mGetClipSegmentEnd = ReflectionHelpers.GetMethod( 32 | typeof(ClipSegmentEndPatch2), nameof(GetClipSegmentEnd)); 33 | 34 | public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) { 35 | CodeInstruction ldSegmentID = GetLDArg(original, "segmentID"); 36 | CodeInstruction callGetClipSegmentStart = new CodeInstruction(OpCodes.Call, mGetClipSegmentStart); 37 | CodeInstruction callGetClipSegmentEnd = new CodeInstruction(OpCodes.Call, mGetClipSegmentEnd); 38 | 39 | int n = 0; 40 | foreach (var instruction in instructions) { 41 | yield return instruction; 42 | if (instruction.LoadsField(f_clipSegmentEnds)) { 43 | yield return ldSegmentID.Clone(); 44 | 45 | // first one is for start node and second one is for end node. 46 | switch(n) { 47 | case 0: 48 | yield return callGetClipSegmentStart.Clone(); 49 | break; 50 | case 1: 51 | yield return callGetClipSegmentEnd.Clone(); 52 | break; 53 | default: 54 | new Exception("expected only 2 occurrences").Log(); 55 | break; 56 | } 57 | 58 | n++; 59 | } 60 | } 61 | 62 | Log.Succeeded($"patched {n} instances of {f_clipSegmentEnds} in {original}"); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/UIResetButton.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework.UI; 3 | using System; 4 | using UnityEngine; 5 | using static KianCommons.HelpersExtensions; 6 | using static KianCommons.Assertion; 7 | using KianCommons; 8 | using KianCommons.UI; 9 | 10 | public class UIResetButton : UIButton, IDataControllerUI { 11 | public static UIResetButton Instance { get; private set; } 12 | public string HintHotkeys => "del => reset all to default"; 13 | 14 | public string HintDescription { 15 | get { 16 | string ret = "Clears all customization. "; 17 | if (root_.NetworkType == NetworkTypeT.Node) 18 | ret += "including segment ends."; 19 | return ret; 20 | } 21 | } 22 | 23 | UIPanelBase root_; 24 | public override void Awake() { 25 | base.Awake(); 26 | Instance = this; 27 | name = nameof(UIResetButton); 28 | 29 | height = 30f; 30 | width = 200; 31 | 32 | textScale = 0.9f; 33 | normalBgSprite = "ButtonMenu"; 34 | hoveredBgSprite = "ButtonMenuHovered"; 35 | pressedBgSprite = "ButtonMenuPressed"; 36 | disabledBgSprite = "ButtonMenuDisabled"; 37 | disabledTextColor = new Color32(128, 128, 128, 255); 38 | canFocus = false; 39 | atlas = TextureUtil.Ingame; 40 | 41 | text = "Reset to default"; 42 | } 43 | 44 | public override void Start() { 45 | base.Start(); 46 | root_ = GetRootContainer() as UIPanelBase; 47 | } 48 | 49 | protected override void OnClick(UIMouseEventParameter p) { 50 | base.OnClick(p); 51 | if (refreshing_) 52 | return; 53 | Apply(); 54 | } 55 | 56 | public void Apply() { 57 | if (VERBOSE) Log.Debug("UIResetButton.Apply called()\n" + Environment.StackTrace); 58 | var data = root_?.GetData(); 59 | data?.ResetToDefault(); // also calls RefreshAndUpdate() 60 | Assert(!refreshing_, "!refreshing_"); 61 | root_.Refresh(); 62 | } 63 | 64 | // protection against unnecessary apply/refresh/infinite recursion. 65 | bool refreshing_ = false; 66 | 67 | public void Refresh() { 68 | if (VERBOSE) Log.Debug("Refresh called()\n" + Environment.StackTrace); 69 | RefreshValues(); 70 | refreshing_ = true; 71 | 72 | parent.isVisible = isVisible = true; 73 | Invalidate(); 74 | refreshing_ = false; 75 | } 76 | 77 | public void RefreshValues() { 78 | refreshing_ = true; 79 | INetworkData data = root_.GetData(); 80 | if (data == null) { 81 | Disable(); 82 | return; 83 | } 84 | isEnabled = !data.IsDefault(); 85 | refreshing_ = false; 86 | } 87 | 88 | public void Reset() { 89 | Apply(); 90 | } 91 | } 92 | } 93 | 94 | 95 | -------------------------------------------------------------------------------- /.github/workflows/Main.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | 7 | jobs: 8 | build: 9 | name: Build and Upload Release Asset 10 | 11 | runs-on: windows-latest 12 | 13 | env: 14 | ModName: NodeController 15 | Solution_File: NodeController.sln 16 | DeployDir: Deploy/Release/bin 17 | 18 | steps: 19 | - name: Get Sources 20 | uses: actions/checkout@v2 21 | with: 22 | submodules: true 23 | 24 | - name: Setup Build Environment 25 | uses: microsoft/setup-msbuild@v1 26 | 27 | - name: Get Referenced Binaries 28 | uses: actions/checkout@v2 29 | with: 30 | repository: kianzarrin/CSBinaries 31 | ref: refs/heads/master 32 | path: dependencies 33 | token: ${{ secrets.CSBINARIES_REPO_PAT }} 34 | 35 | - name: Build Solution 36 | id: build_solution 37 | run: | 38 | msbuild "${{ env.Solution_File }}" /m /verbosity:normal /restore /p:Configuration=Release /p:DeployDir="$env:GITHUB_WORKSPACE/$env:DeployDir" 39 | echo ::set-output name=ZIP_FILE::${$env:GITHUB_WORKSPACE/$env:DeployDir/../*.zip} 40 | 41 | - name: Test vars 42 | run: | 43 | echo zip file is ${{ steps.build_solution.outputs.ZIP_FILE }} 44 | echo asset name is ${{ env.ModName }}-${{ steps.get_version_number.outputs.VERSION }}.zip 45 | 46 | 47 | - name: Get Version Number 48 | id: get_version_number 49 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 50 | shell: bash 51 | 52 | - name: Pack Assets 53 | id: pack_assets 54 | run: | 55 | $ErrorView = 'NormalView' 56 | $env:ASSET_FILE_NAME = "${{ env.ModName }}-${{ steps.get_version_number.outputs.VERSION }}.zip" 57 | $env:ASSET_FILE = "$env:GITHUB_WORKSPACE/$env:DeployDir/$env:ASSET_FILE_NAME" 58 | echo "zipping $env:GITHUB_WORKSPACE/$env:DeployDir/ to $env:ASSET_FILE ..." 59 | Compress-Archive -Path $env:GITHUB_WORKSPACE/$env:DeployDir/** -DestinationPath $env:ASSET_FILE -CompressionLevel Optimal 60 | echo "::set-output name=ASSET_FILE::$env:ASSET_FILE" 61 | echo "::set-output name=ASSET_FILE_NAME::$env:ASSET_FILE_NAME" 62 | 63 | - name: Create Release 64 | id: create_release 65 | uses: actions/create-release@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | tag_name: ${{ github.ref }} 70 | release_name: Release ${{ steps.get_version_number.outputs.VERSION }} 71 | draft: false 72 | prerelease: false 73 | 74 | - name: Upload Release Asset 75 | id: upload-release-asset 76 | uses: actions/upload-release-asset@v1 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | with: 80 | upload_url: ${{ steps.create_release.outputs.upload_url }} 81 | asset_path: ${{ steps.pack_assets.outputs.ASSET_FILE }} 82 | asset_name: ${{ steps.pack_assets.outputs.ASSET_FILE_NAME }} 83 | asset_content_type: application/zip 84 | -------------------------------------------------------------------------------- /NodeController/Util/TextureUTILS.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Util { 2 | using System; 3 | using UnityEngine; 4 | using ColossalFramework.UI; 5 | using KianCommons; 6 | 7 | public static class TextureUtils { 8 | public delegate Texture2D TProcessor(Texture2D tex); 9 | public delegate Texture2D TProcessor2(Texture2D tex, Texture2D tex2); 10 | internal static int ID_Defuse => NetManager.instance.ID_MainTex; 11 | internal static int ID_APRMap => NetManager.instance.ID_APRMap; 12 | internal static int ID_XYSMap => NetManager.instance.ID_XYSMap; 13 | internal static string getTexName(int id) { 14 | if (id == ID_Defuse) return "_MainTex"; 15 | if (id == ID_APRMap) return "_APRMap"; 16 | if (id == ID_XYSMap) return "_XYSMap"; 17 | throw new Exception("Bad Texture ID"); 18 | } 19 | internal static int[] texIDs => new int[] { ID_Defuse, ID_APRMap, ID_XYSMap }; 20 | 21 | public static UITextureAtlas GetAtlas(string name) { 22 | UITextureAtlas[] atlases = Resources.FindObjectsOfTypeAll(typeof(UITextureAtlas)) as UITextureAtlas[]; 23 | foreach(var atlas in atlases) { 24 | if (atlas.name == name) 25 | return atlas; 26 | } 27 | return null; 28 | 29 | } 30 | 31 | /// 32 | /// reteurns a copy of the texture with the differenc that: mipmap=false, linear=false, readable=true; 33 | /// 34 | public static Texture2D GetReadableCopy(this Texture2D tex, bool linear = false) { 35 | Assertion.Assert(tex != null, "tex!=null"); 36 | Assertion.Assert(tex is Texture2D, $"tex is Texture2D"); 37 | Texture2D ret = tex.MakeReadable(linear); 38 | ret.name = tex.name; 39 | ret.anisoLevel = tex.anisoLevel; 40 | ret.filterMode = tex.filterMode; 41 | return ret; 42 | } 43 | 44 | public static Texture2D MakeReadable(this Texture texture, bool linear) { 45 | RenderTextureReadWrite RW_mode = linear ? RenderTextureReadWrite.Linear : RenderTextureReadWrite.Default; 46 | RenderTexture rt = RenderTexture.GetTemporary(texture.width, texture.height, 0, RenderTextureFormat.Default, RW_mode); 47 | Graphics.Blit(texture, rt); 48 | texture = rt.ToTexture2D(); 49 | RenderTexture.ReleaseTemporary(rt); 50 | return texture as Texture2D; 51 | } 52 | 53 | public static bool IsReadable(this Texture2D texture) { 54 | try { 55 | texture.GetPixel(0, 0); 56 | return true; 57 | } 58 | catch { 59 | return false; 60 | } 61 | } 62 | 63 | public static Texture2D TryMakeReadable(this Texture2D texture) { 64 | if (texture.IsReadable()) 65 | return texture; 66 | else 67 | return texture.MakeReadable(); 68 | } 69 | 70 | public static void Finalize(this Texture2D texture, bool lod = false) { 71 | texture.Compress(true); 72 | if (lod) texture.Apply(); 73 | else texture.Apply(true, true); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NodeController/Patches/NetNodePatches/RefreshJunctionData0.cs: -------------------------------------------------------------------------------- 1 | using KianCommons; 2 | using HarmonyLib; 3 | using System; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using JetBrains.Annotations; 7 | using KianCommons.Patches; 8 | using static KianCommons.Patches.TranspilerUtils; 9 | using System.Linq; 10 | using ColossalFramework; 11 | using System.Collections.Generic; 12 | using System.Reflection.Emit; 13 | namespace NodeController.Patches 14 | { 15 | // modify corner offset to fix center pos. 16 | // this patch is not necessary because center pos is calculated in NodeData.GetPosition() 17 | [UsedImplicitly] 18 | //[HarmonyPatch] 19 | class RefreshJunctionData0 { 20 | // main 21 | delegate void dRefreshJunctionData(ushort nodeID, NetInfo info, uint instanceIndex); 22 | static MethodInfo mGetSegment = GetMethod(typeof(NetNode), "GetSegment"); 23 | static FieldInfo f_minCornerOffset = 24 | typeof(NetInfo).GetField(nameof(NetInfo.m_minCornerOffset)) ?? 25 | throw new Exception("f_minCornerOffset is null"); 26 | static MethodInfo mGetMinCornerOffset = 27 | GetMethod(typeof(RefreshJunctionData0), nameof(GetMinCornerOffset)); 28 | 29 | [UsedImplicitly] 30 | static MethodBase TargetMethod() => 31 | DeclaredMethod(typeof(NetNode), "RefreshJunctionData"); 32 | 33 | public static float GetMinCornerOffset(float cornerOffset0, ushort nodeID, ushort segmentID) { 34 | var segmentData = SegmentEndManager.Instance. 35 | GetAt(segmentID: segmentID, nodeID: nodeID); 36 | if (segmentData == null) 37 | return cornerOffset0; 38 | return (segmentData.Corner(true).Offset + segmentData.Corner(false).Offset) * 0.5f; 39 | } 40 | 41 | [UsedImplicitly] 42 | static IEnumerable Transpiler(IEnumerable instructions, MethodBase original) { 43 | var codes = instructions.ToCodeList(); 44 | 45 | CodeInstruction ldarg_nodeID = GetLDArg(original, "nodeID"); // push nodeID into stack, 46 | CodeInstruction ldarg_segmentID = BuildSegmentLDLocFromSTLoc(codes); 47 | CodeInstruction call_GetMinCornerOffset = new CodeInstruction(OpCodes.Call, mGetMinCornerOffset); 48 | 49 | int n = 0; 50 | foreach (var instruction in instructions) { 51 | yield return instruction; 52 | bool is_ldfld_minCornerOffset = 53 | instruction.opcode == OpCodes.Ldfld && instruction.operand == f_minCornerOffset; 54 | if (is_ldfld_minCornerOffset) { 55 | n++; 56 | yield return ldarg_nodeID; 57 | yield return ldarg_segmentID; 58 | yield return call_GetMinCornerOffset; 59 | } 60 | } 61 | } 62 | 63 | public static CodeInstruction BuildSegmentLDLocFromSTLoc( 64 | List codes, int startIndex = 0, int count = 1) { 65 | Assertion.Assert(mGetSegment != null, "mGetSegment!=null"); 66 | int index = codes.Search(c => c.Calls(mGetSegment), startIndex: startIndex, count: count); 67 | index = codes.Search(c => c.IsStloc(), startIndex: index); 68 | return codes[index].BuildLdLocFromStLoc(); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/MassFlattenNodeCheckbox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using KianCommons; 3 | using static KianCommons.Assertion; 4 | using KianCommons.UI; 5 | using UnityEngine; 6 | using ColossalFramework.UI; 7 | 8 | public class MassFlattenNodeCheckbox :UICheckBoxExt, IDataControllerUI { 9 | public override void Awake() { 10 | base.Awake(); 11 | name = nameof(MassFlattenNodeCheckbox); 12 | label.text = "flatten node"; 13 | } 14 | 15 | public string HintHotkeys => null; 16 | 17 | public string HintDescription => "uncheck to give slope to main road\nembanks side roads if any"; 18 | 19 | Color GetColor() { 20 | if (containsMouse) 21 | return Color.white; 22 | return Color.Lerp(Color.grey, Color.white, 0.50f); 23 | } 24 | 25 | public override void Update() { 26 | base.Update(); 27 | var c = GetColor(); 28 | label.textColor = Color.Lerp(c, Color.white, 0.70f); 29 | } 30 | 31 | public override void OnCheckChanged(UIComponent component, bool value) { 32 | base.OnCheckChanged(component, value); 33 | if (refreshing_) return; 34 | Apply(); 35 | } 36 | 37 | UINodeControllerPanel root_; 38 | public override void Start() { 39 | base.Start(); 40 | width = parent.width; 41 | root_ = GetRootContainer() as UINodeControllerPanel; 42 | } 43 | 44 | 45 | public void Apply() { 46 | Assert(!refreshing_, "!refreshing_"); 47 | Log.Debug("FlattenNodeCheckbox.Apply called()\n"/* + Environment.StackTrace*/); 48 | 49 | NodeData data = root_?.NodeData; 50 | if (data == null) return; 51 | 52 | data.IsFlattened = isChecked; 53 | 54 | data.RefreshAndUpdate(); 55 | root_.Refresh(); 56 | } 57 | 58 | // protection against unncessary apply/refresh/infinite recursion. 59 | bool refreshing_ = false; 60 | 61 | public void Refresh() { 62 | //Log.Debug("Refresh called()\n"/* + Environment.StackTrace*/); 63 | RefreshValues(); 64 | refreshing_ = true; 65 | if (isEnabled && root_?.GetData() is NodeData nodeData) { 66 | RefreshNode(nodeData); 67 | } 68 | 69 | parent.isVisible = isVisible = this.isEnabled; 70 | parent.Invalidate(); 71 | Invalidate(); 72 | refreshing_ = false; 73 | } 74 | 75 | public void RefreshNode(NodeData data) { 76 | if (data.HasUniformFlatJunction()) { 77 | checkedBoxObject.color = Color.white; 78 | } else { 79 | checkedBoxObject.color = Color.grey; 80 | } 81 | } 82 | 83 | public void RefreshValues() { 84 | refreshing_ = true; 85 | INetworkData data = root_?.GetData(); 86 | if (data is NodeData nodeData) { 87 | isEnabled = nodeData.CanModifyFlatJunctions(); 88 | if (isEnabled) { 89 | this.isChecked = nodeData.IsFlattened; 90 | } 91 | } 92 | refreshing_ = false; 93 | } 94 | 95 | public void Reset() { 96 | isChecked = false; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /NodeController/Patches/Corner/CalculateCorner_MinCornerOffsetPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.Corner { 2 | using HarmonyLib; 3 | using JetBrains.Annotations; 4 | using KianCommons; 5 | using NodeController; 6 | using NodeController.Patches; 7 | using NodeController.Util; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Reflection; 11 | using System.Reflection.Emit; 12 | using static KianCommons.Patches.TranspilerUtils; 13 | 14 | [UsedImplicitly] 15 | [HarmonyPatch] 16 | static class CalculateCorner_MinCornerOffsetPatch { 17 | /// left side going away from the junction 18 | static float FixMinCornerOffset(float cornerOffset0, ushort nodeID, ushort segmentID, bool leftSide) { 19 | var segmentData = SegmentEndManager.Instance. 20 | GetAt(segmentID: segmentID, nodeID: nodeID); 21 | if (segmentData == null) 22 | return cornerOffset0; 23 | if (segmentData.IsNodeless) 24 | return 0; 25 | return segmentData.Corner(leftSide).Offset; 26 | } 27 | 28 | [UsedImplicitly] 29 | static MethodBase TargetMethod() { 30 | return typeof(NetSegment).GetMethod( 31 | nameof(NetSegment.CalculateCorner), 32 | BindingFlags.Public | BindingFlags.Static) ?? 33 | throw new Exception("CalculateCornerPatch Could not find target method."); 34 | } 35 | 36 | [HarmonyBefore(CSURUtil.HARMONY_ID)] 37 | public static IEnumerable Transpiler( 38 | IEnumerable instructions, MethodBase original) { 39 | FieldInfo f_minCornerOffset = 40 | typeof(NetInfo).GetField(nameof(NetInfo.m_minCornerOffset)) ?? 41 | throw new Exception("f_minCornerOffset is null"); 42 | MethodInfo m_GetMinCornerOffset = 43 | typeof(NetAI).GetMethod(nameof(NetAI.GetMinCornerOffset), throwOnError: true); 44 | 45 | MethodInfo m_FixMinCornerOffset = ReflectionHelpers.GetMethod( 46 | typeof(CalculateCorner_MinCornerOffsetPatch), nameof(FixMinCornerOffset)); 47 | 48 | // apply the flat junctions transpiler 49 | instructions = FlatJunctionsCommons.ModifyFlatJunctionsTranspiler(instructions, original); 50 | 51 | CodeInstruction ldarg_startNodeID = GetLDArg(original, "startNodeID"); // push startNodeID into stack, 52 | CodeInstruction ldarg_segmentID = GetLDArg(original, "ignoreSegmentID"); 53 | CodeInstruction ldarg_leftSide = GetLDArg(original, "leftSide"); 54 | CodeInstruction call_GetMinCornerOffset = new CodeInstruction(OpCodes.Call, m_FixMinCornerOffset); 55 | 56 | int n = 0; 57 | foreach (var instruction in instructions) { 58 | yield return instruction; 59 | bool is_ldfld_minCornerOffset = instruction.LoadsField(f_minCornerOffset); 60 | bool callsGetMinCornerOffset = instruction.Calls(m_GetMinCornerOffset); 61 | if (is_ldfld_minCornerOffset || callsGetMinCornerOffset) { 62 | n++; 63 | yield return ldarg_startNodeID; 64 | yield return ldarg_segmentID; 65 | yield return ldarg_leftSide; 66 | yield return call_GetMinCornerOffset; 67 | } 68 | } 69 | 70 | Log.Debug($"TRANSPILER CalculateCornerPatch: Successfully patched NetSegment.CalculateCorner(). " + 71 | $"found {n} instances of Ldfld NetInfo.m_minCornerOffset or GetMinCornerOffset()"); 72 | yield break; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/LockDirCheckbox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using KianCommons; 3 | using static KianCommons.HelpersExtensions; 4 | using static KianCommons.Assertion; 5 | 6 | using KianCommons.UI; 7 | using UnityEngine; 8 | using ColossalFramework.UI; 9 | using NodeController.Tool; 10 | 11 | public class LockDirCheckbox :UICheckBoxExt, IDataControllerUI, IToolUpdate { 12 | public override void Awake() { 13 | base.Awake(); 14 | name = nameof(LockDirCheckbox); 15 | label.text = "lock"; 16 | } 17 | 18 | public string HintHotkeys => "control + click => link both lock dir toggles"; 19 | 20 | public string HintDescription => null; 21 | 22 | static bool LockMode => ControlIsPressed && !AltIsPressed; 23 | Color GetColor() { 24 | if (containsMouse && LockMode) 25 | return Color.green; 26 | 27 | if (Mirror != null && Mirror.containsMouse && LockMode) 28 | return Color.green; 29 | 30 | if (containsMouse) 31 | return Color.white; 32 | 33 | return Color.Lerp(Color.grey, Color.white, 0.50f); 34 | } 35 | 36 | 37 | public void OnToolUpdate() { 38 | var c = GetColor(); 39 | label.textColor = Color.Lerp(c, Color.white, 0.70f); 40 | } 41 | 42 | public override void OnCheckChanged(UIComponent component, bool value) { 43 | base.OnCheckChanged(component, value); 44 | if (refreshing_) return; 45 | Apply(); 46 | } 47 | 48 | UIPanelBase root_; 49 | public override void Start() { 50 | base.Start(); 51 | width = parent.width; 52 | root_ = GetRootContainer() as UIPanelBase; 53 | } 54 | 55 | public LockDirCheckbox Mirror; 56 | public bool Left; // going away from the junction. 57 | 58 | public void Apply() { 59 | Assert(!refreshing_, "!refreshing_"); 60 | Log.Debug("LockDirCheckbox.Apply called()\n"/* + Environment.StackTrace*/); 61 | 62 | SegmentEndData data = root_?.GetData() as SegmentEndData; 63 | if (data == null) return; 64 | 65 | data.Corner(Left).LockLength = this.isChecked; 66 | if (ControlIsPressed && Mirror != null) 67 | Mirror.isChecked = isChecked; 68 | 69 | data.RefreshAndUpdate(); 70 | root_.Refresh(); 71 | } 72 | 73 | // protection against unnecessary apply/refresh/infinite recursion. 74 | bool refreshing_ = false; 75 | 76 | public void Refresh() { 77 | //Log.Debug("Refresh called()\n"/* + Environment.StackTrace*/); 78 | RefreshValues(); 79 | refreshing_ = true; 80 | //if (root_?.GetData() is NodeData nodeData) 81 | // RefreshNode(nodeData); 82 | 83 | parent.isVisible = isVisible = this.isEnabled; 84 | parent.Invalidate(); 85 | Invalidate(); 86 | refreshing_ = false; 87 | } 88 | 89 | public void RefreshValues() { 90 | refreshing_ = true; 91 | INetworkData data = root_?.GetData(); 92 | if (data is SegmentEndData segmentEndData) { 93 | this.isChecked = segmentEndData.Corner(Left).LockLength; 94 | isEnabled = segmentEndData.CanModifyCorners(); 95 | } else if (data is NodeData nodeData) { 96 | //this.isChecked = nodeData.FlatJunctions; // TODO complete 97 | //isEnabled = nodeData.CanMassEditNodeCorners(); 98 | } else Disable(); 99 | refreshing_ = false; 100 | } 101 | 102 | public void Reset() { 103 | isChecked = false; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /NodeController/Patches/NetManager/CreateSegmentPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches._NetManager 2 | { 3 | using NodeController; 4 | using NodeController.LifeCycle; 5 | using KianCommons; 6 | using static KianCommons.HelpersExtensions; 7 | using NodeController.Patches._NetTool; 8 | using System; 9 | using ColossalFramework.Math; 10 | using UnityEngine; 11 | using KianCommons.Patches; 12 | 13 | [HarmonyPatch2(typeof(NetManager), typeof(CreateSegment))] 14 | public static class CreateSegmentPatch 15 | { 16 | delegate bool CreateSegment(out ushort segment, ref Randomizer randomizer, NetInfo info, TreeInfo treeInfo, ushort startNode, ushort endNode, Vector3 startDirection, Vector3 endDirection, uint buildIndex, uint modifiedIndex, bool invert); 17 | 18 | // pastes segment ends that: 19 | // 1- not nullnot null and 20 | // 2- its nodeID matches input start/end nodeID. 21 | static void PasteSegment( 22 | MoveItSegmentData segmentData, ushort nodeID1, ushort nodeID2, ushort targetSegmentID) { 23 | if (segmentData == null) return; 24 | PasteSegmentEnd(segmentData.Start, nodeID1, nodeID2, targetSegmentID); 25 | PasteSegmentEnd(segmentData.End, nodeID1, nodeID2, targetSegmentID); 26 | } 27 | 28 | static void PasteSegmentEnd( 29 | SegmentEndData data, ushort nodeID1, ushort nodeID2, ushort targetSegmentID) { 30 | if (data != null) { 31 | ushort nodeID = data.NodeID; 32 | if (nodeID == nodeID1 || nodeID == nodeID2) { 33 | MoveItIntegration.PasteSegmentEnd(data, targetNodeID: nodeID, targetSegmentID: targetSegmentID); 34 | } 35 | } 36 | } 37 | 38 | 39 | public static void Postfix(ref ushort segment, ushort startNode, ushort endNode, bool __result) 40 | { 41 | if (!__result || !InSimulationThread()) return; 42 | Log.Debug($"CreateSegment.Postfix( {startNode}.-{segment}-.{endNode} )\n" + Environment.StackTrace, false); 43 | 44 | if (MoveMiddleNodePatch.CopyData) { 45 | var segmentData = MoveMiddleNodePatch.SegmentData; 46 | Log.Debug("Moving middle node: copying data to newly created segment. " + 47 | $"newSegmentID={segment} data={segmentData}\n", false); 48 | PasteSegment(segmentData, startNode, endNode, targetSegmentID: segment); 49 | } else if (SplitSegmentPatch.CopyData) { 50 | var segmentData = SplitSegmentPatch.SegmentData; 51 | var segmentData2 = SplitSegmentPatch.SegmentData2; 52 | var segmentData3 = SplitSegmentPatch.SegmentData3; 53 | Log.Debug("Spliting segment: copying data to newly created segment. " + 54 | $"newSegmentID={segment} data={segmentData} dat2={segmentData2} dat3={segmentData3}\n",false); 55 | 56 | PasteSegment(segmentData, startNode, endNode, targetSegmentID: segment); 57 | PasteSegment(segmentData2, startNode, endNode, targetSegmentID: segment); 58 | PasteSegment(segmentData3, startNode, endNode, targetSegmentID: segment); 59 | } else if(ReleaseSegmentImplementationPatch.UpgradingSegmentData != null){ 60 | if (!ReleaseSegmentImplementationPatch.m_upgrading) { 61 | Log.Error("Unexpected UpgradingSegmentData != null but m_upgrading == false "); 62 | } else { 63 | var segmentData = ReleaseSegmentImplementationPatch.UpgradingSegmentData; 64 | PasteSegment(segmentData, startNode, endNode, targetSegmentID: segment); 65 | } 66 | ReleaseSegmentImplementationPatch.UpgradingSegmentData = null; // consume 67 | } else { 68 | SegmentEndManager.Instance.SetAt(segment, true, null); 69 | SegmentEndManager.Instance.SetAt(segment, false, null); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /NodeController/Manager/LaneCache.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Manager { 2 | using KianCommons; 3 | using NodeController.Util; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using TrafficManager.API.Notifier; 7 | 8 | public class LaneCache { 9 | public static LaneCache Instance { get; private set; } 10 | private struct LaneData { 11 | public bool HideArrows; 12 | } 13 | private readonly LaneData[] buffer = new LaneData[NetManager.MAX_LANE_COUNT]; 14 | static INotifier Notifier => TrafficManager.API.Implementations.Notifier; 15 | 16 | public static void Create() => Instance = new LaneCache(); 17 | public static void Ensure() => Instance ??= new LaneCache(); 18 | public void Release() { 19 | Notifier.EventModified -= Notifier_EventModified; 20 | Instance = null; 21 | } 22 | public bool ShouldHideArrows(uint laneId) => buffer[laneId].HideArrows; 23 | 24 | static Timer timer = new("Notifier_EventModified()", 100); 25 | private void Notifier_EventModified(OnModifiedEventArgs obj) { 26 | SimulationManager.instance.m_ThreadingWrapper.QueueSimulationThread(delegate () { 27 | timer.Start(); 28 | Log.DebugCalled(obj.InstanceID); 29 | if (obj.InstanceID.Type == InstanceType.NetSegment) { 30 | UpdateLanes(obj.InstanceID.NetSegment); 31 | } else if (obj.InstanceID.Type == InstanceType.NetLane) { 32 | UpdateLanes(obj.InstanceID.NetLane.ToLane().m_segment); 33 | } else if (obj.InstanceID.Type == InstanceType.NetNode) { 34 | foreach (ushort segmentId in obj.InstanceID.NetNode.ToNode().IterateSegments()) { 35 | UpdateLanes(segmentId); 36 | } 37 | } 38 | timer.End(); 39 | }); 40 | 41 | } 42 | 43 | public void OnTMPELoaded() { 44 | SimulationManager.instance.m_ThreadingWrapper.QueueSimulationThread(delegate () { 45 | Log.Called(); 46 | for (ushort segmentId = 1; segmentId < NetManager.MAX_SEGMENT_COUNT; ++segmentId) { 47 | UpdateLanes(segmentId); 48 | } 49 | Notifier.EventModified -= Notifier_EventModified; 50 | Notifier.EventModified += Notifier_EventModified; 51 | Log.Succeeded(); 52 | }); 53 | } 54 | 55 | public void UpdateLanes(ushort segmentID) { 56 | if (!NetUtil.IsSegmentValid(segmentID)) { 57 | return; 58 | } 59 | UpdateLanes(segmentID, false); 60 | UpdateLanes(segmentID, true); 61 | } 62 | private void UpdateLanes(ushort segmentID, bool startNode) { 63 | var lanes = NetUtil.IterateLanes(segmentID, startNode: startNode).ToArray(); 64 | ushort nodeID = segmentID.ToSegment().GetNode(startNode); 65 | bool hide; 66 | 67 | if (nodeID.ToNode().flags.IsFlagSet(NetNode.FlagsLong.OneWayOut)) { 68 | NetLane.Flags flags = 0; 69 | foreach (var lane in lanes) { 70 | flags |= lane.Flags; 71 | } 72 | bool allForward = (flags & NetLane.Flags.LeftForwardRight) == NetLane.Flags.Forward; 73 | hide = allForward; 74 | } else { 75 | hide = false; 76 | } 77 | foreach (var lane in lanes) { 78 | buffer[lane.LaneID].HideArrows = hide; 79 | } 80 | } 81 | private static bool TwoSegments(ushort nodeId) { 82 | ref NetNode node = ref nodeId.ToNode(); 83 | int nSegments = 0; 84 | for (int segmentIndex = 0; segmentIndex < 8; ++segmentIndex) { 85 | ushort segmentId2 = node.GetSegment(segmentIndex); 86 | if (segmentId2 > 0) 87 | nSegments++; 88 | if (nSegments >= 3) 89 | return false; 90 | } 91 | return nSegments == 2; 92 | } 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /NodeController/.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*.cs] 4 | max_line_length=100 5 | insert_final_newline=true 6 | trim_trailing_whitespace=true 7 | indent_style=space 8 | 9 | # CS7035: The specified version string does not conform to the recommended format - major.minor.build.revision 10 | dotnet_diagnostic.CS7035.severity = none 11 | 12 | # HAA0201: Implicit string concatenation allocation 13 | dotnet_diagnostic.HAA0201.severity = none 14 | 15 | [*] 16 | charset=utf-8 17 | end_of_line=crlf 18 | indent_style=space 19 | indent_size=4 20 | tab_width=8 21 | 22 | # Microsoft .NET properties 23 | csharp_new_line_before_else=false 24 | csharp_new_line_before_members_in_object_initializers=false 25 | csharp_new_line_before_open_brace=none 26 | csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion 27 | csharp_space_after_cast=false 28 | csharp_style_var_elsewhere=false:hint 29 | csharp_style_var_for_built_in_types=false:hint 30 | csharp_style_var_when_type_is_apparent=true:hint 31 | dotnet_style_predefined_type_for_locals_parameters_members=true:hint 32 | dotnet_style_predefined_type_for_member_access=true:hint 33 | dotnet_style_qualification_for_event=false:none 34 | dotnet_style_qualification_for_field=false:none 35 | dotnet_style_qualification_for_method=false:none 36 | dotnet_style_qualification_for_property=false:none 37 | dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint 38 | 39 | # ReSharper properties 40 | resharper_add_imports_to_deepest_scope=true 41 | resharper_align_linq_query=true 42 | resharper_align_multiline_argument=true 43 | resharper_align_multiline_calls_chain=true 44 | resharper_align_multiline_expression=true 45 | resharper_align_multiline_extends_list=true 46 | resharper_align_multiline_for_stmt=true 47 | resharper_align_multiline_parameter=true 48 | resharper_align_multiple_declaration=true 49 | resharper_align_multline_type_parameter_constrains=true 50 | resharper_align_multline_type_parameter_list=true 51 | resharper_autodetect_indent_settings=true 52 | resharper_braces_for_for=required 53 | resharper_braces_for_foreach=required 54 | resharper_braces_for_ifelse=required 55 | resharper_braces_for_while=required 56 | resharper_csharp_max_line_length=100 57 | resharper_csharp_tab_width=8 58 | resharper_csharp_wrap_arguments_style=chop_if_long 59 | resharper_csharp_wrap_parameters_style=chop_if_long 60 | resharper_empty_block_style=together 61 | resharper_for_built_in_types=use_var_when_evident 62 | resharper_indent_invocation_pars=outside_and_inside 63 | resharper_indent_method_decl_pars=outside_and_inside 64 | resharper_indent_statement_pars=outside_and_inside 65 | resharper_keep_existing_invocation_parens_arrangement=false 66 | resharper_max_attribute_length_for_same_line=1 67 | resharper_place_attribute_on_same_line=False 68 | resharper_use_indent_from_vs=false 69 | resharper_wrap_after_invocation_lpar=true 70 | resharper_wrap_before_extends_colon=true 71 | resharper_wrap_before_first_type_parameter_constraint=true 72 | 73 | # ReSharper inspection severities 74 | resharper_sa1012_highlighting=none 75 | resharper_sa1101_highlighting=none 76 | resharper_sa1116_highlighting=hint 77 | resharper_sa1118_highlighting=hint 78 | resharper_sa1305_highlighting=none 79 | resharper_sa1309_highlighting=hint 80 | resharper_sa1310_highlighting=none 81 | resharper_sa1500_highlighting=none 82 | resharper_sx1309_highlighting=none 83 | resharper_web_config_module_not_resolved_highlighting=warning 84 | resharper_web_config_type_not_resolved_highlighting=warning 85 | resharper_web_config_wrong_module_highlighting=warning 86 | trim_trailing_whitespace=true 87 | insert_final_newline=false 88 | 89 | [*.config] 90 | indent_style=space 91 | indent_size=4 92 | 93 | [{*.csproj,*.ccproj,*.androidproj}] 94 | indent_style=space 95 | indent_size=2 96 | 97 | [*.ruleset] 98 | indent_style=space 99 | indent_size=4 100 | 101 | [.editorconfig] 102 | indent_style=space 103 | indent_size=4 104 | 105 | [*.{appxmanifest,asax,ascx,aspx,build,config,cshtml,dbml,discomap,dtd,htm,html,jsproj,lsproj,master,njsproj,nuspec,proj,props,razor,resw,resx,skin,StyleCop,targets,tasks,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] 106 | indent_style=space 107 | indent_size=4 108 | tab_width=8 109 | 110 | # IDE0003: Remove qualification 111 | dotnet_diagnostic.IDE0003.severity = none 112 | -------------------------------------------------------------------------------- /NodeController/Util/BuildingUtil.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Util { 2 | using ColossalFramework; 3 | using KianCommons; 4 | using System; 5 | using System.Threading; 6 | using UnityEngine; 7 | 8 | public static class BuildingUtil { 9 | internal static ref Building ToBuilding(this ushort buildingId) => 10 | ref BuildingManager.instance.m_buildings.m_buffer[buildingId]; 11 | 12 | internal static bool IsValid(this ref Building building, ushort buildingId)=> 13 | buildingId != 0 && (building.m_flags & (Building.Flags.Created | Building.Flags.Deleted)) == Building.Flags.Created; 14 | 15 | public static void RelocatePillar(ushort buildingId, Vector3 position, float angle) { 16 | if (buildingId == 0) return; 17 | if (!Helpers.InSimulationThread()) { 18 | SimulationManager.instance.AddAction(() => RelocatePillar(buildingId, position, angle)); 19 | return; 20 | } 21 | 22 | ref Building building = ref buildingId.ToBuilding(); 23 | var delta = building.m_position - position; 24 | if (delta.sqrMagnitude < (0.001 * 0.001f)) return; // same place. 25 | 26 | RemoveFromGrid(buildingId, ref building); 27 | building.m_position = position; 28 | building.m_angle = (angle + Mathf.PI * 2) % (Mathf.PI * 2); 29 | AddToGrid(buildingId, ref building); 30 | 31 | if (building.Info != null) { 32 | building.CalculateBuilding(buildingId); 33 | BuildingManager.instance.UpdateBuildingRenderer(buildingId, false); 34 | } else { 35 | BuildingManager.instance.UpdateBuilding(buildingId); 36 | } 37 | } 38 | 39 | private static void AddToGrid(ushort buildingID, ref Building data) { 40 | int num = Mathf.Clamp((int)(data.m_position.x / 64f + 135f), 0, 269); 41 | int num2 = Mathf.Clamp((int)(data.m_position.z / 64f + 135f), 0, 269); 42 | int num3 = num2 * 270 + num; 43 | while (!Monitor.TryEnter(BuildingManager.instance.m_buildingGrid, SimulationManager.SYNCHRONIZE_TIMEOUT)) { 44 | } 45 | try { 46 | buildingID.ToBuilding().m_nextGridBuilding = BuildingManager.instance.m_buildingGrid[num3]; 47 | BuildingManager.instance.m_buildingGrid[num3] = buildingID; 48 | } finally { 49 | Monitor.Exit(BuildingManager.instance.m_buildingGrid); 50 | } 51 | } 52 | 53 | private static void RemoveFromGrid(ushort buildingID, ref Building data) { 54 | BuildingManager buildingManager = BuildingManager.instance; 55 | 56 | BuildingInfo info = data.Info; 57 | int num = Mathf.Clamp((int)(data.m_position.x / 64f + 135f), 0, 269); 58 | int num2 = Mathf.Clamp((int)(data.m_position.z / 64f + 135f), 0, 269); 59 | int num3 = num2 * 270 + num; 60 | while (!Monitor.TryEnter(buildingManager.m_buildingGrid, SimulationManager.SYNCHRONIZE_TIMEOUT)) { 61 | } 62 | try { 63 | ushort num4 = 0; 64 | ushort num5 = buildingManager.m_buildingGrid[num3]; 65 | int num6 = 0; 66 | while (num5 != 0) { 67 | if (num5 == buildingID) { 68 | if (num4 == 0) { 69 | buildingManager.m_buildingGrid[num3] = data.m_nextGridBuilding; 70 | } else { 71 | num4.ToBuilding().m_nextGridBuilding = data.m_nextGridBuilding; 72 | } 73 | break; 74 | } 75 | num4 = num5; 76 | num5 = num5.ToBuilding().m_nextGridBuilding; 77 | if (++num6 > 49152) { 78 | CODebugBase.Error(LogChannel.Core, "Invalid list detected!\n" + Environment.StackTrace); 79 | break; 80 | } 81 | } 82 | data.m_nextGridBuilding = 0; 83 | } finally { 84 | Monitor.Exit(buildingManager.m_buildingGrid); 85 | } 86 | if (info != null) { 87 | Singleton.instance.UpdateGroup(num * 45 / 270, num2 * 45 / 270, info.m_prefabDataLayer); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /NodeController/Patches/Corner/StretchPatches.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.Corner; 2 | using HarmonyLib; 3 | using KianCommons; 4 | using KianCommons.Patches; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Reflection.Emit; 9 | using UnityEngine; 10 | 11 | // vehicle/pedestrian paths take into consideration the stretched lane/segment. 12 | [HarmonyPatch] 13 | static class WidthPatch1 { 14 | static IEnumerable TargetMethods() { 15 | // stretch the area ppl wait at bus stop: 16 | // private Vector4 HumanAI.GetTransportWaitPosition(ushort instanceID, ref CitizenInstance citizenData, ref CitizenInstance.Frame frameData, float minSqrDistance) 17 | yield return typeof(HumanAI).GetMethod("GetTransportWaitPosition", throwOnError: true); 18 | 19 | // stretch the walkable pedestrian area: 20 | // protected Vector4 CitizenAI.GetPathTargetPosition(ushort instanceID, ref CitizenInstance citizenData, ref CitizenInstance.Frame frameData, float minSqrDistance) 21 | yield return typeof(CitizenAI).GetMethod("GetPathTargetPosition", throwOnError: true); 22 | 23 | // push cars to the side: 24 | // private static bool PassengerCarAI.FindParkingSpaceRoadSide(ushort ignoreParked, ushort requireSegment, Vector3 refPos, float width, float length, out Vector3 parkPos, out Quaternion parkRot, out float parkOffset) 25 | yield return typeof(PassengerCarAI).GetMethod("FindParkingSpaceRoadSide", throwOnError: true); 26 | } 27 | 28 | static IEnumerable Transpiler(IEnumerable instructions, MethodBase original) { 29 | FieldInfo fLaneWidth = ReflectionHelpers.GetField(nameof(NetInfo.Lane.m_width)); 30 | FieldInfo fNetHW = ReflectionHelpers.GetField(nameof(NetInfo.m_halfWidth)); 31 | FieldInfo fNetPW = ReflectionHelpers.GetField(nameof(NetInfo.m_pavementWidth)); 32 | MethodInfo mStretch = typeof(WidthPatch1).GetMethod(nameof(Stretch), throwOnError: true); 33 | var codes = instructions.ToList(); 34 | 35 | bool LoadsPathPos(CodeInstruction c) => c.IsLdLoc(typeof(PathUnit.Position), original); 36 | 37 | for (int i = 0; i < codes.Count; i++) { 38 | var code = codes[i]; 39 | if (code.LoadsField(fLaneWidth) || code.LoadsField(fNetHW) || code.LoadsField(fNetPW)) { 40 | int iLoadPathPos = codes.Search(predicate: LoadsPathPos, startIndex: i, count: -1); 41 | i += codes.InsertInstructions( 42 | i + 1, // insert after 43 | new[] { 44 | // m_width or m_halfWidth or m_pavementWidth already on stack 45 | codes[iLoadPathPos].Clone(), 46 | new CodeInstruction(OpCodes.Call, mStretch), 47 | }); 48 | } 49 | } 50 | 51 | return codes; 52 | } 53 | 54 | static float Stretch(float w0, ref PathUnit.Position pathPosition) { 55 | ushort segmentId = pathPosition.m_segment; 56 | ref SegmentEndData segStart = ref SegmentEndManager.Instance.GetAt(segmentId, true); 57 | ref SegmentEndData segEnd = ref SegmentEndManager.Instance.GetAt(segmentId, false); 58 | float stretchStart = segStart?.Stretch ?? 0; 59 | float stretchEnd = segEnd?.Stretch ?? 0; 60 | float stretch = Mathf.Min(stretchStart, stretchEnd); 61 | float ratio = stretch * 0.01f + 1; 62 | return w0 * ratio; 63 | } 64 | } 65 | 66 | 67 | [HarmonyPatch(typeof(NetLane), nameof(NetLane.CalculateStopPositionAndDirection))] 68 | [HarmonyPriority(Priority.High)] 69 | [HarmonyBefore("me.tmpe")] 70 | static class CalculateStopPositionAndDirectionPatch { 71 | static void Prefix(ref NetLane __instance, float laneOffset, ref float stopOffset) { 72 | if (stopOffset != 0) { 73 | ushort segmentId = __instance.m_segment; 74 | ref SegmentEndData segStart = ref SegmentEndManager.Instance.GetAt(segmentId, true); 75 | ref SegmentEndData segEnd = ref SegmentEndManager.Instance.GetAt(segmentId, false); 76 | 77 | float stretchStart = segStart?.Stretch ?? 0; 78 | float stretchEnd = segEnd?.Stretch ?? 0; 79 | float stretch = Mathf.Lerp(stretchStart, stretchEnd, laneOffset) * 0.01f; 80 | float ratio = stretch + 1; 81 | stopOffset *= (ratio + 0.5f * stretch); //0.5f * stretch because bus stop space also stretches 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /NodeController/Patches/VehicleSuperElevation/SimulationStepPatches.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.VehicleSuperElevation { 2 | using ColossalFramework; 3 | using HarmonyLib; 4 | using KianCommons; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Reflection; 8 | using System.Reflection.Emit; 9 | using UnityEngine; 10 | using static SuperElevationCommons; 11 | 12 | //public override void SimulationStep(ushort vehicleID, ref Vehicle vehicleData, ref Vehicle.Frame frameData, 13 | // ushort leaderID, ref Vehicle leaderData, int lodPhysics) 14 | [HarmonyPatch] 15 | static class CarAI_SimulationStepPatch { 16 | internal static MethodBase TargetMethod() => TargetMethod(); 17 | 18 | internal static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions) => 19 | OnRotationUpdatedTranspiler(instructions, TargetMethod() as MethodInfo); 20 | 21 | internal static void Postfix(ref Vehicle vehicleData, ref Vehicle.Frame frameData) { 22 | if (!RotationUpdated) return; 23 | RotationUpdated = false; 24 | SuperElevationCommons.Postfix(ref vehicleData, ref frameData); 25 | } 26 | } 27 | 28 | [HarmonyPatch] 29 | static class CarTrailerAI_SimulationStepPatch { 30 | internal static MethodBase TargetMethod() => TargetMethod(); 31 | 32 | internal static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions) => 33 | OnRotationUpdatedTranspiler(instructions, TargetMethod() as MethodInfo); 34 | 35 | static Vehicle[] VehicleBuffer = VehicleManager.instance.m_vehicles.m_buffer; 36 | internal static void Postfix(ref Vehicle vehicleData, ref Vehicle.Frame frameData) { 37 | if (vehicleData.Info.m_leanMultiplier < 0) 38 | return; // motor cycle. 39 | 40 | if (!RotationUpdated) { 41 | //Log.Debug("CarTrailerAI_SimulationStepPatch rotation was not updated! leadID=" + vehicleData.m_leadingVehicle); 42 | return; 43 | } 44 | RotationUpdated = false; 45 | 46 | ref Vehicle leadingVehicle = ref VehicleBuffer[vehicleData.m_leadingVehicle]; 47 | //Vehicle.Frame lastFrameData = leadingVehicle.GetLastFrameData(); 48 | VehicleInfo leadningInfo = leadingVehicle.Info; 49 | if (!leadingVehicle.GetCurrentPathPos(out var pathPos)) return; 50 | uint laneID = PathManager.GetLaneID(pathPos); 51 | 52 | // Calculate trailer lane offset based on how far the trailer is from the car its attached to. 53 | bool inverted = leadingVehicle.m_flags.IsFlagSet(Vehicle.Flags.Inverted); 54 | float deltaPos = inverted ? leadningInfo.m_attachOffsetBack : leadningInfo.m_attachOffsetFront; 55 | float deltaOffset = deltaPos / laneID.ToLane().m_length; 56 | float offset = leadingVehicle.m_lastPathOffset * (1f / 255f) - deltaOffset; 57 | offset = Mathf.Clamp(offset, 0, 1); 58 | if (float.IsNaN(offset)) return; 59 | 60 | float se = GetCurrentSE(pathPos, offset, ref vehicleData); 61 | var rot = Quaternion.Euler(0, 0f, se); 62 | frameData.m_rotation *= rot; 63 | } 64 | } 65 | 66 | [HarmonyPatch] 67 | static class TrainAI_SimulationStepPatch { 68 | internal static IEnumerable TargetMethods() { 69 | yield return TargetMethod(); 70 | var tmpeTarget = TargetTMPEMethod(); 71 | if(tmpeTarget != null) 72 | yield return tmpeTarget; //old TMPE 73 | } 74 | 75 | internal static void Postfix(ref Vehicle vehicleData, ref Vehicle.Frame frameData) => 76 | SuperElevationCommons.Postfix(ref vehicleData, ref frameData); 77 | } 78 | 79 | [HarmonyPatch] 80 | static class TramBaseAI_SimulationStepPatch { 81 | internal static IEnumerable TargetMethods() { 82 | yield return TargetMethod(); 83 | var tmpeTarget = TargetTMPEMethod(); 84 | if(tmpeTarget != null) 85 | yield return tmpeTarget; //old TMPE 86 | } 87 | 88 | internal static void Postfix(ref Vehicle vehicleData, ref Vehicle.Frame frameData) => 89 | SuperElevationCommons.Postfix(ref vehicleData, ref frameData); 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/FlatJunctionsCheckbox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework.UI; 3 | using System; 4 | using UnityEngine; 5 | using KianCommons; 6 | using static KianCommons.Assertion; 7 | using KianCommons.UI; 8 | 9 | public class UIUnFlattenJunctionsCheckbox : UICheckBox, IDataControllerUI { 10 | public string HintHotkeys => "del => reset hovered value to default"; 11 | public string HintDescription => 12 | "uncheck to give slope to junction/transition. Useful for highway intersections. " + 13 | "the two bigger segments should turn off flat junction. " + 14 | "minor roads joining a sloped intersection twist sideways to match the slope of the intersection."; 15 | 16 | public override void Awake() { 17 | base.Awake(); 18 | name = nameof(UIUnFlattenJunctionsCheckbox); 19 | height = 20f; 20 | clipChildren = true; 21 | 22 | UISprite sprite = AddUIComponent(); 23 | sprite.spriteName = "ToggleBase"; 24 | sprite.size = new Vector2(20f, 20f); 25 | sprite.relativePosition = new Vector2(0, (height-sprite.height)/2 ); 26 | sprite.atlas = TextureUtil.Ingame; 27 | 28 | checkedBoxObject = sprite.AddUIComponent(); 29 | ((UISprite)checkedBoxObject).spriteName = "ToggleBaseFocused"; 30 | ((UISprite)checkedBoxObject).atlas = TextureUtil.Ingame; 31 | checkedBoxObject.size = sprite.size; 32 | checkedBoxObject.relativePosition = Vector3.zero; 33 | 34 | label = AddUIComponent(); 35 | label.text = "flatten segment end"; 36 | label.textScale = 0.9f; 37 | label.relativePosition = new Vector2(sprite.width+5f, (height- label.height)/2+1); 38 | 39 | eventCheckChanged += OnCheckChanged; 40 | } 41 | 42 | void OnCheckChanged(UIComponent component, bool value) { 43 | if (refreshing_) 44 | return; 45 | Apply(); 46 | } 47 | 48 | public override void OnDestroy() { 49 | eventCheckChanged -= OnCheckChanged; 50 | base.OnDestroy(); 51 | } 52 | 53 | UIPanelBase root_; 54 | public override void Start() { 55 | base.Start(); 56 | width = parent.width; 57 | root_ = GetRootContainer() as UIPanelBase; 58 | } 59 | 60 | 61 | public void Apply() { 62 | Log.Debug("UIUnFlattenJunctionsCheckbox.Apply called()\n"/* + Environment.StackTrace*/); 63 | SegmentEndData data = root_?.GetData() as SegmentEndData; 64 | if (data == null) 65 | return; 66 | 67 | data.FlatJunctions = this.isChecked; 68 | if (!this.isChecked) 69 | data.Twist = false; 70 | else 71 | data.Twist = data.DefaultTwist; 72 | data.DeltaSlopeAngleDeg = 0; 73 | Assert(!refreshing_, "!refreshing_"); 74 | data.RefreshAndUpdate(); 75 | root_.Refresh(); 76 | } 77 | 78 | // protection against unnecessary apply/refresh/infinite recursion. 79 | bool refreshing_ = false; 80 | 81 | public void Refresh() { 82 | //Log.Debug("Refresh called()\n"/* + Environment.StackTrace*/); 83 | RefreshValues(); 84 | refreshing_ = true; 85 | 86 | //if (root_?.GetData() is NodeData nodeData) 87 | // RefreshNode(nodeData); 88 | 89 | parent.isVisible = isVisible = this.isEnabled; 90 | parent.Invalidate(); 91 | Invalidate(); 92 | refreshing_ = false; 93 | } 94 | 95 | public void RefreshValues() { 96 | refreshing_ = true; 97 | INetworkData data = root_?.GetData(); 98 | if (data is SegmentEndData segmentEndData) { 99 | isEnabled = segmentEndData.CanModifyFlatJunctions(); 100 | if (isEnabled) { 101 | this.isChecked = segmentEndData.FlatJunctions; 102 | } 103 | } else Disable(); 104 | refreshing_ = false; 105 | } 106 | 107 | public void Reset() { 108 | if (root_?.GetData() is SegmentEndData segmentEndData) { 109 | isChecked = segmentEndData.DefaultFlatJunctions; 110 | } 111 | } 112 | } 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/SharpCornersCheckbox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework.UI; 3 | using System; 4 | using UnityEngine; 5 | using KianCommons; 6 | using static KianCommons.Assertion; 7 | using KianCommons.UI; 8 | using System.Linq; 9 | 10 | public class SharpCornersCheckbox : UICheckBox, IDataControllerUI { 11 | public string HintHotkeys => "del => reset hovered value to default"; 12 | public string HintDescription => 13 | "make corners sharp for straight segments (overrides corner offset)"; 14 | 15 | public override void Awake() { 16 | base.Awake(); 17 | name = nameof(SharpCornersCheckbox); 18 | height = 20f; 19 | clipChildren = true; 20 | 21 | UISprite sprite = AddUIComponent(); 22 | sprite.spriteName = "ToggleBase"; 23 | sprite.size = new Vector2(20f, 20f); 24 | sprite.relativePosition = new Vector2(0, (height-sprite.height)/2 ); 25 | sprite.atlas = TextureUtil.Ingame; 26 | 27 | checkedBoxObject = sprite.AddUIComponent(); 28 | ((UISprite)checkedBoxObject).spriteName = "ToggleBaseFocused"; 29 | ((UISprite)checkedBoxObject).atlas = TextureUtil.Ingame; 30 | checkedBoxObject.size = sprite.size; 31 | checkedBoxObject.relativePosition = Vector3.zero; 32 | 33 | label = AddUIComponent(); 34 | label.text = "sharpen corners"; 35 | label.textScale = 0.9f; 36 | label.relativePosition = new Vector2(sprite.width+5f, (height- label.height)/2+1); 37 | 38 | eventCheckChanged += OnCheckChanged; 39 | } 40 | 41 | void OnCheckChanged(UIComponent component, bool value) { 42 | if (refreshing_) 43 | return; 44 | Apply(); 45 | } 46 | 47 | public override void OnDestroy() { 48 | eventCheckChanged -= OnCheckChanged; 49 | base.OnDestroy(); 50 | } 51 | 52 | UIPanelBase root_; 53 | public override void Start() { 54 | base.Start(); 55 | width = parent.width; 56 | root_ = GetRootContainer() as UIPanelBase; 57 | } 58 | 59 | 60 | public void Apply() { 61 | Log.Called(); 62 | Assert(!refreshing_, "!refreshing_"); 63 | if (root_?.GetData() is NodeData nodeData) { 64 | nodeData.SharpCorners = this.isChecked; 65 | nodeData.RefreshAndUpdate(); 66 | root_.Refresh(); 67 | } else if(root_?.GetData() is SegmentEndData segEnd){ 68 | segEnd.SharpCorners = this.isChecked; 69 | segEnd.RefreshAndUpdate(); 70 | } 71 | root_.Refresh(); 72 | } 73 | 74 | // protection against unnecessary apply/refresh/infinite recursion. 75 | bool refreshing_ = false; 76 | 77 | public void Refresh() { 78 | RefreshValues(); 79 | refreshing_ = true; 80 | parent.isVisible = isVisible = this.isEnabled; 81 | parent.Invalidate(); 82 | Invalidate(); 83 | refreshing_ = false; 84 | } 85 | 86 | public void RefreshValues() { 87 | refreshing_ = true; 88 | INetworkData data = root_?.GetData(); 89 | if (data is NodeData nodeData) { 90 | isEnabled = nodeData.CanModifySharpCorners(); 91 | if (isEnabled) { 92 | this.isChecked = nodeData.SharpCorners; 93 | } 94 | checkedBoxObject.color = nodeData.HasUnifromSharp() ? Color.white : Color.grey; 95 | Log.Debug("checkedBoxObject.color ="+ checkedBoxObject.color); 96 | } else if (data is SegmentEndData segEnd) { 97 | isEnabled = segEnd.NodeData.CanModifySharpCorners(); 98 | if (isEnabled) { 99 | this.isChecked = segEnd.SharpCorners; 100 | } 101 | } 102 | refreshing_ = false; 103 | } 104 | 105 | public void Reset() { 106 | if (root_?.GetData() is NodeData nodeData) { 107 | isChecked = nodeData.IterateSegmentEndDatas().FirstOrDefault()?.DefaultSharpCorners ?? false; 108 | } else if (root_?.GetData() is SegmentEndData segEnd) { 109 | isChecked = segEnd.DefaultSharpCorners; 110 | } 111 | } 112 | } 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/NodelessCheckbox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework.UI; 3 | using KianCommons; 4 | using KianCommons.UI; 5 | using System; 6 | using UnityEngine; 7 | using static KianCommons.HelpersExtensions; 8 | using static KianCommons.ReflectionHelpers; 9 | 10 | public class NodelessCheckbox : UICheckBox, IDataControllerUI { 11 | public static NodelessCheckbox Instance { get; private set; } 12 | 13 | public string HintHotkeys => "del => reset hovered value to default"; 14 | 15 | public string HintDescription => "make this segment end node-less."; 16 | 17 | UIPanelBase root_; 18 | public override void Awake() { 19 | base.Awake(); 20 | Instance = this; 21 | name = nameof(NodelessCheckbox); 22 | height = 30f; 23 | clipChildren = true; 24 | 25 | UISprite sprite = AddUIComponent(); 26 | sprite.spriteName = "ToggleBase"; 27 | sprite.size = new Vector2(19f, 19f); 28 | sprite.relativePosition = new Vector2(0, (height - sprite.height) / 2); 29 | sprite.atlas = KianCommons.UI.TextureUtil.GetAtlas("InGame"); 30 | sprite.atlas = TextureUtil.Ingame; 31 | 32 | checkedBoxObject = sprite.AddUIComponent(); 33 | ((UISprite)checkedBoxObject).spriteName = "ToggleBaseFocused"; 34 | ((UISprite)checkedBoxObject).atlas = TextureUtil.Ingame; 35 | checkedBoxObject.size = sprite.size; 36 | checkedBoxObject.relativePosition = Vector3.zero; 37 | 38 | label = AddUIComponent(); 39 | label.text = "node-less"; 40 | label.textScale = 0.9f; 41 | label.relativePosition = new Vector2(sprite.width + 5f, (height - label.height) / 2 + 1); 42 | 43 | eventCheckChanged += OnCheckChanged; 44 | } 45 | 46 | void OnCheckChanged(UIComponent component, bool value) { 47 | if(refreshing_) 48 | return; 49 | Apply(); 50 | } 51 | 52 | public override void OnDestroy() { 53 | eventCheckChanged -= OnCheckChanged; 54 | base.OnDestroy(); 55 | } 56 | 57 | public override void Start() { 58 | base.Start(); 59 | width = parent.width; 60 | root_ = GetRootContainer() as UIPanelBase; 61 | } 62 | 63 | // protection against unnecessary apply/refresh/infinite recursion. 64 | bool refreshing_ = false; 65 | 66 | public void Apply() { 67 | if(VERBOSE) Log.Debug(ThisMethod + " called\n" + Environment.StackTrace); 68 | if(root_ is UINodeControllerPanel) 69 | throw new NotImplementedException(); 70 | else if(root_ is UISegmentEndControllerPanel segPanel) { 71 | ApplySegmentEnd(); 72 | Assertion.Assert(!refreshing_, "!refreshing_"); 73 | SegmentEndData data = segPanel.SegmentEndData; 74 | data?.RefreshAndUpdate(); 75 | segPanel.Refresh(); 76 | } else { 77 | throw new Exception("Unreachable code. root_=" + root_); 78 | } 79 | } 80 | 81 | public void ApplySegmentEnd() { 82 | SegmentEndData data = (root_ as UISegmentEndControllerPanel).SegmentEndData; 83 | if(data == null) 84 | return; 85 | data.Nodeless = this.isChecked; 86 | Log.Debug($"{ThisMethod}: {data}" + 87 | $"isChecked={isChecked} " + 88 | $"data.Nodeless is set to {data.Nodeless}"); 89 | data.Update(); 90 | } 91 | 92 | public void Refresh() { 93 | if(VERBOSE) Log.Debug("Refresh called()\n" + Environment.StackTrace); 94 | RefreshValues(); 95 | parent.isVisible = isVisible = this.isEnabled; 96 | Invalidate(); 97 | } 98 | 99 | public void RefreshValues() { 100 | refreshing_ = true; 101 | INetworkData data = root_?.GetData(); 102 | if(data is SegmentEndData segmentEndData) { 103 | isChecked = segmentEndData.Nodeless; 104 | isEnabled = segmentEndData.CanModifyNodeless(); 105 | } else if(data is NodeData nodeData) { 106 | throw new NotImplementedException(); 107 | } else Disable(); 108 | refreshing_ = false; 109 | } 110 | 111 | public void Reset() { 112 | isChecked = false; 113 | } 114 | } 115 | } 116 | 117 | 118 | -------------------------------------------------------------------------------- /NodeController/Patches/NetLanePatches/Commons.cs: -------------------------------------------------------------------------------- 1 | //using ICities; 2 | //using HarmonyLib; 3 | //using System.Collections.Generic; 4 | //using System.Reflection.Emit; 5 | //using System.Reflection; 6 | //using System.Linq; 7 | 8 | //using ColossalFramework; 9 | ///* Notes 10 | 11 | // */ 12 | 13 | //namespace NodeController.Patches { 14 | // using System; 15 | // using UnityEngine; 16 | // using static KianCommons.TranspilerUtils; 17 | // using KianCommons; 18 | // public static class Commons { 19 | // public static bool ShouldRenderProp(NetLaneProps.Prop prop, ushort segmentID, uint laneID, NetNode.Flags startFlags, NetNode.Flags endFlags) { 20 | // return true; 21 | // } 22 | 23 | // static Type[] args = new [] { 24 | // typeof(Mesh), 25 | // typeof(Vector3), 26 | // typeof(Quaternion), 27 | // typeof(Material), 28 | // typeof(int), 29 | // typeof(Camera), 30 | // typeof(int), 31 | // typeof(MaterialPropertyBlock) }; 32 | // static MethodInfo mDrawMesh => typeof(Graphics).GetMethod("DrawMesh", args); 33 | // static FieldInfo fNodeMaterial => typeof(NetInfo.Node).GetField("m_nodeMaterial"); 34 | // static MethodInfo mCheckRenderDistance => typeof(RenderManager.CameraInfo).GetMethod("CheckRenderDistance"); 35 | // static MethodInfo mShouldRenderProp => typeof(CalculateMaterialCommons).GetMethod("ShouldRenderProp"); 36 | // static MethodInfo mGetSegment => typeof(NetNode).GetMethod("GetSegment"); 37 | 38 | // // returns the position of First DrawMesh after index. 39 | // public static void PatchCheckFlags(List codes, int occurance, MethodInfo method) { 40 | // Assertion.Assert(mDrawMesh != null, "mDrawMesh!=null failed"); 41 | // Assertion.Assert(fNodeMaterial != null, "fNodeMaterial!=null failed"); 42 | // Assertion.Assert(mCheckRenderDistance != null, "mCheckRenderDistance!=null failed"); 43 | 44 | // int index = 0; 45 | // index = SearchInstruction(codes, new CodeInstruction(OpCodes.Call, mDrawMesh), index, counter: occurance); 46 | // Assertion.Assert(index != 0, "index!=0"); 47 | 48 | 49 | // // find ldfld node.m_material 50 | // index = SearchInstruction(codes, new CodeInstruction(OpCodes.Ldfld, fNodeMaterial), index, dir: -1); 51 | // int insertIndex2 = index + 1; 52 | 53 | // // find: if (cameraInfo.CheckRenderDistance(data.m_position, node.m_lodRenderDistance)) 54 | // /* IL_0627: callvirt instance bool RenderManager CameraInfo::CheckRenderDistance(Vector3, float32) 55 | // * IL_062c brfalse IL_07e2 */ 56 | // index = SearchInstruction(codes, new CodeInstruction(OpCodes.Callvirt, mCheckRenderDistance), index, dir: -1); 57 | // int insertIndex1 = index + 1; // at this point boloean is in stack 58 | 59 | 60 | // CodeInstruction LDArg_NodeID = GetLDArg(method, "nodeID"); // push nodeID into stack 61 | // CodeInstruction LDLoc_segmentID = BuildSegnentLDLocFromPrevSTLoc(codes, index, counter: 1); // push segmentID into stack 62 | 63 | // { // Insert material = CalculateMaterial(material, nodeID, segmentID) 64 | // var newInstructions = new[] { 65 | // LDArg_NodeID, 66 | // LDLoc_segmentID, 67 | // new CodeInstruction(OpCodes.Call, mCalculateMaterial), // call Material CalculateMaterial(material, nodeID, segmentID). 68 | // }; 69 | // InsertInstructions(codes, newInstructions, insertIndex2); 70 | // } 71 | 72 | // { // Insert ShouldHideCrossing(nodeID, segmentID) 73 | // var newInstructions = new[]{ 74 | // LDArg_NodeID, 75 | // LDLoc_segmentID, 76 | // new CodeInstruction(OpCodes.Call, mShouldContinueMedian), // call Material mShouldHideCrossing(nodeID, segmentID). 77 | // new CodeInstruction(OpCodes.Or) }; 78 | 79 | // InsertInstructions(codes, newInstructions, insertIndex1); 80 | // } // end block 81 | // } // end method 82 | 83 | // public static CodeInstruction BuildSegnentLDLocFromPrevSTLoc(List codes, int index, int counter=1) { 84 | // Assertion.Assert(mGetSegment != null, "mGetSegment!=null"); 85 | // index = SearchInstruction(codes, new CodeInstruction(OpCodes.Call, mGetSegment), index, counter: counter, dir: -1); 86 | 87 | // var code = codes[index + 1]; 88 | // Assertion.Assert(IsStLoc(code), $"IsStLoc(code) | code={code}"); 89 | 90 | // return BuildLdLocFromStLoc(code); 91 | // } 92 | 93 | 94 | 95 | // } 96 | //} 97 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/Corner/TwistCheckbox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework.UI; 3 | using System; 4 | using UnityEngine; 5 | using KianCommons.UI; 6 | using static KianCommons.Assertion; 7 | 8 | public class TwistCheckbox : UICheckBox, IDataControllerUI { 9 | public static TwistCheckbox Instance { get; private set; } 10 | public UISprite UncheckedSprite, CheckedSprite; 11 | 12 | public string HintHotkeys => "del => reset hovered value to default"; 13 | public string HintDescription => 14 | "twists the segment end sideways where it meets a sloped intersection so that it matches the slope of the intersection."; 15 | 16 | public override void Awake() { 17 | base.Awake(); 18 | Instance = this; 19 | name = nameof(TwistCheckbox); 20 | height = 20f; 21 | clipChildren = true; 22 | 23 | UISprite sprite = UncheckedSprite = AddUIComponent(); 24 | sprite.spriteName = "ToggleBase"; 25 | sprite.size = new Vector2(20f, 20f); 26 | sprite.relativePosition = new Vector2(0, (height - sprite.height) / 2); 27 | sprite.atlas = TextureUtil.Ingame; 28 | 29 | checkedBoxObject = CheckedSprite = sprite.AddUIComponent(); 30 | ((UISprite)checkedBoxObject).spriteName = "ToggleBaseFocused"; 31 | ((UISprite)checkedBoxObject).atlas = TextureUtil.Ingame; 32 | checkedBoxObject.size = sprite.size; 33 | checkedBoxObject.relativePosition = Vector3.zero; 34 | 35 | label = AddUIComponent(); 36 | label.text = "Twist segment ends"; 37 | label.textScale = 0.9f; 38 | label.relativePosition = new Vector2(sprite.width + 5f, (height - label.height) / 2 + 1); 39 | 40 | eventCheckChanged += OnCheckChanged; 41 | } 42 | 43 | void OnCheckChanged(UIComponent component, bool value) { 44 | if (refreshing_) 45 | return; 46 | Apply(); 47 | } 48 | 49 | public override void OnDestroy() { 50 | eventCheckChanged -= OnCheckChanged; 51 | base.OnDestroy(); 52 | } 53 | 54 | 55 | UIPanelBase root_; 56 | public override void Start() { 57 | base.Start(); 58 | width = parent.width; 59 | root_ = GetRootContainer() as UIPanelBase; 60 | } 61 | 62 | public void Apply() { 63 | //Log.Debug("TwistCheckbox.Apply called()\n"/* + Environment.StackTrace*/); 64 | var data = (root_ as UISegmentEndControllerPanel).SegmentEndData; 65 | if (data == null) 66 | return; 67 | data.Twist = this.isChecked; 68 | data.DeltaSlopeAngleDeg = 0; 69 | Assert(!refreshing_, "!refreshing_"); 70 | data.RefreshAndUpdate(); 71 | root_.Refresh(); 72 | } 73 | 74 | // protection against unnecessary apply/refresh/infinite recursion. 75 | bool refreshing_ = false; 76 | 77 | public void Refresh() { 78 | //Log.Debug("Refresh called()\n" + Environment.StackTrace); 79 | RefreshValues(); 80 | refreshing_ = true; 81 | //if (root_?.GetData() is NodeData nodeData) 82 | // RefreshNode(nodeData); 83 | 84 | if (isEnabled) { 85 | label.color = UncheckedSprite.color = CheckedSprite.color = Color.white; 86 | 87 | } else { 88 | byte c = 75; 89 | label.color = UncheckedSprite.color = CheckedSprite.color = new Color32(c, c, c, c); 90 | } 91 | 92 | parent.Invalidate(); 93 | Invalidate(); 94 | //Log.Debug($"TwistChekbox.Refresh() visible={isVisible}"); 95 | 96 | refreshing_ = false; 97 | } 98 | 99 | public void RefreshValues() { 100 | refreshing_ = true; 101 | INetworkData data = root_?.GetData(); 102 | if (data is SegmentEndData segmentEndData) { 103 | bool b = segmentEndData.CanModifyTwist(); 104 | if (b) { 105 | this.isChecked = segmentEndData.Twist; 106 | isEnabled = segmentEndData.FlatJunctions; 107 | } 108 | isVisible = parent.isVisible = b; 109 | //Log.Debug($"TwistChekbox.Refresh() isVisible={isVisible} b={b}"); 110 | } else Hide(); 111 | refreshing_ = false; 112 | } 113 | 114 | public void Reset() { 115 | if (root_?.GetData() is SegmentEndData segmentEndData) { 116 | isChecked = segmentEndData.DefaultTwist; 117 | } 118 | } 119 | } 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/HintBox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework; 3 | using ColossalFramework.UI; 4 | using KianCommons; 5 | using KianCommons.UI; 6 | using NodeController.Tool; 7 | using System; 8 | using System.Collections.Generic; 9 | using UnityEngine; 10 | 11 | public class HintBox : UILabel, IToolUpdate { 12 | UIPanelBase root_; 13 | IEnumerable controlls_ => root_.Alive()?.Controls; 14 | NodeControllerTool tool_; 15 | NodeControllerTool Tool => tool_ = tool_.Alive() ?? NodeControllerTool.Instance; 16 | 17 | public override void Awake() { 18 | base.Awake(); 19 | this.relativePosition = Vector2.zero; 20 | this.wordWrap = true; 21 | byte intensity = 32; 22 | this.color = new Color32(intensity, intensity, intensity, 190); 23 | 24 | this.textColor = Color.white; 25 | textScale = 0.8f; 26 | padding = new RectOffset(5, 5, 5, 5); 27 | atlas = TextureUtil.Ingame; 28 | } 29 | 30 | public new float width { 31 | get => base.width; 32 | set { 33 | base.width = value; 34 | this.minimumSize = new Vector2(value, 0); 35 | this.maximumSize = new Vector2(value, height); 36 | } 37 | } 38 | 39 | public new float height { 40 | get => base.height; 41 | set { 42 | base.height = value; 43 | this.maximumSize = new Vector2(width, value); 44 | } 45 | } 46 | 47 | public new Vector2 size { 48 | get => base.size; 49 | set { 50 | base.size = value; 51 | this.minimumSize = new Vector2(width, 0); 52 | this.maximumSize = size; 53 | } 54 | } 55 | 56 | public override void Start() { 57 | base.Start(); 58 | this.relativePosition = new Vector2(0, 1f); 59 | this.backgroundSprite = "GenericPanel"; 60 | this.size = new Vector2(parent.width, 600); 61 | this.autoSize = true; 62 | 63 | root_ = GetRootContainer() as UIPanelBase; 64 | Log.Debug($"size={size} minsize={minimumSize} maxsize={maximumSize}"); 65 | Invalidate(); 66 | } 67 | 68 | public string hint1_, hint2_, hint3_; 69 | 70 | /// 71 | /// Controller hotkeys 72 | /// 73 | public string Hint1; 74 | 75 | // Controller description 76 | public string Hint2; 77 | 78 | // tool 79 | public string Hint3; 80 | 81 | public void OnToolUpdate() { 82 | try { 83 | //string rootname = root_?.GetType()?.Name ?? "null"; 84 | //var version = this?.VersionOf()?.ToString() ?? "null"; 85 | //string id = $"{rootname} V{this.VersionOf()}"; 86 | //Log.DebugWait($"HintBox.Update() called", id); 87 | 88 | if (root_ == null || !root_.isVisible) 89 | return; 90 | if (containsMouse) 91 | return; // prevent flickering on mouse hover 92 | 93 | string h1 = null, h2 = null; 94 | IDataControllerUI c = root_?.GethoveredController(); 95 | if (c != null) { 96 | //Log.DebugWait($"{component.name}-{c}@{rootname}"); 97 | h1 = c.HintHotkeys; 98 | h2 = c.HintDescription; 99 | } 100 | // TODO get h3 from tool. 101 | var prev_h1 = Hint1; 102 | var prev_h2 = Hint2; 103 | var prev_h3 = Hint3; 104 | 105 | Hint1 = h1; 106 | Hint2 = h2; 107 | Hint3 = Tool?.Hint; 108 | 109 | if (Hint1 != prev_h1 || Hint2 != prev_h2 || Hint3 != prev_h3) { 110 | RefreshValues(); 111 | } 112 | } 113 | catch (Exception e) { 114 | Hint1 = e.ToString(); 115 | Log.DebugWait(Hint1); 116 | } 117 | } 118 | 119 | public void RefreshValues() { 120 | //Log.Debug($"Refresh called" + Environment.StackTrace); 121 | bool h1 = !Hint1.IsNullOrWhiteSpace(); 122 | bool h2 = !Hint2.IsNullOrWhiteSpace(); 123 | bool h3 = !Hint3.IsNullOrWhiteSpace(); 124 | const string nl = "\n"; 125 | string t = ""; 126 | 127 | if (h1) t += Hint1; 128 | if (h1 && h2) t += nl; 129 | if (h2) t += Hint2; 130 | if (h2 && h3) t += nl; 131 | if (h3) t += Hint3; 132 | 133 | text = t; 134 | isVisible = t != ""; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /NodeController/Patches/NetSegmentPatches/RenderInstance.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches 2 | { 3 | using HarmonyLib; 4 | using KianCommons; 5 | using JetBrains.Annotations; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Reflection; 9 | using System.Reflection.Emit; 10 | using static KianCommons.Patches.TranspilerUtils; 11 | using NodeController.Util; 12 | using UnityEngine; 13 | 14 | [HarmonyPatch] 15 | class RenderInstance 16 | { 17 | /// left side going away from the junction 18 | static Matrix4x4 CalculateNameMatrix(Matrix4x4 mat, ushort segmentID) { 19 | ref NetSegment segment = ref segmentID.ToSegment(); 20 | var segStart = SegmentEndManager.Instance.GetAt(segmentID, true); 21 | var segEnd = SegmentEndManager.Instance.GetAt(segmentID, false); 22 | 23 | Vector3 startPos, endPos, startDir, endDir; 24 | if (segStart != null) { 25 | startPos = segStart.LeftCorner.Pos; 26 | startDir = segStart.LeftCorner.Dir; 27 | }else { 28 | startPos = segment.m_startNode.ToNode().m_position; 29 | startDir = segment.m_startDirection; 30 | } 31 | 32 | if (segEnd != null) { 33 | endPos = segEnd.LeftCorner.Pos; 34 | endDir = segEnd.LeftCorner.Dir; 35 | } else { 36 | endPos = segment.m_endNode.ToNode().m_position; 37 | endDir = segment.m_endDirection; 38 | } 39 | 40 | 41 | NetSegment.CalculateMiddlePoints( 42 | startPos, startDir, endPos, endDir, true, true, out var b, out var c); 43 | return NetSegment.CalculateControlMatrix( 44 | startPos, b, c, endPos, (startPos+endPos)*0.5f, 1f); 45 | } 46 | 47 | static void CalculateNameMatrix2(ushort segmentID, ref Vector3 startPos, ref Vector3 startDir, ref Vector3 endPos, ref Vector3 endDir) { 48 | ref NetSegment segment = ref segmentID.ToSegment(); 49 | var segStart = SegmentEndManager.Instance.GetAt(segmentID, true); 50 | var segEnd = SegmentEndManager.Instance.GetAt(segmentID, false); 51 | 52 | if (segStart != null) { 53 | startPos = segStart.LeftCorner.Pos; 54 | startDir = segStart.LeftCorner.Dir; 55 | } 56 | if (segEnd != null) { 57 | endPos = segEnd.LeftCorner.Pos; 58 | endDir = segEnd.LeftCorner.Dir; 59 | } 60 | } 61 | 62 | [UsedImplicitly] 63 | static MethodBase TargetMethod() { 64 | return typeof(NetSegment).GetMethod( 65 | nameof(NetSegment.RenderInstance), 66 | BindingFlags.NonPublic | BindingFlags.Instance) ?? 67 | throw new System.Exception("RenderInstance Could not find target method."); 68 | } 69 | 70 | static FieldInfo f_dataMatrix2 = 71 | typeof(RenderManager.Instance).GetField(nameof(RenderManager.Instance.m_dataMatrix2)) ?? 72 | throw new Exception("f_dataMatrix2 is null"); 73 | 74 | static MethodInfo mCalculateNameMatrix = AccessTools.DeclaredMethod( 75 | typeof(CalculateCornerPatch), nameof(CalculateNameMatrix)) ?? 76 | throw new Exception("mCalculateNameMatrix is null"); 77 | 78 | static MethodInfo targetMethod_ = TargetMethod() as MethodInfo; 79 | 80 | [HarmonyBefore(CSURUtil.HARMONY_ID)] 81 | public static IEnumerable Transpiler(ILGenerator il, IEnumerable instructions) { 82 | // apply the flat junctions traspiler 83 | instructions = FlatJunctionsCommons.ModifyFlatJunctionsTranspiler(instructions, targetMethod_); 84 | 85 | CodeInstruction ldarg_segmentID = GetLDArg(targetMethod_, "segmentID"); // push startNodeID into stack, 86 | CodeInstruction call_CalculateNameMatrix = new CodeInstruction(OpCodes.Call, mCalculateNameMatrix); 87 | 88 | // TODO complete transpiler. 89 | int n = 0; 90 | foreach (var instruction in instructions) { 91 | yield return instruction; 92 | bool is_stfld_dataMatrix2 = 93 | instruction.opcode == OpCodes.Stfld && instruction.operand == f_dataMatrix2; 94 | if (is_stfld_dataMatrix2) { 95 | n++; 96 | yield return ldarg_segmentID; 97 | yield return call_CalculateNameMatrix; 98 | } 99 | } 100 | 101 | Log.Debug($"TRANSPILER CalculateCornerPatch: Successfully patched NetSegment.CalculateCorner(). " + 102 | $"found {n} instances of Ldfld NetInfo.m_minCornerOffset"); 103 | yield break; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /NodeController/GUI/NodeControllerButton.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using NodeController.Tool; 3 | using System; 4 | using UnityEngine; 5 | using KianCommons; 6 | using KianCommons.UI; 7 | using UIUtils = KianCommons.UI.UIUtils; 8 | using static KianCommons.Assertion; 9 | 10 | /* A lot of copy-pasting from Crossings mod by Spectra and Roundabout Mod by Strad. The sprites are partly copied as well. */ 11 | 12 | namespace NodeController.GUI { 13 | public class NodeControllerButton : UIButton { 14 | public static NodeControllerButton Instace { get; private set;} 15 | 16 | public static string AtlasName = "NodeControllerButtonUI_rev" + 17 | typeof(NodeControllerButton).Assembly.GetName().Version.Revision; 18 | const int SIZE = 31; 19 | const string CONTAINING_PANEL_NAME = "RoadsOptionPanel"; 20 | readonly static Vector2 RELATIVE_POSITION = new Vector3(94, 38); 21 | 22 | const string NodeControllerButtonBg = "NodeControllerButtonBg"; 23 | const string NodeControllerButtonBgActive = "NodeControllerButtonBgFocused"; 24 | const string NodeControllerButtonBgHovered = "NodeControllerButtonBgHovered"; 25 | internal const string NodeControllerIcon = "NodeControllerIcon"; 26 | internal const string NodeControllerIconActive = "NodeControllerIconPressed"; 27 | 28 | static UIComponent GetContainingPanel() { 29 | var ret = UIUtils.Instance.FindComponent(CONTAINING_PANEL_NAME, null, UIUtils.FindOptions.NameContains); 30 | Log.Debug("GetPanel returns " + ret); 31 | return ret ?? throw new Exception("Could not find " + CONTAINING_PANEL_NAME); 32 | } 33 | 34 | public override void Awake() { 35 | base.Awake(); 36 | Log.Debug("NodeControllerButton.Awake() is called."/* + Environment.StackTrace*/); 37 | } 38 | 39 | public override void Start() { 40 | base.Start(); 41 | Log.Info("NodeControllerButton.Start() is called."); 42 | 43 | name = "NodeControllerButton"; 44 | playAudioEvents = true; 45 | tooltip = "Node Controller\nALT+CLICK: Select segment ends."; 46 | 47 | var builtinTabstrip = UIUtils.Instance.FindComponent("ToolMode", GetContainingPanel(), UIUtils.FindOptions.None); 48 | AssertNotNull(builtinTabstrip, "builtinTabstrip"); 49 | 50 | UIButton tabButton = (UIButton)builtinTabstrip.tabs[0]; 51 | 52 | string[] spriteNames = new string[] 53 | { 54 | NodeControllerButtonBg, 55 | NodeControllerButtonBgActive, 56 | NodeControllerButtonBgHovered, 57 | NodeControllerIcon, 58 | NodeControllerIconActive 59 | }; 60 | 61 | var atlas = TextureUtil.GetAtlas(AtlasName); 62 | if (atlas == UIView.GetAView().defaultAtlas) { 63 | atlas = TextureUtil.CreateTextureAtlas("sprites.png", AtlasName, SIZE, SIZE, spriteNames); 64 | } 65 | 66 | Log.Debug("atlas name is: " + atlas.name); 67 | this.atlas = atlas; 68 | 69 | Deactivate(); 70 | hoveredBgSprite = NodeControllerButtonBgHovered; 71 | 72 | 73 | relativePosition = RELATIVE_POSITION; 74 | size = new Vector2(SIZE, SIZE); 75 | Show(); 76 | Log.Info("NodeControllerButton created sucessfully."); 77 | Unfocus(); 78 | Invalidate(); 79 | //if (parent.name == "RoadsOptionPanel(RoadOptions)") { 80 | // Destroy(Instace); // destroy old instance after cloning 81 | //} 82 | Instace = this; 83 | } 84 | 85 | public void Activate() { 86 | focusedFgSprite = normalBgSprite = pressedBgSprite = disabledBgSprite = NodeControllerButtonBgActive; 87 | normalFgSprite = focusedFgSprite = NodeControllerIconActive; 88 | Invalidate(); 89 | } 90 | 91 | public void Deactivate() { 92 | focusedFgSprite = normalBgSprite = pressedBgSprite = disabledBgSprite = NodeControllerButtonBg; 93 | normalFgSprite = focusedFgSprite = NodeControllerIcon; 94 | Invalidate(); 95 | } 96 | 97 | 98 | public static NodeControllerButton CreateButton() { 99 | Log.Info("NodeControllerButton.CreateButton() called"); 100 | return GetContainingPanel().AddUIComponent(); 101 | } 102 | 103 | protected override void OnClick(UIMouseEventParameter p) { 104 | Log.Debug("ON CLICK CALLED" + Environment.StackTrace); 105 | var buttons = UIUtils.GetCompenentsWithName(name); 106 | Log.Debug(buttons.ToSTR()); 107 | 108 | base.OnClick(p); 109 | NodeControllerTool.Instance.ToggleTool(); 110 | } 111 | 112 | public override void OnDestroy() { 113 | base.OnDestroy(); 114 | } 115 | 116 | public override string ToString() => $"NodeControllerButton:|name={name} parent={parent?.name}|"; 117 | 118 | 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /NodeController/LifeCycle/sequence logger.txt: -------------------------------------------------------------------------------- 1 | 2 | SequenceLogger v1.1.2.28140 3 | 4 | UserMod.ctor() static 5 | UserMod.ctor() instance 6 | UserMod.OnEnabled() 7 | [StorageManager.Load] SequenceLogger.Options.xml 8 | PluginManager.eventPluginsChanged 9 | [21 repeat(s)] 10 | LocaleManager.eventLocaleChanged 11 | SceneManager.activeSceneChanged(, Startup) 12 | SceneManager.sceneLoaded(Startup, Single) 13 | SceneManager.sceneLoaded(LoadingAnimation, Additive) 14 | SceneManager.sceneUnloaded(Startup) 15 | SceneManager.sceneUnloaded(LoadingAnimation) 16 | SceneManager.activeSceneChanged(, IntroScreen) 17 | SceneManager.sceneLoaded(IntroScreen, Single) 18 | SceneManager.sceneLoaded(IntroScreen2, Additive) 19 | UserMod.OnSettingsUI() 20 | SceneManager.sceneLoaded(MainMenu, Additive) 21 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 22 | [1 repeat(s)] 23 | LoadingManager.instance.m_introLoaded 24 | PlatformService.workshop.eventUGCQueryCompleted(result, False) 25 | [16 repeat(s)] 26 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 27 | [2 repeat(s)] 28 | PlatformService.workshop.eventUGCQueryCompleted(result, False) 29 | [2 repeat(s)] 30 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 31 | [5 repeat(s)] 32 | PlatformService.workshop.eventUGCQueryCompleted(result, False) 33 | [29 repeat(s)] 34 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 35 | [10 repeat(s)] 36 | PluginManager.eventPluginsStateChanged 37 | UserMod.OnSettingsUI() 38 | LoadingManager.instance.m_levelPreLoaded 39 | SceneManager.sceneUnloaded(IntroScreen) 40 | SceneManager.sceneUnloaded(IntroScreen2) 41 | SceneManager.sceneUnloaded(MainMenu) 42 | SceneManager.activeSceneChanged(, Game) 43 | SceneManager.sceneLoaded(Game, Single) 44 | LoadingExtension.ctor() static 45 | LoadingExtension.ctor() instance 46 | LoadingExtension.OnCreated(...): 47 | - Mode: Game 48 | - Theme: North 49 | - Complete: False 50 | AssetDataExtension.ctor() static 51 | AssetDataExtension.ctor() instance 52 | AssetDataExtension.OnCreated(assetData=AssetDataWrapper) 53 | LoadingManager.instance.m_metaDataReady 54 | SerializableDataExtension.ctor() static 55 | SerializableDataExtension.ctor() instance 56 | SerializableDataExtension.OnCreated(SerializableDataWrapper) 57 | ThreadingExtension.ctor() static 58 | ThreadingExtension.ctor() instance 59 | ThreadingExtension.OnCreated(...) 60 | SceneManager.sceneLoaded(NorthLoading, Additive) 61 | SceneManager.sceneLoaded(NorthPrefabs, Additive) 62 | SceneManager.sceneLoaded(LoginPackPrefabs, Additive) 63 | SceneManager.sceneLoaded(PreorderPackPrefabs, Additive) 64 | SceneManager.sceneLoaded(SignupPackPrefabs, Additive) 65 | AssetDataExtension.OnAssetLoaded(name=AdaptiveRoadsTest, asset=AdaptiveRoadsTest.AdaptiveRoadsTest_Data (NetInfo), userData=System.Collections.Generic.Dictionary`2[System.String,System.Byte[]]) 66 | SceneManager.sceneLoaded(NorthProperties, Additive) 67 | UserMod.OnSettingsUI() 68 | SceneManager.sceneLoaded(Ingame, Additive) 69 | SerializableDataExtension.OnLoadData() 70 | LoadingManager.instance.m_simulationDataReady 71 | ThreadingExtension.OnBeforeSimulationTick() first tick 72 | LoadingManager.instance.m_levelLoaded(NewGameFromMap) 73 | LoadingExtension.OnLevelLoaded(NewGame) 74 | ThreadingExtension.OnUpdate() first update 75 | --------------------------------------- 76 | LoadingExtension.OnLevelUnloading() 77 | LoadingManager.instance.m_levelPreLoaded 78 | LoadingManager.instance.m_metaDataReady 79 | SceneManager.sceneLoaded(NorthLoading, Additive) 80 | SerializableDataExtension.OnLoadData() 81 | LoadingManager.instance.m_simulationDataReady 82 | LoadingManager.instance.m_levelLoaded(LoadGame) 83 | LoadingExtension.OnLevelLoaded(LoadGame) 84 | ThreadingExtension.OnBeforeSimulationFrame() first frame 85 | ---------------------------------------- 86 | LoadingExtension.OnLevelUnloading() 87 | LoadingManager.instance.m_levelPreUnloaded 88 | SceneManager.sceneUnloaded(Game) 89 | SceneManager.sceneUnloaded(NorthLoading) 90 | SceneManager.sceneUnloaded(NorthPrefabs) 91 | SceneManager.sceneUnloaded(LoginPackPrefabs) 92 | SceneManager.sceneUnloaded(PreorderPackPrefabs) 93 | SceneManager.sceneUnloaded(SignupPackPrefabs) 94 | SceneManager.sceneUnloaded(NorthProperties) 95 | SceneManager.sceneUnloaded(Ingame) 96 | SceneManager.sceneUnloaded(NorthLoading) 97 | UserMod.OnSettingsUI() 98 | SceneManager.activeSceneChanged(, MainMenu) 99 | SceneManager.sceneLoaded(MainMenu, Single) 100 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 101 | [1 repeat(s)] 102 | LoadingExtension.OnReleased() 103 | AssetDataExtension.OnReleased() 104 | LoadingManager.instance.m_levelUnloaded 105 | SerializableDataExtension.OnReleased() 106 | ThreadingExtension.OnReleased() 107 | PlatformService.workshop.eventUGCQueryCompleted(result, False) 108 | [9 repeat(s)] 109 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 110 | [1 repeat(s)] 111 | PlatformService.workshop.eventUGCQueryCompleted(result, False) 112 | [8 repeat(s)] 113 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 114 | PlatformService.workshop.eventUGCQueryCompleted(result, False) 115 | [30 repeat(s)] 116 | PlatformService.workshop.eventUGCRequestUGCDetailsCompleted(result, False) 117 | -------------------------------------------------------------------------------- /NodeController/Patches/Nodeless/NodesLengthPatch.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.Nodeless { 2 | using HarmonyLib; 3 | using KianCommons; 4 | using KianCommons.Patches; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Reflection; 8 | using System.Reflection.Emit; 9 | using UnityEngine; 10 | using static KianCommons.Patches.TranspilerUtils; 11 | 12 | /// 13 | /// sets node length to 0 when node-less to avoid glitches. 14 | /// at close up camera, I check for node-less on a per segment basis. 15 | /// at far away camera, small glitches are not noticeable and no need for a complicated transpiler. 16 | /// I settle for only hiding glitches when all segment end are node-less. 17 | /// 18 | [HarmonyPatch] 19 | static class NodesLengthPatch { 20 | delegate void RenderInstance0(RenderManager.CameraInfo cameraInfo, ushort nodeID, int layerMask); 21 | delegate void RenderInstance(RenderManager.CameraInfo cameraInfo, ushort nodeID, NetInfo info, int iter, NetNode.Flags flags, ref uint instanceIndex, ref RenderManager.Instance data); 22 | delegate bool CalculateGroupData(ushort nodeID, int layer, ref int vertexCount, ref int triangleCount, ref int objectCount, ref RenderGroup.VertexArrays vertexArrays); 23 | delegate void PopulateGroupData(ushort nodeID, int groupX, int groupZ, int layer, ref int vertexIndex, ref int triangleIndex, Vector3 groupPosition, RenderGroup.MeshData data, ref Vector3 min, ref Vector3 max, ref float maxRenderDistance, ref float maxInstanceDistance, ref bool requireSurfaceMaps); 24 | 25 | static IEnumerable TargetMethods() { 26 | yield return DeclaredMethod(typeof(NetNode), nameof(NetNode.RenderInstance)); 27 | yield return DeclaredMethod(typeof(NetNode)); 28 | yield return DeclaredMethod(typeof(NetNode)); 29 | yield return DeclaredMethod(typeof(NetNode)); 30 | } 31 | 32 | static int NodesLength(int length0, ushort nodeID, uint instanceIndex) { 33 | try { 34 | bool nodeless; 35 | if(instanceIndex == ushort.MaxValue) { 36 | var nodeData = NodeManager.Instance.buffer[nodeID]; 37 | nodeless = nodeData?.IsNodelessJunction() ?? false; 38 | } else { 39 | ref var renderData = ref RenderManager.instance.m_instances[instanceIndex]; 40 | ref var node = ref nodeID.ToNode(); 41 | ushort segmentID = node.GetSegment(renderData.m_dataInt0 & 7); 42 | var segmentData = SegmentEndManager.Instance.GetAt(segmentID: segmentID, nodeID: nodeID); 43 | bool dc = (renderData.m_dataInt0 & 8) != 0; 44 | if (dc) { 45 | ushort segmentID2 = node.GetSegment(renderData.m_dataInt0 >> 4); 46 | var segmentData2 = SegmentEndManager.Instance.GetAt(segmentID: segmentID2, nodeID: nodeID); 47 | nodeless = (segmentData?.IsNodeless ?? false) || (segmentData2?.IsNodeless ?? false); 48 | } else { 49 | nodeless = segmentData?.IsNodeless ?? false; 50 | } 51 | } 52 | if(nodeless) 53 | return 0; 54 | } catch(Exception ex) { 55 | ex.Log(); 56 | } 57 | return length0; 58 | } 59 | 60 | static FieldInfo f_nodes => 61 | ReflectionHelpers.GetField(nameof(NetInfo.m_nodes)); 62 | 63 | static MethodInfo mNodesLength = ReflectionHelpers.GetMethod( 64 | typeof(NodesLengthPatch), nameof(NodesLength)); 65 | 66 | public static IEnumerable Transpiler(IEnumerable instructions, MethodBase original) { 67 | CodeInstruction ldargNodeID = GetLDArg(original, "nodeID"); 68 | CodeInstruction callNodesLength = new CodeInstruction(OpCodes.Call, mNodesLength); 69 | CodeInstruction LoadRenderIndex = 70 | GetLDArg(original, "instanceIndex", throwOnError:false) ?? // when camera is close, we can easily get segmentID from render data. 71 | new CodeInstruction(OpCodes.Ldc_I4, (int)ushort.MaxValue); // getting segmentIDs is too complicated and not worth it from far camera range. 72 | 73 | bool isLdNodes_prev = false; 74 | int n = 0; 75 | foreach (var instruction in instructions) { 76 | yield return instruction; 77 | if (isLdNodes_prev && instruction.opcode == OpCodes.Ldlen) { 78 | n++; 79 | yield return ldargNodeID.Clone(); 80 | yield return LoadRenderIndex.Clone(); 81 | if(LoadRenderIndex.IsLdarg()) 82 | yield return new CodeInstruction(OpCodes.Ldind_U4); // convert ref uint to uint 83 | yield return callNodesLength.Clone(); 84 | } 85 | 86 | isLdNodes_prev = instruction.LoadsField(f_nodes); 87 | } 88 | 89 | Log.Succeeded($"patched {n} instances of m_nodes.Length in {original}"); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /NodeController/GUI/Panel/HideMarlkingsCheckbox.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework.UI; 3 | using System; 4 | using UnityEngine; 5 | using KianCommons; 6 | using static KianCommons.HelpersExtensions; 7 | using static KianCommons.Assertion; 8 | using KianCommons.UI; 9 | 10 | public class UIHideMarkingsCheckbox : UICheckBox, IDataControllerUI { 11 | public static UIHideMarkingsCheckbox Instance { get; private set; } 12 | 13 | public string HintHotkeys => "del => reset hovered value to default"; 14 | 15 | public string HintDescription => 16 | "Removes crossing texture for all segment ends. " + 17 | "To ban crossings, untick this and use TMPE."; 18 | 19 | UIPanelBase root_; 20 | public override void Awake() { 21 | base.Awake(); 22 | Instance = this; 23 | name = nameof(UIHideMarkingsCheckbox); 24 | height = 30f; 25 | clipChildren = true; 26 | 27 | UISprite sprite = AddUIComponent(); 28 | sprite.spriteName = "ToggleBase"; 29 | sprite.size = new Vector2(19f, 19f); 30 | sprite.relativePosition = new Vector2(0, (height-sprite.height)/2 ); 31 | sprite.atlas = KianCommons.UI.TextureUtil.GetAtlas("InGame"); 32 | sprite.atlas = TextureUtil.Ingame; 33 | 34 | checkedBoxObject = sprite.AddUIComponent(); 35 | ((UISprite)checkedBoxObject).spriteName = "ToggleBaseFocused"; 36 | ((UISprite)checkedBoxObject).atlas = TextureUtil.Ingame; 37 | checkedBoxObject.size = sprite.size; 38 | checkedBoxObject.relativePosition = Vector3.zero; 39 | 40 | label = AddUIComponent(); 41 | label.text = "No junction markings"; 42 | label.textScale = 0.9f; 43 | label.relativePosition = new Vector2(sprite.width+5f, (height- label.height)/2+1); 44 | 45 | eventCheckChanged += OnCheckChanged; 46 | } 47 | 48 | void OnCheckChanged(UIComponent component, bool value) { 49 | if (refreshing_) 50 | return; 51 | Apply(); 52 | } 53 | 54 | public override void OnDestroy() { 55 | eventCheckChanged -= OnCheckChanged; 56 | base.OnDestroy(); 57 | } 58 | 59 | public override void Start() { 60 | base.Start(); 61 | width = parent.width; 62 | root_ = GetRootContainer() as UIPanelBase; 63 | } 64 | 65 | // protection against unnecessary apply/refresh/infinite recursion. 66 | bool refreshing_ = false; 67 | 68 | public void Apply() { 69 | if (VERBOSE) Log.Debug("UIHideMarkingsCheckbox.Apply called()\n" + Environment.StackTrace); 70 | if (root_ is UINodeControllerPanel) 71 | ApplyNode(); 72 | else if (root_ is UISegmentEndControllerPanel) 73 | ApplySegmentEnd(); 74 | else 75 | throw new Exception("Unreachable code. root_="+ root_); 76 | } 77 | 78 | public void ApplyNode() { 79 | NodeData data = (root_ as UINodeControllerPanel).NodeData; 80 | if (data == null) 81 | return; 82 | data.NoMarkings = this.isChecked; 83 | Assert(!refreshing_, "!refreshing_"); 84 | data.Update(); 85 | (root_ as IDataControllerUI).Refresh(); 86 | 87 | } 88 | 89 | public void ApplySegmentEnd() { 90 | SegmentEndData data = (root_ as UISegmentEndControllerPanel).SegmentEndData; 91 | if (data == null) 92 | return; 93 | data.NoMarkings = this.isChecked; 94 | Log.Debug($"UIHideMarkingsCheckbox.ApplySegmentEnd(): {data}" + 95 | $"isChecked={isChecked} " + 96 | $"data.NoMarkings is set to {data.NoMarkings}"); 97 | data.Update(); 98 | } 99 | 100 | public void Refresh() { 101 | if (VERBOSE) Log.Debug("Refresh called()\n" + Environment.StackTrace); 102 | RefreshValues(); 103 | refreshing_ = true; 104 | if (root_?.GetData() is NodeData nodeData) 105 | RefreshNode(nodeData); 106 | 107 | parent.isVisible = isVisible = isEnabled; 108 | Invalidate(); 109 | refreshing_ = false; 110 | } 111 | 112 | public void RefreshNode(NodeData data) { 113 | if (data.HasUniformNoMarkings()) { 114 | checkedBoxObject.color = Color.white; 115 | } else { 116 | checkedBoxObject.color = Color.grey; 117 | } 118 | } 119 | 120 | public void RefreshValues() { 121 | refreshing_ = true; 122 | INetworkData data = root_?.GetData(); 123 | if (data is SegmentEndData segmentEndData) { 124 | isChecked = segmentEndData.NoMarkings; 125 | isEnabled = segmentEndData.ShowNoMarkingsToggle(); 126 | } else if (data is NodeData nodeData) { 127 | isChecked = nodeData.NoMarkings; 128 | isEnabled = nodeData.ShowNoMarkingsToggle(); 129 | } else Disable(); 130 | refreshing_ = false; 131 | } 132 | 133 | public void Reset() { 134 | isChecked = false; 135 | } 136 | } 137 | } 138 | 139 | 140 | -------------------------------------------------------------------------------- /NodeController/LifeCycle/SerializableDataExtension.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.LifeCycle 2 | { 3 | using JetBrains.Annotations; 4 | using ICities; 5 | using KianCommons; 6 | using System; 7 | using NodeController.GUI; 8 | using KianCommons.Serialization; 9 | 10 | [Serializable] 11 | public class NCState { 12 | public static NCState Instance; 13 | 14 | public string Version = typeof(NCState).VersionOf().ToString(3); 15 | public byte[] NodeManagerData; 16 | public byte[] SegmentEndManagerData; 17 | public GameConfigT GameConfig; 18 | 19 | public static byte[] Serialize() { 20 | NodeManager.ValidateAndHeal(false); 21 | Instance = new NCState { 22 | NodeManagerData = NodeManager.Serialize(), 23 | SegmentEndManagerData = SegmentEndManager.Serialize(), 24 | GameConfig = NCSettings.GameConfig, 25 | }; 26 | 27 | Log.Debug("NCState.Serialize(): saving UnviversalSlopeFixes as " + 28 | Instance.GameConfig.UnviversalSlopeFixes); 29 | 30 | return SerializationUtil.Serialize(Instance); 31 | } 32 | 33 | public static NCState DeserializeState(byte[] data) { 34 | if (data == null) { 35 | Log.Debug($"NCState.Deserialize(data=null)"); 36 | return new NCState(); 37 | } else { 38 | Log.Debug($"NCState.Deserialize(data): data.Length={data?.Length}"); 39 | var ret = SerializationUtil.Deserialize(data, default) as NCState; 40 | if (ret?.Version != null) { //2.1.1 or above 41 | Log.Debug("Deserializing V" + ret.Version); 42 | SerializationUtil.DeserializationVersion = new Version(ret.Version); 43 | } else { 44 | // 2.0 45 | Log.Debug("Deserializing version 2.0"); 46 | ret.Version = "2.0"; 47 | ret.GameConfig = GameConfigT.LoadGameDefault; // for the sake of future proofing. 48 | ret.GameConfig.UnviversalSlopeFixes = true; // in this version I do apply slope fixes. 49 | } 50 | return ret; 51 | } 52 | } 53 | 54 | public void DeserilizeConfig() { 55 | if (GameConfig == null) { 56 | NCSettings.LoadDefaltConfig(NCLifeCycle.Mode); 57 | } else { 58 | NCSettings.GameConfig = GameConfig; 59 | } 60 | Log.Info($"UnviversalSlopeFixes={NCSettings.GameConfig.UnviversalSlopeFixes}"); 61 | } 62 | 63 | public void DeserializeManagers() { 64 | try { 65 | var version = new Version(Version); 66 | SegmentEndManager.Deserialize(SegmentEndManagerData, version); 67 | NodeManager.Deserialize(NodeManagerData, version); 68 | } catch (Exception ex) { ex.Log(); } 69 | } 70 | } 71 | 72 | [UsedImplicitly] 73 | public class SerializableDataExtension 74 | : SerializableDataExtensionBase 75 | { 76 | private const string DATA_ID0 = "RoadTransitionManager_V1.0"; 77 | private const string DATA_ID1 = "NodeC+ontroller_V1.0"; 78 | private const string DATA_ID = "NodeController_V2.0"; 79 | private static ISerializableData SerializableData => SimulationManager.instance.m_SerializableDataWrapper; 80 | 81 | public static int LoadingVersion; 82 | 83 | public static void BeforeNetMangerAfterDeserialize() { 84 | try { 85 | Log.Called(); 86 | byte[] data = SerializableData.LoadData(DATA_ID); 87 | NCState.Instance = NCState.DeserializeState(data); 88 | NCState.Instance.DeserilizeConfig(); 89 | } catch (Exception ex) { ex.Log(); } 90 | } 91 | 92 | public override void OnLoadData() => Load(); 93 | public static void Load() { 94 | try { 95 | Log.Called(); 96 | Log.Debug(Helpers.WhatIsCurrentThread()); 97 | Log.Debug("SimulationPaused=" + SimulationManager.instance.SimulationPaused); 98 | Log.Debug($"[before] NetManger to update {NodeManager.CountUpdatingNodes()} nodes and {SegmentEndManager.CountUpdatingSegments()} segments."); 99 | byte[] data = SerializableData.LoadData(DATA_ID); 100 | if (data != null) { 101 | LoadingVersion = 2; 102 | NCState.Instance?.DeserializeManagers(); 103 | } else { 104 | // convert to new version 105 | LoadingVersion = 1; 106 | data = SerializableData.LoadData(DATA_ID1) 107 | ?? SerializableData.LoadData(DATA_ID0); 108 | NodeManager.Deserialize(data, new Version(1, 0)); 109 | } 110 | 111 | NodeManager.ValidateAndHeal(true); 112 | NodeManager.Instance.OnLoad(); 113 | SegmentEndManager.Instance.OnLoad(); 114 | Log.Info($"[after] NetManger to update {NodeManager.CountUpdatingNodes()} nodes and {SegmentEndManager.CountUpdatingSegments()} segments."); 115 | NCLifeCycle.LoadingStage = NCLifeCycle.Stage.DataLoaded; 116 | Log.Succeeded(); 117 | } catch(Exception ex) { ex.Log(); } 118 | } 119 | 120 | public override void OnSaveData() => Save(); 121 | public static void Save() { 122 | try { 123 | Log.Called(); 124 | byte[] data = NCState.Serialize(); 125 | SerializableData.SaveData(DATA_ID, data); 126 | }catch(Exception ex) { ex.Log(); } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /NodeController/DecompiledSources/NetAI.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.Math; 3 | using KianCommons; 4 | using System; 5 | using UnityEngine; 6 | using KianCommons; 7 | 8 | namespace NodeController.DecompiledSources { 9 | class NetAI { 10 | // NetAI 11 | public NetInfo m_info; 12 | 13 | public virtual void UpdateLanes(ushort segmentID, ref NetSegment segment, bool loading) { 14 | NetManager instance = Singleton.instance; 15 | bool flag = Singleton.instance.m_metaData.m_invertTraffic == SimulationMetaData.MetaBool.True; 16 | uint prevLaneID = 0u; 17 | uint laneID = segment.m_lanes; 18 | segment.CalculateCorner(segmentID, heightOffset: true, start: true, true, 19 | out var cornerPosStartLeft, out var cornerDirStartLeft, out _); 20 | segment.CalculateCorner(segmentID, heightOffset: true, start: false, leftSide: true, 21 | out var cornerPosEndLeft, out var cornerDirEndLeft, out bool smoothStart); 22 | segment.CalculateCorner(segmentID, heightOffset: true, start: true, leftSide: false, 23 | out var cornerPosStartRight, out var cornerDirStartRight, out _); 24 | segment.CalculateCorner(segmentID, heightOffset: true, start: false, leftSide: false, 25 | out var cornerPosEndRight, out var cornerDirEndRight, out bool smoothEnd); 26 | 27 | bool segmentInverted = segment.m_flags.IsFlagSet(NetSegment.Flags. Invert); 28 | 29 | float cc = 128 / Mathf.PI;//40.7436638f 30 | Vector3 deltaPosStart = cornerPosStartRight - cornerPosStartLeft; 31 | Vector3 deltaPosEnd = cornerPosEndRight - cornerPosEndLeft; 32 | if (segmentInverted) { 33 | segment.m_cornerAngleStart = (byte)(Mathf.RoundToInt(Mathf.Atan2(deltaPosStart.z, deltaPosStart.x) * cc) & 255); 34 | segment.m_cornerAngleEnd = (byte)(Mathf.RoundToInt(Mathf.Atan2(-deltaPosEnd.z, -deltaPosEnd.x) * cc) & 255); 35 | } else { 36 | segment.m_cornerAngleStart = (byte)(Mathf.RoundToInt(Mathf.Atan2(-deltaPosStart.z, -deltaPosStart.x) * cc) & 255); 37 | segment.m_cornerAngleEnd = (byte)(Mathf.RoundToInt(Mathf.Atan2(deltaPosEnd.z, deltaPosEnd.x) * cc) & 255); 38 | } 39 | NetLane.Flags flags = NetLane.Flags.None; 40 | if (segment.m_flags.IsFlagSet(NetSegment.Flags.YieldStart)) { 41 | flags |= segmentInverted ? NetLane.Flags.YieldStart : NetLane.Flags.YieldEnd; 42 | } 43 | if (segment.m_flags.IsFlagSet(NetSegment.Flags.YieldEnd)) { 44 | flags |= segmentInverted ? NetLane.Flags.YieldEnd : NetLane.Flags.YieldStart; 45 | } 46 | float lengthAcc = 0f; 47 | float laneCount = 0f; 48 | for (int i = 0; i < this.m_info.m_lanes.Length; i++) { 49 | if (laneID == 0u) { 50 | if (!Singleton.instance.CreateLanes( 51 | out laneID, ref Singleton.instance.m_randomizer, segmentID, 1)) { 52 | break; 53 | } 54 | if (prevLaneID != 0u) { 55 | prevLaneID.ToLane().m_nextLane = laneID; 56 | } else { 57 | segment.m_lanes = laneID; 58 | } 59 | } 60 | NetInfo.Lane laneInfo = this.m_info.m_lanes[i]; 61 | float lanePos01 = laneInfo.m_position / (this.m_info.m_halfWidth * 2f) + 0.5f; // lane pos rescaled between 0~1 62 | if (segmentInverted) { 63 | lanePos01 = 1f - lanePos01; 64 | } 65 | Vector3 startPos = cornerPosStartLeft + (cornerPosStartRight - cornerPosStartLeft) * lanePos01; 66 | Vector3 startDir = Vector3.Lerp(cornerDirStartLeft, cornerDirStartRight, lanePos01); 67 | Vector3 endPos = cornerPosEndRight + (cornerPosEndLeft - cornerPosEndRight) * lanePos01; 68 | Vector3 endDir = Vector3.Lerp(cornerDirEndRight, cornerDirEndLeft, lanePos01); 69 | startPos.y += laneInfo.m_verticalOffset; 70 | endPos.y += laneInfo.m_verticalOffset; 71 | Vector3 b; 72 | Vector3 c; 73 | NetSegment.CalculateMiddlePoints(startPos, startDir, endPos, endDir, smoothStart, smoothEnd, out b, out c); 74 | NetLane.Flags flags2 = laneID.ToLane().Flags(); 75 | NetLane.Flags flags3 = flags; 76 | flags2 &= ~(NetLane.Flags.YieldStart | NetLane.Flags.YieldEnd); 77 | if ((byte)(laneInfo.m_finalDirection & NetInfo.Direction.Both) == 2) { 78 | flags3 &= ~NetLane.Flags.YieldEnd; 79 | } 80 | if ((byte)(laneInfo.m_finalDirection & NetInfo.Direction.Both) == 1) { 81 | flags3 &= ~NetLane.Flags.YieldStart; 82 | } 83 | flags2 |= flags3; 84 | if (flag) { 85 | flags2 |= NetLane.Flags.Inverted; 86 | } else { 87 | flags2 &= ~NetLane.Flags.Inverted; 88 | } 89 | laneID.ToLane().m_bezier = new Bezier3(startPos, b, c, endPos); 90 | laneID.ToLane().m_segment = segmentID; 91 | laneID.ToLane().m_flags = (ushort)flags2; 92 | laneID.ToLane().m_firstTarget = 0; 93 | laneID.ToLane().m_lastTarget = byte.MaxValue; 94 | lengthAcc += laneID.ToLane().UpdateLength(); 95 | laneCount += 1f; 96 | prevLaneID = laneID; 97 | laneID = laneID.ToLane().m_nextLane; 98 | } 99 | if (laneCount != 0f) { 100 | segment.m_averageLength = lengthAcc / laneCount; 101 | } else { 102 | segment.m_averageLength = 0f; 103 | } 104 | } 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /NodeController/Patches/VehicleSuperElevation/SuperElevationCommons.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.Patches.VehicleSuperElevation { 2 | using ColossalFramework; 3 | using HarmonyLib; 4 | using KianCommons; 5 | using KianCommons.Plugins; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Reflection; 9 | using System.Reflection.Emit; 10 | using System.Runtime.CompilerServices; 11 | using UnityEngine; 12 | using static KianCommons.Assertion; 13 | using static KianCommons.Patches.TranspilerUtils; 14 | using static KianCommons.ReflectionHelpers; 15 | 16 | 17 | public static class SuperElevationCommons { 18 | delegate void SimulationStepDelegate( 19 | ushort vehicleID, ref Vehicle vehicleData, ref Vehicle.Frame frameData, 20 | ushort leaderID, ref Vehicle leaderData, int lodPhysics); 21 | 22 | public static MethodBase TargetMethod() => 23 | DeclaredMethod(typeof(T), "SimulationStep"); 24 | 25 | public static MethodBase TargetTMPEMethod() { 26 | string typeName = "TrafficManager.Custom.AI.Custom" + typeof(T); 27 | Type customType = 28 | PluginUtil.GetTrafficManager().GetMainAssembly() 29 | .GetType(typeName, throwOnError: false); 30 | 31 | if(customType != null) 32 | return DeclaredMethod(customType, "CustomSimulationStep"); 33 | return null; 34 | } 35 | 36 | static PathUnit[] pathUnitBuffer => Singleton.instance.m_pathUnits.m_buffer; 37 | 38 | static string ToSTR(this ref PathUnit.Position pathPos) { 39 | var info = pathPos.m_segment.ToSegment().Info; 40 | return 41 | $"segment:{pathPos.m_segment} " + 42 | $"info:{info} " + 43 | $"nLanes={info.m_lanes.Length} " + 44 | $"laneIndex={pathPos.m_lane} "; 45 | } 46 | 47 | public static void Postfix(ref Vehicle vehicleData, ref Vehicle.Frame frameData) { 48 | if (!vehicleData.GetCurrentPathPos(out var pathPos)) 49 | return; 50 | try { 51 | float se = GetCurrentSE(pathPos, vehicleData.m_lastPathOffset * (1f / 255f), ref vehicleData); 52 | 53 | var rot = Quaternion.Euler(0, 0f, se); 54 | frameData.m_rotation *= rot; 55 | } catch (Exception ex) { 56 | Log.Exception(ex, pathPos.ToSTR() , showInPanel: false); 57 | } 58 | } 59 | 60 | internal static bool GetCurrentPathPos(this ref Vehicle vehicleData, out PathUnit.Position pathPos) { 61 | byte pathIndex = vehicleData.m_pathPositionIndex; 62 | if (pathIndex == 255) pathIndex = 0; 63 | return pathUnitBuffer[vehicleData.m_path].GetPosition(pathIndex >> 1, out pathPos); 64 | } 65 | 66 | internal static NetInfo.Lane GetLaneInfo(this ref PathUnit.Position pathPos) => 67 | pathPos.m_segment.ToSegment().Info.m_lanes[pathPos.m_lane]; 68 | 69 | 70 | internal static float GetCurrentSE(PathUnit.Position pathPos, float offset, ref Vehicle vehicleData) { 71 | if (float.IsNaN(offset) || float.IsInfinity(offset)) return 0; 72 | // bezier is always from start to end node regardless of direction. 73 | SegmentEndData segStart = SegmentEndManager.Instance.GetAt(pathPos.m_segment, true); 74 | SegmentEndData segEnd = SegmentEndManager.Instance.GetAt(pathPos.m_segment, false); 75 | float startSE = segStart?.CachedSuperElevationDeg ?? 0; 76 | float endSE = -segEnd?.CachedSuperElevationDeg ?? 0; 77 | float se = Mathf.Lerp(startSE , endSE ,offset); 78 | 79 | var lane = pathPos.GetLaneInfo(); 80 | if (lane is null) return 0; 81 | bool invert = pathPos.m_segment.ToSegment().m_flags.IsFlagSet(NetSegment.Flags.Invert); 82 | bool backward = lane.m_finalDirection == NetInfo.Direction.Backward; 83 | bool reversed = vehicleData.m_flags.IsFlagSet(Vehicle.Flags.Reversed); 84 | 85 | bool bidirectional = lane.m_finalDirection.CheckFlags(NetInfo.Direction.Both); 86 | if (invert) se = -se; 87 | if (backward) se = -se; 88 | if (reversed & !bidirectional) se = -se; 89 | return se; 90 | } 91 | 92 | #region rotation updated 93 | 94 | internal static FieldInfo fRotation = GetField( 95 | typeof(Vehicle.Frame), nameof(Vehicle.Frame.m_rotation)); 96 | 97 | internal static bool RotationUpdated = false; 98 | 99 | [MethodImpl(MethodImplOptions.NoInlining)] 100 | internal static void OnRotationUpdated() => RotationUpdated = true; 101 | 102 | 103 | static FieldInfo f_rotation = 104 | GetField(typeof(Vehicle.Frame), nameof(Vehicle.Frame.m_rotation)); 105 | 106 | 107 | static MethodInfo mOnRotationUpdated = ReflectionHelpers.GetMethod( 108 | typeof(SuperElevationCommons), nameof(OnRotationUpdated)); 109 | 110 | public static IEnumerable OnRotationUpdatedTranspiler( 111 | IEnumerable instructions, 112 | MethodInfo targetMethod) { 113 | AssertNotNull(targetMethod, "targetMethod"); 114 | //Log.Debug("targetMethod=" + targetMethod); 115 | 116 | CodeInstruction call_OnRotationUpdated = new CodeInstruction(OpCodes.Call, mOnRotationUpdated); 117 | 118 | int n = 0; 119 | foreach (var instruction in instructions) { 120 | yield return instruction; 121 | bool is_stfld_rotation = 122 | instruction.opcode == OpCodes.Stfld && instruction.operand == f_rotation; 123 | if (is_stfld_rotation) { // it seems in CarAI the second one is the important one. 124 | n++; 125 | yield return call_OnRotationUpdated; 126 | } 127 | } 128 | 129 | Log.Debug($"TRANSPILER SuperElevationCommons: Successfully patched {targetMethod}. " + 130 | $"found {n} instances of Ldfld NetInfo.m_flatJunctions"); 131 | yield break; 132 | } 133 | #endregion 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /NodeController/GUI/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace NodeController.GUI { 2 | using ColossalFramework; 3 | using ColossalFramework.UI; 4 | using ICities; 5 | using KianCommons; 6 | using NodeController.Tool; 7 | using System; 8 | 9 | [Serializable] 10 | public class GameConfigT { 11 | public bool UnviversalSlopeFixes; 12 | 13 | public static GameConfigT NewGameDefault => new GameConfigT { 14 | UnviversalSlopeFixes = true, 15 | }; 16 | 17 | public static GameConfigT LoadGameDefault => new GameConfigT { 18 | UnviversalSlopeFixes = false, 19 | }; 20 | } 21 | 22 | public static class NCSettings { 23 | public const string FileName = nameof(NodeController); 24 | 25 | public static GameConfigT GameConfig; 26 | 27 | static NCSettings() { 28 | // Creating setting file - from SamsamTS 29 | if (GameSettings.FindSettingsFileByName(FileName) == null) { 30 | GameSettings.AddSettingsFile(new SettingsFile[] { new SettingsFile() { fileName = FileName } }); 31 | } 32 | } 33 | 34 | public static void LoadDefaltConfig(LoadMode mode) { 35 | switch (mode) { 36 | case LoadMode.NewGameFromScenario: 37 | case LoadMode.LoadScenario: 38 | case LoadMode.LoadMap: 39 | // no NC or old NC 40 | NCSettings.GameConfig = GameConfigT.LoadGameDefault; 41 | break; 42 | default: 43 | NCSettings.GameConfig = GameConfigT.NewGameDefault; 44 | break; 45 | } 46 | } 47 | 48 | public static void OnSettingsUI(UIHelper helper) { 49 | Log.Debug("Make settings was called"); 50 | MakeGlobalSettings(helper); 51 | if (!Helpers.InStartupMenu) { 52 | MakeGameSettings(helper); 53 | } 54 | } 55 | 56 | public static void FixTooltipAlignment(UIComponent component) { 57 | component.eventTooltipShow += (c, _) => { 58 | if (c.tooltipBox is UILabel label) 59 | label.textAlignment = UIHorizontalAlignment.Left; 60 | }; 61 | } 62 | 63 | public static void MakeGlobalSettings(UIHelper helper) { 64 | UIHelper group = helper.AddGroup("Global settings") as UIHelper; 65 | UIPanel panel = group.self as UIPanel; 66 | 67 | var keymappings = panel.gameObject.AddComponent(); 68 | keymappings.AddKeymapping("Activation Shortcut", NodeControllerTool.ActivationShortcut); 69 | 70 | UICheckBox snapToggle = group.AddCheckbox( 71 | "Snap to middle node", 72 | NodeControllerTool.SnapToMiddleNode.value, 73 | val => NodeControllerTool.SnapToMiddleNode.value = val) as UICheckBox; 74 | snapToggle.tooltip = "when you click near a middle node:\n" + 75 | " - [checked] => Node controller modifies the node\n" + 76 | " - [unchceked] => Node controller moves the node to hovered position."; 77 | FixTooltipAlignment(snapToggle); 78 | 79 | UICheckBox TMPE_Overlay = group.AddCheckbox( 80 | "Hide TMPE overlay on the selected node", 81 | NodeControllerTool.Hide_TMPE_Overlay.value, 82 | val => NodeControllerTool.Hide_TMPE_Overlay.value = val) as UICheckBox; 83 | TMPE_Overlay.tooltip = "Holding control hides all TMPE overlay.\n" + 84 | "but if this is checked, you don't have to (excluding Crossings/Uturn)"; 85 | FixTooltipAlignment(TMPE_Overlay); 86 | 87 | } 88 | 89 | static UICheckBox universalFixes_; 90 | public static void MakeGameSettings(UIHelper helper) { 91 | UIHelper group = helper.AddGroup("Game settings") as UIHelper; 92 | 93 | UIPanel panel = group.self as UIPanel; 94 | 95 | object val = GameConfig?.UnviversalSlopeFixes; val = val ?? "null"; 96 | Log.Debug($"MakeGameSettings: UnviversalSlopeFixes =" + val); 97 | universalFixes_ = group.AddCheckbox( 98 | "apply universal slope fixes(flat junctions, curvature of extreme slopes)", 99 | defaultValue: GameConfig?.UnviversalSlopeFixes ?? GameConfigT.NewGameDefault.UnviversalSlopeFixes, 100 | ApplyUniversalSlopeFixes) as UICheckBox; 101 | universalFixes_.tooltip = "changing this may influence existing custom nodes."; 102 | } 103 | 104 | public static void UpdateGameSettings() { 105 | if (GameConfig == null) { 106 | Log.Error("GameConfig==null"); 107 | return; 108 | } 109 | if(!Helpers.InMainThread()) 110 | Log.Warning("UpdateGameSettings should be called from main thread"); 111 | Log.Debug($"UpdateGameSettings: UnviversalSlopeFixes =" + GameConfig.UnviversalSlopeFixes); 112 | if(universalFixes_) 113 | universalFixes_.isChecked = GameConfig.UnviversalSlopeFixes; 114 | } 115 | 116 | static void ApplyUniversalSlopeFixes(bool value) { 117 | GameConfig.UnviversalSlopeFixes = value; 118 | for (ushort segmentID = 0; segmentID < NetManager.MAX_SEGMENT_COUNT; ++segmentID) { 119 | if (NetUtil.IsSegmentValid(segmentID)) { 120 | // update only those that have flat junctions and not customized (custom nodes use enforced flat junctions). 121 | if (segmentID.ToSegment().Info.m_flatJunctions == false && 122 | !segmentID.ToSegment().m_startNode.ToNode().m_flags.IsFlagSet(NetNode.Flags.Middle) && 123 | !segmentID.ToSegment().m_endNode.ToNode().m_flags.IsFlagSet(NetNode.Flags.Middle) && 124 | SegmentEndManager.Instance.GetAt(segmentID, true) == null && 125 | SegmentEndManager.Instance.GetAt(segmentID, false) == null) { 126 | NetManager.instance.UpdateSegment(segmentID); 127 | } 128 | 129 | // also update segments with extreme slopes. 130 | if (segmentID.ToSegment().m_startDirection.y > 2 || 131 | segmentID.ToSegment().m_endDirection.y > 2) { 132 | NetManager.instance.UpdateSegment(segmentID); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | --------------------------------------------------------------------------------