├── RoundaboutBuilder ├── Resources │ └── sprites.png ├── Tools │ ├── SettingsBool.cs │ ├── RoundaboutNode.cs │ ├── GraphTraveller2.cs │ ├── GlitchedRoadsCheck.cs │ ├── Ellipse.cs │ ├── FinalConnector.cs │ └── NetUtil.cs ├── Properties │ └── AssemblyInfo.cs ├── NetWrappers │ ├── AbstractNetWrapper.cs │ ├── WrappersDictionary.cs │ ├── WrappedNode.cs │ └── WrappedSegment.cs ├── UI │ ├── HoveringLabel.cs │ ├── UIUtil.cs │ ├── NumericTextField.cs │ ├── ResourceLoader.cs │ ├── UIPanelButton.cs │ ├── KeyMappings.cs │ └── UIWindow.cs ├── README.txt ├── ModLoadingExtension.cs ├── GameActions │ ├── AbstractAction.cs │ └── TmpeActions.cs ├── RoundaboutBuilder.csproj ├── ToolBaseExtended.cs ├── ModThreading.cs ├── FreeCursorTool.cs ├── AutomaticRoundaboutBuilder.cs ├── RoundaboutTool.cs └── EllipseTool.cs ├── RoundaboutBuilder.sln ├── .gitattributes └── .gitignore /RoundaboutBuilder/Resources/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Strdate/AutomaticRoundaboutBuilder/HEAD/RoundaboutBuilder/Resources/sprites.png -------------------------------------------------------------------------------- /RoundaboutBuilder/Tools/SettingsBool.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.UI; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace RoundaboutBuilder.Tools 9 | { 10 | public class SettingsBool : SavedBool 11 | { 12 | public string Description { get; private set; } 13 | public string Tooltip { get; private set; } 14 | 15 | public SettingsBool(string description, string tooltip, string name, bool defaultValue) : base(name, RoundAboutBuilder.settingsFileName, defaultValue, true) 16 | { 17 | this.Description = description; 18 | this.Tooltip = tooltip; 19 | } 20 | 21 | public UICheckBox Draw(UIHelper group, ICities.OnCheckChanged callback = null) 22 | { 23 | UICheckBox chb; 24 | chb = (UICheckBox)group.AddCheckbox(Description, this.value, (b) => 25 | { 26 | this.value = b; 27 | if(callback != null) callback.Invoke(b); 28 | }); 29 | chb.tooltip = Tooltip; 30 | return chb; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RoundaboutBuilder.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.271 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoundaboutBuilder", "RoundaboutBuilder\RoundaboutBuilder.csproj", "{FA19FC4E-5FCB-4CF2-96F9-E09B5B9C9A50}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {FA19FC4E-5FCB-4CF2-96F9-E09B5B9C9A50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {FA19FC4E-5FCB-4CF2-96F9-E09B5B9C9A50}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {FA19FC4E-5FCB-4CF2-96F9-E09B5B9C9A50}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {FA19FC4E-5FCB-4CF2-96F9-E09B5B9C9A50}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {2BC9A5E3-8257-4E4B-91ED-5DA3A45926E1} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /RoundaboutBuilder/Tools/RoundaboutNode.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.Math; 3 | using SharedEnvironment; 4 | using System; 5 | using UnityEngine; 6 | 7 | /* By Strad, 01/2019, 10/219 */ 8 | 9 | namespace RoundaboutBuilder.Tools 10 | { 11 | /* In the end not a struct. Whatever. */ 12 | public class RoundaboutNode : IComparable 13 | { 14 | public WrappedNode wrappedNode; 15 | public double angle = 0; 16 | public GameActionExtended action; 17 | 18 | public void Create(ActionGroup group, NetInfo info = null) 19 | { 20 | if(action == null) 21 | { 22 | if(info != null) 23 | { 24 | wrappedNode.NetInfo = info; 25 | } 26 | group.Actions.Add(wrappedNode); 27 | } 28 | } 29 | 30 | /* Constructors */ 31 | 32 | public RoundaboutNode(Vector3 vector) 33 | { 34 | wrappedNode = new WrappedNode(); 35 | wrappedNode.Position = vector; 36 | } 37 | 38 | public RoundaboutNode(WrappedNode wrappedNode) 39 | { 40 | this.wrappedNode = wrappedNode; 41 | } 42 | 43 | /* Utility */ 44 | 45 | public int CompareTo(RoundaboutNode other) 46 | { 47 | if (angle - other.angle < 0.0001f) 48 | return 0; 49 | else if (angle > other.angle) 50 | return -1; 51 | 52 | return 1; 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /RoundaboutBuilder/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RoundaboutBuilder")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("RoundaboutBuilder")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("fa19fc4e-5fcb-4cf2-96f9-e09b5b9c9a50")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /RoundaboutBuilder/NetWrappers/AbstractNetWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace SharedEnvironment 7 | { 8 | public abstract class AbstractNetWrapper : GameAction 9 | { 10 | protected ushort _id; 11 | public ushort Id { get => _id == 0 ? throw new NetWrapperException("Item is not created") : _id; } 12 | 13 | protected bool _isBuildAction = true; 14 | public bool IsBuildAction { get => _isBuildAction; set => _isBuildAction = value; } 15 | 16 | public bool IsCreated() 17 | { 18 | return _id != 0; 19 | } 20 | 21 | public abstract void Create(); 22 | 23 | public abstract bool Release(); 24 | 25 | public override void Do() 26 | { 27 | if(_isBuildAction) 28 | { 29 | Create(); 30 | } 31 | else 32 | { 33 | Release(); 34 | } 35 | } 36 | 37 | public override void Undo() 38 | { 39 | if (_isBuildAction) 40 | { 41 | Release(); 42 | } 43 | else 44 | { 45 | Create(); 46 | } 47 | } 48 | 49 | public override void Redo() 50 | { 51 | Do(); 52 | } 53 | } 54 | 55 | public class NetWrapperException : Exception 56 | { 57 | public NetWrapperException(string message) : base(message) 58 | { 59 | 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RoundaboutBuilder/NetWrappers/WrappersDictionary.cs: -------------------------------------------------------------------------------- 1 | using RoundaboutBuilder.Tools; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace SharedEnvironment 8 | { 9 | public class WrappersDictionary 10 | { 11 | public Dictionary RegisteredNodes { get; private set; } = new Dictionary(); 12 | 13 | public Dictionary RegisteredSegments { get; private set; } = new Dictionary(); 14 | 15 | public WrappedNode RegisterNode(ushort id) 16 | { 17 | if (id == 0) 18 | return null; 19 | 20 | WrappedNode node; 21 | if(!RegisteredNodes.TryGetValue(id,out node)) 22 | { 23 | node = new WrappedNode(id); 24 | RegisteredNodes[id] = node; 25 | } 26 | 27 | return node; 28 | } 29 | 30 | public WrappedSegment RegisterSegment(ushort id) 31 | { 32 | if (id == 0) 33 | return null; 34 | 35 | WrappedSegment segment; 36 | if (!RegisteredSegments.TryGetValue(id, out segment)) 37 | { 38 | var node1 = RegisterNode(NetUtil.Segment(id).m_startNode); 39 | var node2 = RegisterNode(NetUtil.Segment(id).m_endNode); 40 | segment = new WrappedSegment(node1, node2, id); 41 | RegisteredSegments[id] = segment; 42 | } 43 | 44 | return segment; 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /RoundaboutBuilder/UI/HoveringLabel.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEngine; 7 | 8 | /* This was mostly copied from Precision Engineering by Simie. Thanks! */ 9 | 10 | namespace RoundaboutBuilder.UI 11 | { 12 | public class HoveringLabel : UILabel 13 | { 14 | public void SetValue(string v) 15 | { 16 | text = string.Format("{0}", v); 17 | } 18 | 19 | /*public void SetWorldPosition(RenderManager.CameraInfo camera, Vector3 worldPos) 20 | { 21 | var uiView = GetUIView(); 22 | 23 | var vector3_1 = Camera.main.WorldToScreenPoint(worldPos) / uiView.inputScale; 24 | var vector3_3 = uiView.ScreenPointToGUI(vector3_1) - new Vector2(size.x * 0.5f, size.y + 10 * 0.5f ); 25 | // + new Vector2(vector3_2.x, vector3_2.y); 26 | 27 | relativePosition = vector3_3; 28 | textScale = 0.65f; // 0.65f 0.8f 1.1f 29 | }*/ 30 | 31 | public override void Start() 32 | { 33 | base.Start(); 34 | 35 | backgroundSprite = "CursorInfoBack"; 36 | autoSize = true; 37 | padding = new RectOffset(5, 5, 5, 5); 38 | textScale = 0.65f; 39 | textAlignment = UIHorizontalAlignment.Center; 40 | verticalAlignment = UIVerticalAlignment.Middle; 41 | zOrder = 100; 42 | 43 | pivot = UIPivotPoint.MiddleCenter; 44 | 45 | color = new Color32(255, 255, 255, 190); 46 | processMarkup = true; 47 | 48 | isInteractive = false; 49 | 50 | //Construction cost: 520 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /RoundaboutBuilder/README.txt: -------------------------------------------------------------------------------- 1 | 2 | Automatic Roundabout Builder by Strad 3 | Version RELEASE 1.3.0 4 | January 2019 5 | 6 | Well, this file is not updated. See Steam Workshop page 7 | 8 | 9 | VERSION RELEASE 1.0.1 10 | 11 | - BUGFIX: UI components no more protrude from the widnow 12 | - Allowed key navigation in road dropdown menu 13 | 14 | 15 | 16 | VERSION RELEASE 1.0.0 17 | 18 | - UI Button 19 | - More TMPE policies 20 | 21 | 22 | 23 | VERSION BETA 1.2.0 24 | 25 | - UI overhaul 26 | more... 27 | 28 | 29 | 30 | VERSION BETA 1.1.0 31 | - Elliptic roundabouts 32 | - New road snapping algorithm (and new bugs as a bonus) 33 | 34 | 35 | Thanks to boformer and Elektrix for I have reused parts of their code. 36 | 37 | This mod builds a roundabout in place of any intersection. It clips the roads automatically back onto itself. Before using, clean the nearby area of buildings. 38 | Recommended to use only on the ground on flat terrain. Save the game before using. Road anarchy is very recommended. 39 | 40 | Usage: 41 | Press CONTROL+O to open or close the tool menu. There you can select radius of your desired roundabout. It allows you to go as low as 5 units, but 42 | you shouldn't go under 15 units. Alternatively you can use keys + or - to adjust the radius. 43 | Click the Keep open button so it doesn't close when you unselect the tool. Click anywhere in the window to reactivate the tool. 44 | 45 | Warning! If you want to break this mod, you very easily can. Do not use it on map edge, in water, under buildings... 46 | 47 | Issues: 48 | - When building the roundabout, this mod doesn't detect how close are the road nodes it clips onto. Sometimes they are too close and the roads are then buggy. 49 | - Sometimes the direction of the roads is reversed. 50 | - other... 51 | 52 | Please leave any feedback! I am not an experienced programmer and probably I did many things in more complicated way then necessary. 53 | 54 | USE AT YOUR OWN RISK! 55 | 56 | Steam workshop: https://steamcommunity.com/sharedfiles/filedetails/?id=1625704117 57 | -------------------------------------------------------------------------------- /RoundaboutBuilder/NetWrappers/WrappedNode.cs: -------------------------------------------------------------------------------- 1 | using RoundaboutBuilder.Tools; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEngine; 7 | 8 | namespace SharedEnvironment 9 | { 10 | public class WrappedNode : AbstractNetWrapper 11 | { 12 | private Vector3 _position; 13 | public Vector3 Position 14 | { 15 | get => IsCreated() ? NetUtil.Node(_id).m_position : _position; 16 | set => _position = IsCreated() ? throw new NetWrapperException("Cannot modify built node") : value; 17 | } 18 | 19 | private ushort _netInfoIndex; 20 | public NetInfo NetInfo 21 | { 22 | get => IsCreated() ? NetUtil.Node(_id).Info : NetUtil.NetinfoFromIndex(_netInfoIndex); 23 | set => _netInfoIndex = IsCreated() ? throw new NetWrapperException("Cannot modify built node") : NetUtil.NetinfoToIndex( value ); 24 | } 25 | 26 | public ref NetNode Get 27 | { 28 | get => ref NetUtil.Node(Id); 29 | } 30 | 31 | // methods 32 | 33 | public override void Create() 34 | { 35 | if(!IsCreated()) 36 | { 37 | _id = NetUtil.CreateNode( NetUtil.NetinfoFromIndex(_netInfoIndex) , _position); 38 | } 39 | } 40 | 41 | public override bool Release() 42 | { 43 | if(IsCreated()) 44 | { 45 | _position = NetUtil.Node(_id).m_position; 46 | _netInfoIndex = NetUtil.Node(_id).m_infoIndex; 47 | 48 | NetUtil.ReleaseNode(_id); 49 | if(!NetUtil.ExistsNode(_id)) 50 | { 51 | _id = 0; 52 | return true; 53 | } 54 | return false; 55 | } 56 | return true; // ?? true or false 57 | } 58 | 59 | // Constructors 60 | 61 | public WrappedNode() { } 62 | 63 | public WrappedNode(ushort id) 64 | { 65 | if( id != 0 && !NetUtil.ExistsNode(id) ) 66 | { 67 | throw new NetWrapperException("Cannot wrap nonexisting node"); 68 | } 69 | _id = id; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /RoundaboutBuilder/UI/UIUtil.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using UnityEngine; 7 | 8 | namespace RoundaboutBuilder.UI 9 | { 10 | public static class UIUtil 11 | { 12 | /* The code below was copied from Fine Road Tool and More Shortcuts mod by SamsamTS. Thanks! */ 13 | 14 | public static UICheckBox CreateCheckBox(UIComponent parent) 15 | { 16 | UICheckBox checkBox = (UICheckBox)parent.AddUIComponent(); 17 | 18 | checkBox.width = 300f; 19 | checkBox.height = 20f; 20 | checkBox.clipChildren = true; 21 | 22 | UISprite sprite = checkBox.AddUIComponent(); 23 | sprite.atlas = ResourceLoader.GetAtlas("Ingame"); 24 | sprite.spriteName = "ToggleBase"; 25 | sprite.size = new Vector2(16f, 16f); 26 | sprite.relativePosition = Vector3.zero; 27 | 28 | checkBox.checkedBoxObject = sprite.AddUIComponent(); 29 | ((UISprite)checkBox.checkedBoxObject).atlas = ResourceLoader.GetAtlas("Ingame"); 30 | ((UISprite)checkBox.checkedBoxObject).spriteName = "ToggleBaseFocused"; 31 | checkBox.checkedBoxObject.size = new Vector2(16f, 16f); 32 | checkBox.checkedBoxObject.relativePosition = Vector3.zero; 33 | 34 | checkBox.label = checkBox.AddUIComponent(); 35 | checkBox.label.text = " "; 36 | checkBox.label.textScale = 0.9f; 37 | checkBox.label.relativePosition = new Vector3(22f, 2f); 38 | 39 | checkBox.playAudioEvents = true; 40 | 41 | return checkBox; 42 | } 43 | 44 | public static UIButton CreateButton(UIComponent parent) 45 | { 46 | UIButton button = (UIButton)parent.AddUIComponent(); 47 | 48 | button.atlas = ResourceLoader.GetAtlas("Ingame"); 49 | button.size = new Vector2(90f, 30f); 50 | button.textScale = 0.9f; 51 | button.normalBgSprite = "ButtonMenu"; 52 | button.hoveredBgSprite = "ButtonMenuHovered"; 53 | button.pressedBgSprite = "ButtonMenuPressed"; 54 | button.disabledBgSprite = "ButtonMenuDisabled"; 55 | button.canFocus = false; 56 | button.playAudioEvents = true; 57 | 58 | return button; 59 | } 60 | 61 | // Ripped from Elektrix 62 | public static void SetupButtonStateSprites(ref UIButton button, string spriteName, bool noNormal = false) 63 | { 64 | button.normalBgSprite = spriteName + (noNormal ? "" : "Normal"); 65 | button.hoveredBgSprite = spriteName + "Hovered"; 66 | button.focusedBgSprite = spriteName + "Focused"; 67 | button.pressedBgSprite = spriteName + "Pressed"; 68 | button.disabledBgSprite = spriteName + "Disabled"; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /RoundaboutBuilder/UI/NumericTextField.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using System; 3 | using UnityEngine; 4 | 5 | /* Version RELEASE 1.4.0+ */ 6 | 7 | /* By Strad, 2019 */ 8 | 9 | /* Credit to SamsamTS a T__S from whom I copied big chunks of code */ 10 | 11 | namespace RoundaboutBuilder.UI 12 | { 13 | public class NumericTextField : UITextField 14 | { 15 | private string prevText = ""; 16 | 17 | public int? Value 18 | { 19 | get 20 | { 21 | if (int.TryParse(text, out int result) && IsValid(result)) return result; 22 | else return null; 23 | } 24 | set 25 | { 26 | if (value == null) 27 | text = DefaultVal.ToString(); 28 | else 29 | text = value.ToString(); 30 | } 31 | } 32 | 33 | public int MaxVal = 2000; 34 | public int MinVal = 4; 35 | public int Increment = 8; 36 | public int DefaultVal = 40; 37 | 38 | public NumericTextField() 39 | { 40 | atlas = ResourceLoader.GetAtlas("Ingame"); 41 | size = new Vector2(50, 20); 42 | padding = new RectOffset(4, 4, 3, 3); 43 | builtinKeyNavigation = true; 44 | isInteractive = true; 45 | readOnly = false; 46 | horizontalAlignment = UIHorizontalAlignment.Center; 47 | selectionSprite = "EmptySprite"; 48 | selectionBackgroundColor = new Color32(0, 172, 234, 255); 49 | normalBgSprite = "TextFieldPanelHovered"; 50 | disabledBgSprite = "TextFieldPanelHovered"; 51 | textColor = new Color32(0, 0, 0, 255); 52 | disabledTextColor = new Color32(80, 80, 80, 128); 53 | color = new Color32(255, 255, 255, 255); 54 | textScale = 0.9f; 55 | useDropShadow = true; 56 | text = DefaultVal.ToString(); 57 | } 58 | 59 | private bool IsValid(string text) 60 | { 61 | return int.TryParse(text, out int result) && IsValid(result); 62 | } 63 | public bool IsValid(int value) 64 | { 65 | return value >= MinVal && value <= MaxVal; 66 | } 67 | 68 | protected override void OnTextChanged() 69 | { 70 | base.OnTextChanged(); 71 | /*Debug.Log("On text changed"); 72 | 73 | if (text == prevText) return;*/ 74 | 75 | if (int.TryParse(text, out int result) || text == "" || text == "-" ) 76 | { 77 | prevText = text; 78 | } 79 | else 80 | { 81 | text = prevText; 82 | Unfocus(); 83 | } 84 | } 85 | 86 | // Increase value on [+] click 87 | public void Increase() 88 | { 89 | int newValue = 0; 90 | if (Value == null) 91 | { 92 | text = DefaultVal.ToString(); 93 | return; 94 | } 95 | else 96 | { 97 | newValue = Convert.ToInt32(Math.Ceiling( (double)(Value + 1) / (Increment))) * Increment; 98 | } 99 | if (IsValid(newValue)) text = newValue.ToString(); 100 | } 101 | 102 | // Decrease value on [-] click 103 | public void Decrease() 104 | { 105 | int newValue = 0; 106 | if (Value == null) 107 | { 108 | text = DefaultVal.ToString(); 109 | return; 110 | } 111 | else 112 | { 113 | newValue = Convert.ToInt32(Math.Floor((double)(Value - 1) / (Increment))) * Increment; 114 | } 115 | if (IsValid(newValue)) text = newValue.ToString(); 116 | } 117 | 118 | public void Reset() 119 | { 120 | Value = DefaultVal; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /RoundaboutBuilder/UI/ResourceLoader.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using UnityEngine; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | /* Direct copy from Parallel Road Tool */ 7 | 8 | namespace RoundaboutBuilder.UI 9 | { 10 | class ResourceLoader 11 | { 12 | public static UITextureAtlas CreateTextureAtlas(string textureFile, string atlasName, Material baseMaterial, int spriteWidth, int spriteHeight, string[] spriteNames) 13 | { 14 | Texture2D texture2D = new Texture2D(spriteWidth * spriteNames.Length, spriteHeight, TextureFormat.ARGB32, false); 15 | texture2D.filterMode = FilterMode.Bilinear; 16 | Assembly executingAssembly = Assembly.GetExecutingAssembly(); 17 | Stream manifestResourceStream = executingAssembly.GetManifestResourceStream("RoundaboutBuilder.Resources." + textureFile); 18 | byte[] array = new byte[manifestResourceStream.Length]; 19 | manifestResourceStream.Read(array, 0, array.Length); 20 | texture2D.LoadImage(array); 21 | texture2D.Apply(true, true); 22 | UITextureAtlas uitextureAtlas = ScriptableObject.CreateInstance(); 23 | Material material = UnityEngine.Object.Instantiate(baseMaterial); 24 | material.mainTexture = texture2D; 25 | uitextureAtlas.material = material; 26 | uitextureAtlas.name = atlasName; 27 | int num2; 28 | for (int i = 0; i < spriteNames.Length; i = num2) 29 | { 30 | float num = 1f / (float)spriteNames.Length; 31 | UITextureAtlas.SpriteInfo spriteInfo = new UITextureAtlas.SpriteInfo 32 | { 33 | name = spriteNames[i], 34 | texture = texture2D, 35 | region = new Rect((float)i * num, 0f, num, 1f) 36 | }; 37 | uitextureAtlas.AddSprite(spriteInfo); 38 | num2 = i + 1; 39 | } 40 | return uitextureAtlas; 41 | } 42 | 43 | public static void AddTexturesInAtlas(UITextureAtlas atlas, Texture2D[] newTextures, bool locked = false) 44 | { 45 | Texture2D[] textures = new Texture2D[atlas.count + newTextures.Length]; 46 | 47 | for (int i = 0; i < atlas.count; i++) 48 | { 49 | Texture2D texture2D = atlas.sprites[i].texture; 50 | 51 | if (locked) 52 | { 53 | // Locked textures workaround 54 | RenderTexture renderTexture = RenderTexture.GetTemporary(texture2D.width, texture2D.height, 0); 55 | Graphics.Blit(texture2D, renderTexture); 56 | 57 | RenderTexture active = RenderTexture.active; 58 | texture2D = new Texture2D(renderTexture.width, renderTexture.height); 59 | RenderTexture.active = renderTexture; 60 | texture2D.ReadPixels(new Rect(0f, 0f, (float)renderTexture.width, (float)renderTexture.height), 0, 0); 61 | texture2D.Apply(); 62 | RenderTexture.active = active; 63 | 64 | RenderTexture.ReleaseTemporary(renderTexture); 65 | } 66 | 67 | textures[i] = texture2D; 68 | textures[i].name = atlas.sprites[i].name; 69 | } 70 | 71 | for (int i = 0; i < newTextures.Length; i++) 72 | textures[atlas.count + i] = newTextures[i]; 73 | 74 | Rect[] regions = atlas.texture.PackTextures(textures, atlas.padding, 4096, false); 75 | 76 | atlas.sprites.Clear(); 77 | 78 | for (int i = 0; i < textures.Length; i++) 79 | { 80 | UITextureAtlas.SpriteInfo spriteInfo = atlas[textures[i].name]; 81 | atlas.sprites.Add(new UITextureAtlas.SpriteInfo 82 | { 83 | texture = textures[i], 84 | name = textures[i].name, 85 | border = (spriteInfo != null) ? spriteInfo.border : new RectOffset(), 86 | region = regions[i] 87 | }); 88 | } 89 | 90 | atlas.RebuildIndexes(); 91 | } 92 | 93 | public static UITextureAtlas GetAtlas(string name) 94 | { 95 | UITextureAtlas[] atlases = Resources.FindObjectsOfTypeAll(typeof(UITextureAtlas)) as UITextureAtlas[]; 96 | for (int i = 0; i < atlases.Length; i++) 97 | { 98 | if (atlases[i].name == name) 99 | return atlases[i]; 100 | } 101 | 102 | return UIView.GetAView().defaultAtlas; 103 | } 104 | 105 | private static Texture2D loadTextureFromAssembly(string path) 106 | { 107 | Stream manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); 108 | 109 | byte[] array = new byte[manifestResourceStream.Length]; 110 | manifestResourceStream.Read(array, 0, array.Length); 111 | 112 | Texture2D texture2D = new Texture2D(2, 2, TextureFormat.ARGB32, false); 113 | texture2D.LoadImage(array); 114 | 115 | return texture2D; 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /RoundaboutBuilder/Tools/GraphTraveller2.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using System; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | /* By Strad, 01/2019 */ 7 | 8 | /* Version RELEASE 1.4.0+ */ 9 | 10 | namespace RoundaboutBuilder.Tools 11 | { 12 | public class GraphTraveller2 13 | { 14 | /* This class collects all segments and nodes inside the future roundabout. The edge segments and nodes are saved separately. */ 15 | 16 | // bez paddingu 17 | 18 | //public static readonly int DISTANCE_PADDING = 15; 19 | public static readonly int MAX_SEGMENTS_PER_NODE = 8; 20 | 21 | public List OuterNodes { get; private set; } = new List(); // Nodes right behind the edge 22 | public List OuterSegments { get; private set; } = new List(); // Segments crossing the edge 23 | public List InnerNodes { get; private set; } = new List(); 24 | public List InnerSegments { get; private set; } = new List(); 25 | 26 | List visitedNodes = new List(); // Well, not the most efficient way how to store visited nodes, but whatever... 27 | 28 | private int noInfiniteRecursion; 29 | private static readonly int RECURSION_TRESHOLD = 300; 30 | 31 | private ushort m_startNodeId; 32 | private NetNode m_startNode; 33 | private Vector3 m_startNodeVector; 34 | 35 | public Ellipse Ellipse { get; private set; } 36 | 37 | /* Main part: */ 38 | 39 | public GraphTraveller2(ushort startNodeId, Ellipse ellipse) 40 | { 41 | Ellipse = ellipse; 42 | DFS(startNodeId); 43 | noInfiniteRecursion = 0; 44 | m_startNodeId = startNodeId; 45 | m_startNode = GetNode(startNodeId); 46 | m_startNodeVector = m_startNode.m_position; 47 | } 48 | 49 | /* Depth-first search */ 50 | private void DFS(ushort node) 51 | { 52 | noInfiniteRecursion++; 53 | if(noInfiniteRecursion > RECURSION_TRESHOLD) 54 | { 55 | UI.UIWindow.instance.ThrowErrorMsg("Error: DFS method exeeded max recursion limit"); 56 | OuterNodes = OuterSegments = InnerNodes = InnerSegments = new List(); 57 | return; 58 | } 59 | visitedNodes.Add(node); 60 | InnerNodes.Add(node); 61 | NetNode netNode = GetNode(node); 62 | for (int i = 0; i < MAX_SEGMENTS_PER_NODE; i++) 63 | { 64 | //Debug.Log(string.Format("NodeID: {0}; segment no. {1}...",node,i)); 65 | ushort segment = netNode.GetSegment(i); 66 | //Debug.Log(string.Format("ID of that segment is {0}", segment)); 67 | if (!segmentExists(segment)) continue; 68 | 69 | NetSegment curSegment = GetSegment(segment); 70 | ushort newNode = getOtherNode(node, segment); 71 | 72 | if (isVisited(newNode)) continue; 73 | 74 | NetNode newNetNode = GetNode(newNode); 75 | visitedNodes.Add(newNode); 76 | 77 | /* Checking, whether the node is inside the roundabout or not. If not, it is added to the edge nodes and the search ends there. */ 78 | if ( !Ellipse.IsInsideEllipse(newNetNode.m_position)) 79 | { 80 | OuterNodes.Add(newNode); 81 | OuterSegments.Add(segment); 82 | } 83 | else 84 | { 85 | InnerNodes.Add(newNode); 86 | InnerSegments.Add(segment); 87 | DFS(newNode); 88 | } 89 | 90 | } 91 | } 92 | 93 | 94 | 95 | /*private bool IsInside(Vector3 vector) 96 | { 97 | if(isCircle) 98 | { 99 | if (VectorDistance(vector, m_startNodeVector) > (m_radiusMain/* + DISTANCE_PADDING*//*)) return false; 100 | } 101 | else 102 | { 103 | if (!Ellipse.IsInsideEllipse(vector)) return false; 104 | } 105 | 106 | return true; 107 | }*/ 108 | 109 | /* Utility */ 110 | 111 | public NetManager Manager 112 | { 113 | get { return Singleton.instance; } 114 | } 115 | public NetNode GetNode(ushort id) 116 | { 117 | return Manager.m_nodes.m_buffer[id]; 118 | } 119 | public NetSegment GetSegment(ushort id) 120 | { 121 | return Manager.m_segments.m_buffer[id]; 122 | } 123 | public bool isVisited(ushort node) 124 | { 125 | return visitedNodes.Contains(node); 126 | } 127 | //this method calculates distance between two vectors 128 | private double VectorDistance(Vector3 vector1, Vector3 vector2) 129 | { 130 | return Math.Sqrt(Math.Pow(vector1.x - vector2.x, 2) + Math.Pow(vector1.z - vector2.z, 2)); 131 | } 132 | // Returns node of 'segment' which is different from 'node' 133 | private ushort getOtherNode(ushort node, ushort segment) 134 | { 135 | ushort newNode = GetSegment(segment).m_startNode; 136 | if (node != newNode) return newNode; 137 | else return GetSegment(segment).m_endNode; 138 | } 139 | 140 | private bool segmentExists(ushort node) 141 | { 142 | return node != 0; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /RoundaboutBuilder/ModLoadingExtension.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.Plugins; 2 | using ColossalFramework.UI; 3 | using ICities; 4 | using RoundaboutBuilder.Tools; 5 | using RoundaboutBuilder.UI; 6 | using System; 7 | using System.Linq; 8 | using UnityEngine; 9 | 10 | /* By Strad, 01/2019 */ 11 | 12 | /* Version RELEASE 1.4.0+ */ 13 | 14 | /* Most of this is copied from Elektrix's Segment Slope Smoother. 15 | * The oter part is copied from somewhere as well, but unfortunately I don't remeber from where. */ 16 | 17 | namespace RoundaboutBuilder 18 | { 19 | public class ModLoadingExtension : ILoadingExtension 20 | { 21 | public static bool appModeGame = false; 22 | public static bool LevelLoaded = false; 23 | public static bool tmpeDetected = false; 24 | public static bool fineRoadToolDetected = false; 25 | public static bool networkAnarchyDetected = false; 26 | public static bool undoItDetected = false; 27 | 28 | public static readonly UInt64[] TMPE_IDs = { 583429740, 1637663252, 1806963141 }; 29 | public static readonly UInt64[] FINE_ROAD_ANARCHY_IDs = { 651322972, 1844442251 }; 30 | public static readonly UInt64[] NETWORK_ANARCHY_IDs = { 2862881785 }; 31 | public static readonly UInt64[] UNO_IT_IDs = { 1890830956 }; 32 | 33 | // called when level loading begins 34 | public void OnCreated(ILoading loading) 35 | { 36 | appModeGame = loading.currentMode == AppMode.Game; 37 | tmpeDetected = false; 38 | fineRoadToolDetected = false; 39 | networkAnarchyDetected = false; 40 | foreach (PluginManager.PluginInfo current in PluginManager.instance.GetPluginsInfo()) 41 | { 42 | //_string += current.name + " "; 43 | if (!tmpeDetected && current.isEnabled && (current.name.Contains("TrafficManager") || TMPE_IDs.Contains(current.publishedFileID.AsUInt64))) 44 | { 45 | tmpeDetected = true; 46 | //_string += "[TMPE Detected!]"; 47 | } 48 | else if (!networkAnarchyDetected && current.isEnabled && (current.name.Contains("NetworkAnarchy") || NETWORK_ANARCHY_IDs.Contains(current.publishedFileID.AsUInt64))) 49 | { 50 | networkAnarchyDetected = true; 51 | } 52 | else if (!fineRoadToolDetected && current.isEnabled && (current.name.Contains("FineRoadTool") || FINE_ROAD_ANARCHY_IDs.Contains(current.publishedFileID.AsUInt64))) 53 | { 54 | fineRoadToolDetected = true; 55 | } 56 | else if (!undoItDetected && current.isEnabled && (current.name.Contains("UndoMod") || UNO_IT_IDs.Contains(current.publishedFileID.AsUInt64))) 57 | { 58 | undoItDetected = true; 59 | } 60 | } 61 | 62 | //Ads.Destroy(); 63 | //Debug.Log(_string); 64 | } 65 | 66 | // called when level is loaded 67 | public void OnLevelLoaded(LoadMode mode) 68 | { 69 | 70 | //instatiate tools 71 | if (RoundaboutTool.Instance == null || EllipseTool.Instance == null) 72 | { 73 | ToolController toolController = GameObject.FindObjectOfType(); 74 | 75 | RoundaboutTool.Instance = toolController.gameObject.AddComponent(); 76 | RoundaboutTool.Instance.enabled = false; 77 | EllipseTool.Instance = toolController.gameObject.AddComponent(); 78 | EllipseTool.Instance.enabled = false; 79 | FreeCursorTool.Instance = toolController.gameObject.AddComponent(); 80 | FreeCursorTool.Instance.enabled = false; 81 | } 82 | 83 | //instatiate UI 84 | if (UIWindow.instance == null) 85 | { 86 | UIView.GetAView().AddUIComponent(typeof(UIWindow)); 87 | } 88 | 89 | //update msg 90 | /*if (ShowUpdateMsg()) 91 | { 92 | UIWindow2.instance.ThrowErrorMsg("Roundabout Builder now supports undo! Yaay! Moreover, building costs are now taken from your account.\n" + 93 | "Please report any bugs on the Steam Workshop page."); 94 | } 95 | RoundAboutBuilder.SeenUpdateMsg.value = true;*/ 96 | 97 | LevelLoaded = true; 98 | } 99 | 100 | /*private bool ShowUpdateMsg() 101 | { 102 | return !RoundAboutBuilder.SeenUpdateMsg && 103 | }*/ 104 | 105 | /*private void debug() 106 | { 107 | AppDomain currentDomain = AppDomain.CurrentDomain; 108 | //Make an array for the list of assemblies. 109 | Assembly[] assems = currentDomain.GetAssemblies(); 110 | 111 | //List the assemblies in the current application domain. 112 | Console.WriteLine("List of assemblies loaded in current appdomain:"); 113 | foreach (Assembly assem in assems) 114 | Debug.Log(assem.ToString()); 115 | }*/ 116 | 117 | // called when unloading begins 118 | public void OnLevelUnloading() 119 | { 120 | if (UIWindow.instance != null) 121 | { 122 | UIWindow.instance.enabled = false; 123 | } 124 | LevelLoaded = false; 125 | } 126 | 127 | // called when unloading finished 128 | public void OnReleased() 129 | { 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | /dependencies/TrafficManager.dll 263 | /dependencies/RoundaboutBuilder.dll 264 | /dependencies/FineRoadTool.dll 265 | /dependencies/CSUtil.Commons.dll 266 | /dependencies/0Harmony.dll 267 | -------------------------------------------------------------------------------- /RoundaboutBuilder/Tools/GlitchedRoadsCheck.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using System; 3 | using UnityEngine; 4 | 5 | /* By Strad, 01/2019 */ 6 | 7 | /* Version BETA 1.2.0 */ 8 | 9 | /* I put a lot of warnings there because very bad things happened when I ran earlier builds of this class, but now everything should be resolved and work alright. */ 10 | 11 | namespace RoundaboutBuilder.Tools 12 | { 13 | static class GlitchedRoadsCheck 14 | { 15 | private static int count = 0; 16 | private static int unhandledExceptions = 0; 17 | 18 | /* Inspired by Move It */ 19 | public static void RemoveGlitchedRoads() 20 | { 21 | if (!ModLoadingExtension.LevelLoaded) 22 | { 23 | ExceptionPanel notLoaded = UIView.library.ShowModal("ExceptionPanel"); 24 | notLoaded.SetMessage("Not In-Game", "Use this button when in-game to remove glitched roads", false); 25 | return; 26 | } 27 | 28 | ExceptionPanel panel = UIView.library.ShowModal("ExceptionPanel"); 29 | string message; 30 | count = 0; 31 | unhandledExceptions = 0; 32 | 33 | for (ushort segmentId = 0; segmentId < NetManager.instance.m_segments.m_buffer.Length; segmentId++) 34 | { 35 | NetSegment segment = NetManager.instance.m_segments.m_buffer[segmentId]; 36 | 37 | if ((segment.m_flags & NetSegment.Flags.Created) == NetSegment.Flags.None) continue; 38 | 39 | try 40 | { 41 | if(segment.Info.name == "Vehicle Connection") { 42 | if(TraverseVehicleConnections(segment.m_startNode, segmentId, segmentId, 0)) { 43 | Release(segmentId); 44 | } 45 | } 46 | 47 | if(segment.m_startNode == 0 || segment.m_endNode == 0) 48 | { 49 | Release(segmentId); 50 | continue; 51 | } 52 | 53 | try 54 | { 55 | NetNode node1 = NetManager.instance.m_nodes.m_buffer[segment.m_startNode]; 56 | NetNode node2 = NetManager.instance.m_nodes.m_buffer[segment.m_endNode]; 57 | 58 | if((node1.m_flags & NetNode.Flags.Created) == NetNode.Flags.None || (node2.m_flags & NetNode.Flags.Created) == NetNode.Flags.None) 59 | { 60 | Release(segmentId); 61 | continue; 62 | } 63 | 64 | // Teasing methods 65 | node1.CountSegments(); 66 | node2.CountSegments(); 67 | } 68 | catch(Exception e) 69 | { 70 | Debug.Log("Invalid node. Has thrown exception: "); 71 | Debug.Log(e); 72 | Release(segmentId); 73 | } 74 | 75 | 76 | } 77 | catch(Exception e) 78 | { 79 | unhandledExceptions++; 80 | Debug.LogError(e); 81 | } 82 | } 83 | if (unhandledExceptions > 0) 84 | { 85 | message = "ERROR: Unhandled exceptions: " + unhandledExceptions + ". Maybe something went VERY VERY wrong. You should reload last save. (Segments removed: " + count + ")"; 86 | } 87 | else if (count > 0) 88 | { 89 | message = $"Removed {count} segment{(count == 1 ? "" : "s")}! Please check that nothing unpleasant has happened to your game. Save it in a new file."; 90 | } 91 | else 92 | { 93 | message = "No glitched segments found."; 94 | } 95 | 96 | panel.SetMessage("Removing glitched segments", message, false); 97 | } 98 | 99 | private static bool TraverseVehicleConnections(ushort nodeId, ushort startSegmentId, ushort initialSegmentId, int recursionCounter) 100 | { 101 | recursionCounter++; 102 | if (recursionCounter > 12) 103 | return false; 104 | 105 | ushort savedSegId = 0; 106 | 107 | NetNode node = NetManager.instance.m_nodes.m_buffer[nodeId]; 108 | for(int i = 0; i < 8; i++) { 109 | var segId = node.GetSegment(i); 110 | if (segId == startSegmentId) 111 | continue; 112 | if(segId == initialSegmentId) 113 | return true; 114 | var seg = NetUtil.Segment(segId); 115 | if (seg.Info.name != "Vehicle Connection") 116 | continue; 117 | if (savedSegId != 0) 118 | return false; 119 | savedSegId = segId; 120 | } 121 | 122 | if(savedSegId != 0) { 123 | var seg = NetUtil.Segment(savedSegId); 124 | ushort newNodeId = seg.GetOtherNode(nodeId); 125 | if(TraverseVehicleConnections(newNodeId, savedSegId, initialSegmentId, recursionCounter)) { 126 | Release(savedSegId); 127 | return true; 128 | } else { 129 | return false; 130 | } 131 | } 132 | 133 | return false; 134 | } 135 | 136 | private static void Release(ushort segmentId) 137 | { 138 | NetManager.instance.ReleaseSegment(segmentId, true); 139 | count++; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /RoundaboutBuilder/GameActions/AbstractAction.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using RoundaboutBuilder; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using UnityEngine; 7 | 8 | namespace SharedEnvironment 9 | { 10 | public class ActionGroup : GameActionExtended 11 | { 12 | public List Actions = new List(); 13 | public ItemClass ItemClass { get; set; } 14 | 15 | public ActionGroup(string name, bool redoable=false) : base(name) 16 | { 17 | } 18 | 19 | protected override void DoImplementation() 20 | { 21 | ChargePlayer(); 22 | 23 | foreach(GameAction action in Actions) 24 | { 25 | try 26 | { 27 | action.Do(); 28 | } catch(Exception e) 29 | { 30 | Debug.LogError(e); 31 | } 32 | } 33 | } 34 | 35 | protected override void RedoImplementation() 36 | { 37 | ChargePlayer(); 38 | 39 | foreach (GameAction action in Actions) 40 | { 41 | try 42 | { 43 | action.Redo(); 44 | } 45 | catch (Exception e) 46 | { 47 | Debug.LogError(e); 48 | } 49 | } 50 | } 51 | 52 | protected override void UndoImplementation() 53 | { 54 | ReturnMoney(); 55 | 56 | for (int i = Actions.Count - 1; i >= 0; i--) 57 | { 58 | try 59 | { 60 | Actions[i].Undo(); 61 | } 62 | catch (Exception e) 63 | { 64 | Debug.LogError(e); 65 | } 66 | } 67 | } 68 | 69 | public override int DoCost() 70 | { 71 | return Actions.Aggregate(0, (acc, x) => acc += x.DoCost()); 72 | } 73 | 74 | public override int UndoCost() 75 | { 76 | return Actions.Aggregate(0, (acc, x) => acc += x.UndoCost()); 77 | } 78 | 79 | public void ChargePlayer() 80 | { 81 | int Amount = DoCost(); 82 | if (!RoundAboutBuilder.NeedMoney || !ModLoadingExtension.appModeGame) 83 | return; 84 | 85 | if (Amount <= 0) 86 | return; 87 | 88 | if (Singleton.instance.PeekResource(EconomyManager.Resource.Construction, Amount) != Amount) 89 | { 90 | throw new ActionException("Failed to charge player: not enough money",this); 91 | } 92 | 93 | Singleton.instance.FetchResource(EconomyManager.Resource.Construction, Amount, ItemClass); 94 | } 95 | 96 | public void ReturnMoney() 97 | { 98 | int Amount = UndoCost(); 99 | if (!RoundAboutBuilder.NeedMoney || !ModLoadingExtension.appModeGame) 100 | return; 101 | 102 | if (Amount <= 0) 103 | return; 104 | 105 | Singleton.instance.AddResource(EconomyManager.Resource.RefundAmount, Amount, ItemClass); 106 | } 107 | } 108 | 109 | public abstract class GameActionExtended : GameAction 110 | { 111 | public bool IsRedoable { get; protected set; } 112 | public bool IsDone { get; protected set; } 113 | public bool EverDone { get; protected set; } 114 | public int TimesDone { get; protected set; } 115 | 116 | public string Name { get; protected set; } 117 | 118 | public GameActionExtended(string name, bool redoable = false) 119 | { 120 | Name = name; 121 | IsRedoable = redoable; 122 | } 123 | 124 | public override void Do() 125 | { 126 | if (IsDone) 127 | throw new ActionException("Is already done",this); 128 | 129 | DoImplementation(); 130 | 131 | EverDone = true; 132 | IsDone = true; 133 | TimesDone++; 134 | } 135 | 136 | public override void Undo() 137 | { 138 | if (!IsDone) 139 | throw new ActionException("Is not done", this); 140 | 141 | UndoImplementation(); 142 | IsDone = false; 143 | } 144 | 145 | public override void Redo() 146 | { 147 | if (IsDone) 148 | throw new ActionException("Is already done", this); 149 | 150 | if (!EverDone) 151 | throw new ActionException("Was not ever done",this); 152 | 153 | if (!IsRedoable) 154 | throw new ActionException("Is not redoable", this); 155 | 156 | RedoImplementation(); 157 | 158 | IsDone = true; 159 | TimesDone++; 160 | } 161 | 162 | public override string ToString() 163 | { 164 | return Name + " " + base.ToString(); 165 | } 166 | 167 | protected abstract void DoImplementation(); 168 | protected abstract void UndoImplementation(); 169 | protected virtual void RedoImplementation() { } 170 | } 171 | 172 | public abstract class GameAction 173 | { 174 | public abstract void Do(); 175 | public abstract void Undo(); 176 | public abstract void Redo(); 177 | 178 | public virtual int DoCost() { return 0; } 179 | public virtual int UndoCost() { return 0; } 180 | } 181 | 182 | public class ActionException : Exception 183 | { 184 | public GameActionExtended Action { get; private set; } 185 | 186 | public ActionException(string description, GameActionExtended action) : base(ModifyMessage(description,action)) 187 | { 188 | this.Action = action; 189 | } 190 | 191 | private static string ModifyMessage(string description, GameActionExtended action) 192 | { 193 | return "Action " + action.Name + ": " + description; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /RoundaboutBuilder/RoundaboutBuilder.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {FA19FC4E-5FCB-4CF2-96F9-E09B5B9C9A50} 8 | Library 9 | Properties 10 | RoundaboutBuilder 11 | RoundaboutBuilder 12 | v3.5 13 | 512 14 | true 15 | 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | true 35 | false 36 | 37 | 38 | 39 | False 40 | ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\Assembly-CSharp.dll 41 | 42 | 43 | False 44 | ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\ColossalManaged.dll 45 | 46 | 47 | ..\..\CSUtil.Commons.dll 48 | 49 | 50 | ..\..\FineRoadTool.dll 51 | 52 | 53 | False 54 | ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\ICities.dll 55 | 56 | 57 | ..\..\Ionic.Zip.dll 58 | 59 | 60 | ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\workshop\content\255710\2862881785\NetworkAnarchy.dll 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ..\..\TrafficManager.dll 70 | 71 | 72 | C:\Program Files (x86)\Steam\steamapps\common\Cities_Skylines\Cities_Data\Managed\UnityEngine.dll 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" 115 | del "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)\$(TargetFileName)" 116 | xcopy /y "$(TargetPath)" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" 117 | 118 | -------------------------------------------------------------------------------- /RoundaboutBuilder/ToolBaseExtended.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.UI; 3 | using RoundaboutBuilder.Tools; 4 | using RoundaboutBuilder.UI; 5 | using System; 6 | using UnityEngine; 7 | 8 | /* By Strad, 01/2019 */ 9 | 10 | /* Version BETA 1.2.0 */ 11 | 12 | /* This method etends standard tool base. It adds UI environment, which can be called by the main UI class. The subclasses can override 13 | * the +/- button methods to register keyboard input. */ 14 | 15 | namespace RoundaboutBuilder 16 | { 17 | public abstract class ToolBaseExtended : ToolBase 18 | { 19 | protected bool insideUI; 20 | 21 | protected ushort m_hoverNode; 22 | public Vector3 HoverPosition { get; protected set; } 23 | 24 | protected override void OnToolUpdate() 25 | { 26 | base.OnToolUpdate(); 27 | 28 | /* This last part was more or less copied from Elektrix's Segment Slope Smoother. He takes the credit. 29 | * https://github.com/CosignCosine/CS-SegmentSlopeSmoother 30 | * https://steamcommunity.com/sharedfiles/filedetails/?id=1597198847 */ 31 | 32 | if (/*UIWindow2.instance.containsMouse || */(UIView.IsInsideUI() || !Cursor.visible)) 33 | { 34 | m_hoverNode = 0; 35 | insideUI = true; 36 | return; 37 | } 38 | 39 | insideUI = false; 40 | 41 | Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); 42 | RaycastInput input = new RaycastInput(ray, Camera.main.farClipPlane); 43 | 44 | input.m_ignoreNodeFlags = NetNode.Flags.None; 45 | input.m_ignoreSegmentFlags = NetSegment.Flags.None; 46 | input.m_ignoreParkFlags = DistrictPark.Flags.All; 47 | input.m_ignorePropFlags = PropInstance.Flags.All; 48 | input.m_ignoreTreeFlags = TreeInstance.Flags.All; 49 | input.m_ignoreCitizenFlags = CitizenInstance.Flags.All; 50 | input.m_ignoreVehicleFlags = Vehicle.Flags.Created; 51 | input.m_ignoreBuildingFlags = Building.Flags.All; 52 | input.m_ignoreDisasterFlags = DisasterData.Flags.All; 53 | input.m_ignoreTransportFlags = TransportLine.Flags.All; 54 | input.m_ignoreParkedVehicleFlags = VehicleParked.Flags.All; 55 | input.m_ignoreTerrain = false; 56 | 57 | RayCast(input, out RaycastOutput output); 58 | m_hoverNode = output.m_netNode; 59 | /*if(output.m_netNode == 0 && output.m_netSegment != 0) 60 | { 61 | ushort startNode = NetUtil.Segment(output.m_netSegment).m_startNode; 62 | ushort endNode = NetUtil.Segment(output.m_netSegment).m_endNode; 63 | if (VectorDistance(NetUtil.Node(startNode).m_position,output.m_hitPos) > VectorDistance(NetUtil.Node(endNode).m_position, output.m_hitPos)) 64 | { 65 | if((NetUtil.Node(endNode).m_flags & NetNode.Flags.Middle) == NetNode.Flags.None) 66 | { 67 | m_hoverNode = endNode; 68 | } 69 | } else { 70 | if ((NetUtil.Node(startNode).m_flags & NetNode.Flags.Middle) == NetNode.Flags.None) 71 | { 72 | m_hoverNode = startNode; 73 | } 74 | } 75 | }*/ 76 | HoverPosition = output.m_hitPos; 77 | 78 | if (Input.GetMouseButtonUp(0)) 79 | { 80 | OnClick(); 81 | } 82 | } 83 | 84 | protected override void OnDisable() 85 | { 86 | base.OnDisable(); 87 | if(UIWindow.instance != null) 88 | { 89 | if (UIWindow.instance.m_hoveringLabel != null) 90 | UIWindow.instance.m_hoveringLabel.isVisible = false; 91 | } 92 | ToolsModifierControl.SetTool(); // Thanks to Elektrix for pointing this out 93 | } 94 | 95 | public override void RenderOverlay(RenderManager.CameraInfo cameraInfo) 96 | { 97 | base.RenderOverlay(cameraInfo); 98 | if (enabled == true) 99 | { 100 | RenderOverlayExtended(cameraInfo); 101 | } else 102 | { 103 | try 104 | { 105 | UIWindow.instance.m_hoveringLabel.isVisible = false; 106 | } catch { } 107 | } 108 | } 109 | 110 | protected virtual void OnClick() 111 | { 112 | 113 | } 114 | 115 | protected virtual void RenderOverlayExtended(RenderManager.CameraInfo cameraInfo) 116 | { 117 | 118 | } 119 | 120 | public virtual void IncreaseButton() 121 | { 122 | 123 | } 124 | 125 | public virtual void DecreaseButton() 126 | { 127 | 128 | } 129 | 130 | public virtual void PgUpButton() 131 | { 132 | 133 | } 134 | 135 | public virtual void PgDnButton() 136 | { 137 | 138 | } 139 | 140 | public virtual void HomeButton() 141 | { 142 | 143 | } 144 | 145 | public virtual void GoToFirstStage() 146 | { 147 | 148 | } 149 | 150 | protected static bool GetFollowTerrain() 151 | { 152 | return UIWindow.SavedFollowTerrain.value; 153 | } 154 | 155 | protected static void RenderHoveringLabel(string text) 156 | { 157 | UIWindow.instance.m_hoveringLabel.absolutePosition = UIView.GetAView().ScreenPointToGUI(Input.mousePosition) + new Vector2(50, 30); 158 | UIWindow.instance.m_hoveringLabel.SetValue(text); 159 | UIWindow.instance.m_hoveringLabel.isVisible = true; 160 | } 161 | 162 | protected void RenderMousePositionCircle(RenderManager.CameraInfo cameraInfo) 163 | { 164 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.black, HoverPosition, 15f, HoverPosition.y - 1f, HoverPosition.y + 1f, true, true); 165 | } 166 | 167 | /*private static float VectorDistance(Vector3 v1, Vector3 v2) // Without Y coordinate 168 | { 169 | return (float)(Math.Sqrt(Math.Pow(v1.x - v2.x, 2) + Math.Pow(v1.z - v2.z, 2))); 170 | }*/ 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /RoundaboutBuilder/ModThreading.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ICities; 3 | using RoundaboutBuilder.Tools; 4 | using RoundaboutBuilder.UI; 5 | using SharedEnvironment; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Timers; 9 | using UnityEngine; 10 | 11 | /* By Strad, 01/2019 */ 12 | 13 | /* Version RELEASE 1.4.0+ */ 14 | 15 | /* This was shamelessly stolen from bomormer's tutorial on Simtropolis. He takes the credit. 16 | * https://community.simtropolis.com/forums/topic/73490-modding-tutorial-3-show-limits/ */ 17 | 18 | namespace RoundaboutBuilder 19 | { 20 | public class ModThreading : ThreadingExtensionBase 21 | { 22 | private bool _processed = false; 23 | 24 | public static ModThreading instance; 25 | 26 | public ModThreading() 27 | { 28 | instance = this; 29 | } 30 | 31 | public override void OnUpdate(float realTimeDelta, float simulationTimeDelta) 32 | { 33 | if (RoundAboutBuilder.ModShortcut.IsPressed()) 34 | { 35 | // cancel if they key input was already processed in a previous frame 36 | 37 | if (_processed) 38 | return; 39 | 40 | _processed = true; 41 | 42 | if (UIWindow.instance == null) return; 43 | 44 | //Activating/deactivating tool & UI 45 | UIWindow.instance.Toggle(); 46 | } 47 | else if(UIWindow.instance.enabled) 48 | { 49 | _processed = KeysPressed(); 50 | } else 51 | { 52 | _processed = false; 53 | } 54 | 55 | // Mouse wheel 56 | 57 | /*if (Input.GetAxis("Mouse ScrollWheel") > 0f) // forward 58 | { 59 | UIWindow2.instance.toolOnUI?.IncreaseButton(); 60 | } 61 | else if (Input.GetAxis("Mouse ScrollWheel") < 0f) // backwards 62 | { 63 | UIWindow2.instance.toolOnUI?.DecreaseButton(); 64 | }*/ 65 | 66 | /* Check for UI panel button */ 67 | if (UIWindow.instance && !UIPanelButton.Instance && RoundAboutBuilder.ShowUIButton.value) // If UIPanel has been already initialized && button missing 68 | { 69 | UIPanelButton.CreateButton(); 70 | } 71 | 72 | /* Delayed setup */ 73 | if(timeUp) 74 | { 75 | timeUp = false; 76 | 77 | try { m_TMPEaction.Do(); } 78 | catch(Exception e) 79 | { 80 | Debug.LogWarning(e); 81 | } 82 | 83 | } 84 | } 85 | 86 | private bool KeysPressed() 87 | { 88 | if (Input.GetKey("[+]") || (RoundAboutBuilder.UseExtraKeys.value && RoundAboutBuilder.IncreaseShortcut.IsPressed())) 89 | { 90 | if (_processed) 91 | return true; 92 | UIWindow.instance.toolOnUI?.IncreaseButton(); 93 | return true; 94 | } 95 | 96 | if (Input.GetKey("[-]") || (RoundAboutBuilder.UseExtraKeys.value && RoundAboutBuilder.DecreaseShortcut.IsPressed())) 97 | { 98 | if (_processed) 99 | return true; 100 | UIWindow.instance.toolOnUI?.DecreaseButton(); 101 | return true; 102 | } 103 | 104 | // Undo last action 105 | if (UIWindow.instance.toolOnUI != null && UIWindow.instance.toolOnUI.enabled && 106 | (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && Input.GetKey(KeyCode.Z)) 107 | { 108 | if (_processed) 109 | return true; 110 | UndoAction(); 111 | return true; 112 | } 113 | 114 | if (Input.GetKey(KeyCode.PageUp)) 115 | { 116 | if (_processed) 117 | return true; 118 | UIWindow.instance.toolOnUI?.PgUpButton(); 119 | return true; 120 | } 121 | if (Input.GetKey(KeyCode.PageDown)) 122 | { 123 | if (_processed) 124 | return true; 125 | UIWindow.instance.toolOnUI?.PgDnButton(); 126 | return true; 127 | } 128 | if (Input.GetKey(KeyCode.Home)) 129 | { 130 | if (_processed) 131 | return true; 132 | UIWindow.instance.toolOnUI?.HomeButton(); 133 | return true; 134 | } 135 | return false; 136 | } 137 | 138 | public static void PushAction(GameAction actionRoads, GameAction actionTMPE) 139 | { 140 | m_lastAction = actionRoads; 141 | UIWindow.instance.undoButton.isEnabled = true; 142 | Singleton.instance.AddAction(() => { 143 | actionRoads.Do(); 144 | }); 145 | TimerTMPE(actionTMPE); 146 | } 147 | 148 | public static void UndoAction() 149 | { 150 | if(m_lastAction != null) 151 | { 152 | UIWindow.instance.undoButton.isEnabled = false; 153 | Singleton.instance.AddAction(() => { 154 | m_lastAction.Undo(); 155 | m_lastAction = null; 156 | }); 157 | } 158 | } 159 | 160 | private static GameAction m_TMPEaction; 161 | private static GameAction m_lastAction; 162 | 163 | private static Timer aTimer; 164 | private static bool timeUp = false; 165 | private static void TimerTMPE(GameAction action) 166 | { 167 | m_TMPEaction = action; 168 | aTimer = new System.Timers.Timer(); 169 | aTimer.Interval = 500; 170 | 171 | // Hook up the Elapsed event for the timer. 172 | aTimer.Elapsed += (a,b) => 173 | { 174 | timeUp = true; 175 | aTimer.Stop(); 176 | aTimer.Dispose(); 177 | }; 178 | 179 | // Have the timer fire repeated events (true is the default) 180 | aTimer.AutoReset = false; 181 | 182 | // Start the timer 183 | aTimer.Enabled = true; 184 | } 185 | 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /RoundaboutBuilder/NetWrappers/WrappedSegment.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using RoundaboutBuilder.Tools; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using UnityEngine; 8 | 9 | namespace SharedEnvironment 10 | { 11 | public class WrappedSegment : AbstractNetWrapper 12 | { 13 | private WrappedNode _startNode; 14 | public WrappedNode StartNode 15 | { 16 | get => _startNode; 17 | set => _startNode = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : value; 18 | } 19 | 20 | private WrappedNode _endNode; 21 | public WrappedNode EndNode 22 | { 23 | get => _endNode; 24 | set => _endNode = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : value; 25 | } 26 | 27 | private Vector3 _startDir; 28 | public Vector3 StartDirection 29 | { 30 | get => IsCreated() ? NetUtil.Segment(_id).m_startDirection : _startDir; 31 | set => _startDir = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : value; 32 | } 33 | 34 | private Vector3 _endDir; 35 | public Vector3 EndDirection 36 | { 37 | get => IsCreated() ? NetUtil.Segment(_id).m_endDirection : _endDir; 38 | set => _endDir = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : value; 39 | } 40 | 41 | private bool _invert; 42 | public bool Invert 43 | { 44 | get => IsCreated() ? ((NetUtil.Segment(_id).m_flags & NetSegment.Flags.Invert) != NetSegment.Flags.None) : _invert; 45 | set => _invert = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : value; 46 | } 47 | 48 | // confusion intensifies 49 | private bool _switchStartAndEnd; 50 | public bool SwitchStartAndEnd 51 | { 52 | get => _switchStartAndEnd; 53 | set => _switchStartAndEnd = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : value; 54 | } 55 | 56 | private bool _deployPlacementEffects; 57 | public bool DeployPlacementEffects 58 | { 59 | get => _deployPlacementEffects; 60 | set => _deployPlacementEffects = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : value; 61 | } 62 | 63 | private ushort _netInfoIndex; 64 | public NetInfo NetInfo 65 | { 66 | get => IsCreated() ? NetUtil.Segment(_id).Info : NetUtil.NetinfoFromIndex(_netInfoIndex); 67 | set => _netInfoIndex = IsCreated() ? throw new NetWrapperException("Cannot modify built segment") : NetUtil.NetinfoToIndex(value); 68 | } 69 | 70 | public ref NetSegment Get 71 | { 72 | get => ref NetUtil.Segment(Id); 73 | } 74 | 75 | // methods 76 | 77 | public override void Create() 78 | { 79 | if (!IsCreated()) 80 | { 81 | _id = NetUtil.CreateSegment(_startNode.Id, _endNode.Id, _startDir, _endDir, NetUtil.NetinfoFromIndex(_netInfoIndex), _invert, _switchStartAndEnd, _deployPlacementEffects); 82 | } 83 | } 84 | 85 | public override bool Release() 86 | { 87 | if (IsCreated()) 88 | { 89 | _startDir = NetUtil.Segment(_id).m_startDirection; 90 | _endDir = NetUtil.Segment(_id).m_endDirection; 91 | _netInfoIndex = NetUtil.Segment(_id).m_infoIndex; 92 | _invert = (NetUtil.Segment(_id).m_flags & NetSegment.Flags.Invert) != NetSegment.Flags.None; 93 | 94 | NetUtil.ReleaseSegment(_id); 95 | if (!NetUtil.ExistsSegment(_id)) 96 | { 97 | _id = 0; 98 | return true; 99 | } 100 | return false; 101 | } 102 | return true; // ?? true or false 103 | } 104 | 105 | public int ComputeConstructionCost() 106 | { 107 | // See public static ToolBase.ToolErrors NetTool.CreateNode(NetInfo info, NetTool.ControlPoint startPoint, NetTool.ControlPoint middlePoint, NetTool.ControlPoint endPoint, FastList nodeBuffer, int maxSegments, bool test, bool testEnds, bool visualize, bool autoFix, bool needMoney, bool invert, bool switchDir, ushort relocateBuildingID, out ushort firstNode, out ushort lastNode, out ushort segment, out int cost, out int productionRate) 108 | float elevation1, elevation2; 109 | if (StartNode.IsCreated() && EndNode.IsCreated()) 110 | { 111 | // destruction 112 | elevation1 = StartNode.Get.m_elevation; 113 | elevation2 = EndNode.Get.m_elevation; 114 | } 115 | else 116 | { 117 | // construction 118 | elevation1 = NetUtil.GetElevation(StartNode.Position, StartNode.NetInfo.m_netAI); 119 | elevation2 = NetUtil.GetElevation(EndNode.Position, EndNode.NetInfo.m_netAI); 120 | } 121 | return NetInfo.m_netAI.GetConstructionCost(StartNode.Position, EndNode.Position, elevation1, elevation2); 122 | } 123 | 124 | public override int DoCost() 125 | { 126 | return _isBuildAction ? ComputeConstructionCost() : - ComputeConstructionCost() * 3 / 4; 127 | } 128 | 129 | public override int UndoCost() 130 | { 131 | return _isBuildAction ? - ComputeConstructionCost() : ComputeConstructionCost() * 3 / 4; 132 | } 133 | 134 | // Constructors 135 | 136 | public WrappedSegment() { } 137 | 138 | public WrappedSegment(WrappedNode startNode, WrappedNode endNode, ushort id) 139 | { 140 | if (id != 0 && !NetUtil.ExistsSegment(id)) 141 | { 142 | throw new NetWrapperException("Cannot wrap nonexisting segment"); 143 | } 144 | 145 | if(NetUtil.Segment(id).m_startNode != startNode.Id || NetUtil.Segment(id).m_endNode != endNode.Id) 146 | { 147 | throw new NetWrapperException("Cannot wrap segment - Nodes do not match"); 148 | } 149 | 150 | _startNode = startNode; 151 | _endNode = endNode; 152 | 153 | _switchStartAndEnd = false; 154 | _deployPlacementEffects = false; 155 | 156 | _id = id; 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /RoundaboutBuilder/FreeCursorTool.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.UI; 3 | using RoundaboutBuilder.Tools; 4 | using RoundaboutBuilder.UI; 5 | using SharedEnvironment; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Runtime.CompilerServices; 10 | using System.Text; 11 | using UnityEngine; 12 | 13 | /* By Strad, 06/19 */ 14 | 15 | namespace RoundaboutBuilder 16 | { 17 | public class FreeCursorTool : ToolBaseExtended 18 | { 19 | public static FreeCursorTool Instance; 20 | 21 | public bool AbsoluteElevation { get; set; } 22 | 23 | public void CreateRoundabout() 24 | { 25 | float? radiusQ = UIWindow.instance.P_FreeToolPanel.RadiusField.Value; 26 | if (radiusQ == null) 27 | { 28 | UIWindow.instance.ThrowErrorMsg("Radius out of bounds!"); 29 | return; 30 | } 31 | float? elevationFieldQ = UIWindow.instance.P_FreeToolPanel.ElevationField.Value; 32 | if (elevationFieldQ == null) 33 | { 34 | UIWindow.instance.ThrowErrorMsg("Elevation out of bounds!"); 35 | return; 36 | } 37 | 38 | float radius = (float)radiusQ; 39 | float elevation = (float)elevationFieldQ; 40 | 41 | m_hoverNode = 0; 42 | UIWindow.instance.LostFocus(); 43 | 44 | Vector3 vector = HoverPosition; 45 | if (AbsoluteElevation) 46 | { 47 | vector.y = elevation; 48 | } 49 | else 50 | { 51 | vector.y = vector.y + elevation; 52 | } 53 | 54 | if(vector.y < 0 || vector.y > 1000) 55 | { 56 | UIWindow.instance.ThrowErrorMsg("Elevation out of bounds!"); 57 | return; 58 | } 59 | 60 | /* These lines of code do all the work. See documentation in respective classes. */ 61 | /* When the old snapping algorithm is enabled, we create secondary (bigger) ellipse, so the newly connected roads obtained by the 62 | * graph traveller are not too short. They will be at least as long as the padding. */ 63 | Ellipse ellipse = new Ellipse(vector, new Vector3(0f, 0f, 0f), radius, radius); 64 | bool reverseDirection = (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && RoundAboutBuilder.CtrlToReverseDirection.value; 65 | try 66 | { 67 | FinalConnector finalConnector = new FinalConnector(UI.UIWindow.instance.dropDown.Value, null, ellipse, false, elevation == 0 ? GetFollowTerrain() : false,reverseDirection); 68 | finalConnector.Build(); 69 | // Easter egg 70 | RoundAboutBuilder.EasterEggToggle(); 71 | } 72 | catch (ActionException e) 73 | { 74 | UIWindow.instance.ThrowErrorMsg(e.Message); 75 | } 76 | catch (Exception e) 77 | { 78 | Debug.LogError(e); 79 | UIWindow.instance.ThrowErrorMsg(e.ToString(), true); 80 | } 81 | } 82 | 83 | protected override void OnClick() 84 | { 85 | base.OnClick(); 86 | CreateRoundabout(); 87 | } 88 | 89 | protected override void RenderOverlayExtended(RenderManager.CameraInfo cameraInfo) 90 | { 91 | if (UIView.IsInsideUI() || !Cursor.visible) return; 92 | 93 | try 94 | { 95 | float? radiusQ = UIWindow.instance.P_FreeToolPanel.RadiusField.Value; 96 | float? elevationFieldQ = UIWindow.instance.P_FreeToolPanel.ElevationField.Value; 97 | if (radiusQ == null || elevationFieldQ == null) 98 | { 99 | return; 100 | } 101 | 102 | float radius = (float)radiusQ; 103 | float elevation = (float)elevationFieldQ; 104 | 105 | Vector3 vector2 = HoverPosition; 106 | if (AbsoluteElevation) 107 | { 108 | vector2.y = elevation; 109 | } 110 | else 111 | { 112 | vector2.y = vector2.y + elevation; 113 | } 114 | 115 | float roadWidth = UIWindow.instance.dropDown.Value.m_halfWidth; // There is a slight chance that this will throw an exception 116 | float innerCirleRadius = radius - roadWidth > 0 ? 2 * ((float)radius - roadWidth) : 2 * (float)radius; 117 | 118 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.red, vector2, innerCirleRadius, vector2.y - 2f, vector2.y + 2f, true, true); 119 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.red, vector2, 2 * ((float)radius + roadWidth /*DISTANCE_PADDING - 5*/), vector2.y - 1f, vector2.y + 1f, true, true); 120 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.black, vector2, 15f, vector2.y - 1f, vector2.y + 1f, true, true); 121 | //RenderDirectionVectors(cameraInfo); 122 | 123 | float terrainHeight = HoverPosition.y; 124 | 125 | // If the preview is not on the ground, we create a shadow 126 | if ((AbsoluteElevation && Mathf.Abs(terrainHeight - elevation) > 0.01f) || (!AbsoluteElevation && Mathf.Abs(elevation) > 0.01f)) 127 | { 128 | Vector3 vector = HoverPosition; 129 | vector.y = terrainHeight; 130 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.black, vector, 15f, vector.y - 2f, vector.y + 2f, true, true); 131 | 132 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.black, vector, 2 * ((float)radius + roadWidth /*DISTANCE_PADDING - 5*/), vector.y - 1f, vector.y + 1f, true, true); 133 | } 134 | } 135 | catch (Exception e) 136 | { 137 | Debug.LogError(e); 138 | } 139 | } 140 | 141 | public override void IncreaseButton() 142 | { 143 | UIWindow.instance.P_FreeToolPanel.RadiusField.Increase(); 144 | } 145 | 146 | public override void DecreaseButton() 147 | { 148 | UIWindow.instance.P_FreeToolPanel.RadiusField.Decrease(); 149 | } 150 | 151 | public override void PgUpButton() 152 | { 153 | if (enabled) 154 | { 155 | UIWindow.instance.P_FreeToolPanel.ElevationField.Increment = NetUtil.GetElevationStep(); 156 | UIWindow.instance.P_FreeToolPanel.ElevationField.Increase(); 157 | } 158 | } 159 | 160 | public override void PgDnButton() 161 | { 162 | if (enabled) 163 | { 164 | UIWindow.instance.P_FreeToolPanel.ElevationField.Increment = NetUtil.GetElevationStep(); 165 | UIWindow.instance.P_FreeToolPanel.ElevationField.Decrease(); 166 | } 167 | } 168 | 169 | public override void HomeButton() 170 | { 171 | if (enabled) 172 | { 173 | UIWindow.instance.P_FreeToolPanel.RefreshElevation(); 174 | if (AbsoluteElevation) 175 | { 176 | float h = HoverPosition.y; 177 | UIWindow.instance.P_FreeToolPanel.ElevationField.Value = 178 | (int)h; 179 | } 180 | else 181 | { 182 | UIWindow.instance.P_FreeToolPanel.ElevationField.Reset(); 183 | } 184 | } 185 | } 186 | 187 | 188 | 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /RoundaboutBuilder/Tools/Ellipse.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.Math; 2 | using System; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | /* By Strad, 01/2019 */ 7 | 8 | /* Version RELEASE 1.4.0+ */ 9 | 10 | namespace RoundaboutBuilder.Tools 11 | { 12 | public class Ellipse 13 | { 14 | /* Y-axis squished to zero */ 15 | /* The ellipse will be drawn using #DENSITY segments. Four should be enough. 16 | * From ellipse geometry should be multiple of four, so that the points hit the four main control points. */ 17 | public static readonly int DENSITY = 8; 18 | 19 | public float RadiusMain { get; private set; } 20 | public float RadiusMinor { get; private set; } 21 | 22 | public Vector3 Center { get; private set; } 23 | public Vector3 Focal1 { get; private set; } 24 | public Vector3 Focal2 { get; private set; } 25 | 26 | public List Beziers { get; private set; } 27 | 28 | public double Ratio { get; private set; } 29 | public double EllipseRotation { get; private set; } 30 | 31 | private Vector3 mainAxisDirection; 32 | 33 | 34 | public Ellipse(Vector3 center, Vector3 mainAxisDirection, float radiusMain, float radiusMinor) 35 | { 36 | Center = center; 37 | RadiusMain = radiusMain; 38 | RadiusMinor = radiusMinor; 39 | this.mainAxisDirection = mainAxisDirection; 40 | Ratio = ((double)RadiusMinor) / ((double)RadiusMain); 41 | EllipseRotation = VectorsAngle(mainAxisDirection); 42 | CalculateFocals(); 43 | calculateBeziers(); 44 | } 45 | 46 | public bool IsInsideEllipse(Vector3 vector) 47 | { 48 | if (VectorDistance(vector, Focal1) + VectorDistance(vector, Focal2) > 2 * RadiusMain ) return false; 49 | else return true; 50 | } 51 | 52 | public bool IsCircle() 53 | { 54 | return RadiusMain - RadiusMinor < 0.0001f; 55 | } 56 | 57 | /* Well, the vector density is uniform on the circle, but not on the ellipse. But whatever... */ 58 | private void calculateBeziers() 59 | { 60 | if (DENSITY < 4) Beziers = null; 61 | List vectors = new List(); 62 | List tangents = new List(); 63 | List beziers = new List(); 64 | 65 | double angle = (2 * Math.PI) / DENSITY; 66 | 67 | for( int i = 0; i < DENSITY; i++) 68 | { 69 | vectors.Add( VectorAtAngle(angle * i) ); 70 | tangents.Add( TangentAtAngle(angle * i) ); 71 | } 72 | 73 | for (int i = 0; i < DENSITY; i++) 74 | { 75 | Bezier3 bezier = new Bezier3(); 76 | bezier.a = vectors[i]; 77 | bezier.d = vectors[(i + 1) % DENSITY]; // line below: false false or something else? 78 | NetSegment.CalculateMiddlePoints(bezier.a, tangents[i], bezier.d, -1 * tangents[(i + 1) % DENSITY], false, false, out bezier.b, out bezier.c); 79 | beziers.Add(Bezier2.XZ(bezier)); 80 | } 81 | 82 | Beziers = beziers; 83 | } 84 | 85 | /* What is an absolute angle? It is an angle between a point on the ellipse in the coordinate system of the (moved!) ellipse and X axis in standard coords. 86 | * Draw yourself a picture. ;) */ 87 | 88 | public double RadiusAtAbsoluteAngle(double angle) 89 | { 90 | angle = angle - EllipseRotation; 91 | //Debug.Log(string.Format("Angle,er,newAngle {0} {1} {2}",angle+ EllipseRotation, EllipseRotation,angle)); 92 | return RadiusAtAngle(angle); 93 | } 94 | 95 | /* Standard angle - we subtract the ellipse rotation */ 96 | 97 | public double RadiusAtAngle(double angle) 98 | { 99 | return (RadiusMinor * RadiusMain) / Math.Sqrt(Math.Pow(RadiusMain * Math.Sin(angle), 2) + Math.Pow(RadiusMinor * Math.Cos(angle), 2)); 100 | } 101 | 102 | /* Warning - this method works in the context I used it in the code, but didn't work for something else I had on my mind. I had to redo 103 | * it using other procedure. Maybe I am just dumb and can't calculate the math. */ 104 | public Vector3 VectorAtAbsoluteAngle(double angle) 105 | { 106 | return VectorAtAngle(angle - EllipseRotation); 107 | } 108 | 109 | public Vector3 TangentAtAbsoluteAngle(double angle) 110 | { 111 | return TangentAtAngle(angle - EllipseRotation); 112 | } 113 | 114 | public Vector3 VectorAtAngle(double angle) 115 | { 116 | Vector3 initialVector = new Vector3(RadiusMain, 0, 0); 117 | Vector3 rotSqVec = SquishVector(RotateVector(initialVector, angle), (float)Ratio); 118 | return Center + RotateVector(rotSqVec, EllipseRotation); 119 | } 120 | 121 | public Vector3 TangentAtAngle(double angle) 122 | { 123 | Vector3 initialTangent = new Vector3(0, 0, 1); 124 | Vector3 rotSqTan = SquishVector(RotateVector(initialTangent, angle), (float)Ratio); 125 | return RotateVector(rotSqTan, EllipseRotation); 126 | } 127 | 128 | /* How to we create an ellipse? We stomp on a circle ;) */ 129 | public static Vector3 SquishVector(Vector3 vector, float Ratio) 130 | { 131 | return new Vector3(vector.x,vector.y,vector.z*Ratio); 132 | } 133 | 134 | private void CalculateFocals() 135 | { 136 | float linearEccentricity = (float)(Math.Sqrt(RadiusMain * RadiusMain - RadiusMinor * RadiusMinor)); 137 | mainAxisDirection.Normalize(); 138 | Focal1 = Center + linearEccentricity * mainAxisDirection; 139 | Focal2 = Center - linearEccentricity * mainAxisDirection; 140 | } 141 | 142 | /* Utility */ 143 | 144 | public static double VectorDistance(Vector3 vector1, Vector3 vector2) 145 | { 146 | return Math.Sqrt(Math.Pow(vector1.x - vector2.x, 2) + Math.Pow(vector1.z - vector2.z, 2)); 147 | } 148 | 149 | public static Vector3 RotateVector(Vector3 origPoint, double angle) 150 | { 151 | return RotateVector(origPoint, angle, new Vector3(0, 0, 0)); 152 | } 153 | public static Vector3 RotateVector(Vector3 origPoint, double angle, Vector3 pivot) 154 | { 155 | Vector3 difference = origPoint - pivot; 156 | 157 | double sin = Math.Sin(angle); 158 | double cos = Math.Cos(angle); 159 | 160 | Vector3 newPoint = new Vector3((float)(difference.x * cos - difference.z * sin + pivot.x), pivot.y, (float)(difference.x * sin + difference.z * cos + pivot.z)); 161 | 162 | return newPoint; 163 | } 164 | public static double VectorsAngle(Vector3 vector1) 165 | { 166 | return Math.Atan2( vector1.z, vector1.x ); 167 | } 168 | public static double VectorsAngle(Vector3 vector1, Vector3 vector2) 169 | { 170 | return VectorsAngle(vector1, vector2, new Vector3(0,0,0)); 171 | } 172 | public static double VectorsAngle(Vector3 vector1, Vector3 vector2, Vector3 pivot) 173 | { 174 | if (VectorDistance(vector1,vector2) < 0.01f) return (2 * Math.PI); 175 | Vector3 vec1 = vector1 - pivot; 176 | Vector3 vec2 = vector2 - pivot; 177 | float dot = vec1.x * vec2.x + vec1.z * vec2.z; 178 | float det = vec1.x * vec2.z - vec1.z * vec2.x; 179 | double angle = Math.Atan2(det, dot); 180 | if (angle > 0) return angle; 181 | 182 | return (2 * Math.PI + angle); 183 | } 184 | 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /RoundaboutBuilder/UI/UIPanelButton.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using System; 3 | using UnityEngine; 4 | 5 | /* A lot of copy-pasting from Crossings mod by Spectra. The sprites are partly copied as well. */ 6 | 7 | namespace RoundaboutBuilder.UI 8 | { 9 | public class UIPanelButton : UIButton 10 | { 11 | public static UIPanelButton Instance; 12 | 13 | private const float BUTTON_HORIZONTAL_POSITION = 23; 14 | 15 | public UIPanelButton() 16 | { 17 | Instance = this; 18 | } 19 | 20 | public override void Start() 21 | { 22 | var roadsOptionPanel = UIUtils.Instance.FindComponent("RoadsOptionPanel", null, UIUtils.FindOptions.NameContains); 23 | var builtinTabstrip = UIUtils.Instance.FindComponent("ToolMode", roadsOptionPanel, UIUtils.FindOptions.None); 24 | 25 | UIButton uibutton = (UIButton)builtinTabstrip.tabs[0]; 26 | 27 | int num = 31; 28 | int num2 = 31; 29 | string[] spriteNames = new string[] 30 | { 31 | "RoundaboutButtonBg", 32 | "RoundaboutButtonBgPressed", 33 | "RoundaboutButtonBgHovered", 34 | "RoundaboutIcon", 35 | "RoundaboutIconPressed" 36 | }; 37 | if(ResourceLoader.GetAtlas("RoundaboutUI") == UIView.GetAView().defaultAtlas) 38 | { 39 | atlas = ResourceLoader.CreateTextureAtlas("sprites.png", "RoundaboutUI", uibutton.atlas.material, num, num2, spriteNames); 40 | } 41 | else 42 | { 43 | atlas = ResourceLoader.GetAtlas("RoundaboutUI"); 44 | } 45 | 46 | name = "RoundaboutButton"; 47 | size = new Vector2((float)num, (float)num2); 48 | normalBgSprite = "RoundaboutButtonBg"; 49 | disabledBgSprite = "RoundaboutButtonBg"; 50 | hoveredBgSprite = "RoundaboutButtonBgHovered"; 51 | pressedBgSprite = "RoundaboutButtonBgPressed"; 52 | //focusedBgSprite = "RoundaboutButtonBgPressed"; 53 | focusedBgSprite = "RoundaboutButtonBg"; 54 | playAudioEvents = true; 55 | tooltip = "Roundabout Builder (CTRL+O)"; 56 | normalFgSprite = (disabledFgSprite = (hoveredFgSprite = (focusedFgSprite = "RoundaboutIcon"))); 57 | pressedFgSprite = "RoundaboutIconPressed"; 58 | relativePosition = new Vector3(BUTTON_HORIZONTAL_POSITION, 38f); //94,38 59 | size = new Vector2((float)num, (float)num2); 60 | } 61 | 62 | protected override void OnClick(UIMouseEventParameter p) 63 | { 64 | base.OnClick(p); 65 | if (UIWindow.instance == null) 66 | return; 67 | 68 | UIWindow.instance.Toggle(); 69 | } 70 | 71 | /*public void FocusSprites() 72 | { 73 | Debug.Log("focus icon"); 74 | focusedBgSprite = "RoundaboutButtonBgPressed"; 75 | normalBgSprite = "RoundaboutButtonBgPressed"; 76 | normalFgSprite = (disabledFgSprite = (hoveredFgSprite = "RoundaboutIconPressed")); 77 | focusedFgSprite = "RoundaboutIconPressed"; 78 | size = new Vector2(33, 33); 79 | size = new Vector2(32, 32); 80 | } 81 | 82 | public void UnfocusSprites() 83 | { 84 | Debug.Log("unfocus icon"); 85 | focusedBgSprite = "RoundaboutButtonBg"; 86 | normalBgSprite = "RoundaboutButtonBg"; 87 | normalFgSprite = (disabledFgSprite = (hoveredFgSprite = "RoundaboutIcon")); 88 | focusedFgSprite = "RoundaboutIcon"; 89 | size = new Vector2(33, 33); 90 | size = new Vector2(32, 32); 91 | }*/ 92 | 93 | public static void CreateButton() 94 | { 95 | var roadsOptionPanel = UIUtils.Instance.FindComponent("RoadsOptionPanel", null, UIUtils.FindOptions.NameContains); 96 | if(roadsOptionPanel && !Instance) 97 | { 98 | try 99 | { 100 | roadsOptionPanel.AddUIComponent(); 101 | } catch 102 | { 103 | Debug.LogWarning("Failed to create UIPanelButton!"); 104 | } 105 | } 106 | } 107 | } 108 | 109 | 110 | public class UIUtils 111 | { 112 | // Token: 0x17000006 RID: 6 113 | // (get) Token: 0x06000038 RID: 56 RVA: 0x00004CF0 File Offset: 0x00002EF0 114 | public static UIUtils Instance 115 | { 116 | get 117 | { 118 | bool flag = UIUtils.instance == null; 119 | if (flag) 120 | { 121 | UIUtils.instance = new UIUtils(); 122 | } 123 | return UIUtils.instance; 124 | } 125 | } 126 | 127 | // Token: 0x06000039 RID: 57 RVA: 0x00004D20 File Offset: 0x00002F20 128 | private void FindUIRoot() 129 | { 130 | this.uiRoot = null; 131 | foreach (UIView uiview in UnityEngine.Object.FindObjectsOfType()) 132 | { 133 | bool flag = uiview.transform.parent == null && uiview.name == "UIView"; 134 | if (flag) 135 | { 136 | this.uiRoot = uiview; 137 | break; 138 | } 139 | } 140 | } 141 | 142 | // Token: 0x0600003A RID: 58 RVA: 0x00004D84 File Offset: 0x00002F84 143 | public string GetTransformPath(Transform transform) 144 | { 145 | string text = transform.name; 146 | Transform parent = transform.parent; 147 | while (parent != null) 148 | { 149 | text = parent.name + "/" + text; 150 | parent = parent.parent; 151 | } 152 | return text; 153 | } 154 | 155 | // Token: 0x0600003B RID: 59 RVA: 0x00004DD0 File Offset: 0x00002FD0 156 | public T FindComponent(string name, UIComponent parent = null, UIUtils.FindOptions options = UIUtils.FindOptions.None) where T : UIComponent 157 | { 158 | bool flag = this.uiRoot == null; 159 | if (flag) 160 | { 161 | this.FindUIRoot(); 162 | bool flag2 = this.uiRoot == null; 163 | if (flag2) 164 | { 165 | return default(T); 166 | } 167 | } 168 | foreach (T t in UnityEngine.Object.FindObjectsOfType()) 169 | { 170 | bool flag3 = (options & UIUtils.FindOptions.NameContains) > UIUtils.FindOptions.None; 171 | bool flag4; 172 | if (flag3) 173 | { 174 | flag4 = t.name.Contains(name); 175 | } 176 | else 177 | { 178 | flag4 = (t.name == name); 179 | } 180 | bool flag5 = !flag4; 181 | if (!flag5) 182 | { 183 | bool flag6 = parent != null; 184 | Transform transform; 185 | if (flag6) 186 | { 187 | transform = parent.transform; 188 | } 189 | else 190 | { 191 | transform = this.uiRoot.transform; 192 | } 193 | Transform parent2 = t.transform.parent; 194 | while (parent2 != null && parent2 != transform) 195 | { 196 | parent2 = parent2.parent; 197 | } 198 | bool flag7 = parent2 == null; 199 | if (!flag7) 200 | { 201 | return t; 202 | } 203 | } 204 | } 205 | return default(T); 206 | } 207 | 208 | // Token: 0x04000024 RID: 36 209 | private static UIUtils instance = null; 210 | 211 | // Token: 0x04000025 RID: 37 212 | private UIView uiRoot = null; 213 | 214 | // Token: 0x02000010 RID: 16 215 | [Flags] 216 | public enum FindOptions 217 | { 218 | // Token: 0x04000034 RID: 52 219 | None = 0, 220 | // Token: 0x04000035 RID: 53 221 | NameContains = 1 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /RoundaboutBuilder/GameActions/TmpeActions.cs: -------------------------------------------------------------------------------- 1 | using RoundaboutBuilder; 2 | using RoundaboutBuilder.UI; 3 | using System; 4 | using System.Reflection; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace SharedEnvironment 8 | { 9 | public class EnteringBlockedJunctionAllowedAction : GameActionExtended 10 | { 11 | private WrappedSegment m_segment; 12 | private bool m_startNode; 13 | 14 | // Is it the main roundabout road or road entering the roundabout 15 | private bool m_yieldingRoad; 16 | 17 | public EnteringBlockedJunctionAllowedAction(WrappedSegment segment, bool startNode, bool yieldingRoad) : base("TMPE setup", false) 18 | { 19 | m_segment = segment; 20 | m_startNode = startNode; 21 | m_yieldingRoad = yieldingRoad; 22 | } 23 | 24 | protected override void DoImplementation() 25 | { 26 | if (IsPolicyAllowed()) 27 | Implementation(); 28 | } 29 | 30 | protected override void RedoImplementation() 31 | { 32 | if (IsPolicyAllowed()) 33 | Implementation(); 34 | } 35 | 36 | protected override void UndoImplementation() 37 | { 38 | // ignore 39 | } 40 | 41 | protected void Implementation() 42 | { 43 | TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.SetEnteringBlockedJunctionAllowed(m_segment.Id, m_startNode, true); 44 | } 45 | 46 | protected bool IsPolicyAllowed() 47 | { 48 | return ModLoadingExtension.tmpeDetected && UIWindow.SavedSetupTmpe && ( 49 | (!m_yieldingRoad && TmpeSetupPanel.SavedEnterBlockedMainRoad.value) || (m_yieldingRoad && TmpeSetupPanel.SavedEnterBlockedYieldingRoad.value)); 50 | } 51 | 52 | } 53 | 54 | public class NoCrossingsAction : GameActionExtended 55 | { 56 | private WrappedSegment m_segment; 57 | private bool m_startNode; 58 | 59 | public NoCrossingsAction(WrappedSegment segment, bool startNode) : base("TMPE setup", false) 60 | { 61 | m_segment = segment; 62 | m_startNode = startNode; 63 | } 64 | 65 | protected override void DoImplementation() 66 | { 67 | if (IsPolicyAllowed()) 68 | Implementation(); 69 | } 70 | 71 | protected override void RedoImplementation() 72 | { 73 | if (IsPolicyAllowed()) 74 | Implementation(); 75 | } 76 | 77 | protected override void UndoImplementation() 78 | { 79 | // ignore 80 | } 81 | 82 | protected void Implementation() 83 | { 84 | TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.SetPedestrianCrossingAllowed(m_segment.Id, m_startNode, false); 85 | } 86 | 87 | protected bool IsPolicyAllowed() 88 | { 89 | return ModLoadingExtension.tmpeDetected && UIWindow.SavedSetupTmpe && TmpeSetupPanel.SavedNoCrossings; 90 | } 91 | 92 | } 93 | 94 | public class NoParkingAction : GameActionExtended 95 | { 96 | private WrappedSegment m_segment; 97 | 98 | public NoParkingAction(WrappedSegment segment) : base("TMPE setup", false) 99 | { 100 | m_segment = segment; 101 | } 102 | 103 | protected override void DoImplementation() 104 | { 105 | if (IsPolicyAllowed()) 106 | Implementation(); 107 | } 108 | 109 | protected override void RedoImplementation() 110 | { 111 | if (IsPolicyAllowed()) 112 | Implementation(); 113 | } 114 | 115 | protected override void UndoImplementation() 116 | { 117 | // ignore 118 | } 119 | 120 | protected void Implementation() 121 | { 122 | TrafficManager.Manager.Impl.ParkingRestrictionsManager.Instance.SetParkingAllowed(m_segment.Id, NetInfo.Direction.Backward, false); 123 | TrafficManager.Manager.Impl.ParkingRestrictionsManager.Instance.SetParkingAllowed(m_segment.Id, NetInfo.Direction.Forward, false); 124 | } 125 | 126 | protected bool IsPolicyAllowed() 127 | { 128 | return ModLoadingExtension.tmpeDetected && UIWindow.SavedSetupTmpe && TmpeSetupPanel.SavedNoParking; 129 | } 130 | 131 | } 132 | 133 | public class YieldSignAction : GameActionExtended 134 | { 135 | private WrappedSegment m_segment; 136 | private bool m_startNode; 137 | 138 | public YieldSignAction(WrappedSegment segment, bool startNode) : base("TMPE setup", false) 139 | { 140 | m_segment = segment; 141 | m_startNode = startNode; 142 | } 143 | 144 | protected override void DoImplementation() 145 | { 146 | if (IsPolicyAllowed()) 147 | Implementation(); 148 | } 149 | 150 | protected override void RedoImplementation() 151 | { 152 | if (IsPolicyAllowed()) 153 | Implementation(); 154 | } 155 | 156 | protected override void UndoImplementation() 157 | { 158 | // ignore 159 | } 160 | 161 | protected void Implementation() { 162 | Exception ex = new Exception(); 163 | bool invoked = false; 164 | try 165 | { 166 | var tmpeTool = Type.GetType("TrafficManager.UI.ModUI, TrafficManager").GetMethod("GetTrafficManagerTool", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) 167 | .Invoke(null, new object[] { true }); 168 | var prioritySignsTool = Type.GetType("TrafficManager.UI.TrafficManagerTool, TrafficManager").GetMethod("GetSubTool", BindingFlags.Public | BindingFlags.Instance).Invoke(tmpeTool, new object[] { 2 }); 169 | var setPrioritySign = prioritySignsTool.GetType().GetMethod("SetPrioritySign", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 170 | setPrioritySign.Invoke(prioritySignsTool, new object[] { m_segment.Id, m_startNode, 3 }); 171 | invoked = true; 172 | } catch(Exception e) { ex = e; } 173 | 174 | if(!invoked) 175 | try 176 | { 177 | Implementation_preV11(); 178 | invoked = true; 179 | } catch { } 180 | 181 | if (!invoked) 182 | throw ex; 183 | 184 | } 185 | 186 | [MethodImpl(MethodImplOptions.NoInlining)] 187 | private void Implementation_preV11() 188 | { 189 | /*var prioritySignsTool = (TrafficManager.UI.SubTools.PrioritySignsTool)TrafficManager.UI.UIBase.GetTrafficManagerTool().GetSubTool(TrafficManager.UI.ToolMode.AddPrioritySigns); 190 | var setPrioritySign = prioritySignsTool.GetType().GetMethod("SetPrioritySign", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 191 | setPrioritySign.Invoke(prioritySignsTool, new object[] { m_segment.Id, m_startNode, 3 });*/ 192 | } 193 | 194 | protected bool IsPolicyAllowed() 195 | { 196 | return ModLoadingExtension.tmpeDetected && UIWindow.SavedSetupTmpe && TmpeSetupPanel.SavedPrioritySigns; 197 | } 198 | 199 | } 200 | 201 | public class LaneChangingAction : GameActionExtended 202 | { 203 | private WrappedSegment m_segment; 204 | private bool m_startNode; 205 | 206 | public LaneChangingAction(WrappedSegment segment, bool startNode) : base("TMPE setup", false) 207 | { 208 | m_segment = segment; 209 | m_startNode = startNode; 210 | } 211 | 212 | protected override void DoImplementation() 213 | { 214 | if (IsPolicyAllowed()) 215 | Implementation(); 216 | } 217 | 218 | protected override void RedoImplementation() 219 | { 220 | if (IsPolicyAllowed()) 221 | Implementation(); 222 | } 223 | 224 | protected override void UndoImplementation() 225 | { 226 | // ignore 227 | } 228 | 229 | protected void Implementation() 230 | { 231 | //TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.SetLaneChangingAllowedWhenGoingStraight(m_segment.Id, m_startNode, true); 232 | } 233 | 234 | protected bool IsPolicyAllowed() 235 | { 236 | return ModLoadingExtension.tmpeDetected && UIWindow.SavedSetupTmpe && TmpeSetupPanel.SavedAllowLaneChanging; 237 | } 238 | 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /RoundaboutBuilder/AutomaticRoundaboutBuilder.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.PlatformServices; 3 | using ColossalFramework.UI; 4 | using ICities; 5 | using RoundaboutBuilder.Tools; 6 | using RoundaboutBuilder.UI; 7 | using System; 8 | using UnityEngine; 9 | 10 | /* By Strad, 02/2019 - 04/2020 */ 11 | 12 | /* Version RELEASE 1.9.0 */ 13 | 14 | /* Warning: I am lazy thus the version labels across the files may not be updated */ 15 | 16 | /* There was time when I kept the code more tidy than now. If you are learing how to mod, I suggest you to look at older versions of this mod. (Mayebe even the first one.) 17 | * Everything is available on Github. */ 18 | 19 | namespace RoundaboutBuilder 20 | { 21 | public class RoundAboutBuilder : IUserMod 22 | { 23 | public static readonly string VERSION = "RELEASE 1.9.7"; 24 | public static PublishedFileId WORKSHOP_FILE_ID; 25 | 26 | public const string settingsFileName = "RoundaboutBuilder"; 27 | 28 | public static readonly SavedInputKey ModShortcut = new SavedInputKey("modShortcut", settingsFileName, SavedInputKey.Encode(KeyCode.O, true, false, false), true); 29 | public static readonly SavedInputKey IncreaseShortcut = new SavedInputKey("increaseShortcut", settingsFileName, SavedInputKey.Encode(KeyCode.Equals, false, false, false), true); 30 | public static readonly SavedInputKey DecreaseShortcut = new SavedInputKey("decreaseShortcut", settingsFileName, SavedInputKey.Encode(KeyCode.Minus, false, false, false), true); 31 | 32 | public static readonly Vector2 defWindowPosition = new Vector2(85, 10); 33 | 34 | public static readonly SettingsBool SelectTwoWayRoads = new SettingsBool("Allow selection of two-way roads", "You can select two-way roads for your roundabouts through the roads menu (if that option is enabled)", "selectTwoWayRoads", false); 35 | public static readonly SettingsBool DoNotRemoveAnyRoads = new SettingsBool("Do not remove or connect any roads (experimental)", "No roads will be removed or connected when the roundabout is built", "dontRemoveRoads", false); 36 | public static readonly SettingsBool FollowRoadToolSelection = new SettingsBool("Use the selected road in roads menu as the roundabout road", "Your selected road for the roundabout will change as you browse through the roads menu", "followRoadToolSelection",true); 37 | public static readonly SettingsBool ShowUIButton = new SettingsBool("Show mod icon in toolbar (needs reload)", "Show the Roundabout Builder icon in road tools panel (You can always use CTRL+O to open the mod menu)","showUIButton",true); 38 | public static readonly SettingsBool UseExtraKeys = new SettingsBool("Use secondary increase / decrease radius keys", "If checked, you can use bound keys from the list below to increase / decrease radius (besides the ones on numpad)", "useExtraPlusMinusKeys", true); 39 | public static readonly SettingsBool UseOldSnappingAlgorithm = new SettingsBool("Legacy: Use old snapping algorithm", "Old snapping algorithm connects roads at 90° angle, but distorts their geometry","useOldSnappingAlgorithm", false); 40 | public static readonly SettingsBool DoNotFilterPrefabs = new SettingsBool("Do not filter prefabs (include all networks in the menu)", "The dropdown menu will include all prefabs available, not only one-way roads","doNotFilterPrefabs", false); 41 | public static readonly SettingsBool NeedMoney = new SettingsBool("Require money", "Building a roundabout will cost you money", "needMoney",true); 42 | public static readonly SettingsBool CtrlToReverseDirection = new SettingsBool("Hold CTRL to build in opposite direction", "The roundabout will be built in opposite direction if you hold CTRL", "ctrlToOppositeDir", false); 43 | public static readonly SettingsBool UnlimitedRadius = new SettingsBool("Unlimited radius", "(Almost) Unlimited roundabout radius - up to 20 000 units", "unlimitedRadius", false); 44 | public static readonly SettingsBool LegacyEllipticRoundabouts = new SettingsBool("Legacy: Elliptic roundabouts", "Enable elliptic roundabouts. This feature is no longer maintained", "legacyEllipticRoundabouts", false); 45 | 46 | //public static readonly SavedBool SeenUpdateMsg = new SavedBool("seenUpdateMsg170", RoundAboutBuilder.settingsFileName, false, true); 47 | public static readonly SavedInt savedWindowX = new SavedInt("windowX", settingsFileName, (int)defWindowPosition.x, true); 48 | public static readonly SavedInt savedWindowY = new SavedInt("windowY", settingsFileName, (int)defWindowPosition.y, true); 49 | public static readonly SavedInt TotalRoundaboutsBuilt = new SavedInt("totalRoundaboutsBuilt", settingsFileName, 0, true); 50 | 51 | //public static readonly SavedBool ShowUndoItAd = new SavedBool("showUndoItAd", RoundAboutBuilder.settingsFileName, true, true); 52 | 53 | public static bool _settingsFailed = false; 54 | 55 | public RoundAboutBuilder() 56 | { 57 | try 58 | { 59 | // Creating setting file - from SamsamTS 60 | if (GameSettings.FindSettingsFileByName(settingsFileName) == null) 61 | { 62 | GameSettings.AddSettingsFile(new SettingsFile[] { new SettingsFile() { fileName = settingsFileName } }); 63 | } 64 | } 65 | catch (Exception e) 66 | { 67 | _settingsFailed = true; 68 | Debug.Log("Couldn't load/create the setting file."); 69 | Debug.LogException(e); 70 | } 71 | } 72 | 73 | public string Name 74 | { 75 | get { return "Roundabout Builder"; } 76 | } 77 | 78 | public string Description 79 | { 80 | get { return "Press CTRL+O to open mod menu [" + VERSION + "]"; } 81 | //get { return "Automatically builds roundabouts. [" + VERSION + "]"; } 82 | 83 | } 84 | 85 | public void OnEnabled() 86 | { 87 | // Probably useless 88 | try 89 | { 90 | WORKSHOP_FILE_ID = new PublishedFileId(1625704117uL); 91 | } catch 92 | { 93 | Debug.Log("Error when assigning Workshop File ID"); 94 | } 95 | } 96 | 97 | public void OnSettingsUI(UIHelperBase helper) 98 | { 99 | try 100 | { 101 | UIHelper group = helper.AddGroup(Name) as UIHelper; 102 | UIPanel panel = group.self as UIPanel; 103 | 104 | FollowRoadToolSelection.Draw(group); 105 | NeedMoney.Draw(group); 106 | CtrlToReverseDirection.Draw(group); 107 | UnlimitedRadius.Draw(group, (b) => 108 | { 109 | if (UIWindow.instance != null) UIWindow.instance.InitPanels(); 110 | }); 111 | SelectTwoWayRoads.Draw(group, (b) => 112 | { 113 | if (UIWindow.instance != null) UIWindow.instance.dropDown.Populate(); // Reload dropdown menu 114 | }); 115 | DoNotFilterPrefabs.Draw(group, (b) => 116 | { 117 | if (UIWindow.instance != null) UIWindow.instance.dropDown.Populate(); // Reload dropdown menu 118 | }); 119 | DoNotRemoveAnyRoads.Draw(group); 120 | 121 | group.AddSpace(10); 122 | 123 | ShowUIButton.Draw(group); 124 | UseExtraKeys.Draw(group); 125 | UseOldSnappingAlgorithm.Draw(group); 126 | LegacyEllipticRoundabouts.Draw(group, (b) => 127 | { 128 | if (UIWindow.instance != null) UIWindow.instance.InitPanels(); 129 | }); 130 | 131 | group.AddSpace(10); 132 | 133 | panel.gameObject.AddComponent(); 134 | 135 | group.AddSpace(10); 136 | 137 | group.AddButton("Reset tool window position", () => 138 | { 139 | savedWindowX.Delete(); 140 | savedWindowY.Delete(); 141 | 142 | if (UIWindow.instance) 143 | UIWindow.instance.absolutePosition = defWindowPosition; 144 | }); 145 | 146 | group.AddSpace(10); 147 | 148 | group.AddButton("Remove glitched roads (Save game inbefore)", () => 149 | { 150 | Tools.GlitchedRoadsCheck.RemoveGlitchedRoads(); 151 | }); 152 | 153 | } 154 | catch (Exception e) 155 | { 156 | Debug.Log("OnSettingsUI failed"); 157 | Debug.LogException(e); 158 | } 159 | } 160 | 161 | // Easter egg 162 | public static void EasterEggToggle() 163 | { 164 | try 165 | { 166 | int requiredCount = 1024; 167 | if (TotalRoundaboutsBuilt.value < int.MaxValue) 168 | TotalRoundaboutsBuilt.value++; 169 | if (TotalRoundaboutsBuilt.value == requiredCount) 170 | { 171 | UIWindow.instance.ThrowErrorMsg("Congratulations! You've built " + requiredCount + " roundabouts in total! Do you live in France?\n" + 172 | "Fun fact: There is more than 32000 roundabouts in France, which is about one roundabout every 30 km. But the country with the highest number of roundabouts" + 173 | " per km is the United Kingdom (25000 in total/one every 15 km). Thanks to Scott Batson from Quora for this summary (2015)"); 174 | } 175 | } 176 | catch { } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /RoundaboutBuilder/UI/KeyMappings.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.UI; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using UnityEngine; 9 | using RoundaboutBuilder; 10 | 11 | namespace RoundaboutBuilder.UI 12 | { 13 | // Copied from Fine Road Anarchy 14 | public class OptionsKeymapping : UICustomControl 15 | { 16 | // Token: 0x06000023 RID: 35 RVA: 0x000036B8 File Offset: 0x000018B8 17 | private void Awake() 18 | { 19 | AddKeymapping("Mod shortcut", RoundAboutBuilder.ModShortcut); 20 | AddKeymapping("Increase radius (secondary)", RoundAboutBuilder.IncreaseShortcut); 21 | AddKeymapping("Decrease radius (secondary)", RoundAboutBuilder.DecreaseShortcut); 22 | } 23 | 24 | // Token: 0x06000024 RID: 36 RVA: 0x00003718 File Offset: 0x00001918 25 | private void AddKeymapping(string label, SavedInputKey savedInputKey) 26 | { 27 | UIPanel uipanel = base.component.AttachUIComponent(UITemplateManager.GetAsGameObject(OptionsKeymapping.kKeyBindingTemplate)) as UIPanel; 28 | int num = this.count; 29 | this.count = num + 1; 30 | if (num % 2 == 1) 31 | { 32 | uipanel.backgroundSprite = null; 33 | } 34 | UILabel uilabel = uipanel.Find("Name"); 35 | UIButton uibutton = uipanel.Find("Binding"); 36 | uibutton.eventKeyDown += this.OnBindingKeyDown; 37 | uibutton.eventMouseDown += this.OnBindingMouseDown; 38 | uilabel.text = label; 39 | uibutton.text = savedInputKey.ToLocalizedString("KEYNAME"); 40 | uibutton.objectUserData = savedInputKey; 41 | } 42 | 43 | // Token: 0x06000025 RID: 37 RVA: 0x000037B6 File Offset: 0x000019B6 44 | private void OnEnable() 45 | { 46 | //LocaleManager.eventLocaleChanged += this.OnLocaleChanged; 47 | } 48 | 49 | // Token: 0x06000026 RID: 38 RVA: 0x000037C9 File Offset: 0x000019C9 50 | private void OnDisable() 51 | { 52 | //LocaleManager.eventLocaleChanged -= this.OnLocaleChanged; 53 | } 54 | 55 | // Token: 0x06000027 RID: 39 RVA: 0x000037DC File Offset: 0x000019DC 56 | private void OnLocaleChanged() 57 | { 58 | this.RefreshBindableInputs(); 59 | } 60 | 61 | // Token: 0x06000028 RID: 40 RVA: 0x000037E4 File Offset: 0x000019E4 62 | private bool IsModifierKey(KeyCode code) 63 | { 64 | return code == KeyCode.LeftControl || code == KeyCode.RightControl || code == KeyCode.LeftShift || code == KeyCode.RightShift || code == KeyCode.LeftAlt || code == KeyCode.RightAlt; 65 | } 66 | 67 | // Token: 0x06000029 RID: 41 RVA: 0x00003818 File Offset: 0x00001A18 68 | private bool IsControlDown() 69 | { 70 | return Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); 71 | } 72 | 73 | // Token: 0x0600002A RID: 42 RVA: 0x00003832 File Offset: 0x00001A32 74 | private bool IsShiftDown() 75 | { 76 | return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); 77 | } 78 | 79 | // Token: 0x0600002B RID: 43 RVA: 0x0000384C File Offset: 0x00001A4C 80 | private bool IsAltDown() 81 | { 82 | return Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt); 83 | } 84 | 85 | // Token: 0x0600002C RID: 44 RVA: 0x00003866 File Offset: 0x00001A66 86 | private bool IsUnbindableMouseButton(UIMouseButton code) 87 | { 88 | return code == UIMouseButton.Left || code == UIMouseButton.Right; 89 | } 90 | 91 | // Token: 0x0600002D RID: 45 RVA: 0x00003874 File Offset: 0x00001A74 92 | private KeyCode ButtonToKeycode(UIMouseButton button) 93 | { 94 | if (button == UIMouseButton.Left) 95 | { 96 | return KeyCode.Mouse0; 97 | } 98 | if (button == UIMouseButton.Right) 99 | { 100 | return KeyCode.Mouse1; 101 | } 102 | if (button == UIMouseButton.Middle) 103 | { 104 | return KeyCode.Mouse2; 105 | } 106 | if (button == UIMouseButton.Special0) 107 | { 108 | return KeyCode.Mouse3; 109 | } 110 | if (button == UIMouseButton.Special1) 111 | { 112 | return KeyCode.Mouse4; 113 | } 114 | if (button == UIMouseButton.Special2) 115 | { 116 | return KeyCode.Mouse5; 117 | } 118 | if (button == UIMouseButton.Special3) 119 | { 120 | return KeyCode.Mouse6; 121 | } 122 | return KeyCode.None; 123 | } 124 | 125 | // Token: 0x0600002E RID: 46 RVA: 0x000038CC File Offset: 0x00001ACC 126 | private void OnBindingKeyDown(UIComponent comp, UIKeyEventParameter p) 127 | { 128 | if (this.m_EditingBinding != null && !this.IsModifierKey(p.keycode)) 129 | { 130 | p.Use(); 131 | UIView.PopModal(); 132 | KeyCode keycode = p.keycode; 133 | InputKey value = (p.keycode == KeyCode.Escape) ? this.m_EditingBinding.value : SavedInputKey.Encode(keycode, p.control, p.shift, p.alt); 134 | if (p.keycode == KeyCode.Backspace) 135 | { 136 | value = SavedInputKey.Empty; 137 | } 138 | this.m_EditingBinding.value = value; 139 | (p.source as UITextComponent).text = this.m_EditingBinding.ToLocalizedString("KEYNAME"); 140 | this.m_EditingBinding = null; 141 | this.m_EditingBindingCategory = string.Empty; 142 | } 143 | } 144 | 145 | // Token: 0x0600002F RID: 47 RVA: 0x0000398C File Offset: 0x00001B8C 146 | private void OnBindingMouseDown(UIComponent comp, UIMouseEventParameter p) 147 | { 148 | if (this.m_EditingBinding == null) 149 | { 150 | p.Use(); 151 | this.m_EditingBinding = (SavedInputKey)p.source.objectUserData; 152 | this.m_EditingBindingCategory = p.source.stringUserData; 153 | UIButton uibutton = p.source as UIButton; 154 | uibutton.buttonsMask = (UIMouseButton.Left | UIMouseButton.Right | UIMouseButton.Middle | UIMouseButton.Special0 | UIMouseButton.Special1 | UIMouseButton.Special2 | UIMouseButton.Special3); 155 | uibutton.text = "Press any key"; 156 | p.source.Focus(); 157 | UIView.PushModal(p.source); 158 | return; 159 | } 160 | if (!this.IsUnbindableMouseButton(p.buttons)) 161 | { 162 | p.Use(); 163 | UIView.PopModal(); 164 | InputKey value = SavedInputKey.Encode(this.ButtonToKeycode(p.buttons), this.IsControlDown(), this.IsShiftDown(), this.IsAltDown()); 165 | this.m_EditingBinding.value = value; 166 | UIButton uibutton2 = p.source as UIButton; 167 | uibutton2.text = this.m_EditingBinding.ToLocalizedString("KEYNAME"); 168 | uibutton2.buttonsMask = UIMouseButton.Left; 169 | this.m_EditingBinding = null; 170 | this.m_EditingBindingCategory = string.Empty; 171 | } 172 | } 173 | 174 | // Token: 0x06000030 RID: 48 RVA: 0x00003A8C File Offset: 0x00001C8C 175 | private void RefreshBindableInputs() 176 | { 177 | foreach (UIComponent uicomponent in base.component.GetComponentsInChildren()) 178 | { 179 | UITextComponent uitextComponent = uicomponent.Find("Binding"); 180 | if (uitextComponent != null) 181 | { 182 | SavedInputKey savedInputKey = uitextComponent.objectUserData as SavedInputKey; 183 | if (savedInputKey != null) 184 | { 185 | uitextComponent.text = savedInputKey.ToLocalizedString("KEYNAME"); 186 | } 187 | } 188 | UILabel uilabel = uicomponent.Find("Name"); 189 | if (uilabel != null) 190 | { 191 | //uilabel.text = Locale.Get("KEYMAPPING", uilabel.stringUserData); 192 | uilabel.text = uilabel.stringUserData; 193 | } 194 | } 195 | } 196 | 197 | // Token: 0x06000031 RID: 49 RVA: 0x00003B20 File Offset: 0x00001D20 198 | internal InputKey GetDefaultEntry(string entryName) 199 | { 200 | FieldInfo field = typeof(DefaultSettings).GetField(entryName, BindingFlags.Static | BindingFlags.Public); 201 | if (field == null) 202 | { 203 | return 0; 204 | } 205 | object value = field.GetValue(null); 206 | if (value is InputKey) 207 | { 208 | return (InputKey)value; 209 | } 210 | return 0; 211 | } 212 | 213 | // Token: 0x06000032 RID: 50 RVA: 0x00003B68 File Offset: 0x00001D68 214 | private void RefreshKeyMapping() 215 | { 216 | UIComponent[] componentsInChildren = base.component.GetComponentsInChildren(); 217 | for (int i = 0; i < componentsInChildren.Length; i++) 218 | { 219 | UITextComponent uitextComponent = componentsInChildren[i].Find("Binding"); 220 | SavedInputKey savedInputKey = (SavedInputKey)uitextComponent.objectUserData; 221 | if (this.m_EditingBinding != savedInputKey) 222 | { 223 | uitextComponent.text = savedInputKey.ToLocalizedString("KEYNAME"); 224 | } 225 | } 226 | } 227 | 228 | // Token: 0x04000011 RID: 17 229 | private static readonly string kKeyBindingTemplate = "KeyBindingTemplate"; 230 | 231 | // Token: 0x04000012 RID: 18 232 | private SavedInputKey m_EditingBinding; 233 | 234 | // Token: 0x04000013 RID: 19 235 | private string m_EditingBindingCategory; 236 | 237 | // Token: 0x04000019 RID: 25 238 | private int count; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /RoundaboutBuilder/RoundaboutTool.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.UI; 2 | using RoundaboutBuilder.Tools; 3 | using RoundaboutBuilder.UI; 4 | using SharedEnvironment; 5 | using System; 6 | using System.Diagnostics; 7 | using UnityEngine; 8 | 9 | /* By Strad, 01/2019 */ 10 | 11 | /* Version BETA 1.2.0 */ 12 | 13 | namespace RoundaboutBuilder 14 | { 15 | /* Tool for building standard non-elliptic roundabouts */ 16 | 17 | class RoundaboutTool : ToolBaseExtended 18 | { 19 | public static RoundaboutTool Instance; 20 | 21 | private static readonly int DISTANCE_PADDING = 15; 22 | 23 | private FinalConnector m_roundabout; 24 | private ushort m_nodeID; 25 | private float m_radius; 26 | 27 | //string m_radiusField.text = RADIUS_DEF.ToString(); 28 | 29 | /* Main method , called when user cliks on a node to create a roundabout */ 30 | public void CreateRoundabout(ushort nodeID) 31 | { 32 | //Debug.Log(string.Format("Clicked on node ID {0}!", nodeID)); 33 | 34 | UIWindow.instance.LostFocus(); 35 | 36 | try 37 | { 38 | if (m_roundabout != null && nodeID == m_nodeID) 39 | { 40 | m_roundabout.Build(); 41 | m_hoverNode = 0; 42 | m_roundabout = null; 43 | // Easter egg 44 | RoundAboutBuilder.EasterEggToggle(); 45 | } 46 | 47 | // Debug, don't forget to remove 48 | /*foreach(VectorNodeStruct intersection in intersections.Intersections) 49 | { 50 | Debug.Log($"May have/Has restrictions: {TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.MayHaveJunctionRestrictions(intersection.nodeId)}, {TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.HasJunctionRestrictions(intersection.nodeId)}"); 51 | }*/ 52 | 53 | } 54 | catch (ActionException e) 55 | { 56 | UIWindow.instance.ThrowErrorMsg(e.Message); 57 | } 58 | catch (Exception e) 59 | { 60 | UnityEngine.Debug.LogError(e); 61 | UIWindow.instance.ThrowErrorMsg(e.ToString(), true); 62 | } 63 | 64 | } 65 | 66 | private void PreviewRoundabout(float radius) 67 | { 68 | /* These lines of code do all the work. See documentation in respective classes. */ 69 | /* When the old snapping algorithm is enabled, we create secondary (bigger) ellipse, so the newly connected roads obtained by the 70 | * graph traveller are not too short. They will be at least as long as the padding. */ 71 | bool reverseDirection = (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && RoundAboutBuilder.CtrlToReverseDirection.value; 72 | if (m_radius == radius && m_hoverNode == m_nodeID && m_roundabout != null && m_roundabout.m_reverseDirection == reverseDirection) 73 | { 74 | return; 75 | } 76 | try 77 | { 78 | m_radius = radius; 79 | m_nodeID = m_hoverNode; 80 | Ellipse ellipse = new Ellipse(NetUtil.Node(m_nodeID).m_position, new Vector3(0f, 0f, 0f), radius, radius); 81 | Ellipse ellipseWithPadding = ellipse; 82 | if (RoundAboutBuilder.UseOldSnappingAlgorithm.value) 83 | { 84 | ellipseWithPadding = new Ellipse(NetUtil.Node(m_nodeID).m_position, new Vector3(0f, 0f, 0f), radius + DISTANCE_PADDING, radius + DISTANCE_PADDING); 85 | } 86 | GraphTraveller2 traveller; 87 | EdgeIntersections2 intersections = null; 88 | if (!RoundAboutBuilder.DoNotRemoveAnyRoads) 89 | { 90 | traveller = new GraphTraveller2(m_nodeID, ellipse); 91 | intersections = new EdgeIntersections2(traveller, m_nodeID, ellipse, GetFollowTerrain()); 92 | } 93 | m_roundabout = new FinalConnector(NetUtil.Node(m_nodeID).Info, intersections, ellipse, true, GetFollowTerrain(), reverseDirection); 94 | } 95 | catch (Exception e) { UnityEngine.Debug.LogError(e); } 96 | 97 | } 98 | 99 | protected override void OnClick() 100 | { 101 | base.OnClick(); 102 | if (m_hoverNode != 0) 103 | { 104 | CreateRoundabout(m_hoverNode); 105 | } 106 | } 107 | 108 | protected override void OnDisable() 109 | { 110 | base.OnDisable(); 111 | if (UIWindow.instance != null && UIWindow.instance.P_RoundAboutPanel != null && UIWindow.instance.P_RoundAboutPanel.label != null) 112 | { 113 | UIWindow.instance.P_RoundAboutPanel.label.text = "Click inside the window to reactivate the tool"; 114 | } 115 | } 116 | 117 | protected override void OnEnable() 118 | { 119 | base.OnEnable(); 120 | System.Random rand = new System.Random(); 121 | string text = ""; // a little bit of advertising never hurt anyone 122 | switch (rand.Next(6)) 123 | { 124 | case 0: case 1: text = "Tip: Use Network Anarchy for elevated roads"; break; 125 | case 2: case 3: text = "Tip: Use this with any network (see options)"; break; 126 | case 4: text = "Tip: Check out Smart Intersection Builder!"; break; 127 | case 5: text = "Tip: Check out Adjust Pathfinding mod!"; break; 128 | default: text = "Tip: Use Network Anarchy for elevated roads"; break; 129 | } 130 | 131 | if (UIWindow.instance != null && UIWindow.instance.P_RoundAboutPanel != null && UIWindow.instance.P_RoundAboutPanel.label != null) 132 | { 133 | UIWindow.instance.P_RoundAboutPanel.label.text = text; 134 | } 135 | } 136 | 137 | /* UI methods */ 138 | 139 | public override void IncreaseButton() 140 | { 141 | UIWindow.instance.P_RoundAboutPanel.RadiusField.Increase(); 142 | } 143 | 144 | public override void DecreaseButton() 145 | { 146 | UIWindow.instance.P_RoundAboutPanel.RadiusField.Decrease(); 147 | } 148 | 149 | /* This draws the UI circles on the map */ 150 | protected override void RenderOverlayExtended(RenderManager.CameraInfo cameraInfo) 151 | { 152 | try 153 | { 154 | if (insideUI) 155 | return; 156 | 157 | if (m_hoverNode != 0) 158 | { 159 | NetNode hoveredNode = NetUtil.Node(m_hoverNode); 160 | 161 | // kinda stole this color from Move It! 162 | // thanks to SamsamTS because they're a UI god 163 | // ..and then Strad stole it from all of you!! 164 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.black, hoveredNode.m_position, 15f, hoveredNode.m_position.y - 1f, hoveredNode.m_position.y + 1f, true, true); 165 | float? radius = UIWindow.instance.P_RoundAboutPanel.RadiusField.Value; 166 | if (radius != null && NetUtil.ExistsNode(m_hoverNode)) 167 | { 168 | PreviewRoundabout((float)radius); 169 | float roadWidth = UIWindow.instance.dropDown.Value.m_halfWidth; // There is a slight chance that this will throw an exception 170 | float innerCirleRadius = radius - roadWidth > 0 ? 2 * ((float)radius - roadWidth) : 2 * (float)radius; 171 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.red, hoveredNode.m_position, innerCirleRadius, hoveredNode.m_position.y - 2f, hoveredNode.m_position.y + 2f, true, true); 172 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.red, hoveredNode.m_position, 2 * ((float)radius + roadWidth /*DISTANCE_PADDING - 5*/), hoveredNode.m_position.y - 1f, hoveredNode.m_position.y + 1f, true, true); 173 | RenderHoveringLabel("Cost: " + (m_roundabout.Cost / 100) + "\nClick to build\nPress +/- to adjust radius"); 174 | } 175 | else 176 | { 177 | m_roundabout = null; 178 | RenderHoveringLabel("Invalid radius\nPress +/- to adjust radius"); 179 | } 180 | //RenderDirectionVectors(cameraInfo); 181 | } 182 | else 183 | { 184 | m_roundabout = null; 185 | RenderMousePositionCircle(cameraInfo); 186 | RenderHoveringLabel("Hover mouse over intersection"); 187 | } 188 | } 189 | catch (Exception e) 190 | { 191 | UnityEngine.Debug.LogError(e); 192 | } 193 | } 194 | 195 | // Unrelated debug 196 | /*private void NetAnalysis() 197 | { 198 | 199 | int errorsZeros = 0; 200 | int errorsNulls = 0; 201 | for (int i = 0; i < Singleton.instance.m_segments.m_buffer.Length; i++) 202 | //foreach(NetSegment segment in Singleton.instance.m_segments.m_buffer) 203 | 204 | { 205 | try 206 | { 207 | NetSegment segment = Singleton.instance.m_segments.m_buffer[i]; 208 | if (ReferenceEquals(segment, null)) 209 | continue; 210 | if (segment.m_startNode == 0 || segment.m_endNode == 0) 211 | { 212 | errorsZeros++; 213 | NetManager.instance.ReleaseSegment((ushort)i, true); 214 | } 215 | else if (ReferenceEquals(GetNode(segment.m_startNode), null) || ReferenceEquals(GetNode(segment.m_endNode), null)) 216 | { 217 | errorsNulls++; 218 | NetManager.instance.ReleaseSegment((ushort)i, true); 219 | } 220 | 221 | } 222 | catch (Exception) 223 | { 224 | 225 | 226 | } 227 | } 228 | 229 | Debug.Log("errorsZeros: " + errorsZeros + ", errorsNulls: " + errorsNulls); 230 | }*/ 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /RoundaboutBuilder/Tools/FinalConnector.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.Math; 3 | using SharedEnvironment; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using UnityEngine; 8 | 9 | /* By Strad, 01/2019 */ 10 | 11 | /* Version RELEASE 1.2.0+ */ 12 | 13 | namespace RoundaboutBuilder.Tools 14 | { 15 | /* This class takes the nodes obtained from EdgeIntersections2 and connects them together to create the final roundabout. */ 16 | 17 | /* This is all such garbage with old algortithms mixing with new and other stuff... Not the nicest piece of code. */ 18 | 19 | public class FinalConnector 20 | { 21 | private static readonly int DISTANCE_MIN = 20; // Min distance from other nodes when inserting controlling node 22 | 23 | private List intersections; 24 | private bool leftHandTraffic; 25 | private Ellipse ellipse; 26 | private NetInfo centerNodeNetInfo; 27 | private double m_maxAngDistance; 28 | 29 | public bool m_followTerrain { get; private set; } 30 | public bool m_reverseDirection { get; private set; } 31 | 32 | private ActionGroup actionGroupTMPE; 33 | private ActionGroup actionGroupRoads; 34 | private WrappersDictionary wrappersDictionary; 35 | 36 | public int Cost => actionGroupRoads != null ? actionGroupRoads.DoCost() : 0; 37 | 38 | // Time to time something goes wrong. Let's make sure that we don't get stuck in infinite recursion. 39 | // Didn't happen to me since I degbugged it, but one never knows for sure. 40 | private int pleasenoinfiniterecursion; 41 | 42 | public FinalConnector(NetInfo centerNodeNetInfo, EdgeIntersections2 edgeIntersections, Ellipse ellipse, bool insertControllingVertices, bool followTerrain, bool reverseDirection) 43 | { 44 | intersections = edgeIntersections?.Intersections ?? new List(); 45 | actionGroupTMPE = edgeIntersections?.ActionGroupTMPE ?? new ActionGroup("Set up TMPE"); 46 | actionGroupRoads = edgeIntersections?.ActionGroupRoads ?? new ActionGroup("Build roundabout"); 47 | wrappersDictionary = edgeIntersections?.networkDictionary ?? new WrappersDictionary(); 48 | 49 | this.ellipse = ellipse; 50 | pleasenoinfiniterecursion = 0; 51 | this.centerNodeNetInfo = centerNodeNetInfo; 52 | leftHandTraffic = Singleton.instance.m_metaData.m_invertTraffic == 53 | SimulationMetaData.MetaBool.True; 54 | m_followTerrain = followTerrain; 55 | m_reverseDirection = reverseDirection; 56 | 57 | // We ensure that the segments are not too long. For circles only (with ellipses it would be more difficult) 58 | m_maxAngDistance = Math.Min(Math.PI * 25 / ellipse.RadiusMain , Math.PI/2 + 0.1d); 59 | 60 | bool isCircle = ellipse.IsCircle(); 61 | if (!isCircle && insertControllingVertices) 62 | { 63 | /* See doc in the method below */ 64 | InsertIntermediateNodes(); 65 | } 66 | 67 | /* If the list of edge nodes is empty, we add one default intersection. */ 68 | if (isCircle && intersections.Count == 0) 69 | { 70 | Vector3 defaultIntersection = new Vector3(ellipse.RadiusMain, 0, 0) + ellipse.Center; 71 | defaultIntersection = new Vector3(defaultIntersection.x, m_followTerrain ? NetUtil.TerrainHeight(defaultIntersection) : defaultIntersection.y, defaultIntersection.z); 72 | //ushort newNodeId = NetAccess.CreateNode(centerNodeNetInfo, defaultIntersection); 73 | 74 | WrappedNode newNodeW = new WrappedNode(); 75 | newNodeW.Position = defaultIntersection; 76 | newNodeW.NetInfo = centerNodeNetInfo; 77 | RoundaboutNode raNode = new RoundaboutNode(newNodeW); 78 | raNode.Create(actionGroupRoads); 79 | intersections.Add(raNode); 80 | } 81 | 82 | int count = intersections.Count; 83 | foreach (RoundaboutNode item in intersections) 84 | { 85 | item.angle = Ellipse.VectorsAngle(item.wrappedNode.Position - ellipse.Center); 86 | } 87 | 88 | /* We sort the nodes according to their angles */ 89 | intersections.Sort(); 90 | 91 | /* Goes over all the nodes and conntets each of them to the angulary closest neighbour. (In a given direction) */ 92 | 93 | for (int i = 0; i < count; i++) 94 | { 95 | RoundaboutNode prevNode = intersections[i]; 96 | if (isCircle) 97 | prevNode = CheckAngularDistance(intersections[i], intersections[(i + 1) % count]); 98 | ConnectNodes(intersections[(i + 1) % count], prevNode); 99 | } 100 | 101 | // Charge player 102 | actionGroupRoads.ItemClass = centerNodeNetInfo.m_class; 103 | } 104 | 105 | public void Build() 106 | { 107 | ModThreading.PushAction(actionGroupRoads, actionGroupTMPE); 108 | } 109 | 110 | /* For circles only. */ 111 | private RoundaboutNode CheckAngularDistance(RoundaboutNode p1, RoundaboutNode p2) 112 | { 113 | RoundaboutNode prevNode = p1; 114 | double angDif = NormalizeAngle(prevNode.angle - p2.angle); 115 | if (p1 == p2) 116 | angDif = 2 * Math.PI; 117 | 118 | /* If the distance between two nodes is too great, we put in an intermediate node inbetween */ 119 | while ( angDif > m_maxAngDistance ) 120 | { 121 | recursionGuard(); 122 | 123 | double angle = NormalizeAngle(prevNode.angle - angDif / Math.Ceiling(angDif / m_maxAngDistance)); 124 | Vector3 vector = ellipse.VectorAtAbsoluteAngle(angle); 125 | 126 | //ushort newNodeId = NetAccess.CreateNode(centerNodeNetInfo, vector); 127 | WrappedNode newNodeW = new WrappedNode(); 128 | newNodeW.Position = new Vector3(vector.x, m_followTerrain ? NetUtil.TerrainHeight(vector) : vector.y, vector.z); ; 129 | newNodeW.NetInfo = centerNodeNetInfo; 130 | actionGroupRoads.Actions.Add(newNodeW); 131 | 132 | RoundaboutNode newNode = new RoundaboutNode(newNodeW); 133 | newNode.angle = angle; 134 | 135 | ConnectNodes(newNode, prevNode); 136 | 137 | prevNode = newNode; 138 | angDif = NormalizeAngle(prevNode.angle - p2.angle); 139 | } 140 | 141 | return prevNode; 142 | } 143 | 144 | /* Ellipse only. Adds nodes where the ellipse intersects its axes to keep it in shape. User can turn this off. Kepp in mind that since we can only 145 | * approximate the ellipse (maybe I am wrong), every node on its circumference changes its actual shape. */ 146 | private void InsertIntermediateNodes() 147 | { 148 | List newNodes = new List(); 149 | /* Originally I planned to pair every node on the ellipse to make it symmetric... Didn't work, I gave up on making it work. */ 150 | /*foreach(VectorNodeStruct intersection in intersections.ToArray()) 151 | { 152 | double angle = getAbsoluteAngle(intersection.vector); 153 | VectorNodeStruct newNode = new VectorNodeStruct(ellipse.VectorAtAbsoluteAngle(getConjugateAngle(angle))); 154 | intersections.Add( newNode ); 155 | EllipseTool.Instance.debugDrawPositions.Add(newNode.vector); 156 | }*/ 157 | var vec = ellipse.VectorAtAngle(0); 158 | newNodes.Add(new RoundaboutNode(new Vector3(vec.x, m_followTerrain ? NetUtil.TerrainHeight(vec) : vec.y,vec.z))); 159 | vec = ellipse.VectorAtAngle(Math.PI / 2); 160 | newNodes.Add(new RoundaboutNode(new Vector3(vec.x, m_followTerrain ? NetUtil.TerrainHeight(vec) : vec.y, vec.z))); 161 | vec = ellipse.VectorAtAngle(Math.PI); 162 | newNodes.Add(new RoundaboutNode(new Vector3(vec.x, m_followTerrain ? NetUtil.TerrainHeight(vec) : vec.y, vec.z))); 163 | vec = ellipse.VectorAtAngle(3 * Math.PI / 2); 164 | newNodes.Add(new RoundaboutNode(new Vector3(vec.x, m_followTerrain ? NetUtil.TerrainHeight(vec) : vec.y, vec.z))); 165 | /*EllipseTool.Instance.debugDrawPositions.Add(new VectorNodeStruct(ellipse.VectorAtAngle(0)).vector); 166 | EllipseTool.Instance.debugDrawPositions.Add(new VectorNodeStruct(ellipse.VectorAtAngle(Math.PI / 2)).vector); 167 | EllipseTool.Instance.debugDrawPositions.Add(new VectorNodeStruct(ellipse.VectorAtAngle(Math.PI)).vector); 168 | EllipseTool.Instance.debugDrawPositions.Add(new VectorNodeStruct(ellipse.VectorAtAngle(3 * Math.PI / 2)).vector);*/ 169 | /* Disgusting nested FOR cycle. We don't want to cluster the nodes too close to each other. */ 170 | foreach(RoundaboutNode vectorNode in newNodes.ToArray()) 171 | { 172 | foreach(RoundaboutNode intersection in intersections) 173 | { 174 | if(VectorDistance(vectorNode.wrappedNode.Position, intersection.wrappedNode.Position) < DISTANCE_MIN) 175 | { 176 | newNodes.Remove(vectorNode); 177 | //Debug.Log("Node too close, removing from list"); 178 | } 179 | } 180 | } 181 | intersections.AddRange(newNodes); 182 | } 183 | 184 | /* See ellipse class */ 185 | private double getAbsoluteAngle(Vector3 absPosition) 186 | { 187 | return Ellipse.VectorsAngle(absPosition - ellipse.Center); 188 | } 189 | 190 | 191 | private void ConnectNodes(RoundaboutNode vectorNode1, RoundaboutNode vectorNode2) 192 | { 193 | bool invert = leftHandTraffic ^ m_reverseDirection; 194 | 195 | vectorNode1.Create(actionGroupRoads,centerNodeNetInfo); 196 | vectorNode2.Create(actionGroupRoads,centerNodeNetInfo); 197 | 198 | /* NetNode node1 = GetNode(vectorNode1.nodeId); 199 | NetNode node2 = GetNode(vectorNode2.nodeId);*/ 200 | 201 | double angle1 = getAbsoluteAngle(vectorNode1.wrappedNode.Position); 202 | double angle2 = getAbsoluteAngle(vectorNode2.wrappedNode.Position); 203 | 204 | Vector3 vec1 = ellipse.TangentAtAbsoluteAngle(angle1); 205 | Vector3 vec2 = ellipse.TangentAtAbsoluteAngle(angle2); 206 | 207 | vec1.Normalize(); 208 | vec2.Normalize(); 209 | vec2 = -vec2; 210 | 211 | /*EllipseTool.Instance.debugDrawVector(10*vec1, vectorNode1.vector); 212 | EllipseTool.Instance.debugDrawVector(10*vec2, vectorNode2.vector);*/ 213 | 214 | //NetInfo netPrefab = PrefabCollection.FindLoaded("Oneway Road"); 215 | NetInfo netPrefab = UI.UIWindow.instance.dropDown.Value; 216 | //ushort newSegmentId = NetAccess.CreateSegment(vectorNode1.nodeId, vectorNode2.nodeId, vec1, vec2, netPrefab, invert, leftHandTraffic, true); 217 | WrappedSegment newSegment = new WrappedSegment(); 218 | newSegment.StartNode = vectorNode1.wrappedNode; 219 | newSegment.EndNode = vectorNode2.wrappedNode; 220 | newSegment.StartDirection = vec1; 221 | newSegment.EndDirection = vec2; 222 | newSegment.NetInfo = netPrefab; 223 | newSegment.Invert = invert; 224 | newSegment.SwitchStartAndEnd = leftHandTraffic; 225 | newSegment.DeployPlacementEffects = true; 226 | 227 | actionGroupRoads.Actions.Add(newSegment); 228 | 229 | 230 | try 231 | { 232 | SetupTMPE(newSegment); 233 | } 234 | catch (Exception e) 235 | { 236 | Debug.LogError(e); 237 | } 238 | //Debug.Log(string.Format("Building segment between nodes {0}, {1}, bezier scale {2}", node1, node2, scale)); 239 | } 240 | 241 | private void SetupTMPE(WrappedSegment segment) 242 | { 243 | /* None of this below works: */ 244 | /*bool resultPrev1 = TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.IsEnteringBlockedJunctionAllowed(segment,false); 245 | bool resultPrev2 = TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.IsEnteringBlockedJunctionAllowed(segment,true); 246 | bool result1 = TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.SetEnteringBlockedJunctionAllowed(segment, false, true); 247 | bool result2 = TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.SetEnteringBlockedJunctionAllowed(segment, true, true); 248 | bool resultPost1 = TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.IsEnteringBlockedJunctionAllowed(segment, false); 249 | bool resultPost2 = TrafficManager.Manager.Impl.JunctionRestrictionsManager.Instance.IsEnteringBlockedJunctionAllowed(segment, true);*/ 250 | /*Debug.Log($"Setting up tmpe segment {segment}. Result: {resultPrev1}, {resultPrev2}, {result1}, {result2}, {resultPost1}, {resultPost2}"); 251 | ModThreading.Timer(segment);*/ 252 | actionGroupTMPE.Actions.Add(new EnteringBlockedJunctionAllowedAction( segment, true, false) ); 253 | actionGroupTMPE.Actions.Add(new EnteringBlockedJunctionAllowedAction( segment, false, false) ); 254 | actionGroupTMPE.Actions.Add(new LaneChangingAction(segment, true)); 255 | actionGroupTMPE.Actions.Add(new LaneChangingAction(segment, false)); 256 | actionGroupTMPE.Actions.Add(new NoCrossingsAction(segment, true)); 257 | actionGroupTMPE.Actions.Add(new NoCrossingsAction(segment, false)); 258 | actionGroupTMPE.Actions.Add(new NoParkingAction(segment)); 259 | } 260 | 261 | private void recursionGuard() 262 | { 263 | pleasenoinfiniterecursion++; 264 | if (pleasenoinfiniterecursion > 1000) 265 | { 266 | throw new Exception("Something went wrong! Mod got stuck in infinite recursion."); 267 | } 268 | } 269 | 270 | /* Utility */ 271 | 272 | private static float VectorDistance(Vector3 v1, Vector3 v2) // Without Y coordinate 273 | { 274 | return (float)(Math.Sqrt(Math.Pow(v1.x - v2.x, 2) + Math.Pow(v1.z - v2.z, 2))); 275 | } 276 | private static double NormalizeAngle(double angle) 277 | { 278 | angle = angle % (2 * Math.PI); 279 | return angle < 0 ? angle + 2 * Math.PI : angle; 280 | } 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /RoundaboutBuilder/EllipseTool.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework.Math; 2 | using ColossalFramework.UI; 3 | using RoundaboutBuilder.Tools; 4 | using RoundaboutBuilder.UI; 5 | using SharedEnvironment; 6 | using System; 7 | using UnityEngine; 8 | 9 | /* By Strad, 01/2019 */ 10 | 11 | /* Version RELEASE 1.4.0+ */ 12 | 13 | namespace RoundaboutBuilder 14 | { 15 | /* Tool for building elliptic roundabouts */ 16 | 17 | class EllipseTool : ToolBaseExtended 18 | { 19 | public static EllipseTool Instance; 20 | 21 | private static readonly int DISTANCE_PADDING = 15; 22 | private static readonly int RADIUS1_DEF = 60; 23 | private static readonly int RADIUS2_DEF = 30; 24 | 25 | ushort centralNode; 26 | ushort axisNode; 27 | 28 | // Saving radii from the last frame so that we don't have to recalculate the ellipse every time 29 | float prevRadius1 = 0; 30 | float prevRadius2 = 0; 31 | Ellipse ellipse; 32 | 33 | // UI: 34 | private enum Stage 35 | { 36 | CentralPoint, // First user chooses cental point 37 | MainAxis, // Then point on main axis 38 | Final // And then sets radii and stuff 39 | } 40 | Stage stage = Stage.CentralPoint; 41 | public bool ControlVertices = true; 42 | 43 | /* Debug */ 44 | /*public List debugDraw = new List(); 45 | public List debugDrawPositions = new List(); 46 | bool yes = true; // :)*/ 47 | 48 | public void BuildEllipse() 49 | { 50 | if (ellipse == null) 51 | UIWindow.instance.ThrowErrorMsg("Invalid radii!"); 52 | 53 | Ellipse toBeBuiltEllipse = ellipse; 54 | Ellipse ellipseWithPadding = ellipse; 55 | /* When the old snapping algorithm is enabled, we create secondary (bigger) ellipse, so the newly connected roads obtained by the 56 | * graph traveller are not too short. They will be at least as long as the padding. */ 57 | if (RoundAboutBuilder.UseOldSnappingAlgorithm.value) 58 | { 59 | ellipseWithPadding = new Ellipse(NetUtil.Node(centralNode).m_position, NetUtil.Node(axisNode).m_position - NetUtil.Node(centralNode).m_position, prevRadius1 + DISTANCE_PADDING, prevRadius2 + DISTANCE_PADDING); 60 | } 61 | 62 | UIWindow.instance.LostFocus(); 63 | Instance.GoToFirstStage(); 64 | 65 | try 66 | { 67 | bool reverseDirection = (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && RoundAboutBuilder.CtrlToReverseDirection.value; 68 | GraphTraveller2 traveller = new GraphTraveller2(centralNode, ellipseWithPadding); 69 | EdgeIntersections2 intersections = new EdgeIntersections2(traveller, centralNode, toBeBuiltEllipse, GetFollowTerrain()); 70 | FinalConnector finalConnector = new FinalConnector(NetUtil.Node(centralNode).Info, intersections, toBeBuiltEllipse, ControlVertices, GetFollowTerrain(),reverseDirection); 71 | finalConnector.Build(); 72 | 73 | // Easter egg 74 | RoundAboutBuilder.EasterEggToggle(); 75 | } 76 | catch (ActionException e) 77 | { 78 | UIWindow.instance.ThrowErrorMsg(e.Message); 79 | } 80 | catch (Exception e) 81 | { 82 | Debug.LogError(e); 83 | UIWindow.instance.ThrowErrorMsg(e.ToString(), true); 84 | } 85 | 86 | } 87 | 88 | protected override void OnClick() 89 | { 90 | base.OnClick(); 91 | if (m_hoverNode != 0) 92 | { 93 | switch (stage) 94 | { 95 | case Stage.CentralPoint: 96 | centralNode = m_hoverNode; 97 | stage = Stage.MainAxis; 98 | UIWindow.instance.SwitchWindow(UIWindow.instance.P_EllipsePanel_2); 99 | break; 100 | case Stage.MainAxis: 101 | if (m_hoverNode == centralNode) 102 | { 103 | UIWindow.instance.ThrowErrorMsg("You selected the same node!"); 104 | } 105 | else 106 | { 107 | axisNode = m_hoverNode; 108 | stage = Stage.Final; 109 | UIWindow.instance.SwitchWindow(UIWindow.instance.P_EllipsePanel_3); 110 | } 111 | break; 112 | } 113 | } 114 | } 115 | 116 | /* Returns radii */ 117 | private bool Radius(out float radius1, out float radius2) 118 | { 119 | radius1 = radius2 = -1; 120 | float? radius1Q = UIWindow.instance.P_EllipsePanel_3.Radius1tf.Value; 121 | float? radius2Q = UIWindow.instance.P_EllipsePanel_3.Radius2tf.Value; 122 | if (radius1Q == null || radius2Q == null) 123 | { 124 | return false; 125 | } 126 | 127 | radius1 = (float)radius1Q; 128 | radius2 = (float)radius2Q; 129 | 130 | if (radius2 > radius1) 131 | { 132 | return false; 133 | } 134 | return true; 135 | } 136 | 137 | public override void IncreaseButton() 138 | { 139 | if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) { 140 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.Increase(); 141 | } else if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) { 142 | UIWindow.instance.P_EllipsePanel_3.Radius2tf.Increase(); 143 | } else 144 | { 145 | 146 | int newRadius1 = 0; 147 | int newRadius2 = 0; 148 | if (!Radius(out float radius1, out float radius2)) 149 | { 150 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.text = RADIUS1_DEF.ToString(); 151 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.text = RADIUS2_DEF.ToString(); 152 | return; 153 | } 154 | else 155 | { 156 | double ratio = (double)radius2 / (double)radius1; 157 | newRadius1 = Convert.ToInt32(Math.Ceiling(new decimal(radius1 + 1) / new decimal(5))) * 5; 158 | newRadius2 = Convert.ToInt32(ratio * newRadius1); 159 | } 160 | if (UIWindow.instance.P_EllipsePanel_3.Radius1tf.IsValid(newRadius1) && UIWindow.instance.P_EllipsePanel_3.Radius2tf.IsValid(newRadius2) && newRadius1 >= newRadius2) 161 | { 162 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.text = newRadius1.ToString(); 163 | UIWindow.instance.P_EllipsePanel_3.Radius2tf.text = newRadius2.ToString(); 164 | } 165 | 166 | } 167 | } 168 | 169 | public override void DecreaseButton() 170 | { 171 | if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) 172 | { 173 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.Decrease(); 174 | } 175 | else if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) 176 | { 177 | UIWindow.instance.P_EllipsePanel_3.Radius2tf.Decrease(); 178 | } 179 | else 180 | { 181 | 182 | int newRadius1 = 0; 183 | int newRadius2 = 0; 184 | if (!Radius(out float radius1, out float radius2)) 185 | { 186 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.text = RADIUS1_DEF.ToString(); 187 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.text = RADIUS2_DEF.ToString(); 188 | return; 189 | } 190 | else 191 | { 192 | double ratio = (double)radius2 / (double)radius1; 193 | newRadius1 = Convert.ToInt32(Math.Floor(new decimal(radius1 - 1) / new decimal(5))) * 5; 194 | newRadius2 = Convert.ToInt32(ratio * newRadius1); 195 | } 196 | if (UIWindow.instance.P_EllipsePanel_3.Radius1tf.IsValid(newRadius1) && UIWindow.instance.P_EllipsePanel_3.Radius2tf.IsValid(newRadius2) && newRadius1 >= newRadius2) 197 | { 198 | UIWindow.instance.P_EllipsePanel_3.Radius1tf.text = newRadius1.ToString(); 199 | UIWindow.instance.P_EllipsePanel_3.Radius2tf.text = newRadius2.ToString(); 200 | } 201 | 202 | } 203 | } 204 | 205 | protected override void OnDisable() 206 | { 207 | base.OnDisable(); 208 | ellipse = null; 209 | GoToFirstStage(); 210 | } 211 | 212 | public override void GoToFirstStage() 213 | { 214 | stage = Stage.CentralPoint; 215 | if(UIWindow.instance != null && UIWindow.instance.P_EllipsePanel_1 != null 216 | && (UIWindow.instance.m_panelOnUI == UIWindow.instance.P_EllipsePanel_2 || UIWindow.instance.m_panelOnUI == UIWindow.instance.P_EllipsePanel_3)) // hell 217 | { 218 | UIWindow.instance.SwitchWindow(UIWindow.instance.P_EllipsePanel_1); 219 | } 220 | } 221 | 222 | /* This draws UI shapes on the map. */ 223 | protected override void RenderOverlayExtended(RenderManager.CameraInfo cameraInfo) 224 | { 225 | //debugDrawMethod(cameraInfo); 226 | try 227 | { 228 | if (!insideUI) 229 | { 230 | if (m_hoverNode != 0 && (stage == Stage.CentralPoint || stage == Stage.MainAxis)) 231 | { 232 | NetNode hoveredNode = NetUtil.Node(m_hoverNode); 233 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.black, hoveredNode.m_position, 15f, hoveredNode.m_position.y - 1f, hoveredNode.m_position.y + 1f, true, true); 234 | UIWindow.instance.m_hoveringLabel.isVisible = false; 235 | } 236 | else if (stage == Stage.CentralPoint) 237 | { 238 | RenderMousePositionCircle(cameraInfo); 239 | RenderHoveringLabel("Select center of elliptic roundabout"); 240 | } 241 | else if (stage == Stage.MainAxis) 242 | { 243 | RenderMousePositionCircle(cameraInfo); 244 | RenderHoveringLabel("Select any intersection in direction of main axis"); 245 | } 246 | else 247 | { 248 | UIWindow.instance.m_hoveringLabel.isVisible = false; 249 | } 250 | } 251 | 252 | if (stage == Stage.MainAxis || stage == Stage.Final) 253 | { 254 | NetNode centralNodeDraw = NetUtil.Node(centralNode); 255 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.red, centralNodeDraw.m_position, 15f, centralNodeDraw.m_position.y - 1f, centralNodeDraw.m_position.y + 1f, true, true); 256 | } 257 | if (stage == Stage.Final) 258 | { 259 | NetNode axisNodeDraw = NetUtil.Node(axisNode); 260 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.green, axisNodeDraw.m_position, 15f, axisNodeDraw.m_position.y - 1f, axisNodeDraw.m_position.y + 1f, true, true); 261 | 262 | if(Radius(out float radius1, out float radius2)) 263 | { 264 | /* If the radiuses didn't change, we don't have to generate new ellipse. */ 265 | if(radius1 == prevRadius1 && radius2 == prevRadius2 && ellipse != null) 266 | { 267 | DrawEllipse(cameraInfo); 268 | } 269 | else 270 | { 271 | prevRadius1 = radius1; 272 | prevRadius2 = radius2; 273 | ellipse = new Ellipse(NetUtil.Node(centralNode).m_position, NetUtil.Node(axisNode).m_position - NetUtil.Node(centralNode).m_position, radius1, radius2); 274 | DrawEllipse(cameraInfo); 275 | } 276 | } 277 | else 278 | { 279 | ellipse = null; 280 | } 281 | } 282 | } 283 | catch(Exception e) 284 | { 285 | Debug.Log("Catched exception while rendering ellipse UI."); 286 | Debug.Log(e); 287 | stage = Stage.CentralPoint; 288 | enabled = false; // ??? 289 | } 290 | } 291 | 292 | private void DrawEllipse(RenderManager.CameraInfo cameraInfo) 293 | { 294 | foreach(Bezier2 bezier in ellipse.Beziers) 295 | { 296 | RenderManager.instance.OverlayEffect.DrawBezier(cameraInfo, Color.red, ShiftTo3D(bezier), 0.1f, 0, 0, -1f, 1280f, false, true); 297 | } 298 | } 299 | 300 | public static Bezier3 ShiftTo3D(Bezier2 bezier) 301 | { 302 | Vector3 v1 = new Vector3(bezier.a.x, 0, bezier.a.y); 303 | Vector3 v2 = new Vector3(bezier.b.x, 0, bezier.b.y); 304 | Vector3 v3 = new Vector3(bezier.c.x, 0, bezier.c.y); 305 | Vector3 v4 = new Vector3(bezier.d.x, 0, bezier.d.y); 306 | return new Bezier3(v1, v2, v3, v4); 307 | } 308 | 309 | /* Debug methods */ 310 | 311 | /* Allow me tho draw vectors and points on the map */ 312 | 313 | /*private void debugDrawMethod(RenderManager.CameraInfo cameraInfo) 314 | { 315 | if (debugDraw == null) return; 316 | foreach (Bezier2 bezier in debugDraw) 317 | { 318 | if(yes) 319 | RenderManager.instance.OverlayEffect.DrawBezier(cameraInfo, Color.yellow, ShiftTo3D(bezier), 0.1f, 0, 0, -1f, 1280f, false, true); 320 | else 321 | RenderManager.instance.OverlayEffect.DrawBezier(cameraInfo, Color.blue, ShiftTo3D(bezier), 0.1f, 0, 0, -1f, 1280f, false, true); 322 | 323 | yes = !yes; 324 | } 325 | foreach (Vector3 vector in debugDrawPositions) 326 | { 327 | RenderManager.instance.OverlayEffect.DrawCircle(cameraInfo, Color.cyan, vector, 15f, vector.y - 1f, vector.y + 1f, true, true); 328 | } 329 | } 330 | 331 | public void debugDrawVector(Vector3 vector, Vector3 startPos) 332 | { 333 | Vector2 vector2d = new Vector2(vector.x, vector.z); 334 | Vector2 a = new Vector2(startPos.x, startPos.z); 335 | Vector2 b = a + (vector2d * (1 / 3)); 336 | Vector2 c = a + (vector2d * (2 / 3)); 337 | Vector2 d = a + vector2d; 338 | debugDraw.Add(new Bezier2(a, b, c, d)); 339 | } 340 | 341 | public void debugDrawPositionVector(Vector3 vector) 342 | { 343 | debugDrawPositions.Add(vector); 344 | }*/ 345 | 346 | /*private float debugDistanceXZ (Vector3 v1, Vector3 v2) 347 | { 348 | return (float) Math.Sqrt((v1.x - v2.x) * (v1.x - v2.x) + (v1.z - v2.z)*(v1.z - v2.z)); 349 | }*/ 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /RoundaboutBuilder/Tools/NetUtil.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.Math; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using UnityEngine; 10 | using RoundaboutBuilder.UI; 11 | 12 | namespace RoundaboutBuilder.Tools 13 | { 14 | /* By Strad, 2019 */ 15 | 16 | /* This is a little library which makes net stuff (working with segments/nodes) easier. Feel free to reuse it */ 17 | 18 | public static class NetUtil 19 | { 20 | public const int DEFAULT_ELEVATTION_STEP = 3; 21 | 22 | public static bool ReleaseSegment(ushort id, bool tryReleaseNodes = false, bool suppressWarnings = false) 23 | { 24 | if (id > 0 && (NetManager.instance.m_segments.m_buffer[id].m_flags & NetSegment.Flags.Created) != NetSegment.Flags.None) 25 | { 26 | try 27 | { 28 | ushort node1 = Segment(id).m_startNode; 29 | ushort node2 = Segment(id).m_endNode; 30 | NetManager.instance.ReleaseSegment(id, true); 31 | if (tryReleaseNodes) 32 | { 33 | ReleaseNode(node1, true); 34 | ReleaseNode(node2, true); 35 | } 36 | return true; 37 | } 38 | catch (Exception e) 39 | { 40 | if (!suppressWarnings) Debug.LogWarning("Exception while releasing segment " + id + ": " + e); 41 | return false; 42 | } 43 | } 44 | else 45 | { 46 | if (!suppressWarnings) Debug.LogWarning("Failed to release NetSegment " + id + ": Segment does not exist"); 47 | return false; 48 | } 49 | } 50 | 51 | public static bool ReleaseNode(ushort id, bool suppressWarnings = false) 52 | { 53 | if ((Node(id).m_flags & NetNode.Flags.Created) == NetNode.Flags.None) 54 | { 55 | if (!suppressWarnings) Debug.LogWarning("Failed to release NetNode " + id + ": Not Created"); 56 | return false; 57 | } 58 | 59 | if (Node(id).CountSegments() > 0) 60 | { 61 | if (!suppressWarnings) Debug.LogWarning("Failed to release NetNode " + id + ": Has segments"); 62 | return false; 63 | } 64 | 65 | if (id > 0 && (NetManager.instance.m_nodes.m_buffer[id].m_flags & NetNode.Flags.Created) != NetNode.Flags.None) 66 | { 67 | NetManager.instance.ReleaseNode(id); 68 | return true; 69 | } 70 | else 71 | { 72 | if (!suppressWarnings) Debug.LogWarning("Failed to release NetNode " + id); 73 | return false; 74 | } 75 | } 76 | 77 | public static int GetElevationStep() 78 | { 79 | if (ModLoadingExtension.networkAnarchyDetected) 80 | { 81 | try 82 | { 83 | return GetNAElevationStep(); 84 | } 85 | catch { } 86 | } 87 | else if (ModLoadingExtension.fineRoadToolDetected) 88 | { 89 | try 90 | { 91 | return GetFRTElevationStep(); 92 | } 93 | catch { } 94 | } 95 | 96 | switch (Singleton.instance.m_elevationDivider) 97 | { 98 | case 1: return 12; 99 | case 2: return 6; 100 | case 4: return 3; 101 | default: 102 | Debug.LogWarning($"RoundaboutBuilder: Unreachable code. NetToool.m_elvationDivider={Singleton.instance.m_elevationDivider}"); 103 | return DEFAULT_ELEVATTION_STEP; 104 | } 105 | } 106 | 107 | /// 108 | /// Precondition: NA must be enabled 109 | /// Note: Must not be inlined. 110 | /// Throws exception if NetworkAnarchy dll was not found. 111 | /// 112 | /// value of NA elevation slider or DEFAULT_ELEVATTION if slider is uninitialized 113 | /// 114 | [MethodImpl(MethodImplOptions.NoInlining)] 115 | private static int GetNAElevationStep() 116 | { 117 | // The method including the follwoing line throws exceptipn if NetworkAnarchy dll is not present. 118 | // this line must not be inlined. 119 | var ret = NetworkAnarchy.NetworkAnarchy.instance?.elevationStep ?? 0; 120 | 121 | // if user has never clicked on Network Anarchy, NA slider is uninitialized. 122 | return ret != 0 ? ret : DEFAULT_ELEVATTION_STEP; 123 | } 124 | 125 | /// 126 | /// Precondition: FRT must be enabled 127 | /// Note: Must not be inlined. 128 | /// Throws exception if FineRoadTool dll was not found. 129 | /// 130 | /// value of FRT elevation slider or DEFAULT_ELEVATTION if slider is uninitialized 131 | /// 132 | [MethodImpl(MethodImplOptions.NoInlining)] 133 | private static int GetFRTElevationStep() 134 | { 135 | // The method including the follwoing line throws exceptipn if FineRoadTool dll is not present. 136 | // this line must not be inlined. 137 | var ret = FineRoadTool.FineRoadTool.instance?.elevationStep ?? 0; 138 | 139 | // if user has never clicked on Road Tool, FRT slider is uninitialized. 140 | return ret != 0 ? ret : DEFAULT_ELEVATTION_STEP; 141 | } 142 | 143 | public static int GetElevation() 144 | { 145 | var prefab = UIWindow.instance.dropDown.Value; 146 | var ret = GetElevation(prefab); 147 | Debug.Log("GetElevation() returns " + (int)ret); 148 | return (int)ret; 149 | } 150 | 151 | private static float GetElevation(NetInfo info) 152 | { 153 | if (ModLoadingExtension.networkAnarchyDetected) 154 | { 155 | try 156 | { 157 | return GetNAElevation(); 158 | } 159 | catch { } 160 | } 161 | if (ModLoadingExtension.fineRoadToolDetected) 162 | { 163 | try 164 | { 165 | return GetFRTElevation(); 166 | } 167 | catch { } 168 | } 169 | return (float)typeof(NetTool). 170 | GetMethod("GetElevation", BindingFlags.NonPublic | BindingFlags.Instance). 171 | Invoke(Singleton.instance, new object[] { info }); 172 | } 173 | 174 | /// 175 | /// Precondition: NA must be enabled 176 | /// Note: Must not be inlined. 177 | /// Throws exception if NetworkAnarchy dll was not found. 178 | /// 179 | [MethodImpl(MethodImplOptions.NoInlining)] 180 | private static int GetNAElevation() 181 | { 182 | // The method including the follwoing line throws exceptipn if NetworkAnarchy dll is not present. 183 | // this line must not be inlined. 184 | return NetworkAnarchy.NetworkAnarchy.instance?.elevation ?? 0; 185 | } 186 | 187 | /// 188 | /// Precondition: FRT must be enabled 189 | /// Note: Must not be inlined. 190 | /// Throws exception if FineRoadTool dll was not found. 191 | /// 192 | [MethodImpl(MethodImplOptions.NoInlining)] 193 | private static int GetFRTElevation() 194 | { 195 | // The method including the follwoing line throws exceptipn if FineRoadTool dll is not present. 196 | // this line must not be inlined. 197 | return FineRoadTool.FineRoadTool.instance?.elevation ?? 0; 198 | } 199 | 200 | public static byte GetElevation(Vector3 position, NetAI net_ai) 201 | { 202 | if (!net_ai.IsUnderground() && !net_ai.IsOverground()) 203 | { 204 | return 0; // on ground. 205 | } 206 | 207 | net_ai.GetElevationLimits(out int min, out int max); 208 | if (min == max) 209 | { 210 | return 0; // From NetTool.GetElevation() 211 | } 212 | 213 | float elevation = position.y - TerrainHeight(position); 214 | 215 | #if DEBUG 216 | // tolerated error = +-1 217 | if (!(min * 12 - 1 <= elevation && elevation <= max * 12 + 1)) 218 | Debug.LogWarning($"RoundaboutBuilder: ELevation out of range expected {min * 12 - 1} <= {elevation} <={max * 12 + 1}"); 219 | #endif 220 | 221 | elevation = Mathf.Clamp(elevation, min * 12, max * 12); // 12 is from NetTool.GetElevation() 222 | elevation = Mathf.Abs(elevation); 223 | return (byte)Mathf.Clamp(elevation, 1, 255); // underground/overground road should not have 0 elevation. 224 | } 225 | 226 | public static float TerrainHeight(Vector3 position) 227 | { 228 | return Singleton.instance.SampleDetailHeightSmooth(position); 229 | } 230 | 231 | public static ushort CreateNode(NetInfo info, Vector3 position) 232 | { 233 | var randomizer = Singleton.instance.m_randomizer; 234 | bool result = NetManager.instance.CreateNode(out ushort nodeId, ref randomizer, info, position, 235 | Singleton.instance.m_currentBuildIndex); 236 | 237 | if (!result) 238 | throw new Exception("Failed to create NetNode at " + position.ToString()); 239 | 240 | NetManager.instance.m_nodes.m_buffer[nodeId].m_elevation = GetElevation(position, info.m_netAI); 241 | 242 | Singleton.instance.m_currentBuildIndex++; 243 | 244 | return nodeId; 245 | } 246 | 247 | public static ushort CreateSegment(ushort startNodeId, ushort endNodeId, Vector3 startDirection, Vector3 endDirection, NetInfo netInfo, bool invert = false, bool switchStartAndEnd = false, bool dispatchPlacementEffects = false) 248 | { 249 | var randomizer = Singleton.instance.m_randomizer; 250 | 251 | NetNode startNode = Node(startNodeId); 252 | NetNode endNode = Node(endNodeId); 253 | 254 | if ((startNode.m_flags & NetNode.Flags.Created) == NetNode.Flags.None || (endNode.m_flags & NetNode.Flags.Created) == NetNode.Flags.None) 255 | throw new Exception("Failed to create NetSegment: Invalid node(s)"); 256 | 257 | var result = NetManager.instance.CreateSegment(out ushort newSegmentId, ref randomizer, netInfo, switchStartAndEnd ? endNodeId : startNodeId, 258 | switchStartAndEnd ? startNodeId : endNodeId, 259 | (switchStartAndEnd ? endDirection : startDirection), (switchStartAndEnd ? startDirection : endDirection), Singleton.instance.m_currentBuildIndex, 260 | Singleton.instance.m_currentBuildIndex, invert); 261 | 262 | if (!result) 263 | throw new Exception("Failed to create NetSegment"); 264 | 265 | Singleton.instance.m_currentBuildIndex++; 266 | 267 | if (dispatchPlacementEffects) 268 | { 269 | bool smoothStart = (startNode.m_flags & NetNode.Flags.Middle) != NetNode.Flags.None; 270 | bool smoothEnd = (endNode.m_flags & NetNode.Flags.Middle) != NetNode.Flags.None; 271 | NetSegment.CalculateMiddlePoints(startNode.m_position, startDirection, endNode.m_position, endDirection, smoothStart, smoothEnd, out Vector3 b, out Vector3 c); 272 | NetTool.DispatchPlacementEffect(startNode.m_position, b, c, endNode.m_position, netInfo.m_halfWidth, false); 273 | } 274 | 275 | return newSegmentId; 276 | } 277 | 278 | public static bool ExistsSegment(ushort segmentId) 279 | { 280 | return ExistsSegment(Manager.m_segments.m_buffer[segmentId]); 281 | } 282 | 283 | public static bool ExistsSegment(NetSegment segment) 284 | { 285 | return (segment.m_flags & NetSegment.Flags.Created) != NetSegment.Flags.None; 286 | } 287 | 288 | public static bool ExistsNode(ushort nodeId) 289 | { 290 | return ExistsNode(Manager.m_nodes.m_buffer[nodeId]); 291 | } 292 | 293 | public static bool ExistsNode(NetNode node) 294 | { 295 | return (node.m_flags & NetNode.Flags.Created) != NetNode.Flags.None; 296 | } 297 | 298 | public static ushort NetinfoToIndex(NetInfo netInfo) 299 | { 300 | return (ushort)Mathf.Clamp(netInfo.m_prefabDataIndex, 0, 65535); 301 | } 302 | 303 | public static NetInfo NetinfoFromIndex(ushort index) 304 | { 305 | return PrefabCollection.GetPrefab(index); 306 | } 307 | 308 | /* From Elektrix */ 309 | public static bool DoSegmentsIntersect(ushort segment1, ushort segment2, out float t1, out float t2) 310 | { 311 | // First segment data 312 | NetSegment s1 = Segment(segment1); 313 | Bezier3 bezier = default(Bezier3); 314 | 315 | // Second segment data 316 | NetSegment s2 = Segment(segment2); 317 | Bezier3 secondBezier = default(Bezier3); 318 | 319 | // Turn the segment data into a Bezier2 for easier calculations supported by the game 320 | bezier.a = Node(s1.m_startNode).m_position; 321 | bezier.d = Node(s1.m_endNode).m_position; 322 | 323 | bool smoothStart = (Singleton.instance.m_nodes.m_buffer[s1.m_startNode].m_flags & NetNode.Flags.Middle) != NetNode.Flags.None; 324 | bool smoothEnd = (Singleton.instance.m_nodes.m_buffer[s1.m_endNode].m_flags & NetNode.Flags.Middle) != NetNode.Flags.None; 325 | 326 | NetSegment.CalculateMiddlePoints(bezier.a, s1.m_startDirection, bezier.d, s1.m_endDirection, smoothStart, smoothEnd, out bezier.b, out bezier.c); 327 | 328 | Bezier2 xz = Bezier2.XZ(bezier); 329 | 330 | // Second segment: 331 | secondBezier.a = Node(s2.m_startNode).m_position; 332 | secondBezier.d = Node(s2.m_endNode).m_position; 333 | 334 | smoothStart = (Singleton.instance.m_nodes.m_buffer[s2.m_startNode].m_flags & NetNode.Flags.Middle) != NetNode.Flags.None; 335 | smoothEnd = (Singleton.instance.m_nodes.m_buffer[s2.m_endNode].m_flags & NetNode.Flags.Middle) != NetNode.Flags.None; 336 | 337 | NetSegment.CalculateMiddlePoints(secondBezier.a, s2.m_startDirection, secondBezier.d, s2.m_endDirection, smoothStart, smoothEnd, out secondBezier.b, out secondBezier.c); 338 | 339 | Bezier2 xz2 = Bezier2.XZ(secondBezier); 340 | 341 | return xz.Intersect(xz2, out t1, out t2, 5); 342 | } 343 | 344 | /// 345 | /// Returns first non-zero segment found 346 | /// 347 | /// 348 | /// 349 | public static ushort GetNonzeroSegment(ushort nodeId, int index) 350 | { 351 | return GetNonzeroSegment(Node(nodeId), index); 352 | } 353 | 354 | public static ushort GetNonzeroSegment(NetNode node, int index) 355 | { 356 | for (int i = 0; i < 8; i++) 357 | { 358 | if (node.GetSegment(i) != 0) 359 | { 360 | if (index == 0) 361 | { 362 | return node.GetSegment(i); 363 | } 364 | index--; 365 | } 366 | } 367 | return 0; 368 | } 369 | 370 | public static bool IsOneWay(NetInfo netInfo) 371 | { 372 | return netInfo.m_hasForwardVehicleLanes ^ netInfo.m_hasBackwardVehicleLanes; 373 | } 374 | 375 | public static IEnumerable SegmentsFromMask(ulong[] segmentMask) 376 | { 377 | int num = segmentMask.Length; 378 | for (int i = 0; i < num; i++) 379 | { 380 | ulong num3 = segmentMask[i]; 381 | if (num3 != 0UL) 382 | { 383 | for (int j = 0; j < 64; j++) 384 | { 385 | if ((num3 & 1UL << j) != 0UL) 386 | { 387 | int num4 = i << 6 | j; 388 | if (num4 != 0) 389 | { 390 | yield return num4; 391 | } 392 | } 393 | } 394 | } 395 | } 396 | } 397 | 398 | /* As always */ 399 | 400 | public static NetManager Manager 401 | { 402 | get { return Singleton.instance; } 403 | } 404 | public static ref NetNode Node(ushort id) 405 | { 406 | return ref Manager.m_nodes.m_buffer[id]; 407 | } 408 | public static ref NetSegment Segment(ushort id) 409 | { 410 | return ref Manager.m_segments.m_buffer[id]; 411 | } 412 | 413 | // Fastlist extension 414 | public static bool Contains(this FastList list, T item) 415 | { 416 | foreach (T item2 in list) 417 | { 418 | if (item2.Equals(item)) 419 | return true; 420 | } 421 | return false; 422 | } 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /RoundaboutBuilder/UI/UIWindow.cs: -------------------------------------------------------------------------------- 1 | using ColossalFramework; 2 | using ColossalFramework.PlatformServices; 3 | using ColossalFramework.UI; 4 | using System; 5 | using UnityEngine; 6 | 7 | /* Version RELEASE 1.4.0+ */ 8 | 9 | /* By Strad, 2019 */ 10 | 11 | /* Credit to SamsamTS and T__S from whom I copied big chunks of code */ 12 | 13 | /* Welcome to hell on earth */ 14 | 15 | namespace RoundaboutBuilder.UI 16 | { 17 | public class UIWindow : UIPanel 18 | { 19 | public static UIWindow instance; 20 | 21 | private static readonly SavedBool SavedKeepOpen = new SavedBool("savedKeepOpen", RoundAboutBuilder.settingsFileName, false, true); 22 | public static readonly SavedBool SavedSetupTmpe = new SavedBool("savedSetupTMPE", RoundAboutBuilder.settingsFileName, true, true); 23 | public static readonly SavedBool SavedFollowTerrain = new SavedBool("savedFollowTerrain", RoundAboutBuilder.settingsFileName, false, true); 24 | 25 | public ToolBaseExtended toolOnUI; 26 | public AbstractPanel m_panelOnUI; 27 | private AbstractPanel m_lastStandardPanel; 28 | 29 | public RoundAboutPanel P_RoundAboutPanel; 30 | public EllipsePanel_1 P_EllipsePanel_1; 31 | public EllipsePanel_2 P_EllipsePanel_2; 32 | public EllipsePanel_3 P_EllipsePanel_3; 33 | public TmpeSetupPanel P_TmpeSetupPanel; 34 | public FreeToolPanel P_FreeToolPanel; 35 | 36 | public HoveringLabel m_hoveringLabel; 37 | 38 | private UIPanel m_topSection; 39 | private UIPanel m_bottomSection; 40 | private UIPanel m_setupTmpeSection; 41 | private UIPanel m_followTerrainSection; 42 | private UIButton backButton; 43 | private UIButton closeButton; 44 | public UIButton undoButton; 45 | 46 | public UINetInfoDropDown dropDown; 47 | 48 | public UIWindow() 49 | { 50 | instance = this; 51 | } 52 | 53 | public override void Start() 54 | { 55 | CreateOptionPanel(); 56 | } 57 | 58 | protected override void OnMouseDown(UIMouseEventParameter p) 59 | { 60 | base.OnMouseDown(p); 61 | if (toolOnUI != null && !m_panelOnUI.IsSpecialWindow) 62 | toolOnUI.enabled = true; // Reenable the tool when user clicks in the window 63 | } 64 | 65 | private void CreateOptionPanel() 66 | { 67 | name = "RAB_ToolOptionsPanel"; 68 | atlas = ResourceLoader.GetAtlas("Ingame"); 69 | backgroundSprite = "SubcategoriesPanel"; 70 | size = new Vector2(204, 180); 71 | absolutePosition = new Vector3(RoundAboutBuilder.savedWindowX.value, RoundAboutBuilder.savedWindowY.value); 72 | 73 | isVisible = false; 74 | 75 | //DebugUtils.Log("absolutePosition: " + absolutePosition); 76 | 77 | eventPositionChanged += (c, p) => 78 | { 79 | if (absolutePosition.x < 0) 80 | absolutePosition = RoundAboutBuilder.defWindowPosition; 81 | 82 | Vector2 resolution = GetUIView().GetScreenResolution(); 83 | 84 | absolutePosition = new Vector2( 85 | Mathf.Clamp(absolutePosition.x, 0, resolution.x - width), 86 | Mathf.Clamp(absolutePosition.y, 0, resolution.y - height)); 87 | 88 | RoundAboutBuilder.savedWindowX.value = (int)absolutePosition.x; 89 | RoundAboutBuilder.savedWindowY.value = (int)absolutePosition.y; 90 | }; 91 | 92 | UIDragHandle dragHandle = AddUIComponent(); 93 | dragHandle.width = width; 94 | dragHandle.relativePosition = Vector3.zero; 95 | dragHandle.target = parent; 96 | 97 | P_TmpeSetupPanel = AddUIComponent(); 98 | InitPanels(); 99 | 100 | // From Elektrix's Road Tools 101 | UIButton openDescription = AddUIComponent(); 102 | openDescription.relativePosition = new Vector3(width - 24f, 8f); 103 | openDescription.size = new Vector3(15f, 15f); 104 | openDescription.normalFgSprite = "ToolbarIconHelp"; 105 | openDescription.name = "RAB_workshopButton"; 106 | openDescription.tooltip = "Roundabout Builder [" + RoundAboutBuilder.VERSION + "] by Strad\nOpen in Steam Workshop"; 107 | UIUtil.SetupButtonStateSprites(ref openDescription, "OptionBase", true); 108 | if (!PlatformService.IsOverlayEnabled()) 109 | { 110 | openDescription.isVisible = false; 111 | openDescription.isEnabled = false; 112 | } 113 | openDescription.eventClicked += delegate (UIComponent component, UIMouseEventParameter click) 114 | { 115 | if (PlatformService.IsOverlayEnabled() && RoundAboutBuilder.WORKSHOP_FILE_ID != null) 116 | { 117 | PlatformService.ActivateGameOverlayToWorkshopItem(RoundAboutBuilder.WORKSHOP_FILE_ID); 118 | } 119 | openDescription.Unfocus(); 120 | }; 121 | // -- Elektrix 122 | 123 | float cummulativeHeight = 8; 124 | 125 | /* Top section */ 126 | m_topSection = AddUIComponent(); 127 | m_topSection.relativePosition = new Vector2(0, 0); 128 | m_topSection.SendToBack(); 129 | 130 | UILabel label = m_topSection.AddUIComponent(); 131 | label.textScale = 0.9f; 132 | label.text = "Roundabout Builder"; 133 | label.relativePosition = new Vector2(8, cummulativeHeight); 134 | label.SendToBack(); 135 | cummulativeHeight += label.height + 8; 136 | 137 | m_topSection.height = cummulativeHeight; 138 | m_topSection.width = width; 139 | dragHandle.height = cummulativeHeight; 140 | 141 | dropDown = AddUIComponent(); 142 | //dropDown.relativePosition = new Vector2(8, cummulativeHeight); 143 | dropDown.width = width - 16; 144 | //cummulativeHeight += dropDown.height + 8; 145 | 146 | 147 | /* Bottom section */ 148 | 149 | m_followTerrainSection = AddUIComponent(); 150 | m_followTerrainSection.width = 204f; 151 | m_followTerrainSection.clipChildren = true; 152 | var chbFollowTerrain = UIUtil.CreateCheckBox(m_followTerrainSection); 153 | chbFollowTerrain.name = "RAB_followTerrain"; 154 | chbFollowTerrain.label.text = "Level terrain"; 155 | chbFollowTerrain.tooltip = "Make the roundabout the same height everywhere"; 156 | chbFollowTerrain.isChecked = !SavedFollowTerrain; 157 | chbFollowTerrain.relativePosition = new Vector3(8, 0); 158 | chbFollowTerrain.eventCheckChanged += (c, state) => 159 | { 160 | // Let's make it confusing - the variable and the chackbox will have opposite values 161 | SavedFollowTerrain.value = !state; 162 | }; 163 | m_followTerrainSection.height = chbFollowTerrain.height + 8; 164 | 165 | m_setupTmpeSection = AddUIComponent(); 166 | m_setupTmpeSection.width = 204f; 167 | m_setupTmpeSection.clipChildren = true; 168 | var setupTmpe = UIUtil.CreateCheckBox(m_setupTmpeSection); 169 | setupTmpe.name = "RAB_setupTmpe"; 170 | setupTmpe.label.text = "Set up TMPE"; 171 | setupTmpe.tooltip = "Apply TMPE policies to the roundabout"; 172 | setupTmpe.isChecked = SavedSetupTmpe; 173 | setupTmpe.relativePosition = new Vector3(8, 0); 174 | setupTmpe.eventCheckChanged += (c, state) => 175 | { 176 | SavedSetupTmpe.value = state; 177 | }; 178 | var tmpeButton = UIUtil.CreateButton(m_setupTmpeSection); 179 | tmpeButton.text = "..."; 180 | tmpeButton.tooltip = "TMPE settings"; 181 | tmpeButton.height = setupTmpe.height; 182 | tmpeButton.width = 30; 183 | tmpeButton.relativePosition = new Vector2(width - tmpeButton.width - 8, 0); 184 | tmpeButton.eventClick += (c, p) => 185 | { 186 | toolOnUI.enabled = false; 187 | SwitchWindow(P_TmpeSetupPanel); 188 | }; 189 | m_setupTmpeSection.height = setupTmpe.height + 8; 190 | //cummulativeHeight += keepOpen.height + 8; 191 | 192 | cummulativeHeight = 0; 193 | m_bottomSection = AddUIComponent(); 194 | 195 | var keepOpen = UIUtil.CreateCheckBox(m_bottomSection); 196 | keepOpen.name = "RAB_keepOpen"; 197 | keepOpen.label.text = "Keep open"; 198 | keepOpen.tooltip = "Keep the window open after roundabout is built"; 199 | keepOpen.isChecked = SavedKeepOpen.value; 200 | keepOpen.relativePosition = new Vector3(8, cummulativeHeight); 201 | keepOpen.width = 196; // width - padding 202 | keepOpen.eventCheckChanged += (c, state) => 203 | { 204 | SavedKeepOpen.value = state; 205 | }; 206 | cummulativeHeight += keepOpen.height + 8; 207 | 208 | // Back button 209 | backButton = UIUtil.CreateButton(m_bottomSection); 210 | backButton.text = "Back"; 211 | backButton.relativePosition = new Vector2(8, cummulativeHeight); 212 | backButton.width = width - 16; 213 | backButton.eventClick += (c, p) => 214 | { 215 | if (backButton.isVisible) 216 | { 217 | if (m_panelOnUI.IsSpecialWindow) 218 | { 219 | SwitchWindow(m_lastStandardPanel); 220 | toolOnUI.enabled = true; 221 | } 222 | else 223 | { 224 | toolOnUI.GoToFirstStage(); 225 | SwitchTool(RoundaboutTool.Instance); 226 | } 227 | } 228 | }; 229 | 230 | cummulativeHeight += backButton.height + 8; 231 | 232 | closeButton = UIUtil.CreateButton(m_bottomSection); 233 | closeButton.text = "Close"; 234 | closeButton.relativePosition = new Vector2(8, cummulativeHeight); 235 | closeButton.eventClick += (c, p) => 236 | { 237 | enabled = false; 238 | if (toolOnUI != null) 239 | { 240 | toolOnUI.enabled = false; 241 | } 242 | }; 243 | 244 | undoButton = UIUtil.CreateButton(m_bottomSection); 245 | undoButton.text = "Undo"; 246 | undoButton.tooltip = "Remove last built roundabout (CTRL+Z). Warning: Use only right after the roundabout has been built"; 247 | undoButton.relativePosition = new Vector2(16 + closeButton.width, cummulativeHeight); 248 | undoButton.isEnabled = false; 249 | undoButton.eventClick += (c, p) => 250 | { 251 | ModThreading.UndoAction(); 252 | }; 253 | 254 | cummulativeHeight += closeButton.height + 8; 255 | 256 | m_bottomSection.height = cummulativeHeight; 257 | m_bottomSection.width = width; 258 | 259 | m_hoveringLabel = AddUIComponent(); 260 | m_hoveringLabel.isVisible = false; 261 | 262 | /* Enable roundabout tool as default */ 263 | SwitchTool(RoundaboutTool.Instance); 264 | 265 | // Is done by modthreading from 1.5.3 on 266 | /*try 267 | { 268 | if(RoundAboutBuilder.ShowUIButton) 269 | UIPanelButton.CreateButton(); 270 | } 271 | catch(Exception e) 272 | { 273 | Debug.LogWarning("Failed to create UI button."); 274 | Debug.LogWarning(e); 275 | }*/ 276 | 277 | 278 | enabled = false; 279 | } 280 | 281 | internal void GoBack() 282 | { 283 | if (toolOnUI != null) 284 | { 285 | toolOnUI.GoToFirstStage(); 286 | toolOnUI.enabled = false; 287 | } 288 | SwitchTool( RoundaboutTool.Instance ); 289 | } 290 | 291 | public void SwitchTool(ToolBaseExtended tool) 292 | { 293 | if (tool == toolOnUI) return; 294 | 295 | toolOnUI = tool; 296 | 297 | if (tool is EllipseTool) 298 | { 299 | SwitchWindow( P_EllipsePanel_1 ); 300 | } 301 | else if(tool is RoundaboutTool) 302 | { 303 | SwitchWindow( P_RoundAboutPanel ); 304 | }else 305 | { 306 | SwitchWindow( P_FreeToolPanel ); 307 | } 308 | 309 | tool.enabled = true; 310 | } 311 | 312 | public void InitPanels() 313 | { 314 | //bool enabledS = this.enabled; 315 | //enabled = false; 316 | if(P_RoundAboutPanel != null) 317 | { 318 | RemoveUIComponent(P_RoundAboutPanel); 319 | RemoveUIComponent(P_FreeToolPanel); 320 | } 321 | if(P_EllipsePanel_1 != null) 322 | { 323 | RemoveUIComponent(P_EllipsePanel_1); 324 | RemoveUIComponent(P_EllipsePanel_2); 325 | RemoveUIComponent(P_EllipsePanel_3); 326 | } 327 | P_RoundAboutPanel = AddUIComponent(); 328 | P_FreeToolPanel = AddUIComponent(); 329 | if(RoundAboutBuilder.LegacyEllipticRoundabouts.value) 330 | { 331 | P_EllipsePanel_1 = AddUIComponent(); 332 | P_EllipsePanel_2 = AddUIComponent(); 333 | P_EllipsePanel_3 = AddUIComponent(); 334 | } 335 | // this.enabled = enabledS; 336 | } 337 | 338 | public void SwitchWindow(AbstractPanel panel) 339 | { 340 | if (m_panelOnUI != null) 341 | { 342 | m_panelOnUI.isVisible = false; 343 | if (!m_panelOnUI.IsSpecialWindow) m_lastStandardPanel = m_panelOnUI; 344 | } 345 | 346 | panel.isVisible = true; 347 | m_panelOnUI = panel; 348 | 349 | float cumulativeHeight = m_topSection.height; 350 | 351 | if(panel.ShowDropDown) 352 | { 353 | dropDown.relativePosition = new Vector2(8, cumulativeHeight); 354 | cumulativeHeight += dropDown.height + 8; 355 | dropDown.isVisible = true; 356 | } 357 | else 358 | { 359 | dropDown.isVisible = false; 360 | } 361 | dropDown.Populate(true); 362 | 363 | panel.relativePosition = new Vector2(0, cumulativeHeight); 364 | cumulativeHeight += panel.height; 365 | 366 | if (panel.ShowFollowTerrain) 367 | { 368 | m_followTerrainSection.relativePosition = new Vector2(0, cumulativeHeight); 369 | cumulativeHeight += m_followTerrainSection.height; 370 | m_followTerrainSection.isVisible = true; 371 | } 372 | else 373 | { 374 | m_followTerrainSection.isVisible = false; 375 | } 376 | 377 | if (ModLoadingExtension.tmpeDetected && panel.ShowTmpeSetup) 378 | { 379 | m_setupTmpeSection.relativePosition = new Vector2(0, cumulativeHeight); 380 | cumulativeHeight += m_setupTmpeSection.height; 381 | m_setupTmpeSection.isVisible = true; 382 | } 383 | else 384 | { 385 | m_setupTmpeSection.isVisible = false; 386 | } 387 | 388 | // THIS IS HELL ON EARTH 389 | // Adjust position of other buttons and height of the panel if the back button is visible 390 | if(panel.ShowBackButton) 391 | { 392 | if(!backButton.enabled) 393 | { 394 | m_bottomSection.height += backButton.height + 8; 395 | closeButton.relativePosition = new Vector2(closeButton.relativePosition.x, closeButton.relativePosition.y + backButton.height + 8); 396 | undoButton.relativePosition = new Vector2(undoButton.relativePosition.x, undoButton.relativePosition.y + backButton.height + 8); 397 | } 398 | backButton.enabled = true; 399 | } 400 | else 401 | { 402 | if (backButton.enabled) 403 | { 404 | m_bottomSection.height -= backButton.height + 8; 405 | closeButton.relativePosition = new Vector2(closeButton.relativePosition.x, closeButton.relativePosition.y - backButton.height - 8); 406 | undoButton.relativePosition = new Vector2(undoButton.relativePosition.x, undoButton.relativePosition.y - backButton.height - 8); 407 | } 408 | backButton.enabled = false; 409 | } 410 | 411 | m_bottomSection.relativePosition = new Vector2(0, cumulativeHeight); 412 | cumulativeHeight += m_bottomSection.height; 413 | 414 | height = cumulativeHeight; 415 | } 416 | 417 | public void LostFocus() 418 | { 419 | if (!SavedKeepOpen) 420 | { 421 | enabled = false; 422 | } 423 | } 424 | 425 | // Toggles the tool (when clicking the mod button or pressing the shortcut) 426 | public void Toggle() 427 | { 428 | if (instance.toolOnUI != null && (instance.toolOnUI.enabled || (instance.enabled && m_panelOnUI.IsSpecialWindow))) 429 | { 430 | instance.enabled = false; 431 | } 432 | else if (instance.toolOnUI != null && instance.enabled) 433 | { 434 | instance.toolOnUI.enabled = true; 435 | } 436 | else 437 | { 438 | instance.enabled = true; 439 | } 440 | } 441 | 442 | public override void OnEnable() 443 | { 444 | base.OnEnable(); 445 | if (m_panelOnUI != null && m_lastStandardPanel != null && m_panelOnUI.IsSpecialWindow) 446 | { 447 | SwitchWindow(m_lastStandardPanel); 448 | } 449 | isVisible = true; 450 | if (toolOnUI != null) 451 | toolOnUI.enabled = true; 452 | } 453 | 454 | public override void OnDisable() 455 | { 456 | base.OnDisable(); 457 | isVisible = false; 458 | if (toolOnUI != null) 459 | toolOnUI.enabled = false; 460 | } 461 | 462 | public void ThrowErrorMsg(string content, bool error = false) 463 | { 464 | ExceptionPanel panel = UIView.library.ShowModal("ExceptionPanel"); 465 | panel.SetMessage("Roundabout builder", content, error); 466 | } 467 | } 468 | } 469 | --------------------------------------------------------------------------------