diff --git a/Components/MineSharp.Protocol/MinecraftClient.cs b/Components/MineSharp.Protocol/MinecraftClient.cs index 9b5bf8ea..1bff8bd5 100644 --- a/Components/MineSharp.Protocol/MinecraftClient.cs +++ b/Components/MineSharp.Protocol/MinecraftClient.cs @@ -7,7 +7,6 @@ using MineSharp.Auth; using MineSharp.ChatComponent; using MineSharp.ChatComponent.Components; -using MineSharp.Core; using MineSharp.Core.Common.Protocol; using MineSharp.Core.Concurrency; using MineSharp.Core.Events; @@ -19,13 +18,12 @@ using MineSharp.Protocol.Packets; using MineSharp.Protocol.Packets.Clientbound.Status; using MineSharp.Protocol.Packets.Handlers; -using MineSharp.Protocol.Packets.Serverbound.Configuration; using MineSharp.Protocol.Packets.Serverbound.Status; +using MineSharp.Protocol.Registrations; using Newtonsoft.Json.Linq; using NLog; - -using PlayClientInformationPacket = MineSharp.Protocol.Packets.Serverbound.Play.ClientInformationPacket; using ConfigurationClientInformationPacket = MineSharp.Protocol.Packets.Serverbound.Configuration.ClientInformationPacket; +using PlayClientInformationPacket = MineSharp.Protocol.Packets.Serverbound.Play.ClientInformationPacket; namespace MineSharp.Protocol; @@ -109,7 +107,7 @@ public sealed class MinecraftClient : IAsyncDisposable, IDisposable private Task? streamLoop; private int onConnectionLostFired; - private readonly ConcurrentDictionary> packetHandlers; + private readonly ConcurrentDictionary> packetHandlers; private readonly ConcurrentDictionary> packetWaiters; private readonly ConcurrentHashSet packetReceivers; private GameStatePacketHandler gameStatePacketHandler; @@ -161,7 +159,7 @@ public MinecraftClient( packetHandlers = new(); packetWaiters = new(); packetReceivers = new(); - GameJoinedTcs = new(); + GameJoinedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); bundledPackets = null; tcpTcpFactory = tcpFactory; ip = IpHelper.ResolveHostname(hostnameOrIp, ref port); @@ -266,7 +264,7 @@ public async Task Connect(GameState nextState) /// A task that resolves once the packet was actually sent. public async Task SendPacket(IPacket packet, CancellationToken cancellation = default) { - var sendingTask = new PacketSendTask(packet, cancellation, new()); + var sendingTask = new PacketSendTask(packet, cancellation, new(TaskCreationOptions.RunContinuationsAsynchronously)); try { if (!await packetQueue.SendAsync(sendingTask, cancellation)) @@ -326,18 +324,107 @@ private async Task DisconnectInternal(Chat? reason = null) await OnConnectionLost.Dispatch(this); } + /// + /// Represents a registration for a packet handler that will be called whenever a packet of type is received. + /// This registration can be used to unregister the handler. + /// + /// The type of the packet. + public sealed class OnPacketRegistration : AbstractPacketReceiveRegistration + where T : IPacket + { + internal OnPacketRegistration(MinecraftClient client, AsyncPacketHandler handler) + : base(client, handler) + { + } + + /// + protected override void Unregister() + { + var key = T.StaticType; + if (Client.packetHandlers.TryGetValue(key, out var handlers)) + { + handlers.TryRemove(Handler); + } + } + } + /// /// Registers an that will be called whenever an packet of type /// is received /// /// A delegate that will be called when a packet of type T is received /// The type of the packet - public void On(AsyncPacketHandler handler) where T : IPacket + /// A registration object that can be used to unregister the handler. + public OnPacketRegistration? On(AsyncPacketHandler handler) where T : IPacket { var key = T.StaticType; + AsyncPacketHandler rawHandler = packet => handler((T)packet); + var added = packetHandlers.GetOrAdd(key, _ => new ConcurrentHashSet()) + .Add(rawHandler); + return added ? new(this, rawHandler) : null; + } - packetHandlers.GetOrAdd(key, _ => new ConcurrentBag()) - .Add(p => handler((T)p)); + /// + /// Waits until a packet of the specified type is received and matches the given condition. + /// + /// The type of the packet. + /// A function that evaluates the packet and returns true if the condition is met. + /// A token to cancel the wait for the matching packet. + /// A task that completes once a packet matching the condition is received. + public Task WaitForPacketWhere(Func> condition, CancellationToken cancellationToken = default) + where T : IPacket + { + // linked token is required to cancel the task when the client is disconnected + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, CancellationToken); + var token = cts.Token; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + async Task PacketHandler(T packet) + { + try + { + if (tcs.Task.IsCompleted) + { + return; + } + if (await condition(packet).WaitAsync(token)) + { + tcs.TrySetResult(); + } + } + catch (OperationCanceledException e) + { + tcs.TrySetCanceled(e.CancellationToken); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + } + var packetRegistration = On(PacketHandler); + if (packetRegistration == null) + { + // TODO: Can this occur? + cts.Dispose(); + throw new InvalidOperationException("Could not register packet handler"); + } + return tcs.Task.ContinueWith(_ => + { + packetRegistration.Dispose(); + cts.Dispose(); + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// Waits until a packet of the specified type is received and matches the given condition. + /// + /// The type of the packet. + /// A function that evaluates the packet and returns true if the condition is met. + /// A token to cancel the wait for the matching packet. + /// A task that completes once a packet matching the condition is received. + public Task WaitForPacketWhere(Func condition, CancellationToken cancellationToken = default) + where T : IPacket + { + return WaitForPacketWhere(packet => Task.FromResult(condition(packet)), cancellationToken); } /// @@ -348,30 +435,25 @@ public void On(AsyncPacketHandler handler) where T : IPacket public Task WaitForPacket() where T : IPacket { var packetType = T.StaticType; - var tcs = packetWaiters.GetOrAdd(packetType, _ => new TaskCompletionSource()); + var tcs = packetWaiters.GetOrAdd(packetType, _ => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); return tcs.Task.ContinueWith(prev => (T)prev.Result); } - public sealed class OnPacketReceivedRegistration : IDisposable + /// + /// Represents a registration for a packet handler that will be called whenever any packet is received. + /// This registration can be used to unregister the handler. + /// + public sealed class OnPacketReceivedRegistration : AbstractPacketReceiveRegistration { - private readonly MinecraftClient client; - private readonly AsyncPacketHandler handler; - public bool Disposed { get; private set; } - internal OnPacketReceivedRegistration(MinecraftClient client, AsyncPacketHandler handler) + : base(client, handler) { - this.client = client; - this.handler = handler; } - public void Dispose() + /// + protected override void Unregister() { - if (Disposed) - { - return; - } - client.packetReceivers.TryRemove(handler); - Disposed = true; + Client.packetReceivers.TryRemove(Handler); } } @@ -382,6 +464,7 @@ public void Dispose() /// Use this for debugging purposes only. /// /// A delegate that will be called when a packet is received. + /// A registration object that can be used to unregister the handler. public OnPacketReceivedRegistration? OnPacketReceived(AsyncPacketHandler handler) { var added = packetReceivers.Add(handler); @@ -397,10 +480,11 @@ public Task WaitForGame() return GameJoinedTcs.Task; } - internal Task SendClientInformationPacket() + internal Task SendClientInformationPacket(GameState gameState) { - IPacket packet = Data.Version.Protocol >= ProtocolVersion.V_1_20_3 - ? new ConfigurationClientInformationPacket( + IPacket packet = gameState switch + { + GameState.Configuration => new ConfigurationClientInformationPacket( Settings.Locale, Settings.ViewDistance, Settings.ChatMode, @@ -408,8 +492,8 @@ internal Task SendClientInformationPacket() Settings.DisplayedSkinParts, Settings.MainHand, Settings.EnableTextFiltering, - Settings.AllowServerListings) - : new PlayClientInformationPacket( + Settings.AllowServerListings), + GameState.Play => new PlayClientInformationPacket( Settings.Locale, Settings.ViewDistance, Settings.ChatMode, @@ -417,9 +501,9 @@ internal Task SendClientInformationPacket() Settings.DisplayedSkinParts, Settings.MainHand, Settings.EnableTextFiltering, - Settings.AllowServerListings - ); - + Settings.AllowServerListings), + _ => throw new NotImplementedException(), + }; return SendPacket(packet); } @@ -551,21 +635,24 @@ private async Task SendPackets() try { DispatchPacket(task.Packet); - // TrySetResult must be run from a different task to prevent blocking the stream loop - // because the task continuation will be executed inline and might block or cause a deadlock - _ = Task.Run(task.Task.TrySetResult); + task.Task.TrySetResult(); } - catch (SocketException e) + catch (OperationCanceledException e) { - Logger.Error(e, "Encountered exception while dispatching packet {PacketType}", task.Packet.Type); - task.Task.TrySetException(e); - // break the loop to prevent further packets from being sent - // because the connection is probably dead + task.Task.TrySetCanceled(e.CancellationToken); + // we should stop. So we do by rethrowing the exception throw; } catch (Exception e) { + Logger.Error(e, "Encountered exception while dispatching packet {PacketType}", task.Packet.Type); task.Task.TrySetException(e); + if (e is SocketException) + { + // break the loop to prevent further packets from being sent + // because the connection is probably dead + throw; + } } } } @@ -656,7 +743,7 @@ private async Task HandleIncomingPacket(PacketType packetType, PacketBuffer buff return; } - var packet = (IPacket?) await ParsePacket(factory, packetType, buffer); + var packet = (IPacket?)await ParsePacket(factory, packetType, buffer); if (packet == null) { @@ -739,35 +826,34 @@ public static async Task RequestServerStatus( throw new MineSharpHostException("failed to connect to server"); } - var responseTimeoutCts = new CancellationTokenSource(); + using var responseTimeoutCts = new CancellationTokenSource(); var responseTimeoutCancellationToken = responseTimeoutCts.Token; - var taskCompletionSource = new TaskCompletionSource(); - client.On(async packet => - { - var json = packet.Response; - var response = ServerStatus.FromJToken(JToken.Parse(json), client.Data); - taskCompletionSource.TrySetResult(response); - - // the server closes the connection - // after sending the StatusResponsePacket - // so just dispose the client (no point in disconnecting) - await client.DisposeAsync(); - }); + var statusResponsePacketTask = client.WaitForPacket(); await client.SendPacket(new StatusRequestPacket(), responseTimeoutCancellationToken); await client.SendPacket(new PingRequestPacket(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()), responseTimeoutCancellationToken); - responseTimeoutCancellationToken.Register( - () => - { - taskCompletionSource.TrySetCanceled(responseTimeoutCancellationToken); - responseTimeoutCts.Dispose(); - }); - responseTimeoutCts.CancelAfter(responseTimeout); - return await taskCompletionSource.Task; + var statusResponsePacket = await statusResponsePacketTask.WaitAsync(responseTimeoutCancellationToken); + var json = statusResponsePacket.Response; + var response = ServerStatus.FromJToken(JToken.Parse(json), client.Data); + + // the server closes the connection + // after sending the StatusResponsePacket and PingResponsePacket + // so just dispose the client (no point in disconnecting) + try + { + await client.DisposeAsync(); + } + catch (Exception) + { + // ignore all errors + // in most cases the exception is an OperationCanceledException because the connection was terminated + } + + return response; } /// diff --git a/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs index bc39a5db..ad030579 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs @@ -22,7 +22,7 @@ public ConfigurationPacketHandler(MinecraftClient client, MinecraftData data) public override Task StateEntered() { - return client.SendClientInformationPacket(); + return client.SendClientInformationPacket(GameState); } public override Task HandleIncoming(IPacket packet) diff --git a/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs index efd98c69..ad4af480 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs @@ -110,8 +110,8 @@ private async Task HandleEncryptionRequest(EncryptionRequestPacket packet) response = new(sharedSecret, encVerToken, null); } - _ = client.SendPacket(response) - .ContinueWith(_ => client.EnableEncryption(aes.Key)); + await client.SendPacket(response); + client.EnableEncryption(aes.Key); } private Task HandleSetCompression(SetCompressionPacket packet) diff --git a/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs index 3d8811c0..979ff1e5 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs @@ -24,7 +24,7 @@ public override async Task StateEntered() { if (data.Version.Protocol <= ProtocolVersion.V_1_20) { - await client.SendClientInformationPacket(); + await client.SendClientInformationPacket(GameState); } client.GameJoinedTcs.SetResult(); } diff --git a/Components/MineSharp.Protocol/Registrations/AbstractDisposableRegistration.cs b/Components/MineSharp.Protocol/Registrations/AbstractDisposableRegistration.cs new file mode 100644 index 00000000..f6f5f946 --- /dev/null +++ b/Components/MineSharp.Protocol/Registrations/AbstractDisposableRegistration.cs @@ -0,0 +1,40 @@ +namespace MineSharp.Protocol.Registrations; + +/// +/// Abstract class a disposable registration. +/// +public abstract class AbstractDisposableRegistration : IDisposable +{ + private int disposedValue; + /// + /// Whether this registration is disposed and the handler is unregistered. + /// + public bool Disposed => disposedValue != 0; + + /// + /// Initializes a new instance of the class. + /// + protected AbstractDisposableRegistration() + { + disposedValue = 0; + } + + /// + /// This method unregisters the the registered object. + /// Must be overridden by subclasses. + /// + /// This method is called when the registration is disposed. + /// + protected abstract void Unregister(); + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref disposedValue, 1) != 0) + { + // Already disposed + return; + } + Unregister(); + } +} diff --git a/Components/MineSharp.Protocol/Registrations/AbstractPacketReceiveRegistration.cs b/Components/MineSharp.Protocol/Registrations/AbstractPacketReceiveRegistration.cs new file mode 100644 index 00000000..4975549e --- /dev/null +++ b/Components/MineSharp.Protocol/Registrations/AbstractPacketReceiveRegistration.cs @@ -0,0 +1,18 @@ +using static MineSharp.Protocol.MinecraftClient; + +namespace MineSharp.Protocol.Registrations; + +/// +/// Abstract class for packet receive registration. +/// +public abstract class AbstractPacketReceiveRegistration : AbstractDisposableRegistration +{ + protected readonly MinecraftClient Client; + protected readonly AsyncPacketHandler Handler; + + protected AbstractPacketReceiveRegistration(MinecraftClient client, AsyncPacketHandler handler) + { + Client = client; + Handler = handler; + } +} diff --git a/Components/MineSharp.World/AbstractWorld.cs b/Components/MineSharp.World/AbstractWorld.cs index 1e84690d..3c47db7a 100644 --- a/Components/MineSharp.World/AbstractWorld.cs +++ b/Components/MineSharp.World/AbstractWorld.cs @@ -307,7 +307,7 @@ private int NonNegativeMod(int x, int m) private Task RegisterChunkAwaiter(ChunkCoordinates coordinates) { - var tcs = ChunkLoadAwaiters.GetOrAdd(coordinates, _ => new TaskCompletionSource()); + var tcs = ChunkLoadAwaiters.GetOrAdd(coordinates, _ => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); // because there is a small chance that the chunk was loaded before we were able to add the awaiter // we need to check again if the chunk is loaded if (TryGetChunkAt(coordinates, out var chunk)) diff --git a/MineSharp.Bot/MineSharpBot.cs b/MineSharp.Bot/MineSharpBot.cs index 4fb05140..e6b3b156 100644 --- a/MineSharp.Bot/MineSharpBot.cs +++ b/MineSharp.Bot/MineSharpBot.cs @@ -64,9 +64,19 @@ public MineSharpBot(MinecraftClient client) plugins = new Dictionary(); - Client.On(packet => Task.FromResult(Registry = packet.RegistryData)); - Client.On( - packet => Task.FromResult(packet.RegistryCodec != null ? Registry = packet.RegistryCodec : null)); + Client.On(packet => + { + Registry = packet.RegistryData; + return Task.CompletedTask; + }); + Client.On(packet => + { + if (packet.RegistryCodec != null) + { + Registry = packet.RegistryCodec; + } + return Task.CompletedTask; + }); } /// diff --git a/MineSharp.Bot/Plugins/ChatPlugin.cs b/MineSharp.Bot/Plugins/ChatPlugin.cs index 50b528c2..969b00c4 100644 --- a/MineSharp.Bot/Plugins/ChatPlugin.cs +++ b/MineSharp.Bot/Plugins/ChatPlugin.cs @@ -45,10 +45,10 @@ public ChatPlugin(MineSharpBot bot) : base(bot) _ => null }; - Bot.Client.On(HandleChatMessagePacket); - Bot.Client.On(HandleChat); - Bot.Client.On(HandleSystemChatMessage); - Bot.Client.On(HandleDisguisedChatMessage); + OnPacketAfterInitialization(HandleChatMessagePacket); + OnPacketAfterInitialization(HandleChat); + OnPacketAfterInitialization(HandleSystemChatMessage); + OnPacketAfterInitialization(HandleDisguisedChatMessage); initDeclareCommandsPacket = Bot.Client.WaitForPacket(); } @@ -72,7 +72,8 @@ await Bot.Client.SendPacket( )); } - await HandleDeclareCommandsPacket(await initDeclareCommandsPacket.WaitAsync(Bot.CancellationToken)).WaitAsync(Bot.CancellationToken); + var declareCommandsPacket = await initDeclareCommandsPacket.WaitAsync(Bot.CancellationToken); + await HandleDeclareCommandsPacket(declareCommandsPacket).WaitAsync(Bot.CancellationToken); } /// diff --git a/MineSharp.Bot/Plugins/EntityPlugin.cs b/MineSharp.Bot/Plugins/EntityPlugin.cs index 12a4546c..c805bcf2 100644 --- a/MineSharp.Bot/Plugins/EntityPlugin.cs +++ b/MineSharp.Bot/Plugins/EntityPlugin.cs @@ -28,17 +28,17 @@ public EntityPlugin(MineSharpBot bot) : base(bot) { Entities = new ConcurrentDictionary(); - Bot.Client.On(HandleSpawnEntityPacket); - Bot.Client.On(HandleSpawnLivingEntityPacket); - Bot.Client.On(HandleRemoveEntitiesPacket); - Bot.Client.On(HandleSetEntityVelocityPacket); - Bot.Client.On(HandleUpdateEntityPositionPacket); - Bot.Client.On(HandleUpdateEntityPositionAndRotationPacket); - Bot.Client.On(HandleUpdateEntityRotationPacket); - Bot.Client.On(HandleTeleportEntityPacket); - Bot.Client.On(HandleUpdateAttributesPacket); - Bot.Client.On(HandleSynchronizePlayerPosition); - Bot.Client.On(HandleSetPassengersPacket); + OnPacketAfterInitialization(HandleSpawnEntityPacket); + OnPacketAfterInitialization(HandleSpawnLivingEntityPacket); + OnPacketAfterInitialization(HandleRemoveEntitiesPacket); + OnPacketAfterInitialization(HandleSetEntityVelocityPacket); + OnPacketAfterInitialization(HandleUpdateEntityPositionPacket); + OnPacketAfterInitialization(HandleUpdateEntityPositionAndRotationPacket); + OnPacketAfterInitialization(HandleUpdateEntityRotationPacket); + OnPacketAfterInitialization(HandleTeleportEntityPacket); + OnPacketAfterInitialization(HandleUpdateAttributesPacket); + OnPacketAfterInitialization(HandleSynchronizePlayerPosition); + OnPacketAfterInitialization(HandleSetPassengersPacket); } /// diff --git a/MineSharp.Bot/Plugins/PhysicsPlugin.cs b/MineSharp.Bot/Plugins/PhysicsPlugin.cs index 1313f321..143e924b 100644 --- a/MineSharp.Bot/Plugins/PhysicsPlugin.cs +++ b/MineSharp.Bot/Plugins/PhysicsPlugin.cs @@ -404,7 +404,7 @@ public LerpRotation(MinecraftPlayer player, float toYaw, float toPitch, float sm var yawTicks = Math.Abs((int)(deltaYaw / yawPerTick)); var pitchTicks = Math.Abs((int)(deltaPitch / pitchPerTick)); - task = new(); + task = new(TaskCreationOptions.RunContinuationsAsynchronously); remainingYawTicks = yawTicks; remainingPitchTicks = pitchTicks; } diff --git a/MineSharp.Bot/Plugins/PlayerPlugin.cs b/MineSharp.Bot/Plugins/PlayerPlugin.cs index 2a3181f0..051b8078 100644 --- a/MineSharp.Bot/Plugins/PlayerPlugin.cs +++ b/MineSharp.Bot/Plugins/PlayerPlugin.cs @@ -39,15 +39,15 @@ public PlayerPlugin(MineSharpBot bot) : base(bot) Players = new ConcurrentDictionary(); PlayerMap = new ConcurrentDictionary(); - Bot.Client.On(HandleSetHealthPacket); - Bot.Client.On(HandleCombatDeathPacket); - Bot.Client.On(HandleRespawnPacket); - Bot.Client.On(HandleSpawnPlayer); - Bot.Client.On(HandlePlayerInfoUpdate); - Bot.Client.On(HandlePlayerInfoRemove); - Bot.Client.On(HandleGameEvent); - Bot.Client.On(HandleAcknowledgeBlockChange); - Bot.Client.On(HandleEntityStatus); + OnPacketAfterInitialization(HandleSetHealthPacket); + OnPacketAfterInitialization(HandleCombatDeathPacket); + OnPacketAfterInitialization(HandleRespawnPacket); + OnPacketAfterInitialization(HandleSpawnPlayer); + OnPacketAfterInitialization(HandlePlayerInfoUpdate); + OnPacketAfterInitialization(HandlePlayerInfoRemove); + OnPacketAfterInitialization(HandleGameEvent); + OnPacketAfterInitialization(HandleAcknowledgeBlockChange); + OnPacketAfterInitialization(HandleEntityStatus); // already start listening to the packets here, as they sometimes get lost when calling in init() initLoginPacket = Bot.Client.WaitForPacket(); diff --git a/MineSharp.Bot/Plugins/Plugin.cs b/MineSharp.Bot/Plugins/Plugin.cs index 17411d51..897c80c6 100644 --- a/MineSharp.Bot/Plugins/Plugin.cs +++ b/MineSharp.Bot/Plugins/Plugin.cs @@ -1,4 +1,6 @@ -using MineSharp.Protocol.Packets; +using ConcurrentCollections; +using MineSharp.Protocol.Packets; +using MineSharp.Protocol.Registrations; using NLog; using static MineSharp.Protocol.MinecraftClient; @@ -7,7 +9,7 @@ namespace MineSharp.Bot.Plugins; /// /// Plugin for . /// -public abstract class Plugin +public abstract class Plugin : IAsyncDisposable { private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); @@ -18,6 +20,12 @@ public abstract class Plugin /// protected readonly MineSharpBot Bot; + /// + /// All the registrations for packet handlers that this plugin has mode. + /// Used to unregister them when the plugin is disposed. + /// + protected readonly ConcurrentHashSet PacketReceiveRegistrations; + /// /// Create a new Plugin instance /// @@ -26,7 +34,8 @@ protected Plugin(MineSharpBot bot) { Bot = bot; IsEnabled = true; - initializationTask = new(); + initializationTask = new(TaskCreationOptions.RunContinuationsAsynchronously); + PacketReceiveRegistrations = new(); } /// @@ -137,10 +146,14 @@ private AsyncPacketHandler CreateAfterInitializationPacketHandlerWrappe /// The type of the packet. /// The packet handler to be called. /// Whether packets sent before the plugin has been initialized should be queued and processed after initialization. - public void OnPacketAfterInitialization(AsyncPacketHandler packetHandler, bool queuePacketsSentBeforeInitializationCompleted = false) + public void OnPacketAfterInitialization(AsyncPacketHandler packetHandler, bool queuePacketsSentBeforeInitializationCompleted = true) where TPacket : IPacket { - Bot.Client.On(CreateAfterInitializationPacketHandlerWrapper(packetHandler, queuePacketsSentBeforeInitializationCompleted)); + var registration = Bot.Client.On(CreateAfterInitializationPacketHandlerWrapper(packetHandler, queuePacketsSentBeforeInitializationCompleted)); + if (registration != null) + { + PacketReceiveRegistrations.Add(registration); + } } internal async Task Initialize() @@ -171,4 +184,28 @@ internal async Task Initialize() throw; } } + + /// + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return DisposeAsyncInternal(); + } + + /// + /// Disposes the plugin asynchronously. + /// Can be overridden by plugins to add custom dispose logic. + /// IMPORTANT: Always call base.DisposeAsync() in the overridden method (at the end). + /// + /// A ValueTask representing the asynchronous dispose operation. + protected virtual ValueTask DisposeAsyncInternal() + { + var registrations = PacketReceiveRegistrations.ToArray(); + foreach (var registration in registrations) + { + PacketReceiveRegistrations.TryRemove(registration); + registration.Dispose(); + } + return ValueTask.CompletedTask; + } } diff --git a/MineSharp.Bot/Plugins/WindowPlugin.cs b/MineSharp.Bot/Plugins/WindowPlugin.cs index 7d0ac043..a770c7e9 100644 --- a/MineSharp.Bot/Plugins/WindowPlugin.cs +++ b/MineSharp.Bot/Plugins/WindowPlugin.cs @@ -44,7 +44,7 @@ public class WindowPlugin : Plugin /// public WindowPlugin(MineSharpBot bot) : base(bot) { - inventoryLoadedTsc = new(); + inventoryLoadedTsc = new(TaskCreationOptions.RunContinuationsAsynchronously); openWindows = new ConcurrentDictionary(); windowLock = new(); @@ -60,10 +60,10 @@ public WindowPlugin(MineSharpBot bot) : base(bot) // OnPacketAfterInitialization is required to ensure that the plugin is initialized // before handling packets. Otherwise we have race conditions that might cause errors - OnPacketAfterInitialization(HandleWindowItems, true); - OnPacketAfterInitialization(HandleSetSlot, true); - OnPacketAfterInitialization(HandleHeldItemChange, true); - OnPacketAfterInitialization(HandleOpenWindow, true); + OnPacketAfterInitialization(HandleWindowItems); + OnPacketAfterInitialization(HandleSetSlot); + OnPacketAfterInitialization(HandleHeldItemChange); + OnPacketAfterInitialization(HandleOpenWindow); } /// @@ -135,7 +135,7 @@ public async Task OpenContainer(Block block, int timeoutMs = 10 * 1000) throw new ArgumentException("Cannot open block of type " + block.Info.Name); } - openContainerTsc = new(); + openContainerTsc = new(TaskCreationOptions.RunContinuationsAsynchronously); var packet = new PlaceBlockPacket( (int)PlayerHand.MainHand, diff --git a/MineSharp.Bot/Plugins/WorldPlugin.cs b/MineSharp.Bot/Plugins/WorldPlugin.cs index d667ea48..1b9fee1d 100644 --- a/MineSharp.Bot/Plugins/WorldPlugin.cs +++ b/MineSharp.Bot/Plugins/WorldPlugin.cs @@ -31,12 +31,12 @@ public class WorldPlugin : Plugin public WorldPlugin(MineSharpBot bot) : base(bot) { // we want all the packets. Even those that are sent before the plugin is initialized. - OnPacketAfterInitialization(HandleChunkDataAndLightUpdatePacket, true); - OnPacketAfterInitialization(HandleUnloadChunkPacket, true); - OnPacketAfterInitialization(HandleBlockUpdatePacket, true); - OnPacketAfterInitialization(HandleMultiBlockUpdatePacket, true); - OnPacketAfterInitialization(HandleChunkBatchStartPacket, true); - OnPacketAfterInitialization(HandleChunkBatchFinishedPacket, true); + OnPacketAfterInitialization(HandleChunkDataAndLightUpdatePacket); + OnPacketAfterInitialization(HandleUnloadChunkPacket); + OnPacketAfterInitialization(HandleBlockUpdatePacket); + OnPacketAfterInitialization(HandleMultiBlockUpdatePacket); + OnPacketAfterInitialization(HandleChunkBatchStartPacket); + OnPacketAfterInitialization(HandleChunkBatchFinishedPacket); } /// diff --git a/MineSharp.Core/Serialization/PacketBuffer.cs b/MineSharp.Core/Serialization/PacketBuffer.cs index 5e049881..8002a8a9 100644 --- a/MineSharp.Core/Serialization/PacketBuffer.cs +++ b/MineSharp.Core/Serialization/PacketBuffer.cs @@ -1,7 +1,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using System.Text.RegularExpressions; using fNbt; using MineSharp.Core.Common; using MineSharp.Core.Common.Blocks; @@ -35,7 +34,7 @@ public PacketBuffer(int protocolVersion) public PacketBuffer(byte[] bytes, int protocolVersion) { ProtocolVersion = protocolVersion; - buffer = new(bytes); + buffer = new(bytes, false); } /// @@ -81,7 +80,7 @@ public void Dispose() } /// - /// Return the buffer's byte array + /// Return a copy of the buffer's byte array /// /// public byte[] GetBuffer() @@ -96,8 +95,45 @@ public byte[] GetBuffer() /// public string HexDump(bool cutToPosition = false) { - var hex = Convert.ToHexString(GetBuffer().Skip(cutToPosition ? (int)Position : 0).ToArray()); - return Regex.Replace(hex, ".{2}", "$0 ").TrimEnd(); + byte[] dataToDump; + if (cutToPosition) + { + dataToDump = new byte[ReadableBytes]; + PeekBytes(dataToDump); + } + else + { + dataToDump = GetBuffer(); + } + + if (dataToDump.Length == 0) + { + return string.Empty; + } + + var hexStringBuffer = new char[dataToDump.Length * 3 - 1]; + var hexStringBufferSpan = new Span(hexStringBuffer); + for (var i = 0; i < dataToDump.Length; i++) + { + var b = dataToDump[i]; + b.TryFormat(hexStringBufferSpan.Slice(i * 3, 2), out _, "X2"); + if (i != dataToDump.Length - 1) + { + // Add a space between each byte + // but not at the very end + hexStringBufferSpan[i * 3 + 2] = ' '; + } + } + return new string(hexStringBuffer); + } + + private void PeekBytes(Span bytes) + { + EnsureEnoughReadableBytes(bytes.Length); + + var oldPosition = buffer.Position; + buffer.Read(bytes); + buffer.Position = oldPosition; } private void EnsureEnoughReadableBytes(int count)