diff --git a/Assets/Scripts/Net/CanvasInput.cs b/Assets/Scripts/Net/CanvasInput.cs index b2a05b3..7e33eb8 100644 --- a/Assets/Scripts/Net/CanvasInput.cs +++ b/Assets/Scripts/Net/CanvasInput.cs @@ -34,10 +34,12 @@ namespace NeonTea.Quakeball.Net { Net.Singleton.Stop(); }); Send.onClick.AddListener(() => { - if (Net.Singleton.Peer != null && Net.Singleton.Connections.Count > 0) { - HelloPckt pckt = new HelloPckt(); - pckt.Text = MessageField.text; - Net.Singleton.Peer.SendReliable(Net.Singleton.Connections[0], pckt); + if (Net.Singleton.Peer != null) { + foreach (ulong uid in Net.Singleton.Connections) { + HelloPckt pckt = new HelloPckt(); + pckt.Text = MessageField.text; + Net.Singleton.Peer.SendReliable(uid, pckt); + } } }); } diff --git a/Assets/Scripts/Net/Net.cs b/Assets/Scripts/Net/Net.cs index 619863b..3f65d67 100644 --- a/Assets/Scripts/Net/Net.cs +++ b/Assets/Scripts/Net/Net.cs @@ -9,7 +9,7 @@ namespace NeonTea.Quakeball.Net { private static byte[] FP = new byte[] { 0xFF, 0xF7 }; public Peer Peer; - public List Connections = new List(); + public List Connections = new List(); public bool IsServer = false; diff --git a/Assets/Scripts/Net/TestProtocol.cs b/Assets/Scripts/Net/TestProtocol.cs index fe64d9e..cbd6543 100644 --- a/Assets/Scripts/Net/TestProtocol.cs +++ b/Assets/Scripts/Net/TestProtocol.cs @@ -10,7 +10,7 @@ namespace NeonTea.Quakeball.Net { public class TestProtocol : Protocol { public override byte Identifier => 0x7A; - public override string Version => "0.0.2"; + public override string Version => "0.0.1"; public TestProtocol() { RegisterPacket(typeof(HelloPckt)); @@ -18,11 +18,14 @@ namespace NeonTea.Quakeball.Net { public override void ConnectionStatusChanged(ConnectionStatus oldStatus, ConnectionStatus newStatus, Connection conn) { Peer.MessageListener.Message($"Connection Status Changed into {newStatus.ToString()} for {conn.Endpoint}"); - if (newStatus == ConnectionStatus.Ready && !Net.Singleton.Connections.Contains(conn)) { - Net.Singleton.Connections.Add(conn); + if (conn.IsReady() && !Net.Singleton.Connections.Contains(conn.uid)) { + Net.Singleton.Connections.Add(conn.uid); } else if (newStatus == ConnectionStatus.Closed) { Net.Singleton.Peer.MessageListener.Message($"Conncection closed: {conn.ClosingReason}"); } + if (conn.IsDisconnected()) { + Net.Singleton.Connections.Remove(conn.uid); + } } public override void Receive(Connection conn, Packet packet) { diff --git a/Assets/Scripts/TeaNet/Packets/Protocol.cs b/Assets/Scripts/TeaNet/Packets/Protocol.cs index 1709601..fc0e472 100644 --- a/Assets/Scripts/TeaNet/Packets/Protocol.cs +++ b/Assets/Scripts/TeaNet/Packets/Protocol.cs @@ -20,8 +20,8 @@ namespace NeonTea.Quakeball.TeaNet.Packets { public abstract void Timeout(Connection conn); public void SendPacket(Packet p, Connection conn) { - Peer.ConnectionManager.AddPacketToQueue(conn, p); - Peer.ConnectionManager.SendPacketQueue(conn); + Peer.ConnectionManager.AddPacketToQueue(conn.uid, p); + Peer.ConnectionManager.SendPacketQueue(conn.uid); } public int RegisterPacket(Type t) { @@ -50,7 +50,7 @@ namespace NeonTea.Quakeball.TeaNet.Packets { buffer.Write((byte)connection.ClosingReason); } else if (connection.Status == ConnectionStatus.Ready) { buffer.Write((byte)PacketStage.Ready); - buffer.WriteInt(connection.LatestInwardReliable); + buffer.WriteInt(connection.Internal.LatestInwardReliable); } return buffer; } diff --git a/Assets/Scripts/TeaNet/Peers/Connection.cs b/Assets/Scripts/TeaNet/Peers/Connection.cs index 8b4da32..27a0a00 100644 --- a/Assets/Scripts/TeaNet/Peers/Connection.cs +++ b/Assets/Scripts/TeaNet/Peers/Connection.cs @@ -9,24 +9,44 @@ namespace NeonTea.Quakeball.TeaNet.Peers { public class Connection { public IPEndPoint Endpoint; + public ulong uid; public ConnectionStatus Status; - public byte AssignedProtocol; public ClosingReason ClosingReason; - public long LastMessage; - public int LatestOutwardReliable = -1; // Last reliable Packet ID the connection has told us they have - public int LatestOutwardUnreliable = -1; // Last unreliablePacket ID the connection has told us they have - public int LatestInwardReliable = -1; // Last reliable Packet ID we've received from the connection - public int LatestInwardUnreliable = -1; // Last unreliable Packet ID we've received from the connection - - public int ReliablePacketIDCounter; // Reliable Packet ID counter for packets we're sending them - public int UnreliablePacketIDCounter; // Unreliable Packet ID counter for packets we're sending them + public ConnectionInternalData Internal = new ConnectionInternalData(); public Connection(IPEndPoint endpoint, ConnectionStatus status = ConnectionStatus.Establishing) { Endpoint = endpoint; Status = status; - LastMessage = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + Internal.LastMessage = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + Internal.LatestInwardReliable = -1; + Internal.LatestInwardUnreliable = -1; + Internal.LatestOutwardReliable = -1; + Internal.LatestOutwardUnreliable = -1; } + + public bool IsReady() { + return Status == ConnectionStatus.Ready; + } + + public bool IsDisconnected() { + return !(Status == ConnectionStatus.Ready + || Status == ConnectionStatus.Awaiting + || Status == ConnectionStatus.Establishing); + } + } + + public struct ConnectionInternalData { + public byte AssignedProtocol; + public long LastMessage; + public int LatestOutwardReliable; // Last reliable Packet ID the connection has told us they have + public int LatestOutwardUnreliable; // Last unreliablePacket ID the connection has told us they have + public int LatestInwardReliable; // Last reliable Packet ID we've received from the connection + public int LatestInwardUnreliable; // Last unreliable Packet ID we've received from the connection + + public int ReliablePacketIDCounter; // Reliable Packet ID counter for packets we're sending them + public int UnreliablePacketIDCounter; // Unreliable Packet ID counter for packets we're sending them } public enum ConnectionStatus { diff --git a/Assets/Scripts/TeaNet/Peers/ConnectionManager.cs b/Assets/Scripts/TeaNet/Peers/ConnectionManager.cs index c59a3f4..fda380e 100644 --- a/Assets/Scripts/TeaNet/Peers/ConnectionManager.cs +++ b/Assets/Scripts/TeaNet/Peers/ConnectionManager.cs @@ -7,8 +7,10 @@ using NeonTea.Quakeball.TeaNet.Packets; namespace NeonTea.Quakeball.TeaNet.Peers { public class ConnectionManager { - private Dictionary Connections = new Dictionary(); - private Dictionary> PacketQueue = new Dictionary>(); + private ulong ConnectionCounter; + private Dictionary Connections = new Dictionary(); + private Dictionary IPtoID = new Dictionary(); + private Dictionary> PacketQueue = new Dictionary>(); private Peer Peer; private Thread UpdateThread; @@ -27,36 +29,47 @@ namespace NeonTea.Quakeball.TeaNet.Peers { } public Connection Find(IPEndPoint endpoint) { - if (Connections.ContainsKey(endpoint)) { - return Connections[endpoint]; + if (IPtoID.ContainsKey(endpoint)) { + return Connections[IPtoID[endpoint]]; } Connection conn = new Connection(endpoint, ConnectionStatus.Awaiting); - Connections.Add(endpoint, conn); - PacketQueue.Add(conn, new List()); + AddConnection(conn); return conn; } public bool StartConnection(IPEndPoint endpoint, byte protocolIdent) { - if (Connections.ContainsKey(endpoint)) { + if (IPtoID.ContainsKey(endpoint)) { return false; } Connection conn = new Connection(endpoint); - conn.AssignedProtocol = protocolIdent; - Connections.Add(endpoint, conn); - PacketQueue.Add(conn, new List()); + conn.Internal.AssignedProtocol = protocolIdent; + AddConnection(conn); return true; } - public void AddPacketToQueue(Connection conn, Packet p) { - p.Id = conn.ReliablePacketIDCounter++; - PacketQueue[conn].Add(p); + public Connection GetConnection(ulong uid) { + Connection conn; + Connections.TryGetValue(uid, out conn); + return conn; } - public void SendPacketQueue(Connection conn) { - Protocol protocol = Peer.GetProtocol(conn.AssignedProtocol); - if (protocol != null) { + public void AddPacketToQueue(ulong uid, Packet p) { + if (!Connections.ContainsKey(uid)) { + return; + } + p.Id = Connections[uid].Internal.ReliablePacketIDCounter++; + PacketQueue[uid].Add(p); + } + + public void SendPacketQueue(ulong uid) { + if (!Connections.ContainsKey(uid)) { + return; + } + Connection conn = Connections[uid]; + Protocol protocol = Peer.GetProtocol(conn.Internal.AssignedProtocol); + if (protocol != null && conn.IsReady()) { ByteBuffer buffer = protocol.BuildMessage(conn); - List list = PacketQueue[conn]; + List list = PacketQueue[uid]; buffer.WriteInt(list.Count); foreach (Packet p in list) { buffer.WritePacket(protocol, p); @@ -65,11 +78,15 @@ namespace NeonTea.Quakeball.TeaNet.Peers { } } - public void SendSingleUnreliable(Connection conn, Packet p) { - p.Id = conn.UnreliablePacketIDCounter++; + public void SendSingleUnreliable(ulong uid, Packet p) { + if (!Connections.ContainsKey(uid)) { + return; + } + Connection conn = Connections[uid]; + p.Id = conn.Internal.UnreliablePacketIDCounter++; p.Reliable = false; - Protocol protocol = Peer.GetProtocol(conn.AssignedProtocol); - if (protocol != null) { + Protocol protocol = Peer.GetProtocol(conn.Internal.AssignedProtocol); + if (protocol != null && conn.IsReady()) { ByteBuffer buffer = protocol.BuildMessage(conn); buffer.WriteInt(1); buffer.WritePacket(protocol, p); @@ -77,8 +94,21 @@ namespace NeonTea.Quakeball.TeaNet.Peers { } } + private void AddConnection(Connection conn) { + conn.uid = ConnectionCounter++; + Connections.Add(conn.uid, conn); + IPtoID.Add(conn.Endpoint, conn.uid); + PacketQueue.Add(conn.uid, new List()); + } + + private void RemoveConnection(Connection conn) { + Connections.Remove(conn.uid); + IPtoID.Remove(conn.Endpoint); + PacketQueue.Remove(conn.uid); + } + private void SendPlain(Connection conn) { - Protocol protocol = Peer.GetProtocol(conn.AssignedProtocol); + Protocol protocol = Peer.GetProtocol(conn.Internal.AssignedProtocol); if (protocol != null) { ByteBuffer buffer = protocol.BuildMessage(conn); Send(conn, buffer); @@ -96,7 +126,7 @@ namespace NeonTea.Quakeball.TeaNet.Peers { public void Handle(IPEndPoint endpoint, ByteBuffer buffer) { Connection conn = Find(endpoint); - conn.LastMessage = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + conn.Internal.LastMessage = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); ConnectionStatus oldStatus = conn.Status; byte protocolId = buffer.Read(); Protocol protocol = Peer.GetProtocol(protocolId); @@ -104,7 +134,7 @@ namespace NeonTea.Quakeball.TeaNet.Peers { switch (stage) { case PacketStage.Establishing: if (conn.Status == ConnectionStatus.Awaiting) { - conn.AssignedProtocol = protocolId; + conn.Internal.AssignedProtocol = protocolId; string version = buffer.ReadString(); if (protocol == null || !version.Equals(protocol.Version)) { conn.Status = ConnectionStatus.Rejected; @@ -134,29 +164,29 @@ namespace NeonTea.Quakeball.TeaNet.Peers { } break; case PacketStage.Ready: - if (conn.AssignedProtocol != protocolId || protocol == null) { + if (conn.Internal.AssignedProtocol != protocolId || protocol == null) { break; } if (oldStatus == ConnectionStatus.Establishing) { // Update connection status conn.Status = ConnectionStatus.Ready; protocol.ConnectionStatusChanged(oldStatus, conn.Status, conn); } - conn.LatestOutwardReliable = buffer.ReadInt(); + conn.Internal.LatestOutwardReliable = buffer.ReadInt(); - List list = PacketQueue[conn]; - list.RemoveAll(p => p.Id <= conn.LatestOutwardReliable); - PacketQueue[conn] = list; + List list = PacketQueue[conn.uid]; + list.RemoveAll(p => p.Id <= conn.Internal.LatestOutwardReliable); + PacketQueue[conn.uid] = list; int PacketAmount = buffer.ReadInt(); for (int i = 0; i < PacketAmount; i++) { Packet p = buffer.ReadPacket(protocol); if (p.Reliable) { - if (p.Id > conn.LatestInwardReliable) { - conn.LatestInwardReliable = p.Id; + if (p.Id > conn.Internal.LatestInwardReliable) { + conn.Internal.LatestInwardReliable = p.Id; protocol.Receive(conn, p); } - } else if (p.Id > conn.LatestInwardUnreliable) { - conn.LatestInwardUnreliable = p.Id; + } else if (p.Id > conn.Internal.LatestInwardUnreliable) { + conn.Internal.LatestInwardUnreliable = p.Id; protocol.Receive(conn, p); } } @@ -168,30 +198,30 @@ namespace NeonTea.Quakeball.TeaNet.Peers { try { while (Thread.CurrentThread.IsAlive) { long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - List> timedOut = new List>(); - foreach (KeyValuePair pair in Connections) { - Connection conn = pair.Value; - if ((now - conn.LastMessage) > Timeout || conn.Status == ConnectionStatus.Lost) { - timedOut.Add(pair); + List timedOut = new List(); + foreach (ulong uid in Connections.Keys) { + Connection conn = Connections[uid]; + if ((now - conn.Internal.LastMessage) > Timeout || conn.Status == ConnectionStatus.Lost) { + timedOut.Add(uid); } if (conn.Status != ConnectionStatus.Awaiting || conn.Status != ConnectionStatus.Stopped) { if (conn.Status == ConnectionStatus.Ready) { - SendPacketQueue(conn); + SendPacketQueue(uid); } else { SendPlain(conn); } } } - foreach (KeyValuePair pair in timedOut) { - Connections.Remove(pair.Key); - PacketQueue.Remove(pair.Value); - if (pair.Value.Status == ConnectionStatus.Ready - || pair.Value.Status == ConnectionStatus.Establishing - || pair.Value.Status == ConnectionStatus.Awaiting - || pair.Value.Status == ConnectionStatus.Lost) { - Protocol protocol = Peer.GetProtocol(pair.Value.AssignedProtocol); + foreach (ulong uid in timedOut) { + Connection conn = Connections[uid]; + RemoveConnection(conn); + if (conn.Status == ConnectionStatus.Ready + || conn.Status == ConnectionStatus.Establishing + || conn.Status == ConnectionStatus.Awaiting + || conn.Status == ConnectionStatus.Lost) { + Protocol protocol = Peer.GetProtocol(conn.Internal.AssignedProtocol); if (protocol != null) { - protocol.Timeout(pair.Value); + protocol.Timeout(conn); } } } diff --git a/Assets/Scripts/TeaNet/Peers/Peer.cs b/Assets/Scripts/TeaNet/Peers/Peer.cs index de2ab39..cdacc0c 100644 --- a/Assets/Scripts/TeaNet/Peers/Peer.cs +++ b/Assets/Scripts/TeaNet/Peers/Peer.cs @@ -58,13 +58,17 @@ namespace NeonTea.Quakeball.TeaNet.Peers { MessageListener.Message($"Connecting to {endpoint}"); } - public void SendReliable(Connection conn, Packet packet) { - ConnectionManager.AddPacketToQueue(conn, packet); - ConnectionManager.SendPacketQueue(conn); + public void SendReliable(ulong uid, Packet packet) { + ConnectionManager.AddPacketToQueue(uid, packet); + ConnectionManager.SendPacketQueue(uid); } - public void SendUnreliable(Connection conn, Packet packet) { - ConnectionManager.SendSingleUnreliable(conn, packet); + public void SendUnreliable(ulong uid, Packet packet) { + ConnectionManager.SendSingleUnreliable(uid, packet); + } + + public Connection GetConnection(ulong uid) { + return ConnectionManager.GetConnection(uid); } public byte RegisterProtocol(Protocol protocol) { diff --git a/ProjectSettings/Packages/com.unity.probuilder/Settings.json b/ProjectSettings/Packages/com.unity.probuilder/Settings.json index 39e1d03..6a92adb 100644 --- a/ProjectSettings/Packages/com.unity.probuilder/Settings.json +++ b/ProjectSettings/Packages/com.unity.probuilder/Settings.json @@ -18,11 +18,6 @@ "key": "log.path", "value": "{\"m_Value\":\"ProBuilderLog.txt\"}" }, - { - "type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "VertexColorPalette.previousColorPalette", - "value": "{\"m_Value\":\"Assets/ProBuilder Data/Default Color Palette.asset\"}" - }, { "type": "UnityEngine.ProBuilder.SemVer, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "key": "about.identifier", @@ -37,136 +32,6 @@ "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "key": "lightmapping.autoUnwrapLightmapUV", "value": "{\"m_Value\":true}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "mesh.newShapesSnapToGrid", - "value": "{\"m_Value\":true}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "editor.autoRecalculateCollisions", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "mesh.meshColliderIsConvex", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "UnityEngine.ProBuilder.ProBuilderEditor-isUtilityWindow", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "editor.backFaceSelectEnabled", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "editor.toolbarIconGUI", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "editor.showSceneInfo", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "editor.showEditorNotifications", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "GrowSelection.useAngle", - "value": "{\"m_Value\":true}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "GrowSelection.iterativeGrow", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "smoothing.showPreview", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "smoothing.showNormals", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "smoothing.showSettings", - "value": "{\"m_Value\":false}" - }, - { - "type": "System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "smoothing.showHelp", - "value": "{\"m_Value\":false}" - }, - { - "type": "UnityEngine.ProBuilder.PivotLocation, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "mesh.newShapePivotLocation", - "value": "{\"m_Value\":1}" - }, - { - "type": "UnityEngine.Rendering.ShadowCastingMode, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "mesh.shadowCastingMode", - "value": "{\"m_Value\":1}" - }, - { - "type": "UnityEngine.Material, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "mesh.userMaterial", - "value": "{\"m_Value\":{\"instanceID\":0}}" - }, - { - "type": "UnityEditor.StaticEditorFlags, UnityEditor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "mesh.defaultStaticEditorFlags", - "value": "{\"m_Value\":0}" - }, - { - "type": "UnityEngine.ProBuilder.ColliderType, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "mesh.newShapeColliderType", - "value": "{\"m_Value\":2}" - }, - { - "type": "UnityEngine.ProBuilder.UnwrapParameters, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "lightmapping.defaultLightmapUnwrapParameters", - "value": "{\"m_Value\":{\"m_HardAngle\":88.0,\"m_PackMargin\":20.0,\"m_AngleError\":8.0,\"m_AreaError\":15.0}}" - }, - { - "type": "UnityEngine.ProBuilder.SelectionModifierBehavior, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "editor.rectSelectModifier", - "value": "{\"m_Value\":2}" - }, - { - "type": "UnityEngine.ProBuilder.RectSelectMode, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "editor.dragSelectRectMode", - "value": "{\"m_Value\":0}" - }, - { - "type": "UnityEngine.ProBuilder.SelectMode, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "editor.selectMode", - "value": "{\"m_Value\":1}" - }, - { - "type": "UnityEngine.ProBuilder.ExtrudeMethod, Unity.ProBuilder, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", - "key": "editor.extrudeMethod", - "value": "{\"m_Value\":2}" - }, - { - "type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "ExtrudeFaces.distance", - "value": "{\"m_Value\":0.5}" - }, - { - "type": "System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "key": "GrowSelection.angleValue", - "value": "{\"m_Value\":15.0}" } ] }