diff --git a/EpicOnlineTransport/BidirectionalDictionary.cs b/EpicOnlineTransport/BidirectionalDictionary.cs index aac5a4973..cc68a0193 100644 --- a/EpicOnlineTransport/BidirectionalDictionary.cs +++ b/EpicOnlineTransport/BidirectionalDictionary.cs @@ -50,81 +50,71 @@ /// MIT License /// -namespace EpicTransport; - -public class BidirectionalDictionary : IEnumerable -{ - private Dictionary t1ToT2Dict = new(); - private Dictionary t2ToT1Dict = new(); - - public IEnumerable FirstTypes => t1ToT2Dict.Keys; - public IEnumerable SecondTypes => t2ToT1Dict.Keys; - - public IEnumerator GetEnumerator() => t1ToT2Dict.GetEnumerator(); - - public int Count => t1ToT2Dict.Count; - - public void Add(T1 key, T2 value) - { - t1ToT2Dict[key] = value; - t2ToT1Dict[value] = key; - } - - public void Add(T2 key, T1 value) - { - t2ToT1Dict[key] = value; - t1ToT2Dict[value] = key; - } - - public T2 Get(T1 key) => t1ToT2Dict[key]; - - public T1 Get(T2 key) => t2ToT1Dict[key]; - - public bool TryGetValue(T1 key, out T2 value) => t1ToT2Dict.TryGetValue(key, out value); - - public bool TryGetValue(T2 key, out T1 value) => t2ToT1Dict.TryGetValue(key, out value); - - public bool Contains(T1 key) => t1ToT2Dict.ContainsKey(key); - - public bool Contains(T2 key) => t2ToT1Dict.ContainsKey(key); - - public void Remove(T1 key) - { - if (Contains(key)) - { - var val = t1ToT2Dict[key]; - t1ToT2Dict.Remove(key); - t2ToT1Dict.Remove(val); - } - } - - public void Remove(T2 key) - { - if (Contains(key)) - { - var val = t2ToT1Dict[key]; - t1ToT2Dict.Remove(val); - t2ToT1Dict.Remove(key); - } - } - - public T1 this[T2 key] - { - get => t2ToT1Dict[key]; - set - { - t2ToT1Dict[key] = value; - t1ToT2Dict[value] = key; - } - } - - public T2 this[T1 key] - { - get => t1ToT2Dict[key]; - set - { - t1ToT2Dict[key] = value; - t2ToT1Dict[value] = key; - } - } +namespace EpicTransport { + + public class BidirectionalDictionary : IEnumerable { + private Dictionary t1ToT2Dict = new Dictionary(); + private Dictionary t2ToT1Dict = new Dictionary(); + + public IEnumerable FirstTypes => t1ToT2Dict.Keys; + public IEnumerable SecondTypes => t2ToT1Dict.Keys; + + public IEnumerator GetEnumerator() => t1ToT2Dict.GetEnumerator(); + + public int Count => t1ToT2Dict.Count; + + public void Add(T1 key, T2 value) { + t1ToT2Dict[key] = value; + t2ToT1Dict[value] = key; + } + + public void Add(T2 key, T1 value) { + t2ToT1Dict[key] = value; + t1ToT2Dict[value] = key; + } + + public T2 Get(T1 key) => t1ToT2Dict[key]; + + public T1 Get(T2 key) => t2ToT1Dict[key]; + + public bool TryGetValue(T1 key, out T2 value) => t1ToT2Dict.TryGetValue(key, out value); + + public bool TryGetValue(T2 key, out T1 value) => t2ToT1Dict.TryGetValue(key, out value); + + public bool Contains(T1 key) => t1ToT2Dict.ContainsKey(key); + + public bool Contains(T2 key) => t2ToT1Dict.ContainsKey(key); + + public void Remove(T1 key) { + if (Contains(key)) { + T2 val = t1ToT2Dict[key]; + t1ToT2Dict.Remove(key); + t2ToT1Dict.Remove(val); + } + } + public void Remove(T2 key) { + if (Contains(key)) { + T1 val = t2ToT1Dict[key]; + t1ToT2Dict.Remove(val); + t2ToT1Dict.Remove(key); + } + } + + public T1 this[T2 key] { + get => t2ToT1Dict[key]; + set { + t2ToT1Dict[key] = value; + t1ToT2Dict[value] = key; + } + } + + public T2 this[T1 key] { + get => t1ToT2Dict[key]; + set { + t1ToT2Dict[key] = value; + t2ToT1Dict[value] = key; + } + } + + } } \ No newline at end of file diff --git a/EpicOnlineTransport/Client.cs b/EpicOnlineTransport/Client.cs index 9d2075d4d..4ee1616bc 100644 --- a/EpicOnlineTransport/Client.cs +++ b/EpicOnlineTransport/Client.cs @@ -1,196 +1,178 @@ using Epic.OnlineServices; using Epic.OnlineServices.P2P; +using Mirror; using System; using System.Threading; using System.Threading.Tasks; using UnityEngine; -namespace EpicTransport; - -public class Client : Common -{ - public SocketId socketId; - public ProductUserId serverId; - - public bool Connected { get; private set; } - public bool Error { get; private set; } - - private event Action OnReceivedData; - private event Action OnConnected; - public event Action OnDisconnected; - private Action SetTransportError; - - private TimeSpan ConnectionTimeout; - - public bool isConnecting = false; - public string hostAddress = ""; - private ProductUserId hostProductId = null; - private TaskCompletionSource connectedComplete; - private CancellationTokenSource cancelToken; - - private Client(EosTransport transport) : base(transport) => ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.timeout)); - - public static Client CreateClient(EosTransport transport, string host) - { - var c = new Client(transport); - - c.hostAddress = host; - c.socketId = new SocketId() { SocketName = RandomString.Generate(20) }; - - c.OnConnected += () => transport.OnClientConnected.Invoke(); - c.OnDisconnected += () => transport.OnClientDisconnected.Invoke(); - c.OnReceivedData += (data, channel) => transport.OnClientDataReceived.Invoke(new ArraySegment(data), channel); - c.SetTransportError = transport.SetTransportError; - - return c; - } - - public async void Connect(string host) - { - cancelToken = new CancellationTokenSource(); - - try - { - hostProductId = ProductUserId.FromString(host); - serverId = hostProductId; - connectedComplete = new TaskCompletionSource(); - - OnConnected += SetConnectedComplete; - - SendInternal(hostProductId, socketId, InternalMessages.CONNECT); - - Task connectedCompleteTask = connectedComplete.Task; - - if (await Task.WhenAny(connectedCompleteTask, Task.Delay(ConnectionTimeout /*, cancelToken.Token*/)) != connectedCompleteTask) - { - SetTransportError($"Connection to {host} timed out."); - Debug.LogError($"Connection to {host} timed out."); - OnConnected -= SetConnectedComplete; - OnConnectionFailed(hostProductId); - } - - OnConnected -= SetConnectedComplete; - } - catch (FormatException) - { - SetTransportError("Connection string was not in the right format. Did you enter a ProductId?"); - Debug.LogError($"Connection string was not in the right format. Did you enter a ProductId?"); - Error = true; - OnConnectionFailed(hostProductId); - } - catch (Exception ex) - { - SetTransportError(ex.Message); - Debug.LogError(ex.Message); - Error = true; - OnConnectionFailed(hostProductId); - } - finally - { - if (Error) - { - OnConnectionFailed(null); - } - } - } - - public void Disconnect() - { - if (serverId != null) - { - CloseP2PSessionWithUser(serverId, socketId); - - serverId = null; - } - else - { - return; - } - - SendInternal(hostProductId, socketId, InternalMessages.DISCONNECT); - - Dispose(); - cancelToken?.Cancel(); - - WaitForClose(hostProductId, socketId); - } - - private void SetConnectedComplete() => connectedComplete.SetResult(connectedComplete.Task); - - protected override void OnReceiveData(byte[] data, ProductUserId clientUserId, int channel) - { - if (ignoreAllMessages) - { - return; - } - - if (clientUserId != hostProductId) - { - Debug.LogError("Received a message from an unknown"); - return; - } - - OnReceivedData.Invoke(data, channel); - } - - protected override void OnNewConnection(OnIncomingConnectionRequestInfo result) - { - if (ignoreAllMessages) - { - return; - } - - if (deadSockets.Contains(result.SocketId.SocketName)) - { - Debug.LogError("Received incoming connection request from dead socket"); - return; - } - - if (hostProductId == result.RemoteUserId) - { - EOSSDKComponent.GetP2PInterface().AcceptConnection( - new AcceptConnectionOptions() - { - LocalUserId = EOSSDKComponent.LocalUserProductId, - RemoteUserId = result.RemoteUserId, - SocketId = result.SocketId - }); - } - else - { - Debug.LogError("P2P Acceptance Request from unknown host ID."); - } - } - - protected override void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserId, SocketId socketId) - { - if (ignoreAllMessages) - { - return; - } - - switch (type) - { - case InternalMessages.ACCEPT_CONNECT: - Connected = true; - OnConnected.Invoke(); - Debug.Log("Connection established."); - break; - case InternalMessages.DISCONNECT: - SetTransportError("host disconnected"); - Connected = false; - Debug.Log("Disconnected."); - - OnDisconnected.Invoke(); - break; - default: - Debug.Log("Received unknown message type"); - break; - } - } - - public void Send(byte[] data, int channelId) => Send(hostProductId, socketId, data, (byte)channelId); - - protected override void OnConnectionFailed(ProductUserId remoteId) => OnDisconnected.Invoke(); - public void EosNotInitialized() => OnDisconnected.Invoke(); +namespace EpicTransport { + public class Client : Common { + + public SocketId socketId; + public ProductUserId serverId; + + public bool Connected { get; private set; } + public bool Error { get; private set; } + + private event Action OnReceivedData; + private event Action OnConnected; + public event Action OnDisconnected; + // CHANGED + private Action SetTransportError; + + private TimeSpan ConnectionTimeout; + + public bool isConnecting = false; + public string hostAddress = ""; + private ProductUserId hostProductId = null; + private TaskCompletionSource connectedComplete; + private CancellationTokenSource cancelToken; + + private Client(EosTransport transport) : base(transport) { + ConnectionTimeout = TimeSpan.FromSeconds(Math.Max(1, transport.timeout)); + } + + public static Client CreateClient(EosTransport transport, string host) { + Client c = new Client(transport); + + c.hostAddress = host; + c.socketId = new SocketId() { SocketName = RandomString.Generate(20) }; + + c.OnConnected += () => transport.OnClientConnected.Invoke(); + c.OnDisconnected += () => transport.OnClientDisconnected.Invoke(); + c.OnReceivedData += (data, channel) => transport.OnClientDataReceived.Invoke(new ArraySegment(data), channel); + // CHANGED + c.SetTransportError = transport.SetTransportError; + + return c; + } + + public async void Connect(string host) { + cancelToken = new CancellationTokenSource(); + + try { + hostProductId = ProductUserId.FromString(host); + serverId = hostProductId; + connectedComplete = new TaskCompletionSource(); + + OnConnected += SetConnectedComplete; + + SendInternal(hostProductId, socketId, InternalMessages.CONNECT); + + Task connectedCompleteTask = connectedComplete.Task; + + if (await Task.WhenAny(connectedCompleteTask, Task.Delay(ConnectionTimeout/*, cancelToken.Token*/)) != connectedCompleteTask) { + // CHANGED + SetTransportError($"Connection to {host} timed out."); + Debug.LogError($"Connection to {host} timed out."); + OnConnected -= SetConnectedComplete; + OnConnectionFailed(hostProductId); + } + + OnConnected -= SetConnectedComplete; + } catch (FormatException) { + // CHANGED + SetTransportError("Connection string was not in the right format. Did you enter a ProductId?"); + Debug.LogError($"Connection string was not in the right format. Did you enter a ProductId?"); + Error = true; + OnConnectionFailed(hostProductId); + } catch (Exception ex) { + // CHANGED + SetTransportError(ex.Message); + Debug.LogError(ex.Message); + Error = true; + OnConnectionFailed(hostProductId); + } finally { + if (Error) { + OnConnectionFailed(null); + } + } + + } + + public void Disconnect() { + if (serverId != null) { + CloseP2PSessionWithUser(serverId, socketId); + + serverId = null; + } else { + return; + } + + SendInternal(hostProductId, socketId, InternalMessages.DISCONNECT); + + Dispose(); + cancelToken?.Cancel(); + + WaitForClose(hostProductId, socketId); + } + + private void SetConnectedComplete() => connectedComplete.SetResult(connectedComplete.Task); + + protected override void OnReceiveData(byte[] data, ProductUserId clientUserId, int channel) { + if (ignoreAllMessages) { + return; + } + + if (clientUserId != hostProductId) { + Debug.LogError("Received a message from an unknown"); + return; + } + + OnReceivedData.Invoke(data, channel); + } + + protected override void OnNewConnection(OnIncomingConnectionRequestInfo result) { + if (ignoreAllMessages) { + return; + } + + if (deadSockets.Contains(result.SocketId.SocketName)) { + Debug.LogError("Received incoming connection request from dead socket"); + return; + } + + if (hostProductId == result.RemoteUserId) { + EOSSDKComponent.GetP2PInterface().AcceptConnection( + new AcceptConnectionOptions() { + LocalUserId = EOSSDKComponent.LocalUserProductId, + RemoteUserId = result.RemoteUserId, + SocketId = result.SocketId + }); + } else { + Debug.LogError("P2P Acceptance Request from unknown host ID."); + } + } + + protected override void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserId, SocketId socketId) { + if (ignoreAllMessages) { + return; + } + + switch (type) { + case InternalMessages.ACCEPT_CONNECT: + Connected = true; + OnConnected.Invoke(); + Debug.Log("Connection established."); + break; + case InternalMessages.DISCONNECT: + // CHANGED + SetTransportError("host disconnected"); + Connected = false; + Debug.Log("Disconnected."); + + OnDisconnected.Invoke(); + break; + default: + Debug.Log("Received unknown message type"); + break; + } + } + + public void Send(byte[] data, int channelId) => Send(hostProductId, socketId, data, (byte) channelId); + + protected override void OnConnectionFailed(ProductUserId remoteId) => OnDisconnected.Invoke(); + public void EosNotInitialized() => OnDisconnected.Invoke(); + } } \ No newline at end of file diff --git a/EpicOnlineTransport/Common.cs b/EpicOnlineTransport/Common.cs index 0f5164c7e..57abf3da3 100644 --- a/EpicOnlineTransport/Common.cs +++ b/EpicOnlineTransport/Common.cs @@ -1,334 +1,288 @@ -using Epic.OnlineServices; + +using Epic.OnlineServices; using Epic.OnlineServices.P2P; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; -namespace EpicTransport; - -public abstract class Common -{ - private PacketReliability[] channels; - private int internal_ch => channels.Length; - - protected enum InternalMessages : byte - { - CONNECT, - ACCEPT_CONNECT, - DISCONNECT - } - - protected struct PacketKey - { - public ProductUserId productUserId; - public byte channel; - } - - private OnIncomingConnectionRequestCallback OnIncomingConnectionRequest; - private ulong incomingNotificationId = 0; - private OnRemoteConnectionClosedCallback OnRemoteConnectionClosed; - private ulong outgoingNotificationId = 0; - - protected readonly EosTransport transport; - - protected List deadSockets; - public bool ignoreAllMessages = false; - - // Mapping from PacketKey to a List of Packet Lists - protected Dictionary>> incomingPackets = new(); - - protected Common(EosTransport transport) - { - channels = transport.Channels; - - deadSockets = new List(); - - var addNotifyPeerConnectionRequestOptions = new AddNotifyPeerConnectionRequestOptions(); - addNotifyPeerConnectionRequestOptions.LocalUserId = EOSSDKComponent.LocalUserProductId; - addNotifyPeerConnectionRequestOptions.SocketId = null; - - OnIncomingConnectionRequest += OnNewConnection; - OnRemoteConnectionClosed += OnConnectFail; - - incomingNotificationId = EOSSDKComponent.GetP2PInterface().AddNotifyPeerConnectionRequest(addNotifyPeerConnectionRequestOptions, - null, OnIncomingConnectionRequest); - - var addNotifyPeerConnectionClosedOptions = new AddNotifyPeerConnectionClosedOptions(); - addNotifyPeerConnectionClosedOptions.LocalUserId = EOSSDKComponent.LocalUserProductId; - addNotifyPeerConnectionClosedOptions.SocketId = null; - - outgoingNotificationId = EOSSDKComponent.GetP2PInterface().AddNotifyPeerConnectionClosed(addNotifyPeerConnectionClosedOptions, - null, OnRemoteConnectionClosed); - - if (outgoingNotificationId == 0 || incomingNotificationId == 0) - { - Debug.LogError("Couldn't bind notifications with P2P interface"); - } - - incomingPackets = new Dictionary>>(); - - this.transport = transport; - } - - protected void Dispose() - { - EOSSDKComponent.GetP2PInterface().RemoveNotifyPeerConnectionRequest(incomingNotificationId); - EOSSDKComponent.GetP2PInterface().RemoveNotifyPeerConnectionClosed(outgoingNotificationId); - - transport.ResetIgnoreMessagesAtStartUpTimer(); - } - - protected abstract void OnNewConnection(OnIncomingConnectionRequestInfo result); - - private void OnConnectFail(OnRemoteConnectionClosedInfo result) - { - if (ignoreAllMessages) - { - return; - } - - OnConnectionFailed(result.RemoteUserId); - - switch (result.Reason) - { - case ConnectionClosedReason.ClosedByLocalUser: - throw new Exception("Connection cLosed: The Connection was gracecfully closed by the local user."); - case ConnectionClosedReason.ClosedByPeer: - throw new Exception("Connection closed: The connection was gracefully closed by remote user."); - case ConnectionClosedReason.ConnectionClosed: - throw new Exception("Connection closed: The connection was unexpectedly closed."); - case ConnectionClosedReason.ConnectionFailed: - throw new Exception("Connection failed: Failled to establish connection."); - case ConnectionClosedReason.InvalidData: - throw new Exception("Connection failed: The remote user sent us invalid data.."); - case ConnectionClosedReason.InvalidMessage: - throw new Exception("Connection failed: The remote user sent us an invalid message."); - case ConnectionClosedReason.NegotiationFailed: - throw new Exception("Connection failed: Negotiation failed."); - case ConnectionClosedReason.TimedOut: - throw new Exception("Connection failed: Timeout."); - case ConnectionClosedReason.TooManyConnections: - throw new Exception("Connection failed: Too many connections."); - case ConnectionClosedReason.UnexpectedError: - throw new Exception("Unexpected Error, connection will be closed"); - case ConnectionClosedReason.Unknown: - default: - throw new Exception("Unknown Error, connection has been closed."); - } - } - - protected void SendInternal(ProductUserId target, SocketId socketId, InternalMessages type) - { - EOSSDKComponent.GetP2PInterface().SendPacket(new SendPacketOptions() - { - AllowDelayedDelivery = true, - Channel = (byte)internal_ch, - Data = new byte[] { (byte)type }, - LocalUserId = EOSSDKComponent.LocalUserProductId, - Reliability = PacketReliability.ReliableOrdered, - RemoteUserId = target, - SocketId = socketId - }); - } - - protected void Send(ProductUserId host, SocketId socketId, byte[] msgBuffer, byte channel) - { - var result = EOSSDKComponent.GetP2PInterface().SendPacket(new SendPacketOptions() - { - AllowDelayedDelivery = true, - Channel = channel, - Data = msgBuffer, - LocalUserId = EOSSDKComponent.LocalUserProductId, - Reliability = channels[channel], - RemoteUserId = host, - SocketId = socketId - }); - - if (result != Result.Success) - { - Debug.LogError("Send failed " + result); - } - } - - private bool Receive(out ProductUserId clientProductUserId, out SocketId socketId, out byte[] receiveBuffer, byte channel) - { - var result = EOSSDKComponent.GetP2PInterface().ReceivePacket(new ReceivePacketOptions() - { - LocalUserId = EOSSDKComponent.LocalUserProductId, - MaxDataSizeBytes = P2PInterface.MaxPacketSize, - RequestedChannel = channel - }, out clientProductUserId, out socketId, out channel, out receiveBuffer); - - if (result == Result.Success) - { - return true; - } - - receiveBuffer = null; - clientProductUserId = null; - return false; - } - - protected virtual void CloseP2PSessionWithUser(ProductUserId clientUserID, SocketId socketId) - { - if (socketId == null) - { - Debug.LogWarning("Socket ID == null | " + ignoreAllMessages); - return; - } - - if (deadSockets == null) - { - Debug.LogWarning("DeadSockets == null"); - return; - } - - if (deadSockets.Contains(socketId.SocketName)) - { - return; - } - else - { - deadSockets.Add(socketId.SocketName); - } - } - - protected void WaitForClose(ProductUserId clientUserID, SocketId socketId) => transport.StartCoroutine(DelayedClose(clientUserID, socketId)); - - private IEnumerator DelayedClose(ProductUserId clientUserID, SocketId socketId) - { - yield return null; - CloseP2PSessionWithUser(clientUserID, socketId); - } - - public void ReceiveData() - { - try - { - // Internal Channel, no fragmentation here - var socketId = new SocketId(); - while (transport.enabled && Receive(out var clientUserID, out socketId, out var internalMessage, (byte)internal_ch)) - { - if (internalMessage.Length == 1) - { - OnReceiveInternalData((InternalMessages)internalMessage[0], clientUserID, socketId); - return; // Wait one frame - } - else - { - Debug.Log("Incorrect package length on internal channel."); - } - } - - // Insert new packet at the correct location in the incoming queue - for (var chNum = 0; chNum < channels.Length; chNum++) - { - while (transport.enabled && Receive(out var clientUserID, out socketId, out var receiveBuffer, (byte)chNum)) - { - var incomingPacketKey = new PacketKey(); - incomingPacketKey.productUserId = clientUserID; - incomingPacketKey.channel = (byte)chNum; - - var packet = new Packet(); - packet.FromBytes(receiveBuffer); - - if (!incomingPackets.ContainsKey(incomingPacketKey)) - { - incomingPackets.Add(incomingPacketKey, new List>()); - } - - var packetListIndex = incomingPackets[incomingPacketKey].Count; - for (var i = 0; i < incomingPackets[incomingPacketKey].Count; i++) - { - if (incomingPackets[incomingPacketKey][i][0].id == packet.id) - { - packetListIndex = i; - break; - } - } - - if (packetListIndex == incomingPackets[incomingPacketKey].Count) - { - incomingPackets[incomingPacketKey].Add(new List()); - } - - var insertionIndex = -1; - - for (var i = 0; i < incomingPackets[incomingPacketKey][packetListIndex].Count; i++) - { - if (incomingPackets[incomingPacketKey][packetListIndex][i].fragment > packet.fragment) - { - insertionIndex = i; - break; - } - } - - if (insertionIndex >= 0) - { - incomingPackets[incomingPacketKey][packetListIndex].Insert(insertionIndex, packet); - } - else - { - incomingPackets[incomingPacketKey][packetListIndex].Add(packet); - } - } - } - - // Find fully received packets - var emptyPacketLists = new List>(); - foreach (var keyValuePair in incomingPackets) - { - for (var packetList = 0; packetList < keyValuePair.Value.Count; packetList++) - { - var packetReady = true; - var packetLength = 0; - for (var packet = 0; packet < keyValuePair.Value[packetList].Count; packet++) - { - var tempPacket = keyValuePair.Value[packetList][packet]; - if (tempPacket.fragment != packet || packet == keyValuePair.Value[packetList].Count - 1 && tempPacket.moreFragments) - { - packetReady = false; - } - else - { - packetLength += tempPacket.data.Length; - } - } - - if (packetReady) - { - var data = new byte[packetLength]; - var dataIndex = 0; - - for (var packet = 0; packet < keyValuePair.Value[packetList].Count; packet++) - { - Array.Copy(keyValuePair.Value[packetList][packet].data, 0, data, dataIndex, keyValuePair.Value[packetList][packet].data.Length); - dataIndex += keyValuePair.Value[packetList][packet].data.Length; - } - - OnReceiveData(data, keyValuePair.Key.productUserId, keyValuePair.Key.channel); - - //keyValuePair.Value[packetList].Clear(); - emptyPacketLists.Add(keyValuePair.Value[packetList]); - } - } - - for (var i = 0; i < emptyPacketLists.Count; i++) - { - keyValuePair.Value.Remove(emptyPacketLists[i]); - } - - emptyPacketLists.Clear(); - } - } - catch (Exception e) - { - Debug.LogException(e); - } - } - - protected abstract void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserID, SocketId socketId); - protected abstract void OnReceiveData(byte[] data, ProductUserId clientUserID, int channel); - protected abstract void OnConnectionFailed(ProductUserId remoteId); +namespace EpicTransport { + public abstract class Common { + + private PacketReliability[] channels; + private int internal_ch => channels.Length; + + protected enum InternalMessages : byte { + CONNECT, + ACCEPT_CONNECT, + DISCONNECT + } + + protected struct PacketKey { + public ProductUserId productUserId; + public byte channel; + } + + private OnIncomingConnectionRequestCallback OnIncomingConnectionRequest; + ulong incomingNotificationId = 0; + private OnRemoteConnectionClosedCallback OnRemoteConnectionClosed; + ulong outgoingNotificationId = 0; + + protected readonly EosTransport transport; + + protected List deadSockets; + public bool ignoreAllMessages = false; + + // Mapping from PacketKey to a List of Packet Lists + protected Dictionary>> incomingPackets = new Dictionary>>(); + + protected Common(EosTransport transport) { + channels = transport.Channels; + + deadSockets = new List(); + + AddNotifyPeerConnectionRequestOptions addNotifyPeerConnectionRequestOptions = new AddNotifyPeerConnectionRequestOptions(); + addNotifyPeerConnectionRequestOptions.LocalUserId = EOSSDKComponent.LocalUserProductId; + addNotifyPeerConnectionRequestOptions.SocketId = null; + + OnIncomingConnectionRequest += OnNewConnection; + OnRemoteConnectionClosed += OnConnectFail; + + incomingNotificationId = EOSSDKComponent.GetP2PInterface().AddNotifyPeerConnectionRequest(addNotifyPeerConnectionRequestOptions, + null, OnIncomingConnectionRequest); + + AddNotifyPeerConnectionClosedOptions addNotifyPeerConnectionClosedOptions = new AddNotifyPeerConnectionClosedOptions(); + addNotifyPeerConnectionClosedOptions.LocalUserId = EOSSDKComponent.LocalUserProductId; + addNotifyPeerConnectionClosedOptions.SocketId = null; + + outgoingNotificationId = EOSSDKComponent.GetP2PInterface().AddNotifyPeerConnectionClosed(addNotifyPeerConnectionClosedOptions, + null, OnRemoteConnectionClosed); + + if (outgoingNotificationId == 0 || incomingNotificationId == 0) { + Debug.LogError("Couldn't bind notifications with P2P interface"); + } + + incomingPackets = new Dictionary>>(); + + this.transport = transport; + + } + + protected void Dispose() { + EOSSDKComponent.GetP2PInterface().RemoveNotifyPeerConnectionRequest(incomingNotificationId); + EOSSDKComponent.GetP2PInterface().RemoveNotifyPeerConnectionClosed(outgoingNotificationId); + + transport.ResetIgnoreMessagesAtStartUpTimer(); + } + + protected abstract void OnNewConnection(OnIncomingConnectionRequestInfo result); + + private void OnConnectFail(OnRemoteConnectionClosedInfo result) { + if (ignoreAllMessages) { + return; + } + + OnConnectionFailed(result.RemoteUserId); + + switch (result.Reason) { + case ConnectionClosedReason.ClosedByLocalUser: + throw new Exception("Connection cLosed: The Connection was gracecfully closed by the local user."); + case ConnectionClosedReason.ClosedByPeer: + throw new Exception("Connection closed: The connection was gracefully closed by remote user."); + case ConnectionClosedReason.ConnectionClosed: + throw new Exception("Connection closed: The connection was unexpectedly closed."); + case ConnectionClosedReason.ConnectionFailed: + throw new Exception("Connection failed: Failled to establish connection."); + case ConnectionClosedReason.InvalidData: + throw new Exception("Connection failed: The remote user sent us invalid data.."); + case ConnectionClosedReason.InvalidMessage: + throw new Exception("Connection failed: The remote user sent us an invalid message."); + case ConnectionClosedReason.NegotiationFailed: + throw new Exception("Connection failed: Negotiation failed."); + case ConnectionClosedReason.TimedOut: + throw new Exception("Connection failed: Timeout."); + case ConnectionClosedReason.TooManyConnections: + throw new Exception("Connection failed: Too many connections."); + case ConnectionClosedReason.UnexpectedError: + throw new Exception("Unexpected Error, connection will be closed"); + case ConnectionClosedReason.Unknown: + default: + throw new Exception("Unknown Error, connection has been closed."); + } + } + + protected void SendInternal(ProductUserId target, SocketId socketId, InternalMessages type) { + EOSSDKComponent.GetP2PInterface().SendPacket(new SendPacketOptions() { + AllowDelayedDelivery = true, + Channel = (byte) internal_ch, + Data = new byte[] { (byte) type }, + LocalUserId = EOSSDKComponent.LocalUserProductId, + Reliability = PacketReliability.ReliableOrdered, + RemoteUserId = target, + SocketId = socketId + }); + } + + + protected void Send(ProductUserId host, SocketId socketId, byte[] msgBuffer, byte channel) { + Result result = EOSSDKComponent.GetP2PInterface().SendPacket(new SendPacketOptions() { + AllowDelayedDelivery = true, + Channel = channel, + Data = msgBuffer, + LocalUserId = EOSSDKComponent.LocalUserProductId, + Reliability = channels[channel], + RemoteUserId = host, + SocketId = socketId + }); + + if(result != Result.Success) { + Debug.LogError("Send failed " + result); + } + } + + private bool Receive(out ProductUserId clientProductUserId, out SocketId socketId, out byte[] receiveBuffer, byte channel) { + Result result = EOSSDKComponent.GetP2PInterface().ReceivePacket(new ReceivePacketOptions() { + LocalUserId = EOSSDKComponent.LocalUserProductId, + MaxDataSizeBytes = P2PInterface.MaxPacketSize, + RequestedChannel = channel + }, out clientProductUserId, out socketId, out channel, out receiveBuffer); + + if (result == Result.Success) { + return true; + } + + receiveBuffer = null; + clientProductUserId = null; + return false; + } + + protected virtual void CloseP2PSessionWithUser(ProductUserId clientUserID, SocketId socketId) { + if (socketId == null) { + Debug.LogWarning("Socket ID == null | " + ignoreAllMessages); + return; + } + + if (deadSockets == null) { + Debug.LogWarning("DeadSockets == null"); + return; + } + + if (deadSockets.Contains(socketId.SocketName)) { + return; + } else { + deadSockets.Add(socketId.SocketName); + } + } + + + protected void WaitForClose(ProductUserId clientUserID, SocketId socketId) => transport.StartCoroutine(DelayedClose(clientUserID, socketId)); + private IEnumerator DelayedClose(ProductUserId clientUserID, SocketId socketId) { + yield return null; + CloseP2PSessionWithUser(clientUserID, socketId); + } + + public void ReceiveData() { + try { + // Internal Channel, no fragmentation here + SocketId socketId = new SocketId(); + while (transport.enabled && Receive(out ProductUserId clientUserID, out socketId, out byte[] internalMessage, (byte) internal_ch)) { + if (internalMessage.Length == 1) { + OnReceiveInternalData((InternalMessages) internalMessage[0], clientUserID, socketId); + return; // Wait one frame + } else { + Debug.Log("Incorrect package length on internal channel."); + } + } + + // Insert new packet at the correct location in the incoming queue + for (int chNum = 0; chNum < channels.Length; chNum++) { + while (transport.enabled && Receive(out ProductUserId clientUserID, out socketId, out byte[] receiveBuffer, (byte) chNum)) { + PacketKey incomingPacketKey = new PacketKey(); + incomingPacketKey.productUserId = clientUserID; + incomingPacketKey.channel = (byte)chNum; + + Packet packet = new Packet(); + packet.FromBytes(receiveBuffer); + + if (!incomingPackets.ContainsKey(incomingPacketKey)) { + incomingPackets.Add(incomingPacketKey, new List>()); + } + + int packetListIndex = incomingPackets[incomingPacketKey].Count; + for(int i = 0; i < incomingPackets[incomingPacketKey].Count; i++) { + if(incomingPackets[incomingPacketKey][i][0].id == packet.id) { + packetListIndex = i; + break; + } + } + + if (packetListIndex == incomingPackets[incomingPacketKey].Count) { + incomingPackets[incomingPacketKey].Add(new List()); + } + + int insertionIndex = -1; + + for (int i = 0; i < incomingPackets[incomingPacketKey][packetListIndex].Count; i++) { + if (incomingPackets[incomingPacketKey][packetListIndex][i].fragment > packet.fragment) { + insertionIndex = i; + break; + } + } + + if (insertionIndex >= 0) { + incomingPackets[incomingPacketKey][packetListIndex].Insert(insertionIndex, packet); + } else { + incomingPackets[incomingPacketKey][packetListIndex].Add(packet); + } + } + } + + // Find fully received packets + List> emptyPacketLists = new List>(); + foreach(KeyValuePair>> keyValuePair in incomingPackets) { + for(int packetList = 0; packetList < keyValuePair.Value.Count; packetList++) { + bool packetReady = true; + int packetLength = 0; + for (int packet = 0; packet < keyValuePair.Value[packetList].Count; packet++) { + Packet tempPacket = keyValuePair.Value[packetList][packet]; + if (tempPacket.fragment != packet || (packet == keyValuePair.Value[packetList].Count - 1 && tempPacket.moreFragments)) { + packetReady = false; + } else { + packetLength += tempPacket.data.Length; + } + } + + if (packetReady) { + byte[] data = new byte[packetLength]; + int dataIndex = 0; + + for (int packet = 0; packet < keyValuePair.Value[packetList].Count; packet++) { + Array.Copy(keyValuePair.Value[packetList][packet].data, 0, data, dataIndex, keyValuePair.Value[packetList][packet].data.Length); + dataIndex += keyValuePair.Value[packetList][packet].data.Length; + } + + OnReceiveData(data, keyValuePair.Key.productUserId, keyValuePair.Key.channel); + + if(transport.ServerActive() || transport.ClientActive()) + emptyPacketLists.Add(keyValuePair.Value[packetList]); + } + } + + for (int i = 0; i < emptyPacketLists.Count; i++) { + keyValuePair.Value.Remove(emptyPacketLists[i]); + } + emptyPacketLists.Clear(); + } + + + + } catch (Exception e) { + Debug.LogException(e); + } + } + + protected abstract void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserID, SocketId socketId); + protected abstract void OnReceiveData(byte[] data, ProductUserId clientUserID, int channel); + protected abstract void OnConnectionFailed(ProductUserId remoteId); + } } \ No newline at end of file diff --git a/EpicOnlineTransport/EOSSDKComponent.cs b/EpicOnlineTransport/EOSSDKComponent.cs index 2fdd9d776..6a322b860 100644 --- a/EpicOnlineTransport/EOSSDKComponent.cs +++ b/EpicOnlineTransport/EOSSDKComponent.cs @@ -1,6 +1,10 @@ using Epic.OnlineServices; using Epic.OnlineServices.Logging; using Epic.OnlineServices.Platform; + +using System; +using System.Runtime.InteropServices; + using UnityEngine; /// @@ -10,120 +14,142 @@ /// after releasing the SDK the game has to be restarted in order to initialize the SDK again. /// In the unity editor the OnDestroy function will not run so that we dont have to restart the editor after play. /// -namespace EpicTransport; - -[DefaultExecutionOrder(-32000)] -public class EOSSDKComponent : MonoBehaviour -{ - // Unity Inspector shown variables - - [SerializeField] - public EosApiKey apiKeys; - - [Header("User Login")] - public bool authInterfaceLogin = false; - - public Epic.OnlineServices.Auth.LoginCredentialType authInterfaceCredentialType = Epic.OnlineServices.Auth.LoginCredentialType.AccountPortal; - public uint devAuthToolPort = 7878; - public string devAuthToolCredentialName = ""; - public ExternalCredentialType connectInterfaceCredentialType = ExternalCredentialType.DeviceidAccessToken; - public string deviceModel = "PC Windows 64bit"; - [SerializeField] private string displayName = "User"; - - public static string DisplayName - { - get => Instance.displayName; - set => Instance.displayName = value; - } - - [Header("Misc")] - public LogLevel epicLoggerLevel = LogLevel.Error; - - [SerializeField] - private bool collectPlayerMetrics = true; - - public static bool CollectPlayerMetrics => Instance.collectPlayerMetrics; - - public bool checkForEpicLauncherAndRestart = false; - public bool delayedInitialization = false; - public float platformTickIntervalInSeconds = 0.0f; - private float platformTickTimer = 0f; - public uint tickBudgetInMilliseconds = 0; - - // End Unity Inspector shown variables - - private ulong authExpirationHandle; - - private string authInterfaceLoginCredentialId = null; - public static void SetAuthInterfaceLoginCredentialId(string credentialId) => Instance.authInterfaceLoginCredentialId = credentialId; - private string authInterfaceCredentialToken = null; - public static void SetAuthInterfaceCredentialToken(string credentialToken) => Instance.authInterfaceCredentialToken = credentialToken; - private string connectInterfaceCredentialToken = null; - public static void SetConnectInterfaceCredentialToken(string credentialToken) => Instance.connectInterfaceCredentialToken = credentialToken; - - private PlatformInterface EOS; - - // Interfaces - public static Epic.OnlineServices.Achievements.AchievementsInterface GetAchievementsInterface() => Instance.EOS.GetAchievementsInterface(); - public static Epic.OnlineServices.Auth.AuthInterface GetAuthInterface() => Instance.EOS.GetAuthInterface(); - public static Epic.OnlineServices.Connect.ConnectInterface GetConnectInterface() => Instance.EOS.GetConnectInterface(); - public static Epic.OnlineServices.Ecom.EcomInterface GetEcomInterface() => Instance.EOS.GetEcomInterface(); - public static Epic.OnlineServices.Friends.FriendsInterface GetFriendsInterface() => Instance.EOS.GetFriendsInterface(); - public static Epic.OnlineServices.Leaderboards.LeaderboardsInterface GetLeaderboardsInterface() => Instance.EOS.GetLeaderboardsInterface(); - public static Epic.OnlineServices.Lobby.LobbyInterface GetLobbyInterface() => Instance.EOS.GetLobbyInterface(); - public static Epic.OnlineServices.Metrics.MetricsInterface GetMetricsInterface() => Instance.EOS.GetMetricsInterface(); // Handled by the transport automatically, only use this interface if Mirror is not used for singleplayer - public static Epic.OnlineServices.Mods.ModsInterface GetModsInterface() => Instance.EOS.GetModsInterface(); - public static Epic.OnlineServices.P2P.P2PInterface GetP2PInterface() => Instance.EOS.GetP2PInterface(); - public static Epic.OnlineServices.PlayerDataStorage.PlayerDataStorageInterface GetPlayerDataStorageInterface() => Instance.EOS.GetPlayerDataStorageInterface(); - public static Epic.OnlineServices.Presence.PresenceInterface GetPresenceInterface() => Instance.EOS.GetPresenceInterface(); - public static Epic.OnlineServices.Sessions.SessionsInterface GetSessionsInterface() => Instance.EOS.GetSessionsInterface(); - public static Epic.OnlineServices.TitleStorage.TitleStorageInterface GetTitleStorageInterface() => Instance.EOS.GetTitleStorageInterface(); - public static Epic.OnlineServices.UI.UIInterface GetUIInterface() => Instance.EOS.GetUIInterface(); - public static Epic.OnlineServices.UserInfo.UserInfoInterface GetUserInfoInterface() => Instance.EOS.GetUserInfoInterface(); - - protected EpicAccountId localUserAccountId; - public static EpicAccountId LocalUserAccountId => Instance.localUserAccountId; - - protected string localUserAccountIdString; - public static string LocalUserAccountIdString => Instance.localUserAccountIdString; - - protected ProductUserId localUserProductId; - public static ProductUserId LocalUserProductId => Instance.localUserProductId; - - protected string localUserProductIdString; - public static string LocalUserProductIdString => Instance.localUserProductIdString; - - protected bool initialized; - public static bool Initialized => Instance.initialized; - - protected bool isConnecting; - public static bool IsConnecting => Instance.isConnecting; - - protected static EOSSDKComponent instance; - - protected static EOSSDKComponent Instance - { - get - { - if (instance == null) - { - return new GameObject("EOSSDKComponent").AddComponent(); - } - else - { - return instance; - } - } - } - - public static void Tick() - { - instance.platformTickTimer -= Time.deltaTime; - instance.EOS.Tick(); - } - - // If we're in editor, we should dynamically load and unload the SDK between play sessions. - // This allows us to initialize the SDK each time the game is run in editor. +namespace EpicTransport { + [DefaultExecutionOrder(-32000)] + public class EOSSDKComponent : MonoBehaviour { + + // Unity Inspector shown variables + + [SerializeField] + // CHANGED + public EosApiKey apiKeys; + + [Header("User Login")] + public bool authInterfaceLogin = false; + public Epic.OnlineServices.Auth.LoginCredentialType authInterfaceCredentialType = Epic.OnlineServices.Auth.LoginCredentialType.AccountPortal; + public uint devAuthToolPort = 7878; + public string devAuthToolCredentialName = ""; + public Epic.OnlineServices.ExternalCredentialType connectInterfaceCredentialType = Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken; + public string deviceModel = "PC Windows 64bit"; + [SerializeField] private string displayName = "User"; + public static string DisplayName { + get { + return Instance.displayName; + } + set { + Instance.displayName = value; + } + } + + [Header("Misc")] + public LogLevel epicLoggerLevel = LogLevel.Error; + + [SerializeField] private bool collectPlayerMetrics = true; + public static bool CollectPlayerMetrics { + get { + return Instance.collectPlayerMetrics; + } + } + + public bool checkForEpicLauncherAndRestart = false; + public bool delayedInitialization = false; + public float platformTickIntervalInSeconds = 0.0f; + private float platformTickTimer = 0f; + public uint tickBudgetInMilliseconds = 0; + + // End Unity Inspector shown variables + + private ulong authExpirationHandle; + + + private string authInterfaceLoginCredentialId = null; + public static void SetAuthInterfaceLoginCredentialId(string credentialId) => Instance.authInterfaceLoginCredentialId = credentialId; + private string authInterfaceCredentialToken = null; + public static void SetAuthInterfaceCredentialToken(string credentialToken) => Instance.authInterfaceCredentialToken = credentialToken; + private string connectInterfaceCredentialToken = null; + public static void SetConnectInterfaceCredentialToken(string credentialToken) => Instance.connectInterfaceCredentialToken = credentialToken; + + private PlatformInterface EOS; + + // Interfaces + public static Epic.OnlineServices.Achievements.AchievementsInterface GetAchievementsInterface() => Instance.EOS.GetAchievementsInterface(); + public static Epic.OnlineServices.Auth.AuthInterface GetAuthInterface() => Instance.EOS.GetAuthInterface(); + public static Epic.OnlineServices.Connect.ConnectInterface GetConnectInterface() => Instance.EOS.GetConnectInterface(); + public static Epic.OnlineServices.Ecom.EcomInterface GetEcomInterface() => Instance.EOS.GetEcomInterface(); + public static Epic.OnlineServices.Friends.FriendsInterface GetFriendsInterface() => Instance.EOS.GetFriendsInterface(); + public static Epic.OnlineServices.Leaderboards.LeaderboardsInterface GetLeaderboardsInterface() => Instance.EOS.GetLeaderboardsInterface(); + public static Epic.OnlineServices.Lobby.LobbyInterface GetLobbyInterface() => Instance.EOS.GetLobbyInterface(); + public static Epic.OnlineServices.Metrics.MetricsInterface GetMetricsInterface() => Instance.EOS.GetMetricsInterface(); // Handled by the transport automatically, only use this interface if Mirror is not used for singleplayer + public static Epic.OnlineServices.Mods.ModsInterface GetModsInterface() => Instance.EOS.GetModsInterface(); + public static Epic.OnlineServices.P2P.P2PInterface GetP2PInterface() => Instance.EOS.GetP2PInterface(); + public static Epic.OnlineServices.PlayerDataStorage.PlayerDataStorageInterface GetPlayerDataStorageInterface() => Instance.EOS.GetPlayerDataStorageInterface(); + public static Epic.OnlineServices.Presence.PresenceInterface GetPresenceInterface() => Instance.EOS.GetPresenceInterface(); + public static Epic.OnlineServices.Sessions.SessionsInterface GetSessionsInterface() => Instance.EOS.GetSessionsInterface(); + public static Epic.OnlineServices.TitleStorage.TitleStorageInterface GetTitleStorageInterface() => Instance.EOS.GetTitleStorageInterface(); + public static Epic.OnlineServices.UI.UIInterface GetUIInterface() => Instance.EOS.GetUIInterface(); + public static Epic.OnlineServices.UserInfo.UserInfoInterface GetUserInfoInterface() => Instance.EOS.GetUserInfoInterface(); + + + protected EpicAccountId localUserAccountId; + public static EpicAccountId LocalUserAccountId { + get { + return Instance.localUserAccountId; + } + } + + protected string localUserAccountIdString; + public static string LocalUserAccountIdString { + get { + return Instance.localUserAccountIdString; + } + } + + protected ProductUserId localUserProductId; + public static ProductUserId LocalUserProductId { + get { + return Instance.localUserProductId; + } + } + + protected string localUserProductIdString; + public static string LocalUserProductIdString { + get { + return Instance.localUserProductIdString; + } + } + + protected bool initialized; + public static bool Initialized { + get { + return Instance.initialized; + } + } + + protected bool isConnecting; + public static bool IsConnecting { + get { + return Instance.isConnecting; + } + } + + protected static EOSSDKComponent instance; + protected static EOSSDKComponent Instance { + get { + if (instance == null) { + return new GameObject("EOSSDKComponent").AddComponent(); + } else { + return instance; + } + } + } + + public static void Tick() { + instance.platformTickTimer -= Time.deltaTime; + instance.EOS.Tick(); + } + + // If we're in editor, we should dynamically load and unload the SDK between play sessions. + // This allows us to initialize the SDK each time the game is run in editor. #if UNITY_EDITOR_WIN [DllImport("Kernel32.dll")] private static extern IntPtr LoadLibrary(string lpLibFileName); @@ -136,7 +162,7 @@ public static void Tick() private IntPtr libraryPointer; #endif - + #if UNITY_EDITOR_LINUX [DllImport("libdl.so", EntryPoint = "dlopen")] private static extern IntPtr LoadLibrary(String lpFileName, int flags = 2); @@ -163,27 +189,24 @@ private static IntPtr GetProcAddress(IntPtr hModule, string lpProcName) { private IntPtr libraryPointer; #endif - private void Awake() - { - // Initialize Java version of the SDK with a reference to the VM with JNI - // See https://eoshelp.epicgames.com/s/question/0D54z00006ufJBNCA2/cant-get-createdeviceid-to-work-in-unity-android-c-sdk?language=en_US - if (Application.platform == RuntimePlatform.Android) - { - var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); - var activity = unityPlayer.GetStatic("currentActivity"); - var context = activity.Call("getApplicationContext"); - var EOS_SDK_JAVA = new AndroidJavaClass("com.epicgames.mobile.eossdk.EOSSDK"); - EOS_SDK_JAVA.CallStatic("init", context); - } - - // Prevent multiple instances - if (instance != null) - { - Destroy(gameObject); - return; - } - - instance = this; + private void Awake() { + // Initialize Java version of the SDK with a reference to the VM with JNI + // See https://eoshelp.epicgames.com/s/question/0D54z00006ufJBNCA2/cant-get-createdeviceid-to-work-in-unity-android-c-sdk?language=en_US + if (Application.platform == RuntimePlatform.Android) + { + AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); + AndroidJavaObject activity = unityPlayer.GetStatic("currentActivity"); + AndroidJavaObject context = activity.Call("getApplicationContext"); + AndroidJavaClass EOS_SDK_JAVA = new AndroidJavaClass("com.epicgames.mobile.eossdk.EOSSDK"); + EOS_SDK_JAVA.CallStatic("init", context); + } + + // Prevent multiple instances + if (instance != null) { + Destroy(gameObject); + return; + } + instance = this; #if UNITY_EDITOR var libraryPath = "Assets/Mirror/Runtime/Transport/EpicOnlineTransport/EOSSDK/" + Config.LibraryName; @@ -196,260 +219,207 @@ private void Awake() Bindings.Hook(libraryPointer, GetProcAddress); #endif - if (!delayedInitialization) - { - Initialize(); - } - } - - protected void InitializeImplementation() - { - isConnecting = true; - - var initializeOptions = new InitializeOptions() - { - ProductName = apiKeys.epicProductName, - ProductVersion = apiKeys.epicProductVersion - }; - - var initializeResult = PlatformInterface.Initialize(initializeOptions); - - // This code is called each time the game is run in the editor, so we catch the case where the SDK has already been initialized in the editor. - var isAlreadyConfiguredInEditor = Application.isEditor && initializeResult == Result.AlreadyConfigured; - if (initializeResult != Result.Success && !isAlreadyConfiguredInEditor) - { - throw new System.Exception("Failed to initialize platform: " + initializeResult); - } - - // The SDK outputs lots of information that is useful for debugging. - // Make sure to set up the logging interface as early as possible: after initializing. - LoggingInterface.SetLogLevel(LogCategory.AllCategories, epicLoggerLevel); - LoggingInterface.SetCallback(message => Logger.EpicDebugLog(message)); - - var options = new Options() - { - ProductId = apiKeys.epicProductId, - SandboxId = apiKeys.epicSandboxId, - DeploymentId = apiKeys.epicDeploymentId, - ClientCredentials = new ClientCredentials() - { - ClientId = apiKeys.epicClientId, - ClientSecret = apiKeys.epicClientSecret - }, - TickBudgetInMilliseconds = tickBudgetInMilliseconds - }; - - EOS = PlatformInterface.Create(options); - if (EOS == null) - { - throw new System.Exception("Failed to create platform"); - } - - if (checkForEpicLauncherAndRestart) - { - var result = EOS.CheckForLauncherAndRestart(); - - // If not started through epic launcher the app will be restarted and we can quit - if (result != Result.NoChange) - { - // Log error if launcher check failed, but still quit to prevent hacking - if (result == Result.UnexpectedError) - { - Debug.LogError("Unexpected Error while checking if app was started through epic launcher"); - } - - Application.Quit(); - } - } - - // If we use the Auth interface then only login into the Connect interface after finishing the auth interface login - // If we don't use the Auth interface we can directly login to the Connect interface - if (authInterfaceLogin) - { - if (authInterfaceCredentialType == Epic.OnlineServices.Auth.LoginCredentialType.Developer) - { - authInterfaceLoginCredentialId = "localhost:" + devAuthToolPort; - authInterfaceCredentialToken = devAuthToolCredentialName; - } - - // Login to Auth Interface - var loginOptions = new Epic.OnlineServices.Auth.LoginOptions() - { - Credentials = new Epic.OnlineServices.Auth.Credentials() - { - Type = authInterfaceCredentialType, - Id = authInterfaceLoginCredentialId, - Token = authInterfaceCredentialToken - }, - ScopeFlags = Epic.OnlineServices.Auth.AuthScopeFlags.BasicProfile | Epic.OnlineServices.Auth.AuthScopeFlags.FriendsList | Epic.OnlineServices.Auth.AuthScopeFlags.Presence - }; - - EOS.GetAuthInterface().Login(loginOptions, null, OnAuthInterfaceLogin); - } - else - { - // Login to Connect Interface - if (connectInterfaceCredentialType == ExternalCredentialType.DeviceidAccessToken) - { - var createDeviceIdOptions = new Epic.OnlineServices.Connect.CreateDeviceIdOptions(); - createDeviceIdOptions.DeviceModel = deviceModel; - EOS.GetConnectInterface().CreateDeviceId(createDeviceIdOptions, null, OnCreateDeviceId); - } - else - { - ConnectInterfaceLogin(); - } - } - } - - public static void Initialize() - { - if (Instance.initialized || Instance.isConnecting) - { - return; - } - - Instance.InitializeImplementation(); - } - - private void OnAuthInterfaceLogin(Epic.OnlineServices.Auth.LoginCallbackInfo loginCallbackInfo) - { - if (loginCallbackInfo.ResultCode == Result.Success) - { - Debug.Log("Auth Interface Login succeeded"); - - string accountIdString; - var result = loginCallbackInfo.LocalUserId.ToString(out accountIdString); - if (Result.Success == result) - { - Debug.Log("EOS User ID:" + accountIdString); - - localUserAccountIdString = accountIdString; - localUserAccountId = loginCallbackInfo.LocalUserId; - } - - ConnectInterfaceLogin(); - } - else if (Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode)) - { - Debug.Log("Login returned " + loginCallbackInfo.ResultCode); - } - } - - private void OnCreateDeviceId(Epic.OnlineServices.Connect.CreateDeviceIdCallbackInfo createDeviceIdCallbackInfo) - { - if (createDeviceIdCallbackInfo.ResultCode == Result.Success || createDeviceIdCallbackInfo.ResultCode == Result.DuplicateNotAllowed) - { - ConnectInterfaceLogin(); - } - else if (Epic.OnlineServices.Common.IsOperationComplete(createDeviceIdCallbackInfo.ResultCode)) - { - Debug.Log("Device ID creation returned " + createDeviceIdCallbackInfo.ResultCode); - } - } - - private void ConnectInterfaceLogin() - { - var loginOptions = new Epic.OnlineServices.Connect.LoginOptions(); - - if (connectInterfaceCredentialType == ExternalCredentialType.Epic) - { - Epic.OnlineServices.Auth.Token token; - var result = EOS.GetAuthInterface().CopyUserAuthToken(new Epic.OnlineServices.Auth.CopyUserAuthTokenOptions(), localUserAccountId, out token); - - if (result == Result.Success) - { - connectInterfaceCredentialToken = token.AccessToken; - } - else - { - Debug.LogError("Failed to retrieve User Auth Token"); - } - } - else if (connectInterfaceCredentialType == ExternalCredentialType.DeviceidAccessToken) - { - loginOptions.UserLoginInfo = new Epic.OnlineServices.Connect.UserLoginInfo(); - loginOptions.UserLoginInfo.DisplayName = displayName; - } - - loginOptions.Credentials = new Epic.OnlineServices.Connect.Credentials(); - loginOptions.Credentials.Type = connectInterfaceCredentialType; - loginOptions.Credentials.Token = connectInterfaceCredentialToken; - - EOS.GetConnectInterface().Login(loginOptions, null, OnConnectInterfaceLogin); - } - - private void OnConnectInterfaceLogin(Epic.OnlineServices.Connect.LoginCallbackInfo loginCallbackInfo) - { - if (loginCallbackInfo.ResultCode == Result.Success) - { - Debug.Log("Connect Interface Login succeeded"); - - string productIdString; - var result = loginCallbackInfo.LocalUserId.ToString(out productIdString); - if (Result.Success == result) - { - Debug.Log("EOS User Product ID:" + productIdString); - - localUserProductIdString = productIdString; - localUserProductId = loginCallbackInfo.LocalUserId; - } - - initialized = true; - isConnecting = false; - - var authExpirationOptions = new Epic.OnlineServices.Connect.AddNotifyAuthExpirationOptions(); - authExpirationHandle = EOS.GetConnectInterface().AddNotifyAuthExpiration(authExpirationOptions, null, OnAuthExpiration); - } - else if (Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode)) - { - Debug.Log("Login returned " + loginCallbackInfo.ResultCode + "\nRetrying..."); - EOS.GetConnectInterface().CreateUser(new Epic.OnlineServices.Connect.CreateUserOptions() { ContinuanceToken = loginCallbackInfo.ContinuanceToken }, null, (Epic.OnlineServices.Connect.CreateUserCallbackInfo cb) => - { - if (cb.ResultCode != Result.Success) - { - Debug.Log(cb.ResultCode); - return; - } - - localUserProductId = cb.LocalUserId; - ConnectInterfaceLogin(); - }); - } - } - - private void OnAuthExpiration(Epic.OnlineServices.Connect.AuthExpirationCallbackInfo authExpirationCallbackInfo) - { - Debug.Log("AuthExpiration callback"); - EOS.GetConnectInterface().RemoveNotifyAuthExpiration(authExpirationHandle); - ConnectInterfaceLogin(); - } - - // Calling tick on a regular interval is required for callbacks to work. - private void LateUpdate() - { - if (EOS != null) - { - platformTickTimer += Time.deltaTime; - - if (platformTickTimer >= platformTickIntervalInSeconds) - { - platformTickTimer = 0; - EOS.Tick(); - } - } - } - - private void OnApplicationQuit() - { - if (EOS != null) - { - EOS.Release(); - EOS = null; - PlatformInterface.Shutdown(); - } - - // Unhook the library in the editor, this makes it possible to load the library again after stopping to play + if (!delayedInitialization) { + Initialize(); + } + } + + protected void InitializeImplementation() { + isConnecting = true; + + var initializeOptions = new InitializeOptions() { + ProductName = apiKeys.epicProductName, + ProductVersion = apiKeys.epicProductVersion + }; + + var initializeResult = PlatformInterface.Initialize(initializeOptions); + + // This code is called each time the game is run in the editor, so we catch the case where the SDK has already been initialized in the editor. + var isAlreadyConfiguredInEditor = Application.isEditor && initializeResult == Result.AlreadyConfigured; + if (initializeResult != Result.Success && !isAlreadyConfiguredInEditor) { + throw new System.Exception("Failed to initialize platform: " + initializeResult); + } + + // The SDK outputs lots of information that is useful for debugging. + // Make sure to set up the logging interface as early as possible: after initializing. + LoggingInterface.SetLogLevel(LogCategory.AllCategories, epicLoggerLevel); + LoggingInterface.SetCallback(message => Logger.EpicDebugLog(message)); + + var options = new Options() { + ProductId = apiKeys.epicProductId, + SandboxId = apiKeys.epicSandboxId, + DeploymentId = apiKeys.epicDeploymentId, + ClientCredentials = new ClientCredentials() { + ClientId = apiKeys.epicClientId, + ClientSecret = apiKeys.epicClientSecret + }, + TickBudgetInMilliseconds = tickBudgetInMilliseconds + }; + + EOS = PlatformInterface.Create(options); + if (EOS == null) { + throw new System.Exception("Failed to create platform"); + } + + if (checkForEpicLauncherAndRestart) { + Result result = EOS.CheckForLauncherAndRestart(); + + // If not started through epic launcher the app will be restarted and we can quit + if (result != Result.NoChange) { + + // Log error if launcher check failed, but still quit to prevent hacking + if (result == Result.UnexpectedError) { + Debug.LogError("Unexpected Error while checking if app was started through epic launcher"); + } + + Application.Quit(); + } + } + + // If we use the Auth interface then only login into the Connect interface after finishing the auth interface login + // If we don't use the Auth interface we can directly login to the Connect interface + if (authInterfaceLogin) { + if (authInterfaceCredentialType == Epic.OnlineServices.Auth.LoginCredentialType.Developer) { + authInterfaceLoginCredentialId = "localhost:" + devAuthToolPort; + authInterfaceCredentialToken = devAuthToolCredentialName; + } + + // Login to Auth Interface + Epic.OnlineServices.Auth.LoginOptions loginOptions = new Epic.OnlineServices.Auth.LoginOptions() { + Credentials = new Epic.OnlineServices.Auth.Credentials() { + Type = authInterfaceCredentialType, + Id = authInterfaceLoginCredentialId, + Token = authInterfaceCredentialToken + }, + ScopeFlags = Epic.OnlineServices.Auth.AuthScopeFlags.BasicProfile | Epic.OnlineServices.Auth.AuthScopeFlags.FriendsList | Epic.OnlineServices.Auth.AuthScopeFlags.Presence + }; + + EOS.GetAuthInterface().Login(loginOptions, null, OnAuthInterfaceLogin); + } else { + // Login to Connect Interface + if (connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken) { + Epic.OnlineServices.Connect.CreateDeviceIdOptions createDeviceIdOptions = new Epic.OnlineServices.Connect.CreateDeviceIdOptions(); + createDeviceIdOptions.DeviceModel = deviceModel; + EOS.GetConnectInterface().CreateDeviceId(createDeviceIdOptions, null, OnCreateDeviceId); + } else { + ConnectInterfaceLogin(); + } + } + + } + public static void Initialize() { + if (Instance.initialized || Instance.isConnecting) { + return; + } + + Instance.InitializeImplementation(); + } + + private void OnAuthInterfaceLogin(Epic.OnlineServices.Auth.LoginCallbackInfo loginCallbackInfo) { + if (loginCallbackInfo.ResultCode == Result.Success) { + Debug.Log("Auth Interface Login succeeded"); + + string accountIdString; + Result result = loginCallbackInfo.LocalUserId.ToString(out accountIdString); + if (Result.Success == result) { + Debug.Log("EOS User ID:" + accountIdString); + + localUserAccountIdString = accountIdString; + localUserAccountId = loginCallbackInfo.LocalUserId; + } + + ConnectInterfaceLogin(); + } else if(Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode)){ + Debug.Log("Login returned " + loginCallbackInfo.ResultCode); + } + } + + private void OnCreateDeviceId(Epic.OnlineServices.Connect.CreateDeviceIdCallbackInfo createDeviceIdCallbackInfo) { + if (createDeviceIdCallbackInfo.ResultCode == Result.Success || createDeviceIdCallbackInfo.ResultCode == Result.DuplicateNotAllowed) { + ConnectInterfaceLogin(); + } else if(Epic.OnlineServices.Common.IsOperationComplete(createDeviceIdCallbackInfo.ResultCode)) { + Debug.Log("Device ID creation returned " + createDeviceIdCallbackInfo.ResultCode); + } + } + + private void ConnectInterfaceLogin() { + var loginOptions = new Epic.OnlineServices.Connect.LoginOptions(); + + if (connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.Epic) { + Epic.OnlineServices.Auth.Token token; + Result result = EOS.GetAuthInterface().CopyUserAuthToken(new Epic.OnlineServices.Auth.CopyUserAuthTokenOptions(), localUserAccountId, out token); + + if (result == Result.Success) { + connectInterfaceCredentialToken = token.AccessToken; + } else { + Debug.LogError("Failed to retrieve User Auth Token"); + } + } else if (connectInterfaceCredentialType == Epic.OnlineServices.ExternalCredentialType.DeviceidAccessToken) { + loginOptions.UserLoginInfo = new Epic.OnlineServices.Connect.UserLoginInfo(); + loginOptions.UserLoginInfo.DisplayName = displayName; + } + + loginOptions.Credentials = new Epic.OnlineServices.Connect.Credentials(); + loginOptions.Credentials.Type = connectInterfaceCredentialType; + loginOptions.Credentials.Token = connectInterfaceCredentialToken; + + EOS.GetConnectInterface().Login(loginOptions, null, OnConnectInterfaceLogin); + } + + private void OnConnectInterfaceLogin(Epic.OnlineServices.Connect.LoginCallbackInfo loginCallbackInfo) { + if (loginCallbackInfo.ResultCode == Result.Success) { + Debug.Log("Connect Interface Login succeeded"); + + string productIdString; + Result result = loginCallbackInfo.LocalUserId.ToString(out productIdString); + if (Result.Success == result) { + Debug.Log("EOS User Product ID:" + productIdString); + + localUserProductIdString = productIdString; + localUserProductId = loginCallbackInfo.LocalUserId; + } + + initialized = true; + isConnecting = false; + + var authExpirationOptions = new Epic.OnlineServices.Connect.AddNotifyAuthExpirationOptions(); + authExpirationHandle = EOS.GetConnectInterface().AddNotifyAuthExpiration(authExpirationOptions, null, OnAuthExpiration); + } else if (Epic.OnlineServices.Common.IsOperationComplete(loginCallbackInfo.ResultCode)) { + Debug.Log("Login returned " + loginCallbackInfo.ResultCode + "\nRetrying..."); + EOS.GetConnectInterface().CreateUser(new Epic.OnlineServices.Connect.CreateUserOptions() { ContinuanceToken = loginCallbackInfo.ContinuanceToken }, null, (Epic.OnlineServices.Connect.CreateUserCallbackInfo cb) => { + if (cb.ResultCode != Result.Success) { Debug.Log(cb.ResultCode); return; } + localUserProductId = cb.LocalUserId; + ConnectInterfaceLogin(); + }); + } + } + + private void OnAuthExpiration(Epic.OnlineServices.Connect.AuthExpirationCallbackInfo authExpirationCallbackInfo) { + Debug.Log("AuthExpiration callback"); + EOS.GetConnectInterface().RemoveNotifyAuthExpiration(authExpirationHandle); + ConnectInterfaceLogin(); + } + + // Calling tick on a regular interval is required for callbacks to work. + private void LateUpdate() { + if (EOS != null) { + platformTickTimer += Time.deltaTime; + + if (platformTickTimer >= platformTickIntervalInSeconds) { + platformTickTimer = 0; + EOS.Tick(); + } + } + } + + private void OnApplicationQuit() { + if (EOS != null) { + EOS.Release(); + EOS = null; + PlatformInterface.Shutdown(); + } + + // Unhook the library in the editor, this makes it possible to load the library again after stopping to play #if UNITY_EDITOR if (libraryPointer != IntPtr.Zero) { Bindings.Unhook(); @@ -460,5 +430,6 @@ private void OnApplicationQuit() libraryPointer = IntPtr.Zero; } #endif - } -} + } + } +} \ No newline at end of file diff --git a/EpicOnlineTransport/EosApiKey.cs b/EpicOnlineTransport/EosApiKey.cs index 34ee0c19a..ddc037d1a 100644 --- a/EpicOnlineTransport/EosApiKey.cs +++ b/EpicOnlineTransport/EosApiKey.cs @@ -1,3 +1,5 @@ +using System.Collections; +using System.Collections.Generic; using UnityEngine; /// @@ -8,14 +10,14 @@ /// Create -> EOS -> API Key /// in order to create an instance of this scriptable object /// + [CreateAssetMenu(fileName = "EosApiKey", menuName = "EOS/API Key", order = 1)] -public class EosApiKey : ScriptableObject -{ - public string epicProductName = "MyApplication"; - public string epicProductVersion = "1.0"; - public string epicProductId = ""; - public string epicSandboxId = ""; - public string epicDeploymentId = ""; - public string epicClientId = ""; - public string epicClientSecret = ""; +public class EosApiKey : ScriptableObject { + public string epicProductName = "MyApplication"; + public string epicProductVersion = "1.0"; + public string epicProductId = ""; + public string epicSandboxId = ""; + public string epicDeploymentId = ""; + public string epicClientId = ""; + public string epicClientSecret = ""; } diff --git a/EpicOnlineTransport/EosTransport.cs b/EpicOnlineTransport/EosTransport.cs index fc7f7fab4..ca4c60155 100644 --- a/EpicOnlineTransport/EosTransport.cs +++ b/EpicOnlineTransport/EosTransport.cs @@ -1,399 +1,334 @@ -using Epic.OnlineServices; -using Epic.OnlineServices.Metrics; +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; using Epic.OnlineServices.P2P; +using Epic.OnlineServices; using Mirror; -using System; +using Epic.OnlineServices.Metrics; using System.Collections; -using UnityEngine; - -namespace EpicTransport; -/// -/// EOS Transport following the Mirror transport standard -/// -public class EosTransport : Transport -{ - private const string EPIC_SCHEME = "epic"; - - private Client client; - private Server server; - - private Common activeNode; - - [SerializeField] - public PacketReliability[] Channels = new PacketReliability[2] { PacketReliability.ReliableOrdered, PacketReliability.UnreliableUnordered }; - - [Tooltip("Timeout for connecting in seconds.")] - public int timeout = 25; - - [Tooltip("The max fragments used in fragmentation before throwing an error.")] - public int maxFragments = 55; - - public float ignoreCachedMessagesAtStartUpInSeconds = 2.0f; - private float ignoreCachedMessagesTimer = 0.0f; - - public RelayControl relayControl = RelayControl.AllowRelays; - - [Header("Info")] - [Tooltip("This will display your Epic Account ID when you start or connect to a server.")] - public ProductUserId productUserId; - - private int packetId = 0; - - public Action SetTransportError; - - private void Awake() - { - Debug.Assert(Channels != null && Channels.Length > 0, "No channel configured for EOS Transport."); - Debug.Assert(Channels.Length < byte.MaxValue, "Too many channels configured for EOS Transport"); - - if (Channels[0] != PacketReliability.ReliableOrdered) - { - Debug.LogWarning("EOS Transport Channel[0] is not ReliableOrdered, Mirror expects Channel 0 to be ReliableOrdered, only change this if you know what you are doing."); - } - - if (Channels[1] != PacketReliability.UnreliableUnordered) - { - Debug.LogWarning("EOS Transport Channel[1] is not UnreliableUnordered, Mirror expects Channel 1 to be UnreliableUnordered, only change this if you know what you are doing."); - } - - StartCoroutine("FetchEpicAccountId"); - StartCoroutine("ChangeRelayStatus"); - } - - public override void ClientEarlyUpdate() - { - EOSSDKComponent.Tick(); - - if (activeNode != null) - { - ignoreCachedMessagesTimer += Time.deltaTime; - - if (ignoreCachedMessagesTimer <= ignoreCachedMessagesAtStartUpInSeconds) - { - activeNode.ignoreAllMessages = true; - } - else - { - activeNode.ignoreAllMessages = false; - - if (client != null && !client.isConnecting) - { - if (EOSSDKComponent.Initialized) - { - client.Connect(client.hostAddress); - } - else - { - Debug.LogError("EOS not initialized"); - client.EosNotInitialized(); - } - - client.isConnecting = true; - } - } - } - - if (enabled) - { - activeNode?.ReceiveData(); - } - } - - public override void ClientLateUpdate() { } - - public override void ServerEarlyUpdate() - { - EOSSDKComponent.Tick(); - - if (activeNode != null) - { - ignoreCachedMessagesTimer += Time.deltaTime; - - if (ignoreCachedMessagesTimer <= ignoreCachedMessagesAtStartUpInSeconds) - { - activeNode.ignoreAllMessages = true; - } - else - { - activeNode.ignoreAllMessages = false; - } - } - - if (enabled) - { - activeNode?.ReceiveData(); - } - } - - public override void ServerLateUpdate() { } - - public override bool ClientConnected() => ClientActive() && client.Connected; - - public override void ClientConnect(string address) - { - if (!EOSSDKComponent.Initialized) - { - Debug.LogError("EOS not initialized. Client could not be started."); - OnClientDisconnected.Invoke(); - return; - } - - StartCoroutine("FetchEpicAccountId"); - - if (ServerActive()) - { - Debug.LogError("Transport already running as server!"); - return; - } - - if (!ClientActive() || client.Error) - { - Debug.Log($"Starting client, target address {address}."); - - client = Client.CreateClient(this, address); - activeNode = client; - - if (EOSSDKComponent.CollectPlayerMetrics) - { - // Start Metrics colletion session - var sessionOptions = new BeginPlayerSessionOptions(); - sessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; - sessionOptions.ControllerType = UserControllerType.Unknown; - sessionOptions.DisplayName = EOSSDKComponent.DisplayName; - sessionOptions.GameSessionId = null; - sessionOptions.ServerIp = null; - var result = EOSSDKComponent.GetMetricsInterface().BeginPlayerSession(sessionOptions); - - if (result == Result.Success) - { - Debug.Log("Started Metric Session"); - } - } - } - else - { - Debug.LogError("Client already running!"); - } - } - - public override void ClientConnect(Uri uri) - { - if (uri.Scheme != EPIC_SCHEME) - { - throw new ArgumentException($"Invalid url {uri}, use {EPIC_SCHEME}://EpicAccountId instead", nameof(uri)); - } - - ClientConnect(uri.Host); - } - - public override void ClientSend(ArraySegment segment, int channelId) - { - Send(channelId, segment); - } - - public override void ClientDisconnect() - { - if (ClientActive()) - { - Shutdown(); - } - } - - public bool ClientActive() => client != null; - - public override bool ServerActive() => server != null; - - public override void ServerStart() - { - if (!EOSSDKComponent.Initialized) - { - Debug.LogError("EOS not initialized. Server could not be started."); - return; - } - - StartCoroutine("FetchEpicAccountId"); - - if (ClientActive()) - { - Debug.LogError("Transport already running as client!"); - return; - } - - if (!ServerActive()) - { - Debug.Log("Starting server."); - - server = Server.CreateServer(this, NetworkManager.singleton.maxConnections); - activeNode = server; - - if (EOSSDKComponent.CollectPlayerMetrics) - { - // Start Metrics colletion session - var sessionOptions = new BeginPlayerSessionOptions(); - sessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; - sessionOptions.ControllerType = UserControllerType.Unknown; - sessionOptions.DisplayName = EOSSDKComponent.DisplayName; - sessionOptions.GameSessionId = null; - sessionOptions.ServerIp = null; - var result = EOSSDKComponent.GetMetricsInterface().BeginPlayerSession(sessionOptions); - - if (result == Result.Success) - { - Debug.Log("Started Metric Session"); - } - } - } - else - { - Debug.LogError("Server already started!"); - } - } - - public override Uri ServerUri() - { - var epicBuilder = new UriBuilder - { - Scheme = EPIC_SCHEME, - Host = EOSSDKComponent.LocalUserProductIdString - }; - - return epicBuilder.Uri; - } - - public override void ServerSend(int connectionId, ArraySegment segment, int channelId) - { - if (ServerActive()) - { - Send(channelId, segment, connectionId); - } - } - - public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId); - public override string ServerGetClientAddress(int connectionId) => ServerActive() ? server.ServerGetClientAddress(connectionId) : string.Empty; - - public override void ServerStop() - { - if (ServerActive()) - { - Shutdown(); - } - } - - private void Send(int channelId, ArraySegment segment, int connectionId = int.MinValue) - { - var packets = GetPacketArray(channelId, segment); - - for (var i = 0; i < packets.Length; i++) - { - if (connectionId == int.MinValue) - { - client.Send(packets[i].ToBytes(), channelId); - } - else - { - server.SendAll(connectionId, packets[i].ToBytes(), channelId); - } - } - - packetId++; - } - - private Packet[] GetPacketArray(int channelId, ArraySegment segment) - { - var packetCount = Mathf.CeilToInt((float)segment.Count / (float)GetMaxSinglePacketSize(channelId)); - var packets = new Packet[packetCount]; - - for (var i = 0; i < segment.Count; i += GetMaxSinglePacketSize(channelId)) - { - var fragment = i / GetMaxSinglePacketSize(channelId); - - packets[fragment] = new Packet(); - packets[fragment].id = packetId; - packets[fragment].fragment = fragment; - packets[fragment].moreFragments = segment.Count - i > GetMaxSinglePacketSize(channelId); - packets[fragment].data = new byte[segment.Count - i > GetMaxSinglePacketSize(channelId) ? GetMaxSinglePacketSize(channelId) : segment.Count - i]; - Array.Copy(segment.Array, i, packets[fragment].data, 0, packets[fragment].data.Length); - } - - return packets; - } - - public override void Shutdown() - { - if (EOSSDKComponent.CollectPlayerMetrics) - { - // Stop Metrics collection session - var endSessionOptions = new EndPlayerSessionOptions(); - endSessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; - var result = EOSSDKComponent.GetMetricsInterface().EndPlayerSession(endSessionOptions); - - if (result == Result.Success) - { - Debug.LogError("Stopped Metric Session"); - } - } - - server?.Shutdown(); - client?.Disconnect(); - - server = null; - client = null; - activeNode = null; - Debug.Log("Transport shut down."); - } - - public int GetMaxSinglePacketSize(int channelId) => P2PInterface.MaxPacketSize - 10; // 1159 bytes, we need to remove 10 bytes for the packet header (id (4 bytes) + fragment (4 bytes) + more fragments (1 byte)) - - public override int GetMaxPacketSize(int channelId) => P2PInterface.MaxPacketSize * maxFragments; - - public override int GetBatchThreshold(int channelId) => P2PInterface.MaxPacketSize; // Use P2PInterface.MaxPacketSize as everything above will get fragmentated and will be counter effective to batching - - public override bool Available() - { - try - { - return EOSSDKComponent.Initialized; - } - catch - { - return false; - } - } - - private IEnumerator FetchEpicAccountId() - { - while (!EOSSDKComponent.Initialized) - { - yield return null; - } - - productUserId = EOSSDKComponent.LocalUserProductId; - } - - private IEnumerator ChangeRelayStatus() - { - while (!EOSSDKComponent.Initialized) - { - yield return null; - } - - var setRelayControlOptions = new SetRelayControlOptions(); - setRelayControlOptions.RelayControl = relayControl; - - EOSSDKComponent.GetP2PInterface().SetRelayControl(setRelayControlOptions); - } - - public void ResetIgnoreMessagesAtStartUpTimer() - { - ignoreCachedMessagesTimer = 0; - } - - private void OnDestroy() - { - if (activeNode != null) - { - Shutdown(); - } - } -} \ No newline at end of file +namespace EpicTransport { + + /// + /// EOS Transport following the Mirror transport standard + /// + public class EosTransport : Transport { + private const string EPIC_SCHEME = "epic"; + + private Client client; + private Server server; + + private Common activeNode; + + [SerializeField] + public PacketReliability[] Channels = new PacketReliability[2] { PacketReliability.ReliableOrdered, PacketReliability.UnreliableUnordered }; + + [Tooltip("Timeout for connecting in seconds.")] + public int timeout = 25; + + [Tooltip("The max fragments used in fragmentation before throwing an error.")] + public int maxFragments = 55; + + public float ignoreCachedMessagesAtStartUpInSeconds = 2.0f; + private float ignoreCachedMessagesTimer = 0.0f; + + public RelayControl relayControl = RelayControl.AllowRelays; + + [Header("Info")] + [Tooltip("This will display your Epic Account ID when you start or connect to a server.")] + public ProductUserId productUserId; + + private int packetId = 0; + + // CHANGED + public Action SetTransportError; + + private void Awake() { + Debug.Assert(Channels != null && Channels.Length > 0, "No channel configured for EOS Transport."); + Debug.Assert(Channels.Length < byte.MaxValue, "Too many channels configured for EOS Transport"); + + if(Channels[0] != PacketReliability.ReliableOrdered) { + Debug.LogWarning("EOS Transport Channel[0] is not ReliableOrdered, Mirror expects Channel 0 to be ReliableOrdered, only change this if you know what you are doing."); + } + if (Channels[1] != PacketReliability.UnreliableUnordered) { + Debug.LogWarning("EOS Transport Channel[1] is not UnreliableUnordered, Mirror expects Channel 1 to be UnreliableUnordered, only change this if you know what you are doing."); + } + + StartCoroutine("FetchEpicAccountId"); + StartCoroutine("ChangeRelayStatus"); + } + + public override void ClientEarlyUpdate() { + EOSSDKComponent.Tick(); + + if (activeNode != null) { + ignoreCachedMessagesTimer += Time.deltaTime; + + if (ignoreCachedMessagesTimer <= ignoreCachedMessagesAtStartUpInSeconds) { + activeNode.ignoreAllMessages = true; + } else { + activeNode.ignoreAllMessages = false; + + if (client != null && !client.isConnecting) { + if (EOSSDKComponent.Initialized) { + client.Connect(client.hostAddress); + } else { + Debug.LogError("EOS not initialized"); + client.EosNotInitialized(); + } + client.isConnecting = true; + } + } + } + + if (enabled) { + activeNode?.ReceiveData(); + } + } + + public override void ClientLateUpdate() {} + + public override void ServerEarlyUpdate() { + EOSSDKComponent.Tick(); + + if (activeNode != null) { + ignoreCachedMessagesTimer += Time.deltaTime; + + if (ignoreCachedMessagesTimer <= ignoreCachedMessagesAtStartUpInSeconds) { + activeNode.ignoreAllMessages = true; + } else { + activeNode.ignoreAllMessages = false; + } + } + + if (enabled) { + activeNode?.ReceiveData(); + } + } + + public override void ServerLateUpdate() {} + + public override bool ClientConnected() => ClientActive() && client.Connected; + public override void ClientConnect(string address) { + if (!EOSSDKComponent.Initialized) { + Debug.LogError("EOS not initialized. Client could not be started."); + OnClientDisconnected.Invoke(); + return; + } + + StartCoroutine("FetchEpicAccountId"); + + if (ServerActive()) { + Debug.LogError("Transport already running as server!"); + return; + } + + if (!ClientActive() || client.Error) { + Debug.Log($"Starting client, target address {address}."); + + client = Client.CreateClient(this, address); + activeNode = client; + + if (EOSSDKComponent.CollectPlayerMetrics) { + // Start Metrics colletion session + BeginPlayerSessionOptions sessionOptions = new BeginPlayerSessionOptions(); + sessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; + sessionOptions.ControllerType = UserControllerType.Unknown; + sessionOptions.DisplayName = EOSSDKComponent.DisplayName; + sessionOptions.GameSessionId = null; + sessionOptions.ServerIp = null; + Result result = EOSSDKComponent.GetMetricsInterface().BeginPlayerSession(sessionOptions); + + if(result == Result.Success) { + Debug.Log("Started Metric Session"); + } + } + } else { + Debug.LogError("Client already running!"); + } + } + + public override void ClientConnect(Uri uri) { + if (uri.Scheme != EPIC_SCHEME) + throw new ArgumentException($"Invalid url {uri}, use {EPIC_SCHEME}://EpicAccountId instead", nameof(uri)); + + ClientConnect(uri.Host); + } + + public override void ClientSend(ArraySegment segment, int channelId) { + Send(channelId, segment); + } + + public override void ClientDisconnect() { + if (ClientActive()) { + Shutdown(); + } + } + public bool ClientActive() => client != null; + + + public override bool ServerActive() => server != null; + public override void ServerStart() { + if (!EOSSDKComponent.Initialized) { + Debug.LogError("EOS not initialized. Server could not be started."); + return; + } + + StartCoroutine("FetchEpicAccountId"); + + if (ClientActive()) { + Debug.LogError("Transport already running as client!"); + return; + } + + if (!ServerActive()) { + Debug.Log("Starting server."); + + server = Server.CreateServer(this, NetworkManager.singleton.maxConnections); + activeNode = server; + + if (EOSSDKComponent.CollectPlayerMetrics) { + // Start Metrics colletion session + BeginPlayerSessionOptions sessionOptions = new BeginPlayerSessionOptions(); + sessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; + sessionOptions.ControllerType = UserControllerType.Unknown; + sessionOptions.DisplayName = EOSSDKComponent.DisplayName; + sessionOptions.GameSessionId = null; + sessionOptions.ServerIp = null; + Result result = EOSSDKComponent.GetMetricsInterface().BeginPlayerSession(sessionOptions); + + if (result == Result.Success) { + Debug.Log("Started Metric Session"); + } + } + } else { + Debug.LogError("Server already started!"); + } + } + + public override Uri ServerUri() { + UriBuilder epicBuilder = new UriBuilder { + Scheme = EPIC_SCHEME, + Host = EOSSDKComponent.LocalUserProductIdString + }; + + return epicBuilder.Uri; + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) { + if (ServerActive()) { + Send( channelId, segment, connectionId); + } + } + public override void ServerDisconnect(int connectionId) => server.Disconnect(connectionId); + public override string ServerGetClientAddress(int connectionId) => ServerActive() ? server.ServerGetClientAddress(connectionId) : string.Empty; + public override void ServerStop() { + if (ServerActive()) { + Shutdown(); + } + } + + private void Send(int channelId, ArraySegment segment, int connectionId = int.MinValue) { + Packet[] packets = GetPacketArray(channelId, segment); + + for(int i = 0; i < packets.Length; i++) { + if (connectionId == int.MinValue) { + if (client == null) + { + OnClientDisconnected.Invoke(); + return; + } + + client.Send(packets[i].ToBytes(), channelId); + } else { + server.SendAll(connectionId, packets[i].ToBytes(), channelId); + } + } + + packetId++; + } + + private Packet[] GetPacketArray(int channelId, ArraySegment segment) { + int packetCount = Mathf.CeilToInt((float) segment.Count / (float)GetMaxSinglePacketSize(channelId)); + Packet[] packets = new Packet[packetCount]; + + for (int i = 0; i < segment.Count; i += GetMaxSinglePacketSize(channelId)) { + int fragment = i / GetMaxSinglePacketSize(channelId); + + packets[fragment] = new Packet(); + packets[fragment].id = packetId; + packets[fragment].fragment = fragment; + packets[fragment].moreFragments = (segment.Count - i) > GetMaxSinglePacketSize(channelId); + packets[fragment].data = new byte[segment.Count - i > GetMaxSinglePacketSize(channelId) ? GetMaxSinglePacketSize(channelId) : segment.Count - i]; + Array.Copy(segment.Array, i, packets[fragment].data, 0, packets[fragment].data.Length); + } + + return packets; + } + + public override void Shutdown() { + if (EOSSDKComponent.CollectPlayerMetrics) { + // Stop Metrics collection session + EndPlayerSessionOptions endSessionOptions = new EndPlayerSessionOptions(); + endSessionOptions.AccountId = EOSSDKComponent.LocalUserAccountId; + Result result = EOSSDKComponent.GetMetricsInterface().EndPlayerSession(endSessionOptions); + + if (result == Result.Success) { + Debug.LogError("Stopped Metric Session"); + } + } + + server?.Shutdown(); + client?.Disconnect(); + + server = null; + client = null; + activeNode = null; + Debug.Log("Transport shut down."); + } + + public int GetMaxSinglePacketSize(int channelId) => P2PInterface.MaxPacketSize - 10; // 1159 bytes, we need to remove 10 bytes for the packet header (id (4 bytes) + fragment (4 bytes) + more fragments (1 byte)) + + public override int GetMaxPacketSize(int channelId) => P2PInterface.MaxPacketSize * maxFragments; + + public override int GetBatchThreshold(int channelId) => P2PInterface.MaxPacketSize; // Use P2PInterface.MaxPacketSize as everything above will get fragmentated and will be counter effective to batching + + public override bool Available() { + try { + return EOSSDKComponent.Initialized; + } catch { + return false; + } + } + + private IEnumerator FetchEpicAccountId() { + while (!EOSSDKComponent.Initialized) { + yield return null; + } + + productUserId = EOSSDKComponent.LocalUserProductId; + } + + private IEnumerator ChangeRelayStatus() { + while (!EOSSDKComponent.Initialized) { + yield return null; + } + + SetRelayControlOptions setRelayControlOptions = new SetRelayControlOptions(); + setRelayControlOptions.RelayControl = relayControl; + + EOSSDKComponent.GetP2PInterface().SetRelayControl(setRelayControlOptions); + } + + public void ResetIgnoreMessagesAtStartUpTimer() { + ignoreCachedMessagesTimer = 0; + } + + private void OnDestroy() { + if (activeNode != null) { + Shutdown(); + } + } + } +} diff --git a/EpicOnlineTransport/Logger.cs b/EpicOnlineTransport/Logger.cs index c93377fb7..197c6e0c3 100644 --- a/EpicOnlineTransport/Logger.cs +++ b/EpicOnlineTransport/Logger.cs @@ -1,30 +1,30 @@ using Epic.OnlineServices.Logging; using System; +using System.Collections; +using System.Collections.Generic; using UnityEngine; -namespace EpicTransport; +namespace EpicTransport { + public static class Logger { -public static class Logger -{ - public static void EpicDebugLog(LogMessage message) - { - switch (message.Level) - { - case LogLevel.Info: - Debug.Log($"Epic Manager: Category - {message.Category} Message - {message.Message}"); - break; - case LogLevel.Error: - Debug.LogError($"Epic Manager: Category - {message.Category} Message - {message.Message}"); - break; - case LogLevel.Warning: - Debug.LogWarning($"Epic Manager: Category - {message.Category} Message - {message.Message}"); - break; - case LogLevel.Fatal: - Debug.LogException(new Exception($"Epic Manager: Category - {message.Category} Message - {message.Message}")); - break; - default: - Debug.Log($"Epic Manager: Unknown log processing. Category - {message.Category} Message - {message.Message}"); - break; - } - } + public static void EpicDebugLog(LogMessage message) { + switch (message.Level) { + case LogLevel.Info: + Debug.Log($"Epic Manager: Category - {message.Category} Message - {message.Message}"); + break; + case LogLevel.Error: + Debug.LogError($"Epic Manager: Category - {message.Category} Message - {message.Message}"); + break; + case LogLevel.Warning: + Debug.LogWarning($"Epic Manager: Category - {message.Category} Message - {message.Message}"); + break; + case LogLevel.Fatal: + Debug.LogException(new Exception($"Epic Manager: Category - {message.Category} Message - {message.Message}")); + break; + default: + Debug.Log($"Epic Manager: Unknown log processing. Category - {message.Category} Message - {message.Message}"); + break; + } + } + } } \ No newline at end of file diff --git a/EpicOnlineTransport/Packet.cs b/EpicOnlineTransport/Packet.cs index 90c740066..cb43d7556 100644 --- a/EpicOnlineTransport/Packet.cs +++ b/EpicOnlineTransport/Packet.cs @@ -1,50 +1,47 @@ using System; -namespace EpicTransport; - -public struct Packet -{ - public const int headerSize = sizeof(uint) + sizeof(uint) + 1; - public int size => headerSize + data.Length; - - // header - public int id; - public int fragment; - public bool moreFragments; - - // body - public byte[] data; - - public byte[] ToBytes() - { - var array = new byte[size]; - - // Copy id - array[0] = (byte)id; - array[1] = (byte)(id >> 8); - array[2] = (byte)(id >> 0x10); - array[3] = (byte)(id >> 0x18); - - // Copy fragment - array[4] = (byte)fragment; - array[5] = (byte)(fragment >> 8); - array[6] = (byte)(fragment >> 0x10); - array[7] = (byte)(fragment >> 0x18); - - array[8] = moreFragments ? (byte)1 : (byte)0; - - Array.Copy(data, 0, array, 9, data.Length); - - return array; - } - - public void FromBytes(byte[] array) - { - id = BitConverter.ToInt32(array, 0); - fragment = BitConverter.ToInt32(array, 4); - moreFragments = array[8] == 1; - - data = new byte[array.Length - 9]; - Array.Copy(array, 9, data, 0, data.Length); - } +namespace EpicTransport { + public struct Packet { + public const int headerSize = sizeof(uint) + sizeof(uint) + 1; + public int size => headerSize + data.Length; + + // header + public int id; + public int fragment; + public bool moreFragments; + + // body + public byte[] data; + + public byte[] ToBytes() { + byte[] array = new byte[size]; + + // Copy id + array[0] = (byte) id; + array[1] = (byte) (id >> 8); + array[2] = (byte) (id >> 0x10); + array[3] = (byte) (id >> 0x18); + + // Copy fragment + array[4] = (byte) fragment; + array[5] = (byte) (fragment >> 8); + array[6] = (byte) (fragment >> 0x10); + array[7] = (byte) (fragment >> 0x18); + + array[8] = moreFragments ? (byte)1 : (byte)0; + + Array.Copy(data, 0, array, 9, data.Length); + + return array; + } + + public void FromBytes(byte[] array) { + id = BitConverter.ToInt32(array, 0); + fragment = BitConverter.ToInt32(array, 4); + moreFragments = array[8] == 1; + + data = new byte[array.Length - 9]; + Array.Copy(array, 9, data, 0, data.Length); + } + } } \ No newline at end of file diff --git a/EpicOnlineTransport/RandomString.cs b/EpicOnlineTransport/RandomString.cs index fc7ee602f..46b7dc1d6 100644 --- a/EpicOnlineTransport/RandomString.cs +++ b/EpicOnlineTransport/RandomString.cs @@ -1,41 +1,36 @@ using System; using System.Text; -public class RandomString -{ - // Generates a random string with a given size. - public static string Generate(int size) - { - var builder = new StringBuilder(size); +public class RandomString { - var random = new Random(); + // Generates a random string with a given size. + public static string Generate(int size) { + var builder = new StringBuilder(size); - // Unicode/ASCII Letters are divided into two blocks - // (Letters 65–90 / 97–122): - // The first group containing the uppercase letters and - // the second group containing the lowercase. + Random random = new Random(); - // char is a single Unicode character - var offsetLowerCase = 'a'; - var offsetUpperCase = 'A'; - const int lettersOffset = 26; // A...Z or a..z: length=26 + // Unicode/ASCII Letters are divided into two blocks + // (Letters 65–90 / 97–122): + // The first group containing the uppercase letters and + // the second group containing the lowercase. - for (var i = 0; i < size; i++) - { - char offset; - if (random.Next(0, 2) == 0) - { - offset = offsetLowerCase; - } - else - { - offset = offsetUpperCase; - } + // char is a single Unicode character + char offsetLowerCase = 'a'; + char offsetUpperCase = 'A'; + const int lettersOffset = 26; // A...Z or a..z: length=26 + + for (var i = 0; i < size; i++) { + char offset; + if(random.Next(0,2) == 0) { + offset = offsetLowerCase; + } else { + offset = offsetUpperCase; + } - var @char = (char)random.Next(offset, offset + lettersOffset); - builder.Append(@char); - } + var @char = (char) random.Next(offset, offset + lettersOffset); + builder.Append(@char); + } - return builder.ToString(); - } + return builder.ToString(); + } } diff --git a/EpicOnlineTransport/Server.cs b/EpicOnlineTransport/Server.cs index aec8f85d9..6cd8e7ded 100644 --- a/EpicOnlineTransport/Server.cs +++ b/EpicOnlineTransport/Server.cs @@ -4,217 +4,183 @@ using System.Collections.Generic; using UnityEngine; -namespace EpicTransport; - -public class Server : Common -{ - private event Action OnConnected; - private event Action OnReceivedData; - private event Action OnDisconnected; - private event Action OnReceivedError; - - private BidirectionalDictionary epicToMirrorIds; - private Dictionary epicToSocketIds; - private int maxConnections; - private int nextConnectionID; - - public static Server CreateServer(EosTransport transport, int maxConnections) - { - var s = new Server(transport, maxConnections); - - s.OnConnected += (id) => transport.OnServerConnected.Invoke(id); - s.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id); - s.OnReceivedData += (id, data, channel) => transport.OnServerDataReceived.Invoke(id, new ArraySegment(data), channel); - s.OnReceivedError += (id, exception) => transport.OnServerError.Invoke(id, exception); - - if (!EOSSDKComponent.Initialized) - { - Debug.LogError("EOS not initialized."); - } - - return s; - } - - private Server(EosTransport transport, int maxConnections) : base(transport) - { - this.maxConnections = maxConnections; - epicToMirrorIds = new BidirectionalDictionary(); - epicToSocketIds = new Dictionary(); - nextConnectionID = 1; - } - - protected override void OnNewConnection(OnIncomingConnectionRequestInfo result) - { - if (ignoreAllMessages) - { - return; - } - - if (deadSockets.Contains(result.SocketId.SocketName)) - { - Debug.LogError("Received incoming connection request from dead socket"); - return; - } - - EOSSDKComponent.GetP2PInterface().AcceptConnection( - new AcceptConnectionOptions() - { - LocalUserId = EOSSDKComponent.LocalUserProductId, - RemoteUserId = result.RemoteUserId, - SocketId = result.SocketId - }); - } - - protected override void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserId, SocketId socketId) - { - if (ignoreAllMessages) - { - return; - } - - switch (type) - { - case InternalMessages.CONNECT: - if (epicToMirrorIds.Count >= maxConnections) - { - Debug.LogError("Reached max connections"); - //CloseP2PSessionWithUser(clientUserId, socketId); - SendInternal(clientUserId, socketId, InternalMessages.DISCONNECT); - return; - } - - SendInternal(clientUserId, socketId, InternalMessages.ACCEPT_CONNECT); - - var connectionId = nextConnectionID++; - epicToMirrorIds.Add(clientUserId, connectionId); - epicToSocketIds.Add(clientUserId, socketId); - OnConnected.Invoke(connectionId); - - string clientUserIdString; - clientUserId.ToString(out clientUserIdString); - Debug.Log($"Client with Product User ID {clientUserIdString} connected. Assigning connection id {connectionId}"); - break; - case InternalMessages.DISCONNECT: - if (epicToMirrorIds.TryGetValue(clientUserId, out var connId)) - { - OnDisconnected.Invoke(connId); - //CloseP2PSessionWithUser(clientUserId, socketId); - epicToMirrorIds.Remove(clientUserId); - epicToSocketIds.Remove(clientUserId); - Debug.Log($"Client with Product User ID {clientUserId} disconnected."); - } - else - { - OnReceivedError.Invoke(-1, new Exception("ERROR Unknown Product User ID")); - } - - break; - default: - Debug.Log("Received unknown message type"); - break; - } - } - - protected override void OnReceiveData(byte[] data, ProductUserId clientUserId, int channel) - { - if (ignoreAllMessages) - { - return; - } - - if (epicToMirrorIds.TryGetValue(clientUserId, out var connectionId)) - { - OnReceivedData.Invoke(connectionId, data, channel); - } - else - { - SocketId socketId; - epicToSocketIds.TryGetValue(clientUserId, out socketId); - CloseP2PSessionWithUser(clientUserId, socketId); - - string productId; - clientUserId.ToString(out productId); - - Debug.LogError("Data received from epic client thats not known " + productId); - OnReceivedError.Invoke(-1, new Exception("ERROR Unknown product ID")); - } - } - - public void Disconnect(int connectionId) - { - if (epicToMirrorIds.TryGetValue(connectionId, out var userId)) - { - SocketId socketId; - epicToSocketIds.TryGetValue(userId, out socketId); - SendInternal(userId, socketId, InternalMessages.DISCONNECT); - epicToMirrorIds.Remove(userId); - epicToSocketIds.Remove(userId); - } - else - { - Debug.LogWarning("Trying to disconnect unknown connection id: " + connectionId); - } - } - - public void Shutdown() - { - foreach (KeyValuePair client in epicToMirrorIds) - { - Disconnect(client.Value); - SocketId socketId; - epicToSocketIds.TryGetValue(client.Key, out socketId); - WaitForClose(client.Key, socketId); - } - - ignoreAllMessages = true; - ReceiveData(); - - Dispose(); - } - - public void SendAll(int connectionId, byte[] data, int channelId) - { - if (epicToMirrorIds.TryGetValue(connectionId, out var userId)) - { - SocketId socketId; - epicToSocketIds.TryGetValue(userId, out socketId); - Send(userId, socketId, data, (byte)channelId); - } - else - { - Debug.LogError("Trying to send on unknown connection: " + connectionId); - OnReceivedError.Invoke(connectionId, new Exception("ERROR Unknown Connection")); - } - } - - public string ServerGetClientAddress(int connectionId) - { - if (epicToMirrorIds.TryGetValue(connectionId, out var userId)) - { - string userIdString; - userId.ToString(out userIdString); - return userIdString; - } - else - { - Debug.LogError("Trying to get info on unknown connection: " + connectionId); - OnReceivedError.Invoke(connectionId, new Exception("ERROR Unknown Connection")); - return string.Empty; - } - } - - protected override void OnConnectionFailed(ProductUserId remoteId) - { - if (ignoreAllMessages) - { - return; - } - - var connectionId = epicToMirrorIds.TryGetValue(remoteId, out var connId) ? connId : nextConnectionID++; - OnDisconnected.Invoke(connectionId); - - Debug.LogError("Connection Failed, removing user"); - epicToMirrorIds.Remove(remoteId); - epicToSocketIds.Remove(remoteId); - } +namespace EpicTransport { + public class Server : Common { + private event Action OnConnected; + private event Action OnReceivedData; + private event Action OnDisconnected; + private event Action OnReceivedError; + + private BidirectionalDictionary epicToMirrorIds; + private Dictionary epicToSocketIds; + private int maxConnections; + private int nextConnectionID; + + public static Server CreateServer(EosTransport transport, int maxConnections) { + Server s = new Server(transport, maxConnections); + + s.OnConnected += (id) => transport.OnServerConnected.Invoke(id); + s.OnDisconnected += (id) => transport.OnServerDisconnected.Invoke(id); + s.OnReceivedData += (id, data, channel) => transport.OnServerDataReceived.Invoke(id, new ArraySegment(data), channel); + // CHANGED + s.OnReceivedError += (id, exception) => transport.OnServerError?.Invoke(id, Mirror.TransportError.Unexpected, exception.ToString()); + + if (!EOSSDKComponent.Initialized) { + Debug.LogError("EOS not initialized."); + } + + return s; + } + + private Server(EosTransport transport, int maxConnections) : base(transport) { + this.maxConnections = maxConnections; + epicToMirrorIds = new BidirectionalDictionary(); + epicToSocketIds = new Dictionary(); + nextConnectionID = 1; + } + + protected override void OnNewConnection(OnIncomingConnectionRequestInfo result) { + if (ignoreAllMessages) { + return; + } + + if (deadSockets.Contains(result.SocketId.SocketName)) { + Debug.LogError("Received incoming connection request from dead socket"); + return; + } + + EOSSDKComponent.GetP2PInterface().AcceptConnection( + new AcceptConnectionOptions() { + LocalUserId = EOSSDKComponent.LocalUserProductId, + RemoteUserId = result.RemoteUserId, + SocketId = result.SocketId + }); + } + + protected override void OnReceiveInternalData(InternalMessages type, ProductUserId clientUserId, SocketId socketId) { + if (ignoreAllMessages) { + return; + } + + switch (type) { + case InternalMessages.CONNECT: + if (epicToMirrorIds.Count >= maxConnections) { + Debug.LogError("Reached max connections"); + //CloseP2PSessionWithUser(clientUserId, socketId); + SendInternal(clientUserId, socketId, InternalMessages.DISCONNECT); + return; + } + + SendInternal(clientUserId, socketId, InternalMessages.ACCEPT_CONNECT); + + int connectionId = nextConnectionID++; + epicToMirrorIds.Add(clientUserId, connectionId); + epicToSocketIds.Add(clientUserId, socketId); + OnConnected.Invoke(connectionId); + + string clientUserIdString; + clientUserId.ToString(out clientUserIdString); + Debug.Log($"Client with Product User ID {clientUserIdString} connected. Assigning connection id {connectionId}"); + break; + case InternalMessages.DISCONNECT: + if (epicToMirrorIds.TryGetValue(clientUserId, out int connId)) { + OnDisconnected.Invoke(connId); + //CloseP2PSessionWithUser(clientUserId, socketId); + epicToMirrorIds.Remove(clientUserId); + epicToSocketIds.Remove(clientUserId); + Debug.Log($"Client with Product User ID {clientUserId} disconnected."); + } else { + OnReceivedError.Invoke(-1, new Exception("ERROR Unknown Product User ID")); + } + + break; + default: + Debug.Log("Received unknown message type"); + break; + } + } + + protected override void OnReceiveData(byte[] data, ProductUserId clientUserId, int channel) { + if (ignoreAllMessages) { + return; + } + + if (epicToMirrorIds.TryGetValue(clientUserId, out int connectionId)) { + OnReceivedData.Invoke(connectionId, data, channel); + } else { + SocketId socketId; + epicToSocketIds.TryGetValue(clientUserId, out socketId); + CloseP2PSessionWithUser(clientUserId, socketId); + + string productId; + clientUserId.ToString(out productId); + + Debug.LogError("Data received from epic client thats not known " + productId); + OnReceivedError.Invoke(-1, new Exception("ERROR Unknown product ID")); + } + } + + public void Disconnect(int connectionId) { + if (epicToMirrorIds.TryGetValue(connectionId, out ProductUserId userId)) { + SocketId socketId; + epicToSocketIds.TryGetValue(userId, out socketId); + SendInternal(userId, socketId, InternalMessages.DISCONNECT); + epicToMirrorIds.Remove(userId); + epicToSocketIds.Remove(userId); + } else { + Debug.LogWarning("Trying to disconnect unknown connection id: " + connectionId); + } + } + + public void Shutdown() { + foreach (KeyValuePair client in epicToMirrorIds) { + Disconnect(client.Value); + SocketId socketId; + epicToSocketIds.TryGetValue(client.Key, out socketId); + WaitForClose(client.Key, socketId); + } + + ignoreAllMessages = true; + ReceiveData(); + + Dispose(); + } + + public void SendAll(int connectionId, byte[] data, int channelId) { + if (epicToMirrorIds.TryGetValue(connectionId, out ProductUserId userId)) { + SocketId socketId; + epicToSocketIds.TryGetValue(userId, out socketId); + Send(userId, socketId, data, (byte)channelId); + } else { + Debug.LogError("Trying to send on unknown connection: " + connectionId); + OnReceivedError.Invoke(connectionId, new Exception("ERROR Unknown Connection")); + } + + } + + public string ServerGetClientAddress(int connectionId) { + if (epicToMirrorIds.TryGetValue(connectionId, out ProductUserId userId)) { + string userIdString; + userId.ToString(out userIdString); + return userIdString; + } else { + Debug.LogError("Trying to get info on unknown connection: " + connectionId); + OnReceivedError.Invoke(connectionId, new Exception("ERROR Unknown Connection")); + return string.Empty; + } + } + + protected override void OnConnectionFailed(ProductUserId remoteId) { + if (ignoreAllMessages) { + return; + } + + int connectionId = epicToMirrorIds.TryGetValue(remoteId, out int connId) ? connId : nextConnectionID++; + OnDisconnected.Invoke(connectionId); + + Debug.LogError("Connection Failed, removing user"); + epicToMirrorIds.Remove(remoteId); + epicToSocketIds.Remove(remoteId); + } + } } \ No newline at end of file diff --git a/Mirror/Mirror.Components.dll b/Mirror/Mirror.Components.dll index 623a76cf0..de1a9bbb6 100644 Binary files a/Mirror/Mirror.Components.dll and b/Mirror/Mirror.Components.dll differ diff --git a/Mirror/Mirror.Transports.dll b/Mirror/Mirror.Transports.dll new file mode 100644 index 000000000..f9b832637 Binary files /dev/null and b/Mirror/Mirror.Transports.dll differ diff --git a/Mirror/Mirror.dll b/Mirror/Mirror.dll index 26ebbe200..60cc974bf 100644 Binary files a/Mirror/Mirror.dll and b/Mirror/Mirror.dll differ diff --git a/Mirror/Telepathy.dll b/Mirror/Telepathy.dll index abcdc6f5a..c6b7e1eda 100644 Binary files a/Mirror/Telepathy.dll and b/Mirror/Telepathy.dll differ diff --git a/Mirror/kcp2k.dll b/Mirror/kcp2k.dll index 4e223b6ac..2614f3894 100644 Binary files a/Mirror/kcp2k.dll and b/Mirror/kcp2k.dll differ diff --git a/Mirror/where-allocations.dll b/Mirror/where-allocations.dll deleted file mode 100644 index 168e320ec..000000000 Binary files a/Mirror/where-allocations.dll and /dev/null differ diff --git a/MirrorWeaver/Weaver/Extensions.cs b/MirrorWeaver/Weaver/Extensions.cs index a8067f564..db6610cb9 100644 --- a/MirrorWeaver/Weaver/Extensions.cs +++ b/MirrorWeaver/Weaver/Extensions.cs @@ -12,8 +12,18 @@ public static bool Is(this TypeReference td, Type type) => ? td.GetElementType().FullName == type.FullName : td.FullName == type.FullName; + // check if 'td' is exactly of type T. + // it does not check if any base type is of , only the specific type. + // for example: + // NetworkConnection Is NetworkConnection: true + // NetworkConnectionToClient Is NetworkConnection: false public static bool Is(this TypeReference td) => Is(td, typeof(T)); + // check if 'tr' is derived from T. + // it does not check if 'tr' is exactly T. + // for example: + // NetworkConnection IsDerivedFrom: false + // NetworkConnectionToClient IsDerivedFrom: true public static bool IsDerivedFrom(this TypeReference tr) => IsDerivedFrom(tr, typeof(T)); public static bool IsDerivedFrom(this TypeReference tr, Type baseClass) @@ -79,7 +89,10 @@ public static bool IsMultidimensionalArray(this TypeReference tr) => public static bool IsNetworkIdentityField(this TypeReference tr) => tr.Is() || tr.Is() || - tr.IsDerivedFrom(); + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + tr.IsDerivedFrom() || + tr.Is(); public static bool CanBeResolved(this TypeReference parent) { @@ -266,7 +279,7 @@ public static AssemblyNameReference FindReference(this ModuleDefinition module, // Takes generic arguments from child class and applies them to parent reference, if possible // eg makes `Base` in Child : Base have `int` instead of `T` - // Originally by James-Frowen under MIT + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 public static TypeReference ApplyGenericParameters(this TypeReference parentReference, TypeReference childReference) @@ -306,7 +319,7 @@ public static TypeReference ApplyGenericParameters(this TypeReference parentRefe } // Finds the type reference for a generic parameter with the provided name in the child reference - // Originally by James-Frowen under MIT + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 static TypeReference FindMatchingGenericArgument(TypeReference childReference, string paramName) { diff --git a/MirrorWeaver/Weaver/Processors/CommandProcessor.cs b/MirrorWeaver/Weaver/Processors/CommandProcessor.cs index af7762541..9255eb35e 100644 --- a/MirrorWeaver/Weaver/Processors/CommandProcessor.cs +++ b/MirrorWeaver/Weaver/Processors/CommandProcessor.cs @@ -10,10 +10,11 @@ public static class CommandProcessor // generates code like: public void CmdThrust(float thrusting, int spin) { - NetworkWriter networkWriter = new NetworkWriter(); - networkWriter.Write(thrusting); - networkWriter.WritePackedUInt32((uint)spin); - base.SendCommandInternal(cmdName, networkWriter, channel); + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.Write(thrusting); + writer.WritePackedUInt32((uint)spin); + base.SendCommandInternal(cmdName, cmdHash, writer, channel); + NetworkWriterPool.Return(writer); } public void CallCmdThrust(float thrusting, int spin) @@ -38,7 +39,7 @@ public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Write NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes); // NetworkWriter writer = new NetworkWriter(); - NetworkBehaviourProcessor.WriteCreateWriter(worker, weaverTypes); + NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes); // write all the arguments that the user passed to the Cmd call if (!NetworkBehaviourProcessor.WriteArguments(worker, writers, Log, md, RemoteCallType.Command, ref WeavingFailed)) @@ -52,6 +53,11 @@ public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Write worker.Emit(OpCodes.Ldarg_0); // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, channel); @@ -59,7 +65,7 @@ public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Write worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); worker.Emit(OpCodes.Call, weaverTypes.sendCommandInternal); - NetworkBehaviourProcessor.WriteRecycleWriter(worker, weaverTypes); + NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes); worker.Emit(OpCodes.Ret); return cmd; diff --git a/MirrorWeaver/Weaver/Processors/NetworkBehaviourProcessor.cs b/MirrorWeaver/Weaver/Processors/NetworkBehaviourProcessor.cs index b38382557..860be3173 100644 --- a/MirrorWeaver/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/MirrorWeaver/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -137,21 +137,21 @@ public static void WriteServerActiveCheck(ILProcessor worker, WeaverTypes weaver public static void WriteSetupLocals(ILProcessor worker, WeaverTypes weaverTypes) { worker.Body.InitLocals = true; - worker.Body.Variables.Add(new VariableDefinition(weaverTypes.Import())); + worker.Body.Variables.Add(new VariableDefinition(weaverTypes.Import())); } - public static void WriteCreateWriter(ILProcessor worker, WeaverTypes weaverTypes) + public static void WriteGetWriter(ILProcessor worker, WeaverTypes weaverTypes) { // create writer - worker.Emit(OpCodes.Call, weaverTypes.GetPooledWriterReference); + worker.Emit(OpCodes.Call, weaverTypes.GetWriterReference); worker.Emit(OpCodes.Stloc_0); } - public static void WriteRecycleWriter(ILProcessor worker, WeaverTypes weaverTypes) + public static void WriteReturnWriter(ILProcessor worker, WeaverTypes weaverTypes) { // NetworkWriterPool.Recycle(writer); worker.Emit(OpCodes.Ldloc_0); - worker.Emit(OpCodes.Call, weaverTypes.RecycleWriterReference); + worker.Emit(OpCodes.Call, weaverTypes.ReturnWriterReference); } public static bool WriteArguments(ILProcessor worker, Writers writers, Logger Log, MethodDefinition method, RemoteCallType callType, ref bool WeavingFailed) @@ -397,7 +397,7 @@ void GenerateSerialization(ref bool WeavingFailed) MethodDefinition serialize = new MethodDefinition(SerializeMethodName, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - weaverTypes.Import()); + weaverTypes.Import(typeof(void))); serialize.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, weaverTypes.Import())); serialize.Parameters.Add(new ParameterDefinition("forceAll", ParameterAttributes.None, weaverTypes.Import())); @@ -405,10 +405,7 @@ void GenerateSerialization(ref bool WeavingFailed) serialize.Body.InitLocals = true; - // loc_0, this local variable is to determine if any variable was dirty - VariableDefinition dirtyLocal = new VariableDefinition(weaverTypes.Import()); - serialize.Body.Variables.Add(dirtyLocal); - + // base.SerializeSyncVars(writer, forceAll); MethodReference baseSerialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, SerializeMethodName); if (baseSerialize != null) { @@ -419,16 +416,20 @@ void GenerateSerialization(ref bool WeavingFailed) // forceAll worker.Emit(OpCodes.Ldarg_2); worker.Emit(OpCodes.Call, baseSerialize); - // set dirtyLocal to result of base.OnSerialize() - worker.Emit(OpCodes.Stloc_0); } - // Generates: if (forceAll); + // Generates: + // if (forceAll) + // { + // writer.WriteInt(health); + // ... + // } Instruction initialStateLabel = worker.Create(OpCodes.Nop); // forceAll - worker.Emit(OpCodes.Ldarg_2); - worker.Emit(OpCodes.Brfalse, initialStateLabel); + worker.Emit(OpCodes.Ldarg_2); // load 'forceAll' flag + worker.Emit(OpCodes.Brfalse, initialStateLabel); // start the 'if forceAll' branch + // generates write.Write(syncVar) for each SyncVar in forceAll case foreach (FieldDefinition syncVarDef in syncVars) { FieldReference syncVar = syncVarDef; @@ -442,7 +443,21 @@ void GenerateSerialization(ref bool WeavingFailed) // this worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, syncVar); - MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + MethodReference writeFunc; + // For NBs we always need to use the default NetworkBehaviour write func + // since the reader counter part uses that exact layout which is not easy to change + // without introducing more edge cases + // effectively this disallows custom NB-type writers/readers on SyncVars + // see: https://github.com/MirrorNetworking/Mirror/issues/2680 + if (syncVar.FieldType.IsDerivedFrom()) + { + writeFunc = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + } + else + { + writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + } + if (writeFunc != null) { worker.Emit(OpCodes.Call, writeFunc); @@ -455,15 +470,14 @@ void GenerateSerialization(ref bool WeavingFailed) } } - // always return true if forceAll - - // Generates: return true - worker.Emit(OpCodes.Ldc_I4_1); + // if (forceAll) then always return at the end of the 'if' case worker.Emit(OpCodes.Ret); - // Generates: end if (forceAll); + // end the 'if' case for "if (forceAll)" worker.Append(initialStateLabel); + //////////////////////////////////////////////////////////////////// + // write dirty bits before the data fields // Generates: writer.WritePackedUInt64 (base.get_syncVarDirtyBits ()); // writer @@ -480,7 +494,6 @@ void GenerateSerialization(ref bool WeavingFailed) int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); foreach (FieldDefinition syncVarDef in syncVars) { - FieldReference syncVar = syncVarDef; if (netBehaviourSubclass.HasGenericParameters) { @@ -504,7 +517,21 @@ void GenerateSerialization(ref bool WeavingFailed) worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, syncVar); - MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + MethodReference writeFunc; + // For NBs we always need to use the default NetworkBehaviour write func + // since the reader counter part uses that exact layout which is not easy to change + // without introducing more edge cases + // effectively this disallows custom NB-type writers/readers on SyncVars + // see: https://github.com/MirrorNetworking/Mirror/issues/2680 + if (syncVar.FieldType.IsDerivedFrom()) + { + writeFunc = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + } + else + { + writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + } + if (writeFunc != null) { worker.Emit(OpCodes.Call, writeFunc); @@ -516,11 +543,6 @@ void GenerateSerialization(ref bool WeavingFailed) return; } - // something was dirty - worker.Emit(OpCodes.Ldc_I4_1); - // set dirtyLocal to true - worker.Emit(OpCodes.Stloc_0); - worker.Append(varLabel); dirtyBit += 1; } @@ -529,8 +551,7 @@ void GenerateSerialization(ref bool WeavingFailed) //worker.Emit(OpCodes.Ldstr, $"Injected Serialize {netBehaviourSubclass.Name}"); //worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference); - // generate: return dirtyLocal - worker.Emit(OpCodes.Ldloc_0); + // generate: return worker.Emit(OpCodes.Ret); netBehaviourSubclass.Methods.Add(serialize); } @@ -589,10 +610,9 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldflda, netIdField); worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity); } - // TODO this only uses the persistent netId for types DERIVED FROM NB. - // not if the type is just 'NetworkBehaviour'. - // this is what original implementation did too. fix it after. - else if (syncVar.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (syncVar.FieldType.IsDerivedFrom() || syncVar.FieldType.Is()) { // reader worker.Emit(OpCodes.Ldarg_1); diff --git a/MirrorWeaver/Weaver/Processors/RpcProcessor.cs b/MirrorWeaver/Weaver/Processors/RpcProcessor.cs index e26ecbd88..a774c5c6d 100644 --- a/MirrorWeaver/Weaver/Processors/RpcProcessor.cs +++ b/MirrorWeaver/Weaver/Processors/RpcProcessor.cs @@ -68,7 +68,7 @@ public static MethodDefinition ProcessRpcCall(WeaverTypes weaverTypes, Writers w //worker.Emit(OpCodes.Ldstr, $"Call ClientRpc function {md.Name}"); //worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference); - NetworkBehaviourProcessor.WriteCreateWriter(worker, weaverTypes); + NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes); // write all the arguments that the user passed to the Rpc call if (!NetworkBehaviourProcessor.WriteArguments(worker, writers, Log, md, RemoteCallType.ClientRpc, ref WeavingFailed)) @@ -82,6 +82,11 @@ public static MethodDefinition ProcessRpcCall(WeaverTypes weaverTypes, Writers w worker.Emit(OpCodes.Ldarg_0); // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, channel); @@ -89,7 +94,7 @@ public static MethodDefinition ProcessRpcCall(WeaverTypes weaverTypes, Writers w worker.Emit(includeOwner ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); worker.Emit(OpCodes.Callvirt, weaverTypes.sendRpcInternal); - NetworkBehaviourProcessor.WriteRecycleWriter(worker, weaverTypes); + NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes); worker.Emit(OpCodes.Ret); diff --git a/MirrorWeaver/Weaver/Processors/SyncObjectProcessor.cs b/MirrorWeaver/Weaver/Processors/SyncObjectProcessor.cs index e8d8c1345..ffcb79451 100644 --- a/MirrorWeaver/Weaver/Processors/SyncObjectProcessor.cs +++ b/MirrorWeaver/Weaver/Processors/SyncObjectProcessor.cs @@ -16,7 +16,7 @@ public static List FindSyncObjectsFields(Writers writers, Reade foreach (FieldDefinition fd in td.Fields) { - if (fd.FieldType.IsGenericParameter) + if (fd.FieldType.IsGenericParameter || fd.ContainsGenericParameter) { // can't call .Resolve on generic ones continue; diff --git a/MirrorWeaver/Weaver/Processors/SyncVarAttributeProcessor.cs b/MirrorWeaver/Weaver/Processors/SyncVarAttributeProcessor.cs index 2d0b25fa1..9d7231809 100644 --- a/MirrorWeaver/Weaver/Processors/SyncVarAttributeProcessor.cs +++ b/MirrorWeaver/Weaver/Processors/SyncVarAttributeProcessor.cs @@ -203,7 +203,9 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference); worker.Emit(OpCodes.Ret); } - else if (fd.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { // return this.GetSyncVarNetworkBehaviour(ref field, uint netId); // this. @@ -331,10 +333,9 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition worker.Emit(OpCodes.Ldflda, netIdFieldReference); worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_NetworkIdentity); } - // TODO this only uses the persistent netId for types DERIVED FROM NB. - // not if the type is just 'NetworkBehaviour'. - // this is what original implementation did too. fix it after. - else if (fd.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { // NetworkIdentity setter needs one more parameter: netId field ref // (actually its a NetworkBehaviourSyncVar type) @@ -368,11 +369,13 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { netIdField = new FieldDefinition($"___{fd.Name}NetId", FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed - weaverTypes.Import()); + weaverTypes.Import()); netIdField.DeclaringType = td; syncVarNetIds[fd] = netIdField; @@ -475,7 +478,11 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary 0 && - md.Parameters[0].ParameterType.Is(); + if (md.Parameters.Count > 0) + { + // we need to allow both NetworkConnection, and inheriting types. + // NetworkBehaviour.SendTargetRpc takes a NetworkConnection parameter. + // fixes https://github.com/vis2k/Mirror/issues/3290 + TypeReference type = md.Parameters[0].ParameterType; + return type.Is() || + type.IsDerivedFrom(); + } + return false; } public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed) @@ -34,16 +42,25 @@ public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, R // NetworkConnection parameter is optional if (HasNetworkConnectionParameter(md)) { - // on server, the NetworkConnection parameter is a connection to client. - // when the rpc is invoked on the client, it still has the same - // function signature. we pass in the connection to server, - // which is cleaner than just passing null) - //NetworkClient.readyconnection + // TargetRpcs are sent from server to client. + // on server, we currently support two types: + // TargetRpc(NetworkConnection) + // TargetRpc(NetworkConnectionToClient) + // however, it's always a connection to client. + // in the future, only NetworkConnectionToClient will be supported. + // explicit typing helps catch issues at compile time. + // + // on client, InvokeTargetRpc calls the original code. + // we need to fill in the NetworkConnection parameter. + // NetworkClient.connection is always a connection to server. // - // TODO - // a) .connectionToServer = best solution. no doubt. - // b) NetworkClient.connection for now. add TODO to not use static later. - worker.Emit(OpCodes.Call, weaverTypes.NetworkClientConnectionReference); + // we used to pass NetworkClient.connection as the TargetRpc parameter. + // which caused: https://github.com/MirrorNetworking/Mirror/issues/3455 + // when the parameter is defined as a NetworkConnectionToClient. + // + // a client's connection never fits into a NetworkConnectionToClient. + // we need to always pass null here. + worker.Emit(OpCodes.Ldnull); } // process reader parameters and skip first one if first one is NetworkConnection @@ -100,7 +117,7 @@ public static MethodDefinition ProcessTargetRpcCall(WeaverTypes weaverTypes, Wri NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes); - NetworkBehaviourProcessor.WriteCreateWriter(worker, weaverTypes); + NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes); // write all the arguments that the user passed to the TargetRpc call // (skip first one if first one is NetworkConnection) @@ -122,12 +139,17 @@ public static MethodDefinition ProcessTargetRpcCall(WeaverTypes weaverTypes, Wri } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, targetRpcAttr.GetField("channel", 0)); worker.Emit(OpCodes.Callvirt, weaverTypes.sendTargetRpcInternal); - NetworkBehaviourProcessor.WriteRecycleWriter(worker, weaverTypes); + NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes); worker.Emit(OpCodes.Ret); diff --git a/MirrorWeaver/Weaver/Readers.cs b/MirrorWeaver/Weaver/Readers.cs index 4a38ccd4c..0e3800bdf 100644 --- a/MirrorWeaver/Weaver/Readers.cs +++ b/MirrorWeaver/Weaver/Readers.cs @@ -19,6 +19,7 @@ public class Readers AssemblyDefinition assembly; WeaverTypes weaverTypes; TypeDefinition GeneratedCodeClass; + // CHANGED internal Logger Log; Dictionary readFuncs = @@ -114,7 +115,9 @@ MethodReference GenerateReader(TypeReference variableReference, ref bool Weaving return GenerateReadCollection(variableReference, elementType, nameof(NetworkReaderExtensions.ReadList), ref WeavingFailed); } - else if (variableReference.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (variableReference.IsDerivedFrom() || variableReference.Is()) { return GetNetworkBehaviourReader(variableReference); } @@ -138,6 +141,7 @@ MethodReference GenerateReader(TypeReference variableReference, ref bool Weaving WeavingFailed = true; return null; } + // CHANGED /* if (variableDefinition.HasGenericParameters) { @@ -270,6 +274,7 @@ MethodDefinition GenerateClassOrStructReadFunction(TypeReference variable, ref b GenerateNullCheck(worker, ref WeavingFailed); CreateNew(variable, worker, td, ref WeavingFailed); + // CHANGED this.ReadAllFieldsGeneric(variable, worker, ref WeavingFailed); worker.Emit(OpCodes.Ldloc_0); diff --git a/MirrorWeaver/Weaver/Weaver.cs b/MirrorWeaver/Weaver/Weaver.cs index c068a6d22..8eb5afbbe 100644 --- a/MirrorWeaver/Weaver/Weaver.cs +++ b/MirrorWeaver/Weaver/Weaver.cs @@ -25,6 +25,12 @@ internal class Weaver AssemblyDefinition CurrentAssembly; Writers writers; Readers readers; + + // in case of weaver errors, we don't stop immediately. + // we log all errors and then eventually return false if + // weaving has failed. + // this way the user can fix multiple errors at once, instead of having + // to fix -> recompile -> fix -> recompile for one error at a time. bool WeavingFailed; // logger functions can be set from the outside. @@ -200,6 +206,7 @@ public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out b ModuleDefinition moduleDefinition = CurrentAssembly.MainModule; Console.WriteLine($"Script Module: {moduleDefinition.Name}"); + // CHANGED QSBReaderWriterProcessor.Process(moduleDefinition, writers, readers, ref WeavingFailed); modified |= WeaveModule(moduleDefinition); diff --git a/MirrorWeaver/Weaver/WeaverTypes.cs b/MirrorWeaver/Weaver/WeaverTypes.cs index 64e55ac0b..63c421815 100644 --- a/MirrorWeaver/Weaver/WeaverTypes.cs +++ b/MirrorWeaver/Weaver/WeaverTypes.cs @@ -11,8 +11,8 @@ public class WeaverTypes public MethodReference ScriptableObjectCreateInstanceMethod; public MethodReference NetworkBehaviourDirtyBitsReference; - public MethodReference GetPooledWriterReference; - public MethodReference RecycleWriterReference; + public MethodReference GetWriterReference; + public MethodReference ReturnWriterReference; public MethodReference NetworkClientConnectionReference; @@ -77,28 +77,14 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail TypeReference NetworkServerType = Import(typeof(NetworkServer)); NetworkServerGetActive = Resolvers.ResolveMethod(NetworkServerType, assembly, Log, "get_active", ref WeavingFailed); + TypeReference NetworkClientType = Import(typeof(NetworkClient)); NetworkClientGetActive = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_active", ref WeavingFailed); - - TypeReference RemoteCallDelegateType = Import(); - RemoteCallDelegateConstructor = Resolvers.ResolveMethod(RemoteCallDelegateType, assembly, Log, ".ctor", ref WeavingFailed); + NetworkClientConnectionReference = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_connection", ref WeavingFailed); TypeReference NetworkBehaviourType = Import(); - TypeReference RemoteProcedureCallsType = Import(typeof(RemoteCalls.RemoteProcedureCalls)); - - TypeReference ScriptableObjectType = Import(); - - ScriptableObjectCreateInstanceMethod = Resolvers.ResolveMethod( - ScriptableObjectType, assembly, Log, - md => md.Name == "CreateInstance" && md.HasGenericParameters, - ref WeavingFailed); NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, assembly, "syncVarDirtyBits"); - TypeReference NetworkWriterPoolType = Import(typeof(NetworkWriterPool)); - GetPooledWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "GetWriter", ref WeavingFailed); - RecycleWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Recycle", ref WeavingFailed); - - NetworkClientConnectionReference = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_connection", ref WeavingFailed); generatedSyncVarSetter = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter", ref WeavingFailed); generatedSyncVarSetter_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_GameObject", ref WeavingFailed); @@ -114,9 +100,25 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkIdentity", ref WeavingFailed); getSyncVarNetworkBehaviourReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkBehaviour", ref WeavingFailed); + sendCommandInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendCommandInternal", ref WeavingFailed); + sendRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendRPCInternal", ref WeavingFailed); + sendTargetRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendTargetRPCInternal", ref WeavingFailed); + + InitSyncObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "InitSyncObject", ref WeavingFailed); + + TypeReference RemoteProcedureCallsType = Import(typeof(RemoteCalls.RemoteProcedureCalls)); registerCommandReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterCommand", ref WeavingFailed); registerRpcReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterRpc", ref WeavingFailed); + TypeReference RemoteCallDelegateType = Import(); + RemoteCallDelegateConstructor = Resolvers.ResolveMethod(RemoteCallDelegateType, assembly, Log, ".ctor", ref WeavingFailed); + + TypeReference ScriptableObjectType = Import(); + ScriptableObjectCreateInstanceMethod = Resolvers.ResolveMethod( + ScriptableObjectType, assembly, Log, + md => md.Name == "CreateInstance" && md.HasGenericParameters, + ref WeavingFailed); + TypeReference unityDebug = Import(typeof(UnityEngine.Debug)); // these have multiple methods with same name, so need to check parameters too logErrorReference = Resolvers.ResolveMethod(unityDebug, assembly, Log, md => @@ -133,11 +135,10 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail TypeReference typeType = Import(typeof(Type)); getTypeFromHandleReference = Resolvers.ResolveMethod(typeType, assembly, Log, "GetTypeFromHandle", ref WeavingFailed); - sendCommandInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendCommandInternal", ref WeavingFailed); - sendRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendRPCInternal", ref WeavingFailed); - sendTargetRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendTargetRPCInternal", ref WeavingFailed); - InitSyncObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "InitSyncObject", ref WeavingFailed); + TypeReference NetworkWriterPoolType = Import(typeof(NetworkWriterPool)); + GetWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Get", ref WeavingFailed); + ReturnWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Return", ref WeavingFailed); TypeReference readerExtensions = Import(typeof(NetworkReaderExtensions)); readNetworkBehaviourGeneric = Resolvers.ResolveMethod(readerExtensions, assembly, Log, (md => @@ -147,6 +148,7 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail }), ref WeavingFailed); + // CHANGED /* // [InitializeOnLoadMethod] // 'UnityEditor' is not available in builds. diff --git a/MirrorWeaver/Weaver/Writers.cs b/MirrorWeaver/Weaver/Writers.cs index 0b6fe966e..6b6977866 100644 --- a/MirrorWeaver/Weaver/Writers.cs +++ b/MirrorWeaver/Weaver/Writers.cs @@ -116,7 +116,9 @@ MethodReference GenerateWriter(TypeReference variableReference, ref bool Weaving return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteList), ref WeavingFailed); } - if (variableReference.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + if (variableReference.IsDerivedFrom() || variableReference.Is()) { return GetNetworkBehaviourWriter(variableReference); } @@ -139,6 +141,7 @@ MethodReference GenerateWriter(TypeReference variableReference, ref bool Weaving { throw new GenerateWriterException($"Cannot generate writer for {variableReference.Name}. Use a supported type or provide a custom writer", variableReference); } + // CHANGED /* if (variableDefinition.HasGenericParameters) { @@ -219,6 +222,7 @@ MethodDefinition GenerateClassOrStructWriterFunction(TypeReference variable, ref if (!variable.Resolve().IsValueType) WriteNullCheck(worker, ref WeavingFailed); + // CHANGED if (!this.WriteAllFieldsGeneric(variable, worker, ref WeavingFailed)) return null; diff --git a/QSB/ClientServerStateSync/ServerStateManager.cs b/QSB/ClientServerStateSync/ServerStateManager.cs index 4de2a60d4..01bf1d478 100644 --- a/QSB/ClientServerStateSync/ServerStateManager.cs +++ b/QSB/ClientServerStateSync/ServerStateManager.cs @@ -3,6 +3,7 @@ using QSB.Messaging; using QSB.Player; using QSB.Player.TransformSync; +using QSB.TimeSync; using QSB.Utility; using System.Linq; using UnityEngine; @@ -32,7 +33,7 @@ private void Start() QSBSceneManager.OnPostSceneLoad += OnPostSceneLoad; GlobalMessenger.AddListener("TriggerSupernova", OnTriggerSupernova); - Delay.RunWhen(() => PlayerTransformSync.LocalInstance != null, + Delay.RunWhen(() => PlayerTransformSync.LocalInstance != null && WakeUpSync.LocalInstance != null, () => new ServerStateMessage(ForceGetCurrentState()).Send()); } @@ -77,7 +78,7 @@ private static void OnPostSceneLoad(OWScene oldScene, OWScene newScene) } else { - new ServerStateMessage(ServerState.InSolarSystem).Send(); + new ServerStateMessage(ServerState.NotLoaded).Send(); } break; @@ -118,7 +119,11 @@ private ServerState ForceGetCurrentState() switch (currentScene) { case OWScene.SolarSystem: - return ServerState.InSolarSystem; + if (WakeUpSync.LocalInstance.HasWokenUp) + { + return ServerState.InSolarSystem; + } + return ServerState.NotLoaded; case OWScene.EyeOfTheUniverse: return ServerState.InEye; default: diff --git a/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs b/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs index d1963a159..2fbec4a52 100644 --- a/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs +++ b/QSB/EchoesOfTheEye/Ghosts/GhostManager.cs @@ -52,6 +52,10 @@ public override async UniTask BuildWorldObjects(OWScene scene, CancellationToken _zone2Director._cityGhosts[i].OnIdentifyIntruder -= _zone2Director.OnCityGhostsIdentifiedIntruder; _zone2Director._cityGhosts[i].GetWorldObject().OnIdentifyIntruder += CustomOnCityGhostsIdentifiedIntruder; } + + var allCollisionGroups = Resources.FindObjectsOfTypeAll(); + var city = allCollisionGroups.First(x => x.name == "City"); + city.SetSector(_zone2Director._sector); } public static void CustomOnHotelDepthsGhostsIdentifiedIntruder(GhostBrain ghostBrain, QSBGhostData ghostData) diff --git a/QSB/ModelShip/ModelShipThrusterVariableSyncer.cs b/QSB/ModelShip/ModelShipThrusterVariableSyncer.cs index 3e8965871..f1ed0c8ec 100644 --- a/QSB/ModelShip/ModelShipThrusterVariableSyncer.cs +++ b/QSB/ModelShip/ModelShipThrusterVariableSyncer.cs @@ -23,6 +23,17 @@ public void Init(GameObject modelShip) public void Update() { + // fixes #590 + if (ModelShipManager.Instance.CurrentFlyer == uint.MaxValue) + { + if (ThrusterModel) + { + AccelerationSyncer.Value = Vector3.zero; + } + + return; + } + if (PlayerTransformSync.LocalInstance && QSBPlayerManager.LocalPlayer.FlyingModelShip) { GetFromShip(); diff --git a/QSB/QSBNetworkManager.cs b/QSB/QSBNetworkManager.cs index 9d920dbf4..477cee38a 100644 --- a/QSB/QSBNetworkManager.cs +++ b/QSB/QSBNetworkManager.cs @@ -67,7 +67,7 @@ public class QSBNetworkManager : NetworkManager, IAddComponentOnStart private string _lastTransportError; private static readonly string[] _kcpErrorLogs = { - "KCP: received disconnect message", + "KcpPeer: received disconnect message", "Failed to resolve host: .*" }; @@ -110,7 +110,7 @@ public override void Awake() QSBCore.ProfileManager.OnProfileSignInComplete += _ => InitPlayerName(); playerPrefab = QSBCore.NetworkAssetBundle.LoadAsset("Assets/Prefabs/NETWORK_Player_Body.prefab"); - playerPrefab.GetRequiredComponent().SetValue("m_AssetId", 1.ToGuid().ToString("N")); + playerPrefab.GetRequiredComponent().SetValue("_assetId", (uint)1); ShipPrefab = MakeNewNetworkObject(2, "NetworkShip", typeof(ShipTransformSync)); var shipVector3Sync = ShipPrefab.AddComponent(); @@ -202,7 +202,7 @@ private void InitPlayerName() => /// this works by calling Unload(false) and then reloading the AssetBundle, /// which makes LoadAsset give you a new resource. /// see https://docs.unity3d.com/Manual/AssetBundles-Native.html. - private static GameObject MakeNewNetworkObject(int assetId, string name, Type networkBehaviourType) + private static GameObject MakeNewNetworkObject(uint assetId, string name, Type networkBehaviourType) { var bundle = QSBCore.Helper.Assets.LoadBundle("AssetBundles/qsb_empty"); @@ -216,7 +216,7 @@ private static GameObject MakeNewNetworkObject(int assetId, string name, Type ne bundle.Unload(false); template.name = name; - template.AddComponent().SetValue("m_AssetId", assetId.ToGuid().ToString("N")); + template.AddComponent().SetValue("_assetId", assetId); template.AddComponent(networkBehaviourType); return template; } diff --git a/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs b/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs index 97e612050..716287a86 100644 --- a/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs +++ b/QSB/QuantumSync/Patches/Common/QuantumObjectPatches.cs @@ -50,7 +50,14 @@ public static bool OnProbeSnapshotRemoved() [HarmonyPatch(nameof(QuantumObject.IsLockedByProbeSnapshot))] public static bool IsLockedByProbeSnapshot(QuantumObject __instance, ref bool __result) { - __result = __instance._visibleInProbeSnapshot; + if (!QSBWorldSync.AllObjectsReady) + { + return true; + } + + var worldObject = __instance.GetWorldObject(); + var visibleToProbePlayers = worldObject.GetVisibleToProbePlayers(); + __result = visibleToProbePlayers.Any(x => x.ProbeLauncherEquipped != default); return false; } } diff --git a/QSB/QuantumSync/Patches/Server/ServerQuantumMoonPatches.cs b/QSB/QuantumSync/Patches/Server/ServerQuantumMoonPatches.cs index 158263fe8..ae14f348a 100644 --- a/QSB/QuantumSync/Patches/Server/ServerQuantumMoonPatches.cs +++ b/QSB/QuantumSync/Patches/Server/ServerQuantumMoonPatches.cs @@ -61,7 +61,7 @@ public static bool ChangeQuantumState(QuantumMoon __instance, bool skipInstantVi return false; } - if (__instance._stateIndex == 5 && anyPlayerInQM && !__instance.IsPlayerEntangled()) + if (__instance._stateIndex == 5 && anyPlayerInQM && !playersInQuantumMoon.All(x => x.EntangledObject != null && x.EntangledObject.AttachedObject == __instance)) { __result = false; return false; diff --git a/QSB/QuantumSync/WorldObjects/IQSBQuantumObject.cs b/QSB/QuantumSync/WorldObjects/IQSBQuantumObject.cs index 2533e9988..0632f485a 100644 --- a/QSB/QuantumSync/WorldObjects/IQSBQuantumObject.cs +++ b/QSB/QuantumSync/WorldObjects/IQSBQuantumObject.cs @@ -20,4 +20,5 @@ public interface IQSBQuantumObject : IWorldObject VisibilityObject GetVisibilityObject(); void OnTakeProbeSnapshot(PlayerInfo player, ProbeCamera.ID cameraId); void OnRemoveProbeSnapshot(PlayerInfo player); + List GetVisibleToProbePlayers(); } \ No newline at end of file diff --git a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs index 3a2001548..f6df50c68 100644 --- a/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs +++ b/QSB/QuantumSync/WorldObjects/QSBQuantumObject.cs @@ -206,7 +206,7 @@ public void OnTakeProbeSnapshot(PlayerInfo player, ProbeCamera.ID cameraId) _visibleToProbes.Add(player); } - AttachedObject._visibleInProbeSnapshot = _visibleToProbes.Any(x => x != null); + AttachedObject._visibleInProbeSnapshot = true; return; } } @@ -242,8 +242,7 @@ public void OnTakeProbeSnapshot(PlayerInfo player, ProbeCamera.ID cameraId) _visibleToProbes.Add(player); } - _visibleToProbes.Add(player); - AttachedObject._visibleInProbeSnapshot = _visibleToProbes.Any(x => x != null); + AttachedObject._visibleInProbeSnapshot = true; return; } } @@ -266,6 +265,8 @@ public void OnRemoveProbeSnapshot(PlayerInfo player) AttachedObject._visibleInProbeSnapshot = _visibleToProbes.Any(x => x != null); } + public List GetVisibleToProbePlayers() => _visibleToProbes; + public override void DisplayLines() { if (AttachedObject == null) diff --git a/QSB/ShipSync/ShipThrusterVariableSyncer.cs b/QSB/ShipSync/ShipThrusterVariableSyncer.cs index 40ede599f..859837635 100644 --- a/QSB/ShipSync/ShipThrusterVariableSyncer.cs +++ b/QSB/ShipSync/ShipThrusterVariableSyncer.cs @@ -22,6 +22,7 @@ public void Init() public void Update() { // bug : this doesn't account for autopilot + // fixes #590 if (ShipManager.Instance.CurrentFlyer == uint.MaxValue) { if (_thrusterModel) diff --git a/QSB/ShipSync/TransformSync/ShipTransformSync.cs b/QSB/ShipSync/TransformSync/ShipTransformSync.cs index d125079f0..53dc543b2 100644 --- a/QSB/ShipSync/TransformSync/ShipTransformSync.cs +++ b/QSB/ShipSync/TransformSync/ShipTransformSync.cs @@ -1,5 +1,3 @@ -using QSB.HUD; -using QSB.Player; using QSB.Syncs.Sectored.Rigidbodies; using QSB.Utility; using UnityEngine; @@ -67,19 +65,28 @@ protected override void ApplyToAttached() { _lastSetPositionTime = Time.unscaledTime; - var playerBody = Locator.GetPlayerBody(); - var relPos = AttachedTransform.ToRelPos(playerBody.GetPosition()); - var relRot = AttachedTransform.ToRelRot(playerBody.GetRotation()); + if (!PlayerState.IsAttached()) + { + var playerBody = Locator.GetPlayerBody(); + var relPos = AttachedTransform.ToRelPos(playerBody.GetPosition()); + var relRot = AttachedTransform.ToRelRot(playerBody.GetRotation()); - SetPosition(AttachedRigidbody, targetPos); - SetRotation(AttachedRigidbody, targetRot); + SetPosition(AttachedRigidbody, targetPos); + SetRotation(AttachedRigidbody, targetRot); - playerBody.SetPosition(AttachedTransform.FromRelPos(relPos)); - playerBody.SetRotation(AttachedTransform.FromRelRot(relRot)); + playerBody.SetPosition(AttachedTransform.FromRelPos(relPos)); + playerBody.SetRotation(AttachedTransform.FromRelRot(relRot)); - if (!Physics.autoSyncTransforms) + if (!Physics.autoSyncTransforms) + { + Physics.SyncTransforms(); + } + } + else { - Physics.SyncTransforms(); + SetPosition(AttachedRigidbody, targetPos); + SetRotation(AttachedRigidbody, targetRot); + GlobalMessenger.FireEvent("PlayerRepositioned"); } } } @@ -126,7 +133,7 @@ private static void SetVelocity(OWRigidbody @this, Vector3 newVelocity) private bool ShouldMovePlayer => - PlayerState.InZeroG() - && Vector3.Distance(AttachedTransform.position, Locator.GetPlayerBody().GetPosition()) < 100; + PlayerState.IsInsideShip() || + (PlayerState.InZeroG() && Vector3.Distance(AttachedTransform.position, Locator.GetPlayerBody().GetPosition()) < 100); protected override bool UseInterpolation => !ShouldMovePlayer; } diff --git a/QSB/TimeSync/WakeUpSync.cs b/QSB/TimeSync/WakeUpSync.cs index ae2c0a3bc..9b1c4442b 100644 --- a/QSB/TimeSync/WakeUpSync.cs +++ b/QSB/TimeSync/WakeUpSync.cs @@ -35,7 +35,7 @@ public enum State { NotLoaded, Loaded, FastForwarding, Pausing } private float _sendTimer; private float _serverTime; private int _serverLoopCount; - private bool _hasWokenUp; + public bool HasWokenUp; public void OnDisconnect() { @@ -75,10 +75,11 @@ private void OnWakeUp() DebugLog.DebugWrite($"OnWakeUp", MessageType.Info); if (QSBCore.IsHost) { + new ServerStateMessage(ServerState.InSolarSystem).Send(); RespawnOnDeath.Instance.Init(); } - _hasWokenUp = true; + HasWokenUp = true; } public void OnDestroy() @@ -89,12 +90,12 @@ public void OnDestroy() private void OnSceneLoaded(OWScene oldScene, OWScene newScene, bool isInUniverse) { - _hasWokenUp = false; + HasWokenUp = false; if (isInUniverse) { if (newScene == OWScene.EyeOfTheUniverse) { - _hasWokenUp = true; + HasWokenUp = true; } LocalInstance = this; @@ -124,7 +125,7 @@ private void Init() else { // dont bother sleeping, just wake up - if (!_hasWokenUp) + if (!HasWokenUp) { Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, WakeUp); } @@ -178,7 +179,7 @@ private void WakeUpOrSleep() else { // should only happen from Init so we gotta wait - if (!_hasWokenUp) + if (!HasWokenUp) { Delay.RunWhen(() => QSBWorldSync.AllObjectsReady, WakeUp); } @@ -248,7 +249,7 @@ private void ResetTimeScale() QSBInputManager.Instance.SetInputsEnabled(true); - if (!_hasWokenUp) + if (!HasWokenUp) { WakeUp(); } diff --git a/QSB/Translations/fr.json b/QSB/Translations/fr.json index efa92c3a6..7b94d52ad 100644 --- a/QSB/Translations/fr.json +++ b/QSB/Translations/fr.json @@ -40,7 +40,7 @@ "TimeSyncWaitForAllToDie": "Attendez la fin de la boucle...", "GalaxyMapEveryoneNotPresent": "Ce n’est pas encore le moment. Tout le monde doit être ici pour continuer.", "YouAreDead": "Tu es mort.", - "WaitingForRespawn": "Attendez que quelqu'un vouz ressuscité...", + "WaitingForRespawn": "Attendez que quelqu'un vous ressuscite...", "WaitingForAllToDie": "Attendez que {0} joueur(s) meurt/meurent...", "AttachToShip": "S'attacher à la fusée", "DetachFromShip": "Se détacher de la fusée", @@ -123,4 +123,4 @@ "{0} a été écrabouillé par un ascenseur" ] } -} \ No newline at end of file +} diff --git a/QSB/Utility/DebugGUI.cs b/QSB/Utility/DebugGUI.cs index 4f6acf433..ba90b93f9 100644 --- a/QSB/Utility/DebugGUI.cs +++ b/QSB/Utility/DebugGUI.cs @@ -1,4 +1,5 @@ -using QSB.ClientServerStateSync; +using Mirror; +using QSB.ClientServerStateSync; using QSB.EchoesOfTheEye.Ghosts.WorldObjects; using QSB.HUD; using QSB.Player; @@ -8,6 +9,7 @@ using QSB.ShipSync.WorldObjects; using QSB.TimeSync; using QSB.WorldSync; +using System; using System.Linq; using UnityEngine; @@ -100,6 +102,7 @@ private static void DrawGui() #region Column1 - Server data WriteLine(1, $"FPS : {Mathf.Round(1f / Time.smoothDeltaTime)}"); + WriteLine(1, $"Ping : {Math.Round(NetworkTime.rtt * 1000.0)} ms"); if (!QSBCore.DebugSettings.DrawGui) { return; diff --git a/QSB/Utility/QSBNetworkBehaviour.cs b/QSB/Utility/QSBNetworkBehaviour.cs index 534fda1c6..53de8c4d7 100644 --- a/QSB/Utility/QSBNetworkBehaviour.cs +++ b/QSB/Utility/QSBNetworkBehaviour.cs @@ -59,7 +59,7 @@ protected virtual void Update() return; } - using var writer = NetworkWriterPool.GetWriter(); + using var writer = NetworkWriterPool.Get(); Serialize(writer); UpdatePrevData(); @@ -127,7 +127,7 @@ private void OnData(ArraySegment data) Array.Copy(data.Array!, data.Offset, _lastKnownData, 0, data.Count); } - using var reader = NetworkReaderPool.GetReader(data); + using var reader = NetworkReaderPool.Get(data); Deserialize(reader); } } diff --git a/QSB/manifest.json b/QSB/manifest.json index d95fa49b0..5daedbd8d 100644 --- a/QSB/manifest.json +++ b/QSB/manifest.json @@ -7,7 +7,7 @@ "body": "- Disable *all* other mods. (Can heavily affect performance)\n- Make sure you are not running any other network-intensive applications." }, "uniqueName": "Raicuparta.QuantumSpaceBuddies", - "version": "0.26.3", + "version": "0.27.0", "owmlVersion": "2.9.0", "dependencies": [ "_nebula.MenuFramework", "JohnCorby.VanillaFix" ], "pathsToPreserve": [ "debugsettings.json" ], diff --git a/UniTask/UniTask.dll b/UniTask/UniTask.dll index 8f49443a6..f5268d8d6 100644 Binary files a/UniTask/UniTask.dll and b/UniTask/UniTask.dll differ