// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Network; using System.Buffers; using System.IO.Pipelines; using Pipelines.Sockets.Unofficial; /// /// Base class for all classes which read mu online data packets from a . /// /// /// Things to consider here: /// * Do not call FlushAsync if the reader can't start until FlushAsync finishes, as that may cause a deadlock. /// * Ensure that only one context "owns" a PipeReader or PipeWriter or accesses them. These types are not thread-safe. /// public abstract class PacketPipeReaderBase { private readonly byte[] _headerBuffer = new byte[3]; /// /// Gets or sets the from which the packets can be read from at . /// protected PipeReader Source { get; set; } = null!; // will be set in derived classes /// /// Reads the mu online packet. /// /// The mu online packet. /// , if the flush was successful or not required., if the pipe reader is completed and no longer reading data. protected abstract ValueTask ReadPacketAsync(ReadOnlySequence packet); /// /// Called when the completed. /// /// The exception, if any occurred; Otherwise, null. protected abstract ValueTask OnCompleteAsync(Exception? exception); /// /// Reads from the until it's completed or cancelled. /// /// The task. protected async Task ReadSourceAsync() { if (this.Source is null) { throw new InvalidOperationException("Source must be set before."); } try { while (true) { var completed = await this.ReadBufferAsync().ConfigureAwait(false); // Stop reading if there's no more data coming if (completed) { break; } } } catch (ConnectionAbortedException) { // we can ignore that. } catch (Exception e) { await this.OnCompleteAsync(e).ConfigureAwait(false); return; } await this.OnCompleteAsync(null).ConfigureAwait(false); } /// /// Tries to flush the writer. /// /// The pipe writer. /// /// , if the flush was successful or not required., if the pipe reader is completed and no longer reading data. /// protected async ValueTask TryFlushWriterAsync(PipeWriter pipeWriter) { if (pipeWriter is { CanGetUnflushedBytes: true, UnflushedBytes: 0 }) { // It was flushed already in the background. return true; } // todo: what happens if it was flushed in the background in the meantime? race-condition? var flushResult = await pipeWriter.FlushAsync().ConfigureAwait(false); return !flushResult.IsCompleted; } private async Task ReadBufferAsync() { ReadResult result = await this.Source.ReadAsync().ConfigureAwait(false); ReadOnlySequence buffer = result.Buffer; int? length = null; var readingCancelledOrCompleted = false; do { if (buffer.Length > 2) { // peek the length of the next packet buffer.Slice(0, 3).CopyTo(this._headerBuffer); length = this._headerBuffer.AsSpan().GetPacketSize(); if (length == 0) { var exception = new InvalidPacketHeaderException(this._headerBuffer, result.Buffer, buffer.Start); // Notify our source, that we don't intend to read anymore. await this.Source.CompleteAsync(exception).ConfigureAwait(false); await this.OnCompleteAsync(exception).ConfigureAwait(false); throw exception; } } if (length is > 0 && buffer.Length >= length) { var packet = buffer.Slice(0, length.Value); if (!await this.ReadPacketAsync(packet).ConfigureAwait(false)) { readingCancelledOrCompleted = true; break; } buffer = buffer.Slice(buffer.GetPosition(length.Value), buffer.End); length = null; } else { // read more break; } } while (buffer.Length > 2); if (result.IsCanceled || result.IsCompleted) { // Not possible to advance any further, e.g. because of a disconnected network connection. await this.OnCompleteAsync(null).ConfigureAwait(false); } else { // Tell the PipeReader how much of the buffer we have consumed this.Source.AdvanceTo(buffer.Start); } return result.IsCompleted || readingCancelledOrCompleted; } }