// // PacketFileClient.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NosSmooth.Core.Client; using NosSmooth.Core.Commands; using NosSmooth.Core.Extensions; using NosSmooth.Core.Packets; using NosSmooth.Data.NOSFiles; using NosSmooth.Data.NOSFiles.Extensions; using NosSmooth.Game.Extensions; using NosSmooth.Game.Tests.Helpers; using NosSmooth.Packets; using NosSmooth.PacketSerializer; using NosSmooth.PacketSerializer.Abstractions.Attributes; using NosSmooth.PacketSerializer.Errors; using NosSmooth.PacketSerializer.Extensions; using NosSmooth.PacketSerializer.Packets; using Remora.Results; using Xunit.Abstractions; namespace NosSmooth.Game.Tests; /// /// A client used for tests. Supports loading just part of a file with packets. /// public class PacketFileClient : BaseNostaleClient, IDisposable { private const string LineRegex = ".*\\[(Recv|Send)\\]\t(.*)"; private const string LabelRegex = "##(.*)"; // TODO: make this class cleaner private readonly FileStream _stream; private readonly StreamReader _reader; private readonly IPacketSerializer _packetSerializer; private readonly PacketHandler _packetHandler; private readonly ILogger _logger; private string? _nextLabel; private bool _skip; private bool _readToLabel; /// /// Builds a file client for the given test. /// /// The name of the test. /// The output helper to output logs to. /// The test type. /// A file client and the associated game. public static (PacketFileClient Client, Game Game) CreateFor(string testName, ITestOutputHelper testOutputHelper) { var services = new ServiceCollection() .AddLogging(b => b.AddProvider(new XUnitLoggerProvider(testOutputHelper))) .AddNostaleCore() .AddNostaleGame() .AddSingleton(p => CreateFor(p, testName)) .AddSingleton(p => p.GetRequiredService()) .AddNostaleDataFiles() .BuildServiceProvider(); services.GetRequiredService().AddDefaultPackets(); if (!services.GetRequiredService().Initialize().IsSuccess) { throw new Exception("Data not initialized correctly."); } return (services.GetRequiredService(), services.GetRequiredService()); } /// /// Create a file client for the given test. /// /// The services provider. /// The name of the test. /// The test class. /// A client. public static PacketFileClient CreateFor(IServiceProvider services, string testName) { var prefix = "NosSmooth.Game.Tests."; var name = typeof(TTest).FullName!.Substring(prefix.Length).Replace("Tests", string.Empty); var splitted = name.Split('.'); var path = "Packets/"; foreach (var entry in splitted) { path += entry + "/"; } path += testName + ".plog"; return Create ( services, path ); } /// /// Create an instance of PacketFileClient for the given file. /// /// The services provider. /// The file name. /// A client. public static PacketFileClient Create(IServiceProvider services, string fileName) { return (PacketFileClient)ActivatorUtilities.CreateInstance (services, typeof(PacketFileClient), new[] { File.OpenRead(fileName) }); } /// /// Initializes a new instance of the class. /// /// The file stream. /// The packet serializer. /// The command processor. /// The packet handler. /// The logger. public PacketFileClient ( FileStream stream, IPacketSerializer packetSerializer, CommandProcessor commandProcessor, PacketHandler packetHandler, ILogger logger ) : base(commandProcessor, packetSerializer) { _stream = stream; _reader = new StreamReader(_stream); _packetSerializer = packetSerializer; _packetHandler = packetHandler; _logger = logger; } /// /// Start executing until the given label is hit. /// /// The label to hit. /// An asynchronous operation. public async Task ExecuteUntilLabelAsync(string label) { _readToLabel = false; _nextLabel = label; await RunAsync(); if (!_readToLabel) { throw new Exception($"Label {label} not found."); } } /// /// Start executing until the end of the file. /// /// An asynchronous operation. public Task ExecuteToEnd() { _nextLabel = null; return RunAsync(); } /// /// Skip cursor until the given label is hit. /// /// The label to hit. /// An asynchronous operation. public async Task SkipUntilLabelAsync(string label) { try { _readToLabel = false; _nextLabel = label; _skip = true; await RunAsync(); } finally { _skip = false; } if (!_readToLabel) { throw new Exception($"Label {label} not found."); } } /// public override async Task RunAsync(CancellationToken stopRequested = default) { var packetRegex = new Regex(LineRegex); var labelRegex = new Regex(LabelRegex); while (!_reader.EndOfStream) { stopRequested.ThrowIfCancellationRequested(); var line = await _reader.ReadLineAsync(stopRequested); if (string.IsNullOrEmpty(line)) { continue; } var labelMatch = labelRegex.Match(line); if (labelMatch.Success) { var label = labelMatch.Groups[1].Value; if (label == _nextLabel) { _readToLabel = true; break; } continue; } if (_skip) { continue; } var packetMatch = packetRegex.Match(line); if (!packetMatch.Success) { _logger.LogWarning($"Could not find match on line {line}"); continue; } var type = packetMatch.Groups[1].Value; var packetStr = packetMatch.Groups[2].Value; var source = type == "Recv" ? PacketSource.Server : PacketSource.Client; var packet = CreatePacket(packetStr, source); Result result = await _packetHandler.HandlePacketAsync ( this, source, packet, packetStr, stopRequested ); if (!result.IsSuccess) { _logger.LogResultError(result); } } return Result.FromSuccess(); } /// public override Task SendPacketAsync(string packetString, CancellationToken ct = default) { return _packetHandler.HandlePacketAsync ( this, PacketSource.Client, CreatePacket(packetString, PacketSource.Client), packetString, ct ); } /// public override Task ReceivePacketAsync(string packetString, CancellationToken ct = default) { return _packetHandler.HandlePacketAsync ( this, PacketSource.Server, CreatePacket(packetString, PacketSource.Server), packetString, ct ); } private IPacket CreatePacket(string packetStr, PacketSource source) { var packetResult = _packetSerializer.Deserialize(packetStr, source); if (!packetResult.IsSuccess) { if (packetResult.Error is PacketConverterNotFoundError err) { return new UnresolvedPacket(err.Header, packetStr); } return new ParsingFailedPacket(packetResult, packetStr); } return packetResult.Entity; } /// public void Dispose() { _stream.Dispose(); _reader.Dispose(); } }