mirror of
https://github.com/SineVector241/VoiceCraft-MCBE_Proximity_Chat.git
synced 2024-12-14 02:37:54 +00:00
261 lines
10 KiB
C#
261 lines
10 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Numerics;
|
|
using VoiceCraft.Core;
|
|
using VoiceCraft.Core.Packets;
|
|
using VoiceCraft.Core.Packets.CustomClient;
|
|
|
|
namespace VoiceCraft.Network.Sockets
|
|
{
|
|
public class CustomClient : Disposable
|
|
{
|
|
public const int SIO_UDP_CONNRESET = -1744830452;
|
|
|
|
#region Variables
|
|
public int Timeout { get; set; } = 8000;
|
|
public CustomClientSocketState State { get; private set; }
|
|
public IPEndPoint RemoteEndpoint { get; private set; } = new IPEndPoint(IPAddress.Any, 0);
|
|
public Socket Socket { get; set; } = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
|
|
|
private PacketRegistry PacketRegistry { get; set; } = new PacketRegistry();
|
|
private CancellationTokenSource CTS { get; set; } = new CancellationTokenSource();
|
|
private SocketAddress? RemoteAddress { get; set; }
|
|
private Task? ActivityChecker { get; set; }
|
|
private long LastActive { get; set; }
|
|
#endregion
|
|
|
|
#region Debug
|
|
public bool LogExceptions { get; set; } = false;
|
|
public bool LogInbound { get; set; } = false;
|
|
public bool LogOutbound { get; set; } = false;
|
|
public List<CustomClientTypes> InboundFilter { get; set; } = [];
|
|
public List<CustomClientTypes> OutboundFilter { get; set; } = [];
|
|
#endregion
|
|
|
|
#region Delegates
|
|
public delegate void Started();
|
|
public delegate void Stopped(string? reason = null);
|
|
public delegate void Connected(string name);
|
|
public delegate void Disconnected();
|
|
public delegate void Updated(Vector3 position, float rotation, float caveDensity, bool isUnderwater, string dimensionId, string levelId, string serverId);
|
|
public delegate void PacketData<T>(T data, SocketAddress address);
|
|
|
|
//Error and Debug Events
|
|
public delegate void OutboundPacket(CustomClientPacket packet);
|
|
public delegate void InboundPacket(CustomClientPacket packet);
|
|
public delegate void ExceptionError(Exception error);
|
|
public delegate void Failed(Exception ex);
|
|
#endregion
|
|
|
|
#region Events
|
|
public event Started? OnStarted;
|
|
public event Stopped? OnStopped;
|
|
public event Connected? OnConnected;
|
|
public event Disconnected? OnDisconnected;
|
|
public event Updated? OnUpdated;
|
|
|
|
public event PacketData<Login>? OnLoginReceived;
|
|
public event PacketData<Logout>? OnLogoutReceived;
|
|
public event PacketData<Accept>? OnAcceptReceived;
|
|
public event PacketData<Deny>? OnDenyReceived;
|
|
public event PacketData<Update>? OnUpdateReceived;
|
|
|
|
//Error and Debug Events
|
|
public event OutboundPacket? OnOutboundPacket;
|
|
public event InboundPacket? OnInboundPacket;
|
|
public event ExceptionError? OnExceptionError;
|
|
public event Failed? OnFailed;
|
|
#endregion
|
|
|
|
public CustomClient()
|
|
{
|
|
PacketRegistry.RegisterPacket((byte)CustomClientTypes.Login, typeof(Login));
|
|
PacketRegistry.RegisterPacket((byte)CustomClientTypes.Logout, typeof(Logout));
|
|
PacketRegistry.RegisterPacket((byte)CustomClientTypes.Accept, typeof(Accept));
|
|
PacketRegistry.RegisterPacket((byte)CustomClientTypes.Deny, typeof(Deny));
|
|
PacketRegistry.RegisterPacket((byte)CustomClientTypes.Update, typeof(Update));
|
|
}
|
|
|
|
#region Methods
|
|
public async Task HostAsync(int Port)
|
|
{
|
|
ObjectDisposedException.ThrowIf(IsDisposed, this);
|
|
|
|
if (State != CustomClientSocketState.Stopped) throw new Exception("You must stop hosting before starting a host!");
|
|
|
|
CTS.Dispose(); //Prevent memory leak for startup.
|
|
//Socket.IOControl((IOControlCode)SIO_UDP_CONNRESET, [0, 0, 0, 0], null); //I fucking hate this
|
|
|
|
State = CustomClientSocketState.Starting;
|
|
CTS = new CancellationTokenSource();
|
|
|
|
OnLoginReceived += LoginReceived;
|
|
OnLogoutReceived += LogoutReceived;
|
|
OnUpdateReceived += UpdateReceived;
|
|
|
|
try
|
|
{
|
|
RemoteEndpoint = new IPEndPoint(IPAddress.Any, Port);
|
|
Socket.Bind(RemoteEndpoint);
|
|
ActivityChecker = Task.Run(CheckerLoop);
|
|
State = CustomClientSocketState.Started;
|
|
OnStarted?.Invoke();
|
|
await ReceiveAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnFailed?.Invoke(ex);
|
|
await StopAsync(ex.Message);
|
|
}
|
|
}
|
|
|
|
public async Task StopAsync(string? reason = null)
|
|
{
|
|
ObjectDisposedException.ThrowIf(IsDisposed && State == CustomClientSocketState.Stopped, this);
|
|
if (State == CustomClientSocketState.Stopped || State == CustomClientSocketState.Stopping) return;
|
|
State = CustomClientSocketState.Stopping;
|
|
|
|
if(RemoteAddress != null)
|
|
await SocketSendToAsync(new Logout(), RemoteAddress);
|
|
|
|
CTS.Cancel();
|
|
CTS.Dispose();
|
|
Socket.Close();
|
|
Socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
|
ActivityChecker = null;
|
|
State = CustomClientSocketState.Stopped;
|
|
OnStopped?.Invoke(reason);
|
|
}
|
|
|
|
private async Task SocketSendToAsync(CustomClientPacket packet, SocketAddress address)
|
|
{
|
|
var buffer = new List<byte>();
|
|
packet.WritePacket(ref buffer);
|
|
await Socket.SendToAsync(buffer.ToArray(),SocketFlags.None, address, CTS.Token);
|
|
|
|
if (LogOutbound && (OutboundFilter.Count == 0 || OutboundFilter.Contains((CustomClientTypes)packet.PacketId)))
|
|
OnOutboundPacket?.Invoke(packet);
|
|
}
|
|
|
|
private async Task ReceiveAsync()
|
|
{
|
|
byte[] buffer = GC.AllocateArray<byte>(length: 65527, pinned: true);
|
|
Memory<byte> bufferMem = buffer.AsMemory();
|
|
var receivedAddress = new SocketAddress(Socket.AddressFamily);
|
|
|
|
while (!CTS.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var receivedBytes = await Socket.ReceiveFromAsync(bufferMem, SocketFlags.None, receivedAddress, CTS.Token);
|
|
var packet = PacketRegistry.GetCustomPacketFromDataStream(bufferMem.ToArray());
|
|
|
|
if (LogInbound && (InboundFilter.Count == 0 || InboundFilter.Contains((CustomClientTypes)packet.PacketId)))
|
|
OnInboundPacket?.Invoke(packet);
|
|
|
|
HandlePacketReceived(receivedAddress, packet);
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
await StopAsync(ex.Message);
|
|
return;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (LogExceptions)
|
|
OnExceptionError?.Invoke(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandlePacketReceived(SocketAddress address, CustomClientPacket packet)
|
|
{
|
|
switch ((CustomClientTypes)packet.PacketId)
|
|
{
|
|
case CustomClientTypes.Login: OnLoginReceived?.Invoke((Login)packet, address); break;
|
|
case CustomClientTypes.Logout: OnLogoutReceived?.Invoke((Logout)packet, address); break;
|
|
case CustomClientTypes.Accept: OnAcceptReceived?.Invoke((Accept)packet, address); break;
|
|
case CustomClientTypes.Deny: OnDenyReceived?.Invoke((Deny)packet, address); break;
|
|
case CustomClientTypes.Update: OnUpdateReceived?.Invoke((Update)packet, address); break;
|
|
}
|
|
}
|
|
|
|
private async Task CheckerLoop()
|
|
{
|
|
while (!CTS.IsCancellationRequested)
|
|
{
|
|
var dist = Environment.TickCount64 - LastActive;
|
|
if (RemoteAddress != null && dist > Timeout)
|
|
{
|
|
RemoteAddress = null;
|
|
OnDisconnected?.Invoke();
|
|
break;
|
|
}
|
|
|
|
await Task.Delay(1).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
if (State == CustomClientSocketState.Started || State == CustomClientSocketState.Starting)
|
|
StopAsync().Wait();
|
|
|
|
Socket.Dispose();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Event Methods
|
|
private void LoginReceived(Login data, SocketAddress address)
|
|
{
|
|
if(RemoteAddress != null && !RemoteAddress.Equals(address))
|
|
{
|
|
SocketSendToAsync(new Deny() { Reason = "Client is already connected to another instance!" }, address).Wait();
|
|
return;
|
|
}
|
|
|
|
LastActive = Environment.TickCount64;
|
|
RemoteAddress = new SocketAddress(address.Family, address.Size);
|
|
SocketSendToAsync(new Accept(), address).Wait();
|
|
OnConnected?.Invoke(data.Name);
|
|
}
|
|
|
|
private void LogoutReceived(Logout data, SocketAddress address)
|
|
{
|
|
if(RemoteAddress != null && RemoteAddress.Equals(address))
|
|
{
|
|
RemoteAddress = null;
|
|
SocketSendToAsync(new Accept(), address).Wait();
|
|
OnDisconnected?.Invoke();
|
|
}
|
|
return;
|
|
}
|
|
|
|
private void UpdateReceived(Update data, SocketAddress address)
|
|
{
|
|
if (RemoteAddress != null && RemoteAddress.Equals(address))
|
|
{
|
|
LastActive = Environment.TickCount64;
|
|
SocketSendToAsync(new Accept(), address).Wait();
|
|
OnUpdated?.Invoke(data.Position, data.Rotation, data.CaveDensity, data.IsUnderwater, data.DimensionId, data.LevelId, data.ServerId);
|
|
}
|
|
return;
|
|
}
|
|
#endregion
|
|
}
|
|
public enum CustomClientSocketState
|
|
{
|
|
Stopped,
|
|
Stopping,
|
|
Starting,
|
|
Started
|
|
}
|
|
}
|