Added more core features & object / player spawning
This commit is contained in:
parent
c102935df1
commit
4a280c08e3
10
MLAPI/Data/NetworkedClient.cs
Normal file
10
MLAPI/Data/NetworkedClient.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
public class NetworkedClient
|
||||
{
|
||||
public int ClientId;
|
||||
public GameObject PlayerObject;
|
||||
}
|
||||
}
|
74
MLAPI/Data/NetworkingConfiguration.cs
Normal file
74
MLAPI/Data/NetworkingConfiguration.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
public class NetworkingConfiguration
|
||||
{
|
||||
public ushort ProtocolVersion = 0;
|
||||
public Dictionary<string, QosType> Channels = new Dictionary<string, QosType>();
|
||||
public List<string> MessageTypes = new List<string>();
|
||||
public int MessageBufferSize = 65536;
|
||||
public int MaxMessagesPerFrame = 150;
|
||||
public int MaxConnections = 100;
|
||||
public int Port = 7777;
|
||||
public string Address;
|
||||
public int ClientConnectionBufferTimeout = 10;
|
||||
public bool ConnectionApproval = false;
|
||||
public Action<byte[], int, Action<int, bool>> ConnectionApprovalCallback;
|
||||
public byte[] ConnectionData;
|
||||
public bool HandleObjectSpawning = true;
|
||||
//TODO
|
||||
public bool CompressMessages = false;
|
||||
//Should only be used for dedicated servers and will require the servers RSA keypair being hard coded into clients in order to exchange a AES key
|
||||
//TODO
|
||||
public bool EncryptMessages = false;
|
||||
|
||||
|
||||
//Cached config hash
|
||||
private byte[] ConfigHash = null;
|
||||
public byte[] GetConfig(bool cache = true)
|
||||
{
|
||||
if (ConfigHash != null && cache)
|
||||
return ConfigHash;
|
||||
|
||||
using(MemoryStream writeStream = new MemoryStream())
|
||||
{
|
||||
using(BinaryWriter writer = new BinaryWriter(writeStream))
|
||||
{
|
||||
writer.Write(ProtocolVersion);
|
||||
foreach (KeyValuePair<string, QosType> pair in Channels)
|
||||
{
|
||||
writer.Write(pair.Key);
|
||||
writer.Write((int)pair.Value);
|
||||
}
|
||||
for (int i = 0; i < MessageTypes.Count; i++)
|
||||
{
|
||||
writer.Write(MessageTypes[i]);
|
||||
}
|
||||
writer.Write(HandleObjectSpawning);
|
||||
writer.Write(CompressMessages);
|
||||
writer.Write(EncryptMessages);
|
||||
}
|
||||
using(SHA256Managed sha256 = new SHA256Managed())
|
||||
{
|
||||
//Returns a 256 bit / 32 byte long checksum of the config
|
||||
if (cache)
|
||||
{
|
||||
ConfigHash = sha256.ComputeHash(writeStream);
|
||||
return ConfigHash;
|
||||
}
|
||||
return sha256.ComputeHash(writeStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CompareConfig(byte[] hash)
|
||||
{
|
||||
return hash == GetConfig();
|
||||
}
|
||||
}
|
||||
}
|
@ -35,20 +35,18 @@
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="UnityEngine">
|
||||
<HintPath>..\..\..\..\..\..\Program Files\Unity\Editor\Data\Managed\UnityEngine.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="NetworkedBehaviour.cs" />
|
||||
<Compile Include="NetworkedClient.cs" />
|
||||
<Compile Include="NetworkedObject.cs" />
|
||||
<Compile Include="NetworkingConfiguration.cs" />
|
||||
<Compile Include="NetworkingManager.cs" />
|
||||
<Compile Include="MonoBehaviours\Core\NetworkedBehaviour.cs" />
|
||||
<Compile Include="Data\NetworkedClient.cs" />
|
||||
<Compile Include="MonoBehaviours\Core\NetworkedObject.cs" />
|
||||
<Compile Include="Data\NetworkingConfiguration.cs" />
|
||||
<Compile Include="MonoBehaviours\Core\NetworkingManager.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
|
82
MLAPI/MonoBehaviours/Core/NetworkedBehaviour.cs
Normal file
82
MLAPI/MonoBehaviours/Core/NetworkedBehaviour.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
public abstract class NetworkedBehaviour : MonoBehaviour
|
||||
{
|
||||
protected bool isLocalPlayer
|
||||
{
|
||||
get
|
||||
{
|
||||
return networkedObject.isLocalPlayer;
|
||||
}
|
||||
}
|
||||
protected bool isServer = NetworkingManager.singleton.isServer;
|
||||
protected NetworkedObject networkedObject
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_networkedObject == null)
|
||||
{
|
||||
_networkedObject = GetComponentInParent<NetworkedObject>();
|
||||
}
|
||||
return _networkedObject;
|
||||
}
|
||||
}
|
||||
private NetworkedObject _networkedObject = null;
|
||||
protected uint netId
|
||||
{
|
||||
get
|
||||
{
|
||||
return networkedObject.NetworkId;
|
||||
}
|
||||
}
|
||||
|
||||
//Change data type
|
||||
private Dictionary<string, int> registeredMessageHandlers = new Dictionary<string, int>();
|
||||
|
||||
public int RegisterMessageHandler(string name, Action<int, byte[]> action)
|
||||
{
|
||||
int counter = NetworkingManager.singleton.AddIncomingMessageHandler(name, action);
|
||||
registeredMessageHandlers.Add(name, counter);
|
||||
return counter;
|
||||
}
|
||||
|
||||
public void DeregisterMessageHandler(string name, int counter)
|
||||
{
|
||||
NetworkingManager.singleton.RemoveIncomingMessageHandler(name, counter);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (KeyValuePair<string, int> pair in registeredMessageHandlers)
|
||||
{
|
||||
DeregisterMessageHandler(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(int connectionId, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
Send(connectionId, messageType, channelName, data);
|
||||
}
|
||||
|
||||
public void Send(int[] connectonIds, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
for (int i = 0; i < connectonIds.Length; i++)
|
||||
{
|
||||
Send(connectonIds[i], messageType, channelName, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(List<int> connectonIds, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
for (int i = 0; i < connectonIds.Count; i++)
|
||||
{
|
||||
Send(connectonIds[i], messageType, channelName, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
MLAPI/MonoBehaviours/Core/NetworkedObject.cs
Normal file
26
MLAPI/MonoBehaviours/Core/NetworkedObject.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
//TODO
|
||||
//Will be used for objects which will be spawned automatically across clients
|
||||
public class NetworkedObject : MonoBehaviour
|
||||
{
|
||||
public uint NetworkId;
|
||||
public int OwnerClientId = -1;
|
||||
public int SpawnablePrefabId;
|
||||
internal bool IsPlayerObject = false;
|
||||
public bool isLocalPlayer
|
||||
{
|
||||
get
|
||||
{
|
||||
return OwnerClientId == NetworkingManager.singleton.MyClientId;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
NetworkingManager.OnDestroyObject(NetworkId);
|
||||
}
|
||||
}
|
||||
}
|
565
MLAPI/MonoBehaviours/Core/NetworkingManager.cs
Normal file
565
MLAPI/MonoBehaviours/Core/NetworkingManager.cs
Normal file
@ -0,0 +1,565 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
public class NetworkingManager : MonoBehaviour
|
||||
{
|
||||
public List<GameObject> SpawnablePrefabs;
|
||||
public GameObject DefaultPlayerPrefab;
|
||||
public static NetworkingManager singleton;
|
||||
//Client only, what my connectionId is on the server
|
||||
public int MyClientId;
|
||||
private Dictionary<int, NetworkedClient> connectedClients;
|
||||
private HashSet<int> pendingClients;
|
||||
internal bool isServer;
|
||||
internal bool isClient;
|
||||
internal bool isHost
|
||||
{
|
||||
get
|
||||
{
|
||||
return isServer && isClient;
|
||||
}
|
||||
}
|
||||
private bool isListening;
|
||||
private byte[] messageBuffer;
|
||||
private Dictionary<string, int> channels;
|
||||
private Dictionary<string, ushort> messageTypes;
|
||||
private Dictionary<ushort, Dictionary<int, Action<int, byte[]>>> messageCallbacks;
|
||||
private Dictionary<ushort, int> messageHandlerCounter;
|
||||
private Dictionary<ushort, Stack<int>> releasedMessageHandlerCounters;
|
||||
private int localConnectionId;
|
||||
public Scene PlayScene;
|
||||
public Scene MenuScene;
|
||||
private Dictionary<uint, NetworkedObject> spawnedObjects;
|
||||
private List<uint> spawnedObjectIds;
|
||||
private Stack<uint> releasedNetworkObjectIds;
|
||||
private uint networkObjectIdCounter;
|
||||
private uint GetNetworkObjectId()
|
||||
{
|
||||
if (releasedNetworkObjectIds.Count > 0)
|
||||
{
|
||||
return releasedNetworkObjectIds.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
networkObjectIdCounter++;
|
||||
return networkObjectIdCounter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public NetworkingConfiguration NetworkConfig;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
for (int i = 0; i < SpawnablePrefabs.Count; i++)
|
||||
{
|
||||
if(SpawnablePrefabs[i].GetComponentInChildren<NetworkedObject>() == null)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: All SpawnablePrefabs need a NetworkedObject component. Please add one to the prefab " + SpawnablePrefabs[i].gameObject.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal GameObject SpawnObject(int spawnablePrefabIndex, uint networkId, int ownerId)
|
||||
{
|
||||
GameObject go = Instantiate(singleton.SpawnablePrefabs[spawnablePrefabIndex]);
|
||||
NetworkedObject netObject = go.GetComponent<NetworkedObject>();
|
||||
if(netObject == null)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: Please add a NetworkedObject component to the root of all spawnable objects");
|
||||
netObject = go.AddComponent<NetworkedObject>();
|
||||
}
|
||||
netObject.SpawnablePrefabId = spawnablePrefabIndex;
|
||||
if(singleton.isServer)
|
||||
{
|
||||
netObject.NetworkId = singleton.GetNetworkObjectId();
|
||||
}
|
||||
else
|
||||
{
|
||||
netObject.NetworkId = networkId;
|
||||
}
|
||||
netObject.OwnerClientId = ownerId;
|
||||
|
||||
singleton.spawnedObjectIds.Add(netObject.NetworkId);
|
||||
singleton.spawnedObjects.Add(netObject.NetworkId, netObject);
|
||||
return go;
|
||||
}
|
||||
|
||||
internal GameObject SpawnPlayerObject(int connectionId, uint networkId)
|
||||
{
|
||||
GameObject go = Instantiate(DefaultPlayerPrefab);
|
||||
NetworkedObject netObject = go.GetComponent<NetworkedObject>();
|
||||
if (netObject == null)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: Please add a NetworkedObject component to the root of the player prefab");
|
||||
netObject = go.AddComponent<NetworkedObject>();
|
||||
}
|
||||
netObject.OwnerClientId = connectionId;
|
||||
if(isServer)
|
||||
{
|
||||
netObject.NetworkId = singleton.GetNetworkObjectId();
|
||||
}
|
||||
else
|
||||
{
|
||||
netObject.NetworkId = networkId;
|
||||
}
|
||||
netObject.IsPlayerObject = true;
|
||||
connectedClients[connectionId].PlayerObject = go;
|
||||
singleton.spawnedObjectIds.Add(netObject.NetworkId);
|
||||
singleton.spawnedObjects.Add(netObject.NetworkId, netObject);
|
||||
return go;
|
||||
}
|
||||
|
||||
internal static void OnDestroyObject(uint networkId)
|
||||
{
|
||||
if (!singleton.isServer)
|
||||
return;
|
||||
|
||||
NetworkedObject netObject = singleton.spawnedObjects[networkId];
|
||||
singleton.releasedNetworkObjectIds.Push(networkId);
|
||||
}
|
||||
|
||||
internal int AddIncomingMessageHandler(string name, Action<int, byte[]> action)
|
||||
{
|
||||
if(messageTypes.ContainsKey(name))
|
||||
{
|
||||
if(messageCallbacks.ContainsKey(messageTypes[name]))
|
||||
{
|
||||
int handlerId = 0;
|
||||
if (messageHandlerCounter.ContainsKey(messageTypes[name]))
|
||||
{
|
||||
if (!releasedMessageHandlerCounters.ContainsKey(messageTypes[name]))
|
||||
releasedMessageHandlerCounters.Add(messageTypes[name], new Stack<int>());
|
||||
|
||||
if(releasedMessageHandlerCounters[messageTypes[name]].Count == 0)
|
||||
{
|
||||
handlerId = messageHandlerCounter[messageTypes[name]];
|
||||
messageHandlerCounter[messageTypes[name]]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
handlerId = releasedMessageHandlerCounters[messageTypes[name]].Pop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
messageHandlerCounter.Add(messageTypes[name], handlerId + 1);
|
||||
}
|
||||
messageCallbacks[messageTypes[name]].Add(handlerId, action);
|
||||
return handlerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
messageCallbacks.Add(messageTypes[name], new Dictionary<int, Action<int, byte[]>>());
|
||||
messageHandlerCounter.Add(messageTypes[name], 1);
|
||||
messageCallbacks[messageTypes[name]].Add(0, action);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("MLAPI: The message type " + name + " has not been registered. Please define it in the netConfig");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveIncomingMessageHandler(string name, int counter)
|
||||
{
|
||||
if (counter == -1)
|
||||
return;
|
||||
|
||||
if (messageTypes.ContainsKey(name) && messageCallbacks.ContainsKey(messageTypes[name]) && messageCallbacks[messageTypes[name]].ContainsKey(counter))
|
||||
{
|
||||
messageCallbacks[messageTypes[name]].Remove(counter);
|
||||
releasedMessageHandlerCounters[messageTypes[name]].Push(counter);
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionConfig Init(NetworkingConfiguration netConfig)
|
||||
{
|
||||
NetworkConfig = netConfig;
|
||||
|
||||
pendingClients = new HashSet<int>();
|
||||
connectedClients = new Dictionary<int, NetworkedClient>();
|
||||
messageBuffer = new byte[NetworkConfig.MessageBufferSize];
|
||||
channels = new Dictionary<string, int>();
|
||||
messageTypes = new Dictionary<string, ushort>();
|
||||
messageCallbacks = new Dictionary<ushort, Dictionary<int, Action<int, byte[]>>>();
|
||||
messageHandlerCounter = new Dictionary<ushort, int>();
|
||||
releasedMessageHandlerCounters = new Dictionary<ushort, Stack<int>>();
|
||||
spawnedObjects = new Dictionary<uint, NetworkedObject>();
|
||||
spawnedObjectIds = new List<uint>();
|
||||
releasedNetworkObjectIds = new Stack<uint>();
|
||||
if(NetworkConfig.HandleObjectSpawning)
|
||||
{
|
||||
NetworkedObject[] sceneObjects = FindObjectsOfType<NetworkedObject>();
|
||||
for (int i = 0; i < sceneObjects.Length; i++)
|
||||
{
|
||||
uint networkId = GetNetworkObjectId();
|
||||
spawnedObjects.Add(networkId, sceneObjects[i]);
|
||||
spawnedObjectIds.Add(networkId);
|
||||
}
|
||||
}
|
||||
|
||||
if (NetworkConfig.ConnectionApproval)
|
||||
{
|
||||
if(NetworkConfig.ConnectionApprovalCallback == null)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: No ConnectionApproval callback defined. Connection aproval will timeout");
|
||||
}
|
||||
}
|
||||
|
||||
NetworkTransport.Init();
|
||||
ConnectionConfig cConfig = new ConnectionConfig();
|
||||
|
||||
//MLAPI channels and messageTypes
|
||||
NetworkConfig.Channels.Add("MLAPI_RELIABLE_FRAGMENTED", QosType.ReliableFragmented);
|
||||
messageTypes.Add("MLAPI_CONNECTION_REQUEST", 0);
|
||||
messageTypes.Add("MLAPI_CLIENT_LIST", 1);
|
||||
//messageTypes.Add("")
|
||||
|
||||
|
||||
HashSet<string> channelNames = new HashSet<string>();
|
||||
foreach (KeyValuePair<string, QosType> pair in NetworkConfig.Channels)
|
||||
{
|
||||
if(pair.Key.StartsWith("MLAPI_"))
|
||||
{
|
||||
Debug.LogWarning("MLAPI: Channel names are not allowed to start with MLAPI_. This is to prevent name conflicts");
|
||||
continue;
|
||||
}
|
||||
else if(channelNames.Contains(pair.Key))
|
||||
{
|
||||
Debug.LogWarning("MLAPI: Duplicate channel name: " + pair.Key);
|
||||
continue;
|
||||
}
|
||||
channels.Add(pair.Key, cConfig.AddChannel(pair.Value));
|
||||
}
|
||||
//0-32 are reserved for MLAPI messages
|
||||
for (ushort i = 32; i < NetworkConfig.MessageTypes.Count; i++)
|
||||
{
|
||||
messageTypes.Add(NetworkConfig.MessageTypes[i], i);
|
||||
}
|
||||
return cConfig;
|
||||
}
|
||||
|
||||
|
||||
public void StartServer(NetworkingConfiguration netConfig)
|
||||
{
|
||||
ConnectionConfig cConfig = Init(netConfig);
|
||||
HostTopology hostTopology = new HostTopology(cConfig, NetworkConfig.MaxConnections);
|
||||
hostId = NetworkTransport.AddHost(hostTopology, NetworkConfig.Port, null);
|
||||
isServer = true;
|
||||
isClient = false;
|
||||
isListening = true;
|
||||
SceneManager.LoadScene(PlayScene.buildIndex);
|
||||
}
|
||||
|
||||
public void StartClient(NetworkingConfiguration netConfig)
|
||||
{
|
||||
ConnectionConfig cConfig = Init(netConfig);
|
||||
HostTopology hostTopology = new HostTopology(cConfig, NetworkConfig.MaxConnections);
|
||||
hostId = NetworkTransport.AddHost(hostTopology, 0);
|
||||
|
||||
localConnectionId = NetworkTransport.Connect(hostId, NetworkConfig.Address, NetworkConfig.Port, 0, out error);
|
||||
isServer = false;
|
||||
isClient = true;
|
||||
isListening = true;
|
||||
}
|
||||
|
||||
public void StartHost(NetworkingConfiguration netConfig)
|
||||
{
|
||||
ConnectionConfig cConfig = Init(netConfig);
|
||||
HostTopology hostTopology = new HostTopology(cConfig, NetworkConfig.MaxConnections);
|
||||
hostId = NetworkTransport.AddHost(hostTopology, NetworkConfig.Port, null);
|
||||
isServer = true;
|
||||
isClient = true;
|
||||
isListening = true;
|
||||
connectedClients.Add(-1, new NetworkedClient() { ClientId = -1 });
|
||||
SceneManager.LoadScene(PlayScene.buildIndex);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (singleton != null)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: Multiple NetworkingManagers");
|
||||
return;
|
||||
}
|
||||
singleton = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
singleton = null;
|
||||
}
|
||||
|
||||
//Receive stuff
|
||||
int hostId;
|
||||
int connectionId;
|
||||
int channelId;
|
||||
int receivedSize;
|
||||
byte error;
|
||||
private void Update()
|
||||
{
|
||||
if(isListening)
|
||||
{
|
||||
NetworkEventType eventType;
|
||||
int messagesProcessed = 0;
|
||||
do
|
||||
{
|
||||
messagesProcessed++;
|
||||
eventType = NetworkTransport.Receive(out hostId, out connectionId, out channelId, messageBuffer, NetworkConfig.MessageBufferSize, out receivedSize, out error);
|
||||
NetworkError networkError = (NetworkError)error;
|
||||
if (networkError != NetworkError.Ok)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: NetworkTransport receive error: " + networkError.ToString());
|
||||
return;
|
||||
}
|
||||
switch (eventType)
|
||||
{
|
||||
case NetworkEventType.ConnectEvent:
|
||||
if (isServer)
|
||||
{
|
||||
pendingClients.Add(connectionId);
|
||||
StartCoroutine(ApprovalTimeout(connectionId));
|
||||
}
|
||||
else
|
||||
{
|
||||
using (MemoryStream writeStream = new MemoryStream())
|
||||
{
|
||||
using (BinaryWriter writer = new BinaryWriter(writeStream))
|
||||
{
|
||||
writer.Write(NetworkConfig.GetConfig());
|
||||
if(NetworkConfig.ConnectionApproval)
|
||||
{
|
||||
writer.Write(NetworkConfig.ConnectionData);
|
||||
}
|
||||
Send(connectionId, "MLAPI_CONNECTION_REQUEST", "MLAPI_RELIABLE_FRAGMENTED", writeStream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NetworkEventType.DataEvent:
|
||||
HandleIncomingData(connectionId, ref messageBuffer);
|
||||
break;
|
||||
}
|
||||
} while (eventType != NetworkEventType.Nothing &&
|
||||
(messagesProcessed < NetworkConfig.MaxMessagesPerFrame || NetworkConfig.MaxMessagesPerFrame < 0));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator ApprovalTimeout(int connectionId)
|
||||
{
|
||||
float timeStarted = Time.time;
|
||||
//We yield every frame incase a pending client disconnects and someone else gets its connection id
|
||||
while (Time.time - timeStarted < NetworkConfig.ClientConnectionBufferTimeout && pendingClients.Contains(connectionId))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
if(pendingClients.Contains(connectionId))
|
||||
{
|
||||
//Timeout
|
||||
DisconnectClient(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleIncomingData(int connectonId, ref byte[] data)
|
||||
{
|
||||
using(MemoryStream readStream = new MemoryStream(data))
|
||||
{
|
||||
using (BinaryReader reader = new BinaryReader(readStream))
|
||||
{
|
||||
ushort messageType = reader.ReadUInt16();
|
||||
|
||||
//Client tried to send a network message that was not the connection request before he was accepted.
|
||||
if (pendingClients.Contains(connectionId) && messageType != 0)
|
||||
return;
|
||||
|
||||
if (messageType >= 32)
|
||||
{
|
||||
//Custom message, invoke all message handlers
|
||||
foreach (KeyValuePair<int, Action<int, byte[]>> pair in messageCallbacks[messageType])
|
||||
{
|
||||
pair.Value(connectionId, reader.ReadBytes(int.MaxValue));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//MLAPI message
|
||||
switch (messageType)
|
||||
{
|
||||
case 0: //Client to server > sends connection buffer
|
||||
if (isServer)
|
||||
{
|
||||
using (MemoryStream messageReadStream = new MemoryStream(reader.ReadBytes(int.MaxValue)))
|
||||
{
|
||||
using (BinaryReader messageReader = new BinaryReader(messageReadStream))
|
||||
{
|
||||
byte[] configHash = messageReader.ReadBytes(32);
|
||||
if (NetworkConfig.CompareConfig(configHash) == false)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: NetworkConfiguration missmatch. The configuration between the server and client does not match.");
|
||||
DisconnectClient(connectionId);
|
||||
return;
|
||||
}
|
||||
if (NetworkConfig.ConnectionApproval)
|
||||
{
|
||||
byte[] connectionBuffer = messageReader.ReadBytes(int.MaxValue);
|
||||
NetworkConfig.ConnectionApprovalCallback(connectionBuffer, connectionId, HandleApproval);
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleApproval(connectionId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 1: //Server informs client it has been approved:
|
||||
if (isClient)
|
||||
{
|
||||
SceneManager.LoadScene(PlayScene.buildIndex);
|
||||
using (MemoryStream messageReadStream = new MemoryStream(reader.ReadBytes(int.MaxValue)))
|
||||
{
|
||||
using (BinaryReader messageReader = new BinaryReader(messageReadStream))
|
||||
{
|
||||
MyClientId = messageReader.ReadInt32();
|
||||
connectedClients.Add(MyClientId, new NetworkedClient() { ClientId = MyClientId });
|
||||
int clientCount = messageReader.ReadInt32();
|
||||
for (int i = 0; i < clientCount; i++)
|
||||
{
|
||||
int conId = messageReader.ReadInt32();
|
||||
connectedClients.Add(conId, new NetworkedClient() { ClientId = conId });
|
||||
}
|
||||
if(NetworkConfig.HandleObjectSpawning)
|
||||
{
|
||||
int objectCount = messageReader.ReadInt32();
|
||||
for (int i = 0; i < objectCount; i++)
|
||||
{
|
||||
bool isPlayerObject = messageReader.ReadBoolean();
|
||||
uint networkId = messageReader.ReadUInt32();
|
||||
int ownerId = messageReader.ReadInt32();
|
||||
int prefabId = messageReader.ReadInt32();
|
||||
if(isPlayerObject)
|
||||
{
|
||||
SpawnPlayerObject(ownerId, networkId);
|
||||
}
|
||||
else
|
||||
{
|
||||
SpawnObject(prefabId, networkId, ownerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Send(int connectionId, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
{
|
||||
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||
{
|
||||
writer.Write(messageTypes[messageType]);
|
||||
writer.Write(data);
|
||||
NetworkTransport.Send(hostId, connectionId, channels[channelName], data, data.Length, out error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Send(int[] connectonIds, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
for (int i = 0; i < connectonIds.Length; i++)
|
||||
{
|
||||
Send(connectonIds[i], messageType, channelName, data);
|
||||
}
|
||||
}
|
||||
|
||||
internal void Send(List<int> connectonIds, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
for (int i = 0; i < connectonIds.Count; i++)
|
||||
{
|
||||
Send(connectonIds[i], messageType, channelName, data);
|
||||
}
|
||||
}
|
||||
|
||||
internal void DisconnectClient(int connectionId)
|
||||
{
|
||||
if (!isServer)
|
||||
return;
|
||||
if (pendingClients.Contains(connectionId))
|
||||
pendingClients.Remove(connectionId);
|
||||
if (connectedClients.ContainsKey(connectionId))
|
||||
connectedClients.Remove(connectionId);
|
||||
NetworkTransport.Disconnect(hostId, connectionId, out error);
|
||||
|
||||
}
|
||||
|
||||
private void HandleApproval(int connectionId, bool approved)
|
||||
{
|
||||
if(approved)
|
||||
{
|
||||
NetworkedClient client = new NetworkedClient()
|
||||
{
|
||||
ClientId = connectionId
|
||||
};
|
||||
connectedClients.Add(connectionId, client);
|
||||
if(NetworkConfig.HandleObjectSpawning)
|
||||
{
|
||||
uint networkId = GetNetworkObjectId();
|
||||
GameObject go = SpawnPlayerObject(connectionId, networkId);
|
||||
connectedClients[connectionId].PlayerObject = go;
|
||||
}
|
||||
using (MemoryStream writeStream = new MemoryStream())
|
||||
{
|
||||
using (BinaryWriter writer = new BinaryWriter(writeStream))
|
||||
{
|
||||
writer.Write(connectionId);
|
||||
writer.Write(connectedClients.Count - 1);
|
||||
foreach (KeyValuePair<int, NetworkedClient> item in connectedClients)
|
||||
{
|
||||
//Our own ID. Already added as the first one above
|
||||
if (item.Key == connectionId)
|
||||
continue;
|
||||
writer.Write(item.Key); //Connection id
|
||||
}
|
||||
if (NetworkConfig.HandleObjectSpawning)
|
||||
{
|
||||
writer.Write(spawnedObjectIds.Count);
|
||||
for (int i = 0; i < spawnedObjectIds.Count; i++)
|
||||
{
|
||||
writer.Write(spawnedObjects[spawnedObjectIds[i]].IsPlayerObject);
|
||||
writer.Write(spawnedObjects[spawnedObjectIds[i]].NetworkId);
|
||||
writer.Write(spawnedObjects[spawnedObjectIds[i]].OwnerClientId);
|
||||
writer.Write(spawnedObjects[spawnedObjectIds[i]].SpawnablePrefabId);
|
||||
}
|
||||
}
|
||||
|
||||
Send(connectionId, "MLAPI_CLIENT_LIST", "MLAPI_RELIABLE_FRAGMENTED", writeStream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pendingClients.Contains(connectionId))
|
||||
pendingClients.Remove(connectionId);
|
||||
NetworkTransport.Disconnect(hostId, connectionId, out error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
public abstract class NetworkedBehaviour : MonoBehaviour
|
||||
{
|
||||
protected bool isLocalPlayer;
|
||||
protected bool isServer = NetworkingManager.singleton.isServer;
|
||||
|
||||
//Change data type
|
||||
private Dictionary<string, int> registeredMessageHandlers = new Dictionary<string, int>();
|
||||
|
||||
public int RegisterMessageHandler(string name, Action<int, byte[]> action)
|
||||
{
|
||||
int counter = NetworkingManager.singleton.AddIncomingMessageHandler(name, action);
|
||||
registeredMessageHandlers.Add(name, counter);
|
||||
return counter;
|
||||
}
|
||||
|
||||
public void DeregisterMessageHandler(string name, int counter)
|
||||
{
|
||||
NetworkingManager.singleton.RemoveIncomingMessageHandler(name, counter);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach(KeyValuePair<string, int> pair in registeredMessageHandlers)
|
||||
{
|
||||
DeregisterMessageHandler(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace MLAPI
|
||||
{
|
||||
public class NetworkedClient
|
||||
{
|
||||
public int ClientId;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
//TODO
|
||||
//Will be used for objects which will be spawned automatically across clients
|
||||
public class NetworkedObject : MonoBehaviour
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
public class NetworkingConfiguration
|
||||
{
|
||||
public ushort ProtocolVersion = 0;
|
||||
public Dictionary<string, QosType> Channels = new Dictionary<string, QosType>();
|
||||
public List<string> MessageTypes = new List<string>();
|
||||
public int MessageBufferSize = 65536;
|
||||
public int MaxMessagesPerFrame = 150;
|
||||
public int MaxConnections = 100;
|
||||
public int Port = 7777;
|
||||
public int ClientConnectionBufferTimeout = 10;
|
||||
public bool ConnectionApproval = false;
|
||||
public Action<byte[], int, Action<int, bool>> ConnectionApprovalCallback;
|
||||
public byte[] ConnectionData;
|
||||
}
|
||||
}
|
@ -1,320 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace MLAPI
|
||||
{
|
||||
public class NetworkingManager : MonoBehaviour
|
||||
{
|
||||
public static NetworkingManager singleton;
|
||||
//Client only, what my connectionId is on the server
|
||||
public int MyClientId;
|
||||
//Server only
|
||||
private Dictionary<int, NetworkedClient> connectedClients;
|
||||
private HashSet<int> pendingClients;
|
||||
internal bool isServer;
|
||||
private bool isListening;
|
||||
private byte[] messageBuffer;
|
||||
private Dictionary<string, int> channels;
|
||||
private Dictionary<string, ushort> messageTypes;
|
||||
private Dictionary<ushort, Dictionary<int, Action<int, byte[]>>> messageCallbacks;
|
||||
private Dictionary<ushort, int> messageHandlerCounter;
|
||||
private Dictionary<ushort, Stack<int>> releasedMessageHandlerCounters;
|
||||
|
||||
public NetworkingConfiguration NetworkConfig;
|
||||
|
||||
internal int AddIncomingMessageHandler(string name, Action<int, byte[]> action)
|
||||
{
|
||||
if(messageTypes.ContainsKey(name))
|
||||
{
|
||||
if(messageCallbacks.ContainsKey(messageTypes[name]))
|
||||
{
|
||||
int handlerId = 0;
|
||||
if (messageHandlerCounter.ContainsKey(messageTypes[name]))
|
||||
{
|
||||
if (!releasedMessageHandlerCounters.ContainsKey(messageTypes[name]))
|
||||
releasedMessageHandlerCounters.Add(messageTypes[name], new Stack<int>());
|
||||
|
||||
if(releasedMessageHandlerCounters[messageTypes[name]].Count == 0)
|
||||
{
|
||||
handlerId = messageHandlerCounter[messageTypes[name]];
|
||||
messageHandlerCounter[messageTypes[name]]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
handlerId = releasedMessageHandlerCounters[messageTypes[name]].Pop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
messageHandlerCounter.Add(messageTypes[name], handlerId + 1);
|
||||
}
|
||||
messageCallbacks[messageTypes[name]].Add(handlerId, action);
|
||||
return handlerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
messageCallbacks.Add(messageTypes[name], new Dictionary<int, Action<int, byte[]>>());
|
||||
messageHandlerCounter.Add(messageTypes[name], 1);
|
||||
messageCallbacks[messageTypes[name]].Add(0, action);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("MLAPI: The message type " + name + " has not been registered. Please define it in the netConfig");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveIncomingMessageHandler(string name, int counter)
|
||||
{
|
||||
if (counter == -1)
|
||||
return;
|
||||
|
||||
if (messageTypes.ContainsKey(name) && messageCallbacks.ContainsKey(messageTypes[name]) && messageCallbacks[messageTypes[name]].ContainsKey(counter))
|
||||
{
|
||||
messageCallbacks[messageTypes[name]].Remove(counter);
|
||||
releasedMessageHandlerCounters[messageTypes[name]].Push(counter);
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionConfig Init(NetworkingConfiguration netConfig)
|
||||
{
|
||||
NetworkConfig = netConfig;
|
||||
|
||||
pendingClients = new HashSet<int>();
|
||||
connectedClients = new Dictionary<int, NetworkedClient>();
|
||||
messageBuffer = new byte[NetworkConfig.MessageBufferSize];
|
||||
channels = new Dictionary<string, int>();
|
||||
messageTypes = new Dictionary<string, ushort>();
|
||||
messageCallbacks = new Dictionary<ushort, Dictionary<int, Action<int, byte[]>>>();
|
||||
messageHandlerCounter = new Dictionary<ushort, int>();
|
||||
releasedMessageHandlerCounters = new Dictionary<ushort, Stack<int>>();
|
||||
|
||||
if (NetworkConfig.ConnectionApproval)
|
||||
{
|
||||
if(NetworkConfig.ConnectionApprovalCallback == null)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: No ConnectionAproval callback defined. Connection aproval will timeout");
|
||||
}
|
||||
}
|
||||
|
||||
NetworkTransport.Init();
|
||||
ConnectionConfig cConfig = new ConnectionConfig();
|
||||
|
||||
//MLAPI channels and messageTypes
|
||||
NetworkConfig.Channels.Add("MLAPI_RELIABLE_FRAGMENTED", QosType.ReliableFragmented);
|
||||
messageTypes.Add("CONNECTION_REQUEST", 0);
|
||||
|
||||
|
||||
foreach (KeyValuePair<string, QosType> pair in NetworkConfig.Channels)
|
||||
{
|
||||
channels.Add(pair.Key, cConfig.AddChannel(pair.Value));
|
||||
}
|
||||
//0-32 are reserved for MLAPI messages
|
||||
for (ushort i = 32; i < NetworkConfig.MessageTypes.Count; i++)
|
||||
{
|
||||
messageTypes.Add(NetworkConfig.MessageTypes[i], i);
|
||||
}
|
||||
return cConfig;
|
||||
}
|
||||
|
||||
|
||||
public void StartServer(NetworkingConfiguration netConfig)
|
||||
{
|
||||
ConnectionConfig cConfig = Init(netConfig);
|
||||
HostTopology hostTopology = new HostTopology(cConfig, NetworkConfig.MaxConnections);
|
||||
NetworkTransport.AddHost(hostTopology, NetworkConfig.Port);
|
||||
isServer = true;
|
||||
isListening = true;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (singleton != null)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: Multiple NetworkingManagers");
|
||||
return;
|
||||
}
|
||||
singleton = this;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
singleton = null;
|
||||
}
|
||||
|
||||
//Receive stuff
|
||||
int hostId;
|
||||
int connectionId;
|
||||
int channelId;
|
||||
int receivedSize;
|
||||
byte error;
|
||||
private void Update()
|
||||
{
|
||||
if(isListening)
|
||||
{
|
||||
NetworkEventType eventType;
|
||||
int messagesProcessed = 0;
|
||||
do
|
||||
{
|
||||
messagesProcessed++;
|
||||
eventType = NetworkTransport.Receive(out hostId, out connectionId, out channelId, messageBuffer, NetworkConfig.MessageBufferSize, out receivedSize, out error);
|
||||
NetworkError networkError = (NetworkError)error;
|
||||
if (networkError != NetworkError.Ok)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: NetworkTransport receive error: " + networkError.ToString());
|
||||
return;
|
||||
}
|
||||
switch (eventType)
|
||||
{
|
||||
case NetworkEventType.ConnectEvent:
|
||||
if(isServer)
|
||||
{
|
||||
if (NetworkConfig.ConnectionApproval)
|
||||
{
|
||||
pendingClients.Add(connectionId);
|
||||
StartCoroutine(ApprovalTimeout(connectionId));
|
||||
}
|
||||
else
|
||||
{
|
||||
//Connect
|
||||
HandleApproval(connectionId, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (NetworkConfig.ConnectionApproval)
|
||||
Send(connectionId, "CONNECTION_REQUEST", "MLAPI_RELIABLE_FRAGMENTED", NetworkConfig.ConnectionData);
|
||||
}
|
||||
break;
|
||||
case NetworkEventType.DataEvent:
|
||||
HandleIncomingData(connectionId, ref messageBuffer);
|
||||
break;
|
||||
}
|
||||
} while (eventType != NetworkEventType.Nothing &&
|
||||
(messagesProcessed < NetworkConfig.MaxMessagesPerFrame || NetworkConfig.MaxMessagesPerFrame < 0));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator ApprovalTimeout(int connectionId)
|
||||
{
|
||||
float timeStarted = Time.time;
|
||||
//We yield every frame incase a pending client disconnects and someone else gets its connection id
|
||||
while (Time.time - timeStarted < NetworkConfig.ClientConnectionBufferTimeout && pendingClients.Contains(connectionId))
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
if(pendingClients.Contains(connectionId))
|
||||
{
|
||||
//Timeout
|
||||
DisconnectClient(connectionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
//If the key nolonger exist in pending and not in connected, they disconnected
|
||||
if(!connectedClients.ContainsKey(connectionId))
|
||||
{
|
||||
pendingClients.Remove(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleIncomingData(int connectonId, ref byte[] data)
|
||||
{
|
||||
using(MemoryStream stream = new MemoryStream(data))
|
||||
{
|
||||
BinaryReader reader = new BinaryReader(stream);
|
||||
ushort protocolVersion = reader.ReadUInt16();
|
||||
ushort messageType = reader.ReadUInt16();
|
||||
if(messageType >= 32)
|
||||
{
|
||||
//Custom message
|
||||
if(protocolVersion != NetworkConfig.ProtocolVersion)
|
||||
{
|
||||
Debug.LogWarning("MLAPI: Protocol version not matching");
|
||||
DisconnectClient(connectionId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//MLAPI message
|
||||
switch(messageType)
|
||||
{
|
||||
case 0: //Client to server > sends connection buffer
|
||||
byte[] connectionBuffer = reader.ReadBytes(int.MaxValue);
|
||||
NetworkConfig.ConnectionApprovalCallback(connectionBuffer, connectionId, HandleApproval);
|
||||
break;
|
||||
case 1: //Server gives client it's connectionId
|
||||
break;
|
||||
case 2: //Server informs client of spawned objects
|
||||
break;
|
||||
case 3: //Server informs client of spawned players
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void Send(int connectionId, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
{
|
||||
BinaryWriter writer = new BinaryWriter(stream);
|
||||
writer.Write(NetworkConfig.ProtocolVersion);
|
||||
writer.Write(messageTypes[messageType]);
|
||||
writer.Write(data);
|
||||
NetworkTransport.Send(hostId, connectionId, channels[channelName], data, data.Length, out error);
|
||||
}
|
||||
}
|
||||
|
||||
protected void Send(int[] connectonIds, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
for (int i = 0; i < connectonIds.Length; i++)
|
||||
{
|
||||
Send(connectonIds[i], messageType, channelName, data);
|
||||
}
|
||||
}
|
||||
|
||||
protected void Send(List<int> connectonIds, string messageType, string channelName, byte[] data)
|
||||
{
|
||||
for (int i = 0; i < connectonIds.Count; i++)
|
||||
{
|
||||
Send(connectonIds[i], messageType, channelName, data);
|
||||
}
|
||||
}
|
||||
|
||||
protected void DisconnectClient(int connectionId)
|
||||
{
|
||||
if (pendingClients.Contains(connectionId))
|
||||
pendingClients.Remove(connectionId);
|
||||
if (connectedClients.ContainsKey(connectionId))
|
||||
connectedClients.Remove(connectionId);
|
||||
NetworkTransport.Disconnect(hostId, connectionId, out error);
|
||||
|
||||
}
|
||||
|
||||
private void HandleApproval(int connectionId, bool approved)
|
||||
{
|
||||
if(approved)
|
||||
{
|
||||
NetworkedClient client = new NetworkedClient()
|
||||
{
|
||||
ClientId = connectionId
|
||||
};
|
||||
connectedClients.Add(connectionId, client);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pendingClients.Contains(connectionId))
|
||||
pendingClients.Remove(connectionId);
|
||||
NetworkTransport.Disconnect(hostId, connectionId, out error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user