namespace Impostor.Api.Net.Messages
{
- public interface IMessageReader
+ public interface IMessageReader : IDisposable
{
/// <summary>
/// Gets the tag of the message.
IMessageReader Slice(int start, int length);
}
-}
\ No newline at end of file
+}
}
}
- [Benchmark]
- public void Normal_Run_1_000_000()
- {
- for (var i = 0; i < 1_000_000; i++)
- {
- var reader = new MessageReader(_data);
- var inner = reader.ReadMessage();
-
- _ = inner.ReadUInt16();
- _ = inner.ReadByte();
- _ = inner.ReadInt32();
- // inner.ReadPackedInt32();
- }
- }
+ // [Benchmark]
+ // public void Normal_Run_1_000_000()
+ // {
+ // for (var i = 0; i < 1_000_000; i++)
+ // {
+ // var reader = new MessageReader(_data);
+ // var inner = reader.ReadMessage();
+ //
+ // _ = inner.ReadUInt16();
+ // _ = inner.ReadByte();
+ // _ = inner.ReadInt32();
+ // // inner.ReadPackedInt32();
+ // }
+ // }
[Benchmark]
public void Bytes_Run_1_000_000()
NumImpostors = 2
});
- using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023)))
+ // TODO: ObjectPool for MessageReaders
+ using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023), null))
{
var e = new ManualResetEvent(false);
return default;
}
}
-}
\ No newline at end of file
+}
--- /dev/null
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Hazel.Extensions
+{
+ public static class ServiceProviderExtensions
+ {
+ public static void AddHazel(this IServiceCollection services)
+ {
+ services.TryAddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
+
+ services.AddSingleton(serviceProvider =>
+ {
+ var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
+ var policy = ActivatorUtilities.CreateInstance<MessageReaderPolicy>(serviceProvider);
+ return provider.Create(policy);
+ });
+ }
+ }
+}
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0-rc.2.20475.17" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
using System.Runtime.CompilerServices;
using System.Text;
using Impostor.Api.Net.Messages;
+using Microsoft.Extensions.ObjectPool;
namespace Impostor.Hazel
{
public class MessageReader : IMessageReader
{
- public byte Tag { get; }
- public ReadOnlyMemory<byte> Buffer { get; }
+ private readonly ObjectPool<MessageReader> _pool;
+
+ public byte Tag { get; private set; }
+ public ReadOnlyMemory<byte> Buffer { get; private set; }
public int Position { get; set; }
public int Length => Buffer.Length;
- public MessageReader(ReadOnlyMemory<byte> buffer)
+ internal MessageReader(ObjectPool<MessageReader> pool)
{
- Tag = byte.MaxValue;
- Buffer = buffer;
+ _pool = pool;
}
- public MessageReader(byte tag, ReadOnlyMemory<byte> buffer)
+ public void Update(ReadOnlyMemory<byte> buffer)
+ {
+ Update(byte.MaxValue, buffer);
+ }
+
+ public void Update(byte tag, ReadOnlyMemory<byte> buffer)
{
Tag = tag;
Buffer = buffer;
+ Position = 0;
+ }
+
+ internal void Reset()
+ {
+ Tag = byte.MaxValue;
+ Buffer = null;
+ Position = 0;
}
public IMessageReader ReadMessage()
Position += length;
- return new MessageReader(tag, Buffer.Slice(pos, length));
+ var reader = _pool.Get();
+ reader.Update(tag, Buffer.Slice(pos, length));
+ return reader;
}
public bool ReadBoolean()
public IMessageReader Slice(int start)
{
- return new MessageReader(Tag, Buffer.Slice(start));
+ var reader = _pool.Get();
+ reader.Update(Tag, Buffer.Slice(start));
+ return reader;
}
public IMessageReader Slice(int start, int length)
{
- return new MessageReader(Tag, Buffer.Slice(start, length));
+ var reader = _pool.Get();
+ reader.Update(Tag, Buffer.Slice(start, length));
+ return reader;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
{
return Buffer.Span[Position++];
}
+
+ public void Dispose()
+ {
+ _pool.Return(this);
+ }
}
-}
\ No newline at end of file
+}
--- /dev/null
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Hazel
+{
+ public class MessageReaderPolicy : IPooledObjectPolicy<MessageReader>
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ public MessageReaderPolicy(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public MessageReader Create()
+ {
+ return new MessageReader(_serviceProvider.GetRequiredService<Microsoft.Extensions.ObjectPool.ObjectPool<MessageReader>>());
+ }
+
+ public bool Return(MessageReader obj)
+ {
+ obj.Reset();
+ return true;
+ }
+ }
+}
public class MessageWriter : IMessageWriter, IRecyclable, IDisposable
{
private static int BufferSize = 64000;
- private static readonly ObjectPool<MessageWriter> WriterPool = new ObjectPool<MessageWriter>(() => new MessageWriter(BufferSize));
+ private static readonly ObjectPoolCustom<MessageWriter> WriterPool = new ObjectPoolCustom<MessageWriter>(() => new MessageWriter(BufferSize));
public MessageType SendOption { get; private set; }
+++ /dev/null
-using System;
-using System.Collections.Concurrent;
-using System.Threading;
-
-namespace Impostor.Hazel
-{
- /// <summary>
- /// A fairly simple object pool for items that will be created a lot.
- /// </summary>
- /// <typeparam name="T">The type that is pooled.</typeparam>
- /// <threadsafety static="true" instance="true"/>
- public sealed class ObjectPool<T> where T : IRecyclable
- {
- private int numberCreated;
- public int NumberCreated { get { return numberCreated; } }
-
- public int NumberInUse { get { return this.inuse.Count; } }
- public int NumberNotInUse { get { return this.pool.Count; } }
- public int Size { get { return this.NumberInUse + this.NumberNotInUse; } }
-
-#if HAZEL_BAG
- private readonly ConcurrentBag<T> pool = new ConcurrentBag<T>();
-#else
- private readonly List<T> pool = new List<T>();
-#endif
-
- // Unavailable objects
- private readonly ConcurrentDictionary<T, bool> inuse = new ConcurrentDictionary<T, bool>();
-
- /// <summary>
- /// The generator for creating new objects.
- /// </summary>
- /// <returns></returns>
- private readonly Func<T> objectFactory;
-
- /// <summary>
- /// Internal constructor for our ObjectPool.
- /// </summary>
- internal ObjectPool(Func<T> objectFactory)
- {
- this.objectFactory = objectFactory;
- }
-
- /// <summary>
- /// Returns a pooled object of type T, if none are available another is created.
- /// </summary>
- /// <returns>An instance of T.</returns>
- internal T GetObject()
- {
-#if HAZEL_BAG
- if (!pool.TryTake(out T item))
- {
- Interlocked.Increment(ref numberCreated);
- item = objectFactory.Invoke();
- }
-#else
- T item;
- lock (this.pool)
- {
- if (this.pool.Count > 0)
- {
- var idx = this.pool.Count - 1;
- item = this.pool[idx];
- this.pool.RemoveAt(idx);
- }
- else
- {
- Interlocked.Increment(ref numberCreated);
- item = objectFactory.Invoke();
- }
- }
-#endif
-
- if (!inuse.TryAdd(item, true))
- {
- throw new Exception("Duplicate pull " + typeof(T).Name);
- }
-
- return item;
- }
-
- /// <summary>
- /// Returns an object to the pool.
- /// </summary>
- /// <param name="item">The item to return.</param>
- internal void PutObject(T item)
- {
- if (inuse.TryRemove(item, out bool b))
- {
-#if HAZEL_BAG
- pool.Add(item);
-#else
- lock (this.pool)
- {
- pool.Add(item);
- }
-#endif
- }
- else
- {
-#if DEBUG
- throw new Exception("Duplicate add " + typeof(T).Name);
-#endif
- }
- }
- }
-}
--- /dev/null
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+
+namespace Impostor.Hazel
+{
+ /// <summary>
+ /// A fairly simple object pool for items that will be created a lot.
+ /// </summary>
+ /// <typeparam name="T">The type that is pooled.</typeparam>
+ /// <threadsafety static="true" instance="true"/>
+ public sealed class ObjectPoolCustom<T> where T : IRecyclable
+ {
+ private int numberCreated;
+ public int NumberCreated { get { return numberCreated; } }
+
+ public int NumberInUse { get { return this.inuse.Count; } }
+ public int NumberNotInUse { get { return this.pool.Count; } }
+ public int Size { get { return this.NumberInUse + this.NumberNotInUse; } }
+
+#if HAZEL_BAG
+ private readonly ConcurrentBag<T> pool = new ConcurrentBag<T>();
+#else
+ private readonly List<T> pool = new List<T>();
+#endif
+
+ // Unavailable objects
+ private readonly ConcurrentDictionary<T, bool> inuse = new ConcurrentDictionary<T, bool>();
+
+ /// <summary>
+ /// The generator for creating new objects.
+ /// </summary>
+ /// <returns></returns>
+ private readonly Func<T> objectFactory;
+
+ /// <summary>
+ /// Internal constructor for our ObjectPool.
+ /// </summary>
+ internal ObjectPoolCustom(Func<T> objectFactory)
+ {
+ this.objectFactory = objectFactory;
+ }
+
+ /// <summary>
+ /// Returns a pooled object of type T, if none are available another is created.
+ /// </summary>
+ /// <returns>An instance of T.</returns>
+ internal T GetObject()
+ {
+#if HAZEL_BAG
+ if (!pool.TryTake(out T item))
+ {
+ Interlocked.Increment(ref numberCreated);
+ item = objectFactory.Invoke();
+ }
+#else
+ T item;
+ lock (this.pool)
+ {
+ if (this.pool.Count > 0)
+ {
+ var idx = this.pool.Count - 1;
+ item = this.pool[idx];
+ this.pool.RemoveAt(idx);
+ }
+ else
+ {
+ Interlocked.Increment(ref numberCreated);
+ item = objectFactory.Invoke();
+ }
+ }
+#endif
+
+ if (!inuse.TryAdd(item, true))
+ {
+ throw new Exception("Duplicate pull " + typeof(T).Name);
+ }
+
+ return item;
+ }
+
+ /// <summary>
+ /// Returns an object to the pool.
+ /// </summary>
+ /// <param name="item">The item to return.</param>
+ internal void PutObject(T item)
+ {
+ if (inuse.TryRemove(item, out bool b))
+ {
+#if HAZEL_BAG
+ pool.Add(item);
+#else
+ lock (this.pool)
+ {
+ pool.Add(item);
+ }
+#endif
+ }
+ else
+ {
+#if DEBUG
+ throw new Exception("Duplicate add " + typeof(T).Name);
+#endif
+ }
+ }
+ }
+}
using System.Threading.Channels;
using System.Threading.Tasks;
using Impostor.Api.Net.Messages;
+using Microsoft.Extensions.ObjectPool;
using Serilog;
namespace Impostor.Hazel.Udp
/// Creates a new UdpClientConnection.
/// </summary>
/// <param name="remoteEndPoint">A <see cref="NetworkEndPoint"/> to connect to.</param>
- public UdpClientConnection(IPEndPoint remoteEndPoint, IPMode ipMode = IPMode.IPv4) : base(null)
+ public UdpClientConnection(IPEndPoint remoteEndPoint, ObjectPool<MessageReader> readerPool, IPMode ipMode = IPMode.IPv4) : base(null, readerPool)
{
EndPoint = remoteEndPoint;
RemoteEndPoint = remoteEndPoint;
base.Dispose(disposing);
}
}
-}
\ No newline at end of file
+}
/// </summary>
public class PingPacket : IRecyclable
{
- private static readonly ObjectPool<PingPacket> PacketPool = new ObjectPool<PingPacket>(() => new PingPacket());
+ private static readonly ObjectPoolCustom<PingPacket> PacketPool = new ObjectPoolCustom<PingPacket>(() => new PingPacket());
public readonly Stopwatch Stopwatch = new Stopwatch();
/// <summary>
/// Object pool for this event.
/// </summary>
- public static readonly ObjectPool<Packet> PacketPool = new ObjectPool<Packet>(() => new Packet());
+ public static readonly ObjectPoolCustom<Packet> PacketPool = new ObjectPoolCustom<Packet>(() => new Packet());
/// <summary>
/// Returns an instance of this object from the pool.
private static readonly ILogger Logger = Log.ForContext<UdpConnection>();
private readonly ConnectionListener _listener;
+ private readonly Microsoft.Extensions.ObjectPool.ObjectPool<MessageReader> _readerPool;
private readonly CancellationTokenSource _stoppingCts;
private bool _isDisposing;
private bool _isFirst = true;
private Task _executingTask;
- protected UdpConnection(ConnectionListener listener)
+ protected UdpConnection(ConnectionListener listener, Microsoft.Extensions.ObjectPool.ObjectPool<MessageReader> readerPool)
{
_listener = listener;
+ _readerPool = readerPool;
_stoppingCts = new CancellationTokenSource();
Pipeline = Channel.CreateUnbounded<MessageData>(new UnboundedChannelOptions
try
{
- await HandleReceive(new MessageReader(result.Buffer));
+ using (var reader = _readerPool.Get())
+ {
+ reader.Update(byte.MaxValue, result.Buffer);
+ await HandleReceive(reader);
+ }
}
catch (Exception e)
{
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
+using Microsoft.Extensions.ObjectPool;
using Serilog;
namespace Impostor.Hazel.Udp
public delegate bool AcceptConnectionCheck(IPEndPoint endPoint, byte[] input, out byte[] response);
private readonly UdpClient _socket;
+ private readonly ObjectPool<MessageReader> _readerPool;
private readonly MemoryPool<byte> _pool;
private readonly Timer _reliablePacketTimer;
private readonly ConcurrentDictionary<EndPoint, UdpServerConnection> _allConnections;
/// </summary>
/// <param name="endPoint">The endpoint to listen on.</param>
/// <param name="ipMode"></param>
- public UdpConnectionListener(IPEndPoint endPoint, IPMode ipMode = IPMode.IPv4)
+ public UdpConnectionListener(IPEndPoint endPoint, ObjectPool<MessageReader> readerPool, IPMode ipMode = IPMode.IPv4)
{
EndPoint = endPoint;
IPMode = ipMode;
+ _readerPool = readerPool;
_pool = MemoryPool<byte>.Shared;
_socket = new UdpClient(endPoint)
{
}
// Create new client
- client = new UdpServerConnection(this, data.RemoteEndPoint, IPMode);
+ client = new UdpServerConnection(this, data.RemoteEndPoint, IPMode, _readerPool);
// Store the client
if (!_allConnections.TryAdd(data.RemoteEndPoint, client))
using System.Net;
using System.Threading.Tasks;
using Impostor.Api.Net.Messages;
+using Microsoft.Extensions.ObjectPool;
namespace Impostor.Hazel.Udp
{
/// <param name="listener">The listener that created this connection.</param>
/// <param name="endPoint">The endpoint that we are connected to.</param>
/// <param name="IPMode">The IPMode we are connected using.</param>
- internal UdpServerConnection(UdpConnectionListener listener, IPEndPoint endPoint, IPMode IPMode) : base(listener)
+ internal UdpServerConnection(UdpConnectionListener listener, IPEndPoint endPoint, IPMode IPMode, ObjectPool<MessageReader> readerPool) : base(listener, readerPool)
{
this.Listener = listener;
this.RemoteEndPoint = endPoint;
using Impostor.Server.Net.Hazel;
using Impostor.Server.Net.Manager;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
namespace Impostor.Server.Net
{
internal class Matchmaker
{
private readonly ClientManager _clientManager;
+ private readonly ObjectPool<MessageReader> _readerPool;
private readonly ILogger<Matchmaker> _logger;
- private readonly ILogger<Net.Hazel.HazelConnection> _connectionLogger;
+ private readonly ILogger<HazelConnection> _connectionLogger;
private UdpConnectionListener _connection;
public Matchmaker(
ILogger<Matchmaker> logger,
ClientManager clientManager,
+ ObjectPool<MessageReader> readerPool,
ILogger<HazelConnection> connectionLogger)
{
_logger = logger;
_clientManager = clientManager;
+ _readerPool = readerPool;
_connectionLogger = connectionLogger;
}
_ => throw new InvalidOperationException()
};
- _connection = new UdpConnectionListener(ipEndPoint, mode);
+ _connection = new UdpConnectionListener(ipEndPoint, _readerPool, mode);
_connection.NewConnection = OnNewConnection;
await _connection.StartAsync();
await _clientManager.RegisterConnectionAsync(connection, name, clientVersion);
}
}
-}
\ No newline at end of file
+}
using Impostor.Api.Games.Managers;
using Impostor.Api.Net.Manager;
using Impostor.Api.Net.Messages;
+using Impostor.Hazel.Extensions;
using Impostor.Server.Config;
using Impostor.Server.Events;
using Impostor.Server.Net;
services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
}
+ services.AddHazel();
services.AddSingleton<IMessageWriterProvider, MessageWriterProvider>();
services.AddSingleton<IGameCodeFactory, GameCodeFactory>();
services.AddSingleton<IEventManager, EventManager>();
-using Impostor.Hazel;
+using System;
+using Impostor.Hazel;
+using Impostor.Hazel.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
using Xunit;
namespace Impostor.Tests.Hazel
{
public class BufferMessageReaderTests
{
+ private ObjectPool<MessageReader> CreateReaderPool()
+ {
+ var services = new ServiceCollection();
+ services.AddHazel();
+ return services.BuildServiceProvider().GetRequiredService<ObjectPool<MessageReader>>();
+ }
+
[Fact]
public void ReadProperInt()
{
Assert.Equal(11, msg.Length);
Assert.Equal(msg.Length, msg.Position);
- var reader = new MessageReader(msg.Buffer);
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
Assert.Equal(byte.MaxValue, reader.Tag);
var message = reader.ReadMessage();
Assert.Equal(1, message.Tag);
Assert.Equal(5, msg.Length);
Assert.Equal(msg.Length, msg.Position);
- var reader = new MessageReader(msg.Buffer);
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
Assert.Equal(byte.MaxValue, reader.Tag);
var message = reader.ReadMessage();
Assert.Equal(1, message.Tag);
Assert.Equal(msg.Length, msg.Position);
- var reader = new MessageReader(msg.Buffer);
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
Assert.Equal(byte.MaxValue, reader.Tag);
var message = reader.ReadMessage();
Assert.Equal(1, message.Tag);
Assert.Equal(7, msg.Length);
Assert.Equal(msg.Length, msg.Position);
- var reader = new MessageReader(msg.Buffer);
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
Assert.Equal(byte.MaxValue, reader.Tag);
var message = reader.ReadMessage();
Assert.Equal(1, message.Tag);
msg.EndMessage();
- var handleReader = new MessageReader(msg.Buffer);
+ var readerPool = CreateReaderPool();
+ var handleReader = readerPool.Get();
+ handleReader.Update(msg.Buffer);
var handleMessage = handleReader.ReadMessage();
Assert.Equal(1, handleMessage.Tag);
Assert.Equal(msg.Length, msg.Position);
- var reader = new MessageReader(msg.Buffer);
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(msg.Buffer);
Assert.Equal(byte.MaxValue, reader.Tag);
var message = reader.ReadMessage();
Assert.Equal(1, message.Tag);
messageWriter.EndMessage();
// Do the magic.
- var reader = new MessageReader(messageWriter.Buffer);
+ var readerPool = CreateReaderPool();
+ var reader = readerPool.Get();
+ reader.Update(messageWriter.Buffer);
var inner = reader.ReadMessage();
while (inner.Position < inner.Length)
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0-rc.2.20475.5" />
<PackageReference Include="Pcap.Net.x64" Version="1.0.4.1" />
</ItemGroup>
using System.Linq;
using Impostor.Api.Net.Messages;
using Impostor.Hazel;
+using Impostor.Hazel.Extensions;
using Impostor.Hazel.Udp;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ObjectPool;
using PcapDotNet.Core;
using PcapDotNet.Packets;
{14, "ReselectServer"},
{16, "GetGameListV2"}
};
-
+
+ private static IServiceProvider _serviceProvider;
+ private static ObjectPool<MessageReader> _readerPool;
+
private static void Main(string[] args)
{
+ var services = new ServiceCollection();
+ services.AddHazel();
+
+ _serviceProvider = services.BuildServiceProvider();
+ _readerPool = _serviceProvider.GetRequiredService<ObjectPool<MessageReader>>();
+
var devices = LivePacketDevice.AllLocalMachine;
if (devices.Count == 0)
{
// True if this is our own packet.
using (var stream = udp.Payload.ToMemoryStream())
{
- var reader = (IMessageReader) new MessageReader(stream.ToArray());
+ using var reader = _readerPool.Get();
+
+ reader.Update(stream.ToArray());
+
var option = reader.Buffer.Span[0];
if (option == (byte) MessageType.Reliable)
{
- reader = reader.Slice(3);
+ reader.Position += 3;
}
else if (option == (byte) UdpSendOption.Acknowledgement ||
option == (byte) UdpSendOption.Ping ||
}
else
{
- reader = reader.Slice(1);
+ reader.Position += 1;
}
var isSent = ipSrc.StartsWith("192.");
using Impostor.Tools.ServerReplay.Mocks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
using Serilog;
using ILogger = Serilog.ILogger;
private static ServiceProvider _serviceProvider;
+ private static ObjectPool<MessageReader> _readerPool;
private static MockGameCodeFactory _gameCodeFactory;
private static ClientManager _clientManager;
private static GameManager _gameManager;
_serviceProvider = BuildServices();
// Create required instances.
+ _readerPool = _serviceProvider.GetRequiredService<ObjectPool<MessageReader>>();
_gameCodeFactory = _serviceProvider.GetRequiredService<MockGameCodeFactory>();
_clientManager = _serviceProvider.GetRequiredService<ClientManager>();
_gameManager = _serviceProvider.GetRequiredService<GameManager>();
break;
case RecordedPacketType.Message:
- var messageType = (MessageType) reader.ReadByte();
+ {
+ var messageType = (MessageType)reader.ReadByte();
var tag = reader.ReadByte();
var length = reader.ReadInt32();
var buffer = reader.ReadBytes(length);
- var message = new MessageReader(tag, buffer);
+ using var message = _readerPool.Get();
+
+ message.Update(tag, buffer);
if (tag == MessageFlags.HostGame)
{
{
await client.Client!.HandleMessageAsync(message, messageType);
}
+
break;
+ }
case RecordedPacketType.GameCreated:
_gameCodeFactory.Result = GameCode.From(reader.ReadString());
}
}
}
-}
\ No newline at end of file
+}