diff --git a/Runtime/NetworkComponent.cs b/Runtime/NetworkComponent.cs index f1a9b31..70548e4 100644 --- a/Runtime/NetworkComponent.cs +++ b/Runtime/NetworkComponent.cs @@ -1,4 +1,8 @@ -using UnityEngine; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEngine; namespace VelNet { @@ -13,20 +17,95 @@ namespace VelNet /// protected void SendBytes(byte[] message, bool reliable = true) { - networkObject.SendBytes(this, message, reliable); + networkObject.SendBytes(this, false, message, reliable); } - + /// /// call this in child classes to send a message to other people /// protected void SendBytesToGroup(string group, byte[] message, bool reliable = true) { - networkObject.SendBytesToGroup(this, group, message, reliable); + networkObject.SendBytesToGroup(this, false, group, message, reliable); } /// /// This is called by when messages are received for this component /// public abstract void ReceiveBytes(byte[] message); + + public void ReceiveRPC(byte[] message) + { + using MemoryStream mem = new MemoryStream(message); + using BinaryReader reader = new BinaryReader(mem); + byte methodIndex = reader.ReadByte(); + int length = reader.ReadInt32(); + byte[] parameterData = reader.ReadBytes(length); + + MethodInfo[] mInfos = GetType().GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); + Array.Sort(mInfos, (m1, m2) => string.Compare(m1.Name, m2.Name, StringComparison.Ordinal)); + try + { + mInfos[methodIndex].Invoke(this, length > 0 ? new object[] { parameterData } : Array.Empty()); + } + catch (Exception e) + { + Debug.LogError($"Error processing received RPC {e}"); + } + } + + protected void SendRPCToGroup(string group, bool runLocally, string methodName, byte[] parameterData = null) + { + if (GenerateRPC(methodName, parameterData, out byte[] bytes)) return; + + if (runLocally) ReceiveRPC(bytes); + + networkObject.SendBytesToGroup(this, true, group, bytes, true); + } + + protected void SendRPC(string methodName, bool runLocally, byte[] parameterData = null) + { + if (GenerateRPC(methodName, parameterData, out byte[] bytes)) return; + + if (networkObject.SendBytes(this, true, bytes, true)) + { + // only run locally if we can successfully send + if (runLocally) ReceiveRPC(bytes); + } + } + + private bool GenerateRPC(string methodName, byte[] parameterData, out byte[] bytes) + { + bytes = null; + using MemoryStream mem = new MemoryStream(); + using BinaryWriter writer = new BinaryWriter(mem); + + MethodInfo[] mInfos = GetType().GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); + Array.Sort(mInfos, (m1, m2) => string.Compare(m1.Name, m2.Name, StringComparison.Ordinal)); + int methodIndex = mInfos.ToList().FindIndex(m => m.Name == methodName); + switch (methodIndex) + { + case > 255: + Debug.LogError("Too many methods in this class."); + return true; + case < 0: + Debug.LogError("Can't find a method with that name."); + return true; + } + + writer.Write((byte)methodIndex); + if (parameterData != null) + { + writer.Write(parameterData.Length); + writer.Write(parameterData); + } + else + { + writer.Write(0); + } + + bytes = mem.ToArray(); + + return false; + } } } \ No newline at end of file diff --git a/Runtime/NetworkObject.cs b/Runtime/NetworkObject.cs index 0dd136a..dfe1248 100644 --- a/Runtime/NetworkObject.cs +++ b/Runtime/NetworkObject.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.NetworkInformation; #if UNITY_EDITOR using UnityEditor; #endif @@ -19,7 +20,7 @@ namespace VelNet public bool ownershipLocked; public bool IsMine => owner?.isLocal ?? false; - + /// /// This is forged from the combination of the creator's id (-1 in the case of a scene object) and an object id, so it's always unique for a room /// @@ -40,51 +41,95 @@ namespace VelNet public List syncedComponents; - public void SendBytes(NetworkComponent component, byte[] message, bool reliable = true) + /// + /// Player is the new owner + /// + public Action OwnershipChanged; + + public bool SendBytes(NetworkComponent component, bool isRpc, byte[] message, bool reliable = true) { - if (!IsMine) + // only needs to be owner if this isn't an RPC + // RPC calls can be called by non-owner + if (!IsMine && !isRpc) { Debug.LogError("Can't send message if owner is null or not local", this); - return; + return false; + } + + if (!VelNetManager.InRoom) + { + Debug.LogError("Can't send message if not in a room", this); + return false; } // send the message and an identifier for which component it belongs to if (!syncedComponents.Contains(component)) { Debug.LogError("Can't send message if this component is not registered with the NetworkObject.", this); - return; + return false; } - int index = syncedComponents.IndexOf(component); - if (index < 0) + int componentIndex = syncedComponents.IndexOf(component); + switch (componentIndex) { - Debug.LogError("WAAAAAAAH. NetworkObject doesn't have a reference to this component.", component); - } - else - { - VelNetPlayer.SendMessage(this, (byte)index, message, reliable); + case > 127: + Debug.LogError("Too many components.", component); + return false; + case < 0: + Debug.LogError("WAAAAAAAH. NetworkObject doesn't have a reference to this component.", component); + return false; } + + byte componentByte = (byte)(componentIndex << 1); + // the leftmost bit determines if this is an rpc or not + // this leaves only 128 possible NetworkComponents per NetworkObject + componentByte |= (byte)(isRpc ? 1 : 0); + + return VelNetPlayer.SendMessage(this, componentByte, message, reliable); } - public void SendBytesToGroup(NetworkComponent component, string group, byte[] message, bool reliable = true) + + public bool SendBytesToGroup(NetworkComponent component, bool isRpc, string group, byte[] message, bool reliable = true) { - if (!IsMine) + // only needs to be owner if this isn't an RPC + // RPC calls can be called by non-owner + if (!IsMine && !isRpc) { Debug.LogError("Can't send message if owner is null or not local", this); - return; + return false; } // send the message and an identifier for which component it belongs to - int index = syncedComponents.IndexOf(component); - VelNetPlayer.SendGroupMessage(this, group, (byte)index, message, reliable); + int componentIndex = syncedComponents.IndexOf(component); + switch (componentIndex) + { + case > 127: + Debug.LogError("Too many components.", component); + return false; + case < 0: + Debug.LogError("WAAAAAAAH. NetworkObject doesn't have a reference to this component.", component); + return false; + } + + byte componentByte = (byte)(componentIndex << 1); + componentByte |= (byte)(isRpc ? 1 : 0); + + return VelNetPlayer.SendGroupMessage(this, group, componentByte, message, reliable); } - public void ReceiveBytes(byte componentIdx, byte[] message) + public void ReceiveBytes(byte componentIdx, bool isRpc, byte[] message) { // send the message to the right component try { - syncedComponents[componentIdx].ReceiveBytes(message); + if (isRpc) + { + syncedComponents[componentIdx].ReceiveRPC(message); + } + else + { + syncedComponents[componentIdx].ReceiveBytes(message); + } } catch (Exception e) { @@ -123,13 +168,14 @@ namespace VelNet if (GUILayout.Button("Find Network Components and add backreferences.")) { - NetworkComponent[] comps = t.GetComponents(); + NetworkComponent[] comps = t.GetComponentsInChildren(); t.syncedComponents = comps.ToList(); foreach (NetworkComponent c in comps) { c.networkObject = t; PrefabUtility.RecordPrefabInstancePropertyModifications(c); } + PrefabUtility.RecordPrefabInstancePropertyModifications(t); } @@ -139,7 +185,7 @@ namespace VelNet // find the first unused value int[] used = FindObjectsOfType().Select(o => o.sceneNetworkId).ToArray(); int available = -1; - for (int i = 1; i <= used.Max()+1; i++) + for (int i = 1; i <= used.Max() + 1; i++) { if (!used.Contains(i)) { diff --git a/Runtime/Util/BinaryWriterExtensions.cs b/Runtime/Util/BinaryWriterExtensions.cs index 309a239..45ef835 100644 --- a/Runtime/Util/BinaryWriterExtensions.cs +++ b/Runtime/Util/BinaryWriterExtensions.cs @@ -1,5 +1,4 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using UnityEngine; @@ -33,6 +32,24 @@ namespace VelNet writer.Write(c.a); } + public static void Write(this BinaryWriter writer, List l) + { + writer.Write(l.Count()); + foreach (int i in l) + { + writer.Write(i); + } + } + + public static void Write(this BinaryWriter writer, List l) + { + writer.Write(l.Count()); + foreach (string i in l) + { + writer.Write(i); + } + } + #endregion #region Readers @@ -62,14 +79,38 @@ namespace VelNet ); } + public static List ReadIntList(this BinaryReader reader) + { + int length = reader.ReadInt32(); + List l = new List(length); + for (int i = 0; i < length; i++) + { + l.Add(reader.ReadInt32()); + } + + return l; + } + + public static List ReadStringList(this BinaryReader reader) + { + int length = reader.ReadInt32(); + List l = new List(length); + for (int i = 0; i < length; i++) + { + l.Add(reader.ReadString()); + } + + return l; + } + #endregion public static bool BytesSame(byte[] b1, byte[] b2) { - if (b1 == null && b2 != null) return false; // only one null - if (b1 != null && b2 == null) return false; // only one null - if (b1 == null) return true; // both null + if (b1 == null && b2 != null) return false; // only one null + if (b1 != null && b2 == null) return false; // only one null + if (b1 == null) return true; // both null // length doesn't match if (b1.Length != b2.Length) diff --git a/Runtime/Util/SyncRigidbody.cs b/Runtime/Util/SyncRigidbody.cs index 5937ff4..43d849c 100644 --- a/Runtime/Util/SyncRigidbody.cs +++ b/Runtime/Util/SyncRigidbody.cs @@ -139,5 +139,11 @@ namespace VelNet ); } } + + [VelNetRPC] + private void Test() + { + + } } } \ No newline at end of file diff --git a/Runtime/VelNetManager.cs b/Runtime/VelNetManager.cs index 00f6398..c60b674 100644 --- a/Runtime/VelNetManager.cs +++ b/Runtime/VelNetManager.cs @@ -11,6 +11,12 @@ using System.IO; namespace VelNet { + + /// Used to flag methods as remote-callable. + public class VelNetRPC : Attribute + { + } + [AddComponentMenu("VelNet/VelNet Manager")] public class VelNetManager : MonoBehaviour { @@ -170,7 +176,7 @@ namespace VelNet public class RoomDataMessage : Message { public string room; - public readonly List> members = new List>(); + public readonly List<(int, string)> members = new List<(int, string)>(); } public class JoinMessage : Message @@ -354,7 +360,6 @@ namespace VelNet try { - Debug.Log(jm.room); OnJoinedRoom?.Invoke(jm.room); } // prevent errors in subscribers from breaking our code @@ -495,6 +500,13 @@ namespace VelNet sceneObjects[i].networkId = -1 + "-" + sceneObjects[i].sceneNetworkId; sceneObjects[i].owner = masterPlayer; sceneObjects[i].isSceneObject = true; // needed for special handling when deleted + try { + sceneObjects[i].OwnershipChanged?.Invoke(masterPlayer); + } + catch (Exception e) + { + Debug.LogError("Error in event handling.\n" + e); + } if (objects.ContainsKey(sceneObjects[i].networkId)) { @@ -681,7 +693,8 @@ namespace VelNet while (socketConnection.Connected) { //read a byte - MessageReceivedType type = (MessageReceivedType)stream.ReadByte(); + int b = stream.ReadByte(); + MessageReceivedType type = (MessageReceivedType)b; switch (type) { @@ -735,8 +748,7 @@ namespace VelNet int s = stream.ReadByte(); //size of string utf8data = ReadExact(stream, s); //the username string username = Encoding.UTF8.GetString(utf8data); - rdm.members.Add(new Tuple(client_id, username)); - Debug.Log(username); + rdm.members.Add((client_id, username)); } AddMessage(rdm); @@ -1042,7 +1054,7 @@ namespace VelNet SendToGroup(group, mem.ToArray(), reliable); } - internal static void SendToRoom(byte[] message, bool include_self = false, bool reliable = true, bool ordered = false) + internal static bool SendToRoom(byte[] message, bool include_self = false, bool reliable = true, bool ordered = false) { byte sendType = (byte)MessageSendType.MESSAGE_OTHERS; if (include_self && ordered) sendType = (byte)MessageSendType.MESSAGE_ALL_ORDERED; @@ -1057,7 +1069,7 @@ namespace VelNet writer.Write(sendType); writer.Write(get_be_bytes(message.Length)); writer.Write(message); - SendTcpMessage(mem.ToArray()); + return SendTcpMessage(mem.ToArray()); } else { @@ -1066,11 +1078,12 @@ namespace VelNet Array.Copy(get_be_bytes(instance.userid), 0, toSend, 1, 4); Array.Copy(message, 0, toSend, 5, message.Length); SendUdpMessage(toSend, message.Length + 5); //shouldn't be over 1024... + return true; } } - internal static void SendToGroup(string group, byte[] message, bool reliable = true) + internal static bool SendToGroup(string group, byte[] message, bool reliable = true) { byte[] utf8bytes = Encoding.UTF8.GetBytes(group); if (reliable) @@ -1082,7 +1095,7 @@ namespace VelNet writer.Write(message); writer.Write((byte)utf8bytes.Length); writer.Write(utf8bytes); - SendTcpMessage(stream.ToArray()); + return SendTcpMessage(stream.ToArray()); } else { @@ -1093,6 +1106,7 @@ namespace VelNet Array.Copy(utf8bytes, 0, toSend, 6, utf8bytes.Length); Array.Copy(message, 0, toSend, 6 + utf8bytes.Length, message.Length); SendUdpMessage(toSend, 6 + utf8bytes.Length + message.Length); + return true; } } @@ -1148,6 +1162,15 @@ namespace VelNet newObject.networkId = networkId; newObject.prefabName = prefabName; newObject.owner = localPlayer; + try + { + newObject.OwnershipChanged?.Invoke(localPlayer); + } + catch (Exception e) + { + Debug.LogError("Error in event handling.\n" + e); + } + instance.objects.Add(newObject.networkId, newObject); @@ -1170,6 +1193,14 @@ namespace VelNet newObject.networkId = networkId; newObject.prefabName = prefabName; newObject.owner = owner; + try + { + newObject.OwnershipChanged?.Invoke(owner); + } + catch (Exception e) + { + Debug.LogError("Error in event handling.\n" + e); + } instance.objects.Add(newObject.networkId, newObject); } @@ -1230,6 +1261,12 @@ namespace VelNet /// True if successfully transferred, False if transfer message not sent public static bool TakeOwnership(string networkId) { + if (!InRoom) + { + Debug.LogError("Can't take ownership. Not in a room."); + return false; + } + // local player must exist if (LocalPlayer == null) { @@ -1253,6 +1290,14 @@ namespace VelNet // immediately successful instance.objects[networkId].owner = LocalPlayer; + try + { + instance.objects[networkId].OwnershipChanged?.Invoke(LocalPlayer); + } + catch (Exception e) + { + Debug.LogError("Error in event handling.\n" + e); + } // must be ordered, so that ownership transfers are not confused. // Also sent to all players, so that multiple simultaneous requests will result in the same outcome. diff --git a/Runtime/VelNetPlayer.cs b/Runtime/VelNetPlayer.cs index c94b6ab..5ee82c5 100644 --- a/Runtime/VelNetPlayer.cs +++ b/Runtime/VelNetPlayer.cs @@ -24,7 +24,7 @@ namespace VelNet internal int lastObjectId; - private bool isMaster; + public bool IsMaster { get; private set; } public VelNetPlayer() @@ -51,7 +51,7 @@ namespace VelNet } } - if (isMaster) + if (IsMaster) { //send a list of scene object ids when someone joins SendSceneUpdate(); @@ -72,13 +72,15 @@ namespace VelNet { using MemoryStream mem = new MemoryStream(m.data); using BinaryReader reader = new BinaryReader(mem); - + //individual message parameters separated by comma VelNetManager.MessageType messageType = (VelNetManager.MessageType)reader.ReadByte(); switch (messageType) { - case VelNetManager.MessageType.ObjectSync: // sync update for an object I may own + // sync update for an object "I" may own + // "I" being the person sending + case VelNetManager.MessageType.ObjectSync: { string objectKey = reader.ReadString(); byte componentIdx = reader.ReadByte(); @@ -86,9 +88,13 @@ namespace VelNet byte[] syncMessage = reader.ReadBytes(messageLength); if (manager.objects.ContainsKey(objectKey)) { - if (manager.objects[objectKey].owner == this) + bool isRpc = (componentIdx & 1) == 1; + componentIdx = (byte)(componentIdx >> 1); + + // rpcs can be sent by non-owners + if (isRpc || manager.objects[objectKey].owner == this) { - manager.objects[objectKey].ReceiveBytes(componentIdx, syncMessage); + manager.objects[objectKey].ReceiveBytes(componentIdx, isRpc, syncMessage); } } @@ -101,6 +107,14 @@ namespace VelNet if (manager.objects.ContainsKey(networkId)) { manager.objects[networkId].owner = this; + try + { + manager.objects[networkId].OwnershipChanged?.Invoke(this); + } + catch (Exception e) + { + Debug.LogError("Error in event handling.\n" + e); + } } break; @@ -130,9 +144,12 @@ namespace VelNet { VelNetManager.SomebodyDestroyedNetworkObject(reader.ReadString()); } + break; } - case VelNetManager.MessageType.Custom: // custom packets + // Custom packets. These are global data that can be sent from anywhere. + // Any script can subscribe to the callback to receive the message data. + case VelNetManager.MessageType.Custom: { int len = reader.ReadInt32(); try @@ -153,12 +170,12 @@ namespace VelNet public void SetAsMasterPlayer() { - isMaster = true; + IsMaster = true; //if I'm master, I'm now responsible for updating all scene objects //FindObjectsOfType(); } - public static void SendGroupMessage(NetworkObject obj, string group, byte componentIdx, byte[] data, bool reliable = true) + public static bool SendGroupMessage(NetworkObject obj, string group, byte componentIdx, byte[] data, bool reliable = true) { using MemoryStream mem = new MemoryStream(); using BinaryWriter writer = new BinaryWriter(mem); @@ -167,10 +184,10 @@ namespace VelNet writer.Write(componentIdx); writer.Write(data.Length); writer.Write(data); - VelNetManager.SendToGroup(group, mem.ToArray(), reliable); + return VelNetManager.SendToGroup(group, mem.ToArray(), reliable); } - public static void SendMessage(NetworkObject obj, byte componentIdx, byte[] data, bool reliable = true) + public static bool SendMessage(NetworkObject obj, byte componentIdx, byte[] data, bool reliable = true) { using MemoryStream mem = new MemoryStream(); using BinaryWriter writer = new BinaryWriter(mem); @@ -179,7 +196,7 @@ namespace VelNet writer.Write(componentIdx); writer.Write(data.Length); writer.Write(data); - VelNetManager.SendToRoom(mem.ToArray(), false, reliable); + return VelNetManager.SendToRoom(mem.ToArray(), false, reliable); } public void SendSceneUpdate() @@ -223,6 +240,14 @@ namespace VelNet // immediately successful manager.objects[networkId].owner = this; + try + { + manager.objects[networkId].OwnershipChanged?.Invoke(this); + } + catch (Exception e) + { + Debug.LogError("Error in event handling.\n" + e); + } // must be ordered, so that ownership transfers are not confused. // Also sent to all players, so that multiple simultaneous requests will result in the same outcome. diff --git a/package.json b/package.json index 97716cf..5405c76 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "edu.uga.engr.vel.velnet", "displayName": "VelNet", - "version": "1.0.12", + "version": "1.0.13", "unity": "2019.1", "description": "A custom networking library for Unity.", "keywords": [