diff --git a/SRMP/Globals.cs b/SRMP/Globals.cs index 9010532..0d96999 100644 --- a/SRMP/Globals.cs +++ b/SRMP/Globals.cs @@ -70,7 +70,8 @@ namespace SRMultiplayer !assembly.GetName().Name.Contains("Logger") && !assembly.GetName().Name.Contains("Mono.") && !assembly.GetName().Name.Contains("Harmony") && !assembly.GetName().Name.Equals("SRML") && !assembly.GetName().Name.Equals("SRML.Editor") && !assembly.GetName().Name.Equals("Newtonsoft.Json") && !assembly.GetName().Name.Equals("INIFileParser") && !assembly.GetName().Name.Equals("SRMultiplayer") && !assembly.GetName().Name.Contains("Microsoft.") && - !assembly.GetName().Name.Equals("SRMP") && !assembly.GetName().Name.Equals("XGamingRuntime") && !Globals.UserData.IgnoredMods.Contains(assembly.GetName().Name)) + !assembly.GetName().Name.Equals("SRMP") && !assembly.GetName().Name.Equals("XGamingRuntime") && !assembly.GetName().Name.Contains("MonoMod.Utils.") + && !Globals.UserData.IgnoredMods.Contains(assembly.GetName().Name)) { mods.Add(assembly.GetName().Name); } diff --git a/SRMP/Lidgren.Network/NetPeer.Internal.cs b/SRMP/Lidgren.Network/NetPeer.Internal.cs index 6d4996b..feb7c56 100644 --- a/SRMP/Lidgren.Network/NetPeer.Internal.cs +++ b/SRMP/Lidgren.Network/NetPeer.Internal.cs @@ -123,7 +123,7 @@ namespace Lidgren.Network mutex.WaitOne(); if (m_socket == null) - m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + m_socket = new Socket(m_configuration.LocalAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); if (reBind) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, (int)1); @@ -132,7 +132,19 @@ namespace Lidgren.Network m_socket.SendBufferSize = m_configuration.SendBufferSize; m_socket.Blocking = false; - var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress, reBind ? m_listenPort : m_configuration.Port); + if (m_configuration.DualStack) + { + if (m_configuration.LocalAddress.AddressFamily != AddressFamily.InterNetworkV6) + { + LogWarning("Configuration specifies Dual Stack but does not use IPv6 local address; Dual stack will not work."); + } + else + { + m_socket.DualMode = true; + } + } + + var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress, reBind ? m_listenPort : m_configuration.Port); m_socket.Bind(ep); try @@ -412,172 +424,175 @@ namespace Lidgren.Network // update now now = NetTime.Now; - do + try + { + do + { + ReceiveSocketData(now); + } while (m_socket.Available > 0); + } + catch (SocketException sx) + { + switch (sx.SocketErrorCode) + { + case SocketError.ConnectionReset: + // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" + // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! + // So, what to do? + LogWarning("ConnectionReset"); + return; + + case SocketError.NotConnected: + // socket is unbound; try to rebind it (happens on mobile when process goes to sleep) + BindSocket(true); + return; + + default: + LogWarning("Socket exception: " + sx.ToString()); + return; + } + } + } + + private void ReceiveSocketData(double now) + { + int bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); + + if (bytesReceived < NetConstants.HeaderByteSize) + return; + + //LogVerbose("Received " + bytesReceived + " bytes"); + + var ipsender = (NetEndPoint)m_senderRemote; + + if (m_upnp != null && now < m_upnp.m_discoveryResponseDeadline && bytesReceived > 32) { - int bytesReceived = 0; - try + // is this an UPnP response? + string resp = System.Text.Encoding.UTF8.GetString(m_receiveBuffer, 0, bytesReceived); + if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) { - bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); - } - catch (SocketException sx) - { - switch (sx.SocketErrorCode) - { - case SocketError.ConnectionReset: - // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" - // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! - // So, what to do? - LogWarning("ConnectionReset"); - return; - - case SocketError.NotConnected: - // socket is unbound; try to rebind it (happens on mobile when process goes to sleep) - BindSocket(true); - return; - - default: - LogWarning("Socket exception: " + sx.ToString()); - return; - } - } - - if (bytesReceived < NetConstants.HeaderByteSize) - return; - - //LogVerbose("Received " + bytesReceived + " bytes"); - - var ipsender = (NetEndPoint)m_senderRemote; - - if (m_upnp != null && now < m_upnp.m_discoveryResponseDeadline && bytesReceived > 32) - { - // is this an UPnP response? - string resp = System.Text.Encoding.UTF8.GetString(m_receiveBuffer, 0, bytesReceived); - if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0")) - { - try - { - resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); - resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); - m_upnp.ExtractServiceUrl(resp); - return; - } - catch (Exception ex) - { - LogDebug("Failed to parse UPnP response: " + ex.ToString()); - - // don't try to parse this packet further - return; - } - } - } - - NetConnection sender = null; - m_connectionLookup.TryGetValue(ipsender, out sender); - - // - // parse packet into messages - // - int numMessages = 0; - int numFragments = 0; - int ptr = 0; - while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) - { - // decode header - // 8 bits - NetMessageType - // 1 bit - Fragment? - // 15 bits - Sequence number - // 16 bits - Payload length in bits - - numMessages++; - - NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; - - byte low = m_receiveBuffer[ptr++]; - byte high = m_receiveBuffer[ptr++]; - - bool isFragment = ((low & 1) == 1); - ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); - - if (isFragment) - numFragments++; - - ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); - int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); - - if (bytesReceived - ptr < payloadByteLength) - { - LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); - return; - } - - if (tp >= NetMessageType.Unused1 && tp <= NetMessageType.Unused29) - { - ThrowOrLog("Unexpected NetMessageType: " + tp); - return; - } - try { - if (tp >= NetMessageType.LibraryError) - { - if (sender != null) - sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); - else - ReceivedUnconnectedLibraryMessage(now, ipsender, tp, ptr, payloadByteLength); - } - else - { - if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) - return; // dropping unconnected message since it's not enabled - - NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); - msg.m_isFragment = isFragment; - msg.m_receiveTime = now; - msg.m_sequenceNumber = sequenceNumber; - msg.m_receivedMessageType = tp; - msg.m_senderConnection = sender; - msg.m_senderEndPoint = ipsender; - msg.m_bitLength = payloadBitLength; - - Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); - if (sender != null) - { - if (tp == NetMessageType.Unconnected) - { - // We're connected; but we can still send unconnected messages to this peer - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); - } - else - { - // connected application (non-library) message - sender.ReceivedMessage(msg); - } - } - else - { - // at this point we know the message type is enabled - // unconnected application (non-library) message - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); - } - } + resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); + resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); + m_upnp.ExtractServiceUrl(resp); + return; } catch (Exception ex) { - LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + LogDebug("Failed to parse UPnP response: " + ex.ToString()); + + // don't try to parse this packet further + return; } - ptr += payloadByteLength; + } + } + + NetConnection sender = null; + m_connectionLookup.TryGetValue(ipsender, out sender); + + // + // parse packet into messages + // + int numMessages = 0; + int numFragments = 0; + int ptr = 0; + while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) + { + // decode header + // 8 bits - NetMessageType + // 1 bit - Fragment? + // 15 bits - Sequence number + // 16 bits - Payload length in bits + + numMessages++; + + NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; + + byte low = m_receiveBuffer[ptr++]; + byte high = m_receiveBuffer[ptr++]; + + bool isFragment = ((low & 1) == 1); + ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); + + if (isFragment) + numFragments++; + + ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); + int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); + + if (bytesReceived - ptr < payloadByteLength) + { + LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); + return; } - m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); - if (sender != null) - sender.m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + if (tp >= NetMessageType.Unused1 && tp <= NetMessageType.Unused29) + { + ThrowOrLog("Unexpected NetMessageType: " + tp); + return; + } - } while (m_socket.Available > 0); - } + try + { + if (tp >= NetMessageType.LibraryError) + { + if (sender != null) + sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); + else + ReceivedUnconnectedLibraryMessage(now, ipsender, tp, ptr, payloadByteLength); + } + else + { + if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) + return; // dropping unconnected message since it's not enabled - /// + NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); + msg.m_isFragment = isFragment; + msg.m_receiveTime = now; + msg.m_sequenceNumber = sequenceNumber; + msg.m_receivedMessageType = tp; + msg.m_senderConnection = sender; + msg.m_senderEndPoint = ipsender; + msg.m_bitLength = payloadBitLength; + + Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); + if (sender != null) + { + if (tp == NetMessageType.Unconnected) + { + // We're connected; but we can still send unconnected messages to this peer + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + else + { + // connected application (non-library) message + sender.ReceivedMessage(msg); + } + } + else + { + // at this point we know the message type is enabled + // unconnected application (non-library) message + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + } + } + catch (Exception ex) + { + LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + } + ptr += payloadByteLength; + } + + m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + if (sender != null) + sender.m_statistics.PacketReceived(bytesReceived, numMessages, numFragments); + } + + /// /// If NetPeerConfiguration.AutoFlushSendQueue() is false; you need to call this to send all messages queued using SendMessage() /// public void FlushSendQueue() @@ -686,7 +701,7 @@ namespace Lidgren.Network case NetMessageType.Connect: if (m_configuration.AcceptIncomingConnections == false) { - LogWarning(m_configuration.AppIdentifier + " Received Connect, but we're not accepting incoming connections!"); + LogWarning("Received Connect, but we're not accepting incoming connections!"); return; } // handle connect diff --git a/SRMP/Lidgren.Network/NetPeer.LatencySimulation.cs b/SRMP/Lidgren.Network/NetPeer.LatencySimulation.cs index ee029c3..6ef891f 100644 --- a/SRMP/Lidgren.Network/NetPeer.LatencySimulation.cs +++ b/SRMP/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -132,6 +132,9 @@ namespace Lidgren.Network catch { } } + //Avoids allocation on mapping to IPv6 + private IPEndPoint targetCopy = new IPEndPoint(IPAddress.Any, 0); + internal bool ActuallySendPacket(byte[] data, int numBytes, NetEndPoint target, out bool connectionReset) { connectionReset = false; @@ -140,17 +143,25 @@ namespace Lidgren.Network { ba = NetUtility.GetCachedBroadcastAddress(); - // TODO: refactor this check outta here - if (target.Address.Equals(ba)) - { - // Some networks do not allow - // a global broadcast so we use the BroadcastAddress from the configuration - // this can be resolved to a local broadcast addresss e.g 192.168.x.255 - target.Address = m_configuration.BroadcastAddress; - m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); - } + // TODO: refactor this check outta here + if (target.Address.Equals(ba)) + { + // Some networks do not allow + // a global broadcast so we use the BroadcastAddress from the configuration + // this can be resolved to a local broadcast addresss e.g 192.168.x.255 + targetCopy.Address = m_configuration.BroadcastAddress; + targetCopy.Port = target.Port; + m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + } + else if(m_configuration.DualStack && m_configuration.LocalAddress.AddressFamily == AddressFamily.InterNetworkV6) + NetUtility.CopyEndpoint(target, targetCopy); //Maps to IPv6 for Dual Mode + else + { + targetCopy.Port = target.Port; + targetCopy.Address = target.Address; + } - int bytesSent = m_socket.SendTo(data, 0, numBytes, SocketFlags.None, target); + int bytesSent = m_socket.SendTo(data, 0, numBytes, SocketFlags.None, targetCopy); if (numBytes != bytesSent) LogWarning("Failed to send the full " + numBytes + "; only " + bytesSent + " bytes sent in packet!"); @@ -178,7 +189,7 @@ namespace Lidgren.Network } finally { - if (target.Address == ba) + if (target.Address.Equals(ba)) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, false); } return true; diff --git a/SRMP/Lidgren.Network/NetPeer.cs b/SRMP/Lidgren.Network/NetPeer.cs index e9886f4..9dc9edd 100644 --- a/SRMP/Lidgren.Network/NetPeer.cs +++ b/SRMP/Lidgren.Network/NetPeer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Collections.Generic; using System.Net; - +using System.Net.Sockets; #if !__NOIPENDPOINT__ using NetEndPoint = System.Net.IPEndPoint; #endif @@ -32,8 +32,8 @@ namespace Lidgren.Network /// /// Signalling event which can be waited on to determine when a message is queued for reading. - /// Note that there is no guarantee that after the event is signaled the blocked thread will - /// find the message in the queue. Other user created threads could be preempted and dequeue + /// Note that there is no guarantee that after the event is signaled the blocked thread will + /// find the message in the queue. Other user created threads could be preempted and dequeue /// the message before the waiting thread wakes up. /// public AutoResetEvent MessageReceivedEvent @@ -121,9 +121,16 @@ namespace Lidgren.Network m_connections = new List(); m_connectionLookup = new Dictionary(); m_handshakes = new Dictionary(); - m_senderRemote = (EndPoint)new NetEndPoint(IPAddress.Any, 0); + if (m_configuration.LocalAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.IPv6Any, 0); + } + else + { + m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + } m_status = NetPeerStatus.NotRunning; - m_receivedFragmentGroups = new Dictionary>(); + m_receivedFragmentGroups = new Dictionary>(); } /// @@ -148,7 +155,7 @@ namespace Lidgren.Network } InitializeNetwork(); - + // start network thread m_networkThread = new Thread(new ThreadStart(NetworkLoop)); m_networkThread.Name = m_configuration.NetworkThreadName; @@ -183,7 +190,7 @@ namespace Lidgren.Network public NetIncomingMessage WaitMessage(int maxMillis) { NetIncomingMessage msg = ReadMessage(); - + while (msg == null) { // This could return true... @@ -191,11 +198,11 @@ namespace Lidgren.Network { return null; } - + // ... while this will still returns null. That's why we need to cycle. msg = ReadMessage(); } - + return msg; } @@ -215,7 +222,7 @@ namespace Lidgren.Network } return retval; } - + /// /// Reads a pending message from any connection, if any. /// Returns true if message was read, otherwise false. @@ -303,6 +310,8 @@ namespace Lidgren.Network { if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint"); + if(m_configuration.DualStack) + remoteEndPoint = NetUtility.MapToIPv6(remoteEndPoint); lock (m_connections) { diff --git a/SRMP/Lidgren.Network/NetPeerConfiguration.cs b/SRMP/Lidgren.Network/NetPeerConfiguration.cs index e041aaa..447ec7e 100644 --- a/SRMP/Lidgren.Network/NetPeerConfiguration.cs +++ b/SRMP/Lidgren.Network/NetPeerConfiguration.cs @@ -35,12 +35,12 @@ namespace Lidgren.Network // -4 bytes to be on the safe side and align to 8-byte boundary // Total 1408 bytes // Note that lidgren headers (5 bytes) are not included here; since it's part of the "mtu payload" - + /// /// Default MTU value in bytes /// public const int kDefaultMTU = 1408; - + private const string c_isLockedMessage = "You may not modify the NetPeerConfiguration after it has been used to initialize a NetPeer"; private bool m_isLocked; @@ -48,6 +48,8 @@ namespace Lidgren.Network private string m_networkThreadName; private IPAddress m_localAddress; private IPAddress m_broadcastAddress; + private bool m_dualStack; + internal bool m_acceptIncomingConnections; internal int m_maximumConnections; internal int m_defaultOutgoingMessageCapacity; @@ -341,10 +343,26 @@ namespace Lidgren.Network } } - /// - /// Gets or sets the local broadcast address to use when broadcasting - /// - public IPAddress BroadcastAddress + /// + /// Gets or sets a value indicating whether the library should use IPv6 dual stack mode. + /// If you enable this you should make sure that the is an IPv6 address. + /// Cannot be changed once NetPeer is initialized. + /// + public bool DualStack + { + get { return m_dualStack; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_dualStack = value; + } + } + + /// + /// Gets or sets the local broadcast address to use when broadcasting + /// + public IPAddress BroadcastAddress { get { return m_broadcastAddress; } set diff --git a/SRMP/Lidgren.Network/NetUtility.cs b/SRMP/Lidgren.Network/NetUtility.cs index f99d3b3..600447a 100644 --- a/SRMP/Lidgren.Network/NetUtility.cs +++ b/SRMP/Lidgren.Network/NetUtility.cs @@ -98,7 +98,7 @@ namespace Lidgren.Network NetAddress ipAddress = null; if (NetAddress.TryParse(ipOrHost, out ipAddress)) { - if (ipAddress.AddressFamily == AddressFamily.InterNetwork) + if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) { callback(ipAddress); return; @@ -139,7 +139,7 @@ namespace Lidgren.Network // check each entry for a valid IP address foreach (var ipCurrent in entry.AddressList) { - if (ipCurrent.AddressFamily == AddressFamily.InterNetwork) + if (ipCurrent.AddressFamily == AddressFamily.InterNetwork || ipCurrent.AddressFamily == AddressFamily.InterNetworkV6) { callback(ipCurrent); return; @@ -163,7 +163,7 @@ namespace Lidgren.Network } } - /// + /// /// Get IPv4 address from notation (xxx.xxx.xxx.xxx) or hostname /// public static NetAddress Resolve(string ipOrHost) @@ -176,9 +176,9 @@ namespace Lidgren.Network NetAddress ipAddress = null; if (NetAddress.TryParse(ipOrHost, out ipAddress)) { - if (ipAddress.AddressFamily == AddressFamily.InterNetwork) + if (ipAddress.AddressFamily == AddressFamily.InterNetwork || ipAddress.AddressFamily == AddressFamily.InterNetworkV6) return ipAddress; - throw new ArgumentException("This method will not currently resolve other than ipv4 addresses"); + throw new ArgumentException("This method will not currently resolve other than IPv4 or IPv6 addresses"); } // ok must be a host name @@ -189,7 +189,7 @@ namespace Lidgren.Network return null; foreach (var address in addresses) { - if (address.AddressFamily == AddressFamily.InterNetwork) + if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) return address; } return null; @@ -240,7 +240,7 @@ namespace Lidgren.Network } return new string(c); } - + /// /// Returns true if the endpoint supplied is on the same subnet as this host /// @@ -465,5 +465,29 @@ namespace Lidgren.Network // this is defined in the platform specific files return ComputeSHAHash(bytes, 0, bytes.Length); } - } -} \ No newline at end of file + + /// + /// Copies from to . Maps to an IPv6 address + /// + /// Source. + /// Destination. + internal static void CopyEndpoint(IPEndPoint src, IPEndPoint dst) + { + dst.Port = src.Port; + if (src.AddressFamily == AddressFamily.InterNetwork) + dst.Address = src.Address.MapToIPv6(); + else + dst.Address = src.Address; + } + + /// + /// Maps the IPEndPoint object to an IPv6 address. Has allocation + /// + internal static IPEndPoint MapToIPv6(IPEndPoint endPoint) + { + if (endPoint.AddressFamily == AddressFamily.InterNetwork) + return new IPEndPoint(endPoint.Address.MapToIPv6(), endPoint.Port); + return endPoint; + } + } +} diff --git a/SRMP/Properties/AssemblyInfo.cs b/SRMP/Properties/AssemblyInfo.cs index 81d723d..6ce9d2d 100644 --- a/SRMP/Properties/AssemblyInfo.cs +++ b/SRMP/Properties/AssemblyInfo.cs @@ -15,7 +15,7 @@ using System.Resources; [assembly: AssemblyCulture("")] // Version informationr( -[assembly: AssemblyVersion("0.0.0.1524")] -[assembly: AssemblyFileVersion("0.0.0.1524")] +[assembly: AssemblyVersion("0.0.0.1525")] +[assembly: AssemblyFileVersion("0.0.0.1525")] [assembly: NeutralResourcesLanguageAttribute( "en-US" )] diff --git a/SRMP/modinfo.json b/SRMP/modinfo.json index d8f5ec1..bcf0962 100644 --- a/SRMP/modinfo.json +++ b/SRMP/modinfo.json @@ -1,7 +1,7 @@ { "id": "srmp", "name": "Slime Rancher Multiplayer", - "version": "0.0.1524", + "version": "0.0.1525", "author": "SatyPardus", "dependencies": [