Merge branch 'Erdroy-lowlevel-networking'

This commit is contained in:
Wojtek Figat
2021-07-02 09:53:11 +02:00
17 changed files with 7746 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ public class Engine : EngineModule
options.PublicDependencies.Add("Input");
options.PublicDependencies.Add("Level");
options.PublicDependencies.Add("Navigation");
options.PublicDependencies.Add("Networking");
options.PublicDependencies.Add("Physics");
options.PublicDependencies.Add("Particles");
options.PublicDependencies.Add("Scripting");

View File

@@ -0,0 +1,250 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
//
// TODO: Check defines so we can disable ENet
#include "ENetDriver.h"
#include "Engine/Networking/NetworkConfig.h"
#include "Engine/Networking/NetworkChannelType.h"
#include "Engine/Networking/NetworkEvent.h"
#include "Engine/Networking/NetworkPeer.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/Array.h"
#define ENET_IMPLEMENTATION
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <enet/enet.h>
#undef _WINSOCK_DEPRECATED_NO_WARNINGS
#undef SendMessage
ENetPacketFlag ChannelTypeToPacketFlag(const NetworkChannelType channel)
{
int flag = 0; // Maybe use ENET_PACKET_FLAG_NO_ALLOCATE?
// Add reliable flag when it is "reliable" channel
if(channel > NetworkChannelType::UnreliableOrdered)
flag |= ENET_PACKET_FLAG_RELIABLE;
// Use unsequenced flag when the flag is unreliable. We have to sequence all other packets.
if(channel == NetworkChannelType::Unreliable)
flag |= ENET_PACKET_FLAG_UNSEQUENCED;
// Note that all reliable channels are exactly the same. TODO: How to handle unordered reliable packets...?
return static_cast<ENetPacketFlag>(flag);
}
void SendPacketToPeer(ENetPeer* peer, const NetworkChannelType channelType, const NetworkMessage& message)
{
// Covert our channel type to the internal ENet packet flags
const ENetPacketFlag flag = ChannelTypeToPacketFlag(channelType);
// This will copy the data into the packet when ENET_PACKET_FLAG_NO_ALLOCATE is not set.
// Tho, we cannot use it, because we're releasing the message right after the send - and the packet might not
// be sent, yet. To avoid data corruption, we're just using the copy method. We might fix that later, but I'll take
// the smaller risk.
ENetPacket* packet = enet_packet_create(message.Buffer, message.Length, flag);
// And send it!
enet_peer_send (peer, 0, packet);
// TODO: To reduce latency, we can use `enet_host_flush` to flush all packets. Maybe some API, like NetworkManager::FlushQueues()?
}
void ENetDriver::Initialize(NetworkPeer* host, const NetworkConfig& config)
{
_networkHost = host;
_config = config;
_peerMap = Dictionary<uint32, void*>();
if (enet_initialize () != 0) {
LOG(Error, "Failed to initialize ENet driver!");
}
LOG(Info, "Initialized ENet driver!");
}
void ENetDriver::Dispose()
{
if(_peer)
enet_peer_disconnect_now((ENetPeer*)_peer, 0);
enet_host_destroy((ENetHost*)_host);
enet_deinitialize();
_peerMap.Clear();
_peerMap = {};
_peer = nullptr;
_host = nullptr;
LOG(Info, "ENet driver stopped!");
}
bool ENetDriver::Listen()
{
ENetAddress address = {0};
address.port = _config.Port;
address.host = ENET_HOST_ANY;
// Set host address if needed
if(_config.Address != String("any"))
enet_address_set_host(&address, _config.Address.ToStringAnsi().GetText());
// Create ENet host
_host = enet_host_create(&address, _config.ConnectionsLimit, 1, 0, 0);
if(_host == nullptr)
{
LOG(Error, "Failed to initialize ENet host!");
return false;
}
LOG(Info, "Created ENet server!");
return true;
}
bool ENetDriver::Connect()
{
LOG(Info, "Connecting using ENet...");
ENetAddress address = {0};
address.port = _config.Port;
enet_address_set_host(&address, _config.Address.ToStringAnsi().GetText());
// Create ENet host
_host = enet_host_create(nullptr, 1, 1, 0, 0);
if(_host == nullptr)
{
LOG(Error, "Failed to initialize ENet host!");
return false;
}
// Create ENet peer/connect to the server
_peer = enet_host_connect((ENetHost*)_host, &address, 1, 0);
if(_peer == nullptr)
{
LOG(Error, "Failed to create ENet host!");
enet_host_destroy((ENetHost*)_host);
return false;
}
return true;
}
void ENetDriver::Disconnect()
{
ASSERT(_peer != nullptr);
if(_peer)
{
enet_peer_disconnect_now((ENetPeer*)_peer, 0);
_peer = nullptr;
LOG(Info, "Disconnected");
}
}
void ENetDriver::Disconnect(const NetworkConnection& connection)
{
const int connectionId = connection.ConnectionId;
void* peer = nullptr;
if(_peerMap.TryGet(connectionId, peer))
{
enet_peer_disconnect_now((ENetPeer*)_peer, 0);
_peerMap.Remove(connectionId);
}
else
{
LOG(Error, "Failed to kick connection({0}). ENetPeer not found!", connection.ConnectionId);
}
}
bool ENetDriver::PopEvent(NetworkEvent* eventPtr)
{
ENetEvent event;
const int result = enet_host_service((ENetHost*)_host, &event, 0);
if(result < 0)
LOG(Error, "Failed to check ENet events!");
if(result > 0)
{
// Copy sender data
const uint32 connectionId = enet_peer_get_id(event.peer);
eventPtr->Sender = NetworkConnection();
eventPtr->Sender.ConnectionId = connectionId;
switch(event.type)
{
case ENET_EVENT_TYPE_CONNECT:
eventPtr->EventType = NetworkEventType::Connected;
if(IsServer())
_peerMap.Add(connectionId, event.peer);
break;
case ENET_EVENT_TYPE_DISCONNECT:
eventPtr->EventType = NetworkEventType::Disconnected;
if(IsServer())
_peerMap.Remove(connectionId);
break;
case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
eventPtr->EventType = NetworkEventType::Timeout;
if(IsServer())
_peerMap.Remove(connectionId);
break;
case ENET_EVENT_TYPE_RECEIVE:
eventPtr->EventType = NetworkEventType::Message;
// Acquire message and copy message data
eventPtr->Message = _networkHost->CreateMessage();
eventPtr->Message.Length = event.packet->dataLength;
Memory::CopyItems(eventPtr->Message.Buffer, event.packet->data, event.packet->dataLength);
break;
default: break;
}
return true; // Event
}
return false; // No events
}
void ENetDriver::SendMessage(const NetworkChannelType channelType, const NetworkMessage& message)
{
ASSERT(IsServer() == false);
SendPacketToPeer((ENetPeer*)_peer, channelType, message);
}
void ENetDriver::SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target)
{
ASSERT(IsServer());
ENetPeer* peer = *(ENetPeer**)_peerMap.TryGet(target.ConnectionId);
ASSERT(peer != nullptr);
ASSERT(peer->state == ENET_PEER_STATE_CONNECTED);
SendPacketToPeer(peer, channelType, message);
}
void ENetDriver::SendMessage(const NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets)
{
ASSERT(IsServer());
for(NetworkConnection target : targets)
{
ENetPeer* peer = *(ENetPeer**)_peerMap.TryGet(target.ConnectionId);
ASSERT(peer != nullptr);
ASSERT(peer->state == ENET_PEER_STATE_CONNECTED);
SendPacketToPeer(peer, channelType, message);
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Networking/Types.h"
#include "Engine/Networking/INetworkDriver.h"
#include "Engine/Networking/NetworkConnection.h"
#include "Engine/Networking/NetworkConfig.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Low-level network transport interface implementation based on ENet library.
/// </summary>
API_CLASS(Namespace="FlaxEngine.Networking", Sealed) class FLAXENGINE_API ENetDriver : public INetworkDriver
{
DECLARE_SCRIPTING_TYPE_MINIMAL(ENetDriver);
public:
void Initialize(NetworkPeer* host, const NetworkConfig& config) override;
void Dispose() override;
bool Listen() override;
bool Connect() override;
void Disconnect() override;
void Disconnect(const NetworkConnection& connection) override;
bool PopEvent(NetworkEvent* eventPtr) override;
void SendMessage(NetworkChannelType channelType, const NetworkMessage& message) override;
void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target) override;
void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets) override;
private:
bool IsServer() const
{
return _host != nullptr && _peer == nullptr;
}
private:
NetworkConfig _config;
NetworkPeer* _networkHost;
void* _host = nullptr;
void* _peer = nullptr;
Dictionary<uint32, void*> _peerMap;
};

View File

@@ -0,0 +1,110 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
#include "Types.h"
/// <summary>
/// Basic interface for the low-level network transport/driver.
/// </summary>
API_INTERFACE(Namespace="FlaxEngine.Networking") class FLAXENGINE_API INetworkDriver
{
DECLARE_SCRIPTING_TYPE_MINIMAL(INetworkDriver);
public:
/// <summary>
/// Finalizes an instance of the <see cref="INetworkDriver"/> class.
/// </summary>
virtual ~INetworkDriver() = default;
public:
/// <summary>
/// Initializes the instance of this network driver using given configuration.
/// </summary>
/// <param name="host">The peer that this driver has been assigned to.</param>
/// <param name="config">The network config to use to configure this driver.</param>
virtual void Initialize(NetworkPeer* host, const NetworkConfig& config) = 0;
/// <summary>
/// Disposes this driver making it no longer usable.
/// Reserved for resource deallocation etc.
/// </summary>
virtual void Dispose() = 0;
/// <summary>
/// Starts listening for incoming connections.
/// Once this is called, this driver becomes a server.
/// </summary>
/// <returns>True when succeeded.</returns>
virtual bool Listen() = 0;
/// <summary>
/// Starts connection handshake with the end point specified in the <seealso cref="NetworkConfig"/> structure.
/// Once this is called, this driver becomes a client.
/// </summary>
/// <returns>True when succeeded.</returns>
virtual bool Connect() = 0;
/// <summary>
/// Disconnects from the server.
/// </summary>
/// <remarks>Can be used only by the client!</remarks>
virtual void Disconnect() = 0;
/// <summary>
/// Disconnects given connection from the server.
/// </summary>
/// <remarks>Can be used only by the server!</remarks>
virtual void Disconnect(const NetworkConnection& connection) = 0;
/// <summary>
/// Tries to pop an network event from the queue.
/// </summary>
/// <param name="eventPtr">The pointer to event structure.</param>
/// <returns>True when succeeded and the event can be processed.</returns>
virtual bool PopEvent(NetworkEvent* eventPtr) = 0;
/// <summary>
/// Sends given message over specified channel to the server.
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <remarks>Can be used only by the client!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
virtual void SendMessage(NetworkChannelType channelType, const NetworkMessage& message) = 0;
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="target">The client connection to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
virtual void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target) = 0;
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="targets">The connections list to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
virtual void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets) = 0;
// TODO: Stats API
// TODO: Simulation API
};

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Config.h"
/// <summary>
/// The low-level network channel type for message sending.
/// </summary>
API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkChannelType
{
/// <summary>
/// Invalid channel type.
/// </summary>
None = 0,
/// <summary>
/// Unreliable channel type.
/// Messages can be lost or arrive out-of-order.
/// </summary>
Unreliable,
/// <summary>
/// Unreliable-ordered channel type.
/// Messages can be lost but always arrive in order.
/// </summary>
UnreliableOrdered,
/// <summary>
/// Reliable channel type.
/// Messages won't be lost but may arrive out-of-order.
/// </summary>
Reliable,
/// <summary>
/// Reliable-ordered channel type.
/// Messages won't be lost and always arrive in order.
/// </summary>
ReliableOrdered
};

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Platform/Network.h"
/// <summary>
/// Network driver implementations enum.
/// </summary>
API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkDriverType
{
/// <summary>
/// Invalid network driver implementation.
/// </summary>
Undefined = 0,
/// <summary>
/// ENet library based network driver implementation.
/// </summary>
ENet
};
/// <summary>
/// Low-level network configuration structure. Provides settings for the network driver and all internal components.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkConfig
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkConfig);
public:
/// <summary>
/// The network driver that will be used to create the peer.
/// To allow two peers to connect, they must use the same host.
/// </summary>
API_FIELD()
NetworkDriverType NetworkDriverType = NetworkDriverType::ENet;
// TODO: Expose INetworkDriver as a ref not enum, when C++/C# interfaces are done.
public:
/// <summary>
/// The upper limit on how many peers can join when we're listening.
/// </summary>
API_FIELD()
uint16 ConnectionsLimit = 32;
/// <summary>
/// Address used to connect to or listen at.
/// </summary>
/// <remarks>Set it to "any" when you want to listen at all available addresses.</remarks>
/// <remarks>Only IPv4 is supported.</remarks>
API_FIELD()
String Address = String("127.0.0.1");
/// <summary>
/// The port to connect to or listen at.
/// </summary>
API_FIELD()
uint16 Port = 7777;
/// <summary>
/// The size of a message buffer in bytes.
/// Should be lower than the MTU (maximal transmission unit) - typically 1500 bytes.
/// </summary>
API_FIELD()
uint16 MessageSize = 1500;
/// <summary>
/// The amount of pooled messages that can be used at once (receiving and sending!).
/// </summary>
/// <remarks>
/// Creating more messages than this limit will result in a crash!
/// This should be tweaked manually to fit the needs (adjusting this value will increase/decrease memory usage)!
/// </remarks>
API_FIELD()
uint16 MessagePoolSize = 2048;
};

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Network connection structure - used to identify connected peers when we're listening.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkConnection
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkConnection);
public:
/// <summary>
/// The identifier of the connection.
/// </summary>
/// <remarks>Used by network driver implementations.</remarks>
API_FIELD()
uint32 ConnectionId;
};

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "NetworkConnection.h"
#include "NetworkMessage.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Network event type enum contains all possible events that can be returned by PopEvent function.
/// </summary>
API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkEventType
{
/// <summary>
/// Invalid network event type.
/// </summary>
Undefined = 0,
/// <summary>
/// Event "connected" - client connected to our server or we've connected to the server.
/// </summary>
Connected,
/// <summary>
/// Event "disconnected" - client disconnected from our server or we've been kicked from the server.
/// </summary>
Disconnected,
/// <summary>
/// Event "disconnected" - client got a timeout from our server or we've list the connection to the server.
/// </summary>
Timeout,
/// <summary>
/// Event "message" - message received from some client or the server.
/// </summary>
Message
};
/// <summary>
/// Network event structure that wraps all data needed to identify and process it.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkEvent
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkEvent);
public:
/// <summary>
/// The type of the received event.
/// </summary>
API_FIELD();
NetworkEventType EventType;
/// <summary>
/// The message when this event is an "message" event - not valid in any other cases.
/// If this is an message-event, make sure to return the message using RecycleMessage function of the peer after processing it!
/// </summary>
API_FIELD();
NetworkMessage Message;
/// <summary>
/// The connected of the client that has sent message, connected, disconnected or got a timeout.
/// </summary>
/// <remarks>Only valid when event has been received on server-peer.</remarks>
API_FIELD();
NetworkConnection Sender;
};

View File

@@ -0,0 +1,365 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Text;
using FlaxEngine.Assertions;
namespace FlaxEngine.Networking
{
public unsafe partial struct NetworkMessage
{
/// <summary>
/// Writes raw bytes into the message.
/// </summary>
/// <param name="bytes">The bytes that will be written.</param>
/// <param name="length">The amount of bytes to write from the bytes pointer.</param>
public void WriteBytes(byte* bytes, int length)
{
Assert.IsTrue(Position + length <= BufferSize, $"Could not write data of length {length} into message with id={MessageId}! Current write position={Position}");
Utils.MemoryCopy(new IntPtr(bytes), new IntPtr(Buffer + Position), (ulong)length);
Position += (uint)length;
Length = Position;
}
/// <summary>
/// Reads raw bytes from the message into the given byte array.
/// </summary>
/// <param name="buffer">
/// The buffer pointer that will be used to store the bytes.
/// Should be of the same length as length or longer.
/// </param>
/// <param name="length">The minimal amount of bytes that the buffer contains.</param>
public void ReadBytes(byte* buffer, int length)
{
Assert.IsTrue(Position + length <= Length, $"Could not read data of length {length} from message with id={MessageId} and size of {Length}B! Current read position={Position}");
Utils.MemoryCopy(new IntPtr(Buffer + Position), new IntPtr(buffer), (ulong)length);
Position += (uint)length;
}
/// <summary>
/// Writes raw bytes into the message.
/// </summary>
/// <param name="bytes">The bytes that will be written.</param>
/// <param name="length">The amount of bytes to write from the bytes array.</param>
public void WriteBytes(byte[] bytes, int length)
{
fixed (byte* bytesPtr = bytes)
{
WriteBytes(bytesPtr, length);
}
}
/// <summary>
/// Reads raw bytes from the message into the given byte array.
/// </summary>
/// <param name="buffer">
/// The buffer that will be used to store the bytes.
/// Should be of the same length as length or longer.
/// </param>
/// <param name="length">The minimal amount of bytes that the buffer contains.</param>
public void ReadBytes(byte[] buffer, int length)
{
fixed (byte* bufferPtr = buffer)
{
ReadBytes(bufferPtr, length);
}
}
/// <summary>
/// Writes data of type <see cref="UInt64"/> into the message.
/// </summary>
public void WriteInt64(long value)
{
WriteBytes((byte*)&value, sizeof(long));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt64"/> from the message.
/// </summary>
public long ReadInt64()
{
long value = 0;
ReadBytes((byte*)&value, sizeof(long));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt32"/> into the message.
/// </summary>
public void WriteInt32(int value)
{
WriteBytes((byte*)&value, sizeof(int));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt32"/> from the message.
/// </summary>
public int ReadInt32()
{
int value = 0;
ReadBytes((byte*)&value, sizeof(int));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt16"/> into the message.
/// </summary>
public void WriteInt16(short value)
{
WriteBytes((byte*)&value, sizeof(short));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt16"/> from the message.
/// </summary>
public short ReadInt16()
{
short value = 0;
ReadBytes((byte*)&value, sizeof(short));
return value;
}
/// <summary>
/// Writes data of type <see cref="SByte"/> into the message.
/// </summary>
public void WriteSByte(sbyte value)
{
WriteBytes((byte*)&value, sizeof(sbyte));
}
/// <summary>
/// Reads and returns data of type <see cref="SByte"/> from the message.
/// </summary>
public sbyte ReadSByte()
{
sbyte value = 0;
ReadBytes((byte*)&value, sizeof(sbyte));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt64"/> into the message.
/// </summary>
public void WriteUInt64(ulong value)
{
WriteBytes((byte*)&value, sizeof(ulong));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt64"/> from the message.
/// </summary>
public ulong ReadUInt64()
{
ulong value = 0;
ReadBytes((byte*)&value, sizeof(ulong));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt32"/> into the message.
/// </summary>
public void WriteUInt32(uint value)
{
WriteBytes((byte*)&value, sizeof(uint));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt32"/> from the message.
/// </summary>
public uint ReadUInt32()
{
uint value = 0;
ReadBytes((byte*)&value, sizeof(uint));
return value;
}
/// <summary>
/// Writes data of type <see cref="UInt16"/> into the message.
/// </summary>
public void WriteUInt16(ushort value)
{
WriteBytes((byte*)&value, sizeof(ushort));
}
/// <summary>
/// Reads and returns data of type <see cref="UInt16"/> from the message.
/// </summary>
public ushort ReadUInt16()
{
ushort value = 0;
ReadBytes((byte*)&value, sizeof(ushort));
return value;
}
/// <summary>
/// Writes data of type <see cref="Byte"/> into the message.
/// </summary>
public void WriteByte(byte value)
{
WriteBytes(&value, sizeof(byte));
}
/// <summary>
/// Reads and returns data of type <see cref="Byte"/> from the message.
/// </summary>
public byte ReadByte()
{
byte value = 0;
ReadBytes(&value, sizeof(byte));
return value;
}
/// <summary>
/// Writes data of type <see cref="Single"/> into the message.
/// </summary>
public void WriteSingle(float value)
{
WriteBytes((byte*)&value, sizeof(float));
}
/// <summary>
/// Reads and returns data of type <see cref="Single"/> from the message.
/// </summary>
public float ReadSingle()
{
float value = 0.0f;
ReadBytes((byte*)&value, sizeof(float));
return value;
}
/// <summary>
/// Writes data of type <see cref="Double"/> into the message.
/// </summary>
public void WriteDouble(double value)
{
WriteBytes((byte*)&value, sizeof(double));
}
/// <summary>
/// Reads and returns data of type <see cref="Double"/> from the message.
/// </summary>
public double ReadDouble()
{
double value = 0.0;
ReadBytes((byte*)&value, sizeof(double));
return value;
}
/// <summary>
/// Writes data of type <see cref="string"/> into the message. UTF-16 encoded.
/// </summary>
public void WriteString(string value)
{
// Note: Make sure that this is consistent with the C++ message API!
var data = Encoding.Unicode.GetBytes(value);
WriteUInt16((ushort)data.Length); // TODO: Use 1-byte length when possible
WriteBytes(data, data.Length * sizeof(ushort));
}
/// <summary>
/// Reads and returns data of type <see cref="string"/> from the message. UTF-16 encoded.
/// </summary>
public string ReadString()
{
// Note: Make sure that this is consistent with the C++ message API!
var stringLength = ReadUInt16(); // In chars
var stringSize = stringLength * sizeof(ushort); // In bytes
var bytes = stackalloc char[stringSize];
ReadBytes((byte*)bytes, stringSize);
return new string(bytes);
}
/// <summary>
/// Writes data of type <see cref="Vector2"/> into the message.
/// </summary>
public void WriteVector2(Vector2 value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
}
/// <summary>
/// Reads and returns data of type <see cref="Vector2"/> from the message.
/// </summary>
public Vector2 ReadVector2()
{
return new Vector2(ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Vector3"/> into the message.
/// </summary>
public void WriteVector3(Vector3 value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
}
/// <summary>
/// Reads and returns data of type <see cref="Vector3"/> from the message.
/// </summary>
public Vector3 ReadVector3()
{
return new Vector3(ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Vector4"/> into the message.
/// </summary>
public void WriteVector4(Vector4 value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type <see cref="Vector4"/> from the message.
/// </summary>
public Vector4 ReadVector4()
{
return new Vector4(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Quaternion"/> into the message.
/// </summary>
public void WriteQuaternion(Quaternion value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type <see cref="Quaternion"/> from the message.
/// </summary>
public Quaternion ReadQuaternion()
{
return new Quaternion(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type <see cref="Boolean"/> into the message.
/// </summary>
public void WriteBoolean(bool value)
{
WriteBytes((byte*)&value, sizeof(bool));
}
/// <summary>
/// Reads and returns data of type <see cref="Boolean"/> from the message.
/// </summary>
public bool ReadBoolean()
{
bool value = default;
ReadBytes((byte*)&value, sizeof(bool));
return value;
}
}
}

View File

@@ -0,0 +1,213 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Math/Vector2.h"
#include "Engine/Core/Math/Vector3.h"
#include "Engine/Core/Math/Vector4.h"
#include "Engine/Core/Math/Quaternion.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Scripting/ScriptingType.h"
/// <summary>
/// Network message structure. Provides raw data writing and reading to the message buffer.
/// </summary>
API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkMessage
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkMessage);
public:
/// <summary>
/// The raw message buffer.
/// </summary>
API_FIELD()
uint8* Buffer = nullptr;
/// <summary>
/// The unique, internal message identifier.
/// </summary>
API_FIELD()
uint32 MessageId = 0;
/// <summary>
/// The size in bytes of the buffer that this message has.
/// </summary>
API_FIELD()
uint32 BufferSize = 0;
/// <summary>
/// The length in bytes of this message.
/// </summary>
API_FIELD()
uint32 Length = 0;
/// <summary>
/// The position in bytes in buffer where the next read/write will occur.
/// </summary>
API_FIELD()
uint32 Position = 0;
public:
/// <summary>
/// Initializes default values of the <seealso cref="NetworkMessage"/> structure.
/// </summary>
NetworkMessage() = default;
/// <summary>
/// Initializes values of the <seealso cref="NetworkMessage"/> structure.
/// </summary>
NetworkMessage(uint8* buffer, uint32 messageId, uint32 bufferSize, uint32 length, uint32 position) :
Buffer(buffer), MessageId(messageId), BufferSize(bufferSize), Length(length), Position(position)
{ }
~NetworkMessage() = default;
public:
/// <summary>
/// Writes raw bytes into the message.
/// </summary>
/// <param name="bytes">The bytes that will be written.</param>
/// <param name="numBytes">The amount of bytes to write from the bytes pointer.</param>
FORCE_INLINE void WriteBytes(uint8* bytes, const int numBytes)
{
ASSERT(Position + numBytes < BufferSize);
Platform::MemoryCopy(Buffer + Position, bytes, numBytes);
Position += numBytes;
}
/// <summary>
/// Reads raw bytes from the message into the given byte array.
/// </summary>
/// <param name="bytes">
/// The buffer pointer that will be used to store the bytes.
/// Should be of the same length as length or longer.
/// </param>
/// <param name="numBytes">The minimal amount of bytes that the buffer contains.</param>
FORCE_INLINE void ReadBytes(uint8* bytes, const int numBytes)
{
ASSERT(Position + numBytes < BufferSize);
Platform::MemoryCopy(bytes, Buffer + Position, numBytes);
Position += numBytes;
}
#define DECL_READWRITE(type, name) \
FORCE_INLINE void Write##name(type value) { WriteBytes(reinterpret_cast<uint8*>(&value), sizeof(type)); } \
FORCE_INLINE type Read##name() { type value = 0; ReadBytes(reinterpret_cast<uint8*>(&value), sizeof(type)); return value; }
public:
DECL_READWRITE(int8, Int8)
DECL_READWRITE(uint8, UInt8)
DECL_READWRITE(int16, Int16)
DECL_READWRITE(uint16, UInt16)
DECL_READWRITE(int32, Int32)
DECL_READWRITE(uint32, UInt32)
DECL_READWRITE(int64, Int64)
DECL_READWRITE(uint64, UInt64)
DECL_READWRITE(float, Single)
DECL_READWRITE(double, Double)
DECL_READWRITE(bool, Boolean)
public:
/// <summary>
/// Writes data of type Vector2 into the message.
/// </summary>
FORCE_INLINE void WriteVector2(const Vector2& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
}
/// <summary>
/// Reads and returns data of type Vector2 from the message.
/// </summary>
FORCE_INLINE Vector2 ReadVector2()
{
return Vector2(ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type Vector3 into the message.
/// </summary>
FORCE_INLINE void WriteVector3(const Vector3& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
}
/// <summary>
/// Reads and returns data of type Vector3 from the message.
/// </summary>
FORCE_INLINE Vector3 ReadVector3()
{
return Vector3(ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type Vector4 into the message.
/// </summary>
FORCE_INLINE void WriteVector4(const Vector4& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type Vector4 from the message.
/// </summary>
FORCE_INLINE Vector4 ReadVector4()
{
return Vector4(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
/// <summary>
/// Writes data of type Quaternion into the message.
/// </summary>
FORCE_INLINE void WriteQuaternion(const Quaternion& value)
{
WriteSingle(value.X);
WriteSingle(value.Y);
WriteSingle(value.Z);
WriteSingle(value.W);
}
/// <summary>
/// Reads and returns data of type Quaternion from the message.
/// </summary>
FORCE_INLINE Quaternion ReadQuaternion()
{
return Quaternion(ReadSingle(), ReadSingle(), ReadSingle(), ReadSingle());
}
public:
/// <summary>
/// Writes data of type String into the message. UTF-16 encoded.
/// </summary>
FORCE_INLINE void WriteString(const String& value)
{
WriteUInt16(value.Length()); // TODO: Use 1-byte length when possible
WriteBytes((uint8*)value.Get(), value.Length() * sizeof(wchar_t));
}
/// <summary>
/// Reads and returns data of type String from the message. UTF-16 encoded.
/// </summary>
FORCE_INLINE String ReadString()
{
uint16 length = ReadUInt16();
String value;
value.Resize(length);
ReadBytes((uint8*)value.Get(), length * sizeof(wchar_t));
return value;
}
public:
/// <summary>
/// Returns true if the message is valid for reading or writing.
/// </summary>
bool IsValid() const
{
return Buffer != nullptr && BufferSize > 0;
}
};

View File

@@ -0,0 +1,192 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#include "NetworkPeer.h"
#include "NetworkEvent.h"
#include "Drivers/ENetDriver.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Platform/CPUInfo.h"
namespace
{
Array<NetworkPeer*, HeapAllocation> Peers;
uint32 LastHostId = 0;
}
void NetworkPeer::Initialize(const NetworkConfig& config)
{
Config = config;
ASSERT(NetworkDriver == nullptr);
ASSERT(Config.NetworkDriverType != NetworkDriverType::Undefined);
ASSERT(Config.ConnectionsLimit > 0);
ASSERT(Config.MessageSize > 32); // TODO: Adjust this, not sure what the lowest limit should be.
ASSERT(Config.MessagePoolSize > 128);
// TODO: Dynamic message pool allocation
// Setup messages
CreateMessageBuffers();
MessagePool.Clear();
// Warmup message pool
for (uint32 messageId = Config.MessagePoolSize; messageId > 0; messageId --)
MessagePool.Push(messageId);
// Setup network driver
NetworkDriver = New<ENetDriver>();
NetworkDriver->Initialize(this, Config);
LOG(Info, "NetworkManager initialized using driver = {0}", static_cast<int>(Config.NetworkDriverType));
}
void NetworkPeer::Shutdown()
{
NetworkDriver->Dispose();
Delete(NetworkDriver);
DisposeMessageBuffers();
NetworkDriver = nullptr;
}
void NetworkPeer::CreateMessageBuffers()
{
ASSERT(MessageBuffer == nullptr);
const uint32 pageSize = Platform::GetCPUInfo().PageSize;
// Calculate total size in bytes
const uint64 totalSize = static_cast<uint64>(Config.MessagePoolSize) * Config.MessageSize;
// Calculate the amount of pages that we need
const uint32 numPages = totalSize > pageSize ? Math::CeilToInt(totalSize / static_cast<float>(pageSize)) : 1;
MessageBuffer = static_cast<uint8*>(Platform::AllocatePages(numPages, pageSize));
Platform::MemorySet(MessageBuffer, 0, numPages * pageSize);
}
void NetworkPeer::DisposeMessageBuffers()
{
ASSERT(MessageBuffer != nullptr);
Platform::FreePages(MessageBuffer);
MessageBuffer = nullptr;
}
bool NetworkPeer::Listen()
{
LOG(Info, "NetworkManager starting to listen on address = {0}:{1}", Config.Address, Config.Port);
return NetworkDriver->Listen();
}
bool NetworkPeer::Connect()
{
LOG(Info, "Connecting to {0}:{1}...", Config.Address, Config.Port);
return NetworkDriver->Connect();
}
void NetworkPeer::Disconnect()
{
LOG(Info, "Disconnecting...");
NetworkDriver->Disconnect();
}
void NetworkPeer::Disconnect(const NetworkConnection& connection)
{
LOG(Info, "Disconnecting connection with id = {0}...", connection.ConnectionId);
NetworkDriver->Disconnect(connection);
}
bool NetworkPeer::PopEvent(NetworkEvent& eventRef)
{
return NetworkDriver->PopEvent(&eventRef);
}
NetworkMessage NetworkPeer::CreateMessage()
{
const uint32 messageId = MessagePool.Pop();
uint8* messageBuffer = GetMessageBuffer(messageId);
return NetworkMessage(messageBuffer, messageId, Config.MessageSize, 0, 0);
}
void NetworkPeer::RecycleMessage(const NetworkMessage& message)
{
ASSERT(message.IsValid());
#ifdef BUILD_DEBUG
ASSERT(MessagePool.Contains(message.MessageId) == false);
#endif
// Return the message id
MessagePool.Push(message.MessageId);
}
NetworkMessage NetworkPeer::BeginSendMessage()
{
return CreateMessage();
}
void NetworkPeer::AbortSendMessage(const NetworkMessage& message)
{
ASSERT(message.IsValid());
RecycleMessage(message);
}
bool NetworkPeer::EndSendMessage(const NetworkChannelType channelType, const NetworkMessage& message)
{
ASSERT(message.IsValid());
NetworkDriver->SendMessage(channelType, message);
RecycleMessage(message);
return false;
}
bool NetworkPeer::EndSendMessage(const NetworkChannelType channelType, const NetworkMessage& message, const NetworkConnection& target)
{
ASSERT(message.IsValid());
NetworkDriver->SendMessage(channelType, message, target);
RecycleMessage(message);
return false;
}
bool NetworkPeer::EndSendMessage(const NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection>& targets)
{
ASSERT(message.IsValid());
NetworkDriver->SendMessage(channelType, message, targets);
RecycleMessage(message);
return false;
}
NetworkPeer* NetworkPeer::CreatePeer(const NetworkConfig& config)
{
// Validate the address for listen/connect
NetworkEndPoint endPoint = {};
const bool isValidEndPoint = NetworkBase::CreateEndPoint(config.Address, String("7777"), NetworkIPVersion::IPv4, endPoint, false);
ASSERT(config.Address == String("any") || isValidEndPoint);
// Alloc new host
Peers.Add(New<NetworkPeer>());
NetworkPeer* host = Peers.Last();
host->HostId = LastHostId++;
// Initialize the host
host->Initialize(config);
return host;
}
void NetworkPeer::ShutdownPeer(NetworkPeer* peer)
{
ASSERT(peer->IsValid());
peer->Shutdown();
peer->HostId = -1;
Peers.Remove(peer);
Delete(peer);
}

View File

@@ -0,0 +1,194 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/ScriptingObjectReference.h"
#include "Types.h"
#include "NetworkConfig.h"
#include "Engine/Core/Collections/Array.h"
/// <summary>
/// Low-level network peer class. Provides server-client communication functions, message processing and sending.
/// </summary>
API_CLASS(sealed, NoSpawn, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkPeer final : public PersistentScriptingObject
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(NetworkPeer);
friend class NetworkManager;
public:
int HostId = -1;
NetworkConfig Config;
INetworkDriver* NetworkDriver = nullptr;
uint8* MessageBuffer = nullptr;
Array<uint32, HeapAllocation> MessagePool;
public:
/// <summary>
/// Initializes a new instance of the <see cref="NetworkPeer"/> class.
/// </summary>
NetworkPeer() : PersistentScriptingObject(SpawnParams(Guid::New(), TypeInitializer))
{
}
private:
void Initialize(const NetworkConfig& config);
void Shutdown();
private:
void CreateMessageBuffers();
void DisposeMessageBuffers();
public:
/// <summary>
/// Starts listening for incoming connections.
/// Once this is called, this peer becomes a server.
/// </summary>
/// <returns>True when succeeded.</returns>
API_FUNCTION()
bool Listen();
/// <summary>
/// Starts connection handshake with the end point specified in the <seealso cref="NetworkConfig"/> structure.
/// Once this is called, this peer becomes a client.
/// </summary>
/// <returns>True when succeeded.</returns>
API_FUNCTION()
bool Connect();
/// <summary>
/// Disconnects from the server.
/// </summary>
/// <remarks>Can be used only by the client!</remarks>
API_FUNCTION()
void Disconnect();
/// <summary>
/// Disconnects given connection from the server.
/// </summary>
/// <remarks>Can be used only by the server!</remarks>
API_FUNCTION()
void Disconnect(const NetworkConnection& connection);
/// <summary>
/// Tries to pop an network event from the queue.
/// </summary>
/// <param name="eventRef">The reference to event structure.</param>
/// <returns>True when succeeded and the event can be processed.</returns>
/// <remarks>If this returns message event, make sure to recycle the message using <see cref="RecycleMessage"/> function after processing it!</remarks>
API_FUNCTION()
bool PopEvent(API_PARAM(out) NetworkEvent& eventRef);
/// <summary>
/// Acquires new message from the pool.
/// Cannot acquire more messages than the limit specified in the <seealso cref="NetworkConfig"/> structure.
/// </summary>
/// <returns>The acquired message.</returns>
/// <remarks>Make sure to recycle the message to this peer once it is no longer needed!</remarks>
API_FUNCTION()
NetworkMessage CreateMessage();
/// <summary>
/// Returns given message to the pool.
/// </summary>
/// <remarks>Make sure that this message belongs to the peer and has not been recycled already (debug build checks for this)!</remarks>
API_FUNCTION()
void RecycleMessage(const NetworkMessage& message);
/// <summary>
/// Acquires new message from the pool and setups it for sending.
/// </summary>
/// <returns>The acquired message.</returns>
API_FUNCTION()
NetworkMessage BeginSendMessage();
/// <summary>
/// Aborts given message send. This effectively deinitializes the message and returns it to the pool.
/// </summary>
/// <param name="message">The message.</param>
API_FUNCTION()
void AbortSendMessage(const NetworkMessage& message);
/// <summary>
/// Sends given message over specified channel to the server.
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <remarks>Can be used only by the client!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
API_FUNCTION()
bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message);
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="target">The client connection to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
API_FUNCTION()
bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const NetworkConnection& target);
/// <summary>
/// Sends given message over specified channel to the given client connection (target).
/// </summary>
/// <param name="channelType">The channel to send the message over.</param>
/// <param name="message">The message.</param>
/// <param name="targets">The connections list to send the message to.</param>
/// <remarks>Can be used only by the server!</remarks>
/// <remarks>
/// Do not recycle the message after calling this.
/// This function automatically recycles the message.
/// </remarks>
API_FUNCTION()
bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array<NetworkConnection, HeapAllocation>& targets);
public:
/// <summary>
/// Creates new peer using given configuration.
/// </summary>
/// <param name="config">The configuration to create and setup new peer.</param>
/// <returns>The peer.</returns>
/// <remarks>Peer should be destroyed using <see cref="ShutdownPeer"/> once it is no longer in use.</remarks>
API_FUNCTION()
static NetworkPeer* CreatePeer(const NetworkConfig& config);
/// <summary>
/// Shutdowns and destroys given peer.
/// </summary>
/// <param name="peer">The peer to destroy.</param>
API_FUNCTION()
static void ShutdownPeer(NetworkPeer* peer);
public:
bool IsValid() const
{
return NetworkDriver != nullptr && HostId >= 0;
}
uint8* GetMessageBuffer(const uint32 messageId) const
{
// Calculate and return the buffer slice using previously calculated slice.
return MessageBuffer + Config.MessageSize * messageId;
}
public:
FORCE_INLINE bool operator==(const NetworkPeer& other) const
{
return HostId == other.HostId;
}
FORCE_INLINE bool operator!=(const NetworkPeer& other) const
{
return HostId != other.HostId;
}
};

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// Networking module.
/// </summary>
public class Networking : EngineModule
{
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.PublicDefinitions.Add("COMPILE_WITH_NETWORKING");
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
enum class NetworkChannelType;
enum class NetworkEventType;
class INetworkDriver;
class NetworkPeer;
struct NetworkEvent;
struct NetworkConnection;
struct NetworkMessage;
struct NetworkConfig;

22
Source/ThirdParty/enet/enet License.txt vendored Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2002-2016 Lee Salzman
Copyright (c) 2017-2021 Vladyslav Hrytsenko, Dominik Madarász
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

32
Source/ThirdParty/enet/enet.Build.cs vendored Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System.IO;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// https://github.com/zpl-c/enet
/// </summary>
public class ENet : DepsModule
{
/// <inheritdoc />
public override void Init()
{
base.Init();
LicenseType = LicenseTypes.MIT;
LicenseFilePath = "enet License.txt";
// Merge third-party modules into engine binary
BinaryModuleName = "FlaxEngine";
}
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
var depsRoot = options.DepsFolder;
options.PublicDefinitions.Add("ENET");
}
}

6086
Source/ThirdParty/enet/enet.h vendored Normal file

File diff suppressed because it is too large Load Diff